Skip to content

Commit d6c9387

Browse files
committed
fix: share signed thinking replay policy
1 parent 906476a commit d6c9387

6 files changed

Lines changed: 78 additions & 13 deletions

File tree

src/agents/pi-embedded-runner/replay-history.ts

Lines changed: 6 additions & 12 deletions
Original file line numberDiff line numberDiff line change
@@ -35,6 +35,7 @@ import { STREAM_ERROR_FALLBACK_TEXT } from "../stream-message-shared.js";
3535
import { sanitizeToolCallIdsForCloudCodeAssist } from "../tool-call-id.js";
3636
import type { TranscriptPolicy } from "../transcript-policy.js";
3737
import {
38+
providerRequiresSignedThinking,
3839
resolveTranscriptPolicy,
3940
shouldAllowProviderOwnedThinkingReplay,
4041
} from "../transcript-policy.js";
@@ -658,12 +659,6 @@ function isSameModelSnapshot(a: ModelSnapshotEntry, b: ModelSnapshotEntry): bool
658659
);
659660
}
660661

661-
const SIGNED_THINKING_PROVIDERS = new Set(["anthropic", "amazon-bedrock", "anthropic-vertex"]);
662-
663-
function providerRequiresSignedThinking(provider?: string | null): boolean {
664-
return SIGNED_THINKING_PROVIDERS.has(provider ?? "");
665-
}
666-
667662
/**
668663
* Applies the generic replay-history cleanup pipeline before provider-owned
669664
* replay hooks run.
@@ -697,12 +692,11 @@ export async function sanitizeSessionHistory(params: {
697692
});
698693
const withInterSessionMarkers = annotateInterSessionUserMessages(params.messages);
699694
const signedThinkingProvider = providerRequiresSignedThinking(params.provider);
700-
const allowProviderOwnedThinkingReplay =
701-
shouldAllowProviderOwnedThinkingReplay({
702-
modelApi: params.modelApi,
703-
policy,
704-
}) ||
705-
(signedThinkingProvider && !policy.dropThinkingBlocks);
695+
const allowProviderOwnedThinkingReplay = shouldAllowProviderOwnedThinkingReplay({
696+
modelApi: params.modelApi,
697+
provider: params.provider,
698+
policy,
699+
});
706700
const isOpenAIResponsesApi =
707701
params.modelApi === "openai-responses" ||
708702
params.modelApi === "openai-codex-responses" ||

src/agents/pi-embedded-runner/run/attempt.tool-call-normalization.test.ts

Lines changed: 40 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -130,6 +130,46 @@ describe("sanitizeReplayToolCallIdsForStream", () => {
130130
});
131131
});
132132

133+
it("preserves signed-thinking replay ids when requested by provider policy", () => {
134+
const rawId = "call_1";
135+
const out = sanitizeReplayToolCallIdsForStream({
136+
messages: [
137+
{
138+
role: "assistant",
139+
content: [
140+
{ type: "thinking", thinking: "internal", thinkingSignature: "sig_1" },
141+
{ type: "toolUse", id: rawId, name: "read", input: { path: "." } },
142+
],
143+
} as never,
144+
{
145+
role: "toolResult",
146+
toolCallId: rawId,
147+
toolUseId: rawId,
148+
toolName: "read",
149+
content: [{ type: "text", text: "ok" }],
150+
isError: false,
151+
} as never,
152+
],
153+
mode: "strict",
154+
preserveReplaySafeThinkingToolCallIds: true,
155+
repairToolUseResultPairing: true,
156+
});
157+
158+
expect(out.map((message) => message.role)).toEqual(["assistant", "toolResult"]);
159+
expect(requireAssistantMessage(out[0]).content[1]).toMatchObject({
160+
type: "toolUse",
161+
id: "call_1",
162+
name: "read",
163+
});
164+
expect(toolResultSummary(out[1])).toEqual({
165+
role: "toolResult",
166+
toolCallId: "call_1",
167+
toolUseId: "call_1",
168+
toolName: "read",
169+
isError: false,
170+
});
171+
});
172+
133173
it("synthesizes missing tool results after strict id sanitization", () => {
134174
const rawId = "call_function_av7cbkigmk7x1";
135175
const out = sanitizeReplayToolCallIdsForStream({

src/agents/pi-embedded-runner/run/attempt.tool-call-normalization.ts

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -919,6 +919,7 @@ export function wrapStreamFnSanitizeMalformedToolCalls(
919919
TranscriptPolicy,
920920
"validateGeminiTurns" | "validateAnthropicTurns" | "preserveSignatures" | "dropThinkingBlocks"
921921
>,
922+
provider?: string | null,
922923
): StreamFn {
923924
return (model, context, options) => {
924925
const ctx = context as unknown as { messages?: unknown };
@@ -928,6 +929,7 @@ export function wrapStreamFnSanitizeMalformedToolCalls(
928929
}
929930
const allowProviderOwnedThinkingReplay = shouldAllowProviderOwnedThinkingReplay({
930931
modelApi: (model as { api?: unknown })?.api as string | null | undefined,
932+
provider,
931933
policy: {
932934
validateAnthropicTurns: transcriptPolicy?.validateAnthropicTurns === true,
933935
preserveSignatures: transcriptPolicy?.preserveSignatures === true,

src/agents/pi-embedded-runner/run/attempt.ts

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -2855,6 +2855,7 @@ export async function runEmbeddedAttempt(
28552855
preserveNativeAnthropicToolUseIds: transcriptPolicy.preserveNativeAnthropicToolUseIds,
28562856
preserveReplaySafeThinkingToolCallIds: shouldAllowProviderOwnedThinkingReplay({
28572857
modelApi: (model as { api?: unknown })?.api as string | null | undefined,
2858+
provider: params.provider,
28582859
policy: transcriptPolicy,
28592860
}),
28602861
repairToolUseResultPairing: transcriptPolicy.repairToolUseResultPairing,
@@ -2911,6 +2912,7 @@ export async function runEmbeddedAttempt(
29112912
activeSession.agent.streamFn,
29122913
allowedToolNames,
29132914
transcriptPolicy,
2915+
params.provider,
29142916
);
29152917
activeSession.agent.streamFn = wrapStreamFnTrimToolCallNames(
29162918
activeSession.agent.streamFn,

src/agents/transcript-policy.test.ts

Lines changed: 18 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -617,6 +617,24 @@ describe("resolveTranscriptPolicy", () => {
617617
).toBe(true);
618618
});
619619

620+
it.each(["anthropic", "amazon-bedrock"] as const)(
621+
"allows provider-owned thinking replay for signed-thinking %s recovery policies",
622+
(provider) => {
623+
expect(
624+
shouldAllowProviderOwnedThinkingReplay({
625+
provider,
626+
modelApi:
627+
provider === "amazon-bedrock" ? "bedrock-converse-stream" : "anthropic-messages",
628+
policy: {
629+
validateAnthropicTurns: true,
630+
preserveSignatures: false,
631+
dropThinkingBlocks: false,
632+
},
633+
}),
634+
).toBe(true);
635+
},
636+
);
637+
620638
it("does not allow immutable provider-owned thinking replay for github-copilot claude models", () => {
621639
const policy = resolveTranscriptPolicy({
622640
provider: "github-copilot",

src/agents/transcript-policy.ts

Lines changed: 10 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -32,17 +32,26 @@ export type TranscriptPolicy = {
3232
allowSyntheticToolResults: boolean;
3333
};
3434

35+
const SIGNED_THINKING_PROVIDERS = new Set(["anthropic", "amazon-bedrock", "anthropic-vertex"]);
36+
37+
export function providerRequiresSignedThinking(provider?: string | null): boolean {
38+
return SIGNED_THINKING_PROVIDERS.has(normalizeProviderId(provider ?? ""));
39+
}
40+
3541
export function shouldAllowProviderOwnedThinkingReplay(params: {
3642
modelApi?: string | null;
43+
provider?: string | null;
3744
policy: Pick<
3845
TranscriptPolicy,
3946
"validateAnthropicTurns" | "preserveSignatures" | "dropThinkingBlocks"
4047
>;
4148
}): boolean {
49+
const hasProviderOwnedSignedThinking =
50+
params.policy.preserveSignatures || providerRequiresSignedThinking(params.provider);
4251
return (
4352
isAnthropicApi(params.modelApi) &&
4453
params.policy.validateAnthropicTurns &&
45-
params.policy.preserveSignatures &&
54+
hasProviderOwnedSignedThinking &&
4655
!params.policy.dropThinkingBlocks
4756
);
4857
}

0 commit comments

Comments
 (0)