experimental-a2 0.19.0 → 0.21.1

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (320) hide show
  1. package/CHANGELOG.md +50 -0
  2. package/README.md +1 -1
  3. package/dist/{actor-C7b-1arn.d.ts → actor-HnGmrOZZ.d.ts} +5 -5
  4. package/dist/{actor-C7b-1arn.d.ts.map → actor-HnGmrOZZ.d.ts.map} +1 -1
  5. package/dist/actor-client.d.ts +1 -1
  6. package/dist/actor-client.js +2 -2
  7. package/dist/actor-react.d.ts +3 -3
  8. package/dist/actor-react.js +5 -5
  9. package/dist/{actor-shared-t7w8xIKP.js → actor-shared-DFET5q16.js} +3 -3
  10. package/dist/{actor-shared-t7w8xIKP.js.map → actor-shared-DFET5q16.js.map} +1 -1
  11. package/dist/actor.d.ts +1 -1
  12. package/dist/actor.js +54 -43
  13. package/dist/actor.js.map +1 -1
  14. package/dist/{ai-Cz3hqVhk.d.ts → ai-CvEnqVfo.d.ts} +8 -8
  15. package/dist/{ai-Cz3hqVhk.d.ts.map → ai-CvEnqVfo.d.ts.map} +1 -1
  16. package/dist/{ai-mMiWul1T.js → ai-CzE672Q3.js} +9 -9
  17. package/dist/ai-CzE672Q3.js.map +1 -0
  18. package/dist/ai-client.d.ts +2 -2
  19. package/dist/ai-client.js +2 -2
  20. package/dist/ai-fx.d.ts +1 -1
  21. package/dist/ai-fx.d.ts.map +1 -1
  22. package/dist/ai-fx.js +36 -9
  23. package/dist/ai-fx.js.map +1 -1
  24. package/dist/{ai-harness-CV6rXbB3.d.ts → ai-harness-Bfk-Lycn.d.ts} +2 -2
  25. package/dist/ai-harness-Bfk-Lycn.d.ts.map +1 -0
  26. package/dist/{ai-harness-BuuC-gJ3.js → ai-harness-dQeJaR1t.js} +17 -4
  27. package/dist/ai-harness-dQeJaR1t.js.map +1 -0
  28. package/dist/ai-pi.d.ts +1 -1
  29. package/dist/ai-pi.js +4 -4
  30. package/dist/ai-pi.js.map +1 -1
  31. package/dist/ai-react.d.ts +5 -5
  32. package/dist/ai-react.js +2 -2
  33. package/dist/ai-server.d.ts +14 -17
  34. package/dist/ai-server.d.ts.map +1 -1
  35. package/dist/ai-server.js +84 -75
  36. package/dist/ai-server.js.map +1 -1
  37. package/dist/ai.d.ts +2 -2
  38. package/dist/ai.js +2 -2
  39. package/dist/{client-BhHLoNKF.js → client-9IeTsiAs.js} +5 -5
  40. package/dist/{client-BhHLoNKF.js.map → client-9IeTsiAs.js.map} +1 -1
  41. package/dist/{client-h_DKFAjj.d.ts → client-CUMbS_38.d.ts} +3 -3
  42. package/dist/{client-h_DKFAjj.d.ts.map → client-CUMbS_38.d.ts.map} +1 -1
  43. package/dist/client.d.ts +3 -3
  44. package/dist/client.js +1 -1
  45. package/dist/{contract-BYKASlCV.js → contract-CWukt01d.js} +2 -2
  46. package/dist/contract-CWukt01d.js.map +1 -0
  47. package/dist/index.d.ts +5 -5
  48. package/dist/index.js +3 -3
  49. package/dist/{initial-snapshot-Bx-c0pDP.d.ts → initial-snapshot-D3Lve43o.d.ts} +4 -4
  50. package/dist/initial-snapshot-D3Lve43o.d.ts.map +1 -0
  51. package/dist/{internal-CscSY7K_.js → internal-Bc4PvfHk.js} +10 -3
  52. package/dist/internal-Bc4PvfHk.js.map +1 -0
  53. package/dist/otel.d.ts +1 -1
  54. package/dist/otel.js +1 -1
  55. package/dist/otel.js.map +1 -1
  56. package/dist/{react-BBXgHWFc.d.ts → react-BoHvC3v9.d.ts} +4 -4
  57. package/dist/{react-BBXgHWFc.d.ts.map → react-BoHvC3v9.d.ts.map} +1 -1
  58. package/dist/react.d.ts +1 -1
  59. package/dist/react.js +1 -1
  60. package/dist/{reducer-CqA59Q6U.d.ts → reducer-CYPHsSmd.d.ts} +2 -2
  61. package/dist/{reducer-CqA59Q6U.d.ts.map → reducer-CYPHsSmd.d.ts.map} +1 -1
  62. package/dist/{reducer-BAXc8JyF.js → reducer-CjmvZSAb.js} +2 -2
  63. package/dist/{reducer-BAXc8JyF.js.map → reducer-CjmvZSAb.js.map} +1 -1
  64. package/dist/runtime-consumer-BWmht60z.js +12 -0
  65. package/dist/runtime-consumer-BWmht60z.js.map +1 -0
  66. package/dist/scheduler-qstash.d.ts +2 -2
  67. package/dist/scheduler-qstash.d.ts.map +1 -1
  68. package/dist/scheduler-qstash.js +7 -7
  69. package/dist/scheduler-qstash.js.map +1 -1
  70. package/dist/scheduler-task-X1SToDjP.js.map +1 -1
  71. package/dist/scheduler-vercel.d.ts +2 -2
  72. package/dist/scheduler-vercel.d.ts.map +1 -1
  73. package/dist/scheduler-vercel.js +5 -4
  74. package/dist/scheduler-vercel.js.map +1 -1
  75. package/dist/{server-DYAATFuT.d.ts → server-BshFZ0eL.d.ts} +108 -41
  76. package/dist/server-BshFZ0eL.d.ts.map +1 -0
  77. package/dist/{server-D-Y4pqwu.js → server-DvIB8kj5.js} +423 -135
  78. package/dist/server-DvIB8kj5.js.map +1 -0
  79. package/dist/server.browser.js +1 -1
  80. package/dist/server.browser.js.map +1 -1
  81. package/dist/server.d.ts +4 -4
  82. package/dist/server.js +2 -2
  83. package/dist/{session-clear-C-QwTXV1.d.ts → session-clear-Bk_29eEw.d.ts} +2 -2
  84. package/dist/session-clear-Bk_29eEw.d.ts.map +1 -0
  85. package/dist/{session-clear-DGI4jTQ5.js → session-clear-wHd5GCpZ.js} +5 -4
  86. package/dist/session-clear-wHd5GCpZ.js.map +1 -0
  87. package/dist/store-N8PXxDAS.js.map +1 -1
  88. package/dist/store-Z2qjw50n.d.ts +523 -0
  89. package/dist/store-Z2qjw50n.d.ts.map +1 -0
  90. package/dist/store-codec-DwrWlgQx.js +65 -0
  91. package/dist/store-codec-DwrWlgQx.js.map +1 -0
  92. package/dist/store-memory.d.ts +1 -1
  93. package/dist/store-memory.d.ts.map +1 -1
  94. package/dist/store-memory.js +393 -210
  95. package/dist/store-memory.js.map +1 -1
  96. package/dist/{store-polling-7WJV0YWP.js → store-polling-DYNyxCev.js} +2 -2
  97. package/dist/{store-polling-7WJV0YWP.js.map → store-polling-DYNyxCev.js.map} +1 -1
  98. package/dist/store-postgres.d.ts +1 -1
  99. package/dist/store-postgres.d.ts.map +1 -1
  100. package/dist/store-postgres.js +1595 -673
  101. package/dist/store-postgres.js.map +1 -1
  102. package/dist/{store-presence-polling-B1ixeqIS.js → store-presence-polling-CzQ0wdtM.js} +3 -3
  103. package/dist/{store-presence-polling-B1ixeqIS.js.map → store-presence-polling-CzQ0wdtM.js.map} +1 -1
  104. package/dist/store-redis-http.d.ts +1 -1
  105. package/dist/store-redis-http.js +4 -4
  106. package/dist/store-redis-notify-IiVdlYHc.js +2919 -0
  107. package/dist/store-redis-notify-IiVdlYHc.js.map +1 -0
  108. package/dist/store-redis.d.ts +1 -1
  109. package/dist/store-redis.js +2 -2
  110. package/dist/store-sqlite.d.ts +1 -1
  111. package/dist/store-sqlite.d.ts.map +1 -1
  112. package/dist/store-sqlite.js +517 -287
  113. package/dist/store-sqlite.js.map +1 -1
  114. package/dist/{telemetry-Cyfqo6Qt.d.ts → telemetry-CWwsh-Gx.d.ts} +3 -3
  115. package/dist/telemetry-CWwsh-Gx.d.ts.map +1 -0
  116. package/dist/{wire-BKxH1vbP.js → wire-CZGqNZRx.js} +5 -4
  117. package/dist/wire-CZGqNZRx.js.map +1 -0
  118. package/docs/01-quickstart.mdx +28 -19
  119. package/docs/02-examples.mdx +1 -1
  120. package/docs/actors/01-introduction.mdx +9 -1
  121. package/docs/actors/02-concurrency.mdx +1 -1
  122. package/docs/actors/meta.ts +1 -1
  123. package/docs/agents/01-introduction.mdx +1 -1
  124. package/docs/agents/02-tools.mdx +13 -12
  125. package/docs/agents/03-conversations.mdx +31 -21
  126. package/docs/agents/04-harnesses.mdx +7 -3
  127. package/docs/agents/meta.ts +1 -1
  128. package/docs/client/03-presence.mdx +18 -14
  129. package/docs/client/meta.ts +1 -1
  130. package/docs/contract/01-events.mdx +2 -2
  131. package/docs/contract/02-state.mdx +18 -14
  132. package/docs/contract/03-sessions.mdx +32 -25
  133. package/docs/index.mdx +31 -26
  134. package/docs/reference/01-core.mdx +12 -12
  135. package/docs/reference/02-server.mdx +247 -192
  136. package/docs/reference/04-ai.mdx +29 -20
  137. package/docs/reference/05-telemetry.mdx +146 -0
  138. package/docs/reference/06-errors.mdx +24 -18
  139. package/docs/reference/meta.ts +1 -1
  140. package/docs/schedulers/01-overview.mdx +112 -0
  141. package/docs/schedulers/02-vercel-queues.mdx +87 -0
  142. package/docs/schedulers/03-qstash.mdx +123 -0
  143. package/docs/schedulers/meta.ts +1 -0
  144. package/docs/server/01-consumers.mdx +415 -0
  145. package/docs/server/02-timers.mdx +33 -23
  146. package/docs/server/03-cancellation.mdx +52 -44
  147. package/docs/server/04-routes.mdx +4 -1
  148. package/docs/server/05-sql-projections.mdx +391 -0
  149. package/docs/server/06-guarantees.mdx +60 -50
  150. package/docs/stores/01-overview.mdx +37 -0
  151. package/docs/stores/02-redis.mdx +57 -0
  152. package/docs/stores/03-redis-http.mdx +56 -0
  153. package/docs/stores/04-postgres.mdx +172 -0
  154. package/docs/stores/05-sqlite.mdx +36 -0
  155. package/docs/stores/06-memory.mdx +22 -0
  156. package/docs/stores/meta.ts +1 -0
  157. package/docs/upgrades/01-overview.mdx +30 -0
  158. package/docs/upgrades/79-0-21.mdx +165 -0
  159. package/docs/upgrades/80-0-20.mdx +43 -0
  160. package/docs/upgrades/81-0-19.mdx +33 -0
  161. package/docs/upgrades/82-0-18.mdx +33 -0
  162. package/docs/upgrades/83-0-17.mdx +69 -0
  163. package/docs/upgrades/meta.ts +3 -0
  164. package/examples/playground/DEPLOY.md +48 -11
  165. package/examples/playground/app/a0/apps/[appId]/route.ts +6 -10
  166. package/examples/playground/app/a0/chat.test.tsx +421 -330
  167. package/examples/playground/app/a0/sandbox-log.ts +17 -15
  168. package/examples/playground/app/a0/server.ts +4 -4
  169. package/examples/playground/app/a0/shell.tsx +1 -1
  170. package/examples/playground/app/a0/uploads/route.test.ts +59 -0
  171. package/examples/playground/app/a2slack/a2slack.css +1067 -0
  172. package/examples/playground/app/a2slack/c/[channel]/page.tsx +20 -0
  173. package/examples/playground/app/a2slack/consumers/agent.ts +63 -0
  174. package/examples/playground/app/a2slack/consumers/feed.ts +164 -0
  175. package/examples/playground/app/a2slack/consumers/notify.ts +116 -0
  176. package/examples/playground/app/a2slack/consumers/search.ts +37 -0
  177. package/examples/playground/app/a2slack/consumers/triage.ts +31 -0
  178. package/examples/playground/app/a2slack/consumers/welcome.ts +25 -0
  179. package/examples/playground/app/a2slack/db/a2.ts +206 -0
  180. package/examples/playground/app/a2slack/db/index.ts +73 -0
  181. package/examples/playground/app/a2slack/db/migrations/0000_extensions.sql +4 -0
  182. package/examples/playground/app/a2slack/db/migrations/0001_workspace.sql +74 -0
  183. package/examples/playground/app/a2slack/db/migrations/0002_a2.sql +215 -0
  184. package/examples/playground/app/a2slack/db/migrations/0003_a2_batch.sql +67 -0
  185. package/examples/playground/app/a2slack/db/migrations/meta/0000_snapshot.json +18 -0
  186. package/examples/playground/app/a2slack/db/migrations/meta/0001_snapshot.json +500 -0
  187. package/examples/playground/app/a2slack/db/migrations/meta/0002_snapshot.json +1159 -0
  188. package/examples/playground/app/a2slack/db/migrations/meta/0003_snapshot.json +1159 -0
  189. package/examples/playground/app/a2slack/db/migrations/meta/_journal.json +34 -0
  190. package/examples/playground/app/a2slack/db/queries.ts +263 -0
  191. package/examples/playground/app/a2slack/db/schema.ts +145 -0
  192. package/examples/playground/app/a2slack/drills/a2slack.drill.ts +708 -0
  193. package/examples/playground/app/a2slack/drills/harness.ts +244 -0
  194. package/examples/playground/app/a2slack/events/route.ts +36 -0
  195. package/examples/playground/app/a2slack/identity.ts +44 -0
  196. package/examples/playground/app/a2slack/inbox/route.ts +8 -0
  197. package/examples/playground/app/a2slack/lag/data/route.ts +8 -0
  198. package/examples/playground/app/a2slack/lag/page.tsx +101 -0
  199. package/examples/playground/app/a2slack/lag.ts +73 -0
  200. package/examples/playground/app/a2slack/layout.tsx +43 -0
  201. package/examples/playground/app/a2slack/messages/route.ts +17 -0
  202. package/examples/playground/app/a2slack/model.test.ts +208 -0
  203. package/examples/playground/app/a2slack/model.ts +369 -0
  204. package/examples/playground/app/a2slack/models.test.ts +72 -0
  205. package/examples/playground/app/a2slack/page.tsx +10 -0
  206. package/examples/playground/app/a2slack/scheduler/route.ts +5 -0
  207. package/examples/playground/app/a2slack/search/route.ts +40 -0
  208. package/examples/playground/app/a2slack/search-index.ts +108 -0
  209. package/examples/playground/app/a2slack/server.test.ts +315 -0
  210. package/examples/playground/app/a2slack/server.ts +55 -0
  211. package/examples/playground/app/a2slack/sessions.ts +50 -0
  212. package/examples/playground/app/a2slack/thread/route.ts +8 -0
  213. package/examples/playground/app/a2slack/ui/ChannelView.tsx +87 -0
  214. package/examples/playground/app/a2slack/ui/Composer.tsx +117 -0
  215. package/examples/playground/app/a2slack/ui/DotsShadow.tsx +42 -0
  216. package/examples/playground/app/a2slack/ui/Feed.tsx +157 -0
  217. package/examples/playground/app/a2slack/ui/InboxContext.tsx +63 -0
  218. package/examples/playground/app/a2slack/ui/InboxView.tsx +82 -0
  219. package/examples/playground/app/a2slack/ui/Menu.tsx +128 -0
  220. package/examples/playground/app/a2slack/ui/MessageRow.tsx +314 -0
  221. package/examples/playground/app/a2slack/ui/NameGate.tsx +35 -0
  222. package/examples/playground/app/a2slack/ui/SearchContext.tsx +146 -0
  223. package/examples/playground/app/a2slack/ui/SearchResults.tsx +97 -0
  224. package/examples/playground/app/a2slack/ui/Shell.tsx +34 -0
  225. package/examples/playground/app/a2slack/ui/Sidebar.tsx +199 -0
  226. package/examples/playground/app/a2slack/ui/ThreadPanel.tsx +134 -0
  227. package/examples/playground/app/a2slack/ui/useInbox.ts +71 -0
  228. package/examples/playground/app/agent/[agentId]/agent-queue.test.tsx +116 -90
  229. package/examples/playground/app/agent/[agentId]/compaction-panel.test.tsx +107 -76
  230. package/examples/playground/app/agent/ingress.test.ts +41 -17
  231. package/examples/playground/app/agent/model-input.test.tsx +92 -43
  232. package/examples/playground/app/agent/server.ts +3 -3
  233. package/examples/playground/app/chat/[chatId]/agent-stream-drawer.test.tsx +2 -2
  234. package/examples/playground/app/chat/[chatId]/chat-client.tsx +1 -1
  235. package/examples/playground/app/chat/server.test.ts +38 -65
  236. package/examples/playground/app/chat/server.ts +45 -35
  237. package/examples/playground/app/components/activity-history.test.tsx +60 -18
  238. package/examples/playground/app/components/demo-icons.tsx +7 -0
  239. package/examples/playground/app/components/demos.tsx +7 -0
  240. package/examples/playground/app/components/fast-link.test.tsx +3 -5
  241. package/examples/playground/app/initial-state.test.tsx +86 -45
  242. package/examples/playground/app/orders/[orderId]/order-client.tsx +2 -2
  243. package/examples/playground/app/orders/page.tsx +1 -1
  244. package/examples/playground/app/orders/server.ts +49 -30
  245. package/examples/playground/app/recovery/[recoveryId]/recovery-client.tsx +14 -15
  246. package/examples/playground/app/recovery/page.tsx +4 -4
  247. package/examples/playground/app/recovery/server.ts +23 -32
  248. package/examples/playground/drizzle.config.ts +14 -0
  249. package/examples/playground/next.config.ts +6 -1
  250. package/examples/playground/package.json +13 -1
  251. package/examples/playground/scripts/migrate.mjs +31 -0
  252. package/examples/playground/test/agent.ts +35 -0
  253. package/examples/playground/test/model.ts +149 -0
  254. package/examples/playground/test/serve.ts +184 -0
  255. package/examples/playground/test/server-only.ts +1 -0
  256. package/examples/playground/vercel.json +9 -0
  257. package/examples/playground/vitest.config.ts +27 -0
  258. package/examples/playground/vitest.drills.config.ts +32 -0
  259. package/package.json +7 -2
  260. package/sql/postgres/drizzle-schema.ts +206 -0
  261. package/sql/postgres/notifications.sql +24 -0
  262. package/sql/postgres/schema.sql +276 -0
  263. package/src/actor.ts +101 -71
  264. package/src/ai-agent.ts +154 -115
  265. package/src/ai-control-server.ts +8 -8
  266. package/src/ai-fx.ts +41 -9
  267. package/src/ai-harness.ts +28 -2
  268. package/src/ai-pi.ts +2 -2
  269. package/src/ai-server.ts +2 -2
  270. package/src/ai.ts +7 -7
  271. package/src/contract.ts +1 -1
  272. package/src/deterministic-id.ts +26 -3
  273. package/src/errors.ts +9 -6
  274. package/src/idempotent-replay.ts +0 -1
  275. package/src/index.ts +1 -1
  276. package/src/internal.ts +17 -6
  277. package/src/otel.ts +1 -1
  278. package/src/postgres-notifications.ts +0 -22
  279. package/src/postgres-resources.ts +6 -9
  280. package/src/postgres-schema.ts +121 -0
  281. package/src/push-envelope.ts +20 -0
  282. package/src/runtime-consumer.ts +10 -0
  283. package/src/scheduler-qstash.ts +12 -5
  284. package/src/scheduler-task.ts +2 -2
  285. package/src/scheduler-vercel.ts +10 -4
  286. package/src/server.browser.ts +1 -1
  287. package/src/server.ts +839 -238
  288. package/src/session-socket.ts +1 -1
  289. package/src/store-codec.ts +66 -0
  290. package/src/store-memory.ts +581 -255
  291. package/src/store-postgres.ts +1984 -943
  292. package/src/store-redis-core.ts +1624 -1152
  293. package/src/store-redis-notify.ts +2 -0
  294. package/src/store-sqlite.ts +891 -486
  295. package/src/store.ts +227 -97
  296. package/src/telemetry.ts +2 -1
  297. package/src/wire.ts +1 -0
  298. package/dist/ai-harness-BuuC-gJ3.js.map +0 -1
  299. package/dist/ai-harness-CV6rXbB3.d.ts.map +0 -1
  300. package/dist/ai-mMiWul1T.js.map +0 -1
  301. package/dist/contract-BYKASlCV.js.map +0 -1
  302. package/dist/idempotent-replay-JdgMQRRm.js +0 -21
  303. package/dist/idempotent-replay-JdgMQRRm.js.map +0 -1
  304. package/dist/initial-snapshot-Bx-c0pDP.d.ts.map +0 -1
  305. package/dist/internal-CscSY7K_.js.map +0 -1
  306. package/dist/server-D-Y4pqwu.js.map +0 -1
  307. package/dist/server-DYAATFuT.d.ts.map +0 -1
  308. package/dist/session-clear-C-QwTXV1.d.ts.map +0 -1
  309. package/dist/session-clear-DGI4jTQ5.js.map +0 -1
  310. package/dist/store-CyLUy86v.d.ts +0 -404
  311. package/dist/store-CyLUy86v.d.ts.map +0 -1
  312. package/dist/store-codec-DTG0Ftek.js +0 -10
  313. package/dist/store-codec-DTG0Ftek.js.map +0 -1
  314. package/dist/store-redis-notify-CM0AfLY0.js +0 -2544
  315. package/dist/store-redis-notify-CM0AfLY0.js.map +0 -1
  316. package/dist/telemetry-Cyfqo6Qt.d.ts.map +0 -1
  317. package/dist/wire-BKxH1vbP.js.map +0 -1
  318. package/docs/reference/05-adapters.mdx +0 -351
  319. package/docs/server/01-handlers.mdx +0 -200
  320. package/docs/server/05-production.mdx +0 -323
@@ -1,6 +1,6 @@
1
1
  import 'server-only'
2
2
  import { vercelQueues } from 'experimental-a2/scheduler-vercel'
3
- import { createServer } from 'experimental-a2/server'
3
+ import { consumer, createServer } from 'experimental-a2/server'
4
4
  import { store } from '@/lib/store'
5
5
  import { recovery } from './model'
6
6
 
@@ -15,39 +15,30 @@ export const recoveryScheduler =
15
15
  ? vercelQueues({ topic: 'a2-recovery' })
16
16
  : undefined
17
17
 
18
+ /** Each step waits its 8 seconds, then returns the next one, in one lane. */
19
+ const runPipeline = consumer({
20
+ contract: recovery,
21
+ name: 'run-pipeline',
22
+ types: ['pipeline.started', 'step.completed'],
23
+ lane: 'pipeline',
24
+ }).run(async ({ event }) => {
25
+ const step = event.type === 'step.completed' ? event.payload.step : 0
26
+ if (step === stepCount) {
27
+ return {
28
+ type: 'pipeline.completed',
29
+ payload: { stepCount },
30
+ }
31
+ }
32
+ await wait(stepDurationSeconds * 1_000)
33
+ return {
34
+ type: 'step.completed',
35
+ payload: { step: step + 1, durationSeconds: stepDurationSeconds },
36
+ }
37
+ })
38
+
18
39
  export const recoveryServer = createServer({
19
40
  ...(store ? { store } : {}),
20
41
  ...(recoveryScheduler ? { scheduler: recoveryScheduler } : {}),
21
42
  contract: recovery,
22
- handlers: {
23
- 'pipeline.started': {
24
- lane: 'pipeline',
25
- handler: async () => {
26
- await wait(stepDurationSeconds * 1_000)
27
- return {
28
- type: 'step.completed',
29
- payload: { step: 1, durationSeconds: stepDurationSeconds },
30
- }
31
- },
32
- },
33
- 'step.completed': {
34
- lane: 'pipeline',
35
- handler: async ({ event }) => {
36
- if (event.payload.step === stepCount) {
37
- return {
38
- type: 'pipeline.completed',
39
- payload: { stepCount },
40
- }
41
- }
42
- await wait(stepDurationSeconds * 1_000)
43
- return {
44
- type: 'step.completed',
45
- payload: {
46
- step: event.payload.step + 1,
47
- durationSeconds: stepDurationSeconds,
48
- },
49
- }
50
- },
51
- },
52
- },
43
+ consumers: [runPipeline],
53
44
  })
@@ -0,0 +1,14 @@
1
+ // The Postgres at DATABASE_URL: a2slack's tables, and A2's (db/a2.ts, a copy
2
+ // of experimental-a2's drizzle-schema.ts). tablesFilter keeps push and
3
+ // studio to these, since other apps share the database.
4
+ import { defineConfig } from 'drizzle-kit'
5
+
6
+ export default defineConfig({
7
+ dialect: 'postgresql',
8
+ schema: './app/a2slack/db/schema.ts',
9
+ out: './app/a2slack/db/migrations',
10
+ casing: 'snake_case',
11
+ dbCredentials: { url: process.env.DATABASE_URL ?? '' },
12
+ tablesFilter: ['a2slack_*', 'a2_*'],
13
+ migrations: { table: 'a2slack_migrations', schema: 'public' },
14
+ })
@@ -12,7 +12,12 @@ const nextConfig: NextConfig = {
12
12
  // Turbopack panic on its WebAssembly), so it finds photon through this
13
13
  // app: package.json depends on photon at the version Pi pins, and the two
14
14
  // move together.
15
- serverExternalPackages: ['libfx', '@silvia-odwyer/photon-node'],
15
+ serverExternalPackages: [
16
+ 'libfx',
17
+ '@silvia-odwyer/photon-node',
18
+ // a2slack's development database, PGlite, loads its WebAssembly by path.
19
+ '@electric-sql/pglite',
20
+ ],
16
21
  turbopack: {
17
22
  // WebAssembly Pi's code reaches that the bundler cannot trace.
18
23
  resolveAlias: {
@@ -11,18 +11,26 @@
11
11
  "scripts": {
12
12
  "dev": "next dev",
13
13
  "build": "next build",
14
+ "vercel-build": "node scripts/migrate.mjs && turbo run build",
14
15
  "test": "vitest run",
16
+ "drills": "vitest run --config vitest.drills.config.ts",
15
17
  "typecheck": "tsc --noEmit",
18
+ "db:generate": "drizzle-kit generate",
19
+ "db:migrate": "node scripts/migrate.mjs",
16
20
  "a0:template": "node --env-file-if-exists=.env.local scripts/a0-template.ts"
17
21
  },
18
22
  "dependencies": {
19
23
  "@ai-sdk/otel": "1.0.58",
24
+ "@base-ui/react": "^1.7.0",
20
25
  "@codemirror/state": "^6.7.1",
21
26
  "@codemirror/view": "^6.43.8",
27
+ "@electric-sql/pglite": "^0.3.14",
22
28
  "@earendil-works/pi-ai": "1.0.3",
23
29
  "@earendil-works/pi-coding-agent": "1.0.2",
24
30
  "@opentelemetry/api": "^1.9.0",
31
+ "@paper-design/shaders-react": "^0.0.80",
25
32
  "@silvia-odwyer/photon-node": "0.3.4",
33
+ "@upstash/redis": "^1.39.0",
26
34
  "@vercel/blob": "^2.8.0",
27
35
  "@vercel/functions": "^3.9.3",
28
36
  "@vercel/otel": "^2.1.0",
@@ -30,7 +38,8 @@
30
38
  "@vercel/sandbox": "^3.0.0",
31
39
  "ai": "^7.0.58",
32
40
  "codemirror": "^6.0.2",
33
- "experimental-a2": "0.19.0",
41
+ "drizzle-orm": "^0.45.3",
42
+ "experimental-a2": "0.21.1",
34
43
  "geist": "^1.7.2",
35
44
  "ioredis": "^5.9.0",
36
45
  "libfx": "0.0.12",
@@ -52,10 +61,13 @@
52
61
  "@testing-library/react": "^16.1.0",
53
62
  "jsdom": "^26.0.0",
54
63
  "@types/node": "^24.0.0",
64
+ "@types/pg": "^8.15.0",
55
65
  "@types/react": "^19.0.0",
56
66
  "@types/react-dom": "^19.0.0",
57
67
  "@types/ws": "^8.18.1",
68
+ "drizzle-kit": "^0.31.11",
58
69
  "typescript": "^7.0.2",
70
+ "undici": "^8.10.2",
59
71
  "vitest": "^4.1.10"
60
72
  }
61
73
  }
@@ -0,0 +1,31 @@
1
+ /**
2
+ * Applies app/a2slack/db/migrations to the Postgres at DATABASE_URL_UNPOOLED
3
+ * (the Neon integration's direct connection) or DATABASE_URL. drizzle-kit
4
+ * migrate does the same, but hides the database's error when one fails.
5
+ *
6
+ * DATABASE_URL=postgres://… pnpm --filter playground db:migrate
7
+ */
8
+ import { drizzle } from 'drizzle-orm/node-postgres'
9
+ import { migrate } from 'drizzle-orm/node-postgres/migrator'
10
+
11
+ const url = process.env.DATABASE_URL_UNPOOLED || process.env.DATABASE_URL
12
+ if (!url) {
13
+ console.error('error: set DATABASE_URL to the Postgres to migrate')
14
+ process.exit(1)
15
+ }
16
+
17
+ const db = drizzle({ connection: url })
18
+ try {
19
+ await migrate(db, {
20
+ migrationsFolder: new URL('../app/a2slack/db/migrations', import.meta.url)
21
+ .pathname,
22
+ migrationsTable: 'a2slack_migrations',
23
+ migrationsSchema: 'public',
24
+ })
25
+ console.log('migrations applied')
26
+ } catch (error) {
27
+ console.error('error: migrations failed, and none were applied:', error)
28
+ process.exitCode = 1
29
+ } finally {
30
+ await db.$client.end()
31
+ }
@@ -0,0 +1,35 @@
1
+ import { agent } from 'experimental-a2/ai/server'
2
+ import { memory } from 'experimental-a2/store-memory'
3
+ import { demoAgent } from '@/app/agent/model'
4
+ import { authorizer, serve } from './serve'
5
+
6
+ /**
7
+ * The playground's agent on a memory store, served where the page's own
8
+ * client finds it: `/agent/events` on the origin relative URLs resolve
9
+ * against. Its tests never start a turn, so its model never runs.
10
+ */
11
+ export async function serveAgent() {
12
+ const server = agent({
13
+ contract: demoAgent,
14
+ store: memory(),
15
+ model: 'test-model',
16
+ })
17
+ const probe = authorizer()
18
+ const served = await serve({
19
+ routes: {
20
+ '/agent/events': (request) =>
21
+ server.fetch(request, { authorize: probe.authorize }),
22
+ },
23
+ })
24
+ return {
25
+ server,
26
+ probe,
27
+ url: `${served.origin}/agent/events`,
28
+ close: async (): Promise<void> => {
29
+ probe.release()
30
+ await served.close()
31
+ },
32
+ }
33
+ }
34
+
35
+ export type ServedAgent = Awaited<ReturnType<typeof serveAgent>>
@@ -0,0 +1,149 @@
1
+ /**
2
+ * The model behind an agent under test. Agents take AI Gateway model IDs;
3
+ * the AI SDK's global provider serves every one of them from this model,
4
+ * whose steps the test writes part by part.
5
+ */
6
+
7
+ import { MockLanguageModelV4 } from 'ai/test'
8
+ import { vi } from 'vitest'
9
+
10
+ export type StreamPart =
11
+ Awaited<
12
+ ReturnType<MockLanguageModelV4['doStream']>
13
+ >['stream'] extends ReadableStream<infer Part>
14
+ ? Part
15
+ : never
16
+
17
+ export type CallOptions = MockLanguageModelV4['doStreamCalls'][number]
18
+
19
+ /** One model step, open until the test ends it or the agent aborts it. */
20
+ export type ModelStep = {
21
+ /** What the agent sent the model for this step. */
22
+ options: CallOptions
23
+ write(...parts: StreamPart[]): void
24
+ /** Ends the step the way a model does: with a finish part. */
25
+ finish(reason?: 'stop' | 'tool-calls'): void
26
+ }
27
+
28
+ const usage = {
29
+ inputTokens: { total: 1, noCache: 1, cacheRead: 0, cacheWrite: 0 },
30
+ outputTokens: { total: 1, text: 1, reasoning: 0 },
31
+ }
32
+
33
+ /** A text part, whole. */
34
+ export const say = ({ id, text }: { id: string; text: string }) =>
35
+ [
36
+ { type: 'text-start', id },
37
+ { type: 'text-delta', id, delta: text },
38
+ { type: 'text-end', id },
39
+ ] satisfies StreamPart[]
40
+
41
+ /** A tool call whose input streams in as the given chunks of its JSON. */
42
+ export const toolCall = ({
43
+ id,
44
+ name,
45
+ chunks,
46
+ }: {
47
+ id: string
48
+ name: string
49
+ chunks: string[]
50
+ }) =>
51
+ [
52
+ { type: 'tool-input-start', id, toolName: name },
53
+ ...chunks.map((delta) => ({
54
+ type: 'tool-input-delta' as const,
55
+ id,
56
+ delta,
57
+ })),
58
+ { type: 'tool-input-end', id },
59
+ {
60
+ type: 'tool-call',
61
+ toolCallId: id,
62
+ toolName: name,
63
+ input: chunks.join(''),
64
+ },
65
+ ] satisfies StreamPart[]
66
+
67
+ let models = 0
68
+
69
+ /**
70
+ * Installs a model the test drives step by step, served for `id` alone, so
71
+ * an agent left over from an earlier test can't take its steps. Remove it
72
+ * with `vi.unstubAllGlobals()`.
73
+ */
74
+ export function testModel({
75
+ id = `test-model-${++models}`,
76
+ }: {
77
+ /** The model ID the agent asks for. */
78
+ id?: string
79
+ } = {}): {
80
+ /** The agent's `model`. */
81
+ id: string
82
+ /** Every step the agent asked for, in order. */
83
+ steps: ModelStep[]
84
+ /** The `n`th step (from 0), once the agent asks for it. */
85
+ step(n: number): Promise<ModelStep>
86
+ /** Ends every step still open, and each one asked for from now on. */
87
+ close(): void
88
+ } {
89
+ const steps: ModelStep[] = []
90
+ const arrivals: PromiseWithResolvers<ModelStep>[] = []
91
+ const arrival = (n: number): PromiseWithResolvers<ModelStep> =>
92
+ (arrivals[n] ??= Promise.withResolvers<ModelStep>())
93
+ const ends: (() => void)[] = []
94
+ let closed = false
95
+ const model = new MockLanguageModelV4({
96
+ modelId: id,
97
+ doStream: async (options) => {
98
+ let controller!: ReadableStreamDefaultController<StreamPart>
99
+ let open = true
100
+ const end = (): void => {
101
+ if (!open) return
102
+ open = false
103
+ controller.close()
104
+ }
105
+ const stream = new ReadableStream<StreamPart>({
106
+ start(value) {
107
+ controller = value
108
+ controller.enqueue({ type: 'stream-start', warnings: [] })
109
+ },
110
+ })
111
+ options.abortSignal?.addEventListener('abort', end, { once: true })
112
+ const step: ModelStep = {
113
+ options,
114
+ write(...parts) {
115
+ for (const part of parts) if (open) controller.enqueue(part)
116
+ },
117
+ finish(reason = 'stop') {
118
+ step.write({
119
+ type: 'finish',
120
+ finishReason: { unified: reason, raw: reason },
121
+ usage,
122
+ })
123
+ end()
124
+ },
125
+ }
126
+ ends.push(end)
127
+ arrival(steps.length).resolve(step)
128
+ steps.push(step)
129
+ if (closed) step.finish()
130
+ return { stream }
131
+ },
132
+ })
133
+ vi.stubGlobal('AI_SDK_DEFAULT_PROVIDER', {
134
+ specificationVersion: 'v4',
135
+ languageModel: (requested: string) => {
136
+ if (requested !== id) throw new Error(`no test model '${requested}'`)
137
+ return model
138
+ },
139
+ })
140
+ return {
141
+ id,
142
+ steps,
143
+ step: (n) => arrival(n).promise,
144
+ close: () => {
145
+ closed = true
146
+ for (const end of ends) end()
147
+ },
148
+ }
149
+ }
@@ -0,0 +1,184 @@
1
+ /**
2
+ * A real HTTP server for the app's route handlers, so the browser's clients
3
+ * reach A2 servers over the actual wire format.
4
+ */
5
+
6
+ import { createServer, type ServerResponse } from 'node:http'
7
+ import type { AddressInfo, Socket } from 'node:net'
8
+ import type { A2Operation } from 'experimental-a2/server'
9
+ import { setGlobalOrigin } from 'undici'
10
+
11
+ type Handler = (request: Request) => Response | Promise<Response>
12
+
13
+ export type Served = {
14
+ /** Where the server listens; relative URLs resolve against it until it closes. */
15
+ origin: string
16
+ close(): Promise<void>
17
+ }
18
+
19
+ const writeResponse = async (
20
+ res: ServerResponse,
21
+ response: Response,
22
+ ): Promise<void> => {
23
+ res.writeHead(response.status, Object.fromEntries(response.headers.entries()))
24
+ res.flushHeaders()
25
+ if (!response.body) {
26
+ res.end()
27
+ return
28
+ }
29
+ const reader = response.body.getReader()
30
+ // A disconnecting client must cancel the web stream, which closes the
31
+ // store subscription behind it.
32
+ res.on('close', () => {
33
+ reader.cancel().catch(() => {})
34
+ })
35
+ try {
36
+ for (;;) {
37
+ // oxlint-disable-next-line no-await-in-loop -- stream pump
38
+ const { done, value } = await reader.read()
39
+ if (done) break
40
+ res.write(value)
41
+ }
42
+ res.end()
43
+ } catch {
44
+ res.destroy()
45
+ }
46
+ }
47
+
48
+ /**
49
+ * Serves each route's handler at its path. The page and `fetch` resolve
50
+ * relative URLs, like a module-level client's `api`, against this server
51
+ * until it closes.
52
+ */
53
+ export async function serve({
54
+ routes,
55
+ }: {
56
+ routes: Readonly<Record<string, Handler>>
57
+ }): Promise<Served> {
58
+ let origin = ''
59
+ const server = createServer((req, res) => {
60
+ void (async () => {
61
+ try {
62
+ const url = new URL(req.url ?? '/', origin)
63
+ const handler = routes[url.pathname]
64
+ if (handler === undefined) {
65
+ res.writeHead(404).end()
66
+ return
67
+ }
68
+ const chunks: Buffer[] = []
69
+ for await (const chunk of req) chunks.push(chunk as Buffer)
70
+ const request = new Request(url, {
71
+ method: req.method ?? 'GET',
72
+ headers: Object.entries(req.headers).flatMap(([name, value]) =>
73
+ value === undefined
74
+ ? []
75
+ : (Array.isArray(value) ? value : [value]).map(
76
+ (item): [string, string] => [name, item],
77
+ ),
78
+ ),
79
+ ...(chunks.length === 0 ? {} : { body: Buffer.concat(chunks) }),
80
+ })
81
+ await writeResponse(res, await handler(request))
82
+ } catch (error) {
83
+ // A handler's bug answers as one, not as a dropped connection the
84
+ // client would retry.
85
+ if (res.headersSent) res.destroy()
86
+ else res.writeHead(500).end(String(error))
87
+ }
88
+ })()
89
+ })
90
+ // close() must never wait on a request a test parked.
91
+ const sockets = new Set<Socket>()
92
+ server.on('connection', (socket) => {
93
+ sockets.add(socket)
94
+ socket.on('close', () => sockets.delete(socket))
95
+ })
96
+ // The address clients use: on macOS, another process can bind 127.0.0.1 on
97
+ // a port this server holds on every address, and take its connections.
98
+ await new Promise<void>((resolve) => server.listen(0, '127.0.0.1', resolve))
99
+ const { port } = server.address() as AddressInfo
100
+ origin = `http://127.0.0.1:${port}`
101
+ setGlobalOrigin(origin)
102
+ // Vitest's jsdom environment exposes its JSDOM, which sets the page's URL.
103
+ ;(
104
+ globalThis as { jsdom?: { reconfigure(settings: { url: string }): void } }
105
+ ).jsdom?.reconfigure({ url: `${origin}/` })
106
+ return {
107
+ origin,
108
+ close: () =>
109
+ new Promise((resolve) => {
110
+ setGlobalOrigin(undefined)
111
+ for (const socket of sockets) socket.destroy()
112
+ server.close(() => resolve())
113
+ }),
114
+ }
115
+ }
116
+
117
+ type Match = (operation: A2Operation) => boolean
118
+
119
+ export type Held = {
120
+ /** The parked operation, once it arrives. */
121
+ arrived: Promise<A2Operation>
122
+ allow(): void
123
+ deny(): void
124
+ }
125
+
126
+ /**
127
+ * An `authorize` for `server.fetch` that records every operation it sees,
128
+ * and can park or deny the next one a test names.
129
+ */
130
+ export function authorizer(): {
131
+ authorize: (operation: A2Operation) => Promise<boolean>
132
+ /** Every operation so far, in arrival order. */
133
+ operations: A2Operation[]
134
+ /** Parks the next matching operation until the test decides it. */
135
+ hold(match: Match): Held
136
+ /** Denies the next matching operation. */
137
+ deny(match: Match): void
138
+ /** Forgets the rules not yet used, and denies what is still parked. */
139
+ release(): void
140
+ } {
141
+ const operations: A2Operation[] = []
142
+ const rules: {
143
+ match: Match
144
+ decide: (operation: A2Operation) => Promise<boolean>
145
+ }[] = []
146
+ const parked = new Set<Held>()
147
+ return {
148
+ operations,
149
+ authorize: async (operation) => {
150
+ operations.push(operation)
151
+ const at = rules.findIndex((rule) => rule.match(operation))
152
+ return at === -1 || rules.splice(at, 1)[0]!.decide(operation)
153
+ },
154
+ hold(match) {
155
+ const arrival = Promise.withResolvers<A2Operation>()
156
+ const decision = Promise.withResolvers<boolean>()
157
+ const decide = (allowed: boolean): void => {
158
+ parked.delete(held)
159
+ decision.resolve(allowed)
160
+ }
161
+ const held: Held = {
162
+ arrived: arrival.promise,
163
+ allow: () => decide(true),
164
+ deny: () => decide(false),
165
+ }
166
+ parked.add(held)
167
+ rules.push({
168
+ match,
169
+ decide: (operation) => {
170
+ arrival.resolve(operation)
171
+ return decision.promise
172
+ },
173
+ })
174
+ return held
175
+ },
176
+ deny(match) {
177
+ rules.push({ match, decide: async () => false })
178
+ },
179
+ release() {
180
+ rules.length = 0
181
+ for (const held of parked) held.deny()
182
+ },
183
+ }
184
+ }
@@ -0,0 +1 @@
1
+ export {}
@@ -71,6 +71,15 @@
71
71
  "retryAfterSeconds": 30
72
72
  }
73
73
  ]
74
+ },
75
+ "app/a2slack/scheduler/route.ts": {
76
+ "experimentalTriggers": [
77
+ {
78
+ "type": "queue/v2beta",
79
+ "topic": "a2-a2slack",
80
+ "retryAfterSeconds": 30
81
+ }
82
+ ]
74
83
  }
75
84
  }
76
85
  }
@@ -0,0 +1,27 @@
1
+ import { fileURLToPath } from 'node:url'
2
+ import { defineConfig } from 'vitest/config'
3
+
4
+ export default defineConfig({
5
+ resolve: {
6
+ tsconfigPaths: true,
7
+ alias: {
8
+ 'server-only': fileURLToPath(
9
+ new URL('./test/server-only.ts', import.meta.url),
10
+ ),
11
+ },
12
+ },
13
+ test: {
14
+ // The app's servers run in-process: with no store URL they use A2's
15
+ // memory store, never a database a dev shell points at, and with no
16
+ // queues their work drains in the background of the append. Without
17
+ // credentials, nothing can reach the AI Gateway.
18
+ env: {
19
+ REDIS_URL: '',
20
+ DATABASE_URL: '',
21
+ VERCEL: '',
22
+ A2_QUEUES: '',
23
+ AI_GATEWAY_API_KEY: '',
24
+ VERCEL_OIDC_TOKEN: '',
25
+ },
26
+ },
27
+ })
@@ -0,0 +1,32 @@
1
+ // The a2slack drills (app/a2slack/drills): load, rebuild, and failure runs
2
+ // against a throwaway Redis and Postgres they start themselves. Not part of
3
+ // `pnpm test`; run them with `pnpm drills`.
4
+ import { fileURLToPath } from 'node:url'
5
+ import { defineConfig } from 'vitest/config'
6
+
7
+ export default defineConfig({
8
+ resolve: {
9
+ tsconfigPaths: true,
10
+ alias: {
11
+ 'server-only': fileURLToPath(
12
+ new URL('./test/server-only.ts', import.meta.url),
13
+ ),
14
+ },
15
+ },
16
+ test: {
17
+ include: ['app/a2slack/drills/*.drill.ts'],
18
+ fileParallelism: false,
19
+ testTimeout: 15 * 60_000,
20
+ hookTimeout: 120_000,
21
+ env: {
22
+ VERCEL: '',
23
+ A2_QUEUES: '',
24
+ AI_GATEWAY_API_KEY: '',
25
+ VERCEL_OIDC_TOKEN: '',
26
+ KV_REST_API_URL: '',
27
+ KV_REST_API_TOKEN: '',
28
+ REDIS_KV_REST_API_URL: '',
29
+ REDIS_KV_REST_API_TOKEN: '',
30
+ },
31
+ },
32
+ })
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "experimental-a2",
3
- "version": "0.19.0",
3
+ "version": "0.21.1",
4
4
  "description": "Syncs your immutable event log, from server to browser: one contract, handlers, derived state, and a live client per session.",
5
5
  "license": "MIT",
6
6
  "type": "module",
@@ -21,6 +21,7 @@
21
21
  "dist",
22
22
  "docs",
23
23
  "examples",
24
+ "sql",
24
25
  "src",
25
26
  "AGENTS.md",
26
27
  "CHANGELOG.md",
@@ -112,6 +113,9 @@
112
113
  },
113
114
  "devDependencies": {
114
115
  "@electric-sql/pglite": "^0.3.14",
116
+ "experimental-a2-0-20": "npm:experimental-a2@0.20.0",
117
+ "drizzle-kit": "^0.31.11",
118
+ "drizzle-orm": "^0.45.3",
115
119
  "@upstash/qstash": "^2.11.3",
116
120
  "ai": "^7.0.58",
117
121
  "ioredis": "^5.9.0",
@@ -120,6 +124,7 @@
120
124
  "@types/ws": "^8.18.1",
121
125
  "@vercel/queue": "^0.2.0",
122
126
  "pg": "^8.16.0",
127
+ "postgres": "^3.4.9",
123
128
  "ws": "^8.21.3",
124
129
  "@testing-library/react": "^16.1.0",
125
130
  "@types/react": "^19.0.0",
@@ -144,7 +149,7 @@
144
149
  "scripts": {
145
150
  "build": "tsdown",
146
151
  "dev": "tsdown --watch",
147
- "typecheck": "tsc --noEmit",
152
+ "typecheck": "tsc --noEmit && tsc --noEmit -p tsconfig.reference.json",
148
153
  "test": "vitest run",
149
154
  "test:watch": "vitest"
150
155
  }