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
package/src/ai-harness.ts CHANGED
@@ -14,6 +14,7 @@ import type {
14
14
  ReasoningLevel,
15
15
  ToolResultPayload,
16
16
  } from './ai.ts'
17
+ import { createHash } from 'node:crypto'
17
18
  import { STEERED_REASON } from './ai-control.ts'
18
19
  import { boundedCache, type BoundedCache } from './bounded-cache.ts'
19
20
  import type { ContractEvent, EventDefs } from './contract.ts'
@@ -322,6 +323,7 @@ export type HarnessInput = { text: string; files: FileUIPart[] }
322
323
 
323
324
  /** One harness turn to rebuild: its prompt, completed generations, and end. */
324
325
  export type HarnessTurn = {
326
+ /** With any steering that entered before the turn's first completed generation. */
325
327
  prompt: HarnessInput
326
328
  generations: RecordedGeneration[]
327
329
  /** Steering that entered after the generation at that index. */
@@ -440,8 +442,15 @@ export function harnessTurns(
440
442
  files: response.prompt.flatMap((prompt) => prompt.files),
441
443
  }
442
444
  let turn = turns.at(-1)
443
- if (steered && turn !== undefined) {
444
- const at = Math.max(0, turn.generations.length - 1)
445
+ if (steered && turn?.generations.length === 0) {
446
+ // Nothing answered the prompt yet, so the steering joins it, as messages
447
+ // queued before a response join its prompt.
448
+ turn.prompt = {
449
+ text: `${turn.prompt.text}\n\n${input.text}`,
450
+ files: [...turn.prompt.files, ...input.files],
451
+ }
452
+ } else if (steered && turn !== undefined) {
453
+ const at = turn.generations.length - 1
445
454
  turn.steers.set(at, [...(turn.steers.get(at) ?? []), input])
446
455
  } else {
447
456
  turn = { prompt: input, generations: [], steers: new Map(), end: 'live' }
@@ -598,6 +607,23 @@ export function gatewayCredential(harness: string): GatewayCredential {
598
607
  throw new Error(`${harness}() needs AI_GATEWAY_API_KEY or VERCEL_OIDC_TOKEN`)
599
608
  }
600
609
 
610
+ /**
611
+ * The `x-session-affinity` an agent session's Gateway calls carry: stable for
612
+ * the session, so supporting providers can reuse their cache, and free of its
613
+ * raw ID.
614
+ */
615
+ export function gatewaySessionAffinity({
616
+ agentName,
617
+ sessionId,
618
+ }: {
619
+ agentName: string
620
+ sessionId: string
621
+ }): string {
622
+ return createHash('sha256')
623
+ .update(JSON.stringify([agentName, sessionId]))
624
+ .digest('hex')
625
+ }
626
+
601
627
  // Where Vercel Functions keep the current request, as @vercel/oidc reads it.
602
628
  const REQUEST_CONTEXT = Symbol.for('@vercel/request-context')
603
629
 
package/src/ai-pi.ts CHANGED
@@ -436,7 +436,7 @@ async function runStep(
436
436
  const result = results.get(toolCallId)
437
437
  if (result !== undefined) return answeredTool(result, shape)
438
438
  if (state.plan.end === 'live' && state.liveStep) {
439
- // Pi now waits on tools that A2's tool handlers run.
439
+ // Pi now waits on tools that A2's tool consumer run.
440
440
  endStep(state)
441
441
  return pending(signal)
442
442
  }
@@ -962,7 +962,7 @@ function builtinDefinition(
962
962
 
963
963
  /**
964
964
  * The tools Pi's session declares: Pi's own, and the agent's. Their results
965
- * come from A2's log; A2's tool handlers ran them.
965
+ * come from A2's log; A2's tool consumer ran them.
966
966
  */
967
967
  async function sessionTools(
968
968
  modules: PiModules,
package/src/ai-server.ts CHANGED
@@ -1,10 +1,10 @@
1
1
  /**
2
2
  * experimental-a2/ai/server — `agent()`: the experimental-a2/ai protocol,
3
- * its model and tool handlers, and A2's store, scheduler, and telemetry,
3
+ * its model and tool consumers, and A2's store, scheduler, and telemetry,
4
4
  * assembled as one ordinary A2 server.
5
5
  */
6
6
 
7
- export { agent, runWithHandlerContext } from './ai-agent.ts'
7
+ export { agent, runWithConsumerContext } from './ai-agent.ts'
8
8
  export type {
9
9
  AISDKHarness,
10
10
  Agent,
package/src/ai.ts CHANGED
@@ -94,7 +94,7 @@ import {
94
94
  type ProgressBatches,
95
95
  type StoredProgressPayload,
96
96
  } from './ai-progress-batches.ts'
97
- import type { HandlerContext } from './server.ts'
97
+ import type { ConsumerContext } from './server.ts'
98
98
  import type { StandardSchemaV1 } from './standard-schema.ts'
99
99
  import { assertSyncSchema, validateSync } from './validate.ts'
100
100
 
@@ -462,8 +462,8 @@ export type AgentContract<
462
462
  export type AgentToolEventDefs = EventDefs &
463
463
  Pick<AIEventDefs, 'ai.tool.execution.requested'>
464
464
 
465
- /** The durable handler context supplied when A2 executes a local AI tool. */
466
- export type AgentToolContext<D extends AgentToolEventDefs> = HandlerContext<
465
+ /** The durable consumer context supplied when A2 executes a local AI tool. */
466
+ export type AgentToolContext<D extends AgentToolEventDefs> = ConsumerContext<
467
467
  D,
468
468
  'ai.tool.execution.requested'
469
469
  >
@@ -2526,20 +2526,20 @@ export const inputs: AIInputs = {
2526
2526
  }
2527
2527
 
2528
2528
  /**
2529
- * Read the durable handler context ambient to the current AI tool
2529
+ * Read the durable consumer context ambient to the current AI tool
2530
2530
  * execution. A2 runs each local tool's `execute` inside this scope, so
2531
2531
  * plain AI SDK `tool()` definitions reach their `event`, `attempt`,
2532
2532
  * `session`, and `signal` without an A2-specific wrapper. Pass the
2533
2533
  * agent's contract to type application events; it is verified against
2534
2534
  * the executing agent.
2535
2535
  */
2536
- export function handlerContext<D extends AgentToolEventDefs = AIEventDefs>(
2536
+ export function consumerContext<D extends AgentToolEventDefs = AIEventDefs>(
2537
2537
  contract?: Contract<D>,
2538
2538
  ): AgentToolContext<D> {
2539
2539
  const scope = ambientToolScopeStorage()?.getStore()
2540
2540
  if (scope === undefined) {
2541
2541
  throw new TypeError(
2542
- "handlerContext() requires an ambient A2 tool execution: call it inside a local tool's execute, or wrap direct tool tests with runWithHandlerContext",
2542
+ "consumerContext() requires an ambient A2 tool execution: call it inside a local tool's execute, or wrap direct tool tests with runWithConsumerContext",
2543
2543
  )
2544
2544
  }
2545
2545
  if (
@@ -2548,7 +2548,7 @@ export function handlerContext<D extends AgentToolEventDefs = AIEventDefs>(
2548
2548
  scope.contract.name !== contract.name
2549
2549
  ) {
2550
2550
  throw new TypeError(
2551
- `handlerContext('${contract.name}') was called during a tool execution for agent '${scope.contract.name}'`,
2551
+ `consumerContext('${contract.name}') was called during a tool execution for agent '${scope.contract.name}'`,
2552
2552
  )
2553
2553
  }
2554
2554
  return scope.context as AgentToolContext<D>
package/src/contract.ts CHANGED
@@ -123,7 +123,7 @@ export type Contract<
123
123
  readonly events: Readonly<D>
124
124
  /** The presence vocabulary — empty unless declared. */
125
125
  readonly presence: Readonly<P>
126
- /** Preserve literal types for a handler-returned event batch. */
126
+ /** Preserve literal types for a consumer-returned event batch. */
127
127
  readonly batch: EventBatchFactory<D>
128
128
  readonly reducer: R
129
129
  }
@@ -1,5 +1,5 @@
1
1
  /**
2
- * Deterministic default ids for handler `session.append` calls. A handler
2
+ * Deterministic default ids for consumer `session.append` calls. A consumer
3
3
  * re-run after a crash calls `session.append` again; giving
4
4
  * each call a deterministic id — derived from the triggering event's id
5
5
  * and the caller's stable name — makes the re-run a no-op via the store's
@@ -32,20 +32,38 @@ async function deterministicId(parts: Array<string | number>): Promise<string> {
32
32
  return deterministicIdFromInput(parts.join('\u0000'))
33
33
  }
34
34
 
35
+ /**
36
+ * `consumer` keys the id on the consumer that appends, so two consumers of
37
+ * one event never collide. A2's own consumers pass none: their types are
38
+ * disjoint, so the event alone keys them.
39
+ */
35
40
  export async function deterministicEventId(
36
41
  triggerEventId: string,
37
42
  name: string,
38
43
  itemOrdinal: number,
44
+ consumer?: string,
39
45
  ): Promise<string> {
40
46
  if (name.length === 0) throw new TypeError('append name must not be empty')
41
- return deterministicId([APPEND_NAMESPACE, triggerEventId, name, itemOrdinal])
47
+ return deterministicId([
48
+ APPEND_NAMESPACE,
49
+ triggerEventId,
50
+ name,
51
+ itemOrdinal,
52
+ ...(consumer === undefined ? [] : [consumer]),
53
+ ])
42
54
  }
43
55
 
44
56
  export async function deterministicReturnedEventId(
45
57
  triggerEventId: string,
46
58
  itemOrdinal: number,
59
+ consumer?: string,
47
60
  ): Promise<string> {
48
- return deterministicId([RETURN_NAMESPACE, triggerEventId, itemOrdinal])
61
+ return deterministicId([
62
+ RETURN_NAMESPACE,
63
+ triggerEventId,
64
+ itemOrdinal,
65
+ ...(consumer === undefined ? [] : [consumer]),
66
+ ])
49
67
  }
50
68
 
51
69
  /**
@@ -60,6 +78,7 @@ export async function deterministicScheduleId(
60
78
  triggerEventId: string | null,
61
79
  name: string,
62
80
  clearIndex = 0,
81
+ consumer?: string,
63
82
  ): Promise<string> {
64
83
  return deterministicIdFromInput(
65
84
  JSON.stringify([
@@ -69,6 +88,7 @@ export async function deterministicScheduleId(
69
88
  triggerEventId,
70
89
  name,
71
90
  ...(clearIndex > 0 ? [clearIndex] : []),
91
+ ...(consumer === undefined ? [] : [{ consumer }]),
72
92
  ]),
73
93
  )
74
94
  }
@@ -80,6 +100,7 @@ export async function deterministicScheduleClearId({
80
100
  clearIndex,
81
101
  name,
82
102
  ifLastIndex,
103
+ consumer,
83
104
  }: {
84
105
  contract: string
85
106
  sessionId: string
@@ -87,6 +108,7 @@ export async function deterministicScheduleClearId({
87
108
  clearIndex: number
88
109
  name: string
89
110
  ifLastIndex: number
111
+ consumer?: string
90
112
  }): Promise<string> {
91
113
  return deterministicIdFromInput(
92
114
  JSON.stringify([
@@ -97,6 +119,7 @@ export async function deterministicScheduleClearId({
97
119
  clearIndex,
98
120
  name,
99
121
  ifLastIndex,
122
+ ...(consumer === undefined ? [] : [{ consumer }]),
100
123
  ]),
101
124
  )
102
125
  }
package/src/errors.ts CHANGED
@@ -3,13 +3,14 @@ export type A2ServerErrorCode =
3
3
  | 'FORBIDDEN' // external ingress was denied by server.fetch authorization
4
4
  | 'UNKNOWN_EVENT_TYPE' // event type not in the machine's `events` map
5
5
  | 'PARTIAL_DUPLICATE_BATCH' // batch mixed already-appended and fresh events
6
- | 'SUPERSEDED_ATTEMPT' // handler append from an attempt a recovery claim or a clear replaced
6
+ | 'SUPERSEDED_ATTEMPT' // consumer append from an attempt a recovery claim or a clear replaced
7
7
  | 'SESSION_CLEARED' // a read or call reached across the session's latest clear
8
8
  | 'CLAIM_EXPIRED' // ctx.signal abort reason: the attempt's lease lapsed without renewal
9
9
  | 'STORE_UNAVAILABLE' // store backend failure — retryable, nothing was written
10
- | 'STORE_NOT_CONFIGURED' // production boot with no `store` passed
10
+ | 'STORE_NOT_CONFIGURED' // no `store` in production, a missing peer dependency, or Postgres lacks A2's tables
11
11
  | 'UNKNOWN_PRESENCE_FIELD' // presence field not in the contract's `presence` map
12
12
  | 'PRESENCE_NOT_SUPPORTED' // contract declares presence but the store lacks the capability
13
+ | 'CONSUMER_DEAD_LETTERED' // session.consume reached a head that dead-lettered for that consumer
13
14
 
14
15
  export type A2ErrorCode =
15
16
  A2ServerErrorCode | 'TIMEOUT' | 'HTTP_ERROR' | 'CONNECTION_FAILED' | 'UNKNOWN'
@@ -34,14 +35,16 @@ export class A2Error extends Error {
34
35
  }
35
36
  }
36
37
 
37
- /** A handler write whose attempt no longer owns its triggering event. */
38
+ /** A consumer write whose attempt no longer owns its triggering event. */
38
39
  export function supersededAttemptError(
39
40
  sessionId: string,
40
- cause: { index: number; attempt: number },
41
+ cause: { consumer?: string; index: number; attempt: number },
41
42
  ): A2Error {
43
+ const of =
44
+ cause.consumer === undefined ? '' : ` for consumer '${cause.consumer}'`
42
45
  return new A2Error(
43
46
  'SUPERSEDED_ATTEMPT',
44
- `attempt ${cause.attempt} no longer owns event ${cause.index} in session '${sessionId}'`,
47
+ `attempt ${cause.attempt} no longer owns event ${cause.index}${of} in session '${sessionId}'`,
45
48
  )
46
49
  }
47
50
 
@@ -58,7 +61,7 @@ export function asStoreUnavailable(err: unknown): A2Error {
58
61
  }
59
62
 
60
63
  /**
61
- * Throw from a handler to reject retrying: the failure is adjudicated
64
+ * Throw from a consumer to reject retrying: the failure is adjudicated
62
65
  * terminal immediately (dead-lettered on the first throw) instead of
63
66
  * consuming the ten-failure retry budget. For deterministic failures —
64
67
  * a guard that will refuse identically on every attempt — retrying is
@@ -37,7 +37,6 @@ export const idempotentReplay = (
37
37
  stored === undefined ||
38
38
  stored.index !== firstIndex + index ||
39
39
  stored.type !== event.type ||
40
- stored.lane !== (event.lane ?? null) ||
41
40
  stored.cause?.batchSize !== event.cause?.batchSize ||
42
41
  canonicalJson(stored.payload) !== canonicalJson(event.payload)
43
42
  )
package/src/index.ts CHANGED
@@ -3,7 +3,7 @@
3
3
  *
4
4
  * This entry point is isomorphic by construction: the contract (the
5
5
  * vocabulary everything shares), reducers derived from it, and the
6
- * error type. Implementing a contract — storage, handlers, sessions —
6
+ * error type. Implementing a contract — storage, consumers, sessions —
7
7
  * lives in experimental-a2/server, which never enters a browser bundle.
8
8
  */
9
9
 
package/src/internal.ts CHANGED
@@ -7,7 +7,8 @@ import type { ScheduledEvent } from './scheduler-task.ts'
7
7
  import type { DrainableServer } from './server.ts'
8
8
  import type { Event } from './store.ts'
9
9
 
10
- export type DrainOutcome = 'busy' | 'stalled' | 'settled'
10
+ /** `failed`: a scheduler drain threw after arming its successor. */
11
+ export type DrainOutcome = 'busy' | 'stalled' | 'settled' | 'failed'
11
12
 
12
13
  /**
13
14
  * A null-prototype record for maps keyed by caller-controlled strings
@@ -74,7 +75,17 @@ export type ServerInternals = {
74
75
  export const serverInternals: WeakMap<object, ServerInternals> = new WeakMap()
75
76
 
76
77
  /**
77
- * Handler contexts whose previous attempt ended in a failure the handler
78
+ * Consumers A2's own runtimes define: named under `a2.`, with child ids that
79
+ * leave the name out, since their types are disjoint. Defined through
80
+ * `runtimeConsumer` (runtime-consumer.ts), which the server module installs.
81
+ */
82
+ export const runtimeConsumers: WeakSet<object> = new WeakSet()
83
+
84
+ export const runtimeConsumerFactory: { define?: (options: never) => unknown } =
85
+ {}
86
+
87
+ /**
88
+ * Consumer contexts whose previous attempt ended in a failure the consumer
78
89
  * threw, as opposed to a claim that lapsed (a crash, a timeout, a frozen
79
90
  * process). Only the attempt's own outcome is known, not how far it got.
80
91
  */
@@ -83,9 +94,9 @@ export const failedPreviousAttempt: WeakSet<object> = new WeakSet()
83
94
  const objectLike = (value: unknown): value is object =>
84
95
  (typeof value === 'object' && value !== null) || typeof value === 'function'
85
96
 
86
- /** The ambient value delivered to `handlerContext` during a tool execution. */
97
+ /** The ambient value delivered to `consumerContext` during a tool execution. */
87
98
  export type AmbientToolScope = {
88
- /** Executing server identity; a test scope from `runWithHandlerContext` has none. */
99
+ /** Executing server identity; a test scope from `runWithConsumerContext` has none. */
89
100
  contract?: { readonly name: string }
90
101
  context: unknown
91
102
  }
@@ -94,7 +105,7 @@ export type AmbientToolScope = {
94
105
  * Structural AsyncLocalStorage surface for the ambient tool scope. The
95
106
  * instance is created by the server-only AI entry point (the only place
96
107
  * `node:async_hooks` may be imported) and shared through a global
97
- * symbol slot, so `handlerContext` in the isomorphic AI entry point and
108
+ * symbol slot, so `consumerContext` in the isomorphic AI entry point and
98
109
  * independently bundled A2 copies all read the same scope.
99
110
  */
100
111
  export type AmbientToolScopeStorage = {
@@ -266,7 +277,7 @@ export function assertSchedulerTargets(
266
277
  export const DRAIN_TIMINGS: {
267
278
  /** How long a claim lives without renewal. */
268
279
  claimTtlMs: number
269
- /** Heartbeat cadence while a handler is running. */
280
+ /** Heartbeat cadence while a consumer is running. */
270
281
  claimHeartbeatMs: number
271
282
  /** Base delay before retrying a failed renewal while the lease lives. */
272
283
  renewRetryMs: number
package/src/otel.ts CHANGED
@@ -4,7 +4,7 @@
4
4
  * Peer-depends on `@opentelemetry/api` (optional). Spans nest through
5
5
  * the active context, so with a context manager registered (any real
6
6
  * OTel setup, e.g. `@vercel/otel`) the happy path lands in one trace:
7
- * request → a2.append → a2.drain → a2.event → the handler's own spans.
7
+ * request → a2.append → a2.drain → a2.event → the consumer's own spans.
8
8
  */
9
9
 
10
10
  import {
@@ -220,25 +220,3 @@ export function postgresNotifications(
220
220
  }
221
221
  return manager
222
222
  }
223
-
224
- export const POSTGRES_NOTIFICATIONS_SQL = `
225
- create or replace function a2_notify_events() returns trigger language plpgsql as $$
226
- begin
227
- perform pg_notify('a2_events_' || md5(new.session_id), '');
228
- return null;
229
- end $$;
230
- create or replace function a2_notify_presence() returns trigger language plpgsql as $$
231
- begin
232
- perform pg_notify('a2_presence_' || md5(case when TG_OP = 'DELETE' then old.ns else new.ns end),
233
- coalesce(current_setting('a2.presence_source', true), ''));
234
- return null;
235
- end $$;
236
- do $$ begin
237
- if not exists (select 1 from pg_trigger where tgname = 'a2_notify_events' and tgrelid = 'a2_events'::regclass) then
238
- create trigger a2_notify_events after insert on a2_events for each row execute function a2_notify_events();
239
- end if;
240
- if not exists (select 1 from pg_trigger where tgname = 'a2_notify_presence' and tgrelid = 'a2_presence'::regclass) then
241
- create trigger a2_notify_presence after insert or update or delete on a2_presence for each row execute function a2_notify_presence();
242
- end if;
243
- end $$;
244
- `
@@ -15,10 +15,7 @@ export type PostgresQueries = {
15
15
  client: PostgresClient
16
16
  raw: PostgresClient
17
17
  exclusive<T>(fn: () => Promise<T>): Promise<T>
18
- initialize(options: {
19
- schema: string
20
- run: () => Promise<void>
21
- }): Promise<void>
18
+ initialize(options: { key: string; run: () => Promise<void> }): Promise<void>
22
19
  close(): Promise<void>
23
20
  }
24
21
 
@@ -128,7 +125,7 @@ export function postgresConnectionKey({
128
125
 
129
126
  const queries = (raw: PostgresClient): PostgresQueries => {
130
127
  let pending: Promise<unknown> = Promise.resolve()
131
- const schemas = new Map<string, Promise<void>>()
128
+ const initialized = new Map<string, Promise<void>>()
132
129
  const exclusive = <T>(fn: () => Promise<T>): Promise<T> => {
133
130
  const result = pending.then(fn)
134
131
  pending = result.catch(() => {})
@@ -140,13 +137,13 @@ const queries = (raw: PostgresClient): PostgresQueries => {
140
137
  ? raw
141
138
  : { query: (text, values) => exclusive(() => raw.query(text, values)) },
142
139
  exclusive,
143
- initialize({ schema, run }) {
144
- const existing = schemas.get(schema)
140
+ initialize({ key, run }) {
141
+ const existing = initialized.get(key)
145
142
  if (existing) return existing
146
143
  const attempt = run()
147
- schemas.set(schema, attempt)
144
+ initialized.set(key, attempt)
148
145
  void attempt.catch(() => {
149
- if (schemas.get(schema) === attempt) schemas.delete(schema)
146
+ if (initialized.get(key) === attempt) initialized.delete(key)
150
147
  })
151
148
  return attempt
152
149
  },
@@ -0,0 +1,121 @@
1
+ /**
2
+ * The Postgres schema contract. A2 never runs DDL in a Postgres database:
3
+ * `sql/postgres/schema.sql`, or the tables in `drizzle-schema.ts` next to it,
4
+ * create
5
+ * the tables, and `notifications.sql` the triggers `listenConnectionString`
6
+ * needs. A store checks nothing up front. A query that finds a table or column
7
+ * missing fails anyway, so the store only turns that failure into one that
8
+ * says what to apply. Missing triggers fail no query, so a listening store
9
+ * looks for them when its listener starts.
10
+ */
11
+
12
+ import { A2Error } from './errors.ts'
13
+ import type { PostgresClient, PostgresConnection } from './store-postgres.ts'
14
+
15
+ const PACKAGE = 'node_modules/experimental-a2'
16
+
17
+ const SEE_DOCS = `See ${PACKAGE}/docs/stores/04-postgres.mdx.`
18
+
19
+ /** Postgres error codes a store that hasn't been set up raises. */
20
+ const UNDEFINED_TABLE = '42P01'
21
+ const UNDEFINED_COLUMN = '42703'
22
+ const UNDEFINED_FUNCTION = '42883'
23
+ /** `a2_batch` refuses to run outside read committed. */
24
+ const BATCH_ISOLATION = 'A2002'
25
+ const INSUFFICIENT_PRIVILEGE = '42501'
26
+ /** NUL, which Postgres can't store: in jsonb (`\u0000`), or in text. */
27
+ const UNTRANSLATABLE_CHARACTER = '22P05'
28
+ const CHARACTER_NOT_IN_REPERTOIRE = '22021'
29
+
30
+ /** The Postgres error in `error`'s cause chain, for clients that wrap theirs. */
31
+ const postgresError = (
32
+ error: unknown,
33
+ ): { code: string; message: string } | undefined => {
34
+ for (let current = error, depth = 0; depth < 5; depth += 1) {
35
+ if (typeof current !== 'object' || current === null) return undefined
36
+ const { code, message, cause } = current as {
37
+ code?: unknown
38
+ message?: unknown
39
+ cause?: unknown
40
+ }
41
+ if (typeof code === 'string' && /^[0-9A-Z]{5}$/.test(code))
42
+ return { code, message: typeof message === 'string' ? message : code }
43
+ current = cause
44
+ }
45
+ return undefined
46
+ }
47
+
48
+ /** Explains a query that failed because A2's tables aren't set up, or on a NUL, or returns `error`. */
49
+ export const explainPostgresError = (error: unknown): unknown => {
50
+ const found = postgresError(error)
51
+ if (
52
+ found?.code === UNDEFINED_TABLE ||
53
+ found?.code === UNDEFINED_COLUMN ||
54
+ found?.code === UNDEFINED_FUNCTION
55
+ )
56
+ return new A2Error(
57
+ 'STORE_NOT_CONFIGURED',
58
+ `${found.message}: A2's tables or its function are missing, or from an earlier release, and A2 doesn't create or change them at runtime. Apply ${PACKAGE}/sql/postgres/schema.sql to this database, or migrate it to the tables in drizzle-schema.ts next to it, with schema.sql's a2_batch function. ${SEE_DOCS}`,
59
+ { cause: error },
60
+ )
61
+ if (found?.code === INSUFFICIENT_PRIVILEGE)
62
+ return new A2Error(
63
+ 'STORE_NOT_CONFIGURED',
64
+ `${found.message}: the role this store connects as needs select, insert, update, and delete on A2's tables, and execute on a2_batch. Create them as that role, or grant it those privileges. ${SEE_DOCS}`,
65
+ { cause: error },
66
+ )
67
+ // Nothing was written: the payload is the problem, so retrying can't help.
68
+ if (
69
+ found?.code === UNTRANSLATABLE_CHARACTER ||
70
+ found?.code === CHARACTER_NOT_IN_REPERTOIRE
71
+ )
72
+ return new A2Error(
73
+ 'INVALID_PAYLOAD',
74
+ `${found.message}: Postgres can't store a NUL character (\\u0000) in text or jsonb, so nothing was written. Remove it before appending.`,
75
+ { cause: error },
76
+ )
77
+ if (found?.code === BATCH_ISOLATION)
78
+ return new A2Error(
79
+ 'STORE_NOT_CONFIGURED',
80
+ `${found.message}: A2 reads what it locks in read committed, Postgres's default, and its connections here default to a stricter level. Set default_transaction_isolation to 'read committed' for the role or database this store connects to. ${SEE_DOCS}`,
81
+ { cause: error },
82
+ )
83
+ return error
84
+ }
85
+
86
+ const explained = <T>(promise: Promise<T>): Promise<T> =>
87
+ promise.catch((error: unknown) => {
88
+ throw explainPostgresError(error)
89
+ })
90
+
91
+ /** `client`, with every query's setup failures explained. */
92
+ export const explainingPostgresClient = (
93
+ client: PostgresClient,
94
+ ): PostgresClient => ({
95
+ query: (text, values) => explained(client.query(text, values)),
96
+ ...(client.connect && {
97
+ connect: async (): Promise<PostgresConnection> => {
98
+ const connection = await client.connect!()
99
+ return {
100
+ query: (text, values) => explained(connection.query(text, values)),
101
+ release: () => connection.release(),
102
+ }
103
+ },
104
+ }),
105
+ ...(client.end && { end: () => client.end!() }),
106
+ })
107
+
108
+ /** Fails when the triggers `listenConnectionString` listens to are missing. */
109
+ export const checkPostgresTriggers = async (
110
+ client: PostgresClient,
111
+ ): Promise<void> => {
112
+ const { rows } =
113
+ await client.query(`select count(*) as triggers from pg_trigger
114
+ where (tgname = 'a2_notify_events' and tgrelid = to_regclass('a2_events'))
115
+ or (tgname = 'a2_notify_presence' and tgrelid = to_regclass('a2_presence'))`)
116
+ if (Number(rows[0]?.['triggers']) < 2)
117
+ throw new A2Error(
118
+ 'STORE_NOT_CONFIGURED',
119
+ `listenConnectionString needs A2's notification triggers, which aren't in this Postgres database. After schema.sql, apply ${PACKAGE}/sql/postgres/notifications.sql to this database. ${SEE_DOCS}`,
120
+ )
121
+ }
@@ -77,6 +77,13 @@ export function parsePresenceSibling(
77
77
  return out
78
78
  }
79
79
 
80
+ /** The longest event id a push may carry: ids are indexed, and Postgres's index rows have a limit. */
81
+ const MAX_PUSHED_ID = 256
82
+
83
+ /** A UUIDv5, the shape of the ids A2 derives (deterministic-id.ts). */
84
+ const DERIVED_ID =
85
+ /^[0-9a-f]{8}-[0-9a-f]{4}-5[0-9a-f]{3}-[89ab][0-9a-f]{3}-[0-9a-f]{12}$/i
86
+
80
87
  /** The events half of the envelope, shared by the POST body and socket
81
88
  * `push` frames so INVALID_PAYLOAD shapes have one implementation. */
82
89
  export function parsePushEvents(events: unknown): ParsedPushEvent[] {
@@ -94,6 +101,19 @@ export function parsePushEvents(events: unknown): ParsedPushEvent[] {
94
101
  if (id !== undefined && typeof id !== 'string') {
95
102
  throw invalid(`events[${i}].id must be a string when present`)
96
103
  }
104
+ if (id !== undefined && (id.length === 0 || id.length > MAX_PUSHED_ID)) {
105
+ throw invalid(
106
+ `events[${i}].id must be 1-${MAX_PUSHED_ID} characters when present`,
107
+ )
108
+ }
109
+ // A2 derives the ids of what consumers append, return, and schedule as
110
+ // UUIDv5s, from inputs a caller can know: one pushed first would take its
111
+ // place. Callers' own ids are v4 (crypto.randomUUID) or v8 (idempotentId).
112
+ if (id !== undefined && DERIVED_ID.test(id)) {
113
+ throw invalid(
114
+ `events[${i}].id is shaped like the ids A2 derives for consumers (a UUIDv5); use crypto.randomUUID() or idempotentId()`,
115
+ )
116
+ }
97
117
  const out: { type: string; payload: unknown; id?: string } = {
98
118
  type,
99
119
  payload,
@@ -0,0 +1,10 @@
1
+ /**
2
+ * `consumer()` for A2's own runtimes (actors, the AI runtime): their names
3
+ * sit under the reserved `a2.` prefix, and their child ids leave the name
4
+ * out, as the handler tables they replaced had. Not a public entry point.
5
+ */
6
+ import { runtimeConsumerFactory } from './internal.ts'
7
+ import type { consumer } from './server.ts'
8
+
9
+ export const runtimeConsumer = ((options: never) =>
10
+ runtimeConsumerFactory.define!(options)) as typeof consumer
@@ -497,8 +497,11 @@ const readBoundedBody = async (
497
497
  return new TextDecoder().decode(bytes)
498
498
  }
499
499
 
500
- const storeUnavailable = (error: unknown): error is A2Error =>
501
- error instanceof A2Error && error.code === 'STORE_UNAVAILABLE'
500
+ // A store that isn't set up yet (Postgres before schema.sql, a missing driver)
501
+ // is fixed without changing the task, so its work waits like any outage.
502
+ const storeOutage = (error: unknown): error is A2Error =>
503
+ error instanceof A2Error &&
504
+ (error.code === 'STORE_UNAVAILABLE' || error.code === 'STORE_NOT_CONFIGURED')
502
505
 
503
506
  const response = {
504
507
  ok: (): Response => new Response('ok'),
@@ -888,7 +891,7 @@ export function qstash(options: QStashSchedulerOptions = {}): A2Scheduler {
888
891
  await deliverSchedulerAppend(server, task)
889
892
  else await deliverSchedulerClear(server, task)
890
893
  } catch (error) {
891
- if (!storeUnavailable(error)) throw error
894
+ if (!storeOutage(error)) throw error
892
895
  await sendStoreOutageSuccessor(task, outage)
893
896
  }
894
897
  return response.ok()
@@ -906,13 +909,17 @@ export function qstash(options: QStashSchedulerOptions = {}): A2Scheduler {
906
909
  outcome: 'unknown' as const,
907
910
  }
908
911
  } catch (error) {
909
- if (!storeUnavailable(error)) throw error
912
+ if (!storeOutage(error)) throw error
910
913
  await sendStoreOutageSuccessor(task, outage)
911
914
  return response.ok()
912
915
  }
913
916
  if (result.settled) return response.ok()
914
917
 
915
- if (result.outcome === 'busy' || result.outcome === 'stalled') {
918
+ if (
919
+ result.outcome === 'busy' ||
920
+ result.outcome === 'stalled' ||
921
+ result.outcome === 'failed'
922
+ ) {
916
923
  if (result.schedulerArm) await result.schedulerArm
917
924
  else {
918
925
  await sendTask({