agents 0.23.0 → 0.25.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 (187) hide show
  1. package/dist/{agent-routing-DE5zmCQ8.d.ts → agent-routing-0DR0Tk2v.d.ts} +270 -190
  2. package/dist/agent-routing.d.ts +1 -1
  3. package/dist/agent-routing.js.map +1 -1
  4. package/dist/agent-tool-types.d.ts +13 -7
  5. package/dist/agent-tool-types.js +23 -1
  6. package/dist/agent-tool-types.js.map +1 -1
  7. package/dist/{agent-tools-y7zLfw4Q.js → agent-tools-CdJC-8Dc.js} +72 -9
  8. package/dist/agent-tools-CdJC-8Dc.js.map +1 -0
  9. package/dist/{agent-tools-DtXMTDGM.d.ts → agent-tools-YrATZJu2.d.ts} +29 -15
  10. package/dist/agent-tools.d.ts +6 -4
  11. package/dist/agent-tools.js.map +1 -1
  12. package/dist/ai-fm7xx1BK.js +312 -0
  13. package/dist/ai-fm7xx1BK.js.map +1 -0
  14. package/dist/base64-redaction-C1Uu29t5.js +59 -0
  15. package/dist/base64-redaction-C1Uu29t5.js.map +1 -0
  16. package/dist/browser/ai-sdk.d.ts +90 -0
  17. package/dist/browser/ai-sdk.js +51 -0
  18. package/dist/browser/ai-sdk.js.map +1 -0
  19. package/dist/browser/ai.d.ts +4 -4
  20. package/dist/browser/ai.js +1 -347
  21. package/dist/browser/index.d.ts +81 -79
  22. package/dist/browser/index.js +361 -2
  23. package/dist/browser/index.js.map +1 -0
  24. package/dist/browser/tanstack-ai.d.ts +96 -1
  25. package/dist/browser/tanstack-ai.js +62 -2
  26. package/dist/browser/tanstack-ai.js.map +1 -1
  27. package/dist/browser-DE3d-RLk.d.ts +275 -0
  28. package/dist/browser-tool-CQ10e6Kj.js +480 -0
  29. package/dist/browser-tool-CQ10e6Kj.js.map +1 -0
  30. package/dist/browser-tool-DuNdlwZ9.d.ts +73 -0
  31. package/dist/{capability-BjSKYpzg.js → capability-B4WbF81e.js} +2 -1
  32. package/dist/capability-B4WbF81e.js.map +1 -0
  33. package/dist/{capability-runner-Be_-PLR1.d.ts → capability-runner-DBAnGr6L.d.ts} +45 -15
  34. package/dist/chat/index.d.ts +341 -232
  35. package/dist/chat/index.js +602 -72
  36. package/dist/chat/index.js.map +1 -1
  37. package/dist/chat/react.d.ts +42 -2
  38. package/dist/chat/react.js +191 -35
  39. package/dist/chat/react.js.map +1 -1
  40. package/dist/chat/transport.d.ts +1 -1
  41. package/dist/chat/transport.js +1 -1
  42. package/dist/chat-sdk/index.d.ts +17 -3
  43. package/dist/chat-sdk/index.js +28 -2
  44. package/dist/chat-sdk/index.js.map +1 -1
  45. package/dist/client-4OlMIJx-.js +591 -0
  46. package/dist/client-4OlMIJx-.js.map +1 -0
  47. package/dist/{client-jagG8a9_.js → client-fyQrLGkD.js} +2 -2
  48. package/dist/{client-jagG8a9_.js.map → client-fyQrLGkD.js.map} +1 -1
  49. package/dist/client-je3koD9Z.d.ts +446 -0
  50. package/dist/client.d.ts +29 -283
  51. package/dist/client.js +2 -274
  52. package/dist/{connector-CptFKzRh.js → connector-58XDGvc3.js} +251 -120
  53. package/dist/connector-58XDGvc3.js.map +1 -0
  54. package/dist/{connector-CkQD4MK3.d.ts → connector-Cl-VqPmA.d.ts} +244 -146
  55. package/dist/context/index.d.ts +29 -0
  56. package/dist/context/index.js +64 -17
  57. package/dist/context/index.js.map +1 -1
  58. package/dist/current-agent-DhoDkSnH.js.map +1 -1
  59. package/dist/current-agent-vAk_5JMt.d.ts +93 -0
  60. package/dist/{current-agent-Da_C9a3b.d.ts → durable-object-lifecycle-HhgOMWq9.d.ts} +26 -127
  61. package/dist/harnesses/pi/index.d.ts +348 -0
  62. package/dist/harnesses/pi/index.js +754 -0
  63. package/dist/harnesses/pi/index.js.map +1 -0
  64. package/dist/images-iiTDSyRu.js +1631 -0
  65. package/dist/images-iiTDSyRu.js.map +1 -0
  66. package/dist/index-BreL0YrQ.d.ts +78 -0
  67. package/dist/index-kbSUIer3.d.ts +343 -0
  68. package/dist/index.d.ts +43 -41
  69. package/dist/index.js +2 -2
  70. package/dist/internal_context.d.ts +1 -1
  71. package/dist/lifecycle/index.d.ts +19 -17
  72. package/dist/lifecycle/index.js +2 -2
  73. package/dist/{lifecycle-CMRGjZdw.js → lifecycle-CFu6OV03.js} +73 -29
  74. package/dist/lifecycle-CFu6OV03.js.map +1 -0
  75. package/dist/mcp/client/index.d.ts +20 -20
  76. package/dist/mcp/client/index.js +1 -1
  77. package/dist/mcp/client/x402.js +21 -19
  78. package/dist/mcp/client/x402.js.map +1 -1
  79. package/dist/mcp/index.d.ts +35 -35
  80. package/dist/mcp/index.js +2 -2
  81. package/dist/{message-builder-BymO4N_D.js → message-builder-DAl9AhHH.js} +89 -4
  82. package/dist/message-builder-DAl9AhHH.js.map +1 -0
  83. package/dist/models/ai-sdk/index.d.ts +353 -0
  84. package/dist/models/ai-sdk/index.js +2443 -0
  85. package/dist/models/ai-sdk/index.js.map +1 -0
  86. package/dist/models/pi-ai/index.d.ts +313 -0
  87. package/dist/models/pi-ai/index.js +1795 -0
  88. package/dist/models/pi-ai/index.js.map +1 -0
  89. package/dist/observability/ai/index.js +20 -4
  90. package/dist/observability/ai/index.js.map +1 -1
  91. package/dist/{protocol-B0nh6KNf.js → protocol-kHsr-f0X.js} +26 -12
  92. package/dist/{protocol-B0nh6KNf.js.map → protocol-kHsr-f0X.js.map} +1 -1
  93. package/dist/queue/index.d.ts +123 -0
  94. package/dist/queue/index.js +2 -0
  95. package/dist/queue-BB-zfIYF.js +406 -0
  96. package/dist/queue-BB-zfIYF.js.map +1 -0
  97. package/dist/react.d.ts +31 -13
  98. package/dist/react.js +190 -71
  99. package/dist/react.js.map +1 -1
  100. package/dist/{retries-D9Ds-1lz.d.ts → retries-CV6AkXmQ.d.ts} +23 -8
  101. package/dist/retries.d.ts +12 -8
  102. package/dist/retries.js +19 -1
  103. package/dist/retries.js.map +1 -1
  104. package/dist/routing/index.d.ts +2 -2
  105. package/dist/routing/index.js +1 -1
  106. package/dist/{scheduler-DD9NdYbF.js → scheduler-DZ3xQfeK.js} +4 -11
  107. package/dist/scheduler-DZ3xQfeK.js.map +1 -0
  108. package/dist/{scheduler-Dwh85ZGl.d.ts → scheduler-RdACNXDP.d.ts} +6 -6
  109. package/dist/schedules/index.d.ts +1 -1
  110. package/dist/schedules/index.js +1 -1
  111. package/dist/serializable.d.ts +1 -1
  112. package/dist/sessions/index.d.ts +70 -3
  113. package/dist/sessions/index.js +276 -67
  114. package/dist/sessions/index.js.map +1 -1
  115. package/dist/skills/index.d.ts +29 -314
  116. package/dist/socket-address.d.ts +16 -0
  117. package/dist/socket-address.js +19 -0
  118. package/dist/socket-address.js.map +1 -0
  119. package/dist/{src-DlSHshb2.js → src-BZeu-SCD.js} +541 -500
  120. package/dist/src-BZeu-SCD.js.map +1 -0
  121. package/dist/state/index.d.ts +6 -0
  122. package/dist/state/index.js +137 -0
  123. package/dist/state/index.js.map +1 -0
  124. package/dist/streams/index.d.ts +1 -1
  125. package/dist/streams/index.js +1 -1
  126. package/dist/{streams-D6tJ0NN9.d.ts → streams-Cmkj9QfW.d.ts} +4 -2
  127. package/dist/{streams-DZKgAj9b.js → streams-DW-UCOUc.js} +9 -2
  128. package/dist/streams-DW-UCOUc.js.map +1 -0
  129. package/dist/sub-routing.d.ts +12 -12
  130. package/dist/tasks/index.d.ts +1 -1
  131. package/dist/tasks/index.js +1 -1
  132. package/dist/{tasks-ylZgBjhj.js → tasks-68DESxf0.js} +3 -3
  133. package/dist/{tasks-ylZgBjhj.js.map → tasks-68DESxf0.js.map} +1 -1
  134. package/dist/{tasks-BRJ5zgya.d.ts → tasks-9YxYtByh.d.ts} +5 -5
  135. package/dist/transport-PfFnt-Ld.d.ts +525 -0
  136. package/dist/transport-protocol-BxyngQ_R.js +25 -0
  137. package/dist/transport-protocol-BxyngQ_R.js.map +1 -0
  138. package/dist/transport-protocol-ZTew5Wso.d.ts +27 -0
  139. package/dist/types-BA4anrjI.d.ts +70 -0
  140. package/dist/voice/index.d.ts +2 -2
  141. package/dist/websockets/index.d.ts +195 -69
  142. package/dist/websockets/index.js +3 -2
  143. package/dist/websockets-Ci-LZFHH.js +1030 -0
  144. package/dist/websockets-Ci-LZFHH.js.map +1 -0
  145. package/dist/wire-types-CnMt6_HR.js.map +1 -1
  146. package/dist/wire-types-CrjKvDyi.d.ts +300 -0
  147. package/dist/workflow-types.d.ts +25 -25
  148. package/dist/workflows.d.ts +21 -21
  149. package/dist/workflows.js +1 -1
  150. package/dist/{ws-chat-transport-UNRIS2xl.d.ts → ws-chat-transport-BbT1A5Lc.d.ts} +51 -2
  151. package/dist/{ws-chat-transport-rWwta645.js → ws-chat-transport-cqMKq3IY.js} +143 -16
  152. package/dist/ws-chat-transport-cqMKq3IY.js.map +1 -0
  153. package/docs/agent-class.md +2 -2
  154. package/docs/agent-tools.md +29 -4
  155. package/docs/browse-the-web.md +87 -5
  156. package/docs/chat-agents.md +124 -16
  157. package/docs/client-sdk.md +30 -0
  158. package/docs/context.md +27 -0
  159. package/docs/durable-execution.md +2 -0
  160. package/docs/harnesses/pi.md +91 -0
  161. package/docs/human-in-the-loop.md +4 -0
  162. package/docs/index.md +4 -1
  163. package/docs/lifecycle.md +114 -23
  164. package/docs/models-pi-ai.md +325 -0
  165. package/docs/models.md +504 -0
  166. package/docs/queue.md +135 -241
  167. package/docs/resumable-streaming.md +2 -0
  168. package/docs/retries.md +1 -1
  169. package/docs/routing.md +13 -13
  170. package/docs/sessions.md +36 -1
  171. package/docs/state.md +1 -0
  172. package/docs/sub-agents.md +12 -3
  173. package/package.json +51 -3
  174. package/dist/agent-tools-y7zLfw4Q.js.map +0 -1
  175. package/dist/browser/ai.js.map +0 -1
  176. package/dist/capability-BjSKYpzg.js.map +0 -1
  177. package/dist/client-tools-aIBO0Fk7.d.ts +0 -53
  178. package/dist/client.js.map +0 -1
  179. package/dist/connector-CptFKzRh.js.map +0 -1
  180. package/dist/lifecycle-CMRGjZdw.js.map +0 -1
  181. package/dist/message-builder-BymO4N_D.js.map +0 -1
  182. package/dist/scheduler-DD9NdYbF.js.map +0 -1
  183. package/dist/src-DlSHshb2.js.map +0 -1
  184. package/dist/streams-DZKgAj9b.js.map +0 -1
  185. package/dist/websockets-DUfRHPRq.js +0 -502
  186. package/dist/websockets-DUfRHPRq.js.map +0 -1
  187. package/dist/ws-chat-transport-rWwta645.js.map +0 -1
@@ -105,7 +105,7 @@ async () => {
105
105
  };
106
106
  ```
107
107
 
108
- To discover protocol surface, the model calls `cdp.spec()` — the live, normalized CDP protocol description (domains with commands, events, and types) — or uses the runtime's built-in `codemode.search` / `codemode.describe`.
108
+ To discover protocol surface, the model calls `cdp.spec()` — the live, normalized CDP protocol description (domains with commands, events, and types, including each command's parameters and return values) — or uses the runtime's built-in `codemode.search` / `codemode.describe`.
109
109
 
110
110
  ## Use with an Agent
111
111
 
@@ -205,6 +205,57 @@ await connector.sweep(); // reclaim expired/stale sessions — call from a sched
205
205
  await runtime.expirePaused(); // reject stale never-approved pauses, freeing their sessions
206
206
  ```
207
207
 
208
+ ## Persistent browser
209
+
210
+ `browserTool` gives the model one browser that stays open between turns, so tabs, cookies, and logins carry over. You create the browser once on your agent and pass it in; the model never starts or closes it. This example uses a [Think](../think/index.md) agent, whose `getTools()` runs every turn:
211
+
212
+ ```ts
213
+ import { Think } from "@cloudflare/think";
214
+ import { Browser, browserRun } from "agents/browser";
215
+ import { browserTool } from "agents/browser/ai-sdk";
216
+
217
+ export class MyAgent extends Think<Env> {
218
+ browser = new Browser({ provider: browserRun(this.env.BROWSER) });
219
+
220
+ constructor(ctx: DurableObjectState, env: Env) {
221
+ super(ctx, env);
222
+ this.lifecycle.use(this.browser);
223
+ }
224
+
225
+ getModel() {
226
+ return "@cf/moonshotai/kimi-k2.7-code";
227
+ }
228
+
229
+ getTools() {
230
+ return {
231
+ browser: browserTool({ browser: this.browser, loader: this.env.LOADER })
232
+ };
233
+ }
234
+ }
235
+ ```
236
+
237
+ - The model writes CDP code as with `createBrowserTools`. `sessionId: "active"` points at the tab it's working in, and that tab is remembered across turns.
238
+ - Tabs the page opens itself come back as `newTabs` in the result.
239
+ - If the browser was lost (idle past `keepAliveMs`, closed, or crashed), the code still runs in a new browser and the result includes `restarted: true`.
240
+ - Call `this.browser.liveView()` to give a person a Live View link into the same browser, and `this.browser.close()` to shut it down.
241
+ - Set `keepAliveMs`, `recording`, and `guardrails` on `browserRun(binding, options)`. Use a different `name` for each extra browser on the same agent.
242
+
243
+ Only Chromium on a Browser Run binding is supported. It needs the same `LOADER` binding and `CodemodeRuntime` export as `createBrowserTools`.
244
+
245
+ For TanStack AI, import `browserTool` from `agents/browser/tanstack-ai` instead. It takes the same options plus an optional `name` (default `"browser"`) and returns a `ServerTool`:
246
+
247
+ ```ts
248
+ import { browserTool } from "agents/browser/tanstack-ai";
249
+
250
+ const stream = chat({
251
+ adapter,
252
+ tools: [browserTool({ browser: this.browser, loader: this.env.LOADER })],
253
+ messages
254
+ });
255
+ ```
256
+
257
+ The TanStack AI tool doesn't support screenshots yet. TanStack AI gives the host and the model the same output, so if the model returns a screenshot, the tool replaces it with a note saying it was left out, and the tool's instructions tell the model to read the page with `Runtime.evaluate` instead.
258
+
208
259
  ## Quick Actions (stateless browsing)
209
260
 
210
261
  `browser_execute` drives a full, stateful CDP session — the right tool for interactive, multi-step automation. But a lot of agent browsing is really one-shot: _read this page as Markdown_, _extract these fields_, _list the links_. For those, [Quick Actions](https://developers.cloudflare.com/browser-run/quick-actions/) are simpler, faster, and cheaper. They need only the `browser` binding — no Durable Object, Worker Loader, or sandbox — so they work from any Worker.
@@ -404,12 +455,12 @@ Either `browser` or `cdpUrl` must be provided. When both are set, `cdpUrl` takes
404
455
  For custom integrations, import the building blocks directly:
405
456
 
406
457
  ```ts
407
- import { BrowserConnector, CdpSession, connectUrl } from "agents/browser";
458
+ import { BrowserConnector, CdpConnection, connectUrl } from "agents/browser";
408
459
 
409
460
  // Connect to a custom CDP endpoint
410
- const session = await connectUrl("http://localhost:9222");
411
- const version = await session.send("Browser.getVersion");
412
- session.close();
461
+ const connection = await connectUrl("http://localhost:9222");
462
+ const version = await connection.send("Browser.getVersion");
463
+ connection.close();
413
464
 
414
465
  // Or plug the connector into your own codemode runtime
415
466
  const connector = new BrowserConnector(this.ctx, {
@@ -419,6 +470,37 @@ const connector = new BrowserConnector(this.ctx, {
419
470
  });
420
471
  ```
421
472
 
473
+ To load the normalized CDP protocol specification from Worker code, use `loadCdpSpec()`:
474
+
475
+ ```ts
476
+ import { loadCdpSpec } from "agents/browser";
477
+
478
+ const spec = await loadCdpSpec({
479
+ browser: this.env.BROWSER,
480
+ sessionId
481
+ });
482
+ ```
483
+
484
+ Pass a Browser Run binding in `browser` and the Browser Run session ID returned by `createBrowserSession()` in `sessionId`. If you omit `sessionId`, the helper creates a temporary session to fetch the specification, then deletes it. Pass an existing session ID to read the specification without creating another session. If you provide `cdpUrl`, the helper loads the specification from that URL instead; `cdpUrl` takes precedence over `browser` and `sessionId`.
485
+
486
+ You can also manage Browser Rendering sessions directly. `createBrowserSession` accepts [hostname guardrails](https://developers.cloudflare.com/browser-run/features/guardrails/) that restrict which domains the session may reach — fixed at launch for every connection to the session, including Live View (not supported with Kitesurf):
487
+
488
+ ```ts
489
+ import { createBrowserSession, connectBrowserSession } from "agents/browser";
490
+
491
+ const { sessionId } = await createBrowserSession(this.env.BROWSER, {
492
+ guardrails: { allowedDomains: ["example.com", "*.example.com"] }
493
+ });
494
+
495
+ // Connect (and reconnect) without deleting the session on close:
496
+ const session = await connectBrowserSession(this.env.BROWSER, sessionId, {
497
+ timeoutMs: 30_000,
498
+ onClose: () => {
499
+ /* the CDP socket closed; the session itself stays alive */
500
+ }
501
+ });
502
+ ```
503
+
422
504
  ## Local development
423
505
 
424
506
  Recent Wrangler releases support Browser Rendering in local development. `npx wrangler dev` provisions the browser automatically, so the same `browser: env.BROWSER` setup works locally and when deployed.
@@ -587,6 +587,8 @@ When a Durable Object is evicted mid-stream (code update, inactivity timeout, re
587
587
 
588
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`.
589
589
 
590
+ The same bounded recovery handles an `AIChatAgent` stream that fails while the agent stays up. If the response reader throws a platform transient error, such as `Network connection lost.`, the agent keeps the partial response and schedules a continuation instead of ending the turn with an error. If the error arrives before any response part, the agent re-runs the turn instead. The agent calls `onChatRecovery` first, so returning `{ continue: false }` ends the turn with the error. Each repeated transient error waits longer before the next attempt and counts against `maxAttempts`. Other reader errors still end the turn.
591
+
590
592
  Durable recovery is always enabled. Use `chatRecovery` only to tune its budgets and terminal behavior.
591
593
 
592
594
  > **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()`.
@@ -663,10 +665,10 @@ override chatRecovery = {
663
665
  // Primary stuck-turn bound. Resets on every progress-bearing attempt, so a
664
666
  // turn that keeps producing content survives unbounded interruption.
665
667
  noProgressTimeoutMs: 5 * 60 * 1000,
666
- // Runaway-loop guard. Defaults to a finite backstop (1000). Set a different
667
- // value to tune when a turn that keeps emitting content but never converges
668
- // is sealed.
669
- maxRecoveryWork: 200,
668
+ // Runaway-loop guard, counted in durable stream segments. Defaults to a
669
+ // finite backstop (10000). Set a different value to tune when a turn that
670
+ // keeps emitting content but never converges is sealed.
671
+ maxRecoveryWork: 2000,
670
672
  // Tight retry budget for a Durable Object memory-limit reset (the isolate
671
673
  // exceeded its 128 MB limit). Defaults to 3; an OOM usually re-OOMs on
672
674
  // re-run, so recovery seals it with `out_of_memory` after a few attempts.
@@ -693,23 +695,23 @@ override chatRecovery = {
693
695
  | `stableTimeoutMs` | `10_000` | How long a recovery attempt waits for the isolate to reach stable state before rescheduling. |
694
696
  | `terminalMessage` | generic message | The message shown to the user when recovery is given up on. |
695
697
  | `noProgressTimeoutMs` | `300_000` (5 min) | Primary stuck-turn bound: how long an incident may go without forward progress before it is sealed (`no_progress_timeout`). **Resets on every progress-bearing attempt**, so a turn that keeps producing content survives unbounded interruption. |
696
- | `maxRecoveryWork` | `1000` | Runaway-loop guard. Maximum produced content/tool units since the incident began before a still-progressing turn is sealed (`work_budget_exceeded`). A generous finite backstop so an agent that keeps emitting a little content but never converges (for example an isolate that runs out of memory mid-stream on every recovery) cannot loop forever. Work only accrues from the first interruption until the turn completes. Set a higher value, or `Infinity`, for a very long agentic turn that legitimately needs more. |
698
+ | `maxRecoveryWork` | `10000` | Runaway-loop guard. Maximum recovery work since the incident began before a still-progressing turn is sealed (`work_budget_exceeded`), counted in durable stream segments (see [Recovery work units](#recovery-work-units)). A generous finite backstop so an agent that keeps emitting a little content but never converges (for example an isolate that runs out of memory mid-stream on every recovery) cannot loop forever. Work only accrues from the first interruption until the turn completes. Set a higher value, or `Infinity`, for a very long agentic turn that legitimately needs more. |
697
699
  | `maxOomRetries` | `3` | Tight retry budget for the specific case of a Durable Object isolate exceeding its memory limit and being reset mid-turn. An OOM is usually deterministic (the turn's working set no longer fits in 128 MB) so re-running re-OOMs, but a single OOM can be a transient spike — so recovery retries this many times before sealing with `out_of_memory`. Counts only attempts that ended in an OOM (not total attempts), so a turn interrupted by deploys is unaffected. Set `0` to seal on the first OOM. Far tighter than `maxRecoveryWork` because an OOM is attributable and each re-run re-runs the model. |
698
700
  | `shouldKeepRecovering` | — | Caller policy consulted from the second recovery attempt onward (never on the first detection, never once a hard bound has sealed the incident). Return `false` to stop recovery. The hook point for a token/cost budget — `ctx.work` is a coarse segment count, not tokens, so track real spend yourself. |
699
701
  | `onExhausted` | — | Called once when recovery is given up on, before the terminal message is delivered. Inspect `ctx.reason` for why. |
700
702
 
701
703
  **`ChatRecoveryProgressContext`** (the `ctx` passed to `shouldKeepRecovering`):
702
704
 
703
- | Field | Type | Description |
704
- | ----------------------- | ----------------------- | ------------------------------------------------------------------------------------------------- |
705
- | `incidentId` | `string` | Stable ID for this recovery incident. |
706
- | `requestId` | `string` | Request ID for the current continuation (changes per chained continuation). |
707
- | `recoveryRootRequestId` | `string` | Stable ID for the whole continuation chain — the right key for per-incident budget tracking. |
708
- | `attempt` | `number` | Attempt number for this incident (2 or greater when this hook runs). |
709
- | `maxAttempts` | `number` | Configured attempt cap. |
710
- | `recoveryKind` | `"retry" \| "continue"` | Whether recovery retries an unanswered user turn or continues a partial assistant turn. |
711
- | `work` | `number` | Coarse, monotonic count of content/tool segments produced since the incident opened (not tokens). |
712
- | `ageMs` | `number` | Wall-clock ms since the incident's first interruption. |
705
+ | Field | Type | Description |
706
+ | ----------------------- | ----------------------- | -------------------------------------------------------------------------------------------- |
707
+ | `incidentId` | `string` | Stable ID for this recovery incident. |
708
+ | `requestId` | `string` | Request ID for the current continuation (changes per chained continuation). |
709
+ | `recoveryRootRequestId` | `string` | Stable ID for the whole continuation chain — the right key for per-incident budget tracking. |
710
+ | `attempt` | `number` | Attempt number for this incident (2 or greater when this hook runs). |
711
+ | `maxAttempts` | `number` | Configured attempt cap. |
712
+ | `recoveryKind` | `"retry" \| "continue"` | Whether recovery retries an unanswered user turn or continues a partial assistant turn. |
713
+ | `work` | `number` | Monotonic count of durable stream segments produced since the incident opened (not tokens). |
714
+ | `ageMs` | `number` | Wall-clock ms since the incident's first interruption. |
713
715
 
714
716
  A progressing turn survives unbounded interruption (for example a dense deploy window) as long as it keeps making forward progress and stays under the `maxRecoveryWork` backstop. Recovery is sealed only by one of these `ctx.reason` values:
715
717
 
@@ -720,7 +722,19 @@ A progressing turn survives unbounded interruption (for example a dense deploy w
720
722
  - `recovery_aborted` — your `shouldKeepRecovering` hook returned `false`.
721
723
  - `stable_timeout` — recovery attempts kept timing out waiting for stable state until the budget drained (extreme churn).
722
724
 
723
- > `maxRecoveryWork` defaults to a generous finite backstop (`1000`) rather than no cap, so a runaway turn cannot loop forever out of the box. The default is far above what a healthy interrupted turn produces; if you lower it, pick a cap well above a healthy turn's output, and for a precise budget prefer `shouldKeepRecovering` with real token/cost accounting. A very long agentic turn that legitimately produces a large amount of content under heavy interruption can raise the cap or set it to `Infinity` to restore fully-unbounded recovery.
725
+ > `maxRecoveryWork` defaults to a generous finite backstop (`10000`) rather than no cap, so a runaway turn cannot loop forever out of the box. The default is far above what a healthy interrupted turn produces; if you lower it, pick a cap well above a healthy turn's output, and for a precise budget prefer `shouldKeepRecovering` with real token/cost accounting. A very long agentic turn that legitimately produces a large amount of content under heavy interruption can raise the cap or set it to `Infinity` to restore fully-unbounded recovery.
726
+
727
+ #### Recovery work units
728
+
729
+ `maxRecoveryWork` and `ctx.work` count **durable stream segments**, not tokens, messages, or chunks. One unit is one of:
730
+
731
+ - a flushed segment of the turn's stream log, which holds roughly ten packed streaming chunks;
732
+ - a settled tool result, which is flushed as its own segment;
733
+ - an explicit progress credit for output forwarded from a sub-agent.
734
+
735
+ The unit is only meaningful relative to another `ctx.work` reading, so to choose a cap, log `ctx.work` from `shouldKeepRecovering` for a healthy interrupted turn and set `maxRecoveryWork` well above it.
736
+
737
+ > **Upgrading an explicit `maxRecoveryWork`?** Releases before `agents@0.23.0` (`@cloudflare/ai-chat@0.12.0`, `@cloudflare/think@0.18.0`) counted "credited chunks" (one per tool call, one per text or reasoning segment start, and one per five seconds of streamed deltas), and the default was `1000`. The unit changed to durable segments and the default moved to `10000`. An explicit value is not recalibrated automatically, and there is no fixed conversion factor between the two units: a delta-heavy turn produces far more segments than it used to produce credits, while a turn dominated by tool calls changes much less. Measure `ctx.work` for your own turns before carrying an old value forward.
724
738
 
725
739
  > **Out-of-memory crash loops have a last-resort backstop.** A severe memory-limit reset can bypass the recovery budgets above entirely — for example if the Durable Object out-of-memories while _loading its state on wake_, before recovery even evaluates, or if the budget's own bookkeeping writes also out-of-memory. Left unhandled, the platform auto-retries the alarm forever, re-running the doomed (billable) turn each cycle. The SDK guards this at the alarm boundary: after `maxAlarmMemoryLimitStrikes` (a base `Agent` static option, default `3`) consecutive alarms end in a memory-limit reset, it seals the interrupted turn with `out_of_memory` and stops the loop, emitting an `alarm:memory_limit_reset` observability event. This bounds the blast radius (and the bill); it does not shrink the working set — a turn whose context genuinely no longer fits in 128 MB needs a smaller transcript/fewer or smaller tool results.
726
740
 
@@ -885,6 +899,7 @@ function Chat() {
885
899
  | ----------------------------- | ----------------------------------------------- | -------- | ---------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
886
900
  | `agent` | `ReturnType<typeof useAgent>` | Required | Agent connection from `useAgent` |
887
901
  | `onToolCall` | `({ toolCall, addToolOutput }) => void` | — | Handle client-side tool execution |
902
+ | `onTurnEnd` | `(event: ChatTurnEndEvent) => void` | — | Called once for each chat request that ends, with its `messageIds` and `outcome`. See [Settle sends when a turn ends](#settle-sends-when-a-turn-ends) |
888
903
  | `autoContinueAfterToolResult` | `boolean` | `true` | Auto-continue conversation after client tool results and approvals |
889
904
  | `resume` | `boolean` | `true` | Enable automatic stream resumption on reconnect |
890
905
  | `cancelOnClientAbort` | `boolean` | `false` | Cancel the server turn when generic client stream abort/cleanup occurs. Explicit `stop()` always cancels the server turn |
@@ -909,6 +924,47 @@ function Chat() {
909
924
  | `isStreaming` | `boolean` | `true` when any stream is active (client or server-initiated) |
910
925
  | `isRecovering` | `boolean` | `true` while a durable turn is being recovered (interrupted and resuming). Distinct from `isStreaming` — a recovering turn is not producing tokens yet. Render a "recovering…" hint; most UIs treat `isStreaming \|\| isRecovering` as "busy" |
911
926
 
927
+ ### Settle sends when a turn ends
928
+
929
+ An application that renders a send as pending before the server answers needs
930
+ to know when that exact send is finished. `onTurnEnd` is called once for each
931
+ chat request that ends, with the ids of the user messages it carried and how it
932
+ ended:
933
+
934
+ ```tsx
935
+ const chat = useAgentChat({
936
+ agent,
937
+ onTurnEnd: ({ messageIds, outcome, error }) => {
938
+ if (!messageIds) return;
939
+ for (const id of messageIds) {
940
+ settlePendingSend(id, outcome, error);
941
+ }
942
+ }
943
+ });
944
+ ```
945
+
946
+ The event has these fields:
947
+
948
+ - `requestId`: the transport id of the request.
949
+ - `messageIds`: the ids of the user messages the request ended with. These are
950
+ the ids the client minted when it sent them. Absent when the request did not
951
+ end with a user message.
952
+ - `outcome`: `"completed"`, `"error"`, `"aborted"` (cancelled while it ran), or
953
+ `"skipped"` (it never ran, because a newer send superseded it or the
954
+ `messageConcurrency` policy dropped it).
955
+ - `error`: the error message, when `outcome` is `"error"`.
956
+ - `replay`: `true` when the outcome was replayed on reconnect rather than seen
957
+ live.
958
+
959
+ `onTurnEnd` also fires for requests from other tabs and for outcomes replayed
960
+ after a reconnect, so match `messageIds` against the sends this client is
961
+ tracking. It does not fire when recovery continues a turn under a new request;
962
+ the new request reports the same `messageIds` when it ends.
963
+
964
+ A message id can appear in more than one event. With the `latest` or `merge`
965
+ policies, a skipped send can still be answered by the turn that superseded it,
966
+ which lists that id again. Treat the last event for an id as its outcome.
967
+
912
968
  ## Tools
913
969
 
914
970
  `AIChatAgent` supports three tool patterns, all using the AI SDK's `tool()` function:
@@ -989,6 +1045,8 @@ const { messages, sendMessage } = useAgentChat({
989
1045
 
990
1046
  When the LLM invokes `getLocation`, the stream pauses. The `onToolCall` callback fires, your code provides the output, and the conversation continues.
991
1047
 
1048
+ `onToolCall` fires once the response stream ends, for each tool call that is still waiting for a result. A server tool runs and resolves in the same stream, so it never reaches `onToolCall`.
1049
+
992
1050
  ### Dynamic Client Tools (SDK/Platform Pattern)
993
1051
 
994
1052
  For SDKs and platforms where tools are defined dynamically by the embedding application at runtime, use the `tools` option on `useAgentChat` and `createToolsFromClientSchemas()` on the server:
@@ -1300,6 +1358,18 @@ For more details, see [Resumable Streaming](./resumable-streaming.md).
1300
1358
 
1301
1359
  ## Storage Management
1302
1360
 
1361
+ ### Reconcile tool calls across transcript updates
1362
+
1363
+ When a client resubmits a transcript, `AIChatAgent` reconciles assistant messages
1364
+ against server state before persisting them. This keeps tool results attached to
1365
+ the right turn when a provider reuses a `toolCallId`.
1366
+
1367
+ Custom chat hosts that reconcile transcripts can use `reconcileMessages()` from
1368
+ `agents/chat`. It matches messages against server rows one-to-one and checks the
1369
+ tool name and input as well as the call ID. The older
1370
+ `resolveToolMergeId()` helper is deprecated because its conversation-wide
1371
+ lookup can match the wrong turn when call IDs repeat.
1372
+
1303
1373
  ### Row Size Protection
1304
1374
 
1305
1375
  SQLite rows have a maximum size of 2 MB. When a message approaches this limit (for example, a tool returning a very large output), `AIChatAgent` automatically compacts the message:
@@ -1673,6 +1743,44 @@ The chat protocol uses typed JSON messages over WebSocket:
1673
1743
  | `CF_AGENT_STREAM_RESUMING` | Server → Client | Notify of stream resumption |
1674
1744
  | `CF_AGENT_STREAM_RESUME_REQUEST` | Client → Server | Request stream resume check |
1675
1745
 
1746
+ #### Correlate terminal frames with sent messages
1747
+
1748
+ A `CF_AGENT_USE_CHAT_RESPONSE` frame names its request by `id`. Terminal frames
1749
+ (`done: true` or `error: true`) also carry `messageIds`: the ids of the user
1750
+ messages at the end of the request's `messages`. One send carries one id, and
1751
+ queued sends that arrive together carry several. This applies to completion,
1752
+ errors before or during the stream, skipped and cancelled requests, and
1753
+ terminals replayed on reconnect.
1754
+
1755
+ The final `done` frame of a request can also carry `outcome`: `"completed"`,
1756
+ `"error"`, `"aborted"`, `"skipped"`, or `"recovering"`. When it is absent, the
1757
+ request errored if that frame or an earlier frame for the request carried
1758
+ `error: true`, and completed otherwise. `"recovering"` means the request
1759
+ stopped but recovery continues the same turn under a new request, which reports
1760
+ the same `messageIds` when it ends.
1761
+
1762
+ React applications should use [`onTurnEnd`](#settle-sends-when-a-turn-ends),
1763
+ which applies these rules for you. Without React, read the frames directly:
1764
+
1765
+ ```ts
1766
+ const erroredRequests = new Set<string>();
1767
+
1768
+ agent.addEventListener("message", (event) => {
1769
+ const frame = JSON.parse(event.data);
1770
+ if (frame.type !== "cf_agent_use_chat_response") return;
1771
+ if (frame.error) erroredRequests.add(frame.id);
1772
+ if (!frame.done) return;
1773
+ const errored = erroredRequests.delete(frame.id);
1774
+ const outcome = frame.outcome ?? (errored ? "error" : "completed");
1775
+ if (outcome !== "recovering" && frame.messageIds) {
1776
+ settlePendingSends(frame.messageIds, outcome);
1777
+ }
1778
+ });
1779
+ ```
1780
+
1781
+ `messageIds` is absent when the request did not end with a user message, or
1782
+ when the server no longer has a record of the request.
1783
+
1676
1784
  ## Examples
1677
1785
 
1678
1786
  - [AI Chat Example](https://github.com/cloudflare/agents/tree/main/examples/ai-chat) — Modern example with server tools, client tools, and approval
@@ -120,6 +120,35 @@ useAgent({
120
120
  });
121
121
  ```
122
122
 
123
+ ### Transport
124
+
125
+ `useAgent` speaks the Agent protocol over one of two wires. The default,
126
+ `"cf-websocket"`, is a hibernating WebSocket managed by PartySocket. The
127
+ experimental `"capnweb"` transport carries protocol frames through a single
128
+ [Cap'n Web](https://github.com/cloudflare/capnweb) RPC session whose root
129
+ also serves the host's callables natively:
130
+
131
+ ```typescript
132
+ useAgent({
133
+ agent: "ChatAgent",
134
+ name: "room-123",
135
+ transport: "capnweb" // default: "cf-websocket"
136
+ });
137
+ ```
138
+
139
+ Identity, state sync, reconnection with backoff, and terminal close codes are
140
+ identical on both wires; PartySocket still owns the connection and the
141
+ transport only swaps the socket class it instantiates. `call()` and `stub`
142
+ differ by design. On `cf-websocket` they send JSON `rpc` frames. On `capnweb`
143
+ they invoke the host's `callables` natively on the Cap'n Web session root: a
144
+ method that returns an `RpcTarget` hands you a live stub you can keep
145
+ calling, a `ReadableStream` result streams, and chained calls pipeline.
146
+ `@callable()` decorators on an `Agent` are a JSON-wire feature and are not
147
+ available on `capnweb` unless the Agent also passes a `callables` target.
148
+ The trade-off is that a Cap'n Web connection does not hibernate: the Durable
149
+ Object stays in memory while one is open. `AgentClient` accepts the same
150
+ `transport` option.
151
+
123
152
  ### Async Query Parameters
124
153
 
125
154
  For authentication tokens or other async data, pass a function that returns a Promise:
@@ -351,6 +380,7 @@ type UseAgentOptions<State> = {
351
380
  name?: string; // Instance name (default: "default")
352
381
  host?: string; // Custom host
353
382
  path?: string; // Custom path prefix
383
+ transport?: "cf-websocket" | "capnweb"; // Wire (default: "cf-websocket")
354
384
 
355
385
  // Query parameters
356
386
  query?:
package/docs/context.md CHANGED
@@ -96,6 +96,31 @@ const system = await context.freezeSystemPrompt();
96
96
 
97
97
  `refreshSystemPrompt()` reloads every provider, re-renders, and overwrites the stored prompt.
98
98
 
99
+ ### Reminders for changing blocks
100
+
101
+ Some blocks go stale while the prompt is frozen: the current date, workspace state, project instructions. Refreshing the prompt keeps them current but throws away the cached prefix. Mark such a block `whenChanged: "remind"` to keep both:
102
+
103
+ ```ts
104
+ const context = new ContextBlocks([
105
+ {
106
+ label: "environment",
107
+ provider: {
108
+ get: async () => `Today is ${new Date().toISOString().slice(0, 10)}.`
109
+ },
110
+ whenChanged: "remind"
111
+ }
112
+ ]);
113
+
114
+ const system = await context.freezeSystemPrompt();
115
+ const reminder = await context.reminder(); // null until the date changes
116
+ ```
117
+
118
+ `reminder()` re-reads each such block and, for every one whose current rendering no longer appears in the frozen prompt, returns its current value under a short note saying it replaces the prompt's copy. Send that text after the cached prefix, for example at the end of the latest user message, and do not persist it. The frozen prompt does not change, and the reminder keeps coming back on every call until `refreshSystemPrompt()` promotes the value into the prompt. That refresh is the one deliberate cache bust.
119
+
120
+ The comparison is against the stored prompt, not against in-memory state, so a reminder survives a restart. A remind block always has a section in the prompt, even when it is empty, so the model knows it exists and a later value has a copy to replace. Any difference in what the provider returns counts as a change, so return only what should count: a date rather than a timestamp.
121
+
122
+ A standing reminder still costs a little cache: it moves to each new user message, so the previous user message falls out of the cached prefix. The system prompt and everything before that message stay cached.
123
+
99
124
  ## Tools
100
125
 
101
126
  `tools()` returns an AI SDK `ToolSet` wired from what the blocks can do:
@@ -126,6 +151,8 @@ A block declared without a provider is auto-wired to durable per-agent SQLite. T
126
151
 
127
152
  The assembled blocks are available as `this.context` after `onStart()`.
128
153
 
154
+ Think sends reminders itself. Each turn it calls `reminder()` and adds the result to the last user message of the model request, or, when the request continues a partial reply, to the user message that asks the model to continue (`TurnContext.messages` in `beforeTurn` already includes it). Nothing is written to the transcript. Think refreshes the prompt after a compaction, which promotes any pending values.
155
+
129
156
  ## Related
130
157
 
131
158
  - [Sessions](./sessions.md) - durable message trees, streamed history, compaction, and attachment offload
@@ -345,6 +345,7 @@ runFiber("work", fn)
345
345
  _checkRunFibers():
346
346
  ├─ SELECT * FROM cf_agents_runs
347
347
  ├─ For each orphaned row:
348
+ │ ├─ Body already finished (only its cleanup failed) → DELETE, no hook
348
349
  │ ├─ Parse snapshot from JSON
349
350
  │ ├─ Call onFiberRecovered(ctx)
350
351
  │ └─ DELETE the row after successful recovery
@@ -461,6 +462,7 @@ Key points:
461
462
 
462
463
  - **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
464
  - **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.
465
+ - **The hook is meant for interrupted work.** If the function returned or threw and only the row delete failed, the fiber records that it finished, and the next scan deletes the row without calling the hook. This is best-effort: if recording the finish also fails, the row looks interrupted and the hook runs anyway, so keep recovery idempotent.
464
466
  - **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
467
  - **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
468
 
@@ -0,0 +1,91 @@
1
+ ---
2
+ title: Pi harness (Experimental)
3
+ pcx_content_type: get-started
4
+ description: Host pi-durable sessions in a Durable Object with the experimental PiHarness lifecycle capability. Connect a pi-ai model and keep sessions active across eviction.
5
+ ---
6
+
7
+ `PiHarness` hosts [pi-durable](https://github.com/earendil-works/pi/tree/main/packages/durable) in a Durable Object. It connects pi's SQLite-backed sessions to the Agents SDK lifecycle, which wakes active sessions after eviction. The API is experimental and may change.
8
+
9
+ `PiHarness` does not provide a chat protocol or user interface. Your application chooses how clients reach sessions, such as through WebSockets, HTTP, or RPC. The harness exposes pi sessions and their event streams for your application to adapt.
10
+
11
+ ## Install the pi packages
12
+
13
+ Install `agents` with pi-durable and pi-ai:
14
+
15
+ ```sh
16
+ npm install agents @earendil-works/pi-durable @earendil-works/pi-ai
17
+ ```
18
+
19
+ Both pi packages are optional peer dependencies of `agents`. The example below uses Workers AI through the `AI` binding. To configure the binding and choose other models, refer to [Models for pi-ai](../models-pi-ai.md).
20
+
21
+ ## Create the harness
22
+
23
+ Open pi's `Harness` with the storage and background context that `PiHarness` provides. Register the pi-ai provider on a `Models` registry, then attach the capability to the object's lifecycle:
24
+
25
+ ```ts
26
+ import { DurableObject } from "cloudflare:workers";
27
+ import { createModels } from "@earendil-works/pi-ai/models";
28
+ import { createRegistry, Harness } from "@earendil-works/pi-durable";
29
+ import { PiHarness } from "agents/harnesses/pi";
30
+ import { Lifecycle } from "agents/lifecycle";
31
+ import { CLOUDFLARE_PROVIDER_ID, createAI } from "agents/models/pi-ai";
32
+
33
+ export class Assistant extends DurableObject<Env> {
34
+ readonly ai = createAI({ binding: this.env.AI });
35
+ readonly registry = createRegistry();
36
+
37
+ readonly harness = new PiHarness({
38
+ harness: ({ storage, context }) => {
39
+ const models = createModels();
40
+ models.setProvider(this.ai.provider);
41
+ return Harness.open(
42
+ storage,
43
+ { models, registry: this.registry },
44
+ context
45
+ );
46
+ },
47
+ defaults: {
48
+ model: {
49
+ provider: CLOUDFLARE_PROVIDER_ID,
50
+ modelId: "@cf/zai-org/glm-4.7-flash"
51
+ }
52
+ }
53
+ });
54
+
55
+ readonly lifecycle = Lifecycle.install(this).use(this.harness);
56
+
57
+ async prompt(input: string) {
58
+ return this.harness.prompt(input);
59
+ }
60
+ }
61
+ ```
62
+
63
+ The `harness` factory receives a pi storage adapter backed by the Durable Object's SQLite database. It also receives a background context for opening pi. Configure pi's models, registry, and other `Harness.open` options in this factory.
64
+
65
+ `defaults` applies when the harness creates a new session. You can change an individual session's model with `session.setModel()`. Without a model, the session cannot produce an answer until one is set.
66
+
67
+ ## Work with sessions
68
+
69
+ The root session is available through `harness.prompt()` and `harness.submit()`. Create additional sessions with `harness.sessions.create()`, then use the returned session handle:
70
+
71
+ ```ts
72
+ const session = await this.harness.sessions.create();
73
+ const receipt = await session.submit("Summarize the latest report", {
74
+ operationId: "report-summary-42"
75
+ });
76
+ const result = await session.wait(receipt.operationId);
77
+ ```
78
+
79
+ `submit()` returns after pi durably accepts the operation. `wait()` returns its result after pi finishes it. Reusing an `operationId` makes a submission idempotent. Set `whenBusy: "steer"` to join the running work after its current tool round; the default queues a follow-up.
80
+
81
+ Use `session.events()` to read pi's snapshot and committed event batches. Your application can stream those events to clients and build its own view. `session.messages()` returns pi's transcript entries, not a UI-ready chat transcript.
82
+
83
+ ## How recovery works
84
+
85
+ The harness schedules one lifecycle job for each session with active work. The job waits for pi's tasks, refreshes its wake while they remain active, and completes when they settle. If the Durable Object is evicted, the due job wakes it again; pi reopens its stored state and resumes the session's work.
86
+
87
+ Pi owns the transcript, operation inbox, generation, tools, retries, and recovery. `PiHarness` supplies durable storage and wake scheduling. It does not move pi's orchestration into the Agents SDK.
88
+
89
+ ## Run the example
90
+
91
+ The [Pi harness example](https://github.com/cloudflare/agents/tree/main/examples/next/harnesses/pi) connects `session.events()` to WebSockets and projects pi's entries into a browser transcript. It also demonstrates a replay-safe tool and model configuration with Workers AI.
@@ -361,6 +361,10 @@ A Durable Object can be evicted at any time (a deploy, an inactivity timeout, a
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
 
364
+ ### Repair an approved tool that never ran
365
+
366
+ An approval can be recorded even if its tool continuation never starts. When a later user turn moves past that approval, `AIChatAgent` and Think repair the transcript so the model does not receive an unresolved tool call. An approved tool that never ran becomes an error result, and a denied tool becomes a denied result. An approval in the current final message is left for the AI SDK to execute.
367
+
364
368
  ## Client-Side Tool Execution with `onToolCall`
365
369
 
366
370
  For tools that need browser APIs (geolocation, camera, clipboard) or user interaction, define the tool on the server without an `execute` function and handle it on the client with `onToolCall`:
package/docs/index.md CHANGED
@@ -63,7 +63,7 @@ The differentiator is not "we have durable state" — it is what happens when a
63
63
 
64
64
  ## Background Processing
65
65
 
66
- - [Queue](./queue.md) - Immediate background task execution
66
+ - [Queue](./queue.md) - Durable background task execution
67
67
  - [Scheduling](./scheduling.md) - Delayed, scheduled, and cron-based tasks
68
68
  - [Retries](./retries.md) - Automatic retries with exponential backoff and jitter
69
69
  - [Durable Execution](./durable-execution.md) - `runFiber()`, `startFiber()`, `stash()`, and crash recovery for long tasks
@@ -79,6 +79,9 @@ The differentiator is not "we have durable state" — it is what happens when a
79
79
  - [Agent Tools](./agent-tools.md) - Run chat-capable sub-agents as tools with streaming child timelines
80
80
  - [Server-Driven Messages](./server-driven-messages.md) - Autonomous agent workflows: scheduled follow-ups, queue processing, webhooks, chained reasoning
81
81
  - TODO: [Using AI Models](./using-ai-models.md) - OpenAI, Anthropic, Workers AI, and other providers
82
+ - [Models (Experimental)](./models.md) - `createAI` — one AI SDK provider for Workers AI and third-party catalog models, same string space
83
+ - [Models for pi-ai (Experimental)](./models-pi-ai.md) - `createAI` for pi-ai: Workers AI ids and third-party models through AI Gateway, as a pi-ai provider
84
+ - [Pi harness (Experimental)](./harnesses/pi.md) - Host pi-durable sessions in a Durable Object with durable storage and lifecycle wakeups
82
85
  - TODO: [RAG (Retrieval Augmented Generation)](./rag.md) - Vector search with Vectorize
83
86
  - [Sessions (Experimental)](./sessions.md) - Durable message trees, streamed history, compaction, search, and lossless attachment offload
84
87
  - [Context (Experimental)](./context.md) - System-prompt blocks, frozen prompts, writable/searchable/loadable providers, and their tools