agents 0.21.0 → 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 (137) hide show
  1. package/README.md +1 -0
  2. package/dist/{agent-tool-types-CzGGB-20.d.ts → agent-routing-CnkaHb-v.d.ts} +2921 -3057
  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-zR2d5uij.d.ts → agent-tools-C0XJqWzB.d.ts} +7 -7
  8. package/dist/agent-tools.d.ts +21 -21
  9. package/dist/agent-tools.js +2 -1
  10. package/dist/agent-tools.js.map +1 -1
  11. package/dist/browser/ai.js +2 -1
  12. package/dist/browser/ai.js.map +1 -1
  13. package/dist/capability-BjSKYpzg.js +42 -0
  14. package/dist/capability-BjSKYpzg.js.map +1 -0
  15. package/dist/capability-runner-CvHGZqUu.d.ts +150 -0
  16. package/dist/chat/index.d.ts +22 -15
  17. package/dist/chat/index.js +4 -4
  18. package/dist/chat/index.js.map +1 -1
  19. package/dist/chat/react.d.ts +14 -1
  20. package/dist/chat/react.js +82 -52
  21. package/dist/chat/react.js.map +1 -1
  22. package/dist/chat/transport.js +1 -1
  23. package/dist/chat-sdk/index.d.ts +4 -4
  24. package/dist/chat-sdk/index.js +1 -1
  25. package/dist/{client-zqKcsyFa.js → client-jagG8a9_.js} +129 -37
  26. package/dist/client-jagG8a9_.js.map +1 -0
  27. package/dist/client.d.ts +1 -1
  28. package/dist/client.js +1 -1
  29. package/dist/{cloudflare-BduZwmYK.js → cloudflare-Dzvc7V2N.js} +10 -3
  30. package/dist/{cloudflare-BduZwmYK.js.map → cloudflare-Dzvc7V2N.js.map} +1 -1
  31. package/dist/current-agent-CuMErtly.d.ts +283 -0
  32. package/dist/current-agent-DhoDkSnH.js +51 -0
  33. package/dist/current-agent-DhoDkSnH.js.map +1 -0
  34. package/dist/diagnostics-CaBjfz4J.js +41 -0
  35. package/dist/diagnostics-CaBjfz4J.js.map +1 -0
  36. package/dist/{do-oauth-client-provider-VTZj2VtM.d.ts → do-oauth-client-provider-Tmf1vgKz.d.ts} +2 -2
  37. package/dist/durable-object-lifecycle-D6nNQJJd.js +862 -0
  38. package/dist/durable-object-lifecycle-D6nNQJJd.js.map +1 -0
  39. package/dist/{email-CL27preh.d.ts → email-7TatiTnl.d.ts} +38 -9
  40. package/dist/email-send.d.ts +15 -0
  41. package/dist/email-send.js +32 -0
  42. package/dist/email-send.js.map +1 -0
  43. package/dist/email.d.ts +14 -10
  44. package/dist/email.js.map +1 -1
  45. package/dist/experimental/memory/session/index.d.ts +3 -2
  46. package/dist/experimental/memory/session/index.js +15 -10
  47. package/dist/experimental/memory/session/index.js.map +1 -1
  48. package/dist/{handler-stateless-C_bo-Ytq.d.ts → handler-stateless-DxYpJ_XF.d.ts} +3 -3
  49. package/dist/{handler-stateless-CIkKPETH.js → handler-stateless-VvrWSAVA.js} +5 -5
  50. package/dist/handler-stateless-VvrWSAVA.js.map +1 -0
  51. package/dist/{index-BRnybD6X.d.ts → index-DcSAZKsB.d.ts} +19 -31
  52. package/dist/index.d.ts +98 -95
  53. package/dist/index.js +10 -7234
  54. package/dist/internal_context-BlxFEWfn.d.ts +19 -0
  55. package/dist/internal_context.d.ts +10 -4
  56. package/dist/internal_context.js +1 -10
  57. package/dist/{client-invoker-BNSZxAkv.d.ts → invoker-CG0_p_Wq.d.ts} +2 -2
  58. package/dist/{client-invoker-VNZ7X0nn.js → invoker-CHMnoxIA.js} +2 -2
  59. package/dist/invoker-CHMnoxIA.js.map +1 -0
  60. package/dist/lifecycle/index.d.ts +48 -0
  61. package/dist/lifecycle/index.js +4 -0
  62. package/dist/mcp/{do-oauth-client-provider.d.ts → client/do-oauth-client-provider.d.ts} +1 -1
  63. package/dist/mcp/{do-oauth-client-provider.js → client/do-oauth-client-provider.js} +1 -1
  64. package/dist/mcp/client/do-oauth-client-provider.js.map +1 -0
  65. package/dist/mcp/client/index.d.ts +42 -0
  66. package/dist/mcp/{client.js → client/index.js} +1 -1
  67. package/dist/mcp/{x402.d.ts → client/x402.d.ts} +2 -2
  68. package/dist/mcp/{x402.js → client/x402.js} +2 -2
  69. package/dist/mcp/client/x402.js.map +1 -0
  70. package/dist/mcp/index.d.ts +36 -36
  71. package/dist/mcp/index.js +14 -16
  72. package/dist/mcp/index.js.map +1 -1
  73. package/dist/mcp/{server.d.ts → server/index.d.ts} +1 -1
  74. package/dist/mcp/{server.js → server/index.js} +1 -1
  75. package/dist/observability/ai/index.js +50 -35
  76. package/dist/observability/ai/index.js.map +1 -1
  77. package/dist/observability/index.d.ts +4 -4
  78. package/dist/observability/index.js +3 -50
  79. package/dist/observability/index.js.map +1 -1
  80. package/dist/{protocol-Dqc2MQxo.js → protocol-B0nh6KNf.js} +19 -21
  81. package/dist/protocol-B0nh6KNf.js.map +1 -0
  82. package/dist/react.d.ts +4 -4
  83. package/dist/react.js +1 -1
  84. package/dist/schedule.d.ts +25 -94
  85. package/dist/schedule.js +1 -98
  86. package/dist/schedule.js.map +1 -1
  87. package/dist/scheduler-CR9RHGos.js +857 -0
  88. package/dist/scheduler-CR9RHGos.js.map +1 -0
  89. package/dist/scheduler-DQoTGoAW.d.ts +224 -0
  90. package/dist/schedules/index.d.ts +22 -0
  91. package/dist/schedules/index.js +2 -0
  92. package/dist/schedules/parser.d.ts +79 -0
  93. package/dist/schedules/parser.js +103 -0
  94. package/dist/schedules/parser.js.map +1 -0
  95. package/dist/serializable.d.ts +1 -1
  96. package/dist/sql-error-CPY-GXyI.d.ts +12 -0
  97. package/dist/sql-error.d.ts +2 -0
  98. package/dist/sql-error.js +16 -0
  99. package/dist/sql-error.js.map +1 -0
  100. package/dist/src-5W6JNKVb.js +6613 -0
  101. package/dist/src-5W6JNKVb.js.map +1 -0
  102. package/dist/sub-routing.d.ts +12 -12
  103. package/dist/workflow-types.d.ts +25 -25
  104. package/dist/workflows.d.ts +22 -22
  105. package/dist/workflows.js +2 -1
  106. package/dist/workflows.js.map +1 -1
  107. package/dist/{ws-chat-transport-CIoOBbO7.js → ws-chat-transport-rWwta645.js} +152 -15
  108. package/dist/ws-chat-transport-rWwta645.js.map +1 -0
  109. package/docs/agent-class.md +28 -86
  110. package/docs/chat-agents.md +13 -12
  111. package/docs/durable-execution.md +1 -1
  112. package/docs/http-websockets.md +1 -11
  113. package/docs/human-in-the-loop.md +1 -1
  114. package/docs/index.md +1 -0
  115. package/docs/lifecycle.md +323 -0
  116. package/docs/long-running-agents.md +2 -4
  117. package/docs/mcp-client.md +55 -0
  118. package/docs/observability.md +11 -11
  119. package/docs/resumable-streaming.md +1 -1
  120. package/docs/scheduling.md +175 -15
  121. package/docs/server-driven-messages.md +1 -1
  122. package/docs/sub-agents.md +1 -1
  123. package/package.json +30 -21
  124. package/dist/cli/index.d.ts +0 -1
  125. package/dist/cli/index.js +0 -26
  126. package/dist/cli/index.js.map +0 -1
  127. package/dist/client-invoker-VNZ7X0nn.js.map +0 -1
  128. package/dist/client-zqKcsyFa.js.map +0 -1
  129. package/dist/handler-stateless-CIkKPETH.js.map +0 -1
  130. package/dist/index.js.map +0 -1
  131. package/dist/internal_context-Dg4Cgjcu.d.ts +0 -37
  132. package/dist/internal_context.js.map +0 -1
  133. package/dist/mcp/client.d.ts +0 -42
  134. package/dist/mcp/do-oauth-client-provider.js.map +0 -1
  135. package/dist/mcp/x402.js.map +0 -1
  136. package/dist/protocol-Dqc2MQxo.js.map +0 -1
  137. package/dist/ws-chat-transport-CIoOBbO7.js.map +0 -1
@@ -6,10 +6,15 @@ This document tries to bridge that gap, empowering any developer aiming to get s
6
6
 
7
7
  # What is the Agent?
8
8
 
9
- The `Agent` class is an extension of `DurableObject`. That is to say, they _are_ **Durable Objects**. If you're not familiar with Durable Objects, it is highly recommended that you read ["What are Durable Objects"](https://developers.cloudflare.com/durable-objects/) but at their core, Durable Objects are globally addressable (each instance has a unique ID) single-threaded compute instances with long term storage (KV/SQLite).
10
- That being said, `Agent` does **not** extend `DurableObject` directly but instead `Server`. `Server` is a class provided by [PartyKit](https://github.com/cloudflare/partykit/tree/main/packages/partyserver).
9
+ `Agent` directly extends Cloudflare's `DurableObject`, so every Agent is a globally addressable, single-threaded compute instance with durable KV/SQLite storage. If you are unfamiliar with the platform primitive, start with [What are Durable Objects](https://developers.cloudflare.com/durable-objects/).
11
10
 
12
- You can visualize the logic as a Matryoshka doll: **DurableObject** -> **Server** -> **Agent**.
11
+ Each Agent composes a `Lifecycle` instance. The lifecycle installs request, alarm, and hibernating WebSocket entry points while the Agent supplies semantic callbacks and higher-level features:
12
+
13
+ ```text
14
+ DurableObject
15
+ └── Agent
16
+ └── owns Lifecycle
17
+ ```
13
18
 
14
19
  ## Layer 0: Durable Object
15
20
 
@@ -73,11 +78,11 @@ export class MyDurableObject extends DurableObject {
73
78
  }
74
79
  ```
75
80
 
76
- ### `alarm()`
81
+ ### Alarms
77
82
 
78
- HTTP and RPC requests are not the only entrypoints for a DO. Alarms allow developers to schedule an event to trigger at a later time. Whenever the next alarm is due, the runtime will call the `alarm()` method, which is left to the developer to implement.
83
+ `Lifecycle` owns the Agent's physical Durable Object alarm because schedules, keep-alive, fibers, sub-agents, and other capabilities share the same alarm slot. Do not override `alarm()` or call `this.ctx.storage.setAlarm()` from an Agent feature; one caller could overwrite another feature's wake-up.
79
84
 
80
- To schedule an alarm, you can use the `this.ctx.storage.setAlarm()` method. For more information, check [the documentation](https://developers.cloudflare.com/durable-objects/api/alarms/).
85
+ Use `this.schedule()` for named Agent callbacks. A reusable capability with its own durable work implements `getNextAlarm()` and `onAlarm()`, then calls `this.lifecycle.alarms.rearm()` after its durable state changes. See [Durable Object lifecycle](./lifecycle.md#shared-alarm-ownership) and [Scheduling](./scheduling.md).
81
86
 
82
87
  ### `this.ctx`
83
88
 
@@ -102,104 +107,43 @@ const token = kv.get("someToken");
102
107
 
103
108
  Lastly, it's worth mentioning that the DO also has the Worker `Env` in `this.env`. Read more [here](https://developers.cloudflare.com/workers/runtime-apis/bindings).
104
109
 
105
- ## Layer 1: Partykit `Server`
106
-
107
- Now that you've seen what Durable Objects come with out-of-the-box, what [PartyKit](https://github.com/cloudflare/partykit)'s `Server` (package `partyserver`) implements will be clearer. It's an **opinionated `DurableObject` wrapper that improves DX by hiding away DO primitives in favor of more developer friendly callbacks**.
108
-
109
- An important note is that `Server` **does NOT persist to the DO storage** so you will not see extra storage operations by using it.
110
-
111
- ### Addressing
110
+ ## Layer 1: lifecycle composition
112
111
 
113
- `partyserver` exposes helper to address your DOs instead of manually through your bindings. This allows `partyserver` to implement several improvements, including a unique URL routing scheme for your DOs (e.g. `<your-worker>/servers/:durableClass/:durableName`).
114
-
115
- Compare this to the DO addressing [example above](#rpc).
112
+ `Agent` uses `Lifecycle.install(this)`, an explicit side-effect-named factory that constructs the lifecycle and installs the platform-facing `fetch`, `alarm`, `webSocketMessage`, `webSocketClose`, and `webSocketError` handlers. Agent subclasses implement semantic callbacks instead of a second base class:
116
113
 
117
114
  ```ts
118
- // Note the await here!
119
- const stub = await getServerByName(env.MY_DO, "foo");
120
-
121
- // We can still call RPC methods.
122
- await stub.bar();
123
- ```
124
-
125
- Since we have a URL addressing scheme, we also get access to `routePartykitRequest()`.
126
-
127
- ```ts
128
- async fetch(request: Request, env: Env, ctx: ExecutionContext) {
129
- // Behind the scenes, PartyKit normalizes your DO binding names
130
- // and tries to do some pattern matching.
131
- const res = await routePartykitRequest(request, env);
132
-
133
- if (res) return res;
134
-
135
- return Response("Not found", { status: 404 });
136
- }
137
- ```
138
-
139
- You can have a look at [the implementation](https://github.com/cloudflare/partykit/blob/main/packages/partyserver/src/index.ts#L122) if you're interested.
140
-
141
- ### `onStart`
142
-
143
- The extra plumbing that `Server` includes on addressing allows it to expose an `onStart` callback that is **executed every time the DO starts up** (the DO was evicted, hibernated or never created at all) and **before any `fetch` or RPC**.
144
-
145
- ```ts
146
- class MyServer extends Server {
115
+ class MyAgent extends Agent {
147
116
  onStart() {
148
- // Some initialization logic that you wish
149
- // to run every time the DO is started up.
150
- const sql = this.ctx.storage.sql;
151
- sql.exec(`...`);
117
+ // Runs once per in-memory lifetime before work is handled.
152
118
  }
153
- }
154
- ```
155
-
156
- ### `onRequest` and `onConnect`
157
119
 
158
- `Server` already implements `fetch` for the underlying Durable Object and exposes 2 different callbacks that developers can make use of, `onRequest` and `onConnect` for HTTP requests and incoming WS connections, respectively (**WebSocket connections are accepted by default**).
159
-
160
- ```ts
161
- class MyServer extends Server {
162
- async onRequest(request: Request) {
163
- const url = new URL(request.url);
164
-
165
- return new Response(`Hello from ${url.origin}!`);
120
+ onRequest(request: Request) {
121
+ return new Response(`Hello from ${request.url}`);
166
122
  }
167
123
 
168
- async onConnect(conn, ctx) {
169
- const { request } = ctx;
170
- const url = new URL(request.url);
171
-
172
- // Connections are a WebSocket wrapper
173
- conn.send(`Hello from ${url.origin}!`);
124
+ onConnect(connection: Connection) {
125
+ connection.send("connected");
174
126
  }
175
127
  }
176
128
  ```
177
129
 
178
- ### WebSockets
179
-
180
- Just as `onConnect` is the callback for every new connection, `Server` also provides wrappers on top of the default callbacks from the `DurableObject` class: `onMessage`, `onClose` and `onError`.
181
-
182
- There's also `this.broadcast` that sends a WS message to all connected clients (no magic, just a loop over `this.getConnections()`!).
183
-
184
- ### `this.name`
130
+ Lifecycle WebSockets always use Cloudflare's Hibernation API. Idle clients stay connected while the Durable Object can leave memory; constructor fields and `onStart` run again when a message wakes it. Persist anything needed across wakes in storage or `connection.state`.
185
131
 
186
- Since [2026-03-15](https://developers.cloudflare.com/changelog/post/2026-03-15-durable-object-id-name/), the Workers runtime populates `ctx.id.name` inside a Durable Object addressed via `idFromName()` or `getByName()`, including in alarm handlers. Constructor-time availability isn't spelled out in the docs, but workerd's own tests pin it ([workerd#6421](https://github.com/cloudflare/workerd/pull/6421)), as do `partyserver`'s runtime-contract tests. `partyserver` reads `ctx.id.name` first, so for named access `this.name` resolves natively with no extra machinery.
132
+ Reusable capabilities can be installed through `this.lifecycle.use(capability)`. Capabilities start before Agent startup, can intercept requests before `onRequest`, and process alarms before `onAlarm`. See [Durable Object lifecycle](./lifecycle.md).
187
133
 
188
- `ctx.id.name` is still `undefined` in these cases (see [the DO id docs](https://developers.cloudflare.com/durable-objects/api/id/#name)):
134
+ ### Identity
189
135
 
190
- - the object is addressed via `idFromString()` (even if the id was originally created with `idFromName()`) or `newUniqueId()` deliberate design, not a gap;
191
- - the name is longer than 1,024 bytes;
192
- - the alarm firing was scheduled before 2026-03-15, or was scheduled from a context that itself had no name (reschedule it from a `fetch()` or RPC handler where the name is available).
136
+ Since [2026-03-15](https://developers.cloudflare.com/changelog/post/2026-03-15-durable-object-id-name/), Workers exposes the name used by `idFromName()` or `getByName()` as `ctx.id.name`, including in alarm handlers. Agents and Agent facets use named IDs, and `this.name` projects that native identity.
193
137
 
194
- For those cases `partyserver` falls back to a legacy name record in storage (written automatically during named-access initialization, or by the `setName()` bootstrap for raw-id DOs), and `this.name` throws if no name can be resolved at all.
138
+ For migration, lifecycle can read an existing `__ps_name` value written by an older release. It never writes a duplicate name. Raw IDs, `idFromString()`, and names over 1,024 bytes do not provide native identity. Alarms created before 2026-03-15 must be rescheduled from a named fetch or RPC handler.
195
139
 
196
140
  ## Layer 2: Agent
197
141
 
198
- Now finally, the `Agent` class. `Agent` extends `Server` and provides opinionated primitives for stateful, schedulable, and observable agents that can communicate via RPC, WebSockets, and (even!) email.
142
+ The `Agent` class directly extends `DurableObject`, composes the lifecycle above, and provides opinionated primitives for stateful, schedulable, and observable agents that can communicate via RPC, WebSockets, and (even!) email.
199
143
 
200
144
  ### `this.state` and `this.setState()`
201
145
 
202
- One of the core features of `Agent` is **automatic state persistence**. Developers define the shape of their state via the generic parameter and `initialState` (which is only used if no state exists in storage), and the Agent handles loading, saving, and broadcasting state changes (check `Server`'s `this.broadcast()` above).
146
+ One of the core features of `Agent` is **automatic state persistence**. Developers define the shape of their state via the generic parameter and `initialState` (which is only used if no state exists in storage), and the Agent handles loading, saving, and broadcasting state changes (using its lifecycle-managed WebSocket connections).
203
147
 
204
148
  `this.state` is a getter that lazily loads state from storage (SQL). **State is persisted across DO evictions** when it's updated with `this.setState()`, which automatically serializes the state and writes it back to storage.
205
149
  There's also `this.onStateChanged` that you can override to react to state changes.
@@ -419,7 +363,7 @@ function someUtilityFunction() {
419
363
 
420
364
  ### `this.onError`
421
365
 
422
- `Agent` extends `Server`'s `onError` so it can be used to handle errors that are not necessarily WebSocket errors. It is called with a `Connection` or `unknown` error.
366
+ `Agent.onError` handles both WebSocket errors and other Agent errors. It is called with a `Connection` or `unknown` error.
423
367
 
424
368
  ```ts
425
369
  class MyAgent extends Agent {
@@ -495,14 +439,12 @@ application; return a recovery result to update the retained status record.
495
439
 
496
440
  ### Routing
497
441
 
498
- The `Agent` class re-exports PartyKit's [addressing helpers](#addressing) as `getAgentByName` and `routeAgentRequest`.
442
+ Use `getAgentByName` for named RPC stubs and `routeAgentRequest` for `/agents/:class/:name` HTTP and WebSocket routing.
499
443
 
500
444
  ```ts
501
- // Same API as getServerByName
502
445
  const stub = await getAgentByName(env.MY_DO, "foo");
503
446
  // ...
504
447
 
505
- // Same API as routeServerRequest
506
448
  const res = await routeAgentRequest(request, env);
507
449
 
508
450
  if (res) return res;
@@ -590,17 +590,11 @@ If you do not pass `abortSignal` to `streamText`, the LLM call will continue run
590
590
 
591
591
  ### Stream Recovery
592
592
 
593
- When a Durable Object is evicted mid-stream (code update, inactivity timeout, resource limit), the LLM connection is severed permanently and the in-memory streaming state is lost. `chatRecovery` wraps each chat turn in a [`runFiber()`](./durable-execution.md), providing automatic `keepAlive` during streaming and a recovery hook on restart.
593
+ When a Durable Object is evicted mid-stream (code update, inactivity timeout, resource limit), the LLM connection is severed permanently and the in-memory streaming state is lost. Durable recovery wraps every `AIChatAgent` and `Think` chat turn in a [`runFiber()`](./durable-execution.md), providing automatic `keepAlive` during streaming and a recovery hook on restart.
594
594
 
595
- ```typescript
596
- export class ChatAgent extends AIChatAgent {
597
- override chatRecovery = true;
598
- }
599
- ```
600
-
601
- When enabled, every `onChatMessage` call runs inside a fiber. If the agent is evicted mid-stream, the fiber row survives in SQLite. On the next activation, the framework detects the interrupted fiber, reconstructs the partial response from buffered stream chunks, and calls `onChatRecovery`.
595
+ If the agent is evicted mid-stream, the fiber row survives in SQLite. On the next activation, the framework detects the interrupted fiber, reconstructs the partial response from buffered stream chunks, and calls `onChatRecovery`.
602
596
 
603
- `AIChatAgent` defaults `chatRecovery` to `false` so existing chat agents only get client reconnect/resumable-stream behavior. `Think` defaults it to `true`.
597
+ Durable recovery is always enabled. Use `chatRecovery` only to tune its budgets and terminal behavior.
604
598
 
605
599
  > **Assign `chatRecovery` as a class field or in the constructor — never in `onStart()`.** On every wake the SDK evaluates recovery budgets (and may seal an interrupted turn, firing `onExhausted`) _before_ your `onStart()` body runs. A config produced inside `onStart()` is therefore read as the built-in defaults at the moment recovery decides, so your `maxRecoveryWork` / `shouldKeepRecovering` / `onExhausted` silently never apply to the recovery that matters. The SDK logs a one-time warning if it detects `chatRecovery` being assigned during `onStart()`.
606
600
 
@@ -610,8 +604,6 @@ Override to implement provider-specific recovery. The default behavior persists
610
604
 
611
605
  ```typescript
612
606
  export class ChatAgent extends AIChatAgent {
613
- override chatRecovery = true;
614
-
615
607
  override async onChatRecovery(
616
608
  ctx: ChatRecoveryContext
617
609
  ): Promise<ChatRecoveryOptions> {
@@ -659,7 +651,16 @@ Settled work is never dropped: `persist: false` only suppresses persistence of a
659
651
 
660
652
  When recovery happens before any stream chunks were written, there is no partial assistant message to continue. If the latest persisted message is still the unanswered user message from the interrupted turn, the framework retries that turn automatically unless `continue` is `false`.
661
653
 
662
- `chatRecovery` can also be configured with budgets and terminal behavior:
654
+ #### Controlling automatic continuation
655
+
656
+ Durable bookkeeping stays enabled even when automatic continuation is not appropriate:
657
+
658
+ - **Retries or side effects are unsafe:** override `onChatRecovery()` and return `{ continue: false }`. Persist idempotency keys or completion records before external side effects so a recovered turn can tell whether work already happened.
659
+ - **Cancellation must survive eviction:** an `AbortSignal` only cancels the current in-memory turn. Also persist cancellation intent in agent state or SQL, read it in `onChatRecovery()`, and return `{ continue: false }` when cancellation was requested.
660
+ - **Cost must be bounded:** set `maxAttempts`, `noProgressTimeoutMs`, `maxRecoveryWork`, and `maxOomRetries`. Use `shouldKeepRecovering` with durable spend data to stop later attempts. The predicate is not bound to the agent instance, so read spend from a store keyed by `ctx.recoveryRootRequestId`.
661
+ - **A provider can resume without a new model call:** use `this.stash()` to save its response ID, retrieve that response in `onChatRecovery()`, and return `{ persist: false, continue: false }`.
662
+
663
+ `chatRecovery` can be configured with budgets and terminal behavior:
663
664
 
664
665
  ```typescript
665
666
  override chatRecovery = {
@@ -466,7 +466,7 @@ Key points:
466
466
 
467
467
  ### Chat recovery
468
468
 
469
- `AIChatAgent` builds on fibers for LLM streaming recovery. When `chatRecovery` is enabled, each chat turn is wrapped in a fiber automatically. The framework handles the internal recovery path and exposes `onChatRecovery` for provider-specific strategies. See [Long-Running Agents: Recovering interrupted LLM streams](./long-running-agents.md#recovering-interrupted-llm-streams) and the [`forever-chat` example](https://github.com/cloudflare/agents/tree/main/experimental/forever-chat).
469
+ `AIChatAgent` and `Think` build on fibers for LLM streaming recovery. Every chat turn is wrapped in a fiber automatically. The framework handles the internal recovery path and exposes `onChatRecovery` for provider-specific strategies. See [Long-Running Agents: Recovering interrupted LLM streams](./long-running-agents.md#recovering-interrupted-llm-streams) and the [`forever-chat` example](https://github.com/cloudflare/agents/tree/main/experimental/forever-chat).
470
470
 
471
471
  ## Concurrent fibers
472
472
 
@@ -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
 
@@ -357,7 +357,7 @@ See the complete example: [guides/human-in-the-loop/](https://github.com/cloudfl
357
357
 
358
358
  ### Surviving restarts while waiting for a human
359
359
 
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. 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.
361
361
 
362
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.
363
363
 
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.
@@ -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