← Создание агентов на Cloudflare / agents / concepts / agentic-patterns
Паттерны с участием человека
Паттерны с участием человека (HITL) добавляют подтверждение или ввод данных на разных уровнях Agent. Вы можете отвечать на запрос MCP-сервера, удерживать выполнение приложения в устойчивом Workflow или подтверждать вызов коннектора, прежде чем сгенерированный моделью код вызовет инструмент.
Зачем нужно участие человека?
- Соответствие требованиям: Нормативные требования могут предусматривать обязательное одобрение человеком для определённых действий
- Безопасность: Операции с высокими рисками (платежи, удаления, внешние коммуникации) требуют контроля
- Качество: Проверка человеком выявляет ошибки, которые может пропустить ИИ
- Доверие: Пользователи чувствуют себя увереннее, когда могут одобрять критически важные действия
Типичные варианты использования
К типичным сценариям использования относятся финансовые согласования, модерация контента, массовые операции с данными, подтверждение перед вызовом инструментов с побочными эффектами и изменение прав доступа.
Выбор паттерна
Выберите паттерн в зависимости от того, кто вызывает паузу и где она возникает:
| Шаблон | Слой подтверждения | Инициатор | Типичное ожидание | Основной API |
|---|---|---|---|---|
| MCP elicitation | MCP-запрос, обработанный вашим Agent в роли MCP-клиента | Разработчик MCP-сервера | Минуты | configureElicitationHandlers() |
| Подтверждение Workflow | Устойчивая задача приложения или операция инструмента | Разработчик агентного приложения | Месяцы или годы | waitForApproval() |
| Подтверждение Code Mode | Вызов коннектора в коде, сгенерированном моделью | Разработчик агентов Code Mode | До настроенного истечения срока | requiresApproval, approve(), reject() |
Подтверждение на основе Workflow
Используйте Cloudflare Workflows когда вашему приложению нужно удерживать задачу или операцию инструмента столько времени, сколько требует подтверждение. waitForApproval() создаёт устойчивый шлюз на основе Cloudflare Workflows, поэтому ожидание может длиться месяцами и дольше без необходимости держать Agent запущенным.
Базовый паттерн
import { Agent } from "agents";
import { AgentWorkflow } from "agents/workflows";
export class ExpenseWorkflow extends AgentWorkflow {
async run(event, step) {
const expense = event.payload;
// Step 1: Validate the expense
const validated = await step.do("validate", async () => {
if (expense.amount <= 0) {
throw new Error("Invalid expense amount");
}
return { ...expense, validatedAt: Date.now() };
});
// Step 2: Report that we are waiting for approval
await this.reportProgress({
step: "approval",
status: "pending",
message: `Awaiting approval for $${expense.amount}`,
});
// Step 3: Wait for human approval (pauses the workflow)
const approval = await this.waitForApproval(step, {
timeout: "7 days",
});
console.log(`Approved by: ${approval?.approvedBy}`);
// Step 4: Process the approved expense
const result = await step.do("process", async () => {
return { expenseId: crypto.randomUUID(), ...validated };
});
await step.reportComplete(result);
return result;
}
}import { Agent } from "agents";
import { AgentWorkflow } from "agents/workflows";
import type { AgentWorkflowEvent, AgentWorkflowStep } from "agents/workflows";
type ExpenseParams = {
amount: number;
description: string;
requestedBy: string;
};
export class ExpenseWorkflow extends AgentWorkflow<
ExpenseAgent,
ExpenseParams
> {
async run(event: AgentWorkflowEvent<ExpenseParams>, step: AgentWorkflowStep) {
const expense = event.payload;
// Step 1: Validate the expense
const validated = await step.do("validate", async () => {
if (expense.amount <= 0) {
throw new Error("Invalid expense amount");
}
return { ...expense, validatedAt: Date.now() };
});
// Step 2: Report that we are waiting for approval
await this.reportProgress({
step: "approval",
status: "pending",
message: `Awaiting approval for $${expense.amount}`,
});
// Step 3: Wait for human approval (pauses the workflow)
const approval = await this.waitForApproval<{ approvedBy: string }>(step, {
timeout: "7 days",
});
console.log(`Approved by: ${approval?.approvedBy}`);
// Step 4: Process the approved expense
const result = await step.do("process", async () => {
return { expenseId: crypto.randomUUID(), ...validated };
});
await step.reportComplete(result);
return result;
}
}Методы Agent для утверждения
Агент предоставляет методы для подтверждения или отклонения ожидающих workflow:
import { Agent, callable } from "agents";
export class ExpenseAgent extends Agent {
initialState = {
pendingApprovals: [],
};
// Approve a waiting workflow
@callable()
async approve(workflowId, approvedBy) {
await this.approveWorkflow(workflowId, {
reason: "Expense approved",
metadata: { approvedBy, approvedAt: Date.now() },
});
// Update state to reflect approval
this.setState({
...this.state,
pendingApprovals: this.state.pendingApprovals.filter(
(p) => p.workflowId !== workflowId,
),
});
}
// Reject a waiting workflow
@callable()
async reject(workflowId, reason) {
await this.rejectWorkflow(workflowId, { reason });
this.setState({
...this.state,
pendingApprovals: this.state.pendingApprovals.filter(
(p) => p.workflowId !== workflowId,
),
});
}
// Track workflow progress to update pending approvals
async onWorkflowProgress(workflowName, workflowId, progress) {
const p = progress;
if (p.step === "approval" && p.status === "pending") {
// Add to pending approvals list for UI display
this.setState({
...this.state,
pendingApprovals: [
...this.state.pendingApprovals,
{
workflowId,
amount: 0, // Would come from workflow params
description: p.message || "",
requestedBy: "user",
requestedAt: Date.now(),
},
],
});
}
}
}import { Agent, callable } from "agents";
type PendingApproval = {
workflowId: string;
amount: number;
description: string;
requestedBy: string;
requestedAt: number;
};
type ExpenseState = {
pendingApprovals: PendingApproval[];
};
export class ExpenseAgent extends Agent<Env, ExpenseState> {
initialState: ExpenseState = {
pendingApprovals: [],
};
// Approve a waiting workflow
@callable()
async approve(workflowId: string, approvedBy: string): Promise<void> {
await this.approveWorkflow(workflowId, {
reason: "Expense approved",
metadata: { approvedBy, approvedAt: Date.now() },
});
// Update state to reflect approval
this.setState({
...this.state,
pendingApprovals: this.state.pendingApprovals.filter(
(p) => p.workflowId !== workflowId,
),
});
}
// Reject a waiting workflow
@callable()
async reject(workflowId: string, reason: string): Promise<void> {
await this.rejectWorkflow(workflowId, { reason });
this.setState({
...this.state,
pendingApprovals: this.state.pendingApprovals.filter(
(p) => p.workflowId !== workflowId,
),
});
}
// Track workflow progress to update pending approvals
async onWorkflowProgress(
workflowName: string,
workflowId: string,
progress: unknown,
): Promise<void> {
const p = progress as { step: string; status: string; message?: string };
if (p.step === "approval" && p.status === "pending") {
// Add to pending approvals list for UI display
this.setState({
...this.state,
pendingApprovals: [
...this.state.pendingApprovals,
{
workflowId,
amount: 0, // Would come from workflow params
description: p.message || "",
requestedBy: "user",
requestedAt: Date.now(),
},
],
});
}
}
}Обработка тайм-аутов
Устанавливайте тайм-ауты, чтобы workflow не ожидали бесконечно:
const approval = await this.waitForApproval(step, {
timeout: "7 days", // Also supports: "1 hour", "30 minutes", etc.
});
if (!approval) {
// Timeout expired - escalate or auto-reject
await step.reportError("Approval timeout - escalating to manager");
throw new Error("Approval timeout");
}const approval = await this.waitForApproval<{ approvedBy: string }>(step, {
timeout: "7 days", // Also supports: "1 hour", "30 minutes", etc.
});
if (!approval) {
// Timeout expired - escalate or auto-reject
await step.reportError("Approval timeout - escalating to manager");
throw new Error("Approval timeout");
}Эскалация с планированием
Используйте schedule() чтобы настроить напоминания об эскалации:
import { Agent, callable } from "agents";
class ExpenseAgent extends Agent {
@callable()
async submitForApproval(expense) {
// Start the approval workflow
const workflowId = await this.runWorkflow("EXPENSE_WORKFLOW", expense);
// Schedule reminder after 4 hours
await this.schedule(Date.now() + 4 * 60 * 60 * 1000, "sendReminder", {
workflowId,
});
// Schedule escalation after 24 hours
await this.schedule(Date.now() + 24 * 60 * 60 * 1000, "escalateApproval", {
workflowId,
});
return workflowId;
}
async sendReminder(payload) {
const workflow = this.getWorkflow(payload.workflowId);
if (workflow?.status === "waiting") {
// Send reminder notification
console.log("Reminder: approval still pending");
}
}
async escalateApproval(payload) {
const workflow = this.getWorkflow(payload.workflowId);
if (workflow?.status === "waiting") {
// Escalate to manager
console.log("Escalating to manager");
}
}
}import { Agent, callable } from "agents";
class ExpenseAgent extends Agent<Env, ExpenseState> {
@callable()
async submitForApproval(expense: ExpenseParams): Promise<string> {
// Start the approval workflow
const workflowId = await this.runWorkflow("EXPENSE_WORKFLOW", expense);
// Schedule reminder after 4 hours
await this.schedule(Date.now() + 4 * 60 * 60 * 1000, "sendReminder", {
workflowId,
});
// Schedule escalation after 24 hours
await this.schedule(Date.now() + 24 * 60 * 60 * 1000, "escalateApproval", {
workflowId,
});
return workflowId;
}
async sendReminder(payload: { workflowId: string }) {
const workflow = this.getWorkflow(payload.workflowId);
if (workflow?.status === "waiting") {
// Send reminder notification
console.log("Reminder: approval still pending");
}
}
async escalateApproval(payload: { workflowId: string }) {
const workflow = this.getWorkflow(payload.workflowId);
if (workflow?.status === "waiting") {
// Escalate to manager
console.log("Escalating to manager");
}
}
}Журнал аудита на SQL
Используйте this.sql чтобы вести неизменяемый журнал аудита:
import { Agent, callable } from "agents";
class ExpenseAgent extends Agent {
async onStart() {
// Create audit table
this.sql`
CREATE TABLE IF NOT EXISTS approval_audit (
id INTEGER PRIMARY KEY AUTOINCREMENT,
workflow_id TEXT NOT NULL,
decision TEXT NOT NULL CHECK(decision IN ('approved', 'rejected')),
decided_by TEXT NOT NULL,
decided_at INTEGER NOT NULL,
reason TEXT
)
`;
}
@callable()
async approve(workflowId, userId, reason) {
// Record the decision in SQL (immutable audit log)
this.sql`
INSERT INTO approval_audit (workflow_id, decision, decided_by, decided_at, reason)
VALUES (${workflowId}, 'approved', ${userId}, ${Date.now()}, ${reason || null})
`;
// Process the approval
await this.approveWorkflow(workflowId, {
reason: reason || "Approved",
metadata: { approvedBy: userId },
});
}
}import { Agent, callable } from "agents";
class ExpenseAgent extends Agent<Env, ExpenseState> {
async onStart() {
// Create audit table
this.sql`
CREATE TABLE IF NOT EXISTS approval_audit (
id INTEGER PRIMARY KEY AUTOINCREMENT,
workflow_id TEXT NOT NULL,
decision TEXT NOT NULL CHECK(decision IN ('approved', 'rejected')),
decided_by TEXT NOT NULL,
decided_at INTEGER NOT NULL,
reason TEXT
)
`;
}
@callable()
async approve(
workflowId: string,
userId: string,
reason?: string,
): Promise<void> {
// Record the decision in SQL (immutable audit log)
this.sql`
INSERT INTO approval_audit (workflow_id, decision, decided_by, decided_at, reason)
VALUES (${workflowId}, 'approved', ${userId}, ${Date.now()}, ${reason || null})
`;
// Process the approval
await this.approveWorkflow(workflowId, {
reason: reason || "Approved",
metadata: { approvedBy: userId },
});
}
}Конфигурация
{
"name": "expense-approval",
"main": "src/index.ts",
// Set this to today's date
"compatibility_date": "2026-08-28",
"compatibility_flags": ["nodejs_compat"],
"durable_objects": {
"bindings": [{ "name": "EXPENSE_AGENT", "class_name": "ExpenseAgent" }],
},
"workflows": [
{
"name": "expense-workflow",
"binding": "EXPENSE_WORKFLOW",
"class_name": "ExpenseWorkflow",
},
],
"migrations": [{ "tag": "v1", "new_sqlite_classes": ["ExpenseAgent"] }],
}name = "expense-approval"
main = "src/index.ts"
# Set this to today's date
compatibility_date = "2026-08-28"
compatibility_flags = [ "nodejs_compat" ]
[[durable_objects.bindings]]
name = "EXPENSE_AGENT"
class_name = "ExpenseAgent"
[[workflows]]
name = "expense-workflow"
binding = "EXPENSE_WORKFLOW"
class_name = "ExpenseWorkflow"
[[migrations]]
tag = "v1"
new_sqlite_classes = [ "ExpenseAgent" ]MCP elicitation
MCP elicitation позволяет разработчику MCP-сервера указать, что вызову инструмента нужна дополнительная информация или взаимодействие по отдельному каналу. В этом случае ваш Agent выступает в роли MCP-клиента: он показывает запрос пользователю и возвращает его ответ. Такие взаимодействия обычно завершаются за несколько минут.
Режим формы собирает структурированный несекретный ввод. Режим URL предлагает пользователю перейти к сценарию out-of-band, например к авторизации у третьей стороны или оплате.
Настройте обработчики, ориентированные на пользователя, в onStart():
import { Agent } from "agents";
export class MyAgent extends Agent {
onStart() {
this.mcp.configureElicitationHandlers({
form: (request, serverId) =>
this.forwardElicitationToUser(request, serverId),
url: (request, serverId) =>
this.forwardElicitationToUser(request, serverId),
});
}
forwardElicitationToUser(request, serverId) {
// Present the request in your UI and resolve after the user responds.
throw new Error(
`Implement elicitation for ${serverId}: ${request.params.message}`,
);
}
}import { Agent } from "agents";
import type { ElicitRequest, ElicitResult } from "agents/mcp";
export class MyAgent extends Agent<Env> {
onStart() {
this.mcp.configureElicitationHandlers({
form: (request, serverId) =>
this.forwardElicitationToUser(request, serverId),
url: (request, serverId) =>
this.forwardElicitationToUser(request, serverId),
});
}
private forwardElicitationToUser(
request: ElicitRequest,
serverId: string,
): Promise<ElicitResult> {
// Present the request in your UI and resolve after the user responds.
throw new Error(
`Implement elicitation for ${serverId}: ${request.params.message}`,
);
}
}Полные паттерны для формы, URL и передачи браузера см. в Elicitation на стороне MCP-клиента. Для API запросов на стороне сервера см. McpAgent elicitation.
Устойчивые подтверждения Code Mode
Используйте устойчивый runtime Code Mode для агентов написания кода и других Agent, использующих паттерн Code Mode. Пометьте метод коннектора requiresApproval: true чтобы приостановить сгенерированный моделью код перед вызовом базового инструмента.
В следующем примере инструмент GitHub MCP помечается как требующий подтверждения, добавляется в устойчивый runtime как коннектор, а также предоставляются методы, с помощью которых UI может просматривать, подтверждать или отклонять ожидающее действие:
import { AIChatAgent } from "@cloudflare/ai-chat";
import {
createCodemodeRuntime,
DynamicWorkerExecutor,
McpConnector,
} from "@cloudflare/codemode";
import { callable } from "agents";
import { convertToModelMessages, stepCountIs, streamText } from "ai";
import { model } from "./model";
class GitHubConnector extends McpConnector {
connection;
constructor(ctx, env, connection) {
super(ctx, env);
this.connection = connection;
}
name() {
return "github";
}
createConnection() {
return this.connection;
}
tool(name, tool) {
if (name === "create_issue") {
return { ...tool, requiresApproval: true };
}
return tool;
}
}
export class CodingAgent extends AIChatAgent {
runtime() {
const server = this.mcp
.listServers()
.find((item) => item.name === "GitHub");
if (!server) throw new Error("GitHub MCP server is not registered.");
const connection = this.mcp.mcpConnections[server.id];
if (!connection) throw new Error("GitHub MCP connection is unavailable.");
return createCodemodeRuntime({
ctx: this.ctx,
executor: new DynamicWorkerExecutor({ loader: this.env.LOADER }),
connectors: [new GitHubConnector(this.ctx, this.env, connection)],
});
}
async onChatMessage() {
const result = streamText({
model,
messages: await convertToModelMessages(this.messages),
tools: { codemode: this.runtime().tool() },
stopWhen: stepCountIs(10),
});
return result.toUIMessageStreamResponse();
}
@callable()
async pendingApprovals() {
return this.runtime().pending();
}
@callable()
async approveExecution(executionId) {
return this.runtime().approve({ executionId });
}
@callable()
async rejectExecution(executionId, seq) {
return this.runtime().reject({ executionId, seq });
}
}import { AIChatAgent } from "@cloudflare/ai-chat";
import {
createCodemodeRuntime,
DynamicWorkerExecutor,
McpConnector,
type CodemodeRuntimeHandle,
type ConnectorTool,
type McpConnectionLike,
type PendingAction,
} from "@cloudflare/codemode";
import { callable } from "agents";
import { convertToModelMessages, stepCountIs, streamText } from "ai";
import { model } from "./model";
class GitHubConnector extends McpConnector<Env> {
private connection: McpConnectionLike;
constructor(
ctx: DurableObjectState,
env: Env,
connection: McpConnectionLike,
) {
super(ctx, env);
this.connection = connection;
}
override name() {
return "github";
}
protected override createConnection() {
return this.connection;
}
protected override tool(name: string, tool: ConnectorTool): ConnectorTool {
if (name === "create_issue") {
return { ...tool, requiresApproval: true };
}
return tool;
}
}
export class CodingAgent extends AIChatAgent<Env> {
private runtime(): CodemodeRuntimeHandle {
const server = this.mcp
.listServers()
.find((item) => item.name === "GitHub");
if (!server) throw new Error("GitHub MCP server is not registered.");
const connection = this.mcp.mcpConnections[server.id];
if (!connection) throw new Error("GitHub MCP connection is unavailable.");
return createCodemodeRuntime({
ctx: this.ctx,
executor: new DynamicWorkerExecutor({ loader: this.env.LOADER }),
connectors: [new GitHubConnector(this.ctx, this.env, connection)],
});
}
async onChatMessage() {
const result = streamText({
model,
messages: await convertToModelMessages(this.messages),
tools: { codemode: this.runtime().tool() },
stopWhen: stepCountIs(10),
});
return result.toUIMessageStreamResponse();
}
@callable()
async pendingApprovals(): Promise<PendingAction[]> {
return this.runtime().pending();
}
@callable()
async approveExecution(executionId: string) {
return this.runtime().approve({ executionId });
}
@callable()
async rejectExecution(executionId: string, seq: number): Promise<boolean> {
return this.runtime().reject({ executionId, seq });
}
}Когда сгенерированный код вызывает github.create_issue(), среда выполнения записывает ожидающий метод и аргументы, а затем приостанавливается перед выполнением MCP-инструмента. Подтверждение запускает новый проход с тем же исходным кодом и тем же ID выполнения. Ранее завершённые вызовы воспроизводятся из устойчивого журнала, подтверждённый вызов выполняется, и сгенерированный код продолжает работу.
Ожидающие подтверждения и история выполнения сохраняются после завершения запроса и гибернации Durable Object. Модель выполнения и повторного воспроизведения описана в Подтверждения через прерывание и воспроизведение.
Создавайте пользовательские интерфейсы подтверждения для Workflow
Список ожидающих подтверждений
Используйте состояние агента, чтобы отображать в интерфейсе ожидающие подтверждения:
import { useAgent } from "agents/react";
function PendingApprovals() {
const { state, agent } = useAgent({
agent: "expense-agent",
name: "main",
});
if (!state?.pendingApprovals?.length) {
return <p>No pending approvals</p>;
}
return (
<div className="approval-list">
{state.pendingApprovals.map((item) => (
<div key={item.workflowId} className="approval-card">
<h3>${item.amount}</h3>
<p>{item.description}</p>
<p>Requested by {item.requestedBy}</p>
<div className="actions">
<button
onClick={() => agent.stub.approve(item.workflowId, "admin")}
>
Approve
</button>
<button
onClick={() => agent.stub.reject(item.workflowId, "Declined")}
>
Reject
</button>
</div>
</div>
))}
</div>
);
}Добавьте несколько утверждающих Workflow
Для чувствительных операций, требующих нескольких утверждающих:
import { Agent, callable } from "agents";
class MultiApprovalAgent extends Agent {
@callable()
async approveMulti(workflowId, userId) {
const approval = this.state.pendingMultiApprovals.find(
(p) => p.workflowId === workflowId,
);
if (!approval) throw new Error("Approval not found");
// Check if user already approved
if (approval.currentApprovals.some((a) => a.userId === userId)) {
throw new Error("Already approved by this user");
}
// Add this user's approval
approval.currentApprovals.push({ userId, approvedAt: Date.now() });
// Check if we have enough approvals
if (approval.currentApprovals.length >= approval.requiredApprovals) {
// Execute the approved action
await this.approveWorkflow(workflowId, {
metadata: { approvers: approval.currentApprovals },
});
return true;
}
this.setState({ ...this.state });
return false; // Still waiting for more approvals
}
}import { Agent, callable } from "agents";
type MultiApproval = {
workflowId: string;
requiredApprovals: number;
currentApprovals: Array<{ userId: string; approvedAt: number }>;
rejections: Array<{ userId: string; rejectedAt: number; reason: string }>;
};
type State = {
pendingMultiApprovals: MultiApproval[];
};
class MultiApprovalAgent extends Agent<Env, State> {
@callable()
async approveMulti(workflowId: string, userId: string): Promise<boolean> {
const approval = this.state.pendingMultiApprovals.find(
(p) => p.workflowId === workflowId,
);
if (!approval) throw new Error("Approval not found");
// Check if user already approved
if (approval.currentApprovals.some((a) => a.userId === userId)) {
throw new Error("Already approved by this user");
}
// Add this user's approval
approval.currentApprovals.push({ userId, approvedAt: Date.now() });
// Check if we have enough approvals
if (approval.currentApprovals.length >= approval.requiredApprovals) {
// Execute the approved action
await this.approveWorkflow(workflowId, {
metadata: { approvers: approval.currentApprovals },
});
return true;
}
this.setState({ ...this.state });
return false; // Still waiting for more approvals
}
}Рекомендации
- Определите чёткие критерии подтверждения : требуйте подтверждение только для действий с ощутимыми последствиями (платежи, письма, изменение данных)
- Предоставление подробного контекста : показывайте пользователям, что именно сделает действие, включая все аргументы
- Реализация тайм-аутов : используйте
schedule()чтобы эскалировать или автоматически отклонять по истечении разумных периодов - Ведите журналы аудита : используйте
this.sqlчтобы записывать все решения о подтверждении для целей комплаенса - Обрабатывайте обрывы соединения : храните ожидающие подтверждения в состоянии агента, чтобы они сохранялись при разрывах соединения
- Постепенная деградация : предоставьте резервное поведение на случай отклонения подтверждений