experimental-a2 0.20.0 → 0.22.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (309) hide show
  1. package/CHANGELOG.md +55 -0
  2. package/README.md +1 -1
  3. package/dist/{actor-C7b-1arn.d.ts → actor-CqibLgjQ.d.ts} +15 -7
  4. package/dist/actor-CqibLgjQ.d.ts.map +1 -0
  5. package/dist/actor-client.d.ts +1 -1
  6. package/dist/actor-client.js +2 -2
  7. package/dist/actor-react.d.ts +3 -3
  8. package/dist/actor-react.js +5 -5
  9. package/dist/{actor-shared-t7w8xIKP.js → actor-shared-DFET5q16.js} +3 -3
  10. package/dist/actor-shared-DFET5q16.js.map +1 -0
  11. package/dist/actor.d.ts +1 -1
  12. package/dist/actor.js +54 -43
  13. package/dist/actor.js.map +1 -1
  14. package/dist/{ai-Cz3hqVhk.d.ts → ai-DAu77CDr.d.ts} +8 -8
  15. package/dist/{ai-Cz3hqVhk.d.ts.map → ai-DAu77CDr.d.ts.map} +1 -1
  16. package/dist/{ai-mMiWul1T.js → ai-EEfKyc-z.js} +9 -9
  17. package/dist/ai-EEfKyc-z.js.map +1 -0
  18. package/dist/ai-client.d.ts +2 -2
  19. package/dist/ai-client.js +2 -2
  20. package/dist/ai-fx.d.ts +4 -2
  21. package/dist/ai-fx.d.ts.map +1 -1
  22. package/dist/ai-fx.js +214 -67
  23. package/dist/ai-fx.js.map +1 -1
  24. package/dist/{ai-harness-CV6rXbB3.d.ts → ai-harness-DTjkAr2T.d.ts} +2 -2
  25. package/dist/{ai-harness-CV6rXbB3.d.ts.map → ai-harness-DTjkAr2T.d.ts.map} +1 -1
  26. package/dist/{ai-harness-Clbg5ei7.js → ai-harness-dQeJaR1t.js} +7 -3
  27. package/dist/ai-harness-dQeJaR1t.js.map +1 -0
  28. package/dist/ai-pi.d.ts +1 -1
  29. package/dist/ai-pi.js +3 -3
  30. package/dist/ai-pi.js.map +1 -1
  31. package/dist/ai-react.d.ts +5 -5
  32. package/dist/ai-react.js +2 -2
  33. package/dist/ai-server.d.ts +14 -17
  34. package/dist/ai-server.d.ts.map +1 -1
  35. package/dist/ai-server.js +86 -73
  36. package/dist/ai-server.js.map +1 -1
  37. package/dist/ai.d.ts +2 -2
  38. package/dist/ai.js +2 -2
  39. package/dist/{client-BhHLoNKF.js → client-BQS4i_V9.js} +29 -6
  40. package/dist/client-BQS4i_V9.js.map +1 -0
  41. package/dist/{client-h_DKFAjj.d.ts → client-CUMbS_38.d.ts} +3 -3
  42. package/dist/{client-h_DKFAjj.d.ts.map → client-CUMbS_38.d.ts.map} +1 -1
  43. package/dist/client.d.ts +3 -3
  44. package/dist/client.js +1 -1
  45. package/dist/{contract-BYKASlCV.js → contract-CWukt01d.js} +2 -2
  46. package/dist/contract-CWukt01d.js.map +1 -0
  47. package/dist/index.d.ts +5 -5
  48. package/dist/index.js +3 -3
  49. package/dist/{initial-snapshot-Bx-c0pDP.d.ts → initial-snapshot-D3Lve43o.d.ts} +4 -4
  50. package/dist/initial-snapshot-D3Lve43o.d.ts.map +1 -0
  51. package/dist/{internal-CscSY7K_.js → internal-CvsrprnU.js} +40 -16
  52. package/dist/internal-CvsrprnU.js.map +1 -0
  53. package/dist/otel.d.ts +1 -1
  54. package/dist/otel.js +1 -1
  55. package/dist/otel.js.map +1 -1
  56. package/dist/{react-BBXgHWFc.d.ts → react-BoHvC3v9.d.ts} +4 -4
  57. package/dist/{react-BBXgHWFc.d.ts.map → react-BoHvC3v9.d.ts.map} +1 -1
  58. package/dist/react.d.ts +1 -1
  59. package/dist/react.js +1 -1
  60. package/dist/{reducer-CqA59Q6U.d.ts → reducer-CYPHsSmd.d.ts} +2 -2
  61. package/dist/{reducer-CqA59Q6U.d.ts.map → reducer-CYPHsSmd.d.ts.map} +1 -1
  62. package/dist/{reducer-BAXc8JyF.js → reducer-CjmvZSAb.js} +2 -2
  63. package/dist/{reducer-BAXc8JyF.js.map → reducer-CjmvZSAb.js.map} +1 -1
  64. package/dist/runtime-consumer-CIeL33wj.js +12 -0
  65. package/dist/runtime-consumer-CIeL33wj.js.map +1 -0
  66. package/dist/scheduler-qstash.d.ts +2 -2
  67. package/dist/scheduler-qstash.js +3 -3
  68. package/dist/scheduler-task-X1SToDjP.js.map +1 -1
  69. package/dist/scheduler-vercel.d.ts +2 -2
  70. package/dist/scheduler-vercel.js +3 -3
  71. package/dist/scheduler-vercel.js.map +1 -1
  72. package/dist/{server-qyb5aJJG.js → server-DtFrWxi3.js} +412 -116
  73. package/dist/server-DtFrWxi3.js.map +1 -0
  74. package/dist/{server-DYAATFuT.d.ts → server-qgez0Xny.d.ts} +108 -41
  75. package/dist/server-qgez0Xny.d.ts.map +1 -0
  76. package/dist/server.browser.js +1 -1
  77. package/dist/server.browser.js.map +1 -1
  78. package/dist/server.d.ts +4 -4
  79. package/dist/server.js +2 -2
  80. package/dist/{session-clear-C-QwTXV1.d.ts → session-clear-D1PFe7xl.d.ts} +2 -2
  81. package/dist/session-clear-D1PFe7xl.d.ts.map +1 -0
  82. package/dist/{session-clear-DGI4jTQ5.js → session-clear-wHd5GCpZ.js} +5 -4
  83. package/dist/session-clear-wHd5GCpZ.js.map +1 -0
  84. package/dist/store-D0MhBvg6.d.ts +532 -0
  85. package/dist/store-D0MhBvg6.d.ts.map +1 -0
  86. package/dist/store-N8PXxDAS.js.map +1 -1
  87. package/dist/store-codec-DwrWlgQx.js +65 -0
  88. package/dist/store-codec-DwrWlgQx.js.map +1 -0
  89. package/dist/store-memory.d.ts +1 -1
  90. package/dist/store-memory.d.ts.map +1 -1
  91. package/dist/store-memory.js +406 -225
  92. package/dist/store-memory.js.map +1 -1
  93. package/dist/{store-polling-7WJV0YWP.js → store-polling-BZt2Rf3D.js} +2 -2
  94. package/dist/{store-polling-7WJV0YWP.js.map → store-polling-BZt2Rf3D.js.map} +1 -1
  95. package/dist/store-postgres.d.ts +1 -1
  96. package/dist/store-postgres.d.ts.map +1 -1
  97. package/dist/store-postgres.js +1598 -587
  98. package/dist/store-postgres.js.map +1 -1
  99. package/dist/store-presence-polling-JuAy2Ck2.js +111 -0
  100. package/dist/store-presence-polling-JuAy2Ck2.js.map +1 -0
  101. package/dist/store-redis-http.d.ts +1 -1
  102. package/dist/store-redis-http.js +4 -4
  103. package/dist/store-redis-notify-CsJw_hfD.js +3060 -0
  104. package/dist/store-redis-notify-CsJw_hfD.js.map +1 -0
  105. package/dist/store-redis.d.ts +1 -1
  106. package/dist/store-redis.d.ts.map +1 -1
  107. package/dist/store-redis.js +4 -5
  108. package/dist/store-redis.js.map +1 -1
  109. package/dist/store-sqlite.d.ts +1 -1
  110. package/dist/store-sqlite.d.ts.map +1 -1
  111. package/dist/store-sqlite.js +527 -299
  112. package/dist/store-sqlite.js.map +1 -1
  113. package/dist/stream-buffer-DrTyiIuo.js +57 -0
  114. package/dist/stream-buffer-DrTyiIuo.js.map +1 -0
  115. package/dist/{telemetry-Cyfqo6Qt.d.ts → telemetry-CWwsh-Gx.d.ts} +3 -3
  116. package/dist/telemetry-CWwsh-Gx.d.ts.map +1 -0
  117. package/dist/{wire-BKxH1vbP.js → wire-CV-Z7wz-.js} +6 -5
  118. package/dist/wire-CV-Z7wz-.js.map +1 -0
  119. package/docs/01-quickstart.mdx +21 -15
  120. package/docs/02-examples.mdx +1 -1
  121. package/docs/actors/01-introduction.mdx +13 -2
  122. package/docs/actors/02-concurrency.mdx +1 -1
  123. package/docs/agents/01-introduction.mdx +1 -1
  124. package/docs/agents/02-tools.mdx +12 -11
  125. package/docs/agents/03-conversations.mdx +36 -25
  126. package/docs/agents/04-harnesses.mdx +14 -7
  127. package/docs/client/03-presence.mdx +18 -14
  128. package/docs/contract/01-events.mdx +2 -2
  129. package/docs/contract/02-state.mdx +18 -14
  130. package/docs/contract/03-sessions.mdx +32 -25
  131. package/docs/index.mdx +29 -24
  132. package/docs/reference/01-core.mdx +7 -6
  133. package/docs/reference/02-server.mdx +250 -191
  134. package/docs/reference/04-ai.mdx +33 -23
  135. package/docs/reference/05-telemetry.mdx +27 -13
  136. package/docs/reference/06-errors.mdx +25 -19
  137. package/docs/reference/meta.ts +1 -1
  138. package/docs/schedulers/01-overview.mdx +8 -8
  139. package/docs/schedulers/02-vercel-queues.mdx +2 -2
  140. package/docs/schedulers/03-qstash.mdx +4 -4
  141. package/docs/server/01-consumers.mdx +418 -0
  142. package/docs/server/02-timers.mdx +31 -21
  143. package/docs/server/03-cancellation.mdx +52 -44
  144. package/docs/server/04-routes.mdx +4 -1
  145. package/docs/server/05-sql-projections.mdx +391 -0
  146. package/docs/server/06-guarantees.mdx +61 -51
  147. package/docs/stores/01-overview.mdx +3 -3
  148. package/docs/stores/02-redis.mdx +24 -10
  149. package/docs/stores/03-redis-http.mdx +8 -6
  150. package/docs/stores/04-postgres.mdx +65 -14
  151. package/docs/stores/05-sqlite.mdx +5 -3
  152. package/docs/upgrades/01-overview.mdx +31 -0
  153. package/docs/upgrades/78-0-22.mdx +115 -0
  154. package/docs/upgrades/79-0-21.mdx +165 -0
  155. package/docs/upgrades/80-0-20.mdx +43 -0
  156. package/docs/upgrades/81-0-19.mdx +33 -0
  157. package/docs/upgrades/82-0-18.mdx +33 -0
  158. package/docs/upgrades/83-0-17.mdx +69 -0
  159. package/docs/upgrades/meta.ts +3 -0
  160. package/examples/playground/DEPLOY.md +39 -7
  161. package/examples/playground/app/a0/chat.test.tsx +2 -2
  162. package/examples/playground/app/a0/sandbox-log.ts +17 -15
  163. package/examples/playground/app/a0/sandbox.ts +58 -13
  164. package/examples/playground/app/a0/server.ts +4 -4
  165. package/examples/playground/app/a2slack/a2slack.css +1067 -0
  166. package/examples/playground/app/a2slack/c/[channel]/page.tsx +20 -0
  167. package/examples/playground/app/a2slack/consumers/agent.ts +63 -0
  168. package/examples/playground/app/a2slack/consumers/feed.ts +164 -0
  169. package/examples/playground/app/a2slack/consumers/notify.ts +116 -0
  170. package/examples/playground/app/a2slack/consumers/search.ts +37 -0
  171. package/examples/playground/app/a2slack/consumers/triage.ts +31 -0
  172. package/examples/playground/app/a2slack/consumers/welcome.ts +25 -0
  173. package/examples/playground/app/a2slack/db/a2.ts +209 -0
  174. package/examples/playground/app/a2slack/db/index.ts +73 -0
  175. package/examples/playground/app/a2slack/db/migrations/0000_extensions.sql +4 -0
  176. package/examples/playground/app/a2slack/db/migrations/0001_workspace.sql +74 -0
  177. package/examples/playground/app/a2slack/db/migrations/0002_a2.sql +215 -0
  178. package/examples/playground/app/a2slack/db/migrations/0003_a2_batch.sql +67 -0
  179. package/examples/playground/app/a2slack/db/migrations/0004_a2_session_event_ids.sql +2 -0
  180. package/examples/playground/app/a2slack/db/migrations/meta/0000_snapshot.json +18 -0
  181. package/examples/playground/app/a2slack/db/migrations/meta/0001_snapshot.json +500 -0
  182. package/examples/playground/app/a2slack/db/migrations/meta/0002_snapshot.json +1159 -0
  183. package/examples/playground/app/a2slack/db/migrations/meta/0003_snapshot.json +1159 -0
  184. package/examples/playground/app/a2slack/db/migrations/meta/0004_snapshot.json +1165 -0
  185. package/examples/playground/app/a2slack/db/migrations/meta/_journal.json +41 -0
  186. package/examples/playground/app/a2slack/db/queries.ts +263 -0
  187. package/examples/playground/app/a2slack/db/schema.ts +145 -0
  188. package/examples/playground/app/a2slack/drills/a2slack.drill.ts +708 -0
  189. package/examples/playground/app/a2slack/drills/harness.ts +244 -0
  190. package/examples/playground/app/a2slack/events/route.ts +36 -0
  191. package/examples/playground/app/a2slack/identity.ts +44 -0
  192. package/examples/playground/app/a2slack/inbox/route.ts +8 -0
  193. package/examples/playground/app/a2slack/lag/data/route.ts +8 -0
  194. package/examples/playground/app/a2slack/lag/page.tsx +101 -0
  195. package/examples/playground/app/a2slack/lag.ts +73 -0
  196. package/examples/playground/app/a2slack/layout.tsx +43 -0
  197. package/examples/playground/app/a2slack/messages/route.ts +17 -0
  198. package/examples/playground/app/a2slack/model.test.ts +208 -0
  199. package/examples/playground/app/a2slack/model.ts +369 -0
  200. package/examples/playground/app/a2slack/models.test.ts +72 -0
  201. package/examples/playground/app/a2slack/page.tsx +10 -0
  202. package/examples/playground/app/a2slack/scheduler/route.ts +5 -0
  203. package/examples/playground/app/a2slack/search/route.ts +40 -0
  204. package/examples/playground/app/a2slack/search-index.ts +108 -0
  205. package/examples/playground/app/a2slack/server.test.ts +315 -0
  206. package/examples/playground/app/a2slack/server.ts +55 -0
  207. package/examples/playground/app/a2slack/sessions.ts +50 -0
  208. package/examples/playground/app/a2slack/thread/route.ts +8 -0
  209. package/examples/playground/app/a2slack/ui/ChannelView.tsx +87 -0
  210. package/examples/playground/app/a2slack/ui/Composer.tsx +117 -0
  211. package/examples/playground/app/a2slack/ui/DotsShadow.tsx +42 -0
  212. package/examples/playground/app/a2slack/ui/Feed.tsx +157 -0
  213. package/examples/playground/app/a2slack/ui/InboxContext.tsx +63 -0
  214. package/examples/playground/app/a2slack/ui/InboxView.tsx +82 -0
  215. package/examples/playground/app/a2slack/ui/Menu.tsx +128 -0
  216. package/examples/playground/app/a2slack/ui/MessageRow.tsx +314 -0
  217. package/examples/playground/app/a2slack/ui/NameGate.tsx +35 -0
  218. package/examples/playground/app/a2slack/ui/SearchContext.tsx +146 -0
  219. package/examples/playground/app/a2slack/ui/SearchResults.tsx +97 -0
  220. package/examples/playground/app/a2slack/ui/Shell.tsx +34 -0
  221. package/examples/playground/app/a2slack/ui/Sidebar.tsx +199 -0
  222. package/examples/playground/app/a2slack/ui/ThreadPanel.tsx +134 -0
  223. package/examples/playground/app/a2slack/ui/useInbox.ts +71 -0
  224. package/examples/playground/app/agent/server.ts +3 -3
  225. package/examples/playground/app/chat/[chatId]/chat-client.tsx +1 -1
  226. package/examples/playground/app/chat/server.ts +45 -35
  227. package/examples/playground/app/components/demo-icons.tsx +7 -0
  228. package/examples/playground/app/components/demos.tsx +7 -0
  229. package/examples/playground/app/orders/[orderId]/order-client.tsx +2 -2
  230. package/examples/playground/app/orders/page.tsx +1 -1
  231. package/examples/playground/app/orders/server.ts +49 -30
  232. package/examples/playground/app/recovery/[recoveryId]/recovery-client.tsx +14 -15
  233. package/examples/playground/app/recovery/page.tsx +4 -4
  234. package/examples/playground/app/recovery/server.ts +23 -32
  235. package/examples/playground/drizzle.config.ts +14 -0
  236. package/examples/playground/lib/store.ts +1 -1
  237. package/examples/playground/next.config.ts +6 -1
  238. package/examples/playground/package.json +12 -1
  239. package/examples/playground/scripts/migrate.mjs +31 -0
  240. package/examples/playground/vercel.json +9 -0
  241. package/examples/playground/vitest.drills.config.ts +32 -0
  242. package/package.json +5 -2
  243. package/sql/postgres/drizzle-schema.ts +209 -0
  244. package/sql/postgres/schema.sql +227 -67
  245. package/src/actor-shared.ts +10 -2
  246. package/src/actor.ts +101 -71
  247. package/src/ai-agent.ts +162 -111
  248. package/src/ai-control-server.ts +8 -8
  249. package/src/ai-fx.ts +324 -82
  250. package/src/ai-harness.ts +17 -2
  251. package/src/ai-pi.ts +2 -2
  252. package/src/ai-server.ts +2 -2
  253. package/src/ai.ts +7 -7
  254. package/src/client.ts +19 -2
  255. package/src/contract.ts +1 -1
  256. package/src/deterministic-id.ts +27 -4
  257. package/src/errors.ts +8 -5
  258. package/src/idempotent-replay.ts +0 -1
  259. package/src/index.ts +1 -1
  260. package/src/internal.ts +57 -16
  261. package/src/otel.ts +1 -1
  262. package/src/postgres-schema.ts +98 -57
  263. package/src/presence-recovery.ts +18 -11
  264. package/src/presence-tombstones.ts +52 -0
  265. package/src/push-envelope.ts +20 -0
  266. package/src/runtime-consumer.ts +10 -0
  267. package/src/scheduler-task.ts +2 -2
  268. package/src/scheduler-vercel.ts +1 -1
  269. package/src/server.browser.ts +1 -1
  270. package/src/server.ts +819 -218
  271. package/src/session-socket.ts +1 -1
  272. package/src/store-codec.ts +66 -0
  273. package/src/store-memory.ts +604 -286
  274. package/src/store-postgres.ts +2006 -857
  275. package/src/store-presence-polling.ts +59 -34
  276. package/src/store-redis-core.ts +1961 -1333
  277. package/src/store-redis-notify.ts +76 -26
  278. package/src/store-redis.ts +2 -3
  279. package/src/store-sqlite.ts +915 -516
  280. package/src/store.ts +241 -102
  281. package/src/telemetry.ts +2 -1
  282. package/src/wire.ts +1 -0
  283. package/dist/actor-C7b-1arn.d.ts.map +0 -1
  284. package/dist/actor-shared-t7w8xIKP.js.map +0 -1
  285. package/dist/ai-harness-Clbg5ei7.js.map +0 -1
  286. package/dist/ai-mMiWul1T.js.map +0 -1
  287. package/dist/client-BhHLoNKF.js.map +0 -1
  288. package/dist/contract-BYKASlCV.js.map +0 -1
  289. package/dist/idempotent-replay-JdgMQRRm.js +0 -21
  290. package/dist/idempotent-replay-JdgMQRRm.js.map +0 -1
  291. package/dist/initial-snapshot-Bx-c0pDP.d.ts.map +0 -1
  292. package/dist/internal-CscSY7K_.js.map +0 -1
  293. package/dist/server-DYAATFuT.d.ts.map +0 -1
  294. package/dist/server-qyb5aJJG.js.map +0 -1
  295. package/dist/session-clear-C-QwTXV1.d.ts.map +0 -1
  296. package/dist/session-clear-DGI4jTQ5.js.map +0 -1
  297. package/dist/store-CyLUy86v.d.ts +0 -404
  298. package/dist/store-CyLUy86v.d.ts.map +0 -1
  299. package/dist/store-codec-DTG0Ftek.js +0 -10
  300. package/dist/store-codec-DTG0Ftek.js.map +0 -1
  301. package/dist/store-presence-polling-B1ixeqIS.js +0 -94
  302. package/dist/store-presence-polling-B1ixeqIS.js.map +0 -1
  303. package/dist/store-redis-notify-CM0AfLY0.js +0 -2544
  304. package/dist/store-redis-notify-CM0AfLY0.js.map +0 -1
  305. package/dist/stream-buffer-Cx7AAPGq.js +0 -23
  306. package/dist/stream-buffer-Cx7AAPGq.js.map +0 -1
  307. package/dist/telemetry-Cyfqo6Qt.d.ts.map +0 -1
  308. package/dist/wire-BKxH1vbP.js.map +0 -1
  309. package/docs/server/01-handlers.mdx +0 -227
@@ -15,14 +15,15 @@ if (!url || !token) throw new Error('set the Upstash REST URL and token')
15
15
  export const ordersServer = createServer({
16
16
  contract: orders,
17
17
  store: redisHttp({ url, token }),
18
- handlers: {
18
+ consumers: [
19
19
  /* ... */
20
- },
20
+ ],
21
21
  })
22
22
  ```
23
23
 
24
24
  Stores sessions exactly like [Redis](/stores/redis), with no client dependency
25
- and nothing to set up. Use it when your provider only offers a REST API.
25
+ and nothing to set up. Use it when your provider only offers a REST API. On
26
+ Upstash, each script locks only its session's keys, never the whole database.
26
27
 
27
28
  :::warning
28
29
  Live streams trust the messages on the store's `:notify` channels. Browsers
@@ -37,7 +38,7 @@ credentials that can also write the store.
37
38
  | Option | |
38
39
  | --- | --- |
39
40
  | `url`, `token` | your provider's REST endpoint and token |
40
- | `keyPrefix` | isolates several apps on one Redis. Default `'a2'` |
41
+ | `keyPrefix` | isolates several apps on one Redis. Default `'a2'`; no braces |
41
42
  | `clock`, `ids` | test seams for timestamps and generated event ids |
42
43
 
43
44
  ## Live updates
@@ -46,8 +47,9 @@ credentials that can also write the store.
46
47
  same URL and token. Each active channel holds one outbound HTTP subscription
47
48
  per store instance, shared by its viewers and closed when the last one
48
49
  leaves.
49
- - Writes publish their wake-ups in the same Redis script as the append. A
50
- rejected notification doesn't fail the write.
50
+ - Each write publishes its wake-up after its script commits, so a slow
51
+ subscriber never holds the script's lock. A rejected notification doesn't
52
+ fail the write.
51
53
  - Idle streams re-read every ten seconds to recover a missed notification.
52
54
  Subscriptions reconnect on their own and catch up from the event cursor.
53
55
  - After a reconnect, A2 re-reads presence and clears fields deleted during the
@@ -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 (session_id,
42
+ event_id)`, and 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,31 @@
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.22](/upgrades/0-22) | Event ids are unique per session. Postgres: apply the new `schema.sql`, then deploy. Redis: use a new `keyPrefix`. |
11
+ | [0.21](/upgrades/0-21) | Rewrite handlers as consumers. Let earlier deployments finish; on Postgres, then apply the new `schema.sql`. SQLite starts fresh. |
12
+ | [0.20](/upgrades/0-20) | Postgres: apply `schema.sql` yourself, before you deploy. |
13
+ | [0.19](/upgrades/0-19) | Start new agent sessions: earlier ones don't fold. |
14
+ | [0.18](/upgrades/0-18) | Move the AI SDK loop's options into `harness`. |
15
+ | [0.17](/upgrades/0-17) | Redis: use a fresh `keyPrefix`. Serve agents with `agent()`. |
16
+
17
+ ## Across several releases
18
+
19
+ Read every page between the release you run and the one you're moving to,
20
+ oldest first. Code changes add up: make each page's changes. Store steps
21
+ don't always: each release's `schema.sql` covers a database from any earlier
22
+ release, so a Postgres database needs only the newest one, applied the way the
23
+ newest page says, and one fresh Redis `keyPrefix` covers every release that
24
+ asks for one.
25
+
26
+ ## Everything else
27
+
28
+ Each release's changelog lists everything it changes, including what needs no
29
+ steps: [`CHANGELOG.md`](https://github.com/vercel-labs/a2/blob/main/packages/a2/CHANGELOG.md),
30
+ also in the package at `node_modules/experimental-a2/CHANGELOG.md`. Releases
31
+ before 0.17 are only there.
@@ -0,0 +1,115 @@
1
+ ---
2
+ title: '0.22'
3
+ description: Event ids are unique per session, not per store, and a presence clear keeps its stamp.
4
+ ---
5
+
6
+ ## Event ids are unique per session
7
+
8
+ The same event id in two sessions is now two events. Before, appending an id
9
+ that another session already used threw `PARTIAL_DUPLICATE_BATCH`. Inside one
10
+ session nothing changes: an identical retry returns the original rows, a batch
11
+ that mixes sent and fresh events throws, and a consumer's retried completion
12
+ returns the events it committed.
13
+
14
+ - **Ids you derive need only the session's scope.** An
15
+ [`idempotentId()`](/reference/core#idempotentidparts) recipe that includes
16
+ the session still works; one that leaves it out no longer collides with
17
+ another session's events.
18
+ - **Idempotency keys built from ids.** `event.id` and an actor's `ctx.id` are
19
+ unique within their session. Ids your callers pick, and the ids A2 derives
20
+ from them, can now repeat in another session, so a key for an external side
21
+ effect built from one alone can too. When yours can repeat, add the session
22
+ to the key; see
23
+ [Consumers can run twice](/server/consumers#consumers-can-run-twice).
24
+ - **Clear markers** follow the same rule.
25
+ - **Custom stores** check ids against the session's own rows only: the
26
+ idempotent-replay and partial-batch checks in `append` and
27
+ `completeAttempt` ignore other sessions. See
28
+ [`A2Store`](/reference/server#a2store).
29
+
30
+ ## Postgres
31
+
32
+ 0.22's `schema.sql` replaces the unique index on `a2_events (event_id)`,
33
+ `a2_events_event_id`, with one on `a2_events (session_id, event_id)`,
34
+ `a2_events_session_event_id`. Every row is kept, and building the new index
35
+ can't fail: the old one already keeps those pairs unique.
36
+
37
+ 1. **Apply 0.22's `schema.sql`**, as the role your app connects with:
38
+ `node_modules/experimental-a2/sql/postgres/schema.sql`.
39
+ 2. **Deploy 0.22.**
40
+
41
+ The first time it runs, the swap locks `a2_events` against every read and write
42
+ until the new index is built, then drops the old one. On a large table, make
43
+ both changes by hand instead, with nothing blocked. Run each statement on its
44
+ own, outside a transaction:
45
+
46
+ ```sql
47
+ create unique index concurrently if not exists a2_events_session_event_id
48
+ on a2_events (session_id, event_id);
49
+ ```
50
+
51
+ A concurrent build that fails leaves an invalid index of the same name behind.
52
+ Check that this returns `true` before you drop the old index; if it doesn't,
53
+ drop the new one and build it again:
54
+
55
+ ```sql
56
+ select indisvalid from pg_index
57
+ where indexrelid = 'a2_events_session_event_id'::regclass;
58
+ ```
59
+
60
+ ```sql
61
+ drop index concurrently if exists a2_events_event_id;
62
+ ```
63
+
64
+ Then apply `schema.sql`: it finds both done and waits on nothing. It drops the
65
+ old index only once the new one is valid.
66
+
67
+ With Drizzle, update your copy of `drizzle-schema.ts`:
68
+ `a2_events_session_event_id` on `(session_id, event_id)` replaces
69
+ `a2_events_event_id`. On a large table, run the statements above by hand
70
+ instead, and take them out of the migration Drizzle generates.
71
+
72
+ ## SQLite
73
+
74
+ Nothing to do. The store creates the new index and drops the old one when it
75
+ opens the file. Once 0.22 has opened a file, don't open it with an earlier
76
+ release: that release builds the old index again, and can't open the file at
77
+ all once two sessions share an id.
78
+
79
+ ## Redis
80
+
81
+ Redis names every key differently in this release, so it starts fresh:
82
+ **point the store at a new `keyPrefix`**, then deploy. Under its new prefix,
83
+ 0.22 never reads what an earlier release wrote; delete the old prefix's keys
84
+ whenever convenient. On a prefix 0.21 wrote, 0.22 refuses to start with
85
+ `STORE_NOT_CONFIGURED`. A `keyPrefix` can't contain braces.
86
+
87
+ What changes:
88
+
89
+ - **Scripts lock only their session on Upstash.** Every key of a session
90
+ shares one hash tag, so each script sets Upstash's `allow-key-locking` and
91
+ locks its session's keys instead of the whole database. The store asks the
92
+ server once whether it accepts the flag; Redis and Valkey don't, and run one
93
+ command at a time anyway.
94
+ - **No script publishes.** Each write publishes its wake-up after its script,
95
+ so a slow subscriber never holds a lock.
96
+ - **`clear()` works on Upstash.** It used to rename the log aside, which
97
+ Upstash refuses for streams. The marker now joins the same log, and the old
98
+ lifetime's entries stay below it, unread, until `clear()` finishes deleting
99
+ them. A session that stays empty after its clear keeps only its clear marker,
100
+ the log holding it, and its log consumers' positions.
101
+
102
+ ## Memory
103
+
104
+ Nothing to do: the memory store starts empty in every process.
105
+
106
+ ## Presence clears keep their stamp
107
+
108
+ A `null` from `setPresence()` now leaves a tombstone in the store, carrying the
109
+ clear's `at`, until the field's TTL runs out. An older value that reaches the
110
+ store after the clear, such as one sent through another server instance, still
111
+ loses. Before, it brought the field back. Nothing to do: Postgres and SQLite
112
+ keep tombstones in `a2_presence` as a JSON `null` value.
113
+
114
+ Custom stores with presence keep a tombstone for each `null`, compare it by
115
+ `at` like a value until it expires, and leave tombstones out of `read`.
@@ -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 }