数日、数週間、数か月にわたって存続するエージェントを構築します。再起動を乗り越え、必要時に起動し、1 回のリクエストを大きく超える作業を管理します。
要点は次のとおりです。
- エージェントは常時稼働のプロセスではなく、耐久性のある ID です。
- 状態、SQL データ、スケジュール、fiber のチェックポイントは、休止(hibernation)と再起動のあとも残ります。
- メモリ上の変数、タイマー、進行中の fetch、ローカルなクロージャは、エビクション(強制停止)では残りません。
- 分単位の作業には
keepAlive()、回復が必要な作業にはrunFiber()、耐久性のある受付と状態が必要な呼び出し側にはstartFiber()、重い複数ステップのジョブには Workflows を使います。 - 親が多数の長寿命な子コンテキストを調整するときは、サブエージェントを使います。
エージェントはほとんどの時間を待ちます。ユーザー入力(秒から日)、LLM 応答(秒から分)、ツール結果(秒から時間)、人の承認(時間から日)、スケジュールされた起動(分から月)です。従来の VM やコンテナでは、その待機時間にも料金がかかります。99% 休止で 1% だけ動くエージェントでも、サーバーの 100% 分を払うことになります。
Durable Objects はこのモデルを逆転します。エージェントは永続状態を持つアドレス可能な実体として存在しますが、休止中はコンピュートを消費しません。何かが起きると(HTTP リクエスト、WebSocket メッセージ、スケジュールされたアラーム、受信メール)、プラットフォームがエージェントを起こし、SQLite から状態を読み込み、イベントを渡します。エージェントは作業を行い、再び休眠します。
これが アクターモデル ↗ です。各エージェントは ID と耐久状態を持ち、メッセージで起動します。サーバー、ルーティング、ヘルスチェック、再起動ロジックを自分で管理する必要はありません。配置、スケール、回復はプラットフォームが担当します。
コストもこれに従います。
| VM / コンテナ | Durable Objects | |
|---|---|---|
| アイドルコスト | 常にフルのコンピュートコスト | ゼロ(休止中) |
| スケーリング | 容量をプロビジョニングして管理する | 自動、エージェントごと |
| 状態 | 外部データベースが必要 | 組み込み SQLite |
| 回復 | 自分で作る(プロセスマネージャ、ヘルスチェック) | プラットフォームが再起動し、状態は残る |
| ID / ルーティング | 自分で作る(ロードバランサー、スティッキーセッション) | 組み込み(名前からエージェントへ) |
| 1 万エージェント、各々稼働時間 1% | 常時稼働インスタンス 1 万 | 任意の時点で約 100 が稼働 |
エージェントは本質的にバーストし、状態を持ち、長寿命です。この組み合わせに自然に合います。
長時間稼働エージェントは、連続して動き続けるプロセスではありません。存在は連続しますが、実行は断続的です。長い時間軸で確実に動くエージェントを作るには、ライフサイクルの理解が必要です。
起動 → onStart() → イベント処理 → アイドル(約 2 分) → 休止
▲ │
└──────────── アラームまたはリクエストで起動 ──────────┘
エビクション(クラッシュ / 再デプロイ)はいつでも発生します。
状態は SQLite に残ります。次のイベントでエージェントが再起動します。this.state—setState()のたびに SQLite へ永続化されますthis.sqlのデータ — 作成したすべての SQLite テーブル- スケジュールされたタスク — SQLite に保存され、エージェントを起こすアラームを発火します
- 接続状態 — 各 WebSocket クライアントの
connection.setState()データ - fiber のチェックポイントと台帳 —
runFiber()のstash()データと、保持されたstartFiber()の状態行
SQLite 上に作った上位の抽象も、同じ耐久ストレージを共有するため残ります。
- メモリ上の変数 —
setState()やthis.sqlに保存していないクラスフィールド - 実行中のタイマー —
setTimeout、setIntervalは休止 / エビクションで失われます - 進行中の 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() / keepAliveWhile() |
| 分 | 再試行可能な受付が重要なときは startFiber() |
| 分から時間 | Workflows |
| 時間から日 | 非同期パターン: ジョブを開始し、休止し、完了で起動する |
エージェントはいつでもエビクションされます。デプロイ、プラットフォームの再起動、リソース制限です。作業の途中だった場合、チェックポイントがなければその作業は失われます。
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,
);
}
}
}パターンは次です。高価な作業の前にチェックポイントし、最後のチェックポイントから回復する。 自動リプレイではありません。回復の意味はドメインごとに自分で決めます。
FiberContext、FiberRecoveryContext、同時 fiber、インラインと fire-and-forget のパターンなど、API リファレンス全体は Durable Execution を参照してください。
プロジェクトマネージャは、1 回の起動よりはるかに長い作業をよく開始します。CI パイプラインは 20 分、デザインレビューは 1 日、動画アセットの生成は数時間です。エージェントは、このあいだ生き続けるべきではありません。代わりに作業を開始し、ジョブ ID を状態に残して休止します。結果が届くと(コールバック、ポーリング、Workflow 完了)、エージェントが起き、結果を突き合わせ、先へ進みます。
プロジェクトマネージャはタスク用の 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 に委譲し、再試行とステップ順序を任せます。
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 呼び出しは、途中でエビクションされると再開できない接続上でトークンをストリーミングします。
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 を使います。たとえば、回復したチャットターンが数分より古い場合は、部分回答は残しつつ自動継続はスキップし、古い応答でユーザーを驚かせない、といった判断ができます。
AIChatAgent と Think は常に耐久回復を使います。デフォルト経路は部分出力を残し、安全なときにターンを続行または再試行します。プロバイダーにより良い回復戦略があるときは 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 も、エージェント内部のプリミティブ(スケジュール、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 つずつ走らせ、あいだは休止します
- 反応する — webhook、メール、スケジュールで起きます
- 回復する — 中断後は最後のチェックポイントから再開します
- 委譲する — 作業をサブエージェントと Workflows へ渡します
- 維持する — 古いデータを刈り、完了作業をアーカイブし、自身のライフサイクルを管理します
- 終える — プロジェクト完了時に片付けて破棄します
このどれも、連続実行は不要です。存在していれば十分です。
- Durable Execution —
runFiber()、startFiber()、stash()、クラッシュ回復 - タスクのスケジュール — 遅延、cron、間隔タスク
- 再試行 — 再試行オプションとパターン
- Workflows — 耐久性のある複数ステップ処理
- 状態の保存と同期 —
setState()と永続化 - WebSockets — ライフサイクルフックと休止
- 呼び出し可能なメソッド —
@callableとサービスバインディングによる RPC - メールルーティング — 受信メール
- Webhooks — 外部イベントの受信
- ヒューマンインザループ — 承認フロー