experimental-a2 0.16.1 → 0.18.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 (406) hide show
  1. package/CHANGELOG.md +373 -0
  2. package/README.md +1 -1
  3. package/dist/{actor-ohPC-81x.d.ts → actor-Bjce9_5U.d.ts} +21 -11
  4. package/dist/actor-Bjce9_5U.d.ts.map +1 -0
  5. package/dist/actor-client.d.ts +2 -5
  6. package/dist/actor-client.d.ts.map +1 -1
  7. package/dist/actor-client.js +13 -4
  8. package/dist/actor-client.js.map +1 -1
  9. package/dist/actor-react.d.ts +5 -3
  10. package/dist/actor-react.d.ts.map +1 -1
  11. package/dist/actor-react.js +20 -10
  12. package/dist/actor-react.js.map +1 -1
  13. package/dist/{actor-shared-B5tJfzt-.js → actor-shared-t7w8xIKP.js} +12 -7
  14. package/dist/actor-shared-t7w8xIKP.js.map +1 -0
  15. package/dist/actor.d.ts +1 -1
  16. package/dist/actor.js +42 -23
  17. package/dist/actor.js.map +1 -1
  18. package/dist/{ai-DgOBltJ_.js → ai-BpOGm-zB.js} +621 -216
  19. package/dist/ai-BpOGm-zB.js.map +1 -0
  20. package/dist/{ai-CrEf6p_W.d.ts → ai-DrUaMyRt.d.ts} +114 -100
  21. package/dist/ai-DrUaMyRt.d.ts.map +1 -0
  22. package/dist/ai-client.d.ts +27 -0
  23. package/dist/ai-client.d.ts.map +1 -0
  24. package/dist/ai-client.js +29 -0
  25. package/dist/ai-client.js.map +1 -0
  26. package/dist/ai-fx.browser.d.ts +1 -0
  27. package/dist/ai-fx.browser.js +6 -0
  28. package/dist/ai-fx.browser.js.map +1 -0
  29. package/dist/ai-fx.d.ts +10 -0
  30. package/dist/ai-fx.d.ts.map +1 -0
  31. package/dist/ai-fx.js +634 -0
  32. package/dist/ai-fx.js.map +1 -0
  33. package/dist/ai-harness-BuLh_EYi.js +273 -0
  34. package/dist/ai-harness-BuLh_EYi.js.map +1 -0
  35. package/dist/ai-harness-DvsPeJeB.d.ts +26 -0
  36. package/dist/ai-harness-DvsPeJeB.d.ts.map +1 -0
  37. package/dist/ai-pi.browser.d.ts +1 -0
  38. package/dist/ai-pi.browser.js +6 -0
  39. package/dist/ai-pi.browser.js.map +1 -0
  40. package/dist/ai-pi.d.ts +17 -0
  41. package/dist/ai-pi.d.ts.map +1 -0
  42. package/dist/ai-pi.js +904 -0
  43. package/dist/ai-pi.js.map +1 -0
  44. package/dist/ai-react.d.ts +96 -0
  45. package/dist/ai-react.d.ts.map +1 -0
  46. package/dist/ai-react.js +142 -0
  47. package/dist/ai-react.js.map +1 -0
  48. package/dist/ai-server.browser.js +2 -2
  49. package/dist/ai-server.browser.js.map +1 -1
  50. package/dist/ai-server.d.ts +109 -43
  51. package/dist/ai-server.d.ts.map +1 -1
  52. package/dist/ai-server.js +526 -283
  53. package/dist/ai-server.js.map +1 -1
  54. package/dist/ai.d.ts +2 -2
  55. package/dist/ai.js +2 -2
  56. package/dist/bounded-cache-CPHWF-8h.js +41 -0
  57. package/dist/bounded-cache-CPHWF-8h.js.map +1 -0
  58. package/dist/{client-BYzHjkwU.js → client-BReKqwZC.js} +447 -80
  59. package/dist/client-BReKqwZC.js.map +1 -0
  60. package/dist/{client-CzyacQpJ.d.ts → client-h_DKFAjj.d.ts} +14 -13
  61. package/dist/client-h_DKFAjj.d.ts.map +1 -0
  62. package/dist/client.d.ts +4 -2
  63. package/dist/client.js +1 -1
  64. package/dist/{contract-CKRg_E4q.js → contract-BYKASlCV.js} +12 -21
  65. package/dist/contract-BYKASlCV.js.map +1 -0
  66. package/dist/{idempotent-replay-DVOlyYbx.js → idempotent-replay-JdgMQRRm.js} +2 -2
  67. package/dist/{idempotent-replay-DVOlyYbx.js.map → idempotent-replay-JdgMQRRm.js.map} +1 -1
  68. package/dist/index.d.ts +6 -5
  69. package/dist/index.d.ts.map +1 -1
  70. package/dist/index.js +4 -3
  71. package/dist/{errors-DvhSXnxk.d.ts → initial-snapshot-Bx-c0pDP.d.ts} +7 -3
  72. package/dist/initial-snapshot-Bx-c0pDP.d.ts.map +1 -0
  73. package/dist/{internal-Dq2qYxou.js → internal-B2T3U4Rq.js} +29 -2
  74. package/dist/internal-B2T3U4Rq.js.map +1 -0
  75. package/dist/otel.d.ts +1 -1
  76. package/dist/react-BBXgHWFc.d.ts +86 -0
  77. package/dist/react-BBXgHWFc.d.ts.map +1 -0
  78. package/dist/react.d.ts +2 -122
  79. package/dist/react.js +107 -100
  80. package/dist/react.js.map +1 -1
  81. package/dist/reducer-BAXc8JyF.js +87 -0
  82. package/dist/reducer-BAXc8JyF.js.map +1 -0
  83. package/dist/{reducer-otzHjuJj.d.ts → reducer-CqA59Q6U.d.ts} +46 -30
  84. package/dist/reducer-CqA59Q6U.d.ts.map +1 -0
  85. package/dist/reducer-runtime-DQwz_J59.js +28 -0
  86. package/dist/reducer-runtime-DQwz_J59.js.map +1 -0
  87. package/dist/scheduler-qstash.d.ts +3 -3
  88. package/dist/scheduler-qstash.d.ts.map +1 -1
  89. package/dist/scheduler-qstash.js +33 -10
  90. package/dist/scheduler-qstash.js.map +1 -1
  91. package/dist/{scheduler-task-BpzhPnRS.js → scheduler-task-X1SToDjP.js} +18 -3
  92. package/dist/scheduler-task-X1SToDjP.js.map +1 -0
  93. package/dist/scheduler-vercel.d.ts +4 -9
  94. package/dist/scheduler-vercel.d.ts.map +1 -1
  95. package/dist/scheduler-vercel.js +25 -44
  96. package/dist/scheduler-vercel.js.map +1 -1
  97. package/dist/{server-BD5ckxZb.js → server-Dw_k_7kO.js} +904 -219
  98. package/dist/server-Dw_k_7kO.js.map +1 -0
  99. package/dist/{server-DbD7IJVK.d.ts → server-En7cbi3v.d.ts} +116 -26
  100. package/dist/server-En7cbi3v.d.ts.map +1 -0
  101. package/dist/server.d.ts +4 -4
  102. package/dist/server.js +2 -2
  103. package/dist/session-clear-Bo-XTFEy.d.ts +12 -0
  104. package/dist/session-clear-Bo-XTFEy.d.ts.map +1 -0
  105. package/dist/{errors-DCk6ch5n.js → session-clear-DGI4jTQ5.js} +17 -2
  106. package/dist/session-clear-DGI4jTQ5.js.map +1 -0
  107. package/dist/{store-DyZM6fS5.d.ts → store-Djdv0bnZ.d.ts} +73 -3
  108. package/dist/store-Djdv0bnZ.d.ts.map +1 -0
  109. package/dist/store-N8PXxDAS.js.map +1 -1
  110. package/dist/store-memory.d.ts +1 -1
  111. package/dist/store-memory.d.ts.map +1 -1
  112. package/dist/store-memory.js +205 -66
  113. package/dist/store-memory.js.map +1 -1
  114. package/dist/{store-polling-CmxUbV93.js → store-polling-B5FcwJfo.js} +21 -4
  115. package/dist/store-polling-B5FcwJfo.js.map +1 -0
  116. package/dist/store-postgres.d.ts +1 -1
  117. package/dist/store-postgres.d.ts.map +1 -1
  118. package/dist/store-postgres.js +450 -108
  119. package/dist/store-postgres.js.map +1 -1
  120. package/dist/{store-presence-polling-C7-XZyW9.js → store-presence-polling-BAwS8RWn.js} +3 -3
  121. package/dist/{store-presence-polling-C7-XZyW9.js.map → store-presence-polling-BAwS8RWn.js.map} +1 -1
  122. package/dist/store-redis-http.d.ts +1 -1
  123. package/dist/store-redis-http.d.ts.map +1 -1
  124. package/dist/store-redis-http.js +293 -11
  125. package/dist/store-redis-http.js.map +1 -1
  126. package/dist/store-redis-notify-BRITti_o.js +2493 -0
  127. package/dist/store-redis-notify-BRITti_o.js.map +1 -0
  128. package/dist/store-redis.d.ts +1 -1
  129. package/dist/store-redis.d.ts.map +1 -1
  130. package/dist/store-redis.js +11 -11
  131. package/dist/store-redis.js.map +1 -1
  132. package/dist/store-sqlite.d.ts +1 -1
  133. package/dist/store-sqlite.d.ts.map +1 -1
  134. package/dist/store-sqlite.js +197 -33
  135. package/dist/store-sqlite.js.map +1 -1
  136. package/dist/stream-buffer-Cx7AAPGq.js +23 -0
  137. package/dist/stream-buffer-Cx7AAPGq.js.map +1 -0
  138. package/dist/{telemetry-B5jzpy6y.d.ts → telemetry-Cyfqo6Qt.d.ts} +2 -2
  139. package/dist/telemetry-Cyfqo6Qt.d.ts.map +1 -0
  140. package/dist/{wire-BO5wWCb1.js → wire-rSUglYo8.js} +132 -10
  141. package/dist/wire-rSUglYo8.js.map +1 -0
  142. package/docs/01-quickstart.mdx +50 -84
  143. package/docs/{guides/07-examples.mdx → 02-examples.mdx} +10 -20
  144. package/docs/actors/01-introduction.mdx +48 -82
  145. package/docs/actors/02-concurrency.mdx +43 -70
  146. package/docs/actors/03-timers.mdx +28 -42
  147. package/docs/actors/04-routes.mdx +110 -137
  148. package/docs/actors/meta.ts +1 -1
  149. package/docs/agents/01-introduction.mdx +257 -0
  150. package/docs/agents/02-tools.mdx +247 -0
  151. package/docs/agents/03-conversations.mdx +538 -0
  152. package/docs/agents/04-harnesses.mdx +136 -0
  153. package/docs/agents/meta.ts +1 -0
  154. package/docs/client/01-react.mdx +294 -0
  155. package/docs/client/02-transports.mdx +86 -0
  156. package/docs/client/03-presence.mdx +224 -0
  157. package/docs/client/meta.ts +1 -0
  158. package/docs/contract/01-events.mdx +57 -0
  159. package/docs/contract/02-state.mdx +142 -0
  160. package/docs/contract/03-sessions.mdx +175 -0
  161. package/docs/index.mdx +112 -156
  162. package/docs/reference/01-core.mdx +139 -0
  163. package/docs/reference/02-server.mdx +855 -0
  164. package/docs/reference/03-client.mdx +301 -0
  165. package/docs/reference/04-ai.mdx +722 -0
  166. package/docs/reference/05-adapters.mdx +351 -0
  167. package/docs/reference/06-errors.mdx +138 -0
  168. package/docs/reference/meta.ts +1 -1
  169. package/docs/server/01-handlers.mdx +200 -0
  170. package/docs/server/02-timers.mdx +130 -0
  171. package/docs/server/03-cancellation.mdx +141 -0
  172. package/docs/server/04-routes.mdx +243 -0
  173. package/docs/server/05-production.mdx +322 -0
  174. package/docs/server/06-guarantees.mdx +123 -0
  175. package/examples/playground/DEPLOY.md +53 -7
  176. package/examples/playground/app/a0/[appId]/loading.tsx +16 -0
  177. package/examples/playground/app/a0/[appId]/page.tsx +53 -0
  178. package/examples/playground/app/a0/a0.css +818 -0
  179. package/examples/playground/app/a0/app-list.tsx +218 -0
  180. package/examples/playground/app/a0/app-view.tsx +63 -0
  181. package/examples/playground/app/a0/apps/[appId]/preview/route.ts +27 -0
  182. package/examples/playground/app/a0/apps/[appId]/route.ts +28 -0
  183. package/examples/playground/app/a0/apps.test.ts +128 -0
  184. package/examples/playground/app/a0/apps.ts +102 -0
  185. package/examples/playground/app/a0/chat.tsx +410 -0
  186. package/examples/playground/app/a0/columns.tsx +109 -0
  187. package/examples/playground/app/a0/context-menu.tsx +116 -0
  188. package/examples/playground/app/a0/cookies.ts +46 -0
  189. package/examples/playground/app/a0/crash-on-request.ts +72 -0
  190. package/examples/playground/app/a0/crashes/route.ts +3 -0
  191. package/examples/playground/app/a0/crashes.test.ts +33 -0
  192. package/examples/playground/app/a0/crashes.ts +28 -0
  193. package/examples/playground/app/a0/devtools.tsx +164 -0
  194. package/examples/playground/app/a0/events/route.ts +6 -0
  195. package/examples/playground/app/a0/flags.ts +30 -0
  196. package/examples/playground/app/a0/fx/events/route.ts +9 -0
  197. package/examples/playground/app/a0/fx/scheduler/route.ts +7 -0
  198. package/examples/playground/app/a0/fx/short/events/route.ts +11 -0
  199. package/examples/playground/app/a0/layout.tsx +38 -0
  200. package/examples/playground/app/a0/list/route.ts +10 -0
  201. package/examples/playground/app/a0/model.test.ts +73 -0
  202. package/examples/playground/app/a0/model.ts +95 -0
  203. package/examples/playground/app/a0/no-photon.cjs +4 -0
  204. package/examples/playground/app/a0/no-quickjs.cjs +3 -0
  205. package/examples/playground/app/a0/page.tsx +14 -0
  206. package/examples/playground/app/a0/pi/events/route.ts +9 -0
  207. package/examples/playground/app/a0/pi/scheduler/route.ts +7 -0
  208. package/examples/playground/app/a0/pi/short/events/route.ts +11 -0
  209. package/examples/playground/app/a0/preview-pane.tsx +145 -0
  210. package/examples/playground/app/a0/preview.test.ts +53 -0
  211. package/examples/playground/app/a0/preview.ts +26 -0
  212. package/examples/playground/app/a0/sandbox-log.ts +75 -0
  213. package/examples/playground/app/a0/sandbox-session.ts +152 -0
  214. package/examples/playground/app/a0/sandbox.ts +191 -0
  215. package/examples/playground/app/a0/sandboxes/scheduler/route.ts +6 -0
  216. package/examples/playground/app/a0/scheduler/route.ts +7 -0
  217. package/examples/playground/app/a0/server.ts +213 -0
  218. package/examples/playground/app/a0/session.ts +59 -0
  219. package/examples/playground/app/a0/shell.tsx +158 -0
  220. package/examples/playground/app/a0/short/events/route.ts +9 -0
  221. package/examples/playground/app/a0/splitter.tsx +120 -0
  222. package/examples/playground/app/agent/[agentId]/agent-client.tsx +144 -174
  223. package/examples/playground/app/agent/[agentId]/command-receipt.test.tsx +65 -0
  224. package/examples/playground/app/agent/[agentId]/command-receipt.tsx +27 -0
  225. package/examples/playground/app/agent/[agentId]/compaction-event.tsx +25 -24
  226. package/examples/playground/app/agent/[agentId]/compaction-panel.test.tsx +119 -0
  227. package/examples/playground/app/agent/[agentId]/compaction-panel.tsx +208 -165
  228. package/examples/playground/app/agent/[agentId]/page.tsx +2 -16
  229. package/examples/playground/app/agent/events/route.ts +5 -2
  230. package/examples/playground/app/agent/ingress.test.ts +101 -0
  231. package/examples/playground/app/agent/model-input.test.tsx +58 -0
  232. package/examples/playground/app/agent/model-input.tsx +115 -0
  233. package/examples/playground/app/agent/model.test.ts +73 -0
  234. package/examples/playground/app/agent/model.ts +58 -26
  235. package/examples/playground/app/agent/new-agent-session.tsx +37 -19
  236. package/examples/playground/app/agent/page.tsx +4 -4
  237. package/examples/playground/app/agent/server.ts +19 -15
  238. package/examples/playground/app/agent/session.ts +5 -6
  239. package/examples/playground/app/agent/telemetry.test.ts +189 -0
  240. package/examples/playground/app/agent/telemetry.ts +30 -0
  241. package/examples/playground/app/agent/trace.test.ts +23 -0
  242. package/examples/playground/app/agent/trace.ts +18 -0
  243. package/examples/playground/app/canvas/[canvasId]/canvas-client.tsx +12 -12
  244. package/examples/playground/app/canvas/[canvasId]/page.tsx +1 -2
  245. package/examples/playground/app/canvas/[canvasId]/session.ts +2 -2
  246. package/examples/playground/app/canvas/model.ts +53 -53
  247. package/examples/playground/app/chat/[chatId]/agent-state.test.ts +176 -0
  248. package/examples/playground/app/chat/[chatId]/agent-stream-drawer.tsx +16 -1
  249. package/examples/playground/app/chat/[chatId]/chat-client.tsx +55 -77
  250. package/examples/playground/app/chat/[chatId]/chat-view.ts +37 -3
  251. package/examples/playground/app/chat/[chatId]/page.tsx +7 -15
  252. package/examples/playground/app/chat/[chatId]/session.ts +2 -19
  253. package/examples/playground/app/chat/agent/route.ts +9 -0
  254. package/examples/playground/app/chat/model.test.ts +46 -119
  255. package/examples/playground/app/chat/model.ts +82 -128
  256. package/examples/playground/app/chat/scheduler/route.ts +2 -2
  257. package/examples/playground/app/chat/server.test.ts +112 -0
  258. package/examples/playground/app/chat/server.ts +113 -117
  259. package/examples/playground/app/components/activity-feed.tsx +30 -1
  260. package/examples/playground/app/components/activity-history.test.tsx +35 -0
  261. package/examples/playground/app/components/demo-icons.tsx +89 -0
  262. package/examples/playground/app/components/demos.tsx +33 -0
  263. package/examples/playground/app/components/fast-link.test.tsx +58 -0
  264. package/examples/playground/app/components/fast-link.tsx +55 -0
  265. package/examples/playground/app/components/site-header.tsx +96 -0
  266. package/examples/playground/app/counter/counter-client.tsx +3 -3
  267. package/examples/playground/app/counter/model.test.ts +3 -3
  268. package/examples/playground/app/counter/model.ts +11 -21
  269. package/examples/playground/app/counter/page.tsx +2 -15
  270. package/examples/playground/app/counter/session.ts +2 -2
  271. package/examples/playground/app/documents/[documentId]/code-editor.tsx +12 -1
  272. package/examples/playground/app/documents/[documentId]/document-client.tsx +10 -11
  273. package/examples/playground/app/documents/[documentId]/page.tsx +1 -2
  274. package/examples/playground/app/documents/[documentId]/session.ts +2 -2
  275. package/examples/playground/app/documents/model.ts +27 -27
  276. package/examples/playground/app/globals.css +420 -207
  277. package/examples/playground/app/initial-state.test.tsx +54 -0
  278. package/examples/playground/app/layout.tsx +7 -23
  279. package/examples/playground/app/orders/[orderId]/page.tsx +1 -2
  280. package/examples/playground/app/orders/[orderId]/session.ts +2 -2
  281. package/examples/playground/app/orders/model.ts +49 -49
  282. package/examples/playground/app/page.tsx +13 -102
  283. package/examples/playground/app/recovery/[recoveryId]/page.tsx +1 -2
  284. package/examples/playground/app/recovery/[recoveryId]/session.ts +2 -2
  285. package/examples/playground/app/recovery/model.ts +30 -30
  286. package/examples/playground/instrumentation.ts +5 -0
  287. package/examples/playground/next.config.ts +17 -5
  288. package/examples/playground/package.json +13 -2
  289. package/examples/playground/scripts/a0-template.ts +77 -0
  290. package/examples/playground/vercel.json +36 -0
  291. package/package.json +29 -3
  292. package/src/actor-client.ts +16 -4
  293. package/src/actor-react.ts +25 -11
  294. package/src/actor-shared.ts +21 -6
  295. package/src/actor.ts +76 -31
  296. package/src/ai-agent-actions.ts +219 -0
  297. package/src/ai-agent.ts +2517 -0
  298. package/src/ai-app-state.ts +91 -0
  299. package/src/ai-client-state.ts +6 -3
  300. package/src/ai-client.ts +62 -0
  301. package/src/ai-context-schema.ts +25 -5
  302. package/src/ai-context.ts +90 -40
  303. package/src/ai-control-server.ts +92 -72
  304. package/src/ai-coordinator.ts +23 -22
  305. package/src/ai-fx.browser.ts +5 -0
  306. package/src/ai-fx.ts +833 -0
  307. package/src/ai-harness.ts +518 -0
  308. package/src/ai-message-base.ts +209 -0
  309. package/src/ai-message-projection.ts +262 -64
  310. package/src/ai-model-metadata.ts +11 -6
  311. package/src/ai-pi.browser.ts +5 -0
  312. package/src/ai-pi.ts +1263 -0
  313. package/src/ai-progress-batches.ts +26 -0
  314. package/src/ai-projector.ts +95 -14
  315. package/src/ai-react.ts +146 -0
  316. package/src/ai-sdk-step.ts +2 -2
  317. package/src/ai-server.browser.ts +2 -2
  318. package/src/ai-server.ts +23 -1965
  319. package/src/ai-stored-state.ts +48 -163
  320. package/src/ai.ts +339 -155
  321. package/src/bootstrap-stream.ts +72 -0
  322. package/src/bounded-cache.ts +55 -0
  323. package/src/client-render-session.ts +49 -0
  324. package/src/client.ts +688 -151
  325. package/src/contract.ts +92 -42
  326. package/src/deterministic-id.ts +37 -0
  327. package/src/equal-json.ts +32 -0
  328. package/src/errors.ts +13 -1
  329. package/src/index.ts +5 -1
  330. package/src/initial-snapshot.ts +39 -0
  331. package/src/internal.ts +52 -0
  332. package/src/libfx.d.ts +34 -0
  333. package/src/postgres-notification-scope.ts +14 -3
  334. package/src/react.ts +171 -206
  335. package/src/redis-http-reply.ts +256 -0
  336. package/src/redis-http-stream.ts +73 -0
  337. package/src/redis-http-subscriptions.ts +13 -5
  338. package/src/reducer-runtime.ts +52 -0
  339. package/src/reducer.ts +93 -11
  340. package/src/scheduler-qstash.ts +49 -20
  341. package/src/scheduler-task.ts +34 -2
  342. package/src/scheduler-vercel.ts +32 -84
  343. package/src/server-fetch.ts +139 -35
  344. package/src/server.ts +1133 -255
  345. package/src/session-clear.ts +23 -0
  346. package/src/session-socket.ts +154 -49
  347. package/src/sse.ts +71 -4
  348. package/src/store-memory.ts +252 -75
  349. package/src/store-polling.ts +18 -4
  350. package/src/store-postgres.ts +541 -114
  351. package/src/store-redis-core.ts +1450 -493
  352. package/src/store-redis-http.ts +18 -2
  353. package/src/store-redis-notify.ts +262 -85
  354. package/src/store-redis.ts +19 -9
  355. package/src/store-sqlite.ts +319 -45
  356. package/src/store.ts +79 -1
  357. package/src/stream-buffer.ts +43 -0
  358. package/src/telemetry.ts +14 -1
  359. package/src/wire.ts +241 -13
  360. package/dist/actor-ohPC-81x.d.ts.map +0 -1
  361. package/dist/actor-shared-B5tJfzt-.js.map +0 -1
  362. package/dist/ai-CrEf6p_W.d.ts.map +0 -1
  363. package/dist/ai-DgOBltJ_.js.map +0 -1
  364. package/dist/client-BYzHjkwU.js.map +0 -1
  365. package/dist/client-CzyacQpJ.d.ts.map +0 -1
  366. package/dist/contract-CKRg_E4q.js.map +0 -1
  367. package/dist/errors-DCk6ch5n.js.map +0 -1
  368. package/dist/errors-DvhSXnxk.d.ts.map +0 -1
  369. package/dist/internal-Dq2qYxou.js.map +0 -1
  370. package/dist/react.d.ts.map +0 -1
  371. package/dist/reducer-DEMjEY_O.js +0 -29
  372. package/dist/reducer-DEMjEY_O.js.map +0 -1
  373. package/dist/reducer-otzHjuJj.d.ts.map +0 -1
  374. package/dist/scheduler-task-BpzhPnRS.js.map +0 -1
  375. package/dist/server-BD5ckxZb.js.map +0 -1
  376. package/dist/server-DbD7IJVK.d.ts.map +0 -1
  377. package/dist/store-DyZM6fS5.d.ts.map +0 -1
  378. package/dist/store-polling-CmxUbV93.js.map +0 -1
  379. package/dist/store-redis-notify-BVLUfI3j.js +0 -1583
  380. package/dist/store-redis-notify-BVLUfI3j.js.map +0 -1
  381. package/dist/telemetry-B5jzpy6y.d.ts.map +0 -1
  382. package/dist/validate-XKT4FSNn.js +0 -30
  383. package/dist/validate-XKT4FSNn.js.map +0 -1
  384. package/dist/wire-BO5wWCb1.js.map +0 -1
  385. package/docs/concepts/01-contracts.mdx +0 -142
  386. package/docs/concepts/02-handlers.mdx +0 -284
  387. package/docs/concepts/03-durability.mdx +0 -314
  388. package/docs/concepts/04-state.mdx +0 -209
  389. package/docs/guides/01-timers.mdx +0 -185
  390. package/docs/guides/02-cancellation.mdx +0 -161
  391. package/docs/guides/03-react.mdx +0 -384
  392. package/docs/guides/04-local-first.mdx +0 -88
  393. package/docs/guides/05-production.mdx +0 -521
  394. package/docs/guides/06-ai-agents.mdx +0 -1318
  395. package/docs/guides/08-application-data.mdx +0 -113
  396. package/docs/guides/09-presence.mdx +0 -266
  397. package/docs/guides/10-transports.mdx +0 -221
  398. package/docs/reference/01-api.mdx +0 -1896
  399. package/docs/reference/02-errors.mdx +0 -149
  400. package/examples/playground/app/agent/[agentId]/compaction/route.ts +0 -14
  401. package/examples/playground/app/agent/compaction-settings.test.ts +0 -144
  402. package/examples/playground/app/agent/compaction-settings.ts +0 -49
  403. package/examples/playground/app/agent/compaction-timeline.test.ts +0 -377
  404. package/examples/playground/app/agent/compaction-timeline.ts +0 -208
  405. /package/docs/{concepts → contract}/meta.ts +0 -0
  406. /package/docs/{guides → server}/meta.ts +0 -0
package/src/ai-pi.ts ADDED
@@ -0,0 +1,1263 @@
1
+ /**
2
+ * experimental-a2/ai/pi — `pi()`: the Pi harness for `agent({ harness })`.
3
+ * Server-only; Pi comes from the optional `@earendil-works/pi-coding-agent`
4
+ * and `@earendil-works/pi-ai` peer dependencies.
5
+ */
6
+
7
+ // oxlint-disable no-await-in-loop -- turns replay in order, each on the session the last one left.
8
+
9
+ import { createHash } from 'node:crypto'
10
+ import { readFile as readLocalFile, unlink } from 'node:fs/promises'
11
+ import { tmpdir } from 'node:os'
12
+ import { basename, dirname, join } from 'node:path'
13
+ import type {
14
+ AssistantMessage,
15
+ AssistantMessageEvent,
16
+ AssistantMessageEventStream,
17
+ JsonObject,
18
+ TextContent,
19
+ ThinkingContent,
20
+ ToolCall,
21
+ Usage,
22
+ } from '@earendil-works/pi-ai'
23
+ import type {
24
+ AgentSession,
25
+ AgentToolResult,
26
+ BashOperations,
27
+ ModelRuntime,
28
+ ResourceLoader,
29
+ SessionEntry,
30
+ SessionHeader,
31
+ ToolDefinition,
32
+ } from '@earendil-works/pi-coding-agent'
33
+ import {
34
+ asSchema,
35
+ jsonSchema,
36
+ type Experimental_SandboxSession,
37
+ type FinishReason,
38
+ type JSONSchema7,
39
+ type LanguageModelUsage,
40
+ type Tool,
41
+ type ToolExecutionOptions,
42
+ type ToolSet,
43
+ type UIMessageChunk,
44
+ } from 'ai'
45
+ import {
46
+ type Deferred,
47
+ defineHarness,
48
+ deferred,
49
+ Divergence,
50
+ gatewayCredential,
51
+ type Harness,
52
+ type HarnessGeneration,
53
+ type HarnessRead,
54
+ type HarnessSandbox,
55
+ type HarnessStep,
56
+ type HarnessTurn,
57
+ pending,
58
+ readHarness,
59
+ type RecordedGeneration,
60
+ saveHarness,
61
+ } from './ai-harness.ts'
62
+ import type { CompactionPartData, ToolResultPayload } from './ai.ts'
63
+ import { boundedCache } from './bounded-cache.ts'
64
+ import { ambientToolScopeStorage } from './internal.ts'
65
+
66
+ export type {
67
+ Harness,
68
+ HarnessSandbox,
69
+ HarnessSandboxContext,
70
+ } from './ai-harness.ts'
71
+
72
+ /** What `pi()` takes. */
73
+ export type PiOptions = {
74
+ /** The session's sandbox: Pi's tools, and the agent's, work in it. */
75
+ sandbox: HarnessSandbox
76
+ }
77
+
78
+ /**
79
+ * The Pi harness. Every model call Pi makes is one durable step; A2 rebuilds
80
+ * Pi's session for each step from its latest snapshot and the log since.
81
+ */
82
+ export function pi(options: PiOptions): Harness<'pi', Builtin> {
83
+ if (typeof options?.sandbox !== 'function')
84
+ throw new TypeError('pi() requires sandbox')
85
+ return defineHarness<'pi', Builtin>('pi', {
86
+ sandbox: options.sandbox,
87
+ tools: builtinTools,
88
+ step: (step) =>
89
+ runStep(step, async () =>
90
+ options.sandbox({
91
+ session: { id: step.sessionId },
92
+ signal: step.signal,
93
+ }),
94
+ ),
95
+ })
96
+ }
97
+
98
+ const SNAPSHOT_VERSION = 1
99
+ const GATEWAY = 'vercel-ai-gateway'
100
+ // The most a file read or a command's output moves from the sandbox to the
101
+ // server. Pi's tools show the model 50 KB at a time, after reading it all.
102
+ const MAX_TRANSFER_BYTES = 4 * 1024 * 1024
103
+ const MAX_TRANSFER = '4 MiB'
104
+ // Pi's default tools.
105
+ const BUILTINS = ['read', 'write', 'edit', 'bash'] as const
106
+ type Builtin = (typeof BUILTINS)[number]
107
+
108
+ type PiModules = {
109
+ agent: typeof import('@earendil-works/pi-coding-agent')
110
+ ai: typeof import('@earendil-works/pi-ai')
111
+ }
112
+
113
+ /** A content block without the text the transcript already holds. */
114
+ type StoredBlock =
115
+ | (Omit<TextContent, 'text'> & { ref: string })
116
+ | (Omit<ThinkingContent, 'thinking'> & { ref: string })
117
+ | Omit<ToolCall, 'arguments'>
118
+
119
+ /** What a step's transcript parts do not hold of Pi's response. */
120
+ type StoredResponse = Omit<AssistantMessage, 'content'> & {
121
+ content: StoredBlock[]
122
+ }
123
+
124
+ /** The transient record of one model call, for replay. */
125
+ type PiRecord =
126
+ | { call: 'step'; response: StoredResponse }
127
+ | { call: 'compaction'; response: AssistantMessage }
128
+
129
+ type RecordedCall = {
130
+ kind: 'step' | 'compaction'
131
+ response: AssistantMessage
132
+ /** Steering to queue while serving this call. */
133
+ steers: string[]
134
+ }
135
+
136
+ type Saved = {
137
+ cwd: string
138
+ header: SessionHeader
139
+ entries: SessionEntry[]
140
+ }
141
+
142
+ type Completion = { finishReason?: FinishReason; usage?: LanguageModelUsage }
143
+
144
+ /** The turn being replayed or run live, as the model and tool hooks see it. */
145
+ type TurnState = {
146
+ plan: HarnessTurn
147
+ calls: RecordedCall[]
148
+ next: number
149
+ liveStep: boolean
150
+ /** The live step is over; Pi's run is being stopped. */
151
+ over: boolean
152
+ failure?: Error
153
+ /** Resolves when the live step is over: Pi asked for its tools or another step. */
154
+ stepOver: Deferred
155
+ /** Live compaction responses, in request order. */
156
+ compactions: AssistantMessage[]
157
+ }
158
+
159
+ function endStep(state: TurnState): void {
160
+ state.over = true
161
+ state.stepOver.resolve()
162
+ }
163
+
164
+ const diverged = (message: string): Divergence => new Divergence('Pi', message)
165
+
166
+ // ---------------------------------------------------------------- step
167
+
168
+ async function runStep(
169
+ step: HarnessStep,
170
+ sandbox: () => Promise<Experimental_SandboxSession>,
171
+ ): Promise<HarnessGeneration> {
172
+ const modules = await loadPi()
173
+ const credential = gatewayCredential('pi')
174
+ const name = `${step.agentName}.a2.harness.pi`
175
+ const { replayAll, ...first } = await readHarness({
176
+ step,
177
+ name,
178
+ parse: savedSession,
179
+ })
180
+ let read: HarnessRead<Saved> = first
181
+ let results = read.results
182
+ const cwd =
183
+ read.saved?.cwd ??
184
+ (await sessionCwd({
185
+ key: cwdKey(step.agentName, step.sessionId),
186
+ sandbox,
187
+ }))
188
+
189
+ let completion: Completion = {}
190
+ let session: AgentSession | undefined
191
+ let compacting = false
192
+ let turn: TurnState | undefined
193
+
194
+ const fail = (state: TurnState, error: Error): never => {
195
+ state.failure ??= error
196
+ endStep(state)
197
+ void session?.abort()
198
+ throw state.failure
199
+ }
200
+
201
+ let controller: ReadableStreamDefaultController<UIMessageChunk> | undefined
202
+ let closed = false
203
+ const emit = (chunk: UIMessageChunk): void => {
204
+ if (!closed) controller!.enqueue(chunk)
205
+ }
206
+ const record = (data: PiRecord): void =>
207
+ emit({ type: 'data-a2-pi', data, transient: true })
208
+
209
+ // ------------------------------------------------------ model calls
210
+
211
+ const answer = async (
212
+ out: AssistantMessageEventStream,
213
+ model: Parameters<ModelRuntime['streamSimple']>[0],
214
+ context: Parameters<ModelRuntime['streamSimple']>[1],
215
+ options: Parameters<ModelRuntime['streamSimple']>[2],
216
+ ): Promise<void> => {
217
+ const state = turn!
218
+ if (state.failure) throw state.failure
219
+ // Stopping Pi runs its turn-end hooks; nothing they ask the model counts.
220
+ if (state.over) {
221
+ replay(out, interrupted(model))
222
+ return
223
+ }
224
+ const kind = compacting ? 'compaction' : 'step'
225
+ const recorded = state.calls[state.next]
226
+ if (recorded !== undefined) {
227
+ if (recorded.kind !== kind)
228
+ fail(state, diverged(`call ${state.next} is not a ${kind}`))
229
+ state.next++
230
+ // Steering queued while a response streams enters after its tools.
231
+ for (const text of recorded.steers) await session!.steer(text)
232
+ replay(out, recorded.response)
233
+ return
234
+ }
235
+ if (state.plan.end === 'stop')
236
+ fail(state, diverged('Pi continued a turn the log ended'))
237
+ if (state.plan.end === 'interrupted') {
238
+ replay(out, interrupted(model))
239
+ return
240
+ }
241
+ if (kind === 'step' && state.liveStep) {
242
+ // One generation is one step; the next step is A2's next generation.
243
+ endStep(state)
244
+ replay(out, interrupted(model))
245
+ return
246
+ }
247
+ if (kind === 'step') state.liveStep = true
248
+ const upstream = realStream(model, context, {
249
+ ...options,
250
+ headers: { ...options?.headers, ...gatewayHeaders(credential) },
251
+ })
252
+ const live = kind === 'step' ? liveStep() : undefined
253
+ for await (const event of upstream) {
254
+ live?.(event)
255
+ if (event.type === 'done' || event.type === 'error') {
256
+ const response = event.type === 'done' ? event.message : event.error
257
+ if (kind === 'compaction') state.compactions.push(response)
258
+ // A2 retries the step; Pi's own recovery would run outside it.
259
+ if (event.type === 'error' && event.reason === 'error') {
260
+ state.failure ??= new Error(
261
+ response.errorMessage ?? 'the Pi model call failed',
262
+ )
263
+ endStep(state)
264
+ }
265
+ }
266
+ out.push(event)
267
+ }
268
+ }
269
+
270
+ /** Streams one live step response as UI chunks and records it. */
271
+ const liveStep = () => {
272
+ const ref = (index: number) => `pi:${step.requestIndex}:${index}`
273
+ const started = new Set<string>()
274
+ return (event: AssistantMessageEvent): void => {
275
+ switch (event.type) {
276
+ case 'start':
277
+ emit({ type: 'start-step' })
278
+ break
279
+ case 'text_start':
280
+ emit({ type: 'text-start', id: ref(event.contentIndex) })
281
+ break
282
+ case 'text_delta':
283
+ emit({
284
+ type: 'text-delta',
285
+ id: ref(event.contentIndex),
286
+ delta: event.delta,
287
+ })
288
+ break
289
+ case 'text_end':
290
+ emit({ type: 'text-end', id: ref(event.contentIndex) })
291
+ break
292
+ case 'thinking_start':
293
+ emit({ type: 'reasoning-start', id: ref(event.contentIndex) })
294
+ break
295
+ case 'thinking_delta':
296
+ emit({
297
+ type: 'reasoning-delta',
298
+ id: ref(event.contentIndex),
299
+ delta: event.delta,
300
+ })
301
+ break
302
+ case 'thinking_end':
303
+ emit({ type: 'reasoning-end', id: ref(event.contentIndex) })
304
+ break
305
+ case 'toolcall_start':
306
+ case 'toolcall_delta': {
307
+ const call = event.partial.content[event.contentIndex] as ToolCall
308
+ if (!call.id) break
309
+ if (!started.has(call.id)) {
310
+ started.add(call.id)
311
+ emit({
312
+ type: 'tool-input-start',
313
+ toolCallId: call.id,
314
+ toolName: call.name,
315
+ })
316
+ }
317
+ if (event.type === 'toolcall_delta')
318
+ emit({
319
+ type: 'tool-input-delta',
320
+ toolCallId: call.id,
321
+ inputTextDelta: event.delta,
322
+ })
323
+ break
324
+ }
325
+ case 'done':
326
+ case 'error': {
327
+ const response = event.type === 'done' ? event.message : event.error
328
+ if (event.type === 'done') {
329
+ for (const block of response.content) {
330
+ if (block.type !== 'toolCall') continue
331
+ if (!started.has(block.id))
332
+ emit({
333
+ type: 'tool-input-start',
334
+ toolCallId: block.id,
335
+ toolName: block.name,
336
+ })
337
+ // Pi does not run the calls of a cut-off response.
338
+ emit(
339
+ response.stopReason === 'length'
340
+ ? {
341
+ type: 'tool-input-error',
342
+ toolCallId: block.id,
343
+ toolName: block.name,
344
+ input: block.arguments,
345
+ errorText:
346
+ 'The response hit the output token limit before this call was complete.',
347
+ }
348
+ : {
349
+ type: 'tool-input-available',
350
+ toolCallId: block.id,
351
+ toolName: block.name,
352
+ input: block.arguments,
353
+ },
354
+ )
355
+ }
356
+ }
357
+ record({ call: 'step', response: storedResponse(response, ref) })
358
+ completion = {
359
+ finishReason: finishReason(response.stopReason),
360
+ usage: modelUsage(response.usage),
361
+ }
362
+ break
363
+ }
364
+ }
365
+ }
366
+ }
367
+
368
+ // ----------------------------------------------------- tool results
369
+
370
+ const toolResult = async (
371
+ toolCallId: string,
372
+ signal: AbortSignal | undefined,
373
+ shape: 'pi' | 'tool',
374
+ ): Promise<AgentToolResult<unknown>> => {
375
+ const state = turn!
376
+ if (state.failure) throw state.failure
377
+ const result = results.get(toolCallId)
378
+ if (result !== undefined) return answeredTool(result, shape)
379
+ if (state.plan.end === 'live' && state.liveStep) {
380
+ // Pi now waits on tools that A2's tool handlers run.
381
+ endStep(state)
382
+ return pending(signal)
383
+ }
384
+ if (state.plan.end === 'interrupted') {
385
+ void session!.abort()
386
+ return pending(signal)
387
+ }
388
+ return fail(state, diverged(`no recorded result for ${toolCallId}`))
389
+ }
390
+
391
+ // ------------------------------------------------------------ run
392
+
393
+ const runtime = await modules.agent.ModelRuntime.create({
394
+ credentials: new modules.ai.InMemoryCredentialStore(),
395
+ modelsPath: null,
396
+ allowModelNetwork: false,
397
+ refreshOnCreate: false,
398
+ })
399
+ await runtime.setRuntimeApiKey(GATEWAY, credential.token)
400
+ const model = runtime.getModel(GATEWAY, step.model)
401
+ if (model === undefined)
402
+ throw new Error(`Pi does not know the Gateway model ${step.model}`)
403
+ const realStream = runtime.streamSimple.bind(runtime)
404
+ runtime.streamSimple = (requested, context, options?) => {
405
+ const out = modules.ai.createAssistantMessageEventStream()
406
+ answer(out, requested, context, options).catch((error: unknown) => {
407
+ const state = turn
408
+ if (state) state.failure ??= asError(error)
409
+ replay(out, { ...interrupted(requested), stopReason: 'error' })
410
+ })
411
+ return out
412
+ }
413
+
414
+ const tools = await sessionTools(modules, step.tools, cwd, toolResult)
415
+
416
+ const createSession = async (from: Saved | undefined) => {
417
+ const sessionManager =
418
+ from === undefined
419
+ ? modules.agent.SessionManager.inMemory(cwd, {
420
+ id: sessionUuid(step.agentName, step.sessionId),
421
+ })
422
+ : modules.agent.SessionManager.inMemory(cwd, { id: from.header.id }, [
423
+ from.header,
424
+ ...from.entries,
425
+ ])
426
+ const created = await modules.agent.createAgentSession({
427
+ cwd,
428
+ // Pi reads nothing there: every resource and manager comes from A2.
429
+ agentDir: join(tmpdir(), 'a2-pi'),
430
+ model,
431
+ ...thinkingLevel(step),
432
+ modelRuntime: runtime,
433
+ resourceLoader: resources(modules, step.instructions),
434
+ sessionManager,
435
+ settingsManager: modules.agent.SettingsManager.inMemory({
436
+ // A2 retries failed steps.
437
+ retry: { enabled: false, maxRetries: 0, baseDelayMs: 0 },
438
+ cacheWarming: 'off',
439
+ }),
440
+ customTools: tools,
441
+ tools: tools.map((definition) => definition.name),
442
+ })
443
+ created.session.subscribe((event) => {
444
+ if (event.type === 'compaction_start') compacting = true
445
+ if (event.type !== 'compaction_end') return
446
+ compacting = false
447
+ const state = turn
448
+ if (state === undefined || state.over || state.compactions.length === 0)
449
+ return
450
+ for (const response of state.compactions.splice(0))
451
+ record({ call: 'compaction', response })
452
+ emit({
453
+ type: 'data-a2-compaction',
454
+ id: `pi-compaction:${step.requestIndex}:${compactions++}`,
455
+ data: compactionData(event),
456
+ })
457
+ })
458
+ return created.session
459
+ }
460
+ let compactions = 0
461
+
462
+ const saveSnapshot = async (): Promise<void> => {
463
+ const manager = session!.sessionManager
464
+ const header = manager.getHeader()
465
+ const entries = manager.getEntries()
466
+ const last = entries.findLast(
467
+ (entry) => entry.type === 'message' && entry.message.role === 'assistant',
468
+ )
469
+ const stop =
470
+ last?.type === 'message' && last.message.role === 'assistant'
471
+ ? last.message.stopReason
472
+ : undefined
473
+ // Only a turn this attempt finished describes the log.
474
+ if (
475
+ header === null ||
476
+ step.signal.aborted ||
477
+ stop === 'aborted' ||
478
+ stop === 'error'
479
+ )
480
+ return
481
+ await saveHarness({
482
+ step,
483
+ name,
484
+ state: { version: SNAPSHOT_VERSION, cwd, header, entries },
485
+ })
486
+ }
487
+
488
+ const run = async (): Promise<void> => {
489
+ emit({ type: 'start', messageId: step.responseMessageId })
490
+ try {
491
+ session = await createSession(read.saved)
492
+ } catch (error) {
493
+ if (read.saved === undefined) throw error
494
+ // An unusable snapshot: replay the whole log instead.
495
+ read = await replayAll()
496
+ results = read.results
497
+ session = await createSession(undefined)
498
+ }
499
+ for (const plan of read.turns) {
500
+ if (step.signal.aborted) return
501
+ const state: TurnState = {
502
+ plan,
503
+ calls: recordedTurn(plan),
504
+ next: 0,
505
+ liveStep: false,
506
+ over: false,
507
+ stepOver: deferred(),
508
+ compactions: [],
509
+ }
510
+ turn = state
511
+ const running = session.prompt(plan.prompt, {
512
+ expandPromptTemplates: false,
513
+ })
514
+ if (plan.end !== 'live') {
515
+ await running.catch((error: unknown) => {
516
+ throw state.failure ?? error
517
+ })
518
+ if (state.failure) throw state.failure
519
+ continue
520
+ }
521
+ const ended = await Promise.race([
522
+ running.then(() => true),
523
+ state.stepOver.promise.then(() => false),
524
+ ]).catch((error: unknown) => {
525
+ throw state.failure ?? error
526
+ })
527
+ if (!ended) {
528
+ await session.abort()
529
+ await running.catch(() => {})
530
+ }
531
+ if (state.failure) throw state.failure
532
+ if (ended) await saveSnapshot()
533
+ emit({ type: 'finish-step' })
534
+ emit({
535
+ type: 'finish',
536
+ ...(completion.finishReason === undefined
537
+ ? {}
538
+ : { finishReason: completion.finishReason }),
539
+ })
540
+ }
541
+ }
542
+
543
+ const stream = new ReadableStream<UIMessageChunk>({
544
+ start(streamController) {
545
+ controller = streamController
546
+ const abort = () => void session?.abort()
547
+ step.signal.addEventListener('abort', abort, { once: true })
548
+ void (async () => {
549
+ try {
550
+ await run()
551
+ closed = true
552
+ streamController.close()
553
+ } catch (error) {
554
+ closed = true
555
+ streamController.error(
556
+ step.signal.aborted ? step.signal.reason : error,
557
+ )
558
+ } finally {
559
+ step.signal.removeEventListener('abort', abort)
560
+ session?.dispose()
561
+ }
562
+ })()
563
+ },
564
+ cancel() {
565
+ closed = true
566
+ void session?.abort()
567
+ },
568
+ })
569
+
570
+ return { stream, completion: () => completion }
571
+ }
572
+
573
+ // ------------------------------------------------------------ the plan
574
+
575
+ /** A turn's recorded model calls, in the order Pi made them. */
576
+ function recordedTurn(turn: HarnessTurn): RecordedCall[] {
577
+ const calls: RecordedCall[] = []
578
+ turn.generations.forEach((generation, index) => {
579
+ calls.push(...recordedCalls(generation))
580
+ const steers = turn.steers.get(index)
581
+ const last = calls.findLast((call) => call.kind === 'step')
582
+ if (steers !== undefined && last !== undefined) last.steers.push(...steers)
583
+ })
584
+ return calls
585
+ }
586
+
587
+ function recordedCalls(generation: RecordedGeneration): RecordedCall[] {
588
+ const texts = new Map<string, string>()
589
+ const inputs = new Map<string, unknown>()
590
+ const calls: RecordedCall[] = []
591
+ for (const chunk of generation.chunks) {
592
+ switch (chunk.type) {
593
+ case 'text-delta':
594
+ case 'reasoning-delta':
595
+ texts.set(chunk.id, (texts.get(chunk.id) ?? '') + chunk.delta)
596
+ break
597
+ case 'tool-input-available':
598
+ case 'tool-input-error':
599
+ inputs.set(chunk.toolCallId, chunk.input)
600
+ break
601
+ case 'data-a2-pi': {
602
+ const record = chunk.data as PiRecord
603
+ calls.push({
604
+ kind: record.call,
605
+ response:
606
+ record.call === 'step'
607
+ ? restoredResponse(record.response, texts, inputs)
608
+ : record.response,
609
+ steers: [],
610
+ })
611
+ break
612
+ }
613
+ }
614
+ }
615
+ if (!calls.some((call) => call.kind === 'step'))
616
+ throw diverged('a completed generation has no recorded response')
617
+ return calls
618
+ }
619
+
620
+ function storedResponse(
621
+ response: AssistantMessage,
622
+ ref: (index: number) => string,
623
+ ): StoredResponse {
624
+ return {
625
+ ...response,
626
+ content: response.content.map((block, index): StoredBlock => {
627
+ switch (block.type) {
628
+ case 'text': {
629
+ const { text: _, ...rest } = block
630
+ return { ...rest, ref: ref(index) }
631
+ }
632
+ case 'thinking': {
633
+ const { thinking: _, ...rest } = block
634
+ return { ...rest, ref: ref(index) }
635
+ }
636
+ case 'toolCall': {
637
+ const { arguments: _, ...rest } = block
638
+ return rest
639
+ }
640
+ }
641
+ }),
642
+ }
643
+ }
644
+
645
+ function restoredResponse(
646
+ stored: StoredResponse,
647
+ texts: ReadonlyMap<string, string>,
648
+ inputs: ReadonlyMap<string, unknown>,
649
+ ): AssistantMessage {
650
+ return {
651
+ ...stored,
652
+ content: stored.content.map((block) => {
653
+ if (block.type === 'toolCall')
654
+ return {
655
+ ...block,
656
+ arguments: (inputs.get(block.id) ?? {}) as JsonObject,
657
+ }
658
+ const { ref, ...rest } = block
659
+ const text = texts.get(ref) ?? ''
660
+ return rest.type === 'text'
661
+ ? { ...rest, text }
662
+ : { ...rest, thinking: text }
663
+ }),
664
+ }
665
+ }
666
+
667
+ /** Hands a recorded response to Pi's loop. */
668
+ function replay(out: AssistantMessageEventStream, response: AssistantMessage) {
669
+ out.push({ type: 'start', partial: response })
670
+ const reason = response.stopReason
671
+ if (reason === 'error' || reason === 'aborted')
672
+ out.push({ type: 'error', reason, error: response })
673
+ else if (reason === 'pending')
674
+ out.push({ type: 'done', reason: 'stop', message: response })
675
+ else out.push({ type: 'done', reason, message: response })
676
+ }
677
+
678
+ /** The response of a model call that A2 interrupted. */
679
+ function interrupted(
680
+ model: Parameters<ModelRuntime['streamSimple']>[0],
681
+ ): AssistantMessage {
682
+ return {
683
+ role: 'assistant',
684
+ content: [],
685
+ api: model.api,
686
+ provider: model.provider,
687
+ model: model.id,
688
+ usage: {
689
+ input: 0,
690
+ output: 0,
691
+ cacheRead: 0,
692
+ cacheWrite: 0,
693
+ totalTokens: 0,
694
+ cost: { input: 0, output: 0, cacheRead: 0, cacheWrite: 0, total: 0 },
695
+ },
696
+ stopReason: 'aborted',
697
+ timestamp: Date.now(),
698
+ }
699
+ }
700
+
701
+ // ----------------------------------------------------------- the tools
702
+
703
+ const builtinTools: ToolSet = Object.fromEntries(
704
+ BUILTINS.map((name): [string, Tool] => [
705
+ name,
706
+ {
707
+ inputSchema: jsonSchema(
708
+ async () =>
709
+ builtinDefinition(await loadPi(), name, '/')
710
+ .parameters as JSONSchema7,
711
+ ),
712
+ execute: (input: unknown, options: ToolExecutionOptions<unknown>) =>
713
+ runBuiltin(name, input, options),
714
+ },
715
+ ]),
716
+ )
717
+
718
+ /** Runs one of Pi's tools in the session's sandbox, as Pi's loop would. */
719
+ async function runBuiltin(
720
+ name: Builtin,
721
+ input: unknown,
722
+ options: ToolExecutionOptions<unknown>,
723
+ ): Promise<AgentToolResult<unknown>> {
724
+ const sandbox = options.experimental_sandbox
725
+ if (sandbox === undefined)
726
+ throw new Error(`pi() runs ${name} in the agent's sandbox`)
727
+ const modules = await loadPi()
728
+ const scope = ambientToolScopeStorage()?.getStore()
729
+ const event = (scope?.context as { event?: { sessionId?: unknown } })?.event
730
+ const cwd = await sessionCwd({
731
+ key:
732
+ scope?.contract !== undefined && typeof event?.sessionId === 'string'
733
+ ? cwdKey(scope.contract.name, event.sessionId)
734
+ : undefined,
735
+ sandbox: async () => sandbox,
736
+ })
737
+ const definition = builtinDefinition(modules, name, cwd, {
738
+ sandbox,
739
+ signal: options.abortSignal,
740
+ })
741
+ const args: unknown = modules.ai.validateToolArguments(definition, {
742
+ type: 'toolCall',
743
+ id: options.toolCallId,
744
+ name,
745
+ arguments: (definition.prepareArguments?.(input) ?? input) as JsonObject,
746
+ })
747
+ // Pi's bash keeps a long output's full text in a local file, named in its
748
+ // updates and its result. It is removed however the call ends.
749
+ let outputFile: string | undefined
750
+ try {
751
+ const result = await definition.execute(
752
+ options.toolCallId,
753
+ args,
754
+ options.abortSignal,
755
+ (update) => {
756
+ outputFile ??= fullOutputPath(update)
757
+ },
758
+ // Pi's tools take their context from the session, which A2 runs elsewhere.
759
+ undefined as never,
760
+ )
761
+ outputFile ??= fullOutputPath(result)
762
+ return await recordedResult(result, sandbox)
763
+ } catch (error) {
764
+ // Output whose last bytes tip it over Pi's limit opens the file only as
765
+ // it ends, after the last update; a failed call then names it in its error.
766
+ outputFile ??= namedOutputFile(error)
767
+ throw error
768
+ } finally {
769
+ if (outputFile !== undefined) await unlink(outputFile).catch(() => {})
770
+ }
771
+ }
772
+
773
+ type BuiltinDetails =
774
+ { truncation?: { content?: unknown }; fullOutputPath?: unknown } | undefined
775
+
776
+ const fullOutputPath = (
777
+ result: AgentToolResult<unknown>,
778
+ ): string | undefined => {
779
+ const path = (result.details as BuiltinDetails)?.fullOutputPath
780
+ return typeof path === 'string' ? path : undefined
781
+ }
782
+
783
+ /** The full-output file an aborted or timed-out bash call's error names. */
784
+ function namedOutputFile(error: unknown): string | undefined {
785
+ if (!(error instanceof Error)) return undefined
786
+ const path = /Full output: ([^\n]+)\]\n\nCommand [^\n]+$/.exec(
787
+ error.message,
788
+ )?.[1]
789
+ // The command's own output precedes Pi's note, and could name any file.
790
+ return path !== undefined &&
791
+ dirname(path) === tmpdir() &&
792
+ /^pi-bash-[0-9a-f]{16}\.log$/.test(basename(path))
793
+ ? path
794
+ : undefined
795
+ }
796
+
797
+ /**
798
+ * What the log keeps of a result: what Pi's tool-result message holds. A
799
+ * long output's full text moves to the path its result names in the sandbox,
800
+ * where the model's `read` finds it.
801
+ */
802
+ async function recordedResult(
803
+ result: AgentToolResult<unknown>,
804
+ sandbox: Experimental_SandboxSession,
805
+ ): Promise<AgentToolResult<unknown>> {
806
+ // Pi's tool-result message does not carry structured content.
807
+ const { structuredContent: _, ...kept } =
808
+ result as AgentToolResult<unknown> & {
809
+ structuredContent?: unknown
810
+ }
811
+ const details = kept.details as BuiltinDetails
812
+ const path = fullOutputPath(kept)
813
+ if (path !== undefined) {
814
+ try {
815
+ await sandbox.writeBinaryFile({
816
+ path,
817
+ content: await readLocalFile(path),
818
+ })
819
+ } catch {
820
+ // The result stands without it; the model can rerun the command.
821
+ }
822
+ }
823
+ if (details?.truncation === undefined) return kept
824
+ // The truncated text is the result's content already.
825
+ const { content: __, ...truncation } = details.truncation
826
+ return { ...kept, details: { ...details, truncation } }
827
+ }
828
+
829
+ // Each definition is typed by its parameters; a session takes any of them.
830
+ const untyped = (definition: object) => definition as ToolDefinition
831
+
832
+ function builtinDefinition(
833
+ modules: PiModules,
834
+ name: Builtin,
835
+ cwd: string,
836
+ call?: {
837
+ sandbox: Experimental_SandboxSession
838
+ signal: AbortSignal | undefined
839
+ },
840
+ ): ToolDefinition {
841
+ const sandbox = call?.sandbox
842
+ const files =
843
+ call === undefined ? undefined : { operations: sandboxFiles(call) }
844
+ switch (name) {
845
+ case 'read':
846
+ return untyped(modules.agent.createReadToolDefinition(cwd, files))
847
+ case 'write':
848
+ return untyped(modules.agent.createWriteToolDefinition(cwd, files))
849
+ case 'edit':
850
+ return untyped(modules.agent.createEditToolDefinition(cwd, files))
851
+ case 'bash':
852
+ return untyped(
853
+ modules.agent.createBashToolDefinition(cwd, {
854
+ // No PI_* variables reach the sandbox, so Pi's prompt names none.
855
+ exposeSessionEnvironment: false,
856
+ ...(sandbox === undefined
857
+ ? {}
858
+ : { operations: sandboxBash(sandbox) }),
859
+ }),
860
+ )
861
+ }
862
+ }
863
+
864
+ /**
865
+ * The tools Pi's session declares: Pi's own, and the agent's. Their results
866
+ * come from A2's log; A2's tool handlers ran them.
867
+ */
868
+ async function sessionTools(
869
+ modules: PiModules,
870
+ tools: ToolSet,
871
+ cwd: string,
872
+ result: (
873
+ toolCallId: string,
874
+ signal: AbortSignal | undefined,
875
+ shape: 'pi' | 'tool',
876
+ ) => Promise<AgentToolResult<unknown>>,
877
+ ): Promise<ToolDefinition[]> {
878
+ return Promise.all(
879
+ Object.entries(tools)
880
+ .filter(([, tool]) => tool.type !== 'provider')
881
+ .map(async ([name, tool]): Promise<ToolDefinition> => {
882
+ const builtin = BUILTINS.find(
883
+ (candidate) => candidate === name && builtinTools[name] === tool,
884
+ )
885
+ if (builtin !== undefined)
886
+ // Pi's own definition, for its prompt; A2 already ran the call.
887
+ return Object.assign(builtinDefinition(modules, builtin, cwd), {
888
+ execute: (
889
+ toolCallId: string,
890
+ _params: unknown,
891
+ signal?: AbortSignal,
892
+ ) => result(toolCallId, signal, 'pi'),
893
+ })
894
+ return {
895
+ name,
896
+ label: name,
897
+ description:
898
+ typeof tool.description === 'string' ? tool.description : '',
899
+ parameters: (await asSchema(tool.inputSchema)
900
+ .jsonSchema) as ToolDefinition['parameters'],
901
+ execute: (toolCallId, _params, signal) =>
902
+ result(toolCallId, signal, 'tool'),
903
+ }
904
+ }),
905
+ )
906
+ }
907
+
908
+ function answeredTool(
909
+ result: ToolResultPayload,
910
+ shape: 'pi' | 'tool',
911
+ ): AgentToolResult<unknown> {
912
+ if (result.error !== undefined) throw new Error(result.error)
913
+ if (result.denied === true) throw new Error('The tool call was denied.')
914
+ if (shape === 'pi') return result.output as AgentToolResult<unknown>
915
+ const output = result.output
916
+ return {
917
+ content: [
918
+ {
919
+ type: 'text',
920
+ text:
921
+ typeof output === 'string' ? output : (JSON.stringify(output) ?? ''),
922
+ },
923
+ ],
924
+ details: undefined,
925
+ }
926
+ }
927
+
928
+ // ------------------------------------------------------------ sandbox
929
+
930
+ /** Pi's file operations, against the sandbox. */
931
+ function sandboxFiles({
932
+ sandbox,
933
+ signal,
934
+ }: {
935
+ sandbox: Experimental_SandboxSession
936
+ /** The tool call's: it stops a read or write in flight. */
937
+ signal: AbortSignal | undefined
938
+ }) {
939
+ const abortSignal = signal === undefined ? {} : { abortSignal: signal }
940
+ let checked: { path: string; bytes: Buffer } | undefined
941
+ const readFile = async (path: string): Promise<Buffer> => {
942
+ if (checked?.path === path) {
943
+ const { bytes } = checked
944
+ checked = undefined
945
+ return bytes
946
+ }
947
+ const stream = await sandbox.readFile({ path, ...abortSignal })
948
+ if (stream === null)
949
+ throw Object.assign(
950
+ new Error(`ENOENT: no such file or directory, open '${path}'`),
951
+ { code: 'ENOENT' },
952
+ )
953
+ const chunks: Uint8Array[] = []
954
+ let size = 0
955
+ // Leaving the loop cancels the stream.
956
+ for await (const chunk of stream) {
957
+ size += chunk.byteLength
958
+ if (size > MAX_TRANSFER_BYTES)
959
+ throw new Error(
960
+ `${path} is larger than ${MAX_TRANSFER}. Read or change parts of it with bash, using sed, head, or grep.`,
961
+ )
962
+ chunks.push(chunk)
963
+ }
964
+ return Buffer.concat(chunks)
965
+ }
966
+ return {
967
+ readFile,
968
+ // Pi checks a file just before reading it; the check's read serves both.
969
+ access: async (path: string): Promise<void> => {
970
+ checked = { path, bytes: await readFile(path) }
971
+ },
972
+ writeFile: async (path: string, content: string): Promise<void> => {
973
+ checked = undefined
974
+ await sandbox.writeTextFile({ path, content, ...abortSignal })
975
+ },
976
+ // The sandbox creates parent directories as it writes.
977
+ mkdir: async (): Promise<void> => {},
978
+ }
979
+ }
980
+
981
+ /** Pi's shell operations, against the sandbox. */
982
+ const sandboxBash = (sandbox: Experimental_SandboxSession): BashOperations => ({
983
+ // Pi hands over the server's environment; it never reaches the sandbox.
984
+ exec: (command, cwd, { onData, signal, timeout }) =>
985
+ sandboxExec({
986
+ sandbox,
987
+ command,
988
+ workingDirectory: cwd,
989
+ onData,
990
+ signal,
991
+ timeout,
992
+ }),
993
+ })
994
+
995
+ /**
996
+ * Runs a command in the sandbox with Pi's error contract. At most
997
+ * `MAX_TRANSFER_BYTES` of its output reach the server; past that, the
998
+ * command is stopped.
999
+ */
1000
+ async function sandboxExec({
1001
+ sandbox,
1002
+ command,
1003
+ workingDirectory,
1004
+ onData,
1005
+ signal,
1006
+ timeout,
1007
+ }: {
1008
+ sandbox: Experimental_SandboxSession
1009
+ command: string
1010
+ /** The sandbox's own when absent. */
1011
+ workingDirectory?: string
1012
+ onData: (data: Buffer, stream: 'stdout' | 'stderr') => void
1013
+ signal?: AbortSignal | undefined
1014
+ /** In seconds. */
1015
+ timeout?: number | undefined
1016
+ }): Promise<{ exitCode: number }> {
1017
+ if (signal?.aborted) throw new Error('aborted')
1018
+ const stop = new AbortController()
1019
+ const abort = () => stop.abort()
1020
+ signal?.addEventListener('abort', abort, { once: true })
1021
+ let timedOut = false
1022
+ const timer =
1023
+ timeout === undefined || timeout <= 0
1024
+ ? undefined
1025
+ : setTimeout(() => {
1026
+ timedOut = true
1027
+ stop.abort()
1028
+ }, timeout * 1000)
1029
+ let size = 0
1030
+ let overflowed = false
1031
+ const overflow = (options?: ErrorOptions) =>
1032
+ new Error(
1033
+ `The command's output passed ${MAX_TRANSFER}, so it was stopped. Write long output to a file and read parts of it with head, tail, or grep.`,
1034
+ options,
1035
+ )
1036
+ let exitCode: number
1037
+ try {
1038
+ const process = await sandbox.spawn({
1039
+ command,
1040
+ ...(workingDirectory === undefined ? {} : { workingDirectory }),
1041
+ abortSignal: stop.signal,
1042
+ })
1043
+ const pipe = async (stream: 'stdout' | 'stderr') => {
1044
+ for await (const chunk of process[stream]) {
1045
+ size += chunk.byteLength
1046
+ if (size > MAX_TRANSFER_BYTES) {
1047
+ overflowed = true
1048
+ stop.abort()
1049
+ return
1050
+ }
1051
+ onData(Buffer.from(chunk), stream)
1052
+ }
1053
+ }
1054
+ const [exited] = await Promise.all([
1055
+ process.wait(),
1056
+ pipe('stdout'),
1057
+ pipe('stderr'),
1058
+ ])
1059
+ exitCode = exited.exitCode
1060
+ } catch (error) {
1061
+ if (signal?.aborted) throw new Error('aborted', { cause: error })
1062
+ if (timedOut) throw new Error(`timeout:${timeout}`, { cause: error })
1063
+ if (overflowed) throw overflow({ cause: error })
1064
+ throw error
1065
+ } finally {
1066
+ clearTimeout(timer)
1067
+ signal?.removeEventListener('abort', abort)
1068
+ }
1069
+ if (signal?.aborted) throw new Error('aborted')
1070
+ if (timedOut) throw new Error(`timeout:${timeout}`)
1071
+ if (overflowed) throw overflow()
1072
+ return { exitCode }
1073
+ }
1074
+
1075
+ // Sandbox resolvers may return a new object each time; sessions keep theirs.
1076
+ const cwds = boundedCache<string, Promise<string>>({
1077
+ maxEntries: 1000,
1078
+ maxBytes: 1000,
1079
+ })
1080
+
1081
+ const cwdKey = (agentName: string, sessionId: string): string =>
1082
+ `${agentName}\u001f${sessionId}`
1083
+
1084
+ /** A session's working directory in its sandbox: Pi's cwd. */
1085
+ function sessionCwd({
1086
+ key,
1087
+ sandbox,
1088
+ }: {
1089
+ /** The agent and session, when known; otherwise the sandbox is asked. */
1090
+ key: string | undefined
1091
+ sandbox: () => Promise<Experimental_SandboxSession>
1092
+ }): Promise<string> {
1093
+ const cached = key === undefined ? undefined : cwds.get(key)
1094
+ if (cached !== undefined) return cached
1095
+ const cwd = sandbox().then(async (resolved) => {
1096
+ // Bounded, unlike `run`: a login profile in the sandbox can print anything.
1097
+ const output = { stdout: '', stderr: '' }
1098
+ const decoder = { stdout: new TextDecoder(), stderr: new TextDecoder() }
1099
+ const { exitCode } = await sandboxExec({
1100
+ sandbox: resolved,
1101
+ command: 'pwd',
1102
+ onData: (data, stream) => {
1103
+ output[stream] += decoder[stream].decode(data, { stream: true })
1104
+ },
1105
+ }).catch((error: unknown) => {
1106
+ throw new Error("pi() could not read the sandbox's working directory", {
1107
+ cause: error,
1108
+ })
1109
+ })
1110
+ const path = output.stdout.trim()
1111
+ if (exitCode !== 0 || !path.startsWith('/'))
1112
+ throw new Error(
1113
+ `pi() could not read the sandbox's working directory: ${(output.stderr || output.stdout).slice(0, 1000)}`,
1114
+ )
1115
+ return path
1116
+ })
1117
+ if (key !== undefined) {
1118
+ cwds.set(key, cwd, 1)
1119
+ cwd.catch(() => cwds.delete(key))
1120
+ }
1121
+ return cwd
1122
+ }
1123
+
1124
+ // ------------------------------------------------------------- Pi
1125
+
1126
+ let loading: Promise<PiModules> | undefined
1127
+
1128
+ function loadPi(): Promise<PiModules> {
1129
+ loading ??= Promise.all([
1130
+ import('@earendil-works/pi-coding-agent'),
1131
+ import('@earendil-works/pi-ai'),
1132
+ ]).then(
1133
+ ([agent, ai]) => ({ agent, ai }),
1134
+ (error: unknown) => {
1135
+ loading = undefined
1136
+ throw new Error(
1137
+ 'pi() needs @earendil-works/pi-coding-agent and @earendil-works/pi-ai: install them',
1138
+ { cause: error },
1139
+ )
1140
+ },
1141
+ )
1142
+ return loading
1143
+ }
1144
+
1145
+ /** Pi's resources: nothing discovered from disk, the agent's instructions appended. */
1146
+ function resources(modules: PiModules, instructions: string[]): ResourceLoader {
1147
+ const runtime = modules.agent.createExtensionRuntime()
1148
+ return {
1149
+ getExtensions: () => ({ extensions: [], errors: [], runtime }),
1150
+ getSkills: () => ({ skills: [], diagnostics: [] }),
1151
+ getPrompts: () => ({ prompts: [], diagnostics: [] }),
1152
+ getThemes: () => ({ themes: [], diagnostics: [] }),
1153
+ getAgentsFiles: () => ({ agentsFiles: [] }),
1154
+ getSystemPrompt: () => undefined,
1155
+ getSystemPromptSource: () => undefined,
1156
+ getAppendSystemPrompt: () => instructions,
1157
+ getAppendSystemPromptSources: () => [],
1158
+ extendResources: () => {},
1159
+ reload: async () => {},
1160
+ }
1161
+ }
1162
+
1163
+ function thinkingLevel(step: HarnessStep): {
1164
+ thinkingLevel?: 'off' | 'minimal' | 'low' | 'medium' | 'high' | 'xhigh'
1165
+ } {
1166
+ if (step.reasoning === undefined || step.reasoning === 'provider-default')
1167
+ return {}
1168
+ return { thinkingLevel: step.reasoning === 'none' ? 'off' : step.reasoning }
1169
+ }
1170
+
1171
+ function gatewayHeaders(credential: {
1172
+ token: string
1173
+ method: string
1174
+ }): Record<string, string> {
1175
+ return {
1176
+ authorization: `Bearer ${credential.token}`,
1177
+ 'ai-gateway-auth-method': credential.method,
1178
+ }
1179
+ }
1180
+
1181
+ /** A stable Pi session ID for an A2 session, so Pi's cache affinity holds. */
1182
+ function sessionUuid(agentName: string, sessionId: string): string {
1183
+ const hex = createHash('sha256')
1184
+ .update(`${agentName}\u001f${sessionId}`)
1185
+ .digest('hex')
1186
+ return `${hex.slice(0, 8)}-${hex.slice(8, 12)}-8${hex.slice(13, 16)}-${hex.slice(16, 20)}-${hex.slice(20, 32)}`
1187
+ }
1188
+
1189
+ function savedSession(state: unknown): Saved | undefined {
1190
+ if (typeof state !== 'object' || state === null) return undefined
1191
+ const { version, cwd, header, entries } = state as Record<string, unknown>
1192
+ if (
1193
+ version !== SNAPSHOT_VERSION ||
1194
+ typeof cwd !== 'string' ||
1195
+ typeof header !== 'object' ||
1196
+ header === null ||
1197
+ !Array.isArray(entries)
1198
+ )
1199
+ return undefined
1200
+ return {
1201
+ cwd,
1202
+ header: header as SessionHeader,
1203
+ entries: entries as SessionEntry[],
1204
+ }
1205
+ }
1206
+
1207
+ function compactionData(event: {
1208
+ result: { summary: string; tokensBefore: number; usage?: Usage } | undefined
1209
+ aborted: boolean
1210
+ errorMessage?: string
1211
+ }): CompactionPartData {
1212
+ if (event.result === undefined)
1213
+ return { status: event.aborted ? 'interrupted' : 'failed' }
1214
+ return {
1215
+ status: 'completed',
1216
+ inputTokens: event.result.tokensBefore,
1217
+ summary: event.result.summary,
1218
+ ...(event.result.usage === undefined
1219
+ ? {}
1220
+ : { usage: modelUsage(event.result.usage) }),
1221
+ }
1222
+ }
1223
+
1224
+ function finishReason(reason: AssistantMessage['stopReason']): FinishReason {
1225
+ switch (reason) {
1226
+ case 'stop':
1227
+ return 'stop'
1228
+ case 'length':
1229
+ return 'length'
1230
+ case 'toolUse':
1231
+ return 'tool-calls'
1232
+ case 'error':
1233
+ return 'error'
1234
+ default:
1235
+ return 'other'
1236
+ }
1237
+ }
1238
+
1239
+ function modelUsage(usage: Usage): LanguageModelUsage {
1240
+ return {
1241
+ inputTokens: usage.input + usage.cacheRead + usage.cacheWrite,
1242
+ inputTokenDetails: {
1243
+ noCacheTokens: usage.input,
1244
+ cacheReadTokens: usage.cacheRead,
1245
+ cacheWriteTokens: usage.cacheWrite,
1246
+ },
1247
+ outputTokens: usage.output,
1248
+ outputTokenDetails: {
1249
+ textTokens:
1250
+ usage.reasoning === undefined
1251
+ ? undefined
1252
+ : usage.output - usage.reasoning,
1253
+ reasoningTokens: usage.reasoning,
1254
+ },
1255
+ totalTokens: usage.totalTokens,
1256
+ }
1257
+ }
1258
+
1259
+ // ------------------------------------------------------------- helpers
1260
+
1261
+ function asError(error: unknown): Error {
1262
+ return error instanceof Error ? error : new Error(String(error))
1263
+ }