diff --git a/internal/toolcall/toolcalls_parse.go b/internal/toolcall/toolcalls_parse.go index f15c130..772b297 100644 --- a/internal/toolcall/toolcalls_parse.go +++ b/internal/toolcall/toolcalls_parse.go @@ -216,6 +216,7 @@ func updateCDATAStateForStrip(inCDATA bool, cdataFenceMarker, line string) (bool pos := 0 state := inCDATA fenceMarker := cdataFenceMarker + lineForFence := line if !state { start := strings.Index(lower[pos:], "`, + ` <|DSML|invoke name="Bash">`, + ` <|DSML|parameter name="command">`, + ` `, + ``, + "```", + "tail", + }, "\n") + text := `` + + calls := ParseToolCalls(text, []string{"Write"}) + if len(calls) != 1 { + t.Fatalf("expected one compact CDATA call, got %#v", calls) + } + if calls[0].Input["content"] != content { + t.Fatalf("expected compact CDATA content to survive, got %#v", calls[0].Input["content"]) + } +} + func TestParseToolCallsPreservesSimpleCDATAInlineMarkupAsText(t *testing.T) { text := `urgent]]>` calls := ParseToolCalls(text, []string{"Write"}) diff --git a/internal/toolstream/tool_sieve_xml_test.go b/internal/toolstream/tool_sieve_xml_test.go index ab1aa38..ce2ee77 100644 --- a/internal/toolstream/tool_sieve_xml_test.go +++ b/internal/toolstream/tool_sieve_xml_test.go @@ -331,6 +331,51 @@ func TestProcessToolSieveKeepsExtremeHereDocCDATAUntilOuterClose(t *testing.T) { } } +func TestProcessToolSieveKeepsCompactCDATAWithImmediateFencedDSML(t *testing.T) { + var state State + content := strings.Join([]string{ + "```xml", + `<|DSML|tool_calls>`, + ` <|DSML|invoke name="Bash">`, + ` <|DSML|parameter name="command">`, + ` `, + ``, + "```", + "tail", + }, "\n") + chunks := []string{ + ``, + } + + var events []Event + for _, c := range chunks { + events = append(events, ProcessChunk(&state, c, []string{"Write"})...) + } + events = append(events, Flush(&state, []string{"Write"})...) + + var textContent strings.Builder + var gotContent string + toolCalls := 0 + for _, evt := range events { + textContent.WriteString(evt.Content) + if len(evt.ToolCalls) > 0 { + toolCalls += len(evt.ToolCalls) + gotContent, _ = evt.ToolCalls[0].Input["content"].(string) + } + } + if toolCalls != 1 { + t.Fatalf("expected one compact CDATA tool call, got %d events=%#v", toolCalls, events) + } + if textContent.Len() != 0 { + t.Fatalf("expected no leaked text, got %q", textContent.String()) + } + if gotContent != content { + t.Fatalf("expected compact CDATA content to survive, got len=%d want=%d", len(gotContent), len(content)) + } +} + func TestProcessToolSieveFallsBackWhenCDATANeverCloses(t *testing.T) { var state State chunks := []string{