diff --git a/internal/adapter/openai/handler_toolcall_format.go b/internal/adapter/openai/handler_toolcall_format.go
index e69d048..0b10b3d 100644
--- a/internal/adapter/openai/handler_toolcall_format.go
+++ b/internal/adapter/openai/handler_toolcall_format.go
@@ -53,7 +53,7 @@ func injectToolPrompt(messages []map[string]any, tools []any, policy util.ToolCh
if len(toolSchemas) == 0 {
return messages, names
}
- toolPrompt := "You have access to these tools:\n\n" + strings.Join(toolSchemas, "\n\n") + "\n\nWhen you need to use tools, output ONLY XML using this canonical format:\n\n \n tool_name\n {\"param\":\"value\"}\n \n\n\n【EXAMPLE】\nUser: Please check the weather in Beijing and Shanghai, and update my todo list.\nAssistant:\n\n \n get_weather\n {\"city\":\"Beijing\"}\n \n \n get_weather\n {\"city\":\"Shanghai\"}\n \n \n update_todo\n {\"todos\":[{\"content\":\"Buy milk\"},{\"content\":\"Write report\"}]}\n \n\n\nIMPORTANT:\n1) If calling tools, output ONLY XML in the canonical format above. Do NOT include any extra text.\n2) Do NOT wrap tool-call XML in markdown/code fences (for example, do not use triple backticks).\n3) `` MUST be strict JSON object text. Use double quotes for all JSON keys/strings.\n4) If calling multiple tools, emit multiple `` blocks under one `` root.\n5) After receiving a tool result, you MUST use it to produce the final answer.\n6) Only call another tool when the previous result is missing required data or returned an error."
+ toolPrompt := "You have access to these tools:\n\n" + strings.Join(toolSchemas, "\n\n") + "\n\n" + buildToolCallInstructions(names)
if policy.Mode == util.ToolChoiceRequired {
toolPrompt += "\n7) For this response, you MUST call at least one tool from the allowed list."
}
@@ -73,6 +73,90 @@ func injectToolPrompt(messages []map[string]any, tools []any, policy util.ToolCh
return messages, names
}
+// buildToolCallInstructions generates the tool-calling instruction block
+// with attention-optimized structure: rules → negative examples → positive
+// examples (using real tool names) → anchor. This ordering exploits the
+// transformer's recency bias: the last tokens before generation starts
+// carry the strongest influence on the model's first output tokens.
+func buildToolCallInstructions(toolNames []string) string {
+ // Pick real tool names for examples; fall back to generic names.
+ ex1 := "read_file"
+ ex2 := "write_to_file"
+ ex3 := "ask_followup_question"
+ used := map[string]bool{}
+ for _, n := range toolNames {
+ switch {
+ case !used["ex1"] && (n == "read_file" || n == "list_files" || n == "search_files"):
+ ex1 = n
+ used["ex1"] = true
+ case !used["ex2"] && (n == "write_to_file" || n == "apply_diff" || n == "execute_command"):
+ ex2 = n
+ used["ex2"] = true
+ case !used["ex3"] && (n == "ask_followup_question" || n == "attempt_completion" || n == "update_todo_list"):
+ ex3 = n
+ used["ex3"] = true
+ }
+ }
+
+ return `TOOL CALL FORMAT — FOLLOW EXACTLY:
+
+When calling tools, emit ONLY raw XML. No text before, no text after, no markdown fences.
+
+
+
+ TOOL_NAME_HERE
+ {"key":"value"}
+
+
+
+RULES:
+1) Output ONLY the XML above when calling tools. Do NOT mix tool XML with regular text.
+2) MUST contain a strict JSON object. All JSON keys and strings use double quotes.
+3) Multiple tools → multiple blocks inside ONE root.
+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.
+
+❌ WRONG — Do NOT do these:
+Wrong 1 — mixed text and XML:
+ I'll read the file for you. ...
+Wrong 2 — code fence wrapping:
+ ` + "```xml\n ...\n ```" + `
+Wrong 3 — missing wrapper:
+ ` + ex1 + `{}
+
+✅ CORRECT EXAMPLES:
+
+Example A — Single tool:
+
+
+ ` + ex1 + `
+ {"path":"src/main.go"}
+
+
+
+Example B — Two tools in parallel:
+
+
+ ` + ex1 + `
+ {"path":"config.json"}
+
+
+ ` + ex2 + `
+ {"path":"output.txt","content":"Hello world"}
+
+
+
+Example C — Tool with complex JSON parameters (newlines in values use \n):
+
+
+ ` + ex3 + `
+ {"question":"Which approach do you prefer?","follow_up":[{"text":"Option A"},{"text":"Option B"}]}
+
+
+
+Remember: Output ONLY the ... XML block when calling tools.`
+}
func formatIncrementalStreamToolCallDeltas(deltas []toolCallDelta, ids map[int]string) []map[string]any {
if len(deltas) == 0 {
return nil
diff --git a/internal/adapter/openai/prompt_build_test.go b/internal/adapter/openai/prompt_build_test.go
index e820f52..bf8b285 100644
--- a/internal/adapter/openai/prompt_build_test.go
+++ b/internal/adapter/openai/prompt_build_test.go
@@ -71,19 +71,19 @@ func TestBuildOpenAIFinalPrompt_VercelPreparePathKeepsFinalAnswerInstruction(t *
}
finalPrompt, _ := buildOpenAIFinalPrompt(messages, tools, "")
- if !strings.Contains(finalPrompt, "After receiving a tool result, you MUST use it to produce the final answer.") {
+ if !strings.Contains(finalPrompt, "After receiving a tool result, use it directly.") {
t.Fatalf("vercel prepare finalPrompt missing final-answer instruction: %q", finalPrompt)
}
- if !strings.Contains(finalPrompt, "Only call another tool when the previous result is missing required data or returned an error.") {
+ if !strings.Contains(finalPrompt, "Only call another tool if the result is insufficient.") {
t.Fatalf("vercel prepare finalPrompt missing retry guard instruction: %q", finalPrompt)
}
- if !strings.Contains(finalPrompt, "output ONLY XML using this canonical format") {
+ if !strings.Contains(finalPrompt, "TOOL CALL FORMAT") {
t.Fatalf("vercel prepare finalPrompt missing xml format instruction: %q", finalPrompt)
}
- if !strings.Contains(finalPrompt, "Do NOT wrap tool-call XML in markdown/code fences") {
+ if !strings.Contains(finalPrompt, "Do NOT wrap the XML in markdown code fences") {
t.Fatalf("vercel prepare finalPrompt missing no-fence xml instruction: %q", finalPrompt)
}
- if strings.Contains(finalPrompt, "```xml") || strings.Contains(finalPrompt, "```json") {
+ if strings.Contains(finalPrompt, "```json") {
t.Fatalf("vercel prepare finalPrompt should not require fenced tool calls: %q", finalPrompt)
}
}