← Создание агентов на Cloudflare / agents / harnesses / think
Действия
Actions представляют собой серверные инструменты со всем необходимым из коробки. Там, где обычный AI SDK tool() это всего лишь описание, схема и execute функцию, action() добавляет то, что утомительно и рискованно реализовать вручную для инструмента с реальными побочными эффектами:
- Идемпотентность : устойчивый журнал воспроизводит зафиксированный результат по стабильному ключу вместо повторного выполнения побочного эффекта при повторной попытке восстановления.
- Подтверждения : требует одобрения человека для вызова, либо сразу (ход ожидает), либо устойчиво (ход приостанавливается и возобновляется позже, даже из панели управления без активного сокета).
- Авторизация : объявите разрешения, необходимые для вызова, и предоставляйте их на каждый ход.
- Вложения ответа : записывает информативные метаданные доставки (черновик письма, карточку, голосовую заметку), не меняя то, что видит модель.
Actions компилируются в инструменты Think, поэтому модель вызывает их точно так же, как любой другой инструмент. Возвращайте их из getActions(); Think объединяет их в набор инструментов вместе с getTools(), инструменты рабочей области, расширения и MCP-инструменты.
Определите действие
Используйте action() фабрику дескрипторов и возвращайте карту действий из getActions(). Ключом карты служит имя инструмента, которое видит модель (если не задать name). execute тип входных данных выводится из inputSchema:
import { Think, action } from "@cloudflare/think";
import { z } from "zod";
export class Support extends Think {
getActions() {
return {
refundOrder: action({
description: "Refund a customer order.",
inputSchema: z.object({
orderId: z.string(),
amountCents: z.number().int().positive(),
}),
execute: async ({ orderId, amountCents }, ctx) => {
const result = await refund(orderId, amountCents);
return { refundId: result.id, status: result.status };
},
}),
};
}
}import { Think, action } from "@cloudflare/think";
import { z } from "zod";
export class Support extends Think<Env> {
getActions() {
return {
refundOrder: action({
description: "Refund a customer order.",
inputSchema: z.object({
orderId: z.string(),
amountCents: z.number().int().positive(),
}),
execute: async ({ orderId, amountCents }, ctx) => {
const result = await refund(orderId, amountCents);
return { refundId: result.id, status: result.status };
},
}),
};
}
} execute колбэк получает проверенные входные данные и ActionContext:
type ActionContext = {
agent: Think;
env: Cloudflare.Env;
requestId: string;
toolCallId: string;
messages: ReadonlyArray<ModelMessage>;
signal: AbortSignal; // aborts on turn cancel or after `timeoutMs`
attachReply(attachment: ReplyAttachment): void;
};Результат действия нормализуется в JSON и усекается перед показом модели (длинные результаты ограничиваются). Всё, что выбрасывается из execute становится структурированным { error: { name, message } } результат инструмента, вместо того чтобы аварийно завершать ход. У каждого действия тайм-аут по умолчанию составляет 30 секунд; переопределите его для отдельного действия с помощью timeoutMs.
Actions против обычных инструментов
Обычный tool() от getTools() всё ещё работает и является правильным выбором для инструмента, доступного только для чтения, или тривиального инструмента. Обратитесь к action() когда у инструмента есть побочные эффекты, которые нельзя выполнять дважды, требуется подтверждение человеком или декларативная авторизация: реестр, дескрипторы подтверждения, тайм-аут по умолчанию и сопоставление структурированных ошибок применяются только к действиям.
Идемпотентность и журнал действий
Когда действие объявляет idempotencyKey, Think записывает устоявшийся результат в устойчивый журнал, индексированный по action:<name>:<key>. Если тот же ключ встречается снова: при повторной попытке восстановления, повторном подключении или дублирующемся входящем событии, Think возвращает сохранённый результат без повторный запуск execute, чтобы побочный эффект происходил не более одного раза в штатном сценарии.
const chargeInvoice = action({
description: "Charge an invoice.",
inputSchema: z.object({ invoiceId: z.string() }),
// Use a stable domain identifier — never a timestamp, request id, or random value.
idempotencyKey: ({ input }) => `invoice:${input.invoiceId}`,
execute: async ({ invoiceId }) => charge(invoiceId),
});const chargeInvoice = action({
description: "Charge an invoice.",
inputSchema: z.object({ invoiceId: z.string() }),
// Use a stable domain identifier — never a timestamp, request id, or random value.
idempotencyKey: ({ input }) => `invoice:${input.invoiceId}`,
execute: async ({ invoiceId }) => charge(invoiceId),
});idempotencyKey это строка либо функция ({ input, ctx }) => string. Выберите ключ, который сохраняется при повторных попытках восстановления (например, id заказа или id входящего события), а не значение, которое меняется при каждой попытке. Действие без idempotencyKey переключается на per-toolCallId ключ, который устраняет дублирование только в рамках одного вызова инструмента, но не между повторными попытками.
Ожидающие строки и право на повторную попытку
Строка реестра записывается как pending перед execute выполняется и переключается в settled при успехе (выброшенный или завершившийся по тайм-ауту execute удаляет строку, чтобы вызов можно было корректно повторить). Если isolate завершает работу в процессе выполнения, строка остаётся pending. По умолчанию такая устаревшая запись освобождается, а действие повторяется, как только запись становится старше actionLedgerPendingRetryLeaseMs (по умолчанию 5 минут): но только для действий, которые явно объявляют idempotencyKey, поскольку этот ключ служит вашим подтверждением того, что повторный запуск побочного эффекта с данным ключом безопасен. Новая ожидающая запись (или запись без явного ключа) вместо этого возвращает ActionPendingError результат, чтобы модель не повторяла вслепую попытку в неизвестном состоянии. Установите actionLedgerPendingRetryLeaseMs = false чтобы полностью отключить reclaim и всегда показывать ActionPendingError для устаревшей строки.
Подтверждения
Требуйте одобрения человека для действия с помощью approval. Есть два механизма, выбор между ними задаётся kind.
Требует подтверждения (ход ожидает)
Значение по умолчанию при установке approval без kind. Действие компилируется в инструмент с помощью AI SDK needsApproval флаг: поток приостанавливается с approval-requested часть, клиент подтверждает или отклоняет, и ход продолжается непосредственно в потоке. execute выполняется только после подтверждения.
const deleteAccount = action({
description: "Permanently delete a user account.",
inputSchema: z.object({ userId: z.string() }),
approval: true, // or ({ input }) => input.userId !== currentUser
approvalSummary: "Delete an account",
approvalRisk: "high",
execute: async ({ userId }) => deleteAccount(userId),
});const deleteAccount = action({
description: "Permanently delete a user account.",
inputSchema: z.object({ userId: z.string() }),
approval: true, // or ({ input }) => input.userId !== currentUser
approvalSummary: "Delete an account",
approvalRisk: "high",
execute: async ({ userId }) => deleteAccount(userId),
});approval это булево значение либо предикат ({ input, ctx }) => boolean, чтобы вы могли требовать подтверждение только для рискованных входных данных. approvalSummary и approvalRisk ("low" | "medium" | "high") заполняют дескриптор одобрения, который отрисовывает ваш UI.
Устойчивая пауза (ход приостанавливается и позже возобновляется)
Задайте kind: "durable-pause" когда подтверждение может занять минуты или дни, а держать соединение открытым нежелательно. Действие приостанавливается в устойчивом хранилище, и ход завершается; execute ещё не выполняется. Возобновите позже, откуда угодно, включая панель управления без активного WebSocket-соединения, с помощью approveExecution() или rejectExecution():
const deploy = action({
description: "Deploy to production.",
inputSchema: z.object({ ref: z.string() }),
kind: "durable-pause",
approvalSummary: "Deploy to production",
approvalRisk: "high",
permissions: ["deploy:run"],
execute: async ({ ref }) => deploy(ref),
});const deploy = action({
description: "Deploy to production.",
inputSchema: z.object({ ref: z.string() }),
kind: "durable-pause",
approvalSummary: "Deploy to production",
approvalRisk: "high",
permissions: ["deploy:run"],
execute: async ({ ref }) => deploy(ref),
});// List everything waiting on a human (cold-load reconciliation):
const pending = await agent.pendingApprovals();
// [{ executionId, source: "action" | "codemode", descriptor }]
// Approve or reject by execution id (idempotent — a second call is a no-op):
await agent.approveExecution(executionId);
await agent.rejectExecution(executionId, "Not this release");// List everything waiting on a human (cold-load reconciliation):
const pending = await agent.pendingApprovals();
// [{ executionId, source: "action" | "codemode", descriptor }]
// Approve or reject by execution id (idempotent — a second call is a no-op):
await agent.approveExecution(executionId);
await agent.rejectExecution(executionId, "Not this release");approveExecution() выполняет execute один раз и автоматически продолжает ход, даже если ни один клиент не подключён; rejectExecution() разрешает действие, не выполняя его. pendingApprovals() объединяет отложенные действия и приостановленные Codemode выполнений, поэтому оба может обслуживать единый интерфейс одобрения. (durable-pause требует approval политику: действие, которое никогда не приостанавливается, отклоняется уже на этапе определения.)
И части, требующие подтверждения, и части с устойчивой паузой несут стабильный ActionApprovalDescriptor ({ requestId, toolCallId, action, summary, input, permissions, risk, kind }) поэтому у вашего UI есть всё необходимое для отрисовки промпта.
Авторизация
Объявите разрешения, которые требуются действию, с помощью permissions, затем выдавайте их на каждый ход. По умолчанию каждый ход полностью авторизован, поэтому применение авторизации является добровольным.
const refundOrder = action({
description: "Refund a customer order.",
inputSchema: z.object({ orderId: z.string() }),
permissions: ["billing:refund"], // or ({ input }) => [...]
execute: async ({ orderId }) => refund(orderId),
});const refundOrder = action({
description: "Refund a customer order.",
inputSchema: z.object({ orderId: z.string() }),
permissions: ["billing:refund"], // or ({ input }) => [...]
execute: async ({ orderId }) => refund(orderId),
});Переопределить authorizeTurn() чтобы раз в ход решать, какие разрешения предоставляются. Возврат списка сужает выданные права: любое действие, требующее разрешения вне этого набора, отклоняется со структурированной ActionAuthorizationError (модель никогда не вызывает execute):
export class Support extends Think {
authorizeTurn(ctx) {
const role = ctx.body?.role;
if (role === "admin") return true; // full grant (the default)
return { allowed: true, grantedPermissions: ["billing:read"] };
}
}export class Support extends Think<Env> {
override authorizeTurn(ctx: TurnContext): ActionAuthorizationDecision {
const role = (ctx.body as { role?: string })?.role;
if (role === "admin") return true; // full grant (the default)
return { allowed: true, grantedPermissions: ["billing:read"] };
}
}authorizeTurn() возвращает true (полное разрешение), false (запретить всё), или { allowed, reason?, grantedPermissions? }. Для логики на уровне отдельного вызова переопределите authorizeAction(ctx) вместо этого: он получает имя действия, тип, входные данные, а также требуемые и предоставленные разрешения.
Вложения ответа
Действие может записать для хода информативные метаданные доставки, например черновик письма, карточку или голосовую заметку, с помощью ctx.attachReply(). Вложения никогда не изменяют вывод инструмента, который видит модель: они передаются вместе с ответом, чтобы ваш слой доставки мог их отобразить.
const draftReply = action({
description: "Draft an email reply.",
inputSchema: z.object({ to: z.string(), subject: z.string() }),
execute: async ({ to, subject }, ctx) => {
ctx.attachReply({ type: "email_draft", to: [to], subject });
return { drafted: true };
},
});const draftReply = action({
description: "Draft an email reply.",
inputSchema: z.object({ to: z.string(), subject: z.string() }),
execute: async ({ to, subject }, ctx) => {
ctx.attachReply({ type: "email_draft", to: [to], subject });
return { drafted: true };
},
});Прочитайте вложения после хода из onChatResponse() хук, или из replyAttachments(requestId?) геттер:
export class Support extends Think {
async onChatResponse(result) {
for (const attachment of result.attachments ?? []) {
// attachment.type === "email_draft" | "card" | "voice_note" | custom
}
}
}export class Support extends Think<Env> {
override async onChatResponse(result: ChatResponseResult) {
for (const attachment of result.attachments ?? []) {
// attachment.type === "email_draft" | "card" | "voice_note" | custom
}
}
}Вложения нормализуются в JSON, глубоко копируются при чтении, ограничиваются по количеству за ход и отбрасываются, если execute который их записал, завершается сбоем. Повторное воспроизведение реестра не запускает вложения заново (побочный эффект уже произошёл), и attachReply() не выполняет никаких действий при вызове из permissions, approval, или idempotencyKey колбэк: записывайте вложения из execute.
Встроенный ReplyAttachment покрывает email_draft, card, а также voice_note; любой { type: string; ... } форма разрешена для пользовательской доставки. Переопределите renderAttachment() чтобы превратить вложение в уведомление канала.
Справочник
action(config)
| Поле | Тип | Обязательный | По умолчанию | Описание |
|---|---|---|---|---|
description |
string |
Да | , | Описание инструмента, показываемое модели. |
inputSchema |
FlexibleSchema (Zod, Valibot или AI SDK jsonSchema) |
Да | , | Валидирует и типизирует execute вход. |
execute |
(input, ctx) => Output | Promise<Output> |
Да | , | Тело действия. Получает проверенные входные данные и ActionContext. |
name |
string |
Нет | ключ карты | Переопределяет имя инструмента. |
idempotencyKey |
string | ({ input, ctx }) => string |
Нет | за вызов инструмента | Устойчивый ключ для воспроизведения реестра. Используйте идентификатор домена. |
permissions |
readonly string[] | ({ input, ctx }) => readonly string[] |
Нет | нет | Разрешения, которые требуются для этого вызова (см. Authorization). |
approval |
boolean | ({ input, ctx }) => boolean |
Нет | нет | Требуйте одобрения человека для вызова. |
approvalSummary |
string |
Нет | description |
Понятное человеку описание в дескрипторе подтверждения. |
approvalRisk |
"low" | "medium" | "high" |
Нет | , | Подсказка о риске в дескрипторе подтверждения. |
kind |
"server" | "approval-gated" | "durable-pause" |
Нет | выведено | approval-gated когда approval задан, иначе server; задайте durable-pause явно. |
timeoutMs |
number |
Нет | 30000 |
Тайм-аут выполнения на действие (также определяет ctx.signal). |
Хуки и методы агента
| Участник | Описание |
|---|---|
getActions() |
Верните дескрипторы действий для компиляции в инструменты. |
authorizeTurn(ctx) |
Определяйте предоставленные разрешения один раз за ход. По умолчанию предоставляются все права. |
authorizeAction(ctx) |
Определяйте авторизацию для каждого вызова действия. По умолчанию выполняется проверка authorizeTurn предоставляет. |
pendingApprovals(executionId?) |
Выводит список отложенных действий и приостановленных выполнений Codemode, ожидающих подтверждения. |
approveExecution(executionId) |
Подтвердить приостановленное выполнение; запускает execute и автоматически продолжает ход. |
rejectExecution(executionId, reason?) |
Отклоните приостановленное выполнение, не запуская его. |
replyAttachments(requestId?) |
Прочитайте рекомендательные вложения, записанные в течение хода. |
actionLedgerPendingRetryLeaseMs |
Окно освобождения устаревших ожидающих (по умолчанию 300000; false чтобы отключить). |
См. также
- Инструменты : инструменты рабочей области, выполнение кода и расширения.
- Человек в цикле : весь процесс одобрения от начала до конца.
- Каналы : доставляет вложения и уведомления out-of-band.