INTEGRITY Документация

Паттерны с участием человека

Паттерны с участием человека (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
	}
}

Рекомендации

  1. Определите чёткие критерии подтверждения : требуйте подтверждение только для действий с ощутимыми последствиями (платежи, письма, изменение данных)
  2. Предоставление подробного контекста : показывайте пользователям, что именно сделает действие, включая все аргументы
  3. Реализация тайм-аутов : используйте schedule() чтобы эскалировать или автоматически отклонять по истечении разумных периодов
  4. Ведите журналы аудита : используйте this.sql чтобы записывать все решения о подтверждении для целей комплаенса
  5. Обрабатывайте обрывы соединения : храните ожидающие подтверждения в состоянии агента, чтобы они сохранялись при разрывах соединения
  6. Постепенная деградация : предоставьте резервное поведение на случай отклонения подтверждений

Следующие шаги

Запуск Workflows

Полное справочное описание API waitForApproval().

MCP-клиенты

Обрабатывайте запросы Elicitation в Agent, который выступает в роли MCP-клиента.

MCP-серверы

Запрашивает у MCP-сервера форму или elicitation по URL.

Устойчивый рантайм Code Mode

Приостанавливайте сгенерированный моделью код для подтверждения перед вызовами коннектора.