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
@@ -11,9 +11,9 @@ import { orders } from '@/contracts'
11
11
  export const ordersServer = createServer({
12
12
  contract: orders,
13
13
  store: postgres({ connectionString: process.env.DATABASE_URL }),
14
- handlers: {
14
+ consumers: [
15
15
  /* ... */
16
- },
16
+ ],
17
17
  })
18
18
  ```
19
19
 
@@ -26,22 +26,70 @@ poolers.
26
26
 
27
27
  ## Create the tables
28
28
 
29
- A2 doesn't create tables in your database. This is the SQL it needs:
29
+ A2 doesn't create or change tables in your database. This is the SQL it needs:
30
30
  [`schema.sql`](https://github.com/vercel-labs/a2/blob/main/packages/a2/sql/postgres/schema.sql),
31
31
  also in the package at `node_modules/experimental-a2/sql/postgres/schema.sql`.
32
- Apply it once before your first deploy, however you run SQL against your
33
- database, as the role your app connects with.
32
+ Apply it before your first deploy, however you run SQL against your database
33
+ or as one of your migrations, as the role your app connects with. A breakpoint
34
+ comment ends each statement, for tools that send one statement per query.
35
+ `a2_batch` runs the statements it's handed, so only its owner may call it,
36
+ whatever your default privileges grant new functions. An app that connects as
37
+ another role needs `grant execute on function a2_batch(text[], jsonb)` beside
38
+ its grants on the tables, and keeps it when you apply the file again.
39
+
40
+ A2 checks nothing up front. It relies on the table and column names, the
41
+ column types, the primary keys, the unique index on `a2_events.event_id`, and
42
+ the `a2_batch` function; the other indexes keep its queries fast. A query that
43
+ finds a table, column, or the function missing throws `STORE_NOT_CONFIGURED`,
44
+ naming the file. Once it's applied, the next request works, with no redeploy.
45
+
46
+ `a2_batch` runs several statements in one round trip. Each append, claim,
47
+ completion, and `consume` poll is one call of it, so they hold a session's lock
48
+ only while the database runs the call, not across round trips from your app.
49
+ That's what keeps one busy session fast on a database a few milliseconds
50
+ away. It needs `read committed`, Postgres's default isolation level, and
51
+ throws `STORE_NOT_CONFIGURED` under a stricter default.
52
+
53
+ ## With Drizzle
54
+
55
+ [`drizzle-schema.ts`](https://github.com/vercel-labs/a2/blob/main/packages/a2/sql/postgres/drizzle-schema.ts),
56
+ next to `schema.sql`, shows the same tables as Drizzle definitions. It's an
57
+ example, not an API: one way to fit A2's tables into a Drizzle project is a
58
+ copy next to your own schema, so your migrations create them with yours.
59
+
60
+ ```ts drizzle.config.ts
61
+ import { defineConfig } from 'drizzle-kit'
62
+
63
+ export default defineConfig({
64
+ dialect: 'postgresql',
65
+ // your tables, and a copy of drizzle-schema.ts:
66
+ schema: ['./db/schema.ts', './db/a2.ts'],
67
+ out: './drizzle',
68
+ dbCredentials: { url: process.env.DATABASE_URL ?? '' },
69
+ })
70
+ ```
34
71
 
35
- ## Upgrade
72
+ `drizzle-kit generate` then writes A2's tables into a migration you can read,
73
+ and `drizzle-kit migrate` applies it. Adapt the copy to your Drizzle version
74
+ and layout, as long as the names and types stay A2's.
36
75
 
37
- When a release changes the schema, its changelog says so. Apply that
38
- release's `schema.sql` before you deploy it. The version you're running keeps
39
- working against the new schema.
76
+ Drizzle can't declare a function, so add `a2_batch` as a custom migration:
77
+ run `drizzle-kit generate --custom`, and paste the `do` block that creates
78
+ `a2_batch` from `schema.sql` into the file it makes. It creates the function
79
+ only if it's missing, so the migration is safe on any database.
80
+
81
+ A generated migration only creates tables. On a database that already has
82
+ A2's tables, from an earlier release or a `schema.sql` applied by hand, put
83
+ `schema.sql` in place of the SQL drizzle-kit generated for them: it creates
84
+ what's missing and upgrades the rest, keeping their events, and your schema
85
+ still describes them for the next `generate`. To keep a table of your own
86
+ from A2's logs, see [SQL projections](/server/sql-projections).
87
+
88
+ ## Upgrade
40
89
 
41
- If the tables are missing or out of date, the store's first query throws
42
- `STORE_NOT_CONFIGURED` naming the file to apply. Once it's applied, the next
43
- request works, with no redeploy. The store checks with one read before its
44
- first query, which never waits on or blocks writes.
90
+ A release that changes A2's tables says so in its changelog, and its
91
+ [upgrade page](/upgrades/overview) says when to apply its `schema.sql`. With
92
+ Drizzle, bring your copy of `drizzle-schema.ts` in line with the new one.
45
93
 
46
94
  ## Live updates (polling)
47
95
 
@@ -56,7 +104,10 @@ run in different invocations.
56
104
  For instant updates, pass `listenConnectionString`, a direct connection to the
57
105
  same database (not through a pooler), and apply
58
106
  [`notifications.sql`](https://github.com/vercel-labs/a2/blob/main/packages/a2/sql/postgres/notifications.sql)
59
- once, the same way as `schema.sql`.
107
+ once, the same way as `schema.sql` (with Drizzle, as a custom migration:
108
+ `drizzle-kit generate --custom`). A missing trigger fails no query, so this is
109
+ the one thing A2 looks for: a listening store's first subscription throws
110
+ `STORE_NOT_CONFIGURED` until the triggers exist.
60
111
 
61
112
  Its triggers notify listeners of every write, including writes from stores
62
113
  without the option. Notifications arrive after commit, a reconnect catches up
@@ -11,9 +11,9 @@ import { orders } from '@/contracts'
11
11
  export const ordersServer = createServer({
12
12
  contract: orders,
13
13
  store: sqlite({ path: '.a2/orders.db' }),
14
- handlers: {
14
+ consumers: [
15
15
  /* ... */
16
- },
16
+ ],
17
17
  })
18
18
  ```
19
19
 
@@ -22,7 +22,9 @@ your sessions survive `next dev` restarts. Pass it yourself to pick another
22
22
  file.
23
23
 
24
24
  It runs on `node:sqlite`, with no dependencies, and creates its tables when it
25
- opens the file. It's for development: a serverless filesystem doesn't last or
25
+ opens the file. A file a release before [0.21](/upgrades/0-21) wrote throws
26
+ `STORE_NOT_CONFIGURED` when it opens: delete it, or pass another `path`. It's
27
+ for development: a serverless filesystem doesn't last or
26
28
  share one file across instances, so production needs another
27
29
  [store](/stores/overview).
28
30
 
@@ -0,0 +1,30 @@
1
+ ---
2
+ title: Overview
3
+ description: What to do when you move to a new release. One page per release that needs steps from you, newest first.
4
+ ---
5
+
6
+ ## The releases
7
+
8
+ | Release | What you do |
9
+ | --- | --- |
10
+ | [0.21](/upgrades/0-21) | Rewrite handlers as consumers. Let earlier deployments finish; on Postgres, then apply the new `schema.sql`. SQLite starts fresh. |
11
+ | [0.20](/upgrades/0-20) | Postgres: apply `schema.sql` yourself, before you deploy. |
12
+ | [0.19](/upgrades/0-19) | Start new agent sessions: earlier ones don't fold. |
13
+ | [0.18](/upgrades/0-18) | Move the AI SDK loop's options into `harness`. |
14
+ | [0.17](/upgrades/0-17) | Redis: use a fresh `keyPrefix`. Serve agents with `agent()`. |
15
+
16
+ ## Across several releases
17
+
18
+ Read every page between the release you run and the one you're moving to,
19
+ oldest first. Code changes add up: make each page's changes. Store steps
20
+ don't always: each release's `schema.sql` covers a database from any earlier
21
+ release, so a Postgres database needs only the newest one, applied the way the
22
+ newest page says, and one fresh Redis `keyPrefix` covers every release that
23
+ asks for one.
24
+
25
+ ## Everything else
26
+
27
+ Each release's changelog lists everything it changes, including what needs no
28
+ steps: [`CHANGELOG.md`](https://github.com/vercel-labs/a2/blob/main/packages/a2/CHANGELOG.md),
29
+ also in the package at `node_modules/experimental-a2/CHANGELOG.md`. Releases
30
+ before 0.17 are only there.
@@ -0,0 +1,165 @@
1
+ ---
2
+ title: '0.21'
3
+ description: Handlers become consumers, and every store keeps their places in a new layout.
4
+ ---
5
+
6
+ ## Handlers become consumers
7
+
8
+ ```ts
9
+ // before: server/orders.ts on 0.20
10
+ export const ordersServer = createServer({
11
+ contract: orders,
12
+ handlers: {
13
+ created: async ({ event }) => {
14
+ // await sendEmailToShop(event.payload.shopId, { idempotencyKey: event.id })
15
+ return { type: 'shop.notified', payload: {} }
16
+ },
17
+ 'shop.started': { lane: 'shop', handler: async () => {} },
18
+ 'shop.notified': { lane: 'shop', handler: async () => {} },
19
+ },
20
+ })
21
+ ```
22
+
23
+ ```ts server/orders.ts
24
+ import { createServer } from 'experimental-a2/server'
25
+ import { orders } from '@/contracts'
26
+
27
+ export const ordersServer = createServer({
28
+ contract: orders,
29
+ consumers: [
30
+ {
31
+ name: 'notify-shop',
32
+ types: ['created'],
33
+ run: async ({ event }) => {
34
+ // await sendEmailToShop(event.payload.shopId, { idempotencyKey: event.id })
35
+ return { type: 'shop.notified', payload: {} }
36
+ },
37
+ },
38
+ // the types that shared the 'shop' lane, as one consumer:
39
+ {
40
+ name: 'shop-work',
41
+ types: ['shop.started', 'shop.notified'],
42
+ lane: () => 'shop',
43
+ run: async ({ event }) => {
44
+ // your work for event.type
45
+ },
46
+ },
47
+ ],
48
+ })
49
+ ```
50
+
51
+ `handlers` are gone: [consumers](/server/consumers) are the one way to react
52
+ to events. Each entry of a handler table becomes a consumer that takes its
53
+ type, with the same function as its `run`, and its `abortOn` if it had one. A
54
+ lane belongs to one consumer, so entries that shared a lane become one consumer
55
+ that takes all their types, with a lane function returning that lane. A
56
+ consumer's name is its identity: its cursors, failures, and dead letters key
57
+ on it, so pick names you'll keep.
58
+
59
+ | Before | 0.21 |
60
+ | --- | --- |
61
+ | `createServer({ handlers })` | `createServer({ consumers })` |
62
+ | `handler(entry)` | `{ name, types?, lane?, run }` in `consumers`, or `consumer({ contract, ... }).run(fn)` |
63
+ | `Handler`, `HandlerEntry` | removed: `consumer()` types `run`'s context |
64
+ | `Lane` | `ConsumerLane` |
65
+ | `HandlerContext` | `ConsumerContext` |
66
+ | `HandlerAppend` | `ConsumerAppend` |
67
+ | `handlerContext()`, from `experimental-a2/ai` | `consumerContext()` |
68
+ | `runWithHandlerContext`, from `experimental-a2/ai/server` | `runWithConsumerContext` |
69
+ | `agent({ handlers })` | `agent({ consumers })` |
70
+ | `session.redrive(index, { action })` | `session.redrive({ consumer, index }, { action })` |
71
+ | `a2.append.source` = `handler` | `a2.append.source` = `consumer` |
72
+
73
+ - **Dead letters name their consumer.** An event dead-letters for one
74
+ consumer, so each entry from `session.deadLetters()` carries `consumer`.
75
+ Pass it to `redrive` with the index. See
76
+ [Resolve a dead letter](/server/consumers#resolve-a-dead-letter).
77
+ - **Agents take `consumers`.** Write them with `consumer()` against the agent's
78
+ contract, as in [Conversations](/agents/conversations). A consumer of an AI
79
+ event type runs beside the agent's own, so `agent()` no longer rejects one.
80
+ - **Actors keep `actor({ handlers })`.** Nothing changes in an actor's code.
81
+ - **A2's own consumers start with `a2.`** Actors and agents run on consumers
82
+ named `a2.actor`, `a2.actor.concurrent`, `a2.ai.metadata`, `a2.ai.control`,
83
+ `a2.ai.model`, and `a2.ai.tools`, which their dead letters name. Their event
84
+ ids are unchanged. Names starting with `a2.` are reserved.
85
+ - **Telemetry.** Dashboards that filter `a2.append.source` on `handler` read
86
+ `consumer` instead. Each `a2.event` span also carries `a2.event.consumer`;
87
+ see [Telemetry](/reference/telemetry).
88
+ - **Custom stores** implement the cursor contract: `cursors()`, `wakeLog`, and
89
+ the cursor-keyed claim, completion, failure, and redrive methods. See
90
+ [`A2Store`](/reference/server#a2store).
91
+
92
+ ## Postgres
93
+
94
+ 0.21 keeps each consumer's place in new tables, `a2_cursors` and
95
+ `a2_lane_events`. Its `schema.sql` creates them, then drops the columns earlier
96
+ releases dispatched by, and the `a2_schema` table 0.20 reads on a cold start.
97
+ Earlier deployments can't run against the new tables, and 0.21 needs them.
98
+
99
+ 1. **Let earlier deployments finish their work.** Applying the schema drops
100
+ any work their handlers haven't finished, since 0.21 runs no handlers to
101
+ finish it. This query counts that work, dead letters included (retry or
102
+ skip those first, or accept that they're dropped):
103
+
104
+ ```sql
105
+ select count(*) from a2_events where processed_at is null
106
+ ```
107
+
108
+ It doesn't count timers. On [Vercel Queues](/schedulers/vercel-queues#deployments),
109
+ each one goes to the deployment that scheduled it, which can't write once
110
+ the schema is applied, so a timer still waiting then is lost. Let them fire,
111
+ or schedule the ones you need again on 0.21.
112
+
113
+ 2. **Apply 0.21's `schema.sql` while none of them is running**, as the role
114
+ your app connects with:
115
+ `node_modules/experimental-a2/sql/postgres/schema.sql`. Every event is
116
+ kept. There's no separate migration: the file is the whole schema, and
117
+ each statement creates what's missing or removes what an earlier release
118
+ used, so it brings a database from any release to 0.21 and changes nothing
119
+ when you apply it again.
120
+ 3. **Deploy 0.21.** Deployed before the schema, its queries that reach a
121
+ missing table throw `STORE_NOT_CONFIGURED`, naming the file, and the next
122
+ request after you apply it works, with no redeploy.
123
+
124
+ 0.21's `schema.sql` also creates a function, `a2_batch`, that each append,
125
+ claim, completion, and `consume` poll runs through in one round trip. Applying
126
+ the file creates it; with Drizzle, add it as a custom migration (see
127
+ [With Drizzle](/stores/postgres#with-drizzle)). Without it, 0.21's writes throw
128
+ `STORE_NOT_CONFIGURED`, naming `schema.sql`.
129
+
130
+ 0.21 also checks nothing up front: no schema version read before the first
131
+ query. A query that finds a table or column missing, or that its role can't
132
+ use, throws `STORE_NOT_CONFIGURED`, naming the file to apply or the grant to
133
+ make. With Drizzle, `drizzle-schema.ts` shows the new tables; on a database
134
+ that already has A2's tables, use `schema.sql` as the migration for them. See
135
+ [With Drizzle](/stores/postgres#with-drizzle).
136
+
137
+ ## SQLite
138
+
139
+ A file an earlier release wrote throws `STORE_NOT_CONFIGURED` when the store
140
+ opens it. Delete it, `.a2/dev.db` by default, or pass another `path`. Its
141
+ sessions don't carry over.
142
+
143
+ ## Redis
144
+
145
+ 0.21 keeps each consumer's place in keys of its own, and reads the log,
146
+ snapshots, and clears that earlier releases wrote as they are. Keep your
147
+ `keyPrefix`.
148
+
149
+ 1. **Let earlier deployments finish their work, then stop them.** 0.21 runs
150
+ no handlers, so the work theirs haven't finished, dead letters included, is
151
+ dropped. An earlier deployment still running beside 0.21 keeps its own lanes
152
+ on the same prefix, so one lane's events can run on both at once. On
153
+ [Vercel Queues](/schedulers/vercel-queues#deployments) it also keeps
154
+ receiving the timers it scheduled, until they fire: let them, or remove the
155
+ deployment and schedule the ones you need again on 0.21.
156
+ 2. **Deploy 0.21.** Every event is kept. A consumer starts at the next event
157
+ appended, or with `backfill` at the beginning of the log.
158
+
159
+ The keys earlier releases dispatched by (`meta`, `meta:{n}`, `meta-split`,
160
+ `pending`, `ready`, `snapshot-pins:{n}`, `snapshot-pin-counts`, under each
161
+ session) stay until you delete them; 0.21 never reads them.
162
+
163
+ ## Memory
164
+
165
+ Nothing to do: the memory store starts empty in every process.
@@ -0,0 +1,43 @@
1
+ ---
2
+ title: '0.20'
3
+ description: The Postgres store no longer creates its tables. You apply them, before you deploy.
4
+ ---
5
+
6
+ ## Postgres
7
+
8
+ Before you deploy 0.20, apply the SQL its store needs,
9
+ `node_modules/experimental-a2/sql/postgres/schema.sql`, once per database,
10
+ however you run SQL against it. It creates only what's missing and leaves
11
+ existing data alone, so it's safe on a database an earlier release set up, and
12
+ the deployment running now keeps working against it, without waiting on its
13
+ writes.
14
+
15
+ - **Apply it as the role your app connects with.** It records a schema version
16
+ in a new `a2_schema` table, which the store reads on a cold start. If
17
+ another role applies it, grant your app's role `select` on it:
18
+
19
+ ```sql
20
+ grant select on a2_schema to your_app_role;
21
+ ```
22
+
23
+ - **With `listenConnectionString`**, also apply
24
+ `sql/postgres/notifications.sql`. It installs the triggers the store used to
25
+ install for you.
26
+ - **The app's role needs no DDL rights.** Read and write access to A2's tables
27
+ is enough.
28
+
29
+ ## If you deploy first
30
+
31
+ The store throws `STORE_NOT_CONFIGURED` on its first query, naming the file to
32
+ apply. Until you apply it, nothing is written: appends fail, and scheduled work
33
+ waits. Vercel Queues keeps redelivering it, and the QStash scheduler hands each
34
+ task to a later message, 30 seconds out and backing off to one day. The next
35
+ call after you apply the file succeeds, with no redeploy.
36
+
37
+ ## Other stores
38
+
39
+ Nothing to do. SQLite still creates its tables when it opens its file, and
40
+ Redis creates its keys as it writes.
41
+
42
+ Moving past 0.20 to 0.21 or later? Apply that release's `schema.sql` instead,
43
+ the way [0.21](/upgrades/0-21) says. It drops `a2_schema`.
@@ -0,0 +1,33 @@
1
+ ---
2
+ title: '0.19'
3
+ description: An agent's log stores each message once, so agent sessions from earlier releases don't fold.
4
+ ---
5
+
6
+ ## Start new agent sessions
7
+
8
+ 0.19 stores each message's content once, in the `ai.control.requested` command
9
+ that brought it in. The AI reducers advance to `a2.ai.state.v27`,
10
+ `a2.ai.context.v7`, and `a2.ai.control.v6`, and a log written before 0.19
11
+ doesn't fold: `reduceAIState()` and `deriveUIMessages()` throw on an
12
+ `ai.message.created` that no command explains. Start new agent sessions after
13
+ you deploy. Earlier sessions keep their events in history, but their state
14
+ can't be read.
15
+
16
+ ## Payloads and types
17
+
18
+ Code that reads the AI log directly changes with it:
19
+
20
+ - **`ai.message.created`** records an admission as
21
+ `{ messageId, role, generate? }`, without the message.
22
+ `MessageCreatedPayload` takes no type argument.
23
+ - **`ai.control.committed`** carries no message content. Queued inputs and
24
+ `AIState.active.input` are `InputRef`s, `{ id, role, generate, afterStepOf? }`,
25
+ and `ActiveTurn` takes no type argument. `AIState.inbox.items` keep their
26
+ `message`, now with `role`.
27
+
28
+ ## Provider tools under `fx()` and `pi()`
29
+
30
+ `agent()` throws a `TypeError` when a provider-defined tool
31
+ (`type: 'provider'`, such as a provider's web search) is passed with `fx()` or
32
+ `pi()`. Before, both harnesses dropped it without a word, so the model never
33
+ saw it. Take it out of those agents' `tools`.
@@ -0,0 +1,33 @@
1
+ ---
2
+ title: '0.18'
3
+ description: An agent's loop is its harness, and the AI SDK loop's options move into it.
4
+ ---
5
+
6
+ ## Move loop options into `harness`
7
+
8
+ ```ts server/assistant.ts
9
+ import { agent } from 'experimental-a2/ai/server'
10
+
11
+ export const assistant = agent({
12
+ name: 'support-agent',
13
+ model: 'openai/gpt-5.6-terra',
14
+ instructions: 'Help the customer. Be concise.',
15
+ harness: {
16
+ type: 'ai-sdk',
17
+ // generation, generate, compaction, and messageMetadata go here:
18
+ compaction: { thresholdTokens: 80_000 },
19
+ },
20
+ })
21
+ ```
22
+
23
+ `agent()` takes its loop as `harness`. The AI SDK's loop is the default,
24
+ `{ type: 'ai-sdk' }`, and its options move into it: `generation`, `generate`,
25
+ `compaction`, and `messageMetadata` are no longer top-level `agent()` options,
26
+ and passing one there throws a `TypeError`. See
27
+ [Harnesses](/agents/harnesses).
28
+
29
+ ## Models are AI Gateway IDs
30
+
31
+ `model` is an AI Gateway model ID, or `ModelSettings` with one, under every
32
+ harness. `agent()` no longer takes AI SDK provider model objects, and
33
+ `HarnessModelSettings` is gone: use `ModelSettings`.
@@ -0,0 +1,69 @@
1
+ ---
2
+ title: '0.17'
3
+ description: Redis stores need a fresh key prefix, and agents are served with `agent()`.
4
+ ---
5
+
6
+ ## Redis: a fresh `keyPrefix`
7
+
8
+ ```ts server/store.ts
9
+ import { redis } from 'experimental-a2/store-redis'
10
+
11
+ export const store = redis({
12
+ url: process.env.REDIS_URL,
13
+ // a prefix no earlier release wrote under:
14
+ keyPrefix: 'a2-017',
15
+ })
16
+ ```
17
+
18
+ Both Redis stores change how they keep event ids and per-event metadata: one
19
+ key per event id instead of one shared hash, and metadata in hashes of 1,000
20
+ events. 0.17 no longer reads the shared id hash, and earlier releases can't
21
+ see what 0.17 writes, so on a shared prefix a retry on either side can append
22
+ a duplicate event, re-run work, or stall a lane. Switch to a fresh `keyPrefix`
23
+ when you upgrade, and don't roll back to an earlier release on a prefix 0.17
24
+ has written. Vercel Queues keeps running recovery and scheduled work on the
25
+ deployment that published it for up to seven days, so earlier deployments keep
26
+ touching their sessions after you upgrade.
27
+
28
+ ## Before you clear sessions
29
+
30
+ 0.17 adds [`session.clear()`](/contract/sessions#clearing-a-session). Upgrade
31
+ every instance before you clear any session: an earlier release ignores the
32
+ clear and can run cleared events in the new lifetime. Upgrade every scheduler
33
+ route before you schedule clears, and don't roll back while clear tasks are
34
+ pending.
35
+
36
+ ## Vercel Queues
37
+
38
+ `vercelQueues({ scheduledAppends })` is replaced by
39
+ `deploymentId?: string | null`: leave it out to deliver to the current
40
+ deployment, pass a deployment ID to target one, or pass `null` for
41
+ deploymentless delivery. It applies to every task, recovery included, so
42
+ `null` makes recovery deploymentless too, unlike `'deploymentless'` did.
43
+ Changing it doesn't move tasks already queued. A custom `QueueTransport.send`
44
+ no longer takes a fourth routing argument. See
45
+ [Deployments](/schedulers/vercel-queues#deployments).
46
+
47
+ ## Agents
48
+
49
+ | Before | 0.17 |
50
+ | --- | --- |
51
+ | `createAgentServer({ agent: definition, ... })` | `agent({ name, ... })`, or `agent({ contract, ... })` with a shared contract |
52
+ | `agent()` from `experimental-a2/ai` | `agentContract()` |
53
+ | `createReducer()`, `reducerName` | `agentContract({ reducer })` |
54
+ | `createHandlers()`, `authorizeAgentIngress` | removed: pass `handlers` and `events` to `agent()` |
55
+ | `handlerContext(agent)` | `handlerContext()`, or `handlerContext(contract)` |
56
+ | `request` in `generate` and `messageMetadata` | `event` |
57
+ | `requestId` | `event.id` |
58
+ | `responseMessageId` | `event.payload.responseMessageId` |
59
+
60
+ - **`agent({ contract })` takes only an `agentContract()`.** Any other contract
61
+ throws. A fully custom contract is core A2, served by `createServer()`.
62
+ - **Upgrade clients with the server.** A client from before 0.17 ignores the
63
+ new shape of streamed progress, so a response's output doesn't appear in it
64
+ until it reloads.
65
+ - **Custom telemetry recorders** must accept the new span names: core claim
66
+ and commit spans, and `a2.ai.*`.
67
+
68
+ 0.21 later renames `handlers` to `consumers` and `handlerContext()` to
69
+ `consumerContext()`; see [0.21](/upgrades/0-21).
@@ -0,0 +1,3 @@
1
+ // A release's page is numbered 100 minus its minor version (79-0-21.mdx for
2
+ // 0.21), so the newest sorts first without renaming the others.
3
+ export default { order: 9 }
@@ -24,13 +24,31 @@ Redis, poll cadence elsewhere. Upstash is durable by default; use its TCP
24
24
  `rediss://` URL, not the REST one. Each feature creates its own server with
25
25
  the shared store; storage remains namespaced per contract. Without either variable, a
26
26
  production boot fails fast with `STORE_NOT_CONFIGURED` (by design).
27
- Redis needs no setup. With Postgres, create A2's tables once before the
28
- first deploy (A2 never creates them at runtime):
27
+ Redis needs no setup. Postgres is migrated by every Vercel build: the
28
+ `vercel-build` script runs `scripts/migrate.mjs` (Drizzle's migrator, which,
29
+ unlike `drizzle-kit migrate`, prints the error when one fails) before the
30
+ usual `turbo run build`, through `DATABASE_URL_UNPOOLED` when it's set (the Neon
31
+ integration's direct connection). Previews and production share one database here, so a branch's
32
+ migration reaches it when its preview builds: keep migrations additive.
33
+ Elsewhere, migrate by hand (A2 never creates its tables at runtime):
29
34
 
30
35
  ```sh
31
- psql "$DATABASE_URL" -v ON_ERROR_STOP=1 -f node_modules/experimental-a2/sql/postgres/schema.sql
36
+ DATABASE_URL=postgres://… pnpm --filter playground db:migrate
32
37
  ```
33
38
 
39
+ It applies every migration in `app/a2slack/db/migrations`. A2's tables are in
40
+ the Drizzle schema with a2slack's: `app/a2slack/db/a2.ts` is a copy of
41
+ `experimental-a2`'s `sql/postgres/drizzle-schema.ts`. drizzle-kit generated
42
+ their migration (`0002_a2.sql`) from it, and its SQL is `schema.sql`'s
43
+ instead of the generated `CREATE TABLE`s, because the shared database already
44
+ had A2's tables: it creates what's missing and upgrades the rest. Drizzle
45
+ can't declare A2's `a2_batch` function, so `0003_a2_batch.sql` is a custom
46
+ migration (`drizzle-kit generate --custom`) holding `schema.sql`'s block for
47
+ it. When a
48
+ release of `experimental-a2` changes its tables (its changelog says so),
49
+ update `a2.ts` from the new file, run `pnpm db:generate`, and read the SQL it
50
+ writes.
51
+
34
52
  To verify a provider before leaning on it, run the full conformance
35
53
  kit against it (throwaway database; the suite isolates by key prefix
36
54
  and sweeps after itself):
@@ -42,6 +60,19 @@ TEST_REDIS_URL=rediss://… pnpm --filter a2 exec vitest run test/store-redis.co
42
60
  `pg` and `ioredis` are optional peer dependencies of `experimental-a2` and direct
43
61
  dependencies of this app.
44
62
 
63
+ a2slack keeps its tables in the Postgres at `DATABASE_URL`, defined with
64
+ Drizzle in `app/a2slack/db/schema.ts`. With `REDIS_URL` set too, A2 runs on
65
+ Redis and only a2slack's tables are used in Postgres (A2's are created there
66
+ too, and stay empty). Generate a migration with `pnpm db:generate` after
67
+ changing the schema; SQL drizzle-kit can't express, like an extension, goes
68
+ in a custom one (`pnpm db:generate --custom --name …`).
69
+
70
+ Its search index lives in Upstash Redis Search, through the Redis's REST
71
+ credentials (`KV_REST_API_URL` and `KV_REST_API_TOKEN`, or this project's
72
+ `REDIS_KV_REST_API_*`). The index is created the first time it's written.
73
+ Without them, search reads the Postgres table instead. In development the
74
+ tables live in PGlite at `.a2/a2slack`, migrated when it opens.
75
+
45
76
  ## 2. Vercel Queues scheduler
46
77
 
47
78
  Queues need no configuration beyond the triggers in `vercel.json`. A0, Orders,
@@ -153,10 +184,11 @@ vercel deploy
153
184
  ```
154
185
 
155
186
  The Recovery lab tests a real function timeout. Its dedicated trigger and
156
- queue consumer both export `maxDuration = 20`. The pipeline has four 8-second
157
- handlers, so every handler fits in a fresh invocation but the 32-second chain
158
- does not. A2 keeps dispatching events. Vercel stops the first invocation while
159
- the third handler is running, leaving that event without a processed marker.
187
+ queue consumer both export `maxDuration = 20`. The pipeline is four 8-second
188
+ steps, run in order by one A2 consumer, so every step fits in a fresh
189
+ invocation but the 32-second chain does not. A2 keeps dispatching events.
190
+ Vercel stops the first invocation while the third step is running, leaving
191
+ the consumer's cursor on that event.
160
192
  The deadline-capped claim expires, its watchdog retries the same event in a
161
193
  fresh function, and already completed events do not run again. The dedicated
162
194
  `a2-recovery` topic keeps this artificial budget away from the
@@ -10,7 +10,7 @@ import {
10
10
  within,
11
11
  } from '@testing-library/react'
12
12
  import { tool } from 'ai'
13
- import { handlerContext, inputs } from 'experimental-a2/ai'
13
+ import { consumerContext, inputs } from 'experimental-a2/ai'
14
14
  import { agent } from 'experimental-a2/ai/server'
15
15
  import type { A2Operation } from 'experimental-a2/server'
16
16
  import { memory } from 'experimental-a2/store-memory'
@@ -57,7 +57,7 @@ const tools = {
57
57
  bash: tool({
58
58
  inputSchema: z.object({ command: z.string() }),
59
59
  execute: async () => {
60
- const { signal } = handlerContext()
60
+ const { signal } = consumerContext()
61
61
  await new Promise<void>((resolve, reject) => {
62
62
  void bashDone.promise.then(resolve)
63
63
  signal.addEventListener('abort', () => reject(signal.reason), {
@@ -1,7 +1,7 @@
1
1
  import 'server-only'
2
2
  import { contract } from 'experimental-a2'
3
3
  import { vercelQueues } from 'experimental-a2/scheduler-vercel'
4
- import { createServer } from 'experimental-a2/server'
4
+ import { consumer, createServer } from 'experimental-a2/server'
5
5
  import { z } from 'zod'
6
6
  import { store } from '@/lib/store'
7
7
  import { isAppId } from './apps'
@@ -31,24 +31,26 @@ export const sandboxScheduler =
31
31
  ? vercelQueues({ topic: 'a2-a0-sandboxes' })
32
32
  : undefined
33
33
 
34
+ const deleteIdleSandbox = consumer({
35
+ contract: sandboxLog,
36
+ name: 'delete-idle-sandbox',
37
+ types: ['sandbox.idle.checked'],
38
+ }).run(async ({ event }) => {
39
+ const { appId } = event.payload
40
+ // A prompt since the sandbox opened makes it a real app's, whichever
41
+ // harness's agent got it.
42
+ const reads = await Promise.all(
43
+ Object.values(agentServers).map((server) => server.session(appId).state()),
44
+ )
45
+ if (reads.every(({ state }) => state.messages.length === 0))
46
+ await deleteAppSandbox({ appId })
47
+ })
48
+
34
49
  export const sandboxServer = createServer({
35
50
  ...(store ? { store } : {}),
36
51
  ...(sandboxScheduler ? { scheduler: sandboxScheduler } : {}),
37
52
  contract: sandboxLog,
38
- handlers: {
39
- 'sandbox.idle.checked': async ({ event }) => {
40
- const { appId } = event.payload
41
- // A prompt since the sandbox opened makes it a real app's, whichever
42
- // harness's agent got it.
43
- const reads = await Promise.all(
44
- Object.values(agentServers).map((server) =>
45
- server.session(appId).state(),
46
- ),
47
- )
48
- if (reads.every(({ state }) => state.messages.length === 0))
49
- await deleteAppSandbox({ appId })
50
- },
51
- },
53
+ consumers: [deleteIdleSandbox],
52
54
  })
53
55
 
54
56
  /**