experimental-a2 0.19.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 (320) hide show
  1. package/CHANGELOG.md +50 -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.d.ts.map +1 -1
  22. package/dist/ai-fx.js +36 -9
  23. package/dist/ai-fx.js.map +1 -1
  24. package/dist/{ai-harness-CV6rXbB3.d.ts → ai-harness-Bfk-Lycn.d.ts} +2 -2
  25. package/dist/ai-harness-Bfk-Lycn.d.ts.map +1 -0
  26. package/dist/{ai-harness-BuuC-gJ3.js → ai-harness-dQeJaR1t.js} +17 -4
  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 +4 -4
  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 +84 -75
  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-9IeTsiAs.js} +5 -5
  40. package/dist/{client-BhHLoNKF.js.map → client-9IeTsiAs.js.map} +1 -1
  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-Bc4PvfHk.js} +10 -3
  52. package/dist/internal-Bc4PvfHk.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-BWmht60z.js +12 -0
  65. package/dist/runtime-consumer-BWmht60z.js.map +1 -0
  66. package/dist/scheduler-qstash.d.ts +2 -2
  67. package/dist/scheduler-qstash.d.ts.map +1 -1
  68. package/dist/scheduler-qstash.js +7 -7
  69. package/dist/scheduler-qstash.js.map +1 -1
  70. package/dist/scheduler-task-X1SToDjP.js.map +1 -1
  71. package/dist/scheduler-vercel.d.ts +2 -2
  72. package/dist/scheduler-vercel.d.ts.map +1 -1
  73. package/dist/scheduler-vercel.js +5 -4
  74. package/dist/scheduler-vercel.js.map +1 -1
  75. package/dist/{server-DYAATFuT.d.ts → server-BshFZ0eL.d.ts} +108 -41
  76. package/dist/server-BshFZ0eL.d.ts.map +1 -0
  77. package/dist/{server-D-Y4pqwu.js → server-DvIB8kj5.js} +423 -135
  78. package/dist/server-DvIB8kj5.js.map +1 -0
  79. package/dist/server.browser.js +1 -1
  80. package/dist/server.browser.js.map +1 -1
  81. package/dist/server.d.ts +4 -4
  82. package/dist/server.js +2 -2
  83. package/dist/{session-clear-C-QwTXV1.d.ts → session-clear-Bk_29eEw.d.ts} +2 -2
  84. package/dist/session-clear-Bk_29eEw.d.ts.map +1 -0
  85. package/dist/{session-clear-DGI4jTQ5.js → session-clear-wHd5GCpZ.js} +5 -4
  86. package/dist/session-clear-wHd5GCpZ.js.map +1 -0
  87. package/dist/store-N8PXxDAS.js.map +1 -1
  88. package/dist/store-Z2qjw50n.d.ts +523 -0
  89. package/dist/store-Z2qjw50n.d.ts.map +1 -0
  90. package/dist/store-codec-DwrWlgQx.js +65 -0
  91. package/dist/store-codec-DwrWlgQx.js.map +1 -0
  92. package/dist/store-memory.d.ts +1 -1
  93. package/dist/store-memory.d.ts.map +1 -1
  94. package/dist/store-memory.js +393 -210
  95. package/dist/store-memory.js.map +1 -1
  96. package/dist/{store-polling-7WJV0YWP.js → store-polling-DYNyxCev.js} +2 -2
  97. package/dist/{store-polling-7WJV0YWP.js.map → store-polling-DYNyxCev.js.map} +1 -1
  98. package/dist/store-postgres.d.ts +1 -1
  99. package/dist/store-postgres.d.ts.map +1 -1
  100. package/dist/store-postgres.js +1595 -673
  101. package/dist/store-postgres.js.map +1 -1
  102. package/dist/{store-presence-polling-B1ixeqIS.js → store-presence-polling-CzQ0wdtM.js} +3 -3
  103. package/dist/{store-presence-polling-B1ixeqIS.js.map → store-presence-polling-CzQ0wdtM.js.map} +1 -1
  104. package/dist/store-redis-http.d.ts +1 -1
  105. package/dist/store-redis-http.js +4 -4
  106. package/dist/store-redis-notify-IiVdlYHc.js +2919 -0
  107. package/dist/store-redis-notify-IiVdlYHc.js.map +1 -0
  108. package/dist/store-redis.d.ts +1 -1
  109. package/dist/store-redis.js +2 -2
  110. package/dist/store-sqlite.d.ts +1 -1
  111. package/dist/store-sqlite.d.ts.map +1 -1
  112. package/dist/store-sqlite.js +517 -287
  113. package/dist/store-sqlite.js.map +1 -1
  114. package/dist/{telemetry-Cyfqo6Qt.d.ts → telemetry-CWwsh-Gx.d.ts} +3 -3
  115. package/dist/telemetry-CWwsh-Gx.d.ts.map +1 -0
  116. package/dist/{wire-BKxH1vbP.js → wire-CZGqNZRx.js} +5 -4
  117. package/dist/wire-CZGqNZRx.js.map +1 -0
  118. package/docs/01-quickstart.mdx +28 -19
  119. package/docs/02-examples.mdx +1 -1
  120. package/docs/actors/01-introduction.mdx +9 -1
  121. package/docs/actors/02-concurrency.mdx +1 -1
  122. package/docs/actors/meta.ts +1 -1
  123. package/docs/agents/01-introduction.mdx +1 -1
  124. package/docs/agents/02-tools.mdx +13 -12
  125. package/docs/agents/03-conversations.mdx +31 -21
  126. package/docs/agents/04-harnesses.mdx +7 -3
  127. package/docs/agents/meta.ts +1 -1
  128. package/docs/client/03-presence.mdx +18 -14
  129. package/docs/client/meta.ts +1 -1
  130. package/docs/contract/01-events.mdx +2 -2
  131. package/docs/contract/02-state.mdx +18 -14
  132. package/docs/contract/03-sessions.mdx +32 -25
  133. package/docs/index.mdx +31 -26
  134. package/docs/reference/01-core.mdx +12 -12
  135. package/docs/reference/02-server.mdx +247 -192
  136. package/docs/reference/04-ai.mdx +29 -20
  137. package/docs/reference/05-telemetry.mdx +146 -0
  138. package/docs/reference/06-errors.mdx +24 -18
  139. package/docs/reference/meta.ts +1 -1
  140. package/docs/schedulers/01-overview.mdx +112 -0
  141. package/docs/schedulers/02-vercel-queues.mdx +87 -0
  142. package/docs/schedulers/03-qstash.mdx +123 -0
  143. package/docs/schedulers/meta.ts +1 -0
  144. package/docs/server/01-consumers.mdx +415 -0
  145. package/docs/server/02-timers.mdx +33 -23
  146. package/docs/server/03-cancellation.mdx +52 -44
  147. package/docs/server/04-routes.mdx +4 -1
  148. package/docs/server/05-sql-projections.mdx +391 -0
  149. package/docs/server/06-guarantees.mdx +60 -50
  150. package/docs/stores/01-overview.mdx +37 -0
  151. package/docs/stores/02-redis.mdx +57 -0
  152. package/docs/stores/03-redis-http.mdx +56 -0
  153. package/docs/stores/04-postgres.mdx +172 -0
  154. package/docs/stores/05-sqlite.mdx +36 -0
  155. package/docs/stores/06-memory.mdx +22 -0
  156. package/docs/stores/meta.ts +1 -0
  157. package/docs/upgrades/01-overview.mdx +30 -0
  158. package/docs/upgrades/79-0-21.mdx +165 -0
  159. package/docs/upgrades/80-0-20.mdx +43 -0
  160. package/docs/upgrades/81-0-19.mdx +33 -0
  161. package/docs/upgrades/82-0-18.mdx +33 -0
  162. package/docs/upgrades/83-0-17.mdx +69 -0
  163. package/docs/upgrades/meta.ts +3 -0
  164. package/examples/playground/DEPLOY.md +48 -11
  165. package/examples/playground/app/a0/apps/[appId]/route.ts +6 -10
  166. package/examples/playground/app/a0/chat.test.tsx +421 -330
  167. package/examples/playground/app/a0/sandbox-log.ts +17 -15
  168. package/examples/playground/app/a0/server.ts +4 -4
  169. package/examples/playground/app/a0/shell.tsx +1 -1
  170. package/examples/playground/app/a0/uploads/route.test.ts +59 -0
  171. package/examples/playground/app/a2slack/a2slack.css +1067 -0
  172. package/examples/playground/app/a2slack/c/[channel]/page.tsx +20 -0
  173. package/examples/playground/app/a2slack/consumers/agent.ts +63 -0
  174. package/examples/playground/app/a2slack/consumers/feed.ts +164 -0
  175. package/examples/playground/app/a2slack/consumers/notify.ts +116 -0
  176. package/examples/playground/app/a2slack/consumers/search.ts +37 -0
  177. package/examples/playground/app/a2slack/consumers/triage.ts +31 -0
  178. package/examples/playground/app/a2slack/consumers/welcome.ts +25 -0
  179. package/examples/playground/app/a2slack/db/a2.ts +206 -0
  180. package/examples/playground/app/a2slack/db/index.ts +73 -0
  181. package/examples/playground/app/a2slack/db/migrations/0000_extensions.sql +4 -0
  182. package/examples/playground/app/a2slack/db/migrations/0001_workspace.sql +74 -0
  183. package/examples/playground/app/a2slack/db/migrations/0002_a2.sql +215 -0
  184. package/examples/playground/app/a2slack/db/migrations/0003_a2_batch.sql +67 -0
  185. package/examples/playground/app/a2slack/db/migrations/meta/0000_snapshot.json +18 -0
  186. package/examples/playground/app/a2slack/db/migrations/meta/0001_snapshot.json +500 -0
  187. package/examples/playground/app/a2slack/db/migrations/meta/0002_snapshot.json +1159 -0
  188. package/examples/playground/app/a2slack/db/migrations/meta/0003_snapshot.json +1159 -0
  189. package/examples/playground/app/a2slack/db/migrations/meta/_journal.json +34 -0
  190. package/examples/playground/app/a2slack/db/queries.ts +263 -0
  191. package/examples/playground/app/a2slack/db/schema.ts +145 -0
  192. package/examples/playground/app/a2slack/drills/a2slack.drill.ts +708 -0
  193. package/examples/playground/app/a2slack/drills/harness.ts +244 -0
  194. package/examples/playground/app/a2slack/events/route.ts +36 -0
  195. package/examples/playground/app/a2slack/identity.ts +44 -0
  196. package/examples/playground/app/a2slack/inbox/route.ts +8 -0
  197. package/examples/playground/app/a2slack/lag/data/route.ts +8 -0
  198. package/examples/playground/app/a2slack/lag/page.tsx +101 -0
  199. package/examples/playground/app/a2slack/lag.ts +73 -0
  200. package/examples/playground/app/a2slack/layout.tsx +43 -0
  201. package/examples/playground/app/a2slack/messages/route.ts +17 -0
  202. package/examples/playground/app/a2slack/model.test.ts +208 -0
  203. package/examples/playground/app/a2slack/model.ts +369 -0
  204. package/examples/playground/app/a2slack/models.test.ts +72 -0
  205. package/examples/playground/app/a2slack/page.tsx +10 -0
  206. package/examples/playground/app/a2slack/scheduler/route.ts +5 -0
  207. package/examples/playground/app/a2slack/search/route.ts +40 -0
  208. package/examples/playground/app/a2slack/search-index.ts +108 -0
  209. package/examples/playground/app/a2slack/server.test.ts +315 -0
  210. package/examples/playground/app/a2slack/server.ts +55 -0
  211. package/examples/playground/app/a2slack/sessions.ts +50 -0
  212. package/examples/playground/app/a2slack/thread/route.ts +8 -0
  213. package/examples/playground/app/a2slack/ui/ChannelView.tsx +87 -0
  214. package/examples/playground/app/a2slack/ui/Composer.tsx +117 -0
  215. package/examples/playground/app/a2slack/ui/DotsShadow.tsx +42 -0
  216. package/examples/playground/app/a2slack/ui/Feed.tsx +157 -0
  217. package/examples/playground/app/a2slack/ui/InboxContext.tsx +63 -0
  218. package/examples/playground/app/a2slack/ui/InboxView.tsx +82 -0
  219. package/examples/playground/app/a2slack/ui/Menu.tsx +128 -0
  220. package/examples/playground/app/a2slack/ui/MessageRow.tsx +314 -0
  221. package/examples/playground/app/a2slack/ui/NameGate.tsx +35 -0
  222. package/examples/playground/app/a2slack/ui/SearchContext.tsx +146 -0
  223. package/examples/playground/app/a2slack/ui/SearchResults.tsx +97 -0
  224. package/examples/playground/app/a2slack/ui/Shell.tsx +34 -0
  225. package/examples/playground/app/a2slack/ui/Sidebar.tsx +199 -0
  226. package/examples/playground/app/a2slack/ui/ThreadPanel.tsx +134 -0
  227. package/examples/playground/app/a2slack/ui/useInbox.ts +71 -0
  228. package/examples/playground/app/agent/[agentId]/agent-queue.test.tsx +116 -90
  229. package/examples/playground/app/agent/[agentId]/compaction-panel.test.tsx +107 -76
  230. package/examples/playground/app/agent/ingress.test.ts +41 -17
  231. package/examples/playground/app/agent/model-input.test.tsx +92 -43
  232. package/examples/playground/app/agent/server.ts +3 -3
  233. package/examples/playground/app/chat/[chatId]/agent-stream-drawer.test.tsx +2 -2
  234. package/examples/playground/app/chat/[chatId]/chat-client.tsx +1 -1
  235. package/examples/playground/app/chat/server.test.ts +38 -65
  236. package/examples/playground/app/chat/server.ts +45 -35
  237. package/examples/playground/app/components/activity-history.test.tsx +60 -18
  238. package/examples/playground/app/components/demo-icons.tsx +7 -0
  239. package/examples/playground/app/components/demos.tsx +7 -0
  240. package/examples/playground/app/components/fast-link.test.tsx +3 -5
  241. package/examples/playground/app/initial-state.test.tsx +86 -45
  242. package/examples/playground/app/orders/[orderId]/order-client.tsx +2 -2
  243. package/examples/playground/app/orders/page.tsx +1 -1
  244. package/examples/playground/app/orders/server.ts +49 -30
  245. package/examples/playground/app/recovery/[recoveryId]/recovery-client.tsx +14 -15
  246. package/examples/playground/app/recovery/page.tsx +4 -4
  247. package/examples/playground/app/recovery/server.ts +23 -32
  248. package/examples/playground/drizzle.config.ts +14 -0
  249. package/examples/playground/next.config.ts +6 -1
  250. package/examples/playground/package.json +13 -1
  251. package/examples/playground/scripts/migrate.mjs +31 -0
  252. package/examples/playground/test/agent.ts +35 -0
  253. package/examples/playground/test/model.ts +149 -0
  254. package/examples/playground/test/serve.ts +184 -0
  255. package/examples/playground/test/server-only.ts +1 -0
  256. package/examples/playground/vercel.json +9 -0
  257. package/examples/playground/vitest.config.ts +27 -0
  258. package/examples/playground/vitest.drills.config.ts +32 -0
  259. package/package.json +7 -2
  260. package/sql/postgres/drizzle-schema.ts +206 -0
  261. package/sql/postgres/notifications.sql +24 -0
  262. package/sql/postgres/schema.sql +276 -0
  263. package/src/actor.ts +101 -71
  264. package/src/ai-agent.ts +154 -115
  265. package/src/ai-control-server.ts +8 -8
  266. package/src/ai-fx.ts +41 -9
  267. package/src/ai-harness.ts +28 -2
  268. package/src/ai-pi.ts +2 -2
  269. package/src/ai-server.ts +2 -2
  270. package/src/ai.ts +7 -7
  271. package/src/contract.ts +1 -1
  272. package/src/deterministic-id.ts +26 -3
  273. package/src/errors.ts +9 -6
  274. package/src/idempotent-replay.ts +0 -1
  275. package/src/index.ts +1 -1
  276. package/src/internal.ts +17 -6
  277. package/src/otel.ts +1 -1
  278. package/src/postgres-notifications.ts +0 -22
  279. package/src/postgres-resources.ts +6 -9
  280. package/src/postgres-schema.ts +121 -0
  281. package/src/push-envelope.ts +20 -0
  282. package/src/runtime-consumer.ts +10 -0
  283. package/src/scheduler-qstash.ts +12 -5
  284. package/src/scheduler-task.ts +2 -2
  285. package/src/scheduler-vercel.ts +10 -4
  286. package/src/server.browser.ts +1 -1
  287. package/src/server.ts +839 -238
  288. package/src/session-socket.ts +1 -1
  289. package/src/store-codec.ts +66 -0
  290. package/src/store-memory.ts +581 -255
  291. package/src/store-postgres.ts +1984 -943
  292. package/src/store-redis-core.ts +1624 -1152
  293. package/src/store-redis-notify.ts +2 -0
  294. package/src/store-sqlite.ts +891 -486
  295. package/src/store.ts +227 -97
  296. package/src/telemetry.ts +2 -1
  297. package/src/wire.ts +1 -0
  298. package/dist/ai-harness-BuuC-gJ3.js.map +0 -1
  299. package/dist/ai-harness-CV6rXbB3.d.ts.map +0 -1
  300. package/dist/ai-mMiWul1T.js.map +0 -1
  301. package/dist/contract-BYKASlCV.js.map +0 -1
  302. package/dist/idempotent-replay-JdgMQRRm.js +0 -21
  303. package/dist/idempotent-replay-JdgMQRRm.js.map +0 -1
  304. package/dist/initial-snapshot-Bx-c0pDP.d.ts.map +0 -1
  305. package/dist/internal-CscSY7K_.js.map +0 -1
  306. package/dist/server-D-Y4pqwu.js.map +0 -1
  307. package/dist/server-DYAATFuT.d.ts.map +0 -1
  308. package/dist/session-clear-C-QwTXV1.d.ts.map +0 -1
  309. package/dist/session-clear-DGI4jTQ5.js.map +0 -1
  310. package/dist/store-CyLUy86v.d.ts +0 -404
  311. package/dist/store-CyLUy86v.d.ts.map +0 -1
  312. package/dist/store-codec-DTG0Ftek.js +0 -10
  313. package/dist/store-codec-DTG0Ftek.js.map +0 -1
  314. package/dist/store-redis-notify-CM0AfLY0.js +0 -2544
  315. package/dist/store-redis-notify-CM0AfLY0.js.map +0 -1
  316. package/dist/telemetry-Cyfqo6Qt.d.ts.map +0 -1
  317. package/dist/wire-BKxH1vbP.js.map +0 -1
  318. package/docs/reference/05-adapters.mdx +0 -351
  319. package/docs/server/01-handlers.mdx +0 -200
  320. package/docs/server/05-production.mdx +0 -323
@@ -65,9 +65,10 @@ The `UIMessage` type a contract's input commands (`ai.control.requested`)
65
65
  carry, such as `AIMessageOf<typeof contract.events>`; `UIMessage` otherwise.
66
66
  `agent()` types messages with it.
67
67
 
68
- ### `handlerContext(contract?)`
68
+ ### `consumerContext(contract?)`
69
69
 
70
- Returns the current tool execution's durable handler context, an
70
+ Returns the current tool execution's durable
71
+ [consumer context](/server/consumers#the-context), an
71
72
  `AgentToolContext<D>` with `event`, `attempt`, `session`, and `signal`.
72
73
 
73
74
  - Tools are plain AI SDK `tool()` definitions, passed through uninterpreted;
@@ -123,7 +124,7 @@ Pure typed builders of `ai.control.requested` commands, for `append()` and
123
124
  - **Trust.** Browser ingress accepts user messages, approval and input
124
125
  responses, interruptions, and explicit retries. `inputs.requestInput()`,
125
126
  `inputs.toolResult()`, and non-user `inputs.seed()` messages are trusted
126
- server appends; only built-in handlers append generation requests and AI
127
+ server appends; only A2's own consumers append generation requests and AI
127
128
  lifecycle events. No builder covers session lifecycle: append
128
129
  `ai.session.created` and `ai.session.closed` from trusted server code.
129
130
  - **IDs.** Message, seed, and steer commands use the input message ID as their
@@ -336,7 +337,7 @@ agent({
336
337
  progress?,
337
338
  toolLane?: string, // run local tool executions one at a time on this lane
338
339
  toolRecovery?: { [tool]?: ToolRecovery }, // 'rerun' (default) | 'report'
339
- handlers?, // application event handlers
340
+ consumers?, // your consumers, next to A2's own
340
341
  store?, scheduler?, telemetry?,
341
342
  }): Agent<Events, State, Name> // an A2Server
342
343
  ```
@@ -350,10 +351,10 @@ stub.
350
351
  | `name` | the built-in protocol, with optional server-only `events` and `messageSchema` |
351
352
  | `contract` | a shared `agentContract()`, when clients fold application state |
352
353
  | `model` | an AI Gateway model ID, or [`ModelSettings`](#modelsettings) with one, under every harness; a value or per-generation async resolver |
353
- | `tools` | AI SDK `tool()` definitions; local tools read context with [`handlerContext()`](#handlercontextcontract) |
354
+ | `tools` | AI SDK `tool()` definitions; local tools read context with [`consumerContext()`](#consumercontextcontract) |
354
355
  | `instructions` | a value or per-generation async resolver |
355
356
  | `maxSteps` | a positive integer capping one complete assistant response, under every harness; no limit by default |
356
- | `harness` | the agent's loop: `{ type: 'ai-sdk', … }` (`AISDKHarness`, the default), [`fx()`](#fx), or [`pi()`](#pisandbox) |
357
+ | `harness` | the agent's loop: `{ type: 'ai-sdk', … }` (`AISDKHarness`, the default), [`fx()`](#fx), or [`pi()`](#pi-sandbox-) |
357
358
  | `harness.generation` | per-step settings, or a resolver |
358
359
  | `harness.messageMetadata` | maps default stream parts to message metadata |
359
360
  | `harness.generate` | replaces the default model step |
@@ -361,8 +362,8 @@ stub.
361
362
  | `progress` | durable batching: `maxChunks` (128), `maxDelayMs` (0) |
362
363
  | `toolLane` | one lane for every local tool execution, one at a time in admission order; model steps keep their own lane |
363
364
  | `toolRecovery` | `'rerun'` (default) or `'report'` per tool, the harness's own tools included |
364
- | `handlers` | application event handlers; naming an event the agent handles throws a `TypeError` |
365
- | `store`, `scheduler`, `telemetry` | as for any A2 server; `telemetry` covers Core and AI in one trace ([span catalogue](/reference/adapters#the-span-catalogue)) |
365
+ | `consumers` | your [consumers](/server/consumers), next to A2's own: `a2.ai.metadata` (concurrent), `a2.ai.control` (one lane for commands, work reports, and closes), `a2.ai.model` (one lane for generations), and `a2.ai.tools` (on `toolLane`, or concurrent). Yours may take any of the contract's events, the agent's own included, and run beside A2's. Names starting with `a2.` are reserved |
366
+ | `store`, `scheduler`, `telemetry` | as for any A2 server; `telemetry` covers Core and AI in one trace ([span catalogue](/reference/telemetry#the-span-catalogue)) |
366
367
 
367
368
  **Contracts.** A contract not built by `agentContract()` throws a `TypeError`
368
369
  and fails to typecheck (serve custom contracts with `createServer()`). Messages
@@ -384,7 +385,7 @@ overrides model headers. See
384
385
  **`harness`** is the agent's loop. `{ type: 'ai-sdk' }` (`AISDKHarness`), the
385
386
  default, runs one AI SDK `streamText()` step per durable generation, with the
386
387
  `generation`, `messageMetadata`, `generate`, and `compaction` options below.
387
- [`fx()`](#fx) and [`pi()`](#pisandbox) run a coding agent's own loop instead.
388
+ [`fx()`](#fx) and [`pi()`](#pi-sandbox-) run a coding agent's own loop instead.
388
389
  The four options exist only in `{ type: 'ai-sdk' }`; passing one to `agent()`
389
390
  itself throws a `TypeError` naming where it belongs.
390
391
 
@@ -445,11 +446,11 @@ compaction. It returns exactly one model step as a
445
446
  the durable lifecycle. See
446
447
  [Replace generation, not durability](/agents/conversations#replace-generation-not-durability).
447
448
 
448
- **Tools.** Independent authorized tool handlers run concurrently with no
449
- fixed limit. Results and generation completion arrive in either order; a
450
- continuation needs the closed step and every required terminal result.
451
- Single-owner terminal events commit atomically with the parent handler's
452
- completion and carry its durable cause; many-owner joins and preliminary
449
+ **Tools.** Independent authorized tool executions run concurrently with no
450
+ fixed limit, unless `toolLane` orders them. Results and generation completion
451
+ arrive in either order; a continuation needs the closed step and every
452
+ required terminal result. Single-owner terminal events commit atomically with
453
+ the parent run's completion and carry its durable cause; many-owner joins and preliminary
453
454
  outputs append immediately and idempotently. Provider calls and external side
454
455
  effects are at least once: give them their own idempotency. An approved
455
456
  provider-executed call can satisfy the join from its approval response, and a
@@ -489,8 +490,8 @@ See [Delivery semantics](/agents/conversations#delivery-semantics).
489
490
  - Static options are validated at construction, resolved ones before their
490
491
  generation; each session uses its own value.
491
492
  - Discovery: without an explicit threshold, the first generation batches a
492
- metadata request into its start write, and an independent handler records the
493
- model's limits from the public Gateway catalog in `state.modelMetadata`
493
+ metadata request into its start write, and A2's `a2.ai.metadata` consumer
494
+ records the model's limits from the public Gateway catalog in `state.modelMetadata`
494
495
  without delaying dispatch (shared one-hour cache, five-second timeout; no new
495
496
  request per turn, even after exhausted retries). The default threshold is 75%
496
497
  of the context window, lowered for a larger output allowance. While it is
@@ -525,7 +526,7 @@ See [Compact long conversations](/agents/conversations#compact-long-conversation
525
526
  **Harnesses.** Under `fx()` and `pi()`, each model call the harness makes is one
526
527
  durable step of the response, with the same events, progress, steering,
527
528
  interruption, `maxSteps`, and recovery as any step. Tool calls it makes become
528
- ordinary A2 tool calls, executed by A2's tool handlers, and `toolRecovery` may
529
+ ordinary A2 tool calls, executed by A2's `a2.ai.tools` consumer, and `toolRecovery` may
529
530
  name the harness's own tools, such as Pi's `bash`. `model` is resolved per step
530
531
  as for any agent. `instructions` and `tools` reach the harness; how they
531
532
  combine with its own prompt and tools depends on the harness. Tool approval
@@ -569,11 +570,11 @@ resolver. `reasoning` becomes the AI SDK `reasoning` call option
569
570
  model has one; `'standard'` removes a configured speed. Both override the same
570
571
  settings in `generation`. See [Choose the model](/agents/introduction#choose-the-model).
571
572
 
572
- ### `runWithHandlerContext(ctx, fn)`
573
+ ### `runWithConsumerContext(ctx, fn)`
573
574
 
574
575
  The test seam for context-reading tools: runs `fn` with `ctx` as the ambient
575
- handler context, so tests can call `execute` directly. Inside `fn`,
576
- `handlerContext()` returns `ctx`, with or without a contract argument.
576
+ consumer context, so tests can call `execute` directly. Inside `fn`,
577
+ `consumerContext()` returns `ctx`, with or without a contract argument.
577
578
 
578
579
  ## `experimental-a2/ai/fx`
579
580
 
@@ -601,6 +602,14 @@ call. Replay grows with the response, roughly 0.1 to 0.5 seconds for one of 80
601
602
  to 200 tool calls. When libfx can't take a checkpoint, A2 replays from an
602
603
  earlier one.
603
604
 
605
+ FX's model calls carry the same [AI Gateway](#agentoptions) defaults as the
606
+ AI SDK harness: `caching: 'auto'`, under any Gateway options FX sets itself,
607
+ and the session's stable `x-session-affinity` header in place of FX's own,
608
+ which changes every time A2 rebuilds the kernel. A2 leaves every other byte of
609
+ the request as FX wrote it, so it adds caching only where FX's own options come
610
+ last in the request, as libfx writes them; otherwise the request goes
611
+ unchanged.
612
+
604
613
  Credentials come from `AI_GATEWAY_API_KEY`, or Vercel's OIDC token on Vercel.
605
614
  This entry point is server-only.
606
615
 
@@ -0,0 +1,146 @@
1
+ ---
2
+ title: Telemetry
3
+ description: "Trace A2 with OpenTelemetry, and the spans it reports."
4
+ ---
5
+
6
+ ```ts server/orders.ts
7
+ import { createServer } from 'experimental-a2/server'
8
+ import { otel } from 'experimental-a2/otel'
9
+ import { orders } from '@/contracts'
10
+
11
+ export const ordersServer = createServer({
12
+ contract: orders,
13
+ telemetry: otel(),
14
+ // ...store, scheduler, and consumers
15
+ })
16
+ ```
17
+
18
+ With any OpenTelemetry setup registered (on Vercel, `@vercel/otel`), one trace
19
+ shows the request, `a2.append`, the inline `a2.drain`, an `a2.event` span per
20
+ consumer run, and the appends those runs make. Failures mark their `a2.event`
21
+ span, and `a2.event.outcome = dead_lettered` is the attribute to alert on.
22
+ Agent servers add `a2.ai.*` spans.
23
+
24
+ ## `experimental-a2/otel`
25
+
26
+ ```ts
27
+ otel(options?: { tracer?: Tracer; tracerName?: string }): A2Telemetry
28
+ ```
29
+
30
+ OpenTelemetry instrumentation for a server (`@opentelemetry/api` is a peer
31
+ dependency). Pass it as `telemetry`. Spans nest through the active context, so
32
+ the happy path lands in one trace. `A2Telemetry` is a one-method interface:
33
+ anything with `span(name, attributes, fn)` works, and `otel()` is the adapter
34
+ A2 ships.
35
+
36
+ ## The span catalogue
37
+
38
+ This catalogue is a contract: renames and additions are breaking for
39
+ dashboards and are called out in the package's `CHANGELOG.md`. All spans carry
40
+ `a2.contract` and `a2.session_id`. Core spans describe the durable runtime;
41
+ `a2.ai.*` spans describe the AI runtime's consumers.
42
+
43
+ | Layer | Span | Wraps |
44
+ | ----- | ---- | ----- |
45
+ | Core | `a2.append` | validation + the store write (one batch) |
46
+ | Core | `a2.drain` | one drain pass, including the consumer runs it starts |
47
+ | Core | `a2.event` | one claimed head, including the consumer's run and its completion |
48
+ | Core | `a2.claim` | one `claimAvailable` store operation |
49
+ | Core | `a2.commit` | one `completeAttempt` or `failAttempt` store operation |
50
+ | Core | `a2.state` | one `state()` read: snapshot-plus-tail load + reducer fold |
51
+ | Core | `a2.snapshot` | one background batch of checkpoint writes |
52
+ | Core | `a2.redrive` | one `session.redrive()`: the store operation and starting the session's wakeup |
53
+ | Core | `a2.consume` | one `session.consume()`, including the drains it runs and the waits between them |
54
+ | AI | `a2.ai.control` | applying an input/work report/session-close decision and preparing its returned events |
55
+ | AI | `a2.ai.generate` | one generation run, including Core calls and model/SDK work |
56
+ | AI | `a2.ai.context` | constructing model messages, including tool prompt reconstruction when uncached |
57
+ | AI | `a2.ai.compaction` | automatic summarization or the custom compaction callback |
58
+ | AI | `a2.ai.tool` | one durable tool run, including application execution |
59
+ | AI | `a2.ai.ack` | waiting for a persisted work report's canonical events and control receipt |
60
+
61
+ Durations include nested work: `a2.event` contains its AI run and later
62
+ commit, AI runs call Core, and `a2.state` includes reducer code.
63
+ Acknowledgements overlap control processing, claims can cover several events,
64
+ and snapshots run in the background, so summing spans, or subtracting summed
65
+ children, does not yield exclusive Core or AI overhead. Model and application
66
+ I/O is included; its child spans have the detail.
67
+
68
+ Attributes arrive in two waves: **at start** (passed to `span()`) and
69
+ **mid-span** (via `handle.setAttribute`, for outcomes and counts known only
70
+ after the work ran). A recorder must merge both; the ones you alert on are all
71
+ mid-span.
72
+
73
+ | Attribute | Span | When | Values |
74
+ | --------------------- | ----------- | ----- | ----------------------------------------------------- |
75
+ | `a2.append.source` | `a2.append` | start | `external` \| `consumer` |
76
+ | `a2.append.mode` | `a2.append` | start | `inline` \| `dispatch` |
77
+ | `a2.append.types` | `a2.append` | start | comma-joined event types |
78
+ | `a2.append.count` | `a2.append` | start | batch size |
79
+ | `a2.append.armed` | `a2.append` | mid | `false` when the scheduler arm failed and this append degraded to append-driven healing |
80
+ | `a2.drain.outcome` | `a2.drain` | mid | `settled` \| `busy` \| `stalled` \| `failed` |
81
+ | `a2.drain.processed` | `a2.drain` | mid | events processed this pass |
82
+ | `a2.claim.outcome` | `a2.claim` | mid | `claimed` \| `busy` \| `settled` |
83
+ | `a2.claim.count` | `a2.claim` | mid | number of claimed events, or zero |
84
+ | `a2.commit.operation` | `a2.commit` | start | `complete` \| `fail` |
85
+ | `a2.commit.index` | `a2.commit` | start | event log position |
86
+ | `a2.commit.attempt` | `a2.commit` | start | durable 1-based attempt ordinal |
87
+ | `a2.commit.outcome` | `a2.commit` | mid | `completed` \| `failed` \| `dead_lettered` \| `superseded` |
88
+ | `a2.event.type` | `a2.event` | start | the event's type |
89
+ | `a2.event.index` | `a2.event` | start | log position |
90
+ | `a2.event.id` | `a2.event` | start | event id |
91
+ | `a2.event.consumer` | `a2.event` | start | the consumer running the event |
92
+ | `a2.event.attempt` | `a2.event` | start | same durable 1-based ordinal as `ctx.attempt` |
93
+ | `a2.event.lane` | `a2.event` | start | stored lane value; absent for concurrent unlaned work |
94
+ | `a2.event.handled` | `a2.event` | start | normally `true`; `false` when a claimed head's consumer is not registered |
95
+ | `a2.event.age_ms` | `a2.event` | start | milliseconds since the event was appended, when its run starts: how far behind its consumer is in time |
96
+ | `a2.event.lag` | `a2.event` | start | events appended after this one when it was claimed: how far behind its consumer is in the log. Absent when the store doesn't report its head |
97
+ | `a2.event.outcome` | `a2.event` | mid | `processed` \| `failed` \| `dead_lettered` \| `superseded` \| `surrendered` |
98
+ | `a2.event.aborted` | `a2.event` | mid | `true` when `abortOn` fired during the run |
99
+ | `a2.state.reducer` | `a2.state` | start | the reducer's name |
100
+ | `a2.state.snapshot` | `a2.state` | mid | `hit` \| `miss` \| `rejected` (schema guard discarded it) \| `memory` (served from the server's in-memory state) |
101
+ | `a2.state.folded` | `a2.state` | mid | events folded past the snapshot |
102
+ | `a2.state.index` | `a2.state` | mid | the frontier the returned state reflects |
103
+ | `a2.snapshot.reducer` | `a2.snapshot` | start | the reducer's name |
104
+ | `a2.snapshot.index` | `a2.snapshot` | start | greatest checkpoint index in the write batch |
105
+ | `a2.snapshot.count` | `a2.snapshot` | start | number of checkpoints in the write batch |
106
+ | `a2.consume.consumer` | `a2.consume` | start | the consumer being consumed |
107
+ | `a2.consume.through` | `a2.consume` | mid | the index it runs through |
108
+ | `a2.consume.index` | `a2.consume` | mid | the index it handled every event through |
109
+ | `a2.consume.drains` | `a2.consume` | mid | drains it ran to get there |
110
+ | `a2.redrive.index` | `a2.redrive` | start | log position of the event being redriven |
111
+ | `a2.redrive.consumer` | `a2.redrive` | start | the consumer the event is dead for |
112
+ | `a2.redrive.action` | `a2.redrive` | start | `retry` \| `skip` |
113
+ | `a2.redrive.redriven` | `a2.redrive` | mid | `true` when the event was dead-lettered and is now resolved |
114
+ | `a2.redrive.armed` | `a2.redrive` | mid | `false` when the scheduler arm failed and the wakeup degraded to append-driven healing |
115
+ | `a2.ai.control.type` | `a2.ai.control` | start | `ai.control.requested` \| `ai.work.reported` \| `ai.session.closed` |
116
+ | `a2.ai.control.id` | `a2.ai.control` | start | triggering event id |
117
+ | `a2.ai.control.attempt` | `a2.ai.control` | start | durable 1-based attempt ordinal |
118
+ | `a2.ai.generate.request_id` | `a2.ai.generate` | start | generation request event id |
119
+ | `a2.ai.generate.attempt` | `a2.ai.generate` | start | durable 1-based attempt ordinal |
120
+ | `a2.ai.context.request_id` | `a2.ai.context` | start | generation request event id |
121
+ | `a2.ai.context.phase` | `a2.ai.context` | start | `generation` \| `refresh` \| `tool` |
122
+ | `a2.ai.compaction.request_id` | `a2.ai.compaction` | start | generation request event id |
123
+ | `a2.ai.compaction.generation_id` | `a2.ai.compaction` | start | generation attempt id |
124
+ | `a2.ai.compaction.policy` | `a2.ai.compaction` | start | `automatic` \| `custom` |
125
+ | `a2.ai.tool.request_id` | `a2.ai.tool` | start | originating generation request event id |
126
+ | `a2.ai.tool.call_id` | `a2.ai.tool` | start | tool call id |
127
+ | `a2.ai.tool.name` | `a2.ai.tool` | start | tool name |
128
+ | `a2.ai.tool.attempt` | `a2.ai.tool` | start | durable 1-based attempt ordinal |
129
+ | `a2.ai.ack.request_id` | `a2.ai.ack` | start | reporting worker event id |
130
+ | `a2.ai.ack.report_id` | `a2.ai.ack` | start | work report event id, matching the control span's id |
131
+
132
+ - Consumer lag: group `a2.event` spans by `a2.event.consumer`. `a2.event.lag`
133
+ is how many events the session had appended after the one it claimed, and
134
+ `a2.event.age_ms` how old that event was when its run started. A consumer
135
+ that keeps up keeps both near zero; a rising `a2.event.lag` means it is
136
+ falling behind its log.
137
+ - Drain outcomes: `settled` means nothing actionable is left; `busy` means live
138
+ per-event claims or a failed event's retry hold remain; `stalled` means a
139
+ caught failure in this drain needs a later retry. `failed` appears only on
140
+ scheduler deliveries: the drain threw after arming its successor watchdog,
141
+ the span records the error, and that successor retries the session.
142
+ - A failing consumer run marks its `a2.event` span with the exception and
143
+ error status. Alert on `a2.event.outcome = dead_lettered`. A dead-lettered event
144
+ blocks only later events in its lane, until `session.redrive()` resolves it;
145
+ other lanes stay eligible.
146
+ - `superseded` means a stale completion or failure did not change the log.
@@ -10,7 +10,7 @@ exposes `A2ClientError`, which extends `A2Error`, for server failures and local
10
10
  ones alike, so `instanceof A2Error` matches both.
11
11
 
12
12
  ```ts
13
- // e.g. in a route handler:
13
+ // e.g. in a route:
14
14
  import { A2Error } from 'experimental-a2'
15
15
 
16
16
  export async function POST(req: Request) {
@@ -40,16 +40,18 @@ export async function POST(req: Request) {
40
40
  | `UNKNOWN_PRESENCE_FIELD` | a presence field isn't in the contract's `presence` map | no |
41
41
  | `PRESENCE_NOT_SUPPORTED` | the contract declares `presence` but the store has no presence capability. Thrown at construction; for environment-resolved stores, at first store use | no |
42
42
  | `PARTIAL_DUPLICATE_BATCH` | a batch mixes already-appended and fresh events, contains the same id twice, or uses an id already used in a different session (event ids are globally unique). Always a caller bug; nothing was written | no |
43
- | `SUPERSEDED_ATTEMPT` | a handler append or clear comes from an attempt that a recovery claim or a session clear replaced; nothing was written. Also the abort reason on that handler's `ctx.signal` | no |
44
- | `SESSION_CLEARED` | a state read reaches below the latest clear, a handler reads state past a clear of its event, or an actor call is cleared before it receives its answer | no |
43
+ | `SUPERSEDED_ATTEMPT` | a consumer's append or clear comes from an attempt that a recovery claim or a session clear replaced; nothing was written. Also the abort reason on that attempt's `ctx.signal` | no |
44
+ | `SESSION_CLEARED` | a state read reaches below the latest clear, a consumer reads state past a clear of its event, or an actor call is cleared before it receives its answer | no |
45
45
  | `CLAIM_EXPIRED` | never thrown; only the `ctx.signal` abort reason when the attempt's lease lapsed without renewal (assumed loss, decided locally, not proof) | no |
46
+ | `CONSUMER_DEAD_LETTERED` | `session.consume()` reached an event that dead-lettered for that consumer. Resolve it with `session.redrive()`, then consume again | no |
46
47
  | `STORE_UNAVAILABLE` | the store backend failed; nothing was written. The original error is the `cause` | **yes** |
47
- | `STORE_NOT_CONFIGURED` | production boots with no `store` configured (during `next build` page collection, deferred to first use) | no |
48
+ | `STORE_NOT_CONFIGURED` | production boots with no `store` configured (during `next build` page collection, deferred to first use), a store's optional peer dependency is missing, or a Postgres query finds one of A2's tables or columns missing, or that its role can't use (and, with `listenConnectionString`, a subscription finds the notification triggers missing). The message names the fix | no |
48
49
 
49
50
  Retrying an *identical* batch is not `PARTIAL_DUPLICATE_BATCH`: it's an
50
51
  idempotent success that returns the previously appended events. A failing
51
- handler never makes `append` throw; handler failures retry and, if persistent,
52
- [dead-letter the event](/server/guarantees#when-a-handler-keeps-failing). See
52
+ consumer never makes `append` throw; its failures retry and, if persistent,
53
+ [dead-letter the event](/server/guarantees#when-a-consumer-keeps-failing) for
54
+ that consumer. See
53
55
  [What append never throws for](/server/guarantees#what-append-never-throws-for).
54
56
 
55
57
  ## `A2ClientError`
@@ -82,33 +84,37 @@ or `RangeError`.
82
84
 
83
85
  ## NonRetriableError
84
86
 
85
- Throw it (or a subclass, matched by `instanceof`) from a handler when the
87
+ Throw it (or a subclass, matched by `instanceof`) from a consumer when the
86
88
  failure is deterministic: a guard that refuses this attempt refuses every
87
- retry identically. The event dead-letters immediately instead of consuming the
88
- ten-failure retry budget. Any other thrown error keeps the
89
+ retry identically. The event dead-letters for that consumer immediately,
90
+ instead of spending the ten-failure retry budget. Any other thrown error keeps the
89
91
  ordinary retry contract.
90
92
 
91
93
  ```ts
92
94
  import { z } from 'zod'
93
95
  import * as a2 from 'experimental-a2'
94
96
  import { NonRetriableError } from 'experimental-a2'
95
- import { createServer } from 'experimental-a2/server'
97
+ import { consumer, createServer } from 'experimental-a2/server'
96
98
 
97
99
  const orders = a2.contract({
98
100
  name: 'orders',
99
101
  events: { created: z.object({ chargeable: z.boolean() }) },
100
102
  })
101
103
 
104
+ const charge = consumer({
105
+ contract: orders,
106
+ name: 'charge',
107
+ types: ['created'],
108
+ }).run(async ({ event }) => {
109
+ if (!event.payload.chargeable) {
110
+ throw new NonRetriableError('this order cannot be charged')
111
+ }
112
+ // transient failures below retry as usual
113
+ })
114
+
102
115
  export const ordersServer = createServer({
103
116
  contract: orders,
104
- handlers: {
105
- created: async ({ event }) => {
106
- if (!event.payload.chargeable) {
107
- throw new NonRetriableError('this order cannot be charged')
108
- }
109
- // transient failures below retry as usual
110
- },
111
- },
117
+ consumers: [charge],
112
118
  })
113
119
  ```
114
120
 
@@ -1 +1 @@
1
- export default { order: 7 }
1
+ export default { order: 10 }
@@ -0,0 +1,112 @@
1
+ ---
2
+ title: Overview
3
+ description: A clock for consumer retries and timers.
4
+ ---
5
+
6
+ ```ts server/orders.ts
7
+ import { createServer } from 'experimental-a2/server'
8
+ import { redis } from 'experimental-a2/store-redis'
9
+ import { vercelQueues } from 'experimental-a2/scheduler-vercel'
10
+ import { orders } from '@/contracts'
11
+
12
+ export const scheduler = vercelQueues()
13
+
14
+ export const ordersServer = createServer({
15
+ contract: orders,
16
+ store: redis({ url: process.env.REDIS_URL }),
17
+ scheduler,
18
+ consumers: [
19
+ /* ... */
20
+ ],
21
+ })
22
+ ```
23
+
24
+ ```ts app/api/a2/scheduler/route.ts
25
+ import { billingServer, ordersServer, scheduler } from '@/server'
26
+
27
+ export const POST = scheduler.handler(ordersServer, billingServer)
28
+ ```
29
+
30
+ A scheduler wakes sessions on a clock. It retries a consumer that failed or
31
+ whose process died, and it delivers [timers](/server/timers), whose delays must
32
+ fit the [provider's limit](/server/timers#provider-limits). Share one
33
+ scheduler across your servers and mount its route once, with every server it
34
+ delivers to. `scheduler.handler(...)` checks those servers when the module
35
+ loads, so a bad route fails at startup.
36
+
37
+ | Scheduler | Use it for |
38
+ | --- | --- |
39
+ | [Vercel Queues](/schedulers/vercel-queues) | Apps on Vercel (recommended) |
40
+ | [QStash](/schedulers/qstash) | A stable public URL, called from separate infrastructure |
41
+
42
+ ## Without a scheduler
43
+
44
+ If the server has no consumers and nothing calls `session.schedule`, skip it.
45
+ Events settle as they're appended, with no queue messages. That's a complete
46
+ production setup for history, reducers, and live sync.
47
+
48
+ With consumers but no scheduler, a session only wakes on a top-level append or
49
+ an explicit `server.drain()`. Reads never wake it, so a session nobody writes
50
+ to stays stuck. That's fine for internal tools that can heal on the next
51
+ write. For production, add a scheduler.
52
+
53
+ ## More than one scheduler
54
+
55
+ Servers with different scheduler instances need separate routes, even from the
56
+ same adapter. Give each QStash instance its own `url`, and each Vercel Queues
57
+ instance its own `topic` and trigger.
58
+
59
+ ## Dispatch in a fresh invocation
60
+
61
+ `append` starts consumers in the same request and uses the scheduler as a
62
+ watchdog. `append.dispatch` stores the event and hands its consumers to the
63
+ queue instead:
64
+
65
+ ```ts app/api/imports/route.ts
66
+ import { ordersServer } from '@/server'
67
+
68
+ export async function POST(request: Request) {
69
+ const { orderId, shopId, items } = await request.json()
70
+ await ordersServer.session(orderId).append.dispatch({
71
+ id: `created:${orderId}`,
72
+ type: 'created',
73
+ payload: { shopId, items },
74
+ })
75
+ return Response.json({ accepted: true }, { status: 202 })
76
+ }
77
+ ```
78
+
79
+ It needs a scheduler and waits for the queue to accept the message. If that
80
+ send fails, the event may already be stored; its explicit `id` makes a retry
81
+ safe.
82
+
83
+ ## Deploys and rollbacks
84
+
85
+ Queued work can reach a different deployment than the one that sent it. Keep
86
+ event schemas compatible across deployments. Each adapter's page explains
87
+ where its tasks go: [Vercel Queues](/schedulers/vercel-queues#deployments) and
88
+ [QStash](/schedulers/qstash#deployments).
89
+
90
+ :::warning
91
+ Don't roll back to an A2 version without timer delivery while scheduled tasks
92
+ are outstanding. An old route can acknowledge a scheduled append without
93
+ writing its events. Rolling back past scheduled clears fails too: an old
94
+ Vercel Queues route keeps failing the clear until retention ends, and an old
95
+ QStash route moves it to QStash's dead-letter queue. Roll forward, or wait for
96
+ those tasks to deliver first.
97
+ :::
98
+
99
+ ## Send errors
100
+
101
+ When a provider rejects a send, both adapters keep the provider's original
102
+ error.
103
+
104
+ - **Terminal:** a definite 4xx status or error, except 408, 409, 425, and 429.
105
+ The Vercel adapter also treats the SDK's `BadRequestError`,
106
+ `UnauthorizedError`, and `ForbiddenError` as terminal.
107
+ - **Retryable:** 408, 409, 425, 429, 5xx, network failures, and errors with no
108
+ recognized status.
109
+ - Vercel Queues' duplicate-message error counts as success.
110
+
111
+ A2's durable AI tools use this classification. Direct `session.schedule()`
112
+ calls still reject with the original error.
@@ -0,0 +1,87 @@
1
+ ---
2
+ title: Vercel Queues
3
+ description: The recommended scheduler on Vercel.
4
+ ---
5
+
6
+ ```ts server/orders.ts
7
+ import { createServer } from 'experimental-a2/server'
8
+ import { redis } from 'experimental-a2/store-redis'
9
+ import { vercelQueues } from 'experimental-a2/scheduler-vercel'
10
+ import { orders } from '@/contracts'
11
+
12
+ export const scheduler = vercelQueues()
13
+
14
+ export const ordersServer = createServer({
15
+ contract: orders,
16
+ store: redis({ url: process.env.REDIS_URL }),
17
+ scheduler,
18
+ consumers: [
19
+ /* ... */
20
+ ],
21
+ })
22
+ ```
23
+
24
+ ```ts app/api/a2/scheduler/route.ts
25
+ import { ordersServer, scheduler } from '@/server'
26
+
27
+ export const POST = scheduler.handler(ordersServer)
28
+ ```
29
+
30
+ ```json vercel.json
31
+ {
32
+ "functions": {
33
+ "app/api/a2/scheduler/route.ts": {
34
+ "experimentalTriggers": [
35
+ {
36
+ "type": "queue/v2beta",
37
+ "topic": "a2"
38
+ }
39
+ ]
40
+ }
41
+ }
42
+ }
43
+ ```
44
+
45
+ ```package-install
46
+ npm i @vercel/queue
47
+ ```
48
+
49
+ The trigger is required. It connects the route to Vercel Queues and makes the
50
+ function private, so only the queue can call it. Don't also expose the route
51
+ publicly or wrap it in your own authentication.
52
+
53
+ Messages are kept for seven days (Vercel Queues' default is 24 hours, and
54
+ seven days is its maximum), so timers can be scheduled up to seven days out. A
55
+ longer delay goes out as asked, and Vercel Queues refuses it.
56
+
57
+ A drain delivery is acknowledged once its session settles or a later watchdog
58
+ for it is stored, including when the drain fails after storing that watchdog.
59
+ A drain that fails before then stays unacknowledged, so Vercel Queues
60
+ redelivers it.
61
+
62
+ ## Options
63
+
64
+ | Option | Default | |
65
+ | --- | --- | --- |
66
+ | `topic` | `'a2'` | must match the trigger's `topic`, and `[A-Za-z0-9_-]+` |
67
+ | `deploymentId` | `VERCEL_DEPLOYMENT_ID` | which deployment receives tasks; see below |
68
+ | `transport`, `clock` | the `@vercel/queue` client, the system clock | test seams for custom delivery |
69
+
70
+ An injected `transport` receives the topic, the task, and send options, and
71
+ owns its own provider configuration; `deploymentId` only configures the
72
+ built-in client.
73
+
74
+ ## Deployments
75
+
76
+ `deploymentId` applies to every task: retries, timers, and scheduled clears.
77
+ Leave it out to deliver to the deployment that sent the task, pass an ID to
78
+ target one deployment, or pass `null` to let Vercel pick. Every deployment that
79
+ sends or receives must use the same setting.
80
+
81
+ - **Tasks stay with their deployment.** Promoting a new deployment doesn't move
82
+ them, so older deployments keep receiving their own work until it's done or
83
+ expires. Upgrading doesn't fix an older deployment's route.
84
+ - **`null` doesn't mean latest.** Vercel picks any eligible deployment, so every
85
+ one of them must use `deploymentId: null` and handle pending tasks.
86
+ - **Changing the setting doesn't move queued tasks.** Keep the old deployments
87
+ running until those tasks finish.