cloudflare / cloudflare/agents
AIChatAgent terminalizes platform-transient response reader errors instead of routing them through chatRecovery
- Dominant language
- TypeScript
- Stars
- 5.6k
- Forks
- 711
- Avg merge
- 1d 20h
- Merged PRs (30d)
- 53
Description
## Summary
When an AIChatAgent response reader throws `Network connection lost.` after partial output has already streamed, `@cloudflare/ai-chat` terminalizes the turn instead of routing the interruption through its configured bounded `chatRecovery`.
The `agents` package already classifies this exact error as a platform transient through `isPlatformTransientError`, but the response-reader catch path in `@cloudflare/ai-chat` only routes `ChatStreamStalledError` into recovery. All other thrown reader errors are converted to `{ status: "error" }`, broadcast as terminal error frames, and emitted as `message:error`.
This is the server-side failure that can produce the downstream terminal frame described in #1898. That issue tracks how the client processes the frame; this report is about why a recognized platform transient becomes terminal in the first place.
## Environment
- `@cloudflare/ai-chat@0.9.3`
- `agents@0.17.3`
- `ai@6.0.208`
- `@ai-sdk/azure@3.0.74`
- Cloudflare Worker with an `AIChatAgent` Durable Object
- `chatRecovery` enabled with bounded attempts/work and a stream-stall watchdog
The relevant chat catch path is unchanged in `agents@0.17.4` and remains present on current `cloudflare/agents` main as of `e906760381bef1458b758260ac41dc0f7bd921e3`.
## Observed production timeline
An anonymized production turn showed:
1. Response generation started normally and produced 949 AI stream chunks across 26 tool calls.
2. The final tool call completed successfully, with no sandbox error or timeout.
3. The inner AI SDK UI-message stream did not call its `onError` handler. No user abort was requested, the request abort signal was not observed, and the browser connection remained mounted.
4. The Worker script version stayed constant before, during, and after the run, so this was not a code-update/superseded-isolate reset.
5. Roughly 134 seconds after the final observed chunk, the outer response reader threw `Network connection lost.`.
6. `AIChatAgent.onChatResponse` received `status: "error"`, the terminal result contained the string error, and no chat-recovery attempt was scheduled.
This leaves the partial turn stopped even though Cloudflare's shared classifier considers the error transient.
## Current behavior
The catch path routes only `ChatStreamStalledError` through `_routeStallToBoundedRecovery`:
https://github.com/cloudflare/agents/blob/e906760381bef1458b758260ac41dc0f7bd921e3/packages/ai-chat/src/index.ts#L6575-L6641
If that special case does not match, the same catch block flattens the error to its message, marks the stream as errored, broadcasts a terminal error frame, and emits `message:error`.
Meanwhile, the shared classifier explicitly recognizes `Network connection lost.`:
https://github.com/cloudflare/agents/blob/e906760381bef1458b758260ac41dc0f7bd921e3/packages/agents/src/retries.ts#L193
https://github.com/cloudflare/agents/blob/e906760381bef1458b758260ac41dc0f7bd921e3/packages/agents/src/retries.ts#L322-L327
#1740 describes this error as a dropped isolate-to-storage or isolate-to-DO connection that may succeed on retry, and added the shared cause-aware classifier.
## Expected behavior
When the response reader throws before the stream is completed and `isPlatformTransientError(error)` is true, `@cloudflare/ai-chat` should route the interruption through its existing bounded chat-recovery machinery, preserving the partial assistant parts and recovery target. Application/provider errors that do not match the package classifier should remain terminal.
This could generalize the existing stall route into an interruption-recovery route rather than requiring applications to add their own retry wrappers or duplicate Cloudflare's transient classification.
## Observability gap
The generic catch converts the original error object into a string before `message:error` and `onChatResponse`. That discards the error name, cause chain, and platform `retryable` metadata, so consumers can detect a terminal failure but cannot identify which transport/storage hop failed.
A structured lifecycle event containing the original error classification, request/stream IDs, and whether recovery was attempted would make this path diagnosable without application-owned recovery plumbing.
## Suggested regression test
Use a response `ReadableStream` that:
1. emits valid start/text parts,
2. throws `Error("Network connection lost.")` from a later read,
3. runs with `chatRecovery` enabled.
Assert that the partial message is preserved and a bounded continuation is scheduled without broadcasting a terminal error frame. A corresponding non-transient reader error should retain the current terminal behavior.
## Related
- #1898 — client handling of the resulting terminal error frame
- #1740 — shared platform-transient classifier and deferred recovery
- #1626 — precedent for routing a stream interruption into bounded recovery
Contributor guide
Assessment
This issue has not been assessed yet.