agents 0.25.0 → 0.27.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 (238) hide show
  1. package/README.md +8 -9
  2. package/dist/{agent-routing-0DR0Tk2v.d.ts → agent-routing-Bhdbvf7s.d.ts} +13 -8
  3. package/dist/agent-routing.d.ts +1 -1
  4. package/dist/agent-tool-types.d.ts +1 -1
  5. package/dist/{agent-tools-YrATZJu2.d.ts → agent-tools-bJZwBMfq.d.ts} +2 -2
  6. package/dist/agent-tools.d.ts +1 -1
  7. package/dist/ai-sdk-OoGkFy3s.js +2444 -0
  8. package/dist/ai-sdk-OoGkFy3s.js.map +1 -0
  9. package/dist/apply-chunks-1FheRb9W.js +99 -0
  10. package/dist/apply-chunks-1FheRb9W.js.map +1 -0
  11. package/dist/broadcast-state-CEyI4uAK.js +69 -0
  12. package/dist/broadcast-state-CEyI4uAK.js.map +1 -0
  13. package/dist/browser/ai-sdk.d.ts +1 -1
  14. package/dist/browser/ai-sdk.js +2 -2
  15. package/dist/browser/ai-sdk.js.map +1 -1
  16. package/dist/browser/index.d.ts +1 -1
  17. package/dist/browser/tanstack-ai.d.ts +1 -1
  18. package/dist/browser/tanstack-ai.js +1 -1
  19. package/dist/{browser-DE3d-RLk.d.ts → browser-tmkcAAI3.d.ts} +2 -2
  20. package/dist/{browser-tool-DuNdlwZ9.d.ts → browser-tool-Bn-nzJQI.d.ts} +3 -3
  21. package/dist/{browser-tool-CQ10e6Kj.js → browser-tool-CBqs6hqa.js} +58 -18
  22. package/dist/browser-tool-CBqs6hqa.js.map +1 -0
  23. package/dist/capability-B4WbF81e.js.map +1 -1
  24. package/dist/{capability-runner-DBAnGr6L.d.ts → capability-runner-O5VbQzqg.d.ts} +9 -1
  25. package/dist/channel-Bak_6zDP.d.ts +333 -0
  26. package/dist/chat/index.d.ts +11 -9
  27. package/dist/chat/index.js +5 -194
  28. package/dist/chat/index.js.map +1 -1
  29. package/dist/chat/react.d.ts +2 -4
  30. package/dist/chat/react.js +3 -1
  31. package/dist/chat/react.js.map +1 -1
  32. package/dist/chat-sdk/index.d.ts +1 -1
  33. package/dist/chat-sdk/index.js +1 -1
  34. package/dist/cli.js +144 -0
  35. package/dist/{client-fyQrLGkD.js → client-1tEQ1Oqz.js} +27 -2
  36. package/dist/{client-fyQrLGkD.js.map → client-1tEQ1Oqz.js.map} +1 -1
  37. package/dist/{client-je3koD9Z.d.ts → client-DS3Dz9l0.d.ts} +2 -2
  38. package/dist/client-tools-aIBO0Fk7.d.ts +53 -0
  39. package/dist/client.d.ts +1 -1
  40. package/dist/conversations-DH0ut_8n.d.ts +192 -0
  41. package/dist/{current-agent-vAk_5JMt.d.ts → current-agent-C07C5o5Y.d.ts} +3 -3
  42. package/dist/daemon-core-Buubj-y1.js +481 -0
  43. package/dist/daemon-core-Buubj-y1.js.map +1 -0
  44. package/dist/daemon-core-CbohVPAu.d.ts +132 -0
  45. package/dist/{do-oauth-client-provider-Tmf1vgKz.d.ts → do-oauth-client-provider-DfsUcpc3.d.ts} +7 -1
  46. package/dist/{durable-object-lifecycle-HhgOMWq9.d.ts → durable-object-lifecycle-DRqp475_.d.ts} +2 -2
  47. package/dist/{channels/email.d.ts → experimental/channels/email/index.d.ts} +25 -12
  48. package/dist/{channels/email.js → experimental/channels/email/index.js} +13 -35
  49. package/dist/experimental/channels/email/index.js.map +1 -0
  50. package/dist/experimental/channels/index.d.ts +270 -0
  51. package/dist/experimental/channels/index.js +1186 -0
  52. package/dist/experimental/channels/index.js.map +1 -0
  53. package/dist/{channels/slack.d.ts → experimental/channels/slack/index.d.ts} +19 -9
  54. package/dist/experimental/channels/slack/index.js +267 -0
  55. package/dist/experimental/channels/slack/index.js.map +1 -0
  56. package/dist/{channels/telegram.d.ts → experimental/channels/telegram/index.d.ts} +19 -9
  57. package/dist/experimental/channels/telegram/index.js +198 -0
  58. package/dist/experimental/channels/telegram/index.js.map +1 -0
  59. package/dist/experimental/channels/web/ai-sdk.d.ts +55 -0
  60. package/dist/experimental/channels/web/ai-sdk.js +266 -0
  61. package/dist/experimental/channels/web/ai-sdk.js.map +1 -0
  62. package/dist/experimental/channels/web/client.d.ts +97 -0
  63. package/dist/experimental/channels/web/client.js +349 -0
  64. package/dist/experimental/channels/web/client.js.map +1 -0
  65. package/dist/experimental/channels/web/index.d.ts +189 -0
  66. package/dist/experimental/channels/web/index.js +332 -0
  67. package/dist/experimental/channels/web/index.js.map +1 -0
  68. package/dist/gateway-BDC07gYO.d.ts +100 -0
  69. package/dist/handle-zAzh0UW2.d.ts +460 -0
  70. package/dist/harness/ai-sdk/index.d.ts +175 -0
  71. package/dist/harness/ai-sdk/index.js +605 -0
  72. package/dist/harness/ai-sdk/index.js.map +1 -0
  73. package/dist/harness/container/daemon/index.d.ts +76 -0
  74. package/dist/harness/container/daemon/index.js +3 -0
  75. package/dist/harness/container/index.d.ts +740 -0
  76. package/dist/harness/container/index.js +2152 -0
  77. package/dist/harness/container/index.js.map +1 -0
  78. package/dist/harness/container/runtime/index.d.ts +144 -0
  79. package/dist/harness/container/runtime/index.js +1082 -0
  80. package/dist/harness/container/runtime/index.js.map +1 -0
  81. package/dist/harness/opencode/index.d.ts +369 -0
  82. package/dist/harness/opencode/index.js +1072 -0
  83. package/dist/harness/opencode/index.js.map +1 -0
  84. package/dist/{harnesses → harness}/pi/index.d.ts +85 -88
  85. package/dist/{harnesses → harness}/pi/index.js +196 -77
  86. package/dist/harness/pi/index.js.map +1 -0
  87. package/dist/harness/store/index.d.ts +24 -0
  88. package/dist/harness/store/index.js +2 -0
  89. package/dist/harness/think/index.d.ts +821 -0
  90. package/dist/harness/think/index.js +2529 -0
  91. package/dist/harness/think/index.js.map +1 -0
  92. package/dist/harness-B6B9S3rb.d.ts +203 -0
  93. package/dist/{images-iiTDSyRu.js → images-Ce9SCu3X.js} +2 -248
  94. package/dist/images-Ce9SCu3X.js.map +1 -0
  95. package/dist/{index-BreL0YrQ.d.ts → index-D0A5tbWu.d.ts} +2 -2
  96. package/dist/index-Ib_4dwOo.d.ts +200 -0
  97. package/dist/index.d.ts +5 -5
  98. package/dist/index.js +2 -2
  99. package/dist/ingress-EFbrS547.js +9 -0
  100. package/dist/ingress-EFbrS547.js.map +1 -0
  101. package/dist/input-schema-DNrpTu49.js +23 -0
  102. package/dist/input-schema-DNrpTu49.js.map +1 -0
  103. package/dist/internal-kmw70xwR.js +46 -0
  104. package/dist/internal-kmw70xwR.js.map +1 -0
  105. package/dist/internal_context.d.ts +1 -1
  106. package/dist/lifecycle/index.d.ts +3 -3
  107. package/dist/mcp/client/do-oauth-client-provider.d.ts +1 -1
  108. package/dist/mcp/client/do-oauth-client-provider.js +9 -0
  109. package/dist/mcp/client/do-oauth-client-provider.js.map +1 -1
  110. package/dist/mcp/client/index.d.ts +1 -1
  111. package/dist/mcp/client/index.js +1 -1
  112. package/dist/mcp/client/x402.d.ts +35 -4
  113. package/dist/mcp/client/x402.js +24 -3
  114. package/dist/mcp/client/x402.js.map +1 -1
  115. package/dist/mcp/index.d.ts +1 -1
  116. package/dist/mcp/index.js +2 -2
  117. package/dist/models/ai-sdk/index.d.ts +16 -14
  118. package/dist/models/ai-sdk/index.js +1 -2442
  119. package/dist/models/opencode/index.d.ts +65 -0
  120. package/dist/models/opencode/index.js +285 -0
  121. package/dist/models/opencode/index.js.map +1 -0
  122. package/dist/models/pi-ai/index.d.ts +23 -21
  123. package/dist/models/pi-ai/index.js +7 -6
  124. package/dist/models/pi-ai/index.js.map +1 -1
  125. package/dist/options-pwTNq9z6.d.ts +134 -0
  126. package/dist/protocol-BEgrJYC5.d.ts +331 -0
  127. package/dist/protocol-CukuI73M.d.ts +386 -0
  128. package/dist/protocol-cOklVBBD.js +227 -0
  129. package/dist/protocol-cOklVBBD.js.map +1 -0
  130. package/dist/{protocol-kHsr-f0X.js → protocol-eXeJxhgu.js} +2 -67
  131. package/dist/protocol-eXeJxhgu.js.map +1 -0
  132. package/dist/protocol-y2sSY584.js +148 -0
  133. package/dist/protocol-y2sSY584.js.map +1 -0
  134. package/dist/queue/index.d.ts +1 -1
  135. package/dist/react.d.ts +2 -2
  136. package/dist/routing/index.d.ts +2 -2
  137. package/dist/{scheduler-RdACNXDP.d.ts → scheduler-CrFCnv_J.d.ts} +2 -2
  138. package/dist/schedules/index.d.ts +1 -1
  139. package/dist/serializable.d.ts +1 -1
  140. package/dist/sessions/index.d.ts +11 -434
  141. package/dist/sessions/index.js +1 -2271
  142. package/dist/sessions-B9pNuxdT.js +2272 -0
  143. package/dist/sessions-B9pNuxdT.js.map +1 -0
  144. package/dist/settings-C45hDliX.js +253 -0
  145. package/dist/settings-C45hDliX.js.map +1 -0
  146. package/dist/{transport-PfFnt-Ld.d.ts → settings-NgGwF7Qt.d.ts} +14 -208
  147. package/dist/{src-BZeu-SCD.js → src-DgCbZtIR.js} +4 -4
  148. package/dist/{src-BZeu-SCD.js.map → src-DgCbZtIR.js.map} +1 -1
  149. package/dist/stall-watchdog-BpRF2uwG.js +195 -0
  150. package/dist/stall-watchdog-BpRF2uwG.js.map +1 -0
  151. package/dist/state/index.d.ts +1 -1
  152. package/dist/store-DLeqKuj3.d.ts +310 -0
  153. package/dist/store-vgcPYo8K.js +432 -0
  154. package/dist/store-vgcPYo8K.js.map +1 -0
  155. package/dist/streams/index.d.ts +1 -1
  156. package/dist/streams/index.js +1 -1
  157. package/dist/{streams-DW-UCOUc.js → streams-CITClVer.js} +16 -3
  158. package/dist/streams-CITClVer.js.map +1 -0
  159. package/dist/{streams-Cmkj9QfW.d.ts → streams-fbG61qM8.d.ts} +2 -2
  160. package/dist/sub-routing.d.ts +1 -1
  161. package/dist/tasks/index.d.ts +1 -1
  162. package/dist/tasks/index.js +1 -1
  163. package/dist/{tasks-68DESxf0.js → tasks-CkTZNaV1.js} +2 -1
  164. package/dist/tasks-CkTZNaV1.js.map +1 -0
  165. package/dist/{tasks-9YxYtByh.d.ts → tasks-PtlB4Uyn.d.ts} +2 -2
  166. package/dist/tool-3PA8xjB_.js +421 -0
  167. package/dist/tool-3PA8xjB_.js.map +1 -0
  168. package/dist/tool-BPJEbwaX.d.ts +278 -0
  169. package/dist/transport-k2mFrtL8.d.ts +200 -0
  170. package/dist/turns-DCV3ycDl.js +317 -0
  171. package/dist/turns-DCV3ycDl.js.map +1 -0
  172. package/dist/voice/client.js +1 -1
  173. package/dist/voice/index.d.ts +2 -2
  174. package/dist/voice/index.js +1 -1
  175. package/dist/websearch/index.d.ts +58 -0
  176. package/dist/websearch/index.js +2 -0
  177. package/dist/websearch/tools/ai-sdk.d.ts +77 -0
  178. package/dist/websearch/tools/ai-sdk.js +46 -0
  179. package/dist/websearch/tools/ai-sdk.js.map +1 -0
  180. package/dist/websearch/tools/pi.d.ts +82 -0
  181. package/dist/websearch/tools/pi.js +89 -0
  182. package/dist/websearch/tools/pi.js.map +1 -0
  183. package/dist/websearch/tools/tanstack-ai.d.ts +71 -0
  184. package/dist/websearch/tools/tanstack-ai.js +46 -0
  185. package/dist/websearch/tools/tanstack-ai.js.map +1 -0
  186. package/dist/websockets/index.d.ts +12 -296
  187. package/dist/websockets/index.js +1 -1
  188. package/dist/{websockets-Ci-LZFHH.js → websockets-CmENGb-Z.js} +18 -4
  189. package/dist/websockets-CmENGb-Z.js.map +1 -0
  190. package/dist/{wire-types-CrjKvDyi.d.ts → wire-types-Cv0iwjk5.d.ts} +3 -51
  191. package/dist/workflow-types.d.ts +1 -1
  192. package/dist/workflows.d.ts +1 -1
  193. package/dist/workflows.js +1 -1
  194. package/docs/browse-the-web.md +3 -0
  195. package/docs/harnesses/pi.md +114 -32
  196. package/docs/harnesses/think.md +215 -0
  197. package/docs/index.md +4 -3
  198. package/docs/lifecycle.md +40 -0
  199. package/docs/models-pi-ai.md +5 -5
  200. package/docs/search-the-web.md +133 -0
  201. package/package.json +126 -47
  202. package/dist/browser-tool-CQ10e6Kj.js.map +0 -1
  203. package/dist/channel-Bnm4S7T2.d.ts +0 -491
  204. package/dist/channels/ai-sdk.d.ts +0 -46
  205. package/dist/channels/ai-sdk.js +0 -120
  206. package/dist/channels/ai-sdk.js.map +0 -1
  207. package/dist/channels/email.js.map +0 -1
  208. package/dist/channels/index.d.ts +0 -233
  209. package/dist/channels/index.js +0 -608
  210. package/dist/channels/index.js.map +0 -1
  211. package/dist/channels/slack.js +0 -614
  212. package/dist/channels/slack.js.map +0 -1
  213. package/dist/channels/tanstack-ai.d.ts +0 -39
  214. package/dist/channels/tanstack-ai.js +0 -17
  215. package/dist/channels/tanstack-ai.js.map +0 -1
  216. package/dist/channels/telegram.js +0 -427
  217. package/dist/channels/telegram.js.map +0 -1
  218. package/dist/channels/voice.d.ts +0 -45
  219. package/dist/channels/voice.js +0 -122
  220. package/dist/channels/voice.js.map +0 -1
  221. package/dist/harnesses/pi/index.js.map +0 -1
  222. package/dist/images-iiTDSyRu.js.map +0 -1
  223. package/dist/index-BB0kqhIz.d.ts +0 -101
  224. package/dist/ingress-BfetZbMO.js +0 -83
  225. package/dist/ingress-BfetZbMO.js.map +0 -1
  226. package/dist/internal-CYlgHl1l.js +0 -59
  227. package/dist/internal-CYlgHl1l.js.map +0 -1
  228. package/dist/models/ai-sdk/index.js.map +0 -1
  229. package/dist/protocol-kHsr-f0X.js.map +0 -1
  230. package/dist/sessions/index.js.map +0 -1
  231. package/dist/streams-DW-UCOUc.js.map +0 -1
  232. package/dist/surface-bZZJqBka.js +0 -17
  233. package/dist/surface-bZZJqBka.js.map +0 -1
  234. package/dist/tasks-68DESxf0.js.map +0 -1
  235. package/dist/tool-schema-CBjGPrsQ.js +0 -31
  236. package/dist/tool-schema-CBjGPrsQ.js.map +0 -1
  237. package/dist/websockets-Ci-LZFHH.js.map +0 -1
  238. package/docs/channels.md +0 -323
@@ -0,0 +1,215 @@
1
+ ---
2
+ title: Think harness (Experimental)
3
+ pcx_content_type: reference
4
+ description: Run Think's agent loop in a Durable Object with the experimental ThinkHarness lifecycle capability. Durable transcripts and turns that survive eviction.
5
+ ---
6
+
7
+ `ThinkHarness` runs Think's agent loop as a Lifecycle capability. It has the same shape as the [Pi harness](./pi.md): `harness.prompt()`, `harness.sessions` and `harness.session(id)`. The engine is the AI SDK. The harness owns its storage, so you install only the harness:
8
+
9
+ - Each session's transcript is kept as AI SDK `UIMessage`s, in the same tables the `Sessions` capability uses.
10
+ - The output of a model call is kept while it streams, in the same tables the `Streams` capability uses.
11
+ - One Lifecycle job per session wakes the object after an eviction.
12
+
13
+ It is experimental. The API may change in any release.
14
+
15
+ ## Create the harness
16
+
17
+ Install the harness on the Lifecycle:
18
+
19
+ ```ts
20
+ import { DurableObject } from "cloudflare:workers";
21
+ import { tool } from "ai";
22
+ import { z } from "zod";
23
+ import { ThinkHarness } from "agents/harness/think";
24
+ import { Lifecycle } from "agents/lifecycle";
25
+ import { createAI } from "agents/models/ai-sdk";
26
+
27
+ export class Assistant extends DurableObject<Env> {
28
+ ai = createAI({ binding: this.env.AI });
29
+
30
+ harness = new ThinkHarness({
31
+ model: this.ai("@cf/moonshotai/kimi-k2.7-code"),
32
+ system: "You are a helpful assistant.",
33
+ tools: {
34
+ weather: {
35
+ ...tool({
36
+ description: "Get the weather for a city",
37
+ inputSchema: z.object({ city: z.string() }),
38
+ execute: async ({ city }) => `Sunny in ${city}`
39
+ }),
40
+ // Safe to run again if an eviction cuts a call short.
41
+ recovery: "rerun"
42
+ }
43
+ }
44
+ });
45
+
46
+ lifecycle = Lifecycle.install(this).use(this.harness);
47
+
48
+ async ask(prompt: string) {
49
+ const result = await this.harness.prompt(prompt);
50
+ return result.status === "done" ? result.text : result.reason;
51
+ }
52
+ }
53
+ ```
54
+
55
+ On an `Agent`, call `this.lifecycle.use(this.harness)` in the constructor. Do not also install a `Sessions` capability that writes the harness's sessions: the harness caches what it wrote.
56
+
57
+ | Option | What it is |
58
+ | ---------------------- | --------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
59
+ | `model` | Required. An AI SDK `LanguageModel`, or a function of `{ session }` that returns one. |
60
+ | `system` | The system prompt, or a function of `{ session }`. |
61
+ | `tools` | An AI SDK `ToolSet`, or a function of `{ session }`. Tools with `execute` run on the server. Tools without it run on a client. A server tool may carry `recovery: "rerun"`. |
62
+ | `maxSteps` | Most model calls one operation makes. Default 10. |
63
+ | `toolApproval` | Decide per call whether a tool needs approval. A tool's own `needsApproval` also applies. |
64
+ | `recovery` | The budget for recovering interrupted work: `maxAttempts`, `backoffMs`, `stallTimeoutMs`. Refer to [Recovery](#recovery). |
65
+ | `reservedMetadataKeys` | Message-metadata keys only the server may write; stripped from input submitted with `source: "client"`. |
66
+ | `configureSession` | Called with each session's `Session` handle the first time the harness uses it. Set compaction here with `onCompaction()` and `compactAfter()`. |
67
+ | `hooks` | Callbacks into the turn loop. Refer to [Hooks](#hooks). |
68
+
69
+ ## Submit and wait
70
+
71
+ ```ts
72
+ const session = this.harness.session(); // the root session, id ""
73
+
74
+ // Durable before the model runs. The same operationId twice returns the same receipt.
75
+ const receipt = await session.submit("Summarize the README", {
76
+ operationId: requestId
77
+ });
78
+ const result = await session.wait(receipt.operationId);
79
+
80
+ // Or both in one call, with the transcript after it.
81
+ const { text, messages } = await session.prompt("Hello");
82
+ ```
83
+
84
+ `submit()` accepts a string, a `UIMessage` or an array of them, the shared harness input `{ parts }`, or a tool answer. A submission made while the session is busy waits its turn. Steering the running turn is not supported: `whenBusy: "steer"` throws `SteerNotSupportedError`.
85
+
86
+ | Method | What it does |
87
+ | ---------------------------- | -------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
88
+ | `submit(input, options)` | Durably queue input. Returns `{ operationId, session, accepted }`. |
89
+ | `prompt(input, options)` | `submit`, then `wait`, then the transcript. |
90
+ | `wait(operationId, signal?)` | Resolve when the operation settles: `done` with `text`, or `unanswered` with a `reason`. |
91
+ | `inspect(operationId)` | The operation's status, or `undefined`. |
92
+ | `chat(input, callback)` | Stream the answer to `onStart`, `onEvent(json)`, then `onDone` or `onError`, like Think's `chat()`. |
93
+ | `abort(operationId?)` | Withdraw a queued operation or abort a running one. With no id, everything open in the session. |
94
+ | `regenerate(messageId?)` | Answer a user message again, as a new branch. `Session.getBranches()` still reads the earlier answers. |
95
+ | `continue()` | Call the model again to continue the latest message. |
96
+ | `reset(handoff?)` | Abort everything and clear the transcript, optionally leaving a system note. |
97
+ | `messages()` | The active transcript. |
98
+ | `pending()` | Operations not settled yet, oldest first. |
99
+ | `subscribe(listener)` | Live events in the AI SDK's vocabulary: chunks, persisted messages, operation status, run start and end, reset, and `transcript` (deleted messages or a compaction; re-read the transcript). |
100
+ | `inFlight()` | The running operation and its model call's chunks so far, including chunks an eviction left in storage. |
101
+ | `watch()` | The shared harness interface's watch, so a Channels host can serve the session. |
102
+
103
+ `harness.sessions` has `create()`, `fork(from)` and `list()`. A fork copies the source session's active path.
104
+
105
+ A session also reads and shapes its transcript:
106
+
107
+ | Method | What it does |
108
+ | -------------------------- | ------------------------------------------------------------------------------------------------------- |
109
+ | `branches(messageId)` | The answers to a message, its children in the tree. A regenerated answer is a branch beside the others. |
110
+ | `search(query, { limit })` | Full-text search over the session's messages. |
111
+ | `compact()` | Summarize older messages with the compaction function set in `configureSession`. |
112
+
113
+ `configureSession` receives each session's `Session` handle from `agents/sessions`, for compaction settings and for writing messages without starting a turn. Those writes still reach `subscribe()` listeners and `ThinkChat` clients: an append or update as a `message` event, a deletion or compaction as a `transcript` event.
114
+
115
+ ## Tools, approvals and client tools
116
+
117
+ The harness runs every server tool call itself. The model only sees tool definitions, so each model call ends at its tool calls. The harness then runs those calls in parallel and calls the model again with their results. All of this stays in one assistant message.
118
+
119
+ A call that needs approval is recorded as `approval-requested` and the operation settles. Submit the answer to continue:
120
+
121
+ ```ts
122
+ await session.submit({ type: "approval", approvalId, approved: true });
123
+ await session.submit({
124
+ type: "tool-result",
125
+ toolCallId,
126
+ result: { ok: true, output: "42" }
127
+ });
128
+ ```
129
+
130
+ A client tool call works the same way: the operation settles with the call `input-available`, and the client's result continues the turn. Pass `autoContinue: false` with an answer to record it without continuing. Tool schemas a browser sends are kept with the session through `submit(..., { clientTools })`.
131
+
132
+ Input submitted with `source: "client"` is untrusted:
133
+
134
+ - It may only add `user` messages. Any other role ends the operation `unanswered` with reason `client_role`.
135
+ - A message whose id is already stored, on any branch, is dropped rather than rewritten.
136
+ - A client tool never replaces a server tool of the same name.
137
+ - A tool result answers only a client tool. A result for a server tool call ends `unanswered` with reason `not_client_tool`, so a server tool's result always comes from running it under its approval policy.
138
+
139
+ ## Recovery
140
+
141
+ Every step of a turn reads only durable state, so a restarted object continues where the last write left off.
142
+
143
+ - **During a model call.** Chunks are written to a stream as they arrive. When the call ends, the harness persists the message and deletes the stream in one SQLite transaction. After an eviction, the harness rebuilds the partial message from the stream, keeps it, and calls the model again to continue the same message. Tool calls whose input never finished streaming are dropped.
144
+ - **During a tool call.** The harness records each call before it runs. After an eviction, a call with a record and no result was cut short. A tool that carries `recovery: "rerun"` runs again. Any other tool (`"report"`, the default) has the call recorded as failed with an "interrupted" error and the model decides what to do. Use `rerun` only for tools that are safe to repeat.
145
+ - **Budget.** `recovery.maxAttempts` (default 10) counts interruptions without progress. A finished model call or tool call resets the count. Past the budget the operation settles `unanswered` with reason `interrupted`. Retries after the first back off from `recovery.backoffMs` (default 1000), doubling up to a minute.
146
+ - **Stalls.** A model stream that sends nothing for `recovery.stallTimeoutMs` (default 120000) is treated as interrupted.
147
+ - **Memory limits.** The wake jobs are flagged for the Lifecycle's alarm memory-limit breaker, so a turn that keeps running out of memory is backed off. When the breaker seals, the running operation is settled `unanswered` with reason `out_of_memory` on the next start, keeping any partial answer, rather than run again. A restart keeps a wake already set for later, so backoff survives a deploy.
148
+
149
+ ## Hooks
150
+
151
+ | Hook | When |
152
+ | ---------------- | -------------------------------------------------------------------------------------------------------------------------------------------------- |
153
+ | `beforeTurn` | Before every model call. Return `model`, `system`, `messages`, `activeTools`, `toolChoice`, `providerOptions`, `maxOutputTokens` or `temperature`. |
154
+ | `beforeToolCall` | Before each server tool call. Return `{ action: "block", reason }`, `{ action: "substitute", output }`, or `{ action: "run", input }`. |
155
+ | `afterToolCall` | After each server tool call, with its result. |
156
+ | `onStepFinish` | After each model call's message is persisted. |
157
+ | `onChunk` | For each UI chunk the model streams. |
158
+ | `onTurnEnd` | After an operation settles. |
159
+ | `onError` | Observe a model call's error. |
160
+ | `classifyError` | Return `fail`, `retry`, or `context-overflow`. `classifyContextOverflow` matches the common providers' overflow errors. |
161
+ | `onRecovery` | Return `continue` or `abandon` for an interrupted model call. |
162
+
163
+ `beforeTurn` runs before every model call because a turn makes several, and after an eviction the harness has no memory of what an earlier call returned. On `context-overflow` the harness compacts the session with its compaction function and calls the model again, once.
164
+
165
+ ## Serve useAgentChat
166
+
167
+ `ThinkChat` speaks Think's chat WebSocket protocol for one session, so `useAgentChat` works unchanged. It also answers `GET …/get-messages`:
168
+
169
+ ```ts
170
+ import { ThinkChat } from "agents/harness/think";
171
+ import { WebSockets } from "agents/websockets";
172
+
173
+ webSockets = new WebSockets();
174
+ chat = new ThinkChat({ harness: this.harness, webSockets: this.webSockets });
175
+
176
+ lifecycle = Lifecycle.install(this)
177
+ .use(this.harness)
178
+ .use(this.webSockets)
179
+ .use(this.chat);
180
+ ```
181
+
182
+ It handles chat requests (`submit-message` and `regenerate-message`), cancel, clear, client tool results, approvals, and stream resume after a reconnect or an eviction.
183
+
184
+ ## Compared with Think
185
+
186
+ `ThinkHarness` is meant to replace the engine inside `@cloudflare/think`. It reads and writes the same Sessions and Streams tables Think uses, and the root session is Think's conversation. Work in flight when an agent moves over is not carried across.
187
+
188
+ | Think | ThinkHarness |
189
+ | -------------------------------------------------------------------------- | ------------------------------------------------------------------- |
190
+ | `getModel()`, `getSystemPrompt()`, `getTools()` | `model`, `system`, `tools` options |
191
+ | `beforeTurn`, `beforeToolCall`, `afterToolCall`, `onStepFinish`, `onChunk` | Hooks of the same names |
192
+ | `beforeStep` | `beforeTurn`, which runs before every model call |
193
+ | `onChatResponse`, `onChatError`, `classifyChatError` | `onTurnEnd`, `onError`, `classifyError` |
194
+ | `onChatRecovery`, `chatRecovery` | `onRecovery`, `recovery` |
195
+ | `submitMessages`, `waitForSubmission`, `inspectSubmission` | `submit`, `wait`, `inspect` |
196
+ | `saveMessages`, `runTurn` | `prompt` |
197
+ | `chat(message, callback)` | `session.chat(input, callback)` |
198
+ | `cancelChat`, `cancelSubmission`, `cancelAllChats` | `abort(operationId)`, `abort()` |
199
+ | `continueLastTurn`, regeneration | `continue()`, `regenerate()` |
200
+ | `clearMessages`, `getMessages`, `messages` | `reset()`, `messages()` |
201
+ | `configureSession`, compaction | `configureSession` |
202
+ | `useAgentChat` protocol | `ThinkChat` |
203
+ | One conversation per Durable Object | Many sessions per object, with `create`, `fork` and `list` |
204
+ | Tools run inside `streamText`; interrupted calls are repaired | The harness runs each call; a tool can opt into `recovery: "rerun"` |
205
+
206
+ Not supported yet:
207
+
208
+ - Steering a running turn. `submit()` throws `SteerNotSupportedError` for `whenBusy: "steer"` rather than queue the input as a follow-up.
209
+ - `messageConcurrency` strategies other than queueing (`latest`, `merge`, `drop`, debounce).
210
+ - Context blocks from `configureContext()`. Compute the system prompt in `system` or `beforeTurn`.
211
+ - Read-time truncation of older tool results and media eviction.
212
+ - Workspace tools, `getActions()` with its ledger, agent tools, skills, extensions, MCP tools, messengers, channels policy, scheduled tasks, and workflows. Pass the tools these produce in `tools`; the higher-level features are not ported.
213
+ - `listSubmissions` and `deleteSubmissions`. `pending()` lists open operations.
214
+ - Client tool ownership by participant, and the `cf_agent_chat_recovering` frame.
215
+ - `configure()` and `getConfig()`.
package/docs/index.md CHANGED
@@ -58,7 +58,6 @@ The differentiator is not "we have durable state" — it is what happens when a
58
58
  - [Push Notifications](./push-notifications.md) - Browser push notifications via Web Push API and scheduled delivery
59
59
  - TODO: [SMS](./sms.md) - Text message integration (Twilio, etc.)
60
60
  - [Voice Agents](./voice.md) - Build voice agents with real-time speech-to-text, text-to-speech, and conversation persistence
61
- - [Channels](./channels.md) - Send and receive messages through Slack, Telegram, email, browser voice, or custom adapters
62
61
  - [Chat SDK State](./chat-sdk.md) - Store Chat SDK subscriptions, locks, queues, and history in Agents sub-agents
63
62
 
64
63
  ## Background Processing
@@ -80,8 +79,9 @@ The differentiator is not "we have durable state" — it is what happens when a
80
79
  - [Server-Driven Messages](./server-driven-messages.md) - Autonomous agent workflows: scheduled follow-ups, queue processing, webhooks, chained reasoning
81
80
  - TODO: [Using AI Models](./using-ai-models.md) - OpenAI, Anthropic, Workers AI, and other providers
82
81
  - [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
+ - [Models for pi-ai (Beta)](./models-pi-ai.md) - `createAI` for pi-ai: Workers AI ids and third-party models through AI Gateway, as a pi-ai provider
83
+ - [Pi harness (Beta)](./harnesses/pi.md) - Host pi-durable sessions in a Durable Object with durable storage and lifecycle wakeups
84
+ - [Think harness (Experimental)](./harnesses/think.md) - Run Think's agent loop as a Lifecycle capability, with transcripts in Sessions, output in Streams, and turns that survive eviction
85
85
  - TODO: [RAG (Retrieval Augmented Generation)](./rag.md) - Vector search with Vectorize
86
86
  - [Sessions (Experimental)](./sessions.md) - Durable message trees, streamed history, compaction, search, and lossless attachment offload
87
87
  - [Context (Experimental)](./context.md) - System-prompt blocks, frozen prompts, writable/searchable/loadable providers, and their tools
@@ -128,6 +128,7 @@ The differentiator is not "we have durable state" — it is what happens when a
128
128
  ## Compute Environments
129
129
 
130
130
  - [Browse the Web (Experimental)](./browse-the-web.md) - Full CDP access for web inspection, scraping, and debugging
131
+ - [Search the Web (Beta)](./search-the-web.md) - A `websearch` tool over Cloudflare's Web Search API for the pi harness, the AI SDK, and TanStack AI
131
132
  - TODO: [Cloudflare Sandboxes](./sandboxes.md) - Isolated environments for coding agents, ffmpeg, and heavy compute
132
133
 
133
134
  ## Advanced Topics
package/docs/lifecycle.md CHANGED
@@ -156,6 +156,46 @@ Host-specific bindings, authentication, and protocol adapters remain explicit
156
156
  constructor dependencies. Lifecycle never grants a capability the complete
157
157
  host implicitly.
158
158
 
159
+ A capability's public operations can run before startup, because
160
+ [native RPC](#native-rpc) bypasses the automatic startup on fetch, alarm and
161
+ WebSocket events. If an operation starts work that `onStart` also awaits, such
162
+ as lazily opening a store, it must call `await this.lifecycle.ready()` first:
163
+
164
+ ```ts
165
+ class Store extends LifecycleCapability {
166
+ #opening: Promise<Database> | undefined;
167
+
168
+ constructor() {
169
+ super("store");
170
+ }
171
+
172
+ async onStart(): Promise<void> {
173
+ await this.#open();
174
+ }
175
+
176
+ async get(key: string): Promise<string | undefined> {
177
+ const db = await this.#open();
178
+ return db.get(key);
179
+ }
180
+
181
+ async #open(): Promise<Database> {
182
+ // Without this, an RPC that calls get() before startup begins the open
183
+ // outside startup, and onStart then waits on it behind a closed gate.
184
+ await this.lifecycle.ready();
185
+ this.#opening ??= openDatabase(this.lifecycle.storage);
186
+ return this.#opening;
187
+ }
188
+ }
189
+ ```
190
+
191
+ Startup holds the input gate (`blockConcurrencyWhile`) while `onStart` runs, so
192
+ work begun outside startup has its timers and I/O held back and never finishes.
193
+ `onStart` waits on it until startup times out after 30 seconds and the object
194
+ is reset. Whether it happens depends on whether the RPC arrives first, so it
195
+ shows up as an intermittent timeout. `ready()` resolves immediately inside
196
+ startup, so `onStart` can call the same guarded method. `Tasks`, `Streams`,
197
+ `Scheduler`, `Queue` and `PiHarness` guard their operations this way.
198
+
159
199
  Capability hooks run outside host context, but user callbacks run through
160
200
  `this.lifecycle.runInHostContext(fn)` inside the host invocation context.
161
201
  Scheduler and Queue dispatch their registered callbacks through this
@@ -1,4 +1,4 @@
1
- # Models for pi-ai (Experimental)
1
+ # Models for pi-ai (Beta)
2
2
 
3
3
  `agents/models/pi-ai` is the [pi-ai](https://github.com/earendil-works/pi/tree/main/packages/ai)
4
4
  twin of [`agents/models/ai-sdk`](./models.md): the same `createAI` factory, the same options,
@@ -45,7 +45,7 @@ design: a model this package cannot name exactly is yours to construct.
45
45
  store on the gateway (BYOK) — so nothing vendor-shaped needs a secret in your Worker.
46
46
  4. **The binding is the only way in.** Workers AI goes through `env.AI.run`, vendor models
47
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.
48
+ 5. Everything here is in beta and may change in a minor release.
49
49
 
50
50
  ## Quick start
51
51
 
@@ -311,10 +311,10 @@ Image generation is Workers AI only. The knobs a model takes are the model's:
311
311
  in a `cloudflare-compat` diagnostic on the result rather than sent and ignored.
312
312
  Every other model in the catalog takes the full set.
313
313
 
314
- ## Experimental
314
+ ## Beta
315
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
316
+ This subpath needs `@earendil-works/pi-ai@^1.0.0`, an optional peer of `agents`. `agents`
317
+ takes no vendor package as a dependency, optional or otherwise. The surface is in beta
318
318
  and may change in a minor release. An unlisted `@cf/` id resolves with default metadata (a
319
319
  128k context window, 8192 output tokens, zero cost); pass overrides when you know better.
320
320
 
@@ -0,0 +1,133 @@
1
+ # Search the Web (Beta)
2
+
3
+ `agents/websearch` gives a model a `web_search` tool over Cloudflare's [Web Search API](https://developers.cloudflare.com/web-search/), called through the `AI` binding and billed by the account's AI Gateway. The same tool is available for the pi harness, the AI SDK, and TanStack AI.
4
+
5
+ This page covers what the SDK adds on top of the API. For the binding, the providers, pricing, payment, and the error codes, see the [Web Search API docs](https://developers.cloudflare.com/web-search/).
6
+
7
+ > **Beta** — this feature may have breaking changes in future releases.
8
+
9
+ ## Quick Start
10
+
11
+ You need:
12
+
13
+ - An `AI` binding: `"ai": { "binding": "AI" }` in `wrangler.jsonc`.
14
+ - workerd 1.20260924.1 or later, which is where `env.AI.websearch()` arrived. It ships with wrangler 4.141.0 and `@cloudflare/vite-plugin` 1.60.2.
15
+ - AI Gateway credits or a provider key on the gateway, in the account the Worker runs in. That account pays for every search. The `AI` binding always calls Cloudflare, so under `wrangler dev` searches run against, and bill, the account you are logged in to.
16
+
17
+ Then add the tool to your harness. Every adapter takes the same options; the TanStack AI adapter also takes `name`, because TanStack AI tools carry their name in the definition.
18
+
19
+ Pi harness:
20
+
21
+ ```ts
22
+ import { webSearchTool } from "agents/websearch/pi";
23
+
24
+ this.registry.install({
25
+ name: "tools",
26
+ tools: [webSearchTool({ binding: this.env.AI, provider: "exa" })]
27
+ });
28
+ ```
29
+
30
+ AI SDK:
31
+
32
+ ```ts
33
+ import { webSearchTool } from "agents/websearch/ai-sdk";
34
+
35
+ const result = streamText({
36
+ model,
37
+ tools: { web_search: webSearchTool({ binding: this.env.AI }) },
38
+ messages
39
+ });
40
+ ```
41
+
42
+ If you convert UI messages yourself, pass the same tools to `convertToModelMessages(messages, { tools })`. Without them, the AI SDK sends earlier search results back to the model as the full JSON response instead of the trimmed text. `AiSdkHarness` does this for you.
43
+
44
+ TanStack AI:
45
+
46
+ ```ts
47
+ import { webSearchTool } from "agents/websearch/tanstack-ai";
48
+
49
+ const tools = [webSearchTool({ binding: this.env.AI })];
50
+ ```
51
+
52
+ ## Options
53
+
54
+ The model's input is `{ query, limit? }` and nothing else. The host fixes everything that affects cost or data handling. An invalid `limit`, `maxDescriptionChars`, `timeoutMs`, or `byokAlias` throws a `RangeError` when the tool is created:
55
+
56
+ | Option | Default | Notes |
57
+ | --------------------- | -------------------- | --------------------------------------------------------------------------------------------------------------------------------------------------------- |
58
+ | `binding` | — | The `AI` binding. Or pass `source` instead (see [Other sources](#other-sources)). |
59
+ | `gateway` | `"default"` | AI Gateway id. |
60
+ | `provider` | platform default | `"ceramic"`, `"exa"`, or `"linkup"`. The model cannot choose or change it. |
61
+ | `byokAlias` | — | Bill a provider key stored on the gateway. Passed through as the API defines it. |
62
+ | `limit` | `5` | Results per search when the model does not ask for a count, and the most it gets when it asks for more. The tool description tells the model this number. |
63
+ | `maxDescriptionChars` | `600` | Per-result description length in the model's view. `Infinity` passes descriptions through whole. |
64
+ | `description` | built-in description | Replaces the tool description the model sees. |
65
+ | `timeoutMs` | `30000` | Give up on a search after this long, as a retryable `web_search_timeout` failure. |
66
+
67
+ ## Model Interface
68
+
69
+ The model gets text: a numbered list of title, URL, and description, with descriptions trimmed to `maxDescriptionChars`. Some providers return descriptions of several thousand characters per result, so the default keeps a five-result search to a few kilobytes of context.
70
+
71
+ ```
72
+ 3 results for "cloudflare web search api":
73
+
74
+ 1. Introducing the Web Search API
75
+ https://blog.cloudflare.com/introducing-web-search-api/
76
+ Today we are launching the Web Search API in open beta…
77
+
78
+ 2. …
79
+ ```
80
+
81
+ The host gets the full API response, without trimming, as `WebSearchToolOutput`: `items` with their whole descriptions and every documented field, `metadata` with `requestId` and `latencyMs`, plus `provider`. Items without a URL are dropped, a missing title falls back to the URL, and fields the API does not document are left out:
82
+
83
+ - **Pi**: in the tool result's `details`, as `{ ok: true, output }`. The tool is `replay: "safe"`: if a search is interrupted mid-call, for example by an eviction, pi runs it again when the session recovers, and that is a second billed search. Completed results are stored and not searched again.
84
+ - **AI SDK**: as the return value of `execute`, so `onFinish`, UI message parts, and logs see the full response. `toModelOutput` renders the text for the model.
85
+ - **TanStack AI**: the server tool returns the rendered text, so the host gets the same text the model does.
86
+
87
+ `renderWebSearchResults(output, { maxDescriptionChars })` from `agents/websearch` is the renderer, if you want the same text elsewhere.
88
+
89
+ The API has no pagination. The tool description tells the model to search again with a rephrased query when it wants more or different results.
90
+
91
+ ## Failures
92
+
93
+ A failed search becomes a `WebSearchError` with `status`, `code`, `retryable`, and `requestId` (AI Gateway's id for the request, for the gateway log). Every error has a `code`: the API's own when it sends one, for example `web_search_payment_required`, otherwise one derived from the HTTP status, such as `web_search_rate_limited` or `web_search_unavailable`. On a runtime older than the one above, the binding has no `websearch()` and the search fails with code `web_search_unsupported_runtime`.
94
+
95
+ The error's `message` is written for you, and can say to top up credits or configure a key. The model gets different text that tells it what to do next: fix the query, retry once, or carry on without search.
96
+
97
+ - **Pi**: the tool returns an error result with the model's text, and `details` is `{ ok: false, message, status, code, retryable, requestId }`.
98
+ - **AI SDK** and **TanStack AI**: the tool throws a `WebSearchError`, which is how those frameworks report tool errors. Its `message` is the model's text, and its `cause` is the original error with the API's detail.
99
+
100
+ Every adapter entry point re-exports `WebSearchError`, so `instanceof` checks do not need a second import.
101
+
102
+ If the harness cancels the call, the search is aborted and the abort propagates instead of becoming a failed result.
103
+
104
+ ## Other sources
105
+
106
+ `createAIWebSearch` and `createHTTPWebSearch` from `agents/websearch` return a `WebSearchSource`: an object whose `search({ query, limit? }, { signal? })` returns the API response (`limit` defaults to 5), with no model involved. Use them from scheduled jobs, or outside Workers:
107
+
108
+ ```ts
109
+ import { createHTTPWebSearch } from "agents/websearch";
110
+
111
+ const search = createHTTPWebSearch({
112
+ accountId: env.CF_ACCOUNT_ID,
113
+ apiToken: env.CF_API_TOKEN,
114
+ provider: "linkup"
115
+ });
116
+ const { items } = await search.search({
117
+ query: "cloudflare agents sdk",
118
+ limit: 3
119
+ });
120
+ ```
121
+
122
+ Any `WebSearchSource` can be passed to a tool as `source` instead of `binding`. That is how tests substitute a fake, and how you wrap a source with caching or logging. Spread the source you wrap so its `provider` is kept:
123
+
124
+ ```ts
125
+ webSearchTool({
126
+ source: {
127
+ search: async ({ query }) => ({
128
+ items: [{ url: "https://example.com", title: query }],
129
+ metadata: { query, requestId: "test", latencyMs: 0 }
130
+ })
131
+ }
132
+ });
133
+ ```