From 5b7cdaa729ea7163a6b393898d459244bca96c84 Mon Sep 17 00:00:00 2001 From: "CJACK." <155826701+CJackHwang@users.noreply.github.com> Date: Sun, 19 Apr 2026 23:11:24 +0800 Subject: [PATCH 1/2] Fix XML tool-call parsing for fenced markdown examples --- internal/toolcall/toolcalls_parse.go | 57 ++++++++++++++++++++++++++++ internal/toolcall/toolcalls_test.go | 19 ++++++++++ 2 files changed, 76 insertions(+) diff --git a/internal/toolcall/toolcalls_parse.go b/internal/toolcall/toolcalls_parse.go index f0cb2ac..e1e934c 100644 --- a/internal/toolcall/toolcalls_parse.go +++ b/internal/toolcall/toolcalls_parse.go @@ -39,6 +39,11 @@ func parseToolCallsDetailedXMLOnly(text string) ToolCallParseResult { return result } result.SawToolCallSyntax = looksLikeToolCallSyntax(trimmed) + trimmed = stripFencedCodeBlocks(trimmed) + trimmed = strings.TrimSpace(trimmed) + if trimmed == "" { + return result + } parsed := parseXMLToolCalls(trimmed) if len(parsed) == 0 { @@ -83,3 +88,55 @@ func looksLikeToolCallSyntax(text string) bool { strings.Contains(lower, "\n```\nDo not execute it." + res := ParseToolCallsDetailed(text, []string{"read_file"}) + if len(res.Calls) != 0 { + t.Fatalf("expected no parsed calls for fenced example, got %#v", res.Calls) + } +} + +func TestParseToolCallsParsesOnlyNonFencedXMLToolCall(t *testing.T) { + text := "```xml\nread_file{\"path\":\"README.md\"}\n```\nsearch{\"q\":\"golang\"}" + res := ParseToolCallsDetailed(text, []string{"read_file", "search"}) + if len(res.Calls) != 1 { + t.Fatalf("expected exactly one parsed call outside fence, got %#v", res.Calls) + } + if res.Calls[0].Name != "search" { + t.Fatalf("expected non-fenced tool call to be parsed, got %#v", res.Calls[0]) + } +} From 69eb71159d19b2b227dba32b3dd5be1ee52780ae Mon Sep 17 00:00:00 2001 From: "CJACK." <155826701+CJackHwang@users.noreply.github.com> Date: Sun, 19 Apr 2026 23:37:31 +0800 Subject: [PATCH 2/2] Handle variable-length markdown fences in toolcall parser --- internal/toolcall/toolcalls_parse.go | 35 ++++++++++++++++++++++------ internal/toolcall/toolcalls_test.go | 11 +++++++++ 2 files changed, 39 insertions(+), 7 deletions(-) diff --git a/internal/toolcall/toolcalls_parse.go b/internal/toolcall/toolcalls_parse.go index e1e934c..bc61124 100644 --- a/internal/toolcall/toolcalls_parse.go +++ b/internal/toolcall/toolcalls_parse.go @@ -124,19 +124,40 @@ func stripFencedCodeBlocks(text string) string { } func parseFenceOpen(line string) (string, bool) { - if strings.HasPrefix(line, "```") { - return "```", true + if len(line) < 3 { + return "", false } - if strings.HasPrefix(line, "~~~") { - return "~~~", true + ch := line[0] + if ch != '`' && ch != '~' { + return "", false } - return "", false + count := countLeadingFenceChars(line, ch) + if count < 3 { + return "", false + } + return strings.Repeat(string(ch), count), true } func isFenceClose(line, marker string) bool { - if marker == "" || !strings.HasPrefix(line, marker) { + if marker == "" { return false } - rest := strings.TrimSpace(strings.TrimPrefix(line, marker)) + ch := marker[0] + if line == "" || line[0] != ch { + return false + } + count := countLeadingFenceChars(line, ch) + if count < len(marker) { + return false + } + rest := strings.TrimSpace(line[count:]) return rest == "" } + +func countLeadingFenceChars(line string, ch byte) int { + count := 0 + for count < len(line) && line[count] == ch { + count++ + } + return count +} diff --git a/internal/toolcall/toolcalls_test.go b/internal/toolcall/toolcalls_test.go index 1b98d2c..ec1fa5b 100644 --- a/internal/toolcall/toolcalls_test.go +++ b/internal/toolcall/toolcalls_test.go @@ -474,3 +474,14 @@ func TestParseToolCallsParsesOnlyNonFencedXMLToolCall(t *testing.T) { t.Fatalf("expected non-fenced tool call to be parsed, got %#v", res.Calls[0]) } } + +func TestParseToolCallsParsesAfterFourBacktickFence(t *testing.T) { + text := "````markdown\n```xml\nread_file{\"path\":\"README.md\"}\n```\n````\nsearch{\"q\":\"outside\"}" + res := ParseToolCallsDetailed(text, []string{"read_file", "search"}) + if len(res.Calls) != 1 { + t.Fatalf("expected exactly one parsed call outside four-backtick fence, got %#v", res.Calls) + } + if res.Calls[0].Name != "search" { + t.Fatalf("expected non-fenced tool call to be parsed, got %#v", res.Calls[0]) + } +}