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
package/src/ai-agent.ts CHANGED
@@ -1,6 +1,6 @@
1
1
  /**
2
- * The server-only implementation of the experimental-a2/ai protocol: the
3
- * handler table and `agent()`, its assembly as an ordinary A2 server.
2
+ * The server-only implementation of the experimental-a2/ai protocol: its
3
+ * consumers and `agent()`, its assembly as an ordinary A2 server.
4
4
  */
5
5
 
6
6
  // oxlint-disable no-await-in-loop -- stream chunks and durable appends are
@@ -75,14 +75,17 @@ import type {
75
75
  import { A2Error } from './errors.ts'
76
76
  import {
77
77
  consumeSchedulerSendFailure,
78
+ drainSettledHooks,
78
79
  failedPreviousAttempt,
79
80
  installAmbientToolScopeStorage,
80
81
  type AmbientToolScope,
81
82
  } from './internal.ts'
83
+ import { runtimeConsumer } from './runtime-consumer.ts'
82
84
  import {
83
85
  createServer,
84
86
  type A2Server,
85
- type HandlerContext,
87
+ type Consumer,
88
+ type ConsumerContext,
86
89
  type ServerOptions,
87
90
  } from './server.ts'
88
91
  import {
@@ -191,7 +194,7 @@ export type AgentResolverContext<
191
194
  * in the checkpoint the generation already reads, so it costs no I/O.
192
195
  */
193
196
  state: X
194
- session: Pick<HandlerContext<D>['session'], 'state'>
197
+ session: Pick<ConsumerContext<D>['session'], 'state'>
195
198
  signal: AbortSignal
196
199
  }
197
200
 
@@ -239,7 +242,7 @@ export type CompactionOptions<
239
242
 
240
243
  export type AgentGenerationSettings<T extends ToolSet> = AISDKStepSettings<T>
241
244
 
242
- export type HandlersOptions<
245
+ export type ConsumersOptions<
243
246
  M extends UIMessage,
244
247
  D extends AIEventDefs<M> & EventDefs,
245
248
  T extends ToolSet = ToolSet,
@@ -344,27 +347,24 @@ type AgentServerOptions<
344
347
  T extends ToolSet,
345
348
  X extends object = Record<never, never>,
346
349
  H extends string = string,
350
+ C extends readonly unknown[] = readonly Consumer<D>[],
347
351
  > = AgentModelOptions<M, D, T, X, H> &
348
- Omit<
349
- ServerOptions<D, Record<never, never>, Reducer<D, S>>,
350
- 'contract' | 'handlers'
351
- > & {
352
- /** Handlers for application events. */
353
- handlers?: ServerOptions<D, Record<never, never>, Reducer<D, S>>['handlers']
354
- }
352
+ Omit<ServerOptions<D, Record<never, never>, Reducer<D, S>, C>, 'contract'>
355
353
 
356
354
  export type AgentOptions<
357
355
  M extends UIMessage,
358
356
  E extends EventDefs,
359
357
  T extends ToolSet,
360
358
  H extends string = string,
359
+ C extends readonly unknown[] = readonly Consumer<AIEventDefs<M> & E>[],
361
360
  > = AgentServerOptions<
362
361
  NoInfer<M>,
363
362
  AIEventDefs<NoInfer<M>> & E,
364
363
  AIState<NoInfer<M>>,
365
364
  T,
366
365
  Record<never, never>,
367
- H
366
+ H,
367
+ C
368
368
  > & {
369
369
  /** Identity: prefixes storage keys and queue messages. */
370
370
  name: string
@@ -384,7 +384,8 @@ export type AgentFromContractOptions<
384
384
  Name extends string = string,
385
385
  X extends object = Record<never, never>,
386
386
  H extends string = string,
387
- > = AgentServerOptions<AIMessageOf<D>, D, S, T, X, H> & {
387
+ C extends readonly unknown[] = readonly Consumer<D>[],
388
+ > = AgentServerOptions<AIMessageOf<D>, D, S, T, X, H, C> & {
388
389
  /**
389
390
  * A shared `agentContract()`. Its application state reaches every resolver
390
391
  * as `state`; a fully custom contract is core A2, served by `createServer()`.
@@ -480,8 +481,8 @@ const toolScopeStorage = installAmbientToolScopeStorage(
480
481
  () => new AsyncLocalStorage<AmbientToolScope>(),
481
482
  )
482
483
 
483
- /** Run `fn` with `ctx` as the ambient tool handler context, for direct tool tests. */
484
- export function runWithHandlerContext<D extends AgentToolEventDefs, R>(
484
+ /** Run `fn` with `ctx` as the ambient tool consumer context, for direct tool tests. */
485
+ export function runWithConsumerContext<D extends AgentToolEventDefs, R>(
485
486
  ctx: AgentToolContext<D>,
486
487
  fn: () => R,
487
488
  ): R {
@@ -683,7 +684,7 @@ async function* consumeGeneration(options: {
683
684
  const reader = options.source.stream.getReader()
684
685
  // The next read starts before a batch is yielded, and the consumer may
685
686
  // take a while to ask again: a stream that fails meanwhile must not leave
686
- // that read rejected with no handler, which ends the process.
687
+ // that read rejected with nothing to catch it, which ends the process.
687
688
  const readAhead = (): Promise<StreamRead<UIMessageChunk>> => {
688
689
  const read = reader.read()
689
690
  read.catch(() => {})
@@ -1232,14 +1233,12 @@ const assertNoLoopOptions = (options: { harness?: { type: string } }): void => {
1232
1233
  }
1233
1234
  }
1234
1235
 
1235
- export function createHandlers<
1236
+ export function createConsumers<
1236
1237
  M extends UIMessage,
1237
1238
  D extends AIEventDefs<M> & EventDefs,
1238
1239
  T extends ToolSet = ToolSet,
1239
1240
  X extends object = Record<never, never>,
1240
- >(
1241
- options: HandlersOptions<M, D, T, X>,
1242
- ): NonNullable<ServerOptions<D>['handlers']> {
1241
+ >(options: ConsumersOptions<M, D, T, X>): Consumer<D>[] {
1243
1242
  if (options.model === undefined) {
1244
1243
  throw new TypeError('agent requires model')
1245
1244
  }
@@ -1349,7 +1348,7 @@ export function createHandlers<
1349
1348
  contract: options.contract,
1350
1349
  })
1351
1350
  const readContext = async (
1352
- session: HandlerContext<D>['session'],
1351
+ session: ConsumerContext<D>['session'],
1353
1352
  through: number | 'latest',
1354
1353
  ) => {
1355
1354
  const snapshot = await session.state(contextReducer, { through })
@@ -1390,7 +1389,7 @@ export function createHandlers<
1390
1389
  ) => unknown
1391
1390
  }
1392
1391
 
1393
- type ToolHandlerContext = HandlerContext<D, 'ai.tool.execution.requested'>
1392
+ type ToolConsumerContext = ConsumerContext<D, 'ai.tool.execution.requested'>
1394
1393
 
1395
1394
  const resultEvent = async (
1396
1395
  call: ToolCalledPayload,
@@ -1435,7 +1434,7 @@ export function createHandlers<
1435
1434
  }) as AppendInput<D>
1436
1435
 
1437
1436
  const promptMessages = (input: {
1438
- ctx: ToolHandlerContext
1437
+ ctx: ToolConsumerContext
1439
1438
  call: ToolCalledPayload
1440
1439
  generation: GenerationStartedPayload
1441
1440
  }): Promise<ModelMessage[]> => {
@@ -1485,7 +1484,7 @@ export function createHandlers<
1485
1484
  }
1486
1485
 
1487
1486
  const toolExecutionFailure = async (
1488
- ctx: ToolHandlerContext,
1487
+ ctx: ToolConsumerContext,
1489
1488
  call: ToolCalledPayload,
1490
1489
  error: unknown,
1491
1490
  ): Promise<AppendInput<D> | void> => {
@@ -1498,7 +1497,7 @@ export function createHandlers<
1498
1497
  }
1499
1498
 
1500
1499
  const runToolExecution = async (
1501
- ctx: ToolHandlerContext,
1500
+ ctx: ToolConsumerContext,
1502
1501
  call: ToolCalledPayload,
1503
1502
  execute: NonNullable<RuntimeTool['execute']>,
1504
1503
  messages: ModelMessage[],
@@ -1575,7 +1574,7 @@ export function createHandlers<
1575
1574
  }
1576
1575
 
1577
1576
  const executeTool = async (input: {
1578
- ctx: ToolHandlerContext
1577
+ ctx: ToolConsumerContext
1579
1578
  call: ToolCalledPayload
1580
1579
  generation: GenerationStartedPayload
1581
1580
  }): Promise<AppendInput<D> | void> => {
@@ -1629,7 +1628,7 @@ export function createHandlers<
1629
1628
  }
1630
1629
 
1631
1630
  const handleToolExecution = async (
1632
- ctx: ToolHandlerContext,
1631
+ ctx: ToolConsumerContext,
1633
1632
  ): Promise<AppendInput<D>> => {
1634
1633
  try {
1635
1634
  const state = (
@@ -1666,7 +1665,7 @@ export function createHandlers<
1666
1665
  }
1667
1666
 
1668
1667
  const generationHandler = async (
1669
- ctx: HandlerContext<D, 'ai.generation.requested'>,
1668
+ ctx: ConsumerContext<D, 'ai.generation.requested'>,
1670
1669
  ): Promise<void | AppendInput<D> | readonly AppendInput<D>[]> => {
1671
1670
  if (checkAbort(ctx.signal)) return
1672
1671
  const requestId = ctx.event.id
@@ -2150,6 +2149,7 @@ export function createHandlers<
2150
2149
  const source =
2151
2150
  harness !== undefined
2152
2151
  ? await harness.step({
2152
+ scope: states,
2153
2153
  agentName: options.contract.name,
2154
2154
  sessionId: ctx.event.sessionId,
2155
2155
  requestIndex: ctx.event.index,
@@ -2315,23 +2315,41 @@ export function createHandlers<
2315
2315
  }
2316
2316
  }
2317
2317
 
2318
- return {
2319
- 'ai.model.metadata.requested': {
2320
- handler: async (ctx) => {
2321
- const limits = await readModelLimits({
2322
- modelId: ctx.event.payload.modelId,
2323
- signal: ctx.signal,
2324
- })
2325
- ctx.signal.throwIfAborted()
2326
- return {
2327
- type: 'ai.model.metadata.resolved',
2328
- id: await aiEventId('a2.ai.model.metadata.resolved', ctx.event.id),
2329
- payload: { modelId: ctx.event.payload.modelId, limits },
2330
- } as AppendInput<D>
2331
- },
2332
- },
2333
- 'ai.control.requested': {
2318
+ const contract = options.contract as unknown as Contract<
2319
+ D,
2320
+ Record<never, never>,
2321
+ undefined
2322
+ >
2323
+ const { toolLane } = options
2324
+ type Of<K extends string> = ContractEvent<D, K & keyof D & string>
2325
+ type Generation = Of<'ai.generation.requested'>
2326
+ type ToolCall = Of<'ai.tool.execution.requested'>
2327
+ return [
2328
+ runtimeConsumer({
2329
+ contract,
2330
+ name: 'a2.ai.metadata',
2331
+ types: ['ai.model.metadata.requested'],
2332
+ }).run(async (ctx) => {
2333
+ const limits = await readModelLimits({
2334
+ modelId: ctx.event.payload.modelId,
2335
+ signal: ctx.signal,
2336
+ })
2337
+ ctx.signal.throwIfAborted()
2338
+ return {
2339
+ type: 'ai.model.metadata.resolved',
2340
+ id: await aiEventId('a2.ai.model.metadata.resolved', ctx.event.id),
2341
+ payload: { modelId: ctx.event.payload.modelId, limits },
2342
+ } as AppendInput<D>
2343
+ }),
2344
+ // One lane for every control event: commands, work reports, and closes
2345
+ // apply one at a time. Its lane function also rejects a bad command at
2346
+ // append, before anything is written.
2347
+ runtimeConsumer({
2348
+ contract,
2349
+ name: 'a2.ai.control',
2350
+ types: ['ai.control.requested', 'ai.work.reported', 'ai.session.closed'],
2334
2351
  lane: ({ event }) => {
2352
+ if (event.type !== 'ai.control.requested') return 'a2.ai.control'
2335
2353
  const command = event.payload as ControlCommand<M>
2336
2354
  if (event.id === '')
2337
2355
  throw new A2Error(
@@ -2353,17 +2371,25 @@ export function createHandlers<
2353
2371
  )
2354
2372
  return 'a2.ai.control'
2355
2373
  },
2356
- handler: controlHandler,
2357
- },
2358
- 'ai.work.reported': { lane: 'a2.ai.control', handler: controlHandler },
2359
- 'ai.session.closed': { lane: 'a2.ai.control', handler: controlHandler },
2360
- 'ai.generation.requested': {
2361
- lane: 'a2.ai.model',
2374
+ }).run(controlHandler as never),
2375
+ runtimeConsumer({
2376
+ contract,
2377
+ name: 'a2.ai.model',
2378
+ types: ['ai.generation.requested'],
2379
+ lane: () => 'a2.ai.model',
2380
+ // The contract is generic here, so the map's keys widen to string and
2381
+ // each predicate names its own event types.
2362
2382
  abortOn: {
2363
- 'ai.control.requested': (event, trigger) =>
2383
+ 'ai.control.requested': (
2384
+ event: Of<'ai.control.requested'>,
2385
+ trigger: Generation,
2386
+ ) =>
2364
2387
  event.payload.action === 'stop' &&
2365
2388
  event.payload.turnId === trigger.payload.control?.turnId,
2366
- 'ai.control.committed': (event, trigger) => {
2389
+ 'ai.control.committed': (
2390
+ event: Of<'ai.control.committed'>,
2391
+ trigger: Generation,
2392
+ ) => {
2367
2393
  const active = (event.payload as ControlCommit).view
2368
2394
  return (
2369
2395
  active?.turnId !== trigger.payload.control?.turnId ||
@@ -2371,39 +2397,50 @@ export function createHandlers<
2371
2397
  )
2372
2398
  },
2373
2399
  'ai.session.closed': true,
2374
- },
2375
- handler: (ctx) =>
2376
- telemetry.span(
2377
- 'a2.ai.generate',
2378
- {
2379
- 'a2.contract': options.contract.name,
2380
- 'a2.session_id': ctx.event.sessionId,
2381
- 'a2.ai.generate.request_id': ctx.event.id,
2382
- 'a2.ai.generate.attempt': ctx.attempt,
2383
- },
2384
- async () => {
2385
- try {
2386
- return control.settled({
2387
- ctx,
2388
- events: await generationHandler(ctx),
2389
- })
2390
- } catch (error) {
2391
- if (control.cancelled({ error, signal: ctx.signal }))
2392
- return control.settled({ ctx, events: undefined })
2393
- throw error
2394
- }
2395
- },
2396
- ),
2397
- },
2398
- 'ai.tool.execution.requested': {
2399
- ...(options.toolLane === undefined ? {} : { lane: options.toolLane }),
2400
+ } as never,
2401
+ }).run((ctx) =>
2402
+ telemetry.span(
2403
+ 'a2.ai.generate',
2404
+ {
2405
+ 'a2.contract': options.contract.name,
2406
+ 'a2.session_id': ctx.event.sessionId,
2407
+ 'a2.ai.generate.request_id': ctx.event.id,
2408
+ 'a2.ai.generate.attempt': ctx.attempt,
2409
+ },
2410
+ async () => {
2411
+ try {
2412
+ return control.settled({
2413
+ ctx,
2414
+ events: await generationHandler(ctx),
2415
+ })
2416
+ } catch (error) {
2417
+ if (control.cancelled({ error, signal: ctx.signal }))
2418
+ return control.settled({ ctx, events: undefined })
2419
+ throw error
2420
+ }
2421
+ },
2422
+ ),
2423
+ ),
2424
+ runtimeConsumer({
2425
+ contract,
2426
+ name: 'a2.ai.tools',
2427
+ types: ['ai.tool.execution.requested'],
2428
+ ...(toolLane === undefined ? {} : { lane: () => toolLane }),
2400
2429
  abortOn: {
2401
- 'ai.control.requested': (event, trigger) =>
2430
+ 'ai.control.requested': (
2431
+ event: Of<'ai.control.requested'>,
2432
+ trigger: ToolCall,
2433
+ ) =>
2402
2434
  event.payload.action === 'stop' &&
2403
2435
  event.payload.turnId === trigger.payload.turnId,
2404
- 'ai.generation.failed': (event, trigger) =>
2405
- event.payload.generationId === trigger.payload.call.generationId,
2406
- 'ai.control.committed': (event, trigger) => {
2436
+ 'ai.generation.failed': (
2437
+ event: Of<'ai.generation.failed'>,
2438
+ trigger: ToolCall,
2439
+ ) => event.payload.generationId === trigger.payload.call.generationId,
2440
+ 'ai.control.committed': (
2441
+ event: Of<'ai.control.committed'>,
2442
+ trigger: ToolCall,
2443
+ ) => {
2407
2444
  const active = (event.payload as ControlCommit).view
2408
2445
  return (
2409
2446
  active?.turnId !== trigger.payload.turnId ||
@@ -2411,26 +2448,26 @@ export function createHandlers<
2411
2448
  )
2412
2449
  },
2413
2450
  'ai.session.closed': true,
2414
- },
2415
- handler: (ctx) =>
2416
- telemetry.span(
2417
- 'a2.ai.tool',
2418
- {
2419
- 'a2.contract': options.contract.name,
2420
- 'a2.session_id': ctx.event.sessionId,
2421
- 'a2.ai.tool.request_id': ctx.event.payload.call.requestId,
2422
- 'a2.ai.tool.call_id': ctx.event.payload.call.toolCallId,
2423
- 'a2.ai.tool.name': ctx.event.payload.call.toolName,
2424
- 'a2.ai.tool.attempt': ctx.attempt,
2425
- },
2426
- () => handleToolExecution(ctx),
2427
- ),
2428
- },
2429
- } as NonNullable<ServerOptions<D>['handlers']>
2451
+ } as never,
2452
+ }).run((ctx) =>
2453
+ telemetry.span(
2454
+ 'a2.ai.tool',
2455
+ {
2456
+ 'a2.contract': options.contract.name,
2457
+ 'a2.session_id': ctx.event.sessionId,
2458
+ 'a2.ai.tool.request_id': ctx.event.payload.call.requestId,
2459
+ 'a2.ai.tool.call_id': ctx.event.payload.call.toolCallId,
2460
+ 'a2.ai.tool.name': ctx.event.payload.call.toolName,
2461
+ 'a2.ai.tool.attempt': ctx.attempt,
2462
+ },
2463
+ () => handleToolExecution(ctx),
2464
+ ),
2465
+ ),
2466
+ ] as Consumer<D>[]
2430
2467
  }
2431
2468
 
2432
2469
  /**
2433
- * Serve an AI agent: the built-in protocol, its handlers, and A2's store,
2470
+ * Serve an AI agent: the built-in protocol, its consumers, and A2's store,
2434
2471
  * scheduler, and telemetry, as one ordinary A2 server. Pass `name` for the
2435
2472
  * built-in protocol, or a shared `agentContract()` when clients fold
2436
2473
  * application state.
@@ -2440,8 +2477,9 @@ export function agent<
2440
2477
  M extends UIMessage = UIMessage,
2441
2478
  E extends EventDefs = Record<never, never>,
2442
2479
  H extends string = never,
2480
+ const C extends readonly unknown[] = readonly Consumer<AIEventDefs<M> & E>[],
2443
2481
  >(
2444
- options: AgentOptions<M, E, T, H>,
2482
+ options: AgentOptions<M, E, T, H, C>,
2445
2483
  ): Agent<AIEventDefs<M> & E, AIState<M>, typeof AI_STATE_REDUCER>
2446
2484
  export function agent<
2447
2485
  D extends AIEventDefs<AIMessageOf<D>> & EventDefs,
@@ -2450,7 +2488,8 @@ export function agent<
2450
2488
  Name extends string = string,
2451
2489
  X extends object = Record<never, never>,
2452
2490
  H extends string = never,
2453
- >(options: AgentFromContractOptions<D, S, T, Name, X, H>): Agent<D, S, Name>
2491
+ const C extends readonly unknown[] = readonly Consumer<D>[],
2492
+ >(options: AgentFromContractOptions<D, S, T, Name, X, H, C>): Agent<D, S, Name>
2454
2493
  export function agent(
2455
2494
  options:
2456
2495
  | AgentOptions<UIMessage, EventDefs, ToolSet>
@@ -2458,6 +2497,12 @@ export function agent(
2458
2497
  ): Agent<AIEventDefs & EventDefs, AIState> {
2459
2498
  type D = AIEventDefs & EventDefs
2460
2499
  assertNoLoopOptions(options)
2500
+ if (
2501
+ options.toolLane !== undefined &&
2502
+ (typeof options.toolLane !== 'string' || options.toolLane.length === 0)
2503
+ ) {
2504
+ throw new TypeError('toolLane must be a non-empty string')
2505
+ }
2461
2506
  const {
2462
2507
  model,
2463
2508
  tools,
@@ -2467,13 +2512,12 @@ export function agent(
2467
2512
  toolRecovery,
2468
2513
  progress,
2469
2514
  toolLane,
2470
- handlers,
2471
2515
  ...rest
2472
2516
  } = options
2473
2517
  let contract: Contract<D, Record<never, never>, Reducer<D, AIState>>
2474
2518
  let serverOptions: Omit<
2475
2519
  ServerOptions<D, Record<never, never>, Reducer<D, AIState>>,
2476
- 'contract' | 'handlers'
2520
+ 'contract'
2477
2521
  >
2478
2522
  if ('contract' in rest) {
2479
2523
  const { contract: shared, ...remaining } = rest
@@ -2493,7 +2537,7 @@ export function agent(
2493
2537
  serverOptions = remaining
2494
2538
  }
2495
2539
  const states = harnessStates()
2496
- const builtIns = createHandlers<UIMessage, D>({
2540
+ const builtIns = createConsumers<UIMessage, D>({
2497
2541
  contract,
2498
2542
  model,
2499
2543
  harnessStates: states,
@@ -2507,17 +2551,24 @@ export function agent(
2507
2551
  ...(toolRecovery === undefined ? {} : { toolRecovery }),
2508
2552
  ...(progress === undefined ? {} : { progress }),
2509
2553
  ...(toolLane === undefined ? {} : { toolLane }),
2510
- } as HandlersOptions<UIMessage, D>)
2511
- for (const type of Object.keys(handlers ?? {})) {
2512
- if (Object.hasOwn(builtIns, type))
2513
- throw new TypeError(`agent handles '${type}' itself`)
2514
- }
2554
+ } as ConsumersOptions<UIMessage, D>)
2515
2555
  const server = createServer({
2516
2556
  ...serverOptions,
2517
2557
  contract,
2518
- handlers: { ...builtIns, ...handlers },
2558
+ consumers: [...builtIns, ...(serverOptions.consumers ?? [])],
2519
2559
  })
2520
2560
  registerHarnessStates(server, states)
2561
+ const driver =
2562
+ harness === undefined || harness.type === 'ai-sdk'
2563
+ ? undefined
2564
+ : harnessDriver(harness)
2565
+ const release = driver?.release
2566
+ // What a harness kept for a session's next step goes once nothing is left
2567
+ // to run for the session here.
2568
+ if (release !== undefined)
2569
+ drainSettledHooks.set(server, (sessionId) =>
2570
+ release({ scope: states, sessionId }),
2571
+ )
2521
2572
  const applicationEvents = 'contract' in rest
2522
2573
  setServerFetchHooks(server, {
2523
2574
  validateIngress: (context) =>
@@ -31,7 +31,7 @@ import type {
31
31
  ContractEvent,
32
32
  EventDefs,
33
33
  } from './contract.ts'
34
- import type { HandlerContext } from './server.ts'
34
+ import type { ConsumerContext } from './server.ts'
35
35
  import type { Reducer } from './reducer.ts'
36
36
  import type { AICoordinatorState, CoordinatorCall } from './ai-coordinator.ts'
37
37
  import { validateSync } from './validate.ts'
@@ -56,18 +56,18 @@ export type AIControlRuntime<
56
56
  D extends AIEventDefs<M> & EventDefs,
57
57
  > = {
58
58
  handler(
59
- ctx: HandlerContext<
59
+ ctx: ConsumerContext<
60
60
  D,
61
61
  'ai.control.requested' | 'ai.work.reported' | 'ai.session.closed'
62
62
  >,
63
63
  ): Promise<AppendInput<D>[]>
64
64
  append(options: {
65
- ctx: HandlerContext<D>
65
+ ctx: ConsumerContext<D>
66
66
  name: string
67
67
  events: AppendInput<D>[]
68
68
  }): Promise<ContractEvent<D>[]>
69
69
  settled(options: {
70
- ctx: HandlerContext<D>
70
+ ctx: ConsumerContext<D>
71
71
  events: void | AppendInput<D> | readonly AppendInput<D>[]
72
72
  }): Promise<AppendInput<D>>
73
73
  cancelled(options: { error: unknown; signal: AbortSignal }): boolean
@@ -117,7 +117,7 @@ export const createControlRuntime = <
117
117
  const coordinator = aiCoordinatorReducer(options.contract)
118
118
 
119
119
  const handler = async (
120
- ctx: HandlerContext<
120
+ ctx: ConsumerContext<
121
121
  D,
122
122
  'ai.control.requested' | 'ai.work.reported' | 'ai.session.closed'
123
123
  >,
@@ -761,7 +761,7 @@ export const createControlRuntime = <
761
761
  settled,
762
762
  name,
763
763
  }: {
764
- ctx: HandlerContext<D>
764
+ ctx: ConsumerContext<D>
765
765
  events: readonly AppendInput<D>[]
766
766
  settled: boolean
767
767
  name: string
@@ -828,7 +828,7 @@ export const createControlRuntime = <
828
828
  name,
829
829
  events,
830
830
  }: {
831
- ctx: HandlerContext<D>
831
+ ctx: ConsumerContext<D>
832
832
  name: string
833
833
  events: AppendInput<D>[]
834
834
  }): Promise<ContractEvent<D>[]> => {
@@ -896,7 +896,7 @@ export const createControlRuntime = <
896
896
  ctx,
897
897
  events,
898
898
  }: {
899
- ctx: HandlerContext<D>
899
+ ctx: ConsumerContext<D>
900
900
  events: void | AppendInput<D> | readonly AppendInput<D>[]
901
901
  }): Promise<AppendInput<D>> =>
902
902
  report({