既存の Cloudflare Workers プロジェクトへエージェントを追加する手順です。新規作成する場合は、代わりに チャットエージェントの構築 を参照してください。
- Wrangler 設定ファイルがある既存の Cloudflare Workers プロジェクト
- Node.js 18 以降
npm i agentsyarn add agentspnpm add agentsbun add agentsReact アプリでは追加パッケージは不要です。React バインディングは同梱されています。
Hono アプリの場合:
npm i agents hono-agentsyarn add agents hono-agentspnpm add agents hono-agentsbun add agents hono-agentsエージェント用の新しいファイルを作成します(例: src/agents/counter.ts)。
import { Agent, callable } from "agents";
export class CounterAgent extends Agent {
initialState = { count: 0 };
@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;
}
}import { Agent, callable } from "agents";
export type CounterState = {
count: number;
};
export class CounterAgent extends Agent<Env, CounterState> {
initialState: CounterState = { count: 0 };
@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;
}
}Durable Object のバインディングとマイグレーションを追加します。
{
"name": "my-existing-project",
"main": "src/index.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-existing-project"
main = "src/index.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" ]ポイント:
- bindings の
nameがenv上のプロパティになります(例:env.CounterAgent) class_nameはエクスポートしたクラス名と完全一致する必要がありますnew_sqlite_classesは状態永続化用の SQLite ストレージを有効にします- agents パッケージには
nodejs_compatフラグが必要です
上記の例のように @callable() デコレーターを使う場合、ビルド設定が 2 つ必要です。
tsconfig.json — agents/tsconfig を継承します(または手動で "target": "ES2021" を設定します)。
{
"extends": "agents/tsconfig"
}既存の tsconfig.json に独自設定がある場合は、継承したうえで上書きできます。
{
"extends": "agents/tsconfig",
"compilerOptions": {
"paths": { "~/*": ["./src/*"] }
}
}vite.config.ts — agents() プラグインを追加します(Vite 8 向けの TC39 デコレーター変換を処理します)。
import agents from "agents/vite";
export default defineConfig({
plugins: [
agents(),
// ... your existing plugins
],
});import agents from "agents/vite";
export default defineConfig({
plugins: [
agents(),
// ... your existing plugins
],
});Vite を使わないプロジェクトでは、tsconfig.json の変更だけで十分です。バンドラーは TC39 デコレーター(ステージ 3、バージョン 2023-11)に対応している必要があります。
詳細は TypeScript 設定 と Vite 設定 のリファレンスを参照してください。
エージェントクラスは、メインのエントリポイントからエクスポートする必要があります。src/index.ts を更新します。
// Export the agent class (required for Durable Objects)
export { CounterAgent } from "./agents/counter";
// Your existing exports...
export default {
// ...
};// Export the agent class (required for Durable Objects)
export { CounterAgent } from "./agents/counter";
// Your existing exports...
export default {
// ...
} satisfies ExportedHandler<Env>;プロジェクト構成に合う方法を選びます。
import { routeAgentRequest } from "agents";
export { CounterAgent } from "./agents/counter";
export default {
async fetch(request, env, ctx) {
// Try agent routing first
const agentResponse = await routeAgentRequest(request, env);
if (agentResponse) return agentResponse;
// Your existing routing logic
const url = new URL(request.url);
if (url.pathname === "/api/hello") {
return Response.json({ message: "Hello!" });
}
return new Response("Not found", { status: 404 });
},
};import { routeAgentRequest } from "agents";
export { CounterAgent } from "./agents/counter";
export default {
async fetch(request: Request, env: Env, ctx: ExecutionContext) {
// Try agent routing first
const agentResponse = await routeAgentRequest(request, env);
if (agentResponse) return agentResponse;
// Your existing routing logic
const url = new URL(request.url);
if (url.pathname === "/api/hello") {
return Response.json({ message: "Hello!" });
}
return new Response("Not found", { status: 404 });
},
} satisfies ExportedHandler<Env>;import { Hono } from "hono";
import { agentsMiddleware } from "hono-agents";
export { CounterAgent } from "./agents/counter";
const app = new Hono();
// Add agents middleware - handles WebSocket upgrades and agent HTTP requests
app.use("*", agentsMiddleware());
// Your existing routes continue to work
app.get("/api/hello", (c) => c.json({ message: "Hello!" }));
export default app;import { Hono } from "hono";
import { agentsMiddleware } from "hono-agents";
export { CounterAgent } from "./agents/counter";
const app = new Hono<{ Bindings: Env }>();
// Add agents middleware - handles WebSocket upgrades and agent HTTP requests
app.use("*", agentsMiddleware());
// Your existing routes continue to work
app.get("/api/hello", (c) => c.json({ message: "Hello!" }));
export default app;エージェントと一緒に静的アセットを配信する場合、静的アセットがデフォルトで先に配信されます。静的アセットに一致しないパスだけが Worker コードで処理されます。
import { routeAgentRequest } from "agents";
export { CounterAgent } from "./agents/counter";
export default {
async fetch(request, env, ctx) {
// Static assets are served automatically before this runs
// This only handles non-asset requests
// Route to agents
const agentResponse = await routeAgentRequest(request, env);
if (agentResponse) return agentResponse;
return new Response("Not found", { status: 404 });
},
};import { routeAgentRequest } from "agents";
export { CounterAgent } from "./agents/counter";
export default {
async fetch(request: Request, env: Env, ctx: ExecutionContext) {
// Static assets are served automatically before this runs
// This only handles non-asset requests
// Route to agents
const agentResponse = await routeAgentRequest(request, env);
if (agentResponse) return agentResponse;
return new Response("Not found", { status: 404 });
},
} satisfies ExportedHandler<Env>;Wrangler 設定ファイルでアセットを設定します。
{
"assets": {
"directory": "./public",
},
}[assets]
directory = "./public"Env インターフェースは手書きしないでください。wrangler types を実行し、Wrangler 設定と一致する型定義ファイルを生成します。設定とコードの不一致を、デプロイ時ではなくコンパイル時に検出できます。
バインディングを追加または名前変更したら、wrangler types を再実行します。
npx wrangler typesすべてのバインディングが型付きの型定義ファイルが作成されます。エージェントの Durable Object 名前空間も含みます。Agent クラスは生成された Env 型をデフォルトで使うため、型パラメーターとして渡す必要はありません。状態用の第 2 型パラメーターが必要な場合を除き、extends Agent で十分です(例: Agent<Env, CounterState>)。
型生成の詳細は 設定 を参照してください。
import { useState } from "react";
import { useAgent } from "agents/react";
function CounterWidget() {
const [count, setCount] = useState(0);
const agent = useAgent({
agent: "CounterAgent",
onStateUpdate: (state) => setCount(state.count),
});
return (
<>
{count}
<button onClick={() => agent.stub.increment()}>+</button>
<button onClick={() => agent.stub.decrement()}>-</button>
</>
);
}import { useState } from "react";
import { useAgent } from "agents/react";
import type { CounterAgent, CounterState } from "./agents/counter";
function CounterWidget() {
const [count, setCount] = useState(0);
const agent = useAgent<CounterAgent, CounterState>({
agent: "CounterAgent",
onStateUpdate: (state) => setCount(state.count),
});
return (
<>
{count}
<button onClick={() => agent.stub.increment()}>+</button>
<button onClick={() => agent.stub.decrement()}>-</button>
</>
);
}ポイント:
useAgentは WebSocket 経由でエージェントに接続します- エージェントの状態が変わると
onStateUpdateが発火します agent.stub.methodName()は、エージェント上の@callable()付きメソッドを呼び出します
import { AgentClient } from "agents/client";
const agent = new AgentClient({
agent: "CounterAgent",
name: "user-123", // Optional: unique instance name
onStateUpdate: (state) => {
document.getElementById("count").textContent = state.count;
},
});
// Call methods
document.getElementById("increment").onclick = () => agent.call("increment");import { AgentClient } from "agents/client";
const agent = new AgentClient({
agent: "CounterAgent",
name: "user-123", // Optional: unique instance name
onStateUpdate: (state) => {
document.getElementById("count").textContent = state.count;
},
});
// Call methods
document.getElementById("increment").onclick = () => agent.call("increment");ボタンをクリックすると、次の流れで状態が更新されます。
- クライアント が 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 に保存されます |
| リアルタイム同期 | 同じエージェントに接続しているすべてのクライアントが、状態の更新を即座に受け取ります |
| ハイバネーション | 接続中のクライアントがないとき、エージェントはハイバネーションします(課金なし)。次のリクエストで起床します |
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 を実行する |