AIChatAgent と useAgentChat で AI チャットインターフェイスを構築します。メッセージは SQLite に自動永続化され、切断時にストリームが再開し、ツール呼び出しはサーバーとクライアントをまたいで動きます。
@cloudflare/ai-chat パッケージは、次の 2 つの主要な API を提供します。
| エクスポート | インポート | 用途 |
|---|---|---|
AIChatAgent |
@cloudflare/ai-chat |
メッセージの永続化とストリーミングを持つサーバー側エージェントクラス |
useAgentChat |
@cloudflare/ai-chat/react |
チャット UI を構築する React フック |
高度なヘルパーは @cloudflare/ai-chat/react、@cloudflare/ai-chat/types、agents/chat からも利用できます。パッケージ表面の全体は エクスポート を参照してください。
AI SDK ↗ と Cloudflare Durable Objects の上に構築されており、次が得られます。
- 自動メッセージ永続化 — 会話は SQLite に保存され、再起動後も残ります
- 再開可能なストリーミング — 切断したクライアントはデータ損失なしにストリーム途中から再開します
- リアルタイム同期 — メッセージは WebSocket 経由ですべての接続クライアントへブロードキャストされます
- ツール対応 — サーバー側、クライアント側、ヒューマンインザループのツールパターン
- Data parts — テキストとあわせて、引用、進捗、利用量などの型付き JSON をメッセージに付けられます
- 行サイズ保護 — メッセージが SQLite の上限に近づくと自動コンパクションします
npm i @cloudflare/ai-chat agents ai @ai-sdk/react workers-ai-provideryarn add @cloudflare/ai-chat agents ai @ai-sdk/react workers-ai-providerpnpm add @cloudflare/ai-chat agents ai @ai-sdk/react workers-ai-providerbun add @cloudflare/ai-chat agents ai @ai-sdk/react workers-ai-providerimport { AIChatAgent } from "@cloudflare/ai-chat";
import { createWorkersAI } from "workers-ai-provider";
import { streamText, convertToModelMessages } from "ai";
export class ChatAgent extends AIChatAgent {
async onChatMessage() {
// Use any provider such as workers-ai-provider, openai, anthropic, google, etc.
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();
}
}import { AIChatAgent } from "@cloudflare/ai-chat";
import { createWorkersAI } from "workers-ai-provider";
import { streamText, convertToModelMessages } from "ai";
export class ChatAgent extends AIChatAgent {
async onChatMessage() {
// Use any provider such as workers-ai-provider, openai, anthropic, google, etc.
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();
}
}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");
sendMessage({ text: input.value });
input.value = "";
}}
>
<input name="input" placeholder="Type a message..." />
<button type="submit" disabled={status !== "ready"}>
Send
</button>
</form>
</div>
);
}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 !== "ready"}>
Send
</button>
</form>
</div>
);
}// wrangler.jsonc
{
"ai": { "binding": "AI" },
"durable_objects": {
"bindings": [{ "name": "ChatAgent", "class_name": "ChatAgent" }],
},
"migrations": [{ "tag": "v1", "new_sqlite_classes": ["ChatAgent"] }],
}new_sqlite_classes マイグレーションは必須です。AIChatAgent はメッセージの永続化とストリームチャンクのバッファに SQLite を使います。
sequenceDiagram
participant Client as Client (useAgentChat)
participant Agent as AIChatAgent
participant DB as SQLite
Client->>Agent: CF_AGENT_USE_CHAT_REQUEST (WebSocket)
Agent->>DB: Persist messages
Agent->>Agent: onChatMessage()
loop Streaming response
Agent-->>Client: CF_AGENT_USE_CHAT_RESPONSE (chunks)
Agent->>DB: Buffer chunks
end
Agent->>DB: Persist final message
Agent-->>Client: CF_AGENT_CHAT_MESSAGES (broadcast to all clients)
- クライアントは WebSocket 経由でメッセージを送ります
AIChatAgentはメッセージを SQLite に永続化し、onChatMessageメソッドを呼びます- メソッドはストリーミング
Responseを返します(通常はstreamTextから) - チャンクは WebSocket 経由でリアルタイムにストリームバックされます
- ストリームが完了すると、最終メッセージが永続化され、すべての接続へブロードキャストされます
agents パッケージの Agent を拡張します。会話状態、永続化、ストリーミングを管理します。
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: callback for streamText (cleanup is automatic)
// options.abortSignal: cancel signal
// options.body: custom data from client
// options.continuation: true for continuation turns
// Return a Response (streaming or plain text)
}
}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: callback for streamText (cleanup is automatic)
// options.abortSignal: cancel signal
// options.body: custom data from client
// options.continuation: true for continuation turns
// Return a Response (streaming or plain text)
}
}オーバーライドする主なメソッドです。会話コンテキストを受け取り、Response を返す必要があります。
ストリーミング応答(最も一般的):
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"),
system: "You are a helpful assistant.",
messages: await convertToModelMessages(this.messages),
});
return result.toUIMessageStreamResponse();
}
}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"),
system: "You are a helpful assistant.",
messages: await convertToModelMessages(this.messages),
});
return result.toUIMessageStreamResponse();
}
}プレーンテキスト応答:
export class ChatAgent extends AIChatAgent {
async onChatMessage() {
return new Response("Hello! I am a simple agent.", {
headers: { "Content-Type": "text/plain" },
});
}
}カスタム body データとリクエスト ID へのアクセス:
export class ChatAgent extends AIChatAgent {
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);
if (options?.continuation) {
// This turn continues a previous assistant message after a tool result,
// continueLastTurn(), or recovery.
}
}
}options.continuation は、ツール結果または承認のあとの自動継続、continueLastTurn() の呼び出し、回復したターンで true です。継続ターンで別のモデルを選ぶ、システムプロンプトを調整する、高価なコンテキスト組み立てをスキップするために使います。
SQLite から読み込んだ現在の会話履歴です。AI SDK の UIMessage オブジェクトの配列です。メッセージは各やり取りのあと自動永続化されます。
SQLite に保存するメッセージ数の上限です。上限を超えると、最も古いメッセージが削除されます。これはストレージだけを制御し、LLM へ送る内容には影響しません。
export class ChatAgent extends AIChatAgent {
maxPersistedMessages = 200;
}export class ChatAgent extends AIChatAgent {
maxPersistedMessages = 200;
}モデルへ送る内容を制御するには、AI SDK の pruneMessages() を使います。
import { streamText, convertToModelMessages, pruneMessages } 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: pruneMessages({
messages: await convertToModelMessages(this.messages),
reasoning: "before-last-message",
toolCalls: "before-last-2-messages",
}),
});
return result.toUIMessageStreamResponse();
}
}import { streamText, convertToModelMessages, pruneMessages } 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: pruneMessages({
messages: await convertToModelMessages(this.messages),
reasoning: "before-last-message",
toolCalls: "before-last-2-messages",
}),
});
return result.toUIMessageStreamResponse();
}
}onChatMessage を呼ぶ前に、AIChatAgent が MCP サーバー接続の安定を待つかを制御します。this.mcp.getAITools() がツール一式を返すことを確保します。特に Durable Object のハイバネーション後、接続がバックグラウンドで復元されているときに重要です。
| 値 | 動作 |
|---|---|
{ timeout: 10_000 } |
最大 10 秒待ちます(デフォルト) |
{ timeout: N } |
最大 N ミリ秒待ちます |
true |
すべての接続が準備できるまで無期限に待ちます |
false |
待ちません(0.2.0 より前の動作) |
export class ChatAgent extends AIChatAgent {
// Default — waits up to 10 seconds
// waitForMcpConnections = { timeout: 10_000 };
// Wait forever
waitForMcpConnections = true;
// Disable waiting
waitForMcpConnections = false;
}export class ChatAgent extends AIChatAgent {
// Default — waits up to 10 seconds
// waitForMcpConnections = { timeout: 10_000 };
// Wait forever
waitForMcpConnections = true;
// Disable waiting
waitForMcpConnections = false;
}より低レベルの制御には、代わりに onChatMessage 内で this.mcp.waitForConnections() を直接呼びます。
チャットターンがすでにアクティブまたはキュー済みのとき、重なるユーザー送信がどう振る舞うかを制御します。
export class ChatAgent extends AIChatAgent {
messageConcurrency = "queue";
}export class ChatAgent extends AIChatAgent {
messageConcurrency = "queue";
}| 戦略 | 動作 |
|---|---|
"queue"(デフォルト) |
すべての送信をキューし、順に処理します |
"latest" |
重なる送信のうち最新だけを残します。上書きされた送信はユーザーメッセージを永続化しますが、モデルターンは開始しません |
"merge" |
重なる送信をキューし、最新のキュー済みターンが走る前に末尾のユーザーメッセージを 1 つの結合ターンへまとめます |
"drop" |
重なる送信を完全に無視します。メッセージは永続化されません。 |
{ strategy: "debounce", debounceMs?: number } |
静穏ウィンドウ付きの trailing-edge latest です(デフォルト 750ms) |
この設定は sendMessage() の送信にだけ適用されます。再生成、ツール継続、承認、クリア、プログラムによる saveMessages() 呼び出しは、既存の直列化された動作を保ちます。
persistMessages はメッセージを SQLite に保存し、更新をすべての接続クライアントへブロードキャストしますが、モデルターンは 起動しません。新しい応答を始めずに会話へメッセージを注入したいときに使います。
saveMessages はメッセージを永続化し、かつ 新しい応答のために onChatMessage() を起動します。開始前にアクティブなチャットターンの完了を待つため、スケジュールされた、またはプログラムによるメッセージが進行中のストリームと重なりません。
// Store messages without triggering a response
await this.persistMessages(messages);
// Store messages AND trigger onChatMessage
const { requestId, status } = await this.saveMessages(messages);// Store messages without triggering a response
await this.persistMessages(messages);
// Store messages AND trigger onChatMessage
const { requestId, status } = await this.saveMessages(messages);saveMessages はメッセージ配列、または最新の永続化済み this.messages から次のメッセージリストを導く関数のいずれかを受け付けます。複数の呼び出しがキューされるときに古いベースラインを避けるため、関数形式を使います。
await this.saveMessages((messages) => [
...messages,
{
id: crypto.randomUUID(),
role: "user",
parts: [{ type: "text", text: "Summarize the latest data" }],
createdAt: new Date(),
},
]);await this.saveMessages((messages) => [
...messages,
{
id: crypto.randomUUID(),
role: "user",
parts: [{ type: "text", text: "Summarize the latest data" }],
createdAt: new Date(),
},
]);saveMessages は { requestId, status, error? } を返します。status はターンが実行された場合 "completed"、ストリームがエラーを報告した場合 "error"、開始前にチャットがクリアされた場合 "skipped"、外部の AbortSignal が完了前にキャンセルした場合 "aborted" です。status が "error" のとき、利用可能であれば error にストリームのエラーメッセージが含まれます。
プログラムによるターンをチャットエージェントの外からキャンセルするには options.signal を渡します。親のツール呼び出しが、内部生成のリクエスト ID を知らなくても子エージェントのターンをキャンセルする必要があるときに便利です。
const controller = new AbortController();
const result = await this.saveMessages(
(messages) => [...messages, syntheticUserMessage],
{ signal: controller.signal },
);
if (result.status === "aborted") {
// Partial chunks already streamed are persisted.
}const controller = new AbortController();
const result = await this.saveMessages(
(messages) => [...messages, syntheticUserMessage],
{ signal: controller.signal },
);
if (result.status === "aborted") {
// Partial chunks already streamed are persisted.
}continueLastTurn() は同じ options.signal 引数を受け付けます。AbortSignal オブジェクトは Durable Object の RPC 境界を越えられないため、saveMessages() または continueLastTurn() を呼ぶ Durable Object 内でコントローラーを構築します。シグナルはメモリ内だけです。Durable Object がターン途中でハイバネートすると、回復したターンは元のシグナルなしで動きます。キャンセルが再起動後も残る必要がある場合は、キャンセル意図を永続化します。
チャットターンがアシスタントメッセージを生成して永続化したあとに呼ばれます。このフックが走る前にターンロックは解放されるため、内部から saveMessages を呼んでも安全です。アシスタントメッセージを永続化するターン経路で発火します。WebSocket チャットリクエスト、saveMessages、自動継続です。アシスタントパートを生成する前にターンが失敗した場合、エラーは元のリクエスト経由で表面化します。
export class ChatAgent extends AIChatAgent {
async onChatResponse(result) {
if (result.status === "completed") {
console.log("Turn completed:", result.requestId);
}
if (result.status === "error") {
console.error("Turn failed:", result.error);
}
}
}import type { ChatResponseResult } from "@cloudflare/ai-chat";
export class ChatAgent extends AIChatAgent {
protected async onChatResponse(result: ChatResponseResult) {
if (result.status === "completed") {
console.log("Turn completed:", result.requestId);
}
if (result.status === "error") {
console.error("Turn failed:", result.error);
}
}
}ChatResponseResult には次が含まれます。
| フィールド | 型 | 説明 |
|---|---|---|
message |
UIMessage |
このターンの確定したアシスタントメッセージ |
requestId |
string |
このターンに関連するリクエスト ID |
continuation |
boolean |
このターンが前のアシスタントターンの継続かどうか |
status |
"completed" | "error" | "aborted" |
ターンの終了方法 |
error |
string | undefined |
status が "error" のときのエラーメッセージ |
メッセージがストレージへ永続化される前に、カスタム変換を適用するためにこのメソッドをオーバーライドします。このフックは組み込みのサニタイズ(OpenAI メタデータの除去、Anthropic のプロバイダー実行ツールペイロードの切り詰め、空の reasoning パートのフィルター)の あと に走ります。
export class ChatAgent extends AIChatAgent {
sanitizeMessageForPersistence(message) {
return {
...message,
parts: message.parts.map((part) => {
if (
"output" in part &&
typeof part.output === "string" &&
part.output.length > 1000
) {
return { ...part, output: "[redacted]" };
}
return part;
}),
};
}
}export class ChatAgent extends AIChatAgent {
protected sanitizeMessageForPersistence(message: UIMessage): UIMessage {
return {
...message,
parts: message.parts.map((part) => {
if (
"output" in part &&
typeof part.output === "string" &&
part.output.length > 1000
) {
return { ...part, output: "[redacted]" };
}
return part;
}),
};
}
}これらのメソッドは、プログラムによるターンの調整と、保留中のやり取りの待機に役立ちます。
アシスタントメッセージがクライアントツールの結果または承認を待っているとき true を返します。
if (this.hasPendingInteraction()) {
console.log("Waiting for user to approve or provide tool output");
}if (this.hasPendingInteraction()) {
console.log("Waiting for user to approve or provide tool output");
}会話が完全に安定するまで待ちます。アクティブなストリーム、保留中のクライアントツールやり取り、キュー済みの継続ターンがない状態です。安定したら true を返し、保留中のやり取りが解決する前にタイムアウトした場合は false を返します。
const stable = await this.waitUntilStable({ timeout: 30_000 });
if (stable) {
console.log("All turns complete, safe to proceed");
}const stable = await this.waitUntilStable({ timeout: 30_000 });
if (stable) {
console.log("All turns complete, safe to proceed");
}サーバー駆動のフローでは、saveMessages とあわせて特に便利です。
await this.saveMessages((messages) => [...messages, syntheticUserMessage]);
await this.waitUntilStable({ timeout: 60_000 });
// The assistant has finished respondingawait this.saveMessages((messages) => [...messages, syntheticUserMessage]);
await this.waitUntilStable({ timeout: 60_000 });
// The assistant has finished respondingアクティブなターンを中止し、キュー済みの継続を無効化します。組み込みの CF_AGENT_CHAT_CLEAR ハンドラーはこれを自動で呼びますが、必要なら手動でも呼べます。
カスタムロジックを追加するには onConnect と onClose をオーバーライドします。ストリーム再開とメッセージ同期は自動で扱われます。
export class ChatAgent extends AIChatAgent {
async onConnect(connection, ctx) {
// Your custom logic (e.g., logging, auth checks)
console.log("Client connected:", connection.id);
// Stream resumption and message sync are handled automatically
}
async onClose(connection, code, reason, wasClean) {
console.log("Client disconnected:", connection.id);
// Connection cleanup is handled automatically
}
}export class ChatAgent extends AIChatAgent {
async onConnect(connection, ctx) {
// Your custom logic (e.g., logging, auth checks)
console.log("Client connected:", connection.id);
// Stream resumption and message sync are handled automatically
}
async onClose(connection, code, reason, wasClean) {
console.log("Client disconnected:", connection.id);
// Connection cleanup is handled automatically
}
}destroy() メソッドは保留中のチャットリクエストをキャンセルし、ストリーム状態をクリーンアップします。Durable Object が退避されるときに自動で呼ばれますが、必要なら手動でも呼べます。
ユーザーがチャット UI で「stop」をクリックすると、クライアントは CF_AGENT_CHAT_REQUEST_CANCEL メッセージを送ります。サーバーはこれを options の abortSignal へ伝播します。
export class ChatAgent extends AIChatAgent {
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();
}
}export class ChatAgent extends AIChatAgent {
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();
}
}サブクラスは Durable Object 内からもターンをキャンセルできます。
protected abortRequest(requestId: string, reason?: unknown): void
protected abortAllRequests(): voidリクエスト ID が分かっているときは abortRequest() を使います。現在動いているターンをキャンセルすべき単一目的のヘルパーには abortAllRequests() を使います。プログラムによるターンで呼び出し元からシグナルを渡せる場合は、SaveMessagesOptions.signal を優先します。
自動ストリーム再開(useAgentChat の resume オプション)は クライアント再接続回復 です。クライアントが切断して再接続したときにアクティブなストリームを再開します。Durable Object の退避は対象外です。モデル呼び出しの最中に Worker プロセスまたは Durable Object が退避されると、ストリーム自体がなくなります。その場合を durable chat recovery が扱います。
ストリーム途中の Durable Object 退避は、LLM 接続を永続的に切断します。Durable recovery はすべての AIChatAgent と Think のチャットターンを runFiber() で包みます。fiber はストリーミング中の自動 keepAlive と、再起動時の回復フックを提供します。
fiber の行は退避後も SQLite に残ります。次のアクティベーションで、フレームワークは中断された fiber を検出します。バッファされたストリームチャンクから部分応答を再構築し、onChatRecovery を呼びます。
Durable recovery は常にオンです。回復予算と終了時の動作を調整するときにだけ chatRecovery を使います。
export class ChatAgent extends AIChatAgent {
chatRecovery = {
maxAttempts: 10,
stableTimeoutMs: 10_000,
terminalMessage: "The assistant was interrupted and could not recover.",
// Primary stuck-turn bound. Resets on every progress-bearing attempt, so a
// turn that keeps producing content survives unbounded interruption.
noProgressTimeoutMs: 5 * 60 * 1000,
// Runaway-loop guard. Defaults to 1,000. Set a higher value for a long
// agentic turn, or Infinity to remove the cap.
maxRecoveryWork: 200,
// Caller policy consulted from the second recovery attempt onward. Return
// false to stop recovery. This is where you enforce a token/cost budget.
// Note: this is called as `config.shouldKeepRecovering(ctx)`, so it is not
// bound to the agent instance — track spend in your own store keyed by the
// incident.
async shouldKeepRecovering(ctx) {
return (await getSpendForTurn(ctx.recoveryRootRequestId)) < MAX_SPEND;
},
async onExhausted(ctx) {
console.warn("Chat recovery exhausted", ctx.incidentId, ctx.reason);
},
};
}export class ChatAgent extends AIChatAgent {
override chatRecovery = {
maxAttempts: 10,
stableTimeoutMs: 10_000,
terminalMessage: "The assistant was interrupted and could not recover.",
// Primary stuck-turn bound. Resets on every progress-bearing attempt, so a
// turn that keeps producing content survives unbounded interruption.
noProgressTimeoutMs: 5 * 60 * 1000,
// Runaway-loop guard. Defaults to 1,000. Set a higher value for a long
// agentic turn, or Infinity to remove the cap.
maxRecoveryWork: 200,
// Caller policy consulted from the second recovery attempt onward. Return
// false to stop recovery. This is where you enforce a token/cost budget.
// Note: this is called as `config.shouldKeepRecovering(ctx)`, so it is not
// bound to the agent instance — track spend in your own store keyed by the
// incident.
async shouldKeepRecovering(ctx) {
return (await getSpendForTurn(ctx.recoveryRootRequestId)) < MAX_SPEND;
},
async onExhausted(ctx) {
console.warn("Chat recovery exhausted", ctx.incidentId, ctx.reason);
},
};
}chatRecovery オブジェクトは次の設定オプションを受け付けます。
| フィールド | デフォルト | 説明 |
|---|---|---|
maxAttempts |
10 |
終了枯渇前の試行上限です。前進があるとリセットされるため、健全な長いターンではなく、進捗なしの短いアラームループを捕捉します。 |
stableTimeoutMs |
10_000 |
回復試行が isolate の安定状態を待つ時間です。超えると再スケジュールします。 |
terminalMessage |
汎用メッセージ | 回復を諦めたときにユーザーへ表示するメッセージです。 |
noProgressTimeoutMs |
300_000(5 分) |
主なスタックターン上限です。インシデントが前進なしでこの時間を超えると封印されます(no_progress_timeout)。進捗のある試行ごとにリセット されるため、コンテンツを出し続けるターンは無制限の中断を生き抜けます。 |
maxRecoveryWork |
1,000 |
暴走ループのガードです。インシデント開始以降に生成された content/tool 単位の上限です。まだ進捗しているターンでも超えると封印されます。長いエージェントターンではより大きい値または Infinity を設定します。 |
maxOomRetries |
3 |
Durable Object のメモリ上限リセットに対するリトライ予算です。最初のメモリ上限リセットのあとに止めるには 0 を設定します。 |
shouldKeepRecovering |
— | 2 回目の回復試行以降に参照される呼び出し元ポリシーです。false を返すと回復を止めます。トークンまたはコスト予算の強制に使います。ctx.work は粗いセグメント数でありトークンではないため、実際の消費は自分で追跡します。 |
onExhausted |
— | 回復を諦めたときに、終了メッセージを届ける前に一度呼ばれます。理由は ctx.reason を確認します。 |
ChatRecoveryProgressContext(shouldKeepRecovering に渡される ctx)には次のフィールドがあります。
| フィールド | 型 | 説明 |
|---|---|---|
incidentId |
string |
この回復インシデントの安定 ID です。 |
requestId |
string |
現在の継続のリクエスト ID です(チェーンされた継続ごとに変わります)。 |
recoveryRootRequestId |
string |
継続チェーン全体の安定 ID です。インシデント単位の予算追跡に適したキーです。 |
attempt |
number |
このインシデントの試行番号です(このフックが走るときは 2 以上)。 |
maxAttempts |
number |
設定された試行上限です。 |
recoveryKind |
"retry" | "continue" |
未回答のユーザーターンをリトライするか、部分的なアシスタントターンを継続するかです。 |
work |
number |
インシデント開始以降に生成された content/tool セグメントの粗い単調カウントです(トークンではありません)。 |
ageMs |
number |
インシデントの最初の中断からの経過ミリ秒です。 |
進捗しているターンは、maxRecoveryWork 上限内である限り、繰り返しの中断を生き抜けます。回復は次のいずれかの ctx.reason 値で封印されます。
no_progress_timeout— 進捗なしウィンドウ内で前進がありません(スタックしたターン)。max_attempts_exceeded— 進捗なしの短いアラームループに試行上限を使い切りました。work_budget_exceeded— ターンはコンテンツを出し続けましたがmaxRecoveryWorkを超えました(暴走ループ)。recovery_aborted—shouldKeepRecoveringフックがfalseを返しました。out_of_memory— 回復がメモリ上限のリトライ予算を超えました。stable_timeout— 安定状態を待つ回復試行がタイムアウトし続け、予算が尽きました(極端な churn)。
ターンは、自力では解決できないクライアントやり取りで一時停止できます。サーバー execute のないクライアント側ツール呼び出し(結果をクライアントが再生する)、または approval-requested パートです。そのようなターンは人待ちであり、スタックではありません。
やり取りが保留中のあいだ、ターンはすべての回復予算から免除されます。進捗なしウィンドウ、試行上限、maxRecoveryWork、shouldKeepRecovering はすべて停止します。デプロイで中断されたプロンプトにユーザーが数分かけて答えても、封印は起きません。回復は失敗させずにターンを駐車し、ユーザーの最終的な承認またはツール結果が通常の継続経路で再開します。
この免除はクライアント専用です。飛行中に execute() が殺されたサーバーツールは真の orphan であり、免除されず、トランスクリプト修復で回復します。
終了枯渇は可観測性で監視します。
import { subscribe } from "agents/observability";
const unsubscribe = subscribe("chat", (event) => {
if (event.type === "chat:recovery:exhausted") {
console.error("Chat recovery exhausted", event.payload);
}
});import { subscribe } from "agents/observability";
const unsubscribe = subscribe("chat", (event) => {
if (event.type === "chat:recovery:exhausted") {
console.error("Chat recovery exhausted", event.payload);
}
});プロバイダー固有の回復を実装するにはオーバーライドします。デフォルトの動作は部分応答を永続化し、continueLastTurn() 経由で継続をスケジュールします。
export class ChatAgent extends AIChatAgent {
async onChatRecovery(ctx) {
console.log(`Recovered ${ctx.partialText.length} chars of partial text`);
// Default: persist partial + schedule continuation
return {};
}
}import type {
ChatRecoveryContext,
ChatRecoveryOptions,
} from "@cloudflare/ai-chat";
export class ChatAgent extends AIChatAgent {
override async onChatRecovery(
ctx: ChatRecoveryContext,
): Promise<ChatRecoveryOptions> {
console.log(`Recovered ${ctx.partialText.length} chars of partial text`);
// Default: persist partial + schedule continuation
return {};
}
}ChatRecoveryContext:
| フィールド | 型 | 説明 |
|---|---|---|
incidentId |
string |
この回復インシデントの安定 ID |
attempt |
number |
このインシデントの現在の試行番号です。1 から始まります |
maxAttempts |
number |
終了枯渇前の設定された試行上限 |
recoveryKind |
"retry" | "continue" |
未回答のユーザーターンをリトライするか、部分的なアシスタントターンを継続するか |
streamId |
string |
中断されたストリームの ID |
requestId |
string |
元のチャットリクエストの ID |
partialText |
string |
退避前に生成されたテキスト |
partialParts |
MessagePart[] |
退避前に生成されたメッセージパート(テキスト、reasoning、ツール呼び出し) |
recoveryData |
unknown | null |
this.stash() からのデータです。完全にユーザー制御です |
messages |
ChatMessage[] |
会話履歴全体 |
lastBody |
Record<string, unknown> | undefined |
元のリクエストボディ |
lastClientTools |
ClientToolSchema[] | undefined |
元のリクエストからのクライアントツールスキーマ |
createdAt |
number |
中断されたターンが始まったエポックミリ秒 |
ChatRecoveryOptions:
| フィールド | デフォルト | 説明 |
|---|---|---|
persist |
true |
部分応答をアシスタントメッセージとして保存します |
continue |
true |
continueLastTurn() 経由で継続をスケジュールします |
よく使う戻り値:
{}— 部分を永続化し、自動継続します(デフォルト。アシスタント prefill に対応するプロバイダーで動きます){ continue: false }— 部分を永続化しますが自動継続しません(継続は自分で扱います){ persist: false, continue: false }— 未確定の残りを永続化せず、すべて自分で扱います(たとえばプロバイダーから完了済み応答を取得する)
確定済みの作業は決して破棄されません。persist: false は、失う確定済みのものがない部分の永続化だけを抑制します。すでに確定済みのツール結果(完了した、しばしば非冪等な作業)を持つ部分は、設定に関係なく永続化されます。アプリが完了したツール呼び出しを誤って捨てることはなく、安全のために { persist: true } を付ける必要もありません。
ストリームチャンクが書かれる前に回復が起きると、継続する部分アシスタントメッセージはありません。最新の永続化メッセージがまだ中断ターンの未回答ユーザーメッセージなら、continue が false でない限り、フレームワークはそのターンを自動リトライします。
古い回復をスキップするには ctx.createdAt を使います。
override async onChatRecovery(
ctx: ChatRecoveryContext,
): Promise<ChatRecoveryOptions> {
if (Date.now() - ctx.createdAt > 2 * 60 * 1000) {
return { continue: false };
}
return {};
}自動継続が適切でないときでも、durable な記帳は有効のままです。
- 別のモデル呼び出しが安全でないときは
{ continue: false }を返します。 - キャンセル意図を永続化し、
onChatRecovery()で読みます。 - 外部副作用の前に冪等キーを記録します。
- durable な消費データと回復予算を使い、コストを制限します。
保存済みのリクエストボディで onChatMessage を再呼び出しし、最後のアシスタントメッセージに追記します。応答は継続としてストリーミングされます。既存のアシスタントメッセージへの追記であり、新しいメッセージではありません。合成ユーザーメッセージは作成されません。
protected continueLastTurn(
body?: Record<string, unknown>,
options?: SaveMessagesOptions,
): Promise<SaveMessagesResult>;デフォルトの回復経路から自動で呼ばれます。スケジュールされたコールバックや他のエントリポイントから手動で呼ぶこともできます。省略可能な body パラメーターは、この継続の保存済みリクエストボディを上書きします。実行中に継続をキャンセルするには options.signal を渡します。
回復用のプロバイダー固有データを永続化するには、onChatMessage 内で this.stash() を使います。stash は fiber の SQLite 行に保存され、エージェント状態とは別で、onChatRecovery では ctx.recoveryData として利用できます。
export class ChatAgent extends AIChatAgent {
async onChatMessage(_onFinish, options) {
const result = streamText({
model: openai("gpt-5.4"),
messages: await convertToModelMessages(this.messages),
providerOptions: { openai: { store: true } },
includeRawChunks: true,
onChunk: ({ chunk }) => {
if (chunk.type === "raw") {
const raw = chunk.rawValue;
if (raw?.type === "response.created" && raw.response?.id) {
this.stash({ responseId: raw.response.id });
}
}
},
});
return result.toUIMessageStreamResponse();
}
}export class ChatAgent extends AIChatAgent {
async onChatMessage(_onFinish, options) {
const result = streamText({
model: openai("gpt-5.4"),
messages: await convertToModelMessages(this.messages),
providerOptions: { openai: { store: true } },
includeRawChunks: true,
onChunk: ({ chunk }) => {
if (chunk.type === "raw") {
const raw = chunk.rawValue as {
type?: string;
response?: { id?: string };
};
if (raw?.type === "response.created" && raw.response?.id) {
this.stash({ responseId: raw.response.id });
}
}
},
});
return result.toUIMessageStreamResponse();
}
}適切な戦略は、プロバイダーがアシスタント prefill に対応しているかと、切断後に応答がサーバー側で続くかによって決まります。
| プロバイダー | 戦略 | トークンコスト |
|---|---|---|
| Workers AI | continueLastTurn() — アシスタント prefill でモデルが継続します |
低 |
| OpenAI(Responses API) | ID で完了済み応答を取得します — 無駄なトークンはありません | ゼロ |
| Anthropic | 部分を永続化し、継続のために合成ユーザーメッセージを送ります | 中 |
ターンが回復中のとき、エージェントは cf_agent_chat_recovering ステータスフレームをブロードキャストします。クライアントは固まって見える代わりに「recovering…」インジケーターを出せます。回復継続がスケジュールされたときにセットされ、すべての終了結果でクリアされるため、インジケーターが永遠に回り続けることはありません。useAgentChat の isRecovering フラグ経由で消費します(戻り値 を参照)。シグナルは助言的で後方互換です。理解しないクライアントは無視します。
トランスクリプト修復(orphan 化したツール呼び出しの修復。削除せずエラー結果として残すため記録が残り、モデルがツールを黙って再実行しません。プロバイダー呼び出し前に不正または欠落したツール入力を正規化します)は transcript 可観測性チャネルで発行されます。
チャット回復がより広い長時間エージェントの話にどう収まるかは、長時間エージェント: 中断された LLM ストリームの回復 を参照してください。基盤の fiber API は Durable Execution を参照してください。
WebSocket 経由で AIChatAgent に接続する React フックです。AI SDK の useChat をネイティブ WebSocket トランスポートで包みます。
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,
isStreaming,
isServerStreaming,
isToolContinuation,
isRecovering,
} = useAgentChat({ agent });
// ...
}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,
isStreaming,
isServerStreaming,
isToolContinuation,
isRecovering,
} = useAgentChat({ agent });
// ...
}| オプション | 型 | デフォルト | 説明 |
|---|---|---|---|
agent |
ReturnType<typeof useAgent> |
必須 | useAgent からのエージェント接続 |
onToolCall |
({ toolCall, addToolOutput }) => void |
— | クライアント側ツール実行を扱います |
tools |
Record<string, AITool> |
— | 高度: ブラウザーからクライアント実行ツールを動的に登録します |
autoContinueAfterToolResult |
boolean |
true |
クライアントツールの結果と承認のあと、会話を自動継続します |
resume |
boolean |
true |
再接続時の自動ストリーム再開を有効にします |
cancelOnClientAbort |
boolean |
false |
一般的なクライアントストリームの abort またはクリーンアップ時にサーバーテンをキャンセルします |
body |
object | () => object |
— | すべてのリクエストと一緒に送るカスタムデータ |
prepareSendMessagesRequest |
(options) => { body?, headers? } |
— | 高度なリクエスト単位のカスタマイズ |
getInitialMessages |
(options) => Promise<UIMessage[]> または null |
— | カスタムの初期メッセージローダーです。HTTP fetch を完全にスキップするには null にします(messages を直接渡すときに便利です) |
syncMessagesToServer |
boolean |
true |
true のとき、setMessages はトランスクリプトをサーバーへ送ります。サーバー権威のトランスクリプトストレージを持つホストでは false にし、setMessages がローカルビューだけを更新するようにします |
| プロパティ | 型 | 説明 |
|---|---|---|
messages |
UIMessage[] |
現在の会話メッセージ |
sendMessage |
(message) => void |
メッセージを送ります |
clearHistory |
() => void |
会話をクリアします(クライアントとサーバー) |
addToolOutput |
({ toolCallId, output }) => void |
クライアント側ツールの出力を提供します |
addToolApprovalResponse |
({ id, approved }) => void |
承認が必要なツールを承認または拒否します |
setMessages |
(messages | updater) => void |
メッセージを直接設定します(サーバーへ同期します) |
status |
string |
"ready"、"submitted"、"streaming"、または "error" |
isStreaming |
boolean |
エージェントがストリーミング中、またはアクティブなクライアントツール待ちのとき true |
isServerStreaming |
boolean |
サーバー開始のストリームまたはアクティブなクライアントツールフェーズ進行中のとき true |
isToolContinuation |
boolean |
ツール結果または承認のあとの自動継続が動いているとき true |
isRecovering |
boolean |
durable ターンが回復中(中断され再開中)のとき true です。isStreaming とは別です。回復中のターンはまだトークンを出していません。「recovering…」ヒントを描画します。ほとんどの UI は isStreaming || isRecovering を「busy」として扱います |
UI が新しいユーザー送信と、ツール結果のあとの継続を区別すべきときは isToolContinuation を使います。たとえば、タイピングインジケーターは status === "submitted" && !isToolContinuation のときだけ表示し、isStreaming が true のときは読み込みコントロールを無効のままにします。
useAgentChat は React 専用です。Vue、Svelte、またはバニラ JavaScript では、agents/chat/transport が WebSocketChatTransport をエクスポートし、AgentClient の WebSocket 接続を AI SDK のトランスポートインターフェイスに適応します。このエントリポイントに React の peer dependency は不要です。
import { useChat } from "@ai-sdk/vue";
import { AgentClient } from "agents/client";
import { WebSocketChatTransport } from "agents/chat/transport";
const agent = new AgentClient({
agent: "ChatAgent",
name: "user-123",
host: window.location.host,
});
const { messages, sendMessage, status } = useChat({
transport: new WebSocketChatTransport({
agent,
cancelOnClientAbort: true,
}),
});import { useChat } from "@ai-sdk/vue";
import { AgentClient } from "agents/client";
import { WebSocketChatTransport } from "agents/chat/transport";
const agent = new AgentClient({
agent: "ChatAgent",
name: "user-123",
host: window.location.host,
});
const { messages, sendMessage, status } = useChat({
transport: new WebSocketChatTransport({
agent,
cancelOnClientAbort: true,
}),
});トランスポートは新しいターン、再生成されたターン、ストリームキャンセルをカバーします。useAgentChat より低レベルのプリミティブです。永続化済み履歴の読み込み、再接続後の自動ストリーム再開、タブ横断のトランスクリプト同期、クライアント側ツール継続は React フックの責任のままです。クライアントが必要とするものをトランスポートの上に実装します。
vue-chat の例 ↗ は最小の Vue クライアントを示し、比較用の完全な React 統合は ai-chat ↗ を参照してください。
AIChatAgent は 3 つのツールパターンに対応し、いずれも AI SDK の tool() 関数を使います。
| パターン | 実行場所 | 使う場面 |
|---|---|---|
| サーバー側 | サーバー(自動) | API 呼び出し、データベースクエリ、計算 |
| クライアント側 | ブラウザー(onToolCall 経由) |
位置情報、クリップボード、カメラ、ローカルストレージ |
| 承認 | サーバー(ユーザー承認後) | 支払い、削除、外部アクション |
execute 関数を持つツールはサーバー上で自動実行されます。
import { streamText, convertToModelMessages, tool, stepCountIs } 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/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();
}
}import { streamText, convertToModelMessages, tool, stepCountIs } 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/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();
}
}サーバーで execute なしのツールを定義し、クライアントで onToolCall で扱います。ブラウザー API が必要なツールに使います。
サーバー:
tools: {
getLocation: tool({
description: "Get the user's location from the browser",
inputSchema: z.object({}),
// No execute — the client handles it
});
}tools: {
getLocation: tool({
description: "Get the user's location from the browser",
inputSchema: z.object({}),
// No execute — the client handles it
});
}クライアント:
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 },
});
}
},
});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 },
});
}
},
});LLM が getLocation を呼び出すと、ストリームは一時停止します。onToolCall コールバックが発火し、コードが出力を提供し、会話が続きます。
ブラウザーが実行時に利用可能なツールを決める SDK またはプラットフォームでは、useAgentChat に tools オブジェクトを渡します。クライアント側 execute 関数を持つツールはシリアライズされ、サーバーへ自動送信されます。サーバーでは、この動的ツールパターン向けに options.clientTools と createToolsFromClientSchemas() が引き続き対応しています。
実行前にユーザー確認が必要なツールには needsApproval を使います。
サーバー:
tools: {
processPayment: tool({
description: "Process a payment",
inputSchema: z.object({
amount: z.coerce.number(),
recipient: z.string(),
}),
needsApproval: async ({ amount }) => amount > 100,
execute: async ({ amount, recipient }) => charge(amount, recipient),
});
}クライアント:
import { getToolName, isToolUIPart } from "ai";
import {
getToolApproval,
getToolCallId,
getToolPartState,
} from "@cloudflare/ai-chat/react";
const { messages, addToolApprovalResponse } = useAgentChat({ agent });
// Render pending approvals from message parts
{
messages.map((msg) =>
msg.parts
.filter(
(part) =>
isToolUIPart(part) && getToolPartState(part) === "waiting-approval",
)
.map((part) => (
<div key={getToolCallId(part)}>
<p>Approve {getToolName(part)}?</p>
<button
onClick={() => {
const approval = getToolApproval(part);
if (!approval) return;
addToolApprovalResponse({
id: approval.id,
approved: true,
});
}}
>
Approve
</button>
<button
onClick={() => {
const approval = getToolApproval(part);
if (!approval) return;
addToolApprovalResponse({
id: approval.id,
approved: false,
});
}}
>
Reject
</button>
</div>
)),
);
}ユーザーがツールを拒否すると、addToolApprovalResponse({ id, approved: false }) はツール状態を汎用メッセージ付きの output-denied に設定します。拒否のより具体的な理由を LLM に伝えるには、代わりに state: "output-error" の addToolOutput を使います。
const { addToolOutput } = useAgentChat({ agent });
// Reject with a custom error message
addToolOutput({
toolCallId: part.toolCallId,
state: "output-error",
errorText: "User declined: insufficient budget for this quarter",
});const { addToolOutput } = useAgentChat({ agent });
// Reject with a custom error message
addToolOutput({
toolCallId: part.toolCallId,
state: "output-error",
errorText: "User declined: insufficient budget for this quarter",
});これはカスタムエラーテキスト付きの tool_result を LLM へ送り、適切に応答できるようにします(たとえば代替案の提案や確認質問)。
addToolApprovalResponse(approved: false)は、autoContinueAfterToolResult が有効なとき(デフォルト)会話を自動継続します。state: "output-error" の addToolOutput は自動継続し ません。LLM にエラーへ応答させたい場合は、あとで sendMessage() を呼びます。
その他のパターンは ヒューマンインザループ を参照してください。
すべてのチャットリクエストにカスタムデータを含めるには、body オプションを使います。
const { messages, sendMessage } = useAgentChat({
agent,
body: {
timezone: Intl.DateTimeFormat().resolvedOptions().timeZone,
userId: currentUser.id,
},
});const { messages, sendMessage } = useAgentChat({
agent,
body: {
timezone: Intl.DateTimeFormat().resolvedOptions().timeZone,
userId: currentUser.id,
},
});動的な値には関数を使います。
body: () => ({
token: getAuthToken(),
timestamp: Date.now(),
});body: () => ({
token: getAuthToken(),
timestamp: Date.now(),
});サーバーでこれらのフィールドにアクセスします。
export class ChatAgent extends AIChatAgent {
async onChatMessage(_onFinish, options) {
const { timezone, userId } = options?.body ?? {};
// ...
}
}export class ChatAgent extends AIChatAgent {
async onChatMessage(_onFinish, options) {
const { timezone, userId } = options?.body ?? {};
// ...
}
}高度なリクエスト単位のカスタマイズ(カスタムヘッダー、リクエストごとの異なる body)には prepareSendMessagesRequest を使います。
const { messages, sendMessage } = useAgentChat({
agent,
prepareSendMessagesRequest: async ({ messages, trigger }) => ({
headers: { Authorization: `Bearer ${await getToken()}` },
body: { requestedAt: Date.now() },
}),
});const { messages, sendMessage } = useAgentChat({
agent,
prepareSendMessagesRequest: async ({ messages, trigger }) => ({
headers: { Authorization: `Bearer ${await getToken()}` },
body: { requestedAt: Date.now() },
}),
});Data parts を使うと、テキストとあわせて型付き JSON をメッセージに付けられます。進捗インジケーター、ソース引用、トークン利用量、UI が必要とする任意の構造化データです。
サーバーから data parts を送るには、createUIMessageStream と writer.write() を使います。
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 });
}
}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 });
}
}| パターン | 方法 | 永続化? | 用途 |
|---|---|---|---|
| Reconciliation | 同じ type + id → その場で更新 |
はい | 段階的な状態(searching → found) |
| Append | id なし、または異なる id → 追記 |
はい | ログエントリ、複数の引用 |
| Transient | transient: true → message.parts に追加しない |
いいえ | 一時的なステータス(thinking インジケーター) |
Transient parts は接続中のクライアントへリアルタイムで配信されますが、SQLite 永続化と message.parts からは除外されます。消費するには onData コールバックを使います。
非 Transient の data parts は message.parts に現れます。型付けするには UIMessage generic を使います。
import { useAgentChat } from "@cloudflare/ai-chat/react";
const { messages } = useAgentChat({ 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[]
}
}
}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[]
}
}
}Transient の data parts は message.parts にありません。代わりに onData コールバックを使います。
const [thinking, setThinking] = useState(false);
const { messages } = useAgentChat({
agent,
onData(part) {
if (part.type === "data-thinking") {
setThinking(true);
}
},
});const [thinking, setThinking] = useState(false);
const { messages } = useAgentChat<unknown, ChatMessage>({
agent,
onData(part) {
if (part.type === "data-thinking") {
setThinking(true);
}
},
});サーバーでは、transient: true で transient parts を書き込みます。
writer.write({
transient: true,
type: "data-thinking",
data: { model: "glm-4.7-flash", startedAt: new Date().toISOString() },
});writer.write({
transient: true,
type: "data-thinking",
data: { model: "glm-4.7-flash", startedAt: new Date().toISOString() },
});onData はすべてのコードパスで発火します。新しいメッセージ、ストリーム再開、タブ間ブロードキャストです。
クライアントが切断して再接続すると、ストリームは自動的に再開します。設定は不要で、そのまま動作します。
ストリーミング中は次のように動きます。
- 生成されたチャンクはすべて SQLite にバッファされます
- クライアントが切断しても、サーバーはストリーミングとバッファを続けます
- クライアントが再接続すると、バッファ済みチャンクを受け取り、ライブストリーミングを再開します
汎用のクライアントストリーム abort やクリーンアップは、デフォルトではブラウザ内に留まります。サーバーターンは動き続け、あとから再開できます。明示的に stop() を呼ぶとサーバーターンはキャンセルされます。
const { messages, stop } = useAgentChat({ agent });
return <button onClick={stop}>Stop</button>;const { messages, stop } = useAgentChat({ agent });
return <button onClick={stop}>Stop</button>;ブラウザのライフサイクルにサーバーのライフサイクルを意図的に合わせたい場合(リクエスト寿命やトークン節約のフローなど)は、cancelOnClientAbort: true を設定します。明示的な stop() はこのオプションに関係なく、常にサーバー作業をキャンセルします。
無効化するには resume: false を使います。
const { messages } = useAgentChat({ agent, resume: false });const { messages } = useAgentChat({ agent, resume: false });Workers SQLite の行には 2 MB の上限があります。この上限を超えないよう、AIChatAgent はシリアライズ済みメッセージがおよそ 1.8 MB に達すると圧縮を始めます。たとえばツールが非常に大きな出力を返したときです。
- ツール出力の圧縮 — 大きなツール出力は、ツールの再実行を提案するようモデルへ指示する LLM 向け要約に置き換えられます
- テキストの切り詰め — ツール圧縮後もメッセージが大きすぎる場合、テキスト parts は注記付きで切り詰められます
圧縮されたメッセージには metadata.compactedToolOutputs が含まれます。クライアントはこれを検出して、適切に表示できます。
ストレージ(maxPersistedMessages)と LLM コンテキストは独立しています。
| 対象 | 制御 | 範囲 |
|---|---|---|
| SQLite が保存するメッセージ数 | maxPersistedMessages |
永続化 |
| モデルが見る内容 | pruneMessages() |
LLM コンテキスト |
| 行サイズ制限 | 自動圧縮 | メッセージごと |
export class ChatAgent extends AIChatAgent {
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();
}
}export class ChatAgent extends AIChatAgent {
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();
}
}AIChatAgent は、AI SDK 互換の任意のプロバイダーで動作します。使うモデルはサーバーコードが決めます。クライアント側で手動変更する必要はありません。
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),
});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),
});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),
});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),
});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),
});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),
});onChatMessage は streamText 呼び出しを完全に制御できるので、任意の AI SDK 機能を直接使えます。以下のパターンはそのまま動作します。特別な AIChatAgent 設定は不要です。
マルチステップのエージェントループで、ステップ間にモデル、利用可能なツール、システムプロンプトを変えるには prepareStep ↗ を使います。
import { streamText, convertToModelMessages, tool, stepCountIs } from "ai";
import { z } from "zod";
export class ChatAgent extends AIChatAgent {
async onChatMessage() {
const result = streamText({
model: cheapModel, // Default model for simple steps
messages: await convertToModelMessages(this.messages),
tools: {
search: searchTool,
analyze: analyzeTool,
summarize: summarizeTool,
},
stopWhen: stepCountIs(10),
prepareStep: async ({ stepNumber, messages }) => {
// Phase 1: Search (steps 0-2)
if (stepNumber <= 2) {
return {
activeTools: ["search"],
toolChoice: "required", // Force tool use
};
}
// Phase 2: Analyze with a stronger model (steps 3-5)
if (stepNumber <= 5) {
return {
model: expensiveModel,
activeTools: ["analyze"],
};
}
// Phase 3: Summarize
return { activeTools: ["summarize"] };
},
});
return result.toUIMessageStreamResponse();
}
}import { streamText, convertToModelMessages, tool, stepCountIs } from "ai";
import { z } from "zod";
export class ChatAgent extends AIChatAgent {
async onChatMessage() {
const result = streamText({
model: cheapModel, // Default model for simple steps
messages: await convertToModelMessages(this.messages),
tools: {
search: searchTool,
analyze: analyzeTool,
summarize: summarizeTool,
},
stopWhen: stepCountIs(10),
prepareStep: async ({ stepNumber, messages }) => {
// Phase 1: Search (steps 0-2)
if (stepNumber <= 2) {
return {
activeTools: ["search"],
toolChoice: "required", // Force tool use
};
}
// Phase 2: Analyze with a stronger model (steps 3-5)
if (stepNumber <= 5) {
return {
model: expensiveModel,
activeTools: ["analyze"],
};
}
// Phase 3: Summarize
return { activeTools: ["summarize"] };
},
});
return result.toUIMessageStreamResponse();
}
}prepareStep は各ステップの前に実行され、model、activeTools、toolChoice、system、messages の上書きを返せます。次の用途に使います。
- モデルの切り替え — 単純なステップは安いモデル、推論は上位モデル
- ツールの段階化 — 各ステップで使えるツールを制限する
- コンテキスト管理 — トークン上限内に収めるためメッセージを刈り込み、または変換する
- ツール呼び出しの強制 — 特定ツールを必須にするには
toolChoice: { type: "tool", toolName: "search" }を使う
チャットロジックを変えずにガードレール、RAG、キャッシュ、ログを追加するには wrapLanguageModel ↗ を使います。
import { streamText, convertToModelMessages, wrapLanguageModel } from "ai";
const guardrailMiddleware = {
wrapGenerate: async ({ doGenerate }) => {
const { text, ...rest } = await doGenerate();
// Filter PII or sensitive content from the response
const cleaned = text?.replace(/\b\d{3}-\d{2}-\d{4}\b/g, "[REDACTED]");
return { text: cleaned, ...rest };
},
};
export class ChatAgent extends AIChatAgent {
async onChatMessage() {
const model = wrapLanguageModel({
model: baseModel,
middleware: [guardrailMiddleware],
});
const result = streamText({
model,
messages: await convertToModelMessages(this.messages),
});
return result.toUIMessageStreamResponse();
}
}import { streamText, convertToModelMessages, wrapLanguageModel } from "ai";
import type { LanguageModelV3Middleware } from "@ai-sdk/provider";
const guardrailMiddleware: LanguageModelV3Middleware = {
wrapGenerate: async ({ doGenerate }) => {
const { text, ...rest } = await doGenerate();
// Filter PII or sensitive content from the response
const cleaned = text?.replace(/\b\d{3}-\d{2}-\d{4}\b/g, "[REDACTED]");
return { text: cleaned, ...rest };
},
};
export class ChatAgent extends AIChatAgent {
async onChatMessage() {
const model = wrapLanguageModel({
model: baseModel,
middleware: [guardrailMiddleware],
});
const result = streamText({
model,
messages: await convertToModelMessages(this.messages),
});
return result.toUIMessageStreamResponse();
}
}AI SDK には組み込みミドルウェアがあります。
extractReasoningMiddleware— DeepSeek R1 などのモデルから chain-of-thought を取り出すdefaultSettingsMiddleware— デフォルトの temperature、max tokens などを適用するsimulateStreamingMiddleware— 非ストリーミングモデルにストリーミングを足す
複数のミドルウェアは順番に合成されます。middleware: [first, second] は first(second(model)) として適用されます。
構造化データの抽出には、ツール内で generateObject ↗ を使います。
import {
streamText,
generateObject,
convertToModelMessages,
tool,
stepCountIs,
} from "ai";
import { z } from "zod";
export class ChatAgent extends AIChatAgent {
async onChatMessage() {
const result = streamText({
model: myModel,
messages: await convertToModelMessages(this.messages),
tools: {
extractContactInfo: tool({
description:
"Extract structured contact information from the conversation",
inputSchema: z.object({
text: z.string().describe("The text to extract contact info from"),
}),
execute: async ({ text }) => {
const { object } = await generateObject({
model: myModel,
schema: z.object({
name: z.string(),
email: z.string().email(),
phone: z.string().optional(),
}),
prompt: `Extract contact information from: ${text}`,
});
return object;
},
}),
},
stopWhen: stepCountIs(5),
});
return result.toUIMessageStreamResponse();
}
}import {
streamText,
generateObject,
convertToModelMessages,
tool,
stepCountIs,
} from "ai";
import { z } from "zod";
export class ChatAgent extends AIChatAgent {
async onChatMessage() {
const result = streamText({
model: myModel,
messages: await convertToModelMessages(this.messages),
tools: {
extractContactInfo: tool({
description:
"Extract structured contact information from the conversation",
inputSchema: z.object({
text: z.string().describe("The text to extract contact info from"),
}),
execute: async ({ text }) => {
const { object } = await generateObject({
model: myModel,
schema: z.object({
name: z.string(),
email: z.string().email(),
phone: z.string().optional(),
}),
prompt: `Extract contact information from: ${text}`,
});
return object;
},
}),
},
stopWhen: stepCountIs(5),
});
return result.toUIMessageStreamResponse();
}
}ツールは、独自のコンテキストを持つ焦点を絞ったサブ呼び出しに作業を委譲できます。再利用可能なエージェントを定義するには ToolLoopAgent ↗ を使い、ツールの execute から呼び出します。
import {
ToolLoopAgent,
streamText,
convertToModelMessages,
tool,
stepCountIs,
} from "ai";
import { z } from "zod";
// Define a reusable research agent with its own tools and instructions
const researchAgent = new ToolLoopAgent({
model: researchModel,
instructions: "You are a research assistant. Be thorough and cite sources.",
tools: { webSearch: webSearchTool },
stopWhen: stepCountIs(10),
});
export class ChatAgent extends AIChatAgent {
async onChatMessage() {
const result = streamText({
model: orchestratorModel,
messages: await convertToModelMessages(this.messages),
tools: {
deepResearch: tool({
description: "Research a topic in depth",
inputSchema: z.object({
topic: z.string().describe("The topic to research"),
}),
execute: async ({ topic }) => {
const { text } = await researchAgent.generate({
prompt: topic,
});
return { summary: text };
},
}),
},
stopWhen: stepCountIs(5),
});
return result.toUIMessageStreamResponse();
}
}import {
ToolLoopAgent,
streamText,
convertToModelMessages,
tool,
stepCountIs,
} from "ai";
import { z } from "zod";
// Define a reusable research agent with its own tools and instructions
const researchAgent = new ToolLoopAgent({
model: researchModel,
instructions: "You are a research assistant. Be thorough and cite sources.",
tools: { webSearch: webSearchTool },
stopWhen: stepCountIs(10),
});
export class ChatAgent extends AIChatAgent {
async onChatMessage() {
const result = streamText({
model: orchestratorModel,
messages: await convertToModelMessages(this.messages),
tools: {
deepResearch: tool({
description: "Research a topic in depth",
inputSchema: z.object({
topic: z.string().describe("The topic to research"),
}),
execute: async ({ topic }) => {
const { text } = await researchAgent.generate({
prompt: topic,
});
return { summary: text };
},
}),
},
stopWhen: stepCountIs(5),
});
return result.toUIMessageStreamResponse();
}
}リサーチエージェントは独自のコンテキストで動きます。トークン予算はオーケストレーターと別です。親モデルに戻るのは要約だけです。
デフォルトでは、execute が返るまでツール part は読み込み中として表示されます。ツールの作業中にクライアントへ進捗をストリーミングするには、非同期ジェネレーター(async function*)を使います。
deepResearch: tool({
description: "Research a topic in depth",
inputSchema: z.object({
topic: z.string().describe("The topic to research"),
}),
async *execute({ topic }) {
// Preliminary result — the client sees "searching" immediately
yield { status: "searching", topic, summary: undefined };
const { text } = await researchAgent.generate({ prompt: topic });
// Final result — sent to the model for its next step
yield { status: "done", topic, summary: text };
},
});deepResearch: tool({
description: "Research a topic in depth",
inputSchema: z.object({
topic: z.string().describe("The topic to research"),
}),
async *execute({ topic }) {
// Preliminary result — the client sees "searching" immediately
yield { status: "searching", topic, summary: undefined };
const { text } = await researchAgent.generate({ prompt: topic });
// Final result — sent to the model for its next step
yield { status: "done", topic, summary: text };
},
});各 yield はクライアント上のツール part をリアルタイムで更新します(preliminary: true)。最後に yield した値が、モデルが見る最終出力になります。
このパターンは次のときに役立ちます。
- 大量の情報探索が必要で、メインコンテキストを膨らませたくないとき
- 長時間ツールのリアルタイム進捗を見せたいとき
- 独立したリサーチを並列化したいとき(複数のツール呼び出しは同時に動きます)
- サブタスクごとに異なるモデルやシステムプロンプトが必要なとき
詳細は AI SDK Agents ドキュメント ↗、Subagents ↗、Preliminary Tool Results ↗ を参照してください。
複数のクライアントが同じエージェントインスタンスに接続すると、メッセージはすべての接続へ自動ブロードキャストされます。1 つのクライアントがメッセージを送ると、接続中の他のクライアントは更新されたメッセージ一覧を受け取ります。
Client A ──── sendMessage("Hello") ────▶ AIChatAgent
│
persist + stream
│
Client A ◀── CF_AGENT_USE_CHAT_RESPONSE ──────┤
Client B ◀── CF_AGENT_CHAT_MESSAGES ──────────┘発信元のクライアントはストリーミング応答を受け取ります。他のクライアントは CF_AGENT_CHAT_MESSAGES ブロードキャストで最終メッセージを受け取ります。
| インポートパス | エクスポート |
|---|---|
@cloudflare/ai-chat |
AIChatAgent、createToolsFromClientSchemas、ClientToolSchema、ChatRecoveryContext、ChatRecoveryOptions、ChatRecoveryConfig、ChatRecoveryExhaustedContext、ResolvedChatRecoveryConfig、ライフサイクル型 |
@cloudflare/ai-chat/react |
useAgentChat、extractClientToolSchemas、getToolPartState、getToolCallId、getToolInput、getToolOutput、getToolApproval |
@cloudflare/ai-chat/types |
MessageType、OutgoingMessage、IncomingMessage |
agents/chat |
SaveMessagesResult、SaveMessagesOptions、CHAT_MESSAGE_TYPES、ROW_MAX_BYTES、isReplayChunk() などの共有の高度なチャットプリミティブ |
agents/chat/transport |
非 React クライアント向けの WebSocketChatTransport とその AgentConnection 接続型 |
チャットプロトコルは、WebSocket 上の型付き JSON メッセージを使います。
| メッセージ | 方向 | 目的 |
|---|---|---|
CF_AGENT_USE_CHAT_REQUEST |
クライアント → サーバー | チャットメッセージを送る |
CF_AGENT_USE_CHAT_RESPONSE |
サーバー → クライアント | 応答チャンクをストリームする |
CF_AGENT_CHAT_MESSAGES |
サーバー → クライアント | 更新されたメッセージをブロードキャストする |
CF_AGENT_CHAT_CLEAR |
双方向 | 会話をクリアする |
CF_AGENT_CHAT_REQUEST_CANCEL |
クライアント → サーバー | 進行中のストリームをキャンセルする |
CF_AGENT_TOOL_RESULT |
クライアント → サーバー | ツール出力を提供する |
CF_AGENT_TOOL_APPROVAL |
クライアント → サーバー | ツールを承認または拒否する |
CF_AGENT_MESSAGE_UPDATED |
サーバー → クライアント | メッセージ更新を通知する |
CF_AGENT_STREAM_RESUMING |
サーバー → クライアント | ストリーム再開を通知する |
CF_AGENT_STREAM_RESUME_REQUEST |
クライアント → サーバー | ストリーム再開チェックを要求する |
CF_AGENT_STREAM_RESUME_ACK |
サーバー → クライアント | カーソルからストリームを再開する |
CF_AGENT_STREAM_RESUME_NONE |
サーバー → クライアント | 再開可能なストリームがない |
次の API は非推奨です。使用するとコンソール警告が出ます。将来のリリースで削除されます。
| 非推奨 | 置き換え | 備考 |
|---|---|---|
addToolResult({ toolCallId, result }) |
addToolOutput({ toolCallId, output }) |
AI SDK の用語に合わせた名前変更 |
detectToolsRequiringConfirmation() |
ツール定義の needsApproval を使う |
承認はグローバルフィルターではなく、ツール単位になりました |
toolsRequiringConfirmation オプション |
個別ツールの needsApproval を使う |
グローバル一覧の代わりにツール単位の承認 |
以前のバージョンからアップグレードする場合は、非推奨の呼び出しを置き換え先に変えます。非推奨 API はまだ動きますが、将来のメジャーバージョンで削除されます。
createToolsFromClientSchemas()、extractClientToolSchemas()、useAgentChat の tools オプションは、動的なクライアント側ツール向けに引き続きサポートされます。高度な API であり、非推奨 API ではありません。