Chat, tools, and approvals

Build persistent streaming chat

Use the current Agents chat APIs to stream model output, persist messages, resume connections, and bound retained history.

8 minute lesson

~~~

A chat UI built on a bare Worker loses everything when the tab closes. The Agents chat layer connects a stateful Agent to a model and browser client, and the agent instance is where the conversation actually lives.

Messages can persist in SQLite and streams can resume after a connection interruption. The chat agent base class keeps the message history for you — your job is the model call:

export class ChatAgent extends AIChatAgent {
  async onChatMessage(onFinish) {
    const result = streamText({
      model: this.model,
      messages: buildContext(this.messages),
      onFinish,
    })

    return result.toUIMessageStreamResponse()
  }
}

this.messages is the persisted history, restored when the instance wakes. The response streams tokens to the client as they generate.

The callbacks are not optional

Pass cancellation and finish callbacks required by the current SDK so cleanup and persistence complete. The onFinish handed to onChatMessage is how the completed assistant message gets persisted. Swallow it and the symptom is nasty: the reply streams beautifully, then vanishes on the next page load, because it was never written to storage. Cancellation matters the same way — a user who stops generation mid-answer should leave behind a consistent history, not a half-written turn that confuses the next model call.

Bound the context deliberately

Limit retained messages and build a deliberate context window rather than sending an unlimited history to every model call:

function buildContext(messages) {
  return messages.slice(-30)
}

Persisted history and model context are different things. Keep the full transcript in SQLite if the product needs it, but each model call should get a bounded, chosen slice. Send everything and long conversations get slower and more expensive every turn until they exceed the model’s context limit and fail outright — the “chat breaks after 40 messages” bug is almost always this.

Now test the property that makes this architecture worth it. Disconnect a chat client mid-stream, reconnect it, and verify message state and UI converge without duplicating the assistant turn. Kill the network while a long answer streams, reload, reconnect. The history should show exactly one assistant reply, complete or cleanly truncated. Two copies of the same answer means persistence and streaming are racing each other, and users will see it daily on flaky mobile connections.

Lesson completed

Take this course offline

Get every free book and course as PDF and EPUB files.

Get the download library →