agents 0.28.1 → 0.30.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-BMvWTFEp.d.ts → agent-routing-CDbpVjYq.d.ts} +7 -8
- package/dist/agent-routing.d.ts +1 -1
- package/dist/agent-tool-types.d.ts +1 -1
- package/dist/{agent-tools-ohoewW-B.d.ts → agent-tools-C8DH8Yii.d.ts} +2 -2
- package/dist/agent-tools.d.ts +1 -1
- package/dist/{ai-ahruGw7e.d.ts → ai-iXw0uOGj.d.ts} +3 -3
- package/dist/{ai-sdk-C2iTAwUd.js → ai-sdk-DRPh2rRa.js} +185 -125
- package/dist/ai-sdk-DRPh2rRa.js.map +1 -0
- package/dist/browser/ai.d.ts +1 -1
- package/dist/browser/ai.js +2 -2
- package/dist/browser/index.d.ts +8 -8
- package/dist/browser/index.js +3 -3
- package/dist/browser/tools/ai-sdk.d.ts +2 -2
- package/dist/browser/tools/ai-sdk.js +1 -1
- package/dist/browser/tools/pi.d.ts +7 -3
- package/dist/browser/tools/pi.js +9 -5
- package/dist/browser/tools/pi.js.map +1 -1
- package/dist/browser/tools/tanstack-ai.d.ts +2 -2
- package/dist/browser/tools/tanstack-ai.js +1 -1
- package/dist/{browser-CEdNlsaD.js → browser-DccPw_wW.js} +130 -79
- package/dist/browser-DccPw_wW.js.map +1 -0
- package/dist/{browser-D30VlpcA.d.ts → browser-DlXhSAdf.d.ts} +41 -37
- package/dist/{browser-tool-4iomavs_.js → browser-tool-B1mhifin.js} +3 -3
- package/dist/{browser-tool-4iomavs_.js.map → browser-tool-B1mhifin.js.map} +1 -1
- package/dist/{browser-tool-vtrPOV8e.d.ts → browser-tool-Cxjk5sgG.d.ts} +2 -2
- package/dist/chat/index.d.ts +143 -71
- package/dist/chat/index.js +269 -110
- package/dist/chat/index.js.map +1 -1
- package/dist/chat-sdk/index.d.ts +1 -1
- package/dist/chat-sdk/index.js +1 -1
- package/dist/cli.js +53 -52
- package/dist/{client-Bp-JwL3p.d.ts → client-C27CtRZr.d.ts} +2 -2
- package/dist/client.d.ts +1 -1
- package/dist/{connector-DEXzVoaY.d.ts → connector-20DJ6p4k.d.ts} +2 -2
- package/dist/{connector-CtZylXdk.js → connector-DAXBOIIi.js} +2 -2
- package/dist/{connector-CtZylXdk.js.map → connector-DAXBOIIi.js.map} +1 -1
- package/dist/experimental/channels/web/index.js +1 -1
- package/dist/harness/container/index.js +1 -1
- package/dist/harness/container/index.js.map +1 -1
- package/dist/harness/opencode/index.js +1 -1
- package/dist/harness/pi/index.d.ts +70 -40
- package/dist/harness/pi/index.js +266 -330
- package/dist/harness/pi/index.js.map +1 -1
- package/dist/{images-C4hiIexM.js → images-xaDv2Fqe.js} +636 -168
- package/dist/images-xaDv2Fqe.js.map +1 -0
- package/dist/index-DCxk3OSJ.d.ts +378 -0
- package/dist/{index-kbSUIer3.d.ts → index-iv9FTXX9.d.ts} +80 -112
- package/dist/index.d.ts +1 -1
- package/dist/index.js +1 -1
- package/dist/{input-schema-xBofjBKC.js → input-schema-B0Etuo-o.js} +2 -2
- package/dist/{input-schema-xBofjBKC.js.map → input-schema-B0Etuo-o.js.map} +1 -1
- package/dist/lifecycle/index.js +1 -1
- package/dist/{lifecycle-DGorpAmA.js → lifecycle-v1rKHV2C.js} +67 -19
- package/dist/lifecycle-v1rKHV2C.js.map +1 -0
- package/dist/mcp/client/index.d.ts +1 -1
- package/dist/mcp/index.d.ts +1 -1
- package/dist/mcp/index.js +1 -1
- package/dist/models/ai-sdk/index.d.ts +29 -352
- package/dist/models/ai-sdk/index.js +1 -1
- package/dist/models/opencode/index.d.ts +25 -13
- package/dist/models/opencode/index.js +16 -11
- package/dist/models/opencode/index.js.map +1 -1
- package/dist/models/pi-ai/index.d.ts +13 -14
- package/dist/models/pi-ai/index.js +87 -34
- package/dist/models/pi-ai/index.js.map +1 -1
- package/dist/react.d.ts +2 -2
- package/dist/routing/index.d.ts +1 -1
- package/dist/serializable.d.ts +1 -1
- package/dist/{session-store-DoPY12xM.d.ts → session-store-_IK6dV-w.d.ts} +14 -2
- package/dist/{session-store-Do4xkDcf.js → session-store-yvwq1f9x.js} +55 -2
- package/dist/session-store-yvwq1f9x.js.map +1 -0
- package/dist/{settings-BjAiPd6Z.js → settings-Dk2il2Hf.js} +4 -1
- package/dist/settings-Dk2il2Hf.js.map +1 -0
- package/dist/skills/index.d.ts +22 -28
- package/dist/skills/index.js +183 -143
- package/dist/skills/index.js.map +1 -1
- package/dist/{src-CQxJFv4l.js → src-DfLzdwlK.js} +3 -3
- package/dist/src-DfLzdwlK.js.map +1 -0
- package/dist/sub-routing.d.ts +1 -1
- package/dist/{tool-VF0qiO7q.js → tool-DkT_DJBm.js} +2 -2
- package/dist/tool-DkT_DJBm.js.map +1 -0
- package/dist/transport-CPh7YGiw.d.ts +623 -0
- package/dist/voice/index.d.ts +1 -1
- package/dist/web/fetch/tools/pi.d.ts +10 -6
- package/dist/web/fetch/tools/pi.js +12 -8
- package/dist/web/fetch/tools/pi.js.map +1 -1
- package/dist/web/search/index.js +1 -1
- package/dist/web/search/tools/ai-sdk.js +2 -2
- package/dist/web/search/tools/pi.d.ts +10 -6
- package/dist/web/search/tools/pi.js +13 -9
- package/dist/web/search/tools/pi.js.map +1 -1
- package/dist/web/search/tools/tanstack-ai.js +2 -2
- package/dist/websockets/index.js +1 -1
- package/dist/{websockets-C3zy8YR0.js → websockets-CyoeosdO.js} +2 -2
- package/dist/{websockets-C3zy8YR0.js.map → websockets-CyoeosdO.js.map} +1 -1
- package/dist/workflow-types.d.ts +1 -1
- package/dist/workflows.d.ts +1 -1
- package/dist/workflows.js +1 -1
- package/docs/browse-the-web.md +1 -1
- package/docs/chat-agents.md +19 -18
- package/docs/harnesses/pi.md +101 -38
- package/docs/lifecycle.md +7 -1
- package/docs/models-pi-ai.md +66 -20
- package/docs/models.md +197 -172
- package/package.json +12 -7
- package/dist/ai-sdk-C2iTAwUd.js.map +0 -1
- package/dist/browser-CEdNlsaD.js.map +0 -1
- package/dist/images-C4hiIexM.js.map +0 -1
- package/dist/lifecycle-DGorpAmA.js.map +0 -1
- package/dist/session-store-Do4xkDcf.js.map +0 -1
- package/dist/settings-BjAiPd6Z.js.map +0 -1
- package/dist/settings-DnD__xuE.d.ts +0 -349
- package/dist/src-CQxJFv4l.js.map +0 -1
- package/dist/tool-VF0qiO7q.js.map +0 -1
- package/dist/transport-DCyarnwm.d.ts +0 -200
package/docs/chat-agents.md
CHANGED
|
@@ -652,7 +652,7 @@ Durable bookkeeping stays enabled even when automatic continuation is not approp
|
|
|
652
652
|
|
|
653
653
|
- **Retries or side effects are unsafe:** override `onChatRecovery()` and return `{ continue: false }`. Persist idempotency keys or completion records before external side effects so a recovered turn can tell whether work already happened.
|
|
654
654
|
- **Cancellation must survive eviction:** an `AbortSignal` only cancels the current in-memory turn. Also persist cancellation intent in agent state or SQL, read it in `onChatRecovery()`, and return `{ continue: false }` when cancellation was requested.
|
|
655
|
-
- **Cost must be bounded:** set `maxAttempts`, `noProgressTimeoutMs`, `maxRecoveryWork`, and `
|
|
655
|
+
- **Cost must be bounded:** set `maxAttempts`, `noProgressTimeoutMs`, `maxRecoveryWork`, and `maxInterruptedAttempts`. Use `shouldKeepRecovering` with durable spend data to stop later attempts. The predicate is not bound to the agent instance, so read spend from a store keyed by `ctx.recoveryRootRequestId`.
|
|
656
656
|
- **A provider can resume without a new model call:** use `this.stash()` to save its response ID, retrieve that response in `onChatRecovery()`, and return `{ persist: false, continue: false }`.
|
|
657
657
|
|
|
658
658
|
`chatRecovery` can be configured with budgets and terminal behavior:
|
|
@@ -669,10 +669,10 @@ override chatRecovery = {
|
|
|
669
669
|
// finite backstop (10000). Set a different value to tune when a turn that
|
|
670
670
|
// keeps emitting content but never converges is sealed.
|
|
671
671
|
maxRecoveryWork: 2000,
|
|
672
|
-
//
|
|
673
|
-
//
|
|
674
|
-
//
|
|
675
|
-
|
|
672
|
+
// How many of one turn's attempts may die before recovery gives up, for
|
|
673
|
+
// example to a memory-limit reset on every run. Defaults to 5, counting
|
|
674
|
+
// the original turn.
|
|
675
|
+
maxInterruptedAttempts: 5,
|
|
676
676
|
// Caller policy consulted from the second recovery attempt onward. Return
|
|
677
677
|
// false to stop recovery. This is where you enforce a token/cost budget.
|
|
678
678
|
// Note: this is called as `config.shouldKeepRecovering(ctx)`, so it is not
|
|
@@ -689,16 +689,16 @@ override chatRecovery = {
|
|
|
689
689
|
|
|
690
690
|
**`chatRecovery` configuration:**
|
|
691
691
|
|
|
692
|
-
| Field
|
|
693
|
-
|
|
|
694
|
-
| `maxAttempts`
|
|
695
|
-
| `stableTimeoutMs`
|
|
696
|
-
| `terminalMessage`
|
|
697
|
-
| `noProgressTimeoutMs`
|
|
698
|
-
| `maxRecoveryWork`
|
|
699
|
-
| `
|
|
700
|
-
| `shouldKeepRecovering`
|
|
701
|
-
| `onExhausted`
|
|
692
|
+
| Field | Default | Description |
|
|
693
|
+
| ------------------------ | ----------------- | ------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------ |
|
|
694
|
+
| `maxAttempts` | `10` | Attempt cap before terminal exhaustion. Resets on forward progress, so it catches a tight no-progress alarm loop, not a healthy long turn. |
|
|
695
|
+
| `stableTimeoutMs` | `10_000` | How long a recovery attempt waits for the isolate to reach stable state before rescheduling. |
|
|
696
|
+
| `terminalMessage` | generic message | The message shown to the user when recovery is given up on. |
|
|
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. |
|
|
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. |
|
|
699
|
+
| `maxInterruptedAttempts` | `5` | How many of one turn's attempts may be interrupted before recovery gives up (`interrupted_attempts_exceeded`). An attempt is interrupted when its isolate dies before it finishes: a Durable Object memory-limit reset, a deploy, or any other reset. The original turn counts, so the default runs the model at most five times for a turn that dies every time. The count is taken when the next isolate picks the turn up, before it re-runs anything, because an isolate killed by a memory-limit reset cannot record anything. Progress never resets it. A memory-limit reset and a deploy look the same from the next isolate, so a turn interrupted by five deploys in a row is also sealed. Values below `1` are read as `1`; `Infinity` disables the bound. Replaces `maxOomRetries`, which is still read as `maxInterruptedAttempts: maxOomRetries + 1`. |
|
|
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. |
|
|
701
|
+
| `onExhausted` | — | Called once when recovery is given up on, before the terminal message is delivered. Inspect `ctx.reason` for why. |
|
|
702
702
|
|
|
703
703
|
**`ChatRecoveryProgressContext`** (the `ctx` passed to `shouldKeepRecovering`):
|
|
704
704
|
|
|
@@ -713,12 +713,13 @@ override chatRecovery = {
|
|
|
713
713
|
| `work` | `number` | Monotonic count of durable stream segments produced since the incident opened (not tokens). |
|
|
714
714
|
| `ageMs` | `number` | Wall-clock ms since the incident's first interruption. |
|
|
715
715
|
|
|
716
|
-
A progressing turn survives
|
|
716
|
+
A progressing turn survives repeated interruption (for example a dense deploy window) as long as it keeps making forward progress, stays under the `maxRecoveryWork` backstop, and fewer than `maxInterruptedAttempts` of its attempts die. Recovery is sealed only by one of these `ctx.reason` values:
|
|
717
717
|
|
|
718
718
|
- `no_progress_timeout` — no forward progress within the no-progress window (a stuck turn).
|
|
719
719
|
- `max_attempts_exceeded` — the attempt cap was spent on a tight no-progress alarm loop.
|
|
720
720
|
- `work_budget_exceeded` — the turn kept producing content but exceeded `maxRecoveryWork` (a runaway loop).
|
|
721
|
-
- `
|
|
721
|
+
- `interrupted_attempts_exceeded` — the turn's attempts kept dying before they finished (for example a memory-limit reset on every run) until `maxInterruptedAttempts` of them had been interrupted.
|
|
722
|
+
- `out_of_memory` — the alarm memory-limit circuit breaker sealed the recovery after repeated memory-limit resets.
|
|
722
723
|
- `recovery_aborted` — your `shouldKeepRecovering` hook returned `false`.
|
|
723
724
|
- `stable_timeout` — recovery attempts kept timing out waiting for stable state until the budget drained (extreme churn).
|
|
724
725
|
|
|
@@ -736,7 +737,7 @@ The unit is only meaningful relative to another `ctx.work` reading, so to choose
|
|
|
736
737
|
|
|
737
738
|
> **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.
|
|
738
739
|
|
|
739
|
-
> **Out-of-memory crash loops
|
|
740
|
+
> **Out-of-memory crash loops.** A turn that exceeds the Durable Object's 128 MB limit on every run is bounded by `maxInterruptedAttempts`: each run that dies is counted when the next isolate picks the turn up, before it re-runs anything, and the fifth seals the turn with `interrupted_attempts_exceeded`. The isolate that hit the limit cannot record anything itself, because every storage call after the reset fails. The alarm boundary also catches memory-limit resets so the platform does not retry the alarm in a tight loop, and emits an `alarm:memory_limit_reset` observability event. Its strike counter (`maxAlarmMemoryLimitStrikes`, a base `Agent` static option, default `3`) only advances while storage still accepts writes, for example when the reset came from another isolate such as D1. None of this shrinks the working set: a turn whose context no longer fits in 128 MB needs a smaller transcript or fewer or smaller tool results.
|
|
740
741
|
|
|
741
742
|
#### Turns waiting on a human are not sealed
|
|
742
743
|
|
package/docs/harnesses/pi.md
CHANGED
|
@@ -11,10 +11,10 @@ Pi owns everything about a run: the transcript, the inbox of steers and follow-u
|
|
|
11
11
|
## Install the Pi packages
|
|
12
12
|
|
|
13
13
|
```sh
|
|
14
|
-
npm install agents @earendil-works/pi-durable @earendil-works/pi-ai
|
|
14
|
+
npm install agents @earendil-works/pi-durable @earendil-works/pi-ai @earendil-works/chord
|
|
15
15
|
```
|
|
16
16
|
|
|
17
|
-
|
|
17
|
+
The three Pi packages are optional peer dependencies of `agents`, at `~1.4.0`. pi-durable is experimental, and its API can change in minor releases, so `agents` supports one Pi minor at a time. 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
18
|
|
|
19
19
|
## Create the harness
|
|
20
20
|
|
|
@@ -32,12 +32,12 @@ export class Assistant extends Agent<Env> {
|
|
|
32
32
|
registry = createRegistry();
|
|
33
33
|
|
|
34
34
|
harness = new PiHarness({
|
|
35
|
-
harness: ({ storage, context }) => {
|
|
35
|
+
harness: ({ storage, context, onWake }) => {
|
|
36
36
|
const models = createModels();
|
|
37
37
|
models.setProvider(this.ai.provider);
|
|
38
38
|
return Harness.open(
|
|
39
39
|
storage,
|
|
40
|
-
{ models, registry: this.registry },
|
|
40
|
+
{ models, registry: this.registry, onWake },
|
|
41
41
|
context
|
|
42
42
|
);
|
|
43
43
|
},
|
|
@@ -58,12 +58,14 @@ export class Assistant extends Agent<Env> {
|
|
|
58
58
|
}
|
|
59
59
|
```
|
|
60
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.
|
|
61
|
+
| Option | What it is |
|
|
62
|
+
| ---------- | ----------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
|
|
63
|
+
| `harness` | Required. Receives `{ storage, context, onWake }` and returns Pi's `Harness`, usually from `Harness.open`. Pass `onWake` to `Harness.open`, so the object can hibernate while Pi's tasks sleep (see [How recovery works](#how-recovery-works)). 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
65
|
|
|
66
|
-
|
|
66
|
+
Create the registry per object, as the `registry` field above does, not at module scope: Pi keeps every `Harness` subscribed to its registry until the `Harness` closes, and an evicted object never closes it.
|
|
67
|
+
|
|
68
|
+
The factory runs once per open: at startup, and again after a storage error (see [How recovery works](#how-recovery-works)), so keep it safe to run more than once. If it throws the operation that triggered it fails and the next operation tries again.
|
|
67
69
|
|
|
68
70
|
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
71
|
|
|
@@ -89,18 +91,22 @@ const wordCount: ToolRegistration<typeof WordCount> = {
|
|
|
89
91
|
// `text` is typed from `parameters` and is validated by Pi.
|
|
90
92
|
async execute({ text }) {
|
|
91
93
|
const words = text.split(/\s+/).filter(Boolean).length;
|
|
92
|
-
return {
|
|
94
|
+
return { output: [{ type: "text", text: String(words) }] };
|
|
93
95
|
}
|
|
94
96
|
};
|
|
95
97
|
|
|
96
|
-
|
|
97
|
-
|
|
98
|
-
|
|
99
|
-
|
|
100
|
-
|
|
101
|
-
|
|
102
|
-
|
|
103
|
-
|
|
98
|
+
// One per object, as a field: `registry = createWritingRegistry();`
|
|
99
|
+
function createWritingRegistry() {
|
|
100
|
+
const registry = createRegistry();
|
|
101
|
+
registry.install({
|
|
102
|
+
name: "writing",
|
|
103
|
+
sections: [
|
|
104
|
+
{ key: "preamble", render: () => "You are an editor.", tag: false }
|
|
105
|
+
],
|
|
106
|
+
tools: [wordCount]
|
|
107
|
+
});
|
|
108
|
+
return registry;
|
|
109
|
+
}
|
|
104
110
|
```
|
|
105
111
|
|
|
106
112
|
- **Tool arguments are typed from the schema.** Type a tool as `ToolRegistration<typeof Parameters>`; a bare `ToolRegistration` types its arguments as `unknown`.
|
|
@@ -117,9 +123,13 @@ Pi's extensions can do more: hooks on model requests and tool calls (`hook(ToolT
|
|
|
117
123
|
```ts
|
|
118
124
|
import { skills } from "agents/harness/pi";
|
|
119
125
|
|
|
120
|
-
harness: async ({ storage, context }) => {
|
|
126
|
+
harness: async ({ storage, context, onWake }) => {
|
|
121
127
|
this.registry.install(await skills(sources));
|
|
122
|
-
return Harness.open(
|
|
128
|
+
return Harness.open(
|
|
129
|
+
storage,
|
|
130
|
+
{ models, registry: this.registry, onWake },
|
|
131
|
+
context
|
|
132
|
+
);
|
|
123
133
|
};
|
|
124
134
|
```
|
|
125
135
|
|
|
@@ -135,14 +145,18 @@ import { browserTool } from "agents/browser/pi";
|
|
|
135
145
|
|
|
136
146
|
readonly browser = new Browser({ provider: browserRun(this.env.BROWSER) });
|
|
137
147
|
|
|
138
|
-
harness: async ({ storage, context }) => {
|
|
148
|
+
harness: async ({ storage, context, onWake }) => {
|
|
139
149
|
this.registry.install({
|
|
140
150
|
name: "browser",
|
|
141
151
|
tools: [
|
|
142
152
|
browserTool({ ctx: this.ctx, browser: this.browser, loader: this.env.LOADER })
|
|
143
153
|
]
|
|
144
154
|
});
|
|
145
|
-
return Harness.open(
|
|
155
|
+
return Harness.open(
|
|
156
|
+
storage,
|
|
157
|
+
{ models, registry: this.registry, onWake },
|
|
158
|
+
context
|
|
159
|
+
);
|
|
146
160
|
};
|
|
147
161
|
|
|
148
162
|
readonly lifecycle = Lifecycle.install(this).use(this.browser).use(this.harness);
|
|
@@ -169,27 +183,76 @@ const receipt = await session.submit("Summarize the latest report", {
|
|
|
169
183
|
const result = await session.wait(receipt.operationId);
|
|
170
184
|
```
|
|
171
185
|
|
|
172
|
-
| Call
|
|
173
|
-
|
|
|
174
|
-
| `prompt(input)`
|
|
175
|
-
| `submit(input, { operationId, whenBusy })` |
|
|
176
|
-
| `wait(operationId, signal?)`
|
|
177
|
-
| `steer(input)`
|
|
178
|
-
| `abort(operationId?)`
|
|
179
|
-
| `pending()`
|
|
180
|
-
| `
|
|
181
|
-
| `
|
|
182
|
-
| `
|
|
183
|
-
| `
|
|
184
|
-
| `
|
|
185
|
-
|
|
186
|
-
|
|
186
|
+
| Call | What it does |
|
|
187
|
+
| ------------------------------------------------- | ------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------ |
|
|
188
|
+
| `prompt(input)` | Submits and waits: the answer, and the transcript after it. |
|
|
189
|
+
| `submit(input, { operationId, whenBusy, entry })` | Hands the input to Pi and returns as soon as Pi has saved it, without waiting for the model. If the session is already working, the input waits until that work finishes. With `whenBusy: "steer"`, it joins the running work instead, after the current round of tool calls. `entry` records where the input came from (see below). |
|
|
190
|
+
| `wait(operationId, signal?)` | The operation's result once Pi settles it. Aborting `signal` stops only the wait, not the work. |
|
|
191
|
+
| `steer(input)` | `submit` with `whenBusy: "steer"`. |
|
|
192
|
+
| `abort(operationId?)` | Withdraws a queued operation, or with no id, stops everything running in the session. |
|
|
193
|
+
| `pending()` | Operations Pi has not settled yet: queued, part of the running work, or `parked` (see [How recovery works](#how-recovery-works)). |
|
|
194
|
+
| `retry()` | Runs again the tasks Pi's restart limit parked in the session's work. Resolves `false` when nothing was parked. |
|
|
195
|
+
| `messages()` | Pi's transcript entries since the newest reset. |
|
|
196
|
+
| `events()` | Pi's agent events: a `snapshot`, then one batch per commit. |
|
|
197
|
+
| `reset(handoff?)` | Starts a new context, optionally from a handoff note. |
|
|
198
|
+
| `setModel(model)` | Changes this session's model. |
|
|
199
|
+
| `busy()` | Whether a run is going. |
|
|
200
|
+
|
|
201
|
+
`harness.prompt()`, `submit()`, `wait()`, `abort()`, `retry()`, `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.
|
|
187
202
|
|
|
188
203
|
`messages()` and `events()` are Pi's own records, not a UI-ready chat transcript. The example below folds them into one.
|
|
189
204
|
|
|
205
|
+
### Label input the user did not type
|
|
206
|
+
|
|
207
|
+
Pi records every submitted input as a `pi.user` entry, as if the user typed it. Your code often submits input of its own, such as a prompt from a scheduled job, a webhook payload or the outcome of an approval. To record where that input came from, pass `entry` with a `kind` of your own and any `data` you want to keep with it:
|
|
208
|
+
|
|
209
|
+
```ts
|
|
210
|
+
await session.submit("The deploy was approved. Run the migration.", {
|
|
211
|
+
entry: { kind: "app.approval", data: { approved: true, by: "ben" } }
|
|
212
|
+
});
|
|
213
|
+
```
|
|
214
|
+
|
|
215
|
+
The model sees no difference: it still gets the text as a user message. Only the transcript changes. The input is stored with your `kind` and `data` instead of `pi.user`, and `messages()` and `events()` return both, so your UI can show it differently, for example as "Approved by ben" rather than as something the user said. Pi reserves kinds that start with `pi.`, so use a prefix of your own, such as `app.`.
|
|
216
|
+
|
|
217
|
+
The kind is a label, not a permission. It records what your code said the input was, and nothing checks it. The model reads the input as if the user wrote it, so do not put anything in it that you would not let the user send.
|
|
218
|
+
|
|
219
|
+
The example app folds the transcript into chat messages, ignores `kind` and `data`, and shows every input as a user message. To show labelled input differently, read them from the entry.
|
|
220
|
+
|
|
221
|
+
### Inputs queued behind a failed run
|
|
222
|
+
|
|
223
|
+
A run fails with `model_error` when the model errors and Pi does not retry or the error outlasts its retries, and with `no_model` when the session has no usable model. Its own operation settles `unanswered` with that reason. Pi's `settings.afterFailedRun` decides what happens to the inputs queued behind it. Refer to [Failures and Recovery](https://github.com/earendil-works/pi/tree/main/packages/durable#failures-and-recovery) in the pi-durable README for what each value does. Set it in the factory:
|
|
224
|
+
|
|
225
|
+
```ts
|
|
226
|
+
harness: ({ storage, context, onWake }) =>
|
|
227
|
+
Harness.open(
|
|
228
|
+
storage,
|
|
229
|
+
{
|
|
230
|
+
models,
|
|
231
|
+
registry: this.registry,
|
|
232
|
+
onWake,
|
|
233
|
+
settings: { afterFailedRun: "withdraw" }
|
|
234
|
+
},
|
|
235
|
+
context
|
|
236
|
+
);
|
|
237
|
+
```
|
|
238
|
+
|
|
239
|
+
| Value | What callers see |
|
|
240
|
+
| ---------------- | -------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
|
|
241
|
+
| `keep` (default) | `pending()` lists the inputs as `queued`, `busy()` is `false`, and `wait()` keeps waiting. The next `submit()` runs them first, in order, then the new input. The next `reset()` runs them in the new context. |
|
|
242
|
+
| `continue` | `wait()` returns their results as usual. If the cause persists, such as a missing model, each queued input fails in its own run. |
|
|
243
|
+
| `withdraw` | `wait()` returns `{ status: "unanswered", reason: "run_failed" }`, and `pending()` no longer lists them. |
|
|
244
|
+
|
|
245
|
+
With `keep`, the kept inputs are not work to Pi: its `wake()` counts tasks, not queued inputs. The wake job completes, and the object can hibernate until the next `submit()` or `reset()`. Pass a `signal` to `wait()` if a caller should not wait that long. To withdraw a kept input, call `abort(operationId)`, which settles it `unanswered` with reason `aborted`. `session.abort()` with no id stops the whole session, which withdraws every kept input too.
|
|
246
|
+
|
|
190
247
|
## How recovery works
|
|
191
248
|
|
|
192
|
-
The harness schedules one lifecycle job
|
|
249
|
+
The harness schedules one lifecycle job, driven by Pi's `wake()`. While Pi has work running, the job waits on it and refreshes itself as a heartbeat. While Pi's tasks sleep, such as a retry backoff, the job is due when the first one wakes. Pi says when through the `onWake` the factory passes to `Harness.open`, and sets no in-memory timer of its own, so the object can hibernate until then. Without `onWake` the harness still works, but Pi's timer keeps the object in memory, and billed, while a task sleeps, and the harness logs a warning. When Pi has nothing to do, the job completes. 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.
|
|
250
|
+
|
|
251
|
+
A task that keeps crashing the object would restart it forever, so Pi parks a task cut off by a crash or eviction `settings.restartLimit` times in a row (default 5) without progress. A parked task is not work: the job completes and the object can hibernate. Its operations show as `parked` in `pending()`, and `wait()` keeps waiting. If your factory passes `onReport` to `Harness.open`, Pi reports it there as `task_parked`. Once you have dealt with the cause, `session.retry()` runs it again and wakes the harness; `session.abort()` gives up on it instead.
|
|
252
|
+
|
|
253
|
+
A storage error closes Pi for good: the call that hit it rejects with the error, and calls and `events()` streams already underway end with Pi's `SessionFailed`, so retry them. If your factory passes `onReport` to `Harness.open`, Pi reports the failure there as `session_failed`. Once the failed Pi has closed, the harness opens Pi again, which runs the factory again, on the next call or the job's next step. Closing waits for running task code, such as a tool call, to return, so code that ignores its abort signal delays the reopen. Pi recovers from what it committed and resumes its work the same way.
|
|
254
|
+
|
|
255
|
+
If a step of the job fails, such as when the factory throws, the job is not dropped. It tries again after 30 seconds, doubling the delay with each failure in a row up to 10 minutes, and goes back to its usual schedule once a step succeeds. A Durable Object memory-limit reset that fails Pi while the job waits on it counts against the alarm's memory-limit circuit breaker. Lifecycle backs the job off and drops it after repeated resets. The next call opens Pi again and pushes the job again, so Pi's restart limit is what eventually parks a task that keeps causing resets.
|
|
193
256
|
|
|
194
257
|
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.
|
|
195
258
|
|
package/docs/lifecycle.md
CHANGED
|
@@ -234,7 +234,13 @@ physical alarm automatically — there is no explicit rearm call. When the
|
|
|
234
234
|
alarm fires, Lifecycle drives due jobs in due order as an event loop, then
|
|
235
235
|
runs host `onAlarm()`, then re-arms from queue state. Before driving any job
|
|
236
236
|
it arms a deadman pre-alarm so an isolate death mid-drive still wakes the
|
|
237
|
-
object to resume.
|
|
237
|
+
object to resume. While the queue holds jobs, it also arms a fallback alarm
|
|
238
|
+
before startup runs, at the next job's time but at least 30 seconds out. An
|
|
239
|
+
alarm whose startup throws still leaves that wake, whether or not the
|
|
240
|
+
platform's own alarm retries run, so the failed startup is retried then. A
|
|
241
|
+
startup that pushes or reschedules jobs and then throws, from an alarm or a
|
|
242
|
+
request, arms the same fallback from the queue as it stands, so those jobs
|
|
243
|
+
keep a wake too.
|
|
238
244
|
|
|
239
245
|
A job's drive result decides what happens next: returning nothing completes
|
|
240
246
|
and deletes it, `{ rescheduleAt }` suspends it until a future time, and
|
package/docs/models-pi-ai.md
CHANGED
|
@@ -43,8 +43,10 @@ design: a model this package cannot name exactly is yours to construct.
|
|
|
43
43
|
created on first use.
|
|
44
44
|
3. **No provider keys.** AI Gateway holds the credential — unified billing, or a key you
|
|
45
45
|
store on the gateway (BYOK) — so nothing vendor-shaped needs a secret in your Worker.
|
|
46
|
-
4. **The binding
|
|
47
|
-
|
|
46
|
+
4. **The binding, or an account id and token.** Workers AI and catalog models go through
|
|
47
|
+
`env.AI.run`, other model objects through the binding's `fetch` to AI Gateway's
|
|
48
|
+
provider-native path. With [an account id and API token](#account-id-and-api-token) the
|
|
49
|
+
same requests go over HTTP instead.
|
|
48
50
|
5. Everything here is in beta and may change in a minor release.
|
|
49
51
|
|
|
50
52
|
## Quick start
|
|
@@ -77,6 +79,26 @@ export default {
|
|
|
77
79
|
};
|
|
78
80
|
```
|
|
79
81
|
|
|
82
|
+
### Account id and API token
|
|
83
|
+
|
|
84
|
+
To use a gateway in another account, or to call from outside a Worker, pass `accountId` and
|
|
85
|
+
`apiToken` instead of `binding`. Everything else is the same, and the gateway still defaults to
|
|
86
|
+
`default`:
|
|
87
|
+
|
|
88
|
+
```ts
|
|
89
|
+
const ai = createAI({
|
|
90
|
+
accountId: "27b146402af2103944379f33841b6234",
|
|
91
|
+
apiToken: env.CLOUDFLARE_API_TOKEN,
|
|
92
|
+
gateway: "project-gateway"
|
|
93
|
+
});
|
|
94
|
+
```
|
|
95
|
+
|
|
96
|
+
Workers AI and catalog models go to AI Gateway's
|
|
97
|
+
[REST API](https://developers.cloudflare.com/ai-gateway/usage/rest-api/), which runs them on
|
|
98
|
+
`/run` as `env.AI.run` does. Other model objects go to the provider-native path on
|
|
99
|
+
`gateway.ai.cloudflare.com`, with the token as `cf-aig-authorization`. Create the token with
|
|
100
|
+
**Workers AI Read** and **AI Gateway Run**; it is sent to those two hosts only.
|
|
101
|
+
|
|
80
102
|
## Where a model comes from
|
|
81
103
|
|
|
82
104
|
```ts
|
|
@@ -144,6 +166,10 @@ A session stores its model as `{ provider, modelId }`, not as a model object, so
|
|
|
144
166
|
`@cf/…` and `<provider>/<id>` forms there. Options given to `ai(model, options)` do not reach a
|
|
145
167
|
session; set them on `createAI` instead.
|
|
146
168
|
|
|
169
|
+
A model stream that is cut off, or that sends nothing for `streamIdleTimeoutMs`, ends in an error
|
|
170
|
+
pi-ai's `isRetryableAssistantError()` treats as transient, so the harness retries the turn under
|
|
171
|
+
its retry settings rather than failing it.
|
|
172
|
+
|
|
147
173
|
`ai.streamFn` is `streamSimple`, bound, for frameworks that take a stream function, such as
|
|
148
174
|
pi-agent-core's `Agent({ streamFn })`.
|
|
149
175
|
|
|
@@ -179,7 +205,7 @@ ai("anthropic/claude-opus-4.8", {
|
|
|
179
205
|
eventId: "turn-17",
|
|
180
206
|
requestTimeoutMs: 30_000,
|
|
181
207
|
retries: { maxAttempts: 3, backoff: "exponential", retryDelayMs: 500 },
|
|
182
|
-
|
|
208
|
+
byokAlias: "team-a", // which stored BYOK key, by alias
|
|
183
209
|
fallback: ["@cf/zai-org/glm-4.7-flash"], // ids or models, in any mix
|
|
184
210
|
headers: { "x-request-source": "docs" },
|
|
185
211
|
sessionAffinity: "chat-42", // Workers AI prefix-cache routing
|
|
@@ -211,20 +237,29 @@ then model, then `createAI` settings.
|
|
|
211
237
|
|
|
212
238
|
## How a request travels
|
|
213
239
|
|
|
214
|
-
| Model
|
|
215
|
-
|
|
|
216
|
-
| `@cf/…`
|
|
217
|
-
|
|
|
218
|
-
|
|
|
219
|
-
|
|
|
220
|
-
|
|
221
|
-
|
|
222
|
-
|
|
223
|
-
|
|
224
|
-
|
|
225
|
-
|
|
226
|
-
|
|
227
|
-
|
|
240
|
+
| Model | `api` | Through the binding | With an account id and token |
|
|
241
|
+
| --------------------------------------- | -------------------- | --------------------------------------------------------- | ------------------------------------------------------ |
|
|
242
|
+
| `@cf/…`, `dynamic/…` | `cloudflare-ai` | `env.AI.run` + Cloudflare's compat layer | the REST API + Cloudflare's compat layer |
|
|
243
|
+
| `anthropic/…` from pi's gateway catalog | `anthropic-messages` | `env.AI.run` → AI Gateway `/run`, Messages in and out | the REST API's `/ai/v1/messages` → AI Gateway `/run` |
|
|
244
|
+
| `openai/…` from pi's gateway catalog | `openai-responses` | `env.AI.run` → AI Gateway `/run`, Responses in and out | the REST API's `/ai/v1/responses` → AI Gateway `/run` |
|
|
245
|
+
| Any other model object | its own `api` | the binding's `fetch` → AI Gateway's provider-native path | `gateway.ai.cloudflare.com` → the provider-native path |
|
|
246
|
+
|
|
247
|
+
A model from pi's Cloudflare AI Gateway catalog runs on `env.AI.run` under the gateway's
|
|
248
|
+
catalog id (`anthropic/claude-opus-4.8`). pi-ai spells Claude ids as Anthropic does
|
|
249
|
+
(`claude-opus-4-8`), so `agents` lists and resolves them with the catalog's dot instead. The body is pi-ai's own,
|
|
250
|
+
built by pi-ai's converter for the model's `api`, and it is sent in that format: `/run`
|
|
251
|
+
converts nothing, so the answer, streams included, is the shape pi-ai's stream processors
|
|
252
|
+
read. `/run` picks the provider and bills through Unified Billing or a key stored on the
|
|
253
|
+
gateway.
|
|
254
|
+
|
|
255
|
+
Any other model object — one from pi's Anthropic, OpenAI or Groq registry, with the vendor's
|
|
256
|
+
own id — goes to AI Gateway's **provider-native path** through the binding's `fetch`, with the
|
|
257
|
+
provider slug taken from the model's `baseUrl` host. The gateway forwards it to the vendor with
|
|
258
|
+
the credential it holds and streams the vendor's own SSE back untouched.
|
|
259
|
+
|
|
260
|
+
Either way no vendor credential leaves your Worker, and the model's `api`, `provider` and
|
|
261
|
+
`id` are never rewritten, so thinking signatures and encrypted reasoning items replay on the
|
|
262
|
+
next turn.
|
|
228
263
|
|
|
229
264
|
The `api` markers in that table are the only ones this package routes, and the table is the
|
|
230
265
|
whole list: `cloudflare-ai`, `anthropic-messages`, `openai-responses`, `openai-completions`.
|
|
@@ -266,8 +301,10 @@ gateway, an unpaid account) become a `CloudflareAIError`, so fallback and retry
|
|
|
266
301
|
way they treat Workers AI failures. A model whose `api` is not one of the four routed markers
|
|
267
302
|
never gets that far: the stream ends with an `error` event naming the `api`.
|
|
268
303
|
|
|
269
|
-
`
|
|
270
|
-
|
|
304
|
+
`byokAlias` picks a stored key by alias when the gateway holds several for one provider. It is
|
|
305
|
+
sent as `cf-aig-byok-alias` on a catalog model and on any other model object. `zdr` turns Zero
|
|
306
|
+
Data Retention on or off for a Unified Billing request, and applies to model objects outside
|
|
307
|
+
pi's gateway catalog only: `/run` does not read it.
|
|
271
308
|
|
|
272
309
|
## Observability
|
|
273
310
|
|
|
@@ -287,6 +324,15 @@ A failed dispatch ends the stream with an `error` event whose message carries a
|
|
|
287
324
|
`cloudflare-error` diagnostic (`status`, `code`, `logId`), and `onResponse` fires with the raw
|
|
288
325
|
status and headers for every request.
|
|
289
326
|
|
|
327
|
+
## Stream resume
|
|
328
|
+
|
|
329
|
+
Through the binding, a model from pi's AI Gateway catalog streams from `/run`, which names the run in
|
|
330
|
+
`cf-aig-run-id` and keeps its events. If the connection drops, the provider reconnects through
|
|
331
|
+
the binding and replays from the last complete event, so the stream continues with nothing lost
|
|
332
|
+
or repeated. It gives up after five drops or once the gateway no longer holds the run, and an
|
|
333
|
+
abort never resumes. Workers AI streams and models on the provider-native path have no run id
|
|
334
|
+
and are not resumed. The run id is on the `cloudflare` diagnostic as `runId`.
|
|
335
|
+
|
|
290
336
|
## Fallback
|
|
291
337
|
|
|
292
338
|
```ts
|
|
@@ -324,7 +370,7 @@ Every other model in the catalog takes the full set.
|
|
|
324
370
|
|
|
325
371
|
## Beta
|
|
326
372
|
|
|
327
|
-
This subpath needs `@earendil-works/pi-ai
|
|
373
|
+
This subpath needs `@earendil-works/pi-ai@~1.4.0`, an optional peer of `agents`. `agents`
|
|
328
374
|
takes no vendor package as a dependency, optional or otherwise. The surface is in beta
|
|
329
375
|
and may change in a minor release. An unlisted `@cf/` id resolves with default metadata (a
|
|
330
376
|
128k context window, 8192 output tokens, zero cost); pass overrides when you know better.
|