Skip to content

非公式本サイトは非公式の日本語ドキュメントであり、Cloudflare 公式サイトではありません。最新情報はdevelopers.cloudflare.comをご確認ください。

Durable AI エージェントを構築する

最終更新 Markdown で表示Agent セットアップ

このガイドでは、GitHub リポジトリを調査する AI エージェントを構築します。「オープンソースの LLM プロジェクトを比較する」のようなタスクを与えると、次を行います。

  1. 関連するリポジトリを GitHub で検索します
  2. それぞれについて詳細(スター、フォーク、アクティビティ)を取得します
  3. 分析して比較します
  4. 推奨を返します

各 LLM 呼び出しとツール呼び出しは step になります。自己完結し、個別にリトライできる作業単位です。いずれかのステップが失敗すると、Workflows が自動でリトライします。タスクの途中で Workflow 全体がクラッシュしても、最後に成功したステップから再開します。

課題 Workflows での解決策
長時間続くエージェントループ 中断しても継続する耐久実行
不安定な LLM と API 呼び出し 独立したチェックポイントによる自動リトライ
人の承認を待つ waitForEvent() で数時間から数日まで一時停止
ジョブ完了のポーリング リソースを消費せず、確認のあいだに step.sleep()

このガイドでは、リアルタイムの進捗更新に Agents SDK と Workflows を使い、LLM 呼び出しに Anthropic SDK を使います。同じパターンは、任意の LLM SDK(OpenAI、Google AI、Mistral など)に適用できます。

クイックスタート

手順を飛ばして、AI Gateway を使う完成済みエージェントを取得する場合は、次のコマンドを実行します。

npm create cloudflare@latest -- --template cloudflare/docs-examples/workflows/durableAgent

Cloudflare Workflows に慣れている場合や、先にコードを見たい場合は、この方法を使います。

耐久性のある AI エージェントをゼロから作る手順は、次のとおりです。

前提条件

  1. Cloudflare アカウント に登録します。
  2. Node.js をインストールします。

Node.js のバージョンマネージャー

権限の問題を避け、Node.js のバージョンを切り替えられるよう、Voltanvm などの Node バージョンマネージャーを使います。このガイドの後半で説明する Wrangler には、Node バージョン 16.17.0 以降が必要です。

LLM 呼び出し用の Anthropic API キー も必要です。新規アカウントには無料クレジットが含まれます。

1. 新しい Worker プロジェクトを作成する

  1. 次のコマンドで、新しい Worker プロジェクトを作成します。

    npm create cloudflare@latest -- durable-ai-agent

    セットアップでは、次のオプションを選びます。

    • What would you like to start with? では、Hello World example を選びます。
    • Which template would you like to use? では、Worker only を選びます。
    • Which language do you want to use? では、TypeScript を選びます。
    • Do you want to use git for version control? では、Yes を選びます。
    • Do you want to deploy your application? では、No を選びます(デプロイ前にいくつか変更します)。
  2. プロジェクトへ移動します。

    cd durable-ai-agent
  3. 依存関係をインストールします。

    npm install agents @anthropic-ai/sdk

2. ツールを定義する

ツールは、LLM が外部システムとやり取りするために呼べる関数です。スキーマ(ツールが受け付ける入力)と実装(実際の処理)を定義します。LLM はタスクに応じて、各ツールをいつ使うかを決めます。

  1. 補完し合う 2 つのツールを src/tools.ts に作成します。

    src/tools.tsts
    export interface SearchReposInput {
    	query: string;
    	limit?: number;
    }
    
    export interface GetRepoInput {
    	owner: string;
    	repo: string;
    }
    
    interface GitHubSearchResponse {
    	items: Array<{ full_name: string; stargazers_count: number }>;
    }
    
    interface GitHubRepoResponse {
    	full_name: string;
    	description: string;
    	stargazers_count: number;
    	forks_count: number;
    	open_issues_count: number;
    	language: string;
    	license: { name: string } | null;
    	updated_at: string;
    }
    
    export const searchReposTool = {
    	name: "search_repos" as const,
    	description:
    		"Search GitHub repositories by keyword. Returns top results. Use get_repo for details.",
    	input_schema: {
    		type: "object" as const,
    		properties: {
    			query: {
    				type: "string",
    				description: "Search query (e.g., 'typescript orm')",
    			},
    			limit: { type: "number", description: "Max results (default 5)" },
    		},
    		required: ["query"],
    	},
    	run: async (input: SearchReposInput): Promise<string> => {
    		const response = await fetch(
    			`https://api.github.com/search/repositories?q=${encodeURIComponent(input.query)}&sort=stars&per_page=${input.limit ?? 5}`,
    			{
    				headers: {
    					Accept: "application/vnd.github+json",
    					"User-Agent": "DurableAgent/1.0",
    				},
    			},
    		);
    		if (!response.ok) return `Search failed: ${response.status}`;
    		const data = await response.json<GitHubSearchResponse>();
    		return JSON.stringify(
    			data.items.map((r) => ({
    				name: r.full_name,
    				stars: r.stargazers_count,
    			})),
    		);
    	},
    };
    
    export const getRepoTool = {
    	name: "get_repo" as const,
    	description:
    		"Get detailed info about a GitHub repository including stars, forks, and description.",
    	input_schema: {
    		type: "object" as const,
    		properties: {
    			owner: {
    				type: "string",
    				description: "Repository owner (e.g., 'cloudflare')",
    			},
    			repo: {
    				type: "string",
    				description: "Repository name (e.g., 'workers-sdk')",
    			},
    		},
    		required: ["owner", "repo"],
    	},
    	run: async (input: GetRepoInput): Promise<string> => {
    		const response = await fetch(
    			`https://api.github.com/repos/${input.owner}/${input.repo}`,
    			{
    				headers: {
    					Accept: "application/vnd.github+json",
    					"User-Agent": "DurableAgent/1.0",
    				},
    			},
    		);
    		if (!response.ok) return `Repo not found: ${input.owner}/${input.repo}`;
    		const data = await response.json<GitHubRepoResponse>();
    		return JSON.stringify({
    			name: data.full_name,
    			description: data.description,
    			stars: data.stargazers_count,
    			forks: data.forks_count,
    			issues: data.open_issues_count,
    			language: data.language,
    			license: data.license?.name ?? "None",
    			updated: data.updated_at,
    		});
    	},
    };
    
    export const tools = [searchReposTool, getRepoTool];

これらのツールは補完し合います。search_repos がリポジトリを見つけ、get_repo が特定のリポジトリの詳細を取得します。

3. Workflow を書く

Agents SDK の AgentWorkflow クラスは、双方向の Agent 通信で Cloudflare Workflows を拡張します。Workflow は進捗を報告し、WebSocket クライアントへブロードキャストし、RPC で Agent のメソッドを呼べます。

  • step オブジェクトは、耐久ステップを定義するメソッドを提供します。
  • step.do(name, callback) はコードを実行し、結果を永続化します。Workflow が中断されると、最後に成功したステップから再開します。
  • this.reportProgress() は Agent へ進捗更新を送ります(永続化されません)。
  • this.broadcastToClients() は、接続中のすべての WebSocket クライアントへメッセージを送ります(永続化されません)。

よりやさしい導入は、最初の Workflow を構築する を参照してください。

src/workflow.ts を作成します。

src/workflow.tsts
import { AgentWorkflow } from "agents/workflows";
import type { AgentWorkflowEvent, AgentWorkflowStep } from "agents/workflows";
import Anthropic from "@anthropic-ai/sdk";
import {
	tools,
	searchReposTool,
	getRepoTool,
	type SearchReposInput,
	type GetRepoInput,
} from "./tools";
import type { ResearchAgent } from "./agent";

type Params = { task: string };

export class ResearchWorkflow extends AgentWorkflow<ResearchAgent, Params> {
	async run(event: AgentWorkflowEvent<Params>, step: AgentWorkflowStep) {
		const client = new Anthropic({ apiKey: this.env.ANTHROPIC_API_KEY });

		const messages: Anthropic.MessageParam[] = [
			{ role: "user", content: event.payload.task },
		];

		const toolDefinitions = tools.map(({ run, ...rest }) => rest);

		// Durable agent loop - each turn is checkpointed
		for (let turn = 0; turn < 10; turn++) {
			// Report progress to Agent and connected clients
			await this.reportProgress({
				step: `llm-turn-${turn}`,
				status: "running",
				percent: turn / 10,
				message: `Processing turn ${turn + 1}...`,
			});

			const response = (await step.do(
				`llm-turn-${turn}`,
				{ retries: { limit: 3, delay: "10 seconds", backoff: "exponential" } },
				async () => {
					const msg = await client.messages.create({
						model: "claude-sonnet-4-5-20250929",
						max_tokens: 4096,
						tools: toolDefinitions,
						messages,
					});
					// Serialize for Workflow state
					return JSON.parse(JSON.stringify(msg));
				},
			)) as Anthropic.Message;

			if (!response || !response.content) continue;

			messages.push({ role: "assistant", content: response.content });

			if (response.stop_reason === "end_turn") {
				const textBlock = response.content.find(
					(b): b is Anthropic.TextBlock => b.type === "text",
				);
				const result = {
					status: "complete",
					turns: turn + 1,
					result: textBlock?.text ?? null,
				};

				// Report completion (durable)
				await step.reportComplete(result);
				return result;
			}

			const toolResults: Anthropic.ToolResultBlockParam[] = [];

			for (const block of response.content) {
				if (block.type !== "tool_use") continue;

				// Broadcast tool execution to clients
				this.broadcastToClients({
					type: "tool_call",
					tool: block.name,
					turn,
				});

				const result = await step.do(
					`tool-${turn}-${block.id}`,
					{ retries: { limit: 2, delay: "5 seconds" } },
					async () => {
						switch (block.name) {
							case "search_repos":
								return searchReposTool.run(block.input as SearchReposInput);
							case "get_repo":
								return getRepoTool.run(block.input as GetRepoInput);
							default:
								return `Unknown tool: ${block.name}`;
						}
					},
				);

				toolResults.push({
					type: "tool_result",
					tool_use_id: block.id,
					content: result,
				});
			}

			messages.push({ role: "user", content: toolResults });
		}

		return { status: "max_turns_reached", turns: 10 };
	}
}

LLM とツールでステップを分ける理由

step.do() はチェックポイントを作ります。Workflow がクラッシュしたり、Worker が再起動したりすると、次のようになります。

  • LLM ステップのあと: 応答は永続化されます。再開時は LLM 呼び出しを飛ばし、ツール実行へ進みます。
  • ツールステップのあと: 結果は永続化されます。あとのツールが失敗しても、先に実行したツールは再実行されません。

これは、とくに次の場合に重要です。

  • LLM 呼び出し: 高価で遅いので、不要に繰り返すべきではありません
  • 外部 API: レート制限や副作用がある場合があります
  • 冪等性: 一部のツール(メール送信など)は 2 回実行すべきではありません

4. Agent を書く

Agent は HTTP リクエスト、WebSocket 接続、Workflow のライフサイクルイベントを扱います。runWorkflow() で workflow インスタンスを起動し、コールバックで進捗更新を受け取ります。

src/agent.ts を作成します。

src/agent.tsts
import { Agent } from "agents";

type State = {
	currentWorkflow?: string;
	status?: string;
};

export class ResearchAgent extends Agent<Env, State> {
	initialState: State = {};

	// Start a research task - called via HTTP or WebSocket
	async startResearch(task: string) {
		const instanceId = await this.runWorkflow("RESEARCH_WORKFLOW", { task });
		this.setState({
			...this.state,
			currentWorkflow: instanceId,
			status: "running",
		});
		return { instanceId };
	}

	// Get status of a workflow
	async getResearchStatus(instanceId: string) {
		return this.getWorkflow(instanceId);
	}

	// Called when workflow reports progress
	async onWorkflowProgress(
		workflowName: string,
		instanceId: string,
		progress: unknown,
	) {
		// Broadcast to all connected WebSocket clients
		this.broadcast(JSON.stringify({ type: "progress", instanceId, progress }));
	}

	// Called when workflow completes
	async onWorkflowComplete(
		workflowName: string,
		instanceId: string,
		result?: unknown,
	) {
		this.setState({ ...this.state, status: "complete" });
		this.broadcast(JSON.stringify({ type: "complete", instanceId, result }));
	}

	// Called when workflow errors
	async onWorkflowError(
		workflowName: string,
		instanceId: string,
		error: string,
	) {
		this.setState({ ...this.state, status: "error" });
		this.broadcast(JSON.stringify({ type: "error", instanceId, error }));
	}
}

5. プロジェクトを設定する

  1. wrangler.jsonc を開き、Agent と Workflow の設定を追加します。

    {
    	"$schema": "node_modules/wrangler/config-schema.json",
    	"name": "durable-ai-agent",
    	"main": "src/index.ts",
    	// Set this to today's date
    	"compatibility_date": "2026-09-20",
    	"observability": {
    		"enabled": true
    	},
    	"durable_objects": {
    		"bindings": [
    			{
    				"name": "ResearchAgent",
    				"class_name": "ResearchAgent"
    			}
    		]
    	},
    	"workflows": [
    		{
    			"name": "research-workflow",
    			"binding": "RESEARCH_WORKFLOW",
    			"class_name": "ResearchWorkflow"
    		}
    	],
    	"migrations": [
    		{
    			"tag": "v1",
    			"new_sqlite_classes": ["ResearchAgent"]
    		}
    	]
    }
    "$schema" = "node_modules/wrangler/config-schema.json"
    name = "durable-ai-agent"
    main = "src/index.ts"
    # Set this to today's date
    compatibility_date = "2026-09-20"
    
    [observability]
    enabled = true
    
    [[durable_objects.bindings]]
    name = "ResearchAgent"
    class_name = "ResearchAgent"
    
    [[workflows]]
    name = "research-workflow"
    binding = "RESEARCH_WORKFLOW"
    class_name = "ResearchWorkflow"
    
    [[migrations]]
    tag = "v1"
    new_sqlite_classes = [ "ResearchAgent" ]
  2. バインディングの型を生成します。

    npx wrangler types

    これにより、バインディングを含む Env 型が入った worker-configuration.d.ts が作成されます。

6. API を書く

Worker はリクエストを Agent へルーティングし、Agent が workflow のライフサイクルを管理します。WebSocket 接続には routeAgentRequest() を、サーバー側の RPC 呼び出しには getAgentByName() を使います。

src/index.ts を置き換えます。

src/index.tsts
import { getAgentByName, routeAgentRequest } from "agents";

export { ResearchAgent } from "./agent";
export { ResearchWorkflow } from "./workflow";

export default {
	async fetch(request: Request, env: Env): Promise<Response> {
		const url = new URL(request.url);

		// Route WebSocket connections to /agents/research-agent/{name}
		const agentResponse = await routeAgentRequest(request, env);
		if (agentResponse) return agentResponse;

		// HTTP API for starting research tasks
		if (request.method === "POST" && url.pathname === "/research") {
			const { task, agentId } = await request.json<{
				task: string;
				agentId?: string;
			}>();

			// Get agent instance by name (creates if doesn't exist)
			const agent = await getAgentByName(
				env.ResearchAgent,
				agentId ?? "default",
			);

			// Start the research workflow via RPC
			const result = await agent.startResearch(task);
			return Response.json(result);
		}

		// Check workflow status
		if (url.pathname === "/status") {
			const instanceId = url.searchParams.get("instanceId");
			const agentId = url.searchParams.get("agentId") ?? "default";

			if (!instanceId) {
				return Response.json({ error: "instanceId required" }, { status: 400 });
			}

			const agent = await getAgentByName(env.ResearchAgent, agentId);
			const status = await agent.getResearchStatus(instanceId);

			return Response.json(status);
		}

		return new Response("POST /research with { task } to start", {
			status: 400,
		});
	},
} satisfies ExportedHandler<Env>;

7. ローカルで開発する

  1. ローカル開発用の .env ファイル を作成します。

    .envsh
    ANTHROPIC_API_KEY=your-api-key-here
  2. 開発サーバーを起動します。

    npx wrangler dev
  3. 調査タスクを開始します。

    curl -X POST http://localhost:8787/research \
      -H "Content-Type: application/json" \
      -d '{"task": "Compare open-source LLM projects"}'
    { "instanceId": "abc-123-def" }
  4. 進捗を確認します(完了まで数秒かかることがあります)。

    curl "http://localhost:8787/status?instanceId=abc-123-def"

エージェントはリポジトリを検索し、詳細を取得し、比較を返します。進捗更新は、接続中の WebSocket クライアントへブロードキャストされます。

8. デプロイする

  1. Worker をデプロイします。

    npx wrangler deploy
  2. API キーをシークレットとして追加します。

    npx wrangler secret put ANTHROPIC_API_KEY
  3. デプロイした Worker で調査タスクを開始します。

    curl -X POST https://durable-ai-agent.<your-subdomain>.workers.dev/research \
      -H "Content-Type: application/json" \
      -d '{"task": "Compare open-source LLM projects"}'
  4. CLI で workflow の実行を確認します。

    npx wrangler workflows instances describe research-workflow latest

    エージェントが実行したすべてのステップ(LLM 呼び出し、ツール実行、所要時間、リトライ)が表示されます。

    Cloudflare ダッシュボードの research-workflow でも確認できます。

    Workflows を開く ↗

リアルタイムのクライアント連携

WebSocket で Agent に接続すると、リアルタイムの進捗更新を受け取れます。useAgent フックは /agents/{agent-name}/{instance-name} に接続します。

/agents/research-agent/default  → ResearchAgent instance "default"
/agents/research-agent/user-123 → ResearchAgent instance "user-123"
import { useState } from "react";
import { useAgent } from "agents/react";

function ResearchUI({ agentId = "default" }) {
	const [progress, setProgress] = useState(null);

	const { state } = useAgent({
		agent: "research-agent", // Maps to ResearchAgent class
		name: agentId, // Instance name
		onMessage: (message) => {
			const data = JSON.parse(message.data);
			if (data.type === "progress") {
				setProgress(data.progress);
			}
		},
	});

	return (
		<div>
			{progress && (
				<p>
					{progress.message} ({Math.round(progress.percent * 100)}%)
				</p>
			)}
		</div>
	);
}

Agent のクラス名は、URL 用に自動で kebab-case に変換されます(ResearchAgentresearch-agent)。

さらに学ぶ

Agents SDK の Workflows

AgentWorkflow、ライフサイクルコールバック、双方向通信の完全な API リファレンスです。

Workers API

プログラムから制御する Workflows API の全体を確認します。

Agents SDK

リアルタイムチャットと WebSocket 接続を持つ対話型エージェント向けです。

役に立ちましたか?