永続化し、判断し、行動する AI エージェントを構築します。エージェントは Cloudflare のグローバルネットワーク上で動き、リクエストをまたいで状態を保ち、WebSocket 経由でクライアントとリアルタイムに接続します。
作るもの: 永続状態を持つカウンターエージェント。React フロントエンドとリアルタイムで同期します。
所要時間: 約 10 分
npm create cloudflare@latest -- --template cloudflare/agents-starteryarn create cloudflare --template cloudflare/agents-starterpnpm create cloudflare@latest --template cloudflare/agents-starter続けて依存関係をインストールし、開発サーバーを起動します。
cd agents-starter
npm install
npm run dev次の構成のプロジェクトが作成されます。
src/server.ts— エージェントのコードsrc/client.tsx— React フロントエンドwrangler.jsonc— Cloudflare の設定tsconfig.json— デコレーターとモジュール設定のためagents/tsconfigを継承しますvite.config.ts— デコレーター対応のためagents/viteプラグインを含みます
スターターテンプレートには、重要な SDK 統合が 2 つ入っています。手動でプロジェクトを用意する場合は、両方を追加します。
tsconfig.json — agents/tsconfig を継承します。target: "ES2021" とその他の推奨オプションが設定されます。
{
"extends": "agents/tsconfig"
}vite.config.ts — agents() プラグインを含めます。TC39 デコレーター変換を処理します(Vite 8 で @callable() を使うために必要です)。
import { cloudflare } from "@cloudflare/vite-plugin";
import react from "@vitejs/plugin-react";
import agents from "agents/vite";
import { defineConfig } from "vite";
export default defineConfig({
plugins: [agents(), react(), cloudflare()],
});http://localhost:5173 ↗ を開き、エージェントの動作を確認します。
シンプルなカウンターエージェントを一から作ります。src/server.ts を置き換えます。
import { Agent, routeAgentRequest, callable } from "agents";
// Define the state shape
// Create the agent
export class CounterAgent extends Agent {
// Initial state for new instances
initialState = { count: 0 };
// Methods marked with @callable can be called from the client
@callable()
increment() {
this.setState({ count: this.state.count + 1 });
return this.state.count;
}
@callable()
decrement() {
this.setState({ count: this.state.count - 1 });
return this.state.count;
}
@callable()
reset() {
this.setState({ count: 0 });
}
}
// Route requests to agents
export default {
async fetch(request, env, ctx) {
return (
(await routeAgentRequest(request, env)) ??
new Response("Not found", { status: 404 })
);
},
};import { Agent, routeAgentRequest, callable } from "agents";
// Define the state shape
export type CounterState = {
count: number;
};
// Create the agent
export class CounterAgent extends Agent<Env, CounterState> {
// Initial state for new instances
initialState: CounterState = { count: 0 };
// Methods marked with @callable can be called from the client
@callable()
increment() {
this.setState({ count: this.state.count + 1 });
return this.state.count;
}
@callable()
decrement() {
this.setState({ count: this.state.count - 1 });
return this.state.count;
}
@callable()
reset() {
this.setState({ count: 0 });
}
}
// Route requests to agents
export default {
async fetch(request: Request, env: Env, ctx: ExecutionContext) {
return (
(await routeAgentRequest(request, env)) ??
new Response("Not found", { status: 404 })
);
},
} satisfies ExportedHandler<Env>;エージェントを登録するため、wrangler.jsonc を更新します。
{
"name": "my-agent",
"main": "src/server.ts",
// Set this to today's date
"compatibility_date": "2026-09-20",
"compatibility_flags": ["nodejs_compat"],
"durable_objects": {
"bindings": [
{
"name": "CounterAgent",
"class_name": "CounterAgent",
},
],
},
"migrations": [
{
"tag": "v1",
"new_sqlite_classes": ["CounterAgent"],
},
],
}name = "my-agent"
main = "src/server.ts"
# Set this to today's date
compatibility_date = "2026-09-20"
compatibility_flags = [ "nodejs_compat" ]
[[durable_objects.bindings]]
name = "CounterAgent"
class_name = "CounterAgent"
[[migrations]]
tag = "v1"
new_sqlite_classes = [ "CounterAgent" ]ポイント:
- バインディングの
nameがenv上のプロパティになります(例:env.CounterAgent) class_nameは、エクスポートしたクラス名と完全に一致させる必要がありますnew_sqlite_classesで、状態永続化用の SQLite ストレージが有効になります- agents パッケージには
nodejs_compatフラグが必要です
src/client.tsx を置き換えます。
import "./styles.css";
import { createRoot } from "react-dom/client";
import { useState } from "react";
import { useAgent } from "agents/react";
import type { CounterAgent, CounterState } from "./server";
export default function App() {
const [count, setCount] = useState(0);
// Connect to the Counter agent
const agent = useAgent<CounterAgent, CounterState>({
agent: "CounterAgent",
onStateUpdate: (state) => setCount(state.count),
});
return (
<div style={{ padding: "2rem", fontFamily: "system-ui" }}>
<h1>Counter Agent</h1>
<p style={{ fontSize: "3rem" }}>{count}</p>
<div style={{ display: "flex", gap: "1rem" }}>
<button onClick={() => agent.stub.decrement()}>-</button>
<button onClick={() => agent.stub.reset()}>Reset</button>
<button onClick={() => agent.stub.increment()}>+</button>
</div>
</div>
);
}
const root = createRoot(document.getElementById("root")!);
root.render(<App />);ポイント:
useAgentは WebSocket 経由でエージェントに接続しますonStateUpdateは、エージェントの状態が変わるたびに発火しますagent.stub.methodName()は、エージェント上の@callable()付きメソッドを呼び出します
ボタンをクリックすると、次の流れで状態が更新されます。
- クライアント が WebSocket 経由で
agent.stub.increment()を呼びました - Agent が
increment()を実行し、setState()で状態を更新しました - 状態 は SQLite に自動で永続化されました
- ブロードキャスト が接続中のすべてのクライアントへ送られました
- React が
onStateUpdate経由で更新されました
flowchart LR
A["ブラウザー<br/>(React)"] <-->|WebSocket| B["Agent<br/>(Counter)"]
B --> C["SQLite<br/>(状態)"]
| 概念 | 意味 |
|---|---|
| Agent インスタンス | 一意の名前ごとにエージェントが作られます。CounterAgent:user-123 は CounterAgent:user-456 とは別です |
| 永続状態 | 状態は再起動、デプロイ、ハイバネーションを生き延びます。SQLite に保存されます |
| リアルタイム同期 | 同じエージェントに接続しているすべてのクライアントが、状態の更新を即座に受け取ります |
| ハイバネーション | 接続中のクライアントがないとき、エージェントはハイバネーションします(課金なし)。次のリクエストで起床します |
React を使っていない場合:
import { AgentClient } from "agents/client";
const agent = new AgentClient({
agent: "CounterAgent",
name: "my-counter", // optional, defaults to "default"
onStateUpdate: (state) => {
console.log("New count:", state.count);
},
});
// Call methods
await agent.call("increment");
await agent.call("reset");import { AgentClient } from "agents/client";
const agent = new AgentClient({
agent: "CounterAgent",
name: "my-counter", // optional, defaults to "default"
onStateUpdate: (state) => {
console.log("New count:", state.count);
},
});
// Call methods
await agent.call("increment");
await agent.call("reset");npm run deployエージェントは Cloudflare のグローバルネットワーク上で稼働し、ユーザーの近くで実行されます。
エージェントへルーティングする前に認証を確認します。
export default {
async fetch(request, env) {
// Check auth for agent routes
if (request.url.includes("/agents/")) {
const authResult = await checkAuth(request, env);
if (!authResult.valid) {
return new Response("Unauthorized", { status: 401 });
}
}
const agentResponse = await routeAgentRequest(request, env);
if (agentResponse) return agentResponse;
// ... rest of routing
},
};export default {
async fetch(request: Request, env: Env) {
// Check auth for agent routes
if (request.url.includes("/agents/")) {
const authResult = await checkAuth(request, env);
if (!authResult.valid) {
return new Response("Unauthorized", { status: 401 });
}
}
const agentResponse = await routeAgentRequest(request, env);
if (agentResponse) return agentResponse;
// ... rest of routing
},
} satisfies ExportedHandler<Env>;デフォルトでは、エージェントは /agents/{agent-name}/{instance-name} にルーティングされます。次のようにカスタマイズできます。
import { routeAgentRequest } from "agents";
const agentResponse = await routeAgentRequest(request, env, {
prefix: "/api/agents", // Now routes at /api/agents/{agent-name}/{instance-name}
});import { routeAgentRequest } from "agents";
const agentResponse = await routeAgentRequest(request, env, {
prefix: "/api/agents", // Now routes at /api/agents/{agent-name}/{instance-name}
});CORS、カスタムのインスタンス名、ロケーションヒントなどのオプションは ルーティング を参照してください。
Worker のコードから、エージェントを直接操作できます。
import { getAgentByName } from "agents";
export default {
async fetch(request, env) {
if (request.url.endsWith("/api/increment")) {
// Get a specific agent instance
const counter = await getAgentByName(env.CounterAgent, "shared-counter");
const newCount = await counter.increment();
return Response.json({ count: newCount });
}
// ...
},
};import { getAgentByName } from "agents";
export default {
async fetch(request: Request, env: Env) {
if (request.url.endsWith("/api/increment")) {
// Get a specific agent instance
const counter = await getAgentByName(env.CounterAgent, "shared-counter");
const newCount = await counter.increment();
return Response.json({ count: newCount });
}
// ...
},
} satisfies ExportedHandler<Env>;設定を拡張して、エージェントを追加します。
// src/agents/chat.ts
export class Chat extends Agent {
// ...
}
// src/agents/scheduler.ts
export class Scheduler extends Agent {
// ...
}// src/agents/chat.ts
export class Chat extends Agent {
// ...
}
// src/agents/scheduler.ts
export class Scheduler extends Agent {
// ...
}Wrangler 設定ファイルを更新します。
{
"$schema": "./node_modules/wrangler/config-schema.json",
"durable_objects": {
"bindings": [
{
"name": "CounterAgent",
"class_name": "CounterAgent"
},
{
"name": "Chat",
"class_name": "Chat"
},
{
"name": "Scheduler",
"class_name": "Scheduler"
}
]
},
"migrations": [
{
"tag": "v1",
"new_sqlite_classes": [
"CounterAgent",
"Chat",
"Scheduler"
]
}
]
}[[durable_objects.bindings]]
name = "CounterAgent"
class_name = "CounterAgent"
[[durable_objects.bindings]]
name = "Chat"
class_name = "Chat"
[[durable_objects.bindings]]
name = "Scheduler"
class_name = "Scheduler"
[[migrations]]
tag = "v1"
new_sqlite_classes = ["CounterAgent", "Chat", "Scheduler"]エントリポイントからすべてのエージェントをエクスポートします。
export { CounterAgent } from "./agents/counter";
export { Chat } from "./agents/chat";
export { Scheduler } from "./agents/scheduler";export { CounterAgent } from "./agents/counter";
export { Chat } from "./agents/chat";
export { Scheduler } from "./agents/scheduler";- エクスポートを確認する — Agent クラスは、メインのエントリポイントからエクスポートする必要があります。
- バインディングを確認する — Wrangler 設定ファイルの
class_nameは、エクスポートしたクラス名と完全に一致する必要があります。 - ルートを確認する — デフォルトのルートは
/agents/{'{agent-name}'}/{'{instance-name}'}です。クライアント側の Agent 名は、クラス名と一致します(大文字小文字は区別しません)。
Wrangler 設定ファイルにマイグレーションを追加します。
{
"$schema": "./node_modules/wrangler/config-schema.json",
"migrations": [
{
"tag": "v1",
"new_sqlite_classes": [
"YourAgentClass"
]
}
]
}[[migrations]]
tag = "v1"
new_sqlite_classes = ["YourAgentClass"]ルーティングがレスポンスを変更せずに返すようにします。
// Correct - return the response directly
const agentResponse = await routeAgentRequest(request, env);
if (agentResponse) return agentResponse;
// Wrong - this breaks WebSocket connections
if (agentResponse) return new Response(agentResponse.body);// Correct - return the response directly
const agentResponse = await routeAgentRequest(request, env);
if (agentResponse) return agentResponse;
// Wrong - this breaks WebSocket connections
if (agentResponse) return new Response(agentResponse.body);次を確認します。
this.stateを直接変更せず、this.setState()を呼んでいること。- マイグレーションの
new_sqlite_classesに Agent クラスがあること。 - 同じ Agent インスタンス名に接続していること。
- クライアントで
onStateUpdateコールバックを接続していること。 - WebSocket 接続が確立されていること(ブラウザーの開発者ツールで確認します)。
メソッドに @callable() デコレーターを付けます。
import { Agent, callable } from "agents";
export class MyAgent extends Agent {
@callable()
increment() {
// ...
}
}import { Agent, callable } from "agents";
export class MyAgent extends Agent {
@callable()
increment() {
// ...
}
}Agent と state の型パラメーターを追加します。
import { useAgent } from "agents/react";
// Pass the agent and state types to useAgent
const agent = useAgent({
agent: "CounterAgent",
onStateUpdate: (state) => setCount(state.count),
});
// Now agent.stub is fully typed
agent.stub.increment();import { useAgent } from "agents/react";
import type { CounterAgent, CounterState } from "./server";
// Pass the agent and state types to useAgent
const agent = useAgent<CounterAgent, CounterState>({
agent: "CounterAgent",
onStateUpdate: (state) => setCount(state.count),
});
// Now agent.stub is fully typed
agent.stub.increment();開発サーバーが SyntaxError: Invalid or unexpected token で失敗する場合は、tsconfig.json で "target": "ES2021" を設定します。これで、Vite の esbuild トランスパイラーが TC39 デコレーターをネイティブ構文のまま通さず、ダウンレベルします。
{
"compilerOptions": {
"target": "ES2021"
}
}エージェントが動くようになったら、次のトピックを確認してください。
| 内容 | 参照先 |
|---|---|
| AI / LLM 機能を追加する | AI モデルを使う |
| MCP でツールを公開する | MCP サーバー |
| バックグラウンドタスクを実行する | タスクをスケジュールする |
| メールを処理する | メールルーティング |
| Cloudflare Workflows を使う | Workflows を実行する |