agents 0.20.1 → 0.22.0

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 (169) hide show
  1. package/README.md +51 -12
  2. package/dist/{agent-tool-types-BC-WFlsz.d.ts → agent-routing-CnkaHb-v.d.ts} +3305 -3138
  3. package/dist/agent-routing.d.ts +14 -0
  4. package/dist/agent-routing.js +187 -0
  5. package/dist/agent-routing.js.map +1 -0
  6. package/dist/agent-tool-types.d.ts +26 -26
  7. package/dist/{agent-tools-DeHe9Xov.d.ts → agent-tools-C0XJqWzB.d.ts} +7 -7
  8. package/dist/agent-tools.d.ts +44 -28
  9. package/dist/agent-tools.js +16 -7
  10. package/dist/agent-tools.js.map +1 -1
  11. package/dist/browser/ai.d.ts +5 -3
  12. package/dist/browser/ai.js +88 -8
  13. package/dist/browser/ai.js.map +1 -1
  14. package/dist/browser/index.d.ts +1 -1
  15. package/dist/browser/index.js +1 -1
  16. package/dist/browser/tanstack-ai.js +13 -1
  17. package/dist/browser/tanstack-ai.js.map +1 -1
  18. package/dist/capability-BjSKYpzg.js +42 -0
  19. package/dist/capability-BjSKYpzg.js.map +1 -0
  20. package/dist/capability-runner-CvHGZqUu.d.ts +150 -0
  21. package/dist/chat/index.d.ts +51 -15
  22. package/dist/chat/index.js +59 -4
  23. package/dist/chat/index.js.map +1 -1
  24. package/dist/chat/react.d.ts +19 -180
  25. package/dist/chat/react.js +93 -600
  26. package/dist/chat/react.js.map +1 -1
  27. package/dist/chat/transport.d.ts +10 -0
  28. package/dist/chat/transport.js +2 -0
  29. package/dist/chat-sdk/index.d.ts +4 -4
  30. package/dist/chat-sdk/index.js +1 -1
  31. package/dist/{client-zqKcsyFa.js → client-jagG8a9_.js} +129 -37
  32. package/dist/client-jagG8a9_.js.map +1 -0
  33. package/dist/client.d.ts +1 -1
  34. package/dist/client.js +1 -1
  35. package/dist/{cloudflare-BduZwmYK.js → cloudflare-Dzvc7V2N.js} +10 -3
  36. package/dist/{cloudflare-BduZwmYK.js.map → cloudflare-Dzvc7V2N.js.map} +1 -1
  37. package/dist/{connector-v2M1zlZp.d.ts → connector-CkQD4MK3.d.ts} +20 -3
  38. package/dist/{connector-KEJnl6e5.js → connector-CptFKzRh.js} +158 -40
  39. package/dist/connector-CptFKzRh.js.map +1 -0
  40. package/dist/current-agent-CuMErtly.d.ts +283 -0
  41. package/dist/current-agent-DhoDkSnH.js +51 -0
  42. package/dist/current-agent-DhoDkSnH.js.map +1 -0
  43. package/dist/diagnostics-CaBjfz4J.js +41 -0
  44. package/dist/diagnostics-CaBjfz4J.js.map +1 -0
  45. package/dist/{do-oauth-client-provider-VTZj2VtM.d.ts → do-oauth-client-provider-Tmf1vgKz.d.ts} +2 -2
  46. package/dist/durable-object-lifecycle-D6nNQJJd.js +862 -0
  47. package/dist/durable-object-lifecycle-D6nNQJJd.js.map +1 -0
  48. package/dist/{email-CL27preh.d.ts → email-7TatiTnl.d.ts} +38 -9
  49. package/dist/email-send.d.ts +15 -0
  50. package/dist/email-send.js +32 -0
  51. package/dist/email-send.js.map +1 -0
  52. package/dist/email.d.ts +14 -10
  53. package/dist/email.js.map +1 -1
  54. package/dist/experimental/memory/session/index.d.ts +3 -2
  55. package/dist/experimental/memory/session/index.js +15 -10
  56. package/dist/experimental/memory/session/index.js.map +1 -1
  57. package/dist/{handler-stateless-C_bo-Ytq.d.ts → handler-stateless-DxYpJ_XF.d.ts} +3 -3
  58. package/dist/{handler-stateless-CIkKPETH.js → handler-stateless-VvrWSAVA.js} +5 -5
  59. package/dist/handler-stateless-VvrWSAVA.js.map +1 -0
  60. package/dist/{index-BRnybD6X.d.ts → index-DcSAZKsB.d.ts} +19 -31
  61. package/dist/index.d.ts +102 -91
  62. package/dist/index.js +12 -7235
  63. package/dist/internal_context-BlxFEWfn.d.ts +19 -0
  64. package/dist/internal_context.d.ts +10 -4
  65. package/dist/internal_context.js +1 -10
  66. package/dist/{client-invoker-BNSZxAkv.d.ts → invoker-CG0_p_Wq.d.ts} +2 -2
  67. package/dist/{client-invoker-VNZ7X0nn.js → invoker-CHMnoxIA.js} +2 -2
  68. package/dist/invoker-CHMnoxIA.js.map +1 -0
  69. package/dist/lifecycle/index.d.ts +48 -0
  70. package/dist/lifecycle/index.js +4 -0
  71. package/dist/mcp/{do-oauth-client-provider.d.ts → client/do-oauth-client-provider.d.ts} +1 -1
  72. package/dist/mcp/{do-oauth-client-provider.js → client/do-oauth-client-provider.js} +1 -1
  73. package/dist/mcp/client/do-oauth-client-provider.js.map +1 -0
  74. package/dist/mcp/client/index.d.ts +42 -0
  75. package/dist/mcp/{client.js → client/index.js} +1 -1
  76. package/dist/mcp/{x402.d.ts → client/x402.d.ts} +2 -2
  77. package/dist/mcp/{x402.js → client/x402.js} +2 -2
  78. package/dist/mcp/client/x402.js.map +1 -0
  79. package/dist/mcp/index.d.ts +36 -36
  80. package/dist/mcp/index.js +14 -16
  81. package/dist/mcp/index.js.map +1 -1
  82. package/dist/mcp/{server.d.ts → server/index.d.ts} +1 -1
  83. package/dist/mcp/{server.js → server/index.js} +1 -1
  84. package/dist/observability/ai/index.d.ts +1 -130
  85. package/dist/observability/ai/index.js +88 -433
  86. package/dist/observability/ai/index.js.map +1 -1
  87. package/dist/observability/index.d.ts +4 -4
  88. package/dist/observability/index.js +3 -50
  89. package/dist/observability/index.js.map +1 -1
  90. package/dist/{wire-types-CU9rLoeS.js → protocol-B0nh6KNf.js} +20 -66
  91. package/dist/protocol-B0nh6KNf.js.map +1 -0
  92. package/dist/react.d.ts +4 -4
  93. package/dist/react.js +6 -9
  94. package/dist/react.js.map +1 -1
  95. package/dist/schedule.d.ts +25 -94
  96. package/dist/schedule.js +1 -98
  97. package/dist/schedule.js.map +1 -1
  98. package/dist/scheduler-CR9RHGos.js +857 -0
  99. package/dist/scheduler-CR9RHGos.js.map +1 -0
  100. package/dist/scheduler-DQoTGoAW.d.ts +224 -0
  101. package/dist/schedules/index.d.ts +22 -0
  102. package/dist/schedules/index.js +2 -0
  103. package/dist/schedules/parser.d.ts +79 -0
  104. package/dist/schedules/parser.js +103 -0
  105. package/dist/schedules/parser.js.map +1 -0
  106. package/dist/serializable.d.ts +1 -1
  107. package/dist/sql-error-CPY-GXyI.d.ts +12 -0
  108. package/dist/sql-error.d.ts +2 -0
  109. package/dist/sql-error.js +16 -0
  110. package/dist/sql-error.js.map +1 -0
  111. package/dist/src-5W6JNKVb.js +6613 -0
  112. package/dist/src-5W6JNKVb.js.map +1 -0
  113. package/dist/sub-routing.d.ts +18 -6
  114. package/dist/sub-routing.js +92 -2
  115. package/dist/sub-routing.js.map +1 -1
  116. package/dist/wire-types-CnMt6_HR.js +47 -0
  117. package/dist/wire-types-CnMt6_HR.js.map +1 -0
  118. package/dist/workflow-types.d.ts +25 -25
  119. package/dist/workflow-types.js.map +1 -1
  120. package/dist/workflows.d.ts +22 -22
  121. package/dist/workflows.js +2 -1
  122. package/dist/workflows.js.map +1 -1
  123. package/dist/ws-chat-transport-UNRIS2xl.d.ts +184 -0
  124. package/dist/ws-chat-transport-rWwta645.js +698 -0
  125. package/dist/ws-chat-transport-rWwta645.js.map +1 -0
  126. package/docs/adding-to-existing-project.md +4 -2
  127. package/docs/agent-class.md +29 -87
  128. package/docs/agent-tools.md +29 -0
  129. package/docs/browse-the-web.md +16 -1
  130. package/docs/chat-agents.md +16 -13
  131. package/docs/client-sdk.md +12 -8
  132. package/docs/configuration.md +7 -1
  133. package/docs/cross-domain-authentication.md +7 -35
  134. package/docs/durable-execution.md +1 -1
  135. package/docs/email.md +2 -13
  136. package/docs/http-websockets.md +1 -11
  137. package/docs/human-in-the-loop.md +16 -13
  138. package/docs/index.md +1 -0
  139. package/docs/lifecycle.md +323 -0
  140. package/docs/long-running-agents.md +13 -15
  141. package/docs/mcp-client.md +55 -0
  142. package/docs/mcp-servers.md +6 -0
  143. package/docs/mcp-transports.md +18 -12
  144. package/docs/migration-to-ai-sdk-v5.md +2 -2
  145. package/docs/migration-to-ai-sdk-v6.md +5 -1
  146. package/docs/observability.md +33 -77
  147. package/docs/resumable-streaming.md +1 -1
  148. package/docs/routing.md +27 -0
  149. package/docs/scheduling.md +175 -15
  150. package/docs/server-driven-messages.md +1 -1
  151. package/docs/sub-agents.md +47 -2
  152. package/docs/webhooks.md +109 -136
  153. package/docs/workflows.md +9 -1
  154. package/package.json +41 -23
  155. package/dist/cli/index.d.ts +0 -1
  156. package/dist/cli/index.js +0 -26
  157. package/dist/cli/index.js.map +0 -1
  158. package/dist/client-invoker-VNZ7X0nn.js.map +0 -1
  159. package/dist/client-zqKcsyFa.js.map +0 -1
  160. package/dist/connector-KEJnl6e5.js.map +0 -1
  161. package/dist/handler-stateless-CIkKPETH.js.map +0 -1
  162. package/dist/index.js.map +0 -1
  163. package/dist/internal_context-Dg4Cgjcu.d.ts +0 -37
  164. package/dist/internal_context.js.map +0 -1
  165. package/dist/mcp/client.d.ts +0 -42
  166. package/dist/mcp/do-oauth-client-provider.js.map +0 -1
  167. package/dist/mcp/x402.js.map +0 -1
  168. package/dist/wire-types-CU9rLoeS.js.map +0 -1
  169. package/dist/workflow-types-Baz_PO5v.d.ts +0 -280
package/docs/email.md CHANGED
@@ -419,21 +419,10 @@ When your agent sends emails and expects replies, use secure reply routing to pr
419
419
 
420
420
  ### Setup
421
421
 
422
- 1. Add a secret to your `wrangler.jsonc`:
423
-
424
- ```jsonc
425
- // wrangler.jsonc
426
- {
427
- "vars": {
428
- "EMAIL_SECRET": "change-me-in-production"
429
- }
430
- }
431
- ```
432
-
433
- For production, use Wrangler secrets instead:
422
+ 1. Store the signing key as a Wrangler secret. Do not put it in `vars` or commit it to source control:
434
423
 
435
424
  ```bash
436
- wrangler secret put EMAIL_SECRET
425
+ pnpm exec wrangler secret put EMAIL_SECRET
437
426
  ```
438
427
 
439
428
  2. Use the combined resolver pattern:
@@ -408,17 +408,7 @@ export class MyAgent extends Agent {
408
408
 
409
409
  ## Hibernation
410
410
 
411
- Agents support hibernation - they can sleep when inactive and wake when needed. This saves resources while maintaining WebSocket connections.
412
-
413
- ### Enabling Hibernation
414
-
415
- Hibernation is enabled by default. To disable:
416
-
417
- ```typescript
418
- export class AlwaysOnAgent extends Agent {
419
- static options = { hibernate: false };
420
- }
421
- ```
411
+ Agent WebSockets always use Cloudflare's Hibernation API. An idle Agent can leave memory while its WebSocket clients remain connected; there is no non-hibernating mode.
422
412
 
423
413
  ### How Hibernation Works
424
414
 
@@ -196,7 +196,7 @@ Define tools with `needsApproval` to require human confirmation:
196
196
  ```typescript
197
197
  import { AIChatAgent } from "@cloudflare/ai-chat";
198
198
  import { createWorkersAI } from "workers-ai-provider";
199
- import { streamText, tool, convertToModelMessages } from "ai";
199
+ import { streamText, tool, convertToModelMessages, stepCountIs } from "ai";
200
200
  import { z } from "zod";
201
201
 
202
202
  export class MyAgent extends AIChatAgent {
@@ -238,7 +238,7 @@ export class MyAgent extends AIChatAgent {
238
238
  execute: async ({ city }) => fetchWeather(city)
239
239
  })
240
240
  },
241
- maxSteps: 5
241
+ stopWhen: stepCountIs(5)
242
242
  });
243
243
 
244
244
  return result.toUIMessageStreamResponse();
@@ -246,6 +246,8 @@ export class MyAgent extends AIChatAgent {
246
246
  }
247
247
  ```
248
248
 
249
+ The `inputSchema` accepts the AI SDK's flexible schema format, so you are not limited to Zod. You can also use Valibot, a Standard JSON Schema-compatible schema, or a raw JSON Schema wrapped with `jsonSchema()` from `ai`. See [Use Valibot or another schema library](./agent-tools.md#use-valibot-or-another-schema-library) for details.
250
+
249
251
  ### Client
250
252
 
251
253
  Handle approval requests with `addToolApprovalResponse`:
@@ -355,7 +357,7 @@ See the complete example: [guides/human-in-the-loop/](https://github.com/cloudfl
355
357
 
356
358
  ### Surviving restarts while waiting for a human
357
359
 
358
- 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.
360
+ 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. Durable [`chatRecovery`](./chat-agents.md#stream-recovery) is always enabled. 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.
359
361
 
360
362
  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.
361
363
 
@@ -380,7 +382,7 @@ export class MyAgent extends AIChatAgent {
380
382
  inputSchema: z.object({})
381
383
  })
382
384
  },
383
- maxSteps: 3
385
+ stopWhen: stepCountIs(3)
384
386
  });
385
387
 
386
388
  return result.toUIMessageStreamResponse();
@@ -410,17 +412,18 @@ const { messages, sendMessage } = useAgentChat({
410
412
  });
411
413
  ```
412
414
 
413
- 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.
415
+ The server receives the tool output via `CF_AGENT_TOOL_RESULT` and can auto-continue the conversation when `stopWhen` allows another step, letting the LLM respond to the location data in the same turn.
414
416
 
415
417
  ### OpenAI Agents SDK Pattern
416
418
 
417
419
  When using the [OpenAI Agents SDK](https://openai.github.io/openai-agents-js/), use the `needsApproval` function for conditional approval:
418
420
 
419
421
  ```typescript
420
- import { Agent } from "agents";
421
- import { tool, run } from "@openai/agents";
422
+ import { Agent as CloudflareAgent } from "agents";
423
+ import { Agent as OpenAIAgent, tool, run } from "@openai/agents";
424
+ import { z } from "zod";
422
425
 
423
- export class WeatherAgent extends Agent<Env, AgentState> {
426
+ export class WeatherAgent extends CloudflareAgent<Env> {
424
427
  async processQuery(query: string) {
425
428
  const weatherTool = tool({
426
429
  name: "get_weather",
@@ -438,13 +441,13 @@ export class WeatherAgent extends Agent<Env, AgentState> {
438
441
  }
439
442
  });
440
443
 
441
- const result = await run(this.openai, {
442
- model: "gpt-4o",
443
- tools: [weatherTool],
444
- input: query
444
+ const openaiAgent = new OpenAIAgent({
445
+ name: "Weather assistant",
446
+ instructions: "Help the user check the weather.",
447
+ tools: [weatherTool]
445
448
  });
446
449
 
447
- return result;
450
+ return run(openaiAgent, query);
448
451
  }
449
452
  }
450
453
  ```
package/docs/index.md CHANGED
@@ -140,6 +140,7 @@ The differentiator is not "we have durable state" — it is what happens when a
140
140
 
141
141
  ## Reference
142
142
 
143
+ - [Durable Object Lifecycle](./lifecycle.md) - Compose reusable durable components outside the Agent base class
143
144
  - TODO: [API Reference](./api-reference.md) - Complete API documentation
144
145
  - TODO: [FAQ / How is this different from Durable Objects?](./faq.md)
145
146
  - TODO: [Resources & Further Reading](./resources.md)
@@ -0,0 +1,323 @@
1
+ # Durable Object lifecycle
2
+
3
+ > **Experimental.** Everything exported from `agents/lifecycle` — and the
4
+ > capabilities built on it, including `Scheduler` — may change between
5
+ > releases while the composition surface stabilizes.
6
+
7
+ `agents/lifecycle` lets reusable durable capabilities work in both `Agent` and a
8
+ plain Cloudflare Durable Object. It uses composition: your class extends the
9
+ platform `DurableObject`, then constructs a lifecycle with `this`.
10
+
11
+ ## Plain Durable Object
12
+
13
+ ```ts
14
+ import { DurableObject } from "cloudflare:workers";
15
+ import { Lifecycle } from "agents/lifecycle";
16
+
17
+ export class MyObject extends DurableObject<Env> {
18
+ readonly lifecycle = Lifecycle.install(this);
19
+
20
+ onStart(): void {
21
+ // Runs once per in-memory object lifetime, before work is handled.
22
+ }
23
+
24
+ onRequest(request: Request): Response {
25
+ return new Response(`Hello from ${this.lifecycle.name}: ${request.url}`);
26
+ }
27
+
28
+ onAlarm(): void {
29
+ // Runs after lifecycle capabilities process the alarm.
30
+ }
31
+ }
32
+ ```
33
+
34
+ The side-effect-named static factory constructs the lifecycle and installs the
35
+ runtime-facing `fetch`, `alarm`, `webSocketMessage`, `webSocketClose`, and
36
+ `webSocketError` handlers. Do not define forwarding versions of those methods.
37
+ Implement the semantic callbacks instead.
38
+
39
+ The expanded equivalent is available when useful:
40
+
41
+ ```ts
42
+ readonly lifecycle = new Lifecycle(this);
43
+
44
+ constructor(ctx: DurableObjectState, env: Env) {
45
+ super(ctx, env);
46
+ this.lifecycle.installHandlers();
47
+ }
48
+ ```
49
+
50
+ Route named objects from the outer Worker when you want URL routing:
51
+
52
+ ```ts
53
+ import { routeAgentRequest } from "agents";
54
+
55
+ export default {
56
+ async fetch(request: Request, env: Env): Promise<Response> {
57
+ return (
58
+ (await routeAgentRequest(request, env)) ??
59
+ new Response("Not found", { status: 404 })
60
+ );
61
+ }
62
+ };
63
+ ```
64
+
65
+ The default URL shape is `/agents/:binding/:name`. Direct
66
+ `env.MY_OBJECT.getByName(name).fetch(request)` calls work as well.
67
+
68
+ `Agent` already constructs this lifecycle. Existing Agent classes continue to
69
+ override `onStart`, `onRequest`, `onConnect`, `onMessage`, `onClose`, and
70
+ `onError` normally.
71
+
72
+ ## Request call path
73
+
74
+ ```text
75
+ routeAgentRequest(request)
76
+ └─ named Durable Object stub.fetch(request)
77
+ └─ lifecycle-installed fetch
78
+ ├─ lifecycle startup capabilities
79
+ ├─ host onStart
80
+ ├─ lifecycle request capabilities
81
+ │ └─ first Response wins
82
+ └─ host onRequest
83
+ ```
84
+
85
+ A warm object skips startup but still offers every request to its capabilities.
86
+
87
+ ## Reusable capabilities
88
+
89
+ A capability implements only the phases it needs:
90
+
91
+ ```ts
92
+ import type { DurableObjectCapability } from "agents/lifecycle";
93
+
94
+ class AuditLog implements DurableObjectCapability {
95
+ constructor(private readonly storage: DurableObjectStorage) {}
96
+
97
+ onStart(): void {
98
+ this.storage.sql.exec(`
99
+ CREATE TABLE IF NOT EXISTS audit_log (
100
+ message TEXT NOT NULL
101
+ )
102
+ `);
103
+ }
104
+
105
+ onRequest({ request }: { request: Request }): Response | undefined {
106
+ if (new URL(request.url).pathname.endsWith("/health")) {
107
+ return new Response("ok");
108
+ }
109
+ }
110
+
111
+ onAlarm(): void {
112
+ this.storage.sql.exec("DELETE FROM audit_log");
113
+ }
114
+ }
115
+ ```
116
+
117
+ Install it before startup:
118
+
119
+ ```ts
120
+ export class MyObject extends DurableObject<Env> {
121
+ private readonly audit = new AuditLog(this.ctx.storage);
122
+ readonly lifecycle = Lifecycle.install(this).use(this.audit);
123
+
124
+ onRequest(): Response {
125
+ return new Response("application response");
126
+ }
127
+ }
128
+ ```
129
+
130
+ Capabilities run in registration order. Startup and alarms run every hook
131
+ sequentially. Request handling stops at the first returned `Response`. A phase
132
+ failure propagates, and failed startup can be retried.
133
+
134
+ Capabilities extending `LifecycleCapability` receive one standard service
135
+ surface: storage, readiness, startup state, alarm coordination, a host
136
+ invocation boundary, best-effort events, and capability routing.
137
+ Host-specific bindings, authentication, and protocol adapters remain explicit
138
+ constructor dependencies. Lifecycle never grants a capability the complete
139
+ host implicitly.
140
+
141
+ Capability hooks run outside host context, but user callbacks run through
142
+ `this.lifecycle.runInHostContext(fn)` inside the host invocation context.
143
+ Scheduler dispatches its registered callbacks through this boundary, and a
144
+ future capability that calls user code should do the same.
145
+
146
+ ## Shared alarm ownership
147
+
148
+ Lifecycle owns the Durable Object's single physical alarm. A capability that
149
+ needs a future wake-up keeps its work in its own durable storage and implements
150
+ `getNextAlarm()`:
151
+
152
+ ```ts
153
+ import { LifecycleCapability, type AlarmContribution } from "agents/lifecycle";
154
+
155
+ class Cleanup extends LifecycleCapability {
156
+ constructor() {
157
+ super("cleanup");
158
+ }
159
+
160
+ async getNextAlarm(): Promise<AlarmContribution> {
161
+ return (await this.lifecycle.storage.get<number>("cleanup:next")) ?? null;
162
+ }
163
+
164
+ async onAlarm(): Promise<void> {
165
+ const next = await this.lifecycle.storage.get<number>("cleanup:next");
166
+ if (next === undefined || next > Date.now()) return;
167
+ await this.lifecycle.storage.delete("cleanup:next");
168
+ }
169
+
170
+ async scheduleCleanup(time: number): Promise<void> {
171
+ await this.lifecycle.storage.put("cleanup:next", time);
172
+ await this.lifecycle.alarms.rearm();
173
+ }
174
+ }
175
+ ```
176
+
177
+ Lifecycle selects the earliest contribution from every capability and the
178
+ host. It runs all capability `onAlarm()` hooks, then host `onAlarm()`, then
179
+ recalculates the physical alarm. Capabilities do not depend on Scheduler or on
180
+ each other merely to receive alarm wakes.
181
+ A contribution can be `{ time, exclusive: true }` when its wake time must
182
+ replace ordinary wake candidates, such as a pending teardown. This changes only
183
+ which physical alarm is armed; when that alarm fires, normal capability and host
184
+ hook order still applies. Hosts can implement `getNextAlarm()` for alarm work
185
+ that has not yet been extracted into a capability.
186
+
187
+ ## Capability events
188
+
189
+ Capabilities publish best-effort telemetry through their standard service
190
+ surface. Lifecycle assigns the capability source from the stable ID passed to
191
+ `super()`:
192
+
193
+ ```ts
194
+ class Cleanup extends LifecycleCapability {
195
+ constructor() {
196
+ super("cleanup");
197
+ }
198
+
199
+ reportRemoval(key: string): void {
200
+ this.lifecycle.events.emit("cleanup:remove", { key });
201
+ }
202
+ }
203
+ ```
204
+
205
+ Lifecycle publishes events from a plain Lifecycle Object to the existing
206
+ `agents:*` diagnostics channels according to the event type. Delivery is
207
+ best-effort, runs outside ambient host context, and does not fail the emitting
208
+ capability when a telemetry sink throws. Persist an outbox in the capability
209
+ when delivery is part of the durable business operation.
210
+
211
+ ## Capability routing
212
+
213
+ Every `LifecycleCapability` also receives `lifecycle.routes`. `toRoot()` routes
214
+ a message to the matching capability ID on the root Lifecycle; `to(address, …)`
215
+ routes to another addressed Lifecycle. Lifecycle owns the generic envelope and
216
+ dispatch. A host with child objects supplies the transport internally.
217
+
218
+ Agent uses this for facet schedules: Scheduler sends owner-scoped CRUD to the
219
+ root Scheduler and routes due callbacks back to the matching facet Scheduler.
220
+ Existing rows remain in the root `cf_agents_schedules` table. Scheduler does not
221
+ implement facet traversal, and Agent exposes only one internal generic Lifecycle
222
+ route aperture.
223
+
224
+ ## Explicit disposal
225
+
226
+ `lifecycle.dispose()` calls each capability's optional `dispose()` method in
227
+ reverse installation order. This phase releases live resources such as MCP
228
+ transports and listeners. It does not delete capability tables. An explicit
229
+ Lifecycle Object destruction disposes live resources once, then calls
230
+ `storage.deleteAll()` once for all shared durable state. Eviction calls neither.
231
+
232
+ ## Lifecycle Object context
233
+
234
+ `agents/lifecycle` exports the `LifecycleObject` interface for a
235
+ `DurableObject` with an installed `Lifecycle` and the semantic hooks Lifecycle
236
+ dispatches. This is a host type, not the batteries-included `Agent` class
237
+ exported from `agents`.
238
+
239
+ Lifecycle establishes the `getCurrentAgent()` context only while it invokes
240
+ host hooks. Capability hooks run outside that ambient context and use their own
241
+ `this`, hook arguments, and explicitly supplied dependencies.
242
+
243
+ ```ts
244
+ import { getCurrentAgent } from "agents/lifecycle";
245
+
246
+ function currentRequestOrigin(): string | undefined {
247
+ const { request } = getCurrentAgent();
248
+ return request ? new URL(request.url).origin : undefined;
249
+ }
250
+
251
+ export class MyObject extends DurableObject<Env> {
252
+ readonly lifecycle = Lifecycle.install(this);
253
+
254
+ onRequest(): Response {
255
+ return Response.json({ origin: currentRequestOrigin() });
256
+ }
257
+ }
258
+ ```
259
+
260
+ Pass the concrete host class when shared host code needs its additional APIs:
261
+
262
+ ```ts
263
+ const { agent: object } = getCurrentAgent<MyObject>();
264
+ ```
265
+
266
+ Host context values follow the invocation:
267
+
268
+ - `onStart` and `onAlarm`: object;
269
+ - `onRequest`: object and request;
270
+ - `onConnect`: object, connection, and upgrade request;
271
+ - `onMessage`, `onClose`, and `onError`: object and connection.
272
+
273
+ `getConnectionTags(connection, { request })` remains argument-driven because it
274
+ already receives both values explicitly. The root `agents` package continues
275
+ to export `getCurrentAgent()` for the `Agent` class as a compatibility alias.
276
+
277
+ ## WebSockets always hibernate
278
+
279
+ The lifecycle always uses Cloudflare's WebSocket Hibernation API. Idle clients
280
+ remain connected while the Durable Object can leave memory. When a message
281
+ wakes the object, its constructor and lifecycle startup run again before
282
+ `onMessage`.
283
+
284
+ State needed after a wake must be stored durably or through connection state:
285
+
286
+ ```ts
287
+ onConnect(connection: Connection): void {
288
+ connection.setState({ authenticated: true });
289
+ }
290
+
291
+ onMessage(connection: Connection<{ authenticated: boolean }>): void {
292
+ console.log(connection.state?.authenticated);
293
+ }
294
+ ```
295
+
296
+ There is no non-hibernating mode.
297
+
298
+ ## Native RPC
299
+
300
+ Native Durable Object RPC does not pass through `fetch`. An RPC method that
301
+ requires initialized capabilities starts the lifecycle explicitly:
302
+
303
+ ```ts
304
+ async runTask(): Promise<void> {
305
+ await this.lifecycle.start();
306
+ // initialized work
307
+ }
308
+ ```
309
+
310
+ Agent's internal RPC entry points already enforce this boundary.
311
+
312
+ ## Object names
313
+
314
+ Use `idFromName()` or `getByName()`. The lifecycle reads the authoritative name
315
+ from `ctx.id.name` and exposes it as `lifecycle.name`.
316
+
317
+ For migration only, the lifecycle can read an existing `__ps_name` record
318
+ written by an older PartyServer release. It never writes that key. Deprecated
319
+ name headers and bootstrap methods are not supported.
320
+
321
+ If a name cannot be resolved, the error covers named addressing, updating local
322
+ Wrangler/workerd and the compatibility date, unsupported raw IDs and oversized
323
+ names, and rescheduling alarms created before 2026-03-15.
@@ -86,7 +86,7 @@ type Task = {
86
86
  externalJobId?: string;
87
87
  };
88
88
 
89
- export class ProjectManager extends Agent<ProjectState> {
89
+ export class ProjectManager extends Agent<Env, ProjectState> {
90
90
  initialState: ProjectState = {
91
91
  name: "",
92
92
  status: "planning",
@@ -115,7 +115,7 @@ The pattern extends naturally to any event source that can reach a Worker — an
115
115
  The agent does not need to be "started" or "deployed" separately for each wake source — they all route to the same Durable Object instance. The agent's identity (its name) is the routing key.
116
116
 
117
117
  ```typescript
118
- export class ProjectManager extends Agent<ProjectState> {
118
+ export class ProjectManager extends Agent<Env, ProjectState> {
119
119
  async onStart() {
120
120
  // Daily deadline check at 9am UTC — idempotent, safe across restarts
121
121
  await this.schedule(
@@ -163,7 +163,7 @@ Sometimes an agent needs to do work that takes longer than the idle eviction win
163
163
  `keepAlive()` prevents this by creating a heartbeat that resets the inactivity timer:
164
164
 
165
165
  ```typescript
166
- export class ProjectManager extends Agent<ProjectState> {
166
+ export class ProjectManager extends Agent<Env, ProjectState> {
167
167
  async generateProjectPlan(goal: string) {
168
168
  const result = await this.keepAliveWhile(async () => {
169
169
  const plan = await this.callLLM(`Create a project plan for: ${goal}`);
@@ -223,7 +223,7 @@ side effects. If recovery succeeds, return a recovery result from
223
223
  aborted, failed, or intentionally left interrupted.
224
224
 
225
225
  ```typescript
226
- export class ProjectManager extends Agent<ProjectState> {
226
+ export class ProjectManager extends Agent<Env, ProjectState> {
227
227
  async executeTask(task: Task) {
228
228
  await this.runFiber(`task:${task.id}`, async (ctx) => {
229
229
  const resources = await this.gatherResources(task);
@@ -268,7 +268,7 @@ The project manager frequently kicks off work that takes far longer than any sin
268
268
  The project manager starts a CI pipeline for a task. The pipeline takes 20 minutes. Rather than holding a connection open, the agent registers its own URL as the callback and goes to sleep:
269
269
 
270
270
  ```typescript
271
- export class ProjectManager extends Agent<ProjectState> {
271
+ export class ProjectManager extends Agent<Env, ProjectState> {
272
272
  async startCIPipeline(task: Task) {
273
273
  const response = await fetch("https://ci.example.com/api/pipelines", {
274
274
  method: "POST",
@@ -307,7 +307,7 @@ export class ProjectManager extends Agent<ProjectState> {
307
307
  Not every external service supports callbacks. When the project manager submits a video asset for generation, it needs to check back periodically until the job completes:
308
308
 
309
309
  ```typescript
310
- export class ProjectManager extends Agent<ProjectState> {
310
+ export class ProjectManager extends Agent<Env, ProjectState> {
311
311
  async startVideoGeneration(task: Task) {
312
312
  const response = await fetch("https://video-api.example.com/generate", {
313
313
  method: "POST",
@@ -354,7 +354,7 @@ export class ProjectManager extends Agent<ProjectState> {
354
354
  A production deployment involves multiple steps that must each retry independently — build, test, stage, promote. The project manager should not manage these steps internally; it delegates to a [Workflow](./workflows.md) that handles retries and step sequencing:
355
355
 
356
356
  ```typescript
357
- export class ProjectManager extends Agent<ProjectState> {
357
+ export class ProjectManager extends Agent<Env, ProjectState> {
358
358
  async startDeployment(task: Task) {
359
359
  const instanceId = await this.runWorkflow("DEPLOY_WORKFLOW", {
360
360
  taskId: task.id,
@@ -422,7 +422,7 @@ type PlanStep = {
422
422
  result?: unknown;
423
423
  };
424
424
 
425
- export class ProjectManager extends Agent<ProjectState> {
425
+ export class ProjectManager extends Agent<Env, ProjectState> {
426
426
  async createPlan(goal: string) {
427
427
  const steps = await this.keepAliveWhile(async () => {
428
428
  return this.callLLM(`
@@ -508,7 +508,7 @@ This pattern has several advantages for long-running agents:
508
508
  A project manager does not do everything itself. It delegates specialized work to sub-agents — child Durable Objects (facets) spawned under the parent. Each facet has its own isolated SQLite state and runs in parallel, but stays colocated on the same machine as the parent.
509
509
 
510
510
  ```typescript
511
- export class ProjectManager extends Agent<ProjectState> {
511
+ export class ProjectManager extends Agent<Env, ProjectState> {
512
512
  async delegateTask(task: Task) {
513
513
  // Get a stub to a specialized agent (same DO namespace, unique name)
514
514
  const researcher = await this.subAgent(
@@ -537,7 +537,7 @@ For chat-oriented sub-agents, [Think](https://github.com/cloudflare/agents/blob/
537
537
 
538
538
  The patterns above handle the project manager's coordination work — scheduling, delegating, polling. But the project manager also uses an LLM directly: generating plans, summarizing progress, drafting status emails. Those LLM calls stream tokens over a connection that cannot be resumed if the agent is evicted mid-response.
539
539
 
540
- For chat-oriented agents built on `AIChatAgent`, this is an even sharper problem — the user is watching the response stream in real time and sees it stop mid-sentence. `chatRecovery` wraps each chat turn in a `runFiber`, providing automatic `keepAlive` during streaming and a recovery hook when the agent restarts:
540
+ For chat-oriented agents built on `AIChatAgent` or `Think`, this is an even sharper problem — the user is watching the response stream in real time and sees it stop mid-sentence. Durable recovery wraps every chat turn in a `runFiber`, providing automatic `keepAlive` during streaming and a recovery hook when the agent restarts:
541
541
 
542
542
  ```typescript
543
543
  import { AIChatAgent } from "@cloudflare/ai-chat";
@@ -547,8 +547,6 @@ import type {
547
547
  } from "@cloudflare/ai-chat";
548
548
 
549
549
  class ProjectChat extends AIChatAgent<Env> {
550
- override chatRecovery = true;
551
-
552
550
  override async onChatRecovery(
553
551
  ctx: ChatRecoveryContext
554
552
  ): Promise<ChatRecoveryOptions> {
@@ -573,7 +571,7 @@ The right recovery strategy depends on the LLM provider:
573
571
 
574
572
  For a complete multi-provider implementation with full code for each strategy, see the [`forever-chat` example](https://github.com/cloudflare/agents/tree/main/experimental/forever-chat) and the [`forever.md` design doc](https://github.com/cloudflare/agents/tree/main/experimental/forever.md).
575
573
 
576
- [Think](https://github.com/cloudflare/agents/blob/main/docs/think/index.md) enables `chatRecovery` by default. The default path persists partial output and auto-continues or retries the turn when safe, so many apps do not need a custom hook. Override `onChatRecovery` when a provider has a better recovery strategy, or configure `chatRecovery = { maxAttempts, terminalMessage, onExhausted }` to tune the terminal user experience.
574
+ `AIChatAgent` and [Think](https://github.com/cloudflare/agents/blob/main/docs/think/index.md) always enable durable recovery. The default path persists partial output and auto-continues or retries the turn when safe, so many apps do not need a custom hook. Override `onChatRecovery` when a provider has a better recovery strategy, or configure `chatRecovery = { maxAttempts, terminalMessage, onExhausted }` to tune the terminal user experience.
577
575
 
578
576
  If the agent is interrupted before any assistant stream chunks are written, there is no partial assistant message to continue. When the latest persisted message is still the unanswered user message from that turn, chat recovery retries the turn automatically unless `onChatRecovery` returns `{ continue: false }`.
579
577
 
@@ -586,7 +584,7 @@ An agent that runs for months accumulates data: conversation history, timeline e
586
584
  Schedule periodic cleanup to prune old data and archive completed work:
587
585
 
588
586
  ```typescript
589
- export class ProjectManager extends Agent<ProjectState> {
587
+ export class ProjectManager extends Agent<Env, ProjectState> {
590
588
  async onStart() {
591
589
  await this.schedule("0 0 * * *", "housekeeping", {}, { idempotent: true });
592
590
  }
@@ -634,7 +632,7 @@ For simpler cases: keep only the last N messages in the active context (sliding
634
632
  A long-running agent eventually completes its purpose. The project ships, the investigation concludes, the monitoring window closes. Clean up explicitly:
635
633
 
636
634
  ```typescript
637
- export class ProjectManager extends Agent<ProjectState> {
635
+ export class ProjectManager extends Agent<Env, ProjectState> {
638
636
  async completeProject() {
639
637
  // Cancel remaining schedules
640
638
  const schedules = await this.listSchedules();
@@ -13,6 +13,61 @@ The MCP client capability lets your agent:
13
13
 
14
14
  > **Note:** This page covers connecting to MCP servers as a client. To create your own MCP server, see [Creating MCP Servers](./mcp-servers.md).
15
15
 
16
+ ## Compose the MCP client with a Durable Object
17
+
18
+ `MCPClientManager` is a lifecycle capability. A class can extend the platform
19
+ `DurableObject` directly and install the manager without extending `Agent`:
20
+
21
+ ```typescript
22
+ import { DurableObject } from "cloudflare:workers";
23
+ import { Lifecycle } from "agents/lifecycle";
24
+ import { MCPClientManager } from "agents/mcp/client";
25
+
26
+ export class MyObject extends DurableObject<Env> {
27
+ readonly mcp = new MCPClientManager("my-object", "1.0.0");
28
+
29
+ readonly lifecycle = Lifecycle.install(this).use(this.mcp);
30
+
31
+ onRequest() {
32
+ return Response.json({ tools: this.mcp.listTools() });
33
+ }
34
+ }
35
+ ```
36
+
37
+ The lifecycle calls the manager automatically:
38
+
39
+ - `onStart()` initializes its schema and restores persisted connections before
40
+ the host handles work.
41
+ - `onRequest()` intercepts registered OAuth callback URLs before the host's
42
+ request handler.
43
+
44
+ Do not call these hooks manually. For a native Durable Object RPC method,
45
+ which bypasses `fetch`, call `await this.lifecycle.start()` before using the
46
+ manager. Explicit cleanup remains available through `mcp.dispose()`; Durable
47
+ Objects do not provide an eviction callback.
48
+
49
+ The manager does not require a particular OAuth callback route. Pass the exact
50
+ callback URL to `registerServer()` and route that request to the same named
51
+ Durable Object. The manager persists the URL and only intercepts a callback
52
+ whose origin and pathname match it.
53
+
54
+ An HTTP-only manager does not need `env`. Pass the Durable Object environment
55
+ when the catalog can contain RPC servers so persisted binding names can be
56
+ resolved after a wake:
57
+
58
+ ```ts
59
+ readonly mcp = new MCPClientManager("my-object", "1.0.0", {
60
+ env: this.env
61
+ });
62
+ ```
63
+
64
+ If an RPC row exists without `env`, startup logs a warning identifying the
65
+ server and does not recreate that connection.
66
+
67
+ `Agent` installs this same capability object directly. Existing `this.mcp`,
68
+ `addMcpServer()`, `removeMcpServer()`, and `getMcpServers()` APIs remain
69
+ available.
70
+
16
71
  ## Quick Start
17
72
 
18
73
  Install the exact MCP client peer used by this Agents release:
@@ -14,6 +14,12 @@ This guide covers the different ways to create MCP servers with the Agents SDK a
14
14
  - **`McpAgent`** is a retained, feature-frozen SDK v1 path for existing stateful deployments. New servers should use `createMcpHandler()`.
15
15
  - **Raw transport** gives you low-level control if the standard handler lifecycle is not suitable.
16
16
 
17
+ `McpAgent` and the other retained SDK v1 registration APIs use the upstream
18
+ Zod-based schema contract. Define their tool input and output schemas as Zod
19
+ shapes; AI SDK flexible-schema adapters are not accepted. For a new MCP server
20
+ that needs Standard Schema support, use `@modelcontextprotocol/server` v2 with
21
+ `createMcpHandler()`.
22
+
17
23
  ## Stateless MCP Server with `createMcpHandler()`
18
24
 
19
25
  The simplest way to create an MCP server. Install the exact SDK v2 server peer, then use the isolated server entry point so legacy Agents transports and MCP clients stay out of your Worker bundle: