From 69b7bc0c1a9a4ba4b26f34a15f13fe73fa20f584 Mon Sep 17 00:00:00 2001 From: CJACK Date: Sun, 19 Apr 2026 20:11:53 +0800 Subject: [PATCH] refactor: enforce mandatory CDATA wrapping for all string parameters in tool call XML output --- internal/adapter/claude/handler_util_test.go | 2 +- internal/prompt/tool_calls.go | 7 +-- internal/prompt/tool_calls_test.go | 4 +- internal/toolcall/tool_prompt.go | 64 ++++++++++++-------- internal/toolcall/tool_prompt_test.go | 4 +- 5 files changed, 48 insertions(+), 33 deletions(-) diff --git a/internal/adapter/claude/handler_util_test.go b/internal/adapter/claude/handler_util_test.go index 8735790..171c52a 100644 --- a/internal/adapter/claude/handler_util_test.go +++ b/internal/adapter/claude/handler_util_test.go @@ -96,7 +96,7 @@ func TestNormalizeClaudeMessagesToolUseToAssistantToolCalls(t *testing.T) { if !containsStr(content, "") || !containsStr(content, "search_web") { t.Fatalf("expected assistant content to include XML tool call history, got %q", content) } - if !containsStr(content, "\n latest\n ") { + if !containsStr(content, "\n \n ") { t.Fatalf("expected assistant content to include serialized parameters, got %q", content) } } diff --git a/internal/prompt/tool_calls.go b/internal/prompt/tool_calls.go index 41aa011..4c14f6b 100644 --- a/internal/prompt/tool_calls.go +++ b/internal/prompt/tool_calls.go @@ -230,6 +230,8 @@ func renderPromptToolXMLNode(name string, value any, indent string) (string, boo } } +// renderPromptXMLText emits CDATA for every string so prompt-visible tool +// history stays uniform and does not drift back toward ad-hoc escaping. func renderPromptXMLText(text string) string { if text == "" { return "" @@ -237,10 +239,7 @@ func renderPromptXMLText(text string) string { if strings.Contains(text, "]]>") { return "", "]]]]>") + "]]>" } - if strings.ContainsAny(text, "<>&\n\r") { - return "" - } - return escapeXMLText(text) + return "" } func isValidPromptXMLName(name string) bool { diff --git a/internal/prompt/tool_calls_test.go b/internal/prompt/tool_calls_test.go index cea42f7..2d30770 100644 --- a/internal/prompt/tool_calls_test.go +++ b/internal/prompt/tool_calls_test.go @@ -22,7 +22,7 @@ func TestFormatToolCallsForPromptXML(t *testing.T) { if got == "" { t.Fatal("expected non-empty formatted tool calls") } - if got != "\n \n search_web\n \n latest\n \n \n" { + if got != "\n \n search_web\n \n \n \n \n" { t.Fatalf("unexpected formatted tool call XML: %q", got) } } @@ -50,7 +50,7 @@ func TestFormatToolCallsForPromptUsesCDATAForMultilineContent(t *testing.T) { }, }, }) - want := "\n \n write_file\n \n \n script.sh\n \n \n" + want := "\n \n write_file\n \n \n \n \n \n" if got != want { t.Fatalf("unexpected multiline cdata tool call XML: %q", got) } diff --git a/internal/toolcall/tool_prompt.go b/internal/toolcall/tool_prompt.go index de73091..5ee60b6 100644 --- a/internal/toolcall/tool_prompt.go +++ b/internal/toolcall/tool_prompt.go @@ -36,34 +36,40 @@ func BuildToolCallInstructions(toolNames []string) string { return `TOOL CALL FORMAT — FOLLOW EXACTLY: -When calling tools, emit ONLY raw XML at the very end of your response. No text before, no text after, no markdown fences. +If you need to call tools, your entire response must be exactly one XML block and nothing else. TOOL_NAME_HERE - PARAMETER_VALUE + RULES: -1) When calling tools, you MUST use the XML format. -2) No text is allowed AFTER the XML block. -3) should be XML tags, not JSON. Use nested XML elements for structured data (e.g., value). -4) For long text, scripts, novels, or code content, YOU MUST wrap the value in to preserve formatting and avoid character escaping errors. -5) Multiple tools must be inside the same root. -6) Do NOT wrap XML in markdown fences (` + "```" + `). -7) Do NOT invent parameters. Use only the provided schema. -8) CRITICAL: Do NOT output internal monologues (e.g. "I will list files now..."). Just output your answer or the XML. +1) Use the XML format only. Never emit JSON or function-call syntax. +2) Put one or more entries under a single root. +3) Parameters must be XML, not JSON. +4) All string values must use , even short ones. This includes code, scripts, file contents, prompts, paths, names, and queries. +5) Objects use nested XML elements. Arrays may repeat the same tag or use children. +6) Numbers, booleans, and null stay plain text. +7) Use only the parameter names in the tool schema. Do not invent fields. +8) Do NOT wrap XML in markdown fences. Do NOT output explanations, role markers, or internal monologue. + +PARAMETER SHAPES: +- string => +- object => nested XML elements +- array => repeated tags or children +- number/bool/null => plain text ❌ WRONG — Do NOT do these: Wrong 1 — mixed text after XML: ... I hope this helps. Wrong 2 — function-call syntax: Grep({"pattern": "token"}) -Wrong 3 — missing wrapper: - ` + ex1 + `{} +Wrong 3 — JSON parameters: + ` + ex1 + `{"path":"x"} Wrong 4 — Markdown code fences: ` + "```xml" + ` ... @@ -97,7 +103,7 @@ Example B — Two tools in parallel: -Example C — Tool with complex structured XML parameters: +Example C — Tool with nested XML parameters: ` + ex3 + ` @@ -110,7 +116,7 @@ Example D — Tool with long script using CDATA (RELIABLE FOR CODE/SCRIPTS): ` + ex2 + ` - script.sh + ` + promptCDATA("script.sh") + ` README.md` + return `` + promptCDATA("README.md") + `` case "Glob": - return `**/*.go.` + return `` + promptCDATA("**/*.go") + `` + promptCDATA(".") + `` default: - return `src/main.go` + return `` + promptCDATA("src/main.go") + `` } } func exampleWriteOrExecParams(name string) string { switch strings.TrimSpace(name) { case "Bash", "execute_command": - return `pwd` + return `` + promptCDATA("pwd") + `` case "exec_command": - return `pwd` + return `` + promptCDATA("pwd") + `` case "Edit": - return `README.mdfoobar` + return `` + promptCDATA("README.md") + `` + promptCDATA("foo") + `` + promptCDATA("bar") + `` case "MultiEdit": - return `README.mdfoobar` + return `` + promptCDATA("README.md") + `` + promptCDATA("foo") + `` + promptCDATA("bar") + `` default: - return `output.txtHello world` + return `` + promptCDATA("output.txt") + `` + promptCDATA("Hello world") + `` } } func exampleInteractiveParams(name string) string { switch strings.TrimSpace(name) { case "Task": - return `Investigate flaky testsRun targeted tests and summarize failures` + return `` + promptCDATA("Investigate flaky tests") + `` + promptCDATA("Run targeted tests and summarize failures") + `` default: - return `Which approach do you prefer?Option AOption B` + return `` + promptCDATA("Which approach do you prefer?") + `` + promptCDATA("Option A") + `` + promptCDATA("Option B") + `` } } + +func promptCDATA(text string) string { + if text == "" { + return "" + } + if strings.Contains(text, "]]>") { + return "", "]]]]>") + "]]>" + } + return "" +} diff --git a/internal/toolcall/tool_prompt_test.go b/internal/toolcall/tool_prompt_test.go index 10865d4..67aeb27 100644 --- a/internal/toolcall/tool_prompt_test.go +++ b/internal/toolcall/tool_prompt_test.go @@ -10,7 +10,7 @@ func TestBuildToolCallInstructions_ExecCommandUsesCmdExample(t *testing.T) { if !strings.Contains(out, `exec_command`) { t.Fatalf("expected exec_command in examples, got: %s", out) } - if !strings.Contains(out, `pwd`) { + if !strings.Contains(out, ``) { t.Fatalf("expected cmd parameter example for exec_command, got: %s", out) } } @@ -20,7 +20,7 @@ func TestBuildToolCallInstructions_ExecuteCommandUsesCommandExample(t *testing.T if !strings.Contains(out, `execute_command`) { t.Fatalf("expected execute_command in examples, got: %s", out) } - if !strings.Contains(out, `pwd`) { + if !strings.Contains(out, ``) { t.Fatalf("expected command parameter example for execute_command, got: %s", out) } }