experimental-a2 0.20.0 → 0.21.1

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 (286) hide show
  1. package/CHANGELOG.md +34 -0
  2. package/README.md +1 -1
  3. package/dist/{actor-C7b-1arn.d.ts → actor-HnGmrOZZ.d.ts} +5 -5
  4. package/dist/{actor-C7b-1arn.d.ts.map → actor-HnGmrOZZ.d.ts.map} +1 -1
  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-t7w8xIKP.js.map → actor-shared-DFET5q16.js.map} +1 -1
  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-CvEnqVfo.d.ts} +8 -8
  15. package/dist/{ai-Cz3hqVhk.d.ts.map → ai-CvEnqVfo.d.ts.map} +1 -1
  16. package/dist/{ai-mMiWul1T.js → ai-CzE672Q3.js} +9 -9
  17. package/dist/ai-CzE672Q3.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 +1 -1
  21. package/dist/ai-fx.js +3 -6
  22. package/dist/ai-fx.js.map +1 -1
  23. package/dist/{ai-harness-CV6rXbB3.d.ts → ai-harness-Bfk-Lycn.d.ts} +2 -2
  24. package/dist/{ai-harness-CV6rXbB3.d.ts.map → ai-harness-Bfk-Lycn.d.ts.map} +1 -1
  25. package/dist/{ai-harness-Clbg5ei7.js → ai-harness-dQeJaR1t.js} +7 -3
  26. package/dist/ai-harness-dQeJaR1t.js.map +1 -0
  27. package/dist/ai-pi.d.ts +1 -1
  28. package/dist/ai-pi.js +3 -3
  29. package/dist/ai-pi.js.map +1 -1
  30. package/dist/ai-react.d.ts +5 -5
  31. package/dist/ai-react.js +2 -2
  32. package/dist/ai-server.d.ts +14 -17
  33. package/dist/ai-server.d.ts.map +1 -1
  34. package/dist/ai-server.js +80 -73
  35. package/dist/ai-server.js.map +1 -1
  36. package/dist/ai.d.ts +2 -2
  37. package/dist/ai.js +2 -2
  38. package/dist/{client-BhHLoNKF.js → client-9IeTsiAs.js} +5 -5
  39. package/dist/{client-BhHLoNKF.js.map → client-9IeTsiAs.js.map} +1 -1
  40. package/dist/{client-h_DKFAjj.d.ts → client-CUMbS_38.d.ts} +3 -3
  41. package/dist/{client-h_DKFAjj.d.ts.map → client-CUMbS_38.d.ts.map} +1 -1
  42. package/dist/client.d.ts +3 -3
  43. package/dist/client.js +1 -1
  44. package/dist/{contract-BYKASlCV.js → contract-CWukt01d.js} +2 -2
  45. package/dist/contract-CWukt01d.js.map +1 -0
  46. package/dist/index.d.ts +5 -5
  47. package/dist/index.js +3 -3
  48. package/dist/{initial-snapshot-Bx-c0pDP.d.ts → initial-snapshot-D3Lve43o.d.ts} +4 -4
  49. package/dist/initial-snapshot-D3Lve43o.d.ts.map +1 -0
  50. package/dist/{internal-CscSY7K_.js → internal-Bc4PvfHk.js} +10 -3
  51. package/dist/internal-Bc4PvfHk.js.map +1 -0
  52. package/dist/otel.d.ts +1 -1
  53. package/dist/otel.js +1 -1
  54. package/dist/otel.js.map +1 -1
  55. package/dist/{react-BBXgHWFc.d.ts → react-BoHvC3v9.d.ts} +4 -4
  56. package/dist/{react-BBXgHWFc.d.ts.map → react-BoHvC3v9.d.ts.map} +1 -1
  57. package/dist/react.d.ts +1 -1
  58. package/dist/react.js +1 -1
  59. package/dist/{reducer-CqA59Q6U.d.ts → reducer-CYPHsSmd.d.ts} +2 -2
  60. package/dist/{reducer-CqA59Q6U.d.ts.map → reducer-CYPHsSmd.d.ts.map} +1 -1
  61. package/dist/{reducer-BAXc8JyF.js → reducer-CjmvZSAb.js} +2 -2
  62. package/dist/{reducer-BAXc8JyF.js.map → reducer-CjmvZSAb.js.map} +1 -1
  63. package/dist/runtime-consumer-BWmht60z.js +12 -0
  64. package/dist/runtime-consumer-BWmht60z.js.map +1 -0
  65. package/dist/scheduler-qstash.d.ts +2 -2
  66. package/dist/scheduler-qstash.js +3 -3
  67. package/dist/scheduler-task-X1SToDjP.js.map +1 -1
  68. package/dist/scheduler-vercel.d.ts +2 -2
  69. package/dist/scheduler-vercel.js +3 -3
  70. package/dist/scheduler-vercel.js.map +1 -1
  71. package/dist/{server-DYAATFuT.d.ts → server-BshFZ0eL.d.ts} +108 -41
  72. package/dist/server-BshFZ0eL.d.ts.map +1 -0
  73. package/dist/{server-qyb5aJJG.js → server-DvIB8kj5.js} +387 -113
  74. package/dist/server-DvIB8kj5.js.map +1 -0
  75. package/dist/server.browser.js +1 -1
  76. package/dist/server.browser.js.map +1 -1
  77. package/dist/server.d.ts +4 -4
  78. package/dist/server.js +2 -2
  79. package/dist/{session-clear-C-QwTXV1.d.ts → session-clear-Bk_29eEw.d.ts} +2 -2
  80. package/dist/session-clear-Bk_29eEw.d.ts.map +1 -0
  81. package/dist/{session-clear-DGI4jTQ5.js → session-clear-wHd5GCpZ.js} +5 -4
  82. package/dist/session-clear-wHd5GCpZ.js.map +1 -0
  83. package/dist/store-N8PXxDAS.js.map +1 -1
  84. package/dist/store-Z2qjw50n.d.ts +523 -0
  85. package/dist/store-Z2qjw50n.d.ts.map +1 -0
  86. package/dist/store-codec-DwrWlgQx.js +65 -0
  87. package/dist/store-codec-DwrWlgQx.js.map +1 -0
  88. package/dist/store-memory.d.ts +1 -1
  89. package/dist/store-memory.d.ts.map +1 -1
  90. package/dist/store-memory.js +393 -210
  91. package/dist/store-memory.js.map +1 -1
  92. package/dist/{store-polling-7WJV0YWP.js → store-polling-DYNyxCev.js} +2 -2
  93. package/dist/{store-polling-7WJV0YWP.js.map → store-polling-DYNyxCev.js.map} +1 -1
  94. package/dist/store-postgres.d.ts +1 -1
  95. package/dist/store-postgres.d.ts.map +1 -1
  96. package/dist/store-postgres.js +1586 -566
  97. package/dist/store-postgres.js.map +1 -1
  98. package/dist/{store-presence-polling-B1ixeqIS.js → store-presence-polling-CzQ0wdtM.js} +3 -3
  99. package/dist/{store-presence-polling-B1ixeqIS.js.map → store-presence-polling-CzQ0wdtM.js.map} +1 -1
  100. package/dist/store-redis-http.d.ts +1 -1
  101. package/dist/store-redis-http.js +4 -4
  102. package/dist/store-redis-notify-IiVdlYHc.js +2919 -0
  103. package/dist/store-redis-notify-IiVdlYHc.js.map +1 -0
  104. package/dist/store-redis.d.ts +1 -1
  105. package/dist/store-redis.js +2 -2
  106. package/dist/store-sqlite.d.ts +1 -1
  107. package/dist/store-sqlite.d.ts.map +1 -1
  108. package/dist/store-sqlite.js +517 -287
  109. package/dist/store-sqlite.js.map +1 -1
  110. package/dist/{telemetry-Cyfqo6Qt.d.ts → telemetry-CWwsh-Gx.d.ts} +3 -3
  111. package/dist/telemetry-CWwsh-Gx.d.ts.map +1 -0
  112. package/dist/{wire-BKxH1vbP.js → wire-CZGqNZRx.js} +5 -4
  113. package/dist/wire-CZGqNZRx.js.map +1 -0
  114. package/docs/01-quickstart.mdx +21 -15
  115. package/docs/02-examples.mdx +1 -1
  116. package/docs/actors/01-introduction.mdx +8 -0
  117. package/docs/actors/02-concurrency.mdx +1 -1
  118. package/docs/agents/01-introduction.mdx +1 -1
  119. package/docs/agents/02-tools.mdx +12 -11
  120. package/docs/agents/03-conversations.mdx +30 -20
  121. package/docs/agents/04-harnesses.mdx +7 -3
  122. package/docs/client/03-presence.mdx +18 -14
  123. package/docs/contract/01-events.mdx +2 -2
  124. package/docs/contract/02-state.mdx +18 -14
  125. package/docs/contract/03-sessions.mdx +32 -25
  126. package/docs/index.mdx +29 -24
  127. package/docs/reference/01-core.mdx +4 -4
  128. package/docs/reference/02-server.mdx +245 -190
  129. package/docs/reference/04-ai.mdx +20 -19
  130. package/docs/reference/05-telemetry.mdx +27 -13
  131. package/docs/reference/06-errors.mdx +24 -18
  132. package/docs/reference/meta.ts +1 -1
  133. package/docs/schedulers/01-overview.mdx +8 -8
  134. package/docs/schedulers/02-vercel-queues.mdx +2 -2
  135. package/docs/schedulers/03-qstash.mdx +4 -4
  136. package/docs/server/01-consumers.mdx +415 -0
  137. package/docs/server/02-timers.mdx +31 -21
  138. package/docs/server/03-cancellation.mdx +52 -44
  139. package/docs/server/04-routes.mdx +4 -1
  140. package/docs/server/05-sql-projections.mdx +391 -0
  141. package/docs/server/06-guarantees.mdx +55 -48
  142. package/docs/stores/01-overview.mdx +3 -3
  143. package/docs/stores/02-redis.mdx +11 -3
  144. package/docs/stores/03-redis-http.mdx +2 -2
  145. package/docs/stores/04-postgres.mdx +65 -14
  146. package/docs/stores/05-sqlite.mdx +5 -3
  147. package/docs/upgrades/01-overview.mdx +30 -0
  148. package/docs/upgrades/79-0-21.mdx +165 -0
  149. package/docs/upgrades/80-0-20.mdx +43 -0
  150. package/docs/upgrades/81-0-19.mdx +33 -0
  151. package/docs/upgrades/82-0-18.mdx +33 -0
  152. package/docs/upgrades/83-0-17.mdx +69 -0
  153. package/docs/upgrades/meta.ts +3 -0
  154. package/examples/playground/DEPLOY.md +39 -7
  155. package/examples/playground/app/a0/chat.test.tsx +2 -2
  156. package/examples/playground/app/a0/sandbox-log.ts +17 -15
  157. package/examples/playground/app/a0/server.ts +4 -4
  158. package/examples/playground/app/a2slack/a2slack.css +1067 -0
  159. package/examples/playground/app/a2slack/c/[channel]/page.tsx +20 -0
  160. package/examples/playground/app/a2slack/consumers/agent.ts +63 -0
  161. package/examples/playground/app/a2slack/consumers/feed.ts +164 -0
  162. package/examples/playground/app/a2slack/consumers/notify.ts +116 -0
  163. package/examples/playground/app/a2slack/consumers/search.ts +37 -0
  164. package/examples/playground/app/a2slack/consumers/triage.ts +31 -0
  165. package/examples/playground/app/a2slack/consumers/welcome.ts +25 -0
  166. package/examples/playground/app/a2slack/db/a2.ts +206 -0
  167. package/examples/playground/app/a2slack/db/index.ts +73 -0
  168. package/examples/playground/app/a2slack/db/migrations/0000_extensions.sql +4 -0
  169. package/examples/playground/app/a2slack/db/migrations/0001_workspace.sql +74 -0
  170. package/examples/playground/app/a2slack/db/migrations/0002_a2.sql +215 -0
  171. package/examples/playground/app/a2slack/db/migrations/0003_a2_batch.sql +67 -0
  172. package/examples/playground/app/a2slack/db/migrations/meta/0000_snapshot.json +18 -0
  173. package/examples/playground/app/a2slack/db/migrations/meta/0001_snapshot.json +500 -0
  174. package/examples/playground/app/a2slack/db/migrations/meta/0002_snapshot.json +1159 -0
  175. package/examples/playground/app/a2slack/db/migrations/meta/0003_snapshot.json +1159 -0
  176. package/examples/playground/app/a2slack/db/migrations/meta/_journal.json +34 -0
  177. package/examples/playground/app/a2slack/db/queries.ts +263 -0
  178. package/examples/playground/app/a2slack/db/schema.ts +145 -0
  179. package/examples/playground/app/a2slack/drills/a2slack.drill.ts +708 -0
  180. package/examples/playground/app/a2slack/drills/harness.ts +244 -0
  181. package/examples/playground/app/a2slack/events/route.ts +36 -0
  182. package/examples/playground/app/a2slack/identity.ts +44 -0
  183. package/examples/playground/app/a2slack/inbox/route.ts +8 -0
  184. package/examples/playground/app/a2slack/lag/data/route.ts +8 -0
  185. package/examples/playground/app/a2slack/lag/page.tsx +101 -0
  186. package/examples/playground/app/a2slack/lag.ts +73 -0
  187. package/examples/playground/app/a2slack/layout.tsx +43 -0
  188. package/examples/playground/app/a2slack/messages/route.ts +17 -0
  189. package/examples/playground/app/a2slack/model.test.ts +208 -0
  190. package/examples/playground/app/a2slack/model.ts +369 -0
  191. package/examples/playground/app/a2slack/models.test.ts +72 -0
  192. package/examples/playground/app/a2slack/page.tsx +10 -0
  193. package/examples/playground/app/a2slack/scheduler/route.ts +5 -0
  194. package/examples/playground/app/a2slack/search/route.ts +40 -0
  195. package/examples/playground/app/a2slack/search-index.ts +108 -0
  196. package/examples/playground/app/a2slack/server.test.ts +315 -0
  197. package/examples/playground/app/a2slack/server.ts +55 -0
  198. package/examples/playground/app/a2slack/sessions.ts +50 -0
  199. package/examples/playground/app/a2slack/thread/route.ts +8 -0
  200. package/examples/playground/app/a2slack/ui/ChannelView.tsx +87 -0
  201. package/examples/playground/app/a2slack/ui/Composer.tsx +117 -0
  202. package/examples/playground/app/a2slack/ui/DotsShadow.tsx +42 -0
  203. package/examples/playground/app/a2slack/ui/Feed.tsx +157 -0
  204. package/examples/playground/app/a2slack/ui/InboxContext.tsx +63 -0
  205. package/examples/playground/app/a2slack/ui/InboxView.tsx +82 -0
  206. package/examples/playground/app/a2slack/ui/Menu.tsx +128 -0
  207. package/examples/playground/app/a2slack/ui/MessageRow.tsx +314 -0
  208. package/examples/playground/app/a2slack/ui/NameGate.tsx +35 -0
  209. package/examples/playground/app/a2slack/ui/SearchContext.tsx +146 -0
  210. package/examples/playground/app/a2slack/ui/SearchResults.tsx +97 -0
  211. package/examples/playground/app/a2slack/ui/Shell.tsx +34 -0
  212. package/examples/playground/app/a2slack/ui/Sidebar.tsx +199 -0
  213. package/examples/playground/app/a2slack/ui/ThreadPanel.tsx +134 -0
  214. package/examples/playground/app/a2slack/ui/useInbox.ts +71 -0
  215. package/examples/playground/app/agent/server.ts +3 -3
  216. package/examples/playground/app/chat/[chatId]/chat-client.tsx +1 -1
  217. package/examples/playground/app/chat/server.ts +45 -35
  218. package/examples/playground/app/components/demo-icons.tsx +7 -0
  219. package/examples/playground/app/components/demos.tsx +7 -0
  220. package/examples/playground/app/orders/[orderId]/order-client.tsx +2 -2
  221. package/examples/playground/app/orders/page.tsx +1 -1
  222. package/examples/playground/app/orders/server.ts +49 -30
  223. package/examples/playground/app/recovery/[recoveryId]/recovery-client.tsx +14 -15
  224. package/examples/playground/app/recovery/page.tsx +4 -4
  225. package/examples/playground/app/recovery/server.ts +23 -32
  226. package/examples/playground/drizzle.config.ts +14 -0
  227. package/examples/playground/next.config.ts +6 -1
  228. package/examples/playground/package.json +12 -1
  229. package/examples/playground/scripts/migrate.mjs +31 -0
  230. package/examples/playground/vercel.json +9 -0
  231. package/examples/playground/vitest.drills.config.ts +32 -0
  232. package/package.json +5 -2
  233. package/sql/postgres/drizzle-schema.ts +206 -0
  234. package/sql/postgres/schema.sql +206 -67
  235. package/src/actor.ts +101 -71
  236. package/src/ai-agent.ts +149 -111
  237. package/src/ai-control-server.ts +8 -8
  238. package/src/ai-fx.ts +4 -6
  239. package/src/ai-harness.ts +10 -2
  240. package/src/ai-pi.ts +2 -2
  241. package/src/ai-server.ts +2 -2
  242. package/src/ai.ts +7 -7
  243. package/src/contract.ts +1 -1
  244. package/src/deterministic-id.ts +26 -3
  245. package/src/errors.ts +8 -5
  246. package/src/idempotent-replay.ts +0 -1
  247. package/src/index.ts +1 -1
  248. package/src/internal.ts +15 -5
  249. package/src/otel.ts +1 -1
  250. package/src/postgres-schema.ts +98 -57
  251. package/src/push-envelope.ts +20 -0
  252. package/src/runtime-consumer.ts +10 -0
  253. package/src/scheduler-task.ts +2 -2
  254. package/src/scheduler-vercel.ts +1 -1
  255. package/src/server.browser.ts +1 -1
  256. package/src/server.ts +774 -207
  257. package/src/session-socket.ts +1 -1
  258. package/src/store-codec.ts +66 -0
  259. package/src/store-memory.ts +581 -255
  260. package/src/store-postgres.ts +1984 -825
  261. package/src/store-redis-core.ts +1624 -1152
  262. package/src/store-redis-notify.ts +2 -0
  263. package/src/store-sqlite.ts +891 -486
  264. package/src/store.ts +227 -97
  265. package/src/telemetry.ts +2 -1
  266. package/src/wire.ts +1 -0
  267. package/dist/ai-harness-Clbg5ei7.js.map +0 -1
  268. package/dist/ai-mMiWul1T.js.map +0 -1
  269. package/dist/contract-BYKASlCV.js.map +0 -1
  270. package/dist/idempotent-replay-JdgMQRRm.js +0 -21
  271. package/dist/idempotent-replay-JdgMQRRm.js.map +0 -1
  272. package/dist/initial-snapshot-Bx-c0pDP.d.ts.map +0 -1
  273. package/dist/internal-CscSY7K_.js.map +0 -1
  274. package/dist/server-DYAATFuT.d.ts.map +0 -1
  275. package/dist/server-qyb5aJJG.js.map +0 -1
  276. package/dist/session-clear-C-QwTXV1.d.ts.map +0 -1
  277. package/dist/session-clear-DGI4jTQ5.js.map +0 -1
  278. package/dist/store-CyLUy86v.d.ts +0 -404
  279. package/dist/store-CyLUy86v.d.ts.map +0 -1
  280. package/dist/store-codec-DTG0Ftek.js +0 -10
  281. package/dist/store-codec-DTG0Ftek.js.map +0 -1
  282. package/dist/store-redis-notify-CM0AfLY0.js +0 -2544
  283. package/dist/store-redis-notify-CM0AfLY0.js.map +0 -1
  284. package/dist/telemetry-Cyfqo6Qt.d.ts.map +0 -1
  285. package/dist/wire-BKxH1vbP.js.map +0 -1
  286. package/docs/server/01-handlers.mdx +0 -227
@@ -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,10 @@ 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()`. Event IDs are unique across
296
+ a store's sessions, so derive the message ID from the room event's ID with
297
+ `idempotentId()`; a retried run then converges on the same message. A tool
292
298
  the agent calls to speak appends to the room with an `idempotentId()` of its
293
299
  `toolCallId`. Neither fold reads the other's state.
294
300
 
@@ -369,8 +375,12 @@ completed input.
369
375
 
370
376
  ## Delivery semantics
371
377
 
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
378
+ The agent runs on four of A2's own at-least-once
379
+ [consumers](/server/consumers): `a2.ai.control` applies commands and work
380
+ reports one at a time, `a2.ai.model` runs model steps on one lane, `a2.ai.tools` runs local
381
+ tools, and `a2.ai.metadata` looks up model limits. A dead letter names the one
382
+ that failed. A successful completion commits atomically with its terminal
383
+ events, and deterministic IDs
374
384
  make scheduling and lifecycle appends effectively once. An incomplete model
375
385
  attempt is marked `superseded` before its replacement starts. An expired claim
376
386
  or superseded attempt keeps its abort reason through model, resolver,
@@ -379,7 +389,7 @@ attempt; an explicit interruption stays terminal. A retried tool can stream
379
389
  different preliminary output under the same tool-call ID.
380
390
 
381
391
  Provider calls and external side effects sit outside that transaction, so
382
- recovery may repeat a call whose effect landed before its handler committed.
392
+ recovery may repeat a call whose effect landed before its run committed.
383
393
  Give side-effecting tools their own idempotency, normally keyed by the AI SDK
384
394
  `toolCallId`. A fast tool can also start before its model step fails; the
385
395
  failure blocks the continuation but cannot undo the effect.
@@ -530,7 +540,7 @@ application, with `commandId` (the submitted event's ID) and an `outcome` of
530
540
  in flight, each action resolves to its persisted command events, whose `id` is
531
541
  the receipt's `commandId`, or to an empty array when there is nothing to
532
542
  address, such as Stop with no active turn. A rejection is a normal result and
533
- spends no handler retry.
543
+ spends no retry.
534
544
 
535
545
  ### Pause and resume
536
546
 
@@ -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,6 +170,9 @@ 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
 
@@ -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
package/docs/index.mdx CHANGED
@@ -42,8 +42,8 @@ export const orders = a2.contract({
42
42
 
43
43
  ## Implement the server
44
44
 
45
- Bind the contract to a store, a scheduler, and the handlers that react to its
46
- events.
45
+ Bind the contract to a store, a scheduler, and the consumers that react to
46
+ its events.
47
47
 
48
48
  ```ts server/orders.ts
49
49
  import { createServer } from 'experimental-a2/server'
@@ -55,14 +55,18 @@ export const ordersServer = createServer({
55
55
  contract: orders,
56
56
  store: redis({ url: process.env.REDIS_URL }),
57
57
  scheduler: vercelQueues(),
58
- handlers: {
59
- 'order.created': async ({ event }) => {
60
- // your side effect; event.id makes a stable idempotency key:
61
- // await notifyShop(event.payload, { idempotencyKey: event.id })
62
- const { shopId } = event.payload
63
- return { type: 'shop.notified', payload: { shopId } }
58
+ consumers: [
59
+ {
60
+ name: 'notify-shop',
61
+ types: ['order.created'],
62
+ run: async ({ event }) => {
63
+ // your side effect; event.id makes a stable idempotency key:
64
+ // await sendEmailToShop(event.payload, { idempotencyKey: event.id })
65
+ const { shopId } = event.payload
66
+ return { type: 'shop.notified', payload: { shopId } }
67
+ },
64
68
  },
65
- },
69
+ ],
66
70
  })
67
71
  ```
68
72
 
@@ -130,18 +134,18 @@ from the log.
130
134
 
131
135
  - Appends are effectively-once.
132
136
  - The log stays ordered.
133
- - Handlers are at-least-once.
134
- - Returned events commit with the handler's completion.
135
- - Handlers run concurrently, unless they share a lane.
137
+ - Consumers are at-least-once.
138
+ - Returned events commit with the consumer's completion.
139
+ - Consumers run concurrently, unless they share a lane.
136
140
  - Failures stay contained.
137
- - A failing handler never makes `append` throw.
141
+ - A failing consumer never makes `append` throw.
138
142
 
139
143
  [Guarantees](/server/guarantees) has the details and the limits.
140
144
 
141
145
  ## Go further...
142
146
 
143
147
  <CardGroup cols={2}>
144
- <Card title="Handlers" href="/server/handlers" icon="zap">
148
+ <Card title="Consumers" href="/server/consumers" icon="zap">
145
149
  Retries, lanes, and idempotency.
146
150
  </Card>
147
151
  <Card title="State" href="/contract/state" icon="layers">
@@ -164,14 +168,14 @@ from the log.
164
168
  ## FAQ
165
169
 
166
170
  <details>
167
- <summary>Can I use A2 without handlers?</summary>
171
+ <summary>Can I use A2 without consumers?</summary>
168
172
 
169
- Yes. Omit `handlers` and use `append`, `history`, reducers, and live streams as
173
+ Yes. Omit `consumers` and use `append`, `history`, reducers, and live streams as
170
174
  a durable event log. Events settle in their append transaction, with no drain,
171
- claim, or queue message. A contract can also mix handled and history-only event
172
- types. A2 decides per event type.
175
+ claim, or queue message. A contract can also mix consumed and history-only
176
+ event types. A2 decides per event type.
173
177
 
174
- See [Events without handlers](/server/handlers#events-without-handlers).
178
+ See [Servers without consumers](/server/consumers#servers-without-consumers).
175
179
 
176
180
  </details>
177
181
 
@@ -188,18 +192,19 @@ event ids stay stable across retries. See [Timers](/server/timers).
188
192
  <details>
189
193
  <summary>How do I cancel work that's already running?</summary>
190
194
 
191
- Cancellation is an event too: same route, same auth, same log. Handlers
195
+ Cancellation is an event too: same route, same auth, same log. Consumers
192
196
  opt in with `abortOn` and receive a live `AbortSignal`; reducers cut the
193
197
  derived view exactly. See [Cancellation](/server/cancellation).
194
198
 
195
199
  </details>
196
200
 
197
201
  <details>
198
- <summary>What happens when a handler keeps failing?</summary>
202
+ <summary>What happens when a consumer keeps failing?</summary>
199
203
 
200
204
  It retries with backoff, then dead-letters after ten failed attempts, whether
201
- the handler threw or its process ended. Later events in the same lane wait
202
- until you retry or skip it; unlaned events and other lanes continue.
205
+ the consumer threw or its process ended. Later events in the same lane wait
206
+ until you retry or skip it; unlaned events, other lanes, and other consumers
207
+ continue.
203
208
  [Guarantees](/server/guarantees) has the exact guarantees and the
204
209
  recovery story behind them.
205
210
 
@@ -209,7 +214,7 @@ recovery story behind them.
209
214
  <summary>What does production need?</summary>
210
215
 
211
216
  A durable store backend, such as Postgres or Redis. Add a queue-backed scheduler
212
- when the contract has handlers or calls `session.schedule`. A history-only
217
+ when the server has consumers or calls `session.schedule`. A history-only
213
218
  contract with no timers needs no reaction infrastructure. Development needs
214
219
  neither: SQLite appears under `.a2/` and state survives restarts. See
215
220
  [Stores](/stores/overview) and [Schedulers](/schedulers/overview).
@@ -11,7 +11,7 @@ description: "Every entry point, and the isomorphic `experimental-a2` core: cont
11
11
  | `experimental-a2/server` | `createServer`: implement a contract | none |
12
12
  | `experimental-a2/client` | `createClient`: framework-agnostic session client | none |
13
13
  | `experimental-a2/react` | `createReact` | `react` |
14
- | `experimental-a2/ai` | `agentContract`, `handlerContext`, `inputs`, and the AI reducer: isomorphic | `ai` |
14
+ | `experimental-a2/ai` | `agentContract`, `consumerContext`, `inputs`, and the AI reducer: isomorphic | `ai` |
15
15
  | `experimental-a2/ai/server` | `agent`: an agent's loop as an A2 server | `ai` |
16
16
  | `experimental-a2/ai/fx` | `fx`: the FX harness for `agent()` | `ai`, `libfx` |
17
17
  | `experimental-a2/ai/pi` | `pi`: the Pi harness for `agent()` | `ai`, `@earendil-works/pi-coding-agent`, `@earendil-works/pi-ai` |
@@ -63,8 +63,8 @@ ones are rejected at definition time. The validated output is what's stored.
63
63
  ### `contract.batch(...events)`
64
64
 
65
65
  Returns the same events as a readonly tuple, preserving each literal type in a
66
- heterogeneous handler-returned array. A2 validates them when the handler
67
- completes. See [Handlers](/server/handlers#return-what-happens-after-success).
66
+ heterogeneous array a consumer returns. A2 validates them when the consumer
67
+ completes. See [Consumers](/server/consumers#return-what-happens-after-success).
68
68
 
69
69
  ### `reducer(options)`
70
70
 
@@ -120,7 +120,7 @@ export const id = await idempotentId('orders', 'shop-7', 'request-42')
120
120
 
121
121
  An `Event` whose `type` is `'a2.cleared'` and whose `payload` is `null`: the
122
122
  marker a [clear](/reference/server#sessionclearoptions) leaves in
123
- `session.stream()`. It never reaches handlers or `history()`. Comparing
123
+ `session.stream()`. It never reaches consumers or `history()`. Comparing
124
124
  `event.type` with that string narrows a stream item to `SessionCleared`. The
125
125
  session client handles the marker itself, so it matters only when you read the
126
126
  server stream yourself.