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
@@ -1,1318 +0,0 @@
1
- ---
2
- title: Durable AI agents
3
- description: Build an AI SDK agent whose messages, streaming progress, tools, approvals, and recovery live in an A2 event log.
4
- ---
5
-
6
- ## Install
7
-
8
- Install A2, the AI SDK, and Zod for the examples below:
9
-
10
- ```bash
11
- pnpm add experimental-a2 ai zod
12
- ```
13
-
14
- The examples use the AI SDK's default global provider, AI Gateway. Add its key
15
- to your environment:
16
-
17
- ```bash .env.local
18
- AI_GATEWAY_API_KEY=your_api_key
19
- ```
20
-
21
- ## Define the agent
22
-
23
- `agent()` defines an isomorphic event contract, normalized `AIStoredState`,
24
- and its `AIState` view. The same reducer types server snapshots, hydration,
25
- and optimistic client updates.
26
-
27
- ```ts assistant.ts
28
- import { agent } from 'experimental-a2/ai'
29
-
30
- export const assistant = agent({
31
- name: 'support-agent',
32
- })
33
- ```
34
-
35
- Keep this module isomorphic. Model providers, secrets, tools, and storage belong
36
- in the server module.
37
-
38
- ## Connect the model
39
-
40
- `createAgentServer()` adds the AI SDK runner and A2's ordinary store,
41
- scheduler, telemetry, and handler machinery:
42
-
43
- ```ts server/assistant.ts
44
- import { createAgentServer } from 'experimental-a2/ai/server'
45
- import { assistant } from '../assistant'
46
-
47
- export const assistantServer = createAgentServer({
48
- agent: assistant,
49
- model: 'openai/gpt-5.6-terra',
50
- instructions: 'Help the customer. Be concise.',
51
- generation: {
52
- temperature: 0.2,
53
- maxOutputTokens: 2_000,
54
- },
55
- })
56
- ```
57
-
58
- A string model id uses the AI SDK's global provider, which is AI Gateway by
59
- default. `model` also accepts any AI SDK `LanguageModel`, such as a provider
60
- model returned by `openai('gpt-5.6-terra')`, or an async resolver that chooses a
61
- model for each generation.
62
-
63
- Each durable generation runs one AI SDK `streamText()` step. `generation`
64
- contains per-step settings such as sampling, token limits, provider options,
65
- and tool approval policy. A2 owns the stop condition, local tool execution,
66
- and continuation between steps. Responses have no step limit by default. Set
67
- `maxSteps` to a positive integer to limit one complete assistant response.
68
- Individual providers decide which model settings they support. `generation`
69
- therefore excludes `stopWhen`, tool execution callbacks, tool callers, sandbox
70
- execution, and the tool approval secret. A2 preserves all other supplied
71
- settings, including `toolsContext`, when calling the AI SDK. A local tool
72
- reads its durable execution context later, from the handler that runs it. Model-step
73
- timeouts are supported; tool-execution timeouts are not.
74
-
75
- For AI Gateway models, the default generator sets
76
- `generation.providerOptions.gateway.caching` to `'auto'`. A2 merges this
77
- default with your provider options, with caller-supplied values taking
78
- precedence. Other Gateway options and provider namespaces are preserved.
79
- The same default applies to automatic compaction. A custom `generate`
80
- function receives your original settings and owns its caching policy.
81
-
82
- Gateway calls also send an `x-session-affinity` header derived from a SHA-256
83
- hash of the agent name and session ID. The value stays stable across turns,
84
- retries, worker restarts, and automatic compaction. No raw session identifier
85
- is sent in this header. A caller-supplied `generation.headers` entry takes
86
- precedence, regardless of header casing; an explicit `undefined` suppresses
87
- A2's default header. Set overrides here, since A2's per-call default can
88
- override headers configured on the model itself.
89
- Affinity helps supporting providers reuse their cache; it does not select
90
- Gateway's provider or guarantee a cache hit.
91
- See [Gateway automatic caching](https://vercel.com/docs/ai-gateway/models-and-providers/automatic-caching)
92
- for provider support and cache-write costs.
93
-
94
- ### Store assistant metadata
95
-
96
- Extend the agent's `UIMessage` type to keep generation status and usage on the
97
- assistant message:
98
-
99
- ```ts metadata-assistant.ts
100
- import type { LanguageModelUsage, UIMessage } from 'ai'
101
- import { agent } from 'experimental-a2/ai'
102
-
103
- export type MetadataAssistantMessage = UIMessage<{
104
- status: 'writing' | 'complete'
105
- usage?: LanguageModelUsage
106
- }>
107
-
108
- export const metadataAssistant = agent<MetadataAssistantMessage>({
109
- name: 'metadata-assistant',
110
- })
111
- ```
112
-
113
- Map the AI SDK's standard stream parts to that metadata type:
114
-
115
- ```ts server/metadata-assistant.ts
116
- import { createAgentServer } from 'experimental-a2/ai/server'
117
- import { metadataAssistant } from '../metadata-assistant'
118
-
119
- export const metadataAssistantServer = createAgentServer({
120
- agent: metadataAssistant,
121
- model: 'openai/gpt-5.6-terra',
122
- messageMetadata: ({ part }) => {
123
- if (part.type === 'start') return { status: 'writing' as const }
124
- if (part.type === 'finish') {
125
- return { status: 'complete' as const, usage: part.totalUsage }
126
- }
127
- return undefined
128
- },
129
- })
130
- ```
131
-
132
- The default generator passes `messageMetadata` to the AI SDK's
133
- `toUIMessageStream()`. The callback receives the generation context and its
134
- current `TextStreamPart`. Its return type follows the metadata type in the
135
- agent's `UIMessage`. Metadata on standard `start` and `finish` chunks enters
136
- the event log with the rest of the stream. A2 shallow-merges those values onto
137
- the projected assistant message, so the example exposes `writing` immediately
138
- and keeps final token usage after completion. The callback is synchronous.
139
-
140
- `experimental-a2/ai/server` is server-only. The isomorphic
141
- `experimental-a2/ai` entry point contains the contract, reducer, schemas,
142
- `handlerContext()`, and pure inputs; it never imports a model provider or
143
- backend.
144
-
145
- ## Expose the event stream
146
-
147
- One HTTP route gives the browser a read and write path. `GET` streams events;
148
- `POST` accepts optimistic pushes:
149
-
150
- ```ts app/api/agent-events/route.ts
151
- import { assistantServer } from '@/server/assistant'
152
-
153
- export const GET = assistantServer.fetch
154
- export const POST = assistantServer.fetch
155
- ```
156
-
157
- The route never calls the model directly. The browser appends
158
- `ai.control.requested` commands. The controller admits inputs and schedules
159
- `ai.generation.requested`; workers run the AI SDK and append progress to the
160
- same log.
161
-
162
- ## Bind the reducer to React
163
-
164
- Create the typed provider and hook in a client module:
165
-
166
- ```tsx app/agent/session.ts
167
- 'use client'
168
- import { createClient } from 'experimental-a2/client'
169
- import { createReact } from 'experimental-a2/react'
170
- import { assistant } from '@/assistant'
171
-
172
- export const assistantClient = createClient({
173
- reducer: assistant.reducer,
174
- api: '/api/agent-events',
175
- })
176
-
177
- export const { SessionProvider, useSession } = createReact({
178
- client: assistantClient,
179
- })
180
- ```
181
-
182
- Fold the initial state on the server, then let the provider resume the event
183
- stream from that exact index:
184
-
185
- ```tsx app/agent/[sessionId]/page.tsx
186
- import { assistant } from '@/assistant'
187
- import { assistantServer } from '@/server/assistant'
188
- import { AgentClient } from './agent-client'
189
- import { SessionProvider } from '../session'
190
-
191
- export default async function AgentPage({
192
- params,
193
- }: {
194
- params: Promise<{ sessionId: string }>
195
- }) {
196
- const { sessionId } = await params
197
- const session = assistantServer.session(sessionId)
198
- const { state, index } = await session.state(assistant.reducer)
199
-
200
- return (
201
- <SessionProvider
202
- sessionId={sessionId}
203
- initialState={state}
204
- initialIndex={index}
205
- >
206
- <AgentClient />
207
- </SessionProvider>
208
- )
209
- }
210
- ```
211
-
212
- An empty session folds to the initial `AIState`. Its log is created lazily when
213
- the first message is pushed.
214
-
215
- Start the session from the previous route, but wait for its durable append
216
- before navigating. The destination server render then sees the message in its
217
- folded state instead of racing the `POST`:
218
-
219
- ```tsx app/agent/new-agent-session.tsx
220
- 'use client'
221
- import type { UIMessage } from 'ai'
222
- import { inputs } from 'experimental-a2/ai'
223
- import { assistantClient } from './session'
224
-
225
- export async function openAgent(
226
- sessionId: string,
227
- message: UIMessage,
228
- router: { push(href: string): void },
229
- ): Promise<void> {
230
- await assistantClient.session(sessionId).push(...inputs.message(message))
231
- router.push(`/agent/${sessionId}`)
232
- }
233
- ```
234
-
235
- The draft clears immediately while the append is in flight and can be restored
236
- if it fails. Once navigation begins, SSR already has folded state at a durable
237
- frontier. When the destination provider mounts, it resolves the same live
238
- session and reconciles that server render without replacing the object. Idle
239
- session objects expire after five minutes by default; configure `gcTime` on
240
- `createClient` when a different lifetime fits the app.
241
-
242
- ## Send messages from the client
243
-
244
- Build the message inline and pass the resulting events straight to `push()`:
245
-
246
- ```tsx app/agent/[sessionId]/agent-client.tsx
247
- 'use client'
248
- import type { FormEvent } from 'react'
249
- import { useState } from 'react'
250
- import { inputs } from 'experimental-a2/ai'
251
- import { useSession } from '../session'
252
-
253
- export function AgentClient() {
254
- const { state, push } = useSession()
255
- const [draft, setDraft] = useState('')
256
- const [error, setError] = useState<string | null>(null)
257
-
258
- const canSend = state.status !== 'closed'
259
- const lastMessage = state.messages.at(-1)
260
- const lastText =
261
- lastMessage?.parts
262
- .flatMap((part) => (part.type === 'text' ? [part.text] : []))
263
- .join('\n') ?? ''
264
- const showThinking =
265
- state.status === 'generating' &&
266
- (lastMessage?.role !== 'assistant' || lastText.length === 0)
267
-
268
- async function send(event: FormEvent<HTMLFormElement>): Promise<void> {
269
- event.preventDefault()
270
- const previousDraft = draft
271
- const text = previousDraft.trim()
272
- if (!text || !canSend) return
273
-
274
- setDraft('')
275
- setError(null)
276
-
277
- try {
278
- await push(
279
- ...inputs.message({
280
- id: crypto.randomUUID(),
281
- role: 'user',
282
- parts: [{ type: 'text', text }],
283
- }),
284
- )
285
- } catch (cause) {
286
- setDraft(previousDraft)
287
- setError(cause instanceof Error ? cause.message : String(cause))
288
- }
289
- }
290
-
291
- return (
292
- <main>
293
- <section aria-label="Conversation">
294
- {state.messages.map((message) => {
295
- const text = message.parts
296
- .flatMap((part) => (part.type === 'text' ? [part.text] : []))
297
- .join('\n')
298
- if (!text) return null
299
-
300
- return (
301
- <article key={message.id}>
302
- <strong>{message.role === 'user' ? 'You' : 'Agent'}</strong>
303
- <p>{text}</p>
304
- </article>
305
- )
306
- })}
307
-
308
- {showThinking ? (
309
- <article aria-live="polite">
310
- <strong>Agent</strong>
311
- <p>Thinking…</p>
312
- </article>
313
- ) : null}
314
- </section>
315
-
316
- <form onSubmit={send}>
317
- <input
318
- aria-label="Message"
319
- value={draft}
320
- onChange={(event) => setDraft(event.currentTarget.value)}
321
- />
322
- <button disabled={!draft.trim() || !canSend}>Send</button>
323
- </form>
324
-
325
- {error ? <p role="alert">{error}</p> : null}
326
- </main>
327
- )
328
- }
329
- ```
330
-
331
- `push()` persists the command. Its receipt confirms admission to the inbox.
332
- `state.inbox.items` contains pending inputs; `state.messages` contains the
333
- admitted conversation. If persistence fails, the component restores the draft.
334
-
335
- The form gives Enter its normal submit behavior. The thinking row is also
336
- derived from `AIState`: it appears with the Agent label when the server's
337
- generation request folds, then the first visible assistant content replaces
338
- it. Empty stream-start messages never create a blank conversation row.
339
-
340
- Users can send another message while a response is active. The message enters
341
- the durable log immediately, but its turn waits until the active assistant
342
- response, including every tool step and approval, reaches a terminal event.
343
- Inbox order defaults to arrival order and can be edited explicitly.
344
-
345
- Pass `{ generate: false }` when a user message should update the conversation
346
- without starting a model turn:
347
-
348
- ```ts app/agent/passive-message.ts
349
- import type { UIMessage } from 'ai'
350
- import { inputs } from 'experimental-a2/ai'
351
- import { assistantClient } from './session'
352
-
353
- export async function recordPassiveMessage(
354
- sessionId: string,
355
- message: UIMessage,
356
- ): Promise<void> {
357
- await assistantClient
358
- .session(sessionId)
359
- .push(...inputs.message(message, { generate: false }))
360
- }
361
- ```
362
-
363
- The passive user message appears in `AIState.messages` when admitted. A later user
364
- message with the default generation behavior includes passive user messages
365
- before it in model context. Passive user messages after that trigger remain
366
- outside its prompt and wait for the next generating message. This ordering
367
- supports ambient agents without letting new background context leak into an
368
- already queued turn.
369
-
370
- ## What happens after `push()`
371
-
372
- One generating user interaction becomes a durable sequence:
373
-
374
- ```text
375
- browser ai.control.requested durable input command
376
- server ai.message.created input admitted
377
- server ai.control.committed accepted state changes
378
- server ai.control.decided command receipt
379
- server ai.generation.requested scheduled from durable facts
380
- server ai.generation.started
381
- server ai.generation.progress batched UIMessageChunk[]
382
- server ai.tool.called when present
383
- server ai.approval.requested when present
384
- server ai.tool.result after concurrent execution or denial
385
- server ai.generation.completed closes this model step
386
- server ai.generation.requested after all sibling tools are terminal
387
- ... another one-step generation
388
- server ai.message.completed
389
- server ai.generation.requested next queued user turn, when present
390
- ```
391
-
392
- Tool results and generation completion may arrive in either order. A fast
393
- automatic tool can finish while the model stream is still open. The join waits
394
- for both step closure and every required terminal result.
395
-
396
- The SSE connection delivers the server events to the same reducer. Progress
397
- stores every AI SDK chunk once. The reducer and the exported
398
- `deriveUIMessages()` apply those chunks synchronously to produce the current
399
- `UIMessage`; cumulative message snapshots are not duplicated in the log.
400
-
401
- `AIState` exposes `messages`, `status`, `activeGeneration`, `activeRequestId`,
402
- `activeResponseMessageId`, `activeProjection`, `responseOwners`,
403
- `terminalRequestIds`, `terminalGenerations`, `pendingApprovals`,
404
- `pendingInputs`, `tools`, `compaction`, per-generation `usage`, and the last
405
- generation `error`.
406
- `activeProjection` is the temporary indexed generation frontier used for exact
407
- interruption. It remains available after a generation step completes while the
408
- response waits on tool, approval, or input barriers, and after a generation
409
- fails while it awaits retry or interruption. It clears when the response
410
- completes, is interrupted or superseded, a retry begins, or a later generation
411
- replaces it.
412
-
413
- `activeRequestId` identifies the server-authorized generation request. It
414
- prevents a delayed request or recovered attempt from taking ownership from the
415
- current response before its generation starts.
416
-
417
- `activeResponseMessageId` identifies that request's response before
418
- `activeGeneration` exists. It is `null` after the generation starts.
419
-
420
- `responseOwners` keeps the latest generation ID, request ID, and attempt for each response
421
- message. Late lifecycle events from a superseded generation stay in raw
422
- history but cannot alter the projected message, even after its replacement
423
- finishes.
424
-
425
- A2 generates fixed-length deterministic IDs in its reserved `a2.ai:` namespace
426
- for requests, generations, responses, and worker lifecycle events. Treat those
427
- IDs as opaque. An assistant response uses its first model-request event's ID. Generation requests carry
428
- `responseMessageId` explicitly; tool continuations also carry `sourceGenerationId`.
429
- Ownership comes from these fields and the generation's `requestId` and `attempt`,
430
- so ID strings never embed the preceding tool loop.
431
-
432
- Approval and application-input events carry both `requestId` and `generationId`.
433
- Copy both from the pending approval, pending input, or active generation when
434
- sending a response. `activeRequestReason` and `activeRequestSourceGenerationId`
435
- describe the current request independently from the message projection.
436
-
437
- Generation requests share one durable lane, so model calls do not overlap.
438
- The queued-turn policy is the stronger ordering rule: a later user message is
439
- not scheduled until the current response's complete tool loop finishes. Lane
440
- FIFO alone would not prevent a newly appended user turn from overtaking a tool
441
- continuation that has not been appended yet.
442
-
443
- ## The built-in inputs
444
-
445
- `inputs` contains pure, isomorphic builders for protocol interactions:
446
-
447
- | Input | Events |
448
- | --- | --- |
449
- | `inputs.message(message, { generate? })` | queues input; `false` queues passive context |
450
- | `inputs.seed(message)` | records `generate: false`; non-user roles require a trusted append |
451
- | `inputs.approval(response)` | requests a decision on a pending approval |
452
- | `inputs.input(response)` | records an application input response fact |
453
- | `inputs.requestInput(request)` | records a trusted server request for application input |
454
- | `inputs.retry(options)` | requests a fresh attempt for a failed response |
455
- | `inputs.stop({ turnId })` | ends the observed turn and advances |
456
- | `inputs.steer({ turnId, message })` | prioritizes input after the observed turn's current step finishes |
457
- | `inputs.queue.edit({ message })` | edits a pending input |
458
- | `inputs.queue.move({ inputId, beforeId })` | reorders pending input; `null` means the end |
459
- | `inputs.queue.remove({ inputId })` | removes pending input from the inbox |
460
- | `inputs.queue.sendNow({ inputId, turnId })` | atomically prioritizes queued input and interrupts the observed turn |
461
- | `inputs.pause({ when })` | pauses now or after the active turn |
462
- | `inputs.resume()` | resumes retained work and inbox admission |
463
- | `inputs.toolResult(result)` | records a trusted deferred provider-tool result |
464
- | `inputs.interrupt(options)` | addresses a specific generation or request |
465
-
466
- Builders return `ai.control.requested` commands. Message, approval, input, and
467
- retry builders identify a stable interaction. Other controls use the event ID
468
- assigned by `push()` or `append()`; transport retries preserve that ID.
469
- Generation scheduling considers only user-role `ai.message.created` facts.
470
- Assistant and system message facts remain context regardless of the `generate`
471
- field, and model output is recorded as generation progress rather than a new
472
- message fact, so an assistant reply cannot recursively schedule another turn.
473
- Browser `push()` accepts user messages, approval and input responses,
474
- interruptions, and explicit retries. `inputs.requestInput()` and non-user
475
- messages built with `inputs.seed()` are for trusted server appends. Browser
476
- inputs never append server scheduling, seeded non-user messages, or lifecycle
477
- events.
478
- Approval and input request/response payloads carry the active `generationId`;
479
- clients copy it from the pending request so a stale interaction cannot satisfy
480
- a later model step.
481
-
482
- `inputs` deliberately has no session lifecycle methods. The A2 log begins with
483
- the first append. Apps that use explicit `ai.session.created` or
484
- `ai.session.closed` lifecycle events append those trusted server facts
485
- directly.
486
-
487
- ## Add tools and approval
488
-
489
- Tools are ordinary AI SDK `tool()` definitions. A2 passes them through to
490
- the AI SDK without interpreting or restricting them. Approval policy
491
- belongs in `generation`, beside the other per-step settings:
492
-
493
- ```ts server/with-tools.ts
494
- import { tool } from 'ai'
495
- import { z } from 'zod'
496
- import { createAgentServer } from 'experimental-a2/ai/server'
497
- import { assistant } from '../assistant'
498
-
499
- const tools = {
500
- closeTicket: tool({
501
- description: 'Close a resolved support ticket',
502
- inputSchema: z.object({ ticketId: z.string() }),
503
- execute: async ({ ticketId }, { toolCallId }) => {
504
- // your side effect; toolCallId makes a stable idempotency key:
505
- // await closeTicket(ticketId, { idempotencyKey: toolCallId })
506
- return { ticketId, closed: true }
507
- },
508
- }),
509
- }
510
-
511
- export const assistantServerWithTools = createAgentServer({
512
- agent: assistant,
513
- model: 'openai/gpt-5.6-terra',
514
- tools,
515
- generation: {
516
- toolApproval: {
517
- closeTicket: 'user-approval',
518
- },
519
- },
520
- })
521
- ```
522
-
523
- The AI SDK infers the tool input and output from its schemas and
524
- `execute`. A2 additionally runs each local `execute` inside an ambient
525
- scope carrying the current durable handler attempt. Read it with
526
- `handlerContext(agent)` from `experimental-a2/ai`: it returns the same
527
- typed context bag an event handler receives, with `event`, `attempt`,
528
- `session`, and `signal`. Tools execute an `ai.tool.execution.requested`
529
- event after authorization. Its payload contains the admitted `call`, its
530
- `generation`, and its turn and execution version. The agent argument carries the types; A2 verifies it against the
531
- server executing the tool and throws when a tool written for one agent
532
- runs under another, or when `handlerContext()` is called outside a tool
533
- execution. The scope survives awaited helpers and async iteration, so
534
- any code below `execute` can read it.
535
-
536
- `generation.toolApproval` is evaluated during the model step, before A2 has a
537
- durable tool handler. Static policies such as the example above are simplest.
538
- An input-dependent policy may be a function that returns `user-approval` or
539
- `approved` from the validated tool input. Approval predicates are
540
- re-evaluated on recovery for the same durable tool call, so they must be
541
- deterministic over the tool input; an impure predicate can fork one
542
- durable call's flow. Model-phase options such as `toolsContext` and
543
- per-tool `contextSchema` are yours and pass through to the AI SDK
544
- untouched. Any model-phase hook that receives tool definitions sees
545
- them without local `execute` functions. The durable A2 context is
546
- ambient only inside `execute`.
547
-
548
- The agent argument supplies types, not infrastructure. The context's
549
- `session` belongs to the server executing the tool, so its `schedule()`
550
- method uses that server's scheduler. For example, this tool schedules a
551
- reminder as a new user message:
552
-
553
- ```ts server/set-reminder.ts
554
- import { tool } from 'ai'
555
- import { handlerContext, inputs } from 'experimental-a2/ai'
556
- import { z } from 'zod'
557
- import { assistant } from '../assistant'
558
-
559
- export const setReminder = tool({
560
- description: 'Set a reminder for yourself',
561
- inputSchema: z.object({
562
- delaySeconds: z.number().int().min(1).max(6 * 24 * 60 * 60),
563
- reminder: z.string(),
564
- }),
565
- execute: async ({ delaySeconds, reminder }, { toolCallId }) => {
566
- const ctx = handlerContext(assistant)
567
- await ctx.session.schedule(
568
- `reminder:${toolCallId}`,
569
- { delay: `${delaySeconds}s` },
570
- ...inputs.message({
571
- id: `reminder:${ctx.event.id}`,
572
- role: 'user',
573
- parts: [{ type: 'text', text: `Reminder: ${reminder}` }],
574
- }),
575
- )
576
-
577
- return { scheduled: true, delaySeconds }
578
- },
579
- })
580
- ```
581
-
582
- Add `setReminder` to the server's `tools` and configure a
583
- [scheduler](/guides/production).
584
- The numeric schema lets the model choose any whole-second delay in the
585
- provider's supported range instead of selecting from a fixed application enum.
586
- The example uses A2's six-day Vercel Queues limit. The seventh retention day is
587
- reserved for delivery and retries. Adjust the maximum to match your scheduler
588
- and plan.
589
- The schedule name uses `toolCallId`, and the message id uses the durable
590
- triggering event id. A retry therefore converges on the same timer and message,
591
- even if a provider reuses tool-call ids in a later generation. When the timer
592
- arrives, `inputs.message()` queues a durable input command. The
593
- queued-turn policy starts a fresh response after the active response completes
594
- or is interrupted. A failed response must be retried or interrupted first.
595
- If the scheduler send fails ambiguously or transiently, A2 retries the same
596
- durable tool handler so that identity and target time stay fixed. Deterministic
597
- adapter rejections are terminal tool errors. A missing scheduler is also a
598
- normal tool error, so configure one before exposing the tool.
599
-
600
- A tool that never reads `handlerContext()` is just an AI SDK tool;
601
- nothing else changes. To test a context-reading tool directly, wrap the
602
- call with `runWithHandlerContext(ctx, fn)` from
603
- `experimental-a2/ai/server`: inside `fn`, `handlerContext()` returns
604
- `ctx` for any agent.
605
-
606
- The AI SDK emits an approval request instead of executing the tool. A2 records
607
- it as `ai.approval.requested`, and the reducer adds it to
608
- `state.pendingApprovals`. Respond from the same optimistic client path:
609
-
610
- ```tsx app/agent/[sessionId]/approval-controls.tsx
611
- 'use client'
612
- import { inputs } from 'experimental-a2/ai'
613
- import { useSession } from '../session'
614
-
615
- export function ApprovalControls() {
616
- const { state, push } = useSession()
617
-
618
- return state.pendingApprovals.map((approval) => (
619
- <div key={approval.approvalId}>
620
- <span>Allow tool call {approval.toolCallId}?</span>
621
- <button
622
- onClick={() =>
623
- void push(
624
- ...inputs.approval({
625
- requestId: approval.requestId,
626
- messageId: approval.messageId,
627
- generationId: approval.generationId,
628
- approvalId: approval.approvalId,
629
- approved: true,
630
- }),
631
- )
632
- }
633
- >
634
- Allow
635
- </button>
636
- <button
637
- onClick={() =>
638
- void push(
639
- ...inputs.approval({
640
- requestId: approval.requestId,
641
- messageId: approval.messageId,
642
- generationId: approval.generationId,
643
- approvalId: approval.approvalId,
644
- approved: false,
645
- reason: 'Denied by the user',
646
- }),
647
- )
648
- }
649
- >
650
- Deny
651
- </button>
652
- </div>
653
- ))
654
- }
655
- ```
656
-
657
- `inputs.approval()` records only the decision and updates the approval part in
658
- the projected `UIMessage`. A built-in handler executes the approved tool or
659
- records a denied result. It never bypasses the same sibling join used by
660
- automatic tools.
661
-
662
- Tool activity follows the full AI SDK chunk lifecycle. Input validation errors,
663
- execution errors, denials, and preliminary and final outputs become durable
664
- `ai.tool.result` events. Preliminary outputs keep the tool `running`; a final
665
- output completes it. Each result has its own event id, so a preliminary result
666
- cannot deduplicate the final one. Dynamic-tool, provider-executed, provider
667
- metadata, and tool metadata fields are preserved in the projection.
668
-
669
- Independent tool handlers run concurrently as soon as their durable inputs and
670
- authorization are ready. There is no fixed tool concurrency limit. A private
671
- coordinator reducer tracks generation closure, cancellation, calls, approvals,
672
- and terminal results for the active response. Its retained state is bounded;
673
- completed responses do not accumulate in the coordinator. Each join reads a
674
- durable reducer snapshot plus the log tail through
675
- `ctx.session.state(coordinator, { through: 'latest' })`. The snapshot
676
- is only a cache. Recovery can rebuild the same coordinator state from the event
677
- log after process death.
678
-
679
- A continuation needs the generation to close and every sibling tool to have a
680
- terminal success, error, or denied result. Racing join handlers call
681
- `ctx.session.append()` with the same deterministic continuation ID, so append
682
- idempotency resolves the race. A handler's single-owner terminal result is
683
- returned and committed atomically with that handler's completion. Preliminary
684
- streaming tool outputs use immediate appends. An approved provider-executed
685
- call can satisfy the join through its response. A provider tool that declares
686
- deferred-result support can wait for its provider result instead of a local
687
- executor. The authenticated provider callback appends that `ai.tool.result`
688
- through the trusted server session API. Browser `push()` rejects lifecycle
689
- events, including provider results.
690
-
691
- `inputs.input()` provides the same durable request/response shape for
692
- application-defined input. Its value stays in raw history and
693
- `state.pendingInputs`; resolve instructions dynamically or replace `generate`
694
- when the value should become model context.
695
-
696
- Approval and application-input request/response facts carry the active
697
- `generationId`. Echo that value from the pending request so a delayed UI action
698
- cannot authorize or resume a later model step.
699
-
700
- ## Interrupt and retry
701
-
702
- Interrupt the active response from the client:
703
-
704
- ```tsx app/agent/[sessionId]/stop-button.tsx
705
- 'use client'
706
- import { inputs } from 'experimental-a2/ai'
707
- import { useSession } from '../session'
708
-
709
- export function StopButton() {
710
- const { state, push, index } = useSession()
711
- const active = state.active
712
- if (!active) return null
713
-
714
- return (
715
- <button
716
- onClick={() =>
717
- void push(
718
- ...inputs.stop({
719
- turnId: active.turnId,
720
- lastSeenIndex: index,
721
- }),
722
- )
723
- }
724
- >
725
- Stop
726
- </button>
727
- )
728
- }
729
- ```
730
-
731
- The client immediately previews Stop for the observed turn. `lastSeenIndex` is the exact confirmed log frontier visible when the
732
- user clicked. While that generation's indexed projection is retained, the client freezes the response at that
733
- frontier through the request echo and controller decision. A rejected push or decision removes the preview.
734
- The reducer rewinds the indexed generation projection to that
735
- frontier and keeps the partial text the user actually saw. Tool parts whose
736
- arguments are still streaming are drafts: interruption removes them from the
737
- conversation and subsequent model prompts, even if their current JSON parses.
738
- Their progress remains in the raw log. Accepted tools still awaiting completion
739
- become `output-error`. An accepted interruption
740
- terminally fences that request or generation. Later generation, tool, approval,
741
- input, and compaction events remain in raw history but cannot reactivate it or
742
- alter the projection.
743
- Completed tool results at or before the visible frontier remain completed.
744
-
745
- Once Stop is persisted, A2's existing cancellation stream aborts running model
746
- and tool work for that turn without waiting for the control handler. Startup
747
- checks also skip matching jobs created while Stop is pending. Cancellation is
748
- cooperative: tools must honor their abort signal. The control handler remains
749
- responsible for recording the interruption and advancing the inbox; a Stop for
750
- an older turn cannot cancel its successor.
751
-
752
- For the lower-level `inputs.interrupt()` builder, between
753
- `ai.generation.requested` and `ai.generation.started`, copy
754
- `activeRequestId` as `requestId` and `activeResponseMessageId` as `messageId`.
755
- Once `activeGeneration` exists, send its `generationId` and
756
- `responseMessageId` instead. A request-owned interruption remains valid if
757
- that same request starts before the append commits. Every interruption has
758
- exactly one of `requestId` or `generationId`, so a delayed action cannot stop
759
- later work that reuses the response message. After a generation completes,
760
- copy the owner from a pending approval, pending input, or running tool while
761
- the response is still waiting.
762
-
763
- After a failed model call, `inputs.retry({ messageId, responseMessageId,
764
- retryId })` records a retry request. The server reruns that failed model call.
765
- Earlier completed model steps and their tool results remain, including steps
766
- completed after the latest compaction. The committed summary also remains. For
767
- example, if a tool succeeds after compaction and the next model call fails, retry
768
- uses the summary and that tool's result.
769
- `retryId` identifies the user's action, so resending one retry is safe while a
770
- later retry remains distinct. The failed model call's uncommitted step remains in
771
- raw history but is excluded from the new prompt and response. Later user messages remain queued while
772
- the failed response awaits an explicit retry or interruption.
773
-
774
- Applications own conversation forks and edit-and-resend flows for previously
775
- admitted messages. A2's retry operation continues the current response.
776
-
777
- ## Append from the server
778
-
779
- Browser interactions should use `push()` for instant state. Server-originated
780
- turns use the same inputs with `append()`:
781
-
782
- ```ts server/append-message.ts
783
- import { inputs } from 'experimental-a2/ai'
784
- import { assistantServer } from './assistant'
785
-
786
- export async function appendServerMessage(sessionId: string, text: string) {
787
- await assistantServer.session(sessionId).append(
788
- ...inputs.message({
789
- id: crypto.randomUUID(),
790
- role: 'user',
791
- parts: [{ type: 'text', text }],
792
- }),
793
- )
794
- }
795
- ```
796
-
797
- This path fits webhooks, jobs, and other backend sources. A connected browser
798
- still receives the events over SSE, but it has no optimistic overlay for an
799
- interaction that began elsewhere.
800
-
801
- ## Compact long conversations
802
-
803
- Compaction is automatic for supported Gateway models. A2 uses the active model
804
- with the same instructions, tool definitions, and provider settings, and appends
805
- an internal user message asking for a summary. This preserves the request prefix
806
- for prompt caching when the provider has a matching cache entry.
807
-
808
- ```ts server/compacted.ts
809
- import { createAgentServer } from 'experimental-a2/ai/server'
810
- import { assistant } from '../assistant'
811
-
812
- export const compactedAssistantServer = createAgentServer({
813
- agent: assistant,
814
- model: 'openai/gpt-5.6-terra',
815
- })
816
- ```
817
-
818
- On first use of each Gateway model in a session, A2 includes
819
- `ai.model.metadata.requested` in the existing generation-start append. A separate
820
- handler fetches the public Gateway model catalog and records the selected limits
821
- in `ai.model.metadata.resolved`. It runs outside the AI turn lane. The first model
822
- request proceeds immediately; metadata that arrives afterward is available to
823
- later steps. Returning to a previously used model reuses its durable metadata.
824
- The catalog is also cached in memory for one hour across agent servers, with one
825
- shared in-flight request and a five-second timeout. A failed lookup follows A2's
826
- handler retry policy without failing the model generation. A model absent from
827
- the catalog is recorded as unavailable. A pending entry means no result has been
828
- recorded, including when the metadata handler exhausts its retry budget. A2 does
829
- not start a fresh lookup on every turn after exhaustion. Inspect the handler
830
- failure or use an explicit threshold in that case.
831
-
832
- The default threshold is 75% of the context window. A larger configured
833
- `generation.maxOutputTokens` lowers the threshold further to reserve that output
834
- allowance. This is an input-context policy, not a new output limit. Limits and
835
- metadata status are available through an explicit transcript reducer read.
836
-
837
- `compaction: { thresholdTokens: 80_000 }` overrides discovery with an explicit
838
- positive safe integer. `compaction: { instructions: 'Preserve exact IDs.' }`
839
- adds guidance to the built-in summary prompt while keeping the derived threshold.
840
- `compaction: false` disables both compaction and metadata lookup. The `compaction`
841
- option can also be a resolver receiving the same
842
- `{ event, messages, session, signal }` context as `model` and `instructions`. It
843
- returns `false`, automatic options, or a custom policy once per generation and
844
- can await `session.state(settingsReducer)` to read application settings.
845
- Use this to select a threshold from application settings at the prompt boundary.
846
- Changes after resolution apply to a later model step. Disabling compaction
847
- preserves summaries that were already recorded. Custom provider
848
- models and custom global SDK providers need an explicit threshold. A custom
849
- `generate` callback keeps compaction disabled unless explicitly configured. Gateway
850
- fallback model lists also need an explicit threshold appropriate for every model
851
- they may use.
852
-
853
- The input estimate starts from serialized UTF-8 bytes divided by four, including
854
- model messages, instructions, and tool schemas. When a preceding model call has
855
- reported token usage for the same model and context, A2 uses that measurement as
856
- an anchor and estimates subsequent growth. It is not a provider tokenizer and
857
- does not precisely measure new image or audio tokens. A2 performs no remote token
858
- counting.
859
-
860
- On a cold session without recorded metadata or an explicit threshold, automatic
861
- compaction waits for discovery. An oversized first prompt is sent normally and
862
- its provider error becomes a generation failure. A2 does not silently truncate
863
- it or attempt automatic recovery from a context-limit error. Use an explicit threshold for
864
- applications that import a large initial conversation. The estimate is a
865
- compaction trigger, not a definitive overflow check. Provider context-limit
866
- errors during generation or summarization remain generation failures.
867
-
868
- Crossing the threshold adds one summary model request. Application generation
869
- callbacks, stream transforms, and tool hooks do not run for the summary, and it
870
- never executes local tools. Provider-executed tools, structured output, and forced
871
- tool choices disable implicit automatic compaction; explicitly configuring
872
- automatic compaction for those requests throws. Use a custom policy for them.
873
- An empty summary or a summary tool call fails the generation instead of replacing
874
- its context.
875
-
876
- A2 records `ai.compaction.requested` before the call and
877
- `ai.compaction.completed` after a successful summary. The completed event stores
878
- the summary, its usage, and the exact event frontier it covers. Later model
879
- requests receive the summary plus everything produced after that frontier,
880
- including later tool steps in the same assistant message. Queued user messages
881
- remain queued. Compaction waits until earlier tool calls have terminal results.
882
- The raw log and transcript from `agent.reducer` remain available in full.
883
- Server execution uses a separate model-context reducer and checkpoint. Successful
884
- compaction permanently replaces covered context. Callbacks receive their prompt
885
- messages and request information, not reducer state or execution internals.
886
- Application reducers remain explicit reads through `session.state(reducer)`.
887
- To read the full transcript, load `session.state(agent.reducer)` and pass its
888
- `state` to `agent.reducer.view()`. Client hooks continue to expose `AIState`.
889
-
890
- Custom `shouldCompact(context)` and `compact(context)` callbacks remain
891
- available for deterministic replacement messages or application-specific
892
- retention. Both receive the typed UI messages and `modelMessages`, the model-ready
893
- context including any existing summary. Custom generation callbacks also receive
894
- `modelMessages`; use it when forwarding context to a provider. A2 stores built-in
895
- summaries separately from application-typed UI messages, so a summary does not
896
- need your message metadata or schema.
897
-
898
- Render compaction as activity alongside the conversation:
899
-
900
- ```tsx app/agent/compaction-status.tsx
901
- 'use client'
902
- import { useSession } from './session'
903
-
904
- export function CompactionStatus() {
905
- const { state } = useSession()
906
- return state.compaction?.status === 'running' ? (
907
- <p role="status">Compacting…</p>
908
- ) : null
909
- }
910
- ```
911
-
912
- Successful compaction is a permanent boundary. Failure, interruption, retry, and
913
- worker recovery never restore the messages it replaced. Retry reruns the failed
914
- model step from the latest committed context and preserves earlier completed
915
- steps. A failed summary attempt leaves the previous committed context intact.
916
- The synthetic summary request is not a user turn or an assistant reply in
917
- `state.messages`.
918
-
919
- Compaction runs only when retained tool exchanges have final results and all
920
- A2-owned tool work has settled, including work from stopped or superseded turns.
921
- A2 checks a fresh coordinator checkpoint before starting a candidate compaction.
922
- This adds one state read on that path. Unresolved dead-lettered tool work keeps
923
- compaction deferred until repair and settlement or session closure.
924
-
925
- ## Extend the assembly
926
-
927
- The convenience API is ordinary A2 parts:
928
-
929
- - `events` and `createEvents()` export the built-in schemas.
930
- - `createReducer({ contract })` builds the normalized state and `AIState` view for a
931
- compatible contract.
932
- - `agent()` combines the built-in protocol, application events, and reducer.
933
- - `tool()` binds an AI SDK function-tool definition to an agent's typed A2
934
- handler context.
935
- - `createHandlers({ agent, ... })` returns the AI handler table.
936
- - `createAgentServer()` combines those handlers with `createServer()`.
937
-
938
- Use the lower-level pieces when an application needs more handlers or a
939
- different server assembly:
940
-
941
- ```ts server/custom.ts
942
- import { z } from 'zod'
943
- import { agent } from 'experimental-a2/ai'
944
- import { createHandlers } from 'experimental-a2/ai/server'
945
- import { createServer } from 'experimental-a2/server'
946
-
947
- const supportAgent = agent({
948
- name: 'extended-support-agent',
949
- events: {
950
- 'ticket.linked': z.object({ ticketId: z.string() }),
951
- },
952
- })
953
-
954
- const aiHandlers = createHandlers({
955
- agent: supportAgent,
956
- model: 'openai/gpt-5.6-terra',
957
- })
958
-
959
- export const customAssistantServer = createServer({
960
- contract: supportAgent.contract,
961
- handlers: {
962
- ...aiHandlers,
963
- 'ticket.linked': async ({ event }) => {
964
- // your side effect; event.id makes a stable idempotency key:
965
- // await indexTicket(event.payload.ticketId, { idempotencyKey: event.id })
966
- },
967
- },
968
- })
969
- ```
970
-
971
- Application events stay fully typed. The standard AI reducer ignores unknown
972
- events, so another reducer can project application state without forking the AI
973
- protocol. A custom `createServer()` assembly owns its browser ingress policy.
974
- Use `server.fetch(request, { authorize })` to authorize operations, and reject
975
- server-only AI event names there before they reach the custom server.
976
- `createAgentServer()` applies its stricter built-in ingress allowlist
977
- automatically.
978
-
979
- ### Replace generation, not durability
980
-
981
- The default generator uses AI SDK `streamText()` for one model step. Replace
982
- only that model-facing step for fixtures, provider routing, or a different AI
983
- SDK assembly:
984
-
985
- ```ts server/custom-generation.ts
986
- import { convertToModelMessages, streamText } from 'ai'
987
- import { createAgentServer } from 'experimental-a2/ai/server'
988
- import { assistant } from '../assistant'
989
-
990
- export const customGenerationServer = createAgentServer({
991
- agent: assistant,
992
- model: 'openai/gpt-5.6-terra',
993
- generate: async ({
994
- messages,
995
- model,
996
- instructions,
997
- responseMessageId,
998
- signal,
999
- }) => {
1000
- const result = streamText({
1001
- model,
1002
- messages: await convertToModelMessages(messages),
1003
- abortSignal: signal,
1004
- ...(instructions === undefined ? {} : { instructions }),
1005
- })
1006
-
1007
- return result.toUIMessageStream({
1008
- originalMessages: messages,
1009
- generateMessageId: () => responseMessageId,
1010
- })
1011
- },
1012
- })
1013
- ```
1014
-
1015
- `generate` receives compacted messages, resolved model and instructions, tools,
1016
- generation settings, durable request identity, current state, and
1017
- the abort signal. It returns exactly one model step as a
1018
- `ReadableStream<UIMessageChunk>`. A tool-aware replacement sends definitions to
1019
- the model without running local `execute` functions. A2 still owns durable
1020
- progress, tool execution, approval, continuation, interruption, and failure.
1021
- `messageMetadata` applies only to the default generator. A custom `generate`
1022
- function owns its metadata and can pass a callback to `toUIMessageStream()` or
1023
- emit typed metadata chunks itself.
1024
-
1025
- ## Checkpoints and recovery reads
1026
-
1027
- The AI reducer stores normalized `AIStoredState`. `session.state(agent.reducer)`
1028
- returns that state and its log index, ready to pass to `SessionProvider` or client
1029
- hydration. Client and React snapshots derive the familiar `AIState` view
1030
- immediately, including streamed message parts and tool activity.
1031
-
1032
- For server-side inspection, use the reducer's view:
1033
-
1034
- ```ts server/read-agent.ts
1035
- import type { UIMessage } from 'ai'
1036
- import { assistantServer } from './assistant'
1037
- import { assistant } from '../assistant'
1038
-
1039
- export async function readMessages(sessionId: string): Promise<UIMessage[]> {
1040
- const snapshot = await assistantServer.session(sessionId).state(assistant.reducer)
1041
- return assistant.reducer.view(snapshot.state).messages
1042
- }
1043
- ```
1044
-
1045
- Each snapshot stores repeated payloads once and keeps only the changes needed
1046
- to restore the current generation's prompt and projection. The live view is cached while
1047
- events fold; normalization does not run for every token. Reading or serializing
1048
- the stored fields materializes the records. The log and displayed conversation
1049
- retain their full content.
1050
-
1051
- Model steps read the model-context reducer and coordinator at a fixed log
1052
- boundary. Model, instruction, compaction, and custom-generation callbacks
1053
- receive prompt messages and request information without reducer state. Custom transcript folds and
1054
- schemas apply only to the transcript; read application state explicitly with
1055
- `session.state(yourReducer)` when a callback needs it.
1056
- They do not receive a raw history array. Their `session.state(reducer)` reads
1057
- application reducers at the captured prompt boundary by default; explicit
1058
- `through` options follow the ordinary state-read contract. Compaction selection
1059
- can be asynchronous when it needs a settings-state read.
1060
-
1061
- Ordinary generation uses the model-context checkpoint without reading the
1062
- transcript or replaying history to apply an existing compaction. The context
1063
- checkpoint has its own identity and fold. Recovery and explicit retry discard
1064
- only the unfinished model step. Completed compactions remain committed, so these
1065
- paths never reconstruct pre-compaction prompts or retain rollback copies.
1066
-
1067
- Tools recovering in a fresh process read context at their tool-admission index.
1068
- The saved generation projection restores their original prompt, preserving the
1069
- committed summary and excluding later arrivals. No separate history read is
1070
- needed. A2 core can rebuild a missing or invalid reducer snapshot from the log.
1071
-
1072
- Compaction immediately releases covered message and tool content from model
1073
- context. Execution retains the active turn, queued inputs, unsettled tool work
1074
- IDs, and token calibration. Model metadata is cached per distinct model.
1075
- Historical input uniqueness comes from core event deduplication. The full
1076
- transcript remains available independently.
1077
-
1078
- Streaming progress updates the current message incrementally. Snapshots preserve
1079
- open text and reasoning parts and partial tool inputs. Indexed progress remains
1080
- available for exact interruption cutoffs; reordered events rebuild the projection.
1081
-
1082
- While tool arguments arrive, `input-streaming` parts expose a best-effort partial
1083
- JSON value in `input`. A command string can appear and grow before its closing
1084
- quote arrives. This is a display draft, not validated execution input.
1085
- `tool-input-available` supplies the completed input. Interruption still discards
1086
- streaming drafts even when their partial JSON is readable.
1087
-
1088
- ## Delivery semantics
1089
-
1090
- Model steps and tools run inside at-least-once A2 handlers. The returned-event
1091
- transaction makes each successful handler completion and its terminal events
1092
- one atomic durable operation. Deterministic ids make scheduling and lifecycle
1093
- appends effectively once, and A2 marks an incomplete model attempt
1094
- `superseded` before starting its replacement.
1095
-
1096
- An expired claim or superseded attempt preserves its abort reason through model,
1097
- resolver, compaction-policy, and tool calls. A2 retries that attempt without
1098
- spending its failure budget. Explicit user interruption remains terminal.
1099
- Streaming tool progress belongs to an execution attempt, so a retry can produce
1100
- different preliminary output while preserving the tool-call id for idempotency.
1101
-
1102
- Provider calls and external tool side effects remain outside that transaction.
1103
- A process can die after an external effect succeeds and before its handler
1104
- completion commits, so recovery may run the call again. Give side-effecting
1105
- tools their own idempotency strategy, normally keyed by the AI SDK tool call
1106
- id. This is the same at-least-once boundary as every other A2 handler. A fast
1107
- tool may also start before its model step later fails. The failure prevents a
1108
- continuation, but it cannot roll back that external effect.
1109
-
1110
- Configure a [production scheduler](/guides/production) exactly as for any other A2
1111
- server. It recovers an incomplete generation attempt after the original
1112
- serverless invocation disappears.
1113
-
1114
- ## Inbox and turn controls
1115
-
1116
- This input protocol requires matching client and server versions. Start a new
1117
- agent session when adopting it. Existing event logs remain readable; finish
1118
- pending work with the runtime that created it before upgrading.
1119
-
1120
- `inputs.message(message)` places a message in the durable inbox. The controller
1121
- admits one input at a time, freezes its content, and adds it to `state.messages`.
1122
- Pending inputs appear in `state.inbox.items`; they do not enter the model prompt.
1123
- Each item has an `id`, `message`, and `generate` flag.
1124
-
1125
- New-input commands from `inputs.message()`, `inputs.seed()`, and `inputs.steer()`
1126
- use the message ID as their event ID. Those IDs must match on hand-written
1127
- commands too. Caller IDs cannot start with the reserved `a2.ai:` prefix. IDs must
1128
- be unique within the store; include contract/session scope when deriving them
1129
- with `idempotentId()`.
1130
-
1131
- An identical submission replays its original event without creating more work,
1132
- even after removal or compaction. Reusing the ID with different content, a
1133
- different command action, or another session rejects at append with
1134
- `PARTIAL_DUPLICATE_BATCH`. Use `queue.edit()` to change a pending input and a fresh
1135
- message ID to create another input. Edit, move, remove, and other commands have
1136
- their own event IDs. The controller stores no historical input-ID registry.
1137
-
1138
-
1139
- `state.active` identifies the logical turn, its frozen `input`, and its `phase`.
1140
- The turn ID stays the same across model steps, tools, and a pause. Stop ends that
1141
- turn and admits the next queued input when admission is running.
1142
-
1143
- ```ts client/turn-controls.ts
1144
- import { inputs, type AIState } from 'experimental-a2/ai'
1145
- import type { UIMessage } from 'ai'
1146
-
1147
- export function editPending(message: UIMessage) {
1148
- return inputs.queue.edit({ message })
1149
- }
1150
-
1151
- export function movePending(inputId: string, beforeId: string | null) {
1152
- return inputs.queue.move({ inputId, beforeId })
1153
- }
1154
-
1155
- export function removePending(inputId: string) {
1156
- return inputs.queue.remove({ inputId })
1157
- }
1158
-
1159
- export function sendPendingNow(state: AIState, inputId: string) {
1160
- return inputs.queue.sendNow({ inputId, turnId: state.active?.turnId ?? null })
1161
- }
1162
-
1163
- export function stopCurrent(state: AIState) {
1164
- return state.active ? inputs.stop({ turnId: state.active.turnId }) : []
1165
- }
1166
-
1167
- export function steerCurrent(state: AIState, message: UIMessage) {
1168
- return state.active
1169
- ? inputs.steer({ turnId: state.active.turnId, message })
1170
- : inputs.message(message)
1171
- }
1172
-
1173
- export const pauseAfterTurn = () => inputs.pause({ when: 'after-turn' })
1174
- export const pauseNow = () => inputs.pause({ when: 'now' })
1175
- export const resume = () => inputs.resume()
1176
- ```
1177
-
1178
- Pass these arrays to `push(...events)` from `useSession()`, or to a server
1179
- session's `append(...events)`. `beforeId: null` moves an item to the end.
1180
- Removal only applies to pending input. It preserves the immutable log and keeps
1181
- its original submission event, so replay cannot recreate a removed prompt.
1182
- Removing an input that is no longer pending returns `not-pending`, whether it was
1183
- admitted, removed, or never queued.
1184
-
1185
- Edits to pending input use last-write-wins in session log order. Clients can send
1186
- another edit before an earlier one is confirmed. Editing preserves the message's
1187
- identity and role; changing its role returns `role-mismatch`. An edit that loses
1188
- to admission returns `not-pending`. A Stop aimed at a completed turn returns
1189
- `stale-turn`.
1190
-
1191
- Steer validates the new input and places it ahead of the inbox in one decision.
1192
- If the observed turn is still active, it finishes its current model response and
1193
- the tools produced by that response before handing off. Parallel tools all
1194
- settle, and steering runs before the old turn starts another model step.
1195
- Text and tool results produced after the click remain in the conversation.
1196
- `lastSeenIndex` is not used to truncate output for steering or Send now.
1197
-
1198
- When running work settles, unanswered approval or application-input requests
1199
- are closed so the steering message can take over. The unapproved tool never
1200
- executes. Provider-executed deferred results still count as running work when
1201
- execution is authorized; a denied approval cannot block handoff.
1202
- If the targeted turn has already finished, the input starts when idle or waits
1203
- first behind a different active turn. Stop remains an immediate interruption.
1204
-
1205
- Steering preserves the admission gate: steering while paused does not resume
1206
- execution. Duplicate input identities and closed sessions still reject the command.
1207
-
1208
- Send now uses the selected queued message's existing identity and latest
1209
- accepted content. It validates the selected input and the observed `turnId`,
1210
- then moves the input first for the same handoff after the current step. Use
1211
- `turnId: null` when no turn is active. If that expectation is stale, the command
1212
- returns `stale-turn` without changing the queue.
1213
-
1214
- Send now preserves pause just like steering. A paused queue keeps the selected
1215
- input first until Resume. It accepts user-role inputs, including passive user
1216
- input, which it marks for generation. Non-user context returns `not-user-input`;
1217
- reserved internal IDs are rejected before append. Unsaved editor text
1218
- is separate from the accepted queue content; save it before sending.
1219
-
1220
- A queued item's optional `afterStepOf` records the observed turn for steering.
1221
- A matching active turn hands off after its current step.
1222
- Editing preserves this intent; removing the item cancels it. Reordering the
1223
- queue controls which input is admitted at that boundary.
1224
-
1225
- ### Optimistic conversation display
1226
-
1227
- With `agent().reducer` or `createReducer()`, the client state is already optimistic.
1228
- An idle, unpaused send appears in `state.messages` immediately, with
1229
- `state.starting: true`. Another send waits in `state.inbox.items` while that
1230
- optimistic turn or a confirmed turn is active. Paused sessions keep new sends in
1231
- the inbox. Render the conversation and queue from `state`. A pending Stop clears
1232
- the displayed `state.active` turn immediately; rejection restores it. New active
1233
- turn identities still come from accepted control decisions.
1234
- Steering appears immediately in `state.messages`, after the current response,
1235
- and stays there while that step finishes. Send now has the same display when
1236
- an active turn is targeted. These inputs are omitted from the client inbox,
1237
- including while paused, but remain pending on the server until admission.
1238
- Accepted steering survives receipt and hydration in this display. Multiple
1239
- steering inputs appear in admission order, with the most recent first.
1240
- No additional request, subscription, or component-owned queue is needed.
1241
-
1242
- ```tsx app/agent/[sessionId]/conversation.tsx
1243
- 'use client'
1244
- import { useSession } from '../session'
1245
-
1246
- export function Conversation() {
1247
- const { state } = useSession()
1248
- const { messages, inbox, starting } = state
1249
- const text = (message: (typeof messages)[number]) =>
1250
- message.parts.filter((part) => part.type === 'text').map((part) => part.text).join(' ')
1251
- return (
1252
- <section>
1253
- <div aria-label="Conversation">
1254
- {messages.map((message) => <p key={message.id}>{text(message)}</p>)}
1255
- {starting ? <p role="status">Starting…</p> : null}
1256
- </div>
1257
- <ol aria-label="Queued messages">
1258
- {inbox.items.map((item) => <li key={item.id}>{text(item.message)}</li>)}
1259
- </ol>
1260
- </section>
1261
- )
1262
- }
1263
- ```
1264
-
1265
- The request echo keeps the optimistic display until `ai.control.decided` arrives. Accepted
1266
- commits replace the confirmed base; rejected receipts remove the pending intent.
1267
- A failed POST rolls back through the ordinary client optimistic overlay.
1268
- Another client's accepted turn can move a speculative ordinary send back into
1269
- the queue. Steering stays visible as a message while waiting behind that turn. `state.starting` describes an optimistic turn awaiting acceptance; it does
1270
- not indicate a running model.
1271
-
1272
- Server `session.state(reducer)`, reducer folds, checkpoints, and model prompts
1273
- contain confirmed messages and inbox entries, with `starting: false`. The client
1274
- applies pending intent and displays accepted steering only when returning its
1275
- state, after folding events.
1276
- A custom reducer can keep normalized AI state by nesting `agent.reducer.fold()`
1277
- and derive its view with `agent.reducer.view()`. It does not inherit the built-in
1278
- optimistic client display. `reduceAIState()` remains a pure transition over the
1279
- confirmed `AIState` view for applications that project raw event histories.
1280
- `state.pendingQueueCommands` tracks unresolved queue commands and Stops incrementally, so
1281
- the client does not scan raw history. Pending edits apply in order, so the latest
1282
- local edit stays visible while earlier commands are confirmed. Editing and Send
1283
- now can remain available while an edit awaits its receipt. Close an editor as
1284
- soon as its save is pushed; retain the draft if the push fails.
1285
-
1286
- ### Persistence and application are separate
1287
-
1288
- `push()` confirms that the command is persisted. The `ai.control.decided` event
1289
- confirms its application, using the submitted event's ID as `commandId`. Its
1290
- `outcome` is `applied` or `rejected`; a rejection includes a `reason`.
1291
- `state.receipt` exposes the most recent user command receipt. Applications that
1292
- have several commands in flight correlate receipts through the existing event
1293
- stream. A rejection is a normal result and does not spend a handler retry.
1294
-
1295
- ### Pause and resume
1296
-
1297
- Pause after the turn closes inbox admission and lets the active turn finish.
1298
- Pause now also cancels current model and tool execution. `state.active.phase`
1299
- becomes `pausing` while old work settles, then `paused`. Resume reopens admission
1300
- and resumes retained work before admitting another input.
1301
-
1302
- Resume starts a fresh model request from durable state. It does not resume the
1303
- provider's old stream or undo completed tool effects. The controller retains
1304
- accepted tool calls and records their outcomes while paused. A cancelled tool
1305
- without a terminal result can execute again after resume; use the stable SDK
1306
- `toolCallId` for external idempotency. No model or tool claim is held merely to
1307
- keep a turn paused.
1308
-
1309
- The controller runs on a short lane. Each decision commits accepted facts,
1310
- compact state changes, work requests, and its receipt atomically. Workers use
1311
- separate claims, report lifecycle batches, and wait for acceptance at model-start
1312
- and compaction boundaries. Token progress appends directly. Explicit AI history
1313
- reads remain bounded to a relevant generation suffix; core checkpoint rebuilding
1314
- can replay older events when a snapshot is missing.
1315
-
1316
- For a deferred provider-executed tool, trusted server code submits
1317
- `inputs.toolResult(result)`. The controller checks its admitted call and records
1318
- the result before continuing. Browser ingress rejects this server-only input.