Skip to content

Handle terminal error frames in useAgentChat observers - #2023

Merged
threepointone merged 2 commits into
mainfrom
codex/fix-1898-observer-errors
Aug 3, 2026
Merged

Handle terminal error frames in useAgentChat observers#2023
threepointone merged 2 commits into
mainfrom
codex/fix-1898-observer-errors

Conversation

@threepointone

Copy link
Copy Markdown
Contributor

Fixes #1898.

This PR carries forward Tyler Gibbs’s implementation from tylergibbs1/agents@ccf21aa, preserving his commit authorship, after independently reproducing and validating it against current main. I added coverage for the error-only variant where done is omitted.

What changed

  • Short-circuit observer-owned terminal error frames before parsing their human-readable body as a UI stream chunk.
  • Clear observer streaming, replay, recovery, and tool-continuation state on terminal errors.
  • Treat local transport error frames as terminal even when done is omitted.
  • Add real-hook regression coverage for both done: true and error-only terminal frames.
  • Add patch changesets for agents, @cloudflare/ai-chat, and @cloudflare/think, whose hooks expose this core behavior.

Reproduction

On current main, the regression failed after replaying a plain-text Network connection lost. frame: the observer appended an empty assistant message, yielding one message instead of zero. The fixed path neither parses the diagnostic body nor transitions it as a stream chunk.

Validation

  • focused regression failed before the fix and passes for both terminal-frame variants after it
  • full Agents React suite: 9 files, 125 tests passed
  • full Agents chat suite: 25 files, 514 tests passed
  • affected agents, React tests, ai-chat, and think TypeScript configs passed
  • repository formatting and lint passed
  • package export validation passed
  • sherif passed with the two existing missing-package warnings for examples/harness-deck and examples/think-standalone

No documentation update is needed because this restores terminal-error handling without changing the public API.

@changeset-bot

changeset-bot Bot commented Aug 3, 2026

Copy link
Copy Markdown

🦋 Changeset detected

Latest commit: 3dedcc7

The changes in this PR will be included in the next version bump.

This PR includes changesets to release 4 packages
Name Type
agents Patch
@cloudflare/ai-chat Patch
@cloudflare/think Patch
@cloudflare/agent-think Patch

Not sure what this means? Click here to learn what changesets are.

Click here if you're a maintainer who wants to add another changeset to this PR

@pkg-pr-new

pkg-pr-new Bot commented Aug 3, 2026

Copy link
Copy Markdown

Open in StackBlitz

agents

npm i https://pkg.pr.new/agents@2023

@cloudflare/ai-chat

npm i https://pkg.pr.new/@cloudflare/ai-chat@2023

@cloudflare/codemode

npm i https://pkg.pr.new/@cloudflare/codemode@2023

create-think

npm i https://pkg.pr.new/create-think@2023

hono-agents

npm i https://pkg.pr.new/hono-agents@2023

@cloudflare/shell

npm i https://pkg.pr.new/@cloudflare/shell@2023

@cloudflare/think

npm i https://pkg.pr.new/@cloudflare/think@2023

@cloudflare/voice

npm i https://pkg.pr.new/@cloudflare/voice@2023

@cloudflare/worker-bundler

npm i https://pkg.pr.new/@cloudflare/worker-bundler@2023

commit: 3dedcc7

@threepointone
threepointone marked this pull request as ready for review August 3, 2026 15:45
@threepointone
threepointone merged commit 2b2b598 into main Aug 3, 2026
14 of 15 checks passed
@threepointone
threepointone deleted the codex/fix-1898-observer-errors branch August 3, 2026 15:46

@devin-ai-integration devin-ai-integration Bot left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Devin Review found 1 potential issue.

View 1 additional finding in Devin Review.

Open in Devin Review

Comment on lines +2173 to +2193
if (data.error) {
pendingReplayResumeRequestIdsRef.current.delete(data.id);
customTransport.handleServerTurnCompleted(data.id);
fallbackAckedResumeRequestIdsRef.current.delete(data.id);
setIsRecovering(false);

if (
streamStateRef.current.status === "idle" ||
streamStateRef.current.streamId === data.id
) {
const result = broadcastTransition(streamStateRef.current, {
type: "clear"
});
streamStateRef.current = result.state;
setIsServerStreaming(result.isStreaming);
}
if (observedToolContinuationRequestIdRef.current === data.id) {
resetToolContinuation();
}
break;
}

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🟡 Partial assistant answer disappears when a reconnecting tab replays a turn that ended in an error

The partially generated answer that was replayed to a reconnecting tab is thrown away (break in the new error branch at packages/agents/src/chat/react.tsx:2173-2193) before it is written into the chat list, so the user sees an empty turn instead of the text the assistant had produced before failing.

Impact: After reconnecting to a turn that errored mid-generation, the text already produced is silently lost from the conversation instead of being shown alongside the error.

How the replay-then-terminal-error sequence loses the accumulated content

When a client resumes a turn that ended in an error, ResumeHandshake._replayTerminalOnAck (packages/agents/src/chat/resume-handshake.ts:293-322) first replays the stored chunks as replay: true, done: false frames and then sends a terminal { done: true, error: true, body: <diagnostic text> } frame.

In the observer (non transport-owned) path, replayed chunks are accumulated but NOT merged into messages: transition only produces a messagesUpdate for non-replay chunks, replayComplete, or done (packages/agents/src/chat/broadcast-state.ts:130-146). Before this PR, the terminal frame fell through to the response transition with done: true, which ran accumulator.mergeInto(prev) and surfaced the partial text.

With the new short-circuit at packages/agents/src/chat/react.tsx:2173, the terminal error frame instead issues a clear transition (which returns { status: "idle" } with no messagesUpdate) and breaks, so the accumulated replay content is discarded. Consider merging the accumulator (e.g. dispatching a response event with done: true and no chunkData, or explicitly applying accumulator.mergeInto) before clearing observer state.

Prompt for agents
In packages/agents/src/chat/react.tsx, the new observer-side terminal-error short-circuit (around line 2173) clears broadcast stream state and returns without ever merging the accumulated stream content into the message list. For resumed turns, the server (ResumeHandshake._replayTerminalOnAck in packages/agents/src/chat/resume-handshake.ts) replays the errored turn's stored chunks as replay:true/done:false frames and only then sends the terminal error frame. Because the broadcast state machine (packages/agents/src/chat/broadcast-state.ts) only emits a messagesUpdate for non-replay chunks, replayComplete, or done, the replayed partial assistant content is only flushed by the terminal frame. The new short-circuit removes that flush, so the partial answer is lost from the UI. Fix by flushing the observing accumulator into messages (setMessages with accumulator.mergeInto, or by routing through a response transition with done:true and no chunkData) when the observed stream id matches, before clearing observer state — while still avoiding parsing the human-readable error body as a chunk.
Open in Devin Review

Was this helpful? React with 👍 or 👎 to provide feedback.

@github-actions github-actions Bot mentioned this pull request Aug 3, 2026
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

useAgentChat may transition terminal error frames as stream chunks

2 participants