@cloudflare/think では、1 つの基底クラスを継承するだけで、状態を持つ AI チャットエージェントを構築できます。返信をストリーミングし、会話を覚え、ツールを呼び出します。getModel() でモデルを渡すと、Think がチャットライフサイクルの残りを接続します。エージェントループ(モデルがツールを呼び、結果を読み、答えが出るまで続ける)、メッセージ永続化、ストリーミング、クライアントツール、ストリーム再開、拡張機能です。すべては Durable Object の SQLite が支えます。
Think は トップレベルエージェント(useAgentChat 経由でブラウザークライアントと WebSocket チャット)と サブエージェント(別のエージェントが chat() で RPC 駆動する子エージェント)の両方として使えます。
npm i @cloudflare/think @cloudflare/ai-chat agents ai @cloudflare/shell zod workers-ai-provideryarn add @cloudflare/think @cloudflare/ai-chat agents ai @cloudflare/shell zod workers-ai-providerpnpm add @cloudflare/think @cloudflare/ai-chat agents ai @cloudflare/shell zod workers-ai-providerbun add @cloudflare/think @cloudflare/ai-chat agents ai @cloudflare/shell zod workers-ai-providerThink は AI SDK v6 と v7 に対応しています。ai@^6 は @ai-sdk/react@^3 と、ai@^7 は @ai-sdk/react@^4 と組み合わせます。プロジェクト全体で、AI SDK パッケージのメジャーバージョンを揃えてください。
import { Think } from "@cloudflare/think";
import { createWorkersAI } from "workers-ai-provider";
import { routeAgentRequest } from "agents";
export class MyAgent extends Think {
getModel() {
return createWorkersAI({ binding: this.env.AI })(
"@cf/moonshotai/kimi-k2.6",
);
}
}
export default {
async fetch(request, env) {
return (
(await routeAgentRequest(request, env)) ||
new Response("Not found", { status: 404 })
);
},
};import { Think } from "@cloudflare/think";
import { createWorkersAI } from "workers-ai-provider";
import { routeAgentRequest } from "agents";
export class MyAgent extends Think<Env> {
getModel() {
return createWorkersAI({ binding: this.env.AI })(
"@cf/moonshotai/kimi-k2.6",
);
}
}
export default {
async fetch(request: Request, env: Env) {
return (
(await routeAgentRequest(request, env)) ||
new Response("Not found", { status: 404 })
);
},
} satisfies ExportedHandler<Env>;これで完了です。Think が WebSocket チャットプロトコル、メッセージ永続化、エージェントループ、メッセージのサニタイズ、ストリーム再開、クライアントツール対応、ワークスペースファイルツールを処理します。
import { useAgent } from "agents/react";
import { useAgentChat } from "@cloudflare/ai-chat/react";
function Chat() {
const agent = useAgent({ agent: "MyAgent" });
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="Send a message..." />
<button type="submit">Send</button>
</form>
</div>
);
}import { useAgent } from "agents/react";
import { useAgentChat } from "@cloudflare/ai-chat/react";
function Chat() {
const agent = useAgent({ agent: "MyAgent" });
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="Send a message..." />
<button type="submit">Send</button>
</form>
</div>
);
}{
"$schema": "./node_modules/wrangler/config-schema.json",
// Set this to today's date
"compatibility_date": "2026-09-20",
"compatibility_flags": [
"nodejs_compat"
],
"ai": {
"binding": "AI"
},
"durable_objects": {
"bindings": [
{
"class_name": "MyAgent",
"name": "MyAgent"
}
]
},
"migrations": [
{
"new_sqlite_classes": [
"MyAgent"
],
"tag": "v1"
}
]
}# Set this to today's date
compatibility_date = "2026-09-20"
compatibility_flags = ["nodejs_compat"]
[ai]
binding = "AI"
[[durable_objects.bindings]]
class_name = "MyAgent"
name = "MyAgent"
[[migrations]]
new_sqlite_classes = ["MyAgent"]
tag = "v1"Think は内部で wrapAISDK() を使い、モデルターン、ツール呼び出し、承認ライフサイクルの区間を計測します。AI SDK をラップしたり、アダプターを設定したりする必要はありません。invoke_agent、chat、execute_tool、tool_approval のスパンを Workers Observability へ送るには、Workers トレースを有効にします。
{
"$schema": "./node_modules/wrangler/config-schema.json",
"observability": {
"traces": {
"enabled": true
}
}
}[observability.traces]
enabled = trueトレースは Cloudflare ダッシュボードの Agents ビューに表示されます。会話とトレースのタイムラインを確認できます。トレーシングをオフにするには、observability.traces.enabled を false にします。Think のエージェントクラスを変更する必要はありません。
スパン属性、ペイロード制御、トレースのエクスポート、AI SDK v6 または v7 の直接セットアップは トレーシング を参照してください。
Think と AIChatAgent はどちらも Agent を継承し、同じ cf_agent_chat_* WebSocket プロトコルを話します。目的は異なります。
AIChatAgent はプロトコルアダプターです。onChatMessage をオーバーライドし、streamText の呼び出し、ツールの接続、メッセージ変換、Response の返却は自分で担当します。AIChatAgent が処理するのは配管です。メッセージ永続化、ストリーミング、中止、再開です。LLM 呼び出しそのものはすべて呼び出し側の責任です。
Think は方針の決まったフレームワークです。判断は枠組み側が行います。getModel() がモデルを返し、getSystemPrompt() または configureSession() がプロンプトを設定し、getTools() がツールを返します。デフォルトの onChatMessage が完全なエージェントループを実行します。パイプライン全体ではなく、個別の部品をオーバーライドします。
| 観点 | AIChatAgent | Think |
|---|---|---|
| 最小のサブクラス | 約 15 行(streamText + ツール + システムプロンプト + 応答) |
3 行(getModel() のみ) |
| ストレージ | 平坦な SQL テーブル | Session: ツリー構造のメッセージ、コンテキストブロック、コンパクション、FTS5 |
| 再生成 | 破壊的(古い応答を削除) | 非破壊的な分岐(古い応答を保持) |
| コンテキスト管理 | 手動 | LLM が読み書きできる永続メモリ付きコンテキストブロック |
| サブエージェント RPC | 組み込みなし | StreamCallback 付き chat() |
| プログラム起点のターン | saveMessages() |
saveMessages()、submitMessages()、continueLastTurn() |
| コンパクション | maxPersistedMessages(最も古いものを削除) |
オーバーレイによる非破壊的な要約 |
| 検索 | なし | セッション内およびセッション横断の FTS5 全文検索 |
- LLM 呼び出しを完全に制御したい(RAG、複数モデル、独自ストリーミング)
- HTTP ミドルウェアやテスト向けに
Response戻り値が欲しい - メモリ要件のないシンプルなチャットボットを作る
- 早く出荷したい(すべて接続済みの 3 行サブクラス)
- 永続メモリが必要(モデルが読み書きできるコンテキストブロック)
- 長い会話が必要(非破壊的なコンパクション)
- 会話検索が必要(FTS5)
- サブエージェントシステムを作る(ストリーミング付きの親子 RPC)
- 能動的なエージェントが必要(スケジュールタスクや webhook からのプログラム起点ターン)
- webhook や RPC 呼び出し元向けの、耐久的な非同期サブミッションが必要
Think には、ターンを開始または継続する方法がいくつかあります。どれも 1 つの公開エントリポイント runTurn(options) に集まります。古いメソッドは便利なショートカットとして残っています。
runTurn() は、統一されたターン受付 API です。1 つのメソッド、3 つのモードで、options.mode で選びます。
| モード | 使うとき | 戻り値 | ショートカット先 |
|---|---|---|---|
"wait"(デフォルト) |
呼び出し側が、モデル応答の完了までブロックできる | Promise<TurnResult> |
saveMessages() |
"submit" |
素早い耐久的な受付と、あとからの状態確認が必要 | Promise<SubmitMessagesResult> |
submitMessages() |
"stream" |
応答をコールバックへストリーミングしたい(RPC) | Promise<void> |
chat() |
input は文字列、UIMessage、メッセージ配列、または wait と stream モードでは受付時に評価される関数 (current) => UIMessage[] を受け付けます(submit は関数入力を受け付けません)。
export class Assistant extends Think {
async examples(inboundEventId) {
// wait — block for the result
const result = await this.runTurn({ input: "Summarize the latest thread" });
if (result.status === "completed") {
// result.message is the assistant message; result.continuation is false
}
// submit — durable acceptance, check status later
const submission = await this.runTurn({
mode: "submit",
input: "Process this webhook",
idempotencyKey: inboundEventId, // dedupe; safe to retry
});
// submission.accepted is true on first accept; submission.status is "pending"
// stream — drive a callback (the same surface as chat())
await this.runTurn({
mode: "stream",
input: "Stream me",
callback: {
onStart({ requestId }) {},
onEvent(json) {}, // UIMessageChunk JSON
onDone() {},
onError(error) {},
},
});
// continuation — continue the last assistant turn instead of sending input
await this.runTurn({ continuation: true });
}
}export class Assistant extends Think<Env> {
async examples(inboundEventId: string) {
// wait — block for the result
const result = await this.runTurn({ input: "Summarize the latest thread" });
if (result.status === "completed") {
// result.message is the assistant message; result.continuation is false
}
// submit — durable acceptance, check status later
const submission = await this.runTurn({
mode: "submit",
input: "Process this webhook",
idempotencyKey: inboundEventId, // dedupe; safe to retry
});
// submission.accepted is true on first accept; submission.status is "pending"
// stream — drive a callback (the same surface as chat())
await this.runTurn({
mode: "stream",
input: "Stream me",
callback: {
onStart({ requestId }) {},
onEvent(json) {}, // UIMessageChunk JSON
onDone() {},
onError(error) {},
},
});
// continuation — continue the last assistant turn instead of sending input
await this.runTurn({ continuation: true });
}
}主な動作:
- ブロッキングモードは入れ子にできません。 稼働中のターンの 内側(例: ツールの
execute)からwait/stream/continuation(または同等のショートカット)を呼ぶと例外になります。ターンキューがデッドロックするためです。ターン内からはrunTurn({ mode: "submit" })(耐久的で、現在のターンがキューを空けたあとに実行)か、addMessages()(トランスクリプトのみ、推論なし)を使います。 submitはべき等です。submissionIdやidempotencyKeyを渡します。既知のキーを再提出すると、2 つ目のターンは始まらず、既存レコードがaccepted: falseで返ります。プログラム起点のサブミッション を参照してください。- 復旧に安全です。
wait、stream、排出済みのsubmit経路は、復旧ファイバー内で推論を実行します。中断したターンは退避後に再開します。
runTurn は、オプションと結果の型と一緒にエクスポートされます。RunTurnOptions、RunTurnWait、RunTurnSubmit、RunTurnStream、TurnInputMessages、TurnResult です。
次の表は、各シナリオを最も直接的な呼び出しに対応づけます。各ショートカットのシグネチャは変わりません。狭い面だけ欲しいときはショートカットを、1 つのメンタルモデルで揃えたいときは runTurn() を使います。
| 用途 | API |
|---|---|
| ブラウザーユーザーがチャットメッセージを送る | WebSocket チャットプロトコル上の useAgentChat |
| サーバーコードがモデル応答を待てる | saveMessages() |
| サーバーコードが素早い耐久的な受付と、あとからの状態確認を必要とする | submitMessages() |
| 繰り返しのプロンプト駆動ターンやハンドラーを作る | getScheduledTasks() |
| 親コードが特定の子へ直接ストリーミング RPC する | subAgent(...).chat() |
| 親が保持する子エージェントへ作業を委譲する | agentTool() または runAgentTool() |
| ターンをアプリ所有のべき等な副作用で囲む | startFiber() |
| 複数ステップの耐久オーケストレーションを調整する | Workflows |
| モデルターンを始めずにコンテキストやメッセージを追加する | addMessages() |
| 高度なサブクラスや復旧コードがアシスタントターンを継続する | continueLastTurn() |
呼び出し側がトリガーを所有し、ターン完了を待てる場合は saveMessages() を使います。タイムアウトの曖昧さで再試行が危険になる場合は submitMessages() を使います。
モデルターンを始めずにトランスクリプトへ書き込むには addMessages() を使います。過去の履歴の取り込みや、次のターンが見るべき背景コンテキストの注入に使います。
export class Assistant extends Think {
async importContext() {
await this.addMessages([
{
id: crypto.randomUUID(),
role: "user",
parts: [{ type: "text", text: "Imported context" }],
},
]);
}
}export class Assistant extends Think<Env> {
async importContext() {
await this.addMessages([
{
id: crypto.randomUUID(),
role: "user",
parts: [{ type: "text", text: "Imported context" }],
},
]);
}
}addMessages() は Session ツリーへ追加(または upsert)します。
- 推論は実行せず、ターンキューにも入りません。ツールの
execute内から呼んでもデッドロックしません。 - 配列の要素は直線的に追加されます(それぞれ直前の要素の下に付きます)。取り込んだ履歴は 1 本のパスのままです。デフォルトでは最初のメッセージが、最新のコミット済みリーフに付きます。別の場所へ付けるには
parentIdを渡し、ルートメッセージにするにはnullを渡します。 - 追加は メッセージ id でべき等 です。既存メッセージをその場で更新するには
{ mode: "upsert" }を渡します。
推奨パターンは「コンテキストを追加してからターンを実行」です。addMessages() のあと runTurn() を呼びます。
転送、キャンセル、再生ポリシーを自分のコードが持つ、低レベルの親子ストリーミングには chat() を使います。親モデルや workflow が子エージェントへ委譲し、保持された子の実行、イベント再生、中止の橋渡し、UI ドリルインが欲しい場合は ツールとしての Agents を使います。
ターン周辺のアプリケーションジョブが耐久単位である場合(webhook を一度だけ受け付ける、シリアライズされたチャネルやスレッド先を復元する、見える返信を投稿する、アプリレベルの復旧ポリシーを記録する)は、Think の外で startFiber() を使います。Think のサブミッションは会話受付とターンの直列化を担当し、managed fiber は外部ジョブの受付、べき等な副作用、アプリケーション復旧を担当します。
はじめに
設定
ツール
アクション
チャネル
ライフサイクルフック
クライアントツール
メッセンジャー
スケジュールタスク
Workflows
サブエージェント RPC
プログラム起点のサブミッション
耐久復旧
Agent Skills
Think の設計は Pi ↗ に着想を得ています。
Assistant の例
- Sessions — コンテキストブロック、コンパクション、検索、複数セッション(Think が土台にするストレージ層)
- サブエージェント —
subAgent()、abortSubAgent()、deleteSubAgent()(子を生成する基底 Agent メソッド) - チャットエージェント — LLM 呼び出しを完全に制御したいときの
AIChatAgent - 長時間稼働エージェント — 数週間にわたる寿命向けのサブエージェント委譲パターン
- 耐久実行 —
runFiber()とクラッシュ復旧(chatRecoveryが使用) - ウェブを閲覧する — CDP ヘルパー API の全体リファレンス