From 4b1f1ea550f792ea0d21eeef7da11a77dba122e1 Mon Sep 17 00:00:00 2001 From: "CJACK." Date: Fri, 3 Apr 2026 23:36:28 +0800 Subject: [PATCH] Preserve suffix after non-tool XML passthrough --- internal/adapter/openai/tool_sieve_xml.go | 2 +- .../adapter/openai/tool_sieve_xml_test.go | 25 ++++++++++++++++++- 2 files changed, 25 insertions(+), 2 deletions(-) diff --git a/internal/adapter/openai/tool_sieve_xml.go b/internal/adapter/openai/tool_sieve_xml.go index f6d3de1..bd1a47d 100644 --- a/internal/adapter/openai/tool_sieve_xml.go +++ b/internal/adapter/openai/tool_sieve_xml.go @@ -74,7 +74,7 @@ func consumeXMLToolCapture(captured string, toolNames []string) (prefix string, // If this block does not look like an executable tool-call payload, // pass it through as normal content (e.g. user-requested XML snippets). if !looksLikeExecutableXMLToolCallBlock(xmlBlock, pair.open) { - return captured, nil, "", true + return prefixPart + xmlBlock, nil, suffixPart, true } // Looks like XML tool syntax but failed to parse — consume it to avoid leak. return prefixPart, nil, suffixPart, true diff --git a/internal/adapter/openai/tool_sieve_xml_test.go b/internal/adapter/openai/tool_sieve_xml_test.go index 03bea7e..904a250 100644 --- a/internal/adapter/openai/tool_sieve_xml_test.go +++ b/internal/adapter/openai/tool_sieve_xml_test.go @@ -98,6 +98,29 @@ func TestProcessToolSievePassesThroughNonToolXMLBlock(t *testing.T) { } } +func TestProcessToolSieveNonToolXMLKeepsSuffixForToolParsing(t *testing.T) { + var state toolStreamSieveState + chunk := `plain xml{"path":"README.MD"}` + events := processToolSieveChunk(&state, chunk, []string{"read_file"}) + events = append(events, flushToolSieve(&state, []string{"read_file"})...) + + var textContent strings.Builder + toolCalls := 0 + for _, evt := range events { + textContent.WriteString(evt.Content) + toolCalls += len(evt.ToolCalls) + } + if !strings.Contains(textContent.String(), `plain xml`) { + t.Fatalf("expected leading non-tool XML to be preserved, got %q", textContent.String()) + } + if strings.Contains(textContent.String(), ``) { + t.Fatalf("expected invoke tool XML to be intercepted, got %q", textContent.String()) + } + if toolCalls != 1 { + t.Fatalf("expected exactly one parsed tool call from suffix, got %d events=%#v", toolCalls, events) + } +} + func TestProcessToolSievePartialXMLTagHeldBack(t *testing.T) { var state toolStreamSieveState // Chunk ends with a partial XML tool tag. @@ -384,7 +407,7 @@ func TestOpeningXMLTagNotLeakedAsContent(t *testing.T) { func TestProcessToolSieveInterceptsAttemptCompletionLeak(t *testing.T) { var state toolStreamSieveState - // Simulate an agent outputting attempt_completion XML tag + // Simulate an agent outputting attempt_completion XML tag // which shouldn't leak to text output, even if it fails to parse as a valid tool. chunks := []string{ "Done with task.\n",