- 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.
Unified · Split
| @@ -2253,19 +2253,25 @@ func TestConvertClaudeRequestToAntigravity_AlignsPermutedParallelToolResultsWith · 2252 unmodified lines | |||
| 2253 | output := ConvertClaudeRequestToAntigravity("gemini-3.7-flash-high", inputJSON, false) | 2253 | output := ConvertClaudeRequestToAntigravity("gemini-3.7-flash-high", inputJSON, false) |
| 2254 | parts := gjson.GetBytes(output, "request.contents.1.parts").Array() | 2254 | parts := gjson.GetBytes(output, "request.contents.1.parts").Array() |
| 2255 | if len(parts) != 8 { | 2255 | 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) |
| 2257 | } | 2257 | } |
| 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++ { | ||
| 2259 | wantID := fmt.Sprintf("call_162060%d", index+3) | 2262 | wantID := fmt.Sprintf("call_162060%d", index+3) |
| 2260 | if gotID := parts[index].Get("functionResponse.id").String(); gotID != wantID { | 2263 | if gotID := parts[index+1].Get("functionResponse.id").String(); gotID != wantID { |
| 2261 | t.Fatalf("functionResponse[%d].id = %q, want %q; output=%s", index, gotID, wantID, output) | 2264 | t.Fatalf("functionResponse[%d].id = %q, want %q; output=%s", index+1, gotID, wantID, output) |
| 2262 | } | 2265 | } |
| 2263 | } | 2266 | } |
| 2264 | if got := parts[6].Get("text").String(); got != "Tool results follow." { | 2267 | if got := parts[4].Get("text").String(); got != "Continue after reading." { |
| 2265 | t.Fatalf("first trailing text = %q; output=%s", got, output) | 2268 | t.Fatalf("middle text = %q; output=%s", got, output) |
| 2266 | } | 2269 | } |
| 2267 | if got := parts[7].Get("text").String(); got != "Continue after reading." { | 2270 | for index := 3; index < 6; index++ { |
| 2268 | t.Fatalf("second trailing text = %q; output=%s", got, output) | 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 | } | ||
| 2269 | } | 2275 | } |
| 2270 | if errPairing := internalsignature.ValidateGeminiFunctionCallPairing(output); errPairing != nil { | 2276 | if errPairing := internalsignature.ValidateGeminiFunctionCallPairing(output); errPairing != nil { |
| 2271 | t.Fatalf("translated parallel tool history is invalid: %v; output=%s", errPairing, output) | 2277 | t.Fatalf("translated parallel tool history is invalid: %v; output=%s", errPairing, output) |
| @@ -76,9 +76,9 @@ func (a *ClaudeMessageAccumulator) Messages() [][]byte { · 75 unmodified lines | |||
| 76 | return a.messages | 76 | return a.messages |
| 77 | } | 77 | } |
| 78 | 78 | ||
| 79 | // AlignClaudeToolResults orders tool_result blocks by the preceding tool_use IDs. | 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 | 80 | // preserving non-result content blocks at their existing indexes. If a complete |
| 81 | // complete one-to-one match is unavailable, the original content is returned. | 81 | // one-to-one match is unavailable, the original content is returned. |
| 82 | func AlignClaudeToolResults(content gjson.Result, toolUseIDs []string) gjson.Result { | 82 | func AlignClaudeToolResults(content gjson.Result, toolUseIDs []string) gjson.Result { |
| 83 | if !content.IsArray() || len(toolUseIDs) == 0 { | 83 | if !content.IsArray() || len(toolUseIDs) == 0 { |
| 84 | return content | 84 | return content |
| @@ -86,19 +86,18 @@ func AlignClaudeToolResults(content gjson.Result, toolUseIDs []string) gjson.Res · 1 unmodified line | |||
| 86 | 86 | ||
| 87 | parts := content.Array() | 87 | parts := content.Array() |
| 88 | toolResults := make([]gjson.Result, 0, len(toolUseIDs)) | 88 | toolResults := make([]gjson.Result, 0, len(toolUseIDs)) |
| 89 | otherParts := make([]gjson.Result, 0, len(parts)) | 89 | toolResultIndices := make([]int, 0, len(toolUseIDs)) |
| 90 | for _, part := range parts { | 90 | for i, part := range parts { |
| 91 | if part.Get("type").String() == "tool_result" { | 91 | if part.Get("type").String() == "tool_result" { |
| 92 | toolResults = append(toolResults, part) | 92 | toolResults = append(toolResults, part) |
| 93 | continue | 93 | toolResultIndices = append(toolResultIndices, i) |
| 94 | } | 94 | } |
| 95 | otherParts = append(otherParts, part) | ||
| 96 | } | 95 | } |
| 97 | if len(toolResults) != len(toolUseIDs) { | 96 | if len(toolResults) != len(toolUseIDs) { |
| 98 | return content | 97 | return content |
| 99 | } | 98 | } |
| 100 | 99 | ||
| 101 | ordered := make([][]byte, 0, len(parts)) | 100 | reorderedResults := make([]gjson.Result, 0, len(toolUseIDs)) |
| 102 | used := make([]bool, len(toolResults)) | 101 | used := make([]bool, len(toolResults)) |
| 103 | for _, toolUseID := range toolUseIDs { | 102 | for _, toolUseID := range toolUseIDs { |
| 104 | matched := -1 | 103 | matched := -1 |
| @@ -112,10 +111,15 @@ func AlignClaudeToolResults(content gjson.Result, toolUseIDs []string) gjson.Res · 7 unmodified lines | |||
| 112 | return content | 111 | return content |
| 113 | } | 112 | } |
| 114 | used[matched] = true | 113 | 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) | ||
| 116 | } | 120 | } |
| 117 | for _, part := range otherParts { | 121 | for i, slotIndex := range toolResultIndices { |
| 118 | ordered = append(ordered, []byte(part.Raw)) | 122 | ordered[slotIndex] = []byte(reorderedResults[i].Raw) |
| 119 | } | 123 | } |
| 120 | return gjson.ParseBytes(JoinRawArray(ordered)) | 124 | return gjson.ParseBytes(JoinRawArray(ordered)) |
| 121 | } | 125 | } |
| @@ -110,7 +110,7 @@ func TestClaudeMessageAccumulatorPreservesBlockCacheControl(t *testing.T) { · 109 unmodified lines | |||
| 110 | } | 110 | } |
| 111 | 111 | ||
| 112 | func TestAlignClaudeToolResults(t *testing.T) { | 112 | 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) { |
| 114 | input := gjson.Parse(`[ | 114 | input := gjson.Parse(`[ |
| 115 | {"type":"tool_result","tool_use_id":"call_2","content":"two"}, | 115 | {"type":"tool_result","tool_use_id":"call_2","content":"two"}, |
| 116 | {"type":"text","text":"extra user text"}, | 116 | {"type":"text","text":"extra user text"}, |
| @@ -124,11 +124,31 @@ func TestAlignClaudeToolResults(t *testing.T) { · 7 unmodified lines | |||
| 124 | if parts[0].Get("tool_use_id").String() != "call_1" { | 124 | if parts[0].Get("tool_use_id").String() != "call_1" { |
| 125 | t.Fatalf("parts[0].tool_use_id = %q, want call_1", parts[0].Get("tool_use_id").String()) | 125 | t.Fatalf("parts[0].tool_use_id = %q, want call_1", parts[0].Get("tool_use_id").String()) |
| 126 | } | 126 | } |
| 127 | if parts[1].Get("tool_use_id").String() != "call_2" { | 127 | if parts[1].Get("type").String() != "text" || parts[1].Get("text").String() != "extra user text" { |
| 128 | t.Fatalf("parts[1].tool_use_id = %q, want call_2", parts[1].Get("tool_use_id").String()) | 128 | t.Fatalf("parts[1] = %s, want extra user text", parts[1].Raw) |
| 129 | } | 129 | } |
| 130 | if parts[2].Get("type").String() != "text" || parts[2].Get("text").String() != "extra user text" { | 130 | if parts[2].Get("tool_use_id").String() != "call_2" { |
| 131 | t.Fatalf("parts[2] = %s, want extra user text", parts[2].Raw) | 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()) | ||
| 132 | } | 152 | } |
| 133 | }) | 153 | }) |
| 134 | 154 | ||
| @@ -389,18 +389,21 @@ func TestConvertClaudeRequestToGemini_AlignsPermutedParallelToolResultsWithMixed · 388 unmodified lines | |||
| 389 | if gotID := callParts[index].Get("functionCall.id").String(); gotID != wantID { | 389 | if gotID := callParts[index].Get("functionCall.id").String(); gotID != wantID { |
| 390 | t.Fatalf("functionCall[%d].id = %q, want %q; output=%s", index, gotID, wantID, output) | 390 | t.Fatalf("functionCall[%d].id = %q, want %q; output=%s", index, gotID, wantID, output) |
| 391 | } | 391 | } |
| 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 { | ||
| 393 | t.Fatalf("functionResponse[%d].id = %q, want %q; output=%s", index, gotID, wantID, output) | 399 | t.Fatalf("functionResponse[%d].id = %q, want %q; output=%s", index, gotID, wantID, output) |
| 394 | } | 400 | } |
| 395 | if gotName := responseParts[index].Get("functionResponse.name").String(); gotName != "Read" { | 401 | if gotName := responsePart.Get("functionResponse.name").String(); gotName != "Read" { |
| 396 | t.Fatalf("functionResponse[%d].name = %q, want Read; output=%s", index, gotName, output) | 402 | t.Fatalf("functionResponse[%d].name = %q, want Read; output=%s", index, gotName, output) |
| 397 | } | 403 | } |
| 398 | } | 404 | } |
| 399 | if got := responseParts[3].Get("text").String(); got != "Results arrived." { | ||
| 400 | t.Fatalf("first trailing text = %q; output=%s", got, output) | ||
| 401 | } | ||
| 402 | if got := responseParts[4].Get("text").String(); got != "Continue." { | 405 | 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) |
| 404 | } | 407 | } |
| 405 | if errPairing := internalsignature.ValidateGeminiFunctionCallPairing(output); errPairing != nil { | 408 | if errPairing := internalsignature.ValidateGeminiFunctionCallPairing(output); errPairing != nil { |
| 406 | t.Fatalf("translated parallel tool history is invalid: %v; output=%s", errPairing, output) | 409 | t.Fatalf("translated parallel tool history is invalid: %v; output=%s", errPairing, output) |