experimental-a2 0.16.0 → 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 (334) hide show
  1. package/CHANGELOG.md +360 -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-CrLYwNEx.js → ai-UrWfKeuv.js} +966 -541
  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 +466 -265
  32. package/dist/ai-server.js.map +1 -1
  33. package/dist/{ai-HJ9fHfYI.d.ts → ai-vpGtcaCB.d.ts} +115 -101
  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 +680 -580
  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 +662 -195
  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 +39 -2
  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 +94 -55
  256. package/src/ai-control.ts +2 -0
  257. package/src/ai-coordinator.ts +23 -22
  258. package/src/ai-message-base.ts +209 -0
  259. package/src/ai-message-projection.ts +382 -58
  260. package/src/ai-progress-batches.ts +26 -0
  261. package/src/ai-projector.ts +95 -14
  262. package/src/ai-react.ts +146 -0
  263. package/src/ai-server.browser.ts +2 -2
  264. package/src/ai-server.ts +20 -1951
  265. package/src/ai-stored-state.ts +48 -163
  266. package/src/ai.ts +343 -276
  267. package/src/bootstrap-stream.ts +72 -0
  268. package/src/bounded-cache.ts +55 -0
  269. package/src/client-render-session.ts +49 -0
  270. package/src/client.ts +688 -151
  271. package/src/contract.ts +92 -42
  272. package/src/deterministic-id.ts +37 -0
  273. package/src/equal-json.ts +32 -0
  274. package/src/errors.ts +13 -1
  275. package/src/index.ts +5 -1
  276. package/src/initial-snapshot.ts +39 -0
  277. package/src/internal.ts +45 -0
  278. package/src/postgres-notification-scope.ts +14 -3
  279. package/src/react.ts +171 -206
  280. package/src/redis-http-reply.ts +256 -0
  281. package/src/redis-http-stream.ts +73 -0
  282. package/src/redis-http-subscriptions.ts +13 -5
  283. package/src/reducer-runtime.ts +52 -0
  284. package/src/reducer.ts +92 -10
  285. package/src/scheduler-qstash.ts +49 -20
  286. package/src/scheduler-task.ts +34 -2
  287. package/src/scheduler-vercel.ts +32 -84
  288. package/src/server-fetch.ts +139 -35
  289. package/src/server.ts +957 -237
  290. package/src/session-clear.ts +23 -0
  291. package/src/session-socket.ts +154 -49
  292. package/src/sse.ts +71 -4
  293. package/src/store-memory.ts +220 -78
  294. package/src/store-polling.ts +18 -4
  295. package/src/store-postgres.ts +461 -114
  296. package/src/store-redis-core.ts +1260 -434
  297. package/src/store-redis-http.ts +18 -2
  298. package/src/store-redis-notify.ts +260 -85
  299. package/src/store-redis.ts +19 -9
  300. package/src/store-sqlite.ts +259 -45
  301. package/src/store.ts +46 -1
  302. package/src/stream-buffer.ts +43 -0
  303. package/src/telemetry.ts +13 -1
  304. package/src/wire.ts +241 -13
  305. package/dist/actor-ohPC-81x.d.ts.map +0 -1
  306. package/dist/actor-shared-B5tJfzt-.js.map +0 -1
  307. package/dist/ai-CrLYwNEx.js.map +0 -1
  308. package/dist/ai-HJ9fHfYI.d.ts.map +0 -1
  309. package/dist/client-BYzHjkwU.js.map +0 -1
  310. package/dist/client-CzyacQpJ.d.ts.map +0 -1
  311. package/dist/contract-CKRg_E4q.js.map +0 -1
  312. package/dist/errors-DCk6ch5n.js.map +0 -1
  313. package/dist/errors-DvhSXnxk.d.ts.map +0 -1
  314. package/dist/internal-Dq2qYxou.js.map +0 -1
  315. package/dist/react.d.ts.map +0 -1
  316. package/dist/reducer-DEMjEY_O.js +0 -29
  317. package/dist/reducer-DEMjEY_O.js.map +0 -1
  318. package/dist/reducer-otzHjuJj.d.ts.map +0 -1
  319. package/dist/scheduler-task-BpzhPnRS.js.map +0 -1
  320. package/dist/server-BD5ckxZb.js.map +0 -1
  321. package/dist/server-DbD7IJVK.d.ts.map +0 -1
  322. package/dist/store-DyZM6fS5.d.ts.map +0 -1
  323. package/dist/store-polling-CmxUbV93.js.map +0 -1
  324. package/dist/store-redis-notify-BVLUfI3j.js +0 -1583
  325. package/dist/store-redis-notify-BVLUfI3j.js.map +0 -1
  326. package/dist/telemetry-B5jzpy6y.d.ts.map +0 -1
  327. package/dist/validate-XKT4FSNn.js +0 -30
  328. package/dist/validate-XKT4FSNn.js.map +0 -1
  329. package/dist/wire-BO5wWCb1.js.map +0 -1
  330. package/examples/playground/app/agent/[agentId]/compaction/route.ts +0 -14
  331. package/examples/playground/app/agent/compaction-settings.test.ts +0 -144
  332. package/examples/playground/app/agent/compaction-settings.ts +0 -49
  333. package/examples/playground/app/agent/compaction-timeline.test.ts +0 -377
  334. package/examples/playground/app/agent/compaction-timeline.ts +0 -208
package/src/ai.ts CHANGED
@@ -1,14 +1,17 @@
1
+ import type { MessageBase } from './ai-message-base.ts'
2
+ import { reducer as makeReducer } from './reducer.ts'
1
3
  /**
2
- * experimental-a2/ai — an isomorphic, extensible agent definition.
4
+ * experimental-a2/ai — the isomorphic AI protocol.
3
5
  *
4
- * `agent()` assembles a normal A2 contract, a reducer over the built-in
5
- * AI protocol, and pure builders for the events applications append.
6
- * Model calls and tool implementations stay in experimental-a2/ai/server.
6
+ * Event schemas, the `AIState` reducer, `agentContract()` for extending
7
+ * both, and pure builders for the events applications append. Model calls
8
+ * and tool implementations stay in experimental-a2/ai/server.
7
9
  */
8
10
 
9
11
  import type {
10
12
  FinishReason,
11
13
  JSONValue,
14
+ LanguageModelCallOptions,
12
15
  LanguageModelUsage,
13
16
  UIMessage,
14
17
  UIMessageChunk,
@@ -38,31 +41,55 @@ export type {
38
41
  InboxItem,
39
42
  } from './ai-control.ts'
40
43
  import { contract as createContract } from './contract.ts'
41
- import type { Contract, EventDefs } from './contract.ts'
44
+ import type { Contract, ContractEvent, EventDefs } from './contract.ts'
42
45
  import {
43
46
  interruptUIMessage,
44
47
  pauseUIMessage,
45
48
  projectUIMessage,
49
+ projectUIMessageStream,
50
+ hasToolCall,
51
+ type ProjectionChunk,
46
52
  type UIMessageCursor,
47
53
  } from './ai-projector.ts'
48
54
  import { ambientToolScopeStorage } from './internal.ts'
49
55
  import {
50
56
  upsertMessage,
57
+ retainMessageBase,
58
+ findMessage,
51
59
  upsertResponse,
52
60
  answerApproval,
53
61
  approvalRequestedChunk,
54
62
  toolResultChunk,
63
+ toolCalledChunk,
55
64
  advanceProjection,
65
+ projectedTools,
66
+ restoreGenerationBase,
67
+ stopGenerationTools,
68
+ interruptGenerationProjection,
56
69
  projectedMessage,
70
+ projectedMessageStream,
57
71
  reduceToolActivity,
72
+ addCompactionPart,
73
+ isCompactionPart,
74
+ settleAbandonedCompaction,
75
+ settleCompactionPart,
58
76
  } from './ai-message-projection.ts'
59
- import { clientStateProjection, type Reducer } from './reducer.ts'
60
- import { storedAIReducer, type AIStoredReducer } from './ai-stored-state.ts'
61
- export type { AIStoredState, AIStoredReducer } from './ai-stored-state.ts'
77
+ import type { Reducer } from './reducer.ts'
78
+ import { withReducerRuntime } from './reducer-runtime.ts'
79
+ import { withAIStorage } from './ai-stored-state.ts'
80
+ import {
81
+ appSlice,
82
+ isAIStateKey,
83
+ registerAgentReducer,
84
+ withAppSlice,
85
+ type AppReducer,
86
+ } from './ai-app-state.ts'
62
87
  import {
63
88
  flattenProgressBatches,
64
89
  progressBatches,
90
+ progressOwnedBy,
65
91
  type ProgressBatches,
92
+ type StoredProgressPayload,
66
93
  } from './ai-progress-batches.ts'
67
94
  import type { HandlerContext } from './server.ts'
68
95
  import type { StandardSchemaV1 } from './standard-schema.ts'
@@ -73,6 +100,43 @@ export type AgentStatus =
73
100
 
74
101
  export type GenerationReason = 'message' | 'tool' | 'input' | 'retry' | 'resume'
75
102
 
103
+ export type ReasoningLevel = NonNullable<LanguageModelCallOptions['reasoning']>
104
+
105
+ export const reasoningLevels: readonly ReasoningLevel[] = [
106
+ 'provider-default',
107
+ 'none',
108
+ 'minimal',
109
+ 'low',
110
+ 'medium',
111
+ 'high',
112
+ 'xhigh',
113
+ ]
114
+
115
+ export type CompactionPartData = {
116
+ status: 'running' | 'completed' | 'failed' | 'interrupted'
117
+ inputTokens?: number
118
+ thresholdTokens?: number
119
+ summary?: string
120
+ usage?: LanguageModelUsage
121
+ }
122
+
123
+ /** The transcript part A2 adds to a response where its context was compacted. */
124
+ export type CompactionPart = {
125
+ type: 'data-a2-compaction'
126
+ id: string
127
+ data: CompactionPartData
128
+ }
129
+
130
+ export { isCompactionPart }
131
+
132
+ /**
133
+ * A transcript message: `M` plus the compaction part A2 adds, so
134
+ * `isCompactionPart` narrows even when `M` declares its own data parts.
135
+ */
136
+ export type TranscriptMessage<M extends UIMessage> = Omit<M, 'parts'> & {
137
+ parts: Array<M['parts'][number] | CompactionPart>
138
+ }
139
+
76
140
  export type SessionCreatedPayload = { metadata?: JSONValue }
77
141
  export type SessionClosedPayload = { reason?: string }
78
142
  export type MessageCreatedPayload<M extends UIMessage> = {
@@ -111,15 +175,13 @@ export type GenerationStartedPayload = {
111
175
  generationId: string
112
176
  responseMessageId: string
113
177
  attempt: number
114
- model: string
178
+ /** Absent when the attempt failed before its model resolved. */
179
+ model?: string
115
180
  promptThroughIndex?: number
116
181
  }
117
182
  export type GenerationProgressPayload = {
118
- requestId: string
119
- messageId: string
120
- generationId: string
121
- responseMessageId: string
122
- sequence: number
183
+ /** Log index of the generation's `ai.generation.started`. */
184
+ startIndex: number
123
185
  chunks: UIMessageChunk[]
124
186
  }
125
187
  export type GenerationCompletedPayload = {
@@ -271,17 +333,20 @@ export type AIEventDefs<M extends UIMessage = UIMessage> = {
271
333
  'ai.model.metadata.resolved': StandardSchemaV1<ModelMetadataResolvedPayload>
272
334
  }
273
335
 
274
- export type ActiveGeneration = GenerationStartedPayload
336
+ export type ActiveGeneration = GenerationStartedPayload & {
337
+ /** Log index of this generation's `ai.generation.started`. */
338
+ startIndex: number
339
+ }
275
340
 
276
- export type ActiveGenerationProjection<M extends UIMessage = UIMessage> = {
341
+ export type ActiveGenerationProjection = {
277
342
  generationId: string
278
343
  responseMessageId: string
279
344
  /** Log index of the completed model step while its response remains interruptible. */
280
345
  completionIndex?: number
281
- baseMessage?: M
346
+ base?: MessageBase
282
347
  promptThroughIndex?: number
283
348
  cursor?: { throughIndex: number; stream: UIMessageCursor } | null
284
- baseCompaction?: CompactionState<M> | null
349
+ baseCompaction?: CompactionState | null
285
350
  batches: ProgressBatches
286
351
  baseTools: ToolActivity[]
287
352
  toolEvents: Array<
@@ -311,28 +376,20 @@ export type ToolActivity = {
311
376
  generationId: string
312
377
  toolCallId: string
313
378
  toolName?: string
314
- input?: unknown
315
- rawInput?: unknown
316
- output?: unknown
317
379
  error?: string
318
380
  preliminary?: boolean
319
381
  phase?: 'input' | 'execution'
320
382
  dynamic?: boolean
321
383
  providerExecuted?: boolean
322
- callProviderMetadata?: unknown
323
- resultProviderMetadata?: unknown
324
- toolMetadata?: unknown
325
384
  title?: string
326
385
  status: 'running' | 'completed' | 'failed' | 'denied'
327
386
  }
328
387
 
329
- export type CompactionState<M extends UIMessage> = {
388
+ export type CompactionState = {
330
389
  status: 'running' | 'completed'
331
390
  generationId: string
332
391
  throughMessageId: string
333
392
  throughIndex?: number
334
- messages?: M[]
335
- retainedMessageIds?: string[]
336
393
  summary?: string
337
394
  usage?: LanguageModelUsage
338
395
  }
@@ -350,14 +407,14 @@ export type AIState<M extends UIMessage = UIMessage> = {
350
407
  receipt: ControlReceipt | null
351
408
  pausedGenerationIds?: Record<string, true>
352
409
  session: { created: boolean; metadata?: JSONValue }
353
- messages: M[]
410
+ messages: TranscriptMessage<M>[]
354
411
  status: AgentStatus
355
412
  activeGeneration: ActiveGeneration | null
356
413
  activeRequestId: string | null
357
414
  activeRequestReason: GenerationReason | null
358
415
  activeRequestSourceGenerationId: string | null
359
416
  activeResponseMessageId: string | null
360
- activeProjection: ActiveGenerationProjection<M> | null
417
+ activeProjection: ActiveGenerationProjection | null
361
418
  responseOwners: Record<
362
419
  string,
363
420
  Pick<GenerationStartedPayload, 'generationId' | 'requestId' | 'attempt'>
@@ -370,19 +427,29 @@ export type AIState<M extends UIMessage = UIMessage> = {
370
427
  pendingApprovals: PendingApproval[]
371
428
  pendingInputs: PendingInput[]
372
429
  tools: ToolActivity[]
373
- compaction: CompactionState<M> | null
430
+ compaction: CompactionState | null
374
431
  modelMetadata: Record<string, ModelMetadataState>
375
432
  usage: GenerationUsage[]
376
433
  error: string | null
377
434
  }
378
435
 
379
- export type AgentDefinition<
436
+ /** The `UIMessage` type a contract's `ai.message.created` schema carries. */
437
+ export type AIMessageOf<D> = D extends {
438
+ readonly 'ai.message.created': infer S extends StandardSchemaV1
439
+ }
440
+ ? StandardSchemaV1.InferOutput<S> extends MessageCreatedPayload<
441
+ infer M extends UIMessage
442
+ >
443
+ ? M
444
+ : UIMessage
445
+ : UIMessage
446
+
447
+ /** What `agentContract()` returns: the AI protocol, with `X` as application state. */
448
+ export type AgentContract<
380
449
  M extends UIMessage = UIMessage,
381
450
  D extends EventDefs = AIEventDefs<M>,
382
- > = {
383
- readonly contract: Contract<D>
384
- readonly reducer: AIStoredReducer<D, AIState<M>>
385
- }
451
+ X extends object = Record<never, never>,
452
+ > = Contract<D, Record<never, never>, AgentReducer<M, D, X>>
386
453
 
387
454
  export type AgentToolEventDefs = EventDefs &
388
455
  Pick<AIEventDefs, 'ai.tool.execution.requested'>
@@ -393,24 +460,50 @@ export type AgentToolContext<D extends AgentToolEventDefs> = HandlerContext<
393
460
  'ai.tool.execution.requested'
394
461
  >
395
462
 
396
- export type AgentOptions<M extends UIMessage, E extends EventDefs> = {
463
+ /** Application state folded after the AI protocol, on the server and in clients. */
464
+ /** Rejects application state keys that would shadow `AIState`'s. */
465
+ type OwnKeys<X> = { [K in Extract<keyof X, keyof AIState>]: never }
466
+ /** Rejects a stateless application reducer, whose name would not be built in. */
467
+ type NonEmpty<X> = keyof X extends never ? never : unknown
468
+
469
+ /**
470
+ * The reducer `agentContract()` builds. Without application state it carries
471
+ * the built-in name, which is what lets the contract-free client fold it.
472
+ * `~appState` is type-only, absent at runtime: it carries the application
473
+ * state's type to `agent()`, and marks the reducer as `agentContract()`'s.
474
+ */
475
+ export type AgentReducer<
476
+ M extends UIMessage,
477
+ D extends EventDefs,
478
+ X extends object,
479
+ > = (keyof X extends never
480
+ ? Reducer<D, AIState<M>> & { readonly name: typeof AI_STATE_REDUCER }
481
+ : Reducer<D, AIState<M> & X>) & { readonly '~appState': X }
482
+
483
+ export type AgentContractOptions<
484
+ M extends UIMessage,
485
+ E extends EventDefs,
486
+ X extends object,
487
+ > = {
397
488
  name: string
398
489
  events?: E
399
490
  /** Optional stricter schema for custom UIMessage metadata/data/tool types. */
400
491
  messageSchema?: StandardSchemaV1<M>
401
- /** Snapshot identity. Bump when application-level projection semantics change. */
402
- reducerName?: string
492
+ /**
493
+ * Application state, folded from events beside `AIState` and merged into
494
+ * it. `fold` sees only this state, never the transcript.
495
+ */
496
+ reducer?: {
497
+ /** Snapshot identity of the application state. Rename it when `fold` changes. */
498
+ name: string
499
+ initialState: X & OwnKeys<X> & NonEmpty<X>
500
+ fold: (
501
+ state: NoInfer<X>,
502
+ event: ContractEvent<AIEventDefs<M> & NoInfer<E>>,
503
+ ) => NoInfer<X>
504
+ }
403
505
  }
404
506
 
405
- type AIMessageFrom<D extends EventDefs> =
406
- D extends Record<'ai.message.created', infer S extends StandardSchemaV1>
407
- ? StandardSchemaV1.InferOutput<S> extends MessageCreatedPayload<
408
- infer M extends UIMessage
409
- >
410
- ? M
411
- : UIMessage
412
- : UIMessage
413
-
414
507
  type SchemaResult<T> = StandardSchemaV1.Result<T>
415
508
 
416
509
  const issue = <T>(message: string): SchemaResult<T> => ({
@@ -536,13 +629,8 @@ const progressPayloadSchema: StandardSchemaV1<GenerationProgressPayload> =
536
629
  if (
537
630
  !isRecord(value) ||
538
631
  !isJSONCompatible(value) ||
539
- !stringField(value, 'requestId') ||
540
- !stringField(value, 'messageId') ||
541
- !stringField(value, 'generationId') ||
542
- !stringField(value, 'responseMessageId') ||
543
- typeof value['sequence'] !== 'number' ||
544
- !Number.isInteger(value['sequence']) ||
545
- value['sequence'] < 0 ||
632
+ typeof value['startIndex'] !== 'number' ||
633
+ !optionalIndexField(value, 'startIndex') ||
546
634
  !Array.isArray(value['chunks']) ||
547
635
  !value['chunks'].every(
548
636
  (chunk) => isRecord(chunk) && typeof chunk['type'] === 'string',
@@ -818,7 +906,7 @@ export function createEvents<M extends UIMessage = UIMessage>(options?: {
818
906
  typeof value['attempt'] === 'number' &&
819
907
  Number.isInteger(value['attempt']) &&
820
908
  value['attempt'] > 0 &&
821
- stringField(value, 'model') &&
909
+ (value['model'] === undefined || stringField(value, 'model')) &&
822
910
  optionalIndexField(value, 'promptThroughIndex'),
823
911
  ),
824
912
  'ai.generation.progress': progressPayloadSchema,
@@ -975,13 +1063,18 @@ const initialState = <M extends UIMessage>(): AIState<M> => ({
975
1063
  const projectMessageChunks = <M extends UIMessage>(
976
1064
  messages: M[],
977
1065
  messageId: string,
978
- chunks: UIMessageChunk[],
979
- ): M[] =>
980
- messages.map((message) =>
981
- message.id === messageId
982
- ? projectUIMessage({ messageId, base: message, chunks })
983
- : message,
1066
+ chunks: ProjectionChunk[],
1067
+ ): M[] => {
1068
+ const base = findMessage(messages, messageId)
1069
+ return upsertMessage(
1070
+ messages,
1071
+ projectUIMessage<M>({
1072
+ messageId,
1073
+ ...(base === undefined ? {} : { base }),
1074
+ chunks,
1075
+ }),
984
1076
  )
1077
+ }
985
1078
 
986
1079
  const waitingStatus = <M extends UIMessage>(state: AIState<M>): AgentStatus => {
987
1080
  if (state.pendingApprovals.length > 0 || state.pendingInputs.length > 0) {
@@ -1014,9 +1107,15 @@ const isStaleGeneration = <M extends UIMessage>(
1014
1107
 
1015
1108
  const projectedResponse = <M extends UIMessage>(options: {
1016
1109
  state: AIState<M>
1017
- projection: ActiveGenerationProjection<M>
1018
- }): M => {
1019
- const message = projectedMessage(options.projection)
1110
+ projection: ActiveGenerationProjection
1111
+ }): TranscriptMessage<M> => {
1112
+ const message = projectedMessage({
1113
+ projection: options.projection,
1114
+ current: findMessage(
1115
+ options.state.messages,
1116
+ options.projection.responseMessageId,
1117
+ ),
1118
+ })
1020
1119
  return options.state.pausedGenerationIds?.[options.projection.generationId]
1021
1120
  ? pauseUIMessage({
1022
1121
  message,
@@ -1027,127 +1126,6 @@ const projectedResponse = <M extends UIMessage>(options: {
1027
1126
  : message
1028
1127
  }
1029
1128
 
1030
- const projectedTools = <M extends UIMessage>(
1031
- projection: ActiveGenerationProjection<M>,
1032
- ): ToolActivity[] =>
1033
- projection.toolEvents.reduce(
1034
- (tools, event) => reduceToolActivity(tools, event),
1035
- projection.baseTools,
1036
- )
1037
-
1038
- const restoreGenerationBase = <M extends UIMessage>(
1039
- state: AIState<M>,
1040
- generationId: string,
1041
- responseMessageId: string,
1042
- reason: 'interrupted' | 'superseded' | 'failed',
1043
- ): AIState<M> => {
1044
- const projection =
1045
- state.activeProjection?.generationId === generationId
1046
- ? state.activeProjection
1047
- : null
1048
- return {
1049
- ...state,
1050
- messages:
1051
- projection?.baseMessage === undefined
1052
- ? state.messages.filter((message) => message.id !== responseMessageId)
1053
- : upsertMessage(state.messages, projection.baseMessage),
1054
- tools:
1055
- projection === null
1056
- ? state.tools.filter(
1057
- (activity) => activity.generationId !== generationId,
1058
- )
1059
- : projection.baseTools,
1060
- pendingApprovals: state.pendingApprovals.filter(
1061
- (approval) => approval.generationId !== generationId,
1062
- ),
1063
- pendingInputs: state.pendingInputs.filter(
1064
- (input) => input.generationId !== generationId,
1065
- ),
1066
- activeGeneration: null,
1067
- activeProjection: null,
1068
- terminalGenerations: {
1069
- ...state.terminalGenerations,
1070
- [generationId]: reason,
1071
- },
1072
- compaction:
1073
- projection === null
1074
- ? state.compaction?.generationId === generationId
1075
- ? null
1076
- : state.compaction
1077
- : (projection.baseCompaction ?? null),
1078
- }
1079
- }
1080
-
1081
- const stopGenerationTools = (
1082
- tools: ToolActivity[],
1083
- generationId: string | undefined,
1084
- responseMessageId: string,
1085
- error: string,
1086
- ): ToolActivity[] =>
1087
- tools.map((activity) =>
1088
- activity.status === 'running' &&
1089
- (generationId === undefined
1090
- ? activity.messageId === responseMessageId
1091
- : activity.generationId === generationId)
1092
- ? { ...activity, status: 'failed', error, preliminary: false }
1093
- : activity,
1094
- )
1095
-
1096
- const interruptGenerationProjection = <M extends UIMessage>(
1097
- state: AIState<M>,
1098
- generationId: string,
1099
- responseMessageId: string,
1100
- cutoff: number,
1101
- ): AIState<M> | null => {
1102
- const projection =
1103
- state.activeProjection?.generationId === generationId
1104
- ? state.activeProjection
1105
- : null
1106
- if (projection === null) return null
1107
- const retained: ActiveGenerationProjection<M> = {
1108
- ...projection,
1109
- batches: progressBatches(
1110
- flattenProgressBatches(projection.batches).filter(
1111
- (batch) => batch.index <= cutoff,
1112
- ),
1113
- ),
1114
- toolEvents: projection.toolEvents.filter(
1115
- (toolEvent) => toolEvent.index <= cutoff,
1116
- ),
1117
- approvalEvents: projection.approvalEvents.filter(
1118
- (approvalEvent) => approvalEvent.index <= cutoff,
1119
- ),
1120
- }
1121
- const shouldProject =
1122
- retained.baseMessage !== undefined || retained.batches.length > 0
1123
- const restored = restoreGenerationBase(
1124
- state,
1125
- generationId,
1126
- responseMessageId,
1127
- 'interrupted',
1128
- )
1129
- return {
1130
- ...restored,
1131
- messages: shouldProject
1132
- ? upsertMessage(
1133
- state.messages,
1134
- interruptUIMessage(projectedMessage(retained)),
1135
- )
1136
- : restored.messages,
1137
- tools: stopGenerationTools(
1138
- projectedTools(retained),
1139
- generationId,
1140
- responseMessageId,
1141
- 'Tool execution was interrupted.',
1142
- ),
1143
- usage:
1144
- projection.completionIndex === undefined ||
1145
- projection.completionIndex <= cutoff
1146
- ? restored.usage
1147
- : restored.usage.filter((entry) => entry.generationId !== generationId),
1148
- }
1149
- }
1150
-
1151
1129
  const terminalGenerationReason = <M extends UIMessage>(
1152
1130
  state: AIState<M>,
1153
1131
  generationId: string,
@@ -1165,6 +1143,19 @@ const terminalGenerationReason = <M extends UIMessage>(
1165
1143
  export function reduceAIState<M extends UIMessage>(
1166
1144
  state: AIState<M>,
1167
1145
  event: { type: string; payload: unknown; index: number; id?: string },
1146
+ ): AIState<M> {
1147
+ return settleAbandonedCompaction(
1148
+ state,
1149
+ retainMessageBase({
1150
+ before: state,
1151
+ after: reduceAIEvent(state, event),
1152
+ }),
1153
+ )
1154
+ }
1155
+
1156
+ function reduceAIEvent<M extends UIMessage>(
1157
+ state: AIState<M>,
1158
+ event: { type: string; payload: unknown; index: number; id?: string },
1168
1159
  ): AIState<M> {
1169
1160
  if (state.status === 'closed') return state
1170
1161
  switch (event.type) {
@@ -1488,13 +1479,14 @@ export function reduceAIState<M extends UIMessage>(
1488
1479
  'superseded',
1489
1480
  )
1490
1481
  : state
1491
- const baseMessage = baseState.messages.find(
1492
- (message) => message.id === payload.responseMessageId,
1482
+ const baseMessage = findMessage(
1483
+ baseState.messages,
1484
+ payload.responseMessageId,
1493
1485
  )
1494
1486
  return {
1495
1487
  ...baseState,
1496
1488
  status: 'generating',
1497
- activeGeneration: payload,
1489
+ activeGeneration: { ...payload, startIndex: event.index },
1498
1490
  activeResponseMessageId: null,
1499
1491
  responseOwners: {
1500
1492
  ...baseState.responseOwners,
@@ -1508,7 +1500,9 @@ export function reduceAIState<M extends UIMessage>(
1508
1500
  generationId: payload.generationId,
1509
1501
  responseMessageId: payload.responseMessageId,
1510
1502
  promptThroughIndex: payload.promptThroughIndex ?? event.index - 1,
1511
- ...(baseMessage === undefined ? {} : { baseMessage }),
1503
+ ...(baseMessage === undefined
1504
+ ? {}
1505
+ : { base: { partCount: baseMessage.parts.length } }),
1512
1506
  baseCompaction: baseState.compaction,
1513
1507
  batches: progressBatches(),
1514
1508
  baseTools: baseState.tools,
@@ -1519,30 +1513,29 @@ export function reduceAIState<M extends UIMessage>(
1519
1513
  }
1520
1514
  }
1521
1515
  case 'ai.generation.progress': {
1522
- const payload = event.payload as GenerationProgressPayload
1523
- if (state.pausedGenerationIds?.[payload.generationId]) return state
1516
+ const payload = event.payload as StoredProgressPayload
1517
+ const generation = state.activeGeneration
1518
+ const activeProjection = state.activeProjection
1524
1519
  if (
1520
+ generation === null ||
1521
+ !progressOwnedBy({
1522
+ payload,
1523
+ owner: generation,
1524
+ startIndex: generation.startIndex,
1525
+ }) ||
1526
+ state.pausedGenerationIds?.[generation.generationId] ||
1525
1527
  terminalGenerationReason(
1526
1528
  state,
1527
- payload.generationId,
1528
- payload.requestId,
1529
- ) !== undefined
1530
- ) {
1531
- return state
1532
- }
1533
- const activeProjection = state.activeProjection
1534
- if (
1535
- state.activeGeneration?.generationId !== payload.generationId ||
1536
- state.activeGeneration.requestId !== payload.requestId ||
1537
- activeProjection?.generationId !== payload.generationId
1529
+ generation.generationId,
1530
+ generation.requestId,
1531
+ ) !== undefined ||
1532
+ activeProjection?.generationId !== generation.generationId
1538
1533
  ) {
1539
1534
  return state
1540
1535
  }
1541
1536
  const progress = advanceProjection({
1542
1537
  projection: activeProjection,
1543
- base: state.messages.find(
1544
- (message) => message.id === activeProjection.responseMessageId,
1545
- ),
1538
+ base: findMessage(state.messages, activeProjection.responseMessageId),
1546
1539
  chunks: payload.chunks,
1547
1540
  index: event.index,
1548
1541
  })
@@ -1551,7 +1544,7 @@ export function reduceAIState<M extends UIMessage>(
1551
1544
  messages: upsertResponse({
1552
1545
  messages: state.messages,
1553
1546
  message: progress.message,
1554
- inputId: state.activeGeneration.messageId,
1547
+ inputId: generation.messageId,
1555
1548
  }),
1556
1549
  activeProjection: {
1557
1550
  ...activeProjection,
@@ -1638,14 +1631,20 @@ export function reduceAIState<M extends UIMessage>(
1638
1631
  : null
1639
1632
  const shouldProject =
1640
1633
  projection !== null &&
1641
- (projection.baseMessage !== undefined || projection.batches.length > 0)
1634
+ (projection.base !== undefined || projection.batches.length > 0)
1642
1635
  return {
1643
1636
  ...state,
1644
1637
  messages: shouldProject
1645
1638
  ? upsertMessage(
1646
1639
  state.messages,
1647
1640
  interruptUIMessage(
1648
- projectedMessage(projection),
1641
+ projectedMessage({
1642
+ projection,
1643
+ current: findMessage(
1644
+ state.messages,
1645
+ projection.responseMessageId,
1646
+ ),
1647
+ }),
1649
1648
  `Generation failed before tool completion: ${payload.error}`,
1650
1649
  ),
1651
1650
  )
@@ -1949,6 +1948,9 @@ export function reduceAIState<M extends UIMessage>(
1949
1948
  if (projection?.generationId !== payload.generationId) {
1950
1949
  const next = {
1951
1950
  ...state,
1951
+ messages: projectMessageChunks(state.messages, payload.messageId, [
1952
+ toolCalledChunk(payload),
1953
+ ]),
1952
1954
  tools: reduceToolActivity(state.tools, {
1953
1955
  type: 'ai.tool.called',
1954
1956
  payload,
@@ -1956,16 +1958,51 @@ export function reduceAIState<M extends UIMessage>(
1956
1958
  }
1957
1959
  return { ...next, status: lifecycleStatus(state, next) }
1958
1960
  }
1959
- const activeProjection: ActiveGenerationProjection<M> = {
1961
+ const activeProjection: ActiveGenerationProjection = {
1960
1962
  ...projection,
1963
+ cursor: null,
1961
1964
  toolEvents: [
1962
1965
  ...projection.toolEvents,
1963
1966
  { index: event.index, type: 'ai.tool.called', payload },
1964
1967
  ],
1965
1968
  }
1969
+ const base = findMessage(state.messages, payload.messageId)
1970
+ const paused = state.pausedGenerationIds?.[payload.generationId]
1971
+ const cursor = projection.cursor
1972
+ const incremental =
1973
+ base && cursor && !paused && event.index > cursor.throughIndex
1974
+ const projected = incremental
1975
+ ? {
1976
+ ...(hasToolCall({ message: base, call: payload })
1977
+ ? { message: base, cursor: cursor.stream }
1978
+ : projectUIMessageStream({
1979
+ messageId: payload.messageId,
1980
+ base,
1981
+ cursor: cursor.stream,
1982
+ chunks: [toolCalledChunk(payload)],
1983
+ })),
1984
+ throughIndex: event.index,
1985
+ }
1986
+ : projectedMessageStream({
1987
+ projection: activeProjection,
1988
+ current: base,
1989
+ })
1990
+ activeProjection.cursor = paused
1991
+ ? null
1992
+ : { throughIndex: projected.throughIndex, stream: projected.cursor }
1966
1993
  const next = {
1967
1994
  ...state,
1968
1995
  activeProjection,
1996
+ messages: upsertResponse({
1997
+ messages: state.messages,
1998
+ message: paused
1999
+ ? projectedResponse({ state, projection: activeProjection })
2000
+ : projected.message,
2001
+ inputId:
2002
+ state.activeGeneration?.messageId ??
2003
+ state.active?.input.id ??
2004
+ payload.messageId,
2005
+ }),
1969
2006
  tools: projectedTools(activeProjection),
1970
2007
  }
1971
2008
  return { ...next, status: lifecycleStatus(state, next) }
@@ -2020,7 +2057,7 @@ export function reduceAIState<M extends UIMessage>(
2020
2057
  }
2021
2058
  return { ...next, status: lifecycleStatus(state, next) }
2022
2059
  }
2023
- const activeProjection: ActiveGenerationProjection<M> = {
2060
+ const activeProjection: ActiveGenerationProjection = {
2024
2061
  ...projection,
2025
2062
  cursor: null,
2026
2063
  toolEvents: [
@@ -2253,10 +2290,10 @@ export function reduceAIState<M extends UIMessage>(
2253
2290
  if (state.activeGeneration?.generationId !== payload.generationId) {
2254
2291
  return state
2255
2292
  }
2256
- return {
2257
- ...state,
2258
- compaction: { status: 'running', ...payload },
2259
- }
2293
+ return addCompactionPart(
2294
+ { ...state, compaction: { status: 'running', ...payload } },
2295
+ payload,
2296
+ )
2260
2297
  }
2261
2298
  case 'ai.compaction.completed': {
2262
2299
  const payload = event.payload as CompactionCompletedPayload<M>
@@ -2267,20 +2304,41 @@ export function reduceAIState<M extends UIMessage>(
2267
2304
  ) {
2268
2305
  return state
2269
2306
  }
2270
- const compaction: CompactionState<M> = {
2307
+ const {
2308
+ messages: _messages,
2309
+ retainedMessageIds: _retained,
2310
+ ...summary
2311
+ } = payload
2312
+ const compaction: CompactionState = {
2271
2313
  status: 'completed',
2272
- ...payload,
2314
+ ...summary,
2273
2315
  throughIndex:
2274
2316
  payload.throughIndex ?? state.activeProjection!.promptThroughIndex!,
2275
2317
  }
2276
- return {
2277
- ...state,
2278
- compaction,
2279
- activeProjection: state.activeProjection && {
2280
- ...state.activeProjection,
2281
- baseCompaction: compaction,
2318
+ const projection = state.activeProjection
2319
+ return settleCompactionPart({
2320
+ state: {
2321
+ ...state,
2322
+ compaction,
2323
+ activeProjection: projection && {
2324
+ ...projection,
2325
+ baseCompaction: compaction,
2326
+ // A new base identity records the completed part as the step's
2327
+ // starting point, so a later cutoff cannot revert it to running.
2328
+ ...(projection.base === undefined
2329
+ ? {}
2330
+ : { base: { ...projection.base } }),
2331
+ },
2282
2332
  },
2283
- }
2333
+ generationId: payload.generationId,
2334
+ data: {
2335
+ status: 'completed',
2336
+ ...(summary.summary === undefined
2337
+ ? {}
2338
+ : { summary: summary.summary }),
2339
+ ...(summary.usage === undefined ? {} : { usage: summary.usage }),
2340
+ },
2341
+ })
2284
2342
  }
2285
2343
  default:
2286
2344
  return state
@@ -2290,35 +2348,12 @@ export function reduceAIState<M extends UIMessage>(
2290
2348
  /** Derive AI SDK UI messages directly from an A2 event history. */
2291
2349
  export function deriveUIMessages<M extends UIMessage = UIMessage>(
2292
2350
  history: readonly { type: string; payload: unknown; index: number }[],
2293
- ): M[] {
2351
+ ): TranscriptMessage<M>[] {
2294
2352
  let state = initialState<M>()
2295
2353
  for (const event of history) state = reduceAIState(state, event)
2296
2354
  return state.messages
2297
2355
  }
2298
2356
 
2299
- /** Build the standard AI state projection for any contract containing the protocol. */
2300
- export function createReducer<D extends EventDefs>(options: {
2301
- contract: Contract<D>
2302
- name?: string
2303
- fold?: Reducer<D, AIState<AIMessageFrom<D>>>['fold']
2304
- stateSchema?: StandardSchemaV1<unknown, AIState<AIMessageFrom<D>>>
2305
- }): AIStoredReducer<D, AIState<AIMessageFrom<D>>> {
2306
- type M = AIMessageFrom<D>
2307
- const reducer = options.contract
2308
- .reducer({
2309
- name: options.name ?? 'a2.ai.state.v21',
2310
- initialState: initialState<M>(),
2311
- ...(options.stateSchema === undefined
2312
- ? {}
2313
- : { stateSchema: options.stateSchema }),
2314
- })
2315
- .fold(options.fold ?? ((state, event) => reduceAIState(state, event)))
2316
- return storedAIReducer({
2317
- ...reducer,
2318
- [clientStateProjection]: projectAIClientState,
2319
- })
2320
- }
2321
-
2322
2357
  type CommandInput<C> = { type: 'ai.control.requested'; payload: C; id?: string }
2323
2358
  type ControlInput<
2324
2359
  M extends UIMessage,
@@ -2455,22 +2490,24 @@ export const inputs: AIInputs = {
2455
2490
  * Read the durable handler context ambient to the current AI tool
2456
2491
  * execution. A2 runs each local tool's `execute` inside this scope, so
2457
2492
  * plain AI SDK `tool()` definitions reach their `event`, `attempt`,
2458
- * `session`, and `signal` without an A2-specific wrapper. The agent (or
2459
- * contract) argument carries the event types and is verified against
2460
- * the executing server's contract.
2493
+ * `session`, and `signal` without an A2-specific wrapper. Pass the
2494
+ * agent's contract to type application events; it is verified against
2495
+ * the executing agent.
2461
2496
  */
2462
- export function handlerContext<D extends AgentToolEventDefs>(
2463
- agentOrContract: Contract<D> | { readonly contract: Contract<D> },
2497
+ export function handlerContext<D extends AgentToolEventDefs = AIEventDefs>(
2498
+ contract?: Contract<D>,
2464
2499
  ): AgentToolContext<D> {
2465
- const contract =
2466
- 'contract' in agentOrContract ? agentOrContract.contract : agentOrContract
2467
2500
  const scope = ambientToolScopeStorage()?.getStore()
2468
2501
  if (scope === undefined) {
2469
2502
  throw new TypeError(
2470
- `handlerContext('${contract.name}') requires an ambient A2 tool execution: call it inside a local tool's execute, or wrap direct tool tests with runWithHandlerContext`,
2503
+ "handlerContext() requires an ambient A2 tool execution: call it inside a local tool's execute, or wrap direct tool tests with runWithHandlerContext",
2471
2504
  )
2472
2505
  }
2473
- if (scope.contract !== undefined && scope.contract.name !== contract.name) {
2506
+ if (
2507
+ contract !== undefined &&
2508
+ scope.contract !== undefined &&
2509
+ scope.contract.name !== contract.name
2510
+ ) {
2474
2511
  throw new TypeError(
2475
2512
  `handlerContext('${contract.name}') was called during a tool execution for agent '${scope.contract.name}'`,
2476
2513
  )
@@ -2478,14 +2515,23 @@ export function handlerContext<D extends AgentToolEventDefs>(
2478
2515
  return scope.context as AgentToolContext<D>
2479
2516
  }
2480
2517
 
2518
+ /** The built-in AI reducer's name, and so its checkpoint identity. */
2519
+ export const AI_STATE_REDUCER = 'a2.ai.state.v26'
2520
+
2481
2521
  /**
2482
- * Define an agent: built-in AI protocol + application events and one state
2483
- * reducer. The result is isomorphic.
2522
+ * The built-in AI protocol plus application events, as a core contract whose
2523
+ * reducer folds `AIState`. Isomorphic: share it when a client needs
2524
+ * application events or extended state; otherwise `agent({ name })` on the
2525
+ * server builds one and clients use the built-in protocol.
2484
2526
  */
2485
- export function agent<
2527
+ export function agentContract<
2486
2528
  M extends UIMessage = UIMessage,
2487
2529
  E extends EventDefs = Record<never, never>,
2488
- >(options: AgentOptions<M, E>): AgentDefinition<M, AIEventDefs<M> & E> {
2530
+ X extends object = Record<never, never>,
2531
+ >(
2532
+ options: AgentContractOptions<M, E, X>,
2533
+ ): AgentContract<M, AIEventDefs<M> & E, X> {
2534
+ type D = AIEventDefs<M> & E
2489
2535
  const builtIns = createEvents(
2490
2536
  options.messageSchema === undefined
2491
2537
  ? undefined
@@ -2499,25 +2545,46 @@ export function agent<
2499
2545
  )
2500
2546
  }
2501
2547
  }
2502
- const agentContract = createContract({
2548
+ const vocabulary = createContract({
2503
2549
  name: options.name,
2504
- events: { ...builtIns, ...extensions },
2550
+ events: { ...builtIns, ...extensions } as D,
2505
2551
  })
2506
- const reducer = agentContract
2507
- .reducer({
2508
- name: options.reducerName ?? 'a2.ai.state.v21',
2509
- initialState: initialState<M>(),
2510
- })
2511
- .fold((state, event) => reduceAIState(state, event))
2512
-
2513
- const definition = {
2514
- contract: agentContract,
2515
- reducer: storedAIReducer({
2516
- ...reducer,
2517
- [clientStateProjection]: projectAIClientState<M>,
2518
- }),
2552
+ const app = options.reducer as AppReducer<D, X> | undefined
2553
+ const aiState = initialState<M>()
2554
+ for (const key of Object.keys(app?.initialState ?? {})) {
2555
+ if (isAIStateKey(key))
2556
+ throw new TypeError(`agent state '${key}' conflicts with AIState`)
2519
2557
  }
2520
- return definition as unknown as AgentDefinition<M, AIEventDefs<M> & E>
2558
+ const reducer = makeReducer({
2559
+ contract: vocabulary,
2560
+ // Application checkpoints embed AI state, so they advance with it.
2561
+ name:
2562
+ app === undefined ? AI_STATE_REDUCER : `${app.name}+${AI_STATE_REDUCER}`,
2563
+ initialState: { ...aiState, ...app?.initialState } as AIState<M> & X,
2564
+ }).fold((state, event) => {
2565
+ const next = reduceAIState(state, event) as AIState<M> & X
2566
+ if (app === undefined) return next
2567
+ const before = appSlice<X>(state)
2568
+ const after = app.fold(before, event as ContractEvent<D>)
2569
+ return after === before ? next : withAppSlice(next, after)
2570
+ })
2571
+ const shared = createContract({
2572
+ name: options.name,
2573
+ events: vocabulary.events,
2574
+ reducer: withAIStorage(
2575
+ withReducerRuntime({
2576
+ reducer,
2577
+ project: projectAIClientState<M> as unknown as (
2578
+ state: AIState<M> & X,
2579
+ ) => AIState<M> & X,
2580
+ }),
2581
+ ),
2582
+ })
2583
+ registerAgentReducer(
2584
+ shared.reducer,
2585
+ app as unknown as AppReducer<EventDefs, object> | undefined,
2586
+ )
2587
+ return shared as unknown as AgentContract<M, AIEventDefs<M> & E, X>
2521
2588
  }
2522
2589
 
2523
2590
  export type {