agents 0.23.0 → 0.25.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (187) hide show
  1. package/dist/{agent-routing-DE5zmCQ8.d.ts → agent-routing-0DR0Tk2v.d.ts} +270 -190
  2. package/dist/agent-routing.d.ts +1 -1
  3. package/dist/agent-routing.js.map +1 -1
  4. package/dist/agent-tool-types.d.ts +13 -7
  5. package/dist/agent-tool-types.js +23 -1
  6. package/dist/agent-tool-types.js.map +1 -1
  7. package/dist/{agent-tools-y7zLfw4Q.js → agent-tools-CdJC-8Dc.js} +72 -9
  8. package/dist/agent-tools-CdJC-8Dc.js.map +1 -0
  9. package/dist/{agent-tools-DtXMTDGM.d.ts → agent-tools-YrATZJu2.d.ts} +29 -15
  10. package/dist/agent-tools.d.ts +6 -4
  11. package/dist/agent-tools.js.map +1 -1
  12. package/dist/ai-fm7xx1BK.js +312 -0
  13. package/dist/ai-fm7xx1BK.js.map +1 -0
  14. package/dist/base64-redaction-C1Uu29t5.js +59 -0
  15. package/dist/base64-redaction-C1Uu29t5.js.map +1 -0
  16. package/dist/browser/ai-sdk.d.ts +90 -0
  17. package/dist/browser/ai-sdk.js +51 -0
  18. package/dist/browser/ai-sdk.js.map +1 -0
  19. package/dist/browser/ai.d.ts +4 -4
  20. package/dist/browser/ai.js +1 -347
  21. package/dist/browser/index.d.ts +81 -79
  22. package/dist/browser/index.js +361 -2
  23. package/dist/browser/index.js.map +1 -0
  24. package/dist/browser/tanstack-ai.d.ts +96 -1
  25. package/dist/browser/tanstack-ai.js +62 -2
  26. package/dist/browser/tanstack-ai.js.map +1 -1
  27. package/dist/browser-DE3d-RLk.d.ts +275 -0
  28. package/dist/browser-tool-CQ10e6Kj.js +480 -0
  29. package/dist/browser-tool-CQ10e6Kj.js.map +1 -0
  30. package/dist/browser-tool-DuNdlwZ9.d.ts +73 -0
  31. package/dist/{capability-BjSKYpzg.js → capability-B4WbF81e.js} +2 -1
  32. package/dist/capability-B4WbF81e.js.map +1 -0
  33. package/dist/{capability-runner-Be_-PLR1.d.ts → capability-runner-DBAnGr6L.d.ts} +45 -15
  34. package/dist/chat/index.d.ts +341 -232
  35. package/dist/chat/index.js +602 -72
  36. package/dist/chat/index.js.map +1 -1
  37. package/dist/chat/react.d.ts +42 -2
  38. package/dist/chat/react.js +191 -35
  39. package/dist/chat/react.js.map +1 -1
  40. package/dist/chat/transport.d.ts +1 -1
  41. package/dist/chat/transport.js +1 -1
  42. package/dist/chat-sdk/index.d.ts +17 -3
  43. package/dist/chat-sdk/index.js +28 -2
  44. package/dist/chat-sdk/index.js.map +1 -1
  45. package/dist/client-4OlMIJx-.js +591 -0
  46. package/dist/client-4OlMIJx-.js.map +1 -0
  47. package/dist/{client-jagG8a9_.js → client-fyQrLGkD.js} +2 -2
  48. package/dist/{client-jagG8a9_.js.map → client-fyQrLGkD.js.map} +1 -1
  49. package/dist/client-je3koD9Z.d.ts +446 -0
  50. package/dist/client.d.ts +29 -283
  51. package/dist/client.js +2 -274
  52. package/dist/{connector-CptFKzRh.js → connector-58XDGvc3.js} +251 -120
  53. package/dist/connector-58XDGvc3.js.map +1 -0
  54. package/dist/{connector-CkQD4MK3.d.ts → connector-Cl-VqPmA.d.ts} +244 -146
  55. package/dist/context/index.d.ts +29 -0
  56. package/dist/context/index.js +64 -17
  57. package/dist/context/index.js.map +1 -1
  58. package/dist/current-agent-DhoDkSnH.js.map +1 -1
  59. package/dist/current-agent-vAk_5JMt.d.ts +93 -0
  60. package/dist/{current-agent-Da_C9a3b.d.ts → durable-object-lifecycle-HhgOMWq9.d.ts} +26 -127
  61. package/dist/harnesses/pi/index.d.ts +348 -0
  62. package/dist/harnesses/pi/index.js +754 -0
  63. package/dist/harnesses/pi/index.js.map +1 -0
  64. package/dist/images-iiTDSyRu.js +1631 -0
  65. package/dist/images-iiTDSyRu.js.map +1 -0
  66. package/dist/index-BreL0YrQ.d.ts +78 -0
  67. package/dist/index-kbSUIer3.d.ts +343 -0
  68. package/dist/index.d.ts +43 -41
  69. package/dist/index.js +2 -2
  70. package/dist/internal_context.d.ts +1 -1
  71. package/dist/lifecycle/index.d.ts +19 -17
  72. package/dist/lifecycle/index.js +2 -2
  73. package/dist/{lifecycle-CMRGjZdw.js → lifecycle-CFu6OV03.js} +73 -29
  74. package/dist/lifecycle-CFu6OV03.js.map +1 -0
  75. package/dist/mcp/client/index.d.ts +20 -20
  76. package/dist/mcp/client/index.js +1 -1
  77. package/dist/mcp/client/x402.js +21 -19
  78. package/dist/mcp/client/x402.js.map +1 -1
  79. package/dist/mcp/index.d.ts +35 -35
  80. package/dist/mcp/index.js +2 -2
  81. package/dist/{message-builder-BymO4N_D.js → message-builder-DAl9AhHH.js} +89 -4
  82. package/dist/message-builder-DAl9AhHH.js.map +1 -0
  83. package/dist/models/ai-sdk/index.d.ts +353 -0
  84. package/dist/models/ai-sdk/index.js +2443 -0
  85. package/dist/models/ai-sdk/index.js.map +1 -0
  86. package/dist/models/pi-ai/index.d.ts +313 -0
  87. package/dist/models/pi-ai/index.js +1795 -0
  88. package/dist/models/pi-ai/index.js.map +1 -0
  89. package/dist/observability/ai/index.js +20 -4
  90. package/dist/observability/ai/index.js.map +1 -1
  91. package/dist/{protocol-B0nh6KNf.js → protocol-kHsr-f0X.js} +26 -12
  92. package/dist/{protocol-B0nh6KNf.js.map → protocol-kHsr-f0X.js.map} +1 -1
  93. package/dist/queue/index.d.ts +123 -0
  94. package/dist/queue/index.js +2 -0
  95. package/dist/queue-BB-zfIYF.js +406 -0
  96. package/dist/queue-BB-zfIYF.js.map +1 -0
  97. package/dist/react.d.ts +31 -13
  98. package/dist/react.js +190 -71
  99. package/dist/react.js.map +1 -1
  100. package/dist/{retries-D9Ds-1lz.d.ts → retries-CV6AkXmQ.d.ts} +23 -8
  101. package/dist/retries.d.ts +12 -8
  102. package/dist/retries.js +19 -1
  103. package/dist/retries.js.map +1 -1
  104. package/dist/routing/index.d.ts +2 -2
  105. package/dist/routing/index.js +1 -1
  106. package/dist/{scheduler-DD9NdYbF.js → scheduler-DZ3xQfeK.js} +4 -11
  107. package/dist/scheduler-DZ3xQfeK.js.map +1 -0
  108. package/dist/{scheduler-Dwh85ZGl.d.ts → scheduler-RdACNXDP.d.ts} +6 -6
  109. package/dist/schedules/index.d.ts +1 -1
  110. package/dist/schedules/index.js +1 -1
  111. package/dist/serializable.d.ts +1 -1
  112. package/dist/sessions/index.d.ts +70 -3
  113. package/dist/sessions/index.js +276 -67
  114. package/dist/sessions/index.js.map +1 -1
  115. package/dist/skills/index.d.ts +29 -314
  116. package/dist/socket-address.d.ts +16 -0
  117. package/dist/socket-address.js +19 -0
  118. package/dist/socket-address.js.map +1 -0
  119. package/dist/{src-DlSHshb2.js → src-BZeu-SCD.js} +541 -500
  120. package/dist/src-BZeu-SCD.js.map +1 -0
  121. package/dist/state/index.d.ts +6 -0
  122. package/dist/state/index.js +137 -0
  123. package/dist/state/index.js.map +1 -0
  124. package/dist/streams/index.d.ts +1 -1
  125. package/dist/streams/index.js +1 -1
  126. package/dist/{streams-D6tJ0NN9.d.ts → streams-Cmkj9QfW.d.ts} +4 -2
  127. package/dist/{streams-DZKgAj9b.js → streams-DW-UCOUc.js} +9 -2
  128. package/dist/streams-DW-UCOUc.js.map +1 -0
  129. package/dist/sub-routing.d.ts +12 -12
  130. package/dist/tasks/index.d.ts +1 -1
  131. package/dist/tasks/index.js +1 -1
  132. package/dist/{tasks-ylZgBjhj.js → tasks-68DESxf0.js} +3 -3
  133. package/dist/{tasks-ylZgBjhj.js.map → tasks-68DESxf0.js.map} +1 -1
  134. package/dist/{tasks-BRJ5zgya.d.ts → tasks-9YxYtByh.d.ts} +5 -5
  135. package/dist/transport-PfFnt-Ld.d.ts +525 -0
  136. package/dist/transport-protocol-BxyngQ_R.js +25 -0
  137. package/dist/transport-protocol-BxyngQ_R.js.map +1 -0
  138. package/dist/transport-protocol-ZTew5Wso.d.ts +27 -0
  139. package/dist/types-BA4anrjI.d.ts +70 -0
  140. package/dist/voice/index.d.ts +2 -2
  141. package/dist/websockets/index.d.ts +195 -69
  142. package/dist/websockets/index.js +3 -2
  143. package/dist/websockets-Ci-LZFHH.js +1030 -0
  144. package/dist/websockets-Ci-LZFHH.js.map +1 -0
  145. package/dist/wire-types-CnMt6_HR.js.map +1 -1
  146. package/dist/wire-types-CrjKvDyi.d.ts +300 -0
  147. package/dist/workflow-types.d.ts +25 -25
  148. package/dist/workflows.d.ts +21 -21
  149. package/dist/workflows.js +1 -1
  150. package/dist/{ws-chat-transport-UNRIS2xl.d.ts → ws-chat-transport-BbT1A5Lc.d.ts} +51 -2
  151. package/dist/{ws-chat-transport-rWwta645.js → ws-chat-transport-cqMKq3IY.js} +143 -16
  152. package/dist/ws-chat-transport-cqMKq3IY.js.map +1 -0
  153. package/docs/agent-class.md +2 -2
  154. package/docs/agent-tools.md +29 -4
  155. package/docs/browse-the-web.md +87 -5
  156. package/docs/chat-agents.md +124 -16
  157. package/docs/client-sdk.md +30 -0
  158. package/docs/context.md +27 -0
  159. package/docs/durable-execution.md +2 -0
  160. package/docs/harnesses/pi.md +91 -0
  161. package/docs/human-in-the-loop.md +4 -0
  162. package/docs/index.md +4 -1
  163. package/docs/lifecycle.md +114 -23
  164. package/docs/models-pi-ai.md +325 -0
  165. package/docs/models.md +504 -0
  166. package/docs/queue.md +135 -241
  167. package/docs/resumable-streaming.md +2 -0
  168. package/docs/retries.md +1 -1
  169. package/docs/routing.md +13 -13
  170. package/docs/sessions.md +36 -1
  171. package/docs/state.md +1 -0
  172. package/docs/sub-agents.md +12 -3
  173. package/package.json +51 -3
  174. package/dist/agent-tools-y7zLfw4Q.js.map +0 -1
  175. package/dist/browser/ai.js.map +0 -1
  176. package/dist/capability-BjSKYpzg.js.map +0 -1
  177. package/dist/client-tools-aIBO0Fk7.d.ts +0 -53
  178. package/dist/client.js.map +0 -1
  179. package/dist/connector-CptFKzRh.js.map +0 -1
  180. package/dist/lifecycle-CMRGjZdw.js.map +0 -1
  181. package/dist/message-builder-BymO4N_D.js.map +0 -1
  182. package/dist/scheduler-DD9NdYbF.js.map +0 -1
  183. package/dist/src-DlSHshb2.js.map +0 -1
  184. package/dist/streams-DZKgAj9b.js.map +0 -1
  185. package/dist/websockets-DUfRHPRq.js +0 -502
  186. package/dist/websockets-DUfRHPRq.js.map +0 -1
  187. package/dist/ws-chat-transport-rWwta645.js.map +0 -1
package/docs/lifecycle.md CHANGED
@@ -138,11 +138,16 @@ sequentially. Request handling is middleware dispatch: it stops at the first
138
138
  returned `Response`, and returning `undefined` passes the request on. A phase
139
139
  failure propagates, and failed startup can be retried.
140
140
 
141
- A capability installed with `{ fallback: true }` dispatches after every
142
- non-fallback capability, whenever it was installed. This is for a host's
143
- catch-all: `Agent` installs its WebSockets capability as a fallback, so a
144
- subclass that installs request or upgrade middleware from its own
145
- constructor still runs first, even though `Agent`'s constructor ran earlier.
141
+ A capability declares how it claims traffic with `claims`, which defaults to
142
+ `"selective"`. A `"catch-all"` capability dispatches after every other
143
+ capability, whenever it was installed. Catch-alls are unique per dispatch
144
+ hook: Lifecycle refuses a second catch-all for `onRequest` or for
145
+ `onWebSocketUpgrade`, since it could never be reached, but one of each
146
+ coexists. The `WebSockets` capability is a catch-all for upgrades only, so
147
+ an HTTP catch-all installs beside it, and selective HTTP capabilities that
148
+ target their own routes run before both. A subclass that installs request or
149
+ upgrade middleware from its own constructor still runs first, even though
150
+ `Agent`'s constructor ran earlier.
146
151
 
147
152
  Capabilities extending `LifecycleCapability` receive one standard service
148
153
  surface: storage, readiness, startup state, the job queue, a host
@@ -153,8 +158,8 @@ host implicitly.
153
158
 
154
159
  Capability hooks run outside host context, but user callbacks run through
155
160
  `this.lifecycle.runInHostContext(fn)` inside the host invocation context.
156
- Scheduler dispatches its registered callbacks through this boundary, and a
157
- future capability that calls user code should do the same.
161
+ Scheduler and Queue dispatch their registered callbacks through this
162
+ boundary, and a future capability that calls user code should do the same.
158
163
 
159
164
  ## The job queue
160
165
 
@@ -306,8 +311,8 @@ to export `getCurrentAgent()` for the `Agent` class as a compatibility alias.
306
311
 
307
312
  Lifecycle itself does not model WebSockets. Hosts that want connections
308
313
  install the `WebSockets` capability, which owns the subsystem end to end —
309
- it claims upgrades, accepts hibernating sockets, dispatches handlers inside
310
- the host invocation boundary, and answers `getConnections()`:
314
+ it claims upgrades, dispatches handlers inside the host invocation boundary,
315
+ and answers `getConnections()`:
311
316
 
312
317
  ```ts
313
318
  import { WebSockets } from "agents/websockets";
@@ -321,7 +326,8 @@ export class MyObject extends DurableObject<Env> {
321
326
  onMessage: (connection, message) => {
322
327
  connection.send(`echo:${message}`);
323
328
  }
324
- }
329
+ },
330
+ callables: new MyCallables(this)
325
331
  });
326
332
  readonly lifecycle = Lifecycle.install(this).use(this.webSockets);
327
333
  }
@@ -329,18 +335,91 @@ export class MyObject extends DurableObject<Env> {
329
335
 
330
336
  Without the capability installed, WebSocket upgrades are declined.
331
337
 
332
- The capability can also serve remote methods: pass an `RpcTarget` as
333
- `callables` and its prototype methods become the complete remote interface,
334
- served over a Cap'n Web session (`?__agents_rpc=capnweb`). An `Agent` adds
335
- no new surface for this — its `@callable()`-decorated methods are its
336
- interface, served on every wire: natively over the legacy JSON RPC protocol
337
- and, through the decorator-derived target, over the Cap'n Web endpoint.
338
-
339
- Connections use Cloudflare's WebSocket Hibernation API. Idle clients remain
340
- connected while the Durable Object can leave memory; when a message wakes the
341
- object, its constructor and lifecycle startup run again before `onMessage`.
342
- State needed after a wake must be stored durably or through
343
- `connection.setState()`. There is no non-hibernating mode.
338
+ ### A plain host works with `useAgent`
339
+
340
+ The capability speaks the Agent protocol on every connection, so a plain
341
+ Durable Object is reachable from `useAgent` and `AgentClient` exactly like an
342
+ `Agent`:
343
+
344
+ - On connect it sends the identity frame, then the current state when
345
+ `state` is set. The identity frame says when a state frame follows it,
346
+ so the client's `ready` and `identified` wait for that state, and a
347
+ caller awaiting `ready` never reads the default value.
348
+ `protocol` controls this: `true` (default) for every connection, a
349
+ function to decide per connection — `false` marks it no-protocol, so it
350
+ gets no protocol text frames on connect or by broadcast but still sends
351
+ and receives ordinary messages and callables — or `false` to have the
352
+ host drive the sequence itself with `sendConnectFrames()`, or
353
+ `sendIdentity()` and `sendState()` separately (the client then resolves
354
+ `ready` on the identity alone).
355
+ `Agent` passes `false`, because it must decide whether a connection
356
+ belongs to a facet before any frame is sent.
357
+ - `readonly` decides per connection whether state writes over the wire are
358
+ refused; `setReadonly()` flips it later. Both flags are stored in the
359
+ connection's own state under `_cf_` keys, hidden from `connection.state`
360
+ and preserved across `setState`, so they survive hibernation. `Agent`'s
361
+ `isConnectionReadonly`, `setConnectionReadonly`, and
362
+ `isConnectionProtocolEnabled` delegate to the same storage.
363
+ - `callables` is an `RpcTarget` whose prototype methods are the host's
364
+ complete remote interface, reached through `call()` and `stub`. On the
365
+ `cf-websocket` wire the capability answers them as JSON `rpc` frames. On
366
+ the `capnweb` wire they are native Cap'n Web methods on the session root:
367
+ a returned `RpcTarget` arrives as a live stub the client can keep
368
+ calling, a `ReadableStream` streams, and chained calls pipeline. Methods
369
+ run through the host invocation boundary with the calling connection in
370
+ scope.
371
+ - `state` takes a `State` capability (from `agents/state`) and syncs it over
372
+ connections, so `useAgent().state` and `setState()` work against a plain
373
+ host:
374
+
375
+ ```ts
376
+ readonly state = new State({
377
+ initialState: { count: 0 },
378
+ // The state owner decides who hears about a change.
379
+ onChanged: (_state, source) => this.webSockets.broadcastState(source)
380
+ });
381
+ readonly webSockets = new WebSockets({ state: this.state });
382
+ readonly lifecycle = Lifecycle.install(this)
383
+ .use(this.state)
384
+ .use(this.webSockets);
385
+ ```
386
+
387
+ The current value is pushed to each new connection after identity. A
388
+ client's `cf_agent_state` frame goes through the `State` capability, so
389
+ the host's own `validateStateChange` decides; a readonly connection is
390
+ refused, and a rejected change is logged server-side and answered with a
391
+ generic `cf_agent_state_error`. `broadcastState(source)` pushes the
392
+ current value to every protocol-enabled connection except `source`. This
393
+ is distinct from `connection.setState()`, which is per-connection and
394
+ never leaves the host. Without the option, state is never sent or
395
+ accepted over connections.
396
+
397
+ - Any other frame goes to `handlers.onMessage`.
398
+
399
+ An `Agent` adds no new surface for this: its `@callable()`-decorated methods
400
+ are its JSON-wire interface, answered by its own message handler. They are
401
+ not mirrored onto the Cap'n Web root; an Agent that wants native calls on
402
+ `capnweb` passes a `callables` target like any other host.
403
+
404
+ ### Two wires
405
+
406
+ The client chooses how frames travel:
407
+
408
+ - **`cf-websocket`** (default) — accepted with Cloudflare's WebSocket Hibernation
409
+ API. Idle clients remain connected while the Durable Object leaves memory;
410
+ when a message wakes it, the constructor and lifecycle startup run again
411
+ before `onMessage`. State needed after a wake belongs in storage or
412
+ `connection.setState()`.
413
+ - **Cap'n Web** (`?__agents_transport=capnweb`, or
414
+ `useAgent({ transport: "capnweb" })`) — protocol frames travel through one
415
+ pipe method on a Cap'n Web session whose root also carries the host's
416
+ `callables` natively. The connection is an in-memory socket and does not
417
+ hibernate: the object stays pinned while it is open, and clients reconnect
418
+ after an eviction.
419
+
420
+ Handlers are wire-agnostic. Both kinds of connection dispatch the same
421
+ `onConnect`/`onMessage`/`onClose`/`onError`, appear in `getConnections()`,
422
+ and honour `connection.close(code, reason)`.
344
423
 
345
424
  ## Native RPC
346
425
 
@@ -354,7 +433,19 @@ async runTask(): Promise<void> {
354
433
  }
355
434
  ```
356
435
 
357
- Agent's internal RPC entry points already enforce this boundary.
436
+ An `Agent` enforces this boundary for you. Its internal RPC entry points and
437
+ the public `async` methods your subclass exposes over RPC start the lifecycle
438
+ before they run, so a cold instance does not serve those calls against
439
+ uninitialized state. Synchronous methods are never deferred; if one reads state
440
+ that `onStart` sets up, make it `async` or call `await this.lifecycle.start()`
441
+ in it. Concurrent calls to `start()` share the same startup operation, and
442
+ `lifecycle.isStarted()` returns `true` only after capabilities and `onStart()`
443
+ have finished.
444
+
445
+ Startup resolves the Durable Object name. An async RPC to an Agent addressed
446
+ with `newUniqueId()` or `idFromString()` fails instead of running before
447
+ initialization. Address the Agent by name with `getAgentByName()`, `getByName()`,
448
+ or `idFromName()`.
358
449
 
359
450
  ## Object names
360
451
 
@@ -0,0 +1,325 @@
1
+ # Models for pi-ai (Experimental)
2
+
3
+ `agents/models/pi-ai` is the [pi-ai](https://github.com/earendil-works/pi/tree/main/packages/ai)
4
+ twin of [`agents/models/ai-sdk`](./models.md): the same `createAI` factory, the same options,
5
+ the same gateway, returning pi-ai `Model` objects instead of AI SDK ones. Use it with pi-ai's
6
+ `stream`/`complete`, with [pi-durable](https://github.com/earendil-works/pi/tree/main/packages/durable)'s
7
+ `Harness`, or with any framework built on a pi-ai `Models` registry.
8
+
9
+ ## The rule
10
+
11
+ **Workers AI is the catalog Cloudflare keeps up with. Every other vendor is pi-ai's.**
12
+
13
+ `agents` ships no vendor ids, wire formats, thinking rules or effort tables for any model
14
+ that is not `@cf/…`. A third-party model comes from pi-ai's own registry — the package that
15
+ tracks that vendor — and `agents` only routes it through AI Gateway. So there are two call
16
+ forms:
17
+
18
+ ```ts
19
+ import { createAI } from "agents/models/pi-ai";
20
+ import { getBuiltinModel } from "@earendil-works/pi-ai/providers/all";
21
+
22
+ const ai = createAI({ binding: env.AI, id: "prod", cacheTtl: 60 });
23
+
24
+ ai("@cf/zai-org/glm-4.7-flash"); // Workers AI: our catalog, our compat layer
25
+ ai(getBuiltinModel("anthropic", "claude-opus-4-8")); // their model, our transport
26
+ ```
27
+
28
+ For convenience, pi-ai also ships a generated catalog of the models Cloudflare's gateway
29
+ serves, so a `<provider>/<id>` string resolves without importing anything:
30
+
31
+ ```ts
32
+ ai("anthropic/claude-opus-4.8"); // the same model, as AI Gateway's catalog names it
33
+ ai("openai/gpt-5.4");
34
+ ```
35
+
36
+ Any other string is a `TypeError` that names the way in. There is no prefix guessing here by
37
+ design: a model this package cannot name exactly is yours to construct.
38
+
39
+ 1. **Workers AI ids start with `@cf/`.** Everything else is a vendor's, and comes from a
40
+ vendor registry pi-ai maintains.
41
+ 2. **The gateway is an option, not a product.** Caching, logging, metadata, timeouts and
42
+ retries are options on `createAI`, on the model, or on the call; the `default` gateway is
43
+ created on first use.
44
+ 3. **No provider keys.** AI Gateway holds the credential — unified billing, or a key you
45
+ store on the gateway (BYOK) — so nothing vendor-shaped needs a secret in your Worker.
46
+ 4. **The binding is the only way in.** Workers AI goes through `env.AI.run`, vendor models
47
+ through `env.AI.gateway(id).run`; there is no HTTP transport and no API token to hold.
48
+ 5. Everything here is experimental and may change in a minor release.
49
+
50
+ ## Quick start
51
+
52
+ ```jsonc
53
+ // wrangler.jsonc
54
+ { "ai": { "binding": "AI" } }
55
+ ```
56
+
57
+ ```sh
58
+ npm install agents @earendil-works/pi-ai
59
+ ```
60
+
61
+ ```ts
62
+ import { createAI } from "agents/models/pi-ai";
63
+
64
+ export default {
65
+ async fetch(_request: Request, env: { AI: Ai }) {
66
+ const ai = createAI({ binding: env.AI });
67
+ const reply = await ai.complete(ai("@cf/zai-org/glm-4.7-flash"), {
68
+ messages: [{ role: "user", content: "Hello", timestamp: Date.now() }]
69
+ });
70
+ // `content` is an array of parts — text, thinking, tool calls.
71
+ const text = reply.content
72
+ .filter((part) => part.type === "text")
73
+ .map((part) => part.text)
74
+ .join("");
75
+ return Response.json({ text });
76
+ }
77
+ };
78
+ ```
79
+
80
+ ## Where a model comes from
81
+
82
+ ```ts
83
+ // 1. Workers AI — ours.
84
+ ai("@cf/zai-org/glm-4.7-flash");
85
+
86
+ // 2. pi-ai's Cloudflare AI Gateway catalog — a string, resolved by pi, not us.
87
+ ai("anthropic/claude-opus-4.8");
88
+
89
+ // 3. Any pi-ai model object, from any registry you imported.
90
+ import { anthropicProvider } from "@earendil-works/pi-ai/providers/anthropic";
91
+ const claude = anthropicProvider()
92
+ .getModels()
93
+ .find((m) => m.id === "claude-opus-4-8")!;
94
+ ai(claude);
95
+ ```
96
+
97
+ Forms 2 and 3 keep the model's own metadata verbatim — `api`, `compat`, `thinkingLevelMap`,
98
+ cost, context window, max tokens. That metadata is why Claude Opus gets
99
+ `thinking: { type: "adaptive" }` and GPT-5 gets `reasoning.effort`: the model's author said
100
+ so, and this package does not second-guess it. Form 3 never mutates the object you
101
+ passed; the options ride on a copy.
102
+
103
+ ## Running a model
104
+
105
+ The instance carries pi-ai's four entry points, so nothing global is registered:
106
+
107
+ ```ts
108
+ const model = ai("anthropic/claude-opus-4.8");
109
+ const context = {
110
+ messages: [{ role: "user", content: "Hi", timestamp: Date.now() }]
111
+ };
112
+
113
+ const message = await ai.complete(model, context); // AssistantMessage
114
+ for await (const event of ai.stream(model, context)) {
115
+ // AssistantMessageEvent: text_delta, thinking_delta, toolcall_*, done, error
116
+ }
117
+ await ai.completeSimple(model, context, { reasoning: "low" }); // SimpleStreamOptions
118
+ ai.streamSimple(model, context, { reasoning: "high", maxTokens: 2048 });
119
+ ```
120
+
121
+ ### With pi-durable
122
+
123
+ pi-durable's `Harness` takes a `Models` registry. Register the provider on one and name the
124
+ model by id:
125
+
126
+ ```ts
127
+ import { Harness } from "@earendil-works/pi-durable";
128
+ import { createModels } from "@earendil-works/pi-ai/models";
129
+
130
+ const models = createModels();
131
+ models.setProvider(ai.provider);
132
+ const harness = await Harness.open(storage, { models, registry }, context);
133
+ // a session's model: { provider: "cloudflare", modelId: "anthropic/claude-opus-4.8" }
134
+ ```
135
+
136
+ A session stores its model as `{ provider, modelId }`, not as a model object, so use the
137
+ `@cf/…` and `<provider>/<id>` forms there. Options given to `ai(model, options)` do not reach a
138
+ session; set them on `createAI` instead.
139
+
140
+ `ai.streamFn` is `streamSimple`, bound, for frameworks that take a stream function, such as
141
+ pi-agent-core's `Agent({ streamFn })`.
142
+
143
+ ### With a pi-ai `Models` registry
144
+
145
+ ```ts
146
+ import { createModels } from "@earendil-works/pi-ai/models";
147
+
148
+ const models = createModels();
149
+ models.setProvider(ai.provider); // provider id: "cloudflare"
150
+ models.getModel("cloudflare", "anthropic/claude-opus-4.8");
151
+ models.streamSimple(ai("@cf/zai-org/glm-4.7-flash"), context);
152
+ ```
153
+
154
+ The provider's static list is every Workers AI id pi-ai knows plus every `<provider>/<id>` in
155
+ its Cloudflare AI Gateway catalog, so `cloudflare/<id>` specifiers resolve. A model object
156
+ you pass to `ai(model)` streams whether or not it is on that list.
157
+
158
+ ## Options
159
+
160
+ Gateway options may be given flat or nested, on any of the three layers:
161
+
162
+ ```ts
163
+ const ai = createAI({ binding: env.AI, id: "prod", cacheTtl: 60 });
164
+
165
+ ai("anthropic/claude-opus-4.8", {
166
+ gateway: { id: "my-gateway" }, // or a string id
167
+ cacheTtl: 3600, // the same keys, flat
168
+ skipCache: false,
169
+ cacheKey: "greeting-v1",
170
+ collectLog: true,
171
+ metadata: { team: "growth" },
172
+ eventId: "turn-17",
173
+ requestTimeoutMs: 30_000,
174
+ retries: { maxAttempts: 3, backoff: "exponential", retryDelayMs: 500 },
175
+ fallback: ["@cf/zai-org/glm-4.7-flash"], // ids or models, in any mix
176
+ headers: { "x-request-source": "docs" },
177
+ sessionAffinity: "chat-42", // Workers AI prefix-cache routing
178
+ reasoningEffort: "low", // "low" | "medium" | "high" | null
179
+ chatTemplateKwargs: { enable_thinking: false } // Workers AI only
180
+ });
181
+ ```
182
+
183
+ `reasoningEffort` and `chatTemplateKwargs` are Workers AI settings. On a model object or a
184
+ `<slug>/<id>` model they are not sent; the drop is recorded on the message as a
185
+ `cloudflare-compat` diagnostic, and the pi `reasoning` level (mapped by the model's own
186
+ metadata) is the way to ask a vendor model to think. A string in `fallback` follows the same
187
+ rule as `ai(string)`: a `@cf/` id or a registry-known `<slug>/<id>`, never a raw vendor id.
188
+
189
+ The `@cf/` form also takes pi-ai metadata overrides — `name`, `contextWindow`, `maxTokens`,
190
+ `reasoning`, `input`, `cost`, `streamIdleTimeoutMs` — because the Workers AI catalog moves
191
+ faster than any generated registry. A model object brings its own; nothing here overrides it.
192
+
193
+ A pi-ai model is plain data, and these per-model options ride on it under symbol keys, which
194
+ `JSON.stringify` and `structuredClone` drop. Routing survives that — a model stored in `Agent`
195
+ state and read back still reaches its vendor under the vendor's own id — but the options do
196
+ not, so re-apply them with `ai(model, options)` after loading if the model carried any.
197
+
198
+ Per call, pi-ai's own options map onto the same layers: `headers` are merged last, `metadata`
199
+ (scalars) joins the gateway metadata, `sessionId` sets session affinity, and `reasoning` (a pi
200
+ thinking level) is remapped through the model's own `thinkingLevelMap` — as far as the wire
201
+ can carry it; see [thinking shapes](#thinking-shapes) for the one limit. Precedence is call,
202
+ then model, then `createAI` settings.
203
+
204
+ ## How a request travels
205
+
206
+ | Model | `api` | Transport |
207
+ | ------------------------ | -------------------- | ----------------------------------------- |
208
+ | `@cf/…` | `cloudflare-ai` | `env.AI.run` + Cloudflare's compat layer |
209
+ | Anthropic (any registry) | `anthropic-messages` | gateway universal → `v1/messages` |
210
+ | OpenAI (any registry) | `openai-responses` | gateway universal → `v1/responses` |
211
+ | Groq, DeepSeek, xAI, … | `openai-completions` | gateway universal → `v1/chat/completions` |
212
+
213
+ Everything that is not `@cf/` goes through **AI Gateway's universal request**: the vendor's
214
+ own body, built by pi-ai's own converter, posted to the gateway with the provider slug taken
215
+ from the model's `baseUrl` host. The gateway forwards it to the vendor with the credential it
216
+ holds, and streams the vendor's own SSE back untouched — pi-ai's stream processors parse it,
217
+ as they would if you had called the vendor directly, the thinking knobs aside (below). No
218
+ credential header leaves your Worker, and the model's `api`, `provider` and `id` are never
219
+ rewritten, so thinking signatures and encrypted reasoning items replay on the next turn.
220
+
221
+ The `api` markers in that table are the only ones this package routes, and the table is the
222
+ whole list: `cloudflare-ai`, `anthropic-messages`, `openai-responses`, `openai-completions`.
223
+ Check your model's `api` before you pass it — several pi registries declare something else and
224
+ are not routable today, among them `mistral` (`mistral-conversations`), `google`
225
+ (`google-generative-ai`), `google-vertex`, `amazon-bedrock`, `azure-openai-responses` and
226
+ `openai-codex`.
227
+
228
+ Workers AI stays on `env.AI.run`. Its departures from strict OpenAI chat completions (native
229
+ events, heartbeats, per-delta usage, per-family request quirks) are absorbed by the same
230
+ compat layer the [AI SDK provider](./models.md) uses, so the parser only ever sees strict
231
+ OpenAI chunks. Anything the layer had to drop is recorded on the message as a
232
+ `cloudflare-compat` diagnostic.
233
+
234
+ An `api` this package cannot route, or a `baseUrl` whose host AI Gateway does not serve, ends
235
+ the stream with an `error` event naming it.
236
+
237
+ ### Thinking shapes
238
+
239
+ On the `openai-completions` wire this package builds one thinking shape: OpenAI's
240
+ `reasoning_effort`, with the level remapped through the model's own `thinkingLevelMap`, and
241
+ only when the model reasons and its `compat` does not say `supportsReasoningEffort: false`.
242
+
243
+ A model whose `compat.thinkingFormat` is anything else — `deepseek`, `zai`, `openrouter`,
244
+ `together`, `ant-ling`, `qwen`, `qwen-chat-template`, `chat-template`, `string-thinking` — has
245
+ its reasoning level **dropped**, and the drop recorded on the message as a `cloudflare-compat`
246
+ diagnostic. Those shapes are the vendor's, and building them here would mean keeping vendor
247
+ wire formats in `agents`, which is the one thing this package does not do: reach such a model
248
+ through pi-ai's own provider if you need its thinking shape. The `anthropic-messages` and
249
+ `openai-responses` wires build the thinking shape their own model metadata declares.
250
+
251
+ ## Unified billing and BYOK
252
+
253
+ Most vendors work with no key at all: the gateway bills them to your Cloudflare account. A
254
+ vendor that is not on unified billing answers with its own error until you store a key on the
255
+ gateway — Google AI Studio, reached through a routable model, returns `403 unregistered
256
+ callers`. That error is the vendor's, surfaced as-is; the gateway's own failures (no such
257
+ gateway, an unpaid account) become a `CloudflareAIError`, so fallback and retry treat them the
258
+ way they treat Workers AI failures. A model whose `api` is not one of the four routed markers
259
+ never gets that far: the stream ends with an `error` event naming the `api`.
260
+
261
+ ## Observability
262
+
263
+ Every assistant message carries a `cloudflare` diagnostic with the gateway correlation for its
264
+ response:
265
+
266
+ ```ts
267
+ const message = await ai.complete(model, context);
268
+ const cloudflare = message.diagnostics?.find(
269
+ (d) => d.type === "cloudflare"
270
+ )?.details;
271
+ // { model, gateway, provider?, specifier?, logId?, eventId?, cacheStatus?,
272
+ // step?, traceId?, runId? }
273
+ ```
274
+
275
+ A failed dispatch ends the stream with an `error` event whose message carries a
276
+ `cloudflare-error` diagnostic (`status`, `code`, `logId`), and `onResponse` fires with the raw
277
+ status and headers for every request.
278
+
279
+ ## Fallback
280
+
281
+ ```ts
282
+ ai(claude, { fallback: ["@cf/zai-org/glm-4.7-flash"] });
283
+ ```
284
+
285
+ Legs may be Workers AI ids or models, in any mix. They are tried in order: a leg is abandoned
286
+ when its stream ends with an `error` before it produced any output, and once a leg has
287
+ produced output it is committed to. The message that answers carries a `cloudflare-fallback`
288
+ diagnostic listing the abandoned attempts, and `message.model` names the model that actually
289
+ answered.
290
+
291
+ A leg travels with the chain's gateway options — the chain's `id`, `cacheTtl` and the rest win
292
+ over a leg's own, and `metadata` merges — as they do in the [AI SDK provider](./models.md). A
293
+ leg built as `ai(model, options)` keeps everything else it was built with. A leg named by a
294
+ `@cf/` id has no options of its own, so it also inherits the chain's `headers`, session
295
+ affinity and Workers AI knobs; it never inherits the chain's model metadata (`name`, `cost`,
296
+ `contextWindow`, `maxTokens`, `reasoning`, `input`), which describes the model it was written
297
+ on — the leg's usage and cost are its own registry entry's.
298
+
299
+ ## Images
300
+
301
+ ```ts
302
+ const images = ai.images("@cf/black-forest-labs/flux-1-schnell", { steps: 8 });
303
+ const result = await ai.generateImages(images, {
304
+ input: [{ type: "text", text: "A red bicycle" }]
305
+ });
306
+ ```
307
+
308
+ Image generation is Workers AI only. The knobs a model takes are the model's:
309
+ `flux-1-schnell` takes a prompt and `steps` and nothing else, so `width`,
310
+ `height`, `seed`, `guidance` and `negativePrompt` are dropped for it and listed
311
+ in a `cloudflare-compat` diagnostic on the result rather than sent and ignored.
312
+ Every other model in the catalog takes the full set.
313
+
314
+ ## Experimental
315
+
316
+ This subpath needs `@earendil-works/pi-ai@^0.99.2`, an optional peer of `agents`. `agents`
317
+ takes no vendor package as a dependency, optional or otherwise. The surface is experimental
318
+ and may change in a minor release. An unlisted `@cf/` id resolves with default metadata (a
319
+ 128k context window, 8192 output tokens, zero cost); pass overrides when you know better.
320
+
321
+ ## See also
322
+
323
+ - [Models for the AI SDK](./models.md) — the same factory for the Vercel AI SDK
324
+ - [pi-ai](https://github.com/earendil-works/pi/tree/main/packages/ai) and
325
+ [pi-durable](https://github.com/earendil-works/pi/tree/main/packages/durable)