agents 0.24.0 → 0.26.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 (170) hide show
  1. package/dist/{agent-routing-B2XLNMxq.d.ts → agent-routing-0DR0Tk2v.d.ts} +205 -132
  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 +28 -22
  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-Dj5Mzlw7.d.ts → agent-tools-YrATZJu2.d.ts} +34 -20
  10. package/dist/agent-tools.d.ts +20 -18
  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-B4WbF81e.js.map +1 -1
  32. package/dist/{capability-runner-BUBa6Ake.d.ts → capability-runner-DBAnGr6L.d.ts} +7 -2
  33. package/dist/chat/index.d.ts +340 -234
  34. package/dist/chat/index.js +592 -66
  35. package/dist/chat/index.js.map +1 -1
  36. package/dist/chat/react.d.ts +42 -2
  37. package/dist/chat/react.js +191 -35
  38. package/dist/chat/react.js.map +1 -1
  39. package/dist/chat/transport.d.ts +1 -1
  40. package/dist/chat/transport.js +1 -1
  41. package/dist/chat-sdk/index.d.ts +17 -3
  42. package/dist/chat-sdk/index.js +28 -2
  43. package/dist/chat-sdk/index.js.map +1 -1
  44. package/dist/{client-C9DAjZIY.js → client-4OlMIJx-.js} +102 -29
  45. package/dist/client-4OlMIJx-.js.map +1 -0
  46. package/dist/{client-BxdkCuVy.d.ts → client-je3koD9Z.d.ts} +45 -10
  47. package/dist/client.d.ts +7 -5
  48. package/dist/client.js +2 -2
  49. package/dist/{connector-CptFKzRh.js → connector-58XDGvc3.js} +251 -120
  50. package/dist/connector-58XDGvc3.js.map +1 -0
  51. package/dist/{connector-CkQD4MK3.d.ts → connector-Cl-VqPmA.d.ts} +244 -146
  52. package/dist/context/index.d.ts +29 -0
  53. package/dist/context/index.js +64 -17
  54. package/dist/context/index.js.map +1 -1
  55. package/dist/current-agent-DhoDkSnH.js.map +1 -1
  56. package/dist/{current-agent-C4w86kcN.d.ts → current-agent-vAk_5JMt.d.ts} +4 -4
  57. package/dist/{durable-object-lifecycle-ei7M7lqB.d.ts → durable-object-lifecycle-HhgOMWq9.d.ts} +15 -7
  58. package/dist/harness/pi/index.d.ts +344 -0
  59. package/dist/harness/pi/index.js +873 -0
  60. package/dist/harness/pi/index.js.map +1 -0
  61. package/dist/images-iiTDSyRu.js +1631 -0
  62. package/dist/images-iiTDSyRu.js.map +1 -0
  63. package/dist/{index-Dlgtd3Mj.d.ts → index-BreL0YrQ.d.ts} +2 -2
  64. package/dist/index-kbSUIer3.d.ts +343 -0
  65. package/dist/index.d.ts +46 -44
  66. package/dist/index.js +1 -1
  67. package/dist/internal_context.d.ts +1 -1
  68. package/dist/lifecycle/index.d.ts +3 -3
  69. package/dist/lifecycle/index.js +1 -1
  70. package/dist/{lifecycle-Mm_jQh7r.js → lifecycle-CFu6OV03.js} +39 -10
  71. package/dist/{lifecycle-Mm_jQh7r.js.map → lifecycle-CFu6OV03.js.map} +1 -1
  72. package/dist/mcp/client/index.d.ts +20 -20
  73. package/dist/mcp/client/x402.js +21 -19
  74. package/dist/mcp/client/x402.js.map +1 -1
  75. package/dist/mcp/index.d.ts +35 -35
  76. package/dist/mcp/index.js +1 -1
  77. package/dist/{message-builder-BymO4N_D.js → message-builder-DAl9AhHH.js} +89 -4
  78. package/dist/message-builder-DAl9AhHH.js.map +1 -0
  79. package/dist/models/ai-sdk/index.d.ts +353 -0
  80. package/dist/models/ai-sdk/index.js +2443 -0
  81. package/dist/models/ai-sdk/index.js.map +1 -0
  82. package/dist/models/pi-ai/index.d.ts +313 -0
  83. package/dist/models/pi-ai/index.js +1795 -0
  84. package/dist/models/pi-ai/index.js.map +1 -0
  85. package/dist/observability/ai/index.js +20 -4
  86. package/dist/observability/ai/index.js.map +1 -1
  87. package/dist/{protocol-B0nh6KNf.js → protocol-kHsr-f0X.js} +26 -12
  88. package/dist/{protocol-B0nh6KNf.js.map → protocol-kHsr-f0X.js.map} +1 -1
  89. package/dist/queue/index.d.ts +3 -3
  90. package/dist/queue/index.js +1 -1
  91. package/dist/{queue-BC9F5cUo.js → queue-BB-zfIYF.js} +2 -2
  92. package/dist/{queue-BC9F5cUo.js.map → queue-BB-zfIYF.js.map} +1 -1
  93. package/dist/react.d.ts +6 -6
  94. package/dist/react.js +160 -64
  95. package/dist/react.js.map +1 -1
  96. package/dist/{retries-D9B2UCq3.d.ts → retries-CV6AkXmQ.d.ts} +18 -9
  97. package/dist/retries.d.ts +11 -9
  98. package/dist/retries.js +11 -1
  99. package/dist/retries.js.map +1 -1
  100. package/dist/routing/index.d.ts +2 -2
  101. package/dist/routing/index.js +1 -1
  102. package/dist/{scheduler-D_KHqxBa.js → scheduler-DZ3xQfeK.js} +2 -2
  103. package/dist/{scheduler-D_KHqxBa.js.map → scheduler-DZ3xQfeK.js.map} +1 -1
  104. package/dist/{scheduler-BGq6M5Kd.d.ts → scheduler-RdACNXDP.d.ts} +3 -3
  105. package/dist/schedules/index.d.ts +1 -1
  106. package/dist/schedules/index.js +1 -1
  107. package/dist/serializable.d.ts +1 -1
  108. package/dist/sessions/index.d.ts +70 -3
  109. package/dist/sessions/index.js +276 -67
  110. package/dist/sessions/index.js.map +1 -1
  111. package/dist/skills/index.d.ts +29 -314
  112. package/dist/socket-address.d.ts +16 -0
  113. package/dist/socket-address.js +19 -0
  114. package/dist/socket-address.js.map +1 -0
  115. package/dist/{src-BNU3ZiJM.js → src-BZeu-SCD.js} +423 -115
  116. package/dist/src-BZeu-SCD.js.map +1 -0
  117. package/dist/state/index.d.ts +1 -1
  118. package/dist/state/index.js +1 -1
  119. package/dist/streams/index.d.ts +1 -1
  120. package/dist/streams/index.js +1 -1
  121. package/dist/{streams-BcE8JP2X.d.ts → streams-Cmkj9QfW.d.ts} +4 -2
  122. package/dist/{streams-CCPRV6dt.js → streams-DW-UCOUc.js} +9 -2
  123. package/dist/streams-DW-UCOUc.js.map +1 -0
  124. package/dist/sub-routing.d.ts +12 -12
  125. package/dist/tasks/index.d.ts +1 -1
  126. package/dist/tasks/index.js +1 -1
  127. package/dist/{tasks-D4nLqVSI.js → tasks-68DESxf0.js} +2 -2
  128. package/dist/{tasks-D4nLqVSI.js.map → tasks-68DESxf0.js.map} +1 -1
  129. package/dist/{tasks-V2lCT8ZV.d.ts → tasks-9YxYtByh.d.ts} +2 -2
  130. package/dist/transport-PfFnt-Ld.d.ts +525 -0
  131. package/dist/{types-DCD6BNZH.d.ts → types-BA4anrjI.d.ts} +2 -2
  132. package/dist/voice/index.d.ts +2 -2
  133. package/dist/websockets/index.d.ts +23 -6
  134. package/dist/websockets/index.js +1 -1
  135. package/dist/{websockets-D7IzWbZb.js → websockets-Ci-LZFHH.js} +45 -20
  136. package/dist/{websockets-D7IzWbZb.js.map → websockets-Ci-LZFHH.js.map} +1 -1
  137. package/dist/wire-types-CnMt6_HR.js.map +1 -1
  138. package/dist/wire-types-CrjKvDyi.d.ts +300 -0
  139. package/dist/workflow-types.d.ts +25 -25
  140. package/dist/workflows.d.ts +21 -21
  141. package/dist/workflows.js +1 -1
  142. package/dist/{ws-chat-transport-UNRIS2xl.d.ts → ws-chat-transport-BbT1A5Lc.d.ts} +51 -2
  143. package/dist/{ws-chat-transport-rWwta645.js → ws-chat-transport-cqMKq3IY.js} +143 -16
  144. package/dist/ws-chat-transport-cqMKq3IY.js.map +1 -0
  145. package/docs/agent-tools.md +29 -4
  146. package/docs/browse-the-web.md +87 -5
  147. package/docs/chat-agents.md +124 -16
  148. package/docs/context.md +27 -0
  149. package/docs/durable-execution.md +2 -0
  150. package/docs/harnesses/pi.md +173 -0
  151. package/docs/human-in-the-loop.md +4 -0
  152. package/docs/index.md +3 -0
  153. package/docs/lifecycle.md +20 -4
  154. package/docs/models-pi-ai.md +325 -0
  155. package/docs/models.md +504 -0
  156. package/docs/resumable-streaming.md +2 -0
  157. package/docs/routing.md +13 -13
  158. package/docs/sessions.md +36 -1
  159. package/docs/state.md +1 -0
  160. package/docs/sub-agents.md +9 -0
  161. package/package.json +40 -2
  162. package/dist/agent-tools-y7zLfw4Q.js.map +0 -1
  163. package/dist/browser/ai.js.map +0 -1
  164. package/dist/client-C9DAjZIY.js.map +0 -1
  165. package/dist/client-tools-aIBO0Fk7.d.ts +0 -53
  166. package/dist/connector-CptFKzRh.js.map +0 -1
  167. package/dist/message-builder-BymO4N_D.js.map +0 -1
  168. package/dist/src-BNU3ZiJM.js.map +0 -1
  169. package/dist/streams-CCPRV6dt.js.map +0 -1
  170. package/dist/ws-chat-transport-rWwta645.js.map +0 -1
@@ -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
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,173 @@
1
+ ---
2
+ title: Pi harness (Beta)
3
+ pcx_content_type: get-started
4
+ description: Host pi-durable sessions in a Durable Object with the beta PiHarness lifecycle capability. Connect a pi-ai model, add tools and skills, and keep sessions running across eviction.
5
+ ---
6
+
7
+ `PiHarness` hosts [pi-durable](https://github.com/earendil-works/pi/tree/main/packages/durable) in a Durable Object. It opens Pi over the object's SQLite database and connects Pi's sessions to the Agents SDK lifecycle, which wakes sessions with work after eviction. The API is in beta and may change.
8
+
9
+ Pi owns everything about a run: the transcript, the inbox of steers and follow-ups, generation, tool calls, retries, and crash recovery. `PiHarness` supplies the storage and the wake. It does not provide a chat protocol or user interface. Your application chooses how clients reach sessions, such as WebSockets, HTTP, or RPC.
10
+
11
+ ## Install the Pi packages
12
+
13
+ ```sh
14
+ npm install agents @earendil-works/pi-durable @earendil-works/pi-ai
15
+ ```
16
+
17
+ Both Pi packages are optional peer dependencies of `agents`, at `^1.0.0`. The examples use Workers AI through the `AI` binding. To configure the binding and choose other models, refer to [Models for pi-ai](../models-pi-ai.md).
18
+
19
+ ## Create the harness
20
+
21
+ `PiHarness` uses a factory function to construct Pi's `Harness` so it can be instantiated lazily:
22
+
23
+ ```ts
24
+ import { createModels } from "@earendil-works/pi-ai/models";
25
+ import { createRegistry, Harness } from "@earendil-works/pi-durable";
26
+ import { Agent } from "agents";
27
+ import { PiHarness } from "agents/harness/pi";
28
+ import { createAI } from "agents/models/pi-ai";
29
+
30
+ export class Assistant extends Agent<Env> {
31
+ ai = createAI({ binding: this.env.AI });
32
+ registry = createRegistry();
33
+
34
+ harness = new PiHarness({
35
+ harness: ({ storage, context }) => {
36
+ const models = createModels();
37
+ models.setProvider(this.ai.provider);
38
+ return Harness.open(
39
+ storage,
40
+ { models, registry: this.registry },
41
+ context
42
+ );
43
+ },
44
+ defaults: {
45
+ model: this.ai("@cf/zai-org/glm-4.7-flash"),
46
+ thinkingLevel: "low"
47
+ }
48
+ });
49
+
50
+ constructor(ctx: DurableObjectState, env: Env) {
51
+ super(ctx, env);
52
+ this.lifecycle.use(this.harness);
53
+ }
54
+
55
+ async ask(prompt: string) {
56
+ return (await this.harness.prompt(prompt)).text;
57
+ }
58
+ }
59
+ ```
60
+
61
+ | Option | What it is |
62
+ | ---------- | --------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
63
+ | `harness` | Required. Receives `{ storage, context }` and returns Pi's `Harness`, usually from `Harness.open`. Models, the registry, `settings`, `env` and `onReport` are all yours to build here. |
64
+ | `defaults` | What a new session starts with: `model`, a pi-ai `Model` such as `ai("@cf/…")`, and `thinkingLevel`. Without a model, a session's prompts end unanswered (`no_model`) until one is set. |
65
+
66
+ The factory runs as part of startup, once per isolate. If it throws the operation that triggered it fails and the next operation tries again.
67
+
68
+ A model in `defaults` or `session.setModel()` is stored by its provider and id only. Pi resolves it at each request against the `Models` the factory opened Pi with, so it must be a model those `Models` list. Options passed to `ai(id, options)` there, such as `fallback`, are not applied.
69
+
70
+ ## Add tools and prompt sections
71
+
72
+ Tools and system prompt sections are Pi extensions. An extension is a plain object with a `name`, `tools` and `sections`:
73
+
74
+ ```ts
75
+ import { Type } from "@earendil-works/pi-ai";
76
+ import {
77
+ createRegistry,
78
+ type ToolRegistration
79
+ } from "@earendil-works/pi-durable";
80
+
81
+ const WordCount = Type.Object({ text: Type.String() });
82
+
83
+ const wordCount: ToolRegistration<typeof WordCount> = {
84
+ name: "word_count",
85
+ description: "Count the words in a text.",
86
+ parameters: WordCount,
87
+ // Pi may run it again after an eviction interrupted it.
88
+ replay: "safe",
89
+ // `text` is typed from `parameters` and is validated by Pi.
90
+ async execute({ text }) {
91
+ const words = text.split(/\s+/).filter(Boolean).length;
92
+ return { content: [{ type: "text", text: String(words) }] };
93
+ }
94
+ };
95
+
96
+ const registry = createRegistry();
97
+ registry.install({
98
+ name: "writing",
99
+ sections: [
100
+ { key: "preamble", render: () => "You are an editor.", tag: false }
101
+ ],
102
+ tools: [wordCount]
103
+ });
104
+ ```
105
+
106
+ - **Tool arguments are typed from the schema.** Type a tool as `ToolRegistration<typeof Parameters>`; a bare `ToolRegistration` types its arguments as `unknown`.
107
+ - **Replay safety.** `replay: "safe"` lets Pi run a call again after an eviction cut it off. Otherwise the model gets an interrupted result instead of a second run. The default is `"unsafe"`.
108
+ - **What a running call can use.** `execute(args, api, context)` gets Pi's operations for the call: `api.output()` for running output, `api.details()` for a UI, and `api.memo()` for values that survive an eviction. `context.abortSignal` is aborted when the call is.
109
+ - **Sections.** A section's `render` runs before each request, with the conversation, its agent and offered tools, and committed reads. It is wrapped in `<key>` tags unless `tag` is `false`.
110
+
111
+ Pi's extensions can do more: hooks on model requests and tool calls (`hook(ToolTask, { beforeTool })`), durable tasks, wrapping another extension's tools, and choosing extensions per conversation. Refer to the [pi-durable README](https://github.com/earendil-works/pi/tree/main/packages/durable) for each. They all work on the registry `PiHarness` opens Pi with.
112
+
113
+ ### Skills
114
+
115
+ `skills(sources)` turns `agents/skills` sources (refer to [Agent Skills](../../think/index.md#agent-skills)) into a Pi extension named `agents.skills`. It has two tools, `activate_skill` and `read_skill_resource`, and a `skills` section that lists the skills. Install it in the factory, since it reads the sources:
116
+
117
+ ```ts
118
+ import { skills } from "agents/harness/pi";
119
+
120
+ harness: async ({ storage, context }) => {
121
+ this.registry.install(await skills(sources));
122
+ return Harness.open(storage, { models, registry: this.registry }, context);
123
+ };
124
+ ```
125
+
126
+ The existing `@cloudflare/think` skills work out-of-the-box.
127
+
128
+ ## Work with sessions
129
+
130
+ A session is a Pi conversation. Each has its own transcript, inbox, model and run, and they can run at the same time.
131
+
132
+ ```ts
133
+ // The root session.
134
+ const { text } = await this.harness.prompt("What is 47 × 19?");
135
+
136
+ // Another session, on a cheaper model.
137
+ const session = await this.harness.sessions.create();
138
+ await session.setModel(this.ai("@cf/zai-org/glm-4.7-flash"));
139
+
140
+ // Durable once it returns; the same operation id twice is one submission.
141
+ const receipt = await session.submit("Summarize the latest report", {
142
+ operationId: "report-summary-42"
143
+ });
144
+ const result = await session.wait(receipt.operationId);
145
+ ```
146
+
147
+ | Call | What it does |
148
+ | ------------------------------------------ | ---------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
149
+ | `prompt(input)` | Submits and waits: the answer, and the transcript after it. |
150
+ | `submit(input, { operationId, whenBusy })` | Returns once Pi has durably accepted the input, before the model runs. A busy session queues it as a follow-up; `whenBusy: "steer"` joins the running work after its current tool round. |
151
+ | `wait(operationId, signal?)` | The operation's result once Pi settles it. Aborting `signal` stops only the wait, not the work. |
152
+ | `steer(input)` | `submit` with `whenBusy: "steer"`. |
153
+ | `abort(operationId?)` | Withdraws a queued operation, or with no id, stops everything running in the session. |
154
+ | `pending()` | Operations Pi has not settled yet: queued, or part of the running work. |
155
+ | `messages()` | Pi's transcript entries since the newest reset. |
156
+ | `events()` | Pi's agent events: a `snapshot`, then one batch per commit. |
157
+ | `reset(handoff?)` | Starts a new context, optionally from a handoff note. |
158
+ | `setModel(model)` | Changes this session's model. |
159
+ | `busy()` | Whether a run is going. |
160
+
161
+ `harness.prompt()`, `submit()`, `wait()`, `abort()`, `messages()` and `pending()` act on the root session, or on `{ session }`. `harness.sessions` has `create()`, `fork(from)`, `get(id)` and `list()`, which includes sessions subagents created. `ROOT_SESSION` is the root's id.
162
+
163
+ `messages()` and `events()` are Pi's own records, not a UI-ready chat transcript. The example below folds them into one.
164
+
165
+ ## How recovery works
166
+
167
+ The harness schedules one lifecycle job per session with work. The job waits on Pi's tasks, refreshes itself as a heartbeat while they run, and completes when they settle. If the object is evicted or crashes, the job is still due, so its alarm restarts the object. Pi reopens its stored state and resumes the session's work: a replay-safe tool call runs again, and an unsafe one is reported to the model as interrupted.
168
+
169
+ Every operation waits for the object's startup, which runs the factory. A call over RPC that arrives first waits for it rather than opening Pi on its own.
170
+
171
+ ## Run the example
172
+
173
+ The [Pi harness example](https://github.com/cloudflare/agents/tree/main/examples/next/harnesses/pi) streams `session.events()` over WebSockets and folds Pi's entries into a browser transcript. Its tools come from a `Workspace` from `@cloudflare/computer`: files, `exec` for JavaScript in a Dynamic Worker, and git. It uses a Workers AI model.
@@ -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
@@ -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 (Beta)](./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 (Beta)](./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
package/docs/lifecycle.md CHANGED
@@ -341,13 +341,17 @@ The capability speaks the Agent protocol on every connection, so a plain
341
341
  Durable Object is reachable from `useAgent` and `AgentClient` exactly like an
342
342
  `Agent`:
343
343
 
344
- - On connect it sends the identity frame, which resolves the client's
345
- `ready` and `identified`, then the current state when `state` is set.
344
+ - On connect it sends the identity frame, then the current state when
345
+ `state` is set. The identity frame says when a state frame follows it,
346
+ so the client's `ready` and `identified` wait for that state, and a
347
+ caller awaiting `ready` never reads the default value.
346
348
  `protocol` controls this: `true` (default) for every connection, a
347
349
  function to decide per connection — `false` marks it no-protocol, so it
348
350
  gets no protocol text frames on connect or by broadcast but still sends
349
351
  and receives ordinary messages and callables — or `false` to have the
350
- host drive the sequence itself with `sendIdentity()` and `sendState()`.
352
+ host drive the sequence itself with `sendConnectFrames()`, or
353
+ `sendIdentity()` and `sendState()` separately (the client then resolves
354
+ `ready` on the identity alone).
351
355
  `Agent` passes `false`, because it must decide whether a connection
352
356
  belongs to a facet before any frame is sent.
353
357
  - `readonly` decides per connection whether state writes over the wire are
@@ -429,7 +433,19 @@ async runTask(): Promise<void> {
429
433
  }
430
434
  ```
431
435
 
432
- Agent's internal RPC entry points already enforce this boundary.
436
+ An `Agent` enforces this boundary for you. Its internal RPC entry points and
437
+ the public `async` methods your subclass exposes over RPC start the lifecycle
438
+ before they run, so a cold instance does not serve those calls against
439
+ uninitialized state. Synchronous methods are never deferred; if one reads state
440
+ that `onStart` sets up, make it `async` or call `await this.lifecycle.start()`
441
+ in it. Concurrent calls to `start()` share the same startup operation, and
442
+ `lifecycle.isStarted()` returns `true` only after capabilities and `onStart()`
443
+ have finished.
444
+
445
+ Startup resolves the Durable Object name. An async RPC to an Agent addressed
446
+ with `newUniqueId()` or `idFromString()` fails instead of running before
447
+ initialization. Address the Agent by name with `getAgentByName()`, `getByName()`,
448
+ or `idFromName()`.
433
449
 
434
450
  ## Object names
435
451