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.
- package/README.md +1 -0
- package/dist/{agent-tool-types-CzGGB-20.d.ts → agent-routing-CnkaHb-v.d.ts} +2921 -3057
- package/dist/agent-routing.d.ts +14 -0
- package/dist/agent-routing.js +187 -0
- package/dist/agent-routing.js.map +1 -0
- package/dist/agent-tool-types.d.ts +26 -26
- package/dist/{agent-tools-zR2d5uij.d.ts → agent-tools-C0XJqWzB.d.ts} +7 -7
- package/dist/agent-tools.d.ts +21 -21
- package/dist/agent-tools.js +2 -1
- package/dist/agent-tools.js.map +1 -1
- package/dist/browser/ai.js +2 -1
- package/dist/browser/ai.js.map +1 -1
- package/dist/capability-BjSKYpzg.js +42 -0
- package/dist/capability-BjSKYpzg.js.map +1 -0
- package/dist/capability-runner-CvHGZqUu.d.ts +150 -0
- package/dist/chat/index.d.ts +22 -15
- package/dist/chat/index.js +4 -4
- package/dist/chat/index.js.map +1 -1
- package/dist/chat/react.d.ts +14 -1
- package/dist/chat/react.js +82 -52
- package/dist/chat/react.js.map +1 -1
- package/dist/chat/transport.js +1 -1
- package/dist/chat-sdk/index.d.ts +4 -4
- package/dist/chat-sdk/index.js +1 -1
- package/dist/{client-zqKcsyFa.js → client-jagG8a9_.js} +129 -37
- package/dist/client-jagG8a9_.js.map +1 -0
- package/dist/client.d.ts +1 -1
- package/dist/client.js +1 -1
- package/dist/{cloudflare-BduZwmYK.js → cloudflare-Dzvc7V2N.js} +10 -3
- package/dist/{cloudflare-BduZwmYK.js.map → cloudflare-Dzvc7V2N.js.map} +1 -1
- package/dist/current-agent-CuMErtly.d.ts +283 -0
- package/dist/current-agent-DhoDkSnH.js +51 -0
- package/dist/current-agent-DhoDkSnH.js.map +1 -0
- package/dist/diagnostics-CaBjfz4J.js +41 -0
- package/dist/diagnostics-CaBjfz4J.js.map +1 -0
- package/dist/{do-oauth-client-provider-VTZj2VtM.d.ts → do-oauth-client-provider-Tmf1vgKz.d.ts} +2 -2
- package/dist/durable-object-lifecycle-D6nNQJJd.js +862 -0
- package/dist/durable-object-lifecycle-D6nNQJJd.js.map +1 -0
- package/dist/{email-CL27preh.d.ts → email-7TatiTnl.d.ts} +38 -9
- package/dist/email-send.d.ts +15 -0
- package/dist/email-send.js +32 -0
- package/dist/email-send.js.map +1 -0
- package/dist/email.d.ts +14 -10
- package/dist/email.js.map +1 -1
- package/dist/experimental/memory/session/index.d.ts +3 -2
- package/dist/experimental/memory/session/index.js +15 -10
- package/dist/experimental/memory/session/index.js.map +1 -1
- package/dist/{handler-stateless-C_bo-Ytq.d.ts → handler-stateless-DxYpJ_XF.d.ts} +3 -3
- package/dist/{handler-stateless-CIkKPETH.js → handler-stateless-VvrWSAVA.js} +5 -5
- package/dist/handler-stateless-VvrWSAVA.js.map +1 -0
- package/dist/{index-BRnybD6X.d.ts → index-DcSAZKsB.d.ts} +19 -31
- package/dist/index.d.ts +98 -95
- package/dist/index.js +10 -7234
- package/dist/internal_context-BlxFEWfn.d.ts +19 -0
- package/dist/internal_context.d.ts +10 -4
- package/dist/internal_context.js +1 -10
- package/dist/{client-invoker-BNSZxAkv.d.ts → invoker-CG0_p_Wq.d.ts} +2 -2
- package/dist/{client-invoker-VNZ7X0nn.js → invoker-CHMnoxIA.js} +2 -2
- package/dist/invoker-CHMnoxIA.js.map +1 -0
- package/dist/lifecycle/index.d.ts +48 -0
- package/dist/lifecycle/index.js +4 -0
- package/dist/mcp/{do-oauth-client-provider.d.ts → client/do-oauth-client-provider.d.ts} +1 -1
- package/dist/mcp/{do-oauth-client-provider.js → client/do-oauth-client-provider.js} +1 -1
- package/dist/mcp/client/do-oauth-client-provider.js.map +1 -0
- package/dist/mcp/client/index.d.ts +42 -0
- package/dist/mcp/{client.js → client/index.js} +1 -1
- package/dist/mcp/{x402.d.ts → client/x402.d.ts} +2 -2
- package/dist/mcp/{x402.js → client/x402.js} +2 -2
- package/dist/mcp/client/x402.js.map +1 -0
- package/dist/mcp/index.d.ts +36 -36
- package/dist/mcp/index.js +14 -16
- package/dist/mcp/index.js.map +1 -1
- package/dist/mcp/{server.d.ts → server/index.d.ts} +1 -1
- package/dist/mcp/{server.js → server/index.js} +1 -1
- package/dist/observability/ai/index.js +50 -35
- package/dist/observability/ai/index.js.map +1 -1
- package/dist/observability/index.d.ts +4 -4
- package/dist/observability/index.js +3 -50
- package/dist/observability/index.js.map +1 -1
- package/dist/{protocol-Dqc2MQxo.js → protocol-B0nh6KNf.js} +19 -21
- package/dist/protocol-B0nh6KNf.js.map +1 -0
- package/dist/react.d.ts +4 -4
- package/dist/react.js +1 -1
- package/dist/schedule.d.ts +25 -94
- package/dist/schedule.js +1 -98
- package/dist/schedule.js.map +1 -1
- package/dist/scheduler-CR9RHGos.js +857 -0
- package/dist/scheduler-CR9RHGos.js.map +1 -0
- package/dist/scheduler-DQoTGoAW.d.ts +224 -0
- package/dist/schedules/index.d.ts +22 -0
- package/dist/schedules/index.js +2 -0
- package/dist/schedules/parser.d.ts +79 -0
- package/dist/schedules/parser.js +103 -0
- package/dist/schedules/parser.js.map +1 -0
- package/dist/serializable.d.ts +1 -1
- package/dist/sql-error-CPY-GXyI.d.ts +12 -0
- package/dist/sql-error.d.ts +2 -0
- package/dist/sql-error.js +16 -0
- package/dist/sql-error.js.map +1 -0
- package/dist/src-5W6JNKVb.js +6613 -0
- package/dist/src-5W6JNKVb.js.map +1 -0
- package/dist/sub-routing.d.ts +12 -12
- package/dist/workflow-types.d.ts +25 -25
- package/dist/workflows.d.ts +22 -22
- package/dist/workflows.js +2 -1
- package/dist/workflows.js.map +1 -1
- package/dist/{ws-chat-transport-CIoOBbO7.js → ws-chat-transport-rWwta645.js} +152 -15
- package/dist/ws-chat-transport-rWwta645.js.map +1 -0
- package/docs/agent-class.md +28 -86
- package/docs/chat-agents.md +13 -12
- package/docs/durable-execution.md +1 -1
- package/docs/http-websockets.md +1 -11
- package/docs/human-in-the-loop.md +1 -1
- package/docs/index.md +1 -0
- package/docs/lifecycle.md +323 -0
- package/docs/long-running-agents.md +2 -4
- package/docs/mcp-client.md +55 -0
- package/docs/observability.md +11 -11
- package/docs/resumable-streaming.md +1 -1
- package/docs/scheduling.md +175 -15
- package/docs/server-driven-messages.md +1 -1
- package/docs/sub-agents.md +1 -1
- package/package.json +30 -21
- package/dist/cli/index.d.ts +0 -1
- package/dist/cli/index.js +0 -26
- package/dist/cli/index.js.map +0 -1
- package/dist/client-invoker-VNZ7X0nn.js.map +0 -1
- package/dist/client-zqKcsyFa.js.map +0 -1
- package/dist/handler-stateless-CIkKPETH.js.map +0 -1
- package/dist/index.js.map +0 -1
- package/dist/internal_context-Dg4Cgjcu.d.ts +0 -37
- package/dist/internal_context.js.map +0 -1
- package/dist/mcp/client.d.ts +0 -42
- package/dist/mcp/do-oauth-client-provider.js.map +0 -1
- package/dist/mcp/x402.js.map +0 -1
- package/dist/protocol-Dqc2MQxo.js.map +0 -1
- package/dist/ws-chat-transport-CIoOBbO7.js.map +0 -1
package/docs/agent-class.md
CHANGED
|
@@ -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
|
-
|
|
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
|
-
|
|
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
|
-
###
|
|
81
|
+
### Alarms
|
|
77
82
|
|
|
78
|
-
|
|
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
|
-
|
|
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:
|
|
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
|
-
`
|
|
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
|
-
|
|
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
|
-
//
|
|
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
|
-
|
|
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
|
-
|
|
169
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
134
|
+
### Identity
|
|
189
135
|
|
|
190
|
-
-
|
|
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
|
|
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
|
-
|
|
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 (
|
|
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
|
|
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
|
-
|
|
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;
|
package/docs/chat-agents.md
CHANGED
|
@@ -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. `
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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`
|
|
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
|
|
package/docs/http-websockets.md
CHANGED
|
@@ -408,17 +408,7 @@ export class MyAgent extends Agent {
|
|
|
408
408
|
|
|
409
409
|
## Hibernation
|
|
410
410
|
|
|
411
|
-
|
|
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.
|
|
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.
|
|
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)
|
|
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
|
|