agents 0.23.0 → 0.25.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/dist/{agent-routing-DE5zmCQ8.d.ts → agent-routing-0DR0Tk2v.d.ts} +270 -190
- package/dist/agent-routing.d.ts +1 -1
- package/dist/agent-routing.js.map +1 -1
- package/dist/agent-tool-types.d.ts +13 -7
- 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-DtXMTDGM.d.ts → agent-tools-YrATZJu2.d.ts} +29 -15
- package/dist/agent-tools.d.ts +6 -4
- 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-BjSKYpzg.js → capability-B4WbF81e.js} +2 -1
- package/dist/capability-B4WbF81e.js.map +1 -0
- package/dist/{capability-runner-Be_-PLR1.d.ts → capability-runner-DBAnGr6L.d.ts} +45 -15
- package/dist/chat/index.d.ts +341 -232
- package/dist/chat/index.js +602 -72
- 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-4OlMIJx-.js +591 -0
- package/dist/client-4OlMIJx-.js.map +1 -0
- package/dist/{client-jagG8a9_.js → client-fyQrLGkD.js} +2 -2
- package/dist/{client-jagG8a9_.js.map → client-fyQrLGkD.js.map} +1 -1
- package/dist/client-je3koD9Z.d.ts +446 -0
- package/dist/client.d.ts +29 -283
- package/dist/client.js +2 -274
- 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-vAk_5JMt.d.ts +93 -0
- package/dist/{current-agent-Da_C9a3b.d.ts → durable-object-lifecycle-HhgOMWq9.d.ts} +26 -127
- package/dist/harnesses/pi/index.d.ts +348 -0
- package/dist/harnesses/pi/index.js +754 -0
- package/dist/harnesses/pi/index.js.map +1 -0
- package/dist/images-iiTDSyRu.js +1631 -0
- package/dist/images-iiTDSyRu.js.map +1 -0
- package/dist/index-BreL0YrQ.d.ts +78 -0
- package/dist/index-kbSUIer3.d.ts +343 -0
- package/dist/index.d.ts +43 -41
- package/dist/index.js +2 -2
- package/dist/internal_context.d.ts +1 -1
- package/dist/lifecycle/index.d.ts +19 -17
- package/dist/lifecycle/index.js +2 -2
- package/dist/{lifecycle-CMRGjZdw.js → lifecycle-CFu6OV03.js} +73 -29
- package/dist/lifecycle-CFu6OV03.js.map +1 -0
- package/dist/mcp/client/index.d.ts +20 -20
- package/dist/mcp/client/index.js +1 -1
- 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 +2 -2
- 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 +123 -0
- package/dist/queue/index.js +2 -0
- package/dist/queue-BB-zfIYF.js +406 -0
- package/dist/queue-BB-zfIYF.js.map +1 -0
- package/dist/react.d.ts +31 -13
- package/dist/react.js +190 -71
- package/dist/react.js.map +1 -1
- package/dist/{retries-D9Ds-1lz.d.ts → retries-CV6AkXmQ.d.ts} +23 -8
- package/dist/retries.d.ts +12 -8
- package/dist/retries.js +19 -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-DD9NdYbF.js → scheduler-DZ3xQfeK.js} +4 -11
- package/dist/scheduler-DZ3xQfeK.js.map +1 -0
- package/dist/{scheduler-Dwh85ZGl.d.ts → scheduler-RdACNXDP.d.ts} +6 -6
- 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-DlSHshb2.js → src-BZeu-SCD.js} +541 -500
- package/dist/src-BZeu-SCD.js.map +1 -0
- package/dist/state/index.d.ts +6 -0
- package/dist/state/index.js +137 -0
- package/dist/state/index.js.map +1 -0
- package/dist/streams/index.d.ts +1 -1
- package/dist/streams/index.js +1 -1
- package/dist/{streams-D6tJ0NN9.d.ts → streams-Cmkj9QfW.d.ts} +4 -2
- package/dist/{streams-DZKgAj9b.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-ylZgBjhj.js → tasks-68DESxf0.js} +3 -3
- package/dist/{tasks-ylZgBjhj.js.map → tasks-68DESxf0.js.map} +1 -1
- package/dist/{tasks-BRJ5zgya.d.ts → tasks-9YxYtByh.d.ts} +5 -5
- package/dist/transport-PfFnt-Ld.d.ts +525 -0
- package/dist/transport-protocol-BxyngQ_R.js +25 -0
- package/dist/transport-protocol-BxyngQ_R.js.map +1 -0
- package/dist/transport-protocol-ZTew5Wso.d.ts +27 -0
- package/dist/types-BA4anrjI.d.ts +70 -0
- package/dist/voice/index.d.ts +2 -2
- package/dist/websockets/index.d.ts +195 -69
- package/dist/websockets/index.js +3 -2
- package/dist/websockets-Ci-LZFHH.js +1030 -0
- package/dist/websockets-Ci-LZFHH.js.map +1 -0
- 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-class.md +2 -2
- package/docs/agent-tools.md +29 -4
- package/docs/browse-the-web.md +87 -5
- package/docs/chat-agents.md +124 -16
- package/docs/client-sdk.md +30 -0
- package/docs/context.md +27 -0
- package/docs/durable-execution.md +2 -0
- package/docs/harnesses/pi.md +91 -0
- package/docs/human-in-the-loop.md +4 -0
- package/docs/index.md +4 -1
- package/docs/lifecycle.md +114 -23
- package/docs/models-pi-ai.md +325 -0
- package/docs/models.md +504 -0
- package/docs/queue.md +135 -241
- package/docs/resumable-streaming.md +2 -0
- package/docs/retries.md +1 -1
- package/docs/routing.md +13 -13
- package/docs/sessions.md +36 -1
- package/docs/state.md +1 -0
- package/docs/sub-agents.md +12 -3
- package/package.json +51 -3
- package/dist/agent-tools-y7zLfw4Q.js.map +0 -1
- package/dist/browser/ai.js.map +0 -1
- package/dist/capability-BjSKYpzg.js.map +0 -1
- package/dist/client-tools-aIBO0Fk7.d.ts +0 -53
- package/dist/client.js.map +0 -1
- package/dist/connector-CptFKzRh.js.map +0 -1
- package/dist/lifecycle-CMRGjZdw.js.map +0 -1
- package/dist/message-builder-BymO4N_D.js.map +0 -1
- package/dist/scheduler-DD9NdYbF.js.map +0 -1
- package/dist/src-DlSHshb2.js.map +0 -1
- package/dist/streams-DZKgAj9b.js.map +0 -1
- package/dist/websockets-DUfRHPRq.js +0 -502
- package/dist/websockets-DUfRHPRq.js.map +0 -1
- package/dist/ws-chat-transport-rWwta645.js.map +0 -1
package/docs/browse-the-web.md
CHANGED
|
@@ -105,7 +105,7 @@ async () => {
|
|
|
105
105
|
};
|
|
106
106
|
```
|
|
107
107
|
|
|
108
|
-
To discover protocol surface, the model calls `cdp.spec()` — the live, normalized CDP protocol description (domains with commands, events, and types) — or uses the runtime's built-in `codemode.search` / `codemode.describe`.
|
|
108
|
+
To discover protocol surface, the model calls `cdp.spec()` — the live, normalized CDP protocol description (domains with commands, events, and types, including each command's parameters and return values) — or uses the runtime's built-in `codemode.search` / `codemode.describe`.
|
|
109
109
|
|
|
110
110
|
## Use with an Agent
|
|
111
111
|
|
|
@@ -205,6 +205,57 @@ await connector.sweep(); // reclaim expired/stale sessions — call from a sched
|
|
|
205
205
|
await runtime.expirePaused(); // reject stale never-approved pauses, freeing their sessions
|
|
206
206
|
```
|
|
207
207
|
|
|
208
|
+
## Persistent browser
|
|
209
|
+
|
|
210
|
+
`browserTool` gives the model one browser that stays open between turns, so tabs, cookies, and logins carry over. You create the browser once on your agent and pass it in; the model never starts or closes it. This example uses a [Think](../think/index.md) agent, whose `getTools()` runs every turn:
|
|
211
|
+
|
|
212
|
+
```ts
|
|
213
|
+
import { Think } from "@cloudflare/think";
|
|
214
|
+
import { Browser, browserRun } from "agents/browser";
|
|
215
|
+
import { browserTool } from "agents/browser/ai-sdk";
|
|
216
|
+
|
|
217
|
+
export class MyAgent extends Think<Env> {
|
|
218
|
+
browser = new Browser({ provider: browserRun(this.env.BROWSER) });
|
|
219
|
+
|
|
220
|
+
constructor(ctx: DurableObjectState, env: Env) {
|
|
221
|
+
super(ctx, env);
|
|
222
|
+
this.lifecycle.use(this.browser);
|
|
223
|
+
}
|
|
224
|
+
|
|
225
|
+
getModel() {
|
|
226
|
+
return "@cf/moonshotai/kimi-k2.7-code";
|
|
227
|
+
}
|
|
228
|
+
|
|
229
|
+
getTools() {
|
|
230
|
+
return {
|
|
231
|
+
browser: browserTool({ browser: this.browser, loader: this.env.LOADER })
|
|
232
|
+
};
|
|
233
|
+
}
|
|
234
|
+
}
|
|
235
|
+
```
|
|
236
|
+
|
|
237
|
+
- The model writes CDP code as with `createBrowserTools`. `sessionId: "active"` points at the tab it's working in, and that tab is remembered across turns.
|
|
238
|
+
- Tabs the page opens itself come back as `newTabs` in the result.
|
|
239
|
+
- If the browser was lost (idle past `keepAliveMs`, closed, or crashed), the code still runs in a new browser and the result includes `restarted: true`.
|
|
240
|
+
- Call `this.browser.liveView()` to give a person a Live View link into the same browser, and `this.browser.close()` to shut it down.
|
|
241
|
+
- Set `keepAliveMs`, `recording`, and `guardrails` on `browserRun(binding, options)`. Use a different `name` for each extra browser on the same agent.
|
|
242
|
+
|
|
243
|
+
Only Chromium on a Browser Run binding is supported. It needs the same `LOADER` binding and `CodemodeRuntime` export as `createBrowserTools`.
|
|
244
|
+
|
|
245
|
+
For TanStack AI, import `browserTool` from `agents/browser/tanstack-ai` instead. It takes the same options plus an optional `name` (default `"browser"`) and returns a `ServerTool`:
|
|
246
|
+
|
|
247
|
+
```ts
|
|
248
|
+
import { browserTool } from "agents/browser/tanstack-ai";
|
|
249
|
+
|
|
250
|
+
const stream = chat({
|
|
251
|
+
adapter,
|
|
252
|
+
tools: [browserTool({ browser: this.browser, loader: this.env.LOADER })],
|
|
253
|
+
messages
|
|
254
|
+
});
|
|
255
|
+
```
|
|
256
|
+
|
|
257
|
+
The TanStack AI tool doesn't support screenshots yet. TanStack AI gives the host and the model the same output, so if the model returns a screenshot, the tool replaces it with a note saying it was left out, and the tool's instructions tell the model to read the page with `Runtime.evaluate` instead.
|
|
258
|
+
|
|
208
259
|
## Quick Actions (stateless browsing)
|
|
209
260
|
|
|
210
261
|
`browser_execute` drives a full, stateful CDP session — the right tool for interactive, multi-step automation. But a lot of agent browsing is really one-shot: _read this page as Markdown_, _extract these fields_, _list the links_. For those, [Quick Actions](https://developers.cloudflare.com/browser-run/quick-actions/) are simpler, faster, and cheaper. They need only the `browser` binding — no Durable Object, Worker Loader, or sandbox — so they work from any Worker.
|
|
@@ -404,12 +455,12 @@ Either `browser` or `cdpUrl` must be provided. When both are set, `cdpUrl` takes
|
|
|
404
455
|
For custom integrations, import the building blocks directly:
|
|
405
456
|
|
|
406
457
|
```ts
|
|
407
|
-
import { BrowserConnector,
|
|
458
|
+
import { BrowserConnector, CdpConnection, connectUrl } from "agents/browser";
|
|
408
459
|
|
|
409
460
|
// Connect to a custom CDP endpoint
|
|
410
|
-
const
|
|
411
|
-
const version = await
|
|
412
|
-
|
|
461
|
+
const connection = await connectUrl("http://localhost:9222");
|
|
462
|
+
const version = await connection.send("Browser.getVersion");
|
|
463
|
+
connection.close();
|
|
413
464
|
|
|
414
465
|
// Or plug the connector into your own codemode runtime
|
|
415
466
|
const connector = new BrowserConnector(this.ctx, {
|
|
@@ -419,6 +470,37 @@ const connector = new BrowserConnector(this.ctx, {
|
|
|
419
470
|
});
|
|
420
471
|
```
|
|
421
472
|
|
|
473
|
+
To load the normalized CDP protocol specification from Worker code, use `loadCdpSpec()`:
|
|
474
|
+
|
|
475
|
+
```ts
|
|
476
|
+
import { loadCdpSpec } from "agents/browser";
|
|
477
|
+
|
|
478
|
+
const spec = await loadCdpSpec({
|
|
479
|
+
browser: this.env.BROWSER,
|
|
480
|
+
sessionId
|
|
481
|
+
});
|
|
482
|
+
```
|
|
483
|
+
|
|
484
|
+
Pass a Browser Run binding in `browser` and the Browser Run session ID returned by `createBrowserSession()` in `sessionId`. If you omit `sessionId`, the helper creates a temporary session to fetch the specification, then deletes it. Pass an existing session ID to read the specification without creating another session. If you provide `cdpUrl`, the helper loads the specification from that URL instead; `cdpUrl` takes precedence over `browser` and `sessionId`.
|
|
485
|
+
|
|
486
|
+
You can also manage Browser Rendering sessions directly. `createBrowserSession` accepts [hostname guardrails](https://developers.cloudflare.com/browser-run/features/guardrails/) that restrict which domains the session may reach — fixed at launch for every connection to the session, including Live View (not supported with Kitesurf):
|
|
487
|
+
|
|
488
|
+
```ts
|
|
489
|
+
import { createBrowserSession, connectBrowserSession } from "agents/browser";
|
|
490
|
+
|
|
491
|
+
const { sessionId } = await createBrowserSession(this.env.BROWSER, {
|
|
492
|
+
guardrails: { allowedDomains: ["example.com", "*.example.com"] }
|
|
493
|
+
});
|
|
494
|
+
|
|
495
|
+
// Connect (and reconnect) without deleting the session on close:
|
|
496
|
+
const session = await connectBrowserSession(this.env.BROWSER, sessionId, {
|
|
497
|
+
timeoutMs: 30_000,
|
|
498
|
+
onClose: () => {
|
|
499
|
+
/* the CDP socket closed; the session itself stays alive */
|
|
500
|
+
}
|
|
501
|
+
});
|
|
502
|
+
```
|
|
503
|
+
|
|
422
504
|
## Local development
|
|
423
505
|
|
|
424
506
|
Recent Wrangler releases support Browser Rendering in local development. `npx wrangler dev` provisions the browser automatically, so the same `browser: env.BROWSER` setup works locally and when deployed.
|
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/client-sdk.md
CHANGED
|
@@ -120,6 +120,35 @@ useAgent({
|
|
|
120
120
|
});
|
|
121
121
|
```
|
|
122
122
|
|
|
123
|
+
### Transport
|
|
124
|
+
|
|
125
|
+
`useAgent` speaks the Agent protocol over one of two wires. The default,
|
|
126
|
+
`"cf-websocket"`, is a hibernating WebSocket managed by PartySocket. The
|
|
127
|
+
experimental `"capnweb"` transport carries protocol frames through a single
|
|
128
|
+
[Cap'n Web](https://github.com/cloudflare/capnweb) RPC session whose root
|
|
129
|
+
also serves the host's callables natively:
|
|
130
|
+
|
|
131
|
+
```typescript
|
|
132
|
+
useAgent({
|
|
133
|
+
agent: "ChatAgent",
|
|
134
|
+
name: "room-123",
|
|
135
|
+
transport: "capnweb" // default: "cf-websocket"
|
|
136
|
+
});
|
|
137
|
+
```
|
|
138
|
+
|
|
139
|
+
Identity, state sync, reconnection with backoff, and terminal close codes are
|
|
140
|
+
identical on both wires; PartySocket still owns the connection and the
|
|
141
|
+
transport only swaps the socket class it instantiates. `call()` and `stub`
|
|
142
|
+
differ by design. On `cf-websocket` they send JSON `rpc` frames. On `capnweb`
|
|
143
|
+
they invoke the host's `callables` natively on the Cap'n Web session root: a
|
|
144
|
+
method that returns an `RpcTarget` hands you a live stub you can keep
|
|
145
|
+
calling, a `ReadableStream` result streams, and chained calls pipeline.
|
|
146
|
+
`@callable()` decorators on an `Agent` are a JSON-wire feature and are not
|
|
147
|
+
available on `capnweb` unless the Agent also passes a `callables` target.
|
|
148
|
+
The trade-off is that a Cap'n Web connection does not hibernate: the Durable
|
|
149
|
+
Object stays in memory while one is open. `AgentClient` accepts the same
|
|
150
|
+
`transport` option.
|
|
151
|
+
|
|
123
152
|
### Async Query Parameters
|
|
124
153
|
|
|
125
154
|
For authentication tokens or other async data, pass a function that returns a Promise:
|
|
@@ -351,6 +380,7 @@ type UseAgentOptions<State> = {
|
|
|
351
380
|
name?: string; // Instance name (default: "default")
|
|
352
381
|
host?: string; // Custom host
|
|
353
382
|
path?: string; // Custom path prefix
|
|
383
|
+
transport?: "cf-websocket" | "capnweb"; // Wire (default: "cf-websocket")
|
|
354
384
|
|
|
355
385
|
// Query parameters
|
|
356
386
|
query?:
|
package/docs/context.md
CHANGED
|
@@ -96,6 +96,31 @@ const system = await context.freezeSystemPrompt();
|
|
|
96
96
|
|
|
97
97
|
`refreshSystemPrompt()` reloads every provider, re-renders, and overwrites the stored prompt.
|
|
98
98
|
|
|
99
|
+
### Reminders for changing blocks
|
|
100
|
+
|
|
101
|
+
Some blocks go stale while the prompt is frozen: the current date, workspace state, project instructions. Refreshing the prompt keeps them current but throws away the cached prefix. Mark such a block `whenChanged: "remind"` to keep both:
|
|
102
|
+
|
|
103
|
+
```ts
|
|
104
|
+
const context = new ContextBlocks([
|
|
105
|
+
{
|
|
106
|
+
label: "environment",
|
|
107
|
+
provider: {
|
|
108
|
+
get: async () => `Today is ${new Date().toISOString().slice(0, 10)}.`
|
|
109
|
+
},
|
|
110
|
+
whenChanged: "remind"
|
|
111
|
+
}
|
|
112
|
+
]);
|
|
113
|
+
|
|
114
|
+
const system = await context.freezeSystemPrompt();
|
|
115
|
+
const reminder = await context.reminder(); // null until the date changes
|
|
116
|
+
```
|
|
117
|
+
|
|
118
|
+
`reminder()` re-reads each such block and, for every one whose current rendering no longer appears in the frozen prompt, returns its current value under a short note saying it replaces the prompt's copy. Send that text after the cached prefix, for example at the end of the latest user message, and do not persist it. The frozen prompt does not change, and the reminder keeps coming back on every call until `refreshSystemPrompt()` promotes the value into the prompt. That refresh is the one deliberate cache bust.
|
|
119
|
+
|
|
120
|
+
The comparison is against the stored prompt, not against in-memory state, so a reminder survives a restart. A remind block always has a section in the prompt, even when it is empty, so the model knows it exists and a later value has a copy to replace. Any difference in what the provider returns counts as a change, so return only what should count: a date rather than a timestamp.
|
|
121
|
+
|
|
122
|
+
A standing reminder still costs a little cache: it moves to each new user message, so the previous user message falls out of the cached prefix. The system prompt and everything before that message stay cached.
|
|
123
|
+
|
|
99
124
|
## Tools
|
|
100
125
|
|
|
101
126
|
`tools()` returns an AI SDK `ToolSet` wired from what the blocks can do:
|
|
@@ -126,6 +151,8 @@ A block declared without a provider is auto-wired to durable per-agent SQLite. T
|
|
|
126
151
|
|
|
127
152
|
The assembled blocks are available as `this.context` after `onStart()`.
|
|
128
153
|
|
|
154
|
+
Think sends reminders itself. Each turn it calls `reminder()` and adds the result to the last user message of the model request, or, when the request continues a partial reply, to the user message that asks the model to continue (`TurnContext.messages` in `beforeTurn` already includes it). Nothing is written to the transcript. Think refreshes the prompt after a compaction, which promotes any pending values.
|
|
155
|
+
|
|
129
156
|
## Related
|
|
130
157
|
|
|
131
158
|
- [Sessions](./sessions.md) - durable message trees, streamed history, compaction, and attachment offload
|
|
@@ -345,6 +345,7 @@ runFiber("work", fn)
|
|
|
345
345
|
_checkRunFibers():
|
|
346
346
|
├─ SELECT * FROM cf_agents_runs
|
|
347
347
|
├─ For each orphaned row:
|
|
348
|
+
│ ├─ Body already finished (only its cleanup failed) → DELETE, no hook
|
|
348
349
|
│ ├─ Parse snapshot from JSON
|
|
349
350
|
│ ├─ Call onFiberRecovered(ctx)
|
|
350
351
|
│ └─ DELETE the row after successful recovery
|
|
@@ -461,6 +462,7 @@ Key points:
|
|
|
461
462
|
|
|
462
463
|
- **The original lambda is gone.** On recovery, you only have the `name` and `snapshot`. The lambda cannot be serialized — recovery logic must be in the hook.
|
|
463
464
|
- **The row is deleted after the hook returns successfully.** If you want to continue the work, call `runFiber()` again inside the hook — this creates a new row.
|
|
465
|
+
- **The hook is meant for interrupted work.** If the function returned or threw and only the row delete failed, the fiber records that it finished, and the next scan deletes the row without calling the hook. This is best-effort: if recording the finish also fails, the row looks interrupted and the hook runs anyway, so keep recovery idempotent.
|
|
464
466
|
- **You control what recovery means.** Retry from the beginning, resume from a checkpoint, skip and notify the user, or do nothing. The framework does not impose a strategy.
|
|
465
467
|
- **If the hook throws, the row is kept (up to a bound).** A later startup or alarm scan will try recovery again, which protects against transient storage or scheduling failures. Catch application-level errors yourself when you want to mark the work terminal instead of retrying. A hook that always throws is retried on a backing-off schedule (the recovery alarm uses an exponential delay capped at 5 minutes, so it is not a busy-loop) until the row exceeds `fiberRecoveryMaxAgeMs` (default 24h), after which it is discarded with a `fiber:recovery:skipped` (`reason: "max_age_exceeded"`) event. Setting `fiberRecoveryMaxAgeMs: 0` retains such rows indefinitely — recovery keeps retrying on the capped backoff, and the Durable Object never idle-evicts while an un-recoverable row exists, so prefer a finite age unless you intend to inspect or clear those rows yourself.
|
|
466
468
|
|
|
@@ -0,0 +1,91 @@
|
|
|
1
|
+
---
|
|
2
|
+
title: Pi harness (Experimental)
|
|
3
|
+
pcx_content_type: get-started
|
|
4
|
+
description: Host pi-durable sessions in a Durable Object with the experimental PiHarness lifecycle capability. Connect a pi-ai model and keep sessions active across eviction.
|
|
5
|
+
---
|
|
6
|
+
|
|
7
|
+
`PiHarness` hosts [pi-durable](https://github.com/earendil-works/pi/tree/main/packages/durable) in a Durable Object. It connects pi's SQLite-backed sessions to the Agents SDK lifecycle, which wakes active sessions after eviction. The API is experimental and may change.
|
|
8
|
+
|
|
9
|
+
`PiHarness` does not provide a chat protocol or user interface. Your application chooses how clients reach sessions, such as through WebSockets, HTTP, or RPC. The harness exposes pi sessions and their event streams for your application to adapt.
|
|
10
|
+
|
|
11
|
+
## Install the pi packages
|
|
12
|
+
|
|
13
|
+
Install `agents` with pi-durable and pi-ai:
|
|
14
|
+
|
|
15
|
+
```sh
|
|
16
|
+
npm install agents @earendil-works/pi-durable @earendil-works/pi-ai
|
|
17
|
+
```
|
|
18
|
+
|
|
19
|
+
Both pi packages are optional peer dependencies of `agents`. The example below uses Workers AI through the `AI` binding. To configure the binding and choose other models, refer to [Models for pi-ai](../models-pi-ai.md).
|
|
20
|
+
|
|
21
|
+
## Create the harness
|
|
22
|
+
|
|
23
|
+
Open pi's `Harness` with the storage and background context that `PiHarness` provides. Register the pi-ai provider on a `Models` registry, then attach the capability to the object's lifecycle:
|
|
24
|
+
|
|
25
|
+
```ts
|
|
26
|
+
import { DurableObject } from "cloudflare:workers";
|
|
27
|
+
import { createModels } from "@earendil-works/pi-ai/models";
|
|
28
|
+
import { createRegistry, Harness } from "@earendil-works/pi-durable";
|
|
29
|
+
import { PiHarness } from "agents/harnesses/pi";
|
|
30
|
+
import { Lifecycle } from "agents/lifecycle";
|
|
31
|
+
import { CLOUDFLARE_PROVIDER_ID, createAI } from "agents/models/pi-ai";
|
|
32
|
+
|
|
33
|
+
export class Assistant extends DurableObject<Env> {
|
|
34
|
+
readonly ai = createAI({ binding: this.env.AI });
|
|
35
|
+
readonly registry = createRegistry();
|
|
36
|
+
|
|
37
|
+
readonly harness = new PiHarness({
|
|
38
|
+
harness: ({ storage, context }) => {
|
|
39
|
+
const models = createModels();
|
|
40
|
+
models.setProvider(this.ai.provider);
|
|
41
|
+
return Harness.open(
|
|
42
|
+
storage,
|
|
43
|
+
{ models, registry: this.registry },
|
|
44
|
+
context
|
|
45
|
+
);
|
|
46
|
+
},
|
|
47
|
+
defaults: {
|
|
48
|
+
model: {
|
|
49
|
+
provider: CLOUDFLARE_PROVIDER_ID,
|
|
50
|
+
modelId: "@cf/zai-org/glm-4.7-flash"
|
|
51
|
+
}
|
|
52
|
+
}
|
|
53
|
+
});
|
|
54
|
+
|
|
55
|
+
readonly lifecycle = Lifecycle.install(this).use(this.harness);
|
|
56
|
+
|
|
57
|
+
async prompt(input: string) {
|
|
58
|
+
return this.harness.prompt(input);
|
|
59
|
+
}
|
|
60
|
+
}
|
|
61
|
+
```
|
|
62
|
+
|
|
63
|
+
The `harness` factory receives a pi storage adapter backed by the Durable Object's SQLite database. It also receives a background context for opening pi. Configure pi's models, registry, and other `Harness.open` options in this factory.
|
|
64
|
+
|
|
65
|
+
`defaults` applies when the harness creates a new session. You can change an individual session's model with `session.setModel()`. Without a model, the session cannot produce an answer until one is set.
|
|
66
|
+
|
|
67
|
+
## Work with sessions
|
|
68
|
+
|
|
69
|
+
The root session is available through `harness.prompt()` and `harness.submit()`. Create additional sessions with `harness.sessions.create()`, then use the returned session handle:
|
|
70
|
+
|
|
71
|
+
```ts
|
|
72
|
+
const session = await this.harness.sessions.create();
|
|
73
|
+
const receipt = await session.submit("Summarize the latest report", {
|
|
74
|
+
operationId: "report-summary-42"
|
|
75
|
+
});
|
|
76
|
+
const result = await session.wait(receipt.operationId);
|
|
77
|
+
```
|
|
78
|
+
|
|
79
|
+
`submit()` returns after pi durably accepts the operation. `wait()` returns its result after pi finishes it. Reusing an `operationId` makes a submission idempotent. Set `whenBusy: "steer"` to join the running work after its current tool round; the default queues a follow-up.
|
|
80
|
+
|
|
81
|
+
Use `session.events()` to read pi's snapshot and committed event batches. Your application can stream those events to clients and build its own view. `session.messages()` returns pi's transcript entries, not a UI-ready chat transcript.
|
|
82
|
+
|
|
83
|
+
## How recovery works
|
|
84
|
+
|
|
85
|
+
The harness schedules one lifecycle job for each session with active work. The job waits for pi's tasks, refreshes its wake while they remain active, and completes when they settle. If the Durable Object is evicted, the due job wakes it again; pi reopens its stored state and resumes the session's work.
|
|
86
|
+
|
|
87
|
+
Pi owns the transcript, operation inbox, generation, tools, retries, and recovery. `PiHarness` supplies durable storage and wake scheduling. It does not move pi's orchestration into the Agents SDK.
|
|
88
|
+
|
|
89
|
+
## Run the example
|
|
90
|
+
|
|
91
|
+
The [Pi harness example](https://github.com/cloudflare/agents/tree/main/examples/next/harnesses/pi) connects `session.events()` to WebSockets and projects pi's entries into a browser transcript. It also demonstrates a replay-safe tool and model configuration with Workers AI.
|
|
@@ -361,6 +361,10 @@ A Durable Object can be evicted at any time (a deploy, an inactivity timeout, a
|
|
|
361
361
|
|
|
362
362
|
This protection applies to interactions only the client can resolve — `approval-requested` parts and `input-available` parts for client-side tools (those without a server `execute`). A server tool whose `execute()` was killed mid-flight is a genuine orphan and recovers through the normal transcript-repair path instead.
|
|
363
363
|
|
|
364
|
+
### Repair an approved tool that never ran
|
|
365
|
+
|
|
366
|
+
An approval can be recorded even if its tool continuation never starts. When a later user turn moves past that approval, `AIChatAgent` and Think repair the transcript so the model does not receive an unresolved tool call. An approved tool that never ran becomes an error result, and a denied tool becomes a denied result. An approval in the current final message is left for the AI SDK to execute.
|
|
367
|
+
|
|
364
368
|
## Client-Side Tool Execution with `onToolCall`
|
|
365
369
|
|
|
366
370
|
For tools that need browser APIs (geolocation, camera, clipboard) or user interaction, define the tool on the server without an `execute` function and handle it on the client with `onToolCall`:
|
package/docs/index.md
CHANGED
|
@@ -63,7 +63,7 @@ The differentiator is not "we have durable state" — it is what happens when a
|
|
|
63
63
|
|
|
64
64
|
## Background Processing
|
|
65
65
|
|
|
66
|
-
- [Queue](./queue.md) -
|
|
66
|
+
- [Queue](./queue.md) - Durable background task execution
|
|
67
67
|
- [Scheduling](./scheduling.md) - Delayed, scheduled, and cron-based tasks
|
|
68
68
|
- [Retries](./retries.md) - Automatic retries with exponential backoff and jitter
|
|
69
69
|
- [Durable Execution](./durable-execution.md) - `runFiber()`, `startFiber()`, `stash()`, and crash recovery for long tasks
|
|
@@ -79,6 +79,9 @@ The differentiator is not "we have durable state" — it is what happens when a
|
|
|
79
79
|
- [Agent Tools](./agent-tools.md) - Run chat-capable sub-agents as tools with streaming child timelines
|
|
80
80
|
- [Server-Driven Messages](./server-driven-messages.md) - Autonomous agent workflows: scheduled follow-ups, queue processing, webhooks, chained reasoning
|
|
81
81
|
- TODO: [Using AI Models](./using-ai-models.md) - OpenAI, Anthropic, Workers AI, and other providers
|
|
82
|
+
- [Models (Experimental)](./models.md) - `createAI` — one AI SDK provider for Workers AI and third-party catalog models, same string space
|
|
83
|
+
- [Models for pi-ai (Experimental)](./models-pi-ai.md) - `createAI` for pi-ai: Workers AI ids and third-party models through AI Gateway, as a pi-ai provider
|
|
84
|
+
- [Pi harness (Experimental)](./harnesses/pi.md) - Host pi-durable sessions in a Durable Object with durable storage and lifecycle wakeups
|
|
82
85
|
- TODO: [RAG (Retrieval Augmented Generation)](./rag.md) - Vector search with Vectorize
|
|
83
86
|
- [Sessions (Experimental)](./sessions.md) - Durable message trees, streamed history, compaction, search, and lossless attachment offload
|
|
84
87
|
- [Context (Experimental)](./context.md) - System-prompt blocks, frozen prompts, writable/searchable/loadable providers, and their tools
|