Chat handler: every HelixError answered typed
This guide covers the release in which handleChatStream answers every HelixError as a typed HTTP error and no longer throws one, and @helix-agents/ai-sdk exports the typed wire-error builders from its root. It is a major release of @helix-agents/ai-sdk: a call that used to reject now resolves.
Typed answers for a refused send (Typed chat resume / submit failures) and a refused resume or submit already shipped. This release covers the failures handleChatStream still threw: the session and run reads that route a request, the resume path's snapshot read, and the run re-read after a send's run-start commit. Every host's route answered them 500 { error: <raw message> }, with no code. A client could not tell a retryable failure from a refused one, and an unknown error's text reached the client verbatim.
What changed
handleChatStream resolves a typed Response for every HelixError
Every handleChatStream host is affected: agent-server POST /chat, the Express adapter, createCloudflareChatHandler (handleChat) and any custom route.
| A request whose … | Before | Now |
|---|---|---|
session or run read throws a HelixError (e.g. the CF DO client's DOPeerError for a 5xx /state) | rejects; the route's catch answered 500 | { error, code }: 503 when retryable, 409 for a non-retryable state_* |
resume's snapshot read throws a HelixError | rejects | the same typed answer |
send committed its run, then the response-message-id re-read throws a retryable HelixError | rejects | { error, code } with the non-retryable row (500, or 409 for state_*), never 503 |
any step throws a non-Helix error (AiSdkHandlerError included) | rejects | unchanged: rejects |
The body is core typedErrorResponse's { error, code } with the message scrubbed, plus the error's reason as cause when it has one (corrupt_row, short_read, …), and the X-Session-Id header.
Never 503 once the input is committed. handleChatStream tracks whether this request's own input reached a committed run: a send's execute() returned its handle, or a submitted tool result was stored. A failure after that answers the non-retryable row, because a re-sent request would append the message (or submit the answer) twice.
New root exports: the typed wire-error builders
@helix-agents/ai-sdk now exports, from its root and runtime-agnostically:
chatErrorResponse(error, { sessionId?, logger? })is a chat route's answer for a failure of its own: body parsing, its own lookups, auth, or anythinghandleChatStreamrejects with.AiSdkHandlerError→ itsstatusCodeand{ error, code };HelixError→ coretypedErrorResponse(itsreasonascause);- anything else →
500 { error: 'Internal error', code: 'framework_internal_error' }. The unknown error's text is never echoed; log it on the server.
snapshotNotFoundResponse(),snapshotErrorResponse(error)andsubmitToolResultErrorResponse(error)are the same functions@helix-agents/ai-sdk/cloudflareexports, which it still does.
How to upgrade
A route that returns the handler's Response needs no change. It now answers typed errors by default.
A route that catches around handleChatStream:
import { chatErrorResponse } from '@helix-agents/ai-sdk';
export async function POST(req: Request, { params }: { params: { sessionId: string } }) {
const { sessionId } = params;
try {
const body = await req.json();
return await handleChatStream(deps, { sessionId, messages: body.messages ?? [], request: req });
} catch (error) {
console.error('[POST /api/chat]', error);
return chatErrorResponse(error, { sessionId, logger: console });
}
}- Before: the
catchmappedAiSdkHandlerErrorand answered everything else500 { error: error instanceof Error ? error.message : 'Internal error' }. - After:
chatErrorResponsecovers both, and never echoes an unknown error's text.
Code that caught a HelixError from handleChatStream (or from createCloudflareChatHandler().handleChat) to read its code now receives the typed Response. Read response.status and the JSON body's code, or on the client parseHelixChatError.
A Node host's local snapshot-response.ts (the copy the docs and examples used to recommend) can be deleted. Import snapshotErrorResponse / snapshotNotFoundResponse from @helix-agents/ai-sdk.