diff --git a/src/agents/openai-transport-stream.ts b/src/agents/openai-transport-stream.ts index 1fe8f3d672e..2f39d08bb38 100644 --- a/src/agents/openai-transport-stream.ts +++ b/src/agents/openai-transport-stream.ts @@ -2524,7 +2524,11 @@ function hasToolHistory(messages: Context["messages"]): boolean { return messages.some( (message) => message.role === "toolResult" || - (message.role === "assistant" && message.content.some((block) => block.type === "toolCall")), + // Assistant content can be a raw string from transcript replay; a string + // never carries tool calls, so it should not count toward tool history. + (message.role === "assistant" && + Array.isArray(message.content) && + message.content.some((block) => block.type === "toolCall")), ); } diff --git a/src/llm/providers/openai-completions.ts b/src/llm/providers/openai-completions.ts index 3c41fe20803..0fa4d9b3ea0 100644 --- a/src/llm/providers/openai-completions.ts +++ b/src/llm/providers/openai-completions.ts @@ -63,7 +63,9 @@ function hasToolHistory(messages: Message[]): boolean { return true; } if (msg.role === "assistant") { - if (msg.content.some((block) => block.type === "toolCall")) { + // Assistant content can be a raw string from transcript replay; a string + // never carries tool calls, so it should not count toward tool history. + if (Array.isArray(msg.content) && msg.content.some((block) => block.type === "toolCall")) { return true; } } diff --git a/src/llm/providers/transform-messages.ts b/src/llm/providers/transform-messages.ts index 68ff48c86d5..25a3b94ab51 100644 --- a/src/llm/providers/transform-messages.ts +++ b/src/llm/providers/transform-messages.ts @@ -117,7 +117,15 @@ export function transformMessages( assistantMsg.api === model.api && assistantMsg.model === model.id); - const transformedContent = assistantMsg.content.flatMap((block) => { + // Assistant content is typed as a block array, but transcript replay can + // hand us a raw string (JSONL passthrough). Normalize it to an equivalent + // single text block before transforming, matching the string->text block + // handling already used in anthropic-payload-policy.ts. + const contentBlocks = Array.isArray(assistantMsg.content) + ? assistantMsg.content + : [{ type: "text" as const, text: assistantMsg.content as unknown as string }]; + + const transformedContent = contentBlocks.flatMap((block) => { if (block.type === "thinking") { if (modelBoundThinkingReplayMode === "drop") { return [];