Repository navigation
DurableAgent should preserve reasoning content in conversation history across tool loop steps #1393
Description
I ran into this as I was researching differences between AI SDK's streamText() and DurableAgent's implementation. I came upon sanitizeProviderMetadataForToolCall() and tried to find out why it exists in the first place. Is there a reason why reasoning is not tracked or is that an oversight?
Problem
When DurableAgent's tool loop continues to the next step, the assistant message sent back to the model only contains tool-call parts — reasoning content is omitted:
// stream-text-iterator.ts, inside finishReason === 'tool-calls' branch
conversationPrompt.push({
role: 'assistant',
content: toolCalls.map(toolCall => ({
type: 'tool-call',
toolCallId: toolCall.toolCallId,
toolName: toolCall.toolName,
input: JSON.parse(toolCall.input),
})),
});The AI SDK's own streamText includes reasoning parts in the assistant message via toResponseMessages():
case 'reasoning':
content.push({
type: 'reasoning',
text: part.text,
providerOptions: part.providerMetadata,
});This means reasoning models (OpenAI o-series, Anthropic extended thinking, Gemini thinking) lose access to their own prior reasoning during multi-step tool loops in DurableAgent, but not in streamText.
Impact
-
OpenAI
itemIdworkaround would become unnecessary. PR fix(ai): strip openai itemID from tool call metadata #889 stripsitemIdfrom OpenAI metadata because it references reasoning items that aren't in the conversation. If reasoning items were preserved,itemIdreferences would be valid and no sanitization would be needed. -
Reasoning model quality during tool loops. As raised in Bug: DurableAgent + OpenAI Responses API fails on tool calls due to missing required
reasoningitem #880 by @bhuvaneshprasad: "without (itemId + reasoningItem) or previousResponseId, the code will still function, but I'm unclear on whether the reasoning models would be able to access and leverage the prior reasoning text to improve performance." This was never answered and the workaround was merged instead. -
Parity with AI SDK.
streamTextpreserves reasoning across steps. DurableAgent should too.
Proposed Fix
The reasoning data is already available — chunksToStep() in do-stream-step.ts collects reasoning chunks into the step result. The fix is to include reasoning content parts in the assistant message alongside tool-call parts in stream-text-iterator.ts, mirroring what toResponseMessages() does in the AI SDK.
This would also allow removing sanitizeProviderMetadataForToolCall() and the provider-specific itemId stripping from PR #889.
Context
- Bug: DurableAgent + OpenAI Responses API fails on tool calls due to missing required
reasoningitem #880 — OpenAI Responses API fails on tool calls due to missing reasoning item - fix(ai): strip openai itemID from tool call metadata #889 — Workaround: strip
itemIdfrom metadata - fix(ai): remove itemId from providerOptions for openai models #886 — Alternative approach (closed): use
previousResponseId - Gemini tool-calls fail after first step because thought_signature is dropped #727 — Gemini
thoughtSignaturedropped (fixed in fix(ai): preserve providerMetadata as providerOptions in multi-turn tool calls #733, but reasoning content still not preserved)
Activity
Preserve reasoning content in DurableAgent conversation history
craze3 commentedon Mar 29, 2026
I hit what looks like the same root problem in a real multi-step DurableAgent run with
openai/gpt-5.4and high reasoning.What I observed:
finishReason: 'tool-calls'finishReason: 'error'Error processing UI message chunks: Error: [object Object]After digging locally, this lines up with the current behavior where DurableAgent does not preserve reasoning content across tool-loop steps, while OpenAI reasoning models still appear to depend on that continuity.
I also confirmed the published package still has:
do-stream-step.js:error instanceof Error ? error.message : String(error)stream-text-iterator.js: the OpenAIitemIdsanitization path with the comment about reasoning items not being preservedSo from my side this issue is not just theoretical:
[object Object], which makes the actual cause hard to diagnoseI’ve worked around it locally by disabling provider-level reasoning for Workflow chat turns on GPT-5.4, but that is not a real solution 🙃
iNishant commentedon May 14, 2026
Heads up: the same class of bug as this issue exists for assistant
text content emitted alongside tool calls, and was not fixed by
#1444.
In stream-text-iterator.ts, the finishReason === 'tool-calls'
branch builds the assistant ModelMessage from reasoningParts +
toolCalls — text parts from step.content are still being dropped:
The 'stop' branch correctly carries text forward, and the AI SDK's
own toResponseMessages() includes both text and tool-calls in the
same assistant message
(ai/src/generate-text/to-response-messages.ts). The 'tool-calls'
branch is the only place this divergence remains.
Repro
Prompt an agent to (a) call a tool, and (b) emit text alongside the
tool call ("explain what you're about to do" + todo_write).
Anthropic emits a text block followed by a tool_use block in the
same assistant turn — both are present in step.content. On the next
step, only the tool-call shows up in the conversation prompt; the
text is gone.
Concrete symptom we hit: model emitted intro text + todo_write on
step 1, then on step 2 (with its own prior text invisible to it)
started the entire task over from scratch, duplicated the intro,
and ran into maxOutputTokens on what should have been a short
follow-up turn.
Suggested fix
Mirror the 'stop' branch / toResponseMessages() — one extra spread:
Note: step.content is built by chunksToStep() as [{type:'text',
text: }, ...toolCalls], so the ordering above is the only
ordering available — original streaming interleaving of text →
tool_use → text → tool_use is already collapsed before this code
runs. Preserving full interleaving would require a separate change
in chunksToStep.