Web Engineering

AI SDK 5 made chat state an interface

2 min read
type ChatPart =
  | { type: "text"; text: string }
  | { type: "tool-request"; name: string; status: "pending" | "approved" | "rejected" }
  | { type: "tool-result"; name: string; ok: boolean; summary?: string }
  | { type: "file"; id: string; name: string }
  | { type: "error"; retryable: boolean; message: string }

The type is more interesting than the chat bubble around it.

A streaming demo can treat every assistant message as a string with a role label. That stops working as soon as the response includes a tool call, citation, file, partial result, approval, or recoverable error. The UI needs to know what it is rendering instead of scraping its own transcript.

The states I want named are not exotic:

idle
composing
submitted
streaming
tool-pending
approval-required
tool-running
recoverable-error
terminal-error
cancelled
replayed

Once these are explicit, the app can answer practical questions. Can the user send another prompt while the tool is running? Does retry rerun the tool or regenerate only the text? What does a half-finished response look like after the network drops? Which message owns the error?

That is why I found the move toward typed chat state useful. Types do not make the model correct. They stop the application from pretending every reply has the same shape.

The model does not own the conversation. The app does. The app decides which parts are visible, which actions are retryable, which tool calls need approval, and what survives a refresh.

Streaming is the easy path. Recovery is the product.

A chat interface is ready when it can show the incomplete, rejected, cancelled, and replayed states without lying about them.

Jeremy London

About Jeremy London

Engineering leader and builder in Denver. I write about AI platforms, agents, security, reliability, homelab infrastructure, and the parts of engineering work that have to survive production.