DurableAgent: tool-output chunks dropped from result.uiMessages when stopWhen fires on a tool-call step
Nobody has claimed this yet.
- Dominant language
- TypeScript
- Stars
- 2.4k
- Forks
- 365
- Avg merge
- 2d 11h
- Merged PRs (30d)
- 169
Description
Bug
When DurableAgent.stream({ collectUIMessages: true }) is configured with a stopWhen that fires after a tool call, the tool executes and its output appears in result.toolResults, but the matching tool part in result.uiMessages stays at state: 'input-available' instead of 'output-available'.
This breaks any downstream code that persists or renders uiMessages (DB writes, replay, UI rendering) — the saved/replayed message looks like a tool was called and never returned.
Versions
@workflow/ai:4.1.2workflow:4.2.4ai:6.0.168
Expected
After stopWhen fires on a tool-call step, result.uiMessages should contain tool-output-available parts for every tool result that's already present in result.toolResults.
Actual
Tool parts in result.uiMessages remain at state: 'input-available'. The corresponding tool-output-available UI chunks never reach the agent's collected chunk buffer.
Root cause
In dist/agent/stream-text-iterator.js (verified against @workflow/ai@4.1.2), the iterator does this on a tool-call step:
- Yields
{ uiChunks: allStepUIChunks, ... }to the agent (onlytool-input-availablechunks at this point). - Receives
toolResultsback from the agent. - Calls
writeToolOutputToUI(...)— producestool-output-availablechunks and merges them intoallAccumulatedUIChunks/allStepUIChunks. - Evaluates
stopConditions. If matched → setsdone = true, the outerwhile (!done)loop exits.
After the loop, the final flush yield at the bottom of the iterator is gated on !lastStepWasToolCalls:
if (lastStep && !lastStepWasToolCalls) {
const finalUIChunks = [
...allAccumulatedUIChunks,
...(lastStepUIChunks ?? []),
];
yield { ..., uiChunks: finalUIChunks, ... };
}
Since lastStepWasToolCalls was set to true earlier in the tool-call branch, this flush is skipped — so the post-step tool-output-available chunks never reach allUIChunks in durable-agent.js, and convertChunksToUIMessages(allUIChunks) returns UIMessages with the tool parts stuck at input-available.
The companion result.toolResults (top-level) is populated correctly via lastStepToolResults in durable-agent.js, so the data isn't lost — just absent from the UI-message view.
Suggested fix
Either:
- Yield a final flush even when
lastStepWasToolCallsis true, so the post-stoptool-output-availablechunks reach the agent's chunk buffer; or - In
durable-agent.js, after the iterator returns, append any unflushed UI chunks captured fromwriteToolOutputToUIdirectly intoallUIChunksbefore callingconvertChunksToUIMessages.
Workaround (in our codebase)
We post-process result.uiMessages by matching tool parts by toolCallId against result.toolResults and stamping state: 'output-available' + output onto any tool part that's missing its result.
Contributor guide
No contributing guide indexed for this repository
First steps
- Read the whole issue, then the project's contributing guide.
- Comment on the issue to say you are picking it up — it saves two people doing the same work.
- Fork the repository and make your change on a branch.
- Open a pull request that references the issue number.
Research direction
Start by tracing the tool-call path in dist/agent/stream-text-iterator.js and how durable-agent.js collects uiChunks into result.uiMessages. Reproduce the stopWhen case with collectUIMessages enabled, then verify that tool results already present in result.toolResults also produce tool-output-available parts in result.uiMessages after collection.
Written by the indexing model from the issue text.
Assessment
- Tech stack
- typescript
- Domain
- backend
- Issue type
- Bug
- Difficulty
- 3/5
- Estimated time
- 1-2 days
- Activity status
- Quiet
- Clarity
- Clearly specified
- Newbie friendliness
- 72/100