Handle terminal error frames in useAgentChat observers - #2023
Conversation
🦋 Changeset detectedLatest commit: 3dedcc7 The changes in this PR will be included in the next version bump. This PR includes changesets to release 4 packages
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 |
agents
@cloudflare/ai-chat
@cloudflare/codemode
create-think
hono-agents
@cloudflare/shell
@cloudflare/think
@cloudflare/voice
@cloudflare/worker-bundler
commit: |
| 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; | ||
| } |
There was a problem hiding this comment.
🟡 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.
Was this helpful? React with 👍 or 👎 to provide feedback.
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
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
No documentation update is needed because this restores terminal-error handling without changing the public API.