Package Exports
- @cloudflare/ai-chat
- @cloudflare/ai-chat/ai-chat-v5-migration
- @cloudflare/ai-chat/experimental/forever
- @cloudflare/ai-chat/react
- @cloudflare/ai-chat/types
Readme
@cloudflare/ai-chat
AI chat agents with automatic message persistence, resumable streaming, and tool support. Built on Cloudflare Durable Objects and the AI SDK.
Install
npm install @cloudflare/ai-chat agents ai workers-ai-providerQuick Start
Server
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/moonshotai/kimi-k2.5"),
messages: await convertToModelMessages(this.messages)
});
return result.toUIMessageStreamResponse();
}
}That gives you: automatic message persistence in SQLite, resumable streaming on disconnect/reconnect, and real-time WebSocket delivery to all connected clients.
Client
import { useAgent } from "agents/react";
import { useAgentChat } from "@cloudflare/ai-chat/react";
function Chat() {
const agent = useAgent({ agent: "ChatAgent" });
const { messages, sendMessage, clearHistory, 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({
role: "user",
parts: [{ type: "text", text: input.value }]
});
input.value = "";
}}
>
<input name="input" placeholder="Type a message..." />
</form>
</div>
);
}Wrangler Config
// wrangler.jsonc
{
"ai": { "binding": "AI" },
"durable_objects": {
"bindings": [{ "name": "ChatAgent", "class_name": "ChatAgent" }]
},
"migrations": [
{
"tag": "v1",
"new_sqlite_classes": ["ChatAgent"]
}
]
}Tools
Server-side tools
Tools with an execute function run on the server automatically:
import { createWorkersAI } from "workers-ai-provider";
import { streamText, convertToModelMessages, tool } from "ai";
import { z } from "zod";
export class ChatAgent extends AIChatAgent {
async onChatMessage() {
const workersai = createWorkersAI({ binding: this.env.AI });
const result = streamText({
model: workersai("@cf/moonshotai/kimi-k2.5"),
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 };
}
})
},
maxSteps: 5
});
return result.toUIMessageStreamResponse();
}
}Client-side tools
Tools without execute are handled on the client via onToolCall. Use this for tools that need browser APIs (geolocation, clipboard, camera):
// Server: define tool without execute
getLocation: tool({
description: "Get the user's location from their browser",
inputSchema: z.object({})
// No execute -- client handles it
});// Client: handle via onToolCall
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 }
});
}
}
});Tool approval (human-in-the-loop)
Use needsApproval for tools that require user confirmation before executing:
// Server
processPayment: tool({
description: "Process a payment",
inputSchema: z.object({ amount: z.number(), recipient: z.string() }),
needsApproval: async ({ amount }) => amount > 100, // Only require approval for large amounts
execute: async ({ amount, recipient }) => charge(amount, recipient)
});// Client
const { messages, addToolApprovalResponse } = useAgentChat({ agent });
// When rendering tool parts with state === "approval-requested":
<button onClick={() => addToolApprovalResponse({ id: approvalId, approved: true })}>
Approve
</button>
<button onClick={() => addToolApprovalResponse({ id: approvalId, approved: false })}>
Reject
</button>Resumable Streaming
Streams automatically resume on disconnect/reconnect. No configuration needed.
When a client disconnects mid-stream, chunks are buffered in SQLite. On reconnect, the client receives all buffered chunks and continues receiving the live stream.
Disable with resume: false:
const { messages } = useAgentChat({ agent, resume: false });Storage Management
Limiting stored messages
Cap the number of messages kept in SQLite:
export class ChatAgent extends AIChatAgent {
maxPersistedMessages = 200; // Keep last 200 messages
async onChatMessage() {
// ...
}
}Oldest messages are deleted when the count exceeds the limit. This controls storage only -- it does not affect what is sent to the LLM.
Controlling LLM context
Use the AI SDK's pruneMessages() to control what is sent to the model, independently of what is stored:
import { createWorkersAI } from "workers-ai-provider";
import { streamText, convertToModelMessages, pruneMessages } from "ai";
export class ChatAgent extends AIChatAgent {
maxPersistedMessages = 200;
async onChatMessage() {
const workersai = createWorkersAI({ binding: this.env.AI });
const result = streamText({
model: workersai("@cf/moonshotai/kimi-k2.5"),
messages: pruneMessages({
messages: await convertToModelMessages(this.messages),
reasoning: "before-last-message",
toolCalls: "before-last-2-messages"
})
});
return result.toUIMessageStreamResponse();
}
}Row size protection
Messages approaching SQLite's 2MB row limit are automatically compacted. Large tool outputs are replaced with an LLM-friendly summary that instructs the model to suggest re-running the tool. Compacted messages include metadata.compactedToolOutputs so clients can detect and display this gracefully.
Custom Request Data
Include custom data with every chat request using the body option:
const { messages, sendMessage } = useAgentChat({
agent,
body: {
timezone: Intl.DateTimeFormat().resolvedOptions().timeZone,
userId: "abc"
}
});
// Or use a function for dynamic values:
body: () => ({ token: getAuthToken(), timestamp: Date.now() });Access these fields on the server via options.body:
async onChatMessage(onFinish, options) {
const { timezone, userId } = options?.body ?? {};
}API Reference
AIChatAgent<Env, State>
Extends Agent from the agents package.
| Property / Method | Type | Description |
|---|---|---|
messages |
UIMessage[] |
Current conversation messages (loaded from SQLite) |
maxPersistedMessages |
number | undefined |
Max messages to keep in SQLite. Default: unlimited |
onChatMessage(onFinish?, options?) |
Override | Handle incoming chat messages. Return a Response. onFinish is optional. |
persistMessages(messages) |
Promise<void> |
Manually persist messages (usually automatic) |
saveMessages(messages) |
Promise<void> |
Persist messages and trigger onChatMessage |
waitUntilStable() |
Promise<boolean> |
Protected helper to wait until the conversation is fully stable |
resetTurnState() |
void |
Protected helper to abort the active turn and invalidate queued continuations |
hasPendingInteraction() |
boolean |
Protected helper to detect pending tool input or approval in assistant messages |
useAgentChat(options)
React hook for chat interactions. Wraps the AI SDK's useChat with WebSocket transport.
Options:
| Option | Type | Description |
|---|---|---|
agent |
ReturnType<typeof useAgent> |
Agent connection (required) |
onToolCall |
({ toolCall, addToolOutput }) => void |
Handle client-side tool execution |
autoContinueAfterToolResult |
boolean |
Auto-continue after client tool results. Default: true |
resume |
boolean |
Enable stream resumption. Default: true |
body |
object | () => object |
Custom data sent with every request (see below) |
prepareSendMessagesRequest |
(options) => { body?, headers? } |
Advanced per-request customization |
getInitialMessages |
(options) => Promise<UIMessage[]> |
Custom initial message loader |
Returns:
| Property | Type | Description |
|---|---|---|
messages |
UIMessage[] |
Chat messages |
sendMessage |
(message) => void |
Send a message |
clearHistory |
() => void |
Clear conversation |
addToolOutput |
({ toolCallId, output }) => void |
Provide tool output |
addToolApprovalResponse |
({ id, approved }) => void |
Approve/reject a tool |
setMessages |
(messages | updater) => void |
Set messages (syncs to server) |
status |
string |
"idle" | "submitted" | "streaming" | "error" |
Exports
| Import path | What it provides |
|---|---|
@cloudflare/ai-chat |
AIChatAgent, createToolsFromClientSchemas |
@cloudflare/ai-chat/react |
useAgentChat |
@cloudflare/ai-chat/types |
MessageType, OutgoingMessage, IncomingMessage |
Examples
- Resumable streaming chat -- automatic stream resumption
- Human-in-the-loop guide -- tool approval with
needsApproval+onToolCall - Playground -- kitchen-sink demo of all SDK features
License
MIT