agents 0.21.0 → 0.23.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 +21 -1
- package/dist/{agent-tool-types-CzGGB-20.d.ts → agent-routing-DE5zmCQ8.d.ts} +1844 -1336
- 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-DtXMTDGM.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 +6 -2
- package/dist/browser/ai.js.map +1 -1
- package/dist/browser/tanstack-ai.js.map +1 -1
- package/dist/callable-decorator-DP__HhBA.d.ts +72 -0
- package/dist/callable-decorator.d.ts +18 -0
- package/dist/callable-decorator.js +71 -0
- package/dist/callable-decorator.js.map +1 -0
- package/dist/capability-BjSKYpzg.js +42 -0
- package/dist/capability-BjSKYpzg.js.map +1 -0
- package/dist/capability-runner-Be_-PLR1.d.ts +459 -0
- package/dist/channel-Bnm4S7T2.d.ts +491 -0
- package/dist/channels/ai-sdk.d.ts +46 -0
- package/dist/channels/ai-sdk.js +120 -0
- package/dist/channels/ai-sdk.js.map +1 -0
- package/dist/channels/email.d.ts +95 -0
- package/dist/channels/email.js +323 -0
- package/dist/channels/email.js.map +1 -0
- package/dist/channels/index.d.ts +233 -0
- package/dist/channels/index.js +608 -0
- package/dist/channels/index.js.map +1 -0
- package/dist/channels/slack.d.ts +140 -0
- package/dist/channels/slack.js +614 -0
- package/dist/channels/slack.js.map +1 -0
- package/dist/channels/tanstack-ai.d.ts +39 -0
- package/dist/channels/tanstack-ai.js +17 -0
- package/dist/channels/tanstack-ai.js.map +1 -0
- package/dist/channels/telegram.d.ts +106 -0
- package/dist/channels/telegram.js +427 -0
- package/dist/channels/telegram.js.map +1 -0
- package/dist/channels/voice.d.ts +45 -0
- package/dist/channels/voice.js +122 -0
- package/dist/channels/voice.js.map +1 -0
- package/dist/chat/index.d.ts +2328 -2015
- package/dist/chat/index.js +891 -521
- 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 +7 -7
- 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/context/index.d.ts +216 -0
- package/dist/context/index.js +454 -0
- package/dist/context/index.js.map +1 -0
- package/dist/current-agent-Da_C9a3b.d.ts +266 -0
- package/dist/current-agent-DhoDkSnH.js +51 -0
- package/dist/current-agent-DhoDkSnH.js.map +1 -0
- package/dist/diagnostics-BzvaX2UT.js +45 -0
- package/dist/diagnostics-BzvaX2UT.js.map +1 -0
- package/dist/diagnostics-C4jcz3VK.js +360 -0
- package/dist/diagnostics-C4jcz3VK.js.map +1 -0
- package/dist/{do-oauth-client-provider-VTZj2VtM.d.ts → do-oauth-client-provider-Tmf1vgKz.d.ts} +2 -2
- 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/{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-BB0kqhIz.d.ts +101 -0
- package/dist/index-XDkuQ7zm.d.ts +89 -0
- package/dist/{index-BRnybD6X.d.ts → index-YSKgfgg9.d.ts} +21 -31
- package/dist/index.d.ts +113 -101
- package/dist/index.js +11 -7234
- package/dist/ingress-BfetZbMO.js +83 -0
- package/dist/ingress-BfetZbMO.js.map +1 -0
- package/dist/internal-CYlgHl1l.js +59 -0
- package/dist/internal-CYlgHl1l.js.map +1 -0
- 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 +66 -0
- package/dist/lifecycle/index.js +4 -0
- package/dist/lifecycle-CMRGjZdw.js +1299 -0
- package/dist/lifecycle-CMRGjZdw.js.map +1 -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/{retries-CAvxtG9d.d.ts → retries-D9Ds-1lz.d.ts} +17 -6
- package/dist/retries.d.ts +8 -6
- package/dist/retries.js +13 -1
- package/dist/retries.js.map +1 -1
- package/dist/routing/index.d.ts +137 -0
- package/dist/routing/index.js +244 -0
- package/dist/routing/index.js.map +1 -0
- package/dist/sanitize-D9TujEK8.js +79 -0
- package/dist/sanitize-D9TujEK8.js.map +1 -0
- package/dist/schedule.d.ts +25 -94
- package/dist/schedule.js +1 -98
- package/dist/schedule.js.map +1 -1
- package/dist/scheduler-DD9NdYbF.js +665 -0
- package/dist/scheduler-DD9NdYbF.js.map +1 -0
- package/dist/scheduler-Dwh85ZGl.d.ts +223 -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/sentence-chunker-BAidJ4DA.d.ts +68 -0
- package/dist/serializable.d.ts +1 -1
- package/dist/sessions/index.d.ts +441 -0
- package/dist/sessions/index.js +2063 -0
- package/dist/sessions/index.js.map +1 -0
- package/dist/skills/index.d.ts +99 -0
- package/dist/skills/index.js +254 -5
- package/dist/skills/index.js.map +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-DlSHshb2.js +6963 -0
- package/dist/src-DlSHshb2.js.map +1 -0
- package/dist/streams/index.d.ts +120 -0
- package/dist/streams/index.js +107 -0
- package/dist/streams/index.js.map +1 -0
- package/dist/streams-D6tJ0NN9.d.ts +370 -0
- package/dist/streams-DZKgAj9b.js +709 -0
- package/dist/streams-DZKgAj9b.js.map +1 -0
- package/dist/sub-routing.d.ts +12 -12
- package/dist/surface-bZZJqBka.js +17 -0
- package/dist/surface-bZZJqBka.js.map +1 -0
- package/dist/tasks/index.d.ts +64 -0
- package/dist/tasks/index.js +2 -0
- package/dist/tasks-BRJ5zgya.d.ts +517 -0
- package/dist/tasks-ylZgBjhj.js +1656 -0
- package/dist/tasks-ylZgBjhj.js.map +1 -0
- package/dist/text-segment-joiner-BtAFQSA_.js +57 -0
- package/dist/text-segment-joiner-BtAFQSA_.js.map +1 -0
- package/dist/text-stream-CpdiKrJB.js +272 -0
- package/dist/text-stream-CpdiKrJB.js.map +1 -0
- package/dist/tokens-nHAKcN6M.js +52 -0
- package/dist/tokens-nHAKcN6M.js.map +1 -0
- package/dist/tool-schema-CBjGPrsQ.js +31 -0
- package/dist/tool-schema-CBjGPrsQ.js.map +1 -0
- package/dist/types-B7LojTe4.d.ts +202 -0
- package/dist/types-_Faxb570.d.ts +439 -0
- package/dist/voice/client.d.ts +226 -0
- package/dist/voice/client.js +932 -0
- package/dist/voice/client.js.map +1 -0
- package/dist/voice/errors.d.ts +43 -0
- package/dist/voice/errors.js +41 -0
- package/dist/voice/errors.js.map +1 -0
- package/dist/voice/index.d.ts +271 -0
- package/dist/voice/index.js +1812 -0
- package/dist/voice/index.js.map +1 -0
- package/dist/voice/react.d.ts +167 -0
- package/dist/voice/react.js +234 -0
- package/dist/voice/react.js.map +1 -0
- package/dist/voice/sfu.d.ts +71 -0
- package/dist/voice/sfu.js +157 -0
- package/dist/voice/sfu.js.map +1 -0
- package/dist/voice/text.d.ts +6 -0
- package/dist/voice/text.js +2 -0
- package/dist/voice/types.d.ts +58 -0
- package/dist/voice/types.js +18 -0
- package/dist/voice/types.js.map +1 -0
- package/dist/voice/workers-ai.d.ts +136 -0
- package/dist/voice/workers-ai.js +568 -0
- package/dist/voice/workers-ai.js.map +1 -0
- package/dist/websockets/index.d.ts +192 -0
- package/dist/websockets/index.js +2 -0
- package/dist/websockets-DUfRHPRq.js +502 -0
- package/dist/websockets-DUfRHPRq.js.map +1 -0
- 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 +29 -87
- package/docs/agent-tools.md +2 -1
- package/docs/channels.md +323 -0
- package/docs/chat-agents.md +19 -25
- package/docs/context.md +131 -0
- 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 +16 -12
- package/docs/lifecycle.md +370 -0
- package/docs/long-running-agents.md +4 -6
- package/docs/mcp-client.md +55 -0
- package/docs/mcp-servers.md +5 -1
- package/docs/observability.md +11 -11
- package/docs/resumable-streaming.md +2 -2
- package/docs/routing.md +105 -0
- package/docs/scheduling.md +175 -15
- package/docs/server-driven-messages.md +1 -1
- package/docs/sessions.md +237 -871
- package/docs/streams.md +213 -0
- package/docs/sub-agents.md +185 -125
- package/docs/tasks.md +246 -0
- package/docs/voice.md +745 -0
- package/package.json +144 -33
- 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/compaction-helpers-iiKMr2TQ.js +0 -340
- package/dist/compaction-helpers-iiKMr2TQ.js.map +0 -1
- package/dist/compaction-helpers-wUz6M3us.d.ts +0 -621
- package/dist/experimental/memory/session/index.d.ts +0 -670
- package/dist/experimental/memory/session/index.js +0 -2374
- package/dist/experimental/memory/session/index.js.map +0 -1
- package/dist/experimental/memory/utils/index.d.ts +0 -96
- package/dist/experimental/memory/utils/index.js +0 -79
- package/dist/experimental/memory/utils/index.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/tool-output-truncation-CNnnGZQ3.js +0 -98
- package/dist/tool-output-truncation-CNnnGZQ3.js.map +0 -1
- package/dist/ws-chat-transport-CIoOBbO7.js.map +0 -1
- /package/dist/{cli/index.d.ts → index-BVVgDSdq.d.ts} +0 -0
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 pushes jobs through `this.lifecycle.jobs` and implements `onJob()`; Lifecycle drives due jobs and re-arms the alarm from queue state. See [Durable Object lifecycle](./lifecycle.md#the-job-queue) 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.
|
|
@@ -334,7 +278,7 @@ class MyAgent extends Agent {
|
|
|
334
278
|
}
|
|
335
279
|
```
|
|
336
280
|
|
|
337
|
-
Schedules are stored in the `
|
|
281
|
+
Schedules are stored as jobs in the `cf_agents_jobs` SQL table, the Lifecycle-owned job queue. Cron schedules automatically reschedule themselves after execution, while one-time schedules are deleted.
|
|
338
282
|
|
|
339
283
|
### `this.mcp` and friends
|
|
340
284
|
|
|
@@ -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/agent-tools.md
CHANGED
|
@@ -1,7 +1,8 @@
|
|
|
1
1
|
# Agent Tools
|
|
2
2
|
|
|
3
3
|
Agent tools let one chat agent dispatch another chat-capable sub-agent as part
|
|
4
|
-
of its work
|
|
4
|
+
of its work — per-run, parent-supervised delegation is the flagship use case
|
|
5
|
+
for facet-backed dynamic agents. The child is a real sub-agent with its own Durable Object storage,
|
|
5
6
|
messages, tools, resumable stream, and drill-in URL. The parent keeps a small
|
|
6
7
|
run registry so clients can render the child timeline, replay it after refresh,
|
|
7
8
|
and clean it up later.
|
package/docs/channels.md
ADDED
|
@@ -0,0 +1,323 @@
|
|
|
1
|
+
# `agents/channels`
|
|
2
|
+
|
|
3
|
+
`agents/channels` gives an agent one interface for sending and receiving
|
|
4
|
+
messages across different platforms. Use a Channel directly, expose it as an AI
|
|
5
|
+
tool, or register it with a durable `ChannelHost` that owns routing and delivery
|
|
6
|
+
recovery.
|
|
7
|
+
|
|
8
|
+
> [!NOTE]
|
|
9
|
+
> Channels is experimental. Its interface _will_ change before the package
|
|
10
|
+
> reaches a stable release.
|
|
11
|
+
|
|
12
|
+
## Install
|
|
13
|
+
|
|
14
|
+
```bash
|
|
15
|
+
npm install agents
|
|
16
|
+
```
|
|
17
|
+
|
|
18
|
+
## Create channels
|
|
19
|
+
|
|
20
|
+
Each adapter turns provider configuration into the same `Channel` interface. A
|
|
21
|
+
`ChannelHost` holds them, keyed by names you choose:
|
|
22
|
+
|
|
23
|
+
```typescript
|
|
24
|
+
import { ChannelHost, routes } from "agents/channels";
|
|
25
|
+
import { email } from "agents/channels/email";
|
|
26
|
+
import { slack } from "agents/channels/slack";
|
|
27
|
+
import { telegram } from "agents/channels/telegram";
|
|
28
|
+
|
|
29
|
+
const host = new ChannelHost({
|
|
30
|
+
channels: {
|
|
31
|
+
slack: slack({
|
|
32
|
+
botToken: env.SLACK_BOT_TOKEN,
|
|
33
|
+
webhook: {
|
|
34
|
+
signingSecret: env.SLACK_SIGNING_SECRET,
|
|
35
|
+
botUserId: env.SLACK_BOT_USER_ID
|
|
36
|
+
},
|
|
37
|
+
route: routes.perThread
|
|
38
|
+
}),
|
|
39
|
+
telegram: telegram({
|
|
40
|
+
botToken: env.TELEGRAM_BOT_TOKEN,
|
|
41
|
+
webhook: { secretToken: env.TELEGRAM_WEBHOOK_SECRET },
|
|
42
|
+
route: routes.perThread
|
|
43
|
+
}),
|
|
44
|
+
email: email({
|
|
45
|
+
binding: env.EMAIL,
|
|
46
|
+
from: "agent@example.com",
|
|
47
|
+
route: routes.perThread
|
|
48
|
+
})
|
|
49
|
+
},
|
|
50
|
+
|
|
51
|
+
async onMessage({ route, dispatchId, message }) {
|
|
52
|
+
await conversationFor(route).receive(dispatchId, message);
|
|
53
|
+
}
|
|
54
|
+
});
|
|
55
|
+
```
|
|
56
|
+
|
|
57
|
+
## 1. Send a message
|
|
58
|
+
|
|
59
|
+
A **surface** is a destination for an outbound message.
|
|
60
|
+
You get one from an inbound message's reply field, or by constructing one from a raw channel identifier through the host.
|
|
61
|
+
|
|
62
|
+
```typescript
|
|
63
|
+
const surface = host.contactSurface({
|
|
64
|
+
channelKey: "slack",
|
|
65
|
+
scope: "T123",
|
|
66
|
+
subject: "U456"
|
|
67
|
+
});
|
|
68
|
+
|
|
69
|
+
await host.deliver(surface, {
|
|
70
|
+
title: "Import needs attention",
|
|
71
|
+
markdown: "The customer import stopped after **1,240 records**."
|
|
72
|
+
});
|
|
73
|
+
```
|
|
74
|
+
|
|
75
|
+
An identity's `channelKey` names the configured Channel that observed it. Its
|
|
76
|
+
optional `scope` names a tenant within that Channel and defaults to `"default"`;
|
|
77
|
+
for example, one configured Slack app can observe the same user ID in several
|
|
78
|
+
workspaces. The Host stamps `channelKey` on inbound identities because an adapter
|
|
79
|
+
does not know the key it was configured under.
|
|
80
|
+
|
|
81
|
+
The same human observed through two configured Channels on one platform is two
|
|
82
|
+
Channel identities, just as the same human on Slack and email is. Applications
|
|
83
|
+
that know they are the same person link those identities explicitly.
|
|
84
|
+
|
|
85
|
+
Compose destinations with `fallback()` and `fanout()`:
|
|
86
|
+
|
|
87
|
+
```typescript
|
|
88
|
+
import { fallback, fanout } from "agents/channels";
|
|
89
|
+
|
|
90
|
+
await host.deliver(fallback([slackSurface, emailSurface]), message);
|
|
91
|
+
await host.deliver(fanout([slackSurface, emailSurface]), message);
|
|
92
|
+
```
|
|
93
|
+
|
|
94
|
+
`fallback()` tries destinations in order, advancing only after a _confirmed_
|
|
95
|
+
failure, so it can never duplicate a delivery. For a stream, it advances only
|
|
96
|
+
when that failure happens before the destination starts reading; replaying an
|
|
97
|
+
arbitrarily large consumed prefix would require an unbounded buffer. `fanout()`
|
|
98
|
+
sends to all destinations; a partial or uncertain result is reported as
|
|
99
|
+
`uncertain` for the same reason.
|
|
100
|
+
The Host installs both policies as ordinary Channels under reserved keys. You
|
|
101
|
+
can register another composite policy as an ordinary Channel under your own key
|
|
102
|
+
and pair it with a surface constructor that writes that key; inject only the
|
|
103
|
+
outbound resolution capability the policy needs, as the exported built-in
|
|
104
|
+
policy Channels do.
|
|
105
|
+
|
|
106
|
+
## 2. Give a destination to a model
|
|
107
|
+
|
|
108
|
+
```typescript
|
|
109
|
+
import { generateText, stepCountIs } from "ai";
|
|
110
|
+
import { createWorkersAI } from "workers-ai-provider";
|
|
111
|
+
import { createSendMessageTool } from "agents/channels/ai-sdk";
|
|
112
|
+
|
|
113
|
+
const workersai = createWorkersAI({ binding: env.AI });
|
|
114
|
+
|
|
115
|
+
await generateText({
|
|
116
|
+
model: workersai("@cf/moonshotai/kimi-k2.7-code"),
|
|
117
|
+
prompt: "An import stopped after 1,240 records. Notify support.",
|
|
118
|
+
tools: {
|
|
119
|
+
contactSupport: createSendMessageTool(
|
|
120
|
+
host,
|
|
121
|
+
fallback([slackSurface, emailSurface]),
|
|
122
|
+
{ description: "Contact support when a person needs to intervene" }
|
|
123
|
+
)
|
|
124
|
+
},
|
|
125
|
+
stopWhen: stepCountIs(2)
|
|
126
|
+
});
|
|
127
|
+
```
|
|
128
|
+
|
|
129
|
+
The model writes the message; you chose the destination. TanStack AI exports the
|
|
130
|
+
same `createSendMessageTool(host, surface, options)` from
|
|
131
|
+
`agents/channels/tanstack-ai`.
|
|
132
|
+
|
|
133
|
+
## 3. Receive messages
|
|
134
|
+
|
|
135
|
+
One entry point covers every configured Channel's webhook, and Workers Email
|
|
136
|
+
arrives the same way:
|
|
137
|
+
|
|
138
|
+
```typescript
|
|
139
|
+
export default {
|
|
140
|
+
async fetch(request: Request): Promise<Response> {
|
|
141
|
+
const response = await host.handleRequest(request);
|
|
142
|
+
if (response) return response;
|
|
143
|
+
return new Response("Not found", { status: 404 });
|
|
144
|
+
},
|
|
145
|
+
|
|
146
|
+
async email(message: ForwardableEmailMessage): Promise<void> {
|
|
147
|
+
await host.handleEmail(message);
|
|
148
|
+
}
|
|
149
|
+
} satisfies ExportedHandler<Env>;
|
|
150
|
+
```
|
|
151
|
+
|
|
152
|
+
Each Channel authenticates its own input and declines what isn't its business,
|
|
153
|
+
so the Host asks them in configuration order and the first to claim it wins.
|
|
154
|
+
|
|
155
|
+
### Routing
|
|
156
|
+
|
|
157
|
+
A Channel's `route` turns one normalized event into an opaque application
|
|
158
|
+
string — a Durable Object name, a queue key, a database id — or `null` to ignore
|
|
159
|
+
the event entirely. The key is used to identify a common destination for
|
|
160
|
+
messages -- i.e. typically a single conversation:
|
|
161
|
+
|
|
162
|
+
```typescript
|
|
163
|
+
telegram({
|
|
164
|
+
// …credentials…
|
|
165
|
+
route(event) {
|
|
166
|
+
return event.thread.isDirectMessage === true ? event.thread.id : null;
|
|
167
|
+
}
|
|
168
|
+
});
|
|
169
|
+
```
|
|
170
|
+
|
|
171
|
+
Channels exposes builtin helpers for the common mappings — `routes.perThread`
|
|
172
|
+
and `routes.perEvent`, which namespace their routes as `thread:…` and
|
|
173
|
+
`event:…`.
|
|
174
|
+
|
|
175
|
+
Deciding whether an event is relevant at all is a different question, and one
|
|
176
|
+
only your application can answer, because the answer usually depends on state
|
|
177
|
+
you hold. Write that in your own `route` and return `null` to ignore the event:
|
|
178
|
+
|
|
179
|
+
```typescript
|
|
180
|
+
route(event) {
|
|
181
|
+
// Slack shows a bot every message in every channel it belongs to. Answer
|
|
182
|
+
// when addressed, and otherwise only join threads we are already in.
|
|
183
|
+
const thread = routes.perThread(event);
|
|
184
|
+
const addressed =
|
|
185
|
+
event.thread.isDirectMessage === true || event.message.isMention === true;
|
|
186
|
+
if (addressed) return thread;
|
|
187
|
+
return (await myConversations.knows(thread)) ? thread : null;
|
|
188
|
+
}
|
|
189
|
+
```
|
|
190
|
+
|
|
191
|
+
A lookup like that runs for every event reaching it, so put it behind a cheaper
|
|
192
|
+
check, keep it read-only, and do not let it create the state it is testing for —
|
|
193
|
+
otherwise the first stray message conjures the thing the check is looking for.
|
|
194
|
+
|
|
195
|
+
### Link identities
|
|
196
|
+
|
|
197
|
+
Personal agents often wwant to resolve users regardless of the channel they messaged on. Your application can explicitly record connections between channel identities and expose them to the Host for messages to be routed on:
|
|
198
|
+
|
|
199
|
+
```typescript
|
|
200
|
+
const host = new ChannelHost({
|
|
201
|
+
channels,
|
|
202
|
+
findUser: (identity) => users.findUser(identity),
|
|
203
|
+
onMessage,
|
|
204
|
+
onApprovalResponse
|
|
205
|
+
});
|
|
206
|
+
|
|
207
|
+
// ...
|
|
208
|
+
// route to a user's central conversaion if one exists, else start a new
|
|
209
|
+
// conversation for each thread:
|
|
210
|
+
route: routes.byUser(routes.perThread);
|
|
211
|
+
|
|
212
|
+
// or prefer the linked person, then the sender we recognise, then a new
|
|
213
|
+
// conversation per event:
|
|
214
|
+
route: routes.byUser(routes.byIdentity(routes.perEvent));
|
|
215
|
+
```
|
|
216
|
+
|
|
217
|
+
`byIdentity` groups events carrying the _same_ identity. It never infers that
|
|
218
|
+
two different identities belong to one person — that stays an explicit
|
|
219
|
+
application decision, which `byUser` then exposes to routing. Omit its fallback
|
|
220
|
+
to ignore events that carry no identity at all.
|
|
221
|
+
|
|
222
|
+
If your application does not already store user identities, `createUserIdentityStore(storage)` creates a Durable Object SQL store of the right shape. Your application can call `store.link` to connect multiple identities together.
|
|
223
|
+
|
|
224
|
+
### Ask for approval
|
|
225
|
+
|
|
226
|
+
### Request approval
|
|
227
|
+
|
|
228
|
+
The Host exposes a utility for durably correlating inbound approvals to outbound requests:
|
|
229
|
+
|
|
230
|
+
```typescript
|
|
231
|
+
await host.requestApproval({
|
|
232
|
+
interactionId: "deploy-42",
|
|
233
|
+
request: {
|
|
234
|
+
title: "Production deployment",
|
|
235
|
+
summary: "Deploy version 2026.08.17 to production?",
|
|
236
|
+
input: {
|
|
237
|
+
version: "2026.08.17",
|
|
238
|
+
environment: "production"
|
|
239
|
+
}
|
|
240
|
+
}
|
|
241
|
+
});
|
|
242
|
+
await host.requestApproval(surface, {
|
|
243
|
+
interactionId: crypto.randomUUID(),
|
|
244
|
+
request: {
|
|
245
|
+
title: "Production deployment",
|
|
246
|
+
summary: "Deploy version 2026.08.17 to production?",
|
|
247
|
+
input: { version: "2026.08.17" }
|
|
248
|
+
}
|
|
249
|
+
});
|
|
250
|
+
```
|
|
251
|
+
|
|
252
|
+
Users can respond to approval requests through native surfaces (e.g. Telegram
|
|
253
|
+
buttons) or HTTP inbound URLs, resolved by the Channel Host itself for your application to settle.
|
|
254
|
+
|
|
255
|
+
## Custom channels
|
|
256
|
+
|
|
257
|
+
Any transport can become a Channel:
|
|
258
|
+
|
|
259
|
+
```typescript
|
|
260
|
+
import { matchesPath, routes, type Channel } from "agents/channels";
|
|
261
|
+
|
|
262
|
+
const supportForm: Channel = {
|
|
263
|
+
route: routes.perEvent,
|
|
264
|
+
ingress: {
|
|
265
|
+
async receive(request) {
|
|
266
|
+
if (!matchesPath(request, "/support")) return null;
|
|
267
|
+
|
|
268
|
+
const raw = await request.json<{ message: string; email: string }>();
|
|
269
|
+
const eventId = crypto.randomUUID();
|
|
270
|
+
return {
|
|
271
|
+
events: [
|
|
272
|
+
{
|
|
273
|
+
raw,
|
|
274
|
+
event: {
|
|
275
|
+
type: "message",
|
|
276
|
+
eventId,
|
|
277
|
+
thread: { id: eventId, isDirectMessage: true },
|
|
278
|
+
actor: {
|
|
279
|
+
id: raw.email,
|
|
280
|
+
// The Host stamps the configured `channelKey`.
|
|
281
|
+
identity: { subject: raw.email }
|
|
282
|
+
},
|
|
283
|
+
message: { id: eventId, text: raw.message }
|
|
284
|
+
}
|
|
285
|
+
}
|
|
286
|
+
],
|
|
287
|
+
response: Response.json({ accepted: true }, { status: 202 })
|
|
288
|
+
};
|
|
289
|
+
}
|
|
290
|
+
}
|
|
291
|
+
};
|
|
292
|
+
```
|
|
293
|
+
|
|
294
|
+
Returning `null` declines the request so another Channel can claim it.
|
|
295
|
+
|
|
296
|
+
## Durability contract
|
|
297
|
+
|
|
298
|
+
Channels holds no state: no outbox, no retries, no deduplication, no scheduler.
|
|
299
|
+
Durability is a property of how your application uses it. A caller-supplied
|
|
300
|
+
`deliveryId` is correlation metadata, not an idempotency guarantee; an adapter
|
|
301
|
+
may map it to a provider primitive when one exists.
|
|
302
|
+
|
|
303
|
+
| Channels guarantees | Your application must |
|
|
304
|
+
| ----------------------------------------------------------------------- | --------------------------------------------------------------------------- |
|
|
305
|
+
| A `dispatchId` stable across redelivery and unaffected by routing | Deduplicate on it before starting any side effect |
|
|
306
|
+
| The Host awaits your callback before the provider is acknowledged | Hand off durably before returning — a DO RPC, queue send, or workflow start |
|
|
307
|
+
| One outbound attempt per `deliver()` or `stream()`, reported honestly | Decide whether to retry; `uncertain` may duplicate a real delivery |
|
|
308
|
+
| Surfaces are plain JSON you can persist | Keep configured channel keys stable |
|
|
309
|
+
| Decisions arrive as normalized events carrying your own `interactionId` | Own settlement; an interaction id is not an authorization credential |
|
|
310
|
+
|
|
311
|
+
## Future work
|
|
312
|
+
|
|
313
|
+
- [ ] Approval-link ingress: signing, verification, and a confirmation page, so
|
|
314
|
+
link approvals return through the same normalized path as Slack buttons
|
|
315
|
+
- [ ] Reader-initiated stream cancellation: Slack's `message_stream_stopped`
|
|
316
|
+
and Telegram's `stopped_message_generation` should reach the running
|
|
317
|
+
generation as ordinary ingress, so aborting it errors the stream and
|
|
318
|
+
each Channel finalizes on the path it already has
|
|
319
|
+
- [ ] More built-in channels
|
|
320
|
+
- [ ] Rendering templates (pretty emails)
|
|
321
|
+
- [ ] Automatic webhook registration
|
|
322
|
+
- [ ] Security review of approval flows
|
|
323
|
+
- [ ] Conformance tests of adapters
|
package/docs/chat-agents.md
CHANGED
|
@@ -520,13 +520,9 @@ their cleanup and skipped continuations return immediately.
|
|
|
520
520
|
|
|
521
521
|
#### Overriding the clear handler
|
|
522
522
|
|
|
523
|
-
The SDK's built-in `CF_AGENT_CHAT_CLEAR` handler calls `resetTurnState()`
|
|
524
|
-
automatically. If your `onMessage` override intercepts `CF_AGENT_CHAT_CLEAR`
|
|
525
|
-
and returns before the SDK sees the message — for example, to scope the delete
|
|
526
|
-
to a specific workflow — the built-in handler never runs. The active stream
|
|
527
|
-
continues and queued continuations persist into the newly-cleared conversation.
|
|
523
|
+
The SDK's built-in `CF_AGENT_CHAT_CLEAR` handler calls `resetTurnState()` and clears the default Sessions handle. If an `onMessage` override intercepts this frame and returns early, the built-in handler cannot stop the active stream or clear history.
|
|
528
524
|
|
|
529
|
-
|
|
525
|
+
Perform authorization or logging, then pass the frame to the original handler:
|
|
530
526
|
|
|
531
527
|
```typescript
|
|
532
528
|
import { MessageType } from "@cloudflare/ai-chat/types";
|
|
@@ -536,19 +532,16 @@ this.onMessage = async (connection, message) => {
|
|
|
536
532
|
if (typeof message === "string") {
|
|
537
533
|
const data = JSON.parse(message);
|
|
538
534
|
if (data.type === MessageType.CF_AGENT_CHAT_CLEAR) {
|
|
539
|
-
this.
|
|
540
|
-
|
|
541
|
-
DELETE FROM cf_ai_chat_agent_messages
|
|
542
|
-
WHERE workflow_id = ${this.workflowId}
|
|
543
|
-
`;
|
|
544
|
-
await this.saveMessages([]);
|
|
545
|
-
return;
|
|
535
|
+
await this.authorizeClear(connection);
|
|
536
|
+
console.log("clearing conversation");
|
|
546
537
|
}
|
|
547
538
|
}
|
|
548
539
|
return _onMessage(connection, message);
|
|
549
540
|
};
|
|
550
541
|
```
|
|
551
542
|
|
|
543
|
+
Do not write directly to Sessions tables. Use `this.sessions.session().clearMessages()` when implementing a separate server-side history operation outside the chat protocol.
|
|
544
|
+
|
|
552
545
|
### Lifecycle Hooks
|
|
553
546
|
|
|
554
547
|
Override `onConnect` and `onClose` to add custom logic. Stream resumption and message sync are handled for you automatically — you do not need to call `super`:
|
|
@@ -590,17 +583,11 @@ If you do not pass `abortSignal` to `streamText`, the LLM call will continue run
|
|
|
590
583
|
|
|
591
584
|
### Stream Recovery
|
|
592
585
|
|
|
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. `
|
|
594
|
-
|
|
595
|
-
```typescript
|
|
596
|
-
export class ChatAgent extends AIChatAgent {
|
|
597
|
-
override chatRecovery = true;
|
|
598
|
-
}
|
|
599
|
-
```
|
|
586
|
+
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.
|
|
600
587
|
|
|
601
|
-
|
|
588
|
+
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
589
|
|
|
603
|
-
|
|
590
|
+
Durable recovery is always enabled. Use `chatRecovery` only to tune its budgets and terminal behavior.
|
|
604
591
|
|
|
605
592
|
> **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
593
|
|
|
@@ -610,8 +597,6 @@ Override to implement provider-specific recovery. The default behavior persists
|
|
|
610
597
|
|
|
611
598
|
```typescript
|
|
612
599
|
export class ChatAgent extends AIChatAgent {
|
|
613
|
-
override chatRecovery = true;
|
|
614
|
-
|
|
615
600
|
override async onChatRecovery(
|
|
616
601
|
ctx: ChatRecoveryContext
|
|
617
602
|
): Promise<ChatRecoveryOptions> {
|
|
@@ -659,7 +644,16 @@ Settled work is never dropped: `persist: false` only suppresses persistence of a
|
|
|
659
644
|
|
|
660
645
|
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
646
|
|
|
662
|
-
|
|
647
|
+
#### Controlling automatic continuation
|
|
648
|
+
|
|
649
|
+
Durable bookkeeping stays enabled even when automatic continuation is not appropriate:
|
|
650
|
+
|
|
651
|
+
- **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.
|
|
652
|
+
- **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.
|
|
653
|
+
- **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`.
|
|
654
|
+
- **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 }`.
|
|
655
|
+
|
|
656
|
+
`chatRecovery` can be configured with budgets and terminal behavior:
|
|
663
657
|
|
|
664
658
|
```typescript
|
|
665
659
|
override chatRecovery = {
|