> ## Documentation Index
> Fetch the complete documentation index at: https://mintlify.com/cloudflare/agents/llms.txt
> Use this file to discover all available pages before exploring further.

# Chat Agents

> Build AI-powered chat interfaces with automatic message persistence, resumable streaming, and server/client tool support

Build AI-powered chat interfaces with `AIChatAgent` and `useAgentChat`. Messages are automatically persisted to SQLite, streams resume on disconnect, and tool calls work across server and client.

## Overview

`@cloudflare/ai-chat` provides two main exports:

| Export         | Import                      | Purpose                                                        |
| -------------- | --------------------------- | -------------------------------------------------------------- |
| `AIChatAgent`  | `@cloudflare/ai-chat`       | Server-side agent class with message persistence and streaming |
| `useAgentChat` | `@cloudflare/ai-chat/react` | React hook for building chat UIs                               |

Built on the [AI SDK](https://ai-sdk.dev) and Cloudflare Durable Objects, you get:

* **Automatic message persistence** — conversations stored in SQLite, survive restarts
* **Resumable streaming** — disconnected clients resume mid-stream without data loss
* **Real-time sync** — messages broadcast to all connected clients via WebSocket
* **Tool support** — server-side, client-side, and human-in-the-loop tool patterns
* **Data parts** — attach typed JSON (citations, progress, usage) to messages alongside text
* **Row size protection** — automatic compaction when messages approach SQLite limits

## Quick Start

<Steps>
  <Step title="Install dependencies">
    ```bash theme={null}
    npm install @cloudflare/ai-chat agents ai workers-ai-provider
    ```
  </Step>

  <Step title="Create server agent">
    ```typescript theme={null}
    import { AIChatAgent } from "@cloudflare/ai-chat";
    import { createWorkersAI } from "workers-ai-provider";
    import { streamText, convertToModelMessages } from "ai";

    export class ChatAgent extends AIChatAgent {
      async onChatMessage() {
        const workersai = createWorkersAI({ binding: this.env.AI });

        const result = streamText({
          model: workersai("@cf/zai-org/glm-4.7-flash"),
          messages: await convertToModelMessages(this.messages)
        });

        return result.toUIMessageStreamResponse();
      }
    }
    ```
  </Step>

  <Step title="Create client UI">
    ```tsx theme={null}
    import { useAgent } from "agents/react";
    import { useAgentChat } from "@cloudflare/ai-chat/react";

    function Chat() {
      const agent = useAgent({ agent: "ChatAgent" });
      const { messages, sendMessage, status } = useAgentChat({ agent });

      return (
        <div>
          {messages.map((msg) => (
            <div key={msg.id}>
              <strong>{msg.role}:</strong>
              {msg.parts.map((part, i) =>
                part.type === "text" ? <span key={i}>{part.text}</span> : null
              )}
            </div>
          ))}

          <form
            onSubmit={(e) => {
              e.preventDefault();
              const input = e.currentTarget.elements.namedItem(
                "input"
              ) as HTMLInputElement;
              sendMessage({ text: input.value });
              input.value = "";
            }}
          >
            <input name="input" placeholder="Type a message..." />
            <button type="submit" disabled={status === "streaming"}>
              Send
            </button>
          </form>
        </div>
      );
    }
    ```
  </Step>

  <Step title="Configure Wrangler">
    ```jsonc wrangler.jsonc theme={null}
    {
      "ai": { "binding": "AI" },
      "durable_objects": {
        "bindings": [{ "name": "ChatAgent", "class_name": "ChatAgent" }]
      },
      "migrations": [{ "tag": "v1", "new_sqlite_classes": ["ChatAgent"] }]
    }
    ```

    <Note>
      The `new_sqlite_classes` migration is required — `AIChatAgent` uses SQLite for message persistence and stream chunk buffering.
    </Note>
  </Step>
</Steps>

## How It Works

```
┌──────────┐               WebSocket                ┌──────────────┐
│  Client  │ ◀──────────────────────────────────▶   │ AIChatAgent  │
│          │                                        │              │
│ useAgent │   CF_AGENT_USE_CHAT_REQUEST ──────▶    │ onChatMessage│
│    Chat  │                                        │              │
│          │   ◀────── CF_AGENT_USE_CHAT_RESPONSE   │  streamText  │
│          │          (UIMessageChunk stream)       │              │
│          │                                        │   SQLite     │
│          │   ◀────── CF_AGENT_CHAT_MESSAGES       │  (messages,  │
│          │          (broadcast to all clients)    │   chunks)    │
└──────────┘                                        └──────────────┘
```

<Steps>
  <Step title="Client sends message">
    The client sends a message via WebSocket
  </Step>

  <Step title="Agent persists and calls handler">
    `AIChatAgent` persists messages to SQLite and calls your `onChatMessage` method
  </Step>

  <Step title="Stream response">
    Your method returns a streaming `Response` (typically from `streamText`)
  </Step>

  <Step title="Real-time chunks">
    Chunks stream back over WebSocket in real-time
  </Step>

  <Step title="Broadcast final message">
    When the stream completes, the final message is persisted and broadcast to all connections
  </Step>
</Steps>

## Server API

### AIChatAgent

Extends `Agent` from the `agents` package. Manages conversation state, persistence, and streaming.

```typescript theme={null}
import { AIChatAgent } from "@cloudflare/ai-chat";

export class ChatAgent extends AIChatAgent {
  // Access current messages
  // this.messages: UIMessage[]

  // Limit stored messages (optional)
  maxPersistedMessages = 200;

  async onChatMessage(onFinish?, options?) {
    // onFinish: optional callback for streamText (cleanup is automatic)
    // options.abortSignal: cancel signal
    // options.body: custom data from client
    // Return a Response (streaming or plain text)
  }
}
```

### onChatMessage

This is the main method you override. It receives the conversation context and should return a `Response`.

<CodeGroup>
  ```typescript Streaming response theme={null}
  async onChatMessage() {
    const workersai = createWorkersAI({ binding: this.env.AI });

    const result = streamText({
      model: workersai("@cf/zai-org/glm-4.7-flash"),
      system: "You are a helpful assistant.",
      messages: await convertToModelMessages(this.messages)
    });

    return result.toUIMessageStreamResponse();
  }
  ```

  ```typescript Plain text response theme={null}
  async onChatMessage() {
    return new Response("Hello! I am a simple agent.", {
      headers: { "Content-Type": "text/plain" }
    });
  }
  ```

  ```typescript Custom body data theme={null}
  async onChatMessage(_onFinish, options) {
    const { timezone, userId } = options?.body ?? {};
    // Use these values in your LLM call or business logic

    // options.requestId — unique identifier for this chat request,
    // useful for logging and correlating events
    console.log("Request ID:", options?.requestId);
  }
  ```
</CodeGroup>

### this.messages

The current conversation history, loaded from SQLite. This is an array of `UIMessage` objects from the AI SDK. Messages are automatically persisted after each interaction.

### maxPersistedMessages

Cap the number of messages stored in SQLite. When the limit is exceeded, the oldest messages are deleted. This controls storage only — it does not affect what is sent to the LLM.

```typescript theme={null}
export class ChatAgent extends AIChatAgent {
  maxPersistedMessages = 200;
}
```

To control what is sent to the model, use the AI SDK's `pruneMessages()`:

```typescript theme={null}
import { streamText, convertToModelMessages, pruneMessages } from "ai";

async onChatMessage() {
  const workersai = createWorkersAI({ binding: this.env.AI });

  const result = streamText({
    model: workersai("@cf/zai-org/glm-4.7-flash"),
    messages: pruneMessages({
      messages: await convertToModelMessages(this.messages),
      reasoning: "before-last-message",
      toolCalls: "before-last-2-messages"
    })
  });

  return result.toUIMessageStreamResponse();
}
```

### waitForMcpConnections

Controls whether `AIChatAgent` waits for MCP server connections to settle before calling `onChatMessage`. This ensures `this.mcp.getAITools()` returns the full set of tools, especially after Durable Object hibernation when connections are being restored in the background.

| Value                 | Behavior                                      |
| --------------------- | --------------------------------------------- |
| `{ timeout: 10_000 }` | Wait up to 10 seconds (default)               |
| `{ timeout: N }`      | Wait up to `N` milliseconds                   |
| `true`                | Wait indefinitely until all connections ready |
| `false`               | Do not wait (old behavior before 0.2.0)       |

```typescript theme={null}
export class ChatAgent extends AIChatAgent {
  // Default — waits up to 10 seconds
  // waitForMcpConnections = { timeout: 10_000 };

  // Wait forever
  waitForMcpConnections = true;

  // Disable waiting
  waitForMcpConnections = false;
}
```

### Request Cancellation

When a user clicks "stop" in the chat UI, the client sends a `CF_AGENT_CHAT_REQUEST_CANCEL` message. The server propagates this to the `abortSignal` in `options`:

```typescript theme={null}
async onChatMessage(_onFinish, options) {
  const result = streamText({
    model: workersai("@cf/zai-org/glm-4.7-flash"),
    messages: await convertToModelMessages(this.messages),
    abortSignal: options?.abortSignal // Pass through for cancellation
  });

  return result.toUIMessageStreamResponse();
}
```

<Warning>
  If you do not pass `abortSignal` to `streamText`, the LLM call will continue running in the background even after the user cancels. Always forward it when possible.
</Warning>

## Client API

### useAgentChat

React hook that connects to an `AIChatAgent` over WebSocket. Wraps the AI SDK's `useChat` with a native WebSocket transport.

```tsx theme={null}
import { useAgent } from "agents/react";
import { useAgentChat } from "@cloudflare/ai-chat/react";

function Chat() {
  const agent = useAgent({ agent: "ChatAgent" });
  const {
    messages,
    sendMessage,
    clearHistory,
    addToolOutput,
    addToolApprovalResponse,
    setMessages,
    status
  } = useAgentChat({ agent });

  // ...
}
```

### Options

| Option                        | Type                                          | Default  | Description                                                                                                              |
| ----------------------------- | --------------------------------------------- | -------- | ------------------------------------------------------------------------------------------------------------------------ |
| `agent`                       | `ReturnType<typeof useAgent>`                 | Required | Agent connection from `useAgent`                                                                                         |
| `onToolCall`                  | `({ toolCall, addToolOutput }) => void`       | —        | Handle client-side tool execution                                                                                        |
| `autoContinueAfterToolResult` | `boolean`                                     | `true`   | Auto-continue conversation after client tool results and approvals                                                       |
| `resume`                      | `boolean`                                     | `true`   | Enable automatic stream resumption on reconnect                                                                          |
| `body`                        | `object \| () => object`                      | —        | Custom data sent with every request                                                                                      |
| `prepareSendMessagesRequest`  | `(options) => { body?, headers? }`            | —        | Advanced per-request customization                                                                                       |
| `tools`                       | `Record<string, AITool>`                      | —        | Dynamic client-defined tools for SDK/platform use cases. Schemas are sent to the server automatically                    |
| `getInitialMessages`          | `(options) => Promise<UIMessage[]>` or `null` | —        | Custom initial message loader. Set to `null` to skip the HTTP fetch entirely (useful when providing `messages` directly) |

### Return Values

| Property                  | Type                               | Description                                          |
| ------------------------- | ---------------------------------- | ---------------------------------------------------- |
| `messages`                | `UIMessage[]`                      | Current conversation messages                        |
| `sendMessage`             | `(message) => void`                | Send a message                                       |
| `clearHistory`            | `() => void`                       | Clear conversation (client and server)               |
| `addToolOutput`           | `({ toolCallId, output }) => void` | Provide output for a client-side tool                |
| `addToolApprovalResponse` | `({ id, approved }) => void`       | Approve or reject a tool requiring approval          |
| `setMessages`             | `(messages \| updater) => void`    | Set messages directly (syncs to server)              |
| `status`                  | `string`                           | `"idle"`, `"submitted"`, `"streaming"`, or `"error"` |

## Tools

`AIChatAgent` supports three tool patterns, all using the AI SDK's `tool()` function:

| Pattern     | Where it runs                | When to use                                   |
| ----------- | ---------------------------- | --------------------------------------------- |
| Server-side | Server (automatic)           | API calls, database queries, computations     |
| Client-side | Browser (via `onToolCall`)   | Geolocation, clipboard, camera, local storage |
| Approval    | Server (after user approval) | Payments, deletions, external actions         |

### Server-Side Tools

Tools with an `execute` function run automatically on the server:

```typescript theme={null}
import { streamText, convertToModelMessages, tool, stepCountIs } from "ai";
import { z } from "zod";

async onChatMessage() {
  const workersai = createWorkersAI({ binding: this.env.AI });

  const result = streamText({
    model: workersai("@cf/zai-org/glm-4.7-flash"),
    messages: await convertToModelMessages(this.messages),
    tools: {
      getWeather: tool({
        description: "Get weather for a city",
        inputSchema: z.object({ city: z.string() }),
        execute: async ({ city }) => {
          const data = await fetchWeather(city);
          return { temperature: data.temp, condition: data.condition };
        }
      })
    },
    stopWhen: stepCountIs(5)
  });

  return result.toUIMessageStreamResponse();
}
```

### Client-Side Tools

Define a tool on the server without `execute`, then handle it on the client with `onToolCall`. Use this for tools that need browser APIs:

<CodeGroup>
  ```typescript Server theme={null}
  tools: {
    getLocation: tool({
      description: "Get the user's location from the browser",
      inputSchema: z.object({})
      // No execute — the client handles it
    });
  }
  ```

  ```tsx Client theme={null}
  const { messages, sendMessage } = useAgentChat({
    agent,
    onToolCall: async ({ toolCall, addToolOutput }) => {
      if (toolCall.toolName === "getLocation") {
        const pos = await new Promise((resolve, reject) =>
          navigator.geolocation.getCurrentPosition(resolve, reject)
        );
        addToolOutput({
          toolCallId: toolCall.toolCallId,
          output: { lat: pos.coords.latitude, lng: pos.coords.longitude }
        });
      }
    }
  });
  ```
</CodeGroup>

When the LLM invokes `getLocation`, the stream pauses. The `onToolCall` callback fires, your code provides the output, and the conversation continues.

### Dynamic Client Tools

For SDKs and platforms where tools are defined dynamically by the embedding application at runtime, use the `tools` option on `useAgentChat` and `createToolsFromClientSchemas()` on the server:

<CodeGroup>
  ```typescript Server theme={null}
  import { createToolsFromClientSchemas } from "@cloudflare/ai-chat";

  async onChatMessage(_onFinish, options) {
    const result = streamText({
      model: workersai("@cf/zai-org/glm-4.7-flash"),
      messages: await convertToModelMessages(this.messages),
      tools: createToolsFromClientSchemas(options?.clientTools)
    });
    return result.toUIMessageStreamResponse();
  }
  ```

  ```tsx Client theme={null}
  import { useAgentChat, type AITool } from "@cloudflare/ai-chat/react";

  const tools: Record<string, AITool> = {
    getPageTitle: {
      description: "Get the current page title",
      parameters: { type: "object", properties: {} },
      execute: async () => ({ title: document.title })
    }
  };

  const { messages, sendMessage } = useAgentChat({
    agent,
    tools,
    onToolCall: async ({ toolCall, addToolOutput }) => {
      const tool = tools[toolCall.toolName];
      if (tool?.execute) {
        const output = await tool.execute(toolCall.input);
        addToolOutput({ toolCallId: toolCall.toolCallId, output });
      }
    }
  });
  ```
</CodeGroup>

<Note>
  For most apps, server-side tools with `tool()` and `onToolCall` are simpler and provide full Zod type safety. Use dynamic client tools when the server does not know the tool surface at deploy time.
</Note>

### Tool Approval (Human-in-the-Loop)

Use `needsApproval` for tools that require user confirmation before executing:

<CodeGroup>
  ```typescript Server theme={null}
  tools: {
    processPayment: tool({
      description: "Process a payment",
      inputSchema: z.object({
        amount: z.number(),
        recipient: z.string()
      }),
      needsApproval: async ({ amount }) => amount > 100,
      execute: async ({ amount, recipient }) => charge(amount, recipient)
    });
  }
  ```

  ```tsx Client theme={null}
  import { isToolUIPart, getToolName } from "ai";

  const { messages, addToolApprovalResponse } = useAgentChat({ agent });

  // Render pending approvals from message parts
  {
    messages.map((msg) =>
      msg.parts
        .filter(
          (part) =>
            isToolUIPart(part) &&
            "approval" in part &&
            part.state === "approval-requested"
        )
        .map((part) => (
          <div key={part.toolCallId}>
            <p>Approve {getToolName(part)}?</p>
            <button
              onClick={() =>
                addToolApprovalResponse({
                  id: part.approval?.id,
                  approved: true
                })
              }
            >
              Approve
            </button>
            <button
              onClick={() =>
                addToolApprovalResponse({
                  id: part.approval?.id,
                  approved: false
                })
              }
            >
              Reject
            </button>
          </div>
        ))
    );
  }
  ```
</CodeGroup>

## Data Parts

Data parts let you attach typed JSON to messages alongside text — progress indicators, source citations, token usage, or any structured data your UI needs.

### Writing Data Parts (Server)

Use `createUIMessageStream` with `writer.write()` to send data parts from the server:

```typescript theme={null}
import {
  streamText,
  convertToModelMessages,
  createUIMessageStream,
  createUIMessageStreamResponse
} from "ai";

export class ChatAgent extends AIChatAgent {
  async onChatMessage() {
    const workersai = createWorkersAI({ binding: this.env.AI });

    const stream = createUIMessageStream({
      execute: async ({ writer }) => {
        const result = streamText({
          model: workersai("@cf/zai-org/glm-4.7-flash"),
          messages: await convertToModelMessages(this.messages)
        });

        // Merge the LLM stream
        writer.merge(result.toUIMessageStream());

        // Write a data part — persisted to message.parts
        writer.write({
          type: "data-sources",
          id: "src-1",
          data: { query: "agents", status: "searching", results: [] }
        });

        // Later: update the same part in-place (same type + id)
        writer.write({
          type: "data-sources",
          id: "src-1",
          data: {
            query: "agents",
            status: "found",
            results: ["Agents SDK docs", "Durable Objects guide"]
          }
        });
      }
    });

    return createUIMessageStreamResponse({ stream });
  }
}
```

### Three Patterns

| Pattern            | How                                              | Persisted? | Use case                              |
| ------------------ | ------------------------------------------------ | ---------- | ------------------------------------- |
| **Reconciliation** | Same `type` + `id` → updates in-place            | Yes        | Progressive state (searching → found) |
| **Append**         | No `id`, or different `id` → appends             | Yes        | Log entries, multiple citations       |
| **Transient**      | `transient: true` → not added to `message.parts` | No         | Ephemeral status (thinking indicator) |

### Reading Data Parts (Client)

Non-transient data parts appear in `message.parts`. Use the `UIMessage` generic to type them:

```typescript theme={null}
import { useAgentChat } from "@cloudflare/ai-chat/react";
import type { UIMessage } from "ai";

type ChatMessage = UIMessage<
  unknown,
  {
    sources: { query: string; status: string; results: string[] };
    usage: { model: string; inputTokens: number; outputTokens: number };
  }
>;

const { messages } = useAgentChat<unknown, ChatMessage>({ agent });

// Typed access — no casts needed
for (const msg of messages) {
  for (const part of msg.parts) {
    if (part.type === "data-sources") {
      console.log(part.data.results); // string[]
    }
  }
}
```

## Resumable Streaming

Streams automatically resume when a client disconnects and reconnects. No configuration is needed — it works out of the box.

When streaming is active:

1. All chunks are buffered in SQLite as they are generated
2. If the client disconnects, the server continues streaming and buffering
3. When the client reconnects, it receives all buffered chunks and resumes live streaming

Disable with `resume: false`:

```tsx theme={null}
const { messages } = useAgentChat({ agent, resume: false });
```

For more details, see [Resumable Streaming](/ai/resumable-streaming).

## Storage Management

### Row Size Protection

SQLite rows have a maximum size of 2 MB. When a message approaches this limit (for example, a tool returning a very large output), `AIChatAgent` automatically compacts the message:

1. **Tool output compaction** — Large tool outputs are replaced with an LLM-friendly summary that instructs the model to suggest re-running the tool
2. **Text truncation** — If the message is still too large after tool compaction, text parts are truncated with a note

Compacted messages include `metadata.compactedToolOutputs` so clients can detect and display this gracefully.

### Controlling LLM Context vs Storage

Storage (`maxPersistedMessages`) and LLM context are independent:

| Concern                         | Control                | Scope       |
| ------------------------------- | ---------------------- | ----------- |
| How many messages SQLite stores | `maxPersistedMessages` | Persistence |
| What the model sees             | `pruneMessages()`      | LLM context |
| Row size limits                 | Automatic compaction   | Per-message |

```typescript theme={null}
export class ChatAgent extends AIChatAgent {
  maxPersistedMessages = 200; // Storage limit

  async onChatMessage() {
    const result = streamText({
      model: workersai("@cf/zai-org/glm-4.7-flash"),
      messages: pruneMessages({
        // LLM context limit
        messages: await convertToModelMessages(this.messages),
        reasoning: "before-last-message",
        toolCalls: "before-last-2-messages"
      })
    });

    return result.toUIMessageStreamResponse();
  }
}
```

## Using Different AI Providers

`AIChatAgent` works with any AI SDK-compatible provider. The server code determines which model to use — the client does not need to change.

<CodeGroup>
  ```typescript Workers AI (Cloudflare) theme={null}
  import { createWorkersAI } from "workers-ai-provider";

  const workersai = createWorkersAI({ binding: this.env.AI });
  const result = streamText({
    model: workersai("@cf/zai-org/glm-4.7-flash"),
    messages: await convertToModelMessages(this.messages)
  });
  ```

  ```typescript OpenAI theme={null}
  import { createOpenAI } from "@ai-sdk/openai";

  const openai = createOpenAI({ apiKey: this.env.OPENAI_API_KEY });
  const result = streamText({
    model: openai.chat("gpt-4o"),
    messages: await convertToModelMessages(this.messages)
  });
  ```

  ```typescript Anthropic theme={null}
  import { createAnthropic } from "@ai-sdk/anthropic";

  const anthropic = createAnthropic({ apiKey: this.env.ANTHROPIC_API_KEY });
  const result = streamText({
    model: anthropic("claude-sonnet-4-20250514"),
    messages: await convertToModelMessages(this.messages)
  });
  ```
</CodeGroup>

## Multi-Client Sync

When multiple clients connect to the same agent instance, messages are automatically broadcast to all connections. If one client sends a message, all other connected clients receive the updated message list.

```
Client A ──── sendMessage("Hello") ────▶ AIChatAgent
                                              │
                                        persist + stream
                                              │
Client A ◀── CF_AGENT_USE_CHAT_RESPONSE ──────┤
Client B ◀── CF_AGENT_CHAT_MESSAGES ──────────┘
```

The originating client receives the streaming response. All other clients receive the final messages via a `CF_AGENT_CHAT_MESSAGES` broadcast.

## Related Documentation

* [Resumable Streaming](/ai/resumable-streaming) — How stream resumption works
* [Client Tools Continuation](/ai/client-tools-continuation) — Advanced client-side tool patterns
* [Codemode](/ai/codemode) — Let LLMs write code to orchestrate tools
