diff --git a/src/agents/pi-embedded-runner.anthropic-tool-replay.live.test.ts b/src/agents/pi-embedded-runner.anthropic-tool-replay.live.test.ts index ab587fd38ba..1b2b0749a3d 100644 --- a/src/agents/pi-embedded-runner.anthropic-tool-replay.live.test.ts +++ b/src/agents/pi-embedded-runner.anthropic-tool-replay.live.test.ts @@ -76,6 +76,8 @@ describeLive("pi embedded anthropic replay sanitization (live)", () => { const wrapped = wrapStreamFnSanitizeMalformedToolCalls(baseFn as never, new Set(["noop"]), { validateGeminiTurns: false, validateAnthropicTurns: true, + preserveSignatures: false, + dropThinkingBlocks: false, }); await Promise.resolve(wrapped(model as never, { messages } as never, {} as never)); diff --git a/src/agents/pi-embedded-runner.sanitize-session-history.test.ts b/src/agents/pi-embedded-runner.sanitize-session-history.test.ts index 0252f7a6d15..fb5625d84b2 100644 --- a/src/agents/pi-embedded-runner.sanitize-session-history.test.ts +++ b/src/agents/pi-embedded-runner.sanitize-session-history.test.ts @@ -1010,6 +1010,34 @@ describe("sanitizeSessionHistory", () => { }); }); + it("uses immutable thinking replay for amazon-bedrock claude providers when policy preserves signatures", async () => { + setNonGoogleModelApi(); + + const messages = castAgentMessages([ + makeUserMessage("retry"), + makeAssistantMessage([ + { + type: "thinking", + thinking: "internal", + thinkingSignature: "sig_1", + }, + { type: "toolCall", id: "call_1", name: " read ", arguments: {} }, + ] as unknown as AssistantMessage["content"]), + ]); + + const result = await sanitizeAnthropicHistory({ + provider: "amazon-bedrock", + modelApi: "bedrock-converse-stream", + messages, + }); + + expect(result).toHaveLength(1); + expect(result[0]).toMatchObject({ + role: "user", + content: "retry", + }); + }); + it("keeps mutable thinking turns outside exact anthropic replay", async () => { setNonGoogleModelApi(); diff --git a/src/agents/pi-embedded-runner/run/attempt.test.ts b/src/agents/pi-embedded-runner/run/attempt.test.ts index b96ff7e9533..5460876a3e3 100644 --- a/src/agents/pi-embedded-runner/run/attempt.test.ts +++ b/src/agents/pi-embedded-runner/run/attempt.test.ts @@ -1006,6 +1006,50 @@ describe("wrapStreamFnSanitizeMalformedToolCalls", () => { ]); }); + it("drops signed thinking turns for bedrock claude replay when sibling tool calls are not replay-safe", async () => { + const messages = [ + { + role: "assistant", + content: [ + { type: "thinking", thinking: "internal", thinkingSignature: "sig_1" }, + { type: "toolCall", id: "toolu_legacy", name: "gateway", arguments: {} }, + ], + }, + { + role: "user", + content: [{ type: "text", text: "retry" }], + }, + ]; + const baseFn = vi.fn((_model, _context) => + createFakeStream({ events: [], resultMessage: { role: "assistant", content: [] } }), + ); + + const wrapped = wrapStreamFnSanitizeMalformedToolCalls( + baseFn as never, + new Set(["read"]), + { + validateAnthropicTurns: true, + preserveSignatures: true, + dropThinkingBlocks: false, + } as never, + ); + const stream = wrapped( + { api: "bedrock-converse-stream" } as never, + { messages } as never, + {} as never, + ) as FakeWrappedStream | Promise; + await Promise.resolve(stream); + + expect(baseFn).toHaveBeenCalledTimes(1); + const seenContext = baseFn.mock.calls[0]?.[1] as { messages: unknown[] }; + expect(seenContext.messages).toEqual([ + { + role: "user", + content: [{ type: "text", text: "retry" }], + }, + ]); + }); + it("drops signed thinking turns when replay would expose inline sessions_spawn attachments", async () => { const attachmentContent = "SIGNED_THINKING_INLINE_ATTACHMENT"; const messages = [ @@ -1588,6 +1632,8 @@ describe("wrapStreamFnSanitizeMalformedToolCalls", () => { const wrapped = wrapStreamFnSanitizeMalformedToolCalls(baseFn as never, new Set(["read"]), { validateGeminiTurns: false, validateAnthropicTurns: true, + preserveSignatures: false, + dropThinkingBlocks: false, }); const stream = wrapped({} as never, { messages } as never, {} as never) as | FakeWrappedStream @@ -1633,6 +1679,8 @@ describe("wrapStreamFnSanitizeMalformedToolCalls", () => { const wrapped = wrapStreamFnSanitizeMalformedToolCalls(baseFn as never, new Set(["read"]), { validateGeminiTurns: false, validateAnthropicTurns: true, + preserveSignatures: false, + dropThinkingBlocks: false, }); const stream = wrapped({} as never, { messages } as never, {} as never) as | FakeWrappedStream @@ -1682,6 +1730,8 @@ describe("wrapStreamFnSanitizeMalformedToolCalls", () => { const wrapped = wrapStreamFnSanitizeMalformedToolCalls(baseFn as never, new Set(["read"]), { validateGeminiTurns: false, validateAnthropicTurns: true, + preserveSignatures: false, + dropThinkingBlocks: false, }); const stream = wrapped({} as never, { messages } as never, {} as never) as | FakeWrappedStream @@ -1722,6 +1772,8 @@ describe("wrapStreamFnSanitizeMalformedToolCalls", () => { const wrapped = wrapStreamFnSanitizeMalformedToolCalls(baseFn as never, new Set(["read"]), { validateGeminiTurns: false, validateAnthropicTurns: true, + preserveSignatures: false, + dropThinkingBlocks: false, }); const stream = wrapped({} as never, { messages } as never, {} as never) as | FakeWrappedStream