← Создание агентов на Cloudflare / agents / runtime / execution
Дочерние агенты
Порождайте дочерних агентов в виде совместно размещённых Durable Objects с собственным изолированным хранилищем SQLite. Родитель получает типизированную RPC-заглушку для вызова методов дочернего агента: каждый публичный метод класса потомка становится доступен как удалённый вызов процедуры с типами возврата, обёрнутыми в Promise.
Используйте дочерних агентов, если один пользователь или сущность владеет открытым набором долгоживущих агентов, таких как чаты, документы, сессии, шарды или проекты. Каждый дочерний агент выполняется параллельно с собственным состоянием, пока родительский агент координирует обнаружение, контроль доступа и жизненный цикл.
Если вы хотите, чтобы родительский чат-агент в рамках одного хода запускал другого агента с поддержкой чата и отображал прогресс этого дочернего агента прямо в потоке, используйте Agents как инструменты. Агенты как инструменты строятся на основе суб-агентов, но добавляют реестр запусков на стороне родителя, стриминг agent-tool-event фреймы, повтор, отмену и очистку.
быстрый старт
import { Agent } from "agents";
export class Orchestrator extends Agent {
async delegateWork() {
const researcher = await this.subAgent(Researcher, "research-1");
const findings = await researcher.search("cloudflare agents sdk");
return findings;
}
}
export class Researcher extends Agent {
async search(query) {
const results = await fetch(`https://api.example.com/search?q=${query}`);
return results.json();
}
}import { Agent } from "agents";
export class Orchestrator extends Agent {
async delegateWork() {
const researcher = await this.subAgent(Researcher, "research-1");
const findings = await researcher.search("cloudflare agents sdk");
return findings;
}
}
export class Researcher extends Agent {
async search(query: string) {
const results = await fetch(`https://api.example.com/search?q=${query}`);
return results.json();
}
}Оба класса должны экспортироваться из точки входа Worker. Для классов, предназначенных только для потомков, отдельные привязки Durable Object не нужны: классы потомков определяются автоматически через ctx.exports.
{
"$schema": "./node_modules/wrangler/config-schema.json",
// Set this to today's date
"compatibility_date": "2026-08-28",
"compatibility_flags": [
"nodejs_compat"
],
"durable_objects": {
"bindings": [
{
"class_name": "Orchestrator",
"name": "Orchestrator"
}
]
},
"migrations": [
{
"new_sqlite_classes": [
"Orchestrator"
],
"tag": "v1"
}
]
}# Set this to today's date
compatibility_date = "2026-08-28"
compatibility_flags = ["nodejs_compat"]
[[durable_objects.bindings]]
class_name = "Orchestrator"
name = "Orchestrator"
[[migrations]]
new_sqlite_classes = ["Orchestrator"]
tag = "v1"Только агенту верхнего уровня, родителю, нужны привязка и миграция Durable Object. Дочерние агенты создаются как грани родителя: они используют ту же машину, но имеют полностью изолированное хранилище SQLite.
subAgent
Получить или создать именованного дочернего агента. Первый вызов для заданного имени запускает у дочернего агента onStart(). Последующие вызовы возвращают существующий экземпляр.
class Agent {}class Agent {
async subAgent<T extends Agent>(
cls: SubAgentClass<T>,
name: string,
): Promise<SubAgentStub<T>>;
}| Параметр | Тип | Описание |
|---|---|---|
cls |
SubAgentClass<T> |
Подкласс Agent. Должен экспортироваться из точки входа Worker, а имя экспорта должно совпадать с именем класса. |
name |
string |
Уникальное имя для этого дочернего экземпляра. Одно и то же имя всегда возвращает один и тот же дочерний экземпляр. |
Возвращает SubAgentStub<T> : типизированная RPC-заглушка, где каждый определённый пользователем метод у T доступен как удалённый вызов, возвращающий Promise.
SubAgentStub
Stub предоставляет доступ ко всем публичным методам экземпляра, которые вы определяете в дочернем классе. Методы, унаследованные от Agent (хуки жизненного цикла, setState, broadcast, sql, и так далее) исключены: на stub отображаются только ваши собственные методы.
Типы возврата автоматически оборачиваются в Promise если это ещё не так:
class MyChild extends Agent {
greet(name) {
return `Hello, ${name}`;
}
async fetchData(url) {
return fetch(url).then((r) => r.json());
}
}
// On the stub:
// greet(name: string) => Promise<string> (sync → wrapped)
// fetchData(url: string) => Promise<unknown> (already async → unchanged)class MyChild extends Agent {
greet(name: string): string {
return `Hello, ${name}`;
}
async fetchData(url: string): Promise<unknown> {
return fetch(url).then((r) => r.json());
}
}
// On the stub:
// greet(name: string) => Promise<string> (sync → wrapped)
// fetchData(url: string) => Promise<unknown> (already async → unchanged)Требования
- Дочерний класс должен расширять
Agent - Дочерний класс должен быть экспортирован из точки входа worker (
export class MyChild extends Agent) - Имя экспорта должно совпадать с именем класса:
export { Foo as Bar }не поддерживается - Класс верхнеуровневого родителя должен быть привязан как Durable Object namespace в
wrangler.jsonc - Дочерний класс, состоящий только из граней, не нужно регистрировать под
new_sqlite_classesесли только тот же класс не привязан где-то ещё как Durable Object верхнего уровня - Вложенным родительским граням не нужны собственные привязки Durable Object верхнего уровня; рантайм разрешает вложенные дочерние грани через пространство имён корневого родителя
- Имя дочернего класса не может быть
Sub, потому что/sub/зарезервирован как разделитель URL для вложенных маршрутов
Заметки для тестирования
Тесты, которые используют @cloudflare/vitest-plugin может понадобиться указать классы граней как тестовые привязки Durable Object, чтобы ctx.exports предоставляет значение класса, совместимое с гранью. Не держите такие классы граней в new_sqlite_classes; дополнительная привязка относится только к тестовой wrangler.jsonc файлов и не является обязательным требованием для продакшен-Worker.
abortSubAgent
Принудительно остановите работающего дочернего агента. Потомок немедленно прекращает выполнение и перезапускается при следующем subAgent() вызов. Хранилище сохраняется: завершается только запущенный экземпляр.
class Agent {}class Agent {
abortSubAgent(cls: SubAgentClass, name: string, reason?: unknown): void;
}| Параметр | Тип | Описание |
|---|---|---|
cls |
SubAgentClass |
Подкласс Agent, используемый при создании дочернего агента |
name |
string |
Имя дочернего элемента, который нужно прервать |
reason |
unknown |
Ошибка, передаваемая ожидающим или будущим вызывающим RPC |
Прерывание транзитивно: если у дочернего агента есть собственные sub-agents, они тоже прерываются.
deleteSubAgent
Прервите дочерний агент (если он выполняется) и полностью очистите его хранилище. Следующий subAgent() вызов создаёт новый экземпляр с пустой SQLite.
class Agent {}class Agent {
deleteSubAgent(cls: SubAgentClass, name: string): void;
}| Параметр | Тип | Описание |
|---|---|---|
cls |
SubAgentClass |
Подкласс Agent, используемый при создании дочернего агента |
name |
string |
Имя дочернего элемента, который нужно удалить |
Удаление транзитивно: собственные дочерние агенты потомка также удаляются.
Интроспекция и контроль доступа
hasSubAgent
Проверяет, был ли создан дочерний агент и не удалён ли он. В основе лежит реестр SQLite, который поддерживается фреймворком.
if (!this.hasSubAgent(Chat, id)) {
return new Response("Not found", { status: 404 });
}if (!this.hasSubAgent(Chat, id)) {
return new Response("Not found", { status: 404 });
}listSubAgents
Выводит список порождённых дочерних агентов, опционально отфильтрованных по классу. Строки возвращаются в порядке создания.
const chats = this.listSubAgents(Chat);
// [{ className: "Chat", name: "chat-abc", createdAt: 1700000000000 }]const chats = this.listSubAgents(Chat);
// [{ className: "Chat", name: "chat-abc", createdAt: 1700000000000 }]onBeforeSubAgent
Переопределите этот хук middleware на родительском агенте, чтобы ограничивать доступ, изменять или прерывать входящие /sub/ запросов, прежде чем фреймворк разбудит дочерний агент. Он повторяет onBeforeConnect и onBeforeRequest.
Hook может возвращать:
| Возвращаемое значение | Эффект |
|---|---|
void |
Пересылка исходного запроса потомку |
Request |
Пересылка изменённого запроса |
Response |
Short-circuit и не пробуждайте дочерний процесс |
export class Inbox extends Agent {
async onBeforeSubAgent(_request, { className, name }) {
// Strict registry gate: only allow clients to reach chats that were created.
if (!this.hasSubAgent(className, name)) {
return new Response(`${className} "${name}" not found`, {
status: 404,
});
}
}
}export class Inbox extends Agent {
override async onBeforeSubAgent(_request, { className, name }) {
// Strict registry gate: only allow clients to reach chats that were created.
if (!this.hasSubAgent(className, name)) {
return new Response(`${className} "${name}" not found`, {
status: 404,
});
}
}
}Запросы на обновление протокола до WebSocket проходят через этот хук так же, как обычные HTTP-запросы. Если вы возвращаете изменённый Request, сохраняйте исходные заголовки апгрейда WebSocket.
Идентичность родительского и дочернего агента
Дочерние агенты узнают своего родителя через this.parentPath и this.selfPath.
// Inside a Chat spawned by Inbox:
this.parentPath;
// [{ className: "Inbox", name: "user-123" }]
this.selfPath;
// [
// { className: "Inbox", name: "user-123" },
// { className: "Chat", name: "chat-abc" }
// ]// Inside a Chat spawned by Inbox:
this.parentPath;
// [{ className: "Inbox", name: "user-123" }]
this.selfPath;
// [
// { className: "Inbox", name: "user-123" },
// { className: "Chat", name: "chat-abc" }
// ]parentPath идёт от корня, поэтому непосредственным родителем всегда является parentPath.at(-1). Агенты верхнего уровня имеют parentPath === [].
Используйте parentAgent(Cls) из дочернего агента, чтобы получить типизированную RPC-заглушку для его непосредственного родителя:
const inbox = await this.parentAgent(Inbox);
await inbox.recordTurn(this.name, "...");const inbox = await this.parentAgent(Inbox);
await inbox.recordTurn(this.name, "...");parentAgent() разрешает прямого родителя, даже если этот родитель сам является sub-agent, состоящим только из граней, используя под капотом RPC-мост на стороне root. Это даёт типизированные вызовы методов непосредственного родителя без необходимости привязывать каждый вложенный родительский класс как Durable Object верхнего уровня.
Для прародителей и более дальних предков перебирайте this.parentPath и вызвать getAgentByName() напрямую. Если имя привязки не совпадает с именем класса, вызовите getAgentByName(env.MY_BINDING, this.parentPath.at(-1)!.name) вместо parentAgent().
Клиентская маршрутизация
useAgent({ sub })
Расширить любой useAgent вызов с sub цепочку для подключения к дочерней грани:
const chat = useAgent({
agent: "Inbox",
name: userId,
sub: [{ agent: "Chat", name: chatId }],
});const chat = useAgent({
agent: "Inbox",
name: userId,
sub: [{ agent: "Chat", name: chatId }],
});Hook формирует URL вида /agents/inbox/user-123/sub/chat/chat-abc и открывает прямое соединение WebSocket с Chat потомок. Каждый следующий useAgent функция работает как обычно: синхронизация состояния, stub вызовы, @callable RPC и useAgentChat поверх возвращённого сокета.
Пользовательская маршрутизация HTTP
Для fetch-обработчиков, которые самостоятельно разбирают URL на верхнем уровне, используйте routeSubAgentRequest() чтобы передать запрос в под-агента из уже разрешённой родительской заглушки:
import { getAgentByName, routeSubAgentRequest } from "agents";
export default {
async fetch(request, env) {
const url = new URL(request.url);
const match = url.pathname.match(/^\/api\/u\/([^/]+)(\/.*)$/);
if (!match) return new Response("Not found", { status: 404 });
const [, userId, rest] = match;
const parent = await getAgentByName(env.Inbox, userId);
return routeSubAgentRequest(request, parent, { fromPath: rest });
},
};import { getAgentByName, routeSubAgentRequest } from "agents";
export default {
async fetch(request: Request, env: Env) {
const url = new URL(request.url);
const match = url.pathname.match(/^\/api\/u\/([^/]+)(\/.*)$/);
if (!match) return new Response("Not found", { status: 404 });
const [, userId, rest] = match;
const parent = await getAgentByName(env.Inbox, userId);
return routeSubAgentRequest(request, parent, { fromPath: rest });
},
};fromPath берёт хвост дочернего агента, например /sub/chat/chat-abc. Вспомогательная функция разбирает его и запускает родительский onBeforeSubAgent хук и перенаправляет запрос в грань.
Внешний типизированный RPC
Изнутри родительского Durable Object this.subAgent(Cls, name) возвращает типизированный stub. Снаружи родителя используйте getSubAgentByName():
import { getAgentByName, getSubAgentByName } from "agents";
const inbox = await getAgentByName(env.Inbox, userId);
const chat = await getSubAgentByName(inbox, Chat, chatId);
await chat.addMessage({ role: "user", content: "hello" });import { getAgentByName, getSubAgentByName } from "agents";
const inbox = await getAgentByName(env.Inbox, userId);
const chat = await getSubAgentByName(inbox, Chat, chatId);
await chat.addMessage({ role: "user", content: "hello" });getSubAgentByName() возвращает прокси только для RPC. Вызовы методов работают, но .fetch() выбрасывает исключение. Используйте routeSubAgentRequest() для пересылки HTTP и WebSocket.
Изоляция хранилища
Каждый дочерний агент имеет собственную базу данных SQLite, полностью изолированную от родителя и от других дочерних агентов. Родительский агент, записывающий в this.sql и дочерний узел, записывающий в this.sql работают с разными базами данных:
export class Parent extends Agent {
async demonstrate() {
this.sql`INSERT INTO parent_data (key, value) VALUES ('color', 'blue')`;
const child = await this.subAgent(Child, "child-1");
await child.increment("clicks");
// Parent's SQL and child's SQL are completely separate
}
}
export class Child extends Agent {
async increment(key) {
this
.sql`CREATE TABLE IF NOT EXISTS counters (key TEXT PRIMARY KEY, value INTEGER DEFAULT 0)`;
this
.sql`INSERT INTO counters (key, value) VALUES (${key}, 1) ON CONFLICT(key) DO UPDATE SET value = value + 1`;
const row = this.sql`SELECT value FROM counters WHERE key = ${key}`.one();
return row?.value ?? 0;
}
}export class Parent extends Agent {
async demonstrate() {
this.sql`INSERT INTO parent_data (key, value) VALUES ('color', 'blue')`;
const child = await this.subAgent(Child, "child-1");
await child.increment("clicks");
// Parent's SQL and child's SQL are completely separate
}
}
export class Child extends Agent {
async increment(key: string): Promise<number> {
this
.sql`CREATE TABLE IF NOT EXISTS counters (key TEXT PRIMARY KEY, value INTEGER DEFAULT 0)`;
this
.sql`INSERT INTO counters (key, value) VALUES (${key}, 1) ON CONFLICT(key) DO UPDATE SET value = value + 1`;
const row = this.sql<{
value: number;
}>`SELECT value FROM counters WHERE key = ${key}`.one();
return row?.value ?? 0;
}
}Именование и идентичность
Два разных класса могут иметь одинаковое видимое пользователю имя: они разрешаются независимо друг от друга. Внутренний ключ представляет собой сочетание имени класса и имени грани:
const counter = await this.subAgent(Counter, "shared-name");
const logger = await this.subAgent(Logger, "shared-name");
// These are two separate sub-agents with separate storageconst counter = await this.subAgent(Counter, "shared-name");
const logger = await this.subAgent(Logger, "shared-name");
// These are two separate sub-agents with separate storageДочернего класса this.name свойство возвращает имя грани (а не имя родителя):
export class Child extends Agent {
getName() {
return this.name; // Returns "shared-name", not the parent's ID
}
}export class Child extends Agent {
getName(): string {
return this.name; // Returns "shared-name", not the parent's ID
}
}Паттерны
Параллельные дочерние агенты
Запускайте несколько дочерних агентов одновременно:
export class Orchestrator extends Agent {
async runAll(queries) {
const results = await Promise.all(
queries.map(async (query, i) => {
const worker = await this.subAgent(Researcher, `research-${i}`);
return worker.search(query);
}),
);
return results;
}
}export class Orchestrator extends Agent {
async runAll(queries: string[]) {
const results = await Promise.all(
queries.map(async (query, i) => {
const worker = await this.subAgent(Researcher, `research-${i}`);
return worker.search(query);
}),
);
return results;
}
}Вложенные дочерние агенты
Дочерние агенты могут порождать собственных дочерних агентов, формируя дерево:
export class Manager extends Agent {
async delegate(task) {
const team = await this.subAgent(TeamLead, "team-a");
return team.assign(task);
}
}
export class TeamLead extends Agent {
async assign(task) {
const worker = await this.subAgent(Worker, "worker-1");
return worker.execute(task);
}
}
export class Worker extends Agent {
async execute(task) {
return { completed: task };
}
}export class Manager extends Agent {
async delegate(task: string) {
const team = await this.subAgent(TeamLead, "team-a");
return team.assign(task);
}
}
export class TeamLead extends Agent {
async assign(task: string) {
const worker = await this.subAgent(Worker, "worker-1");
return worker.execute(task);
}
}
export class Worker extends Agent {
async execute(task: string) {
return { completed: task };
}
}Потоковая передача через колбэк
Передайте RpcTarget колбэк для потоковой передачи результатов от дочернего агента обратно родительскому:
import { RpcTarget } from "cloudflare:workers";
class StreamCollector extends RpcTarget {
chunks = [];
onChunk(text) {
this.chunks.push(text);
}
}
export class Parent extends Agent {
async streamFromChild() {
const child = await this.subAgent(Streamer, "streamer-1");
const collector = new StreamCollector();
await child.generate("Write a poem", collector);
return collector.chunks;
}
}
export class Streamer extends Agent {
async generate(prompt, callback) {
const chunks = ["Once ", "upon ", "a ", "time..."];
for (const chunk of chunks) {
callback.onChunk(chunk);
}
}
}import { RpcTarget } from "cloudflare:workers";
class StreamCollector extends RpcTarget {
chunks: string[] = [];
onChunk(text: string) {
this.chunks.push(text);
}
}
export class Parent extends Agent {
async streamFromChild() {
const child = await this.subAgent(Streamer, "streamer-1");
const collector = new StreamCollector();
await child.generate("Write a poem", collector);
return collector.chunks;
}
}
export class Streamer extends Agent {
async generate(prompt: string, callback: StreamCollector) {
const chunks = ["Once ", "upon ", "a ", "time..."];
for (const chunk of chunks) {
callback.onChunk(chunk);
}
}
}Планирование и устойчивая работа
Дочерние агенты могут планировать собственные колбэки и запускать устойчивые fiber:
| Метод | Поведение у дочернего агента |
|---|---|
schedule() / scheduleEvery() |
Работает в обычном режиме и запускает колбэки внутри дочернего агента |
cancelSchedule() |
Работает для расписаний, принадлежащих вызывающему дочернему агенту |
getScheduleById() / listSchedules() |
Работает и возвращает расписания в рамках вызывающего дочернего агента |
keepAlive() / keepAliveWhile() |
Работает, делегируя heartbeat родительскому агенту верхнего уровня |
runFiber() |
Работает, при этом строки fiber и снимки сохраняются в базе данных SQLite дочернего агента |
setState() |
Работает как обычно и записывает данные в собственное хранилище дочернего агента |
this.sql |
Работает как обычно и указывает на собственную базу данных SQLite дочернего агента |
subAgent() |
Работает, поэтому дочерние агенты могут порождать собственных потомков |
Верхнеуровневый родитель по-прежнему владеет физическим alarm Durable Object, поскольку у фасетов нет собственных независимых слотов для alarm. Agents SDK запоминает, какой дочерний агент владеет каждым запланированным колбэком или проверкой восстановления, пробуждает родителя и направляет работу обратно в дочерний агент. Колбэк по-прежнему выполняется с дочерним агентом в роли this, чтобы использовать состояние дочернего агента, его хранилище SQLite и getCurrentAgent() контекст.
Более старый синхронный getSchedule() и getSchedules() API выбрасывают исключение внутри дочерних агентов, поскольку строки расписания хранятся в родительском агенте верхнего уровня. Используйте getScheduleById() и listSchedules() взамен.
Вызов this.destroy() внутри дочернего агента делегирует очистку родительскому агенту. Тот отменяет запланированные задачи этого дочернего агента, удаляет метаданные восстановления для дочернего агента и его потомков, удаляет запись в реестре и просит рантайм очистить хранилище дочернего агента. Считайте this.destroy() как fire-and-forget, поскольку удаление дочернего агента может прервать его isolate до того, как метод корректно завершится.
Workflows от дочерних агентов
Дочерние агенты также могут запускать Workflows с this.runWorkflow(). Отслеживание Workflow ведётся локально, в базе данных SQLite этого sub-agent, и AgentWorkflow.agent направляет RPC, callbacks, обновления состояния и broadcasts обратно к исходному sub-agent. Родительские агенты не отображают и не управляют автоматически workflows, запущенными дочерними агентами.
Поскольку SubAgentStub<T> предоставляет только пользовательские дочерние методы, добавьте дочерние методы-обёртки для элементов управления, таких как getWorkflow(), approveWorkflow(), или terminateWorkflow(), затем вызовите эти обёртки через await this.subAgent(Child, name). Если вы передадите runWorkflow(..., { agentBinding }) из дочернего агента используйте имя привязки корневого Agent, а не имя дочерней привязки.
Для источников от workflow дочернего агента, AgentWorkflow.agent доступен только по RPC. Используйте его для вызова методов Agent, но используйте routeSubAgentRequest() или вложенный /agents/{parent}/{name}/sub/{child}/{name} структуру URL для внешней маршрутизации по HTTP или WebSocket вместо this.agent.fetch().
Пример
См. также
- Think,
chat()метод для потоковой передачи ходов ИИ через суб-агентов - Долгоживущие агенты : делегирование дочерним агентам в контексте многонедельных жизненных циклов агента
- Вызываемые методы : RPC через
@callableи service bindings - Agents как инструменты : запускает Think или
AIChatAgentдочерних агентов как удерживаемые потоковые инструменты - Планирование задач : примитивы планирования для агентов верхнего уровня и дочерних агентов