- 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 | 2253 | output := ConvertClaudeRequestToAntigravity("gemini-3.7-flash-high", inputJSON, false) |
| 2254 | 2254 | parts := gjson.GetBytes(output, "request.contents.1.parts").Array() |
| 2255 | 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 | 2262 | 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) | |
| 2262 | 2265 | } |
| 2263 | 2266 | } |
| 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) | |
| 2266 | 2269 | } |
| 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 | + } | |
| 2269 | 2275 | } |
| 2270 | 2276 | if errPairing := internalsignature.ValidateGeminiFunctionCallPairing(output); errPairing != nil { |
| 2271 | 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 | 76 | return a.messages |
| 77 | 77 | } |
| 78 | 78 | |
| 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. | |
| 82 | 82 | func AlignClaudeToolResults(content gjson.Result, toolUseIDs []string) gjson.Result { |
| 83 | 83 | if !content.IsArray() || len(toolUseIDs) == 0 { |
| 84 | 84 | return content |
| @@ -86,19 +86,18 @@ func AlignClaudeToolResults(content gjson.Result, toolUseIDs []string) gjson.Res · 1 unmodified line | ||
| 86 | 86 | |
| 87 | 87 | parts := content.Array() |
| 88 | 88 | 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 { | |
| 91 | 91 | if part.Get("type").String() == "tool_result" { |
| 92 | 92 | toolResults = append(toolResults, part) |
| 93 | − continue | |
| 93 | + toolResultIndices = append(toolResultIndices, i) | |
| 94 | 94 | } |
| 95 | − otherParts = append(otherParts, part) | |
| 96 | 95 | } |
| 97 | 96 | if len(toolResults) != len(toolUseIDs) { |
| 98 | 97 | return content |
| 99 | 98 | } |
| 100 | 99 | |
| 101 | − ordered := make([][]byte, 0, len(parts)) | |
| 100 | + reorderedResults := make([]gjson.Result, 0, len(toolUseIDs)) | |
| 102 | 101 | used := make([]bool, len(toolResults)) |
| 103 | 102 | for _, toolUseID := range toolUseIDs { |
| 104 | 103 | matched := -1 |
| @@ -112,10 +111,15 @@ func AlignClaudeToolResults(content gjson.Result, toolUseIDs []string) gjson.Res · 7 unmodified lines | ||
| 112 | 111 | return content |
| 113 | 112 | } |
| 114 | 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 { | |
| 118 | − ordered = append(ordered, []byte(part.Raw)) | |
| 121 | + for i, slotIndex := range toolResultIndices { | |
| 122 | + ordered[slotIndex] = []byte(reorderedResults[i].Raw) | |
| 119 | 123 | } |
| 120 | 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 | 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 | 114 | input := gjson.Parse(`[ |
| 115 | 115 | {"type":"tool_result","tool_use_id":"call_2","content":"two"}, |
| 116 | 116 | {"type":"text","text":"extra user text"}, |
| @@ -124,11 +124,31 @@ func TestAlignClaudeToolResults(t *testing.T) { · 7 unmodified lines | ||
| 124 | 124 | if parts[0].Get("tool_use_id").String() != "call_1" { |
| 125 | 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" { | |
| 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) | |
| 129 | 129 | } |
| 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()) | |
| 132 | 152 | } |
| 133 | 153 | }) |
| 134 | 154 | |
| @@ -389,18 +389,21 @@ func TestConvertClaudeRequestToGemini_AlignsPermutedParallelToolResultsWithMixed · 388 unmodified lines | ||
| 389 | 389 | if gotID := callParts[index].Get("functionCall.id").String(); gotID != wantID { |
| 390 | 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 | 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 | 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 | 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 | 408 | if errPairing := internalsignature.ValidateGeminiFunctionCallPairing(output); errPairing != nil { |
| 406 | 409 | t.Fatalf("translated parallel tool history is invalid: %v; output=%s", errPairing, output) |