INTEGRITY Dokumentace

Vzory s lidským zásahem

Vzory s lidským zásahem (HITL) přidávají schválení nebo vstup na různých úrovních agenta. Můžete reagovat na požadavek serveru MCP, ponechat práci aplikace v trvalém Workflow, nebo schválit volání konektoru, než kód vygenerovaný modelem spustí nástroj.

Proč human-in-the-loop?

Běžné případy použití

Mezi běžné případy použití patří finanční schvalování, moderace obsahu, hromadné operace s daty, schválení před voláním nástrojů s vedlejšími efekty a změny v řízení přístupu.

Výběr vzoru

Zvolte vzor podle toho, kdo zavádí pauzu a kde k ní dochází:

Vzor Vrstva schvalování Iniciátor Typické čekání Klíčové API
Elicitace MCP Požadavek MCP zpracovaný klientem Agent Vývojář serveru MCP Minuty configureElicitationHandlers()
Schválení workflow Trvalá úloha aplikace nebo operace nástroje Vývojář aplikací s agenty Měsíce nebo roky waitForApproval()
Schválení Code Mode Volání konektoru v kódu vygenerovaném modelem Vývojář agentů Code Mode Do nakonfigurovaného vypršení platnosti requiresApproval, approve(), reject()

Schválení založené na workflow

Použijte Cloudflare Workflows když vaše aplikace potřebuje podržet úlohu nebo operaci nástroje po celou dobu, kterou schválení vyžaduje. waitForApproval() vytvoří trvalou bránu podloženou Cloudflare Workflows, takže čekání může pokračovat měsíce i déle, aniž by musel Agent zůstat spuštěný.

Základní vzor

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;
	}
}

Metody Agenta pro schválení

Agent poskytuje metody pro schválení nebo zamítnutí čekajících 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(),
					},
				],
			});
		}
	}
}

Zpracování časových limitů

Nastavte časové limity, aby workflow nečekala neomezeně dlouho:

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");
}

Eskalace s plánováním

Použijte schedule() k nastavení připomínek eskalace:

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");
		}
	}
}

Auditní záznam pomocí SQL

Použijte this.sql k udržování neměnného auditního záznamu:

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 },
		});
	}
}

Konfigurace

{
	"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" ]

Elicitace MCP

Elicitace MCP umožňuje vývojáři serveru MCP rozhodnout, že volání nástroje vyžaduje více informací nebo mimopásmovou interakci. Váš Agent funguje jako klient MCP: uživateli předloží požadavek a vrátí jeho odpověď. Tyto interakce obvykle proběhnou během několika minut.

Režim formuláře shromažďuje strukturovaný, necitlivý vstup. Režim URL vyzve uživatele k otevření mimopásmového postupu, jako je autorizace třetí strany nebo platba.

Konfigurace uživatelsky orientovaných handlerů v 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}`,
		);
	}
}

Úplné vzory pro formulář, URL a předávání prohlížeče najdete v Elicitace klienta MCP. Pro server-side request API si přečtěte McpAgent elicitation.

Trvalá schválení Code Mode

Použijte trvalý runtime Code Mode pro kódovací agenty a další Agenty používající vzor Code Mode. Metodu konektoru označte requiresApproval: true k pozastavení kódu vygenerovaného modelem předtím, než zavolá podkladový nástroj.

Následující příklad označí nástroj GitHub MCP jako podmíněný schválením, přidá konektor do trvalého runtime a zpřístupní metody, pomocí kterých může uživatelské rozhraní čekající akci zkontrolovat, schválit nebo zamítnout:

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 });
	}
}

Když vygenerovaný kód zavolá github.create_issue(), runtime zaznamená čekající metodu a argumenty a poté se pozastaví, než se spustí nástroj MCP. Schválení zahájí další průchod se stejným zdrojovým kódem a ID provedení. Dříve dokončená volání se přehrají z trvalého protokolu, schválené volání se provede a vygenerovaný kód pokračuje.

Čekající schválení a historie provedení přetrvávají i po dokončení požadavku a hibernaci Durable Object. Model provedení a přehrávání najdete v Schválení pomocí přerušení a přehrání.

Vytvářejte uživatelská rozhraní pro schvalování ve Workflow

Seznam čekajících schválení

Použijte stav agenta k zobrazení čekajících schválení ve svém UI:

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>
	);
}

Přidání více schvalovatelů Workflow

Pro citlivé operace vyžadující více schvalovatelů:

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
	}
}

Osvědčené postupy

  1. Definujte jasná kritéria pro schvalování : Potvrzení vyžadujte pouze u akcí s významnými důsledky (platby, e-maily, změny dat)
  2. Poskytování podrobného kontextu : Ukažte uživatelům přesně, co daná akce provede, včetně všech argumentů
  3. Implementace časových limitů : použijte schedule() k eskalaci nebo automatickému odmítnutí po přiměřené době
  4. Udržujte auditní stopy : použijte this.sql k zaznamenávání všech rozhodnutí o schválení pro účely compliance
  5. Zpracování výpadků spojení : Ukládejte čekající schválení do stavu agenta, aby přežila odpojení
  6. Elegantní degradace : Zajistěte náhradní chování pro případ zamítnutí schválení

Další kroky

Spouštění Workflows

Kompletní referenční dokumentace API pro waitForApproval().

Klienti MCP

Zpracovávejte požadavky na elicitaci v agentovi, který funguje jako klient MCP.

MCP servery

Vyžádá elicitaci formuláře nebo URL od serveru MCP.

Trvalý runtime Code Mode

Před voláními konektoru pozastavte kód vygenerovaný modelem kvůli schválení.