← Создание агентов на Cloudflare / agents / concepts / agentic-patterns
Долгоживущие агенты
Создавайте агентов, которые сохраняются днями, неделями или месяцами: переживают перезапуски, просыпаются по требованию и управляют работой, которая длится намного дольше одного запроса.
Коротко:
- Agents представляют собой устойчивые сущности, а не постоянно работающие процессы.
- Состояние, данные SQL, расписания и контрольные точки fiber переживают гибернацию и перезапуски.
- Переменные в памяти, таймеры, открытые fetch-запросы и локальные closures не переживают вытеснение.
- Используйте
keepAlive()для активной работы, измеряемой в минутах,runFiber()когда работе требуется восстановление,startFiber()когда вызывающему коду нужны устойчивый приём и статус, а для тяжеловесных многошаговых задач использовать Workflows. - Используйте дочерних агентов, если один родительский агент координирует множество долгоживущих дочерних контекстов.
Почему Cloudflare подходит для долгоживущих агентов
Agents большую часть времени проводят в ожидании: ввода пользователя (от секунд до дней), ответов LLM (от секунд до минут), результатов инструментов (от секунд до часов), одобрений человека (от часов до дней) или запланированных пробуждений (от минут до месяцев). На обычной виртуальной машине или в контейнере вы платите за всё это время простоя. Агент, который на 99% бездействует и лишь на 1% активен, всё равно обходится вам в 100% стоимости сервера.
Durable Objects переворачивают эту модель. Агент существует как адресуемая сущность с постоянным состоянием, но в гибернации не потребляет вычислительных ресурсов. Как только что-то происходит, будь то HTTP-запрос, сообщение WebSocket, запланированный alarm или входящее письмо, платформа пробуждает агента, загружает его состояние из SQLite и передаёт ему это событие. Агент выполняет свою работу и снова засыпает.
Это модель акторов ↗: у каждого агента есть идентификатор, устойчивое состояние, и он пробуждается при получении сообщения. Вам не нужно управлять серверами, маршрутизацией, проверками работоспособности или логикой перезапуска. Платформа берёт на себя размещение, масштабирование и восстановление.
Экономика вытекает из этого напрямую:
| Виртуальные машины / контейнеры | Durable Objects | |
|---|---|---|
| Стоимость простоя | Полная стоимость вычислений, всегда | Ноль (в гибернации) |
| Масштабирование | Выделение и управление ёмкостью | Автоматически, для каждого агента |
| Состояние | Требуется внешняя база данных | Встроенный SQLite |
| Восстановление | Вы создаёте это самостоятельно (менеджеры процессов, проверки работоспособности) | Перезапуски платформы, состояние сохраняется |
| Идентификация / маршрутизация | Вы создаёте это самостоятельно (балансировщики нагрузки, липкие сессии) | Встроенное (сопоставление имени и агента) |
| 10,000 агентов, каждый активен 1% времени | 10,000 постоянно работающих инстансов | ~100 активных в любой момент |
Для агентов, которые по своей природе неравномерны по нагрузке, обладают состоянием и работают длительное время, это естественное решение.
Жизненный цикл долгоживущего агента
Долгоживущий агент не является процессом, который выполняется непрерывно. Это сущность, которая существует непрерывно, но запуски периодически. Понимание жизненного цикла необходимо для создания агентов, которые надёжно работают на длинных временных горизонтах.
Wake → onStart() → handle events → idle (~2 min) → hibernation
▲ │
└──────────────── alarm or request wakes agent ────────┘
Eviction (crash / redeploy) can happen at any point.
State persists in SQLite. Agent restarts on next event.Что сохраняется
this.state: сохраняется в SQLite при каждомsetState()вызовthis.sqlданные : все создаваемые вами таблицы SQLite- Запланированные задачи : хранятся в SQLite, запускают будильники, которые пробуждают агента
- Состояние подключения,
connection.setState()данные для каждого клиента WebSocket - Контрольные точки и реестры Fiber,
stash()данные изrunFiber()и сохранённыйstartFiber()строки статуса
Любые высокоуровневые абстракции поверх SQLite тоже сохраняются, поскольку используют то же долговременное хранилище.
Что не сохраняется
- Переменные в памяти : поля класса, не сохранённые через
setState()илиthis.sql - Работающие таймеры,
setTimeout,setIntervalтеряются при гибернации или вытеснении - Открытые fetch-запросы : незавершённые вызовы HTTP отбрасываются
- Локальные замыкания : колбэки и цепочки promise теряются
Вывод таков: любая важная работа должна сохраняться или быть восстанавливаемой. SDK предоставляет для этого примитивы: расписания, fiber и очереди, но принципиально важно понимать границу между «in-memory» и «durable».
Сквозной пример: агент проектного менеджера
На протяжении этого документа мы пошагово создаём агента управления проектами, который:
- Существует в течение всего проекта (недели или месяцы)
- Отслеживает задачи, распределяет работу между дочерними агентами и сообщает о ходе выполнения
- Просыпается по расписанию, чтобы проверить сроки и отправить напоминания
- Реагирует на внешние события (вебхуки от GitHub, письма от участников команды)
- Обрабатывает долгие операции (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 |
| Соединение WebSocket | Клиент подключается, что запускает onConnect() |
Участник команды открывает дашборд |
| RPC-вызов | Другой Worker или агент вызывает метод через привязка к сервису или @callable |
Координирующий агент делегирует задачу |
| Запланированный alarm | Сохранённое расписание срабатывает, запуская ваш колбэк | Напоминание о ежедневном стендапе в 9:00 |
| Электронная почта | Входящее письмо запускает onEmail() |
Участник команды отвечает на письмо со статусом |
Этот паттерн естественным образом распространяется на любой источник событий, который может обратиться к Worker: от вебхуков телефонии до ботов на чат-платформах. Приходит внешний сигнал, платформа пробуждает агента, и агент его обрабатывает.
Агенту не нужно отдельно «запускаться» или «разворачиваться» для каждого источника пробуждения: все они ведут к одному и тому же экземпляру Durable Object. Ключом маршрутизации служит идентичность агента, то есть его имя.
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() предотвращает это, создавая heartbeat, который сбрасывает таймер бездействия:
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() это рекомендуемый подход: он гарантирует, что heartbeat будет остановлен по завершении работы (или при исключении). Для ручного управления keepAlive() возвращает disposer:
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 когда запрос должен оставаться открытым, пока принятое задание не достигнет конечного статуса. Это хорошо подходит для вебхуков, где провайдер может повторять доставку, а агент должен избегать повторного запуска видимых побочных эффектов.
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,
);
}
}
}Паттерн такой: создать контрольную точку перед затратной работой, восстановиться из последней контрольной точки. Это не автоматическое воспроизведение: вы сами определяете, что означает восстановление в вашей предметной области.
Полную справочную информацию по API см. здесь: FiberContext, FiberRecoveryContext, параллельные fiber, паттерны inline и fire-and-forget: подробнее см. Durable Execution.
Обработка длительных асинхронных операций
Проджект-менеджер часто запускает работу, которая занимает намного больше времени, чем длится одна активация: CI-пайплайн выполняется 20 минут, ревью дизайна занимает день, а генерация видео растягивается на часы. Агенту не нужно оставаться активным всё это время. Вместо этого он запускает работу, сохраняет 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
Развёртывание в продакшене включает несколько шагов, каждый из которых должен повторяться независимо: сборка, тестирование, staging, продвижение. Менеджер проекта не должен управлять этими шагами самостоятельно, он делегирует это 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, и не решают эту проблему напрямую.
Сегодня работают три подхода:
Воспроизведите всю историю разговора. 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 для построения целенаправленного промпта вместо повторного воспроизведения всего подряд.
Используйте план в качестве контекста. Если у агента есть структурированный план, сам план даёт достаточный контекст: "Я нахожусь на шаге 3 из 7, шаг назывался 'run CI pipeline', результат только что получен". Это самый надёжный подход для длительно работающих агентов: план служит одновременно механизмом восстановления и стратегией восстановления контекста. См. следующий раздел.
Планирование как стратегия устойчивости
Структурированный план полезен не только для того, чтобы показывать пользователям прогресс: он также служит механизмом устойчивости. Агент с планом может восстановиться после любого прерывания, посмотрев, на чём он остановился.
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, где он находится, что произошло и что делать дальше, без необходимости заново воспроизводить весь разговор
Делегирование дочерним агентам
Менеджер проекта не делает всё сам. Он делегирует специализированную работу дочерним агентам, у каждого из которых есть собственная идентичность, состояние и жизненный цикл.
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.
Поскольку у граней нет собственных слотов alarm, физический alarm Durable Object принадлежит родительскому агенту верхнего уровня. Agents SDK отслеживает, какому дочернему агенту принадлежит каждое расписание или право на восстановление fiber, пробуждает родителя и направляет callback обратно потомку. Родителю не обязательно оставаться активным, пока работает дочерний агент: он может запустить работу, перейти в гибернацию и быть разбуженным расписанием или проверкой восстановления, которыми владеет потомок.
Полный subAgent() API: типизированные заглушки RPC, маршрутизация клиентов, контроль доступа, изоляция хранилища и API на основе alarm. См. Дочерние агенты. Для стриминга суб-агентов, специфичного для ИИ (выполнение полных ходов LLM через дочернего агента), см. Think: RPC дочернего агента.
Восстановление прерванных потоков LLM
Паттерны выше решают задачи координации, которые выполняет проджект-менеджер: планирование, делегирование, опрос статуса. Но проджект-менеджер также напрямую использует LLM: строит планы, суммирует прогресс, готовит черновики отчётных писем. Эти вызовы LLM передают токены потоком по соединению, которое нельзя восстановить, если агент был вытеснен в середине ответа.
Для чат-ориентированных агентов, построенных на AIChatAgent или Think, это ещё более острая проблема: пользователь наблюдает за потоком ответа в реальном времени и видит, как он обрывается на середине предложения. Устойчивое восстановление оборачивает каждый ход чата в runFiber. Это обеспечивает автоматическую keepAlive во время передачи потоком и recovery hook при перезапуске агента:
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() : модель продолжает работу через assistant prefill |
Низкая |
| OpenAI (Responses API) | Получить завершённый ответ | Stash responseId во время передачи потоком, получение при восстановлении |
Ноль |
| Anthropic | Синтетическое продолжение | Сохраните частичный результат и отправьте синтетическое сообщение пользователя с просьбой к модели продолжить | Средняя |
| Прочее | Сначала пробуйте prefill, затем переходите к synthetic | continueLastTurn() если это поддерживает провайдер, иначе синтетическое сообщение |
Варьируется |
Используйте ctx.createdAt чтобы подавлять устаревшие восстановления. Например, если восстановленный ход чата старше нескольких минут, можно сохранить частичный ответ, но пропустить автоматическое продолжение, чтобы не удивлять пользователя устаревшим ответом.
AIChatAgent и Think всегда используют устойчивое восстановление. Путь по умолчанию сохраняет частичный вывод и продолжает или повторяет ход, когда это безопасно. Переопределите onChatRecovery когда у провайдера есть более эффективная стратегия восстановления. Настройте chatRecovery = { maxAttempts, terminalMessage, onExhausted } чтобы настроить работу в терминале.
Если агент прерывается до того, как записан хотя бы один чанк потока ответа ассистента, продолжать частичное сообщение ассистента невозможно, поскольку его не существует. Если последнее сохранённое сообщение по-прежнему остаётся сообщением пользователя без ответа из этого хода, восстановление чата автоматически повторяет ход, если только 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, история разговора может сильно разрастись за долгий срок жизни. Без управления трёхмесячный разговор исчерпает контекстное окно 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, очереди) поддерживают долго выполняющуюся работу. Правильный выбор зависит от характера самой работы:
| Внутренний для Agent | Workflows | |
|---|---|---|
| Подходит для | Работа, ориентированная на Agent: планирование, опрос, обновления состояния | Независимые многоэтапные конвейеры |
| Долговечность | SQLite (переживает вытеснение) | Движок Workflow (переживает всё) |
| Повторы | this.retry(), повторные попытки на уровне расписания |
Повторные попытки на каждом шаге с задержкой |
| Максимальная длительность | Минуты на активацию (с keepAlive) |
30 минут на шаг, количество шагов не ограничено |
| Подтверждение человеком | Сделайте сами (состояние + WebSocket) | Встроенный waitForApproval() |
| Сложность | Ниже: всё внутри агента | Выше: отдельный класс, конфигурация Wrangler |
Практическое правило: если речь идёт о том, что агент управляет собственным жизненным циклом (проверяет сроки, синхронизирует состояние, отправляет напоминания), используйте расписания и fiber. Если речь идёт об отдельном конвейере, который может завершиться ошибкой и повторяться независимо (развёртывание, обработка данных, генерация отчётов), используйте Workflow.
Агент-проджект-менеджер использует оба механизма: расписания для собственных ритмов (ежедневные стендапы, синхронизации прогресса) и Workflows для тяжёлых операций (развёртывания, CI-пайплайны).
Сводка
Долгоживущие агенты на Cloudflare представляют собой не долгоживущие процессы, а устойчивые сущности, которые просыпаются, работают и засыпают, порой на протяжении недель или месяцев. Ключевые примитивы:
| Примитив | Назначение |
|---|---|
setState() / this.sql |
Сохранение состояния между активациями |
schedule() / scheduleEvery() |
Пробудите агента в определённый момент в будущем |
keepAlive() / keepAliveWhile() |
Предотвращение вытеснения во время активной работы |
runFiber() / stash() |
Создайте контрольную точку и восстановите длительные задачи |
startFiber() |
Устойчиво принимайте, проверяйте и отменяйте задания |
chatRecovery |
Восстанавливайте прерванные потоки LLM |
onRequest() / onEmail() / RPC |
Пробуждение при внешних событиях |
runWorkflow() |
Делегируйте объёмную многошаговую работу |
subAgent() |
Делегируйте специализированную работу дочерним агентам |
| Структурированные планы в состоянии | Включите восстановление, видимость и повторное планирование |
Для агента менеджера проекта эти элементы складываются в агента, который:
- Планы : разбивает цели на шаги, сохраняет план в состоянии
- Выполняет : выполняет шаги по одному, переходя в гибернацию между ними
- Реагирует : просыпается по вебхукам, письмам и расписаниям
- Восстанавливает : возобновляется с последней контрольной точки после любого прерывания
- Делегирует : передаёт работу дочерним агентам и Workflows
- Поддерживает : обрезает старые данные, архивирует завершённую работу, управляет собственным жизненным циклом
- Конец : очищается и уничтожает себя после завершения проекта
Агенту не нужно работать непрерывно, чтобы всё это делать. Достаточно того, что он существует.
См. также
- Durable Execution,
runFiber(),startFiber(),stash(), и восстановление после сбоя - Планирование задач : отложенные, cron и периодические задачи
- Повторы : параметры и паттерны повторных попыток
- Workflows : устойчивая многошаговая обработка
- Хранение и синхронизация состояния,
setState()и сохранение состояния - WebSockets : хуки жизненного цикла и гибернация
- Вызываемые методы : RPC через
@callableи service bindings - Маршрутизация электронной почты : приём входящей почты
- Webhooks : приём внешних событий
- Человек в цикле : процессы подтверждения