Skip to content

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 …BeforeNow
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 HelixErrorrejectsthe same typed answer
send committed its run, then the response-message-id re-read throws a retryable HelixErrorrejects{ error, code } with the non-retryable row (500, or 409 for state_*), never 503
any step throws a non-Helix error (AiSdkHandlerError included)rejectsunchanged: 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 anything handleChatStream rejects with.
    • AiSdkHandlerError → its statusCode and { error, code };
    • HelixError → core typedErrorResponse (its reason as cause);
    • 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) and submitToolResultErrorResponse(error) are the same functions @helix-agents/ai-sdk/cloudflare exports, 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:

typescript
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 catch mapped AiSdkHandlerError and answered everything else 500 { error: error instanceof Error ? error.message : 'Internal error' }.
  • After: chatErrorResponse covers 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.

Released under the MIT License.