diff --git a/internal/util/tool_prompt.go b/internal/util/tool_prompt.go index b0b0f75..9394eda 100644 --- a/internal/util/tool_prompt.go +++ b/internal/util/tool_prompt.go @@ -1,5 +1,7 @@ package util +import "strings" + // BuildToolCallInstructions generates the unified tool-calling instruction block // used by all adapters (OpenAI, Claude, Gemini). It uses attention-optimized // structure: rules → negative examples → positive examples → anchor. @@ -19,7 +21,7 @@ func BuildToolCallInstructions(toolNames []string) string { ex1 = n used["ex1"] = true // Write/execute-type tools - case !used["ex2"] && matchAny(n, "write_to_file", "apply_diff", "execute_command", "Write", "Edit", "MultiEdit", "Bash"): + case !used["ex2"] && matchAny(n, "write_to_file", "apply_diff", "execute_command", "exec_command", "Write", "Edit", "MultiEdit", "Bash"): ex2 = n used["ex2"] = true // Interactive/meta tools @@ -28,6 +30,9 @@ func BuildToolCallInstructions(toolNames []string) string { used["ex3"] = true } } + ex1Params := exampleReadParams(ex1) + ex2Params := exampleWriteOrExecParams(ex2) + ex3Params := exampleInteractiveParams(ex3) return `TOOL CALL FORMAT — FOLLOW EXACTLY: @@ -47,6 +52,7 @@ RULES: 4) Do NOT wrap the XML in markdown code fences (no triple backticks). 5) After receiving a tool result, use it directly. Only call another tool if the result is insufficient. 6) If you want to say something AND call a tool, output text first, then the XML block on its own. +7) Parameters MUST use the exact field names from the selected tool schema. ❌ WRONG — Do NOT do these: Wrong 1 — mixed text and XML: @@ -62,7 +68,7 @@ Example A — Single tool: ` + ex1 + ` - {"path":"src/main.go"} + ` + ex1Params + ` @@ -70,11 +76,11 @@ Example B — Two tools in parallel: ` + ex1 + ` - {"path":"config.json"} + ` + ex1Params + ` ` + ex2 + ` - {"path":"output.txt","content":"Hello world"} + ` + ex2Params + ` @@ -82,7 +88,7 @@ Example C — Tool with complex nested JSON parameters: ` + ex3 + ` - {"question":"Which approach do you prefer?","follow_up":[{"text":"Option A"},{"text":"Option B"}]} + ` + ex3Params + ` @@ -97,3 +103,38 @@ func matchAny(name string, candidates ...string) bool { } return false } + +func exampleReadParams(name string) string { + switch strings.TrimSpace(name) { + case "Read": + return `{"file_path":"README.md"}` + case "Glob": + return `{"pattern":"**/*.go","path":"."}` + default: + return `{"path":"src/main.go"}` + } +} + +func exampleWriteOrExecParams(name string) string { + switch strings.TrimSpace(name) { + case "Bash", "execute_command": + return `{"command":"pwd"}` + case "exec_command": + return `{"cmd":"pwd"}` + case "Edit": + return `{"file_path":"README.md","old_string":"foo","new_string":"bar"}` + case "MultiEdit": + return `{"file_path":"README.md","edits":[{"old_string":"foo","new_string":"bar"}]}` + default: + return `{"path":"output.txt","content":"Hello world"}` + } +} + +func exampleInteractiveParams(name string) string { + switch strings.TrimSpace(name) { + case "Task": + return `{"description":"Investigate flaky tests","prompt":"Run targeted tests and summarize failures"}` + default: + return `{"question":"Which approach do you prefer?","follow_up":[{"text":"Option A"},{"text":"Option B"}]}` + } +} diff --git a/internal/util/tool_prompt_test.go b/internal/util/tool_prompt_test.go new file mode 100644 index 0000000..e10f176 --- /dev/null +++ b/internal/util/tool_prompt_test.go @@ -0,0 +1,26 @@ +package util + +import ( + "strings" + "testing" +) + +func TestBuildToolCallInstructions_ExecCommandUsesCmdExample(t *testing.T) { + out := BuildToolCallInstructions([]string{"exec_command"}) + if !strings.Contains(out, `exec_command`) { + t.Fatalf("expected exec_command in examples, got: %s", out) + } + if !strings.Contains(out, `{"cmd":"pwd"}`) { + t.Fatalf("expected cmd parameter example for exec_command, got: %s", out) + } +} + +func TestBuildToolCallInstructions_ExecuteCommandUsesCommandExample(t *testing.T) { + out := BuildToolCallInstructions([]string{"execute_command"}) + if !strings.Contains(out, `execute_command`) { + t.Fatalf("expected execute_command in examples, got: %s", out) + } + if !strings.Contains(out, `{"command":"pwd"}`) { + t.Fatalf("expected command parameter example for execute_command, got: %s", out) + } +}