← Vytvářejte agenty na Cloudflare / agents / concepts / agentic-patterns
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?
- Dodržování předpisů: Regulatorní požadavky mohou u určitých akcí vyžadovat schválení člověkem
- Bezpečnost: Operace s vysokým rizikem (platby, mazání, externí komunikace) vyžadují dohled
- Kvalita: Lidská kontrola zachytí chyby, které by AI mohla přehlédnout
- Důvěra: Uživatelé se cítí jistěji, když mohou schvalovat kritické akce
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
- 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)
- Poskytování podrobného kontextu : Ukažte uživatelům přesně, co daná akce provede, včetně všech argumentů
- Implementace časových limitů : použijte
schedule()k eskalaci nebo automatickému odmítnutí po přiměřené době - Udržujte auditní stopy : použijte
this.sqlk zaznamenávání všech rozhodnutí o schválení pro účely compliance - Zpracování výpadků spojení : Ukládejte čekající schválení do stavu agenta, aby přežila odpojení
- Elegantní degradace : Zajistěte náhradní chování pro případ zamítnutí schválení