Skip to content

DurableAgent: tool-output chunks dropped from result.uiMessages when stopWhen fires on a tool-call step #1912

@Dinesh563

Description

@Dinesh563

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.2
  • workflow: 4.2.4
  • ai: 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:

  1. Yields { uiChunks: allStepUIChunks, ... } to the agent (only tool-input-available chunks at this point).
  2. Receives toolResults back from the agent.
  3. Calls writeToolOutputToUI(...) — produces tool-output-available chunks and merges them into allAccumulatedUIChunks / allStepUIChunks.
  4. Evaluates stopConditions. If matched → sets done = true, the outer while (!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 lastStepWasToolCalls is true, so the post-stop tool-output-available chunks reach the agent's chunk buffer; or
  • In durable-agent.js, after the iterator returns, append any unflushed UI chunks captured from writeToolOutputToUI directly into allUIChunks before calling convertChunksToUIMessages.

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.

Metadata

Metadata

Assignees

No one assigned

    Labels

    No labels
    No labels

    Type

    No type
    No fields configured for issues without a type.

    Projects

    No projects

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions