agents 0.16.2 → 0.17.1

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (126) hide show
  1. package/README.md +11 -8
  2. package/dist/{agent-tool-types-CTw3UJUP.d.ts → agent-tool-types-CNyE1iz_.d.ts} +671 -138
  3. package/dist/agent-tool-types.d.ts +34 -18
  4. package/dist/agent-tool-types.js +20 -1
  5. package/dist/agent-tool-types.js.map +1 -0
  6. package/dist/agent-tools-BXlsuX0d.js +304 -0
  7. package/dist/agent-tools-BXlsuX0d.js.map +1 -0
  8. package/dist/agent-tools-CSnyGvJ2.d.ts +119 -0
  9. package/dist/agent-tools.d.ts +24 -18
  10. package/dist/agent-tools.js.map +1 -1
  11. package/dist/ai-chat-agent.d.ts +1 -1
  12. package/dist/ai-chat-agent.js +1 -2
  13. package/dist/ai-chat-agent.js.map +1 -1
  14. package/dist/ai-chat-v5-migration.d.ts +1 -1
  15. package/dist/ai-chat-v5-migration.js +1 -2
  16. package/dist/ai-chat-v5-migration.js.map +1 -1
  17. package/dist/ai-react.d.ts +1 -1
  18. package/dist/ai-react.js +1 -2
  19. package/dist/ai-react.js.map +1 -1
  20. package/dist/ai-types.d.ts +1 -7
  21. package/dist/ai-types.js +2 -8
  22. package/dist/ai-types.js.map +1 -1
  23. package/dist/browser/ai.js +1 -1
  24. package/dist/browser/index.js +1 -1
  25. package/dist/chat/index.d.ts +2033 -77
  26. package/dist/chat/index.js +1828 -245
  27. package/dist/chat/index.js.map +1 -1
  28. package/dist/chat/react.d.ts +602 -0
  29. package/dist/chat/react.js +1506 -0
  30. package/dist/chat/react.js.map +1 -0
  31. package/dist/chat-sdk/index.d.ts +4 -4
  32. package/dist/{classPrivateFieldGet2-CZ7QjTXN.js → classPrivateFieldGet2-DZBYAB34.js} +5 -5
  33. package/dist/{classPrivateMethodInitSpec-D-0__zd9.js → classPrivateMethodInitSpec-qMjJ6sHQ.js} +2 -2
  34. package/dist/{client-BXJ9n2f7.js → client-BZ-B3NhC.js} +2 -2
  35. package/dist/client-BZ-B3NhC.js.map +1 -0
  36. package/dist/client-tools-aIBO0Fk7.d.ts +53 -0
  37. package/dist/client.d.ts +76 -57
  38. package/dist/client.js +33 -5
  39. package/dist/client.js.map +1 -1
  40. package/dist/{connector-CrKhowfD.js → connector-CdldGF3h.js} +5 -5
  41. package/dist/{connector-CrKhowfD.js.map → connector-CdldGF3h.js.map} +1 -1
  42. package/dist/email.js +1 -1
  43. package/dist/email.js.map +1 -1
  44. package/dist/{index-B7IbEeze.d.ts → index-BRnybD6X.d.ts} +197 -14
  45. package/dist/index.d.ts +95 -73
  46. package/dist/index.js +694 -25
  47. package/dist/index.js.map +1 -1
  48. package/dist/mcp/client.d.ts +18 -14
  49. package/dist/mcp/client.js +1 -1
  50. package/dist/mcp/index.d.ts +30 -30
  51. package/dist/mcp/index.js +7 -7
  52. package/dist/mcp/index.js.map +1 -1
  53. package/dist/{agent-tools-3zLG7MgA.js → message-builder-BymO4N_D.js} +45 -126
  54. package/dist/message-builder-BymO4N_D.js.map +1 -0
  55. package/dist/observability/index.d.ts +1 -1
  56. package/dist/observability/index.js +4 -2
  57. package/dist/observability/index.js.map +1 -1
  58. package/dist/react.d.ts +123 -110
  59. package/dist/react.js +44 -11
  60. package/dist/react.js.map +1 -1
  61. package/dist/{retries-CwlpAGet.d.ts → retries-CvHJwSuh.d.ts} +15 -6
  62. package/dist/retries.d.ts +8 -6
  63. package/dist/retries.js +44 -1
  64. package/dist/retries.js.map +1 -1
  65. package/dist/serializable.d.ts +1 -1
  66. package/dist/skills/compile.js +1 -1
  67. package/dist/skills/compile.js.map +1 -1
  68. package/dist/skills/index.js +5 -5
  69. package/dist/skills/index.js.map +1 -1
  70. package/dist/sub-routing.d.ts +6 -6
  71. package/dist/utils.js +1 -1
  72. package/dist/utils.js.map +1 -1
  73. package/dist/vite.js +5 -5
  74. package/dist/vite.js.map +1 -1
  75. package/dist/wire-types-nflOzNuU.js +240 -0
  76. package/dist/wire-types-nflOzNuU.js.map +1 -0
  77. package/dist/{workflow-types-SrZK_o9p.d.ts → workflow-types-Baz_PO5v.d.ts} +42 -22
  78. package/dist/workflow-types.d.ts +25 -21
  79. package/dist/workflow-types.js.map +1 -1
  80. package/dist/workflows.d.ts +22 -21
  81. package/dist/workflows.js +31 -7
  82. package/dist/workflows.js.map +1 -1
  83. package/docs/adding-to-existing-project.md +450 -0
  84. package/docs/agent-class.md +503 -0
  85. package/docs/agent-tools.md +552 -0
  86. package/docs/browse-the-web.md +430 -0
  87. package/docs/callable-methods.md +627 -0
  88. package/docs/chat-agents.md +1696 -0
  89. package/docs/chat-sdk.md +181 -0
  90. package/docs/client-sdk.md +520 -0
  91. package/docs/client-tools-continuation.md +177 -0
  92. package/docs/codemode.md +440 -0
  93. package/docs/configuration.md +775 -0
  94. package/docs/cross-domain-authentication.md +171 -0
  95. package/docs/durable-execution.md +537 -0
  96. package/docs/email.md +663 -0
  97. package/docs/get-current-agent.md +204 -0
  98. package/docs/getting-started.md +305 -0
  99. package/docs/http-websockets.md +668 -0
  100. package/docs/human-in-the-loop.md +661 -0
  101. package/docs/index.md +151 -0
  102. package/docs/long-running-agents.md +730 -0
  103. package/docs/mcp-client.md +620 -0
  104. package/docs/mcp-servers.md +526 -0
  105. package/docs/mcp-transports.md +308 -0
  106. package/docs/migration-to-ai-sdk-v5.md +96 -0
  107. package/docs/migration-to-ai-sdk-v6.md +163 -0
  108. package/docs/observability.md +261 -0
  109. package/docs/push-notifications.md +367 -0
  110. package/docs/queue.md +329 -0
  111. package/docs/readonly-connections.md +278 -0
  112. package/docs/resumable-streaming.md +127 -0
  113. package/docs/retries.md +444 -0
  114. package/docs/routing.md +749 -0
  115. package/docs/scheduling.md +898 -0
  116. package/docs/securing-mcp-servers.md +359 -0
  117. package/docs/server-driven-messages.md +477 -0
  118. package/docs/sessions.md +1024 -0
  119. package/docs/state.md +512 -0
  120. package/docs/sub-agents.md +389 -0
  121. package/docs/webhooks.md +604 -0
  122. package/docs/workflows.md +877 -0
  123. package/package.json +41 -14
  124. package/dist/agent-tools-3zLG7MgA.js.map +0 -1
  125. package/dist/agent-tools-DZhI5F6Q.d.ts +0 -14
  126. package/dist/client-BXJ9n2f7.js.map +0 -1
@@ -0,0 +1,661 @@
1
+ # Human in the Loop
2
+
3
+ Human-in-the-loop (HITL) patterns allow agents to pause execution and wait for human approval, confirmation, or input before proceeding. This is essential for compliance, safety, and oversight in agentic systems.
4
+
5
+ ## Overview
6
+
7
+ ### Why Human in the Loop?
8
+
9
+ - **Compliance**: Regulatory requirements may mandate human approval for certain actions
10
+ - **Safety**: High-stakes operations (payments, deletions, external communications) need oversight
11
+ - **Quality**: Human review catches errors AI might miss
12
+ - **Trust**: Users feel more confident when they can approve critical actions
13
+
14
+ ### Common Use Cases
15
+
16
+ | Use Case | Example |
17
+ | ------------------- | ---------------------------------------- |
18
+ | Financial approvals | Expense reports, payment processing |
19
+ | Content moderation | Publishing, email sending |
20
+ | Data operations | Bulk deletions, exports |
21
+ | AI tool execution | Confirming LLM tool calls before running |
22
+ | Access control | Granting permissions, role changes |
23
+
24
+ ## Choosing an Approach
25
+
26
+ Agents SDK supports multiple human-in-the-loop patterns. Choose based on your use case:
27
+
28
+ | Use Case | Pattern | Best For | Example |
29
+ | ---------------------- | ----------------- | -------------------------------------------------- | ------------------------------------------------------------------------------------------------------------ |
30
+ | Long-running workflows | Workflow Approval | Multi-step processes, durable approval gates | [examples/workflows/](https://github.com/cloudflare/agents/tree/main/examples/workflows) |
31
+ | AIChatAgent tools | `needsApproval` | Chat-based tool calls with `@cloudflare/ai-chat` | [guides/human-in-the-loop/](https://github.com/cloudflare/agents/tree/main/guides/human-in-the-loop) |
32
+ | OpenAI Agents SDK | `needsApproval` | Using OpenAI's agent SDK with conditional approval | [openai-sdk/human-in-the-loop/](https://github.com/cloudflare/agents/tree/main/openai-sdk/human-in-the-loop) |
33
+ | Client-side tools | `onToolCall` | Tools that need browser APIs or user interaction | Pattern below |
34
+ | MCP Servers | Elicitation | MCP tools requesting structured user input | [examples/mcp-elicitation/](https://github.com/cloudflare/agents/tree/main/examples/mcp-elicitation) |
35
+
36
+ ### Decision Guide
37
+
38
+ ```
39
+ Is this part of a multi-step workflow?
40
+ ├── Yes → Use Workflow Approval (waitForApproval)
41
+ └── No → Are you building an MCP server?
42
+ ├── Yes → Use MCP Elicitation (elicitInput)
43
+ └── No → Is this an AI chat interaction?
44
+ ├── Yes → Does the tool need browser APIs?
45
+ │ ├── Yes → Use onToolCall (client-side execution)
46
+ │ └── No → Use needsApproval (server-side with approval)
47
+ └── No → Use State + WebSocket for simple confirmations
48
+ ```
49
+
50
+ ## Workflow-Based Approval
51
+
52
+ For durable, multi-step processes, use Cloudflare Workflows with the `waitForApproval()` helper. The workflow pauses until a human approves or rejects.
53
+
54
+ ### Basic Pattern
55
+
56
+ ```typescript
57
+ import { Agent, AgentWorkflow, callable } from "agents";
58
+ import type { AgentWorkflowEvent, AgentWorkflowStep } from "agents";
59
+
60
+ // Workflow that pauses for approval
61
+ export class ExpenseWorkflow extends AgentWorkflow<
62
+ ExpenseAgent,
63
+ ExpenseParams
64
+ > {
65
+ async run(event: AgentWorkflowEvent<ExpenseParams>, step: AgentWorkflowStep) {
66
+ const expense = event.payload;
67
+
68
+ // Step 1: Validate the expense
69
+ const validated = await step.do("validate", async () => {
70
+ return validateExpense(expense);
71
+ });
72
+
73
+ // Step 2: Wait for manager approval
74
+ await this.reportProgress({
75
+ step: "approval",
76
+ status: "pending",
77
+ message: `Awaiting approval for $${expense.amount}`
78
+ });
79
+
80
+ // This pauses the workflow until approved/rejected
81
+ const approval = await this.waitForApproval<{ approvedBy: string }>(step, {
82
+ timeout: "7 days"
83
+ });
84
+
85
+ console.log(`Approved by: ${approval.approvedBy}`);
86
+
87
+ // Step 3: Process the approved expense
88
+ const result = await step.do("process", async () => {
89
+ return processExpense(validated);
90
+ });
91
+
92
+ await step.reportComplete(result);
93
+ return result;
94
+ }
95
+ }
96
+ ```
97
+
98
+ ### Agent Methods for Approval
99
+
100
+ The agent provides methods to approve or reject waiting workflows:
101
+
102
+ ```typescript
103
+ export class ExpenseAgent extends Agent<Env, ExpenseState> {
104
+ initialState: ExpenseState = {
105
+ pendingApprovals: [],
106
+ status: "idle"
107
+ };
108
+
109
+ // Approve a waiting workflow
110
+ @callable()
111
+ async approve(workflowId: string, approvedBy: string): Promise<void> {
112
+ await this.approveWorkflow(workflowId, {
113
+ reason: "Expense approved",
114
+ metadata: { approvedBy, approvedAt: Date.now() }
115
+ });
116
+
117
+ // Update state to reflect approval
118
+ this.setState({
119
+ ...this.state,
120
+ pendingApprovals: this.state.pendingApprovals.filter(
121
+ (p) => p.workflowId !== workflowId
122
+ )
123
+ });
124
+ }
125
+
126
+ // Reject a waiting workflow
127
+ @callable()
128
+ async reject(workflowId: string, reason: string): Promise<void> {
129
+ await this.rejectWorkflow(workflowId, { reason });
130
+
131
+ this.setState({
132
+ ...this.state,
133
+ pendingApprovals: this.state.pendingApprovals.filter(
134
+ (p) => p.workflowId !== workflowId
135
+ )
136
+ });
137
+ }
138
+
139
+ // Track workflow progress
140
+ async onWorkflowProgress(
141
+ workflowName: string,
142
+ workflowId: string,
143
+ progress: unknown
144
+ ): Promise<void> {
145
+ const p = progress as { step: string; status: string };
146
+
147
+ if (p.step === "approval" && p.status === "pending") {
148
+ // Add to pending approvals list
149
+ this.setState({
150
+ ...this.state,
151
+ pendingApprovals: [
152
+ ...this.state.pendingApprovals,
153
+ { workflowId, requestedAt: Date.now() }
154
+ ]
155
+ });
156
+ }
157
+ }
158
+ }
159
+ ```
160
+
161
+ ### Timeout Handling
162
+
163
+ Set timeouts to prevent workflows from waiting indefinitely:
164
+
165
+ ```typescript
166
+ const approval = await this.waitForApproval(step, {
167
+ timeout: "7 days" // or "1 hour", "30 minutes", etc.
168
+ });
169
+ ```
170
+
171
+ If the timeout expires, the workflow continues without approval data. Handle this case:
172
+
173
+ ```typescript
174
+ const approval = await this.waitForApproval<{ approvedBy: string }>(step, {
175
+ timeout: "24 hours"
176
+ });
177
+
178
+ if (!approval) {
179
+ // Timeout expired - escalate or auto-reject
180
+ await step.reportError("Approval timeout - escalating to manager");
181
+ throw new Error("Approval timeout");
182
+ }
183
+ ```
184
+
185
+ For more details, see [Workflows Integration](./workflows.md).
186
+
187
+ ## AI Tool Approval with `needsApproval`
188
+
189
+ When building AI chat agents, you often want humans to approve certain tool calls before execution. The AI SDK's `needsApproval` option pauses tool execution until the user approves or rejects.
190
+
191
+ ### Server
192
+
193
+ Define tools with `needsApproval` to require human confirmation:
194
+
195
+ ```typescript
196
+ import { AIChatAgent } from "@cloudflare/ai-chat";
197
+ import { createWorkersAI } from "workers-ai-provider";
198
+ import { streamText, tool, convertToModelMessages } from "ai";
199
+ import { z } from "zod";
200
+
201
+ export class MyAgent extends AIChatAgent {
202
+ async onChatMessage() {
203
+ const workersai = createWorkersAI({ binding: this.env.AI });
204
+
205
+ const result = streamText({
206
+ model: workersai("@cf/moonshotai/kimi-k2.7-code"),
207
+ messages: await convertToModelMessages(this.messages),
208
+ tools: {
209
+ // Tool with conditional approval
210
+ processPayment: tool({
211
+ description: "Process a payment",
212
+ inputSchema: z.object({
213
+ amount: z.number(),
214
+ recipient: z.string()
215
+ }),
216
+ // Approval required for amounts over $100
217
+ needsApproval: async ({ amount }) => amount > 100,
218
+ execute: async ({ amount, recipient }) => {
219
+ return await chargeCard(amount, recipient);
220
+ }
221
+ }),
222
+
223
+ // Tool that always requires approval
224
+ deleteAccount: tool({
225
+ description: "Delete a user account",
226
+ inputSchema: z.object({ userId: z.string() }),
227
+ needsApproval: true,
228
+ execute: async ({ userId }) => {
229
+ return await deleteUser(userId);
230
+ }
231
+ }),
232
+
233
+ // Tool that executes automatically (no approval)
234
+ getWeather: tool({
235
+ description: "Get weather for a city",
236
+ inputSchema: z.object({ city: z.string() }),
237
+ execute: async ({ city }) => fetchWeather(city)
238
+ })
239
+ },
240
+ maxSteps: 5
241
+ });
242
+
243
+ return result.toUIMessageStreamResponse();
244
+ }
245
+ }
246
+ ```
247
+
248
+ ### Client
249
+
250
+ Handle approval requests with `addToolApprovalResponse`:
251
+
252
+ ```tsx
253
+ import { useAgent } from "agents/react";
254
+ import { useAgentChat } from "@cloudflare/ai-chat/react";
255
+ import { isToolUIPart, getToolName } from "ai";
256
+
257
+ function Chat() {
258
+ const agent = useAgent({ agent: "MyAgent" });
259
+ const { messages, sendMessage, addToolApprovalResponse } = useAgentChat({
260
+ agent
261
+ });
262
+
263
+ return (
264
+ <div>
265
+ {messages.map((message) => (
266
+ <div key={message.id}>
267
+ {message.parts?.map((part, i) => {
268
+ if (part.type === "text") {
269
+ return <p key={i}>{part.text}</p>;
270
+ }
271
+
272
+ if (isToolUIPart(part)) {
273
+ // Tool waiting for approval
274
+ if ("approval" in part && part.state === "approval-requested") {
275
+ const approvalId = part.approval?.id;
276
+ return (
277
+ <div key={part.toolCallId} className="approval-card">
278
+ <p>
279
+ Approve <strong>{getToolName(part)}</strong> with{" "}
280
+ {JSON.stringify(part.input)}?
281
+ </p>
282
+ <button
283
+ onClick={() =>
284
+ addToolApprovalResponse({
285
+ id: approvalId,
286
+ approved: true
287
+ })
288
+ }
289
+ >
290
+ Approve
291
+ </button>
292
+ <button
293
+ onClick={() =>
294
+ addToolApprovalResponse({
295
+ id: approvalId,
296
+ approved: false
297
+ })
298
+ }
299
+ >
300
+ Reject
301
+ </button>
302
+ </div>
303
+ );
304
+ }
305
+
306
+ // Tool was denied
307
+ if (part.state === "output-denied") {
308
+ return (
309
+ <div key={part.toolCallId}>{getToolName(part)}: Denied</div>
310
+ );
311
+ }
312
+
313
+ // Tool completed
314
+ if (part.state === "output-available") {
315
+ return (
316
+ <div key={part.toolCallId}>
317
+ {getToolName(part)}: {JSON.stringify(part.output)}
318
+ </div>
319
+ );
320
+ }
321
+ }
322
+
323
+ return null;
324
+ })}
325
+ </div>
326
+ ))}
327
+ </div>
328
+ );
329
+ }
330
+ ```
331
+
332
+ ### Custom denial messages with `addToolOutput`
333
+
334
+ When a user rejects a tool, `addToolApprovalResponse({ id, approved: false })` sets the tool state to `output-denied` with a generic "Tool execution denied." message. If you need to give the LLM a more specific reason for the denial, use `addToolOutput` with `state: "output-error"` instead:
335
+
336
+ ```tsx
337
+ const { addToolOutput } = useAgentChat({ agent });
338
+
339
+ // Reject with a custom error message
340
+ addToolOutput({
341
+ toolCallId: part.toolCallId,
342
+ state: "output-error",
343
+ errorText: "User declined: insufficient budget for this quarter"
344
+ });
345
+ ```
346
+
347
+ This sends a `tool_result` to the LLM with your custom error text, so it can respond appropriately (e.g. suggest an alternative, ask clarifying questions). The `addToolOutput` function also works for tools in `approval-requested` or `approval-responded` states, not just `input-available`.
348
+
349
+ `addToolApprovalResponse` (with `approved: false`) auto-continues the conversation when `autoContinueAfterToolResult` is enabled (the default), so the LLM sees the denial and can respond naturally.
350
+
351
+ `addToolOutput` with `state: "output-error"` does **not** auto-continue — it gives you full control over what happens next. If you want the LLM to respond to the error, call `sendMessage()` afterward.
352
+
353
+ See the complete example: [guides/human-in-the-loop/](https://github.com/cloudflare/agents/tree/main/guides/human-in-the-loop)
354
+
355
+ ### Surviving restarts while waiting for a human
356
+
357
+ A Durable Object can be evicted at any time (a deploy, an inactivity timeout, a resource limit), including while a turn is paused on an approval prompt or a client-side tool call. When [`chatRecovery`](./chat-agents.md#stream-recovery) is enabled (the default for `Think`), the SDK recognizes that such a turn is _waiting on the human_, not stuck, and does **not** seal it: the no-progress window, attempt cap, `maxRecoveryWork`, and `shouldKeepRecovering` are all suspended while the interaction is pending. Recovery parks the turn instead of failing it, and the user's eventual approval or `tool_result` resumes the conversation through the normal continuation path. A user who takes minutes to respond to a prompt that was interrupted by a deploy therefore does not see a spurious "session interrupted" error.
358
+
359
+ This protection applies to interactions only the client can resolve — `approval-requested` parts and `input-available` parts for client-side tools (those without a server `execute`). A server tool whose `execute()` was killed mid-flight is a genuine orphan and recovers through the normal transcript-repair path instead.
360
+
361
+ ## Client-Side Tool Execution with `onToolCall`
362
+
363
+ For tools that need browser APIs (geolocation, camera, clipboard) or user interaction, define the tool on the server without an `execute` function and handle it on the client with `onToolCall`:
364
+
365
+ ### Server
366
+
367
+ ```typescript
368
+ export class MyAgent extends AIChatAgent {
369
+ async onChatMessage() {
370
+ const workersai = createWorkersAI({ binding: this.env.AI });
371
+
372
+ const result = streamText({
373
+ model: workersai("@cf/moonshotai/kimi-k2.7-code"),
374
+ messages: await convertToModelMessages(this.messages),
375
+ tools: {
376
+ // No execute function - client handles via onToolCall
377
+ getUserLocation: tool({
378
+ description: "Get the user's current location from their browser",
379
+ inputSchema: z.object({})
380
+ })
381
+ },
382
+ maxSteps: 3
383
+ });
384
+
385
+ return result.toUIMessageStreamResponse();
386
+ }
387
+ }
388
+ ```
389
+
390
+ ### Client
391
+
392
+ ```tsx
393
+ const { messages, sendMessage } = useAgentChat({
394
+ agent,
395
+ onToolCall: async ({ toolCall, addToolOutput }) => {
396
+ if (toolCall.toolName === "getUserLocation") {
397
+ const position = await new Promise((resolve, reject) => {
398
+ navigator.geolocation.getCurrentPosition(resolve, reject);
399
+ });
400
+ addToolOutput({
401
+ toolCallId: toolCall.toolCallId,
402
+ output: {
403
+ lat: position.coords.latitude,
404
+ lng: position.coords.longitude
405
+ }
406
+ });
407
+ }
408
+ }
409
+ });
410
+ ```
411
+
412
+ The server receives the tool output via `CF_AGENT_TOOL_RESULT` and can auto-continue the conversation (with `maxSteps > 1`), letting the LLM respond to the location data in the same turn.
413
+
414
+ ### OpenAI Agents SDK Pattern
415
+
416
+ When using the [OpenAI Agents SDK](https://openai.github.io/openai-agents-js/), use the `needsApproval` function for conditional approval:
417
+
418
+ ```typescript
419
+ import { Agent } from "agents";
420
+ import { tool, run } from "@openai/agents";
421
+
422
+ export class WeatherAgent extends Agent<Env, AgentState> {
423
+ async processQuery(query: string) {
424
+ const weatherTool = tool({
425
+ name: "get_weather",
426
+ description: "Get weather for a location",
427
+ parameters: z.object({ location: z.string() }),
428
+
429
+ // Conditional approval - only for certain locations
430
+ needsApproval: async (_context, { location }) => {
431
+ return location === "San Francisco"; // Require approval for SF
432
+ },
433
+
434
+ execute: async ({ location }) => {
435
+ const conditions = ["sunny", "cloudy", "rainy"];
436
+ return conditions[Math.floor(Math.random() * conditions.length)];
437
+ }
438
+ });
439
+
440
+ const result = await run(this.openai, {
441
+ model: "gpt-4o",
442
+ tools: [weatherTool],
443
+ input: query
444
+ });
445
+
446
+ return result;
447
+ }
448
+ }
449
+ ```
450
+
451
+ See the complete example: [openai-sdk/human-in-the-loop/](https://github.com/cloudflare/agents/tree/main/openai-sdk/human-in-the-loop)
452
+
453
+ ### MCP Elicitation
454
+
455
+ When building MCP servers with `McpAgent`, you can request additional user input during tool execution using **elicitation**. The MCP client (like Claude Desktop) renders a form based on your JSON Schema and returns the user's response.
456
+
457
+ ```typescript
458
+ import { McpServer } from "@modelcontextprotocol/sdk/server/mcp.js";
459
+ import { Agent } from "agents";
460
+
461
+ export class MyMcpAgent extends Agent<Env, State> {
462
+ server = new McpServer({
463
+ name: "my-mcp-server",
464
+ version: "1.0.0"
465
+ });
466
+
467
+ onStart() {
468
+ this.server.registerTool(
469
+ "increase-counter",
470
+ {
471
+ description: "Increase the counter by a user-specified amount",
472
+ inputSchema: {
473
+ confirm: z.boolean().describe("Do you want to increase the counter?")
474
+ }
475
+ },
476
+ async ({ confirm }, extra) => {
477
+ if (!confirm) {
478
+ return { content: [{ type: "text", text: "Cancelled." }] };
479
+ }
480
+
481
+ // Request additional input from the user
482
+ const userInput = await this.server.server.elicitInput(
483
+ {
484
+ message: "By how much do you want to increase the counter?",
485
+ requestedSchema: {
486
+ type: "object",
487
+ properties: {
488
+ amount: {
489
+ type: "number",
490
+ title: "Amount",
491
+ description: "The amount to increase the counter by"
492
+ }
493
+ },
494
+ required: ["amount"]
495
+ }
496
+ },
497
+ { relatedRequestId: extra.requestId }
498
+ );
499
+
500
+ // Check if user accepted or cancelled
501
+ if (userInput.action !== "accept" || !userInput.content) {
502
+ return { content: [{ type: "text", text: "Cancelled." }] };
503
+ }
504
+
505
+ // Use the input
506
+ const amount = Number(userInput.content.amount);
507
+ this.setState({
508
+ ...this.state,
509
+ counter: this.state.counter + amount
510
+ });
511
+
512
+ return {
513
+ content: [
514
+ {
515
+ type: "text",
516
+ text: `Counter increased by ${amount}, now at ${this.state.counter}`
517
+ }
518
+ ]
519
+ };
520
+ }
521
+ );
522
+ }
523
+ }
524
+ ```
525
+
526
+ **Key differences from other patterns:**
527
+
528
+ - Used by **MCP servers** exposing tools to clients, not agents calling tools
529
+ - Uses **JSON Schema** for structured form-based input
530
+ - The **MCP client** (Claude Desktop, etc.) handles UI rendering
531
+ - Returns `{ action: "accept" | "decline", content: {...} }`
532
+
533
+ See the complete example: [examples/mcp-elicitation/](https://github.com/cloudflare/agents/tree/main/examples/mcp-elicitation)
534
+
535
+ ## State Patterns for Approvals
536
+
537
+ Track pending approvals in agent state for UI rendering and persistence:
538
+
539
+ ```typescript
540
+ type PendingApproval = {
541
+ id: string;
542
+ workflowId?: string;
543
+ type: "expense" | "publish" | "delete";
544
+ description: string;
545
+ amount?: number;
546
+ requestedBy: string;
547
+ requestedAt: number;
548
+ expiresAt?: number;
549
+ };
550
+
551
+ type ApprovalRecord = {
552
+ id: string;
553
+ approvalId: string;
554
+ decision: "approved" | "rejected";
555
+ decidedBy: string;
556
+ decidedAt: number;
557
+ reason?: string;
558
+ };
559
+
560
+ type ApprovalState = {
561
+ pending: PendingApproval[];
562
+ history: ApprovalRecord[];
563
+ };
564
+ ```
565
+
566
+ ### Multi-Approver Patterns
567
+
568
+ For sensitive operations requiring multiple approvers:
569
+
570
+ ```typescript
571
+ type MultiApproval = {
572
+ id: string;
573
+ requiredApprovals: number; // e.g., 2
574
+ currentApprovals: Array<{
575
+ userId: string;
576
+ approvedAt: number;
577
+ }>;
578
+ rejections: Array<{
579
+ userId: string;
580
+ rejectedAt: number;
581
+ reason: string;
582
+ }>;
583
+ };
584
+
585
+ @callable()
586
+ async approveMulti(approvalId: string, userId: string): Promise<boolean> {
587
+ const approval = this.state.pending.find(p => p.id === approvalId);
588
+ if (!approval) throw new Error("Approval not found");
589
+
590
+ // Add this user's approval
591
+ approval.currentApprovals.push({ userId, approvedAt: Date.now() });
592
+
593
+ // Check if we have enough approvals
594
+ if (approval.currentApprovals.length >= approval.requiredApprovals) {
595
+ // Execute the approved action
596
+ await this.executeApprovedAction(approval);
597
+ return true;
598
+ }
599
+
600
+ this.setState({ ...this.state });
601
+ return false; // Still waiting for more approvals
602
+ }
603
+ ```
604
+
605
+ ## Timeouts and Escalation
606
+
607
+ ### Setting Approval Timeouts
608
+
609
+ ```typescript
610
+ const approval = await this.waitForApproval(step, {
611
+ timeout: "24 hours"
612
+ });
613
+ ```
614
+
615
+ ### Escalation with Scheduling
616
+
617
+ Use `schedule()` to set up escalation reminders:
618
+
619
+ ```typescript
620
+ @callable()
621
+ async submitForApproval(request: ApprovalRequest): Promise<string> {
622
+ const approvalId = crypto.randomUUID();
623
+
624
+ // Add to pending
625
+ this.setState({
626
+ ...this.state,
627
+ pending: [...this.state.pending, { id: approvalId, ...request }]
628
+ });
629
+
630
+ // Schedule reminder after 4 hours
631
+ await this.schedule(
632
+ Date.now() + 4 * 60 * 60 * 1000,
633
+ "sendReminder",
634
+ { approvalId }
635
+ );
636
+
637
+ // Schedule escalation after 24 hours
638
+ await this.schedule(
639
+ Date.now() + 24 * 60 * 60 * 1000,
640
+ "escalateApproval",
641
+ { approvalId }
642
+ );
643
+
644
+ return approvalId;
645
+ }
646
+ ```
647
+
648
+ ## Complete Examples
649
+
650
+ | Pattern | Location | Description |
651
+ | ----------------- | ------------------------------------------------------------------------------------------------------------ | -------------------------------------------------- |
652
+ | Workflow approval | [examples/workflows/](https://github.com/cloudflare/agents/tree/main/examples/workflows) | Multi-step task processing with approval gate |
653
+ | AIChatAgent tools | [guides/human-in-the-loop/](https://github.com/cloudflare/agents/tree/main/guides/human-in-the-loop) | Chat tool approval with needsApproval + onToolCall |
654
+ | OpenAI Agents SDK | [openai-sdk/human-in-the-loop/](https://github.com/cloudflare/agents/tree/main/openai-sdk/human-in-the-loop) | Conditional tool approval with modal |
655
+ | MCP Elicitation | [examples/mcp-elicitation/](https://github.com/cloudflare/agents/tree/main/examples/mcp-elicitation) | MCP server requesting structured user input |
656
+
657
+ For detailed API documentation, see:
658
+
659
+ - [Workflows](./workflows.md) - `waitForApproval()`, `approveWorkflow()`, `rejectWorkflow()`
660
+ - [MCP Servers](./mcp-servers.md) - `elicitInput()` for MCP elicitation
661
+ - [Callable Methods](./callable-methods.md) - `@callable()` decorator for approval endpoints