agents 0.16.1 → 0.17.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 (127) hide show
  1. package/README.md +11 -8
  2. package/dist/{agent-tool-types-NofdbL9X.d.ts → agent-tool-types-Cd1TZPfB.d.ts} +595 -137
  3. package/dist/agent-tool-types.d.ts +34 -18
  4. package/dist/agent-tool-types.js +20 -1
  5. package/dist/agent-tool-types.js.map +1 -0
  6. package/dist/agent-tools-BXlsuX0d.js +304 -0
  7. package/dist/agent-tools-BXlsuX0d.js.map +1 -0
  8. package/dist/agent-tools-_E8wxUIK.d.ts +119 -0
  9. package/dist/agent-tools.d.ts +24 -18
  10. package/dist/agent-tools.js.map +1 -1
  11. package/dist/ai-chat-agent.d.ts +1 -1
  12. package/dist/ai-chat-agent.js +1 -2
  13. package/dist/ai-chat-agent.js.map +1 -1
  14. package/dist/ai-chat-v5-migration.d.ts +1 -1
  15. package/dist/ai-chat-v5-migration.js +1 -2
  16. package/dist/ai-chat-v5-migration.js.map +1 -1
  17. package/dist/ai-react.d.ts +1 -1
  18. package/dist/ai-react.js +1 -2
  19. package/dist/ai-react.js.map +1 -1
  20. package/dist/ai-types.d.ts +1 -7
  21. package/dist/ai-types.js +2 -8
  22. package/dist/ai-types.js.map +1 -1
  23. package/dist/browser/ai.js +1 -1
  24. package/dist/browser/index.js +1 -1
  25. package/dist/chat/index.d.ts +1923 -76
  26. package/dist/chat/index.js +1784 -278
  27. package/dist/chat/index.js.map +1 -1
  28. package/dist/chat/react.d.ts +602 -0
  29. package/dist/chat/react.js +1506 -0
  30. package/dist/chat/react.js.map +1 -0
  31. package/dist/chat-sdk/index.d.ts +4 -4
  32. package/dist/{classPrivateFieldGet2-CZ7QjTXN.js → classPrivateFieldGet2-DZBYAB34.js} +5 -5
  33. package/dist/{classPrivateMethodInitSpec-D-0__zd9.js → classPrivateMethodInitSpec-qMjJ6sHQ.js} +2 -2
  34. package/dist/{client-FUizKzj2.js → client-BZ-B3NhC.js} +19 -3
  35. package/dist/client-BZ-B3NhC.js.map +1 -0
  36. package/dist/client-tools-aIBO0Fk7.d.ts +53 -0
  37. package/dist/client.d.ts +76 -57
  38. package/dist/client.js +33 -5
  39. package/dist/client.js.map +1 -1
  40. package/dist/{compaction-helpers-DVcu5lPN.d.ts → compaction-helpers-wUz6M3us.d.ts} +20 -1
  41. package/dist/{connector-CrKhowfD.js → connector-CdldGF3h.js} +5 -5
  42. package/dist/{connector-CrKhowfD.js.map → connector-CdldGF3h.js.map} +1 -1
  43. package/dist/email.js +1 -1
  44. package/dist/email.js.map +1 -1
  45. package/dist/experimental/memory/session/index.d.ts +1 -1
  46. package/dist/experimental/memory/session/index.js +20 -2
  47. package/dist/experimental/memory/session/index.js.map +1 -1
  48. package/dist/experimental/memory/utils/index.d.ts +1 -1
  49. package/dist/{index-B7IbEeze.d.ts → index-CcbnKkNh.d.ts} +188 -14
  50. package/dist/index.d.ts +91 -71
  51. package/dist/index.js +562 -24
  52. package/dist/index.js.map +1 -1
  53. package/dist/mcp/client.d.ts +18 -14
  54. package/dist/mcp/client.js +1 -1
  55. package/dist/mcp/index.d.ts +30 -30
  56. package/dist/mcp/index.js +7 -7
  57. package/dist/mcp/index.js.map +1 -1
  58. package/dist/{agent-tools-3zLG7MgA.js → message-builder-BymO4N_D.js} +45 -126
  59. package/dist/message-builder-BymO4N_D.js.map +1 -0
  60. package/dist/observability/index.d.ts +1 -1
  61. package/dist/observability/index.js +4 -2
  62. package/dist/observability/index.js.map +1 -1
  63. package/dist/react.d.ts +123 -110
  64. package/dist/react.js +44 -11
  65. package/dist/react.js.map +1 -1
  66. package/dist/serializable.d.ts +1 -1
  67. package/dist/skills/compile.js +1 -1
  68. package/dist/skills/compile.js.map +1 -1
  69. package/dist/skills/index.js +5 -5
  70. package/dist/skills/index.js.map +1 -1
  71. package/dist/sub-routing.d.ts +6 -6
  72. package/dist/utils.js +1 -1
  73. package/dist/utils.js.map +1 -1
  74. package/dist/vite.js +5 -5
  75. package/dist/vite.js.map +1 -1
  76. package/dist/wire-types-nflOzNuU.js +240 -0
  77. package/dist/wire-types-nflOzNuU.js.map +1 -0
  78. package/dist/{workflow-types-SrZK_o9p.d.ts → workflow-types-Baz_PO5v.d.ts} +42 -22
  79. package/dist/workflow-types.d.ts +25 -21
  80. package/dist/workflow-types.js.map +1 -1
  81. package/dist/workflows.d.ts +22 -21
  82. package/dist/workflows.js +31 -7
  83. package/dist/workflows.js.map +1 -1
  84. package/docs/adding-to-existing-project.md +450 -0
  85. package/docs/agent-class.md +503 -0
  86. package/docs/agent-tools.md +552 -0
  87. package/docs/browse-the-web.md +430 -0
  88. package/docs/callable-methods.md +627 -0
  89. package/docs/chat-agents.md +1687 -0
  90. package/docs/chat-sdk.md +181 -0
  91. package/docs/client-sdk.md +520 -0
  92. package/docs/client-tools-continuation.md +177 -0
  93. package/docs/codemode.md +440 -0
  94. package/docs/configuration.md +775 -0
  95. package/docs/cross-domain-authentication.md +171 -0
  96. package/docs/durable-execution.md +537 -0
  97. package/docs/email.md +663 -0
  98. package/docs/get-current-agent.md +204 -0
  99. package/docs/getting-started.md +305 -0
  100. package/docs/http-websockets.md +668 -0
  101. package/docs/human-in-the-loop.md +661 -0
  102. package/docs/index.md +151 -0
  103. package/docs/long-running-agents.md +730 -0
  104. package/docs/mcp-client.md +620 -0
  105. package/docs/mcp-servers.md +526 -0
  106. package/docs/mcp-transports.md +308 -0
  107. package/docs/migration-to-ai-sdk-v5.md +96 -0
  108. package/docs/migration-to-ai-sdk-v6.md +163 -0
  109. package/docs/observability.md +261 -0
  110. package/docs/push-notifications.md +367 -0
  111. package/docs/queue.md +329 -0
  112. package/docs/readonly-connections.md +278 -0
  113. package/docs/resumable-streaming.md +127 -0
  114. package/docs/retries.md +444 -0
  115. package/docs/routing.md +749 -0
  116. package/docs/scheduling.md +898 -0
  117. package/docs/securing-mcp-servers.md +359 -0
  118. package/docs/server-driven-messages.md +477 -0
  119. package/docs/sessions.md +1024 -0
  120. package/docs/state.md +512 -0
  121. package/docs/sub-agents.md +389 -0
  122. package/docs/webhooks.md +604 -0
  123. package/docs/workflows.md +877 -0
  124. package/package.json +47 -15
  125. package/dist/agent-tools-3zLG7MgA.js.map +0 -1
  126. package/dist/agent-tools-DLquv-dp.d.ts +0 -14
  127. package/dist/client-FUizKzj2.js.map +0 -1
@@ -0,0 +1,537 @@
1
+ # Durable Execution
2
+
3
+ Run work that survives Durable Object eviction. `runFiber()` registers a task in SQLite, keeps the agent alive during execution, lets you checkpoint intermediate state with `stash()`, and calls `onFiberRecovered()` on the next activation if the agent was evicted mid-task.
4
+
5
+ > For how fibers fit into the bigger picture of building agents that run for weeks or months, see [Long-Running Agents](./long-running-agents.md).
6
+
7
+ ## Quick start
8
+
9
+ ```typescript
10
+ import { Agent } from "agents";
11
+ import type { FiberRecoveryContext } from "agents";
12
+
13
+ class MyAgent extends Agent {
14
+ async doWork() {
15
+ await this.runFiber("my-task", async (ctx) => {
16
+ const step1 = await expensiveOperation();
17
+ ctx.stash({ step1 });
18
+
19
+ const step2 = await anotherExpensiveOperation(step1);
20
+ this.setState({ ...this.state, result: step2 });
21
+ });
22
+ }
23
+
24
+ async onFiberRecovered(ctx: FiberRecoveryContext) {
25
+ if (ctx.name !== "my-task") return;
26
+ const snapshot = ctx.snapshot as { step1: unknown } | null;
27
+ if (snapshot) {
28
+ // Resume from the checkpoint — step1 is done, run step2
29
+ const step2 = await anotherExpensiveOperation(snapshot.step1);
30
+ this.setState({ ...this.state, result: step2 });
31
+ }
32
+ }
33
+ }
34
+ ```
35
+
36
+ ## Why fibers exist
37
+
38
+ Durable Objects get evicted for three reasons:
39
+
40
+ 1. **Inactivity timeout** — ~70–140 seconds with no incoming requests or open WebSockets
41
+ 2. **Code updates / runtime restarts** — non-deterministic, 1–2x per day
42
+ 3. **Alarm handler timeout** — 15 minutes
43
+
44
+ When eviction happens mid-work, the upstream HTTP connection (to an LLM provider, an API, a database) is severed permanently. In-memory state — streaming buffers, partial responses, loop counters — is lost. Multi-turn agent loops lose their position entirely.
45
+
46
+ `keepAlive()` reduces the chance of eviction. `runFiber()` makes eviction survivable.
47
+
48
+ For work that should run independently of the agent with per-step retries and multi-step orchestration, use [Workflows](./workflows.md) instead. Fibers are for work that is part of the agent's own execution. See [Long-Running Agents: Workflows vs agent-internal patterns](./long-running-agents.md#when-to-use-workflows-vs-agent-internal-patterns) for a comparison.
49
+
50
+ ## `keepAlive()`
51
+
52
+ Prevents idle eviction by creating a 30-second alarm heartbeat that resets the inactivity timer.
53
+
54
+ ```typescript
55
+ class Agent {
56
+ keepAlive(): Promise<() => void>;
57
+ keepAliveWhile<T>(fn: () => Promise<T>): Promise<T>;
58
+ }
59
+ ```
60
+
61
+ `keepAliveWhile()` is the recommended approach — it runs an async function and automatically cleans up the heartbeat when it completes or throws:
62
+
63
+ ```typescript
64
+ const result = await this.keepAliveWhile(async () => {
65
+ return await slowAPICall();
66
+ });
67
+ ```
68
+
69
+ For manual control, `keepAlive()` returns a disposer. Always call it when done — otherwise the heartbeat continues indefinitely:
70
+
71
+ ```typescript
72
+ const dispose = await this.keepAlive();
73
+ try {
74
+ await longWork();
75
+ } finally {
76
+ dispose();
77
+ }
78
+ ```
79
+
80
+ ### How it works
81
+
82
+ While any `keepAlive` ref is held, an alarm fires every 30 seconds that resets the inactivity timer. When all disposers are called, alarms stop and the DO can go idle naturally.
83
+
84
+ The heartbeat is invisible to `listSchedules()` — no schedule rows are created. It does not conflict with your own schedules; the alarm system multiplexes all schedules and the keepAlive heartbeat through a single alarm slot.
85
+
86
+ ### Configurable interval
87
+
88
+ Default: 30 seconds. The inactivity timeout is ~70–140 seconds, so 30 seconds gives comfortable margin. Override via static options:
89
+
90
+ ```typescript
91
+ class MyAgent extends Agent {
92
+ static options = { keepAliveIntervalMs: 2_000 }; // useful for testing recovery locally
93
+ }
94
+ ```
95
+
96
+ ### When to use keepAlive vs runFiber
97
+
98
+ `keepAlive` prevents eviction but does nothing about recovery. If the agent _is_ evicted despite the heartbeat (code update, alarm timeout, resource limit), any in-progress work is lost.
99
+
100
+ `runFiber` calls `keepAlive` internally _and_ persists the work in SQLite so it can be recovered. Use `keepAlive` alone when the work is cheap to redo or does not need checkpointing. Use `runFiber` when the work is expensive and you need to resume from where you left off.
101
+
102
+ | Scenario | Use |
103
+ | ------------------------------------------------ | --------------------------- |
104
+ | Waiting on a slow API call | `keepAlive()` |
105
+ | Streaming an LLM response (via `AIChatAgent`) | Automatic (built in) |
106
+ | Multi-step computation with intermediate results | `runFiber()` |
107
+ | Background research loop that takes 10+ minutes | `runFiber()` with `stash()` |
108
+
109
+ ## `runFiber()`
110
+
111
+ Durable execution with checkpointing and recovery.
112
+
113
+ ```typescript
114
+ class Agent {
115
+ runFiber<T>(name: string, fn: (ctx: FiberContext) => Promise<T>): Promise<T>;
116
+ startFiber(
117
+ name: string,
118
+ fn: (ctx: FiberContext) => Promise<void>,
119
+ options?: StartFiberOptions
120
+ ): Promise<StartFiberResult>;
121
+ inspectFiber(fiberId: string): Promise<FiberInspection | null>;
122
+ inspectFiberByKey(idempotencyKey: string): Promise<FiberInspection | null>;
123
+ listFibers(options?: ListFibersOptions): Promise<FiberInspection[]>;
124
+ cancelFiber(fiberId: string, reason?: string): Promise<boolean>;
125
+ cancelFiberByKey(idempotencyKey: string, reason?: string): Promise<boolean>;
126
+ deleteFibers(options?: DeleteFibersOptions): Promise<number>;
127
+ resolveFiber(fiberId: string, result: FiberRecoveryResult): Promise<boolean>;
128
+ stash(data: unknown): void;
129
+ onFiberRecovered(
130
+ ctx: FiberRecoveryContext
131
+ ): Promise<void | FiberRecoveryResult>;
132
+ }
133
+
134
+ type FiberContext = {
135
+ id: string;
136
+ signal: AbortSignal;
137
+ stash(data: unknown): void;
138
+ snapshot: unknown | null;
139
+ };
140
+
141
+ type FiberRecoveryContext = {
142
+ id: string;
143
+ name: string;
144
+ status?: FiberStatus;
145
+ idempotencyKey?: string;
146
+ metadata?: Record<string, unknown> | null;
147
+ snapshot: unknown | null;
148
+ createdAt: number;
149
+ recoveryReason: "interrupted";
150
+ };
151
+ ```
152
+
153
+ ## `startFiber()`
154
+
155
+ Use `startFiber()` when a caller needs to durably accept background work,
156
+ return quickly, and safely dedupe retries. It stores a retained fiber record
157
+ before the callback runs, then starts the callback in the background using the
158
+ same keep-alive and recovery machinery as `runFiber()`.
159
+
160
+ ```typescript
161
+ const receipt = await this.startFiber(
162
+ "reply-to-webhook",
163
+ async (ctx) => {
164
+ ctx.stash({ webhookId, threadId });
165
+ await postReply(threadId);
166
+ },
167
+ {
168
+ idempotencyKey: `webhook:${webhookId}`,
169
+ metadata: { threadId }
170
+ }
171
+ );
172
+
173
+ if (!receipt.accepted) {
174
+ // This webhook was already accepted by an earlier delivery.
175
+ }
176
+ ```
177
+
178
+ By default, `startFiber()` returns after the work is durably accepted. Pass
179
+ `waitForCompletion: true` when the caller should remain open until the accepted
180
+ fiber reaches a terminal status. Duplicate calls with the same idempotency key
181
+ join an active in-memory execution when possible, then return the retained
182
+ status with `accepted: false`.
183
+
184
+ ```typescript
185
+ const result = await this.startFiber("reply-to-webhook", reply, {
186
+ idempotencyKey: `webhook:${webhookId}`,
187
+ waitForCompletion: true
188
+ });
189
+
190
+ if (result.status === "error") {
191
+ console.error(result.error);
192
+ }
193
+ ```
194
+
195
+ `startFiber()` is a durable acceptance API, not a value-return API. It returns
196
+ the managed fiber status, but not the callback's result. Inspect status later
197
+ with `inspectFiber()` or `inspectFiberByKey()`.
198
+
199
+ ```typescript
200
+ const current = await this.inspectFiberByKey(`webhook:${webhookId}`);
201
+
202
+ await this.cancelFiber(current.fiberId, "No longer needed");
203
+
204
+ await this.deleteFibers({
205
+ status: ["completed", "error", "aborted"],
206
+ settledBefore: new Date(Date.now() - 7 * 24 * 60 * 60 * 1000)
207
+ });
208
+ ```
209
+
210
+ By default, `deleteFibers()` deletes settled `completed`, `error`, and
211
+ `aborted` rows. It does not delete `interrupted` rows unless you pass that status
212
+ explicitly, because interrupted rows often need inspection or manual resolution.
213
+
214
+ Cancellation is cooperative. `cancelFiber()` records an aborted terminal state
215
+ and aborts `ctx.signal` if the fiber is running in the current isolate. Your
216
+ callback should check `ctx.signal.aborted` around expensive work and before
217
+ visible side effects. Callers using `waitForCompletion: true` return when the
218
+ ledger reaches `aborted`, even if a non-cooperative callback keeps running in
219
+ the current isolate.
220
+
221
+ If the Durable Object is evicted mid-fiber, the retained record is marked
222
+ `interrupted` and `onFiberRecovered()` receives the last checkpoint. The
223
+ original closure cannot be replayed automatically; use `ctx.name`,
224
+ `ctx.snapshot`, and metadata to decide whether to resume, compensate, or leave
225
+ the record for inspection.
226
+
227
+ Return a `FiberRecoveryResult` from `onFiberRecovered()` to record the policy
228
+ decision:
229
+
230
+ ```typescript
231
+ async onFiberRecovered(ctx: FiberRecoveryContext) {
232
+ if (ctx.name !== "reply-to-webhook") return;
233
+
234
+ const snapshot = ctx.snapshot as { webhookId: string; threadId: string };
235
+ await postRecoveryMessage(snapshot.threadId);
236
+
237
+ return {
238
+ status: "completed",
239
+ snapshot: { ...snapshot, recovered: true }
240
+ };
241
+ }
242
+ ```
243
+
244
+ Returning `undefined` keeps a managed fiber `interrupted`. Throwing leaves it
245
+ `interrupted` and records the recovery error for inspection. Terminal managed
246
+ fibers such as `aborted` are not recovered again if a stale run row remains.
247
+ If a non-terminal managed ledger row loses its transient `cf_agents_runs` row
248
+ before completion, housekeeping also marks it `interrupted` and runs the same
249
+ recovery hook.
250
+
251
+ If recovery is triggered by a later duplicate webhook instead of
252
+ `onFiberRecovered()`, use `resolveFiber()` with the same result shape after your
253
+ application-level recovery succeeds. `resolveFiber()` only updates managed
254
+ fibers that are currently `interrupted`; it returns `false` for pending,
255
+ running, or already-terminal rows.
256
+
257
+ ### Webhook recipe
258
+
259
+ Use `startFiber()` around webhook side effects when the provider may retry the
260
+ same delivery and the visible work must happen once.
261
+
262
+ ```typescript
263
+ class WebhookAgent extends Agent {
264
+ async handleWebhook(event: WebhookEvent) {
265
+ const result = await this.startFiber(
266
+ "send-reply",
267
+ async (ctx) => {
268
+ ctx.stash({ eventId: event.id, target: event.replyTarget });
269
+ await sendReply(event.replyTarget, { signal: ctx.signal });
270
+ },
271
+ {
272
+ idempotencyKey: `webhook:${event.id}`,
273
+ metadata: { source: event.source },
274
+ waitForCompletion: true
275
+ }
276
+ );
277
+
278
+ if (result.status === "interrupted") {
279
+ await this.recoverWebhookReply(result);
280
+ await this.resolveFiber(result.fiberId, { status: "completed" });
281
+ }
282
+
283
+ return result;
284
+ }
285
+
286
+ async onFiberRecovered(ctx: FiberRecoveryContext) {
287
+ if (ctx.name !== "send-reply") return;
288
+
289
+ const snapshot = ctx.snapshot as {
290
+ eventId: string;
291
+ target: string;
292
+ } | null;
293
+
294
+ if (!snapshot) {
295
+ return {
296
+ status: "error",
297
+ error: "Missing webhook recovery snapshot"
298
+ };
299
+ }
300
+
301
+ await sendRecoveryMessage(snapshot.target);
302
+ return { status: "completed", snapshot };
303
+ }
304
+ }
305
+ ```
306
+
307
+ In this pattern:
308
+
309
+ - The provider's delivery ID becomes the `idempotencyKey`.
310
+ - `ctx.stash()` stores enough serializable data to recover without replaying the
311
+ original closure.
312
+ - `waitForCompletion: true` keeps the caller open until the retained job reaches
313
+ a terminal status, while duplicate deliveries still dedupe by key.
314
+ - `onFiberRecovered()` handles platform restarts and eviction.
315
+ - `resolveFiber()` is useful when a later duplicate delivery performs the
316
+ application-level recovery instead of the alarm hook.
317
+
318
+ ### Lifecycle
319
+
320
+ #### Normal execution
321
+
322
+ ```
323
+ runFiber("work", fn)
324
+ ├─ INSERT row into cf_agents_runs
325
+ ├─ keepAlive() — heartbeat starts
326
+ ├─ Execute fn(ctx)
327
+ │ ├─ ctx.stash(data) → UPDATE snapshot in SQLite
328
+ │ ├─ ctx.stash(data) → UPDATE snapshot in SQLite
329
+ │ └─ return result
330
+ ├─ DELETE row from cf_agents_runs
331
+ ├─ keepAlive dispose — heartbeat stops
332
+ └─ Return result to caller
333
+ ```
334
+
335
+ #### Eviction and recovery
336
+
337
+ ```
338
+ [DO evicted — all in-memory state lost]
339
+
340
+ On next activation:
341
+ ├─ Request/connection → onStart() → _checkRunFibers() [primary path]
342
+ │ OR
343
+ ├─ Persisted alarm fires → _onAlarmHousekeeping() [fallback path]
344
+
345
+ _checkRunFibers():
346
+ ├─ SELECT * FROM cf_agents_runs
347
+ ├─ For each orphaned row:
348
+ │ ├─ Parse snapshot from JSON
349
+ │ ├─ Call onFiberRecovered(ctx)
350
+ │ └─ DELETE the row after successful recovery
351
+ └─ If onFiberRecovered calls runFiber() again → new row, normal execution
352
+ ```
353
+
354
+ Both recovery paths call the same hook. The alarm path is critical for background agents that have no incoming client connections — the persisted alarm wakes the agent on its own.
355
+
356
+ #### Sub-agents
357
+
358
+ Fibers also work inside sub-agents. The fiber row and snapshots are stored in the sub-agent's own SQLite database, and `onFiberRecovered()` runs with the sub-agent as `this`.
359
+
360
+ Sub-agents do not have independent alarm slots, so the top-level parent owns the physical heartbeat. When a sub-agent starts a fiber, the parent stores a small root-side index entry for that facet and fiber ID. Root alarm housekeeping uses that index to route recovery checks back into the owning sub-agent, even if the child has no client connection or incoming RPC.
361
+
362
+ This keeps recovery local to the child while preserving the single physical alarm slot owned by the parent. A recovered continuation can use `schedule()` from inside the facet; the parent owns the physical alarm and routes the callback back to the child.
363
+
364
+ #### Error during execution
365
+
366
+ ```
367
+ fn(ctx) throws Error
368
+ ├─ DELETE row from cf_agents_runs
369
+ ├─ keepAlive dispose
370
+ └─ Error propagates to caller (or logged if fire-and-forget)
371
+ ```
372
+
373
+ No automatic retries. Recovery logic belongs in `onFiberRecovered`, where you have the snapshot and full context about what went wrong.
374
+
375
+ ### Inline vs fire-and-forget
376
+
377
+ `runFiber()` supports both patterns:
378
+
379
+ ```typescript
380
+ // Inline — await the result
381
+ const result = await this.runFiber("work", async (ctx) => {
382
+ return computeExpensiveThing();
383
+ });
384
+
385
+ // Fire-and-forget — caller does not wait
386
+ void this.runFiber("background", async (ctx) => {
387
+ await longRunningProcess();
388
+ });
389
+ ```
390
+
391
+ If the DO is evicted during an inline `await`, the caller is gone. On recovery, `onFiberRecovered` fires — it cannot return a result to the original caller. This is the inherent limitation of durable execution across process boundaries. For long-running work that is likely to outlive a single DO lifetime, fire-and-forget with checkpoint/recovery is the safer pattern.
392
+
393
+ ## Checkpoints with `stash()`
394
+
395
+ `ctx.stash(data)` writes to SQLite **synchronously**. There is no async gap between "I decided to save" and "it is saved." If eviction happens after `stash()` returns, the data is guaranteed to be in SQLite.
396
+
397
+ Each call **fully replaces** the previous snapshot — it is not a merge. Write the complete recovery state you need:
398
+
399
+ ```typescript
400
+ await this.runFiber("research", async (ctx) => {
401
+ const steps = ["search", "analyze", "synthesize"];
402
+ const completed: string[] = [];
403
+ const results: Record<string, unknown> = {};
404
+
405
+ for (const step of steps) {
406
+ results[step] = await executeStep(step);
407
+ completed.push(step);
408
+
409
+ ctx.stash({
410
+ completed,
411
+ results,
412
+ pendingSteps: steps.slice(completed.length)
413
+ });
414
+ }
415
+ });
416
+ ```
417
+
418
+ ### `this.stash()` vs `ctx.stash()`
419
+
420
+ Both do the same thing. `ctx.stash()` uses a direct closure over the fiber ID. `this.stash()` uses `AsyncLocalStorage` to find the currently executing fiber — it works correctly even with concurrent fibers, since each fiber's ALS context is independent.
421
+
422
+ `this.stash()` is convenient when calling from nested functions that do not have access to `ctx`. It throws if called outside a `runFiber` callback.
423
+
424
+ ## Recovery
425
+
426
+ Override `onFiberRecovered` to handle interrupted fibers. The default implementation logs a warning and deletes the row.
427
+
428
+ ```typescript
429
+ class ResearchAgent extends Agent {
430
+ async onFiberRecovered(ctx: FiberRecoveryContext) {
431
+ if (ctx.name !== "research") return;
432
+
433
+ const snapshot = ctx.snapshot as {
434
+ completed: string[];
435
+ results: Record<string, unknown>;
436
+ pendingSteps: string[];
437
+ } | null;
438
+
439
+ if (snapshot && snapshot.pendingSteps.length > 0) {
440
+ // Resume from where we left off
441
+ void this.runFiber("research", async (fiberCtx) => {
442
+ const { completed, results, pendingSteps } = snapshot;
443
+
444
+ for (const step of pendingSteps) {
445
+ results[step] = await this.executeStep(step);
446
+ completed.push(step);
447
+
448
+ fiberCtx.stash({
449
+ completed,
450
+ results,
451
+ pendingSteps: pendingSteps.slice(pendingSteps.indexOf(step) + 1)
452
+ });
453
+ }
454
+ });
455
+ }
456
+ }
457
+ }
458
+ ```
459
+
460
+ Key points:
461
+
462
+ - **The original lambda is gone.** On recovery, you only have the `name` and `snapshot`. The lambda cannot be serialized — recovery logic must be in the hook.
463
+ - **The row is deleted after the hook returns successfully.** If you want to continue the work, call `runFiber()` again inside the hook — this creates a new row.
464
+ - **You control what recovery means.** Retry from the beginning, resume from a checkpoint, skip and notify the user, or do nothing. The framework does not impose a strategy.
465
+ - **If the hook throws, the row is kept (up to a bound).** A later startup or alarm scan will try recovery again, which protects against transient storage or scheduling failures. Catch application-level errors yourself when you want to mark the work terminal instead of retrying. A hook that always throws is retried on a backing-off schedule (the recovery alarm uses an exponential delay capped at 5 minutes, so it is not a busy-loop) until the row exceeds `fiberRecoveryMaxAgeMs` (default 24h), after which it is discarded with a `fiber:recovery:skipped` (`reason: "max_age_exceeded"`) event. Setting `fiberRecoveryMaxAgeMs: 0` retains such rows indefinitely — recovery keeps retrying on the capped backoff, and the Durable Object never idle-evicts while an un-recoverable row exists, so prefer a finite age unless you intend to inspect or clear those rows yourself.
466
+
467
+ ### Chat recovery
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).
470
+
471
+ ## Concurrent fibers
472
+
473
+ Multiple fibers can run at the same time. Each has its own row in SQLite with its own snapshot, and each calls `keepAlive()` independently (ref-counted, so the DO stays alive until all fibers complete).
474
+
475
+ ```typescript
476
+ // Run two fibers concurrently
477
+ void this.runFiber("fetch-data", async (ctx) => {
478
+ /* ... */
479
+ });
480
+ void this.runFiber("process-queue", async (ctx) => {
481
+ /* ... */
482
+ });
483
+ ```
484
+
485
+ On recovery, `_checkRunFibers()` iterates all orphaned rows and calls `onFiberRecovered` for each. Use `ctx.name` to distinguish between fiber types in your recovery hook.
486
+
487
+ ## Testing locally
488
+
489
+ In `wrangler dev`, fiber recovery works identically to production. SQLite and alarm state persist to disk between restarts.
490
+
491
+ 1. Start your agent and trigger a fiber (`runFiber`)
492
+ 2. Kill the wrangler process (Ctrl-C or SIGKILL)
493
+ 3. Restart wrangler
494
+ 4. Recovery fires automatically — via `onStart()` if a request arrives, or via the persisted alarm if no clients connect
495
+
496
+ The E2E test in `packages/agents/src/e2e-tests/` validates this: it starts wrangler, spawns a fiber, kills the process with SIGKILL, restarts with the same persist directory, and verifies the fiber recovers automatically.
497
+
498
+ ## API reference
499
+
500
+ ### `runFiber(name, fn)`
501
+
502
+ Execute a durable fiber. The fiber is registered in SQLite before `fn` runs and deleted after it completes (or throws). `keepAlive()` is held for the duration.
503
+
504
+ - **`name`** — identifier for the fiber, used in `onFiberRecovered` to distinguish fiber types. Not unique — multiple fibers can share a name.
505
+ - **`fn`** — async function receiving a `FiberContext`. Closures work naturally (`this` and local variables are captured).
506
+ - **Returns** — the value returned by `fn`. If the DO is evicted before completion, the return value is lost; recovery happens through the hook.
507
+
508
+ ### `stash(data)` / `ctx.stash(data)`
509
+
510
+ Checkpoint the current fiber's state. Writes synchronously to SQLite. Each call fully replaces the previous snapshot. `data` must be JSON-serializable.
511
+
512
+ ### `onFiberRecovered(ctx)`
513
+
514
+ Called once per orphaned fiber row on agent restart. Override to implement recovery. The row is deleted after this hook returns successfully; if recovery throws, the row is left for a later scan so transient failures do not lose the recovery handle.
515
+
516
+ - **`ctx.id`** — unique fiber ID
517
+ - **`ctx.name`** — the name passed to `runFiber()`
518
+ - **`ctx.snapshot`** — the last `stash()` data, or `null` if `stash()` was never called
519
+ - **`ctx.createdAt`** — epoch milliseconds when `runFiber` started. Compare against `Date.now()` to gate stale recoveries (e.g. skip work that has been orphaned too long to replay safely)
520
+ - **`ctx.recoveryReason`** — why recovery is running. Currently always `"interrupted"` for eviction or restart recovery.
521
+
522
+ ### `keepAlive()`
523
+
524
+ Create a 30-second alarm heartbeat. Returns a disposer function. Idempotent — calling the disposer multiple times is safe.
525
+
526
+ ### `keepAliveWhile(fn)`
527
+
528
+ Run an async function while keeping the DO alive. Heartbeat starts before `fn` and stops when it completes or throws. Returns the value returned by `fn`.
529
+
530
+ ## Related
531
+
532
+ - [Long-Running Agents](./long-running-agents.md) — how fibers compose with schedules, plans, and async operations
533
+ - [Scheduling](./scheduling.md) — `keepAlive` details and the alarm system
534
+ - [Workflows](./workflows.md) — durable multi-step execution outside the agent
535
+ - [Chat Agents](./chat-agents.md) — `chatRecovery` and `onChatRecovery`
536
+ - [`forever-chat` example](https://github.com/cloudflare/agents/tree/main/experimental/forever-chat) — multi-provider LLM recovery demo
537
+ - [`forever.md` design doc](https://github.com/cloudflare/agents/tree/main/experimental/forever.md) — internal design details, tradeoffs, and architecture