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",