Skip to content

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

長時間稼働するエージェント

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

数日、数週間、数か月にわたって存続するエージェントを構築します。再起動を乗り越え、必要時に起動し、1 回のリクエストを大きく超える作業を管理します。

要点は次のとおりです。

  • エージェントは常時稼働のプロセスではなく、耐久性のある ID です。
  • 状態、SQL データ、スケジュール、fiber のチェックポイントは、休止(hibernation)と再起動のあとも残ります。
  • メモリ上の変数、タイマー、進行中の fetch、ローカルなクロージャは、エビクション(強制停止)では残りません。
  • 分単位の作業には keepAlive()、回復が必要な作業には runFiber()、耐久性のある受付と状態が必要な呼び出し側には startFiber()、重い複数ステップのジョブには Workflows を使います。
  • 親が多数の長寿命な子コンテキストを調整するときは、サブエージェントを使います。

長時間稼働エージェントに Cloudflare を使う理由

エージェントはほとんどの時間を待ちます。ユーザー入力(秒から日)、LLM 応答(秒から分)、ツール結果(秒から時間)、人の承認(時間から日)、スケジュールされた起動(分から月)です。従来の VM やコンテナでは、その待機時間にも料金がかかります。99% 休止で 1% だけ動くエージェントでも、サーバーの 100% 分を払うことになります。

Durable Objects はこのモデルを逆転します。エージェントは永続状態を持つアドレス可能な実体として存在しますが、休止中はコンピュートを消費しません。何かが起きると(HTTP リクエスト、WebSocket メッセージ、スケジュールされたアラーム、受信メール)、プラットフォームがエージェントを起こし、SQLite から状態を読み込み、イベントを渡します。エージェントは作業を行い、再び休眠します。

これが アクターモデル です。各エージェントは ID と耐久状態を持ち、メッセージで起動します。サーバー、ルーティング、ヘルスチェック、再起動ロジックを自分で管理する必要はありません。配置、スケール、回復はプラットフォームが担当します。

コストもこれに従います。

VM / コンテナ Durable Objects
アイドルコスト 常にフルのコンピュートコスト ゼロ(休止中)
スケーリング 容量をプロビジョニングして管理する 自動、エージェントごと
状態 外部データベースが必要 組み込み SQLite
回復 自分で作る(プロセスマネージャ、ヘルスチェック) プラットフォームが再起動し、状態は残る
ID / ルーティング 自分で作る(ロードバランサー、スティッキーセッション) 組み込み(名前からエージェントへ)
1 万エージェント、各々稼働時間 1% 常時稼働インスタンス 1 万 任意の時点で約 100 が稼働

エージェントは本質的にバーストし、状態を持ち、長寿命です。この組み合わせに自然に合います。

長時間稼働エージェントのライフサイクル

長時間稼働エージェントは、連続して動き続けるプロセスではありません。存在は連続しますが、実行は断続的です。長い時間軸で確実に動くエージェントを作るには、ライフサイクルの理解が必要です。

起動 → onStart() → イベント処理 → アイドル(約 2 分) → 休止
  ▲                                                      │
  └──────────── アラームまたはリクエストで起動 ──────────┘

エビクション(クラッシュ / 再デプロイ)はいつでも発生します。
状態は SQLite に残ります。次のイベントでエージェントが再起動します。

残るもの

  • this.statesetState() のたびに SQLite へ永続化されます
  • this.sql のデータ — 作成したすべての SQLite テーブル
  • スケジュールされたタスク — SQLite に保存され、エージェントを起こすアラームを発火します
  • 接続状態 — 各 WebSocket クライアントの connection.setState() データ
  • fiber のチェックポイントと台帳runFiber()stash() データと、保持された startFiber() の状態行

SQLite 上に作った上位の抽象も、同じ耐久ストレージを共有するため残ります。

残らないもの

  • メモリ上の変数setState()this.sql に保存していないクラスフィールド
  • 実行中のタイマーsetTimeoutsetInterval は休止 / エビクションで失われます
  • 進行中の fetch — 飛行中の HTTP 呼び出しは破棄されます
  • ローカルなクロージャ — コールバックと Promise チェーンは失われます

つまり、重要な作業は永続化するか、回復できるようにする必要があります。SDK はスケジュール、fiber、キューといったプリミティブを提供します。ただし「メモリ上」と「耐久」の境界を理解することが不可欠です。

実行例: プロジェクトマネージャエージェント

このドキュメントでは、次のプロジェクトマネージャエージェントを段階的に組み立てます。

  • プロジェクト期間(数週間または数か月)にわたって存続する
  • タスクを追跡し、作業をサブエージェントに割り当て、進捗を報告する
  • スケジュールで起動し、期限を確認してリマインダーを送る
  • 外部イベント(GitHub からの webhook、チームメンバーからのメール)に反応する
  • 長時間の操作(CI パイプライン、コードレビュー、デプロイ)を扱う
  • 途中の再起動やエビクションを何度でも乗り越える
import { Agent } from "agents";

type ProjectState = {
	name: string;
	status: "planning" | "active" | "review" | "complete";
	tasks: Task[];
	plan: Plan | null;
};

type Task = {
	id: string;
	title: string;
	status: "pending" | "in_progress" | "blocked" | "complete";
	assignee?: string;
	dueDate?: string;
	completedAt?: number;
	externalJobId?: string;
};

export class ProjectManager extends Agent<Env, ProjectState> {
	initialState: ProjectState = {
		name: "",
		status: "planning",
		tasks: [],
		plan: null,
	};
}

Plan 型は 耐久戦略としての計画 で導入します。このエージェントへ、節ごとに機能を足していきます。

起動: エージェントが起きる仕組み

休止中のエージェントは、次のいずれかで起きます。

起動源 仕組み
HTTP リクエスト エージェントの URL へのリクエストが onRequest() を起動します GitHub からの webhook
WebSocket 接続 クライアントが接続し、onConnect() が起動します チームメンバーがダッシュボードを開く
RPC 呼び出し 別の Worker またはエージェントが サービスバインディング または @callable 経由でメソッドを呼びます コーディネータエージェントがタスクを委譲する
スケジュールされたアラーム 保存されたスケジュールが発火し、コールバックを起動します 午前 9 時のデイリースタンドアップリマインダー
メール 受信メールが onEmail() を起動します チームメンバーがステータスメールに返信する

このパターンは、Worker に届く任意のイベント源へ自然に広がります。電話の webhook からチャットプラットフォームのボットまでです。外部シグナルが届き、プラットフォームがエージェントを起こし、エージェントが処理します。

起動源ごとにエージェントを「起動」したり「デプロイ」したりする必要はありません。すべて同じ Durable Object インスタンスへルーティングされます。エージェントの ID(名前)がルーティングキーです。

export class ProjectManager extends Agent<Env, ProjectState> {
	async onStart() {
		// Daily deadline check at 9am UTC — idempotent, safe across restarts
		await this.schedule(
			"0 9 * * *",
			"checkDeadlines",
			{},
			{
				idempotent: true,
			},
		);

		// Progress sync every 30 minutes
		await this.scheduleEvery(1800, "syncProgress");
	}

	async onRequest(request: Request): Promise<Response> {
		const url = new URL(request.url);

		if (url.pathname.endsWith("/github-webhook")) {
			const event = await request.json();
			await this.handleGitHubEvent(event);
			return new Response("OK");
		}

		return Response.json({
			project: this.state.name,
			status: this.state.status,
		});
	}

	async checkDeadlines() {
		/* ... find overdue tasks, broadcast alerts ... */
	}
	async syncProgress() {
		/* ... check on sub-agents, update task statuses ... */
	}
}

長い作業のあいだ生き続ける

エージェントが、アイドル退避ウィンドウ(約 70〜140 秒)より長い作業をする場合があります。LLM 応答のストリーミング、複数ステップのツールチェーンの調整、遅い API の待機は、途中でエビクションされる危険があります。

keepAlive() はハートビートを作り、非活動タイマーをリセットしてこれを防ぎます。

export class ProjectManager extends Agent<Env, ProjectState> {
	async generateProjectPlan(goal: string) {
		const result = await this.keepAliveWhile(async () => {
			const plan = await this.callLLM(`Create a project plan for: ${goal}`);
			const tasks = await this.callLLM(
				`Break this into tasks: ${JSON.stringify(plan)}`,
			);
			return { plan, tasks };
		});

		this.setState({
			...this.state,
			status: "active",
			plan: result.plan,
			tasks: result.tasks,
		});
	}
}

推奨は keepAliveWhile() です。作業が終わる(または例外を投げる)ときにハートビートが確実に片付けられます。手動制御では、keepAlive() が破棄関数を返します。

const dispose = await this.keepAlive();
try {
	await longWork();
} finally {
	dispose();
}

keepAlive では足りないとき

keepAlive は分単位の作業向けです。本当に長い操作には、別の戦略を使います。

期間 戦略
通常のリクエスト処理
keepAlive() / keepAliveWhile()
再試行可能な受付が重要なときは startFiber()
分から時間 Workflows
時間から日 非同期パターン: ジョブを開始し、休止し、完了で起動する

クラッシュを乗り越える: fiber と回復

エージェントはいつでもエビクションされます。デプロイ、プラットフォームの再起動、リソース制限です。作業の途中だった場合、チェックポイントがなければその作業は失われます。

runFiber() はクラッシュから回復できる実行を提供します。作業期間中は SQLite に行を残し、中間状態を stash() できます。エージェントがエビクションされても fiber 行は残り、次の起動で onFiberRecovered() が呼ばれます。

重要な境界が耐久性のある受付であるときは startFiber() を使います。同じ fiber 機構の上に、べき等キー、保持される状態レコード、検査、キャンセル、クリーンアップが加わります。デフォルトでは受付後に戻ります。受理したジョブが終端状態になるまでリクエストを開いたままにする場合は waitForCompletion: true を渡します。プロバイダーが再送する可能性があり、エージェントが重複した可視副作用を始めてはいけない webhook に向きます。

export class ProjectManager extends Agent<Env, ProjectState> {
	async executeTask(task: Task) {
		await this.runFiber(`task:${task.id}`, async (ctx) => {
			const resources = await this.gatherResources(task);
			ctx.stash({ phase: "prepared", resources, task });

			const result = await this.runSubAgent(task, resources);
			ctx.stash({ phase: "executed", result, task });

			await this.updateTaskStatus(task.id, "complete", result);
		});
	}

	async onFiberRecovered(ctx: FiberRecoveryContext) {
		if (!ctx.name.startsWith("task:")) return;
		const { phase, task } = ctx.snapshot as { phase: string; task: Task };

		if (phase === "prepared") {
			await this.executeTask(task);
		} else if (phase === "executed") {
			await this.updateTaskStatus(
				task.id,
				"complete",
				(ctx.snapshot as { result: unknown }).result,
			);
		}
	}
}

パターンは次です。高価な作業の前にチェックポイントし、最後のチェックポイントから回復する。 自動リプレイではありません。回復の意味はドメインごとに自分で決めます。

FiberContextFiberRecoveryContext、同時 fiber、インラインと fire-and-forget のパターンなど、API リファレンス全体は Durable Execution を参照してください。

長い非同期操作を扱う

プロジェクトマネージャは、1 回の起動よりはるかに長い作業をよく開始します。CI パイプラインは 20 分、デザインレビューは 1 日、動画アセットの生成は数時間です。エージェントは、このあいだ生き続けるべきではありません。代わりに作業を開始し、ジョブ ID を状態に残して休止します。結果が届くと(コールバック、ポーリング、Workflow 完了)、エージェントが起き、結果を突き合わせ、先へ進みます。

パターン: webhook コールバック

プロジェクトマネージャはタスク用の CI パイプラインを開始します。パイプラインは 20 分かかります。接続を開いたままにする代わりに、自分の URL をコールバックとして登録して休眠します。

export class ProjectManager extends Agent<Env, ProjectState> {
	async startCIPipeline(task: Task) {
		const response = await fetch("https://ci.example.com/api/pipelines", {
			method: "POST",
			body: JSON.stringify({
				repo: "org/project",
				branch: "main",
				callback_url: `${this.url}/ci-callback?taskId=${task.id}`,
			}),
		});

		const { pipelineId } = await response.json();
		this.updateTask(task.id, {
			status: "in_progress",
			externalJobId: pipelineId,
		});
	}

	async onRequest(request: Request): Promise<Response> {
		const url = new URL(request.url);
		if (url.pathname.endsWith("/ci-callback")) {
			const taskId = url.searchParams.get("taskId");
			const result = await request.json();
			this.updateTask(taskId, {
				status: result.status === "success" ? "complete" : "blocked",
			});
			return new Response("OK");
		}
		// ... other routes
	}
}

パターン: スケジュールでポーリングする

すべての外部サービスがコールバックに対応しているわけではありません。プロジェクトマネージャが動画アセットの生成を依頼する場合は、ジョブが完了するまで定期的に確認する必要があります。

export class ProjectManager extends Agent<Env, ProjectState> {
	async startVideoGeneration(task: Task) {
		const response = await fetch("https://video-api.example.com/generate", {
			method: "POST",
			body: JSON.stringify({ prompt: task.title }),
		});
		const { jobId } = await response.json();
		this.updateTask(task.id, { status: "in_progress", externalJobId: jobId });
		await this.schedule(60, "pollExternalJob", {
			taskId: task.id,
			jobId,
			attempt: 1,
		});
	}

	async pollExternalJob(payload: {
		taskId: string;
		jobId: string;
		attempt: number;
	}) {
		const response = await fetch(
			`https://video-api.example.com/status/${payload.jobId}`,
		);
		const status = await response.json();

		if (status.state === "complete" || status.state === "failed") {
			this.updateTask(payload.taskId, {
				status: status.state === "complete" ? "complete" : "blocked",
			});
			return;
		}

		const nextDelay = Math.min(60 * payload.attempt, 600);
		await this.schedule(nextDelay, "pollExternalJob", {
			...payload,
			attempt: payload.attempt + 1,
		});
	}
}

パターン: Workflow への委譲

本番デプロイは、それぞれ独立して再試行すべき複数ステップ(ビルド、テスト、ステージ、プロモート)を含みます。プロジェクトマネージャはこれらのステップを内部で管理すべきではありません。Workflow に委譲し、再試行とステップ順序を任せます。

export class ProjectManager extends Agent<Env, ProjectState> {
	async startDeployment(task: Task) {
		const instanceId = await this.runWorkflow("DEPLOY_WORKFLOW", {
			taskId: task.id,
			environment: "production",
		});
		this.updateTask(task.id, {
			status: "in_progress",
			externalJobId: instanceId,
		});
	}

	async onWorkflowComplete(
		workflowName: string,
		instanceId: string,
		result?: unknown,
	) {
		const task = this.state.tasks.find((t) => t.externalJobId === instanceId);
		if (task) this.updateTask(task.id, { status: "complete" });
	}
}

長い待機のあとでコンテキストを再構築する

CI パイプラインは 20 分後に終わります。webhook がプロジェクトマネージャを起こします。タスク状態は更新されます。では次は何か。エージェントが LLM で作業を調整していた場合(次にどのタスクを走らせるか、ステータス報告を下書きするか、ブロッカーを推論するか)、その推論の糸を拾い直す必要があります。元のプロンプト、進行中のツール呼び出し、思考の連鎖は、メモリから消えています。

これが長時間稼働 AI エージェントの根本的な難しさです。多くのフレームワークは、ツール呼び出しが LLM のタイムアウト内に終わると仮定し、この問題を直接扱いません。

いま使えるアプローチは 3 つです。

会話履歴全体を再生する。 AIChatAgent はすべてのメッセージを SQLite に残します。結果が届いたら履歴に追加し、LLM を再呼び出しします。いちばん単純ですが、コンテキストウィンドウ全体を再処理します。

継続用の要約を stash する。 休止前に、何をしていたか、結果をどう扱うかの短い説明を残します。

ctx.stash({
	task: "Waiting for CI results",
	onSuccess: "Mark task complete, move to next step in plan",
	onFailure: "Notify team, schedule retry in 1 hour",
	relevantContext: { taskId, planStep: 3 },
});

回復時は、全部を再生するのではなく、stash から焦点を絞ったプロンプトを組み立てます。

計画をコンテキストとして使う。 エージェントに構造化された計画があれば、計画自体が十分なコンテキストになります。「7 ステップ中の 3 番目、ステップは『CI パイプラインを実行』、結果が今届いた」です。長時間稼働エージェントではこれがもっとも堅牢です。計画は回復手段であり、コンテキスト再構築の戦略でもあります。次の節を参照してください。

耐久戦略としての計画

構造化された計画は、ユーザーへの進捗表示だけではありません。耐久性の仕組みです。計画を持つエージェントは、どこで止まったかを見て、任意の中断から回復できます。

type Plan = {
	goal: string;
	steps: PlanStep[];
	currentStep: number;
	createdAt: string;
	updatedAt: string;
};

type PlanStep = {
	id: string;
	description: string;
	status: "pending" | "in_progress" | "complete" | "failed" | "skipped";
	result?: unknown;
};

export class ProjectManager extends Agent<Env, ProjectState> {
	async createPlan(goal: string) {
		const steps = await this.keepAliveWhile(async () => {
			return this.callLLM(`
        Break down this project goal into concrete steps.
        Return a JSON array of { id, description } objects.
        Goal: ${goal}
      `);
		});

		this.setState({
			...this.state,
			plan: {
				goal,
				steps: steps.map((s: { id: string; description: string }) => ({
					...s,
					status: "pending" as const,
				})),
				currentStep: 0,
				createdAt: new Date().toISOString(),
				updatedAt: new Date().toISOString(),
			},
		});

		await this.schedule(0, "executeNextStep");
	}

	async executeNextStep() {
		const { plan } = this.state;
		if (!plan || plan.currentStep >= plan.steps.length) {
			this.setState({ ...this.state, status: "complete" });
			return;
		}

		const step = plan.steps[plan.currentStep];

		try {
			const result = await this.keepAliveWhile(() => this.executeStep(step));

			const updatedSteps = plan.steps.map((s) =>
				s.id === step.id ? { ...s, status: "complete" as const, result } : s,
			);
			this.setState({
				...this.state,
				plan: {
					...plan,
					steps: updatedSteps,
					currentStep: plan.currentStep + 1,
					updatedAt: new Date().toISOString(),
				},
			});

			await this.schedule(0, "executeNextStep");
		} catch (error) {
			const updatedSteps = plan.steps.map((s) =>
				s.id === step.id ? { ...s, status: "failed" as const } : s,
			);
			this.setState({
				...this.state,
				plan: {
					...plan,
					steps: updatedSteps,
					updatedAt: new Date().toISOString(),
				},
			});
		}
	}
}

このパターンは、長時間稼働エージェントに次の利点があります。

  • 回復が単純 — 再起動後は plan.currentStep を見て再開します
  • 進捗が見える — クライアントは完了したステップと次のステップを見られます
  • 再計画ができる — ステップが失敗したり要件が変わったりしても、完了済み作業を失わずに残りのステップを直せます
  • 人の監督 — 計画は自然な承認チェックポイントです(「これからこれをします。進めてよいですか?」)
  • コンテキスト再構築 — 計画が LLM に、今どこにいるか、何が起きたか、次に何をするかを伝えます。会話全体の再生は不要です

サブエージェントへの委譲

プロジェクトマネージャはすべてを自分でやりません。専門作業はサブエージェントへ委譲します。それぞれが独自の ID、状態、ライフサイクルを持ちます。

export class ProjectManager extends Agent<Env, ProjectState> {
	async delegateTask(task: Task) {
		const researcher = await this.subAgent(
			ResearchAgent,
			`research-${task.id}`,
		);

		const findings = await researcher.research(task.title);

		this.updateTask(task.id, { status: "complete" });
		return findings;
	}
}

サブエージェントは独自の状態、スケジュール、耐久 fiber、ライフサイクルを持ちます。親の下に同居しますが、各子は自分の SQLite データを保存し、コールバックは子を this として実行します。

facet には独立したアラスロットがありません。物理的な Durable Object アラームはトップレベルの親が持ちます。Agents SDK は、各スケジュールまたは fiber 回復リースの所有者であるサブエージェントを記録し、親を起こし、コールバックを子へ戻します。親はサブエージェントの作業中に起きている必要はありません。作業を開始して休止し、子が所有するスケジュールまたは回復チェックで起きれば十分です。

型付き RPC スタブ、クライアントルーティング、アクセス制御、ストレージ分離、アラーム連携 API など、subAgent() API 全体は サブエージェント を参照してください。AI 向けのサブエージェントストリーミング(子エージェントで LLM ターン全体を走らせる)は Think: サブエージェント RPC を参照してください。

中断された LLM ストリームを回復する

上のパターンは、プロジェクトマネージャの調整作業(スケジュール、委譲、ポーリング)を扱います。一方でプロジェクトマネージャは LLM も直接使います。計画の生成、進捗の要約、ステータスメールの下書きです。これらの LLM 呼び出しは、途中でエビクションされると再開できない接続上でトークンをストリーミングします。

AIChatAgent または Think 上のチャット向けエージェントでは、問題はさらに鋭くなります。ユーザーは応答ストリームをリアルタイムで見て、文の途中で止まるのを目撃します。耐久回復は、すべてのチャットターンを runFiber で包みます。ストリーミング中の自動 keepAlive と、エージェント再起動時の回復フックを提供します。

import { AIChatAgent } from "@cloudflare/ai-chat";
import type {
	ChatRecoveryContext,
	ChatRecoveryOptions,
} from "@cloudflare/ai-chat";

class ProjectChat extends AIChatAgent<Env> {
	override async onChatRecovery(
		ctx: ChatRecoveryContext,
	): Promise<ChatRecoveryOptions> {
		// ctx.partialText    — text generated before eviction
		// ctx.recoveryData   — whatever you stashed via this.stash()
		// ctx.messages        — full conversation history
		// ctx.createdAt       — when the interrupted turn started
		return {};
	}
}

適切な回復戦略は LLM プロバイダーによって異なります。

プロバイダー 戦略 仕組み トークンコスト
Workers AI 途中から続行 continueLastTurn() — アシスタント prefills でモデルが続行します 低い
OpenAI(Responses API) 完了した応答を取得 ストリーミング中に responseId を stash し、回復時に取得します ゼロ
Anthropic 合成による継続 部分結果を残し、続行を求める合成ユーザーメッセージを送ります 中程度
その他 prefill を試し、だめなら合成にフォールバック プロバイダーが対応していれば continueLastTurn()、そうでなければ合成メッセージ まちまち

古い回復を抑えるには ctx.createdAt を使います。たとえば、回復したチャットターンが数分より古い場合は、部分回答は残しつつ自動継続はスキップし、古い応答でユーザーを驚かせない、といった判断ができます。

AIChatAgentThink は常に耐久回復を使います。デフォルト経路は部分出力を残し、安全なときにターンを続行または再試行します。プロバイダーにより良い回復戦略があるときは onChatRecovery を上書きします。終端体験の調整には chatRecovery = { maxAttempts, terminalMessage, onExhausted } を設定します。

アシスタントのストリームチャンクが 1 つも書かれる前にエージェントが中断された場合、続行できる部分アシスタントメッセージはありません。そのターンの未回答ユーザーメッセージが、永続化された最新メッセージのままであるときは、onChatRecovery{ continue: false } を返さない限り、チャット回復はターンを自動再試行します。

時間とともに状態を管理する

数か月動くエージェントはデータを蓄積します。会話履歴、タイムラインイベント、完了したタスク、スケジュール記録です。管理しないと際限なく増えます。

ハウスキーピング

定期クリーンアップをスケジュールし、古いデータを刈り、完了した作業をアーカイブします。

export class ProjectManager extends Agent<Env, ProjectState> {
	async onStart() {
		await this.schedule("0 0 * * *", "housekeeping", {}, { idempotent: true });
	}

	async housekeeping() {
		const cutoff = Date.now() - 30 * 24 * 60 * 60 * 1000;
		const toArchive = this.state.tasks.filter(
			(t) => t.status === "complete" && (t.completedAt ?? 0) < cutoff,
		);
		for (const task of toArchive) {
			this
				.sql`INSERT INTO archived_tasks (id, data) VALUES (${task.id}, ${JSON.stringify(task)})`;
		}
		this.setState({
			...this.state,
			tasks: this.state.tasks.filter(
				(t) => !toArchive.some((a) => a.id === t.id),
			),
		});

		this.deleteWorkflows({
			status: ["complete", "errored"],
			createdBefore: new Date(Date.now() - 7 * 24 * 60 * 60 * 1000),
		});
	}
}

会話履歴の管理

AIChatAgent を使うエージェントでは、寿命が長いと会話履歴が大きくなります。管理しないと、プロジェクトが終わる前に 3 か月分の会話が LLM のコンテキストウィンドウを使い果たします。

会話サイズを抑える戦略:

  • スライディングウィンドウ — アクティブなコンテキストには直近 N 件だけ残します。単純で予測しやすいです。
  • 要約 — 古いメッセージを定期的に要約し、短い要約で置き換えます。元のメッセージは監査用に SQLite に残せます。
  • 選択的な保持 — 決定、承認、重要なコンテキストを含むメッセージは残し、日常のやり取りは刈ります。

寿命の終わり

長時間稼働エージェントは、いずれ目的を終えます。プロジェクトが出荷され、調査が終わり、監視期間が閉じます。明示的に片付けます。

export class ProjectManager extends Agent<Env, ProjectState> {
	async completeProject() {
		const schedules = await this.listSchedules();
		for (const schedule of schedules) {
			await this.cancelSchedule(schedule.id);
		}

		this.setState({ ...this.state, status: "complete" });

		// All SQLite data, schedules, and state are permanently deleted
		await this.destroy();
	}
}

this.destroy() は永続です。あとでエージェントのデータが必要になる可能性がある場合は、破棄前に外部ストア(R2、D1、または API 呼び出し)へアーカイブします。再起動するかもしれないエージェントは、完了と印を付けて休止させるだけで十分です。アイドル時のコストはゼロです。

Workflows とエージェント内部パターンの使い分け

Workflows も、エージェント内部のプリミティブ(スケジュール、fiber、キュー)も、長時間の作業を支えられます。どちらが適切かは、作業の性質で決まります。

エージェント内部 Workflows
向いているもの エージェント中心の作業: スケジュール、ポーリング、状態更新 独立した複数ステップのパイプライン
耐久性 SQLite(エビクションを越えて残る) Workflow エンジン(あらゆる障害を越える)
再試行 this.retry()、スケジュール単位の再試行 バックオフ付きのステップ単位再試行
最大時間 起動あたり数分(keepAlive あり) ステップあたり 30 分、ステップ数は無制限
人の承認 自分で作る(状態 + WebSocket) 組み込みの waitForApproval()
複雑さ 低い — すべてがエージェント内 高い — 別クラスと wrangler 設定が必要

実務的な目安です。作業がエージェント自身のライフサイクル管理(期限確認、状態同期、リマインダー送信)なら、スケジュールと fiber を使います。独立して失敗・再試行できる一連のパイプライン(デプロイ、データ処理、レポート生成)なら、Workflow を使います。

プロジェクトマネージャエージェントは両方使います。自身のリズム(デイリースタンドアップ、進捗同期)にはスケジュール、重い操作(デプロイ、CI パイプライン)には Workflows です。

まとめ

Cloudflare 上の長時間稼働エージェントは、長時間稼働プロセスではありません。起きて、作業して、眠る耐久実体です。数週間から数か月続くこともあります。主要なプリミティブは次のとおりです。

プリミティブ 目的
setState() / this.sql 起動をまたいで状態を永続化する
schedule() / scheduleEvery() 将来の時点でエージェントを起こす
keepAlive() / keepAliveWhile() 作業中のエビクションを防ぐ
runFiber() / stash() 長いタスクをチェックポイントし、回復する
startFiber() ジョブを耐久的に受け付け、検査し、キャンセルする
chatRecovery 中断された LLM ストリームを回復する
onRequest() / onEmail() / RPC 外部イベントで起動する
runWorkflow() 重い複数ステップ作業を委譲する
subAgent() 専門作業を子エージェントへ委譲する
状態内の構造化計画 回復、可視化、再計画を可能にする

プロジェクトマネージャエージェントでは、これらが次のように組み合わさります。

  1. 計画する — 目標をステップに分解し、計画を状態に残します
  2. 実行する — ステップを 1 つずつ走らせ、あいだは休止します
  3. 反応する — webhook、メール、スケジュールで起きます
  4. 回復する — 中断後は最後のチェックポイントから再開します
  5. 委譲する — 作業をサブエージェントと Workflows へ渡します
  6. 維持する — 古いデータを刈り、完了作業をアーカイブし、自身のライフサイクルを管理します
  7. 終える — プロジェクト完了時に片付けて破棄します

このどれも、連続実行は不要です。存在していれば十分です。

関連情報

役に立ちましたか?