diff --git a/src/agents/pi-embedded-helpers.validate-turns.test.ts b/src/agents/pi-embedded-helpers.validate-turns.test.ts index 93271d85225..fab5f157cf9 100644 --- a/src/agents/pi-embedded-helpers.validate-turns.test.ts +++ b/src/agents/pi-embedded-helpers.validate-turns.test.ts @@ -555,6 +555,34 @@ describe("validateAnthropicTurns strips dangling tool_use blocks", () => { ]); }); + it("preserves signed-thinking turns when the matching tool result is embedded in user content", () => { + const msgs = asMessages([ + { role: "user", content: [{ type: "text", text: "Use tool" }] }, + { + role: "assistant", + content: [ + { type: "thinking", thinking: "internal", thinkingSignature: "sig_1" }, + { type: "toolUse", id: "tool-1", name: "gateway", arguments: {} }, + ], + }, + { + role: "user", + content: [ + { type: "toolResult", toolUseId: "tool-1", 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: "toolUse", id: "tool-1", 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 b890214361d..b683500b498 100644 --- a/src/agents/pi-embedded-helpers/turns.ts +++ b/src/agents/pi-embedded-helpers/turns.ts @@ -88,11 +88,13 @@ function collectTrustedToolResultMatches(message: AgentMessage): Map>(); const role = (message as { role?: unknown }).role; const addMatch = (id: string | null, toolName: string | null) => { - if (!id || !toolName) { + if (!id) { return; } const bucket = matches.get(id) ?? new Set(); - bucket.add(toolName); + if (toolName) { + bucket.add(toolName); + } matches.set(id, bucket); }; @@ -107,6 +109,22 @@ function collectTrustedToolResultMatches(message: AgentMessage): Map; + if (record.type !== "toolResult" && record.type !== "tool") { + continue; + } + addMatch(extractToolResultMatchId(record), extractToolResultMatchName(record)); + } + return matches; } @@ -198,9 +216,14 @@ function stripDanglingAnthropicToolUses(messages: AgentMessage[]): AgentMessage[ } const blockId = normalizeOptionalString(block.id); const blockName = normalizeOptionalString(block.name); - return blockId && blockName - ? validToolResultMatches.get(blockId)?.has(blockName) === true - : false; + if (!blockId || !blockName) { + return false; + } + const matchingToolNames = validToolResultMatches.get(blockId); + if (!matchingToolNames) { + return false; + } + return matchingToolNames.size === 0 || matchingToolNames.has(blockName); }); if (allToolCallsResolvable) { result.push(msg);