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.
- package/dist/{agent-routing-B2XLNMxq.d.ts → agent-routing-0DR0Tk2v.d.ts} +205 -132
- package/dist/agent-routing.d.ts +1 -1
- package/dist/agent-routing.js.map +1 -1
- package/dist/agent-tool-types.d.ts +28 -22
- package/dist/agent-tool-types.js +23 -1
- package/dist/agent-tool-types.js.map +1 -1
- package/dist/{agent-tools-y7zLfw4Q.js → agent-tools-CdJC-8Dc.js} +72 -9
- package/dist/agent-tools-CdJC-8Dc.js.map +1 -0
- package/dist/{agent-tools-Dj5Mzlw7.d.ts → agent-tools-YrATZJu2.d.ts} +34 -20
- package/dist/agent-tools.d.ts +20 -18
- package/dist/agent-tools.js.map +1 -1
- package/dist/ai-fm7xx1BK.js +312 -0
- package/dist/ai-fm7xx1BK.js.map +1 -0
- package/dist/base64-redaction-C1Uu29t5.js +59 -0
- package/dist/base64-redaction-C1Uu29t5.js.map +1 -0
- package/dist/browser/ai-sdk.d.ts +90 -0
- package/dist/browser/ai-sdk.js +51 -0
- package/dist/browser/ai-sdk.js.map +1 -0
- package/dist/browser/ai.d.ts +4 -4
- package/dist/browser/ai.js +1 -347
- package/dist/browser/index.d.ts +81 -79
- package/dist/browser/index.js +361 -2
- package/dist/browser/index.js.map +1 -0
- package/dist/browser/tanstack-ai.d.ts +96 -1
- package/dist/browser/tanstack-ai.js +62 -2
- package/dist/browser/tanstack-ai.js.map +1 -1
- package/dist/browser-DE3d-RLk.d.ts +275 -0
- package/dist/browser-tool-CQ10e6Kj.js +480 -0
- package/dist/browser-tool-CQ10e6Kj.js.map +1 -0
- package/dist/browser-tool-DuNdlwZ9.d.ts +73 -0
- package/dist/capability-B4WbF81e.js.map +1 -1
- package/dist/{capability-runner-BUBa6Ake.d.ts → capability-runner-DBAnGr6L.d.ts} +7 -2
- package/dist/chat/index.d.ts +340 -234
- package/dist/chat/index.js +592 -66
- package/dist/chat/index.js.map +1 -1
- package/dist/chat/react.d.ts +42 -2
- package/dist/chat/react.js +191 -35
- package/dist/chat/react.js.map +1 -1
- package/dist/chat/transport.d.ts +1 -1
- package/dist/chat/transport.js +1 -1
- package/dist/chat-sdk/index.d.ts +17 -3
- package/dist/chat-sdk/index.js +28 -2
- package/dist/chat-sdk/index.js.map +1 -1
- package/dist/{client-C9DAjZIY.js → client-4OlMIJx-.js} +102 -29
- package/dist/client-4OlMIJx-.js.map +1 -0
- package/dist/{client-BxdkCuVy.d.ts → client-je3koD9Z.d.ts} +45 -10
- package/dist/client.d.ts +7 -5
- package/dist/client.js +2 -2
- package/dist/{connector-CptFKzRh.js → connector-58XDGvc3.js} +251 -120
- package/dist/connector-58XDGvc3.js.map +1 -0
- package/dist/{connector-CkQD4MK3.d.ts → connector-Cl-VqPmA.d.ts} +244 -146
- package/dist/context/index.d.ts +29 -0
- package/dist/context/index.js +64 -17
- package/dist/context/index.js.map +1 -1
- package/dist/current-agent-DhoDkSnH.js.map +1 -1
- package/dist/{current-agent-C4w86kcN.d.ts → current-agent-vAk_5JMt.d.ts} +4 -4
- package/dist/{durable-object-lifecycle-ei7M7lqB.d.ts → durable-object-lifecycle-HhgOMWq9.d.ts} +15 -7
- package/dist/harness/pi/index.d.ts +344 -0
- package/dist/harness/pi/index.js +873 -0
- package/dist/harness/pi/index.js.map +1 -0
- package/dist/images-iiTDSyRu.js +1631 -0
- package/dist/images-iiTDSyRu.js.map +1 -0
- package/dist/{index-Dlgtd3Mj.d.ts → index-BreL0YrQ.d.ts} +2 -2
- package/dist/index-kbSUIer3.d.ts +343 -0
- package/dist/index.d.ts +46 -44
- package/dist/index.js +1 -1
- package/dist/internal_context.d.ts +1 -1
- package/dist/lifecycle/index.d.ts +3 -3
- package/dist/lifecycle/index.js +1 -1
- package/dist/{lifecycle-Mm_jQh7r.js → lifecycle-CFu6OV03.js} +39 -10
- package/dist/{lifecycle-Mm_jQh7r.js.map → lifecycle-CFu6OV03.js.map} +1 -1
- package/dist/mcp/client/index.d.ts +20 -20
- package/dist/mcp/client/x402.js +21 -19
- package/dist/mcp/client/x402.js.map +1 -1
- package/dist/mcp/index.d.ts +35 -35
- package/dist/mcp/index.js +1 -1
- package/dist/{message-builder-BymO4N_D.js → message-builder-DAl9AhHH.js} +89 -4
- package/dist/message-builder-DAl9AhHH.js.map +1 -0
- package/dist/models/ai-sdk/index.d.ts +353 -0
- package/dist/models/ai-sdk/index.js +2443 -0
- package/dist/models/ai-sdk/index.js.map +1 -0
- package/dist/models/pi-ai/index.d.ts +313 -0
- package/dist/models/pi-ai/index.js +1795 -0
- package/dist/models/pi-ai/index.js.map +1 -0
- package/dist/observability/ai/index.js +20 -4
- package/dist/observability/ai/index.js.map +1 -1
- package/dist/{protocol-B0nh6KNf.js → protocol-kHsr-f0X.js} +26 -12
- package/dist/{protocol-B0nh6KNf.js.map → protocol-kHsr-f0X.js.map} +1 -1
- package/dist/queue/index.d.ts +3 -3
- package/dist/queue/index.js +1 -1
- package/dist/{queue-BC9F5cUo.js → queue-BB-zfIYF.js} +2 -2
- package/dist/{queue-BC9F5cUo.js.map → queue-BB-zfIYF.js.map} +1 -1
- package/dist/react.d.ts +6 -6
- package/dist/react.js +160 -64
- package/dist/react.js.map +1 -1
- package/dist/{retries-D9B2UCq3.d.ts → retries-CV6AkXmQ.d.ts} +18 -9
- package/dist/retries.d.ts +11 -9
- package/dist/retries.js +11 -1
- package/dist/retries.js.map +1 -1
- package/dist/routing/index.d.ts +2 -2
- package/dist/routing/index.js +1 -1
- package/dist/{scheduler-D_KHqxBa.js → scheduler-DZ3xQfeK.js} +2 -2
- package/dist/{scheduler-D_KHqxBa.js.map → scheduler-DZ3xQfeK.js.map} +1 -1
- package/dist/{scheduler-BGq6M5Kd.d.ts → scheduler-RdACNXDP.d.ts} +3 -3
- package/dist/schedules/index.d.ts +1 -1
- package/dist/schedules/index.js +1 -1
- package/dist/serializable.d.ts +1 -1
- package/dist/sessions/index.d.ts +70 -3
- package/dist/sessions/index.js +276 -67
- package/dist/sessions/index.js.map +1 -1
- package/dist/skills/index.d.ts +29 -314
- package/dist/socket-address.d.ts +16 -0
- package/dist/socket-address.js +19 -0
- package/dist/socket-address.js.map +1 -0
- package/dist/{src-BNU3ZiJM.js → src-BZeu-SCD.js} +423 -115
- package/dist/src-BZeu-SCD.js.map +1 -0
- package/dist/state/index.d.ts +1 -1
- package/dist/state/index.js +1 -1
- package/dist/streams/index.d.ts +1 -1
- package/dist/streams/index.js +1 -1
- package/dist/{streams-BcE8JP2X.d.ts → streams-Cmkj9QfW.d.ts} +4 -2
- package/dist/{streams-CCPRV6dt.js → streams-DW-UCOUc.js} +9 -2
- package/dist/streams-DW-UCOUc.js.map +1 -0
- package/dist/sub-routing.d.ts +12 -12
- package/dist/tasks/index.d.ts +1 -1
- package/dist/tasks/index.js +1 -1
- package/dist/{tasks-D4nLqVSI.js → tasks-68DESxf0.js} +2 -2
- package/dist/{tasks-D4nLqVSI.js.map → tasks-68DESxf0.js.map} +1 -1
- package/dist/{tasks-V2lCT8ZV.d.ts → tasks-9YxYtByh.d.ts} +2 -2
- package/dist/transport-PfFnt-Ld.d.ts +525 -0
- package/dist/{types-DCD6BNZH.d.ts → types-BA4anrjI.d.ts} +2 -2
- package/dist/voice/index.d.ts +2 -2
- package/dist/websockets/index.d.ts +23 -6
- package/dist/websockets/index.js +1 -1
- package/dist/{websockets-D7IzWbZb.js → websockets-Ci-LZFHH.js} +45 -20
- package/dist/{websockets-D7IzWbZb.js.map → websockets-Ci-LZFHH.js.map} +1 -1
- package/dist/wire-types-CnMt6_HR.js.map +1 -1
- package/dist/wire-types-CrjKvDyi.d.ts +300 -0
- package/dist/workflow-types.d.ts +25 -25
- package/dist/workflows.d.ts +21 -21
- package/dist/workflows.js +1 -1
- package/dist/{ws-chat-transport-UNRIS2xl.d.ts → ws-chat-transport-BbT1A5Lc.d.ts} +51 -2
- package/dist/{ws-chat-transport-rWwta645.js → ws-chat-transport-cqMKq3IY.js} +143 -16
- package/dist/ws-chat-transport-cqMKq3IY.js.map +1 -0
- package/docs/agent-tools.md +29 -4
- package/docs/browse-the-web.md +87 -5
- package/docs/chat-agents.md +124 -16
- package/docs/context.md +27 -0
- package/docs/durable-execution.md +2 -0
- package/docs/harnesses/pi.md +173 -0
- package/docs/human-in-the-loop.md +4 -0
- package/docs/index.md +3 -0
- package/docs/lifecycle.md +20 -4
- package/docs/models-pi-ai.md +325 -0
- package/docs/models.md +504 -0
- package/docs/resumable-streaming.md +2 -0
- package/docs/routing.md +13 -13
- package/docs/sessions.md +36 -1
- package/docs/state.md +1 -0
- package/docs/sub-agents.md +9 -0
- package/package.json +40 -2
- package/dist/agent-tools-y7zLfw4Q.js.map +0 -1
- package/dist/browser/ai.js.map +0 -1
- package/dist/client-C9DAjZIY.js.map +0 -1
- package/dist/client-tools-aIBO0Fk7.d.ts +0 -53
- package/dist/connector-CptFKzRh.js.map +0 -1
- package/dist/message-builder-BymO4N_D.js.map +0 -1
- package/dist/src-BNU3ZiJM.js.map +0 -1
- package/dist/streams-CCPRV6dt.js.map +0 -1
- package/dist/ws-chat-transport-rWwta645.js.map +0 -1
package/docs/chat-agents.md
CHANGED
|
@@ -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
|
|
667
|
-
// value to tune when a turn that
|
|
668
|
-
// is sealed.
|
|
669
|
-
maxRecoveryWork:
|
|
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` | `
|
|
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` |
|
|
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 (`
|
|
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,
|
|
345
|
-
`
|
|
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 `
|
|
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
|
|
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
|
|