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,1896 +0,0 @@
1
- ---
2
- title: API
3
- description: "The whole surface, small enough to read in one sitting: contract, server, session, reducer, and the React and HTTP helpers."
4
- ---
5
-
6
- ## `experimental-a2`
7
-
8
- ### `a2.contract(options)`
9
-
10
- ```ts
11
- a2.contract(options: {
12
- name: string
13
- events: Record<string, StandardSchemaV1>
14
- presence?: Record<string, StandardSchemaV1>
15
- }): Contract
16
- ```
17
-
18
- Defines one kind of lifecycle: a name plus the events it understands. The name
19
- must be unique per app; it identifies the contract in storage and queue
20
- messages. The result is a plain, importable, **isomorphic** value: the server
21
- implements it, reducers derive from it, the browser types its pushes off it.
22
- See [Contracts and sessions](/concepts/contracts).
23
-
24
- `presence` is the optional second vocabulary: each key a field of one
25
- participant's ephemeral state, each value its schema. Presence fields are
26
- replicated last-writer-wins and never stored in the log; reducers cannot
27
- see them. See [Presence](/guides/presence).
28
-
29
- Schemas are [Standard Schema](https://standardschema.dev): Zod, Valibot,
30
- ArkType, anything that implements it. Validators must be synchronous
31
- (async ones are rejected here, at definition time), and the validated
32
- output is what's stored.
33
-
34
- ### `contract.batch(...events)`
35
-
36
- Preserves the literal types in a heterogeneous handler-returned event array.
37
- It returns the same events as a readonly tuple; A2 validates them when the
38
- handler completes.
39
-
40
- ### `contract.reducer(options)`
41
-
42
- ```ts
43
- contract.reducer(options: {
44
- name: string // identity; keys cached snapshots
45
- initialState: S // the seed (schema input, when stateSchema is set)
46
- stateSchema?: StandardSchemaV1 // declares S, checks initialState, guards cached folds
47
- }): { fold(fn: (state: S, event: Event) => S): Reducer<S> }
48
- ```
49
-
50
- Derives a view from the contract. Two steps by design: the first call
51
- fixes the state and event types, so `.fold()` receives fully concrete
52
- ones. Literal unions in your state survive, with zero annotations.
53
-
54
- The name is the reducer's identity; it keys cached snapshots, so rename it
55
- when the fold's logic changes. Two reducers never fight over a cache
56
- entry, and a rename (or a `stateSchema` mismatch, when present)
57
- invalidates, and the next read refolds from raw events. See
58
- [Reading state](/concepts/state).
59
-
60
- ### `A2Error`
61
-
62
- The shared base for A2 failures, discriminated by `code`. The session
63
- client exposes its `A2ClientError` subclass. See
64
- [Errors](/reference/errors).
65
-
66
- ### `idempotentId(...parts)`
67
-
68
- `idempotentId(...parts: string[]): Promise<string>` derives a stable UUIDv8
69
- from an ordered tuple of strings. It runs on the server and in the browser.
70
-
71
- ```ts shared/request-id.ts
72
- import { idempotentId } from 'experimental-a2'
73
-
74
- export const id = await idempotentId('orders', 'shop-7', 'request-42')
75
- ```
76
-
77
- Pass the result as an event's `id` when retries should converge on the same
78
- event. The same tuple produces the same ID across runtimes and A2 versions.
79
- String boundaries, order, and case matter; strings are not normalized.
80
- Empty strings and an empty tuple are valid inputs.
81
-
82
- Every argument has the same role. A prefix such as `'orders'` is a namespace
83
- by convention. Include enough context to distinguish operations across
84
- contracts and sessions, since event IDs are globally unique within a store.
85
-
86
- The helper performs no I/O. A2 preserves explicit IDs and keeps its automatic
87
- ID generation unchanged. Changing an existing application's ID recipe changes
88
- retry identity, so retries of previously committed requests need to retain
89
- their original IDs.
90
-
91
- ## `experimental-a2/server`
92
-
93
- ### `handler(entry)`
94
-
95
- Define handlers inline in `createServer({ handlers })` for ordinary use.
96
- Use this helper when composing an existing handler.
97
-
98
- Accepts a handler function or an object with `handler` and optional `lane` and
99
- `abortOn`. Returns the object shape: a function becomes `{ handler: entry }`,
100
- and an object is returned unchanged. The original function and object types
101
- are preserved. Use it to compose an existing handler without branching on its
102
- shape.
103
-
104
- ```ts server/composed-orders.ts
105
- import * as a2 from 'experimental-a2'
106
- import { createServer, handler, type HandlerContext } from 'experimental-a2/server'
107
- import { z } from 'zod'
108
-
109
- const orders = a2.contract({
110
- name: 'composed-orders',
111
- events: {
112
- created: z.object({ shopId: z.string() }),
113
- notified: z.object({ shopId: z.string() }),
114
- },
115
- })
116
-
117
- const created = handler({
118
- lane: 'orders',
119
- handler: async (ctx: HandlerContext<typeof orders.events, 'created'>) => {
120
- return orders.batch({ type: 'notified', payload: ctx.event.payload })
121
- },
122
- })
123
-
124
- export const ordersServer = createServer({
125
- contract: orders,
126
- handlers: {
127
- created: {
128
- ...created,
129
- handler: async (ctx) => {
130
- const result = await created.handler(ctx)
131
- // Your additional work goes here:
132
- // await notifyShop(ctx.event.payload, { idempotencyKey: ctx.event.id })
133
- return result
134
- },
135
- },
136
- },
137
- })
138
- ```
139
-
140
- Spreading the object preserves its `lane` and `abortOn`. Set either property
141
- after the spread to override it. Changing a built-in AI handler's lane can
142
- separate it from the handlers it coordinates with.
143
-
144
- Composition runs in one handler attempt. Work after the original function
145
- resolves still runs before its returned events commit. A thrown error fails
146
- the composed attempt; on retry, the wrapper and original handler can run again.
147
- For a new handler, annotate its context as above or pass an already typed
148
- function. `handler` does not bind a handler to a contract;
149
- `createServer({ handlers })` checks that it matches the contract.
150
-
151
- ### `createServer(options)`
152
-
153
- ```ts
154
- createServer(options: {
155
- contract: Contract // the vocabulary this server implements
156
- store?: A2Store // default: sqlite in dev, memory in tests, required in prod
157
- scheduler?: A2Scheduler
158
- telemetry?: A2Telemetry // optional instrumentation; see experimental-a2/otel
159
- presence?: { ttlMs?: number } // presence expiry policy; default 60s
160
- handlers?: {
161
- [type]:
162
- | Handler
163
- | {
164
- abortOn?: AbortSpec
165
- lane?: string | ((ctx: LaneContext) => string)
166
- handler: Handler
167
- }
168
- }
169
- }): A2Server
170
-
171
- type Handler = (
172
- ctx: Context,
173
- ) => Promise<void | AppendInput | readonly AppendInput[]>
174
- ```
175
-
176
- Implements a contract: binds the vocabulary to storage and reactions.
177
- Handlers are one table, complete at construction, so a handler can never
178
- be silently missing because the module that registered it wasn't
179
- imported. Compose across files by spreading objects into `handlers`
180
- (note: a duplicate key under spread silently last-wins).
181
-
182
- Handlers are concurrent by default. A2 starts every eligible event after its
183
- append commits. Add `lane` when events share a resource and must not overlap.
184
- Within one session, events with the same lane value run one at a time in log
185
- order. Different lanes and events without a lane run concurrently. A lane
186
- resolver receives `{ sessionId, event }`; A2 resolves and stores the value when
187
- the event is appended, so a later deployment cannot reinterpret pending work.
188
- For example, `lane: ({ event }) => event.payload.warehouseId` serializes work
189
- per warehouse while different warehouses continue concurrently.
190
-
191
- Handlers can return one event or an array. A2 marks the triggering event
192
- processed and appends the returned batch in one atomic store operation. Returned
193
- events do not exist when the handler throws. `ctx.session.append(name, ...events)`
194
- is different: it commits immediately, so its events may run while the current
195
- handler is still active unless a lane orders them.
196
- `ctx.session.schedule(name, timing, ...events)` waits for provider acceptance;
197
- its events append later and are not part of the parent completion transaction.
198
-
199
- Handlers are optional per event type. An event type without one settles in
200
- the append transaction with no dispatch attempt. If the session has no older
201
- pending handler work, A2 starts no drain or scheduler arm. A server with no
202
- handlers is a durable event log with no reaction infrastructure. See
203
- [Events without handlers](/concepts/handlers#events-without-handlers).
204
-
205
- Server-only by construction: `experimental-a2/server` is the only entry point that
206
- can reach a store backend, and its exports map resolves to a loud error
207
- under the browser condition.
208
-
209
- `presence.ttlMs` sets how long a presence value survives without a
210
- refreshing set (default 60 seconds, as a positive integer of
211
- milliseconds). Expiry counts on the storage's own clock, never on the
212
- sender's stamp. The option is valid only on contracts that declare
213
- `presence`; anywhere else construction throws a `TypeError`.
214
-
215
- `abortOn` names the events that fire `ctx.signal` while a handler runs.
216
- an array matches by type; an object takes per-type predicates for
217
- targeted cancellation:
218
-
219
- ```ts
220
- generate: {
221
- abortOn: {
222
- cancelled: (event, trigger, { attempt }) =>
223
- event.payload.of === `${trigger.id}:${attempt}`,
224
- },
225
- handler: async (ctx) => { /* ... */ },
226
- }
227
- ```
228
-
229
- The predicate's third argument contains the triggering event's durable
230
- `attempt`, so a cancellation can fence a specific recovered run.
231
-
232
- The context every handler receives:
233
-
234
- | Property | Type |
235
- | ------------- | --------------------------------------------------------- |
236
- | `ctx.event` | `Event`: the triggering event |
237
- | `ctx.attempt` | durable 1-based dispatch claim ordinal |
238
- | `ctx.session` | this session's `id`, `append`, `schedule`, `history`, `state`, `stream`, `setPresence` |
239
- | `ctx.signal` | `AbortSignal`: fires on lease lapse (`CLAIM_EXPIRED`), proven supersession (`SUPERSEDED_ATTEMPT`), and matching `abortOn` events |
240
-
241
- `ctx.attempt` starts at `1` and increments on every durable claim. It may skip
242
- when a process dies before handler entry.
243
-
244
- `ctx.session.id` equals `ctx.event.sessionId`. Its `history`, `state`, and
245
- `stream` methods share the session surface returned by `server.session(id)`,
246
- with one causal default: `ctx.session.state(reducer)` folds through the
247
- triggering event's index. The same event sees the same state across concurrent
248
- appends and retries. Ask for `{ through: 'latest' }` when a handler intentionally
249
- needs the committed frontier captured when that read runs.
250
-
251
- The handler-local append is specialized:
252
-
253
- ```ts
254
- ctx.session.append(name: string, ...events: AppendInput[]): Promise<Event[]>
255
- ```
256
-
257
- The name is an idempotency key scoped to the triggering event for events whose
258
- `id` is omitted. A2 derives each omitted id from the trigger id, name, and event
259
- position. The same name and generated-id batch returns the existing rows
260
- across retries. Changing that batch conflicts with those rows. An explicit
261
- event `id` wins over the generated id, which lets different triggering events
262
- converge on one fact. A root `server.session(id).append(...events)` takes no
263
- name.
264
-
265
- `ctx.session.schedule(name, timing, ...events)` includes the triggering event
266
- in its stable task identity. A relative delay is anchored to that event's
267
- durable `createdAt`, so a handler retry keeps the same delivery time. The call
268
- returns `Promise<void>` after the configured scheduler accepts the task.
269
-
270
- A latest state read and following append are not atomic. Concurrent appends and
271
- retries may move the frontier between them. A generic join should read with
272
- `{ through: 'latest' }`, use a monotone readiness predicate, and give its output
273
- a stable explicit event `id`, so every eligible attempt converges on the same
274
- append.
275
-
276
- ### `server.fetch(request, options?)`
277
-
278
- ```ts
279
- server.fetch(
280
- request: Request,
281
- options?: {
282
- authorize?: (operation: A2Operation) => boolean | Promise<boolean>
283
- upgradeWebSocket?: (
284
- attach: (socket: A2Socket) => void,
285
- ) => Response | Promise<Response>
286
- multiplexWebSocket?: boolean
287
- },
288
- ): Promise<Response>
289
-
290
- type A2Operation =
291
- | {
292
- type: 'stream'
293
- sessionId: string
294
- startAfter: number
295
- transport: 'http' | 'websocket'
296
- }
297
- | {
298
- type: 'history'
299
- sessionId: string
300
- gte: number
301
- lte: number
302
- transport: 'http'
303
- }
304
- | {
305
- type: 'push'
306
- sessionId: string
307
- events: readonly {
308
- type: ContractEventType | string
309
- payload: unknown
310
- id?: string
311
- }[]
312
- presence?: {
313
- participant: string
314
- values: Readonly<Record<string, unknown>>
315
- seen?: number
316
- at?: number
317
- }
318
- transport: 'http' | 'websocket'
319
- }
320
- ```
321
-
322
- The bound Fetch API route for this server. An unprotected route can export it
323
- directly:
324
-
325
- ```ts app/api/public-order-events/route.ts
326
- import { ordersServer } from '@/server/orders'
327
-
328
- export const GET = ordersServer.fetch
329
- export const POST = ordersServer.fetch
330
- ```
331
-
332
- A plain GET streams one session over SSE, resumed after the `index` query
333
- parameter. A GET with `gte` and `lte` returns the closed history range as JSON
334
- and sets `a2-history-covered` to `true` when every requested index was returned.
335
- POST accepts `{ sessionId, events, presence? }` and returns the appended events.
336
- Presence is inferred from the contract. A contract that declares presence
337
- streams it and accepts presence pushes automatically.
338
-
339
- `authorize` runs after the request or socket frame has been parsed, but before
340
- contract schema validation, presence validation, or I/O. Event names and
341
- presence field names are typed from the contract. Their unvalidated values stay
342
- `unknown`. Return `false` to deny the operation with `FORBIDDEN`. A thrown error
343
- becomes `STORE_UNAVAILABLE`, because authorization infrastructure failed rather
344
- than denied access. Operation objects, push events, event arrays, and presence
345
- patches are frozen. Authorize them, do not rewrite them.
346
-
347
- Authentication stays outside A2, where the route can read headers, cookies, and
348
- framework context. Capture the authenticated principal in `authorize`:
349
-
350
- ```ts app/api/protected-order-events/route.ts
351
- import { ordersServer } from '@/server/orders'
352
-
353
- async function fetchOrders(request: Request): Promise<Response> {
354
- // your authentication:
355
- const user = { id: 'user-1' }
356
- if (!user) return new Response(null, { status: 401 })
357
-
358
- return ordersServer.fetch(request, {
359
- authorize(operation) {
360
- // your per-session and per-operation authorization:
361
- // return canAccess(user, operation)
362
- return true
363
- },
364
- })
365
- }
366
-
367
- export const GET = fetchOrders
368
- export const POST = fetchOrders
369
- ```
370
-
371
- Direct `session.append`, handler appends, scheduled appends, and server-side
372
- `setPresence` bypass `authorize`. It is an ingress policy for this fetch call,
373
- not a global server policy.
374
-
375
- A GET with `Upgrade: websocket` uses `upgradeWebSocket`. Without that option it
376
- returns 426. The upgrade function receives an `attach` callback for the
377
- platform socket:
378
-
379
- ```ts app/api/socket-order-events/route.ts
380
- import { experimental_upgradeWebSocket } from '@vercel/functions'
381
- import { ordersServer } from '@/server/orders'
382
-
383
- export const POST = ordersServer.fetch
384
-
385
- export function GET(request: Request): Promise<Response> {
386
- return ordersServer.fetch(request, {
387
- upgradeWebSocket: (attach) =>
388
- experimental_upgradeWebSocket(attach, {
389
- maxPayload: 4 * 1024 * 1024,
390
- }),
391
- })
392
- }
393
- ```
394
-
395
- By default, the `sessionId` and `index` query parameters bind one session to the
396
- socket. A2 authorizes the stream before upgrading. Frames on that socket cannot
397
- select another session.
398
-
399
- Set `multiplexWebSocket: true` here and `multiplex: true` in the client's `ws`
400
- API to share one socket.
401
- Authenticate the physical WebSocket GET before calling `server.fetch`. Validate
402
- `Origin` too when browser cookies authenticate it; WebSocket handshakes do not
403
- use a CORS preflight. Use `authorize` for each session subscribe and each event
404
- or presence push. Only enable multiplexing when that request context may access
405
- every session accepted by `authorize`. A denied subscribe receives
406
- `unsubscribed` and does not affect other sessions. A denied event push receives
407
- a `FORBIDDEN` ack. Denied presence is silently dropped because it is
408
- fire-and-forget.
409
-
410
- SSE and WebSocket lifetimes use the platform's ambient invocation deadline.
411
- A2 also uses the platform's ambient `waitUntil` capability for background work.
412
- Neither capability is reordered or passed through `server.fetch` options.
413
-
414
- ### `server.session(id)`
415
-
416
- ```ts
417
- server.session(id: string): Session
418
- ```
419
-
420
- A handle on one instance of the contract. The session is the unit of log
421
- ordering, lane keys, recovery, state, and live sync. Handler execution is
422
- concurrent unless events share a lane. Creating the handle does no I/O;
423
- nothing loads until you append, read, or stream.
424
-
425
- ```ts
426
- const session = server.session('order-42')
427
-
428
- session.id
429
- session.append(...events)
430
- session.schedule('expire-order', { delay: '5d' }, {
431
- type: 'expired',
432
- payload: {},
433
- })
434
- session.history({ gte, lte })
435
- session.state(reducer)
436
- session.stream({ startAfter })
437
- ```
438
-
439
- `session.id` is the id passed to `server.session(id)`. Root `append` takes only
440
- events. The handler-local form at `ctx.session.append` adds its required name
441
- before the events. Both root and handler sessions expose `schedule`.
442
-
443
- ### `server.drain(sessionId)`
444
-
445
- ```ts
446
- server.drain(sessionId: string): Promise<{ settled: boolean }>
447
- ```
448
-
449
- Claims every currently eligible event and runs their handlers concurrently.
450
- `settled` means no actionable or live-claimed work remains. A dead-lettered
451
- event can leave later work in its lane blocked while other lanes continue.
452
- You'll rarely call this yourself; it is the primitive scheduler callbacks use.
453
- The public result stays this simple boolean.
454
-
455
- ### `A2Store`
456
-
457
- Custom adapters implement these atomic drain methods:
458
-
459
- ```ts
460
- // Custom server store adapter:
461
- type EventCause = {
462
- index: number
463
- attempt: number
464
- batchSize?: number // present on named handler appends
465
- }
466
-
467
- type AppendEvent = {
468
- type: string
469
- payload: unknown
470
- id?: string
471
- cause?: EventCause
472
- lane?: string
473
- settled?: true // internal: no handler is registered for this event type
474
- }
475
-
476
- type ReturnedEvent = AppendEvent & { id: string }
477
-
478
- type StoredEvent = Event & {
479
- cause: EventCause | null
480
- lane: string | null
481
- processedAt: Date | null
482
- processedByAttempt: number | null
483
- returnedEventIds: string[] | null
484
- firstClaimedAt: Date | null
485
- lastClaimedAt: Date | null
486
- attemptCount: number
487
- claimHolder: string | null
488
- claimExpiresAt: Date | null
489
- failureCount: number
490
- lastFailedAt: Date | null
491
- lastFailedAttempt: number | null
492
- lastError: string | null
493
- failedAt: Date | null
494
- }
495
-
496
- type StoreAppendResult = {
497
- events: StoredEvent[]
498
- hasPending: boolean
499
- }
500
-
501
- type StoreClaimAvailableResult =
502
- | { outcome: 'claimed'; events: StoredEvent[] }
503
- | { outcome: 'busy'; retryAt: Date }
504
- | { outcome: 'settled' }
505
-
506
- type CompleteAttemptResult =
507
- | { outcome: 'completed'; events: StoredEvent[] }
508
- | { outcome: 'superseded' }
509
-
510
- interface A2Store {
511
- append(
512
- sessionId: string,
513
- events: AppendEvent[],
514
- ): Promise<StoreAppendResult>
515
-
516
- claimAvailable(options: {
517
- sessionId: string
518
- holder: string
519
- ttlMs: number
520
- expiresAtMs?: number
521
- excludeIndexes?: readonly number[]
522
- }): Promise<StoreClaimAvailableResult>
523
-
524
- renewClaims(options: {
525
- sessionId: string
526
- holder: string
527
- claims: readonly { index: number; attempt: number }[]
528
- ttlMs: number
529
- expiresAtMs?: number
530
- }): Promise<{ renewed: number[]; superseded: number[] }>
531
-
532
- completeAttempt(options: {
533
- sessionId: string
534
- index: number
535
- attempt: number
536
- events: ReturnedEvent[]
537
- }): Promise<CompleteAttemptResult>
538
-
539
- failAttempt(options: {
540
- sessionId: string
541
- index: number
542
- attempt: number
543
- error: string
544
- maxFailures: number
545
- }): Promise<{
546
- outcome: 'failed' | 'dead_lettered' | 'superseded'
547
- failureCount: number
548
- }>
549
- }
550
- ```
551
-
552
- | Method | Atomic effect |
553
- | --- | --- |
554
- | `append` | Write a consecutive batch. Events carrying core's internal `settled` flag receive `processedAt` in the same transaction, with no dispatch attempt. An event carrying `cause` is fenced by attempt currency: it commits only while `cause.attempt` is still the parent's latest attempt and the parent is not dead-lettered, throwing `SUPERSEDED_ATTEMPT` otherwise (the check serializes against a concurrent claim; the idempotent-replay path runs first). |
555
- | `claimAvailable` | Claim every eligible event. All unlaned events are independent; only the lowest-index unfinished event in each lane is eligible. A claim records its holder, expiry, timestamps, and next `attemptCount`. |
556
- | `renewClaims` | Extend the listed live claims still owned by the holder. `renewed` lists the claims that remain owned; `superseded` lists the claims whose event's `attemptCount` passed the listed attempt. An expired claim no successor took appears in neither: it may still complete. Renewal never revives an expired claim. |
557
- | `completeAttempt` | Fence on the current attempt, mark the parent processed, store its exact ordered `returnedEventIds`, and append the returned batch in the same transaction. A same-attempt retry returns the committed children; a stale attempt returns `superseded`. |
558
- | `failAttempt` | Record one current caught failure, clear its claim, and dead-letter at `maxFailures`. A repeated failure acknowledgment is idempotent; stale attempts return `superseded`. |
559
-
560
- `attemptCount` counts claims, including abandoned ones. `failureCount` counts
561
- caught failures and dead-letters at ten. See
562
- [Durability](/concepts/durability#one-event-many-attempts) for the recovery
563
- model. `cause` identifies the event and handler attempt whose
564
- `ctx.session.append` or return value first persisted the child. A null cause
565
- means a top-level event.
566
- `lane` is the session-scoped serialized group resolved before append.
567
- `returnedEventIds` makes a lost completion
568
- acknowledgment recoverable without accepting a partial child batch. Lifecycle
569
- timestamps are adapter clock values for their atomic store operations, not exact
570
- database commit times.
571
- Built-in adapters persist these fields inside their existing atomic operations,
572
- with no extra backend round trip.
573
-
574
- ### `A2Scheduler`
575
-
576
- Custom scheduler adapters implement two methods:
577
-
578
- | Method | Effect |
579
- | --- | --- |
580
- | `schedule(task: SchedulerTask): Promise<void>` | put one versioned task on durable delivery infrastructure |
581
- | `handler(...servers)` | return the route that receives drain and append tasks |
582
-
583
- Application routes call `scheduler.handler(...servers)` on the configured
584
- adapter. The handler requires at least one drainable server and rejects
585
- duplicate contract names synchronously.
586
-
587
- `SchedulerTask` is `SchedulerDrainTask | SchedulerAppendTask`. These task types,
588
- plus `ScheduledEvent`, are exported from `experimental-a2/server`.
589
-
590
- | Task | Fields |
591
- | --- | --- |
592
- | `SchedulerDrainTask` | `version: 1`, `kind: 'drain'`, `contract`, `sessionId`, required epoch-ms `dueAt` |
593
- | `SchedulerAppendTask` | `version: 1`, `kind: 'append'`, stable `id`, `contract`, `sessionId`, required epoch-ms `dueAt`, readonly `events` |
594
- | `ScheduledEvent` | fixed `id`, `type`, and JSON `payload` |
595
-
596
- Drain tasks wake pending work and carry no application payload. Append tasks
597
- carry the events for `session.schedule`.
598
-
599
- Custom adapters deliver an authenticated, normalized append task through the
600
- server helper:
601
-
602
- ```ts custom-scheduler.ts
603
- import {
604
- deliverSchedulerAppend,
605
- type DrainableServer,
606
- type SchedulerAppendTask,
607
- } from 'experimental-a2/server'
608
-
609
- export async function deliver(
610
- server: DrainableServer,
611
- task: SchedulerAppendTask,
612
- ) {
613
- await deliverSchedulerAppend(server, task)
614
- }
615
- ```
616
-
617
- Import `deliverSchedulerAppend` from `experimental-a2/server`. It verifies the
618
- task and server contract match, revalidates the carried event inputs, and uses
619
- the ordinary top-level append path. The target server must have a scheduler.
620
- When the append leaves pending handler work, the helper resolves only after a
621
- recovery watchdog is durable; a send failure keeps the provider delivery
622
- retryable. Authenticate the transport and validate the task envelope before
623
- calling it.
624
-
625
- ## Session
626
-
627
- ### `session.append(...events)`
628
-
629
- ```ts
630
- session.append(
631
- ...events: Array<{ type: string; payload: unknown; id?: string }>
632
- ): Promise<Event[]>
633
- ```
634
-
635
- The only way to move a session forward. Payloads are validated against
636
- the contract's schemas before anything is written. A multi-event append
637
- is atomic: all-or-nothing, consecutive positions, one transaction. Pass
638
- `id` to make an append idempotent across retries; re-sending an
639
- identical batch returns the original rows. (Events that arrived through
640
- `server.fetch`'s push lane are accepted directly.) See
641
- [Durability](/concepts/durability).
642
-
643
- After the write, A2 dispatches only event types with registered handlers.
644
- Other event types are already settled by the append itself. An unhandled
645
- append still starts session healing when older handled work is pending. The
646
- store reports that session-wide pending state as part of the atomic append, so
647
- this decision needs no follow-up read.
648
-
649
- ### `session.append.dispatch(...events)`
650
-
651
- ```ts
652
- session.append.dispatch(
653
- ...events: Array<{ type: string; payload: unknown; id?: string }>
654
- ): Promise<Event[]>
655
- ```
656
-
657
- Commits the same atomic batch, but sends pending work directly to configured
658
- scheduler instead of starting an inline drain. The call awaits acceptance of an
659
- immediate scheduler message. It throws before writing if the server has no
660
- scheduler adapter.
661
-
662
- If the queue send fails, the events are already durable. Give them explicit
663
- IDs and retry the same dispatch safely. A batch that leaves no pending handler
664
- work does not send a scheduler message. Dispatch chooses how this append wakes
665
- the session; it does not reserve events for one worker. A drain that is already
666
- active may still claim newly eligible work first.
667
-
668
- `dispatch` exists only on a server session's top-level append. Handler-scoped
669
- `ctx.session.append(name, ...events)` already runs inside an active drain and
670
- does not expose `dispatch`. Its immediate children and atomically returned
671
- children become eligible in that drain.
672
-
673
- ### `session.schedule(name, timing, ...events)`
674
-
675
- `schedule` returns `Promise<void>` and is available on both root and handler
676
- sessions. `name` must be non-empty. At least one event is required.
677
- `ScheduleDelay`, `ScheduleTiming`, and `SessionSchedule` are exported from
678
- `experimental-a2/server`.
679
-
680
- | Timing | Meaning |
681
- | --- | --- |
682
- | `{ delay: '30s' }` | relative delay with unit `ms`, `s`, `m`, `h`, or `d` |
683
- | `{ at: Date }` | absolute delivery time |
684
-
685
- The template type checks the unit suffix. At runtime, the delay number must be
686
- an unsigned base-10 decimal without leading zeros, finite, and greater than
687
- zero. `0s`, signs, exponent notation, malformed durations, invalid dates, and
688
- objects containing both timing fields are rejected. A root relative delay
689
- starts from that call's wall clock. A handler relative delay starts from the
690
- triggering event's durable `createdAt`, so retries preserve its delivery time.
691
- Use `{ at }` when a retried root call needs one target time. The requested
692
- target is the earliest useful delivery time. Adapters encode provider timing
693
- in whole-second slots. An already-due target is sent immediately.
694
-
695
- The stable task id derives from the contract, session, and name. Handler tasks
696
- also include the triggering event id. Omitted event ids derive from that task
697
- id and event position; explicit ids win. Reuse a name only for an identical
698
- timing and event batch. Provider deduplication is an optimization. Stable event
699
- ids make later duplicate deliveries converge in the store.
700
-
701
- A2 snapshots payloads into plain JSON trees, then validates the event schemas
702
- before sending. Encode dates as ISO strings and custom classes as plain data;
703
- values such as `undefined`, bigint, functions, symbols, cycles, accessors, and
704
- non-plain prototypes are rejected. The promise resolves after provider
705
- acceptance, not after append. Shared object references become independent JSON
706
- values. The task carries the snapshotted JSON schema input. At delivery, the
707
- ordinary top-level append validates that input, so a schema
708
- transform produces the stored payload from the original input. The events have
709
- `cause: null` and enter normal handlers, lanes, state, and streams.
710
-
711
- There is no cancellation, reschedule, cron, or timer-listing API. See
712
- [Timers and delays](/guides/timers) for stale-timer guards, provider limits,
713
- and deployment compatibility.
714
-
715
- ### `session.history()`
716
-
717
- ```ts
718
- session.history(options?: { gte?: number; lte?: number }): Promise<Event[]>
719
- ```
720
-
721
- Raw events from the session, oldest first. `gte` and `lte` are inclusive event
722
- indexes; omit a bound to leave that end of the log open.
723
- Bounds are non-negative safe integers. `lte: 0` returns `[]`; `gte > lte` throws.
724
-
725
- Redis reads a fixed committed range in pages of at most 256 events, with up to
726
- sixteen page reads in flight. Public history omits dispatch metadata. PostgreSQL
727
- uses one indexed range query selecting only public event columns. Custom store
728
- adapters can implement `readHistory(sessionId, options)` with the same exclusive
729
- `afterIndex` and inclusive `throughIndex` bounds as `read()`. A2 falls back to
730
- `read()` when that capability is absent.
731
-
732
- ### `session.state(reducer, options?)`
733
-
734
- ```ts
735
- session.state(
736
- reducer: Reducer<S>,
737
- options?: { through?: number | 'latest' },
738
- ): Promise<{ state: S; index: number }>
739
- ```
740
-
741
- The log folded through one committed prefix. `index` is its exact boundary,
742
- an append-order cursor rather than a handler-settlement marker. Hand it to the
743
- client to resume there. Snapshot write-back uses platform `waitUntil`; this
744
- read never dispatches handlers or waits for pending work.
745
-
746
- On a root session from `server.session(id)`, omitting `through` reads the latest
747
- committed frontier. On the handler-scoped `ctx.session`, omitting it reads
748
- through `ctx.event.index`. That boundary is inclusive and stable across retries.
749
- Use `{ through: 'latest' }` inside a handler only when later committed events
750
- are intentionally part of the decision. A numeric `through` is an inclusive,
751
- non-negative safe integer; `through: 0` returns the reducer's initial state.
752
-
753
- The default handler read and numeric handler reads automatically retain their
754
- exact folded checkpoint while the triggering event is unfinished. Completion
755
- or dead-lettering releases the reference and collects the checkpoint when no
756
- other unfinished handler uses it. There are no retention options. Handler
757
- `{ through: 'latest' }` reads and all root reads do not retain history.
758
-
759
- If the latest snapshot is ahead of a numeric boundary, A2 can use the greatest
760
- eligible retained checkpoint. A root historical read never creates retention
761
- or writes a stale checkpoint behind the latest snapshot; it folds from the log
762
- when no eligible checkpoint exists.
763
-
764
- Operational snapshot-read failures preserve existing A2 errors; other errors
765
- become `STORE_UNAVAILABLE` with the original error as the cause. They do not retry through individual
766
- reads or raw history. Cache misses and `stateSchema` rejection still rebuild
767
- from the log. Snapshot write-back remains background work; failures are reported
768
- through `a2.snapshot` telemetry or the default console sink, without failing the
769
- completed read or attaching snapshot contents.
770
-
771
- ### `session.stream(options?)`
772
-
773
- ```ts
774
- session.stream(options?: { startAfter?: number }): AsyncIterable<Event>
775
- session.stream(options: {
776
- startAfter?: number
777
- presence: true
778
- }): AsyncIterable<Event | PresencePatch | PresenceSnapshot>
779
- ```
780
-
781
- A live feed of the session's events. `startAfter` is a non-negative safe integer;
782
- `startAfter: 20` begins with event 21.
783
- Server-side only; `server.fetch` exposes it over SSE and WebSocket. Subscribing
784
- never dispatches handlers.
785
-
786
- With `presence: true`, the feed yields one `PresenceSnapshot` first:
787
- `{ snapshot }`, the current pruned map with each field's own `value`,
788
- `seen`, and `at` stamp. Live presence patches then interleave with
789
- events. The stream response sends both as named SSE frames, so clients
790
- that don't know them skip them. The return type widens only under the
791
- literal `presence: true`; without it, existing consumers keep
792
- `AsyncIterable<Event>`. The option itself exists only on sessions of
793
- contracts that declare `presence`; elsewhere it is a type error, not a
794
- runtime throw. A `PresencePatch` is `{ participant, values, seen, at }`
795
- with `values` typed by the contract's presence schemas (`null` marks a
796
- cleared field).
797
-
798
- ### `session.setPresence(patch)`
799
-
800
- ```ts
801
- session.setPresence(patch: {
802
- participant: string
803
- values: { [field]: payload | null } // partial; null clears a field
804
- seen?: number
805
- at?: number // the sender's LWW stamp, epoch ms; receipt time when absent
806
- }): Promise<void>
807
- ```
808
-
809
- Validates each field against the contract's `presence` schemas, then
810
- broadcasts. Last writer wins per participant per field, ordered by
811
- `at`, the sender's stamp in epoch milliseconds (the browser client
812
- stamps its sends automatically). When `at` is absent, receipt time
813
- stands in: a stampless caller is a single writer, so receipt order is
814
- its send order. No log row, no
815
- dispatch, no scheduler arm; `history()` and `state()` never see it. A
816
- bad payload throws `INVALID_PAYLOAD`; an unknown field throws
817
- `UNKNOWN_PRESENCE_FIELD`; a non-finite or negative `at` throws a
818
- `TypeError`. `seen` is the sender's stream frontier, the
819
- stamp view code uses to reconcile presence against the fold (see
820
- [Presence](/guides/presence#reconciling-the-planes-seen)). Values expire
821
- by backend TTL when a participant goes silent (default 60 seconds; set
822
- `createServer({ presence: { ttlMs } })` to change it). Expiry counts on
823
- the storage clock, not the sender stamp, so a hostile stamp can only
824
- vandalize its own field and still expires on schedule.
825
-
826
- The patch is structurally the `presence` sibling of the push envelope;
827
- `server.fetch`'s push lane forwards it whole to `session.setPresence(presence)`.
828
- The API mirrors the wire.
829
-
830
- The handler-scoped form `ctx.session.setPresence(...)` is the same
831
- operation; `seen` defaults to the triggering event's `index`.
832
-
833
- ### `session.presence()`
834
-
835
- ```ts
836
- session.presence(): Promise<{
837
- [participant: string]: {
838
- [field: string]: { value: unknown; seen: number; at: Date }
839
- }
840
- }>
841
- ```
842
-
843
- The current presence map, expired values pruned. A point-in-time read
844
- for server renders; live consumers use `stream({ presence: true })`.
845
- Each entry's `at` is the sender's stamp for when the value was set,
846
- the last-writer-wins comparator; how long a value stays painted is
847
- still view logic, decided at render time.
848
-
849
- ## Event
850
-
851
- ```ts
852
- type Event = {
853
- id: string
854
- type: string
855
- payload: unknown // typed by the contract's schema for `type`
856
- index: number // position in the session's log, from 1
857
- sessionId: string
858
- createdAt: Date
859
- }
860
- ```
861
-
862
- ## `experimental-a2/react`
863
-
864
- ### `createReact(options)`
865
-
866
- ```ts
867
- createReact(options: {
868
- client: A2Client // bind a shared client: api + reducer + session identity
869
- }): { SessionProvider, useSession }
870
-
871
- // or, standalone (one client per provider mount):
872
- createReact(options: {
873
- reducer: Reducer // types everything: state, pushes, the feed
874
- cache?: A2ClientCache // optional; see local-first
875
- }): { SessionProvider, useSession }
876
- ```
877
-
878
- A factory, like `createContext`: call it once in a `'use client'`
879
- module, export the bound pair. Server components import
880
- `SessionProvider` from that module as a client reference; client
881
- components import `useSession` from the same file. See
882
- [Live UI](/guides/react#the-session-module).
883
-
884
- `createReact({ reducer })` is also supported for provider-bound clients. Use a
885
- shared `A2Client` when code outside the provider needs to push before a route
886
- transition and the destination should adopt the same optimistic session.
887
-
888
- ### `<SessionProvider>`
889
-
890
- | Prop | What it is |
891
- | --------------- | ------------------------------------------------------ |
892
- | `sessionId` | which session to subscribe to |
893
- | `initialState` | server-rendered state |
894
- | `initialIndex` | the fold's frontier, where a new session starts streaming |
895
- | `initialEvents` | optional earlier raw events for a history UI |
896
- | `participant` | overrides the client's `participant`; one of the two is required to call `setPresence` |
897
-
898
- Opens the stream on mount, closes it on unmount, reconnects with
899
- backoff from its last streamed event. `participant` binds at the session's
900
- first resolution; a changed provider prop is ignored for the session's
901
- runtime lifetime.
902
-
903
- `initialState` and `initialIndex` are the complete server-to-client handoff.
904
- For a new session, the stream starts after `initialIndex`, including events
905
- appended between the server render and the connection. `initialEvents` only preloads the raw
906
- `events` feed for a UI that needs entries from before that frontier. It does
907
- not affect state hydration or stream resumption. Later snapshots advance
908
- state while preserving the stream's resume position, so delayed events
909
- still enter the feed without folding into state again.
910
-
911
- ### `useSession()`
912
-
913
- ```ts
914
- const { state, push, reconnect, events, index, loadHistory, history, connection,
915
- presence, setPresence } = useSession()
916
- ```
917
-
918
- `state` starts from the initial snapshot and folds live events through the
919
- shared reducer. `events` is the raw event feed: observed, explicitly
920
- seeded, or backscrolled. Unless `initialEvents` seeds earlier entries or
921
- `loadHistory` fetches them, it begins after `initialIndex`. `index` is
922
- the latest server-confirmed state index, the `lastSeenIndex` for
923
- [cancellation](/guides/cancellation).
924
-
925
- `loadHistory({ before?, limit? })` backscrolls: it fetches a bounded
926
- slice of the log from below the frontier (the same route, `gte`/`lte`
927
- query parameters) and merges it into `events`, deduped, ordered, and
928
- shared across every handle of the session. It resolves with the events
929
- in the requested range. Defaults walk backward 50 at a time from the
930
- oldest loaded event; `before` is an exclusive upper bound. After a
931
- newer snapshot hydrates the session, default paging still continues from
932
- the oldest loaded event. Pass an explicit `before` to read another range.
933
- It never touches `state` or the optimistic overlay. Calls
934
- serialize per session and already-loaded ranges are not refetched.
935
- `history` is the progress: `{ loading, complete, oldestLoaded }`, where
936
- `complete` means the feed reaches index 1 (or the log is empty). The `ws` api has no history lane; there `loadHistory`
937
- throws a `TypeError`.
938
-
939
- `presence` is the replicated ephemeral map,
940
- `Record<participantId, { [field]: { value, seen, at } }>`, including
941
- this client. `setPresence(values)` is fire-and-forget: validated
942
- locally, merged into the local map immediately, coalesced on the wire
943
- to a fixed cadence (a leading send immediately, then at most one
944
- merged send per interval; later values win per field), stamped with
945
- the frontier at send time as `seen`. Parallel sends are safe: merges
946
- are last-writer-wins by `at`, and the client's stamps are monotonic
947
- per session (wall-seeded, guarded so overlapping sends can never
948
- invert). No ack, no `confirmed`, no retry.
949
-
950
- Both members exist only when the contract declares `presence`; their
951
- value and field types come from its schemas, through the reducer, with
952
- no type arguments (the reducer is the client's typed handle on the
953
- contract; it still never folds presence). `setPresence` requires a
954
- `participant`: the client's default, or the provider's override. See
955
- [Presence](/guides/presence).
956
-
957
- `push` appends optimistically: validated locally, rolled back on
958
- rejection, retried for `STORE_UNAVAILABLE` and transport failures. Awaiting it gives the
959
- server ack; the same result carries `confirmed`, a lazy promise for
960
- the later moment when the live stream has delivered the batch back and
961
- the view shows server truth:
962
-
963
- ```ts
964
- const t0 = performance.now()
965
- const result = push({ type: 'created', payload })
966
- await result // the server ack: push→ack latency
967
- await result.confirmed // the stream round-trip: ack→stream latency
968
- const roundtripMs = performance.now() - t0
969
- ```
970
-
971
- Overlapping calls through the same session client update the view
972
- immediately, then enter the transport in invocation order. Each call waits
973
- only for the previous call's ack or final rejection, not for `confirmed`.
974
- Other session IDs and clients remain concurrent; they converge on the log's
975
- durable order.
976
-
977
- A rejected push rejects both promises with the same `A2ClientError`;
978
- `confirmed` is materialized only when accessed, so ignoring it costs
979
- nothing. It also reads as intent: `await push(...).confirmed` is
980
- "continue once this is server truth".
981
-
982
- `session.reconnect(): void` immediately restarts a leased subscription,
983
- including one paused by the reconnection policy,
984
- from its current cursor. It preserves state, pending writes, and leases, and
985
- does nothing without an active lease. The React `useSession()` result exposes
986
- the same `reconnect` function. Use it after a custom REST mutation; it requests
987
- a restart rather than returning a promise for catch-up completion. A
988
- multiplexed WebSocket keeps its physical socket and other session lanes;
989
- a single-session WebSocket replaces its socket.
990
-
991
- `connection` is a discriminated union; impossible states are
992
- unrepresentable (an `error` only exists while disconnected):
993
-
994
- ```ts
995
- // in a client module:
996
- import type { A2ClientError } from 'experimental-a2/client'
997
-
998
- type Connection =
999
- | { status: 'idle' }
1000
- | { status: 'connecting'; reconnects: number; error: A2ClientError | null }
1001
- | { status: 'paused'; reconnects: number; error: A2ClientError | null }
1002
- | { status: 'live'; reconnects: number }
1003
- | { status: 'closed' }
1004
- ```
1005
-
1006
- "Reconnecting…" is `status === 'connecting' && reconnects > 0`. And
1007
- it's honest about silence: the server heartbeats the stream (`: ping`
1008
- every 15s), and a client that hears nothing for two beats treats the
1009
- connection as dead (aborts it and reconnects), so `live` means bytes
1010
- are actually flowing, not "the socket hasn't errored yet". See
1011
- [Live UI](/guides/react).
1012
-
1013
- ### `useSession(client, sessionId, options?)`
1014
-
1015
- ```ts
1016
- useSession(client: A2Client, sessionId: string, options?: {
1017
- participant?: string // overrides the client's participant
1018
- hydrate?: { state: S; index: number }
1019
- }): UseSessionResult
1020
- ```
1021
-
1022
- The standalone, provider-less hook. It returns the same result shape
1023
- as the bound hook, for apps whose components reach sessions ad hoc (a
1024
- sidebar of channel sessions, a user session read from a menu). The
1025
- handle is identity-mapped: every hook and provider mounting the same
1026
- session shares one runtime. The stream is refcounted: the first mount
1027
- connects, the last unmount closes (StrictMode-safe).
1028
-
1029
- `hydrate` is the hydration input, one atomic option: the
1030
- `{ state, index }` pair `session.state(reducer)` returns, fetched by
1031
- your app through its own route and handed over whenever it lands.
1032
- While it is `undefined`, the hook holds the stream (connecting without
1033
- a fold would replay the whole log). There is no pending flag on the
1034
- result: whether the fold has been handed over is your own input, so
1035
- your data layer's loading state is the pending state.
1036
- When it arrives, the session hydrates under the usual
1037
- never-move-backward rule and the stream connects at that frontier.
1038
- For a deliberate full replay, hand the fold's true starting point:
1039
- `{ state: reducer.initialState, index: 0 }`. State at index 0 is the
1040
- reducer's seed by definition, so the explicit replay needs no special
1041
- vocabulary. See
1042
- [Client-first sessions](/guides/react#client-first-sessions).
1043
-
1044
- ## `experimental-a2/ai`
1045
-
1046
- ### `agent(options)`
1047
-
1048
- ```ts
1049
- agent(options: {
1050
- name: string
1051
- events?: Record<string, StandardSchemaV1>
1052
- messageSchema?: StandardSchemaV1<UIMessage>
1053
- reducerName?: string
1054
- }): AgentDefinition
1055
- ```
1056
-
1057
- Defines an isomorphic agent as ordinary A2 parts. Its `contract` contains the
1058
- built-in AI protocol plus `events`, and its `reducer` projects `AIState`. The
1059
- definition exposes only `contract` and `reducer`.
1060
-
1061
- Built-in event names cannot be replaced by `options.events`.
1062
-
1063
- ### `handlerContext(agent)`
1064
-
1065
- Reads the durable A2 handler context ambient to the current tool
1066
- execution. Tools are plain AI SDK `tool()` definitions; A2 passes their
1067
- options through to the AI SDK without interpreting them and runs each
1068
- local `execute` inside an ambient context scope. Call
1069
- `handlerContext(agent)` (or pass a contract) anywhere below `execute`,
1070
- including awaited helpers and async iteration. The argument types the
1071
- result and is verified against the executing server's contract: a
1072
- mismatched agent, or a call outside any tool execution, throws a
1073
- `TypeError`.
1074
-
1075
- The returned `AgentToolContext<D>` supplies the current A2 handler's `event`,
1076
- `attempt`, `session`, and `signal`. Local tools execute the explicit
1077
- `ai.tool.execution.requested` work event. Its payload contains the admitted
1078
- `call`, `generation`, `turnId`, and execution `version`. The session has the
1079
- supplied agent's complete event vocabulary. Calls such as
1080
- `ctx.session.schedule()` use the scheduler of the server executing the tool.
1081
-
1082
- The context is runtime capability, not event data. A2 creates it for the
1083
- current attempt, never persists it, and never sends it to the model or a
1084
- provider-executed tool. Each retry reads that attempt's fresh context.
1085
- Model-phase tool options (`contextSchema`, dynamic descriptions,
1086
- `needsApproval` predicates, input lifecycle callbacks) are the AI SDK's
1087
- own; A2 forwards them untouched, and durable execution passes no AI SDK
1088
- `context` option. `needsApproval` and `generation.toolApproval`
1089
- predicates run during the model step and are re-evaluated on recovery
1090
- for the same durable tool call, so keep them deterministic over the tool
1091
- input. Model-phase hooks see tool definitions without local `execute`
1092
- functions.
1093
-
1094
- ### `inputs`
1095
-
1096
- Pure typed inputs for `append()` and `push()`:
1097
-
1098
- | Input | Events |
1099
- | --- | --- |
1100
- | `inputs.message(message, { generate? })` | queues input; `generate: false` records passive context at admission |
1101
- | `inputs.seed(message)` | queues passive context; non-user roles require a trusted append |
1102
- | `inputs.approval(response)` | answers a pending tool approval |
1103
- | `inputs.input(response)` | answers a pending application input |
1104
- | `inputs.requestInput(request)` | requests application input from trusted server code |
1105
- | `inputs.retry(options)` | retries a failed response |
1106
- | `inputs.stop({ turnId, lastSeenIndex? })` | ends the observed turn and advances when admission is open |
1107
- | `inputs.steer({ turnId, message, lastSeenIndex? })` | puts the new input first for a handoff after the observed turn's current step |
1108
- | `inputs.queue.edit({ message })` | changes a pending input; its ID stays fixed |
1109
- | `inputs.queue.move({ inputId, beforeId })` | moves pending input before an ID; `null` means the end |
1110
- | `inputs.queue.remove({ inputId })` | removes pending input; its submission stays in the log |
1111
- | `inputs.queue.sendNow({ inputId, turnId, lastSeenIndex? })` | promotes an existing queued input for a handoff after the current step; preserves pause |
1112
- | `inputs.pause({ when: 'now' \| 'after-turn' })` | pauses execution now or inbox admission after the turn |
1113
- | `inputs.resume()` | resumes retained work, then inbox admission |
1114
- | `inputs.toolResult(result)` | records a deferred provider-executed tool result from trusted server code |
1115
- | `inputs.interrupt(options)` | interrupts a specific generation/request owner |
1116
-
1117
- Edits are last-write-wins in session log order and preserve the pending message's
1118
- identity and role. They require no revision or confirmation before another edit.
1119
- Admission freezes the message; subsequent edits return `not-pending`.
1120
-
1121
- All builders emit `ai.control.requested`. Persistence and application are
1122
- separate: `ai.control.decided` returns `{ commandId, outcome, reason? }` through
1123
- the existing stream. Outcomes are `applied` or `rejected`. Rejection reasons
1124
- include `not-pending`, `role-mismatch`, `stale-turn`, `not-found`,
1125
- `not-user-input`, and `closed`. `state.receipt` holds the latest user receipt.
1126
-
1127
- Steering validates and prioritizes new input even if the observed turn has
1128
- finished. A matching active turn finishes its current model response and tool
1129
- batch, then yields before another model step. Running local workers and deferred
1130
- provider results settle first; unanswered human approval/input waits can be
1131
- closed at that point. Pause stays closed. Otherwise the input starts when idle
1132
- or waits first behind a different active turn. The priority change is atomic;
1133
- the later handoff and new work are committed atomically at the step boundary.
1134
- Steer and Send now do not use `lastSeenIndex` to cut output.
1135
-
1136
- Queue removal leaves the original submission in the log, so replay does not recreate the input.
1137
- Send now preserves the queued input ID and latest accepted content. It requires
1138
- an observed `turnId` (or null for idle), preserves pause, and only accepts
1139
- user-role input. It promotes passive user input to generation. Missing targets
1140
- and stale turns reject before any queue change.
1141
-
1142
- `state.inbox` contains `paused` and ordered `items`. Each item contains
1143
- `{ id, message, generate, afterStepOf? }`. The optional turn ID
1144
- `afterStepOf` records the observed steering turn while that item remains queued;
1145
- a matching active turn hands off after its current step.
1146
- Removal cancels it and admission consumes it. `state.active` is null or contains
1147
- `{ turnId, input, phase }`; admission freezes `input`. On the server, pending
1148
- inputs are absent from `messages` and model context. The `paused` status and the `pausing`/`paused`
1149
- active phases represent retained work. Resume uses a fresh model request and
1150
- execution version. Tools without terminal outcomes can retry; their stable
1151
- `toolCallId` supports external idempotency.
1152
-
1153
- `inputs` deliberately has no session lifecycle methods. Append explicit
1154
- `ai.session.created` and `ai.session.closed` events from trusted server code
1155
- when an application uses them. Message, seed, and steering builders set the event ID to the input message ID.
1156
- The two IDs must match for all new-input commands. Caller command/input IDs must
1157
- be nonempty and cannot use the reserved `a2.ai:` prefix. Identical events replay;
1158
- conflicting ID reuse rejects before append under core deduplication rules. IDs
1159
- are unique across the store, including different contracts and sessions.
1160
- Approval, input-response, and retry builders use stable interaction IDs.
1161
- Other commands use IDs assigned by `push()` or `append()`; transport retries
1162
- preserve those IDs. Browser
1163
- ingress accepts user messages, approval and input responses, interruptions,
1164
- and explicit retries. Only built-in server handlers append generation requests
1165
- and AI lifecycle events. `inputs.requestInput()`, `inputs.toolResult()`, and non-user messages built
1166
- with `inputs.seed()` are for trusted server appends.
1167
-
1168
- `inputs.message(message, { generate: false })` is valid browser input. It admits
1169
- the user message to `AIState.messages` without starting a model turn. The next
1170
- user message that allows generation includes passive user messages before it
1171
- in model context. Passive user messages after that trigger wait for a later
1172
- generating message. Omitting the option preserves the default scheduling
1173
- behavior. `inputs.seed()` writes
1174
- the same passive flag. Trusted assistant and system seeds are context, not user
1175
- queue cutpoints. The server never schedules them, regardless of the `generate`
1176
- field. Model output is generation progress rather than another
1177
- `ai.message.created` fact, so an assistant response cannot recursively start a
1178
- new turn.
1179
-
1180
- Approval and input request/response payloads require the active
1181
- `requestId` and `generationId`. Clients copy both from the pending request, which prevents a
1182
- delayed response from satisfying a newer model step.
1183
-
1184
- `ai.retry.requested` carries `{ messageId, responseMessageId, retryId }`.
1185
- `retryId` identifies one user action. The server converts the fact into an
1186
- `ai.generation.requested` whose reason is `retry`.
1187
- Retry reruns the failed model call while retaining earlier completed model steps,
1188
- their tool results, and committed compactions, including steps completed after
1189
- the latest compaction.
1190
- The failed model call's uncommitted step is removed from the active prompt and
1191
- response. Conversation forks and edit-and-resend flows for admitted messages are
1192
- application-owned.
1193
-
1194
- A2-generated requests, generations, assistant responses, worker jobs, reports,
1195
- and lifecycle events have fixed-length deterministic IDs in the reserved
1196
- `a2.ai:` namespace. An assistant message's ID equals its initial model-request
1197
- event ID. Control state keeps no lifetime input/response-ID registry. IDs are opaque;
1198
- ownership is carried by event fields. `ai.generation.requested` requires
1199
- `responseMessageId`; a tool continuation also requires `sourceGenerationId`.
1200
-
1201
- `ai.generation.failed` sets `stepLimit: true` when `maxSteps` rejects a
1202
- continuation before another model step starts.
1203
-
1204
- `ai.message.interrupted` carries either `{ messageId, requestId, reason?,
1205
- lastSeenIndex? }` or `{ messageId, generationId, reason?, lastSeenIndex? }`.
1206
- Exactly one owner ID is required. Clients copy `requestId` from
1207
- `activeRequestId` before generation start and switch to the observed
1208
- `activeGeneration.generationId` after start. A request-owned interruption
1209
- remains valid if that same request starts before the append commits. A delayed
1210
- interruption for an older owner is a projection and cancellation no-op.
1211
- The message and owner IDs are validated as one pair. If either ID does not
1212
- identify that same response owner, the interruption is stale and is a no-op.
1213
-
1214
- An accepted interruption terminally fences its request or generation. Later
1215
- generation, tool, approval, input, and compaction events remain in raw history
1216
- but cannot alter `AIState`, satisfy pending work, or reactivate the response.
1217
- Completed tool results at or before `lastSeenIndex` remain completed. A final
1218
- tool result that races an ordinary generation failure remains authoritative in
1219
- either commit order, while the generation stays failed.
1220
-
1221
- Interruption and failure projections discard tool parts still in
1222
- `input-streaming`. These argument drafts are not completed tool calls and do not
1223
- enter later model prompts. Raw progress remains in the log. Available inputs,
1224
- approval-waiting calls, and completed results retain their existing terminal
1225
- handling.
1226
-
1227
- ### `events` and `createEvents(options?)`
1228
-
1229
- The built-in Standard Schema definitions. Use `events` for the default AI SDK
1230
- `UIMessage`; use `createEvents({ messageSchema })` to validate a more specific
1231
- message type. Both are isomorphic.
1232
-
1233
- ### Optimistic AI client state
1234
-
1235
- Clients using `agent().reducer` or `createReducer()` expose `state.messages`,
1236
- `state.inbox`, and `state.starting` with pending send, edit, move, remove,
1237
- Send now, steer, pause, resume, and Stop commands applied for display. Idle, unpaused
1238
- sends appear in the conversation immediately. A generating input occupies the
1239
- optimistic turn, so subsequent sends queue. Busy or paused sessions queue new
1240
- sends. `starting` indicates a speculative turn awaiting acceptance; it does not
1241
- indicate model execution. Confirmed turns take precedence during reconciliation.
1242
- Steering inputs, and Send now inputs targeting an active turn, appear in
1243
- `state.messages` after the current response and are omitted from the client
1244
- inbox. This display survives receipt and hydration, including while paused.
1245
- They remain pending server inputs until admission and do not enter the model
1246
- prompt early. Multiple steering inputs follow inbox admission order.
1247
-
1248
- New active turn identities come from accepted control decisions. A pending Stop
1249
- for that turn clears the displayed `state.active`, generation, and pending
1250
- approvals/inputs, and previews an idle status (or paused when the inbox is paused). With
1251
- `lastSeenIndex`, its retained generation projection freezes at the observed
1252
- frontier. The preview lasts through the request echo until the controller's
1253
- decision; rejected requests restore the confirmed active turn and output.
1254
- Server reads and reducer folds retain confirmed messages and inbox entries;
1255
- `state.starting` is false there. Custom reducers that call `reduceAIState()` or
1256
- nest the AI fold do not inherit the client display behavior. Repeat editing and
1257
- Send now remain available while earlier edits are pending. Later local edits
1258
- stay visible through earlier commands' acknowledgments and receipts.
1259
-
1260
- The pending-command ledger survives request echo and retires on the matching
1261
- `ai.control.decided` receipt or session close. Failed pushes roll back through
1262
- the client's existing optimistic overlay. This display adds no I/O and does not
1263
- read raw history.
1264
-
1265
- ### `createReducer(options)`
1266
-
1267
- ```ts
1268
- createReducer({ contract, name?, fold?, stateSchema? }): AIStoredReducer
1269
- ```
1270
-
1271
- Builds a normalized AI reducer for a compatible contract. Its `initialState`,
1272
- `fold`, and server `session.state(reducer)` use `AIStoredState`. Client and
1273
- React snapshots expose the inferred `AIState` view automatically.
1274
-
1275
- `reducer.view(storedState)` derives confirmed `AIState` for server code.
1276
- `reducer.fromView(state)` converts a confirmed view into stored state, including
1277
- when migrating an application-owned hydration value. Keep the stored state and
1278
- its log index together when hydrating; do not fold already-seen history twice.
1279
- `fold` can customize the confirmed `AIState` transition, and `stateSchema`
1280
- validates that view after restoration. Both apply only to the transcript.
1281
- Model context has its own built-in fold and schema. Changed custom fold semantics
1282
- still require a new `name`.
1283
-
1284
- Stored state is versioned, serializable data. Shared payload records and
1285
- inverse changes replace repeated tool values and complete recovery copies.
1286
- The reducer caches its live view and builds the records when the stored fields
1287
- are read or serialized. Treat state and views as immutable. No event or tool
1288
- payload is truncated. `AIState`
1289
- contains `inbox`, `active`, `starting`, the latest command `receipt`, session lifecycle,
1290
- messages, generation status, pending approvals
1291
- and input, tool activity, compaction, usage, the last error, and
1292
- `activeRequestId`, `activeRequestReason`, `activeRequestSourceGenerationId`, `activeResponseMessageId`,
1293
- `responseOwners: Record<string, { generationId: string; requestId: string; attempt: number }>`, and terminal request and
1294
- generation ownership fences.
1295
- `activeRequestId` is the server-authorized generation request and fences
1296
- delayed requests before their generation starts. `activeResponseMessageId`
1297
- identifies the requested response until `activeGeneration` exists.
1298
- `activeProjection` holds the indexed generation frontier while a generation is
1299
- active, after a generation step completes while its response waits on tool,
1300
- approval, or input barriers, while paused, and after a generation fails while it awaits retry
1301
- or interruption. Response completion, interruption, supersession, retry, or a
1302
- later generation clears or replaces it. `responseOwners` keeps the
1303
- latest generation owner for each response message, so late events from a
1304
- superseded owner cannot alter the projection. `terminalRequestIds` and
1305
- `terminalGenerations` preserve accepted completion, failure, interruption,
1306
- and supersession fences across snapshots and recovery. They are optional
1307
- snapshot-compatible fields with the shapes `Record<string, true>` and
1308
- `Record<string, 'completed' | 'failed' | 'interrupted' | 'superseded'>`.
1309
- The active projection also stores its prompt boundary and a serializable stream
1310
- cursor for open text, reasoning, and tool-input parts. Ordered progress applies
1311
- only its new chunks. `activeProjection.batches` stores indexed batches as
1312
- `{ length, tail, blocks }`: a bounded tail and shared completed blocks. Appending
1313
- ordered progress does not copy the full retained batch list. The blocks are
1314
- ordinary JSON with logarithmic nesting depth, so checkpoints can be serialized
1315
- and cloned. Retained batches support exact interruption cutoffs and out-of-order
1316
- reconstruction. The cursor and blocks share the active projection's lifetime.
1317
- Checkpoint size and serialization cost still grow with retained progress.
1318
- Tool-input deltas update `input-streaming` parts with best-effort partial JSON
1319
- for display. The state stays `input-streaming` until a completed input arrives;
1320
- partial parsing does not authorize execution or change interruption handling.
1321
- Extension events are ignored. The default reducer name is `a2.ai.state.v22:records-v2`.
1322
-
1323
- ### `deriveUIMessages(history)` and `reduceAIState(state, event)`
1324
-
1325
- `deriveUIMessages()` folds a raw A2 event history directly into AI SDK
1326
- `UIMessage[]`. `reduceAIState()` is the event-at-a-time form used by the
1327
- standard reducer. Both synchronously project the same chunk-only protocol.
1328
-
1329
- ## `experimental-a2/ai/server`
1330
-
1331
- ### `createAgentServer(options)`
1332
-
1333
- ```ts
1334
- createAgentServer({
1335
- agent: AgentDefinition,
1336
- model: LanguageModel | (context => LanguageModel),
1337
- tools?,
1338
- instructions?,
1339
- generation?,
1340
- maxSteps?,
1341
- messageMetadata?,
1342
- generate?,
1343
- compaction?,
1344
- progress?,
1345
- store?, scheduler?, telemetry?, handlers?,
1346
- }): A2Server
1347
- ```
1348
-
1349
- Creates the standard server for an agent. A2 runs one AI SDK `streamText()`
1350
- model step per durable generation, persists each `UIMessageChunk` once in a
1351
- durable progress batch, projects messages synchronously, extracts tool and
1352
- approval lifecycle events, executes local tools, and records completion,
1353
- usage, interruption, and failure. `model` accepts an AI SDK model string or
1354
- provider model. `model` and `instructions` can be values or per-generation
1355
- async resolvers.
1356
-
1357
- `generation` contains per-step settings such as `temperature`,
1358
- `maxOutputTokens`, `topP`, provider options, and tool approval policy. A2 owns
1359
- the one-step stop condition, local tool execution, and continuation. Responses
1360
- have no step limit by default. Set `maxSteps` to a positive integer to limit one
1361
- complete assistant response. Support for individual model settings depends on
1362
- the selected model and provider. `generation` excludes `stopWhen`, tool
1363
- execution callbacks, tool callers, sandbox execution, and the tool approval
1364
- secret. Everything else, including `toolsContext`, passes through to the AI SDK
1365
- untouched. A local tool reads its durable context with `handlerContext()` when
1366
- its handler executes. Model-step timeouts remain available; tool-execution
1367
- timeouts are not.
1368
-
1369
- `messageMetadata(context)` synchronously maps each default AI SDK stream part
1370
- to the metadata type of the agent's `UIMessage`. The context includes the
1371
- generation state and the current `TextStreamPart`. Metadata returned for
1372
- standard `start` and `finish` parts is recorded with generation progress and
1373
- shallow-merged onto the projected assistant message. Use the `finish` part's
1374
- `totalUsage` to retain final token usage. A custom `generate` function owns its
1375
- metadata chunks and does not use this mapper.
1376
-
1377
- `generate(context)` optionally replaces the default AI SDK generation. It
1378
- receives messages, the resolved model and instructions, tools, generation
1379
- settings, request state, and the abort signal. It returns exactly one model
1380
- step as a `ReadableStream<UIMessageChunk>`. A2 continues to own tool execution,
1381
- continuation, and the durable lifecycle around that stream.
1382
-
1383
- Generation requests use one durable lane, so model calls for a session do not
1384
- overlap. Queued-turn policy also waits to schedule a later user message until
1385
- the active assistant response, including its complete tool loop, reaches a
1386
- terminal event. The policy matters in addition to lane FIFO because a tool
1387
- continuation may be appended after the later user message. A generation
1388
- failure keeps later messages queued until the failed response is explicitly
1389
- retried or interrupted.
1390
-
1391
- Independent authorized tool handlers run concurrently without a fixed
1392
- concurrency limit. A private coordinator reducer tracks generation closure,
1393
- cancellation, calls, approvals, and terminal results for the active response.
1394
- Completed responses do not accumulate in its state. Its
1395
- `ctx.session.state(coordinator, { through: 'latest' })` reads the durable
1396
- snapshot plus log tail because these joins intentionally observe concurrent
1397
- tool results. Process memory is not authoritative. Concurrent join checks return
1398
- the same deterministic continuation event, so they use `ctx.session.append()`
1399
- and storage deduplicates the race.
1400
-
1401
- Tool results and generation completion have no fixed relative order. The
1402
- continuation predicate needs both the closed model step and every required
1403
- terminal result.
1404
-
1405
- Single-owner terminal events use returned-event causality. They commit
1406
- atomically with parent completion and receive the parent's durable cause.
1407
- Many-owner joins and preliminary streaming outputs use immediate idempotent
1408
- appends. Provider calls and external tool side effects are at least once and
1409
- require their own idempotency. Approved provider-executed calls may satisfy the
1410
- join from the approval response. Provider tools with declared deferred-result
1411
- support wait for their provider result instead of a local executor. An
1412
- authenticated provider callback appends the terminal `ai.tool.result` through
1413
- the trusted server session API; the browser push allowlist rejects it.
1414
-
1415
- `compaction` defaults to automatic for supported Gateway requests. It accepts
1416
- `false` to disable compaction and discovery, `{ thresholdTokens?, instructions? }`
1417
- to override automatic behavior, or the existing custom
1418
- `{ shouldCompact(context), compact(context) }` policy. It also accepts a
1419
- resolver `(context: AgentResolverContext) => CompactionOptions | Promise<CompactionOptions>`.
1420
- The resolver runs once per generation and returns a valid policy or `false`.
1421
- It can await application reducers through `context.session.state(reducer)`, whose
1422
- default boundary is the captured prompt frontier. Model, instruction, compaction,
1423
- metadata, and custom-generation contexts expose state access and omit raw history.
1424
- Callbacks receive prompt messages and request identity, without `state` or
1425
- `execution` arguments. Model and instruction resolvers receive
1426
- `{ event, messages, session, signal }`. Compaction and generation callbacks also
1427
- receive `modelMessages`, including committed summaries. Custom transcript folds
1428
- and schemas apply only to the transcript. `agent.reducer` and `createReducer()`
1429
- continue to expose the full
1430
- transcript view. To read it explicitly, load
1431
- `context.session.state(agent.reducer)` and pass its `state` to
1432
- `agent.reducer.view()`. The stored state also remains suitable for client
1433
- hydration.
1434
- Static options are validated at construction; resolved options are validated
1435
- before that generation starts. Each session uses its own resolved value. `thresholdTokens` is an
1436
- optional positive safe integer. `instructions` adds to A2's internal summary
1437
- prompt without replacing the agent's instructions.
1438
-
1439
- Without an explicit threshold, the first generation batches a metadata request
1440
- into its existing start write. An independent handler reads the public Gateway
1441
- catalog and records that session/model's limits. It does not delay model dispatch.
1442
- The default threshold is 75% of the context window, lowered for a larger configured
1443
- output allowance. A pending or unavailable lookup leaves the threshold unknown;
1444
- the first request proceeds, and provider context-limit errors fail normally.
1445
- `state.modelMetadata` retains each model's pending, resolved, or unavailable state.
1446
- Pending also covers exhausted handler retries; no new request is added per turn.
1447
- The catalog has a shared one-hour process cache and a five-second fetch timeout.
1448
-
1449
- Direct/custom providers, fallback model lists, and custom generators need explicit
1450
- compaction configuration. Provider-executed tools, structured output, and forced
1451
- tool choices skip implicit compaction and reject explicit automatic policies.
1452
- Custom policies remain available for these configurations.
1453
-
1454
- The raw input estimate uses serialized bytes of messages, tools, and instructions,
1455
- excluding provider options on messages and content parts. These options still reach
1456
- the model unchanged. It is not a provider tokenizer. A preceding same-model
1457
- completion anchors later growth to reported input usage, correcting both overestimates
1458
- and underestimates. A smaller prompt, retry, compaction, or model change stops
1459
- that calibration.
1460
-
1461
- Completed generations record the raw `inputTokenEstimate` beside usage.
1462
- Automatic `ai.compaction.requested` events also record the pre-compaction
1463
- `inputTokenEstimate`, the calibrated `inputTokens` used for the decision, and
1464
- `thresholdTokens`. Custom-policy requests omit these diagnostics. They add no
1465
- model calls or database round trips.
1466
-
1467
- The built-in summary adds one model request and preserves the request prefix for
1468
- caching. It does not invoke application generation callbacks, transforms, or tool
1469
- hooks. Both custom policy callbacks and `generate` receive `modelMessages`, the
1470
- model-ready prompt including summaries, alongside typed UI `messages`.
1471
-
1472
- Built-in summaries remain separate from application-typed messages. Render
1473
- `state.compaction?.status === 'running'` as current activity, and the durable
1474
- `ai.compaction.*` events as historical timeline markers. The conversation in
1475
- `state.messages` from the transcript reducer remains intact. Model execution
1476
- uses a separate context checkpoint; existing compactions do not require a
1477
- transcript read. Successful compaction is permanent, including across failure,
1478
- interruption, retry, and recovery. Retry reruns the failed step from the latest
1479
- committed context. Compaction defers while tool exchanges or A2-owned tool work
1480
- remain unsettled, checked with one fresh coordinator state read on each candidate.
1481
- Unresolved dead-lettered tool work also defers compaction until repair and
1482
- settlement or session closure. `progress` controls durable batching with
1483
- `maxChunks` and `maxDelayMs`.
1484
-
1485
- This entry point is server-only and resolves to a throwing browser stub.
1486
-
1487
- ### `runWithHandlerContext(ctx, fn)`
1488
-
1489
- The testing seam for context-reading tools: runs `fn` with `ctx`
1490
- installed as the ambient handler context, so a tool's `execute` can be
1491
- called directly in tests. Inside `fn`, `handlerContext()` returns `ctx`
1492
- for any agent argument.
1493
-
1494
- ### `createHandlers(options)`
1495
-
1496
- Returns the built-in A2 handler table without constructing a server. Spread
1497
- it into `createServer({ handlers })` beside application handlers when you need
1498
- a custom assembly. The table handles input facts, generation requests, model
1499
- step completion, tool calls, approval responses, and terminal tool results.
1500
- Application handlers spread later can deliberately replace a built-in
1501
- handler. Use `handler` from `experimental-a2/server` to normalize an
1502
- existing entry before wrapping its `handler` and spreading its execution
1503
- policy. Entries in this table's type are optional; narrow an entry before
1504
- passing it to `handler`.
1505
-
1506
- A custom assembly owns its browser ingress policy. Use
1507
- `server.fetch(request, { authorize })` to reject server-authored AI event names
1508
- before they reach the server. `createAgentServer()` installs the built-in
1509
- browser allowlist automatically.
1510
-
1511
- See [Durable AI agents](/guides/ai-agents) for the protocol and complete
1512
- examples.
1513
-
1514
- ## `experimental-a2/client`
1515
-
1516
- ```ts
1517
- createClient(options: {
1518
- reducer: Reducer
1519
- api: ClientApi
1520
- gcTime?: number // idle session lifetime; 5 minutes by default
1521
- participant?: string // default presence identity for every session
1522
- reconnect?: (failure: {
1523
- sessionId: string
1524
- error: A2ClientError | null
1525
- attempt: number
1526
- }) => number | false
1527
- }): A2Client
1528
-
1529
- type ClientApi =
1530
- | SessionUrl // one route: GET SSE stream + POST push
1531
- | { type: 'http'; push: SessionUrl; stream: SessionUrl }
1532
- | { type: 'ws'; url: SessionUrl; multiplex?: false }
1533
- | { type: 'ws'; url: string; multiplex: true }
1534
-
1535
- type SessionUrl = string | ((sessionId: string) => string)
1536
- ```
1537
-
1538
- `reconnect` runs after a failed connection or a stream ending, including a
1539
- clean end with `error: null`. Return the delay in milliseconds before the next
1540
- attempt, or `false` to pause automatic reconnection. Delays must be finite,
1541
- non-negative, and at most 2,147,483,647 ms. The callback is synchronous; a
1542
- throw or invalid result pauses the connection with an `A2ClientError`.
1543
-
1544
- `attempt` starts at 1 for the first retry. It counts consecutive attempts
1545
- per session and resets when the stream becomes live or `session.reconnect()`
1546
- explicitly restarts it. It is separate from the lifetime `reconnects` count.
1547
- Without a callback, A2 retries indefinitely with delays starting at 500 ms,
1548
- doubling up to five seconds. The callback does not run for an explicit
1549
- restart, an idle-stream refresh, or a caller closing the stream.
1550
-
1551
- While paused, `connection.status` is `paused` and its last error remains
1552
- available. State, hydration, optimistic writes, and connection leases remain
1553
- intact. Another consumer taking a lease does not resume a paused stream.
1554
- Call `session.reconnect()` to resume immediately with a fresh attempt count.
1555
- Releasing the last lease still closes the stream. A later first lease starts
1556
- a new connection. Push retries are independent of this policy; a push does
1557
- not resume a paused stream. An acknowledged push's `confirmed` promise waits
1558
- until streamed events or a newer hydrated snapshot cover the acknowledged
1559
- state index. Resumption preserves the stream's own position, even when
1560
- hydration has advanced state while paused.
1561
-
1562
- A `SessionUrl` function runs for each HTTP request or WebSocket connection.
1563
- Use it for session-specific paths. A2 still adds its `sessionId` and read-bound
1564
- query parameters. The function does not authorize that ID. Bind any path scope
1565
- or URL token to `operation.sessionId` in `authorize`; keep URL tokens short-lived
1566
- because URLs can appear in infrastructure logs. A multiplexed WebSocket URL is
1567
- a string because its connection carries more than one session.
1568
-
1569
- The framework-agnostic session client `experimental-a2/react` is built on: the SSE
1570
- subscription with frontier resume and reconnection, the optimistic push
1571
- queue with ack/rollback, and the local fold. `client.session(id, {
1572
- initialState?, initialIndex?, initialEvents?, participant? })` returns a
1573
- handle with `getSnapshot()`/`subscribe()` (the `useSyncExternalStore`
1574
- contract), `push()`, `loadHistory()`, `connect()`, `reconnect()`, and `close()`.
1575
- `connect()` takes a lease on the live stream and returns its release:
1576
- leases refcount per handle (the first connects, releasing the last
1577
- closes, releasing twice is a no-op), so independent consumers of one
1578
- identity-mapped handle never fight over the stream. `close()` is the
1579
- hard stop: it drops every outstanding lease and closes now; a later
1580
- `connect()` starts fresh.
1581
- Snapshots carry `state`, `events`, `index`, `history`, and `connection`
1582
- (the same fields `useSession` exposes), and `push` returns the same
1583
- ack-then-`confirmed` result. On contracts that declare `presence` the handle also carries
1584
- `setPresence()` and snapshots carry the `presence` map, exactly like
1585
- the hook; `participant` is the identity `setPresence` sends under:
1586
- stated once on `createClient` as the default for every handle, or per
1587
- session as the override. Use
1588
- it directly from any other framework, or none.
1589
-
1590
- Within one `A2Client`, repeated `session(id)` calls return the same live
1591
- handle. A later call with a further server-rendered frontier advances the
1592
- handle and does not drop pending optimistic events; a stale render cannot move
1593
- it backward. Optional `initialEvents` seed earlier raw events for history UIs
1594
- and merge into that feed. They do not affect the folded state or stream
1595
- frontier. Idle handles are evicted after `gcTime`. This memory layer is
1596
- separate from `experimental-a2/cache-indexeddb`: memory preserves identity
1597
- across route transitions, while IndexedDB preserves the replica across reloads.
1598
-
1599
- ## `experimental-a2/cache-indexeddb`
1600
-
1601
- ```ts
1602
- indexedDb(): A2ClientCache
1603
- ```
1604
-
1605
- A browser-side cache for `SessionProvider`: the session's events
1606
- (immutable; writes are idempotent by index), a folded snapshot
1607
- (invalidated by a reducer rename), and the pending-push queue.
1608
- `cache.clear(sessionId?)` is the logout story. Create instances in client
1609
- code; they're live objects, not serializable props. See
1610
- [Local-first](/guides/local-first).
1611
-
1612
- ## `experimental-a2/otel`
1613
-
1614
- ```ts
1615
- otel(options?: { tracer?: Tracer; tracerName?: string }): A2Telemetry
1616
- ```
1617
-
1618
- OpenTelemetry instrumentation for a server (`@opentelemetry/api` is a
1619
- peer dependency). Pass it as `telemetry`; spans nest through the active
1620
- context, so the happy path lands in one trace: request → append → drain →
1621
- each handler → the events those handlers append.
1622
-
1623
- ### The span catalogue
1624
-
1625
- Five spans, all carrying `a2.contract` and `a2.session_id`:
1626
-
1627
- | Span | Wraps |
1628
- | ----------- | ------------------------------------------------------ |
1629
- | `a2.append` | validation + the store write (one batch) |
1630
- | `a2.drain` | one drain pass over a session's backlog |
1631
- | `a2.event` | one claimed dispatch of one event |
1632
- | `a2.state` | one `state()` read: snapshot-plus-tail load + fold |
1633
- | `a2.snapshot` | one background batch of checkpoint writes |
1634
-
1635
- Attributes arrive in two waves: **at start** (passed to `span()`) and
1636
- **mid-span** (via `handle.setAttribute`; outcomes and counts aren't
1637
- known until the work ran). A recorder must merge both; the ones you
1638
- alert on are all mid-span.
1639
-
1640
- | Attribute | Span | When | Values |
1641
- | --------------------- | ----------- | ----- | ----------------------------------------------------- |
1642
- | `a2.append.source` | `a2.append` | start | `external` \| `handler` |
1643
- | `a2.append.mode` | `a2.append` | start | `inline` \| `dispatch` |
1644
- | `a2.append.types` | `a2.append` | start | comma-joined event types |
1645
- | `a2.append.count` | `a2.append` | start | batch size |
1646
- | `a2.append.armed` | `a2.append` | mid | `false` when the scheduler arm failed and this append degraded to append-driven healing |
1647
- | `a2.drain.outcome` | `a2.drain` | mid | `settled` \| `busy` \| `stalled` |
1648
- | `a2.drain.processed` | `a2.drain` | mid | events processed this pass |
1649
- | `a2.event.type` | `a2.event` | start | the event's type |
1650
- | `a2.event.index` | `a2.event` | start | log position |
1651
- | `a2.event.id` | `a2.event` | start | event id |
1652
- | `a2.event.attempt` | `a2.event` | start | same durable 1-based ordinal as `ctx.attempt` |
1653
- | `a2.event.lane` | `a2.event` | start | stored lane value; absent for concurrent unlaned work |
1654
- | `a2.event.handled` | `a2.event` | start | normally `true`; `false` when a custom-adapter row has no handler |
1655
- | `a2.event.outcome` | `a2.event` | mid | `processed` \| `failed` \| `dead_lettered` \| `superseded` \| `surrendered` |
1656
- | `a2.event.aborted` | `a2.event` | mid | `true` when `abortOn` fired during the run |
1657
- | `a2.state.reducer` | `a2.state` | start | the reducer's name |
1658
- | `a2.state.snapshot` | `a2.state` | mid | `hit` \| `miss` \| `rejected` (schema guard discarded it) |
1659
- | `a2.state.folded` | `a2.state` | mid | events folded past the snapshot |
1660
- | `a2.state.index` | `a2.state` | mid | the frontier the returned state reflects |
1661
- | `a2.snapshot.reducer` | `a2.snapshot` | start | the reducer's name |
1662
- | `a2.snapshot.index` | `a2.snapshot` | start | greatest checkpoint index in the write batch |
1663
- | `a2.snapshot.count` | `a2.snapshot` | start | number of checkpoints in the write batch |
1664
-
1665
- Drain outcomes: `settled` means nothing actionable is left; `busy` means live
1666
- per-event claims remain; `stalled` means a caught failure needs a later retry.
1667
- A dead-lettered event blocks only later events in its lane. Other lanes remain
1668
- eligible.
1669
-
1670
- A failing handler marks its `a2.event` span with the exception and
1671
- error status; `a2.event.outcome = dead_lettered` is the attribute to
1672
- alert on. `superseded` means a stale completion or failure did not change the
1673
- log.
1674
- `A2Telemetry` is a one-method interface; anything with
1675
- `span(name, attributes, fn)` works;
1676
- `otel()` is simply the adapter A2 ships. This catalogue is a contract:
1677
- renames and additions are breaking for dashboards, and are called out in the
1678
- package's `CHANGELOG.md`.
1679
-
1680
- ## `experimental-a2/scheduler-vercel`
1681
-
1682
- `vercelQueues(options?)` returns an `A2Scheduler` backed by Vercel Queues.
1683
-
1684
- ```ts server/scheduler-vercel.ts
1685
- import { vercelQueues } from 'experimental-a2/scheduler-vercel'
1686
-
1687
- export const scheduler = vercelQueues()
1688
- ```
1689
-
1690
- | Option | Type | Default |
1691
- | --- | --- | --- |
1692
- | `topic` | `string` matching `[A-Za-z0-9_-]+` | `'a2'` |
1693
- | `scheduledAppends` | `'deployment' \| 'deploymentless'` | `'deployment'` |
1694
- | `transport` | `QueueTransport` | lazy `@vercel/queue` client |
1695
- | `clock` | `{ now(): Date }` | system clock |
1696
-
1697
- The default pins recovery watchdogs and scheduled appends to the deployment
1698
- that publishes them. Recovery always remains pinned. Set `scheduledAppends` to
1699
- `'deploymentless'` only when every eligible deploymentless consumer for the
1700
- topic can decode pending tasks and validate their events. This option removes
1701
- deployment affinity. It does not select the latest production deployment.
1702
-
1703
- The adapter retains every task for seven days and accepts delays through six
1704
- days, leaving the final day for delivery and retries. `transport` and `clock`
1705
- are advanced injection seams for tests and custom delivery. An injected
1706
- transport receives the selected deployment affinity as an optional fourth send
1707
- argument while its message remains an exact `SchedulerTask`. It must honor that
1708
- hint for `scheduledAppends: 'deploymentless'` to affect its provider-specific
1709
- routing. The built-in transport adds its callback-routing metadata only after
1710
- this injection seam.
1711
-
1712
- ## `experimental-a2/scheduler-qstash`
1713
-
1714
- `qstash(options?)` returns an `A2Scheduler` backed by QStash HTTP delivery.
1715
- Vercel Queues remains the recommended default on Vercel. Use QStash when you
1716
- want a scheduler that calls a stable public endpoint.
1717
-
1718
- ```ts server/scheduler-qstash.ts
1719
- import { qstash } from 'experimental-a2/scheduler-qstash'
1720
-
1721
- export const scheduler = qstash()
1722
- ```
1723
-
1724
- | Option | Type | Default |
1725
- | --- | --- | --- |
1726
- | `url` | `string` | conventional local or Vercel callback; required for process-less runtimes, other production, and custom routes |
1727
- | `token` | `string` | `QSTASH_TOKEN` |
1728
- | `currentSigningKey` | `string` | `QSTASH_CURRENT_SIGNING_KEY` |
1729
- | `nextSigningKey` | `string` | `QSTASH_NEXT_SIGNING_KEY` |
1730
- | `headers` | `Readonly<Record<string, string>>` | no additional callback headers |
1731
- | `client` | `QStashPublishingClient` | lazy `@upstash/qstash` client |
1732
- | `retries` | non-negative integer | the QStash provider default |
1733
- | `maxBodyBytes` | positive integer | `1_048_576` (1 MiB) for callbacks and reserved outbound bodies |
1734
- | `transport` | `QStashTransport` | lazy `@upstash/qstash` client and receiver |
1735
- | `clock` | `{ now(): Date }` | system clock |
1736
-
1737
- `headers` adds static headers to every callback delivery, including recovery
1738
- successors. A2 validates and snapshots the record when `qstash()` runs, then
1739
- asks QStash to redact every configured value from its dashboard and API. QStash
1740
- still receives, stores, and delivers the original values. Header names are
1741
- case-insensitive. Provider-owned `Upstash-*` headers, `Content-Type*`, HTTP
1742
- framing headers, and A2's managed Vercel identity headers are reserved.
1743
-
1744
- `client` accepts the structural `publishJSON(...)` part of a configured
1745
- `@upstash/qstash` `Client`. A2 still creates the `Receiver` lazily from the
1746
- signing-key options. `client` cannot be combined with `token` or `transport`.
1747
- Use it to configure the QStash API endpoint, publish-request retry policy, or
1748
- telemetry. Use A2's `headers` option for callback credentials. Client-global
1749
- callback headers and redaction are outside A2's merge contract. The injected
1750
- client receives raw callback credentials, including an automatic preview
1751
- identity, and must honor the per-publish redaction request.
1752
-
1753
- `transport` and `clock` are advanced injection seams for tests and custom
1754
- delivery. A `QStashTransport` implements `publish(request)` and
1755
- `verify(request)`. Publish requests contain the callback `url`, a complete
1756
- `SchedulerTask` body, optional callback `headers`, optional `redact.header`
1757
- names, optional Unix-seconds `notBefore`, and a deterministic
1758
- `deduplicationId`. Store-outage successors add provider-private generation
1759
- metadata to that body. Custom transports must preserve the body, deliver every
1760
- header, and apply every redaction request. An explicit `retries` value is
1761
- forwarded; omission lets QStash use its provider default. Verify requests
1762
- contain the signature, raw body, exact callback URL, and optional Upstash
1763
- region.
1764
-
1765
- An explicit `url` always wins. Otherwise, Vercel production resolves to
1766
- `https://${VERCEL_PROJECT_PRODUCTION_URL}/api/a2/scheduler` and preview resolves
1767
- to `https://${VERCEL_BRANCH_URL}/api/a2/scheduler`. Outside those deployments,
1768
- a runtime that exposes `process.env` and is not in `NODE_ENV=production`
1769
- resolves to `http://localhost:${PORT ?? 3000}/api/a2/scheduler` and passes
1770
- `devMode: true` to the SDK client and receiver. `QSTASH_DEV` is not required.
1771
- `PORT` must be an integer from 1 through 65535. `QSTASH_DEV_PORT` configures the
1772
- local QStash server, not the application callback. Automatic startup requires
1773
- `@upstash/qstash` 2.11.0 or newer in a Node.js runtime. The lazy transport
1774
- starts it on the first publish or verification. The direct SDK override means
1775
- `QSTASH_DEV=false` does not disable an inferred local route.
1776
-
1777
- A non-Vercel production runtime or a process-less runtime requires an explicit
1778
- absolute URL. Pass one for a custom host or path, an application port not
1779
- available through `PORT`, or Docker networking. An Edge runtime that exposes
1780
- `process.env` can infer the local URL, but it must start the QStash development
1781
- server separately. Outside production, an explicit URL does not override the
1782
- SDK's development mode, so `QSTASH_DEV` and manually supplied `QSTASH_URL` plus
1783
- credentials retain their SDK-defined behavior. A2 passes `devMode: false` in
1784
- production. The inferred Vercel hosts are moving aliases, so pending tasks can
1785
- reach later deployments on the same production or branch URL.
1786
-
1787
- Mount the application handler once and pass every server that shares the
1788
- scheduler:
1789
-
1790
- ```ts app/api/a2/scheduler/route.ts
1791
- import { billingServer, ordersServer, scheduler } from '@/server'
1792
-
1793
- export const POST = scheduler.handler(ordersServer, billingServer)
1794
- ```
1795
-
1796
- The route must be reachable by the selected QStash server after any platform
1797
- edge authentication. The development server calls localhost directly. The
1798
- adapter reads its raw body once and verifies `Upstash-Signature`, the exact URL,
1799
- and `Upstash-Region` before it parses the delivery or touches a server. Missing
1800
- or invalid signatures return 401, so ordinary unsigned traffic cannot execute
1801
- scheduler work. A signed malformed envelope returns 489 with
1802
- `Upstash-NonRetryable-Error: true`. The same non-retryable response applies
1803
- when the declared or streamed body exceeds `maxBodyBytes`. Before an initial
1804
- publish, the adapter checks the normalized task with enough reserved room for
1805
- its largest possible private store-outage lineage. Append tasks reserve a
1806
- maximum safe-integer generation. Drain tasks also reserve their actual root
1807
- `dueAt`. Store-outage successors check their actual body. The adapter therefore
1808
- cannot initially accept a task that would exceed the same limit solely when
1809
- lineage is added. Raise the option to match a larger paid-plan payload limit
1810
- when needed. Keep one value across every deployment that can receive this
1811
- callback URL. A lower value deployed while older tasks remain can return a
1812
- non-retryable 489 for a task accepted by the previous value.
1813
-
1814
- For an inferred Vercel preview callback, the adapter reads the current
1815
- invocation's `x-vercel-oidc-token` for each publish. When it is a parseable JWT
1816
- whose expiration remains at least 60 seconds after the task's first due time,
1817
- the adapter supplies it as `x-vercel-trusted-oidc-idp-token` and requests QStash
1818
- header redaction. A matching Vercel Trusted Sources rule can then admit the
1819
- first delivery. QStash receives the credential even though its dashboard and
1820
- API redact it.
1821
-
1822
- This is opportunistic, not durable. Missing, malformed, and near-expiry tokens
1823
- fall back to an ordinary publish, preserving public-preview behavior. An
1824
- explicit `url` opts out. Inferred production and local callbacks never forward
1825
- the token. QStash stores the static header and reuses it on retries, so delayed
1826
- tasks and retries can outlive it. Use an explicit automation bypass or public
1827
- ingress when every protected delivery must remain authorized. Applications can
1828
- supply a dedicated bypass through `headers`; A2 requests redaction for its
1829
- value, but pending messages retain the captured credential after rotation. A
1830
- delivery-time Vercel relay remains a separate application architecture that
1831
- requires a Trusted Sources rule, strict target allowlisting, and a versioned
1832
- routing envelope. QStash signature verification remains mandatory after the
1833
- edge-access check.
1834
-
1835
- Settled deliveries return 200. Busy deliveries await a durable successor
1836
- watchdog before returning 200. Stalled deliveries publish a fresh successor
1837
- before returning 200, which resets the per-message retry budget. A retryable
1838
- store outage also hands both drain and append tasks to a fresh successor before
1839
- acknowledging. Store-outage successors wait 30 seconds, then multiply the delay
1840
- by four for each signed generation, capped at one day. Their generation keeps
1841
- increasing after the cap so each handoff has a new deduplication id. Drain
1842
- lineage includes the first failing task's `dueAt`, contract, and session. Append
1843
- lineage includes its task id, contract, and session. Duplicate delivery of one
1844
- generation therefore publishes the same successor key without merging a later
1845
- drain episode. Append tasks otherwise return 200 after their ordinary fixed-id
1846
- append succeeds and any recovery watchdog for pending handler work is durable.
1847
- Unknown contracts, missing scheduler configuration, schema failures, and
1848
- successor publish failures keep the current delivery retryable. Delivery is at
1849
- least once, so handler side effects still need idempotency keys.
1850
-
1851
- The adapter leaves `retries` unset by default so QStash applies its provider
1852
- default. An explicit non-negative value is passed through. Every delivery
1853
- attempt is billable. A prolonged outage in which QStash cannot reach the
1854
- callback still requires DLQ monitoring and redrive because no running callback
1855
- can publish a successor. QStash's
1856
- documented 10-minute deduplication window can reduce duplicate watchdogs but is
1857
- not part of A2's correctness model. Stable scheduled-event ids also make timer
1858
- redelivery safe after that window. One-shot delays are currently capped at 7
1859
- days on free plans and 1 year on usage-based plans, with no fixed maximum on
1860
- fixed plans. Leave at least one second of margin at a plan maximum because
1861
- `notBefore` uses whole Unix seconds. The adapter carries both recovery watchdogs and
1862
- `session.schedule` tasks.
1863
-
1864
- The built-in QStash and Vercel adapters preserve the original provider error
1865
- from a rejected send. A recognized definite 4xx status or error is terminal
1866
- except for 408, 409, 425, and 429. Those statuses, 5xx responses, network
1867
- failures, and errors with no recognized status remain retryable. The Vercel
1868
- adapter also recognizes the SDK's name-only `BadRequestError`,
1869
- `UnauthorizedError`, and `ForbiddenError` as terminal. Its native
1870
- duplicate-message error is success. A2's durable AI tool execution uses this
1871
- private classification. Direct `session.schedule()` calls still reject with
1872
- the original error object.
1873
-
1874
- ## Entry points
1875
-
1876
- | Entry point | Ships | Peer dependency |
1877
- | -------------------- | ---------------------------------- | --------------- |
1878
- | `experimental-a2` | `contract`, `A2Error`: isomorphic core | none |
1879
- | `experimental-a2/server` | `createServer`: implement a contract | none |
1880
- | `experimental-a2/client` | `createClient`: framework-agnostic session client | none |
1881
- | `experimental-a2/react` | `createReact` | `react` |
1882
- | `experimental-a2/ai` | agent contract, `handlerContext`, reducer, and append builders: isomorphic | `ai` |
1883
- | `experimental-a2/ai/server` | AI SDK runner and built-in handlers | `ai` |
1884
- | `experimental-a2/store-postgres` | `postgres`: Postgres store backend | `pg` (or inject a client) |
1885
- | `experimental-a2/store-redis` | `redis`: Redis Streams store backend, push-native streaming | `ioredis` (or inject a client) |
1886
- | `experimental-a2/store-redis-http` | `redisHttp`: the same Redis store over provider REST APIs (Upstash) | none |
1887
- | `experimental-a2/store-sqlite` | SQLite store backend | none |
1888
- | `experimental-a2/store-memory` | in-memory store backend | none |
1889
- | `experimental-a2/scheduler-vercel` | `vercelQueues`: recovery and timers | `@vercel/queue` |
1890
- | `experimental-a2/scheduler-qstash` | `qstash`: signed recovery and timers | `@upstash/qstash` |
1891
- | `experimental-a2/cache-indexeddb` | `indexedDb` browser cache | none |
1892
- | `experimental-a2/otel` | `otel` telemetry adapter | `@opentelemetry/api` |
1893
-
1894
- Core `experimental-a2` imports none of the backends, enforced by a browser-bundle
1895
- test in CI, not just convention. Importing `experimental-a2/store-postgres` is what
1896
- pulls in Postgres, never `experimental-a2` itself, and never your client bundle.