diff --git a/cmd/root/exec.go b/cmd/root/exec.go index e9ce5eaf7..a605279ef 100644 --- a/cmd/root/exec.go +++ b/cmd/root/exec.go @@ -25,6 +25,7 @@ func newExecCmd() *cobra.Command { addRunOrExecFlags(cmd, &flags) addRuntimeConfigFlags(cmd, &flags.runConfig) + cmd.PersistentFlags().BoolVar(&flags.hideToolCalls, "hide-tool-calls", false, "Hide the tool calls in the output") return cmd } diff --git a/cmd/root/run.go b/cmd/root/run.go index f37033fcf..d3429ef10 100644 --- a/cmd/root/run.go +++ b/cmd/root/run.go @@ -28,6 +28,9 @@ type runExecFlags struct { modelOverrides []string dryRun bool runConfig config.RuntimeConfig + + // Exec only + hideToolCalls bool } func newRunCmd() *cobra.Command { @@ -197,6 +200,7 @@ func (f *runExecFlags) handleExecMode(ctx context.Context, out *cli.Printer, rt err := cli.Run(ctx, out, cli.Config{ AppName: AppName, AttachmentPath: f.attachmentPath, + HideToolCalls: f.hideToolCalls, }, rt, sess, execArgs) if cliErr, ok := err.(cli.RuntimeError); ok { return RuntimeError{Err: cliErr.Err} diff --git a/e2e/cagent_exec_test.go b/e2e/cagent_exec_test.go index 10dfe76f7..94388bb8d 100644 --- a/e2e/cagent_exec_test.go +++ b/e2e/cagent_exec_test.go @@ -15,67 +15,73 @@ import ( func TestExec_OpenAI(t *testing.T) { out := cagentExec(t, "testdata/basic.yaml", "What's 2+2?") - require.Equal(t, "\n--- Agent: root ---\n\n2 + 2 equals 4.", out) + require.Equal(t, "\n--- Agent: root ---\n2 + 2 equals 4.", out) } func TestExec_OpenAI_ToolCall(t *testing.T) { out := cagentExec(t, "testdata/fs_tools.yaml", "How many files in testdata/working_dir? Only output the number.") - require.Equal(t, "\n--- Agent: root ---\n\nCalling search_files(\n path: \"testdata/working_dir\"\n pattern: \"*\"\n)\n\nsearch_files response → \"1 files found:\\ntestdata/working_dir/README.me\"\n\n1", out) + require.Equal(t, "\n--- Agent: root ---\n\nCalling search_files(\n path: \"testdata/working_dir\"\n pattern: \"*\"\n)\n\nsearch_files response → \"1 files found:\\ntestdata/working_dir/README.me\"\n1", out) +} + +func TestExec_OpenAI_HideToolCalls(t *testing.T) { + out := cagentExec(t, "testdata/fs_tools.yaml", "--hide-tool-calls", "How many files in testdata/working_dir? Only output the number.") + + require.Equal(t, "\n--- Agent: root ---\n1", out) } func TestExec_OpenAI_gpt5(t *testing.T) { out := cagentExec(t, "testdata/basic.yaml", "--model=openai/gpt-5", "What's 2+2?") - require.Equal(t, "\n--- Agent: root ---\n\n4", out) + require.Equal(t, "\n--- Agent: root ---\n4", out) } func TestExec_OpenAI_gpt5_1(t *testing.T) { out := cagentExec(t, "testdata/basic.yaml", "--model=openai/gpt-5.1", "What's 2+2?") - require.Equal(t, "\n--- Agent: root ---\n\n2 + 2 = 4.", out) + require.Equal(t, "\n--- Agent: root ---\n2 + 2 = 4.", out) } func TestExec_OpenAI_gpt5_codex(t *testing.T) { out := cagentExec(t, "testdata/basic.yaml", "--model=openai/gpt-5-codex", "What's 2+2?") - require.Equal(t, "\n--- Agent: root ---\n\n2 + 2 equals 4.", out) + require.Equal(t, "\n--- Agent: root ---\n2 + 2 equals 4.", out) } func TestExec_Anthropic(t *testing.T) { out := cagentExec(t, "testdata/basic.yaml", "--model=anthropic/claude-sonnet-4-0", "What's 2+2?") - require.Equal(t, "\n--- Agent: root ---\n\n2 + 2 = 4", out) + require.Equal(t, "\n--- Agent: root ---\n2 + 2 = 4", out) } func TestExec_Anthropic_ToolCall(t *testing.T) { out := cagentExec(t, "testdata/fs_tools.yaml", "--model=anthropic/claude-sonnet-4-0", "How many files in testdata/working_dir? Only output the number.") - require.Equal(t, "\n--- Agent: root ---\n\nCalling list_directory(path: \"testdata/working_dir\")\n\nlist_directory response → \"FILE README.me\\n\"\n\n1", out) + require.Equal(t, "\n--- Agent: root ---\n\nCalling list_directory(path: \"testdata/working_dir\")\n\nlist_directory response → \"FILE README.me\\n\"\n1", out) } func TestExec_Gemini(t *testing.T) { out := cagentExec(t, "testdata/basic.yaml", "--model=google/gemini-2.5-flash", "What's 2+2?") - require.Equal(t, "\n--- Agent: root ---\n\n2 + 2 = 4", out) + require.Equal(t, "\n--- Agent: root ---\n2 + 2 = 4", out) } func TestExec_Gemini_ToolCall(t *testing.T) { out := cagentExec(t, "testdata/fs_tools.yaml", "--model=google/gemini-2.5-flash", "How many files in testdata/working_dir? Only output the number.") - require.Equal(t, "\n--- Agent: root ---\n\nCalling list_directory(path: \"testdata/working_dir\")\n\nlist_directory response → \"FILE README.me\\n\"\n\n1", out) + require.Equal(t, "\n--- Agent: root ---\n\nCalling list_directory(path: \"testdata/working_dir\")\n\nlist_directory response → \"FILE README.me\\n\"\n1", out) } func TestExec_Mistral(t *testing.T) { out := cagentExec(t, "testdata/basic.yaml", "--model=mistral/mistral-small", "What's 2+2?") - require.Equal(t, "\n--- Agent: root ---\n\nThe sum of 2 + 2 is 4.", out) + require.Equal(t, "\n--- Agent: root ---\nThe sum of 2 + 2 is 4.", out) } func TestExec_Mistral_ToolCall(t *testing.T) { out := cagentExec(t, "testdata/fs_tools.yaml", "--model=mistral/mistral-small", "How many files in testdata/working_dir? Only output the number.") - require.Equal(t, "\n--- Agent: root ---\n\nCalling list_directory(path: \"testdata/working_dir\")\n\nlist_directory response → \"FILE README.me\\n\"\n\n1", out) + require.Equal(t, "\n--- Agent: root ---\n\nCalling list_directory(path: \"testdata/working_dir\")\n\nlist_directory response → \"FILE README.me\\n\"\n1", out) } func TestExec_ToolCallsNeedAcceptance(t *testing.T) { diff --git a/e2e/testdata/cassettes/TestExec_OpenAI_HideToolCalls.yaml b/e2e/testdata/cassettes/TestExec_OpenAI_HideToolCalls.yaml new file mode 100644 index 000000000..12762696d --- /dev/null +++ b/e2e/testdata/cassettes/TestExec_OpenAI_HideToolCalls.yaml @@ -0,0 +1,43 @@ +--- +version: 2 +interactions: +- id: 0 + request: + proto: HTTP/1.1 + proto_major: 1 + proto_minor: 1 + content_length: 0 + host: api.openai.com + body: "{\"messages\":[{\"content\":\"You are a knowledgeable assistant that helps users with various tasks.\\nBe helpful, accurate, and concise in your responses.\\n\",\"role\":\"system\"},{\"content\":\"## Filesystem Tool Instructions\\n\\nThis toolset provides comprehensive filesystem operations with built-in security restrictions.\\n\\n### Security Model\\n- All operations are restricted to allowed directories only\\n- Use list_allowed_directories to see available paths\\n- Subdirectories within allowed directories are accessible\\n- Use add_allowed_directory to request access to new directories (requires user consent)\\n\\n### Directory Access Management\\n- If you need access to a directory outside the allowed list, use add_allowed_directory\\n- This will request user consent before expanding filesystem access\\n- Always provide a clear reason when requesting new directory access\\n\\n### Common Patterns\\n- Always check if directories exist before creating files\\n- Prefer read_multiple_files for batch operations\\n- Use search_files_content for finding specific code or text\\n\\n### Performance Tips\\n- Use read_multiple_files instead of multiple read_file calls\\n- Use directory_tree with max_depth to limit large traversals\\n- Use appropriate exclude patterns in search operations\",\"role\":\"system\"},{\"content\":\"How many files in testdata/working_dir? Only output the number.\",\"role\":\"user\"}],\"model\":\"gpt-4o\",\"stream_options\":{\"include_usage\":true},\"tools\":[{\"function\":{\"name\":\"directory_tree\",\"description\":\"Get a recursive tree view of files and directories as a JSON structure.\",\"parameters\":{\"additionalProperties\":false,\"properties\":{\"path\":{\"description\":\"The directory path to traverse\",\"type\":\"string\"}},\"required\":[\"path\"],\"type\":\"object\"}},\"type\":\"function\"},{\"function\":{\"name\":\"edit_file\",\"description\":\"Make line-based edits to a text file. Each edit replaces exact line sequences with new content.\",\"parameters\":{\"additionalProperties\":false,\"properties\":{\"edits\":{\"description\":\"Array of edit operations\",\"items\":{\"additionalProperties\":false,\"properties\":{\"newText\":{\"description\":\"The replacement text\",\"type\":\"string\"},\"oldText\":{\"description\":\"The exact text to replace\",\"type\":\"string\"}},\"required\":[\"oldText\",\"newText\"],\"type\":\"object\"},\"type\":\"array\"},\"path\":{\"description\":\"The file path to edit\",\"type\":\"string\"}},\"required\":[\"path\",\"edits\"],\"type\":\"object\"}},\"type\":\"function\"},{\"function\":{\"name\":\"list_allowed_directories\",\"description\":\"Returns a list of directories that the server has permission to access. Don't call if you access only the current working directory. It's always allowed.\",\"parameters\":{\"properties\":{},\"type\":\"object\"}},\"type\":\"function\"},{\"function\":{\"name\":\"add_allowed_directory\",\"description\":\"Request to add a new directory to the allowed directories list. This requires explicit user consent for security reasons.\",\"parameters\":{\"additionalProperties\":false,\"properties\":{\"path\":{\"description\":\"The directory path to add to allowed directories\",\"type\":\"string\"}},\"required\":[\"path\"],\"type\":\"object\"}},\"type\":\"function\"},{\"function\":{\"name\":\"list_directory\",\"description\":\"Get a detailed listing of all files and directories in a specified path.\",\"parameters\":{\"additionalProperties\":false,\"properties\":{\"path\":{\"description\":\"The directory path to list\",\"type\":\"string\"}},\"required\":[\"path\"],\"type\":\"object\"}},\"type\":\"function\"},{\"function\":{\"name\":\"read_file\",\"description\":\"Read the complete contents of a file from the file system.\",\"parameters\":{\"additionalProperties\":false,\"properties\":{\"path\":{\"description\":\"The file path to read\",\"type\":\"string\"}},\"required\":[\"path\"],\"type\":\"object\"}},\"type\":\"function\"},{\"function\":{\"name\":\"read_multiple_files\",\"description\":\"Read the contents of multiple files simultaneously.\",\"parameters\":{\"additionalProperties\":false,\"properties\":{\"json\":{\"description\":\"Whether to return the result as JSON\",\"type\":\"boolean\"},\"paths\":{\"description\":\"Array of file paths to read\",\"items\":{\"type\":\"string\"},\"type\":\"array\"}},\"required\":[\"paths\"],\"type\":\"object\"}},\"type\":\"function\"},{\"function\":{\"name\":\"search_files\",\"description\":\"Recursively search for files and directories matching a pattern. Prints the full paths of matching files and the total number of files found. The pattern syntax is the same as Go's filepath.Match.\",\"parameters\":{\"additionalProperties\":false,\"properties\":{\"excludePatterns\":{\"description\":\"Patterns to exclude from search\",\"items\":{\"type\":\"string\"},\"type\":\"array\"},\"path\":{\"description\":\"The starting directory path\",\"type\":\"string\"},\"pattern\":{\"description\":\"The search pattern\",\"type\":\"string\"}},\"required\":[\"path\",\"pattern\"],\"type\":\"object\"}},\"type\":\"function\"},{\"function\":{\"name\":\"search_files_content\",\"description\":\"Searches for text or regex patterns in the content of files matching a GLOB pattern.\",\"parameters\":{\"additionalProperties\":false,\"properties\":{\"excludePatterns\":{\"description\":\"Patterns to exclude from search\",\"items\":{\"type\":\"string\"},\"type\":\"array\"},\"is_regex\":{\"description\":\"If true, treat query as regex; otherwise literal text\",\"type\":\"boolean\"},\"path\":{\"description\":\"The starting directory path\",\"type\":\"string\"},\"query\":{\"description\":\"The text or regex pattern to search for\",\"type\":\"string\"}},\"required\":[\"path\",\"query\"],\"type\":\"object\"}},\"type\":\"function\"},{\"function\":{\"name\":\"write_file\",\"description\":\"Create a new file or completely overwrite an existing file with new content.\",\"parameters\":{\"additionalProperties\":false,\"properties\":{\"content\":{\"description\":\"The content to write to the file\",\"type\":\"string\"},\"path\":{\"description\":\"The file path to write\",\"type\":\"string\"}},\"required\":[\"path\",\"content\"],\"type\":\"object\"}},\"type\":\"function\"}],\"stream\":true}" + url: https://api.openai.com/v1/chat/completions + method: POST + response: + proto: HTTP/2.0 + proto_major: 2 + proto_minor: 0 + content_length: -1 + body: "data: {\"id\":\"chatcmpl-ChxkKxXhlzkY6XLfiDlZm7VuV4l4N\",\"object\":\"chat.completion.chunk\",\"created\":1764593804,\"model\":\"gpt-4o-2024-08-06\",\"service_tier\":\"default\",\"system_fingerprint\":\"fp_ed643dde95\",\"choices\":[{\"index\":0,\"delta\":{\"role\":\"assistant\",\"content\":null,\"tool_calls\":[{\"index\":0,\"id\":\"call_xv3Z7uP1CllRslKoGuKuCTVH\",\"type\":\"function\",\"function\":{\"name\":\"search_files\",\"arguments\":\"\"}}],\"refusal\":null},\"logprobs\":null,\"finish_reason\":null}],\"usage\":null,\"obfuscation\":\"GhRgNREK\"}\n\ndata: {\"id\":\"chatcmpl-ChxkKxXhlzkY6XLfiDlZm7VuV4l4N\",\"object\":\"chat.completion.chunk\",\"created\":1764593804,\"model\":\"gpt-4o-2024-08-06\",\"service_tier\":\"default\",\"system_fingerprint\":\"fp_ed643dde95\",\"choices\":[{\"index\":0,\"delta\":{\"tool_calls\":[{\"index\":0,\"function\":{\"arguments\":\"{\\\"\"}}]},\"logprobs\":null,\"finish_reason\":null}],\"usage\":null,\"obfuscation\":\"sLl\"}\n\ndata: {\"id\":\"chatcmpl-ChxkKxXhlzkY6XLfiDlZm7VuV4l4N\",\"object\":\"chat.completion.chunk\",\"created\":1764593804,\"model\":\"gpt-4o-2024-08-06\",\"service_tier\":\"default\",\"system_fingerprint\":\"fp_ed643dde95\",\"choices\":[{\"index\":0,\"delta\":{\"tool_calls\":[{\"index\":0,\"function\":{\"arguments\":\"path\"}}]},\"logprobs\":null,\"finish_reason\":null}],\"usage\":null,\"obfuscation\":\"KK\"}\n\ndata: {\"id\":\"chatcmpl-ChxkKxXhlzkY6XLfiDlZm7VuV4l4N\",\"object\":\"chat.completion.chunk\",\"created\":1764593804,\"model\":\"gpt-4o-2024-08-06\",\"service_tier\":\"default\",\"system_fingerprint\":\"fp_ed643dde95\",\"choices\":[{\"index\":0,\"delta\":{\"tool_calls\":[{\"index\":0,\"function\":{\"arguments\":\"\\\":\\\"\"}}]},\"logprobs\":null,\"finish_reason\":null}],\"usage\":null,\"obfuscation\":\"c\"}\n\ndata: {\"id\":\"chatcmpl-ChxkKxXhlzkY6XLfiDlZm7VuV4l4N\",\"object\":\"chat.completion.chunk\",\"created\":1764593804,\"model\":\"gpt-4o-2024-08-06\",\"service_tier\":\"default\",\"system_fingerprint\":\"fp_ed643dde95\",\"choices\":[{\"index\":0,\"delta\":{\"tool_calls\":[{\"index\":0,\"function\":{\"arguments\":\"test\"}}]},\"logprobs\":null,\"finish_reason\":null}],\"usage\":null,\"obfuscation\":\"V2\"}\n\ndata: {\"id\":\"chatcmpl-ChxkKxXhlzkY6XLfiDlZm7VuV4l4N\",\"object\":\"chat.completion.chunk\",\"created\":1764593804,\"model\":\"gpt-4o-2024-08-06\",\"service_tier\":\"default\",\"system_fingerprint\":\"fp_ed643dde95\",\"choices\":[{\"index\":0,\"delta\":{\"tool_calls\":[{\"index\":0,\"function\":{\"arguments\":\"data\"}}]},\"logprobs\":null,\"finish_reason\":null}],\"usage\":null,\"obfuscation\":\"6V\"}\n\ndata: {\"id\":\"chatcmpl-ChxkKxXhlzkY6XLfiDlZm7VuV4l4N\",\"object\":\"chat.completion.chunk\",\"created\":1764593804,\"model\":\"gpt-4o-2024-08-06\",\"service_tier\":\"default\",\"system_fingerprint\":\"fp_ed643dde95\",\"choices\":[{\"index\":0,\"delta\":{\"tool_calls\":[{\"index\":0,\"function\":{\"arguments\":\"/\"}}]},\"logprobs\":null,\"finish_reason\":null}],\"usage\":null,\"obfuscation\":\"W2E1L\"}\n\ndata: {\"id\":\"chatcmpl-ChxkKxXhlzkY6XLfiDlZm7VuV4l4N\",\"object\":\"chat.completion.chunk\",\"created\":1764593804,\"model\":\"gpt-4o-2024-08-06\",\"service_tier\":\"default\",\"system_fingerprint\":\"fp_ed643dde95\",\"choices\":[{\"index\":0,\"delta\":{\"tool_calls\":[{\"index\":0,\"function\":{\"arguments\":\"working\"}}]},\"logprobs\":null,\"finish_reason\":null}],\"usage\":null,\"obfuscation\":\"ICgrwMnauCDvUlM\"}\n\ndata: {\"id\":\"chatcmpl-ChxkKxXhlzkY6XLfiDlZm7VuV4l4N\",\"object\":\"chat.completion.chunk\",\"created\":1764593804,\"model\":\"gpt-4o-2024-08-06\",\"service_tier\":\"default\",\"system_fingerprint\":\"fp_ed643dde95\",\"choices\":[{\"index\":0,\"delta\":{\"tool_calls\":[{\"index\":0,\"function\":{\"arguments\":\"_dir\"}}]},\"logprobs\":null,\"finish_reason\":null}],\"usage\":null,\"obfuscation\":\"4e\"}\n\ndata: {\"id\":\"chatcmpl-ChxkKxXhlzkY6XLfiDlZm7VuV4l4N\",\"object\":\"chat.completion.chunk\",\"created\":1764593804,\"model\":\"gpt-4o-2024-08-06\",\"service_tier\":\"default\",\"system_fingerprint\":\"fp_ed643dde95\",\"choices\":[{\"index\":0,\"delta\":{\"tool_calls\":[{\"index\":0,\"function\":{\"arguments\":\"\\\",\\\"\"}}]},\"logprobs\":null,\"finish_reason\":null}],\"usage\":null,\"obfuscation\":\"d\"}\n\ndata: {\"id\":\"chatcmpl-ChxkKxXhlzkY6XLfiDlZm7VuV4l4N\",\"object\":\"chat.completion.chunk\",\"created\":1764593804,\"model\":\"gpt-4o-2024-08-06\",\"service_tier\":\"default\",\"system_fingerprint\":\"fp_ed643dde95\",\"choices\":[{\"index\":0,\"delta\":{\"tool_calls\":[{\"index\":0,\"function\":{\"arguments\":\"pattern\"}}]},\"logprobs\":null,\"finish_reason\":null}],\"usage\":null,\"obfuscation\":\"gLPN2ExdSvJpZRr\"}\n\ndata: {\"id\":\"chatcmpl-ChxkKxXhlzkY6XLfiDlZm7VuV4l4N\",\"object\":\"chat.completion.chunk\",\"created\":1764593804,\"model\":\"gpt-4o-2024-08-06\",\"service_tier\":\"default\",\"system_fingerprint\":\"fp_ed643dde95\",\"choices\":[{\"index\":0,\"delta\":{\"tool_calls\":[{\"index\":0,\"function\":{\"arguments\":\"\\\":\\\"\"}}]},\"logprobs\":null,\"finish_reason\":null}],\"usage\":null,\"obfuscation\":\"T\"}\n\ndata: {\"id\":\"chatcmpl-ChxkKxXhlzkY6XLfiDlZm7VuV4l4N\",\"object\":\"chat.completion.chunk\",\"created\":1764593804,\"model\":\"gpt-4o-2024-08-06\",\"service_tier\":\"default\",\"system_fingerprint\":\"fp_ed643dde95\",\"choices\":[{\"index\":0,\"delta\":{\"tool_calls\":[{\"index\":0,\"function\":{\"arguments\":\"*\"}}]},\"logprobs\":null,\"finish_reason\":null}],\"usage\":null,\"obfuscation\":\"3KAwT\"}\n\ndata: {\"id\":\"chatcmpl-ChxkKxXhlzkY6XLfiDlZm7VuV4l4N\",\"object\":\"chat.completion.chunk\",\"created\":1764593804,\"model\":\"gpt-4o-2024-08-06\",\"service_tier\":\"default\",\"system_fingerprint\":\"fp_ed643dde95\",\"choices\":[{\"index\":0,\"delta\":{\"tool_calls\":[{\"index\":0,\"function\":{\"arguments\":\"\\\"}\"}}]},\"logprobs\":null,\"finish_reason\":null}],\"usage\":null,\"obfuscation\":\"lQB\"}\n\ndata: {\"id\":\"chatcmpl-ChxkKxXhlzkY6XLfiDlZm7VuV4l4N\",\"object\":\"chat.completion.chunk\",\"created\":1764593804,\"model\":\"gpt-4o-2024-08-06\",\"service_tier\":\"default\",\"system_fingerprint\":\"fp_ed643dde95\",\"choices\":[{\"index\":0,\"delta\":{},\"logprobs\":null,\"finish_reason\":\"tool_calls\"}],\"usage\":null,\"obfuscation\":\"P22c\"}\n\ndata: {\"id\":\"chatcmpl-ChxkKxXhlzkY6XLfiDlZm7VuV4l4N\",\"object\":\"chat.completion.chunk\",\"created\":1764593804,\"model\":\"gpt-4o-2024-08-06\",\"service_tier\":\"default\",\"system_fingerprint\":\"fp_ed643dde95\",\"choices\":[],\"usage\":{\"prompt_tokens\":761,\"completion_tokens\":22,\"total_tokens\":783,\"prompt_tokens_details\":{\"cached_tokens\":0,\"audio_tokens\":0},\"completion_tokens_details\":{\"reasoning_tokens\":0,\"audio_tokens\":0,\"accepted_prediction_tokens\":0,\"rejected_prediction_tokens\":0}},\"obfuscation\":\"ZhFVDZkiPSfAb\"}\n\ndata: [DONE]\n\n" + headers: {} + status: 200 OK + code: 200 + duration: 1.129136333s +- id: 1 + request: + proto: HTTP/1.1 + proto_major: 1 + proto_minor: 1 + content_length: 0 + host: api.openai.com + body: "{\"messages\":[{\"content\":\"You are a knowledgeable assistant that helps users with various tasks.\\nBe helpful, accurate, and concise in your responses.\\n\",\"role\":\"system\"},{\"content\":\"## Filesystem Tool Instructions\\n\\nThis toolset provides comprehensive filesystem operations with built-in security restrictions.\\n\\n### Security Model\\n- All operations are restricted to allowed directories only\\n- Use list_allowed_directories to see available paths\\n- Subdirectories within allowed directories are accessible\\n- Use add_allowed_directory to request access to new directories (requires user consent)\\n\\n### Directory Access Management\\n- If you need access to a directory outside the allowed list, use add_allowed_directory\\n- This will request user consent before expanding filesystem access\\n- Always provide a clear reason when requesting new directory access\\n\\n### Common Patterns\\n- Always check if directories exist before creating files\\n- Prefer read_multiple_files for batch operations\\n- Use search_files_content for finding specific code or text\\n\\n### Performance Tips\\n- Use read_multiple_files instead of multiple read_file calls\\n- Use directory_tree with max_depth to limit large traversals\\n- Use appropriate exclude patterns in search operations\",\"role\":\"system\"},{\"content\":\"How many files in testdata/working_dir? Only output the number.\",\"role\":\"user\"},{\"tool_calls\":[{\"id\":\"call_xv3Z7uP1CllRslKoGuKuCTVH\",\"function\":{\"arguments\":\"{\\\"path\\\":\\\"testdata/working_dir\\\",\\\"pattern\\\":\\\"*\\\"}\",\"name\":\"search_files\"},\"type\":\"function\"}],\"role\":\"assistant\"},{\"content\":\"1 files found:\\ntestdata/working_dir/README.me\",\"tool_call_id\":\"call_xv3Z7uP1CllRslKoGuKuCTVH\",\"role\":\"tool\"}],\"model\":\"gpt-4o\",\"stream_options\":{\"include_usage\":true},\"tools\":[{\"function\":{\"name\":\"directory_tree\",\"description\":\"Get a recursive tree view of files and directories as a JSON structure.\",\"parameters\":{\"additionalProperties\":false,\"properties\":{\"path\":{\"description\":\"The directory path to traverse\",\"type\":\"string\"}},\"required\":[\"path\"],\"type\":\"object\"}},\"type\":\"function\"},{\"function\":{\"name\":\"edit_file\",\"description\":\"Make line-based edits to a text file. Each edit replaces exact line sequences with new content.\",\"parameters\":{\"additionalProperties\":false,\"properties\":{\"edits\":{\"description\":\"Array of edit operations\",\"items\":{\"additionalProperties\":false,\"properties\":{\"newText\":{\"description\":\"The replacement text\",\"type\":\"string\"},\"oldText\":{\"description\":\"The exact text to replace\",\"type\":\"string\"}},\"required\":[\"oldText\",\"newText\"],\"type\":\"object\"},\"type\":\"array\"},\"path\":{\"description\":\"The file path to edit\",\"type\":\"string\"}},\"required\":[\"path\",\"edits\"],\"type\":\"object\"}},\"type\":\"function\"},{\"function\":{\"name\":\"list_allowed_directories\",\"description\":\"Returns a list of directories that the server has permission to access. Don't call if you access only the current working directory. It's always allowed.\",\"parameters\":{\"properties\":{},\"type\":\"object\"}},\"type\":\"function\"},{\"function\":{\"name\":\"add_allowed_directory\",\"description\":\"Request to add a new directory to the allowed directories list. This requires explicit user consent for security reasons.\",\"parameters\":{\"additionalProperties\":false,\"properties\":{\"path\":{\"description\":\"The directory path to add to allowed directories\",\"type\":\"string\"}},\"required\":[\"path\"],\"type\":\"object\"}},\"type\":\"function\"},{\"function\":{\"name\":\"list_directory\",\"description\":\"Get a detailed listing of all files and directories in a specified path.\",\"parameters\":{\"additionalProperties\":false,\"properties\":{\"path\":{\"description\":\"The directory path to list\",\"type\":\"string\"}},\"required\":[\"path\"],\"type\":\"object\"}},\"type\":\"function\"},{\"function\":{\"name\":\"read_file\",\"description\":\"Read the complete contents of a file from the file system.\",\"parameters\":{\"additionalProperties\":false,\"properties\":{\"path\":{\"description\":\"The file path to read\",\"type\":\"string\"}},\"required\":[\"path\"],\"type\":\"object\"}},\"type\":\"function\"},{\"function\":{\"name\":\"read_multiple_files\",\"description\":\"Read the contents of multiple files simultaneously.\",\"parameters\":{\"additionalProperties\":false,\"properties\":{\"json\":{\"description\":\"Whether to return the result as JSON\",\"type\":\"boolean\"},\"paths\":{\"description\":\"Array of file paths to read\",\"items\":{\"type\":\"string\"},\"type\":\"array\"}},\"required\":[\"paths\"],\"type\":\"object\"}},\"type\":\"function\"},{\"function\":{\"name\":\"search_files\",\"description\":\"Recursively search for files and directories matching a pattern. Prints the full paths of matching files and the total number of files found. The pattern syntax is the same as Go's filepath.Match.\",\"parameters\":{\"additionalProperties\":false,\"properties\":{\"excludePatterns\":{\"description\":\"Patterns to exclude from search\",\"items\":{\"type\":\"string\"},\"type\":\"array\"},\"path\":{\"description\":\"The starting directory path\",\"type\":\"string\"},\"pattern\":{\"description\":\"The search pattern\",\"type\":\"string\"}},\"required\":[\"path\",\"pattern\"],\"type\":\"object\"}},\"type\":\"function\"},{\"function\":{\"name\":\"search_files_content\",\"description\":\"Searches for text or regex patterns in the content of files matching a GLOB pattern.\",\"parameters\":{\"additionalProperties\":false,\"properties\":{\"excludePatterns\":{\"description\":\"Patterns to exclude from search\",\"items\":{\"type\":\"string\"},\"type\":\"array\"},\"is_regex\":{\"description\":\"If true, treat query as regex; otherwise literal text\",\"type\":\"boolean\"},\"path\":{\"description\":\"The starting directory path\",\"type\":\"string\"},\"query\":{\"description\":\"The text or regex pattern to search for\",\"type\":\"string\"}},\"required\":[\"path\",\"query\"],\"type\":\"object\"}},\"type\":\"function\"},{\"function\":{\"name\":\"write_file\",\"description\":\"Create a new file or completely overwrite an existing file with new content.\",\"parameters\":{\"additionalProperties\":false,\"properties\":{\"content\":{\"description\":\"The content to write to the file\",\"type\":\"string\"},\"path\":{\"description\":\"The file path to write\",\"type\":\"string\"}},\"required\":[\"path\",\"content\"],\"type\":\"object\"}},\"type\":\"function\"}],\"stream\":true}" + url: https://api.openai.com/v1/chat/completions + method: POST + response: + proto: HTTP/2.0 + proto_major: 2 + proto_minor: 0 + content_length: -1 + body: "data: {\"id\":\"chatcmpl-ChxkMXJQzXCU43SId9AtFjhVgXXHv\",\"object\":\"chat.completion.chunk\",\"created\":1764593806,\"model\":\"gpt-4o-2024-08-06\",\"service_tier\":\"default\",\"system_fingerprint\":\"fp_ed643dde95\",\"choices\":[{\"index\":0,\"delta\":{\"role\":\"assistant\",\"content\":\"\",\"refusal\":null},\"logprobs\":null,\"finish_reason\":null}],\"usage\":null,\"obfuscation\":\"GtM4Y4JjicIZH6\"}\n\ndata: {\"id\":\"chatcmpl-ChxkMXJQzXCU43SId9AtFjhVgXXHv\",\"object\":\"chat.completion.chunk\",\"created\":1764593806,\"model\":\"gpt-4o-2024-08-06\",\"service_tier\":\"default\",\"system_fingerprint\":\"fp_ed643dde95\",\"choices\":[{\"index\":0,\"delta\":{\"content\":\"1\"},\"logprobs\":null,\"finish_reason\":null}],\"usage\":null,\"obfuscation\":\"IpwEmuIu3ekNfPg\"}\n\ndata: {\"id\":\"chatcmpl-ChxkMXJQzXCU43SId9AtFjhVgXXHv\",\"object\":\"chat.completion.chunk\",\"created\":1764593806,\"model\":\"gpt-4o-2024-08-06\",\"service_tier\":\"default\",\"system_fingerprint\":\"fp_ed643dde95\",\"choices\":[{\"index\":0,\"delta\":{},\"logprobs\":null,\"finish_reason\":\"stop\"}],\"usage\":null,\"obfuscation\":\"I47liwIouT\"}\n\ndata: {\"id\":\"chatcmpl-ChxkMXJQzXCU43SId9AtFjhVgXXHv\",\"object\":\"chat.completion.chunk\",\"created\":1764593806,\"model\":\"gpt-4o-2024-08-06\",\"service_tier\":\"default\",\"system_fingerprint\":\"fp_ed643dde95\",\"choices\":[],\"usage\":{\"prompt_tokens\":803,\"completion_tokens\":2,\"total_tokens\":805,\"prompt_tokens_details\":{\"cached_tokens\":0,\"audio_tokens\":0},\"completion_tokens_details\":{\"reasoning_tokens\":0,\"audio_tokens\":0,\"accepted_prediction_tokens\":0,\"rejected_prediction_tokens\":0}},\"obfuscation\":\"fHJIIJuB1ov1cm\"}\n\ndata: [DONE]\n\n" + headers: {} + status: 200 OK + code: 200 + duration: 904.019292ms diff --git a/pkg/cli/runner.go b/pkg/cli/runner.go index e1a35b4f2..8ff466d3a 100644 --- a/pkg/cli/runner.go +++ b/pkg/cli/runner.go @@ -35,6 +35,7 @@ func (e RuntimeError) Unwrap() error { type Config struct { AppName string AttachmentPath string + HideToolCalls bool } // Run executes an agent in non-TUI mode, handling user input and runtime events @@ -83,57 +84,23 @@ func Run(ctx context.Context, out *Printer, cfg Config, rt runtime.Runtime, sess firstLoop := true lastAgent := rt.CurrentAgentName() - llmIsTyping := false - reasoningStarted := false // Track if we've printed "Thinking:" prefix var lastConfirmedToolCallID string for event := range rt.RunStream(ctx, sess) { agentName := event.GetAgentName() if agentName != "" && (firstLoop || lastAgent != agentName) { if !firstLoop { - if llmIsTyping { - out.Println() - llmIsTyping = false - } out.Println() } out.PrintAgentName(agentName) firstLoop = false lastAgent = agentName - reasoningStarted = false // Reset reasoning state on agent change } switch e := event.(type) { case *runtime.AgentChoiceEvent: - agentChanged := lastAgent != e.AgentName - if !llmIsTyping { - // Only add newline if we're not already typing - if !agentChanged { - out.Println() - } - llmIsTyping = true - } - // Add newline when transitioning from reasoning to regular content - if reasoningStarted { - out.Println() - } - reasoningStarted = false // Reset when regular content starts out.Print(e.Content) case *runtime.AgentChoiceReasoningEvent: - if !reasoningStarted { - // First reasoning chunk: print prefix - prefix := "Thinking: " - if e.AgentName != "" && e.AgentName != "root" { - prefix = prefix + e.AgentName + ": " - } - out.Printf("\n%s", prefix) - reasoningStarted = true - } - // Continue printing reasoning content out.Print(e.Content) case *runtime.ToolCallConfirmationEvent: - if llmIsTyping { - out.Println() - llmIsTyping = false - } result := out.PrintToolCallWithConfirmation(ctx, e.ToolCall, rd) // If interrupted, skip resuming; the runtime will notice context cancellation and stop if ctx.Err() != nil { @@ -155,18 +122,16 @@ func Run(ctx context.Context, out *Printer, cfg Config, rt runtime.Runtime, sess continue } case *runtime.ToolCallEvent: - if llmIsTyping { - out.Println() - llmIsTyping = false + if cfg.HideToolCalls { + continue } // Only print if this wasn't already shown during confirmation if e.ToolCall.ID != lastConfirmedToolCallID { out.PrintToolCall(e.ToolCall) } case *runtime.ToolCallResponseEvent: - if llmIsTyping { - out.Println() - llmIsTyping = false + if cfg.HideToolCalls { + continue } out.PrintToolCallResponse(e.ToolCall, e.Response) // Clear the confirmed ID after the tool completes @@ -174,10 +139,6 @@ func Run(ctx context.Context, out *Printer, cfg Config, rt runtime.Runtime, sess lastConfirmedToolCallID = "" } case *runtime.ErrorEvent: - if llmIsTyping { - out.Println() - llmIsTyping = false - } lowerErr := strings.ToLower(e.Error) if strings.Contains(lowerErr, "context cancel") && ctx.Err() != nil { // treat Ctrl+C cancellations as non-errors lastErr = nil @@ -186,11 +147,6 @@ func Run(ctx context.Context, out *Printer, cfg Config, rt runtime.Runtime, sess out.PrintError(lastErr) } case *runtime.MaxIterationsReachedEvent: - if llmIsTyping { - out.Println() - llmIsTyping = false - } - result := out.PromptMaxIterationsContinue(ctx, e.MaxIterations) switch result { case ConfirmationApprove: @@ -203,11 +159,6 @@ func Run(ctx context.Context, out *Printer, cfg Config, rt runtime.Runtime, sess return nil } case *runtime.ElicitationRequestEvent: - if llmIsTyping { - out.Println() - llmIsTyping = false - } - serverURL := e.Meta["cagent/server_url"].(string) result := out.PromptOAuthAuthorization(ctx, serverURL) switch {