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,855 @@
1
+ ---
2
+ title: Server
3
+ description: "`experimental-a2/server`: `createServer`, its route, the store and scheduler adapter interfaces, and every session method."
4
+ ---
5
+
6
+ ## `experimental-a2/server`
7
+
8
+ ### `handler(entry)`
9
+
10
+ Normalizes a handler entry so you can compose an existing handler without
11
+ branching on its shape. For ordinary use, define handlers inline in
12
+ `createServer({ handlers })`.
13
+
14
+ - Accepts a function or `{ handler, lane?, abortOn? }` and returns the object
15
+ shape: a function becomes `{ handler: entry }`, an object is returned
16
+ unchanged, and both keep their original types.
17
+ - Does not bind a contract; `createServer({ handlers })` checks the match.
18
+ Annotate a new handler's context, as below, or pass an already typed
19
+ function.
20
+ - Spreading the result keeps `lane` and `abortOn`; set either after the spread
21
+ to override it. Changing a built-in AI handler's lane can separate it from
22
+ the handlers it coordinates with.
23
+ - A composition runs in one handler attempt. Work after the original resolves
24
+ still runs before its returned events commit, a throw fails the composed
25
+ attempt, and a retry can run both again.
26
+
27
+ ```ts server/composed-orders.ts
28
+ import * as a2 from 'experimental-a2'
29
+ import { createServer, handler, type HandlerContext } from 'experimental-a2/server'
30
+ import { z } from 'zod'
31
+
32
+ const orders = a2.contract({
33
+ name: 'composed-orders',
34
+ events: {
35
+ created: z.object({ shopId: z.string() }),
36
+ notified: z.object({ shopId: z.string() }),
37
+ },
38
+ })
39
+
40
+ const created = handler({
41
+ lane: 'orders',
42
+ handler: async (ctx: HandlerContext<typeof orders.events, 'created'>) => {
43
+ return orders.batch({ type: 'notified', payload: ctx.event.payload })
44
+ },
45
+ })
46
+
47
+ export const ordersServer = createServer({
48
+ contract: orders,
49
+ handlers: {
50
+ created: {
51
+ ...created,
52
+ handler: async (ctx) => {
53
+ const result = await created.handler(ctx)
54
+ // Your additional work goes here:
55
+ // await notifyShop(ctx.event.payload, { idempotencyKey: ctx.event.id })
56
+ return result
57
+ },
58
+ },
59
+ },
60
+ })
61
+ ```
62
+
63
+ ### `createServer(options)`
64
+
65
+ ```ts
66
+ createServer(options: {
67
+ contract: Contract // the vocabulary this server implements
68
+ store?: A2Store // default: sqlite in dev, memory in tests, required in prod
69
+ scheduler?: A2Scheduler
70
+ telemetry?: A2Telemetry // optional instrumentation; see experimental-a2/otel
71
+ presence?: { ttlMs?: number } // presence expiry policy; default 60s
72
+ handlers?: {
73
+ [type]:
74
+ | Handler
75
+ | {
76
+ abortOn?: AbortSpec
77
+ lane?: string | ((ctx: LaneContext) => string)
78
+ handler: Handler
79
+ }
80
+ }
81
+ }): A2Server
82
+
83
+ type Handler = (
84
+ ctx: Context,
85
+ ) => Promise<void | AppendInput | readonly AppendInput[]>
86
+ ```
87
+
88
+ Implements a contract: binds its vocabulary to storage and reactions.
89
+ `experimental-a2/server` is the only entry point that can reach a store
90
+ backend; under the browser condition its exports resolve to a loud error. See
91
+ [Handlers](/server/handlers).
92
+
93
+ | Option | Behavior |
94
+ | --- | --- |
95
+ | `handlers` | One table, complete at construction. Compose files by spreading objects in; a duplicate key silently last-wins. |
96
+ | `lane` | Within one session, same-lane events run one at a time in log order; all others run concurrently. A resolver receives `{ sessionId, event }`. The value is resolved and stored at append, so a later deployment can't reinterpret pending work. See [Lanes](/server/handlers#lanes). |
97
+ | `abortOn` | Events that fire `ctx.signal` while the handler runs: an array matches by type; an object maps types to predicates `(event, trigger, { attempt })`, whose `attempt` is the trigger's durable attempt, so a cancellation can fence one recovered run. See [Cancellation](/server/cancellation). |
98
+ | `presence.ttlMs` | How long a presence value survives without a refreshing set: a positive integer of milliseconds, default 60 seconds, counted on the storage's clock, never the sender's stamp. Only for contracts that declare `presence`; elsewhere construction throws a `TypeError`. |
99
+
100
+ ```ts
101
+ generate: {
102
+ abortOn: {
103
+ cancelled: (event, trigger, { attempt }) =>
104
+ event.payload.of === `${trigger.id}:${attempt}`,
105
+ },
106
+ handler: async (ctx) => { /* ... */ },
107
+ }
108
+ ```
109
+
110
+ - Handlers are concurrent by default. A2 starts every eligible event once its
111
+ append commits.
112
+ - A handler returns nothing, one event, or an array. A2 marks the trigger
113
+ processed and appends the returned batch in one atomic store operation; when
114
+ the handler throws, the returned events never exist.
115
+ - Handlers are optional per event type. An event type without one settles in
116
+ its append transaction with no dispatch attempt, and starts no drain or
117
+ scheduler arm when the session has no older pending handler work. See
118
+ [Events without handlers](/server/handlers#events-without-handlers).
119
+
120
+ The context every handler receives:
121
+
122
+ | Property | Meaning |
123
+ | --- | --- |
124
+ | `ctx.event` | `Event`: the triggering event |
125
+ | `ctx.attempt` | durable 1-based dispatch claim ordinal; increments on every durable claim and may skip when a process dies before handler entry |
126
+ | `ctx.session` | this session: `id` (equal to `ctx.event.sessionId`), `append`, `schedule`, `clear`, `history`, `state`, `stream`, `setPresence` |
127
+ | `ctx.signal` | `AbortSignal`: fires on lease lapse (`CLAIM_EXPIRED`), proven supersession (`SUPERSEDED_ATTEMPT`), and matching `abortOn` events |
128
+
129
+ `ctx.session` shares the `server.session(id)` surface; handler-specific
130
+ behavior is noted under each method. Its `state(reducer)` folds through the
131
+ trigger's index by default (see
132
+ [`session.state`](#sessionstatereducer-options)), and its `append` takes a
133
+ name:
134
+
135
+ ```ts
136
+ ctx.session.append(name: string, ...events: AppendInput[]): Promise<Event[]>
137
+ ```
138
+
139
+ - It commits immediately, so its events may run while the handler is still
140
+ active unless a lane orders them. See
141
+ [Append what should happen now](/server/handlers#append-what-should-happen-now).
142
+ - The name is the idempotency key, scoped to the trigger, for events whose
143
+ `id` is omitted: each such id derives from the trigger id, the name, and the
144
+ event's position. The same name and generated-id batch returns the existing
145
+ rows on retry; a changed batch conflicts with them.
146
+ - An explicit `id` wins, so different triggering events can converge on one
147
+ fact. Root `server.session(id).append(...events)` takes no name.
148
+
149
+ ### `server.fetch(request, options?)`
150
+
151
+ ```ts
152
+ server.fetch(
153
+ request: Request,
154
+ options?: {
155
+ authorize?: (operation: A2Operation) => boolean | Promise<boolean>
156
+ upgradeWebSocket?: (
157
+ attach: (socket: A2Socket) => void,
158
+ ) => Response | Promise<Response>
159
+ multiplexWebSocket?: boolean
160
+ },
161
+ ): Promise<Response>
162
+
163
+ type A2Operation =
164
+ | {
165
+ type: 'stream'
166
+ sessionId: string
167
+ startAfter: number
168
+ transport: 'http' | 'websocket'
169
+ }
170
+ | {
171
+ type: 'history'
172
+ sessionId: string
173
+ gte: number
174
+ lte: number
175
+ transport: 'http'
176
+ }
177
+ | {
178
+ type: 'push'
179
+ sessionId: string
180
+ events: readonly {
181
+ type: ContractEventType | string
182
+ payload: unknown
183
+ id?: string
184
+ }[]
185
+ presence?: {
186
+ participant: string
187
+ values: Readonly<Record<string, unknown>>
188
+ seen?: number
189
+ at?: number
190
+ }
191
+ transport: 'http' | 'websocket'
192
+ }
193
+ ```
194
+
195
+ The bound Fetch API route for this server: an unprotected route exports it
196
+ directly as `GET` and `POST`. See [Routes](/server/routes).
197
+
198
+ | Request | Response |
199
+ | --- | --- |
200
+ | `GET` | streams one session over SSE, resumed after the `index` query parameter |
201
+ | `GET` with `gte` and `lte` | the closed history range as JSON; `a2-history-covered` is `true` when every requested index was returned. If the range includes the latest clear marker, `a2-clear-index` is set and coverage counts only the requested indexes after it; a range entirely below the marker returns `[]` |
202
+ | `GET` with `Upgrade: websocket` | calls `upgradeWebSocket`, or returns 426 without it |
203
+ | `POST` | accepts `{ sessionId, events, presence? }` and returns the appended events |
204
+
205
+ A contract that declares presence streams it and accepts presence pushes
206
+ automatically.
207
+
208
+ | Option | Behavior |
209
+ | --- | --- |
210
+ | `authorize` | Runs after the request or socket frame is parsed, before schema validation, presence validation, or I/O. Event and presence field names are typed from the contract; values stay `unknown`. Operation objects, push events, event arrays, and presence patches are frozen: authorize them, don't rewrite them. `false` denies with `FORBIDDEN`; a throw becomes `STORE_UNAVAILABLE`. |
211
+ | `upgradeWebSocket` | receives an `attach` callback for the platform socket and returns the upgrade response |
212
+ | `multiplexWebSocket` | `true` shares one socket across sessions, with `multiplex: true` in the client's `ws` API |
213
+
214
+ - Authentication stays in the route, where headers, cookies, and framework
215
+ context are available. Capture the authenticated principal in `authorize`.
216
+ - `authorize` is an ingress policy for this `fetch` call, not a global server
217
+ policy: direct `session.append`, handler appends, scheduled appends, and
218
+ server-side `setPresence` bypass it.
219
+ - By default the `sessionId` and `index` query parameters bind one session to
220
+ the socket. A2 authorizes the stream before upgrading, and frames can't
221
+ select another session.
222
+ - A multiplexed socket runs `authorize` for each session subscribe and each
223
+ event or presence push. Authenticate the physical WebSocket GET before
224
+ calling `server.fetch`, and validate `Origin` when browser cookies
225
+ authenticate it (handshakes skip the CORS preflight). Enable multiplexing
226
+ only when that request context may access every session `authorize` accepts.
227
+ - A denied subscribe receives `unsubscribed` and leaves other sessions alone. A
228
+ denied event push receives a `FORBIDDEN` ack. Denied presence is silently
229
+ dropped, because it is fire-and-forget.
230
+ - SSE and WebSocket lifetimes use the platform's ambient invocation deadline,
231
+ and background work uses its ambient `waitUntil`. Neither is an option.
232
+
233
+ ### `server.session(id)`
234
+
235
+ ```ts
236
+ server.session(id: string): Session
237
+ ```
238
+
239
+ A handle on one instance of the contract: the unit of log ordering, lane keys,
240
+ recovery, state, and live sync. Creating it does no I/O; nothing loads until
241
+ you append, read, or stream. `session.id` is the `id` passed in. See
242
+ [Sessions](/contract/sessions).
243
+
244
+ It has every [session method](#session), plus
245
+ [`deadLetters()`](#sessiondeadletters) and
246
+ [`redrive()`](#sessionredriveindex-options), which `ctx.session` doesn't.
247
+
248
+ ### `server.drain(sessionId)`
249
+
250
+ ```ts
251
+ server.drain(sessionId: string): Promise<{ settled: boolean }>
252
+ ```
253
+
254
+ Claims every eligible event and runs their handlers concurrently.
255
+ `settled` means no actionable or live-claimed work remains. A dead-lettered
256
+ event can block later work in its lane while other lanes continue, until
257
+ [`session.redrive()`](#sessionredriveindex-options) resolves it. Scheduler
258
+ callbacks use it; you'll rarely call it yourself. The public result stays this
259
+ simple boolean.
260
+
261
+ ### `A2Store`
262
+
263
+ Custom store adapters implement these atomic drain methods:
264
+
265
+ ```ts
266
+ // Custom server store adapter:
267
+ type EventCause = {
268
+ index: number
269
+ attempt: number
270
+ batchSize?: number // present on named handler appends
271
+ }
272
+
273
+ type AppendEvent = {
274
+ type: string
275
+ payload: unknown
276
+ id?: string
277
+ cause?: EventCause
278
+ lane?: string
279
+ settled?: true // internal: no handler is registered for this event type
280
+ }
281
+
282
+ type ReturnedEvent = AppendEvent & { id: string }
283
+
284
+ type StoredEvent = Event & {
285
+ cause: EventCause | null
286
+ lane: string | null
287
+ processedAt: Date | null
288
+ processedByAttempt: number | null
289
+ returnedEventIds: string[] | null
290
+ firstClaimedAt: Date | null
291
+ lastClaimedAt: Date | null
292
+ attemptCount: number
293
+ claimHolder: string | null
294
+ claimExpiresAt: Date | null
295
+ failureCount: number
296
+ lastFailedAt: Date | null
297
+ lastFailedAttempt: number | null
298
+ lastError: string | null
299
+ failedAt: Date | null
300
+ clearIndex?: number // on claimed events, once the session has been cleared
301
+ }
302
+
303
+ type StoreAppendResult = {
304
+ events: StoredEvent[]
305
+ hasPending: boolean
306
+ }
307
+
308
+ type StoreClaimAvailableResult =
309
+ | { outcome: 'claimed'; events: StoredEvent[] }
310
+ | { outcome: 'busy'; retryAt: Date }
311
+ | { outcome: 'settled' }
312
+
313
+ type CompleteAttemptResult =
314
+ | { outcome: 'completed'; events: StoredEvent[] }
315
+ | { outcome: 'superseded' }
316
+
317
+ interface A2Store {
318
+ append(
319
+ sessionId: string,
320
+ events: AppendEvent[],
321
+ options?: { originIndex?: number },
322
+ ): Promise<StoreAppendResult>
323
+
324
+ // optional together; required for session.clear()
325
+ clear?(options: {
326
+ sessionId: string
327
+ ifLastIndex?: number
328
+ cause?: EventCause
329
+ }): Promise<{ cleared: boolean; index: number; clearIndex: number }>
330
+ reclaim?(sessionId: string): Promise<{ deleted: number; done: boolean }>
331
+
332
+ // optional together; required for session.deadLetters() and redrive()
333
+ deadLetters?(sessionId: string): Promise<StoredEvent[]>
334
+ redrive?(options: {
335
+ sessionId: string
336
+ index: number
337
+ action: 'retry' | 'skip'
338
+ }): Promise<{ redriven: boolean; hasPending: boolean }>
339
+
340
+ claimAvailable(options: {
341
+ sessionId: string
342
+ holder: string
343
+ ttlMs: number
344
+ expiresAtMs?: number
345
+ excludeIndexes?: readonly number[]
346
+ }): Promise<StoreClaimAvailableResult>
347
+
348
+ renewClaims(options: {
349
+ sessionId: string
350
+ holder: string
351
+ claims: readonly { index: number; attempt: number }[]
352
+ ttlMs: number
353
+ expiresAtMs?: number
354
+ }): Promise<{ renewed: number[]; superseded: number[] }>
355
+
356
+ completeAttempt(options: {
357
+ sessionId: string
358
+ index: number
359
+ attempt: number
360
+ events: ReturnedEvent[]
361
+ }): Promise<CompleteAttemptResult>
362
+
363
+ failAttempt(options: {
364
+ sessionId: string
365
+ index: number
366
+ attempt: number
367
+ error: string
368
+ maxFailures: number
369
+ }): Promise<{
370
+ outcome: 'failed' | 'dead_lettered' | 'superseded'
371
+ failureCount: number
372
+ }>
373
+ }
374
+ ```
375
+
376
+ | Method | Atomic effect |
377
+ | --- | --- |
378
+ | `append` | Write a consecutive batch and report session-wide pending handler work as `hasPending`. Events with the internal `settled` flag get `processedAt` in the same transaction, with no dispatch attempt. An event with a `cause` commits only while `cause.attempt` is the parent's latest attempt and the parent isn't dead-lettered; otherwise throw `SUPERSEDED_ATTEMPT`. The check serializes against a concurrent claim, after the idempotent-replay path. |
379
+ | `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`. |
380
+ | `renewClaims` | Extend the listed live claims the holder still owns. `renewed` lists those; `superseded` lists claims whose event's `attemptCount` passed the listed attempt. An expired claim no successor took is in neither and may still complete; renewal never revives it. |
381
+ | `completeAttempt` | In one transaction: fence on the current attempt, mark the parent processed, store its exact ordered `returnedEventIds`, and append the returned batch. A same-attempt retry returns the committed children; a stale attempt returns `superseded`. |
382
+ | `failAttempt` | Record one current caught failure, clear its claim, and dead-letter at `maxFailures`. A repeated acknowledgment is idempotent; a stale attempt returns `superseded`. |
383
+ | `clear` | Serialized with `append`. Check `ifLastIndex` against the head and the latest marker, then append a settled clear marker at the next index, touching no existing rows. A `cause` is fenced like a handler append. |
384
+ | `reclaim` | Delete one bounded batch of rows below the latest clear marker, with their bookkeeping, event-id records, lane pointers, and cached state. Report how many rows went, and `done` when none are left. Never wait on a lock other methods hold. |
385
+ | `deadLetters` | Return the unprocessed events with `failedAt` set above the latest clear marker, oldest first. |
386
+ | `redrive` | Only for an unprocessed event with `failedAt` set above the latest clear marker. `retry` clears `failedAt` and `failureCount`, leaving claims and the last failure. `skip` sets `processedAt` with no `processedByAttempt` or `returnedEventIds` and exposes the next event in its lane. Anything else reports `redriven: false` and writes nothing. Both serialize with claims and handler appends, `skip` also with lane promotion; report `hasPending` like `append`. |
387
+
388
+ `readHistory(sessionId, options)` is an optional capability with the same
389
+ exclusive `afterIndex` and inclusive `throughIndex` bounds as `read()`. Without
390
+ it, `session.history()` falls back to `read()`.
391
+
392
+ A store that supports clearing treats every event at or below the latest
393
+ marker as superseded, whether or not its row still exists. Each check belongs
394
+ inside the operation's existing atomic step:
395
+
396
+ - `claimAvailable` never claims it; lane readiness at append and completion
397
+ ignores it, so a dead-lettered lane head stops blocking.
398
+ - `renewClaims` reports it in `superseded`; `completeAttempt` and
399
+ `failAttempt` return `superseded`.
400
+ - An `append` whose `cause` names it throws `SUPERSEDED_ATTEMPT`; one whose
401
+ `originIndex` is at or below the marker throws `SESSION_CLEARED`.
402
+ - Reads start at the marker; `readState` returns its index as `clearIndex`,
403
+ and claimed events carry it as `clearIndex`.
404
+ - In `append` and `completeAttempt`, an event id whose row is below this
405
+ session's marker is free to reuse.
406
+
407
+ Without `clear` and `reclaim`, `session.clear()` and `session.schedule.clear()`
408
+ throw a `TypeError` before writing anything. Without `deadLetters` and
409
+ `redrive`, so do `session.deadLetters()` and `session.redrive()`. All built-in
410
+ stores support both.
411
+
412
+ Stored fields:
413
+
414
+ - `attemptCount` counts claims, including abandoned ones. `failureCount`
415
+ counts caught failures and dead-letters at ten. See
416
+ [Guarantees](/server/guarantees#one-event-many-attempts).
417
+ - `cause` names the event and handler attempt whose `ctx.session.append` or
418
+ return value first persisted the child; `null` means a top-level event.
419
+ - `lane` is the session-scoped serialized group, resolved before append.
420
+ - `returnedEventIds` makes a lost completion acknowledgment recoverable without
421
+ accepting a partial child batch.
422
+ - Lifecycle timestamps are adapter clock values for their atomic operations,
423
+ not exact database commit times. Built-in adapters persist all these fields
424
+ inside their existing atomic operations, with no extra round trip.
425
+
426
+ ### `A2Scheduler`
427
+
428
+ Custom scheduler adapters implement two methods:
429
+
430
+ | Method | Effect |
431
+ | --- | --- |
432
+ | `schedule(task: SchedulerTask): Promise<void>` | put one versioned task on durable delivery infrastructure |
433
+ | `handler(...servers)` | return the route that receives drain, append, and clear tasks; requires at least one drainable server and rejects duplicate contract names synchronously |
434
+
435
+ Application routes call `scheduler.handler(...servers)` on the configured
436
+ adapter. See
437
+ [Production](/server/production#2-add-a-scheduler-for-handlers-and-timers).
438
+
439
+ `SchedulerTask` is `SchedulerDrainTask | SchedulerAppendTask |
440
+ SchedulerClearTask`. These task types, plus `ScheduledEvent`, are exported
441
+ from `experimental-a2/server`.
442
+
443
+ | Task | Fields |
444
+ | --- | --- |
445
+ | `SchedulerDrainTask` | `version: 1`, `kind: 'drain'`, `contract`, `sessionId`, required epoch-ms `dueAt` |
446
+ | `SchedulerAppendTask` | `version: 1`, `kind: 'append'`, stable `id`, `contract`, `sessionId`, required epoch-ms `dueAt`, readonly `events`, optional `originIndex` |
447
+ | `SchedulerClearTask` | `version: 1`, `kind: 'clear'`, stable `id`, `contract`, `sessionId`, required epoch-ms `dueAt`, `ifLastIndex` |
448
+ | `ScheduledEvent` | fixed `id`, `type`, and JSON `payload` |
449
+
450
+ Drain tasks wake pending work and carry no application payload. Append tasks
451
+ carry the events for `session.schedule`; `originIndex` is the scheduling
452
+ handler's event, absent for root schedules. Clear tasks carry the condition for
453
+ `session.schedule.clear`.
454
+
455
+ Deliver an authenticated, normalized append or clear task through the server
456
+ helpers:
457
+
458
+ ```ts custom-scheduler.ts
459
+ import {
460
+ deliverSchedulerAppend,
461
+ deliverSchedulerClear,
462
+ type DrainableServer,
463
+ type SchedulerAppendTask,
464
+ type SchedulerClearTask,
465
+ } from 'experimental-a2/server'
466
+
467
+ export async function deliver(
468
+ server: DrainableServer,
469
+ task: SchedulerAppendTask | SchedulerClearTask,
470
+ ) {
471
+ if (task.kind === 'clear') await deliverSchedulerClear(server, task)
472
+ else await deliverSchedulerAppend(server, task)
473
+ }
474
+ ```
475
+
476
+ Authenticate the transport and validate the task envelope first. Each helper
477
+ verifies that the task and server contract match.
478
+
479
+ - `deliverSchedulerAppend` revalidates the carried event inputs and uses the
480
+ ordinary top-level append. The target server must have a scheduler. When
481
+ the append leaves pending handler work, it resolves only after a recovery
482
+ watchdog is durable, so a send failure keeps the delivery retryable. A task
483
+ whose `originIndex` was cleared resolves without appending.
484
+ - `deliverSchedulerClear` runs `clear({ ifLastIndex })` and resolves after its
485
+ deletion finishes. A store failure rejects, so the provider redelivers and
486
+ the retry finishes deleting.
487
+
488
+ ## Session
489
+
490
+ ### `session.append(...events)`
491
+
492
+ ```ts
493
+ session.append(
494
+ ...events: Array<{ type: string; payload: unknown; id?: string }>
495
+ ): Promise<Event[]>
496
+ ```
497
+
498
+ The only way to move a session forward. See [Guarantees](/server/guarantees).
499
+
500
+ - Validates payloads against the contract's schemas before writing anything.
501
+ - A multi-event append is atomic: all-or-nothing, consecutive positions, one
502
+ transaction.
503
+ - An explicit `id` makes it idempotent across retries; re-sending an identical
504
+ batch returns the original rows.
505
+ - Events pushed through `server.fetch` arrive through this same append.
506
+ - Only event types with registered handlers are dispatched; the append itself
507
+ settles the rest. An unhandled append still starts session healing when
508
+ older handled work is pending. The store reports that pending state in the
509
+ atomic append, so the decision needs no follow-up read.
510
+
511
+ ### `session.append.dispatch(...events)`
512
+
513
+ ```ts
514
+ session.append.dispatch(
515
+ ...events: Array<{ type: string; payload: unknown; id?: string }>
516
+ ): Promise<Event[]>
517
+ ```
518
+
519
+ Commits the same atomic batch, then sends pending work to the configured
520
+ scheduler instead of starting an inline drain. See
521
+ [Dispatch in a fresh invocation](/server/production#dispatch-in-a-fresh-invocation).
522
+
523
+ - Awaits acceptance of an immediate scheduler message. A batch that leaves no
524
+ pending handler work sends none.
525
+ - Throws before writing when the server has no scheduler.
526
+ - If the send fails, the events are already durable: give them explicit IDs
527
+ and retry the same dispatch.
528
+ - It picks how the session wakes, not which worker runs: an already active
529
+ drain may claim the new work first.
530
+ - Exists only on a server session's top-level append. `ctx.session.append`
531
+ runs inside an active drain, where its immediate and returned children
532
+ become eligible, so it has no `dispatch`.
533
+
534
+ ### `session.schedule(name, timing, ...events)`
535
+
536
+ ```ts
537
+ session.schedule(
538
+ name: string,
539
+ timing: ScheduleTiming, // { delay: ScheduleDelay } | { at: Date }
540
+ ...events: Array<{ type: string; payload: unknown; id?: string }>
541
+ ): Promise<void>
542
+ ```
543
+
544
+ Appends the events later through the configured scheduler, from root and
545
+ handler sessions. Resolves after provider acceptance, not after append. `name`
546
+ must be non-empty, with at least one event. `ScheduleDelay`, `ScheduleTiming`,
547
+ and `SessionSchedule` are exported from `experimental-a2/server`. See
548
+ [Timers](/server/timers) for stale-timer guards, provider limits, and
549
+ deployment compatibility.
550
+
551
+ | Timing | Meaning |
552
+ | --- | --- |
553
+ | `{ delay: '30s' }` | relative delay with unit `ms`, `s`, `m`, `h`, or `d`; from the call's wall clock on a root session, from the trigger's durable `createdAt` in a handler, so handler retries keep the delivery time |
554
+ | `{ at: Date }` | absolute delivery time; use it when a retried root call needs one target |
555
+
556
+ - The template type checks the unit. At runtime the number must be an
557
+ unsigned base-10 decimal without leading zeros, finite, and above zero; `0s`,
558
+ signs, exponents, malformed durations, invalid dates, and both fields at
559
+ once are rejected.
560
+ - The target is the earliest useful delivery time. Adapters encode it in
561
+ whole-second slots; an already-due target is sent immediately.
562
+ - The task id derives from the contract, session, and name. Handler tasks add
563
+ the trigger id and, once the session has been cleared, the clear, so a
564
+ trigger id reused in a new lifetime schedules a new task. Omitted event ids
565
+ derive from the task id and position; explicit ids win.
566
+ - Reuse a name only for identical timing and events. Provider deduplication is
567
+ an optimization; stable event ids make later duplicate deliveries converge in
568
+ the store.
569
+ - Payloads are snapshotted into plain JSON, then validated before sending.
570
+ Encode dates as ISO strings and classes as plain data. `undefined`, bigint,
571
+ functions, symbols, cycles, accessors, and non-plain prototypes are rejected;
572
+ shared references become independent values.
573
+ - Delivery validates the snapshotted schema input through an ordinary
574
+ top-level append, outside any handler's completion transaction, so a schema
575
+ transform produces the stored payload from the original input. The events
576
+ have `cause: null` and run through handlers, lanes, state, and streams
577
+ normally.
578
+ - A handler task belongs to its trigger's lifetime: if the session is cleared
579
+ before delivery, it is acknowledged without appending. A root task joins
580
+ whichever lifetime exists at delivery, and its name identifies one task
581
+ across lifetimes, so put something from the new lifetime in the name when
582
+ scheduling again after a clear.
583
+ - There is no cancellation, reschedule, cron, or timer-listing API.
584
+
585
+ ### `session.clear(options?)`
586
+
587
+ ```ts
588
+ session.clear(options?: { ifLastIndex?: number }): Promise<{
589
+ cleared: boolean
590
+ index: number
591
+ }>
592
+ ```
593
+
594
+ Ends the session's current lifetime: one atomic step checks the condition and
595
+ appends a reserved clear marker at the next index. The session id stays usable
596
+ and indexes never restart. See
597
+ [Clearing a session](/contract/sessions#clearing-a-session).
598
+
599
+ `ifLastIndex` is a non-negative safe integer. The first matching rule decides,
600
+ and `index` is always the head after the call:
601
+
602
+ | When | Result |
603
+ | --- | --- |
604
+ | the latest marker is at `ifLastIndex + 1` | `{ cleared: true, index }`, a retry of that clear |
605
+ | `ifLastIndex` is given and is not the head | `{ cleared: false, index }`, nothing changes |
606
+ | the head is `0` or already a clear marker | `{ cleared: true, index }`, nothing changes |
607
+ | otherwise | `{ cleared: true, index }`, the new marker |
608
+
609
+ - Every event at or below the marker is superseded: never dispatched again,
610
+ never blocking its lane. Its running handler's appends throw
611
+ `SUPERSEDED_ATTEMPT` and its state reads throw `SESSION_CLEARED` rather than
612
+ fold the new lifetime. Exception: after a clear from another instance, a
613
+ read bounded at the handler's own event may return that event's state from
614
+ memory until its `ctx.signal` aborts.
615
+ - Pushes and appends after the marker start the new lifetime.
616
+ - `clear()` then deletes the old events, their bookkeeping and event-id
617
+ records, and cached state below the marker in short batches, resolving when
618
+ they're gone. Other work on the session runs between batches. There is no
619
+ background sweep.
620
+ - If deletion fails, the call rejects with the session already cleared; any
621
+ later clear of the session, including a retry, finishes the deletion.
622
+ - Old event ids can be used again. Presence is not cleared.
623
+ - On `ctx.session`, the clear is fenced like the handler's appends and ends the
624
+ handler's own attempt: `ctx.signal` aborts with `SUPERSEDED_ATTEMPT` and
625
+ returned events are discarded. Its deletion keeps running under the
626
+ platform's `waitUntil`.
627
+ - All built-in stores support clearing.
628
+
629
+ ### `session.schedule.clear(name, options)`
630
+
631
+ ```ts
632
+ session.schedule.clear(
633
+ name: string,
634
+ options: ({ delay: ScheduleDelay } | { at: Date }) & {
635
+ ifLastIndex: number
636
+ },
637
+ ): Promise<void>
638
+ ```
639
+
640
+ Publishes a conditional clear to the configured scheduler and resolves after
641
+ provider acceptance. `name` is non-empty and `ifLastIndex` is required; timing
642
+ works as in `session.schedule`, with the same provider limits. See
643
+ [Scheduling a clear](/contract/sessions#scheduling-a-clear).
644
+
645
+ - The task id derives from the contract, session, name, and condition, plus
646
+ the trigger id and any earlier clear inside a handler. Reuse a name only for
647
+ the same timing.
648
+ - Delivery runs `clear({ ifLastIndex })` and acknowledges after the deletion
649
+ finishes. No event is appended; a stale or duplicate delivery changes
650
+ nothing. Indexes never repeat, so a condition from an older lifetime never
651
+ matches a newer one.
652
+ - `ifLastIndex: ctx.event.index` matches only while the trigger is still the
653
+ head. The handler's own appends and returned events move the head, so a
654
+ handler that writes should use the index of its last `ctx.session.append`.
655
+ Actor handlers always commit state, so they can't use the trigger's index.
656
+ - A delay short enough to deliver while the handler still runs can match
657
+ before its writes land; the clear then ends the handler's attempt and
658
+ discards its returned events.
659
+ - Each delivery attempt resumes the deletion. Vercel Queues redelivers until
660
+ retention ends. QStash retries up to its `retries` budget; a deletion that
661
+ outlasts them leaves the message in QStash's dead-letter queue, the rest of
662
+ the old history still invisible, and any later clear of the session finishes
663
+ it.
664
+
665
+ ### `session.deadLetters()`
666
+
667
+ ```ts
668
+ session.deadLetters(): Promise<DeadLetter[]>
669
+
670
+ type DeadLetter = Event & {
671
+ lane: string | null
672
+ attemptCount: number
673
+ failureCount: number
674
+ lastError: string | null
675
+ failedAt: Date
676
+ }
677
+ ```
678
+
679
+ The session's dead-lettered events, oldest first, each with why it failed. See
680
+ [When an event dead-letters](/server/production#3-when-an-event-dead-letters).
681
+
682
+ - `failureCount` counts the caught failures that spent its budget;
683
+ `attemptCount` also counts claims abandoned by hard kills. `lastError` is the
684
+ last failure, stringified.
685
+ - Events at or below the latest clear marker are never listed.
686
+ - Only on `server.session(id)`. `DeadLetter` is exported from
687
+ `experimental-a2/server`.
688
+
689
+ ### `session.redrive(index, options)`
690
+
691
+ ```ts
692
+ session.redrive(
693
+ index: number,
694
+ options: { action: 'retry' | 'skip' },
695
+ ): Promise<{ redriven: boolean }>
696
+ ```
697
+
698
+ Resolves the dead-lettered event at `index` in one atomic store operation.
699
+
700
+ | Action | Effect |
701
+ | --- | --- |
702
+ | `retry` | The handler runs again as a new attempt, with a fresh ten-failure budget. Once it completes, its lane continues. |
703
+ | `skip` | The event is marked processed without running its handler, and the next event in its lane runs. Its effects never happen. |
704
+
705
+ - `index` is a positive safe integer and `action` is required; anything else
706
+ throws a `TypeError` before any I/O.
707
+ - `redriven` is `false`, with nothing written, when the event isn't
708
+ dead-lettered: still pending, already processed, already redriven, missing,
709
+ or at or below the latest clear marker. Calling it again is safe.
710
+ - Afterwards, unfinished work wakes the session as an append does: an inline
711
+ drain, plus a scheduler watchdog when one is configured, joined for up to
712
+ `schedulerArmTimeoutMs`. A failed send doesn't reject the call; it marks the
713
+ `a2.redrive` span and leaves later appends, `server.drain()`, or another
714
+ `redrive` to wake the session.
715
+ - A retried event keeps its `attemptCount` and last failure, so the next run's
716
+ `ctx.attempt` keeps counting. A skipped event keeps `failedAt` and has no
717
+ `processedByAttempt`.
718
+ - Only on `server.session(id)`. `SessionRedriveOptions` and
719
+ `SessionRedriveResult` are exported from `experimental-a2/server`.
720
+
721
+ ### `session.history()`
722
+
723
+ ```ts
724
+ session.history(options?: { gte?: number; lte?: number }): Promise<Event[]>
725
+ ```
726
+
727
+ Raw events, oldest first, without dispatch metadata. See
728
+ [History](/contract/state#history).
729
+
730
+ - `gte` and `lte` are inclusive, non-negative safe-integer indexes; omit one to
731
+ leave that end open. `lte: 0` returns `[]`; `gte > lte` throws.
732
+ - After a clear, history starts after the marker; the marker itself is not
733
+ returned.
734
+
735
+ ### `session.state(reducer, options?)`
736
+
737
+ ```ts
738
+ session.state(
739
+ reducer: Reducer<S>,
740
+ options?: { through?: number | 'latest' },
741
+ ): Promise<{ state: S; index: number; clearIndex?: number }>
742
+ ```
743
+
744
+ The log folded through one committed prefix. `index` is its exact boundary, an
745
+ append-order cursor rather than a handler-settlement marker; hand it to the
746
+ client to resume there. See [State](/contract/state).
747
+
748
+ - Omitting `through` reads the latest committed frontier on a root session,
749
+ and through `ctx.event.index` on `ctx.session`, inclusive and stable across
750
+ concurrent appends and retries. In a handler, use `{ through: 'latest' }`
751
+ only when later committed events are intentionally part of the decision.
752
+ - A numeric `through` is an inclusive, non-negative safe integer;
753
+ `through: 0` returns the reducer's initial state.
754
+ - A latest read and a following append are not atomic. See
755
+ [State](/contract/state#reducers) for joins that converge.
756
+ - Never dispatches handlers or waits for pending work. Snapshot write-back
757
+ uses platform `waitUntil`, and its failures never fail the completed read:
758
+ they go to `a2.snapshot` telemetry or the default console sink, without
759
+ snapshot contents.
760
+ - After a clear, the result includes `clearIndex`, the latest marker, and
761
+ state folds from there; forward the whole result when hydrating a client. A
762
+ positive `through` below `clearIndex` throws `SESSION_CLEARED`; `through: 0`
763
+ returns the seed without `clearIndex`.
764
+ - On `ctx.session`, every state read throws `SESSION_CLEARED` once the trigger
765
+ has been cleared, including `{ through: 'latest' }`. Handler `history()` and
766
+ `stream()` are not fenced.
767
+ - Default and numeric handler reads retain their exact folded checkpoint until
768
+ the trigger completes or dead-letters and no other unfinished handler uses
769
+ it. Handler `{ through: 'latest' }` reads and root reads retain nothing.
770
+ There are no retention options. A numeric read behind the latest snapshot
771
+ can use the greatest eligible retained checkpoint. See
772
+ [Snapshots are a cache](/contract/state#snapshots-are-a-cache).
773
+ - Operational snapshot-read failures keep existing A2 errors and turn others
774
+ into `STORE_UNAVAILABLE` with the original error as `cause`, without falling
775
+ back to individual reads or raw history. Cache misses and `stateSchema`
776
+ rejections rebuild from the log.
777
+
778
+ ### `session.stream(options?)`
779
+
780
+ ```ts
781
+ session.stream(options?: { startAfter?: number }): AsyncIterable<Event | SessionCleared>
782
+ session.stream(options: {
783
+ startAfter?: number
784
+ presence: true
785
+ }): AsyncIterable<Event | SessionCleared | PresencePatch | PresenceSnapshot>
786
+ ```
787
+
788
+ A live feed of the session's events. Server-side only; `server.fetch` exposes
789
+ it over SSE and WebSocket. Subscribing never dispatches handlers.
790
+
791
+ - `startAfter` is a non-negative safe integer; `startAfter: 20` begins with
792
+ event 21.
793
+ - A clear arrives in order as a `SessionCleared` marker (`type: 'a2.cleared'`).
794
+ A stream that opens below the latest marker begins with it. A running stream
795
+ delivers the log in order, so events from before the clear can arrive ahead
796
+ of the marker. Reset when it arrives. Clearing never closes a stream.
797
+ - With `presence: true`, the feed first yields one `PresenceSnapshot`,
798
+ `{ snapshot }`: the current pruned map with each field's `value`, `seen`, and
799
+ `at`. Live `PresencePatch` values, `{ participant, values, seen, at }`, then
800
+ interleave with events; `values` is typed by the contract's presence schemas,
801
+ and `null` marks a cleared field. Over SSE both are named frames, which
802
+ clients that don't know them skip.
803
+ - The return type widens only under the literal `presence: true`. The option
804
+ exists only on sessions of contracts that declare `presence`; elsewhere it is
805
+ a type error, not a runtime throw.
806
+
807
+ ### `session.setPresence(patch)`
808
+
809
+ ```ts
810
+ session.setPresence(patch: {
811
+ participant: string
812
+ values: { [field]: payload | null } // partial; null clears a field
813
+ seen?: number
814
+ at?: number // the sender's LWW stamp, epoch ms; receipt time when absent
815
+ }): Promise<void>
816
+ ```
817
+
818
+ Validates each field against the contract's `presence` schemas, then
819
+ broadcasts. No log row, no dispatch, no scheduler arm; `history()` and
820
+ `state()` never see it. See [Presence](/client/presence).
821
+
822
+ - Last writer wins per participant per field, ordered by `at`, the sender's
823
+ epoch-ms stamp; the browser client stamps its sends automatically. Without
824
+ `at`, receipt time stands in: a stampless caller is a single writer, so
825
+ receipt order is its send order.
826
+ - A bad payload throws `INVALID_PAYLOAD`, an unknown field throws
827
+ `UNKNOWN_PRESENCE_FIELD`, and a non-finite or negative `at` throws a
828
+ `TypeError`.
829
+ - `seen` is the sender's stream frontier, which view code uses to reconcile
830
+ presence with the fold. See
831
+ [Reconciling the planes](/client/presence#reconciling-the-planes-seen).
832
+ - Values expire by backend TTL when a participant goes silent (default 60
833
+ seconds; see [`presence.ttlMs`](#createserveroptions)). Expiry counts on the
834
+ storage clock, so a hostile stamp can only vandalize its own field, which
835
+ still expires on schedule.
836
+ - The patch is the push envelope's `presence` sibling; `server.fetch`'s push
837
+ lane forwards it whole to `session.setPresence(presence)`.
838
+ - `ctx.session.setPresence(...)` is the same operation; `seen` defaults to the
839
+ triggering event's `index`.
840
+
841
+ ### `session.presence()`
842
+
843
+ ```ts
844
+ session.presence(): Promise<{
845
+ [participant: string]: {
846
+ [field: string]: { value: unknown; seen: number; at: Date }
847
+ }
848
+ }>
849
+ ```
850
+
851
+ The current presence map, expired values pruned: a point-in-time read for
852
+ server renders. Live consumers use `stream({ presence: true })`. Each entry's
853
+ `at` is the sender's stamp for when the value was set, the last-writer-wins
854
+ comparator; how long a value
855
+ stays painted is view logic, decided at render time.