构建可持续数天、数周或数月的 Agent——在重启后存活、按需唤醒,并管理远超单次请求时长的任务。
简要说明:
- Agent 是持久身份,而非始终在线的进程。
- 状态、SQL 数据、调度与 fiber 检查点在休眠与重启后仍然保留。
- 内存变量、定时器、进行中的 fetch 与本地闭包在驱逐后不会保留。
- 对以分钟计量的活跃工作使用
keepAlive();需要恢复的工作使用runFiber();调用方需要持久接受与状态时使用startFiber();重量级多步任务使用 Workflows。 - 当一个父 Agent 协调多个长期存活的子上下文时,使用子 agent。
Agent 大部分时间处于等待状态:等待用户输入(数秒到数天)、LLM 响应(数秒到数分钟)、工具结果(数秒到数小时)、人工审批(数小时到数天)或计划唤醒(数分钟到数月)。在传统虚拟机或容器上,你为所有这些空闲时间付费。一个 99% 休眠、1% 活跃的 Agent 仍要承担 100% 的服务器成本。
Durable Objects 反转了这一模型。Agent 作为可寻址实体存在并持有持久化状态,但休眠时零计算消耗。当有事发生——HTTP 请求、WebSocket 消息、计划闹钟、入站邮件——平台唤醒 Agent,从 SQLite 加载状态并交付事件。Agent 完成工作后再次休眠。
这就是 actor 模型 ↗:每个 Agent 有身份、持久状态,并在收到消息时唤醒。你无需管理服务器、路由、健康检查或重启逻辑,平台负责放置、扩展与恢复。
经济效益直接体现在下表:
| 虚拟机 / 容器 | Durable Objects | |
|---|---|---|
| 空闲成本 | 始终全额计算成本 | 零(已休眠) |
| 扩展 | 预配并管理容量 | 自动、按 Agent |
| 状态 | 需要外部数据库 | 内置 SQLite |
| 恢复 | 自行构建(进程管理器、健康检查) | 平台重启、状态保留 |
| 身份 / 路由 | 自行构建(负载均衡、粘性会话) | 内置(名称到 Agent) |
| 10,000 个 Agent,每个活跃 1% | 10,000 个始终在线实例 | 任意时刻约 100 个活跃 |
对本质上是突发、有状态且长期存活的 Agent 而言,这是自然契合。
长期运行 Agent 不是持续运行的进程,而是持续存在但间歇运行的实体。理解生命周期是构建在长时间尺度上可靠工作的 Agent 的关键。
Wake → onStart() → handle events → idle (~2 min) → hibernation
▲ │
└──────────────── alarm or request wakes agent ────────┘
Eviction(崩溃 / 重新部署)可能在任意时刻发生。
状态保留在 SQLite 中。Agent 在下次事件时重启。this.state— 每次setState()调用时持久化到 SQLitethis.sql数据 — 你创建的所有 SQLite 表- 定时任务 — 存储在 SQLite 中,触发闹钟唤醒 Agent
- 连接状态 — 每个 WebSocket 客户端的
connection.setState()数据 - Fiber 检查点与账本 —
runFiber()的stash()数据与保留的startFiber()状态行
基于 SQLite 构建的任何更高层抽象也会保留,因为它们共享同一持久存储。
- 内存变量 — 未通过
setState()或this.sql存储的 class 字段 - 运行中的定时器 —
setTimeout、setInterval在休眠/驱逐时丢失 - 进行中的 fetch 请求 — 进行中的 HTTP 调用会被放弃
- 本地闭包 — 回调与 promise 链会丢失
含义是:任何重要的工作都必须持久化或可恢复。SDK 提供调度、fiber、队列等原语,但理解「内存中」与「持久」之间的边界至关重要。
本文档逐步构建一个项目管理 Agent,它将:
- 在项目周期内存活(数周或数月)
- 跟踪任务、将工作分配给子 agent 并报告进度
- 按计划唤醒以检查截止日期并发送提醒
- 响应外部事件(来自 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<ProjectState> {
initialState: ProjectState = {
name: "",
status: "planning",
tasks: [],
plan: null,
};
}Plan 类型在将计划作为持久性策略中引入。我们将逐节为该 Agent 添加能力。
已休眠的 Agent 可由以下来源唤醒:
| 唤醒来源 | 工作方式 | 示例 |
|---|---|---|
| HTTP 请求 | 对 Agent URL 的任意请求触发 onRequest() |
来自 GitHub 的 webhook |
| WebSocket 连接 | 客户端连接时触发 onConnect() |
团队成员打开仪表板 |
| RPC 调用 | 另一个 Worker 或 Agent 通过 服务绑定 或 @callable 调用方法 |
协调 Agent 委派任务 |
| 定时闹钟 | 存储的调度触发,执行你的回调 | 每天上午 9 点的站会提醒 |
| 邮件 | 入站邮件触发 onEmail() |
团队成员回复状态邮件 |
该模式自然扩展到任何能到达 Worker 的事件源——从电话 webhook 到聊天平台 bot。外部信号到达,平台唤醒 Agent,Agent 处理它。
Agent 无需为每个唤醒来源单独「启动」或「部署」——它们都路由到同一 Durable Object 实例。Agent 的身份(其 name)即路由键。
export class ProjectManager extends Agent<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 ... */
}
}有时 Agent 需要执行超过空闲驱逐窗口(约 70–140 秒)的工作。流式 LLM 响应、编排多步工具链或等待慢速 API 都有 Agent 在进行中被驱逐的风险。
keepAlive() 通过创建重置不活动计时器的心跳来防止这种情况:
export class ProjectManager extends Agent<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 |
| 小时到天 | 异步模式:启动任务、休眠、完成时唤醒 |
Agent 可能在任意时刻被驱逐——部署、平台重启或达到资源限制。若 Agent 处于任务中途,除非已检查点,否则工作会丢失。
runFiber() 提供可崩溃恢复的执行。它在工作持续期间在 SQLite 中持久化一行,并允许 stash() 中间状态。若 Agent 被驱逐,fiber 行保留,下次激活时调用 onFiberRecovered()。
当重要边界是持久接受时,使用 startFiber()。它在同一 fiber 机制上增加幂等键、保留的状态记录、检查、取消与清理。默认在接受后返回;当请求应保持打开直至已接受任务达到终态时,传入 waitForCompletion: true。这适合提供商可能重试投递且 Agent 必须避免启动重复可见副作用的 webhook。
export class ProjectManager extends Agent<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,
);
}
}
}模式是:在昂贵工作前做检查点,从最后检查点恢复。 这不是自动重放——你决定恢复对领域意味着什么。
完整 API 参考——FiberContext、FiberRecoveryContext、并发 fiber、内联与即发即忘模式——请参阅 持久执行。
项目管理 Agent 经常启动远超单次激活时长的工作——CI 流水线运行 20 分钟、设计审查耗时一天、视频资源生成需数小时。Agent 不应为此保持存活。相反,它启动工作、在状态中持久化作业 ID 并休眠。当结果通过回调、轮询或 workflow 完成到达时,Agent 唤醒、关联结果并继续。
项目管理 Agent 为任务启动 CI 流水线。流水线耗时 20 分钟。Agent 不保持连接打开,而是注册自身 URL 作为回调并休眠:
export class ProjectManager extends Agent<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
}
}并非所有外部服务都支持回调。当项目管理 Agent 提交视频资源生成时,需要定期回查直至任务完成:
export class ProjectManager extends Agent<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,
});
}
}生产部署涉及多个需独立重试的步骤——构建、测试、预发布、提升。项目管理 Agent 不应在内部管理这些步骤;它委派给处理重试与步骤顺序的 Workflow:
export class ProjectManager extends Agent<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 唤醒项目管理 Agent。任务状态已更新。但接下来呢?若 Agent 使用 LLM 编排工作——决定下一个任务、起草状态报告、推理阻塞项——它需要接续该推理线程。原始 prompt、进行中的工具调用、思维链——全部从内存中消失。
这是长期运行 AI Agent 的根本难题。大多数框架假设工具调用在 LLM 超时内完成,并未直接解决此问题。
目前有三种可行方法:
重放完整对话历史。 AIChatAgent 将所有消息持久化到 SQLite。结果到达时,将其追加到历史并重新调用 LLM。这是最简单的方法,但会重新处理整个上下文窗口。
暂存续写摘要。 休眠前,持久化 Agent 正在做什么以及如何处理结果的紧凑描述:
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 构建聚焦 prompt,而非重放一切。
以计划作为上下文。 若 Agent 有结构化计划,计划本身提供足够上下文:「我在第 3/7 步,该步是『运行 CI 流水线』,结果刚到达。」这对长期运行 Agent 最稳健——计划既是恢复机制也是上下文重建策略。请参阅下一节。
结构化计划不仅用于向用户展示进度——它是持久性机制。有计划的 Agent 可通过查看中断位置从任意中断恢复。
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<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(),
},
});
}
}
}该模式对长期运行 Agent 有多项优势:
- 恢复简单 — 重启时检查
plan.currentStep并继续 - 进度可见 — 客户端可见哪些步骤完成、下一步是什么
- 可重新规划 — 若某步失败或需求变更,Agent 可修订剩余步骤而不丢失已完成工作
- 人工监督 — 计划是自然的审批检查点(「我将要做这些——继续吗?」)
- 上下文重建 — 计划告诉 LLM 当前位置、已发生的事与下一步,无需重放完整对话
项目管理 Agent 不会包办一切。它将专业工作委派给子 agent——每个都有独立身份、状态与生命周期。
export class ProjectManager extends Agent<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;
}
}子 agent 有独立状态、调度、持久 fiber 与生命周期。它们与父级同处,但每个子级存储自己的 SQLite 数据并以子级为 this 运行回调。
由于 facet 没有独立闹钟槽,顶层父级拥有物理 Durable Object 闹钟。Agents SDK 记录哪个子 agent 拥有每个调度或 fiber 恢复租约,唤醒父级并将回调路由回子级。父级无需在子 agent 工作时保持活跃——可启动工作、休眠,并由子级拥有的调度或恢复检查唤醒。
完整 subAgent() API——带类型的 RPC stub、客户端路由、访问控制、存储隔离与闹钟支持的 API——请参阅 子 agent。AI 专用子 agent 流式传输(通过子 agent 运行完整 LLM 轮次)请参阅 Think:子 agent RPC。
上述模式处理项目管理 Agent 的协调工作——调度、委派、轮询。但项目管理 Agent 也直接使用 LLM:生成计划、汇总进度、起草状态邮件。这些 LLM 调用在连接上流式传输 token,若 Agent 在响应中途被驱逐则无法恢复。
对于基于 AIChatAgent 的聊天导向 Agent,问题更尖锐——用户实时观看响应流,会看到中途停住。chatRecovery 将每个聊天轮次包装在 runFiber 中,流式传输期间提供自动 keepAlive,Agent 重启时提供恢复钩子:
import { AIChatAgent } from "@cloudflare/ai-chat";
import type {
ChatRecoveryContext,
ChatRecoveryOptions,
} from "@cloudflare/ai-chat";
class ProjectChat extends AIChatAgent<Env> {
override chatRecovery = true;
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 提供商:
| 提供商 | 策略 | 工作方式 | Token 成本 |
|---|---|---|---|
| Workers AI | 从部分内容继续 | continueLastTurn() — 模型通过 assistant 预填充继续 |
低 |
| OpenAI (Responses API) | 检索已完成响应 | 流式传输期间 stash responseId,恢复时检索 |
零 |
| Anthropic | 合成续传 | 持久化部分内容,发送合成用户消息请求模型继续 | 中 |
| 其他 | 尝试预填充,回退到合成 | 提供商支持时用 continueLastTurn(),否则用合成消息 |
不定 |
使用 ctx.createdAt 抑制过期恢复。例如,若恢复的聊天轮次已超过数分钟,可持久化部分答案但跳过自动续传,避免用旧响应惊扰用户。
Think 默认启用 chatRecovery。默认路径持久化部分输出,并在安全时自动继续或重试轮次,许多应用无需自定义钩子。当提供商有更好的恢复策略时重写 onChatRecovery,或配置 chatRecovery = { maxAttempts, terminalMessage, onExhausted } 调整终端用户体验。
若在写入任何 assistant 流分块前 Agent 被中断,则没有可继续的部分 assistant 消息。当最新持久化消息仍是该轮次未回答的用户消息时,聊天恢复自动重试轮次,除非 onChatRecovery 返回 { continue: false }。
运行数月的 Agent 会累积数据:对话历史、时间线事件、已完成任务、调度记录。不管理会无限增长。
安排定期清理以修剪旧数据并归档已完成工作:
export class ProjectManager extends Agent<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 的 Agent,对话历史在长期运行中会变大。不管理的话,3 个月对话会在项目结束前耗尽 LLM 上下文窗口。
管理对话大小的策略:
- 滑动窗口 — 仅在活跃上下文中保留最后 N 条消息。简单可预测。
- 摘要 — 定期摘要较旧消息并用紧凑摘要替换。原始消息可保留在 SQLite 供审计。
- 选择性保留 — 保留含决策、审批与关键上下文的消息,修剪常规往来。
长期运行 Agent 最终会完成使命。项目交付、调查结束、监控窗口关闭。请显式清理:
export class ProjectManager extends Agent<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() 是永久性的。若日后可能需要 Agent 数据,销毁前归档到外部存储(R2、D1 或 API 调用)。可能被重新激活的 Agent,只需标记为完成并休眠——空闲时零成本。
Workflows 与 agent 内部原语(调度、fiber、队列)都支持长期工作。正确选择取决于工作性质:
| Agent 内部 | Workflows | |
|---|---|---|
| 最适合 | 以 Agent 为中心的工作:调度、轮询、状态更新 | 独立多步流水线 |
| 持久性 | SQLite(经受驱逐) | Workflow 引擎(经受一切) |
| 重试 | this.retry()、调度级重试 |
每步带退避的重试 |
| 最大时长 | 每次激活数分钟(配合 keepAlive) |
每步 30 分钟、步骤数不限 |
| 人工审批 | 自行构建(状态 + WebSocket) | 内置 waitForApproval() |
| 复杂度 | 较低——一切在 Agent 内 | 较高——独立 class、wrangler 配置 |
实用规则:若工作是 Agent 管理自身生命周期(检查截止日期、同步状态、发送提醒),用调度与 fiber。若工作是可独立失败并重试的离散流水线(部署、数据处理、报告生成),用 Workflow。
项目管理 Agent 两者都用:调度用于自身节奏(每日站会、进度同步),Workflow 用于重量级操作(部署、CI 流水线)。
Cloudflare 上的长期运行 Agent 不是长期运行进程。它们是持久实体,唤醒、工作、休眠——可能持续数周或数月。关键原语:
| 原语 | 用途 |
|---|---|
setState() / this.sql |
跨激活持久化状态 |
schedule() / scheduleEvery() |
在未来时间唤醒 Agent |
keepAlive() / keepAliveWhile() |
活跃工作期间防止驱逐 |
runFiber() / stash() |
检查点并恢复长任务 |
startFiber() |
持久接受、检查与取消任务 |
chatRecovery |
恢复中断的 LLM 流 |
onRequest() / onEmail() / RPC |
由外部事件唤醒 |
runWorkflow() |
委派重量级多步工作 |
subAgent() |
将专业工作委派给子 agent |
| 状态中的结构化计划 | 启用恢复、可见性与重新规划 |
对项目管理 Agent,这些组合成一个 Agent,它将:
- 规划 — 将目标分解为步骤,在状态中持久化计划
- 执行 — 逐步运行,步骤间休眠
- 响应 — 由 webhook、邮件与调度唤醒
- 恢复 — 任意中断后从最后检查点继续
- 委派 — 将工作交给子 agent 与 Workflow
- 维护 — 修剪旧数据、归档已完成工作、管理自身生命周期
- 结束 — 项目完成时清理并销毁自身
Agent 无需持续运行即可完成以上一切,只需存在。