mirror of
https://github.com/RooVetGit/Roo-Code.git
synced 2026-09-07 08:26:51 +00:00
fix: reorder tool_result blocks to match tool_use order (fixes #11804)
When the Anthropic API returns multiple tool_use blocks, tool_result blocks must appear in the same order. The existing validateAndFixToolResultIds() already handled missing/duplicate results but did not reorder them. This adds a reorderToolResults() helper that sorts tool_result blocks by their corresponding tool_use index while keeping non-tool-result blocks (text, image) in their original positions.
This commit is contained in:
parent
193053110b
commit
2bd2c13790
2 changed files with 211 additions and 15 deletions
|
|
@ -611,11 +611,10 @@ describe("validateAndFixToolResultIds", () => {
|
|||
const resultContent = result.content as Anthropic.ToolResultBlockParam[]
|
||||
// Should now have 2 tool_results: one fixed and one added for the missing tool_use
|
||||
expect(resultContent.length).toBe(2)
|
||||
// The missing tool_result is prepended
|
||||
expect(resultContent[0].tool_use_id).toBe("tool-2")
|
||||
expect(resultContent[0].content).toBe("Tool execution was interrupted before completion.")
|
||||
// The original is fixed
|
||||
expect(resultContent[1].tool_use_id).toBe("tool-1")
|
||||
// Reordered to match tool_use order: tool-1 first (fixed from wrong-1), tool-2 second (injected)
|
||||
expect(resultContent[0].tool_use_id).toBe("tool-1")
|
||||
expect(resultContent[1].tool_use_id).toBe("tool-2")
|
||||
expect(resultContent[1].content).toBe("Tool execution was interrupted before completion.")
|
||||
})
|
||||
})
|
||||
|
||||
|
|
@ -736,12 +735,11 @@ describe("validateAndFixToolResultIds", () => {
|
|||
expect(Array.isArray(result.content)).toBe(true)
|
||||
const resultContent = result.content as Anthropic.ToolResultBlockParam[]
|
||||
expect(resultContent.length).toBe(2)
|
||||
// Missing tool_result for tool-2 should be prepended
|
||||
expect(resultContent[0].tool_use_id).toBe("tool-2")
|
||||
expect(resultContent[0].content).toBe("Tool execution was interrupted before completion.")
|
||||
// Existing tool_result should be preserved
|
||||
expect(resultContent[1].tool_use_id).toBe("tool-1")
|
||||
expect(resultContent[1].content).toBe("Content for tool 1")
|
||||
// Reordered to match tool_use order: tool-1 first (existing), tool-2 second (injected)
|
||||
expect(resultContent[0].tool_use_id).toBe("tool-1")
|
||||
expect(resultContent[0].content).toBe("Content for tool 1")
|
||||
expect(resultContent[1].tool_use_id).toBe("tool-2")
|
||||
expect(resultContent[1].content).toBe("Tool execution was interrupted before completion.")
|
||||
})
|
||||
|
||||
it("should handle empty user content array by adding all missing tool_results", () => {
|
||||
|
|
@ -994,4 +992,116 @@ describe("validateAndFixToolResultIds", () => {
|
|||
expect(TelemetryService.instance.captureException).not.toHaveBeenCalled()
|
||||
})
|
||||
})
|
||||
|
||||
describe("tool_result reordering to match tool_use order", () => {
|
||||
it("should reorder out-of-order tool_results to match tool_use order", () => {
|
||||
const assistantMessage: Anthropic.MessageParam = {
|
||||
role: "assistant",
|
||||
content: [
|
||||
{ type: "tool_use", id: "tool-A", name: "read_file", input: { path: "a.txt" } },
|
||||
{ type: "tool_use", id: "tool-B", name: "read_file", input: { path: "b.txt" } },
|
||||
{ type: "tool_use", id: "tool-C", name: "read_file", input: { path: "c.txt" } },
|
||||
],
|
||||
}
|
||||
|
||||
// tool_results arrive in reverse order (C, B, A) instead of (A, B, C)
|
||||
const userMessage: Anthropic.MessageParam = {
|
||||
role: "user",
|
||||
content: [
|
||||
{ type: "tool_result", tool_use_id: "tool-C", content: "Result C" },
|
||||
{ type: "tool_result", tool_use_id: "tool-B", content: "Result B" },
|
||||
{ type: "tool_result", tool_use_id: "tool-A", content: "Result A" },
|
||||
],
|
||||
}
|
||||
|
||||
const result = validateAndFixToolResultIds(userMessage, [assistantMessage])
|
||||
const content = result.content as Anthropic.ToolResultBlockParam[]
|
||||
|
||||
expect(content[0].tool_use_id).toBe("tool-A")
|
||||
expect(content[1].tool_use_id).toBe("tool-B")
|
||||
expect(content[2].tool_use_id).toBe("tool-C")
|
||||
})
|
||||
|
||||
it("should keep non-tool-result blocks in their original positions when reordering", () => {
|
||||
const assistantMessage: Anthropic.MessageParam = {
|
||||
role: "assistant",
|
||||
content: [
|
||||
{ type: "tool_use", id: "tool-1", name: "read_file", input: { path: "a.txt" } },
|
||||
{ type: "tool_use", id: "tool-2", name: "write_file", input: { path: "b.txt" } },
|
||||
],
|
||||
}
|
||||
|
||||
// tool_results are reversed, with a text block in between
|
||||
const userMessage: Anthropic.MessageParam = {
|
||||
role: "user",
|
||||
content: [
|
||||
{ type: "tool_result", tool_use_id: "tool-2", content: "Result 2" },
|
||||
{ type: "text", text: "Here are the results" },
|
||||
{ type: "tool_result", tool_use_id: "tool-1", content: "Result 1" },
|
||||
],
|
||||
}
|
||||
|
||||
const result = validateAndFixToolResultIds(userMessage, [assistantMessage])
|
||||
const content = result.content as Anthropic.Messages.ContentBlockParam[]
|
||||
|
||||
// tool_results should be reordered, but text block stays at index 1
|
||||
expect(content[0]).toEqual({ type: "tool_result", tool_use_id: "tool-1", content: "Result 1" })
|
||||
expect(content[1]).toEqual({ type: "text", text: "Here are the results" })
|
||||
expect(content[2]).toEqual({ type: "tool_result", tool_use_id: "tool-2", content: "Result 2" })
|
||||
})
|
||||
|
||||
it("should not modify content when tool_results are already in correct order", () => {
|
||||
const assistantMessage: Anthropic.MessageParam = {
|
||||
role: "assistant",
|
||||
content: [
|
||||
{ type: "tool_use", id: "tool-X", name: "read_file", input: { path: "x.txt" } },
|
||||
{ type: "tool_use", id: "tool-Y", name: "read_file", input: { path: "y.txt" } },
|
||||
],
|
||||
}
|
||||
|
||||
const userMessage: Anthropic.MessageParam = {
|
||||
role: "user",
|
||||
content: [
|
||||
{ type: "tool_result", tool_use_id: "tool-X", content: "Result X" },
|
||||
{ type: "tool_result", tool_use_id: "tool-Y", content: "Result Y" },
|
||||
],
|
||||
}
|
||||
|
||||
const result = validateAndFixToolResultIds(userMessage, [assistantMessage])
|
||||
|
||||
expect(result).toEqual(userMessage)
|
||||
})
|
||||
|
||||
it("should reorder tool_results even when mixed with missing result injection", () => {
|
||||
const assistantMessage: Anthropic.MessageParam = {
|
||||
role: "assistant",
|
||||
content: [
|
||||
{ type: "tool_use", id: "tool-1", name: "read_file", input: { path: "a.txt" } },
|
||||
{ type: "tool_use", id: "tool-2", name: "write_file", input: { path: "b.txt" } },
|
||||
{ type: "tool_use", id: "tool-3", name: "list_files", input: { path: "." } },
|
||||
],
|
||||
}
|
||||
|
||||
// Only tool-3 and tool-1 have results (out of order), tool-2 is missing
|
||||
const userMessage: Anthropic.MessageParam = {
|
||||
role: "user",
|
||||
content: [
|
||||
{ type: "tool_result", tool_use_id: "tool-3", content: "Result 3" },
|
||||
{ type: "tool_result", tool_use_id: "tool-1", content: "Result 1" },
|
||||
],
|
||||
}
|
||||
|
||||
const result = validateAndFixToolResultIds(userMessage, [assistantMessage])
|
||||
const toolResults = (result.content as Anthropic.Messages.ContentBlockParam[]).filter(
|
||||
(b): b is Anthropic.ToolResultBlockParam => b.type === "tool_result",
|
||||
)
|
||||
|
||||
// Should have 3 tool_results (including injected one for tool-2)
|
||||
expect(toolResults).toHaveLength(3)
|
||||
// They should be in tool_use order: tool-1, tool-2, tool-3
|
||||
expect(toolResults[0].tool_use_id).toBe("tool-1")
|
||||
expect(toolResults[1].tool_use_id).toBe("tool-2")
|
||||
expect(toolResults[2].tool_use_id).toBe("tool-3")
|
||||
})
|
||||
})
|
||||
})
|
||||
|
|
|
|||
|
|
@ -122,8 +122,16 @@ export function validateAndFixToolResultIds(
|
|||
// Check if any tool_result has an invalid ID
|
||||
const hasInvalidIds = toolResults.some((result) => !validToolUseIds.has(result.tool_use_id))
|
||||
|
||||
// If no missing tool_results and no invalid IDs, no changes needed
|
||||
// If no missing tool_results and no invalid IDs, check if reordering is needed
|
||||
if (missingToolUseIds.length === 0 && !hasInvalidIds) {
|
||||
// Reorder tool_result blocks to match tool_use order (required by Anthropic API)
|
||||
const reordered = reorderToolResults(
|
||||
userMessage.content as Anthropic.Messages.ContentBlockParam[],
|
||||
toolUseBlocks,
|
||||
)
|
||||
if (reordered) {
|
||||
return { ...userMessage, content: reordered }
|
||||
}
|
||||
return userMessage
|
||||
}
|
||||
|
||||
|
|
@ -223,12 +231,90 @@ export function validateAndFixToolResultIds(
|
|||
content: "Tool execution was interrupted before completion.",
|
||||
}))
|
||||
|
||||
// Insert missing tool_results at the beginning of the content array
|
||||
// This ensures they come before any text blocks that may summarize the results
|
||||
const finalContent = missingToolResults.length > 0 ? [...missingToolResults, ...correctedContent] : correctedContent
|
||||
// Combine missing tool_results with corrected content
|
||||
const combinedContent =
|
||||
missingToolResults.length > 0 ? [...missingToolResults, ...correctedContent] : correctedContent
|
||||
|
||||
// Reorder tool_result blocks to match the tool_use order (required by Anthropic API).
|
||||
// This handles the case where tool results were appended in completion order rather
|
||||
// than the original tool_use order.
|
||||
const finalContent = reorderToolResults(combinedContent, toolUseBlocks) ?? combinedContent
|
||||
|
||||
return {
|
||||
...userMessage,
|
||||
content: finalContent,
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Reorders tool_result blocks within a content array to match the order of
|
||||
* their corresponding tool_use blocks from the assistant message.
|
||||
*
|
||||
* Non-tool-result blocks (text, image, etc.) remain in their original
|
||||
* positions relative to the tool_result blocks -- only tool_results are
|
||||
* reordered among themselves.
|
||||
*
|
||||
* Returns `null` if the tool_results are already in the correct order
|
||||
* (no reordering needed).
|
||||
*/
|
||||
function reorderToolResults(
|
||||
content: Anthropic.Messages.ContentBlockParam[],
|
||||
toolUseBlocks: Anthropic.ToolUseBlock[],
|
||||
): Anthropic.Messages.ContentBlockParam[] | null {
|
||||
if (toolUseBlocks.length === 0) {
|
||||
return null
|
||||
}
|
||||
|
||||
// Build an order map: tool_use_id -> position index
|
||||
const orderMap = new Map<string, number>()
|
||||
toolUseBlocks.forEach((block, index) => {
|
||||
orderMap.set(block.id, index)
|
||||
})
|
||||
|
||||
// Separate tool_result blocks from non-tool-result blocks, preserving indices
|
||||
const toolResultEntries: { index: number; block: Anthropic.ToolResultBlockParam }[] = []
|
||||
const nonToolResultEntries: { index: number; block: Anthropic.Messages.ContentBlockParam }[] = []
|
||||
|
||||
content.forEach((block, index) => {
|
||||
if (block.type === "tool_result") {
|
||||
toolResultEntries.push({ index, block: block as Anthropic.ToolResultBlockParam })
|
||||
} else {
|
||||
nonToolResultEntries.push({ index, block })
|
||||
}
|
||||
})
|
||||
|
||||
if (toolResultEntries.length <= 1) {
|
||||
return null // Nothing to reorder
|
||||
}
|
||||
|
||||
// Sort tool_result blocks by their corresponding tool_use order
|
||||
const sortedToolResults = [...toolResultEntries].sort((a, b) => {
|
||||
const orderA = orderMap.get(a.block.tool_use_id) ?? Number.MAX_SAFE_INTEGER
|
||||
const orderB = orderMap.get(b.block.tool_use_id) ?? Number.MAX_SAFE_INTEGER
|
||||
return orderA - orderB
|
||||
})
|
||||
|
||||
// Check if already in correct order
|
||||
const alreadyOrdered = sortedToolResults.every((entry, i) => entry === toolResultEntries[i])
|
||||
if (alreadyOrdered) {
|
||||
return null
|
||||
}
|
||||
|
||||
// Reconstruct the array: place sorted tool_results into the original
|
||||
// tool_result positions, keeping non-tool-result blocks where they were.
|
||||
const result: Anthropic.Messages.ContentBlockParam[] = new Array(content.length)
|
||||
|
||||
// First, place non-tool-result blocks back at their original indices
|
||||
for (const entry of nonToolResultEntries) {
|
||||
result[entry.index] = entry.block
|
||||
}
|
||||
|
||||
// Then, place sorted tool_results into the slots that were originally
|
||||
// occupied by tool_result blocks (preserving relative position of non-tool blocks)
|
||||
const toolResultSlots = toolResultEntries.map((e) => e.index)
|
||||
sortedToolResults.forEach((entry, i) => {
|
||||
result[toolResultSlots[i]] = entry.block
|
||||
})
|
||||
|
||||
return result
|
||||
}
|
||||
|
|
|
|||
Loading…
Add table
Reference in a new issue