experimental-a2 0.16.1 → 0.17.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 (333) hide show
  1. package/CHANGELOG.md +352 -0
  2. package/dist/{actor-ohPC-81x.d.ts → actor-DBbe5xeE.d.ts} +21 -11
  3. package/dist/actor-DBbe5xeE.d.ts.map +1 -0
  4. package/dist/actor-client.d.ts +2 -5
  5. package/dist/actor-client.d.ts.map +1 -1
  6. package/dist/actor-client.js +13 -4
  7. package/dist/actor-client.js.map +1 -1
  8. package/dist/actor-react.d.ts +5 -3
  9. package/dist/actor-react.d.ts.map +1 -1
  10. package/dist/actor-react.js +20 -10
  11. package/dist/actor-react.js.map +1 -1
  12. package/dist/{actor-shared-B5tJfzt-.js → actor-shared-t7w8xIKP.js} +12 -7
  13. package/dist/actor-shared-t7w8xIKP.js.map +1 -0
  14. package/dist/actor.d.ts +1 -1
  15. package/dist/actor.js +42 -23
  16. package/dist/actor.js.map +1 -1
  17. package/dist/{ai-DgOBltJ_.js → ai-UrWfKeuv.js} +621 -216
  18. package/dist/ai-UrWfKeuv.js.map +1 -0
  19. package/dist/ai-client.d.ts +27 -0
  20. package/dist/ai-client.d.ts.map +1 -0
  21. package/dist/ai-client.js +29 -0
  22. package/dist/ai-client.js.map +1 -0
  23. package/dist/ai-react.d.ts +96 -0
  24. package/dist/ai-react.d.ts.map +1 -0
  25. package/dist/ai-react.js +142 -0
  26. package/dist/ai-react.js.map +1 -0
  27. package/dist/ai-server.browser.js +2 -2
  28. package/dist/ai-server.browser.js.map +1 -1
  29. package/dist/ai-server.d.ts +80 -36
  30. package/dist/ai-server.d.ts.map +1 -1
  31. package/dist/ai-server.js +432 -258
  32. package/dist/ai-server.js.map +1 -1
  33. package/dist/{ai-CrEf6p_W.d.ts → ai-vpGtcaCB.d.ts} +114 -100
  34. package/dist/ai-vpGtcaCB.d.ts.map +1 -0
  35. package/dist/ai.d.ts +2 -2
  36. package/dist/ai.js +2 -2
  37. package/dist/{client-BYzHjkwU.js → client-COaiaDub.js} +447 -80
  38. package/dist/client-COaiaDub.js.map +1 -0
  39. package/dist/{client-CzyacQpJ.d.ts → client-qRnRUzBS.d.ts} +14 -13
  40. package/dist/client-qRnRUzBS.d.ts.map +1 -0
  41. package/dist/client.d.ts +4 -2
  42. package/dist/client.js +1 -1
  43. package/dist/{contract-CKRg_E4q.js → contract-BYKASlCV.js} +12 -21
  44. package/dist/contract-BYKASlCV.js.map +1 -0
  45. package/dist/{idempotent-replay-DVOlyYbx.js → idempotent-replay-JdgMQRRm.js} +2 -2
  46. package/dist/{idempotent-replay-DVOlyYbx.js.map → idempotent-replay-JdgMQRRm.js.map} +1 -1
  47. package/dist/index.d.ts +6 -5
  48. package/dist/index.d.ts.map +1 -1
  49. package/dist/index.js +4 -3
  50. package/dist/{errors-DvhSXnxk.d.ts → initial-snapshot-BrLso5x_.d.ts} +7 -3
  51. package/dist/initial-snapshot-BrLso5x_.d.ts.map +1 -0
  52. package/dist/{internal-Dq2qYxou.js → internal-CbVvEwad.js} +23 -2
  53. package/dist/internal-CbVvEwad.js.map +1 -0
  54. package/dist/otel.d.ts +1 -1
  55. package/dist/react-BbJ-g6Uu.d.ts +86 -0
  56. package/dist/react-BbJ-g6Uu.d.ts.map +1 -0
  57. package/dist/react.d.ts +2 -122
  58. package/dist/react.js +107 -100
  59. package/dist/react.js.map +1 -1
  60. package/dist/reducer-BAXc8JyF.js +87 -0
  61. package/dist/reducer-BAXc8JyF.js.map +1 -0
  62. package/dist/{reducer-otzHjuJj.d.ts → reducer-BYu6DT7h.d.ts} +45 -29
  63. package/dist/reducer-BYu6DT7h.d.ts.map +1 -0
  64. package/dist/reducer-runtime-DQwz_J59.js +28 -0
  65. package/dist/reducer-runtime-DQwz_J59.js.map +1 -0
  66. package/dist/scheduler-qstash.d.ts +3 -3
  67. package/dist/scheduler-qstash.d.ts.map +1 -1
  68. package/dist/scheduler-qstash.js +33 -10
  69. package/dist/scheduler-qstash.js.map +1 -1
  70. package/dist/{scheduler-task-BpzhPnRS.js → scheduler-task-X1SToDjP.js} +18 -3
  71. package/dist/scheduler-task-X1SToDjP.js.map +1 -0
  72. package/dist/scheduler-vercel.d.ts +4 -9
  73. package/dist/scheduler-vercel.d.ts.map +1 -1
  74. package/dist/scheduler-vercel.js +25 -44
  75. package/dist/scheduler-vercel.js.map +1 -1
  76. package/dist/{server-DbD7IJVK.d.ts → server-CGM8WzRG.d.ts} +87 -26
  77. package/dist/server-CGM8WzRG.d.ts.map +1 -0
  78. package/dist/{server-BD5ckxZb.js → server-Rj64fNvX.js} +854 -202
  79. package/dist/server-Rj64fNvX.js.map +1 -0
  80. package/dist/server.d.ts +4 -4
  81. package/dist/server.js +2 -2
  82. package/dist/{errors-DCk6ch5n.js → session-clear-DGI4jTQ5.js} +17 -2
  83. package/dist/session-clear-DGI4jTQ5.js.map +1 -0
  84. package/dist/session-clear-Dshpuhff.d.ts +12 -0
  85. package/dist/session-clear-Dshpuhff.d.ts.map +1 -0
  86. package/dist/{store-DyZM6fS5.d.ts → store-Gpkq7WBo.d.ts} +44 -3
  87. package/dist/store-Gpkq7WBo.d.ts.map +1 -0
  88. package/dist/store-N8PXxDAS.js.map +1 -1
  89. package/dist/store-memory.d.ts +1 -1
  90. package/dist/store-memory.d.ts.map +1 -1
  91. package/dist/store-memory.js +182 -69
  92. package/dist/store-memory.js.map +1 -1
  93. package/dist/{store-polling-CmxUbV93.js → store-polling-4_y8lETm.js} +21 -4
  94. package/dist/store-polling-4_y8lETm.js.map +1 -0
  95. package/dist/store-postgres.d.ts +1 -1
  96. package/dist/store-postgres.d.ts.map +1 -1
  97. package/dist/store-postgres.js +399 -108
  98. package/dist/store-postgres.js.map +1 -1
  99. package/dist/{store-presence-polling-C7-XZyW9.js → store-presence-polling-Q1C-k2dd.js} +3 -3
  100. package/dist/{store-presence-polling-C7-XZyW9.js.map → store-presence-polling-Q1C-k2dd.js.map} +1 -1
  101. package/dist/store-redis-http.d.ts +1 -1
  102. package/dist/store-redis-http.d.ts.map +1 -1
  103. package/dist/store-redis-http.js +293 -11
  104. package/dist/store-redis-http.js.map +1 -1
  105. package/dist/store-redis-notify-tqWNnMRu.js +2374 -0
  106. package/dist/store-redis-notify-tqWNnMRu.js.map +1 -0
  107. package/dist/store-redis.d.ts +1 -1
  108. package/dist/store-redis.d.ts.map +1 -1
  109. package/dist/store-redis.js +11 -11
  110. package/dist/store-redis.js.map +1 -1
  111. package/dist/store-sqlite.d.ts +1 -1
  112. package/dist/store-sqlite.d.ts.map +1 -1
  113. package/dist/store-sqlite.js +166 -32
  114. package/dist/store-sqlite.js.map +1 -1
  115. package/dist/stream-buffer-Cx7AAPGq.js +23 -0
  116. package/dist/stream-buffer-Cx7AAPGq.js.map +1 -0
  117. package/dist/{telemetry-B5jzpy6y.d.ts → telemetry-cZH-cibj.d.ts} +2 -2
  118. package/dist/telemetry-cZH-cibj.d.ts.map +1 -0
  119. package/dist/{wire-BO5wWCb1.js → wire-DjfHOvdW.js} +132 -10
  120. package/dist/wire-DjfHOvdW.js.map +1 -0
  121. package/docs/01-quickstart.mdx +2 -2
  122. package/docs/actors/01-introduction.mdx +4 -0
  123. package/docs/actors/04-routes.mdx +14 -1
  124. package/docs/concepts/01-contracts.mdx +95 -0
  125. package/docs/concepts/04-state.mdx +3 -3
  126. package/docs/guides/01-timers.mdx +22 -14
  127. package/docs/guides/02-cancellation.mdx +1 -1
  128. package/docs/guides/03-react.mdx +268 -328
  129. package/docs/guides/04-local-first.mdx +32 -73
  130. package/docs/guides/05-production.mdx +40 -27
  131. package/docs/guides/06-ai-agents.mdx +671 -581
  132. package/docs/guides/09-presence.mdx +3 -7
  133. package/docs/guides/10-transports.mdx +5 -5
  134. package/docs/index.mdx +10 -18
  135. package/docs/reference/01-api.mdx +656 -194
  136. package/docs/reference/02-errors.mdx +2 -1
  137. package/examples/playground/DEPLOY.md +52 -6
  138. package/examples/playground/app/a0/[appId]/loading.tsx +16 -0
  139. package/examples/playground/app/a0/[appId]/page.tsx +48 -0
  140. package/examples/playground/app/a0/a0.css +818 -0
  141. package/examples/playground/app/a0/app-list.tsx +211 -0
  142. package/examples/playground/app/a0/app-view.tsx +55 -0
  143. package/examples/playground/app/a0/apps/[appId]/preview/route.ts +27 -0
  144. package/examples/playground/app/a0/apps/[appId]/route.ts +23 -0
  145. package/examples/playground/app/a0/apps.test.ts +116 -0
  146. package/examples/playground/app/a0/apps.ts +87 -0
  147. package/examples/playground/app/a0/chat.tsx +364 -0
  148. package/examples/playground/app/a0/columns.tsx +109 -0
  149. package/examples/playground/app/a0/context-menu.tsx +116 -0
  150. package/examples/playground/app/a0/cookies.ts +46 -0
  151. package/examples/playground/app/a0/crash-on-request.ts +72 -0
  152. package/examples/playground/app/a0/crashes/route.ts +3 -0
  153. package/examples/playground/app/a0/crashes.test.ts +33 -0
  154. package/examples/playground/app/a0/crashes.ts +28 -0
  155. package/examples/playground/app/a0/devtools.tsx +164 -0
  156. package/examples/playground/app/a0/events/route.ts +6 -0
  157. package/examples/playground/app/a0/flags.ts +30 -0
  158. package/examples/playground/app/a0/layout.tsx +38 -0
  159. package/examples/playground/app/a0/list/route.ts +10 -0
  160. package/examples/playground/app/a0/model.test.ts +56 -0
  161. package/examples/playground/app/a0/model.ts +58 -0
  162. package/examples/playground/app/a0/page.tsx +14 -0
  163. package/examples/playground/app/a0/preview-pane.tsx +145 -0
  164. package/examples/playground/app/a0/preview.test.ts +53 -0
  165. package/examples/playground/app/a0/preview.ts +26 -0
  166. package/examples/playground/app/a0/sandbox-log.ts +69 -0
  167. package/examples/playground/app/a0/sandbox.ts +191 -0
  168. package/examples/playground/app/a0/sandboxes/scheduler/route.ts +6 -0
  169. package/examples/playground/app/a0/scheduler/route.ts +7 -0
  170. package/examples/playground/app/a0/server.ts +158 -0
  171. package/examples/playground/app/a0/session.ts +47 -0
  172. package/examples/playground/app/a0/shell.tsx +150 -0
  173. package/examples/playground/app/a0/short/events/route.ts +9 -0
  174. package/examples/playground/app/a0/splitter.tsx +120 -0
  175. package/examples/playground/app/agent/[agentId]/agent-client.tsx +144 -174
  176. package/examples/playground/app/agent/[agentId]/command-receipt.test.tsx +65 -0
  177. package/examples/playground/app/agent/[agentId]/command-receipt.tsx +27 -0
  178. package/examples/playground/app/agent/[agentId]/compaction-event.tsx +25 -24
  179. package/examples/playground/app/agent/[agentId]/compaction-panel.test.tsx +119 -0
  180. package/examples/playground/app/agent/[agentId]/compaction-panel.tsx +208 -165
  181. package/examples/playground/app/agent/[agentId]/page.tsx +2 -16
  182. package/examples/playground/app/agent/events/route.ts +5 -2
  183. package/examples/playground/app/agent/ingress.test.ts +98 -0
  184. package/examples/playground/app/agent/model-input.test.tsx +58 -0
  185. package/examples/playground/app/agent/model-input.tsx +115 -0
  186. package/examples/playground/app/agent/model.test.ts +73 -0
  187. package/examples/playground/app/agent/model.ts +58 -26
  188. package/examples/playground/app/agent/new-agent-session.tsx +37 -19
  189. package/examples/playground/app/agent/page.tsx +4 -4
  190. package/examples/playground/app/agent/server.ts +15 -14
  191. package/examples/playground/app/agent/session.ts +5 -6
  192. package/examples/playground/app/agent/telemetry.test.ts +168 -0
  193. package/examples/playground/app/agent/telemetry.ts +30 -0
  194. package/examples/playground/app/agent/trace.test.ts +23 -0
  195. package/examples/playground/app/agent/trace.ts +18 -0
  196. package/examples/playground/app/canvas/[canvasId]/canvas-client.tsx +11 -11
  197. package/examples/playground/app/canvas/[canvasId]/page.tsx +1 -2
  198. package/examples/playground/app/canvas/[canvasId]/session.ts +2 -2
  199. package/examples/playground/app/canvas/model.ts +53 -53
  200. package/examples/playground/app/chat/[chatId]/agent-state.test.ts +176 -0
  201. package/examples/playground/app/chat/[chatId]/agent-stream-drawer.tsx +16 -1
  202. package/examples/playground/app/chat/[chatId]/chat-client.tsx +55 -77
  203. package/examples/playground/app/chat/[chatId]/chat-view.ts +37 -3
  204. package/examples/playground/app/chat/[chatId]/page.tsx +7 -15
  205. package/examples/playground/app/chat/[chatId]/session.ts +2 -19
  206. package/examples/playground/app/chat/agent/route.ts +9 -0
  207. package/examples/playground/app/chat/model.test.ts +46 -119
  208. package/examples/playground/app/chat/model.ts +82 -128
  209. package/examples/playground/app/chat/scheduler/route.ts +2 -2
  210. package/examples/playground/app/chat/server.test.ts +108 -0
  211. package/examples/playground/app/chat/server.ts +69 -73
  212. package/examples/playground/app/components/activity-feed.tsx +30 -1
  213. package/examples/playground/app/components/activity-history.test.tsx +35 -0
  214. package/examples/playground/app/components/demo-icons.tsx +89 -0
  215. package/examples/playground/app/components/demos.tsx +33 -0
  216. package/examples/playground/app/components/fast-link.test.tsx +58 -0
  217. package/examples/playground/app/components/fast-link.tsx +55 -0
  218. package/examples/playground/app/components/site-header.tsx +96 -0
  219. package/examples/playground/app/counter/counter-client.tsx +3 -3
  220. package/examples/playground/app/counter/model.test.ts +3 -3
  221. package/examples/playground/app/counter/model.ts +11 -21
  222. package/examples/playground/app/counter/page.tsx +2 -15
  223. package/examples/playground/app/counter/session.ts +2 -2
  224. package/examples/playground/app/documents/[documentId]/code-editor.tsx +12 -1
  225. package/examples/playground/app/documents/[documentId]/document-client.tsx +10 -11
  226. package/examples/playground/app/documents/[documentId]/page.tsx +1 -2
  227. package/examples/playground/app/documents/[documentId]/session.ts +2 -2
  228. package/examples/playground/app/documents/model.ts +27 -27
  229. package/examples/playground/app/globals.css +420 -207
  230. package/examples/playground/app/initial-state.test.tsx +54 -0
  231. package/examples/playground/app/layout.tsx +7 -23
  232. package/examples/playground/app/orders/[orderId]/page.tsx +1 -2
  233. package/examples/playground/app/orders/[orderId]/session.ts +2 -2
  234. package/examples/playground/app/orders/model.ts +49 -49
  235. package/examples/playground/app/page.tsx +13 -102
  236. package/examples/playground/app/recovery/[recoveryId]/page.tsx +1 -2
  237. package/examples/playground/app/recovery/[recoveryId]/session.ts +2 -2
  238. package/examples/playground/app/recovery/model.ts +30 -30
  239. package/examples/playground/instrumentation.ts +5 -0
  240. package/examples/playground/package.json +10 -2
  241. package/examples/playground/scripts/a0-template.ts +77 -0
  242. package/examples/playground/vercel.json +18 -0
  243. package/package.json +3 -1
  244. package/src/actor-client.ts +16 -4
  245. package/src/actor-react.ts +25 -11
  246. package/src/actor-shared.ts +21 -6
  247. package/src/actor.ts +76 -31
  248. package/src/ai-agent-actions.ts +219 -0
  249. package/src/ai-agent.ts +2343 -0
  250. package/src/ai-app-state.ts +91 -0
  251. package/src/ai-client-state.ts +6 -3
  252. package/src/ai-client.ts +62 -0
  253. package/src/ai-context-schema.ts +25 -5
  254. package/src/ai-context.ts +90 -40
  255. package/src/ai-control-server.ts +92 -72
  256. package/src/ai-coordinator.ts +23 -22
  257. package/src/ai-message-base.ts +209 -0
  258. package/src/ai-message-projection.ts +262 -64
  259. package/src/ai-progress-batches.ts +26 -0
  260. package/src/ai-projector.ts +95 -14
  261. package/src/ai-react.ts +146 -0
  262. package/src/ai-server.browser.ts +2 -2
  263. package/src/ai-server.ts +20 -1964
  264. package/src/ai-stored-state.ts +48 -163
  265. package/src/ai.ts +339 -155
  266. package/src/bootstrap-stream.ts +72 -0
  267. package/src/bounded-cache.ts +55 -0
  268. package/src/client-render-session.ts +49 -0
  269. package/src/client.ts +688 -151
  270. package/src/contract.ts +92 -42
  271. package/src/deterministic-id.ts +37 -0
  272. package/src/equal-json.ts +32 -0
  273. package/src/errors.ts +13 -1
  274. package/src/index.ts +5 -1
  275. package/src/initial-snapshot.ts +39 -0
  276. package/src/internal.ts +45 -0
  277. package/src/postgres-notification-scope.ts +14 -3
  278. package/src/react.ts +171 -206
  279. package/src/redis-http-reply.ts +256 -0
  280. package/src/redis-http-stream.ts +73 -0
  281. package/src/redis-http-subscriptions.ts +13 -5
  282. package/src/reducer-runtime.ts +52 -0
  283. package/src/reducer.ts +92 -10
  284. package/src/scheduler-qstash.ts +49 -20
  285. package/src/scheduler-task.ts +34 -2
  286. package/src/scheduler-vercel.ts +32 -84
  287. package/src/server-fetch.ts +139 -35
  288. package/src/server.ts +957 -237
  289. package/src/session-clear.ts +23 -0
  290. package/src/session-socket.ts +154 -49
  291. package/src/sse.ts +71 -4
  292. package/src/store-memory.ts +220 -78
  293. package/src/store-polling.ts +18 -4
  294. package/src/store-postgres.ts +461 -114
  295. package/src/store-redis-core.ts +1260 -434
  296. package/src/store-redis-http.ts +18 -2
  297. package/src/store-redis-notify.ts +260 -85
  298. package/src/store-redis.ts +19 -9
  299. package/src/store-sqlite.ts +259 -45
  300. package/src/store.ts +46 -1
  301. package/src/stream-buffer.ts +43 -0
  302. package/src/telemetry.ts +13 -1
  303. package/src/wire.ts +241 -13
  304. package/dist/actor-ohPC-81x.d.ts.map +0 -1
  305. package/dist/actor-shared-B5tJfzt-.js.map +0 -1
  306. package/dist/ai-CrEf6p_W.d.ts.map +0 -1
  307. package/dist/ai-DgOBltJ_.js.map +0 -1
  308. package/dist/client-BYzHjkwU.js.map +0 -1
  309. package/dist/client-CzyacQpJ.d.ts.map +0 -1
  310. package/dist/contract-CKRg_E4q.js.map +0 -1
  311. package/dist/errors-DCk6ch5n.js.map +0 -1
  312. package/dist/errors-DvhSXnxk.d.ts.map +0 -1
  313. package/dist/internal-Dq2qYxou.js.map +0 -1
  314. package/dist/react.d.ts.map +0 -1
  315. package/dist/reducer-DEMjEY_O.js +0 -29
  316. package/dist/reducer-DEMjEY_O.js.map +0 -1
  317. package/dist/reducer-otzHjuJj.d.ts.map +0 -1
  318. package/dist/scheduler-task-BpzhPnRS.js.map +0 -1
  319. package/dist/server-BD5ckxZb.js.map +0 -1
  320. package/dist/server-DbD7IJVK.d.ts.map +0 -1
  321. package/dist/store-DyZM6fS5.d.ts.map +0 -1
  322. package/dist/store-polling-CmxUbV93.js.map +0 -1
  323. package/dist/store-redis-notify-BVLUfI3j.js +0 -1583
  324. package/dist/store-redis-notify-BVLUfI3j.js.map +0 -1
  325. package/dist/telemetry-B5jzpy6y.d.ts.map +0 -1
  326. package/dist/validate-XKT4FSNn.js +0 -30
  327. package/dist/validate-XKT4FSNn.js.map +0 -1
  328. package/dist/wire-BO5wWCb1.js.map +0 -1
  329. package/examples/playground/app/agent/[agentId]/compaction/route.ts +0 -14
  330. package/examples/playground/app/agent/compaction-settings.test.ts +0 -144
  331. package/examples/playground/app/agent/compaction-settings.ts +0 -49
  332. package/examples/playground/app/agent/compaction-timeline.test.ts +0 -377
  333. package/examples/playground/app/agent/compaction-timeline.ts +0 -208
@@ -1,10 +1,10 @@
1
+ import { reducer as makeReducer } from './reducer.ts'
1
2
  // oxlint-disable no-await-in-loop -- work acknowledgements follow the durable stream.
2
3
  import { applyControlChanges, controlCommit } from './ai-control-state.ts'
3
4
  import type { ControlChange, ControlCommit } from './ai-control.ts'
4
5
  import type { UIMessage } from 'ai'
5
6
  import type {
6
7
  AIEventDefs,
7
- AgentDefinition,
8
8
  GenerationRequestedPayload,
9
9
  MessageInterruptedPayload,
10
10
  } from './ai.ts'
@@ -21,7 +21,12 @@ import {
21
21
  type WorkFact,
22
22
  type WorkReport,
23
23
  } from './ai-control.ts'
24
- import type { AppendInput, ContractEvent, EventDefs } from './contract.ts'
24
+ import type {
25
+ AppendInput,
26
+ Contract,
27
+ ContractEvent,
28
+ EventDefs,
29
+ } from './contract.ts'
25
30
  import type { HandlerContext } from './server.ts'
26
31
  import type { Reducer } from './reducer.ts'
27
32
  import type { AICoordinatorState, CoordinatorCall } from './ai-coordinator.ts'
@@ -29,6 +34,7 @@ import { validateSync } from './validate.ts'
29
34
  import type { StandardSchemaV1 } from './standard-schema.ts'
30
35
  import { A2Error } from './errors.ts'
31
36
  import { aiEventId } from './ai-id.ts'
37
+ import { CONSOLE_TELEMETRY, type A2Telemetry } from './telemetry.ts'
32
38
 
33
39
  export class ControlCancelled extends Error {}
34
40
 
@@ -69,41 +75,42 @@ export const createControlRuntime = <
69
75
  M extends UIMessage,
70
76
  D extends AIEventDefs<M> & EventDefs,
71
77
  >(options: {
72
- agent: AgentDefinition<M, D>
78
+ contract: Contract<D, Record<never, never>, unknown>
79
+ telemetry?: A2Telemetry
73
80
  }): AIControlRuntime<M, D> => {
74
- const reducer = options.agent.contract
75
- .reducer({
76
- name: 'a2.ai.control.v5',
77
- initialState: initialControlState<M>(),
78
- })
79
- .fold((state, event) => {
80
- if (event.type === 'ai.control.requested') {
81
- const command = event.payload as ControlCommand<M>
82
- if (command.action === 'stop')
83
- return {
84
- ...state,
85
- pendingStops: {
86
- ...state.pendingStops,
87
- [event.id]: command.turnId,
88
- },
89
- }
90
- }
91
- if (event.type === 'ai.control.decided') {
92
- const receipt = event.payload as ControlReceipt
93
- if (Object.hasOwn(state.pendingStops, receipt.commandId)) {
94
- const pendingStops = { ...state.pendingStops }
95
- delete pendingStops[receipt.commandId]
96
- return { ...state, pendingStops }
81
+ const telemetry = options.telemetry ?? CONSOLE_TELEMETRY
82
+ const reducer = makeReducer({
83
+ contract: options.contract,
84
+ name: 'a2.ai.control.v5',
85
+ initialState: initialControlState<M>(),
86
+ }).fold((state, event) => {
87
+ if (event.type === 'ai.control.requested') {
88
+ const command = event.payload as ControlCommand<M>
89
+ if (command.action === 'stop')
90
+ return {
91
+ ...state,
92
+ pendingStops: {
93
+ ...state.pendingStops,
94
+ [event.id]: command.turnId,
95
+ },
97
96
  }
97
+ }
98
+ if (event.type === 'ai.control.decided') {
99
+ const receipt = event.payload as ControlReceipt
100
+ if (Object.hasOwn(state.pendingStops, receipt.commandId)) {
101
+ const pendingStops = { ...state.pendingStops }
102
+ delete pendingStops[receipt.commandId]
103
+ return { ...state, pendingStops }
98
104
  }
99
- return event.type === 'ai.control.committed'
100
- ? applyControlChanges({
101
- state,
102
- changes: (event.payload as ControlCommit).changes,
103
- })
104
- : state
105
- })
106
- const coordinator = aiCoordinatorReducer(options.agent.contract)
105
+ }
106
+ return event.type === 'ai.control.committed'
107
+ ? applyControlChanges({
108
+ state,
109
+ changes: (event.payload as ControlCommit).changes,
110
+ })
111
+ : state
112
+ })
113
+ const coordinator = aiCoordinatorReducer(options.contract)
107
114
 
108
115
  const handler = async (
109
116
  ctx: HandlerContext<
@@ -438,7 +445,7 @@ export const createControlRuntime = <
438
445
  },
439
446
  id: await aiEventId(
440
447
  'a2.ai.request.retry',
441
- options.agent.contract.name,
448
+ options.contract.name,
442
449
  ctx.event.sessionId,
443
450
  command.request.retryId,
444
451
  ),
@@ -668,7 +675,7 @@ export const createControlRuntime = <
668
675
  payload: { message: item.message, generate: item.generate },
669
676
  id: await aiEventId(
670
677
  'a2.ai.admitted',
671
- options.agent.contract.name,
678
+ options.contract.name,
672
679
  ctx.event.sessionId,
673
680
  item.id,
674
681
  ),
@@ -684,7 +691,7 @@ export const createControlRuntime = <
684
691
  }
685
692
  const requestId = await aiEventId(
686
693
  'a2.ai.request.message',
687
- options.agent.contract.name,
694
+ options.contract.name,
688
695
  ctx.event.sessionId,
689
696
  item.id,
690
697
  )
@@ -736,7 +743,7 @@ export const createControlRuntime = <
736
743
  GenerationRequestedPayload | { turnId: string; version: number }
737
744
  const { turnId, version } = 'turnId' in payload ? payload : payload.control!
738
745
  for (const event of events) {
739
- const schema = options.agent.contract.events[
746
+ const schema = options.contract.events[
740
747
  event.type
741
748
  ] as StandardSchemaV1<unknown>
742
749
  const result = validateSync(
@@ -808,41 +815,54 @@ export const createControlRuntime = <
808
815
  const input = await report({ ctx, events: lifecycle, settled: false, name })
809
816
  const persisted = await ctx.session.append(name, ...progress, input)
810
817
  const index = persisted.at(-1)!.index
811
- const feed = ctx.session.stream({ startAfter: index })
812
- const stream = feed[Symbol.asyncIterator]()
813
- const canonical: ContractEvent<D>[] = []
814
- const wanted = new Set(lifecycle.map((event) => event.id))
815
- let rejectAbort!: (error: unknown) => void
816
- const aborted = new Promise<never>((_, reject) => {
817
- rejectAbort = reject
818
- })
819
- const onAbort = (): void => {
820
- rejectAbort(ctx.signal.reason)
821
- }
822
- ctx.signal.addEventListener('abort', onAbort, { once: true })
823
- try {
824
- ctx.signal.throwIfAborted()
825
- for (;;) {
826
- const item = await Promise.race([stream.next(), aborted])
827
- if (item.done)
828
- throw new Error('AI control acknowledgement stream ended')
829
- if (wanted.has(item.value.id)) canonical.push(item.value)
830
- if (
831
- item.value.type === 'ai.control.decided' &&
832
- (item.value.payload as ControlReceipt).commandId === input.id
833
- ) {
834
- if ((item.value.payload as ControlReceipt).outcome === 'rejected')
835
- throw new ControlCancelled('stale AI work')
836
- return [
837
- ...persisted.filter((event) => event.type !== 'ai.work.reported'),
838
- ...canonical,
839
- ]
818
+ return telemetry.span(
819
+ 'a2.ai.ack',
820
+ {
821
+ 'a2.contract': options.contract.name,
822
+ 'a2.session_id': ctx.event.sessionId,
823
+ 'a2.ai.ack.request_id': ctx.event.id,
824
+ 'a2.ai.ack.report_id': input.id!,
825
+ },
826
+ async () => {
827
+ const feed = ctx.session.stream({ startAfter: index })
828
+ const stream = feed[Symbol.asyncIterator]()
829
+ const canonical: ContractEvent<D>[] = []
830
+ const wanted = new Set(lifecycle.map((event) => event.id))
831
+ let rejectAbort!: (error: unknown) => void
832
+ const aborted = new Promise<never>((_, reject) => {
833
+ rejectAbort = reject
834
+ })
835
+ const onAbort = (): void => {
836
+ rejectAbort(ctx.signal.reason)
840
837
  }
841
- }
842
- } finally {
843
- ctx.signal.removeEventListener('abort', onAbort)
844
- await stream.return?.()
845
- }
838
+ ctx.signal.addEventListener('abort', onAbort, { once: true })
839
+ try {
840
+ ctx.signal.throwIfAborted()
841
+ for (;;) {
842
+ const item = await Promise.race([stream.next(), aborted])
843
+ if (item.done)
844
+ throw new Error('AI control acknowledgement stream ended')
845
+ if (wanted.has(item.value.id)) canonical.push(item.value)
846
+ if (
847
+ item.value.type === 'ai.control.decided' &&
848
+ (item.value.payload as ControlReceipt).commandId === input.id
849
+ ) {
850
+ if ((item.value.payload as ControlReceipt).outcome === 'rejected')
851
+ throw new ControlCancelled('stale AI work')
852
+ return [
853
+ ...persisted.filter(
854
+ (event) => event.type !== 'ai.work.reported',
855
+ ),
856
+ ...canonical,
857
+ ]
858
+ }
859
+ }
860
+ } finally {
861
+ ctx.signal.removeEventListener('abort', onAbort)
862
+ await stream.return?.()
863
+ }
864
+ },
865
+ )
846
866
  }
847
867
 
848
868
  const settled = ({
@@ -1,3 +1,4 @@
1
+ import { reducer } from './reducer.ts'
1
2
  import type { UIMessage } from 'ai'
2
3
  import { applyControlChanges } from './ai-control-state.ts'
3
4
  import type { ControlCommit, ControlWork } from './ai-control.ts'
@@ -255,7 +256,8 @@ export const foldCoordinator = <M extends UIMessage, D extends EventDefs>(
255
256
  return inputTokens !== undefined &&
256
257
  Number.isFinite(inputTokens) &&
257
258
  inputTokens >= 0 &&
258
- estimate !== undefined
259
+ estimate !== undefined &&
260
+ state.response.generation.model !== undefined
259
261
  ? {
260
262
  ...uncalibrated,
261
263
  calibration: {
@@ -436,28 +438,27 @@ export const aiCoordinatorReducer = <
436
438
  M extends UIMessage,
437
439
  D extends AIEventDefs<M> & EventDefs,
438
440
  >(
439
- contract: Contract<D>,
441
+ contract: Contract<D, Record<never, never>, unknown>,
440
442
  ): Reducer<D, AICoordinatorState> =>
441
- contract
442
- .reducer({
443
- name: 'a2.ai.coordinator.v6',
444
- initialState: {
445
- closed: false,
446
- queued: [],
447
- } as AICoordinatorState,
448
- })
449
- .fold((state, event) =>
450
- event.type === 'ai.control.committed'
451
- ? applyControlChanges({
452
- state,
453
- changes: (event.payload as ControlCommit).changes
454
- .filter((change) => change.path[0] === 'coordinator')
455
- .map((change) =>
456
- Object.assign({}, change, { path: change.path.slice(1) }),
457
- ),
458
- })
459
- : state,
460
- )
443
+ reducer({
444
+ contract: contract,
445
+ name: 'a2.ai.coordinator.v6',
446
+ initialState: {
447
+ closed: false,
448
+ queued: [],
449
+ } as AICoordinatorState,
450
+ }).fold((state, event) =>
451
+ event.type === 'ai.control.committed'
452
+ ? applyControlChanges({
453
+ state,
454
+ changes: (event.payload as ControlCommit).changes
455
+ .filter((change) => change.path[0] === 'coordinator')
456
+ .map((change) =>
457
+ Object.assign({}, change, { path: change.path.slice(1) }),
458
+ ),
459
+ })
460
+ : state,
461
+ )
461
462
 
462
463
  export const continuationReady = (state: AICoordinatorState): boolean => {
463
464
  const response = state.response
@@ -0,0 +1,209 @@
1
+ import type { UIMessage } from 'ai'
2
+ import { sameValue } from './equal-json.ts'
3
+
4
+ type FieldUndo = { value?: unknown } | { remove: true } | { length: number }
5
+ type FieldsUndo = Record<string, FieldUndo>
6
+ type PartUndo = { value: UIMessage['parts'][number] } | { fields: FieldsUndo }
7
+
8
+ export type MessageBase = {
9
+ partCount: number
10
+ fields?: FieldsUndo
11
+ parts?: Record<string, PartUndo>
12
+ }
13
+
14
+ const record = (value: unknown): value is Record<string, unknown> =>
15
+ typeof value === 'object' && value !== null && !Array.isArray(value)
16
+
17
+ const assertFields = (fields: unknown): void => {
18
+ if (fields === undefined) return
19
+ if (!record(fields)) throw new TypeError('invalid AI message base fields')
20
+ for (const [key, undo] of Object.entries(fields)) {
21
+ if (
22
+ !record(undo) ||
23
+ Object.keys(undo).length > 1 ||
24
+ !(
25
+ Object.keys(undo).length === 0 ||
26
+ 'value' in undo ||
27
+ undo['remove'] === true ||
28
+ (key === 'text' &&
29
+ Number.isSafeInteger(undo['length']) &&
30
+ (undo['length'] as number) >= 0)
31
+ )
32
+ )
33
+ throw new TypeError('invalid AI message base field')
34
+ }
35
+ }
36
+
37
+ /** Checks decoded recovery data; `restoreMessageBase` trusts this shape. */
38
+ export function assertMessageBase(
39
+ value: unknown,
40
+ ): asserts value is MessageBase {
41
+ if (
42
+ !record(value) ||
43
+ !Number.isSafeInteger(value['partCount']) ||
44
+ (value['partCount'] as number) < 0 ||
45
+ (value['partCount'] as number) > 0xffffffff - 1 ||
46
+ Object.keys(value).some(
47
+ (key) => !['partCount', 'fields', 'parts'].includes(key),
48
+ ) ||
49
+ (value['parts'] !== undefined && !record(value['parts']))
50
+ )
51
+ throw new TypeError('invalid AI message base')
52
+ assertFields(value['fields'])
53
+ const partCount = value['partCount'] as number
54
+ for (const [key, undo] of Object.entries(value['parts'] ?? {})) {
55
+ const index = Number(key)
56
+ if (
57
+ !Number.isSafeInteger(index) ||
58
+ String(index) !== key ||
59
+ index < 0 ||
60
+ index >= partCount ||
61
+ !record(undo) ||
62
+ Object.keys(undo).length !== 1 ||
63
+ !(('value' in undo && record(undo['value'])) || 'fields' in undo)
64
+ )
65
+ throw new TypeError('invalid AI message base part')
66
+ if ('fields' in undo) assertFields(undo['fields'])
67
+ }
68
+ }
69
+
70
+ const restoreFields = (
71
+ current: Record<string, unknown>,
72
+ fields: FieldsUndo | undefined,
73
+ ): Record<string, unknown> | undefined => {
74
+ if (fields === undefined) return current
75
+ const restored = { ...current }
76
+ for (const [key, undo] of Object.entries(fields)) {
77
+ if ('remove' in undo) delete restored[key]
78
+ else if ('length' in undo) {
79
+ const text = current[key]
80
+ if (typeof text !== 'string' || undo.length > text.length)
81
+ return undefined
82
+ restored[key] = text.slice(0, undo.length)
83
+ } else
84
+ Object.defineProperty(restored, key, {
85
+ value: undo.value,
86
+ enumerable: true,
87
+ writable: true,
88
+ configurable: true,
89
+ })
90
+ }
91
+ return restored
92
+ }
93
+
94
+ /** `undefined` when `base` no longer describes `current`, including a removed message. */
95
+ export const restoreMessageBase = <M extends UIMessage>(options: {
96
+ current: M | undefined
97
+ base: MessageBase | undefined
98
+ }): M | undefined => {
99
+ const { current, base } = options
100
+ if (base === undefined || current === undefined) return undefined
101
+ const parts = current.parts.slice(0, base.partCount)
102
+ for (const [key, undo] of Object.entries(base.parts ?? {})) {
103
+ const index = Number(key)
104
+ if ('value' in undo) {
105
+ parts[index] = undo.value as M['parts'][number]
106
+ continue
107
+ }
108
+ const part = parts[index]
109
+ const restored =
110
+ part === undefined
111
+ ? undefined
112
+ : restoreFields(part as unknown as Record<string, unknown>, undo.fields)
113
+ if (!restored) return undefined
114
+ parts[index] = restored as M['parts'][number]
115
+ }
116
+ if (
117
+ parts.length !== base.partCount ||
118
+ Object.keys(parts).length !== parts.length
119
+ )
120
+ return undefined
121
+ const { parts: _parts, ...fields } = current
122
+ const restored = restoreFields(fields, base.fields)
123
+ return restored && ({ ...restored, parts } as M)
124
+ }
125
+
126
+ const updateFields = (options: {
127
+ before: Record<string, unknown>
128
+ after: Record<string, unknown>
129
+ undo?: FieldsUndo
130
+ }): FieldsUndo | undefined => {
131
+ const { before, after, undo } = options
132
+ const original = restoreFields(before, undo)
133
+ if (!original) return undefined
134
+ return Object.fromEntries(
135
+ [...new Set([...Object.keys(original), ...Object.keys(after)])].flatMap(
136
+ (key) => {
137
+ if (
138
+ Object.hasOwn(original, key) === Object.hasOwn(after, key) &&
139
+ sameValue(original[key], after[key])
140
+ )
141
+ return []
142
+ const value = original[key]
143
+ const next = after[key]
144
+ const change: FieldUndo = !Object.hasOwn(original, key)
145
+ ? { remove: true }
146
+ : key === 'text' &&
147
+ typeof value === 'string' &&
148
+ typeof next === 'string' &&
149
+ next.startsWith(value)
150
+ ? { length: value.length }
151
+ : value === undefined
152
+ ? {}
153
+ : { value }
154
+ return [[key, change]]
155
+ },
156
+ ),
157
+ )
158
+ }
159
+
160
+ /** `undefined` when `before` no longer matches `base`, so the recovery must be dropped. */
161
+ export const updateMessageBase = (options: {
162
+ base: MessageBase
163
+ before: UIMessage
164
+ after: UIMessage
165
+ }): MessageBase | undefined => {
166
+ const { base, before, after } = options
167
+ if (before === after) return base
168
+ const { parts: beforeParts, ...beforeFields } = before
169
+ const { parts: afterParts, ...afterFields } = after
170
+ const fields = updateFields({
171
+ before: beforeFields,
172
+ after: afterFields,
173
+ ...(base.fields && { undo: base.fields }),
174
+ })
175
+ if (!fields) return undefined
176
+ const parts: Array<[string, PartUndo]> = []
177
+ for (let index = 0; index < base.partCount; index += 1) {
178
+ const previous = beforeParts[index]
179
+ const current = afterParts[index]
180
+ const undo = base.parts?.[index]
181
+ const original = undo && 'value' in undo ? undo.value : previous
182
+ if (!original) return undefined
183
+ if (previous === current && undo === undefined) continue
184
+ const originalFields = original as unknown as Record<string, unknown>
185
+ const undoFields = undo && 'fields' in undo ? undo.fields : undefined
186
+ if (!current) {
187
+ const value = restoreFields(originalFields, undoFields)
188
+ if (!value) return undefined
189
+ parts.push([
190
+ String(index),
191
+ { value: value as UIMessage['parts'][number] },
192
+ ])
193
+ continue
194
+ }
195
+ const changed = updateFields({
196
+ before: originalFields,
197
+ after: current as unknown as Record<string, unknown>,
198
+ ...(undoFields && { undo: undoFields }),
199
+ })
200
+ if (!changed) return undefined
201
+ if (Object.keys(changed).length > 0)
202
+ parts.push([String(index), { fields: changed }])
203
+ }
204
+ return {
205
+ partCount: base.partCount,
206
+ ...(Object.keys(fields).length > 0 && { fields }),
207
+ ...(parts.length > 0 && { parts: Object.fromEntries(parts) }),
208
+ }
209
+ }