永続メモリ、組み込みファイルツール、ストリーミングを備えたチャットエージェントを、手順どおりに構築します。
Cloudflare Agents が初めてなら、先に エージェントとは で中核の考え方を確認してください。そうでなければ、ここからそのまま進められます。
このチュートリアルの終わりには、次ができる Think エージェントがあります。
- React チャット UI へ応答をストリーミングする
- モデルが読み書きできる永続メモリを持つ
- ワークスペースのファイルツール(read、write、edit、find、grep、delete)を含む
- カスタムのサーバー側ツールに対応する
- Node.js 24+
- Workers AI にアクセスできる Cloudflare アカウント
- TypeScript と Cloudflare Workers の基礎知識
mkdir my-think-agent && cd my-think-agent
npm init -y依存関係をインストールします。
npm install @cloudflare/think @cloudflare/ai-chat agents ai @cloudflare/shell zod workers-ai-provider react react-dom
npm install -D wrangler @cloudflare/vite-plugin @cloudflare/workers-types @vitejs/plugin-react @tailwindcss/vite tailwindcss typescript vitewrangler.jsonc を作成します。
{
"name": "my-think-agent",
"compatibility_date": "2026-01-28",
"compatibility_flags": ["nodejs_compat"],
"ai": { "binding": "AI" },
"assets": {
"not_found_handling": "single-page-application",
"run_worker_first": ["/agents/*"]
},
"durable_objects": {
"bindings": [{ "class_name": "MyAgent", "name": "MyAgent" }]
},
"migrations": [{ "new_sqlite_classes": ["MyAgent"], "tag": "v1" }],
"main": "src/server.ts"
}name = "my-think-agent"
compatibility_date = "2026-01-28"
compatibility_flags = [ "nodejs_compat" ]
main = "src/server.ts"
[ai]
binding = "AI"
[assets]
not_found_handling = "single-page-application"
run_worker_first = [ "/agents/*" ]
[[durable_objects.bindings]]
class_name = "MyAgent"
name = "MyAgent"
[[migrations]]
new_sqlite_classes = [ "MyAgent" ]
tag = "v1"vite.config.ts を作成します。
import { cloudflare } from "@cloudflare/vite-plugin";
import tailwindcss from "@tailwindcss/vite";
import react from "@vitejs/plugin-react";
import { defineConfig } from "vite";
export default defineConfig({
plugins: [react(), cloudflare(), tailwindcss()],
});import { cloudflare } from "@cloudflare/vite-plugin";
import tailwindcss from "@tailwindcss/vite";
import react from "@vitejs/plugin-react";
import { defineConfig } from "vite";
export default defineConfig({
plugins: [react(), cloudflare(), tailwindcss()],
});tsconfig.json を作成します。
{
"extends": "agents/tsconfig"
}src/server.ts を作成します。
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",
);
}
getSystemPrompt() {
return "You are a helpful assistant with access to a workspace filesystem.";
}
}
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",
);
}
getSystemPrompt() {
return "You are a helpful assistant with access to a workspace filesystem.";
}
}
export default {
async fetch(request: Request, env: Env) {
return (
(await routeAgentRequest(request, env)) ||
new Response("Not found", { status: 404 })
);
},
} satisfies ExportedHandler<Env>;これで動くエージェントです。Think は次を自動で提供します。
- WebSocket チャットプロトコル(
useAgentChat互換) - SQLite へのメッセージ永続化
- 再開可能なストリーミング(ページ更新でバッファ済みチャンクを再生)
- ワークスペースのファイルツール(read、write、edit、list、find、grep、delete)
- 中止 / キャンセル対応
- 部分メッセージを残すエラー処理
src/client.tsx を作成します。
import { createRoot } from "react-dom/client";
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>
<h1>Think Agent</h1>
{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");
if (!input.value.trim()) return;
sendMessage({ text: input.value });
input.value = "";
}}
>
<input name="input" placeholder="Send a message..." />
<button type="submit">Send</button>
</form>
<p>Status: {status}</p>
</div>
);
}
const root = document.getElementById("root");
if (root) {
createRoot(root).render(<Chat />);
}import { createRoot } from "react-dom/client";
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>
<h1>Think Agent</h1>
{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;
if (!input.value.trim()) return;
sendMessage({ text: input.value });
input.value = "";
}}
>
<input name="input" placeholder="Send a message..." />
<button type="submit">Send</button>
</form>
<p>Status: {status}</p>
</div>
);
}
const root = document.getElementById("root");
if (root) {
createRoot(root).render(<Chat />);
}index.html を作成します。
<!doctype html>
<html lang="en">
<head>
<meta charset="UTF-8" />
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
<title>Think Agent</title>
</head>
<body>
<div id="root"></div>
<script type="module" src="/src/client.tsx"></script>
</body>
</html>npx vite devブラウザを開き、メッセージを送ります。エージェントはテキストをストリーミングで返し、ワークスペースのファイルツールはモデルから自動で使えます。
configureSession をオーバーライドし、再起動後も残る書き込み可能なメモリをモデルに渡します。
export class MyAgent extends Think {
getModel() {
return createWorkersAI({ binding: this.env.AI })(
"@cf/moonshotai/kimi-k2.6",
);
}
configureSession(session) {
return session
.withContext("soul", {
provider: {
get: async () =>
"You are a helpful assistant. Remember important facts about the user.",
},
})
.withContext("memory", {
description: "Important facts about the user and conversation.",
maxTokens: 2000,
})
.withCachedPrompt();
}
}export class MyAgent extends Think<Env> {
getModel(): LanguageModel {
return createWorkersAI({ binding: this.env.AI })(
"@cf/moonshotai/kimi-k2.6",
);
}
configureSession(session: Session) {
return session
.withContext("soul", {
provider: {
get: async () =>
"You are a helpful assistant. Remember important facts about the user.",
},
})
.withContext("memory", {
description: "Important facts about the user and conversation.",
maxTokens: 2000,
})
.withCachedPrompt();
}
}これでモデルはシステムプロンプトに MEMORY セクションを見ます。更新には set_context ツールを使います。メモリに書いた事実は SQLite に残り、Durable Object のハイバネーションと再起動を越えます。
configureSession を使うと、システムプロンプトは getSystemPrompt() ではなくコンテキストブロックから組み立てます。上の "soul" ブロックがシステムアイデンティティです。読み取り専用で、常に先頭に出ます。"memory" ブロックは書き込み可能で、役立つ情報を得るとモデルが先回りして更新します。
コンテキストブロック、コンパクション、検索、スキル、複数セッションは Sessions のドキュメント を参照してください。
getTools() をオーバーライドし、組み込みのワークスペースツールと並べて独自ツールを追加します。
import { tool } from "ai";
import { z } from "zod";
export class MyAgent extends Think {
getModel() {
/* ... */
}
configureSession(session) {
/* ... */
}
getTools() {
return {
getWeather: tool({
description: "Get the current weather for a city",
inputSchema: z.object({
city: z.string().describe("City name"),
}),
execute: async ({ city }) => {
const res = await fetch(
`https://api.weatherapi.com/v1/current.json?key=${this.env.WEATHER_KEY}&q=${city}`,
);
return res.json();
},
}),
};
}
}import { tool } from "ai";
import { z } from "zod";
export class MyAgent extends Think<Env> {
getModel(): LanguageModel {
/* ... */
}
configureSession(session: Session) {
/* ... */
}
getTools(): ToolSet {
return {
getWeather: tool({
description: "Get the current weather for a city",
inputSchema: z.object({
city: z.string().describe("City name"),
}),
execute: async ({ city }) => {
const res = await fetch(
`https://api.weatherapi.com/v1/current.json?key=${this.env.WEATHER_KEY}&q=${city}`,
);
return res.json();
},
}),
};
}
}Think は複数ソースのツールを自動でマージします。各ターンで、モデルは次にアクセスできます。
- ワークスペースツール — read、write、edit、list、find、grep、delete、bash(組み込み)
- 独自ツール —
getTools()から - 拡張ツール — 読み込んだ拡張から
- セッションツール — set_context、load_context、search_context(
configureSessionから) - スキルツール — activate_skill、read_skill_resource、任意の run_skill_script(
getSkills()から) - MCP ツール — 接続中の MCP サーバーから(ある場合)
- クライアントツール — ブラウザから(ある場合)
Think は、エントリパスに関係なく、各ターンで発火するフックを提供します。
export class MyAgent extends Think {
getModel() {
/* ... */
}
beforeTurn(ctx) {
console.log(
`Turn starting: ${Object.keys(ctx.tools).length} tools available`,
);
}
onChatResponse(result) {
console.log(`Turn ${result.status}: ${result.message.parts.length} parts`);
}
}import type {
TurnContext,
TurnConfig,
ChatResponseResult,
} from "@cloudflare/think";
export class MyAgent extends Think<Env> {
getModel(): LanguageModel {
/* ... */
}
beforeTurn(ctx: TurnContext): TurnConfig | void {
console.log(
`Turn starting: ${Object.keys(ctx.tools).length} tools available`,
);
}
onChatResponse(result: ChatResponseResult) {
console.log(`Turn ${result.status}: ${result.message.parts.length} parts`);
}
}全体のリファレンスは ライフサイクルフック を参照してください。
- ライフサイクルフック — モデルの振る舞いを制御し、ターンごとにモデルを切り替え、ツールを制限します
- ツール — ワークスペースツール、コード実行、拡張
- クライアントツール — ブラウザ側ツール、承認フロー、同時実行
- サブエージェント RPC とプログラムターン — RPC ストリーミング、スケジュールターン、復旧
- Sessions — コンテキストブロック、コンパクション、検索、複数セッション