experimental-a2 0.20.0 → 0.22.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 (309) hide show
  1. package/CHANGELOG.md +55 -0
  2. package/README.md +1 -1
  3. package/dist/{actor-C7b-1arn.d.ts → actor-CqibLgjQ.d.ts} +15 -7
  4. package/dist/actor-CqibLgjQ.d.ts.map +1 -0
  5. package/dist/actor-client.d.ts +1 -1
  6. package/dist/actor-client.js +2 -2
  7. package/dist/actor-react.d.ts +3 -3
  8. package/dist/actor-react.js +5 -5
  9. package/dist/{actor-shared-t7w8xIKP.js → actor-shared-DFET5q16.js} +3 -3
  10. package/dist/actor-shared-DFET5q16.js.map +1 -0
  11. package/dist/actor.d.ts +1 -1
  12. package/dist/actor.js +54 -43
  13. package/dist/actor.js.map +1 -1
  14. package/dist/{ai-Cz3hqVhk.d.ts → ai-DAu77CDr.d.ts} +8 -8
  15. package/dist/{ai-Cz3hqVhk.d.ts.map → ai-DAu77CDr.d.ts.map} +1 -1
  16. package/dist/{ai-mMiWul1T.js → ai-EEfKyc-z.js} +9 -9
  17. package/dist/ai-EEfKyc-z.js.map +1 -0
  18. package/dist/ai-client.d.ts +2 -2
  19. package/dist/ai-client.js +2 -2
  20. package/dist/ai-fx.d.ts +4 -2
  21. package/dist/ai-fx.d.ts.map +1 -1
  22. package/dist/ai-fx.js +214 -67
  23. package/dist/ai-fx.js.map +1 -1
  24. package/dist/{ai-harness-CV6rXbB3.d.ts → ai-harness-DTjkAr2T.d.ts} +2 -2
  25. package/dist/{ai-harness-CV6rXbB3.d.ts.map → ai-harness-DTjkAr2T.d.ts.map} +1 -1
  26. package/dist/{ai-harness-Clbg5ei7.js → ai-harness-dQeJaR1t.js} +7 -3
  27. package/dist/ai-harness-dQeJaR1t.js.map +1 -0
  28. package/dist/ai-pi.d.ts +1 -1
  29. package/dist/ai-pi.js +3 -3
  30. package/dist/ai-pi.js.map +1 -1
  31. package/dist/ai-react.d.ts +5 -5
  32. package/dist/ai-react.js +2 -2
  33. package/dist/ai-server.d.ts +14 -17
  34. package/dist/ai-server.d.ts.map +1 -1
  35. package/dist/ai-server.js +86 -73
  36. package/dist/ai-server.js.map +1 -1
  37. package/dist/ai.d.ts +2 -2
  38. package/dist/ai.js +2 -2
  39. package/dist/{client-BhHLoNKF.js → client-BQS4i_V9.js} +29 -6
  40. package/dist/client-BQS4i_V9.js.map +1 -0
  41. package/dist/{client-h_DKFAjj.d.ts → client-CUMbS_38.d.ts} +3 -3
  42. package/dist/{client-h_DKFAjj.d.ts.map → client-CUMbS_38.d.ts.map} +1 -1
  43. package/dist/client.d.ts +3 -3
  44. package/dist/client.js +1 -1
  45. package/dist/{contract-BYKASlCV.js → contract-CWukt01d.js} +2 -2
  46. package/dist/contract-CWukt01d.js.map +1 -0
  47. package/dist/index.d.ts +5 -5
  48. package/dist/index.js +3 -3
  49. package/dist/{initial-snapshot-Bx-c0pDP.d.ts → initial-snapshot-D3Lve43o.d.ts} +4 -4
  50. package/dist/initial-snapshot-D3Lve43o.d.ts.map +1 -0
  51. package/dist/{internal-CscSY7K_.js → internal-CvsrprnU.js} +40 -16
  52. package/dist/internal-CvsrprnU.js.map +1 -0
  53. package/dist/otel.d.ts +1 -1
  54. package/dist/otel.js +1 -1
  55. package/dist/otel.js.map +1 -1
  56. package/dist/{react-BBXgHWFc.d.ts → react-BoHvC3v9.d.ts} +4 -4
  57. package/dist/{react-BBXgHWFc.d.ts.map → react-BoHvC3v9.d.ts.map} +1 -1
  58. package/dist/react.d.ts +1 -1
  59. package/dist/react.js +1 -1
  60. package/dist/{reducer-CqA59Q6U.d.ts → reducer-CYPHsSmd.d.ts} +2 -2
  61. package/dist/{reducer-CqA59Q6U.d.ts.map → reducer-CYPHsSmd.d.ts.map} +1 -1
  62. package/dist/{reducer-BAXc8JyF.js → reducer-CjmvZSAb.js} +2 -2
  63. package/dist/{reducer-BAXc8JyF.js.map → reducer-CjmvZSAb.js.map} +1 -1
  64. package/dist/runtime-consumer-CIeL33wj.js +12 -0
  65. package/dist/runtime-consumer-CIeL33wj.js.map +1 -0
  66. package/dist/scheduler-qstash.d.ts +2 -2
  67. package/dist/scheduler-qstash.js +3 -3
  68. package/dist/scheduler-task-X1SToDjP.js.map +1 -1
  69. package/dist/scheduler-vercel.d.ts +2 -2
  70. package/dist/scheduler-vercel.js +3 -3
  71. package/dist/scheduler-vercel.js.map +1 -1
  72. package/dist/{server-qyb5aJJG.js → server-DtFrWxi3.js} +412 -116
  73. package/dist/server-DtFrWxi3.js.map +1 -0
  74. package/dist/{server-DYAATFuT.d.ts → server-qgez0Xny.d.ts} +108 -41
  75. package/dist/server-qgez0Xny.d.ts.map +1 -0
  76. package/dist/server.browser.js +1 -1
  77. package/dist/server.browser.js.map +1 -1
  78. package/dist/server.d.ts +4 -4
  79. package/dist/server.js +2 -2
  80. package/dist/{session-clear-C-QwTXV1.d.ts → session-clear-D1PFe7xl.d.ts} +2 -2
  81. package/dist/session-clear-D1PFe7xl.d.ts.map +1 -0
  82. package/dist/{session-clear-DGI4jTQ5.js → session-clear-wHd5GCpZ.js} +5 -4
  83. package/dist/session-clear-wHd5GCpZ.js.map +1 -0
  84. package/dist/store-D0MhBvg6.d.ts +532 -0
  85. package/dist/store-D0MhBvg6.d.ts.map +1 -0
  86. package/dist/store-N8PXxDAS.js.map +1 -1
  87. package/dist/store-codec-DwrWlgQx.js +65 -0
  88. package/dist/store-codec-DwrWlgQx.js.map +1 -0
  89. package/dist/store-memory.d.ts +1 -1
  90. package/dist/store-memory.d.ts.map +1 -1
  91. package/dist/store-memory.js +406 -225
  92. package/dist/store-memory.js.map +1 -1
  93. package/dist/{store-polling-7WJV0YWP.js → store-polling-BZt2Rf3D.js} +2 -2
  94. package/dist/{store-polling-7WJV0YWP.js.map → store-polling-BZt2Rf3D.js.map} +1 -1
  95. package/dist/store-postgres.d.ts +1 -1
  96. package/dist/store-postgres.d.ts.map +1 -1
  97. package/dist/store-postgres.js +1598 -587
  98. package/dist/store-postgres.js.map +1 -1
  99. package/dist/store-presence-polling-JuAy2Ck2.js +111 -0
  100. package/dist/store-presence-polling-JuAy2Ck2.js.map +1 -0
  101. package/dist/store-redis-http.d.ts +1 -1
  102. package/dist/store-redis-http.js +4 -4
  103. package/dist/store-redis-notify-CsJw_hfD.js +3060 -0
  104. package/dist/store-redis-notify-CsJw_hfD.js.map +1 -0
  105. package/dist/store-redis.d.ts +1 -1
  106. package/dist/store-redis.d.ts.map +1 -1
  107. package/dist/store-redis.js +4 -5
  108. package/dist/store-redis.js.map +1 -1
  109. package/dist/store-sqlite.d.ts +1 -1
  110. package/dist/store-sqlite.d.ts.map +1 -1
  111. package/dist/store-sqlite.js +527 -299
  112. package/dist/store-sqlite.js.map +1 -1
  113. package/dist/stream-buffer-DrTyiIuo.js +57 -0
  114. package/dist/stream-buffer-DrTyiIuo.js.map +1 -0
  115. package/dist/{telemetry-Cyfqo6Qt.d.ts → telemetry-CWwsh-Gx.d.ts} +3 -3
  116. package/dist/telemetry-CWwsh-Gx.d.ts.map +1 -0
  117. package/dist/{wire-BKxH1vbP.js → wire-CV-Z7wz-.js} +6 -5
  118. package/dist/wire-CV-Z7wz-.js.map +1 -0
  119. package/docs/01-quickstart.mdx +21 -15
  120. package/docs/02-examples.mdx +1 -1
  121. package/docs/actors/01-introduction.mdx +13 -2
  122. package/docs/actors/02-concurrency.mdx +1 -1
  123. package/docs/agents/01-introduction.mdx +1 -1
  124. package/docs/agents/02-tools.mdx +12 -11
  125. package/docs/agents/03-conversations.mdx +36 -25
  126. package/docs/agents/04-harnesses.mdx +14 -7
  127. package/docs/client/03-presence.mdx +18 -14
  128. package/docs/contract/01-events.mdx +2 -2
  129. package/docs/contract/02-state.mdx +18 -14
  130. package/docs/contract/03-sessions.mdx +32 -25
  131. package/docs/index.mdx +29 -24
  132. package/docs/reference/01-core.mdx +7 -6
  133. package/docs/reference/02-server.mdx +250 -191
  134. package/docs/reference/04-ai.mdx +33 -23
  135. package/docs/reference/05-telemetry.mdx +27 -13
  136. package/docs/reference/06-errors.mdx +25 -19
  137. package/docs/reference/meta.ts +1 -1
  138. package/docs/schedulers/01-overview.mdx +8 -8
  139. package/docs/schedulers/02-vercel-queues.mdx +2 -2
  140. package/docs/schedulers/03-qstash.mdx +4 -4
  141. package/docs/server/01-consumers.mdx +418 -0
  142. package/docs/server/02-timers.mdx +31 -21
  143. package/docs/server/03-cancellation.mdx +52 -44
  144. package/docs/server/04-routes.mdx +4 -1
  145. package/docs/server/05-sql-projections.mdx +391 -0
  146. package/docs/server/06-guarantees.mdx +61 -51
  147. package/docs/stores/01-overview.mdx +3 -3
  148. package/docs/stores/02-redis.mdx +24 -10
  149. package/docs/stores/03-redis-http.mdx +8 -6
  150. package/docs/stores/04-postgres.mdx +65 -14
  151. package/docs/stores/05-sqlite.mdx +5 -3
  152. package/docs/upgrades/01-overview.mdx +31 -0
  153. package/docs/upgrades/78-0-22.mdx +115 -0
  154. package/docs/upgrades/79-0-21.mdx +165 -0
  155. package/docs/upgrades/80-0-20.mdx +43 -0
  156. package/docs/upgrades/81-0-19.mdx +33 -0
  157. package/docs/upgrades/82-0-18.mdx +33 -0
  158. package/docs/upgrades/83-0-17.mdx +69 -0
  159. package/docs/upgrades/meta.ts +3 -0
  160. package/examples/playground/DEPLOY.md +39 -7
  161. package/examples/playground/app/a0/chat.test.tsx +2 -2
  162. package/examples/playground/app/a0/sandbox-log.ts +17 -15
  163. package/examples/playground/app/a0/sandbox.ts +58 -13
  164. package/examples/playground/app/a0/server.ts +4 -4
  165. package/examples/playground/app/a2slack/a2slack.css +1067 -0
  166. package/examples/playground/app/a2slack/c/[channel]/page.tsx +20 -0
  167. package/examples/playground/app/a2slack/consumers/agent.ts +63 -0
  168. package/examples/playground/app/a2slack/consumers/feed.ts +164 -0
  169. package/examples/playground/app/a2slack/consumers/notify.ts +116 -0
  170. package/examples/playground/app/a2slack/consumers/search.ts +37 -0
  171. package/examples/playground/app/a2slack/consumers/triage.ts +31 -0
  172. package/examples/playground/app/a2slack/consumers/welcome.ts +25 -0
  173. package/examples/playground/app/a2slack/db/a2.ts +209 -0
  174. package/examples/playground/app/a2slack/db/index.ts +73 -0
  175. package/examples/playground/app/a2slack/db/migrations/0000_extensions.sql +4 -0
  176. package/examples/playground/app/a2slack/db/migrations/0001_workspace.sql +74 -0
  177. package/examples/playground/app/a2slack/db/migrations/0002_a2.sql +215 -0
  178. package/examples/playground/app/a2slack/db/migrations/0003_a2_batch.sql +67 -0
  179. package/examples/playground/app/a2slack/db/migrations/0004_a2_session_event_ids.sql +2 -0
  180. package/examples/playground/app/a2slack/db/migrations/meta/0000_snapshot.json +18 -0
  181. package/examples/playground/app/a2slack/db/migrations/meta/0001_snapshot.json +500 -0
  182. package/examples/playground/app/a2slack/db/migrations/meta/0002_snapshot.json +1159 -0
  183. package/examples/playground/app/a2slack/db/migrations/meta/0003_snapshot.json +1159 -0
  184. package/examples/playground/app/a2slack/db/migrations/meta/0004_snapshot.json +1165 -0
  185. package/examples/playground/app/a2slack/db/migrations/meta/_journal.json +41 -0
  186. package/examples/playground/app/a2slack/db/queries.ts +263 -0
  187. package/examples/playground/app/a2slack/db/schema.ts +145 -0
  188. package/examples/playground/app/a2slack/drills/a2slack.drill.ts +708 -0
  189. package/examples/playground/app/a2slack/drills/harness.ts +244 -0
  190. package/examples/playground/app/a2slack/events/route.ts +36 -0
  191. package/examples/playground/app/a2slack/identity.ts +44 -0
  192. package/examples/playground/app/a2slack/inbox/route.ts +8 -0
  193. package/examples/playground/app/a2slack/lag/data/route.ts +8 -0
  194. package/examples/playground/app/a2slack/lag/page.tsx +101 -0
  195. package/examples/playground/app/a2slack/lag.ts +73 -0
  196. package/examples/playground/app/a2slack/layout.tsx +43 -0
  197. package/examples/playground/app/a2slack/messages/route.ts +17 -0
  198. package/examples/playground/app/a2slack/model.test.ts +208 -0
  199. package/examples/playground/app/a2slack/model.ts +369 -0
  200. package/examples/playground/app/a2slack/models.test.ts +72 -0
  201. package/examples/playground/app/a2slack/page.tsx +10 -0
  202. package/examples/playground/app/a2slack/scheduler/route.ts +5 -0
  203. package/examples/playground/app/a2slack/search/route.ts +40 -0
  204. package/examples/playground/app/a2slack/search-index.ts +108 -0
  205. package/examples/playground/app/a2slack/server.test.ts +315 -0
  206. package/examples/playground/app/a2slack/server.ts +55 -0
  207. package/examples/playground/app/a2slack/sessions.ts +50 -0
  208. package/examples/playground/app/a2slack/thread/route.ts +8 -0
  209. package/examples/playground/app/a2slack/ui/ChannelView.tsx +87 -0
  210. package/examples/playground/app/a2slack/ui/Composer.tsx +117 -0
  211. package/examples/playground/app/a2slack/ui/DotsShadow.tsx +42 -0
  212. package/examples/playground/app/a2slack/ui/Feed.tsx +157 -0
  213. package/examples/playground/app/a2slack/ui/InboxContext.tsx +63 -0
  214. package/examples/playground/app/a2slack/ui/InboxView.tsx +82 -0
  215. package/examples/playground/app/a2slack/ui/Menu.tsx +128 -0
  216. package/examples/playground/app/a2slack/ui/MessageRow.tsx +314 -0
  217. package/examples/playground/app/a2slack/ui/NameGate.tsx +35 -0
  218. package/examples/playground/app/a2slack/ui/SearchContext.tsx +146 -0
  219. package/examples/playground/app/a2slack/ui/SearchResults.tsx +97 -0
  220. package/examples/playground/app/a2slack/ui/Shell.tsx +34 -0
  221. package/examples/playground/app/a2slack/ui/Sidebar.tsx +199 -0
  222. package/examples/playground/app/a2slack/ui/ThreadPanel.tsx +134 -0
  223. package/examples/playground/app/a2slack/ui/useInbox.ts +71 -0
  224. package/examples/playground/app/agent/server.ts +3 -3
  225. package/examples/playground/app/chat/[chatId]/chat-client.tsx +1 -1
  226. package/examples/playground/app/chat/server.ts +45 -35
  227. package/examples/playground/app/components/demo-icons.tsx +7 -0
  228. package/examples/playground/app/components/demos.tsx +7 -0
  229. package/examples/playground/app/orders/[orderId]/order-client.tsx +2 -2
  230. package/examples/playground/app/orders/page.tsx +1 -1
  231. package/examples/playground/app/orders/server.ts +49 -30
  232. package/examples/playground/app/recovery/[recoveryId]/recovery-client.tsx +14 -15
  233. package/examples/playground/app/recovery/page.tsx +4 -4
  234. package/examples/playground/app/recovery/server.ts +23 -32
  235. package/examples/playground/drizzle.config.ts +14 -0
  236. package/examples/playground/lib/store.ts +1 -1
  237. package/examples/playground/next.config.ts +6 -1
  238. package/examples/playground/package.json +12 -1
  239. package/examples/playground/scripts/migrate.mjs +31 -0
  240. package/examples/playground/vercel.json +9 -0
  241. package/examples/playground/vitest.drills.config.ts +32 -0
  242. package/package.json +5 -2
  243. package/sql/postgres/drizzle-schema.ts +209 -0
  244. package/sql/postgres/schema.sql +227 -67
  245. package/src/actor-shared.ts +10 -2
  246. package/src/actor.ts +101 -71
  247. package/src/ai-agent.ts +162 -111
  248. package/src/ai-control-server.ts +8 -8
  249. package/src/ai-fx.ts +324 -82
  250. package/src/ai-harness.ts +17 -2
  251. package/src/ai-pi.ts +2 -2
  252. package/src/ai-server.ts +2 -2
  253. package/src/ai.ts +7 -7
  254. package/src/client.ts +19 -2
  255. package/src/contract.ts +1 -1
  256. package/src/deterministic-id.ts +27 -4
  257. package/src/errors.ts +8 -5
  258. package/src/idempotent-replay.ts +0 -1
  259. package/src/index.ts +1 -1
  260. package/src/internal.ts +57 -16
  261. package/src/otel.ts +1 -1
  262. package/src/postgres-schema.ts +98 -57
  263. package/src/presence-recovery.ts +18 -11
  264. package/src/presence-tombstones.ts +52 -0
  265. package/src/push-envelope.ts +20 -0
  266. package/src/runtime-consumer.ts +10 -0
  267. package/src/scheduler-task.ts +2 -2
  268. package/src/scheduler-vercel.ts +1 -1
  269. package/src/server.browser.ts +1 -1
  270. package/src/server.ts +819 -218
  271. package/src/session-socket.ts +1 -1
  272. package/src/store-codec.ts +66 -0
  273. package/src/store-memory.ts +604 -286
  274. package/src/store-postgres.ts +2006 -857
  275. package/src/store-presence-polling.ts +59 -34
  276. package/src/store-redis-core.ts +1961 -1333
  277. package/src/store-redis-notify.ts +76 -26
  278. package/src/store-redis.ts +2 -3
  279. package/src/store-sqlite.ts +915 -516
  280. package/src/store.ts +241 -102
  281. package/src/telemetry.ts +2 -1
  282. package/src/wire.ts +1 -0
  283. package/dist/actor-C7b-1arn.d.ts.map +0 -1
  284. package/dist/actor-shared-t7w8xIKP.js.map +0 -1
  285. package/dist/ai-harness-Clbg5ei7.js.map +0 -1
  286. package/dist/ai-mMiWul1T.js.map +0 -1
  287. package/dist/client-BhHLoNKF.js.map +0 -1
  288. package/dist/contract-BYKASlCV.js.map +0 -1
  289. package/dist/idempotent-replay-JdgMQRRm.js +0 -21
  290. package/dist/idempotent-replay-JdgMQRRm.js.map +0 -1
  291. package/dist/initial-snapshot-Bx-c0pDP.d.ts.map +0 -1
  292. package/dist/internal-CscSY7K_.js.map +0 -1
  293. package/dist/server-DYAATFuT.d.ts.map +0 -1
  294. package/dist/server-qyb5aJJG.js.map +0 -1
  295. package/dist/session-clear-C-QwTXV1.d.ts.map +0 -1
  296. package/dist/session-clear-DGI4jTQ5.js.map +0 -1
  297. package/dist/store-CyLUy86v.d.ts +0 -404
  298. package/dist/store-CyLUy86v.d.ts.map +0 -1
  299. package/dist/store-codec-DTG0Ftek.js +0 -10
  300. package/dist/store-codec-DTG0Ftek.js.map +0 -1
  301. package/dist/store-presence-polling-B1ixeqIS.js +0 -94
  302. package/dist/store-presence-polling-B1ixeqIS.js.map +0 -1
  303. package/dist/store-redis-notify-CM0AfLY0.js +0 -2544
  304. package/dist/store-redis-notify-CM0AfLY0.js.map +0 -1
  305. package/dist/stream-buffer-Cx7AAPGq.js +0 -23
  306. package/dist/stream-buffer-Cx7AAPGq.js.map +0 -1
  307. package/dist/telemetry-Cyfqo6Qt.d.ts.map +0 -1
  308. package/dist/wire-BKxH1vbP.js.map +0 -1
  309. package/docs/server/01-handlers.mdx +0 -227
@@ -5,7 +5,7 @@ description: Define a contract, react to its events, and make your first durable
5
5
 
6
6
  ## What you'll build
7
7
 
8
- An order that notifies a shop when it arrives: one contract, one handler, two
8
+ An order that notifies a shop when it arrives: one contract, one consumer, two
9
9
  routes, and a log you can read back.
10
10
 
11
11
  ## 1. Install
@@ -33,28 +33,34 @@ export const orders = a2.contract({
33
33
 
34
34
  ## 3. Implement it
35
35
 
36
- `createServer` binds the contract to storage and handlers. Same file, below
37
- the contract:
36
+ A consumer reacts to events, and `createServer` binds the contract to storage
37
+ and its consumers. Same file, below the contract:
38
38
 
39
39
  ```ts server/orders.ts
40
40
  import { createServer } from 'experimental-a2/server'
41
41
 
42
42
  export const ordersServer = createServer({
43
43
  contract: orders,
44
- handlers: {
45
- created: async ({ event }) => {
46
- // your side effect, e.g. email the shop, idempotent via event.id:
47
- // await sendEmailToShop(event.payload, { idempotencyKey: event.id })
48
- return { type: 'shop.notified', payload: {} }
44
+ consumers: [
45
+ {
46
+ name: 'notify-shop',
47
+ types: ['created'],
48
+ run: async ({ event }) => {
49
+ // your side effect, e.g. email the shop, idempotent via event.id:
50
+ // await sendEmailToShop(event.payload, { idempotencyKey: event.id })
51
+ return { type: 'shop.notified', payload: {} }
52
+ },
49
53
  },
50
- },
54
+ ],
51
55
  })
52
56
  ```
53
57
 
54
- - Handlers can run more than once (at-least-once delivery), so make external
58
+ - `name` is the consumer's identity: A2 keeps its place in every session under
59
+ it. `types` lists the events it runs on.
60
+ - Consumers can run more than once (at-least-once delivery), so make external
55
61
  calls safe to repeat. `event.id` is stable across re-runs: use it as the
56
62
  idempotency key.
57
- - The returned event commits atomically with handler success. If the handler
63
+ - The returned event commits atomically with the consumer's success. If `run`
58
64
  throws or the process dies first, `shop.notified` doesn't appear. Return one
59
65
  event or an array.
60
66
 
@@ -92,7 +98,7 @@ The payload is validated before anything is written; a bad one fails with
92
98
  `INVALID_PAYLOAD` and the issues.
93
99
 
94
100
  When `append` returns, the event is durably in the log and A2 has started the
95
- `created` handler inline. In production, a queue-backed scheduler wakes pending
101
+ `notify-shop` consumer inline. In production, a queue-backed scheduler wakes pending
96
102
  work after a failure or process death. See [Guarantees](/server/guarantees).
97
103
 
98
104
  ## 5. Read the log back
@@ -122,7 +128,7 @@ curl localhost:3000/api/orders/order-42
122
128
  ```
123
129
 
124
130
  Each event also carries `id`, `sessionId`, and `createdAt` (trimmed here). You
125
- appended one event and the handler returned the second. Debugging in A2 is
131
+ appended one event and the consumer returned the second. Debugging in A2 is
126
132
  reading the log.
127
133
 
128
134
  ## 6. Fold state out of the log
@@ -165,12 +171,12 @@ State isn't a second record to update. It's a fold over the log; see
165
171
 
166
172
  ## That's it
167
173
 
168
- You have a validated write path, a handler, a log you can read, and a view
174
+ You have a validated write path, a consumer, a log you can read, and a view
169
175
  folded from it. The rest of these docs apply the same two moves, append an
170
176
  event and react to it, to timers, cancellation, live UIs, and production.
171
177
 
172
178
  <CardGroup cols={3}>
173
- <Card title="Handlers" href="/server/handlers" icon="file">
179
+ <Card title="Consumers" href="/server/consumers" icon="file">
174
180
  Context, chaining, and re-runs.
175
181
  </Card>
176
182
  <Card title="Stores" href="/stores/overview" icon="rocket">
@@ -24,7 +24,7 @@ work inside the directory.
24
24
 
25
25
  - `app/counter` is the smallest complete contract, reducer, server, and React
26
26
  client.
27
- - `app/orders` adds handlers and a feature-owned scheduler.
27
+ - `app/orders` adds consumers and a feature-owned scheduler.
28
28
  - `app/chat` shows request-scoped authorization and durable AI generation.
29
29
  - `app/agent` shows tools, approvals, reminders, and sandboxed commands.
30
30
  - `app/canvas` adds optimistic state, presence, and WebSocket upgrades.
@@ -103,8 +103,11 @@ no retry. The message settles, and the caller receives
103
103
  backoff while the instance's lane waits; see
104
104
  [Errors](/reference/errors).
105
105
  - **A crash** re-runs the handler from the pre-message state. `ctx.id`
106
- is the message id, stable across re-runs: use it as the idempotency
107
- key for external I/O.
106
+ is the message id, stable across re-runs and unique within the
107
+ instance: use it as the idempotency key for external I/O. An explicit
108
+ message `id` that repeats in another instance repeats there too, so
109
+ derive explicit ids per instance, for example with
110
+ [`idempotentId()`](/reference/core#idempotentidparts).
108
111
  - **A clear** by `handle.session.clear()` before the answer rejects the
109
112
  call with an `A2Error` whose code is `SESSION_CLEARED`. The handler
110
113
  may already have run. Over HTTP it's a `410`.
@@ -153,3 +156,11 @@ be plain JSON: a `Date`, a `Map`, or `NaN` fails the commit loudly.
153
156
  streams, scheduling). The definition exposes its assembled `contract`,
154
157
  `server`, and `reducer`, and devtools show the full mailbox. Event
155
158
  names starting with `a2.` are reserved for the actor's own bookkeeping.
159
+
160
+ The server runs the handlers on two of A2's own
161
+ [consumers](/server/consumers): `a2.actor` for serial messages, on one
162
+ lane per instance, and `a2.actor.concurrent` for
163
+ [concurrent](/actors/concurrency#concurrent-handlers) ones. A message
164
+ that keeps failing dead-letters for one of them; list and redrive it
165
+ through `server.session(id)` like any
166
+ [dead letter](/server/consumers#resolve-a-dead-letter).
@@ -5,7 +5,7 @@ description: "Each instance processes one message at a time. Slow work goes in c
5
5
 
6
6
  ## One message at a time
7
7
 
8
- Every message runs on the instance's [lane](/server/handlers#lanes):
8
+ Every message runs on the instance's [lane](/server/consumers#lanes):
9
9
  one handler at a time, in arrival order. A handler sees the state every
10
10
  previous message produced, no other message can race its checks, and
11
11
  its changes commit before the next message runs.
@@ -22,7 +22,7 @@ export const assistant = agent({
22
22
  ```
23
23
 
24
24
  `agent()` is the whole server: the AI protocol, the agent's loop, and A2's
25
- store, scheduler, and handlers. `name` is its identity. The loop is the
25
+ store, scheduler, and consumers. `name` is its identity. The loop is the
26
26
  agent's `harness`. By default it's `{ type: 'ai-sdk' }`: each response runs as
27
27
  durable one-step `streamText()` generations, tuned with `harness.generation`
28
28
  and `maxSteps`, listed under [`agent()`](/reference/ai#agentoptions). The same
@@ -42,11 +42,11 @@ Approval policy lives in `harness.generation.toolApproval`, beside the other
42
42
  per-step settings. Later examples that import `assistant` use the agent from
43
43
  [Agents](/agents/introduction#define-the-agent).
44
44
 
45
- ### Read the handler context
45
+ ### Read the tool's context
46
46
 
47
47
  ```ts server/set-reminder.ts
48
48
  import { tool } from 'ai'
49
- import { handlerContext, inputs } from 'experimental-a2/ai'
49
+ import { consumerContext, inputs } from 'experimental-a2/ai'
50
50
  import { z } from 'zod'
51
51
 
52
52
  export const setReminder = tool({
@@ -56,7 +56,7 @@ export const setReminder = tool({
56
56
  reminder: z.string(),
57
57
  }),
58
58
  execute: async ({ delaySeconds, reminder }, { toolCallId }) => {
59
- const ctx = handlerContext()
59
+ const ctx = consumerContext()
60
60
  await ctx.session.schedule(
61
61
  `reminder:${toolCallId}`,
62
62
  { delay: `${delaySeconds}s` },
@@ -72,11 +72,12 @@ export const setReminder = tool({
72
72
  })
73
73
  ```
74
74
 
75
- `handlerContext()` returns the running tool's durable handler context: `event`,
76
- `attempt`, `session`, and `signal`. It works anywhere below `execute`, including
75
+ `consumerContext()` returns the running tool's
76
+ [consumer context](/server/consumers#the-context): `event`, `attempt`,
77
+ `session`, and `signal`. It works anywhere below `execute`, including
77
78
  awaited helpers, and throws outside a tool execution. Pass a shared
78
79
  `agentContract()` to type application events; A2 checks it against the
79
- executing agent. See [`handlerContext()`](/reference/ai#handlercontextcontract)
80
+ executing agent. See [`consumerContext()`](/reference/ai#consumercontextcontract)
80
81
  for the event payload.
81
82
 
82
83
  This tool schedules a reminder as a new user message. `ctx.session` belongs to
@@ -86,12 +87,12 @@ Naming the timer after `toolCallId` and the message after the triggering
86
87
  event's ID makes a retry converge on the same timer and message, even if a
87
88
  provider reuses tool-call IDs. The six-day maximum is Vercel Queues'
88
89
  [delay limit](/server/timers#provider-limits); adjust it for your scheduler. A
89
- transient or ambiguous scheduler failure retries the handler with the same
90
+ transient or ambiguous scheduler failure retries the tool's run with the same
90
91
  identity and target time; a deterministic rejection or a missing scheduler is
91
92
  an ordinary tool error.
92
93
 
93
- To test a context-reading tool, call it inside `runWithHandlerContext(ctx, fn)`
94
- from `experimental-a2/ai/server`; within `fn`, `handlerContext()` returns `ctx`.
94
+ To test a context-reading tool, call it inside `runWithConsumerContext(ctx, fn)`
95
+ from `experimental-a2/ai/server`; within `fn`, `consumerContext()` returns `ctx`.
95
96
 
96
97
  ### Answer approvals
97
98
 
@@ -125,8 +126,8 @@ request as `ai.approval.requested`, and the reducer adds it to
125
126
  `state.pendingApprovals`. `approve()` and `deny()` copy
126
127
  the request, message, generation, and approval IDs from it, so a delayed click
127
128
  cannot authorize a later model step. The decision updates the approval part in
128
- the message, and a built-in handler then runs the approved tool or records the
129
- denied result.
129
+ the message, and A2 then runs the approved tool or records the denied
130
+ result.
130
131
 
131
132
  A policy can also be a function that returns `'user-approval'` or
132
133
  `'approved'` from the validated input. It runs during the model step and again
@@ -50,12 +50,12 @@ Provider-executed tools, structured output, and forced tool choices skip
50
50
  automatic compaction, and configuring it for them throws; use custom callbacks.
51
51
 
52
52
  The first generation with each Gateway model in a session records
53
- `ai.model.metadata.requested`; a separate handler fetches the catalog and
54
- records `ai.model.metadata.resolved`, without delaying or failing the
55
- generation. `state.modelMetadata` holds each model's limits and status:
53
+ `ai.model.metadata.requested`; A2's `a2.ai.metadata` consumer fetches the
54
+ catalog and records `ai.model.metadata.resolved`, without delaying or failing
55
+ the generation. `state.modelMetadata` holds each model's limits and status:
56
56
  pending, resolved, or unavailable (absent from the catalog). A lookup that
57
- exhausted its retries stays pending without a new lookup each turn, so inspect
58
- that handler's failure or set a threshold. The estimate is serialized UTF-8
57
+ exhausted its retries stays pending without a new lookup each turn, so check
58
+ the session's dead letters for `a2.ai.metadata` or set a threshold. The estimate is serialized UTF-8
59
59
  bytes divided by four, calibrated by the previous same-model call's usage. It is
60
60
  not a tokenizer, makes no remote calls, and does not precisely measure new image
61
61
  or audio tokens.
@@ -191,11 +191,13 @@ transcript. The session's state is `AIState` merged with yours, so a key that
191
191
  shadows one of `AIState`'s is a type error. The reducer's `name` is its snapshot
192
192
  identity: rename it when `fold` changes.
193
193
 
194
- Serve it with `agent({ contract })`. Every resolver receives your state as
194
+ Serve it with `agent({ contract })`, and react to your own events with
195
+ `consumers`, next to the agent's. Every resolver receives your state as
195
196
  `state`:
196
197
 
197
198
  ```ts server/support-desk.ts
198
199
  import { agent } from 'experimental-a2/ai/server'
200
+ import { consumer } from 'experimental-a2/server'
199
201
  import { supportDesk } from '../support-desk'
200
202
 
201
203
  export const supportDeskAgent = agent({
@@ -204,12 +206,16 @@ export const supportDeskAgent = agent({
204
206
  state.plan === 'pro'
205
207
  ? { model: 'openai/gpt-5.6-terra', reasoning: 'high' }
206
208
  : { model: 'openai/gpt-5.6-luna', speed: 'fast' },
207
- handlers: {
208
- 'ticket.flagged': async ({ event }) => {
209
- // your side effect; event.id makes a stable idempotency key:
210
- // await notifyOnCall(event.payload.ticketId, { idempotencyKey: event.id })
209
+ consumers: [
210
+ {
211
+ name: 'page-on-call',
212
+ types: ['ticket.flagged'],
213
+ run: async ({ event }) => {
214
+ // your side effect; event.id makes a stable idempotency key:
215
+ // await notifyOnCall(event.payload.ticketId, { idempotencyKey: event.id })
216
+ },
211
217
  },
212
- },
218
+ ],
213
219
  })
214
220
  ```
215
221
 
@@ -277,7 +283,7 @@ export const POST = handle
277
283
  Server appends, such as
278
284
  `supportDeskAgent.session(id).append({ type: 'plan.changed', payload })`, are
279
285
  trusted and skip `authorize`. `push()` also accepts any `inputs` builder, and
280
- `handlerContext(supportDesk)` types application events inside tools.
286
+ `consumerContext(supportDesk)` types application events inside tools.
281
287
 
282
288
  The contract-free client, `createAgentClient()` or `useAgent({ api | client })`
283
289
  with a type-only import, folds only the built-in reducer. It accepts
@@ -285,10 +291,11 @@ with a type-only import, folds only the built-in reducer. It accepts
285
291
  no `reducer`; the types enforce this through `AI_STATE_REDUCER`.
286
292
 
287
293
  When the agent belongs inside something larger, such as a shared chat room,
288
- keep two sessions. The room's handler appends each message the agent should see
289
- to the agent's session with `inputs.message()`. Event IDs are unique across a
290
- store's sessions, so derive the message ID from the room event's ID with
291
- `idempotentId()`; a retried handler then converges on the same message. A tool
294
+ keep two sessions. A consumer of the room appends each message the agent should
295
+ see to the agent's session with `inputs.message()`. Derive the message ID from
296
+ the room event's ID with `idempotentId()`, so a retried run converges on the
297
+ same message. Event IDs are unique per session, so when several rooms feed one
298
+ agent session, include the room's session ID too. A tool
292
299
  the agent calls to speak appends to the room with an `idempotentId()` of its
293
300
  `toolCallId`. Neither fold reads the other's state.
294
301
 
@@ -369,8 +376,12 @@ completed input.
369
376
 
370
377
  ## Delivery semantics
371
378
 
372
- Model steps and tools run inside at-least-once A2 handlers. A successful
373
- completion commits atomically with its terminal events, and deterministic IDs
379
+ The agent runs on four of A2's own at-least-once
380
+ [consumers](/server/consumers): `a2.ai.control` applies commands and work
381
+ reports one at a time, `a2.ai.model` runs model steps on one lane, `a2.ai.tools` runs local
382
+ tools, and `a2.ai.metadata` looks up model limits. A dead letter names the one
383
+ that failed. A successful completion commits atomically with its terminal
384
+ events, and deterministic IDs
374
385
  make scheduling and lifecycle appends effectively once. An incomplete model
375
386
  attempt is marked `superseded` before its replacement starts. An expired claim
376
387
  or superseded attempt keeps its abort reason through model, resolver,
@@ -379,7 +390,7 @@ attempt; an explicit interruption stays terminal. A retried tool can stream
379
390
  different preliminary output under the same tool-call ID.
380
391
 
381
392
  Provider calls and external side effects sit outside that transaction, so
382
- recovery may repeat a call whose effect landed before its handler committed.
393
+ recovery may repeat a call whose effect landed before its run committed.
383
394
  Give side-effecting tools their own idempotency, normally keyed by the AI SDK
384
395
  `toolCallId`. A fast tool can also start before its model step fails; the
385
396
  failure blocks the continuation but cannot undo the effect.
@@ -499,11 +510,11 @@ full handoff rules.
499
510
  New-input commands from `inputs.message()`, `inputs.seed()`, and
500
511
  `inputs.steer()` use the message ID as their event ID, and hand-written commands
501
512
  must too. Caller IDs cannot start with the reserved `a2.ai:` prefix and must be
502
- unique across the store; include the contract and session when deriving them
503
- with `idempotentId()`. An identical submission replays without new work, even
504
- after removal or compaction. Reusing an ID with different content, action, or
505
- session rejects with `PARTIAL_DUPLICATE_BATCH`: use `queue.edit()` to change a
506
- pending input and a fresh ID for a new one.
513
+ unique within the session; the same ID in another session is another message.
514
+ An identical submission replays without new work, even after removal or
515
+ compaction. Reusing an ID in the session with different content or action
516
+ rejects with `PARTIAL_DUPLICATE_BATCH`: use `queue.edit()` to change a pending
517
+ input and a fresh ID for a new one.
507
518
 
508
519
  ### Optimistic conversation display
509
520
 
@@ -530,7 +541,7 @@ application, with `commandId` (the submitted event's ID) and an `outcome` of
530
541
  in flight, each action resolves to its persisted command events, whose `id` is
531
542
  the receipt's `commandId`, or to an empty array when there is nothing to
532
543
  address, such as Stop with no active turn. A rejection is a normal result and
533
- spends no handler retry.
544
+ spends no retry.
534
545
 
535
546
  ### Pause and resume
536
547
 
@@ -31,7 +31,7 @@ everything it can call:
31
31
  import { tool } from 'ai'
32
32
  import { Sandbox } from '@vercel/sandbox'
33
33
  import { z } from 'zod'
34
- import { handlerContext } from 'experimental-a2/ai'
34
+ import { consumerContext } from 'experimental-a2/ai'
35
35
  import { agent } from 'experimental-a2/ai/server'
36
36
  import { fx } from 'experimental-a2/ai/fx'
37
37
 
@@ -45,7 +45,7 @@ export const coder = agent({
45
45
  description: 'Run a shell command in the repository.',
46
46
  inputSchema: z.object({ command: z.string() }),
47
47
  execute: async ({ command }) => {
48
- const { session, signal } = handlerContext()
48
+ const { session, signal } = consumerContext()
49
49
  // One Vercel Sandbox per session, created on first use.
50
50
  const sandbox = await Sandbox.getOrCreate({
51
51
  name: session.id,
@@ -125,7 +125,8 @@ turn, so a process keeps the images it fetched instead of fetching them again.
125
125
 
126
126
  A file that isn't an image, or that A2 can't fetch, fails the turn with an
127
127
  error that names it. So does a steer with files under FX, which steers with
128
- text only. The harness applies its own rules to the images it gets: FX fails a
128
+ text only. A steer that joins the turn's prompt, as described below, sends its
129
+ files with the prompt. The harness applies its own rules to the images it gets: FX fails a
129
130
  turn whose model doesn't take images, and Pi resizes them. Every step sends the
130
131
  conversation so far, files and all, so a file that fails one turn fails the
131
132
  turns after it too.
@@ -169,14 +170,20 @@ sets `needsApproval` is rejected. It hands the engine only tools A2 runs, so a
169
170
  provider-defined tool, such as a provider's web search, is rejected too. `maxSteps` limits the model calls in one
170
171
  response under every harness. Steering enters at the next step, as with any
171
172
  agent: the harness sees the steered message before its next model call.
173
+ Steering that hands off before any of the turn's model calls finished, as after
174
+ a pause during the first one, joins the turn's prompt instead, so the turn's
175
+ first model call sees it.
172
176
 
173
177
  ## Recovery
174
178
 
175
179
  A2 records each model call and each tool result as the harness works. When a
176
180
  process dies in the middle of a response, the next attempt resumes at the
177
181
  interrupted step: finished model calls are not made again and finished tools
178
- do not run again. A2 stores nothing outside the log: a process that ran the
179
- last response resumes the harness from its state at the end of that response,
180
- FX's checkpoint or Pi's session, kept in memory, and any other process replays
181
- the session from the log. A tool call that a crash cut short reruns unless its
182
+ do not run again. A2 keeps nothing durable outside the log: a process that ran
183
+ the last response resumes the harness from its state at the end of that
184
+ response, FX's checkpoint or Pi's session, kept in memory, and any other
185
+ process replays the session from the log. Under `fx()`, the process that ran a
186
+ step also keeps FX's kernel waiting on that step's tool calls until the session
187
+ has nothing left to run there, so the response's next step, or the first step
188
+ of a response that steers it, continues it without a replay. A tool call that a crash cut short reruns unless its
182
189
  `toolRecovery` is `'report'`; see [Delivery semantics](/agents/conversations#delivery-semantics).
@@ -13,7 +13,7 @@ replay value, and a lost update is repainted by the next one. The test:
13
13
  > passes? Yes: event, in the log. No: presence.
14
14
 
15
15
  Messages, document updates, cancellations: events. Cursors, selections,
16
- typing, "who's online", a handler's live progress: presence.
16
+ typing, "who's online", a consumer's live progress: presence.
17
17
 
18
18
  Presence rides the session you already have: same contract, route, auth, and
19
19
  stream. A2 replicates each participant's latest field values to every
@@ -167,30 +167,34 @@ render-time rule:
167
167
  The durable event at index 214 beats any presence stamped 213 or lower. A2
168
168
  carries the stamp; the comparison is yours, in view code.
169
169
 
170
- ## Handlers broadcast too
170
+ ## Consumers broadcast too
171
171
 
172
- `ctx.session.setPresence` is the same operation, so a handler can publish live
172
+ `ctx.session.setPresence` is the same operation, so a consumer can publish live
173
173
  progress without writing non-facts to the log:
174
174
 
175
175
  ```ts server/canvas.ts
176
- import { createServer } from 'experimental-a2/server'
176
+ import { consumer, createServer } from 'experimental-a2/server'
177
177
  import { canvas } from '@/contracts'
178
178
 
179
+ const thumbnails = consumer({
180
+ contract: canvas,
181
+ name: 'thumbnails',
182
+ types: ['shape.created'],
183
+ }).run(async (ctx) => {
184
+ // long work; broadcast progress as it goes:
185
+ // await ctx.session.setPresence({
186
+ // participant: 'system:thumbnails',
187
+ // values: { progress: { step: 2, of: 5 } },
188
+ // })
189
+ })
190
+
179
191
  export const canvasServer = createServer({
180
192
  contract: canvas,
181
- handlers: {
182
- 'shape.created': async (ctx) => {
183
- // long work; broadcast progress as it goes:
184
- // await ctx.session.setPresence({
185
- // participant: 'system:thumbnails',
186
- // values: { progress: { step: 2, of: 5 } },
187
- // })
188
- },
189
- },
193
+ consumers: [thumbnails],
190
194
  })
191
195
  ```
192
196
 
193
- The participant id is the handler's choice; a `system:` prefix keeps robots
197
+ The participant id is the consumer's choice; a `system:` prefix keeps robots
194
198
  apart from humans in the map. When the work completes, append the outcome as
195
199
  an event: the progress was never a fact, the result is.
196
200
 
@@ -33,7 +33,7 @@ schema for its payload. These docs use Zod, but any
33
33
  your own.
34
34
 
35
35
  The contract is a plain, importable, **isomorphic** value. The server
36
- implements it with [handlers](/server/handlers), [reducers](/contract/state)
36
+ implements it with [consumers](/server/consumers), [reducers](/contract/state)
37
37
  derive the current view from it, and the browser types its pushes off it.
38
38
 
39
39
  ## Validation
@@ -52,6 +52,6 @@ typing, live progress) that is replicated but never stored in the log. See
52
52
  :::tip
53
53
  Events are facts, so name them in past tense: `created`, `expired`,
54
54
  `shop.notified`. A name that reads like a command (`notifyShop`) describes
55
- what a handler should do, not what happened. Dots are a naming convention, not
55
+ what a consumer should do, not what happened. Dots are a naming convention, not
56
56
  a namespace.
57
57
  :::
@@ -11,8 +11,8 @@ const events = await ordersServer.session(orderId).history()
11
11
  ```
12
12
 
13
13
  Every event in the session, oldest first: always the raw log, never a summary
14
- or snapshot. Use it for audits, debugging, and handler questions like "did the
15
- shop already start?" Pass inclusive `gte` or `lte` indexes to read a slice.
14
+ or snapshot. Use it for audits, debugging, and consumer questions like "did
15
+ the shop already start?" Pass inclusive `gte` or `lte` indexes to read a slice.
16
16
 
17
17
  ## Reducers
18
18
 
@@ -72,7 +72,7 @@ server-only app can keep the contract next to `createServer` instead.
72
72
 
73
73
  ## Read boundaries
74
74
 
75
- A root session reads the latest committed frontier. A handler's session stops
75
+ A root session reads the latest committed frontier. A consumer's session stops
76
76
  at its triggering event by default, stable across concurrent appends and
77
77
  retries:
78
78
 
@@ -83,23 +83,27 @@ import { ordersReducer } from './reducer'
83
83
 
84
84
  export const ordersServer = createServer({
85
85
  contract: orders,
86
- handlers: {
87
- created: async ({ event, session }) => {
88
- const atTrigger = await session.state(ordersReducer)
89
- // atTrigger.index === event.index
90
-
91
- const latest = await session.state(ordersReducer, { through: 'latest' })
92
- // latest also folds events appended after this one
86
+ consumers: [
87
+ {
88
+ name: 'read-status',
89
+ types: ['created'],
90
+ run: async ({ event, session }) => {
91
+ const atTrigger = await session.state(ordersReducer)
92
+ // atTrigger.index === event.index
93
+
94
+ const latest = await session.state(ordersReducer, { through: 'latest' })
95
+ // latest also folds events appended after this one
96
+ },
93
97
  },
94
- },
98
+ ],
95
99
  })
96
100
  ```
97
101
 
98
102
  Use `{ through: 'latest' }` for joins or other logic that intentionally
99
103
  observes later events. A numeric `{ through: index }` reads any inclusive log
100
104
  boundary; `through: 0` returns `initialState`. `state()` is observational: it
101
- never runs handlers or waits for pending work, and its index marks committed
102
- history, not handler completion.
105
+ never runs consumers or waits for pending work, and its index marks committed
106
+ history, not consumer progress.
103
107
 
104
108
  A latest read and a following `ctx.session.append(name, ...events)` are
105
109
  separate operations, and concurrent appends and retries can move the frontier
@@ -125,7 +129,7 @@ telemetry span (or `console.error` by default) without snapshot contents, and
125
129
  the next read folds the missing tail again.
126
130
 
127
131
  Each session and reducer has one latest snapshot. Older ones exist only while
128
- handler events that read at their boundary are unfinished (not yet completed
132
+ consumer runs that read at their boundary are unfinished (not yet completed
129
133
  or dead-lettered). This is automatic: there is no TTL, history limit, or
130
134
  retention setting.
131
135
 
@@ -4,7 +4,7 @@ description: "A session is one instance of a contract, with its own ordered log,
4
4
  ---
5
5
 
6
6
  ```ts
7
- // anywhere on the server: a route, a handler, a script
7
+ // anywhere on the server: a route, a consumer, a script
8
8
  const order = ordersServer.session('order-42')
9
9
  ```
10
10
 
@@ -19,7 +19,7 @@ Put events in one session when they form one history, fold, recovery unit, or
19
19
  live view. Split what should be read and recovered independently.
20
20
 
21
21
  A session is not a table or a general query boundary: one `orders` contract can
22
- serve millions of order sessions through the same server and handlers.
22
+ serve millions of order sessions through the same server and consumers.
23
23
  Collections, joins, search, and constraints across many records belong in
24
24
  [your database](#sessions-and-your-database).
25
25
 
@@ -48,7 +48,7 @@ same atomic step that appends the marker; if another event landed first, the
48
48
  clear skips and returns `{ cleared: false, index }` with the current head. A
49
49
  conditional clear is safe to retry.
50
50
 
51
- The old lifetime's pending events are never dispatched, and a handler still
51
+ The old lifetime's pending events are never dispatched, and a consumer still
52
52
  running for one can't append, return events, or read the new lifetime's state.
53
53
  Clearing can't stop code that is already running or undo its side effects.
54
54
 
@@ -70,7 +70,7 @@ authorize your own route before calling it.
70
70
 
71
71
  ### Scheduling a clear
72
72
 
73
- With a [scheduler](/schedulers/overview) configured, a handler can schedule a
73
+ With a [scheduler](/schedulers/overview) configured, a consumer can schedule a
74
74
  clear. This one clears an idle cart a day after its last activity:
75
75
 
76
76
  ```ts server/carts.ts
@@ -89,20 +89,24 @@ export const carts = a2.contract({
89
89
  export const cartsServer = createServer({
90
90
  contract: carts,
91
91
  scheduler: vercelQueues(),
92
- handlers: {
93
- 'item.added': async ({ event, session }) => {
94
- await session.schedule.clear('idle', {
95
- delay: '1d',
96
- ifLastIndex: event.index,
97
- })
92
+ consumers: [
93
+ {
94
+ name: 'clear-idle',
95
+ types: ['item.added'],
96
+ run: async ({ event, session }) => {
97
+ await session.schedule.clear('idle', {
98
+ delay: '1d',
99
+ ifLastIndex: event.index,
100
+ })
101
+ },
98
102
  },
99
- },
103
+ ],
100
104
  })
101
105
  ```
102
106
 
103
107
  The delay counts from the event's stored time. A later event moves the head
104
- past `event.index`, so this delivery skips and that event's handler schedules
105
- the next check; only events with a handler extend the policy. A handler's own
108
+ past `event.index`, so this delivery skips and that event's run schedules the
109
+ next check; only events the consumer takes extend the policy. A consumer's own
106
110
  writes move the head too, so one that appends should pass the index its last
107
111
  append returned.
108
112
 
@@ -148,27 +152,30 @@ attached to it:
148
152
  The investigation's session refers to the issue id without becoming a second
149
153
  owner of the issue. A2 can own individual issues when their history and live
150
154
  state are the center of the product, but boards and search then span many
151
- sessions and need another query model. Don't put a whole workspace in one
152
- session for cross-issue queries: every event joins one growing log that every
153
- client follows.
155
+ sessions and need another query model. A whole workspace can live in one
156
+ session when its pages read from [your tables](/server/sql-projections) instead
157
+ of folding the log, but every client following that session's live stream
158
+ receives every event, so weigh that traffic before many people share one.
154
159
 
155
- ### Database writes from handlers
160
+ ### Database writes from consumers
156
161
 
157
- Handlers may call your database like any other side effect. They are
162
+ Consumers may call your database like any other side effect. They are
158
163
  at-least-once, so make the write idempotent with `event.id`; it runs after the
159
164
  event is durable and may be retried. That is not one atomic write across A2
160
165
  and your tables: if a relational change must commit before the request
161
166
  succeeds, make it through the database's write path.
162
167
 
163
- ### Cross-session indexes
168
+ ### Tables kept from the logs
164
169
 
165
- A handler can maintain a small database index over A2-owned sessions, such as a
166
- list of active imports. Treat it as an advanced integration:
170
+ A consumer can keep a table in your database from A2-owned sessions, such as a
171
+ list of active imports. Committing its position in the same transaction as the
172
+ rows applies each event exactly once; [SQL projections](/server/sql-projections)
173
+ shows how. The table:
167
174
 
168
- - it is eventually consistent with the session logs;
169
- - writes must be idempotent;
170
- - A2 orders events within one session, not across sessions;
171
- - no A2 API scans and replays every session to rebuild it.
175
+ - is eventually consistent with the session logs;
176
+ - has each session's events in order, not an order across sessions;
177
+ - fills from history with `backfill`, one session at a time, as each next
178
+ runs: nothing enumerates sessions, so to fill it for idle ones, consume each.
172
179
 
173
180
  Back up and migrate it like application data. If search, joins, and
174
181
  cross-session reporting are central, the database should probably own that