diff --git a/src/agents/pi-embedded-helpers.validate-turns.test.ts b/src/agents/pi-embedded-helpers.validate-turns.test.ts index fab5f157cf9..f87e61d1014 100644 --- a/src/agents/pi-embedded-helpers.validate-turns.test.ts +++ b/src/agents/pi-embedded-helpers.validate-turns.test.ts @@ -583,6 +583,39 @@ describe("validateAnthropicTurns strips dangling tool_use blocks", () => { ]); }); + it("preserves signed-thinking turns when a tool result carries both stale and current id aliases", () => { + const msgs = asMessages([ + { role: "user", content: [{ type: "text", text: "Use tool" }] }, + { + role: "assistant", + content: [ + { type: "thinking", thinking: "internal", thinkingSignature: "sig_1" }, + { type: "toolCall", id: "tool-current", name: "gateway", arguments: {} }, + ], + }, + { + role: "user", + content: [ + { + type: "toolResult", + toolUseId: "tool-stale", + toolCallId: "tool-current", + content: [{ type: "text", text: "ok" }], + }, + { type: "text", text: "Continue" }, + ], + }, + ]); + + const result = validateAnthropicTurns(msgs); + + expect(result).toHaveLength(3); + expect((result[1] as { content?: unknown[] }).content).toEqual([ + { type: "thinking", thinking: "internal", thinkingSignature: "sig_1" }, + { type: "toolCall", id: "tool-current", name: "gateway", arguments: {} }, + ]); + }); + it("drops signed-thinking turns whose sibling tool calls are dangling", () => { const msgs = asMessages([ { role: "user", content: [{ type: "text", text: "Use tool" }] }, diff --git a/src/agents/pi-embedded-helpers/turns.ts b/src/agents/pi-embedded-helpers/turns.ts index b683500b498..e9dd1dd15e7 100644 --- a/src/agents/pi-embedded-helpers/turns.ts +++ b/src/agents/pi-embedded-helpers/turns.ts @@ -28,16 +28,22 @@ function isAbortedAssistantTurn(message: AgentMessage): boolean { return stopReason === "aborted" || stopReason === "error"; } -function extractToolResultMatchId(record: Record): string | null { - return ( - normalizeOptionalString(record.toolUseId) ?? - normalizeOptionalString(record.toolCallId) ?? - normalizeOptionalString(record.tool_use_id) ?? - normalizeOptionalString(record.tool_call_id) ?? - normalizeOptionalString(record.callId) ?? - normalizeOptionalString(record.call_id) ?? - null - ); +function extractToolResultMatchIds(record: Record): Set { + const ids = new Set(); + for (const value of [ + record.toolUseId, + record.toolCallId, + record.tool_use_id, + record.tool_call_id, + record.callId, + record.call_id, + ]) { + const id = normalizeOptionalString(value); + if (id) { + ids.add(id); + } + } + return ids; } function extractToolResultMatchName(record: Record): string | null { @@ -56,8 +62,7 @@ function collectAnyToolResultIds(message: AgentMessage): Set { } } else if (role === "tool") { const record = message as unknown as Record; - const id = extractToolResultMatchId(record); - if (id) { + for (const id of extractToolResultMatchIds(record)) { ids.add(id); } } @@ -75,8 +80,7 @@ function collectAnyToolResultIds(message: AgentMessage): Set { if (record.type !== "toolResult" && record.type !== "tool") { continue; } - const id = extractToolResultMatchId(record); - if (id) { + for (const id of extractToolResultMatchIds(record)) { ids.add(id); } } @@ -87,26 +91,33 @@ function collectAnyToolResultIds(message: AgentMessage): Set { function collectTrustedToolResultMatches(message: AgentMessage): Map> { const matches = new Map>(); const role = (message as { role?: unknown }).role; - const addMatch = (id: string | null, toolName: string | null) => { - if (!id) { - return; + const addMatch = (ids: Iterable, toolName: string | null) => { + for (const id of ids) { + const bucket = matches.get(id) ?? new Set(); + if (toolName) { + bucket.add(toolName); + } + matches.set(id, bucket); } - const bucket = matches.get(id) ?? new Set(); - if (toolName) { - bucket.add(toolName); - } - matches.set(id, bucket); }; if (role === "toolResult") { const record = message as unknown as Record; addMatch( - extractToolResultId(message as Extract), + [ + ...extractToolResultMatchIds(record), + ...(() => { + const canonicalId = extractToolResultId( + message as Extract, + ); + return canonicalId ? [canonicalId] : []; + })(), + ], extractToolResultMatchName(record), ); } else if (role === "tool") { const record = message as unknown as Record; - addMatch(extractToolResultMatchId(record), extractToolResultMatchName(record)); + addMatch(extractToolResultMatchIds(record), extractToolResultMatchName(record)); } const content = (message as { content?: unknown }).content; @@ -122,7 +133,7 @@ function collectTrustedToolResultMatches(message: AgentMessage): Map