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
@@ -0,0 +1,243 @@
1
+ ---
2
+ title: Routes
3
+ description: Serve every session with `server.fetch` over HTTP or a WebSocket, or build your own route from session methods.
4
+ ---
5
+
6
+ ## The HTTP route
7
+
8
+ ```ts app/api/public-order-events/route.ts
9
+ import { ordersServer } from '@/server/orders'
10
+
11
+ export const GET = ordersServer.fetch
12
+ export const POST = ordersServer.fetch
13
+ ```
14
+
15
+ `server.fetch` is bound, so an unprotected route is two exports. A plain GET
16
+ streams one session over SSE, a GET with `gte` and `lte` returns a history
17
+ slice, and POST appends the pushed events. If the contract declares presence,
18
+ the same route streams and accepts it.
19
+
20
+ Wrap the call when the route needs authentication or authorization:
21
+
22
+ ```ts app/api/protected-order-events/route.ts
23
+ import { ordersServer } from '@/server/orders'
24
+
25
+ async function fetchOrders(request: Request): Promise<Response> {
26
+ // your authentication:
27
+ const user = { id: 'user-1' }
28
+ if (!user) return new Response(null, { status: 401 })
29
+
30
+ return ordersServer.fetch(request, {
31
+ authorize(operation) {
32
+ // your per-session and per-operation authorization:
33
+ // return canAccess(user, operation)
34
+ return true
35
+ },
36
+ })
37
+ }
38
+
39
+ export const GET = fetchOrders
40
+ export const POST = fetchOrders
41
+ ```
42
+
43
+ Authenticate in the route, where headers, cookies, and framework context
44
+ live. `authorize` then checks each typed operation (`stream`, `history`, or
45
+ `push` with its parsed events and presence patch) after A2 parses it and
46
+ before schema validation or durable I/O. Return `false` to answer
47
+ `FORBIDDEN` (403). A throw is an infrastructure failure and answers
48
+ `STORE_UNAVAILABLE`.
49
+
50
+ If a URL or connection token is scoped to one session, `authorize` must
51
+ compare that scope to `operation.sessionId`. Keep URL tokens short-lived:
52
+ URLs can appear in infrastructure logs.
53
+
54
+ ## The WebSocket route
55
+
56
+ ```ts app/api/socket-order-events/route.ts
57
+ import { experimental_upgradeWebSocket } from '@vercel/functions'
58
+ import { ordersServer } from '@/server/orders'
59
+
60
+ export const POST = ordersServer.fetch
61
+
62
+ export function GET(request: Request): Promise<Response> {
63
+ return ordersServer.fetch(request, {
64
+ upgradeWebSocket: (attach) =>
65
+ experimental_upgradeWebSocket(attach, {
66
+ maxPayload: 4 * 1024 * 1024,
67
+ }),
68
+ })
69
+ }
70
+ ```
71
+
72
+ Pass the platform's upgrade capability on GET; POST can stay a direct export.
73
+ The socket is bound to the `sessionId` in its upgrade URL: A2 authorizes that
74
+ session before upgrading, and frames can't switch sessions. Each connected
75
+ session has its own socket.
76
+
77
+ Authenticate the upgrade request before calling `server.fetch`. For browser
78
+ sockets authenticated by cookies, validate the request's `Origin` too: a
79
+ WebSocket handshake has no CORS preflight. Then `authorize` checks the session
80
+ stream and every push or presence frame. There is no separate upgrade
81
+ operation.
82
+
83
+ To share one socket across sessions, opt in on the route and set
84
+ `multiplex: true` on the [client](/client/transports):
85
+
86
+ ```ts app/api/multiplexed-order-events/route.ts
87
+ import { experimental_upgradeWebSocket } from '@vercel/functions'
88
+ import { ordersServer } from '@/server/orders'
89
+
90
+ export function GET(request: Request): Promise<Response> {
91
+ return ordersServer.fetch(request, {
92
+ multiplexWebSocket: true,
93
+ upgradeWebSocket: (attach) =>
94
+ experimental_upgradeWebSocket(attach, {
95
+ maxPayload: 4 * 1024 * 1024,
96
+ }),
97
+ })
98
+ }
99
+ ```
100
+
101
+ Enable multiplexing only when the request's authentication context may open
102
+ more than one session and `authorize` checks each session id. Subscribe frames
103
+ open sessions after the upgrade.
104
+
105
+ Denials stay scoped:
106
+
107
+ - A denied subscribe on a multiplexed socket gets an `unsubscribed` notice
108
+ with `FORBIDDEN` and leaves other sessions connected. A failed stream or a
109
+ thrown authorization check carries `STORE_UNAVAILABLE`. The client hands
110
+ these to its reconnection policy as `A2ClientError`s.
111
+ - A denied event push gets a non-retryable `FORBIDDEN` ack.
112
+ - Denied presence is dropped silently; presence repaints on the next update.
113
+
114
+ A2 reads the platform's invocation deadline and closes the socket cleanly
115
+ before it.
116
+
117
+ ## Build your own
118
+
119
+ `server.fetch` speaks the wire A2's client expects. For anything else, such as
120
+ another framework, your own framing, or a consumer that isn't A2's client,
121
+ build the route from session methods. It's just an event log: read a snapshot,
122
+ follow the stream, append.
123
+
124
+ ```ts app/api/orders/[orderId]/live/route.ts
125
+ import { orders } from '@/contracts'
126
+ import { ordersServer } from '@/server/orders'
127
+
128
+ type Params = { params: Promise<{ orderId: string }> }
129
+
130
+ const encoder = new TextEncoder()
131
+ const frame = (name: string, data: unknown): Uint8Array =>
132
+ encoder.encode(`event: ${name}\ndata: ${JSON.stringify(data)}\n\n`)
133
+
134
+ export async function GET(
135
+ _request: Request,
136
+ { params }: Params,
137
+ ): Promise<Response> {
138
+ const { orderId } = await params
139
+ // here's where you'd do auth, or any other checks
140
+ const session = ordersServer.session(orderId)
141
+ const snapshot = await session.state(orders.reducer)
142
+ const events = session
143
+ .stream({ startAfter: snapshot.index })
144
+ [Symbol.asyncIterator]()
145
+
146
+ const body = new ReadableStream<Uint8Array>({
147
+ start(controller) {
148
+ controller.enqueue(frame('snapshot', snapshot))
149
+ },
150
+ async pull(controller) {
151
+ const next = await events.next()
152
+ if (next.done) return controller.close()
153
+ const event = next.value
154
+ const name = event.type === 'a2.cleared' ? 'cleared' : 'event'
155
+ controller.enqueue(frame(name, event))
156
+ },
157
+ // the browser left: end the subscription
158
+ async cancel() {
159
+ await events.return?.()
160
+ },
161
+ })
162
+ return new Response(body, {
163
+ headers: { 'content-type': 'text/event-stream' },
164
+ })
165
+ }
166
+ ```
167
+
168
+ `session.state()` returns the snapshot's `index`, so the stream picks up
169
+ exactly where the snapshot ends. A clear arrives as an `a2.cleared` marker in
170
+ the same stream. Writes go through `session.append`, which validates against
171
+ the contract:
172
+
173
+ ```ts app/api/orders/[orderId]/live/route.ts
174
+ import { A2Error } from 'experimental-a2'
175
+
176
+ export async function POST(
177
+ request: Request,
178
+ { params }: Params,
179
+ ): Promise<Response> {
180
+ const { orderId } = await params
181
+ // here's where you'd do auth, or any other checks
182
+ const { shopId, items } = await request.json()
183
+ try {
184
+ const [event] = await ordersServer.session(orderId).append({
185
+ type: 'created',
186
+ payload: { shopId, items },
187
+ })
188
+ return Response.json(event)
189
+ } catch (error) {
190
+ if (error instanceof A2Error && error.code === 'INVALID_PAYLOAD') {
191
+ return Response.json({ issues: error.details }, { status: 400 })
192
+ }
193
+ throw error
194
+ }
195
+ }
196
+ ```
197
+
198
+ The browser follows it with `EventSource` and folds with the contract's own
199
+ reducer, the same fold the server runs:
200
+
201
+ ```ts app/orders/[orderId]/follow-order.ts
202
+ 'use client'
203
+ import { orders } from '@/contracts'
204
+
205
+ type OrderState = typeof orders.reducer.initialState
206
+
207
+ export function followOrder(
208
+ orderId: string,
209
+ onChange: (state: OrderState) => void,
210
+ ): () => void {
211
+ const url = `/api/orders/${encodeURIComponent(orderId)}/live`
212
+ const source = new EventSource(url)
213
+ let state = orders.reducer.initialState
214
+ const update = (next: OrderState): void => {
215
+ state = next
216
+ onChange(state)
217
+ }
218
+ source.addEventListener('snapshot', (message) => {
219
+ update(JSON.parse(message.data).state)
220
+ })
221
+ source.addEventListener('event', (message) => {
222
+ update(orders.reducer.fold(state, JSON.parse(message.data)))
223
+ })
224
+ source.addEventListener('cleared', () => {
225
+ update(orders.reducer.initialState)
226
+ })
227
+ return () => source.close()
228
+ }
229
+ ```
230
+
231
+ `EventSource` reconnects on its own, and each connection starts from a fresh
232
+ snapshot. What `server.fetch` adds on top, and a custom route takes on itself:
233
+
234
+ - resuming a stream from the client's last `index` instead of a new snapshot
235
+ - bounded history reads (`gte` and `lte`)
236
+ - the push envelope with acks and `.confirmed`, and presence
237
+ - WebSocket upgrades
238
+ - `authorize` on every operation; direct `session.append` bypasses it, so the
239
+ route is where your checks go
240
+ - closing streams before the platform's invocation deadline
241
+
242
+ A2's client, `createClient` and `useSession`, talks to `server.fetch`. Pair a
243
+ custom route with your own client.
@@ -0,0 +1,322 @@
1
+ ---
2
+ title: Production
3
+ description: Choose a durable store, add a scheduler for recovery and timers, and know what to do when an event dead-letters.
4
+ ---
5
+
6
+ ## 1. Choose a store
7
+
8
+ ```ts server/orders.ts
9
+ import { createServer } from 'experimental-a2/server'
10
+ import { redis } from 'experimental-a2/store-redis'
11
+ import { orders } from '@/contracts'
12
+
13
+ export const ordersServer = createServer({
14
+ contract: orders,
15
+ store: redis({ url: process.env.REDIS_URL }),
16
+ handlers: {
17
+ /* ... */
18
+ },
19
+ })
20
+ ```
21
+
22
+ | Entry point | Backend | Use it for |
23
+ | --- | --- | --- |
24
+ | `experimental-a2/store-redis` | Redis | Production (recommended) |
25
+ | `experimental-a2/store-redis-http` | Redis over a REST API | Production |
26
+ | `experimental-a2/store-postgres` | Postgres | Production |
27
+ | `experimental-a2/store-sqlite` | SQLite | Development: the default, at `.a2/dev.db` |
28
+ | `experimental-a2/store-memory` | In-memory | Tests: the default when `NODE_ENV=test` |
29
+
30
+ When you omit `store`, development gets SQLite (state survives `next dev`
31
+ restarts) and tests get memory. Production has no default, on purpose: the
32
+ server throws `STORE_NOT_CONFIGURED` at startup rather than write events to a
33
+ filesystem that evaporates.
34
+
35
+ The store holds A2's session histories. It does not connect A2 to your
36
+ application tables; see [Sessions and your database](/contract/sessions#sessions-and-your-database).
37
+
38
+ ### Redis (recommended)
39
+
40
+ `redis({ url })` from `experimental-a2/store-redis` stores each session as a
41
+ Redis Stream and streams push-natively, so connections scale with your
42
+ processes, not your audience. `ioredis` is an optional peer dependency; pass
43
+ `url` or inject a client. It works on single instances and non-cluster
44
+ providers such as Upstash, where durability is on by default.
45
+
46
+ On providers that bill per command, such as Upstash, appending an event with a
47
+ handler and running it takes 24 commands, and an idle live stream takes 6 a
48
+ minute. See
49
+ [`experimental-a2/store-redis`](/reference/adapters#experimental-a2store-redis)
50
+ for how live streams recover a lost wake-up.
51
+
52
+ When only a REST API is available, `redisHttp({ url, token })` from
53
+ `experimental-a2/store-redis-http` speaks the same storage over `fetch`, with
54
+ no client dependency or extra setup. Live events and presence use Upstash's
55
+ HTTP `SUBSCRIBE` endpoint with the same URL and token. If that endpoint answers
56
+ 404, 405, or 501, the store polls instead; other errors stay errors. See
57
+ [`experimental-a2/store-redis-http`](/reference/adapters#experimental-a2store-redis-http)
58
+ for its subscriptions, reconnects, and fallback.
59
+
60
+ :::warning
61
+ `redisHttp` live streams trust the messages on the store's `:notify`
62
+ channels. Browsers never hold Redis credentials, so they can't reach those
63
+ channels. The risk is a Redis credential that can publish but not write, such
64
+ as a token another service uses: it could add or hide events in live views.
65
+ Grant publish access only to credentials that can also write the store.
66
+ :::
67
+
68
+ ### Postgres
69
+
70
+ Any Postgres works: Neon, Supabase, RDS, or your own box, including
71
+ transaction-mode poolers. `pg` is an optional peer dependency. Pass
72
+ `connectionString`, or inject your own pool as `client`; an injected pool's
73
+ platform lifecycle is yours to manage.
74
+
75
+ Live streams poll by default, backing off when a session goes quiet. For
76
+ native notifications, also pass `listenConnectionString`: a direct connection
77
+ to the same database. It installs notification triggers on A2's event and
78
+ presence tables, which stay installed and also cover writes from instances
79
+ without the option. Stores in one runtime share a listener, but every
80
+ serverless instance opens its own direct connection, so leave connection
81
+ headroom. See
82
+ [`experimental-a2/store-postgres`](/reference/adapters#experimental-a2store-postgres)
83
+ for live-stream latency and recovery.
84
+
85
+ ## 2. Add a scheduler for handlers and timers
86
+
87
+ The scheduler puts a clock on recovery and on [timers](/server/timers).
88
+ Vercel Queues is the recommended default on Vercel. QStash delivers over HTTP
89
+ from separate infrastructure. Either adapter carries recovery and
90
+ `session.schedule` tasks through one route. Scheduled delays must fit the
91
+ [provider's limit](/server/timers#provider-limits).
92
+
93
+ ### Vercel Queues (recommended)
94
+
95
+ ```ts
96
+ // server/orders.ts, now with a scheduler:
97
+ import { createServer } from 'experimental-a2/server'
98
+ import { redis } from 'experimental-a2/store-redis'
99
+ import { vercelQueues } from 'experimental-a2/scheduler-vercel'
100
+ import { orders } from '@/contracts'
101
+
102
+ export const scheduler = vercelQueues()
103
+
104
+ export const ordersServer = createServer({
105
+ contract: orders,
106
+ store: redis({ url: process.env.REDIS_URL }),
107
+ scheduler,
108
+ handlers: {
109
+ /* ... */
110
+ },
111
+ })
112
+ ```
113
+
114
+ `@vercel/queue` is a peer dependency. Share one scheduler instance across
115
+ servers, and mount its handler once with every server it may deliver to:
116
+
117
+ ```ts app/api/a2/scheduler/route.ts
118
+ import { billingServer, ordersServer, scheduler } from '@/server'
119
+
120
+ export const POST = scheduler.handler(ordersServer, billingServer)
121
+ ```
122
+
123
+ `scheduler.handler(...)` checks its servers when the module loads: at least
124
+ one drainable server, with unique contract names. A bad route fails at
125
+ startup, or in a build that evaluates route modules.
126
+
127
+ ```json vercel.json
128
+ {
129
+ "functions": {
130
+ "app/api/a2/scheduler/route.ts": {
131
+ "experimentalTriggers": [
132
+ {
133
+ "type": "queue/v2beta",
134
+ "topic": "a2"
135
+ }
136
+ ]
137
+ }
138
+ }
139
+ }
140
+ ```
141
+
142
+ The trigger is required. It connects the route to Vercel Queues and makes the
143
+ function private, so only queue infrastructure can invoke it. Don't expose the
144
+ callback as a public route or wrap it in a second authentication scheme.
145
+
146
+ The adapter keeps messages for 7 days (Vercel Queues' default is 24 hours) and
147
+ accepts scheduled appends and clears up to 6 days out.
148
+
149
+ ### QStash
150
+
151
+ ```ts
152
+ // server/orders.ts, with QStash instead:
153
+ import { createServer } from 'experimental-a2/server'
154
+ import { qstash } from 'experimental-a2/scheduler-qstash'
155
+ import { redis } from 'experimental-a2/store-redis'
156
+ import { orders } from '@/contracts'
157
+
158
+ export const scheduler = qstash()
159
+
160
+ export const ordersServer = createServer({
161
+ contract: orders,
162
+ store: redis({ url: process.env.REDIS_URL }),
163
+ scheduler,
164
+ handlers: {
165
+ /* ... */
166
+ },
167
+ })
168
+ ```
169
+
170
+ `@upstash/qstash` 2.11.0 or newer is a peer dependency. Mount
171
+ `scheduler.handler(...)` in the same `app/api/a2/scheduler/route.ts`, and set
172
+ `QSTASH_TOKEN`, `QSTASH_CURRENT_SIGNING_KEY`, and `QSTASH_NEXT_SIGNING_KEY`.
173
+
174
+ - **Callback URL:** inferred from `PORT` locally, and from
175
+ `VERCEL_PROJECT_PRODUCTION_URL` or `VERCEL_BRANCH_URL` on Vercel. Pass `url`
176
+ anywhere else.
177
+ - **Local development:** A2 starts QStash's development server on the first
178
+ publish or verification, with no QStash variables needed.
179
+ - **Security:** the route verifies `Upstash-Signature` before touching the
180
+ store and answers unsigned traffic with 401. Behind Deployment Protection,
181
+ pass an automation bypass as a callback header.
182
+ - **Outages:** delivery is at least once and A2 keeps recovery going, but when
183
+ QStash can't reach the route at all, redrive its DLQ.
184
+
185
+ The [adapter reference](/reference/adapters#experimental-a2scheduler-qstash)
186
+ covers the options, protected previews, Edge runtimes, and body limits.
187
+
188
+ ### More than one scheduler
189
+
190
+ Servers with different scheduler instances need separate routes, even from the
191
+ same adapter. Each extra QStash instance passes its own `url`. Each Vercel
192
+ Queues instance needs its own `topic` and a matching `experimentalTriggers`
193
+ entry.
194
+
195
+ ### Dispatch in a fresh invocation
196
+
197
+ Ordinary `append` starts handlers inline and uses the scheduler as a watchdog.
198
+ `append.dispatch` persists the input and hands the pending work straight to the
199
+ queue instead:
200
+
201
+ ```ts app/api/imports/route.ts
202
+ import { ordersServer } from '@/server'
203
+
204
+ export async function POST(request: Request) {
205
+ const { orderId, shopId, items } = await request.json()
206
+ await ordersServer.session(orderId).append.dispatch({
207
+ id: `created:${orderId}`,
208
+ type: 'created',
209
+ payload: { shopId, items },
210
+ })
211
+ return Response.json({ accepted: true }, { status: 202 })
212
+ }
213
+ ```
214
+
215
+ It requires a scheduler. The call awaits the queue send and starts no inline
216
+ drain. If the send fails, the event may already be durable; its explicit id
217
+ makes the retry idempotent. Dispatch controls the wakeup, not which worker
218
+ claims the event.
219
+
220
+ ### Running without a scheduler
221
+
222
+ If no event type has a handler and nothing calls `session.schedule`, skip
223
+ `scheduler`. Events settle in their append transaction, with no drains, claims,
224
+ or queue messages. That's a complete production configuration: a durable event
225
+ log for history, reducers, and live sync.
226
+
227
+ With handlers, skipping it means the only wakeups are a top-level append or an
228
+ explicit `server.drain()`. Reads never wake a session, so one nobody wakes
229
+ stays stuck. That's fine for internal tools where "heals on the next write" is
230
+ acceptable. For production, configure a scheduler.
231
+
232
+ ## 3. When an event dead-letters
233
+
234
+ A [dead-lettered](/server/guarantees#when-a-handler-keeps-failing) event waits
235
+ for you. List a session's dead letters, then resolve each one:
236
+
237
+ ```ts
238
+ // anywhere on the server, behind your own operator checks:
239
+ import { ordersServer } from '@/server'
240
+
241
+ const order = ordersServer.session(orderId)
242
+ const [dead] = await order.deadLetters()
243
+ if (dead) {
244
+ // dead.lastError says why; deploy the handler fix first, then:
245
+ await order.redrive(dead.index, { action: 'retry' })
246
+ }
247
+ ```
248
+
249
+ - **Fix and retry.** `{ action: 'retry' }` runs the handler again with a fresh
250
+ ten-failure budget. Once it completes, its lane continues.
251
+ - **Skip.** `{ action: 'skip' }` marks the event processed without running it,
252
+ accepting that its effects never happen. The next event in its lane runs.
253
+
254
+ Each redrive is one atomic store operation, and it wakes the session the way an
255
+ append does, so the lane resumes without another write. An event that isn't
256
+ dead-lettered anymore is left alone: `redrive` reports `{ redriven: false }`.
257
+
258
+ ## 4. Deploy safely
259
+
260
+ The Vercel adapter's `deploymentId` applies to every task: recovery, scheduled
261
+ appends, and scheduled clears. Omit it to use the publishing deployment, pass
262
+ a deployment ID to target one, or pass `null` for deploymentless delivery.
263
+ Publishing and consuming clients must use the same scope.
264
+
265
+ - **Pinned tasks stay put.** Promoting a new deployment doesn't move them;
266
+ older deployments keep receiving their own work until it is acknowledged or
267
+ expires. Upgrading doesn't repair an older deployment's callback handler.
268
+ - **Deploymentless doesn't mean latest.** Vercel picks an eligible consumer,
269
+ not the latest production deployment. Every eligible consumer must use
270
+ `deploymentId: null` and stay compatible with pending tasks and session work.
271
+ - **Changing the setting doesn't move queued tasks.** Keep consumers for the
272
+ original scope until those tasks finish.
273
+
274
+ QStash's inferred production and branch URLs are moving aliases, so pending
275
+ tasks can reach a later deployment. Keep task envelopes and event schemas
276
+ compatible across deployments, and keep the same `maxBodyBytes` on every
277
+ deployment that can receive the callback: lowering it mid-rollout can reject
278
+ tasks the previous deployment accepted.
279
+
280
+ :::warning
281
+ Don't roll back to an A2 version without timer delivery while scheduled tasks
282
+ are outstanding.
283
+ An old route can acknowledge a new append task as a recovery message without
284
+ writing its events. Rolling back past scheduled clears fails too: an old
285
+ Vercel Queues route keeps failing a clear task until retention ends, and an
286
+ old QStash route rejects it as non-retryable, moving it to QStash's
287
+ dead-letter queue. Roll forward, or wait for those tasks to deliver or remove
288
+ them through the provider first.
289
+ :::
290
+
291
+ ## Watch it in traces
292
+
293
+ ```ts
294
+ // server/orders.ts, with telemetry:
295
+ import { createServer } from 'experimental-a2/server'
296
+ import { otel } from 'experimental-a2/otel'
297
+ import { orders } from '@/contracts'
298
+
299
+ export const ordersServer = createServer({
300
+ contract: orders,
301
+ // ...store, scheduler, and handlers as above
302
+ telemetry: otel(),
303
+ })
304
+ ```
305
+
306
+ `telemetry` takes an instrumentation hook; `experimental-a2/otel` implements
307
+ it over OpenTelemetry (`@opentelemetry/api` is a peer dependency). With any
308
+ OTel setup registered (on Vercel, `@vercel/otel`), one trace shows the request,
309
+ `a2.append`, the inline `a2.drain`, an `a2.event` span per handler, and the
310
+ appends those handlers make. Failures mark their `a2.event` span,
311
+ `a2.event.attempt` reports `ctx.attempt`, and `a2.event.outcome =
312
+ dead_lettered` is the attribute to alert on. Agent servers add `a2.ai.*`
313
+ spans. See the [span catalogue](/reference/adapters#the-span-catalogue).
314
+
315
+ ## Checklist
316
+
317
+ | Piece | Done when |
318
+ | ------------------- | -------------------------------------------------------------------- |
319
+ | Store | `store: redis(...)` (or `postgres(...)`) on every server |
320
+ | Scheduler | for servers with handlers or timers: one shared adapter, route, and provider config |
321
+ | Idempotent handlers | external side effects take `event.id` as an idempotency key |
322
+ | Client split | contracts/reducers isomorphic; only `experimental-a2/server` touches backends |
@@ -0,0 +1,123 @@
1
+ ---
2
+ title: Guarantees
3
+ description: Appends are effectively-once, handlers are at-least-once, and retries keep event identity across crashes.
4
+ ---
5
+
6
+ ## In short
7
+
8
+ - **Appends are effectively-once.** Event ids deduplicate writes. A batch commits
9
+ before its handlers start; they may start before `append` resolves.
10
+ - **Handlers are at-least-once.** They may run again after a crash, so
11
+ [side effects use idempotency keys](/server/handlers#handlers-can-run-twice).
12
+ - **The log stays ordered.** Every session has one immutable append order.
13
+ - **Handlers are concurrent by default.** Log order does not serialize them.
14
+ Only events in the same lane run one at a time in log order.
15
+
16
+ ## One event, many attempts
17
+
18
+ An event is pending until A2 marks it processed. Every wakeup (an append, a
19
+ scheduler watchdog, or `server.drain()`) claims the session's eligible events,
20
+ and the log decides what remains. Reads never dispatch handlers.
21
+
22
+ Each claim expires. If its holder dies, the claim lapses and the next wakeup
23
+ claims the event again with a larger `ctx.attempt`. The retry gets the same
24
+ event id, index, type, and payload. A kill after the claim can use up an
25
+ attempt before your code runs, but it doesn't count as a caught failure. An
26
+ older attempt can never overwrite a newer one's completion or failure.
27
+
28
+ :::note[Recovery is not an event]
29
+ A2 does not append `recovered` or `continued`. Recovery retries the same event.
30
+ Your log contains only application facts.
31
+ :::
32
+
33
+ ## Immediate and dependent events
34
+
35
+ [`ctx.session.append(name, ...events)`](/server/handlers#append-what-should-happen-now)
36
+ commits immediately: its events stay in the log if the parent later crashes,
37
+ and run concurrently with it unless both share a lane.
38
+ [Returned events](/server/handlers#return-what-happens-after-success) commit
39
+ with the parent's completion, in one transaction.
40
+
41
+ If the handler throws or dies before that transaction, neither the completion
42
+ nor the returned events exist, and the retry produces the same deterministic
43
+ ids. If the transaction commits but its acknowledgement is lost, the retry gets
44
+ the committed rows back. A partial or changed batch is rejected.
45
+
46
+ The transaction covers only A2's log. An HTTP call, database write, or other
47
+ external effect can't join it, so a crash after the effect but before
48
+ completion repeats it. Use `event.id` as an idempotency key or add fencing in
49
+ the external system.
50
+
51
+ ## Lanes
52
+
53
+ [Lanes](/server/handlers#lanes) are session-scoped serial groups. Only the
54
+ lowest-index unfinished event in a lane is eligible, so a failure blocks later
55
+ events in that lane, not the whole session. Unlaned events and different lanes
56
+ run concurrently. A2 stores the lane at append, so a changed lane resolver
57
+ affects only new events.
58
+
59
+ A lane controls A2 dispatch, not the outside world. A2 claims every eligible
60
+ event, with no global concurrency limit or priority queue. Put capacity limits
61
+ near the constrained resource, such as an API client, connection pool, or
62
+ runtime.
63
+
64
+ ## Append and scheduling
65
+
66
+ A top-level append commits before any wakeup. A2 then starts the handlers
67
+ inline and, with a scheduler, arms a watchdog in parallel. Handlers never wait
68
+ for the queue, and a handler may start before `append` returns. `append` waits
69
+ up to two seconds for the watchdog send, never for handlers. A failed arm
70
+ records `a2.append.armed = false` on the trace. Handler appends and returned
71
+ events join the running drain and add no arm.
72
+
73
+ [`append.dispatch`](/server/production#dispatch-in-a-fresh-invocation) skips
74
+ the inline drain and hands the work to the scheduler instead.
75
+
76
+ An event type without a handler settles in the append transaction. It starts
77
+ no drain or arm unless older handled work in the session is still pending.
78
+
79
+ ## Causal trees are durable
80
+
81
+ Every event a handler appends or returns stores a `cause`: the triggering
82
+ event's index and dispatch attempt. Top-level appends store `null`. Children
83
+ of one handler are siblings, nested handlers add levels, and a retried child
84
+ keeps its original cause.
85
+
86
+ ## A chain that crosses the function timeout
87
+
88
+ A function killed mid-handler restarts that handler from its entry point in a
89
+ fresh invocation: same event, new attempt, no caught failure. Events already
90
+ processed stay processed, so a chain of handlers can outlast one invocation,
91
+ but each handler must fit in a fresh one. Split longer work into smaller
92
+ events or increase the function duration. On Vercel, A2 caps the claim window
93
+ at the function deadline when the platform exposes it, so a retry starts
94
+ promptly after a timeout.
95
+
96
+ ## When a handler keeps failing
97
+
98
+ A caught failure counts against that event, releases its claim, and leaves
99
+ other eligible events running. A2 retries it with backoff. Ten caught failures
100
+ dead-letter the event: later events in its lane stay blocked, while unlaned
101
+ events and other lanes continue. Hard kills don't consume this budget.
102
+ The event waits until you
103
+ [retry or skip it](/server/production#3-when-an-event-dead-letters) with
104
+ `session.redrive()`.
105
+
106
+ ## Limits
107
+
108
+ | Situation | Result |
109
+ | --- | --- |
110
+ | Process dies before the first arm is durable | Retry with the same ids, append again later, or call `drain()`. |
111
+ | No scheduler configured | A later top-level append or explicit `drain()` wakes the session. Reads never do. |
112
+ | Scheduler invocation dies after claiming but before arming | Its unacknowledged queue delivery is the slower fallback. |
113
+ | Store backend unavailable | Safe progress stops until it returns. |
114
+ | External effect succeeds before A2 completion | The handler may repeat it. Use idempotency or external fencing. |
115
+
116
+ ## What append never throws for
117
+
118
+ A failing handler never makes `append` throw. Handler failures are retried and,
119
+ if persistent, [dead-letter the event](#when-a-handler-keeps-failing).
120
+
121
+ Append success means the event is durable, not that its handlers finished. An
122
+ identical batch retry returns rows with the same ids. A mixed batch throws
123
+ [`PARTIAL_DUPLICATE_BATCH`](/reference/errors).