diff --git a/src/agents/pi-embedded-runner/replay-history.ts b/src/agents/pi-embedded-runner/replay-history.ts index 29fe1692eef..45aaf25499c 100644 --- a/src/agents/pi-embedded-runner/replay-history.ts +++ b/src/agents/pi-embedded-runner/replay-history.ts @@ -418,7 +418,10 @@ export async function sanitizeSessionHistory(params: { : sanitizedImages; const sanitizedToolCalls = sanitizeToolCallInputs(droppedThinking, { allowedToolNames: params.allowedToolNames, - preserveImmutableThinkingTurns: policy.validateAnthropicTurns, + allowProviderOwnedThinkingReplay: + policy.validateAnthropicTurns && + params.provider === "anthropic" && + params.modelApi === "anthropic-messages", }); const repairedTools = policy.repairToolUseResultPairing ? sanitizeToolUseResultPairing(sanitizedToolCalls, { diff --git a/src/agents/pi-embedded-runner/run/attempt.tool-call-normalization.ts b/src/agents/pi-embedded-runner/run/attempt.tool-call-normalization.ts index e7b5fad16a8..b73ad716534 100644 --- a/src/agents/pi-embedded-runner/run/attempt.tool-call-normalization.ts +++ b/src/agents/pi-embedded-runner/run/attempt.tool-call-normalization.ts @@ -2,7 +2,10 @@ import type { AgentMessage, StreamFn } from "@mariozechner/pi-agent-core"; import { streamSimple } from "@mariozechner/pi-ai"; import { normalizeLowercaseStringOrEmpty } from "../../../shared/string-coerce.js"; import { validateAnthropicTurns, validateGeminiTurns } from "../../pi-embedded-helpers.js"; -import { sanitizeToolUseResultPairing } from "../../session-transcript-repair.js"; +import { + isRedactedSessionsSpawnAttachment, + sanitizeToolUseResultPairing, +} from "../../session-transcript-repair.js"; import { normalizeToolName } from "../../tool-policy.js"; import type { TranscriptPolicy } from "../../transcript-policy.js"; @@ -251,14 +254,7 @@ function hasUnredactedSessionsSpawnAttachments(block: ReplayToolCallBlock): bool continue; } for (const attachment of attachments) { - if (!attachment || typeof attachment !== "object") { - continue; - } - if (!Object.hasOwn(attachment, "content")) { - continue; - } - const content = (attachment as { content?: unknown }).content; - if (content !== "__OPENCLAW_REDACTED__") { + if (!isRedactedSessionsSpawnAttachment(attachment)) { return true; } } @@ -331,7 +327,7 @@ function resolveReplayToolCallName( function sanitizeReplayToolCallInputs( messages: AgentMessage[], allowedToolNames?: Set, - preserveImmutableThinkingTurns?: boolean, + allowProviderOwnedThinkingReplay?: boolean, ): ReplayToolCallSanitizeReport { let changed = false; let droppedAssistantMessages = 0; @@ -347,7 +343,7 @@ function sanitizeReplayToolCallInputs( continue; } if ( - preserveImmutableThinkingTurns && + allowProviderOwnedThinkingReplay && message.content.some((block) => isThinkingLikeReplayBlock(block)) && message.content.some((block) => isReplayToolCallBlock(block)) ) { @@ -641,7 +637,8 @@ export function wrapStreamFnSanitizeMalformedToolCalls( const sanitized = sanitizeReplayToolCallInputs( messages as AgentMessage[], allowedToolNames, - transcriptPolicy?.validateAnthropicTurns === true, + transcriptPolicy?.validateAnthropicTurns === true && + (model as { api?: unknown })?.api === "anthropic-messages", ); if (sanitized.messages === messages) { return baseFn(model, context, options); diff --git a/src/agents/session-transcript-repair.ts b/src/agents/session-transcript-repair.ts index b7196c0745d..81cd70ad378 100644 --- a/src/agents/session-transcript-repair.ts +++ b/src/agents/session-transcript-repair.ts @@ -8,6 +8,8 @@ import { extractToolCallsFromAssistant, extractToolResultId } from "./tool-call- const TOOL_CALL_NAME_MAX_CHARS = 64; const TOOL_CALL_NAME_RE = /^[A-Za-z0-9_:.-]+$/; +const REDACTED_SESSIONS_SPAWN_ATTACHMENT_CONTENT = "__OPENCLAW_REDACTED__"; +const SESSIONS_SPAWN_ATTACHMENT_METADATA_KEYS = ["name", "encoding", "mimeType"] as const; type RawToolCallBlock = { type?: unknown; @@ -94,20 +96,59 @@ function redactSessionsSpawnAttachmentsArgs(value: unknown): unknown { if (!Array.isArray(raw)) { return value; } + let changed = false; const next = raw.map((item) => { - if (!item || typeof item !== "object") { + if (isRedactedSessionsSpawnAttachment(item)) { return item; } - const a = item as Record; - if (!Object.hasOwn(a, "content")) { - return item; - } - const { content: _content, ...rest } = a; - return { ...rest, content: "__OPENCLAW_REDACTED__" }; + changed = true; + return redactSessionsSpawnAttachment(item); }); + if (!changed) { + return value; + } return { ...rec, attachments: next }; } +function redactSessionsSpawnAttachment(item: unknown): Record { + const next: Record = { + content: REDACTED_SESSIONS_SPAWN_ATTACHMENT_CONTENT, + }; + if (!item || typeof item !== "object") { + return next; + } + const attachment = item as Record; + for (const key of SESSIONS_SPAWN_ATTACHMENT_METADATA_KEYS) { + const value = attachment[key]; + if (typeof value === "string" && value.trim().length > 0) { + next[key] = value; + } + } + return next; +} + +export function isRedactedSessionsSpawnAttachment(item: unknown): boolean { + if (!item || typeof item !== "object") { + return false; + } + const attachment = item as Record; + if (attachment.content !== REDACTED_SESSIONS_SPAWN_ATTACHMENT_CONTENT) { + return false; + } + for (const key of Object.keys(attachment)) { + if (key === "content") { + continue; + } + if (!(SESSIONS_SPAWN_ATTACHMENT_METADATA_KEYS as readonly string[]).includes(key)) { + return false; + } + if (typeof attachment[key] !== "string" || (attachment[key] as string).trim().length === 0) { + return false; + } + } + return true; +} + function sanitizeToolCallBlock(block: RawToolCallBlock): RawToolCallBlock { const rawName = readStringValue(block.name); const trimmedName = rawName?.trim(); @@ -232,7 +273,7 @@ export type ToolCallInputRepairReport = { export type ToolCallInputRepairOptions = { allowedToolNames?: Iterable; - preserveImmutableThinkingTurns?: boolean; + allowProviderOwnedThinkingReplay?: boolean; }; export type ErroredAssistantResultPolicy = "preserve" | "drop"; @@ -270,7 +311,7 @@ export function repairToolCallInputs( let changed = false; const out: AgentMessage[] = []; const allowedToolNames = normalizeAllowedToolNames(options?.allowedToolNames); - const preserveImmutableThinkingTurns = options?.preserveImmutableThinkingTurns === true; + const allowProviderOwnedThinkingReplay = options?.allowProviderOwnedThinkingReplay === true; for (const msg of messages) { if (!msg || typeof msg !== "object") { @@ -284,7 +325,7 @@ export function repairToolCallInputs( } if ( - preserveImmutableThinkingTurns && + allowProviderOwnedThinkingReplay && msg.content.some((block) => isThinkingLikeBlock(block)) && countRawToolCallBlocks(msg.content) > 0 ) {