History

fix(claude): preserve non-result block positions during tool result alignment

- Maintain non-tool-result content blocks at their original indices in `AlignClaudeToolResults` instead of moving them after results. - Reorder tool results in-place into their original slot indices based on preceding tool use IDs.

Luis Pater committed 2 months ago

Commit 85641421 parent 5dc428f4 files · +63 −30Browse files

Unified · Split

4 files
  1. internal/translator/antigravity/claude/antigravity_claude_request_test.go +14 −8
  2. internal/translator/common/claude_messages.go +15 −11
  3. internal/translator/common/claude_messages_test.go +25 −5
  4. internal/translator/gemini/claude/gemini_claude_request_test.go +9 −6
internal/translator/antigravity/claude/antigravity_claude_request_test.go+14−8
@@ -2253,19 +2253,25 @@ func TestConvertClaudeRequestToAntigravity_AlignsPermutedParallelToolResultsWith · 2252 unmodified lines
22532253 output := ConvertClaudeRequestToAntigravity("gemini-3.7-flash-high", inputJSON, false)
22542254 parts := gjson.GetBytes(output, "request.contents.1.parts").Array()
22552255 if len(parts) != 8 {
2256− t.Fatalf("parts = %d, want six responses followed by two text parts; output=%s", len(parts), output)
2256+ t.Fatalf("parts = %d, want eight parts; output=%s", len(parts), output)
22572257 }
2258− for index := 0; index < 6; index++ {
2258+ if got := parts[0].Get("text").String(); got != "Tool results follow." {
2259+ t.Fatalf("leading text = %q; output=%s", got, output)
2260+ }
2261+ for index := 0; index < 3; index++ {
22592262 wantID := fmt.Sprintf("call_162060%d", index+3)
2260− if gotID := parts[index].Get("functionResponse.id").String(); gotID != wantID {
2261− t.Fatalf("functionResponse[%d].id = %q, want %q; output=%s", index, gotID, wantID, output)
2263+ if gotID := parts[index+1].Get("functionResponse.id").String(); gotID != wantID {
2264+ t.Fatalf("functionResponse[%d].id = %q, want %q; output=%s", index+1, gotID, wantID, output)
22622265 }
22632266 }
2264− if got := parts[6].Get("text").String(); got != "Tool results follow." {
2265− t.Fatalf("first trailing text = %q; output=%s", got, output)
2267+ if got := parts[4].Get("text").String(); got != "Continue after reading." {
2268+ t.Fatalf("middle text = %q; output=%s", got, output)
22662269 }
2267− if got := parts[7].Get("text").String(); got != "Continue after reading." {
2268− t.Fatalf("second trailing text = %q; output=%s", got, output)
2270+ for index := 3; index < 6; index++ {
2271+ wantID := fmt.Sprintf("call_162060%d", index+3)
2272+ if gotID := parts[index+2].Get("functionResponse.id").String(); gotID != wantID {
2273+ t.Fatalf("functionResponse[%d].id = %q, want %q; output=%s", index+2, gotID, wantID, output)
2274+ }
22692275 }
22702276 if errPairing := internalsignature.ValidateGeminiFunctionCallPairing(output); errPairing != nil {
22712277 t.Fatalf("translated parallel tool history is invalid: %v; output=%s", errPairing, output)
internal/translator/common/claude_messages.go+15−11
@@ -76,9 +76,9 @@ func (a *ClaudeMessageAccumulator) Messages() [][]byte { · 75 unmodified lines
7676 return a.messages
7777 }
7878
79−// AlignClaudeToolResults orders tool_result blocks by the preceding tool_use IDs.
80−// Other content blocks retain their relative order after the tool results. If a
81−// complete one-to-one match is unavailable, the original content is returned.
79+// AlignClaudeToolResults orders tool_result blocks by the preceding tool_use IDs,
80+// preserving non-result content blocks at their existing indexes. If a complete
81+// one-to-one match is unavailable, the original content is returned.
8282 func AlignClaudeToolResults(content gjson.Result, toolUseIDs []string) gjson.Result {
8383 if !content.IsArray() || len(toolUseIDs) == 0 {
8484 return content
@@ -86,19 +86,18 @@ func AlignClaudeToolResults(content gjson.Result, toolUseIDs []string) gjson.Res · 1 unmodified line
8686
8787 parts := content.Array()
8888 toolResults := make([]gjson.Result, 0, len(toolUseIDs))
89− otherParts := make([]gjson.Result, 0, len(parts))
90− for _, part := range parts {
89+ toolResultIndices := make([]int, 0, len(toolUseIDs))
90+ for i, part := range parts {
9191 if part.Get("type").String() == "tool_result" {
9292 toolResults = append(toolResults, part)
93− continue
93+ toolResultIndices = append(toolResultIndices, i)
9494 }
95− otherParts = append(otherParts, part)
9695 }
9796 if len(toolResults) != len(toolUseIDs) {
9897 return content
9998 }
10099
101− ordered := make([][]byte, 0, len(parts))
100+ reorderedResults := make([]gjson.Result, 0, len(toolUseIDs))
102101 used := make([]bool, len(toolResults))
103102 for _, toolUseID := range toolUseIDs {
104103 matched := -1
@@ -112,10 +111,15 @@ func AlignClaudeToolResults(content gjson.Result, toolUseIDs []string) gjson.Res · 7 unmodified lines
112111 return content
113112 }
114113 used[matched] = true
115− ordered = append(ordered, []byte(toolResults[matched].Raw))
114+ reorderedResults = append(reorderedResults, toolResults[matched])
115+ }
116+
117+ ordered := make([][]byte, len(parts))
118+ for i, part := range parts {
119+ ordered[i] = []byte(part.Raw)
116120 }
117− for _, part := range otherParts {
118− ordered = append(ordered, []byte(part.Raw))
121+ for i, slotIndex := range toolResultIndices {
122+ ordered[slotIndex] = []byte(reorderedResults[i].Raw)
119123 }
120124 return gjson.ParseBytes(JoinRawArray(ordered))
121125 }
internal/translator/common/claude_messages_test.go+25−5
@@ -110,7 +110,7 @@ func TestClaudeMessageAccumulatorPreservesBlockCacheControl(t *testing.T) { · 109 unmodified lines
110110 }
111111
112112 func TestAlignClaudeToolResults(t *testing.T) {
113− t.Run("reorders permuted results and keeps other parts after", func(t *testing.T) {
113+ t.Run("reorders permuted results while preserving non-result slots", func(t *testing.T) {
114114 input := gjson.Parse(`[
115115 {"type":"tool_result","tool_use_id":"call_2","content":"two"},
116116 {"type":"text","text":"extra user text"},
@@ -124,11 +124,31 @@ func TestAlignClaudeToolResults(t *testing.T) { · 7 unmodified lines
124124 if parts[0].Get("tool_use_id").String() != "call_1" {
125125 t.Fatalf("parts[0].tool_use_id = %q, want call_1", parts[0].Get("tool_use_id").String())
126126 }
127− if parts[1].Get("tool_use_id").String() != "call_2" {
128− t.Fatalf("parts[1].tool_use_id = %q, want call_2", parts[1].Get("tool_use_id").String())
127+ if parts[1].Get("type").String() != "text" || parts[1].Get("text").String() != "extra user text" {
128+ t.Fatalf("parts[1] = %s, want extra user text", parts[1].Raw)
129129 }
130− if parts[2].Get("type").String() != "text" || parts[2].Get("text").String() != "extra user text" {
131− t.Fatalf("parts[2] = %s, want extra user text", parts[2].Raw)
130+ if parts[2].Get("tool_use_id").String() != "call_2" {
131+ t.Fatalf("parts[2].tool_use_id = %q, want call_2", parts[2].Get("tool_use_id").String())
132+ }
133+
134+ leadingTextInput := gjson.Parse(`[
135+ {"type":"text","text":"leading text"},
136+ {"type":"tool_result","tool_use_id":"call_2","content":"two"},
137+ {"type":"tool_result","tool_use_id":"call_1","content":"one"}
138+ ]`)
139+ leadingAligned := AlignClaudeToolResults(leadingTextInput, []string{"call_1", "call_2"})
140+ leadingParts := leadingAligned.Array()
141+ if len(leadingParts) != 3 {
142+ t.Fatalf("len(leadingParts) = %d, want 3", len(leadingParts))
143+ }
144+ if leadingParts[0].Get("type").String() != "text" || leadingParts[0].Get("text").String() != "leading text" {
145+ t.Fatalf("leadingParts[0] = %s, want leading text", leadingParts[0].Raw)
146+ }
147+ if leadingParts[1].Get("tool_use_id").String() != "call_1" {
148+ t.Fatalf("leadingParts[1].tool_use_id = %q, want call_1", leadingParts[1].Get("tool_use_id").String())
149+ }
150+ if leadingParts[2].Get("tool_use_id").String() != "call_2" {
151+ t.Fatalf("leadingParts[2].tool_use_id = %q, want call_2", leadingParts[2].Get("tool_use_id").String())
132152 }
133153 })
134154
internal/translator/gemini/claude/gemini_claude_request_test.go+9−6
@@ -389,18 +389,21 @@ func TestConvertClaudeRequestToGemini_AlignsPermutedParallelToolResultsWithMixed · 388 unmodified lines
389389 if gotID := callParts[index].Get("functionCall.id").String(); gotID != wantID {
390390 t.Fatalf("functionCall[%d].id = %q, want %q; output=%s", index, gotID, wantID, output)
391391 }
392− if gotID := responseParts[index].Get("functionResponse.id").String(); gotID != wantID {
392+ }
393+ if got := responseParts[0].Get("text").String(); got != "Results arrived." {
394+ t.Fatalf("leading text = %q; output=%s", got, output)
395+ }
396+ for index, wantID := range []string{"call_1", "call_2", "call_3"} {
397+ responsePart := responseParts[index+1]
398+ if gotID := responsePart.Get("functionResponse.id").String(); gotID != wantID {
393399 t.Fatalf("functionResponse[%d].id = %q, want %q; output=%s", index, gotID, wantID, output)
394400 }
395− if gotName := responseParts[index].Get("functionResponse.name").String(); gotName != "Read" {
401+ if gotName := responsePart.Get("functionResponse.name").String(); gotName != "Read" {
396402 t.Fatalf("functionResponse[%d].name = %q, want Read; output=%s", index, gotName, output)
397403 }
398404 }
399− if got := responseParts[3].Get("text").String(); got != "Results arrived." {
400− t.Fatalf("first trailing text = %q; output=%s", got, output)
401− }
402405 if got := responseParts[4].Get("text").String(); got != "Continue." {
403− t.Fatalf("second trailing text = %q; output=%s", got, output)
406+ t.Fatalf("trailing text = %q; output=%s", got, output)
404407 }
405408 if errPairing := internalsignature.ValidateGeminiFunctionCallPairing(output); errPairing != nil {
406409 t.Fatalf("translated parallel tool history is invalid: %v; output=%s", errPairing, output)