experimental-a2 0.16.1 → 0.17.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 (333) hide show
  1. package/CHANGELOG.md +352 -0
  2. package/dist/{actor-ohPC-81x.d.ts → actor-DBbe5xeE.d.ts} +21 -11
  3. package/dist/actor-DBbe5xeE.d.ts.map +1 -0
  4. package/dist/actor-client.d.ts +2 -5
  5. package/dist/actor-client.d.ts.map +1 -1
  6. package/dist/actor-client.js +13 -4
  7. package/dist/actor-client.js.map +1 -1
  8. package/dist/actor-react.d.ts +5 -3
  9. package/dist/actor-react.d.ts.map +1 -1
  10. package/dist/actor-react.js +20 -10
  11. package/dist/actor-react.js.map +1 -1
  12. package/dist/{actor-shared-B5tJfzt-.js → actor-shared-t7w8xIKP.js} +12 -7
  13. package/dist/actor-shared-t7w8xIKP.js.map +1 -0
  14. package/dist/actor.d.ts +1 -1
  15. package/dist/actor.js +42 -23
  16. package/dist/actor.js.map +1 -1
  17. package/dist/{ai-DgOBltJ_.js → ai-UrWfKeuv.js} +621 -216
  18. package/dist/ai-UrWfKeuv.js.map +1 -0
  19. package/dist/ai-client.d.ts +27 -0
  20. package/dist/ai-client.d.ts.map +1 -0
  21. package/dist/ai-client.js +29 -0
  22. package/dist/ai-client.js.map +1 -0
  23. package/dist/ai-react.d.ts +96 -0
  24. package/dist/ai-react.d.ts.map +1 -0
  25. package/dist/ai-react.js +142 -0
  26. package/dist/ai-react.js.map +1 -0
  27. package/dist/ai-server.browser.js +2 -2
  28. package/dist/ai-server.browser.js.map +1 -1
  29. package/dist/ai-server.d.ts +80 -36
  30. package/dist/ai-server.d.ts.map +1 -1
  31. package/dist/ai-server.js +432 -258
  32. package/dist/ai-server.js.map +1 -1
  33. package/dist/{ai-CrEf6p_W.d.ts → ai-vpGtcaCB.d.ts} +114 -100
  34. package/dist/ai-vpGtcaCB.d.ts.map +1 -0
  35. package/dist/ai.d.ts +2 -2
  36. package/dist/ai.js +2 -2
  37. package/dist/{client-BYzHjkwU.js → client-COaiaDub.js} +447 -80
  38. package/dist/client-COaiaDub.js.map +1 -0
  39. package/dist/{client-CzyacQpJ.d.ts → client-qRnRUzBS.d.ts} +14 -13
  40. package/dist/client-qRnRUzBS.d.ts.map +1 -0
  41. package/dist/client.d.ts +4 -2
  42. package/dist/client.js +1 -1
  43. package/dist/{contract-CKRg_E4q.js → contract-BYKASlCV.js} +12 -21
  44. package/dist/contract-BYKASlCV.js.map +1 -0
  45. package/dist/{idempotent-replay-DVOlyYbx.js → idempotent-replay-JdgMQRRm.js} +2 -2
  46. package/dist/{idempotent-replay-DVOlyYbx.js.map → idempotent-replay-JdgMQRRm.js.map} +1 -1
  47. package/dist/index.d.ts +6 -5
  48. package/dist/index.d.ts.map +1 -1
  49. package/dist/index.js +4 -3
  50. package/dist/{errors-DvhSXnxk.d.ts → initial-snapshot-BrLso5x_.d.ts} +7 -3
  51. package/dist/initial-snapshot-BrLso5x_.d.ts.map +1 -0
  52. package/dist/{internal-Dq2qYxou.js → internal-CbVvEwad.js} +23 -2
  53. package/dist/internal-CbVvEwad.js.map +1 -0
  54. package/dist/otel.d.ts +1 -1
  55. package/dist/react-BbJ-g6Uu.d.ts +86 -0
  56. package/dist/react-BbJ-g6Uu.d.ts.map +1 -0
  57. package/dist/react.d.ts +2 -122
  58. package/dist/react.js +107 -100
  59. package/dist/react.js.map +1 -1
  60. package/dist/reducer-BAXc8JyF.js +87 -0
  61. package/dist/reducer-BAXc8JyF.js.map +1 -0
  62. package/dist/{reducer-otzHjuJj.d.ts → reducer-BYu6DT7h.d.ts} +45 -29
  63. package/dist/reducer-BYu6DT7h.d.ts.map +1 -0
  64. package/dist/reducer-runtime-DQwz_J59.js +28 -0
  65. package/dist/reducer-runtime-DQwz_J59.js.map +1 -0
  66. package/dist/scheduler-qstash.d.ts +3 -3
  67. package/dist/scheduler-qstash.d.ts.map +1 -1
  68. package/dist/scheduler-qstash.js +33 -10
  69. package/dist/scheduler-qstash.js.map +1 -1
  70. package/dist/{scheduler-task-BpzhPnRS.js → scheduler-task-X1SToDjP.js} +18 -3
  71. package/dist/scheduler-task-X1SToDjP.js.map +1 -0
  72. package/dist/scheduler-vercel.d.ts +4 -9
  73. package/dist/scheduler-vercel.d.ts.map +1 -1
  74. package/dist/scheduler-vercel.js +25 -44
  75. package/dist/scheduler-vercel.js.map +1 -1
  76. package/dist/{server-DbD7IJVK.d.ts → server-CGM8WzRG.d.ts} +87 -26
  77. package/dist/server-CGM8WzRG.d.ts.map +1 -0
  78. package/dist/{server-BD5ckxZb.js → server-Rj64fNvX.js} +854 -202
  79. package/dist/server-Rj64fNvX.js.map +1 -0
  80. package/dist/server.d.ts +4 -4
  81. package/dist/server.js +2 -2
  82. package/dist/{errors-DCk6ch5n.js → session-clear-DGI4jTQ5.js} +17 -2
  83. package/dist/session-clear-DGI4jTQ5.js.map +1 -0
  84. package/dist/session-clear-Dshpuhff.d.ts +12 -0
  85. package/dist/session-clear-Dshpuhff.d.ts.map +1 -0
  86. package/dist/{store-DyZM6fS5.d.ts → store-Gpkq7WBo.d.ts} +44 -3
  87. package/dist/store-Gpkq7WBo.d.ts.map +1 -0
  88. package/dist/store-N8PXxDAS.js.map +1 -1
  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 +182 -69
  92. package/dist/store-memory.js.map +1 -1
  93. package/dist/{store-polling-CmxUbV93.js → store-polling-4_y8lETm.js} +21 -4
  94. package/dist/store-polling-4_y8lETm.js.map +1 -0
  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 +399 -108
  98. package/dist/store-postgres.js.map +1 -1
  99. package/dist/{store-presence-polling-C7-XZyW9.js → store-presence-polling-Q1C-k2dd.js} +3 -3
  100. package/dist/{store-presence-polling-C7-XZyW9.js.map → store-presence-polling-Q1C-k2dd.js.map} +1 -1
  101. package/dist/store-redis-http.d.ts +1 -1
  102. package/dist/store-redis-http.d.ts.map +1 -1
  103. package/dist/store-redis-http.js +293 -11
  104. package/dist/store-redis-http.js.map +1 -1
  105. package/dist/store-redis-notify-tqWNnMRu.js +2374 -0
  106. package/dist/store-redis-notify-tqWNnMRu.js.map +1 -0
  107. package/dist/store-redis.d.ts +1 -1
  108. package/dist/store-redis.d.ts.map +1 -1
  109. package/dist/store-redis.js +11 -11
  110. package/dist/store-redis.js.map +1 -1
  111. package/dist/store-sqlite.d.ts +1 -1
  112. package/dist/store-sqlite.d.ts.map +1 -1
  113. package/dist/store-sqlite.js +166 -32
  114. package/dist/store-sqlite.js.map +1 -1
  115. package/dist/stream-buffer-Cx7AAPGq.js +23 -0
  116. package/dist/stream-buffer-Cx7AAPGq.js.map +1 -0
  117. package/dist/{telemetry-B5jzpy6y.d.ts → telemetry-cZH-cibj.d.ts} +2 -2
  118. package/dist/telemetry-cZH-cibj.d.ts.map +1 -0
  119. package/dist/{wire-BO5wWCb1.js → wire-DjfHOvdW.js} +132 -10
  120. package/dist/wire-DjfHOvdW.js.map +1 -0
  121. package/docs/01-quickstart.mdx +2 -2
  122. package/docs/actors/01-introduction.mdx +4 -0
  123. package/docs/actors/04-routes.mdx +14 -1
  124. package/docs/concepts/01-contracts.mdx +95 -0
  125. package/docs/concepts/04-state.mdx +3 -3
  126. package/docs/guides/01-timers.mdx +22 -14
  127. package/docs/guides/02-cancellation.mdx +1 -1
  128. package/docs/guides/03-react.mdx +268 -328
  129. package/docs/guides/04-local-first.mdx +32 -73
  130. package/docs/guides/05-production.mdx +40 -27
  131. package/docs/guides/06-ai-agents.mdx +671 -581
  132. package/docs/guides/09-presence.mdx +3 -7
  133. package/docs/guides/10-transports.mdx +5 -5
  134. package/docs/index.mdx +10 -18
  135. package/docs/reference/01-api.mdx +656 -194
  136. package/docs/reference/02-errors.mdx +2 -1
  137. package/examples/playground/DEPLOY.md +52 -6
  138. package/examples/playground/app/a0/[appId]/loading.tsx +16 -0
  139. package/examples/playground/app/a0/[appId]/page.tsx +48 -0
  140. package/examples/playground/app/a0/a0.css +818 -0
  141. package/examples/playground/app/a0/app-list.tsx +211 -0
  142. package/examples/playground/app/a0/app-view.tsx +55 -0
  143. package/examples/playground/app/a0/apps/[appId]/preview/route.ts +27 -0
  144. package/examples/playground/app/a0/apps/[appId]/route.ts +23 -0
  145. package/examples/playground/app/a0/apps.test.ts +116 -0
  146. package/examples/playground/app/a0/apps.ts +87 -0
  147. package/examples/playground/app/a0/chat.tsx +364 -0
  148. package/examples/playground/app/a0/columns.tsx +109 -0
  149. package/examples/playground/app/a0/context-menu.tsx +116 -0
  150. package/examples/playground/app/a0/cookies.ts +46 -0
  151. package/examples/playground/app/a0/crash-on-request.ts +72 -0
  152. package/examples/playground/app/a0/crashes/route.ts +3 -0
  153. package/examples/playground/app/a0/crashes.test.ts +33 -0
  154. package/examples/playground/app/a0/crashes.ts +28 -0
  155. package/examples/playground/app/a0/devtools.tsx +164 -0
  156. package/examples/playground/app/a0/events/route.ts +6 -0
  157. package/examples/playground/app/a0/flags.ts +30 -0
  158. package/examples/playground/app/a0/layout.tsx +38 -0
  159. package/examples/playground/app/a0/list/route.ts +10 -0
  160. package/examples/playground/app/a0/model.test.ts +56 -0
  161. package/examples/playground/app/a0/model.ts +58 -0
  162. package/examples/playground/app/a0/page.tsx +14 -0
  163. package/examples/playground/app/a0/preview-pane.tsx +145 -0
  164. package/examples/playground/app/a0/preview.test.ts +53 -0
  165. package/examples/playground/app/a0/preview.ts +26 -0
  166. package/examples/playground/app/a0/sandbox-log.ts +69 -0
  167. package/examples/playground/app/a0/sandbox.ts +191 -0
  168. package/examples/playground/app/a0/sandboxes/scheduler/route.ts +6 -0
  169. package/examples/playground/app/a0/scheduler/route.ts +7 -0
  170. package/examples/playground/app/a0/server.ts +158 -0
  171. package/examples/playground/app/a0/session.ts +47 -0
  172. package/examples/playground/app/a0/shell.tsx +150 -0
  173. package/examples/playground/app/a0/short/events/route.ts +9 -0
  174. package/examples/playground/app/a0/splitter.tsx +120 -0
  175. package/examples/playground/app/agent/[agentId]/agent-client.tsx +144 -174
  176. package/examples/playground/app/agent/[agentId]/command-receipt.test.tsx +65 -0
  177. package/examples/playground/app/agent/[agentId]/command-receipt.tsx +27 -0
  178. package/examples/playground/app/agent/[agentId]/compaction-event.tsx +25 -24
  179. package/examples/playground/app/agent/[agentId]/compaction-panel.test.tsx +119 -0
  180. package/examples/playground/app/agent/[agentId]/compaction-panel.tsx +208 -165
  181. package/examples/playground/app/agent/[agentId]/page.tsx +2 -16
  182. package/examples/playground/app/agent/events/route.ts +5 -2
  183. package/examples/playground/app/agent/ingress.test.ts +98 -0
  184. package/examples/playground/app/agent/model-input.test.tsx +58 -0
  185. package/examples/playground/app/agent/model-input.tsx +115 -0
  186. package/examples/playground/app/agent/model.test.ts +73 -0
  187. package/examples/playground/app/agent/model.ts +58 -26
  188. package/examples/playground/app/agent/new-agent-session.tsx +37 -19
  189. package/examples/playground/app/agent/page.tsx +4 -4
  190. package/examples/playground/app/agent/server.ts +15 -14
  191. package/examples/playground/app/agent/session.ts +5 -6
  192. package/examples/playground/app/agent/telemetry.test.ts +168 -0
  193. package/examples/playground/app/agent/telemetry.ts +30 -0
  194. package/examples/playground/app/agent/trace.test.ts +23 -0
  195. package/examples/playground/app/agent/trace.ts +18 -0
  196. package/examples/playground/app/canvas/[canvasId]/canvas-client.tsx +11 -11
  197. package/examples/playground/app/canvas/[canvasId]/page.tsx +1 -2
  198. package/examples/playground/app/canvas/[canvasId]/session.ts +2 -2
  199. package/examples/playground/app/canvas/model.ts +53 -53
  200. package/examples/playground/app/chat/[chatId]/agent-state.test.ts +176 -0
  201. package/examples/playground/app/chat/[chatId]/agent-stream-drawer.tsx +16 -1
  202. package/examples/playground/app/chat/[chatId]/chat-client.tsx +55 -77
  203. package/examples/playground/app/chat/[chatId]/chat-view.ts +37 -3
  204. package/examples/playground/app/chat/[chatId]/page.tsx +7 -15
  205. package/examples/playground/app/chat/[chatId]/session.ts +2 -19
  206. package/examples/playground/app/chat/agent/route.ts +9 -0
  207. package/examples/playground/app/chat/model.test.ts +46 -119
  208. package/examples/playground/app/chat/model.ts +82 -128
  209. package/examples/playground/app/chat/scheduler/route.ts +2 -2
  210. package/examples/playground/app/chat/server.test.ts +108 -0
  211. package/examples/playground/app/chat/server.ts +69 -73
  212. package/examples/playground/app/components/activity-feed.tsx +30 -1
  213. package/examples/playground/app/components/activity-history.test.tsx +35 -0
  214. package/examples/playground/app/components/demo-icons.tsx +89 -0
  215. package/examples/playground/app/components/demos.tsx +33 -0
  216. package/examples/playground/app/components/fast-link.test.tsx +58 -0
  217. package/examples/playground/app/components/fast-link.tsx +55 -0
  218. package/examples/playground/app/components/site-header.tsx +96 -0
  219. package/examples/playground/app/counter/counter-client.tsx +3 -3
  220. package/examples/playground/app/counter/model.test.ts +3 -3
  221. package/examples/playground/app/counter/model.ts +11 -21
  222. package/examples/playground/app/counter/page.tsx +2 -15
  223. package/examples/playground/app/counter/session.ts +2 -2
  224. package/examples/playground/app/documents/[documentId]/code-editor.tsx +12 -1
  225. package/examples/playground/app/documents/[documentId]/document-client.tsx +10 -11
  226. package/examples/playground/app/documents/[documentId]/page.tsx +1 -2
  227. package/examples/playground/app/documents/[documentId]/session.ts +2 -2
  228. package/examples/playground/app/documents/model.ts +27 -27
  229. package/examples/playground/app/globals.css +420 -207
  230. package/examples/playground/app/initial-state.test.tsx +54 -0
  231. package/examples/playground/app/layout.tsx +7 -23
  232. package/examples/playground/app/orders/[orderId]/page.tsx +1 -2
  233. package/examples/playground/app/orders/[orderId]/session.ts +2 -2
  234. package/examples/playground/app/orders/model.ts +49 -49
  235. package/examples/playground/app/page.tsx +13 -102
  236. package/examples/playground/app/recovery/[recoveryId]/page.tsx +1 -2
  237. package/examples/playground/app/recovery/[recoveryId]/session.ts +2 -2
  238. package/examples/playground/app/recovery/model.ts +30 -30
  239. package/examples/playground/instrumentation.ts +5 -0
  240. package/examples/playground/package.json +10 -2
  241. package/examples/playground/scripts/a0-template.ts +77 -0
  242. package/examples/playground/vercel.json +18 -0
  243. package/package.json +3 -1
  244. package/src/actor-client.ts +16 -4
  245. package/src/actor-react.ts +25 -11
  246. package/src/actor-shared.ts +21 -6
  247. package/src/actor.ts +76 -31
  248. package/src/ai-agent-actions.ts +219 -0
  249. package/src/ai-agent.ts +2343 -0
  250. package/src/ai-app-state.ts +91 -0
  251. package/src/ai-client-state.ts +6 -3
  252. package/src/ai-client.ts +62 -0
  253. package/src/ai-context-schema.ts +25 -5
  254. package/src/ai-context.ts +90 -40
  255. package/src/ai-control-server.ts +92 -72
  256. package/src/ai-coordinator.ts +23 -22
  257. package/src/ai-message-base.ts +209 -0
  258. package/src/ai-message-projection.ts +262 -64
  259. package/src/ai-progress-batches.ts +26 -0
  260. package/src/ai-projector.ts +95 -14
  261. package/src/ai-react.ts +146 -0
  262. package/src/ai-server.browser.ts +2 -2
  263. package/src/ai-server.ts +20 -1964
  264. package/src/ai-stored-state.ts +48 -163
  265. package/src/ai.ts +339 -155
  266. package/src/bootstrap-stream.ts +72 -0
  267. package/src/bounded-cache.ts +55 -0
  268. package/src/client-render-session.ts +49 -0
  269. package/src/client.ts +688 -151
  270. package/src/contract.ts +92 -42
  271. package/src/deterministic-id.ts +37 -0
  272. package/src/equal-json.ts +32 -0
  273. package/src/errors.ts +13 -1
  274. package/src/index.ts +5 -1
  275. package/src/initial-snapshot.ts +39 -0
  276. package/src/internal.ts +45 -0
  277. package/src/postgres-notification-scope.ts +14 -3
  278. package/src/react.ts +171 -206
  279. package/src/redis-http-reply.ts +256 -0
  280. package/src/redis-http-stream.ts +73 -0
  281. package/src/redis-http-subscriptions.ts +13 -5
  282. package/src/reducer-runtime.ts +52 -0
  283. package/src/reducer.ts +92 -10
  284. package/src/scheduler-qstash.ts +49 -20
  285. package/src/scheduler-task.ts +34 -2
  286. package/src/scheduler-vercel.ts +32 -84
  287. package/src/server-fetch.ts +139 -35
  288. package/src/server.ts +957 -237
  289. package/src/session-clear.ts +23 -0
  290. package/src/session-socket.ts +154 -49
  291. package/src/sse.ts +71 -4
  292. package/src/store-memory.ts +220 -78
  293. package/src/store-polling.ts +18 -4
  294. package/src/store-postgres.ts +461 -114
  295. package/src/store-redis-core.ts +1260 -434
  296. package/src/store-redis-http.ts +18 -2
  297. package/src/store-redis-notify.ts +260 -85
  298. package/src/store-redis.ts +19 -9
  299. package/src/store-sqlite.ts +259 -45
  300. package/src/store.ts +46 -1
  301. package/src/stream-buffer.ts +43 -0
  302. package/src/telemetry.ts +13 -1
  303. package/src/wire.ts +241 -13
  304. package/dist/actor-ohPC-81x.d.ts.map +0 -1
  305. package/dist/actor-shared-B5tJfzt-.js.map +0 -1
  306. package/dist/ai-CrEf6p_W.d.ts.map +0 -1
  307. package/dist/ai-DgOBltJ_.js.map +0 -1
  308. package/dist/client-BYzHjkwU.js.map +0 -1
  309. package/dist/client-CzyacQpJ.d.ts.map +0 -1
  310. package/dist/contract-CKRg_E4q.js.map +0 -1
  311. package/dist/errors-DCk6ch5n.js.map +0 -1
  312. package/dist/errors-DvhSXnxk.d.ts.map +0 -1
  313. package/dist/internal-Dq2qYxou.js.map +0 -1
  314. package/dist/react.d.ts.map +0 -1
  315. package/dist/reducer-DEMjEY_O.js +0 -29
  316. package/dist/reducer-DEMjEY_O.js.map +0 -1
  317. package/dist/reducer-otzHjuJj.d.ts.map +0 -1
  318. package/dist/scheduler-task-BpzhPnRS.js.map +0 -1
  319. package/dist/server-BD5ckxZb.js.map +0 -1
  320. package/dist/server-DbD7IJVK.d.ts.map +0 -1
  321. package/dist/store-DyZM6fS5.d.ts.map +0 -1
  322. package/dist/store-polling-CmxUbV93.js.map +0 -1
  323. package/dist/store-redis-notify-BVLUfI3j.js +0 -1583
  324. package/dist/store-redis-notify-BVLUfI3j.js.map +0 -1
  325. package/dist/telemetry-B5jzpy6y.d.ts.map +0 -1
  326. package/dist/validate-XKT4FSNn.js +0 -30
  327. package/dist/validate-XKT4FSNn.js.map +0 -1
  328. package/dist/wire-BO5wWCb1.js.map +0 -1
  329. package/examples/playground/app/agent/[agentId]/compaction/route.ts +0 -14
  330. package/examples/playground/app/agent/compaction-settings.test.ts +0 -144
  331. package/examples/playground/app/agent/compaction-settings.ts +0 -49
  332. package/examples/playground/app/agent/compaction-timeline.test.ts +0 -377
  333. package/examples/playground/app/agent/compaction-timeline.ts +0 -208
package/src/ai-server.ts CHANGED
@@ -1,1970 +1,26 @@
1
1
  /**
2
- * experimental-a2/ai/server — the server-only implementation of an experimental-a2/ai definition.
3
- *
4
- * `createHandlers()` exposes the ordinary A2 handler table;
5
- * `createAgentServer()` is its batteries-included assembly with createServer.
2
+ * experimental-a2/ai/server — `agent()`: the experimental-a2/ai protocol,
3
+ * its model and tool handlers, and A2's store, scheduler, and telemetry,
4
+ * assembled as one ordinary A2 server.
6
5
  */
7
6
 
8
- // oxlint-disable no-await-in-loop -- stream chunks and durable appends are
9
- // ordered; parallel consumption would corrupt progress sequence and state.
10
-
11
- import { createControlRuntime, ControlCancelled } from './ai-control-server.ts'
12
- import {
13
- createContextReducer,
14
- contextForAttempt,
15
- generationBase,
16
- type AIContextState,
17
- type AIModelContext,
18
- } from './ai-context.ts'
19
- import type { Reducer } from './reducer.ts'
20
- import { aiStateReducer } from './ai-stored-state.ts'
21
- import { aiEventId, AI_EVENT_ID_PREFIX } from './ai-id.ts'
22
- import type { ControlCommit, ControlCommand } from './ai-control.ts'
23
- import { AsyncLocalStorage } from 'node:async_hooks'
24
- import { createHash } from 'node:crypto'
25
- import { convertToModelMessages, isToolUIPart } from 'ai'
26
- import type {
27
- FinishReason,
7
+ export { agent, runWithHandlerContext } from './ai-agent.ts'
8
+ export type {
9
+ Agent,
10
+ AgentFromContractOptions,
11
+ AgentGenerate,
12
+ AgentGenerateContext,
13
+ AgentGenerationSettings,
14
+ AgentMessageMetadata,
15
+ AgentModelOptions,
16
+ AgentOptions,
17
+ AgentResolverContext,
18
+ AutomaticCompaction,
19
+ CompactionOptions,
20
+ CompactionPolicy,
28
21
  Instructions,
29
22
  LanguageModel,
30
- LanguageModelUsage,
31
- ModelMessage,
32
- TextStreamPart,
23
+ ModelSettings,
24
+ Resolvable,
33
25
  ToolSet,
34
- UIMessage,
35
- UIMessageChunk,
36
- } from 'ai'
37
- import { generateAISDKStep, type AISDKStepSettings } from './ai-sdk-step.ts'
38
- import { gatewayModelId, readModelLimits } from './ai-model-metadata.ts'
39
- import { estimateInputTokens, measuredInputTokens } from './ai-input-tokens.ts'
40
- import type {
41
- AIEventDefs,
42
- AgentDefinition,
43
- AgentToolContext,
44
- AgentToolEventDefs,
45
- ApprovalRequestedPayload,
46
- CompactionCompletedPayload,
47
- CompactionRequestedPayload,
48
- GenerationCompletedPayload,
49
- GenerationFailedPayload,
50
- GenerationProgressPayload,
51
- GenerationStartedPayload,
52
- MessageCompletedPayload,
53
- ToolCalledPayload,
54
- ToolResultPayload,
55
- } from './ai.ts'
56
- import type { AICoordinatorState } from './ai-coordinator.ts'
57
- import type { AppendInput, ContractEvent, EventDefs } from './contract.ts'
58
- import { A2Error } from './errors.ts'
59
- import {
60
- consumeSchedulerSendFailure,
61
- installAmbientToolScopeStorage,
62
- type AmbientToolScope,
63
- } from './internal.ts'
64
- import {
65
- createServer,
66
- type A2Server,
67
- type HandlerContext,
68
- type ServerOptions,
69
- } from './server.ts'
70
- import {
71
- setServerFetchHooks,
72
- type ServerIngressContext,
73
- } from './server-fetch.ts'
74
-
75
- function validateAgentIngress(context: ServerIngressContext): void {
76
- const rejected = context.events.find((event) => {
77
- if (event.type !== 'ai.control.requested') return true
78
- const command = event.payload as Record<string, unknown>
79
- if (
80
- command['action'] === 'request-input' ||
81
- command['action'] === 'tool-result'
82
- )
83
- return true
84
- if (
85
- command['action'] === 'send' ||
86
- command['action'] === 'edit' ||
87
- command['action'] === 'steer'
88
- ) {
89
- return (
90
- (command['message'] as { role?: unknown } | undefined)?.role !== 'user'
91
- )
92
- }
93
- return false
94
- })
95
- if (rejected !== undefined) {
96
- throw new A2Error(
97
- 'INVALID_PAYLOAD',
98
- `event '${rejected.type}' is server-only for an AI agent`,
99
- )
100
- }
101
- }
102
-
103
- type AgentGenerationProgress = {
104
- type: 'progress'
105
- chunks: UIMessageChunk[]
106
- }
107
-
108
- type AgentGenerationFinish = {
109
- type: 'finish'
110
- finishReason?: FinishReason
111
- usage?: LanguageModelUsage
112
- }
113
-
114
- type AgentGenerationUpdate = AgentGenerationProgress | AgentGenerationFinish
115
-
116
- export type AgentGenerateContext<
117
- M extends UIMessage,
118
- D extends AIEventDefs<M> & EventDefs,
119
- T extends ToolSet,
120
- > = {
121
- request: ContractEvent<D, 'ai.generation.requested'>
122
- requestId: string
123
- generationId: string
124
- responseMessageId: string
125
- messages: M[]
126
- modelMessages: ModelMessage[]
127
- session: Pick<HandlerContext<D>['session'], 'state'>
128
- signal: AbortSignal
129
- model: LanguageModel
130
- tools: T
131
- instructions?: Instructions
132
- generation: AgentGenerationSettings<T>
133
- }
134
-
135
- export type AgentGenerate<
136
- M extends UIMessage,
137
- D extends AIEventDefs<M> & EventDefs,
138
- T extends ToolSet,
139
- > = (
140
- context: AgentGenerateContext<M, D, T>,
141
- ) => ReadableStream<UIMessageChunk> | Promise<ReadableStream<UIMessageChunk>>
142
-
143
- type UIMessageMetadata<M extends UIMessage> =
144
- M extends UIMessage<infer Metadata> ? Metadata : unknown
145
-
146
- export type AgentMessageMetadata<
147
- M extends UIMessage,
148
- D extends AIEventDefs<M> & EventDefs,
149
- T extends ToolSet,
150
- > = (
151
- context: AgentGenerateContext<M, D, T> & { part: TextStreamPart<T> },
152
- ) => UIMessageMetadata<M> | undefined
153
-
154
- export type AgentResolverContext<
155
- M extends UIMessage,
156
- D extends AIEventDefs<M> & EventDefs,
157
- > = {
158
- event: ContractEvent<D, 'ai.generation.requested'>
159
- messages: M[]
160
- session: Pick<HandlerContext<D>['session'], 'state'>
161
- signal: AbortSignal
162
- }
163
-
164
- export type Resolvable<T, C> = T | ((context: C) => T | Promise<T>)
165
-
166
- export type CompactionPolicy<
167
- M extends UIMessage,
168
- D extends AIEventDefs<M> & EventDefs,
169
- > = {
170
- shouldCompact(
171
- context: AgentResolverContext<M, D> & {
172
- messages: M[]
173
- modelMessages: ModelMessage[]
174
- },
175
- ): boolean | Promise<boolean>
176
- compact(
177
- context: AgentResolverContext<M, D> & {
178
- messages: M[]
179
- modelMessages: ModelMessage[]
180
- },
181
- ): M[] | Promise<M[]>
182
- }
183
-
184
- export type AutomaticCompaction = {
185
- thresholdTokens?: number
186
- instructions?: string
187
- }
188
-
189
- export type CompactionOptions<
190
- M extends UIMessage,
191
- D extends AIEventDefs<M> & EventDefs,
192
- > = false | AutomaticCompaction | CompactionPolicy<M, D>
193
-
194
- export type AgentGenerationSettings<T extends ToolSet> = AISDKStepSettings<T>
195
-
196
- export type CreateHandlersOptions<
197
- M extends UIMessage,
198
- D extends AIEventDefs<M> & EventDefs,
199
- T extends ToolSet = ToolSet,
200
- > = {
201
- agent: AgentDefinition<M, D>
202
- /** AI SDK model string or provider model, optionally resolved per generation. */
203
- model: Resolvable<LanguageModel, AgentResolverContext<M, D>>
204
- tools?: T
205
- instructions?: Resolvable<
206
- Instructions | undefined,
207
- AgentResolverContext<M, D>
208
- >
209
- /** Settings for each one-step AI SDK streamText call. */
210
- generation?: AgentGenerationSettings<T>
211
- /**
212
- * Maximum model steps in one assistant response. Defaults to Infinity.
213
- */
214
- maxSteps?: number
215
- /** Map default AI SDK stream parts to typed assistant message metadata. */
216
- messageMetadata?: AgentMessageMetadata<M, D, T>
217
- /**
218
- * Replace model generation with an AI SDK UI message chunk stream.
219
- * The custom stream owns its metadata chunks.
220
- */
221
- generate?: AgentGenerate<M, D, T>
222
- compaction?: Resolvable<CompactionOptions<M, D>, AgentResolverContext<M, D>>
223
- /** Progress is durably flushed at either limit, whichever is reached first. */
224
- progress?: { maxChunks?: number; maxDelayMs?: number }
225
- }
226
-
227
- export type CreateAgentServerOptions<
228
- M extends UIMessage,
229
- D extends AIEventDefs<M> & EventDefs,
230
- T extends ToolSet = ToolSet,
231
- > = CreateHandlersOptions<M, D, T> &
232
- Omit<ServerOptions<D>, 'contract' | 'handlers'> & {
233
- handlers?: ServerOptions<D>['handlers']
234
- }
235
-
236
- const checkAbort = (signal: AbortSignal): boolean => {
237
- if (!signal.aborted) return false
238
- const reason: unknown = signal.reason
239
- if (
240
- reason instanceof A2Error &&
241
- (reason.code === 'CLAIM_EXPIRED' || reason.code === 'SUPERSEDED_ATTEMPT')
242
- ) {
243
- throw reason
244
- }
245
- return true
246
- }
247
-
248
- const resolve = async <T, C extends { signal: AbortSignal }>(
249
- value: Resolvable<T, C>,
250
- context: C,
251
- ): Promise<T> => {
252
- let result: T
253
- try {
254
- result =
255
- typeof value === 'function'
256
- ? await (value as (context: C) => T | Promise<T>)(context)
257
- : value
258
- } catch (error) {
259
- checkAbort(context.signal)
260
- throw error
261
- }
262
- checkAbort(context.signal)
263
- return result
264
- }
265
-
266
- const modelName = (model: LanguageModel): string => {
267
- if (typeof model === 'string') return model
268
- if ('modelId' in model && typeof model.modelId === 'string') {
269
- return model.modelId
270
- }
271
- return 'custom'
272
- }
273
-
274
- const errorMessage = (error: unknown): string =>
275
- error instanceof Error ? error.message : String(error)
276
-
277
- const toolScopeStorage = installAmbientToolScopeStorage(
278
- () => new AsyncLocalStorage<AmbientToolScope>(),
279
- )
280
-
281
- /** Run `fn` with `ctx` as the ambient tool handler context, for direct tool tests. */
282
- export function runWithHandlerContext<D extends AgentToolEventDefs, R>(
283
- ctx: AgentToolContext<D>,
284
- fn: () => R,
285
- ): R {
286
- return toolScopeStorage.run({ context: ctx }, fn)
287
- }
288
-
289
- const promptCacheKey = (sessionId: string, generationId: string): string =>
290
- `${sessionId}\u001f${generationId}`
291
-
292
- const asyncIterator = (value: unknown): AsyncIterator<unknown> | undefined => {
293
- if (typeof value !== 'object' || value === null) return undefined
294
- const factory = Reflect.get(value, Symbol.asyncIterator)
295
- if (typeof factory !== 'function') return undefined
296
- return (factory as (this: object) => AsyncIterator<unknown>).call(value)
297
- }
298
-
299
- const isBoundaryChunk = (chunk: UIMessageChunk | undefined): boolean =>
300
- chunk !== undefined &&
301
- (chunk.type === 'text-end' ||
302
- chunk.type === 'reasoning-end' ||
303
- chunk.type === 'tool-input-available' ||
304
- chunk.type === 'tool-input-error' ||
305
- chunk.type === 'tool-output-available' ||
306
- chunk.type === 'tool-output-error' ||
307
- chunk.type === 'tool-output-denied' ||
308
- chunk.type === 'tool-approval-request' ||
309
- chunk.type === 'finish' ||
310
- chunk.type === 'abort' ||
311
- chunk.type === 'error')
312
-
313
- type StreamRead<T> = Awaited<ReturnType<ReadableStreamDefaultReader<T>['read']>>
314
-
315
- const nextOrTimer = async <T>(
316
- next: Promise<StreamRead<T>>,
317
- delayMs: number,
318
- ): Promise<{ type: 'next'; result: StreamRead<T> } | { type: 'timer' }> => {
319
- let timer: ReturnType<typeof setTimeout> | undefined
320
- try {
321
- return await Promise.race([
322
- next.then((result) => ({ type: 'next' as const, result })),
323
- new Promise<{ type: 'timer' }>((finishTimer) => {
324
- timer = setTimeout(() => finishTimer({ type: 'timer' }), delayMs)
325
- ;(timer as { unref?: () => void }).unref?.()
326
- }),
327
- ])
328
- } finally {
329
- if (timer !== undefined) clearTimeout(timer)
330
- }
331
- }
332
-
333
- type GenerationCompletion = {
334
- finishReason?: FinishReason
335
- usage?: LanguageModelUsage
336
- }
337
-
338
- type GenerationSource = {
339
- stream: ReadableStream<UIMessageChunk>
340
- completion?: () => GenerationCompletion
341
- }
342
-
343
- async function generateWithAISDK<
344
- M extends UIMessage,
345
- D extends AIEventDefs<M> & EventDefs,
346
- T extends ToolSet,
347
- >(options: {
348
- agentName: string
349
- context: AgentGenerateContext<M, D, T>
350
- messageMetadata: AgentMessageMetadata<M, D, T> | undefined
351
- }): Promise<GenerationSource> {
352
- const { agentName, context, messageMetadata } = options
353
- const headers = context.generation.headers ?? {}
354
- const generation =
355
- gatewayModelId(context.model) === undefined
356
- ? context.generation
357
- : {
358
- ...context.generation,
359
- headers: Object.keys(headers).some(
360
- (key) => key.toLowerCase() === 'x-session-affinity',
361
- )
362
- ? headers
363
- : {
364
- ...headers,
365
- 'x-session-affinity': createHash('sha256')
366
- .update(
367
- JSON.stringify([agentName, context.request.sessionId]),
368
- )
369
- .digest('hex'),
370
- },
371
- providerOptions: {
372
- ...context.generation.providerOptions,
373
- gateway: {
374
- caching: 'auto',
375
- ...context.generation.providerOptions?.['gateway'],
376
- },
377
- },
378
- }
379
- const step = await generateAISDKStep<M, T>({
380
- model: context.model,
381
- tools: context.tools,
382
- messages: context.messages,
383
- modelMessages: context.modelMessages,
384
- responseMessageId: context.responseMessageId,
385
- abortSignal: context.signal,
386
- ...(context.instructions === undefined
387
- ? {}
388
- : { instructions: context.instructions }),
389
- settings: generation,
390
- ...(messageMetadata === undefined
391
- ? {}
392
- : {
393
- messageMetadata: ({ part }) =>
394
- messageMetadata({ ...context, generation, part }),
395
- }),
396
- })
397
- return {
398
- stream: step.stream as ReadableStream<UIMessageChunk>,
399
- completion: step.completion,
400
- }
401
- }
402
-
403
- async function* consumeGeneration(options: {
404
- source: GenerationSource
405
- progress?: { maxChunks?: number; maxDelayMs?: number }
406
- }): AsyncGenerator<AgentGenerationUpdate> {
407
- const pending: UIMessageChunk[] = []
408
- let streamedFinishReason: FinishReason | undefined
409
- let streamedError: string | undefined
410
- const maxChunks = options.progress?.maxChunks ?? 16
411
- const maxDelayMs = options.progress?.maxDelayMs ?? 30
412
- let lastFlush = Date.now()
413
- const reader = options.source.stream.getReader()
414
- let pendingChunk = reader.read()
415
- try {
416
- for (;;) {
417
- let result: StreamRead<UIMessageChunk>
418
- if (pending.length > 0) {
419
- const remaining = Math.max(0, maxDelayMs - (Date.now() - lastFlush))
420
- const outcome = await nextOrTimer(pendingChunk, remaining)
421
- if (outcome.type === 'timer') {
422
- yield { type: 'progress', chunks: pending.splice(0) }
423
- lastFlush = Date.now()
424
- continue
425
- }
426
- result = outcome.result
427
- } else {
428
- result = await pendingChunk
429
- }
430
- if (result.done) break
431
- const chunk = result.value
432
- pending.push(chunk)
433
- if (chunk.type === 'finish') streamedFinishReason = chunk.finishReason
434
- if (chunk.type === 'error') streamedError = chunk.errorText
435
- pendingChunk = reader.read()
436
-
437
- if (pending.length >= maxChunks || isBoundaryChunk(pending.at(-1))) {
438
- yield { type: 'progress', chunks: pending.splice(0) }
439
- lastFlush = Date.now()
440
- }
441
- }
442
- } catch (error) {
443
- if (pending.length > 0)
444
- yield { type: 'progress', chunks: pending.splice(0) }
445
- throw error
446
- } finally {
447
- void pendingChunk.catch(() => {})
448
- try {
449
- await reader.cancel()
450
- } catch {
451
- // Cleanup must not replace the error that ended generation.
452
- } finally {
453
- reader.releaseLock()
454
- }
455
- }
456
-
457
- let completion: GenerationCompletion | undefined
458
- try {
459
- completion = options.source.completion?.()
460
- } catch (error) {
461
- if (pending.length > 0) {
462
- yield { type: 'progress', chunks: pending.splice(0) }
463
- }
464
- throw error
465
- }
466
- if (pending.length > 0) {
467
- yield { type: 'progress', chunks: pending.splice(0) }
468
- }
469
- if (streamedError !== undefined) throw new Error(streamedError)
470
- const finishReason = completion?.finishReason ?? streamedFinishReason
471
- yield {
472
- type: 'finish',
473
- ...(finishReason === undefined ? {} : { finishReason }),
474
- ...(completion?.usage === undefined ? {} : { usage: completion.usage }),
475
- }
476
- }
477
-
478
- const foldAIEvents = <
479
- M extends UIMessage,
480
- D extends AIEventDefs<M> & EventDefs,
481
- >(options: {
482
- reducer: Reducer<D, AIContextState<M>>
483
- state: AIContextState<M>
484
- events: ContractEvent<D>[]
485
- }): AIContextState<M> => {
486
- let state = options.state
487
- for (const event of options.events) state = options.reducer.fold(state, event)
488
- return state
489
- }
490
-
491
- const summarize = async <
492
- M extends UIMessage,
493
- D extends AIEventDefs<M> & EventDefs,
494
- T extends ToolSet,
495
- >(options: {
496
- agentName: string
497
- context: AgentGenerateContext<M, D, T>
498
- instructions: string | undefined
499
- }): Promise<{ summary: string; usage?: LanguageModelUsage }> => {
500
- const instruction = [
501
- 'Summarize this conversation so you can continue the task after the earlier context is replaced by your summary.',
502
- 'Preserve the objective, constraints, decisions, exact identifiers, completed actions and their results, unresolved questions, and next steps. Distinguish observations from assumptions.',
503
- 'Respond only with the summary as text. Do not call any tools or continue working on the task.',
504
- options.instructions,
505
- ]
506
- .filter(Boolean)
507
- .join('\n')
508
- const generation = { ...options.context.generation }
509
- for (const key of Object.keys(generation)) {
510
- if (
511
- key.startsWith('on') ||
512
- key.startsWith('experimental_on') ||
513
- [
514
- 'repairToolCall',
515
- 'experimental_repairToolCall',
516
- 'experimental_refineToolInput',
517
- 'experimental_transform',
518
- 'toolApproval',
519
- ].includes(key)
520
- ) {
521
- Reflect.deleteProperty(generation, key)
522
- }
523
- }
524
- const tools = Object.fromEntries(
525
- Object.entries(options.context.tools).map(([name, tool]) => {
526
- const definition = { ...tool, needsApproval: false }
527
- for (const key of [
528
- 'execute',
529
- 'onInputStart',
530
- 'onInputDelta',
531
- 'onInputAvailable',
532
- ]) {
533
- Reflect.deleteProperty(definition, key)
534
- }
535
- return [name, definition]
536
- }),
537
- ) as T
538
- const source = await generateWithAISDK({
539
- agentName: options.agentName,
540
- context: {
541
- ...options.context,
542
- generation,
543
- tools,
544
- modelMessages: [
545
- ...options.context.modelMessages,
546
- { role: 'user', content: instruction },
547
- ],
548
- responseMessageId: await aiEventId(
549
- 'a2.ai.summary',
550
- options.context.generationId,
551
- ),
552
- },
553
- messageMetadata: undefined,
554
- })
555
- let summary = ''
556
- let calledTool = false
557
- let finish: AgentGenerationFinish | undefined
558
- for await (const update of consumeGeneration({ source })) {
559
- options.context.signal.throwIfAborted()
560
- if (update.type === 'finish') {
561
- finish = update
562
- continue
563
- }
564
- for (const chunk of update.chunks) {
565
- if (chunk.type === 'text-delta') summary += chunk.delta
566
- if (chunk.type.startsWith('tool-')) calledTool = true
567
- }
568
- }
569
- if (
570
- calledTool ||
571
- summary.trim().length === 0 ||
572
- finish?.finishReason !== 'stop'
573
- ) {
574
- throw new Error(
575
- 'compaction must produce a complete text summary without calling tools',
576
- )
577
- }
578
- return {
579
- summary,
580
- ...(finish.usage === undefined ? {} : { usage: finish.usage }),
581
- }
582
- }
583
-
584
- const activeCompaction = <M extends UIMessage>(
585
- state: AIModelContext<M>,
586
- ): AIModelContext<M>['compaction'] => {
587
- const compaction = state.compaction
588
- return compaction?.status === 'completed' ? compaction : null
589
- }
590
-
591
- const contextMessages = <M extends UIMessage>({
592
- state,
593
- coordinator,
594
- }: {
595
- state: AIModelContext<M>
596
- coordinator: AICoordinatorState
597
- }): M[] => {
598
- const queued = new Set(coordinator.queued.map((item) => item.messageId))
599
- return state.messages.filter((message) => !queued.has(message.id))
600
- }
601
-
602
- const modelContext = async <M extends UIMessage>(options: {
603
- messages: M[]
604
- state: AIModelContext<M>
605
- tools: ToolSet
606
- }): Promise<ModelMessage[]> => {
607
- const messages = await convertToModelMessages(options.messages, {
608
- tools: options.tools,
609
- })
610
- const summary = activeCompaction(options.state)?.summary
611
- return summary === undefined
612
- ? messages
613
- : [{ role: 'user', content: summary }, ...messages]
614
- }
615
-
616
- type ToolCallClassification = 'automatic' | 'approval' | 'unknown'
617
-
618
- type PendingToolCall = {
619
- call: ToolCalledPayload
620
- classification: ToolCallClassification
621
- }
622
-
623
- const toolCalledEvent = async <D extends EventDefs>(
624
- payload: ToolCalledPayload,
625
- ): Promise<AppendInput<D>> =>
626
- ({
627
- type: 'ai.tool.called',
628
- id: await aiEventId(
629
- 'a2.ai.tool.called',
630
- payload.generationId,
631
- payload.toolCallId,
632
- ),
633
- payload,
634
- }) as AppendInput<D>
635
-
636
- const approvalClassification = <T extends ToolSet>(
637
- tools: T,
638
- generation: AgentGenerationSettings<T>,
639
- toolName: string,
640
- ): ToolCallClassification => {
641
- const configured = generation.toolApproval
642
- if (typeof configured === 'function') return 'unknown'
643
- const policy = configured?.[toolName]
644
- if (typeof policy === 'function') return 'unknown'
645
- if (
646
- policy === 'user-approval' ||
647
- (typeof policy === 'object' && policy?.type === 'user-approval')
648
- ) {
649
- return 'approval'
650
- }
651
- if (
652
- policy === 'denied' ||
653
- (typeof policy === 'object' && policy?.type === 'denied')
654
- ) {
655
- return 'approval'
656
- }
657
- const tool = tools[toolName] as { needsApproval?: unknown } | undefined
658
- if (typeof tool?.needsApproval === 'function') return 'unknown'
659
- return tool?.needsApproval === true ? 'approval' : 'automatic'
660
- }
661
-
662
- const lifecycleEvents = async <
663
- D extends EventDefs,
664
- T extends ToolSet,
665
- >(options: {
666
- requestId: string
667
- messageId: string
668
- generationId: string
669
- sequence: number
670
- chunks: UIMessageChunk[]
671
- pending: PendingToolCall[]
672
- tools: T
673
- generation: AgentGenerationSettings<T>
674
- custom: boolean
675
- }): Promise<{ events: AppendInput<D>[]; pending: PendingToolCall[] }> => {
676
- const result: AppendInput<D>[] = []
677
- const pending = [...options.pending]
678
- const take = (toolCallId: string): PendingToolCall | undefined => {
679
- const index = pending.findIndex(
680
- (candidate) => candidate.call.toolCallId === toolCallId,
681
- )
682
- if (index === -1) return undefined
683
- return pending.splice(index, 1)[0]
684
- }
685
- const flush = async (toolCallId: string): Promise<void> => {
686
- const pendingCall = take(toolCallId)
687
- if (pendingCall) result.push(await toolCalledEvent<D>(pendingCall.call))
688
- }
689
- for (const [chunkIndex, chunk] of options.chunks.entries()) {
690
- const resultId = (): Promise<string> =>
691
- aiEventId(
692
- 'a2.ai.tool.stream-result',
693
- options.generationId,
694
- String(options.sequence),
695
- String(chunkIndex),
696
- )
697
- if (chunk.type === 'tool-input-available') {
698
- const payload: ToolCalledPayload = {
699
- requestId: options.requestId,
700
- messageId: options.messageId,
701
- generationId: options.generationId,
702
- toolCallId: chunk.toolCallId,
703
- toolName: chunk.toolName,
704
- input: chunk.input,
705
- ...(chunk.dynamic === undefined ? {} : { dynamic: chunk.dynamic }),
706
- ...(chunk.providerExecuted === undefined
707
- ? {}
708
- : { providerExecuted: chunk.providerExecuted }),
709
- ...((
710
- options.tools[chunk.toolName] as
711
- { supportsDeferredResults?: boolean } | undefined
712
- )?.supportsDeferredResults === true
713
- ? { supportsDeferredResults: true }
714
- : {}),
715
- ...(chunk.providerMetadata === undefined
716
- ? {}
717
- : { providerMetadata: chunk.providerMetadata }),
718
- ...(chunk.toolMetadata === undefined
719
- ? {}
720
- : { toolMetadata: chunk.toolMetadata }),
721
- ...(chunk.title === undefined ? {} : { title: chunk.title }),
722
- }
723
- take(payload.toolCallId)
724
- const classification = approvalClassification(
725
- options.tools,
726
- options.generation,
727
- chunk.toolName,
728
- )
729
- if (classification === 'automatic') {
730
- result.push(await toolCalledEvent<D>(payload))
731
- } else {
732
- pending.push({ call: payload, classification })
733
- }
734
- } else if (chunk.type === 'tool-input-error') {
735
- const payload: ToolResultPayload = {
736
- requestId: options.requestId,
737
- messageId: options.messageId,
738
- generationId: options.generationId,
739
- toolCallId: chunk.toolCallId,
740
- toolName: chunk.toolName,
741
- ...(chunk.dynamic ? { input: chunk.input } : { rawInput: chunk.input }),
742
- error: chunk.errorText,
743
- phase: 'input',
744
- ...(chunk.dynamic === undefined ? {} : { dynamic: chunk.dynamic }),
745
- ...(chunk.providerExecuted === undefined
746
- ? {}
747
- : { providerExecuted: chunk.providerExecuted }),
748
- ...(chunk.providerMetadata === undefined
749
- ? {}
750
- : { providerMetadata: chunk.providerMetadata }),
751
- ...(chunk.toolMetadata === undefined
752
- ? {}
753
- : { toolMetadata: chunk.toolMetadata }),
754
- }
755
- const called: ToolCalledPayload = {
756
- requestId: options.requestId,
757
- messageId: options.messageId,
758
- generationId: options.generationId,
759
- toolCallId: chunk.toolCallId,
760
- toolName: chunk.toolName,
761
- input: chunk.input,
762
- }
763
- take(chunk.toolCallId)
764
- result.push(await toolCalledEvent<D>(called), {
765
- type: 'ai.tool.result',
766
- id: await resultId(),
767
- payload,
768
- } as AppendInput<D>)
769
- } else if (chunk.type === 'tool-output-available') {
770
- await flush(chunk.toolCallId)
771
- const payload: ToolResultPayload = {
772
- requestId: options.requestId,
773
- messageId: options.messageId,
774
- generationId: options.generationId,
775
- toolCallId: chunk.toolCallId,
776
- output: chunk.output,
777
- phase: 'execution',
778
- ...(chunk.preliminary === undefined
779
- ? {}
780
- : { preliminary: chunk.preliminary }),
781
- ...(chunk.dynamic === undefined ? {} : { dynamic: chunk.dynamic }),
782
- ...(chunk.providerExecuted === undefined
783
- ? {}
784
- : { providerExecuted: chunk.providerExecuted }),
785
- ...(chunk.providerMetadata === undefined
786
- ? {}
787
- : { providerMetadata: chunk.providerMetadata }),
788
- ...(chunk.toolMetadata === undefined
789
- ? {}
790
- : { toolMetadata: chunk.toolMetadata }),
791
- }
792
- result.push({
793
- type: 'ai.tool.result',
794
- id: await resultId(),
795
- payload,
796
- } as AppendInput<D>)
797
- } else if (chunk.type === 'tool-output-error') {
798
- await flush(chunk.toolCallId)
799
- const payload: ToolResultPayload = {
800
- requestId: options.requestId,
801
- messageId: options.messageId,
802
- generationId: options.generationId,
803
- toolCallId: chunk.toolCallId,
804
- error: chunk.errorText,
805
- phase: 'execution',
806
- ...(chunk.dynamic === undefined ? {} : { dynamic: chunk.dynamic }),
807
- ...(chunk.providerExecuted === undefined
808
- ? {}
809
- : { providerExecuted: chunk.providerExecuted }),
810
- ...(chunk.providerMetadata === undefined
811
- ? {}
812
- : { providerMetadata: chunk.providerMetadata }),
813
- ...(chunk.toolMetadata === undefined
814
- ? {}
815
- : { toolMetadata: chunk.toolMetadata }),
816
- }
817
- result.push({
818
- type: 'ai.tool.result',
819
- id: await resultId(),
820
- payload,
821
- } as AppendInput<D>)
822
- } else if (chunk.type === 'tool-output-denied') {
823
- await flush(chunk.toolCallId)
824
- const payload: ToolResultPayload = {
825
- requestId: options.requestId,
826
- messageId: options.messageId,
827
- generationId: options.generationId,
828
- toolCallId: chunk.toolCallId,
829
- denied: true,
830
- phase: 'execution',
831
- }
832
- result.push({
833
- type: 'ai.tool.result',
834
- id: await resultId(),
835
- payload,
836
- } as AppendInput<D>)
837
- } else if (chunk.type === 'tool-approval-request') {
838
- await flush(chunk.toolCallId)
839
- const payload: ApprovalRequestedPayload = {
840
- requestId: options.requestId,
841
- messageId: options.messageId,
842
- generationId: options.generationId,
843
- approvalId: chunk.approvalId,
844
- toolCallId: chunk.toolCallId,
845
- ...(chunk.isAutomatic === undefined
846
- ? {}
847
- : { isAutomatic: chunk.isAutomatic }),
848
- ...(chunk.signature === undefined
849
- ? {}
850
- : { signature: chunk.signature }),
851
- }
852
- result.push({
853
- type: 'ai.approval.requested',
854
- id: await aiEventId(
855
- 'a2.ai.approval.requested',
856
- options.generationId,
857
- chunk.approvalId,
858
- ),
859
- payload,
860
- } as AppendInput<D>)
861
- } else if (chunk.type === 'tool-approval-response') {
862
- result.push({
863
- type: 'ai.approval.responded',
864
- id: await aiEventId(
865
- 'a2.ai.approval.automatic-response',
866
- options.generationId,
867
- chunk.approvalId,
868
- ),
869
- payload: {
870
- requestId: options.requestId,
871
- messageId: options.messageId,
872
- generationId: options.generationId,
873
- approvalId: chunk.approvalId,
874
- approved: chunk.approved,
875
- ...(chunk.reason === undefined ? {} : { reason: chunk.reason }),
876
- },
877
- } as AppendInput<D>)
878
- }
879
- }
880
- return { events: result, pending }
881
- }
882
-
883
- const validateCompaction = <
884
- M extends UIMessage,
885
- D extends AIEventDefs<M> & EventDefs,
886
- T extends ToolSet,
887
- >(options: {
888
- compaction: CompactionOptions<M, D>
889
- explicit: boolean
890
- generation: AgentGenerationSettings<T> | undefined
891
- tools: T | undefined
892
- }): CompactionOptions<M, D> => {
893
- let compaction = options.compaction
894
- if (
895
- compaction !== false &&
896
- (typeof compaction !== 'object' || compaction === null)
897
- ) {
898
- throw new TypeError('compaction must resolve to false or a policy object')
899
- }
900
- if (compaction !== false && 'then' in compaction) {
901
- void Promise.resolve(compaction).catch(() => {})
902
- throw new TypeError('compaction options must be a policy or a resolver')
903
- }
904
- if (compaction !== false && !('shouldCompact' in compaction)) {
905
- if (
906
- compaction.thresholdTokens !== undefined &&
907
- (!Number.isSafeInteger(compaction.thresholdTokens) ||
908
- compaction.thresholdTokens < 1)
909
- ) {
910
- throw new TypeError(
911
- 'compaction.thresholdTokens must be a positive safe integer',
912
- )
913
- }
914
- const choice = options.generation?.toolChoice
915
- const unsupported =
916
- options.generation?.output !== undefined
917
- ? 'structured output'
918
- : choice !== undefined && choice !== 'auto' && choice !== 'none'
919
- ? 'forced tool choice'
920
- : Object.values(options.tools ?? {}).some(
921
- (tool) =>
922
- tool.type === 'provider' && tool.isProviderExecuted === true,
923
- )
924
- ? 'provider-executed tools'
925
- : undefined
926
- if (unsupported !== undefined) {
927
- if (options.explicit) {
928
- throw new TypeError(
929
- `automatic compaction does not support ${unsupported}; use a custom policy or compaction: false`,
930
- )
931
- }
932
- compaction = false
933
- }
934
- }
935
-
936
- return compaction
937
- }
938
-
939
- /**
940
- * Build the ordinary A2 handler table for the built-in agent protocol.
941
- * Application handlers can be spread beside this table.
942
- */
943
- export function createHandlers<
944
- M extends UIMessage,
945
- D extends AIEventDefs<M> & EventDefs,
946
- T extends ToolSet = ToolSet,
947
- >(
948
- options: CreateHandlersOptions<M, D, T>,
949
- ): NonNullable<ServerOptions<D>['handlers']> {
950
- if (options.model === undefined) {
951
- throw new TypeError('createHandlers requires model')
952
- }
953
- if (
954
- options.progress?.maxChunks !== undefined &&
955
- (!Number.isInteger(options.progress.maxChunks) ||
956
- options.progress.maxChunks < 1)
957
- ) {
958
- throw new TypeError('progress.maxChunks must be a positive integer')
959
- }
960
- if (
961
- options.progress?.maxDelayMs !== undefined &&
962
- (!Number.isFinite(options.progress.maxDelayMs) ||
963
- options.progress.maxDelayMs < 0)
964
- ) {
965
- throw new TypeError('progress.maxDelayMs must be a non-negative number')
966
- }
967
- if (
968
- options.maxSteps !== undefined &&
969
- options.maxSteps !== Number.POSITIVE_INFINITY &&
970
- (!Number.isInteger(options.maxSteps) || options.maxSteps < 1)
971
- ) {
972
- throw new TypeError('maxSteps must be a positive integer or Infinity')
973
- }
974
-
975
- const configuredCompaction =
976
- options.compaction ?? (options.generate === undefined ? {} : false)
977
- const staticCompaction =
978
- typeof configuredCompaction === 'function'
979
- ? undefined
980
- : validateCompaction({
981
- compaction: configuredCompaction,
982
- explicit: options.compaction !== undefined,
983
- generation: options.generation,
984
- tools: options.tools,
985
- })
986
-
987
- const tools = options.tools ?? ({} as T)
988
- const generation: AgentGenerationSettings<T> = options.generation ?? {}
989
- const maxSteps = options.maxSteps ?? Number.POSITIVE_INFINITY
990
- const control = createControlRuntime({ agent: options.agent })
991
- const coordinator = control.coordinator
992
- const contextReducer = createContextReducer({ agent: options.agent })
993
- const readContext = async (
994
- session: HandlerContext<D>['session'],
995
- through: number | 'latest',
996
- ) => {
997
- const snapshot = await session.state(contextReducer, { through })
998
- return { state: contextReducer.view(snapshot.state), index: snapshot.index }
999
- }
1000
- const promptCache = new Map<string, Promise<ModelMessage[]>>()
1001
-
1002
- type RuntimeTool = {
1003
- execute?: (
1004
- input: unknown,
1005
- options: {
1006
- toolCallId: string
1007
- messages: ModelMessage[]
1008
- abortSignal?: AbortSignal
1009
- },
1010
- ) => unknown
1011
- }
1012
-
1013
- type ToolHandlerContext = HandlerContext<D, 'ai.tool.execution.requested'>
1014
-
1015
- const resultEvent = async (
1016
- call: ToolCalledPayload,
1017
- suffix: string,
1018
- result: {
1019
- output?: unknown
1020
- error?: string
1021
- denied?: boolean
1022
- preliminary?: boolean
1023
- },
1024
- ): Promise<AppendInput<D>> =>
1025
- ({
1026
- type: 'ai.tool.result',
1027
- id: await aiEventId(
1028
- 'a2.ai.tool.result',
1029
- call.generationId,
1030
- call.toolCallId,
1031
- suffix,
1032
- ),
1033
- payload: {
1034
- requestId: call.requestId,
1035
- messageId: call.messageId,
1036
- generationId: call.generationId,
1037
- toolCallId: call.toolCallId,
1038
- toolName: call.toolName,
1039
- input: call.input,
1040
- phase: 'execution',
1041
- ...(result.output === undefined ? {} : { output: result.output }),
1042
- ...(result.error === undefined ? {} : { error: result.error }),
1043
- ...(result.denied === undefined ? {} : { denied: result.denied }),
1044
- ...(result.preliminary === undefined
1045
- ? {}
1046
- : { preliminary: result.preliminary }),
1047
- ...(call.dynamic === undefined ? {} : { dynamic: call.dynamic }),
1048
- ...(call.providerExecuted === undefined
1049
- ? {}
1050
- : { providerExecuted: call.providerExecuted }),
1051
- ...(call.toolMetadata === undefined
1052
- ? {}
1053
- : { toolMetadata: call.toolMetadata }),
1054
- },
1055
- }) as AppendInput<D>
1056
-
1057
- const promptMessages = (input: {
1058
- ctx: ToolHandlerContext
1059
- call: ToolCalledPayload
1060
- generation: GenerationStartedPayload
1061
- }): Promise<ModelMessage[]> => {
1062
- const { ctx, call, generation: owner } = input
1063
- const key = promptCacheKey(ctx.event.sessionId, call.generationId)
1064
- const cached = promptCache.get(key)
1065
- if (cached) return cached
1066
- const computation = (async () => {
1067
- const frontier = owner.promptThroughIndex
1068
- if (frontier === undefined)
1069
- throw new TypeError('AI work requires a prompt checkpoint')
1070
- const snapshot = await readContext(ctx.session, ctx.event.index)
1071
- if (
1072
- snapshot.state.generation?.owner.generationId !== owner.generationId ||
1073
- snapshot.state.generation.owner.requestId !== owner.requestId ||
1074
- snapshot.state.activeProjection?.generationId !== owner.generationId
1075
- )
1076
- throw new TypeError(
1077
- 'Tool prompt checkpoint does not own its generation',
1078
- )
1079
- const state = generationBase(snapshot.state)
1080
- return modelContext({
1081
- messages: state.messages.filter(
1082
- (message) => state.messageIndexes[message.id]! <= frontier,
1083
- ),
1084
- state,
1085
- tools,
1086
- })
1087
- })()
1088
- promptCache.set(key, computation)
1089
- void computation.catch(() => {
1090
- if (promptCache.get(key) === computation) promptCache.delete(key)
1091
- })
1092
- return computation
1093
- }
1094
-
1095
- const toolExecutionFailure = async (
1096
- ctx: ToolHandlerContext,
1097
- call: ToolCalledPayload,
1098
- error: unknown,
1099
- ): Promise<AppendInput<D> | void> => {
1100
- const schedulerFailure = consumeSchedulerSendFailure(error)
1101
- if (checkAbort(ctx.signal)) return
1102
- if (schedulerFailure === 'retryable') throw error
1103
- return resultEvent(call, `${ctx.event.id}:execution:error`, {
1104
- error: errorMessage(error),
1105
- })
1106
- }
1107
-
1108
- const runToolExecution = async (
1109
- ctx: ToolHandlerContext,
1110
- call: ToolCalledPayload,
1111
- execute: NonNullable<RuntimeTool['execute']>,
1112
- messages: ModelMessage[],
1113
- ): Promise<AppendInput<D> | void> => {
1114
- let output: unknown
1115
- try {
1116
- output = await execute(call.input, {
1117
- toolCallId: call.toolCallId,
1118
- messages,
1119
- abortSignal: ctx.signal,
1120
- })
1121
- } catch (error) {
1122
- return toolExecutionFailure(ctx, call, error)
1123
- }
1124
- let iterator: AsyncIterator<unknown> | undefined
1125
- try {
1126
- iterator = asyncIterator(output)
1127
- } catch (error) {
1128
- return toolExecutionFailure(ctx, call, error)
1129
- }
1130
- if (iterator === undefined) {
1131
- return resultEvent(call, `${ctx.event.id}:execution:0`, { output })
1132
- }
1133
- let last: unknown
1134
- let sequence = 0
1135
- let done = false
1136
- try {
1137
- for (;;) {
1138
- let result: IteratorResult<unknown>
1139
- try {
1140
- result = await iterator.next()
1141
- } catch (error) {
1142
- return toolExecutionFailure(ctx, call, error)
1143
- }
1144
- if (result.done) {
1145
- if (ctx.signal.reason instanceof A2Error) throw ctx.signal.reason
1146
- done = true
1147
- break
1148
- }
1149
- if (checkAbort(ctx.signal)) return
1150
- last = result.value
1151
- await control.append({
1152
- ctx,
1153
- name: `tool:${call.toolCallId}:preliminary:${sequence}`,
1154
- events: [
1155
- await resultEvent(
1156
- call,
1157
- `${ctx.event.id}:execution:${ctx.attempt}:${sequence}:preliminary`,
1158
- {
1159
- output: result.value,
1160
- preliminary: true,
1161
- },
1162
- ),
1163
- ],
1164
- })
1165
- sequence += 1
1166
- }
1167
- } finally {
1168
- if (!done) {
1169
- try {
1170
- await iterator.return?.()
1171
- } catch {
1172
- // Preserve the failure or cancellation that ended execution.
1173
- }
1174
- }
1175
- }
1176
- return resultEvent(
1177
- call,
1178
- `${ctx.event.id}:execution:${sequence}:final`,
1179
- sequence === 0 ? {} : { output: last },
1180
- )
1181
- }
1182
-
1183
- const executeTool = async (input: {
1184
- ctx: ToolHandlerContext
1185
- call: ToolCalledPayload
1186
- generation: GenerationStartedPayload
1187
- }): Promise<AppendInput<D> | void> => {
1188
- const { ctx, call } = input
1189
- const tool = tools[call.toolName] as RuntimeTool | undefined
1190
- const execute = tool?.execute
1191
- if (execute === undefined) {
1192
- return resultEvent(call, `${ctx.event.id}:execution:error`, {
1193
- error: `Tool '${call.toolName}' has no server executor`,
1194
- })
1195
- }
1196
- const messages = await promptMessages(input)
1197
- if (checkAbort(ctx.signal)) return
1198
- const scope: AmbientToolScope = {
1199
- contract: options.agent.contract,
1200
- context: ctx,
1201
- }
1202
- // Async iteration resumes in the caller's context, so the whole
1203
- // execution, not just the execute call, runs inside the scope.
1204
- return toolScopeStorage.run(scope, () =>
1205
- runToolExecution(ctx, call, execute, messages),
1206
- )
1207
- }
1208
-
1209
- const handleToolExecution = async (
1210
- ctx: ToolHandlerContext,
1211
- ): Promise<AppendInput<D>> => {
1212
- try {
1213
- const state = (
1214
- await ctx.session.state(control.reducer, { through: 'latest' })
1215
- ).state
1216
- const request = ctx.event.payload
1217
- const call = state.coordinator.response?.calls.find(
1218
- (candidate) => candidate.work?.id === ctx.event.id,
1219
- )
1220
- if (
1221
- state.active?.turnId !== request.turnId ||
1222
- Object.values(state.pendingStops).includes(request.turnId) ||
1223
- state.active.suspended ||
1224
- state.active.version !== request.version ||
1225
- state.coordinator.response?.failure !== undefined ||
1226
- !call ||
1227
- call.terminal ||
1228
- call.work?.settled
1229
- )
1230
- return control.settled({ ctx, events: undefined })
1231
- return control.settled({
1232
- ctx,
1233
- events: await executeTool({
1234
- ctx,
1235
- call: request.call,
1236
- generation: request.generation,
1237
- }),
1238
- })
1239
- } catch (error) {
1240
- if (control.cancelled({ error, signal: ctx.signal }))
1241
- return control.settled({ ctx, events: undefined })
1242
- throw error
1243
- }
1244
- }
1245
-
1246
- const generationHandler = async (
1247
- ctx: HandlerContext<D, 'ai.generation.requested'>,
1248
- ): Promise<void | AppendInput<D> | readonly AppendInput<D>[]> => {
1249
- if (checkAbort(ctx.signal)) return
1250
- const requestId = ctx.event.id
1251
- const request = ctx.event.payload
1252
- const snapshot = await readContext(ctx.session, 'latest')
1253
- if (
1254
- snapshot.state.closed ||
1255
- snapshot.state.authority?.requestId !== requestId ||
1256
- snapshot.state.authority?.phase === 'paused' ||
1257
- snapshot.state.authority?.phase === 'pausing'
1258
- )
1259
- return
1260
- const controlState = (
1261
- await ctx.session.state(control.reducer, { through: snapshot.index })
1262
- ).state
1263
- if (
1264
- request.control !== undefined &&
1265
- Object.values(controlState.pendingStops).includes(request.control.turnId)
1266
- )
1267
- return
1268
- const coordinatorState = controlState.coordinator
1269
- if (
1270
- coordinatorState.closed ||
1271
- coordinatorState.response?.activeRequestId !== requestId
1272
- )
1273
- return
1274
- const response = coordinatorState.response
1275
- const current =
1276
- response.generation?.requestId === requestId
1277
- ? response.generation
1278
- : undefined
1279
- if (
1280
- current !== undefined &&
1281
- (response.completion !== undefined ||
1282
- (response.failure !== undefined &&
1283
- response.failure.superseded !== true))
1284
- )
1285
- return
1286
- if (current !== undefined && current.attempt >= ctx.attempt) return
1287
- const attempt = ctx.attempt
1288
- const generationId = await aiEventId(
1289
- 'a2.ai.generation',
1290
- requestId,
1291
- String(attempt),
1292
- )
1293
- const responseMessageId = request.responseMessageId
1294
- const responseStepCount = response.stepCount
1295
- if (
1296
- request.reason === 'tool' &&
1297
- (request.sourceGenerationId !==
1298
- response.source?.generation.generationId ||
1299
- response.responseMessageId !== request.messageId ||
1300
- response.responseMessageId !== request.responseMessageId)
1301
- )
1302
- return
1303
- const previous = current
1304
- const state =
1305
- previous === undefined
1306
- ? snapshot.state
1307
- : contextForAttempt(snapshot.state)
1308
- const resolverContext: AgentResolverContext<M, D> = {
1309
- event: ctx.event,
1310
- messages: contextMessages({ state, coordinator: coordinatorState }),
1311
- session: {
1312
- state: (reducer, readOptions) =>
1313
- ctx.session.state(reducer, {
1314
- ...readOptions,
1315
- through: readOptions?.through ?? snapshot.index,
1316
- }),
1317
- },
1318
- signal: ctx.signal,
1319
- }
1320
-
1321
- let generationStarted = false
1322
- try {
1323
- const resolvedModel = await resolve(options.model, resolverContext)
1324
- if (resolvedModel === undefined) {
1325
- throw new TypeError('the model resolver returned undefined')
1326
- }
1327
- if (checkAbort(ctx.signal)) return
1328
- const resolvedInstructions =
1329
- options.instructions === undefined
1330
- ? undefined
1331
- : await resolve(options.instructions, resolverContext)
1332
- if (checkAbort(ctx.signal)) return
1333
- const compaction =
1334
- typeof configuredCompaction === 'function'
1335
- ? validateCompaction({
1336
- compaction: await resolve(configuredCompaction, resolverContext),
1337
- explicit: true,
1338
- generation: options.generation,
1339
- tools: options.tools,
1340
- })
1341
- : staticCompaction!
1342
- if (checkAbort(ctx.signal)) return
1343
-
1344
- if (request.reason === 'tool' && responseStepCount >= maxSteps) {
1345
- const source = response.source?.generation
1346
- if (source === undefined) return
1347
- return {
1348
- type: 'ai.generation.failed',
1349
- id: await aiEventId('a2.ai.step-limit', requestId),
1350
- payload: {
1351
- requestId: source.requestId,
1352
- messageId: source.messageId,
1353
- generationId: source.generationId,
1354
- responseMessageId,
1355
- error: `agent exceeded the ${maxSteps}-step limit`,
1356
- stepLimit: true,
1357
- },
1358
- } as AppendInput<D>
1359
- }
1360
-
1361
- const started: GenerationStartedPayload = {
1362
- requestId,
1363
- messageId: request.messageId,
1364
- generationId,
1365
- responseMessageId,
1366
- attempt,
1367
- model: modelName(resolvedModel),
1368
- promptThroughIndex: snapshot.index,
1369
- }
1370
- const catalogModelId = gatewayModelId(resolvedModel)
1371
- const gatewayOptions = options.generation?.providerOptions?.['gateway']
1372
- const usesFallbackModels =
1373
- Array.isArray(gatewayOptions?.['models']) &&
1374
- gatewayOptions['models'].length > 0
1375
- const discoversMetadata =
1376
- compaction !== false &&
1377
- !('shouldCompact' in compaction) &&
1378
- compaction.thresholdTokens === undefined &&
1379
- !usesFallbackModels &&
1380
- catalogModelId !== undefined
1381
- const startEvents: AppendInput<D>[] = []
1382
- if (
1383
- discoversMetadata &&
1384
- !Object.hasOwn(state.modelMetadata, catalogModelId)
1385
- ) {
1386
- startEvents.push({
1387
- type: 'ai.model.metadata.requested',
1388
- id: await aiEventId(
1389
- 'a2.ai.model.metadata',
1390
- options.agent.contract.name,
1391
- ctx.event.sessionId,
1392
- catalogModelId,
1393
- ),
1394
- payload: { modelId: catalogModelId },
1395
- } as AppendInput<D>)
1396
- }
1397
- if (previous) {
1398
- const payload = previous
1399
- const superseded: GenerationFailedPayload = {
1400
- requestId,
1401
- messageId: payload.messageId,
1402
- generationId: payload.generationId,
1403
- responseMessageId: payload.responseMessageId,
1404
- error: 'generation attempt was superseded after an incomplete run',
1405
- superseded: true,
1406
- }
1407
- startEvents.push({
1408
- type: 'ai.generation.failed',
1409
- id: await aiEventId('a2.ai.superseded', payload.generationId),
1410
- payload: superseded,
1411
- } as AppendInput<D>)
1412
- }
1413
- startEvents.push({
1414
- type: 'ai.generation.started',
1415
- id: generationId,
1416
- payload: started,
1417
- } as AppendInput<D>)
1418
- const generationEvents = await control.append({
1419
- ctx,
1420
- name: 'generation-start',
1421
- events: startEvents,
1422
- })
1423
- generationStarted = true
1424
-
1425
- if (checkAbort(ctx.signal)) return
1426
-
1427
- const promptCoordinatorState = coordinatorState
1428
- const messages = contextMessages({
1429
- state,
1430
- coordinator: promptCoordinatorState,
1431
- })
1432
- let generationMessages = messages
1433
- let modelMessages = await modelContext({ messages, state, tools })
1434
- const baseContext: AgentGenerateContext<M, D, T> = {
1435
- request: ctx.event,
1436
- requestId,
1437
- generationId,
1438
- responseMessageId,
1439
- messages,
1440
- modelMessages,
1441
- session: resolverContext.session,
1442
- signal: ctx.signal,
1443
- model: resolvedModel,
1444
- tools,
1445
- ...(resolvedInstructions === undefined
1446
- ? {}
1447
- : { instructions: resolvedInstructions }),
1448
- generation,
1449
- }
1450
- const policy = compaction
1451
- const tracksInput =
1452
- policy !== false &&
1453
- !('shouldCompact' in policy) &&
1454
- (policy.thresholdTokens !== undefined || discoversMetadata)
1455
- const metadata =
1456
- catalogModelId === undefined
1457
- ? undefined
1458
- : state.modelMetadata[catalogModelId]
1459
- const limits =
1460
- metadata?.status === 'resolved' && !usesFallbackModels
1461
- ? metadata.limits
1462
- : undefined
1463
- let inputTokenEstimate: number | undefined
1464
- let compacted = false
1465
- let compactionEstimate:
1466
- | Pick<
1467
- CompactionRequestedPayload,
1468
- 'inputTokenEstimate' | 'inputTokens' | 'thresholdTokens'
1469
- >
1470
- | undefined
1471
- const hasPendingTools = messages.some((message) =>
1472
- message.parts.some(
1473
- (part) =>
1474
- isToolUIPart(part) &&
1475
- !(
1476
- part.state === 'output-error' ||
1477
- part.state === 'output-denied' ||
1478
- (part.state === 'output-available' && part.preliminary !== true)
1479
- ),
1480
- ),
1481
- )
1482
- if (policy && !hasPendingTools) {
1483
- const compactionContext = {
1484
- ...resolverContext,
1485
- messages,
1486
- modelMessages,
1487
- }
1488
- let shouldCompact: boolean
1489
- if ('shouldCompact' in policy) {
1490
- shouldCompact = await policy.shouldCompact(compactionContext)
1491
- } else if (tracksInput) {
1492
- inputTokenEstimate = await estimateInputTokens({
1493
- messages: modelMessages,
1494
- instructions: resolvedInstructions,
1495
- tools,
1496
- })
1497
- const inputTokens = measuredInputTokens({
1498
- estimate: inputTokenEstimate,
1499
- model: modelName(resolvedModel),
1500
- calibration: coordinatorState.calibration,
1501
- })
1502
- const threshold =
1503
- policy.thresholdTokens ??
1504
- (limits === undefined
1505
- ? undefined
1506
- : Math.floor(
1507
- Math.min(
1508
- limits.contextWindow * 0.75,
1509
- limits.contextWindow - (generation.maxOutputTokens ?? 0),
1510
- ),
1511
- ))
1512
- if (
1513
- limits !== undefined &&
1514
- threshold !== undefined &&
1515
- threshold <= 0
1516
- ) {
1517
- throw new Error(
1518
- `output allowance exhausts the ${limits.contextWindow}-token context window`,
1519
- )
1520
- }
1521
- shouldCompact = threshold !== undefined && inputTokens >= threshold
1522
- if (threshold !== undefined && shouldCompact) {
1523
- compactionEstimate = {
1524
- inputTokenEstimate,
1525
- inputTokens,
1526
- thresholdTokens: threshold,
1527
- }
1528
- }
1529
- } else {
1530
- shouldCompact = false
1531
- }
1532
- if (checkAbort(ctx.signal)) return
1533
- if (shouldCompact) {
1534
- const latest = (
1535
- await ctx.session.state(coordinator, { through: 'latest' })
1536
- ).state
1537
- shouldCompact = Object.keys(latest.unsettledTools ?? {}).length === 0
1538
- }
1539
- if (checkAbort(ctx.signal)) return
1540
- if (shouldCompact) {
1541
- const throughMessageId =
1542
- state.messages.findLast(
1543
- (message) =>
1544
- !promptCoordinatorState.queued.some(
1545
- (queued) => queued.messageId === message.id,
1546
- ),
1547
- )?.id ?? request.messageId
1548
- const throughIndex = snapshot.index
1549
- generationEvents.push(
1550
- ...(await control.append({
1551
- ctx,
1552
- name: 'compaction-requested',
1553
- events: [
1554
- {
1555
- type: 'ai.compaction.requested',
1556
- id: await aiEventId(
1557
- 'a2.ai.compaction.requested',
1558
- generationId,
1559
- ),
1560
- payload: {
1561
- generationId,
1562
- throughMessageId,
1563
- throughIndex,
1564
- ...compactionEstimate,
1565
- },
1566
- } as AppendInput<D>,
1567
- ],
1568
- })),
1569
- )
1570
- const result = !('shouldCompact' in policy)
1571
- ? {
1572
- messages: [] as M[],
1573
- ...(await summarize({
1574
- agentName: options.agent.contract.name,
1575
- context: baseContext,
1576
- instructions: policy.instructions,
1577
- })),
1578
- }
1579
- : { messages: await policy.compact(compactionContext) }
1580
- if (checkAbort(ctx.signal)) return
1581
- const completed: CompactionCompletedPayload<M> = {
1582
- generationId,
1583
- throughMessageId,
1584
- throughIndex,
1585
- ...result,
1586
- ...(promptCoordinatorState.queued.length === 0
1587
- ? {}
1588
- : {
1589
- retainedMessageIds: promptCoordinatorState.queued.map(
1590
- (item) => item.messageId,
1591
- ),
1592
- }),
1593
- }
1594
- generationEvents.push(
1595
- ...(await control.append({
1596
- ctx,
1597
- name: 'compaction-completed',
1598
- events: [
1599
- {
1600
- type: 'ai.compaction.completed',
1601
- id: await aiEventId(
1602
- 'a2.ai.compaction.completed',
1603
- generationId,
1604
- ),
1605
- payload: completed,
1606
- } as AppendInput<D>,
1607
- ],
1608
- })),
1609
- )
1610
- const queued = new Set(
1611
- promptCoordinatorState.queued.map((item) => item.messageId),
1612
- )
1613
- generationMessages = result.messages.filter(
1614
- (message) => !queued.has(message.id),
1615
- )
1616
- compacted = true
1617
- }
1618
- }
1619
-
1620
- const currentState = foldAIEvents({
1621
- reducer: contextReducer[aiStateReducer],
1622
- events: generationEvents,
1623
- state,
1624
- })
1625
- if (request.reason === 'retry' || previous !== undefined) {
1626
- generationMessages = contextMessages({
1627
- state: currentState,
1628
- coordinator: promptCoordinatorState,
1629
- })
1630
- }
1631
- if (
1632
- (policy && 'shouldCompact' in policy) ||
1633
- generationMessages !== messages ||
1634
- activeCompaction(currentState)?.summary !==
1635
- activeCompaction(state)?.summary
1636
- ) {
1637
- modelMessages = await modelContext({
1638
- messages: generationMessages,
1639
- state: currentState,
1640
- tools,
1641
- })
1642
- }
1643
- promptCache.set(
1644
- promptCacheKey(ctx.event.sessionId, generationId),
1645
- Promise.resolve(modelMessages),
1646
- )
1647
- if (tracksInput && (inputTokenEstimate === undefined || compacted)) {
1648
- inputTokenEstimate = await estimateInputTokens({
1649
- messages: modelMessages,
1650
- instructions: resolvedInstructions,
1651
- tools,
1652
- })
1653
- }
1654
-
1655
- let pendingToolCalls: PendingToolCall[] = []
1656
- if (checkAbort(ctx.signal)) return
1657
- const generateContext: AgentGenerateContext<M, D, T> = {
1658
- request: ctx.event,
1659
- requestId,
1660
- generationId,
1661
- responseMessageId,
1662
- messages: generationMessages,
1663
- modelMessages,
1664
- session: resolverContext.session,
1665
- signal: ctx.signal,
1666
- model: resolvedModel,
1667
- tools,
1668
- ...(resolvedInstructions === undefined
1669
- ? {}
1670
- : { instructions: resolvedInstructions }),
1671
- generation: options.generation ?? {},
1672
- }
1673
- const custom = options.generate !== undefined
1674
- const source =
1675
- options.generate === undefined
1676
- ? await generateWithAISDK({
1677
- agentName: options.agent.contract.name,
1678
- context: generateContext,
1679
- messageMetadata: options.messageMetadata,
1680
- })
1681
- : { stream: await options.generate(generateContext) }
1682
- const updates = consumeGeneration({
1683
- source,
1684
- ...(options.progress === undefined
1685
- ? {}
1686
- : { progress: options.progress }),
1687
- })
1688
-
1689
- let sequence = 0
1690
- let finish: AgentGenerationFinish | undefined
1691
- for await (const update of updates) {
1692
- ctx.signal.throwIfAborted()
1693
- if (update.type === 'finish') {
1694
- finish = update
1695
- continue
1696
- }
1697
- const progress: GenerationProgressPayload = {
1698
- requestId,
1699
- messageId: request.messageId,
1700
- generationId,
1701
- responseMessageId,
1702
- sequence,
1703
- chunks: update.chunks,
1704
- }
1705
- const lifecycle = await lifecycleEvents<D, T>({
1706
- requestId,
1707
- messageId: responseMessageId,
1708
- generationId,
1709
- sequence,
1710
- chunks: update.chunks,
1711
- pending: pendingToolCalls,
1712
- tools,
1713
- generation,
1714
- custom,
1715
- })
1716
- pendingToolCalls = lifecycle.pending
1717
- await control.append({
1718
- ctx,
1719
- name: `generation-progress:${sequence}`,
1720
- events: [
1721
- {
1722
- type: 'ai.generation.progress',
1723
- id: await aiEventId(
1724
- 'a2.ai.progress',
1725
- generationId,
1726
- String(sequence),
1727
- ),
1728
- payload: progress,
1729
- } as AppendInput<D>,
1730
- ...lifecycle.events,
1731
- ],
1732
- })
1733
- sequence += 1
1734
- }
1735
-
1736
- if (!finish) throw new Error('agent generation finished without output')
1737
- const completed: GenerationCompletedPayload = {
1738
- requestId,
1739
- messageId: request.messageId,
1740
- generationId,
1741
- responseMessageId,
1742
- ...(inputTokenEstimate === undefined ? {} : { inputTokenEstimate }),
1743
- ...(finish.finishReason === undefined
1744
- ? {}
1745
- : { finishReason: finish.finishReason }),
1746
- ...(finish.usage === undefined ? {} : { usage: finish.usage }),
1747
- }
1748
- const completionEvent = {
1749
- type: 'ai.generation.completed',
1750
- id: await aiEventId('a2.ai.completed', generationId),
1751
- payload: completed,
1752
- } as AppendInput<D>
1753
- const unresolved = options.generate
1754
- ? pendingToolCalls.filter(
1755
- (pending) => pending.classification !== 'automatic',
1756
- )
1757
- : []
1758
- if (unresolved.length > 0) {
1759
- throw new Error(
1760
- `generation ended with unresolved tool authorization for '${unresolved[0]!.call.toolName}'`,
1761
- )
1762
- }
1763
- const pendingEvents = await Promise.all(
1764
- pendingToolCalls
1765
- .filter(
1766
- (pending) =>
1767
- pending.classification === 'automatic' ||
1768
- (options.generate === undefined &&
1769
- pending.classification === 'unknown'),
1770
- )
1771
- .map((pending) => toolCalledEvent<D>(pending.call)),
1772
- )
1773
- if (finish.finishReason === 'tool-calls') {
1774
- return [...pendingEvents, completionEvent]
1775
- }
1776
- return [
1777
- ...pendingEvents,
1778
- completionEvent,
1779
- {
1780
- type: 'ai.message.completed',
1781
- id: await aiEventId('a2.ai.message.completed', generationId),
1782
- payload: {
1783
- messageId: responseMessageId,
1784
- } satisfies MessageCompletedPayload,
1785
- } as AppendInput<D>,
1786
- ]
1787
- } catch (error) {
1788
- if (error instanceof ControlCancelled) throw error
1789
- if (error instanceof A2Error) {
1790
- checkAbort(ctx.signal)
1791
- throw error
1792
- }
1793
- if (checkAbort(ctx.signal)) return
1794
- if (!generationStarted) throw error
1795
- const failed: GenerationFailedPayload = {
1796
- requestId,
1797
- messageId: request.messageId,
1798
- generationId,
1799
- responseMessageId,
1800
- error: errorMessage(error),
1801
- }
1802
- return {
1803
- type: 'ai.generation.failed',
1804
- id: await aiEventId('a2.ai.failed', generationId),
1805
- payload: failed,
1806
- } as AppendInput<D>
1807
- }
1808
- }
1809
-
1810
- const clearPromptCache = (sessionId: string): void => {
1811
- const prefix = `${sessionId}\u001f`
1812
- for (const key of promptCache.keys()) {
1813
- if (key.startsWith(prefix)) promptCache.delete(key)
1814
- }
1815
- }
1816
-
1817
- return {
1818
- 'ai.model.metadata.requested': {
1819
- handler: async (ctx) => {
1820
- const limits = await readModelLimits({
1821
- modelId: ctx.event.payload.modelId,
1822
- signal: ctx.signal,
1823
- })
1824
- ctx.signal.throwIfAborted()
1825
- return {
1826
- type: 'ai.model.metadata.resolved',
1827
- id: await aiEventId('a2.ai.model.metadata.resolved', ctx.event.id),
1828
- payload: { modelId: ctx.event.payload.modelId, limits },
1829
- } as AppendInput<D>
1830
- },
1831
- },
1832
- 'ai.control.requested': {
1833
- lane: ({ event }) => {
1834
- const command = event.payload as ControlCommand<M>
1835
- if (event.id === '')
1836
- throw new A2Error(
1837
- 'INVALID_PAYLOAD',
1838
- 'AI command IDs must be nonempty',
1839
- )
1840
- if (event.id?.startsWith(AI_EVENT_ID_PREFIX))
1841
- throw new A2Error(
1842
- 'INVALID_PAYLOAD',
1843
- `AI command IDs cannot use the reserved '${AI_EVENT_ID_PREFIX}' prefix`,
1844
- )
1845
- if (
1846
- (command.action === 'send' || command.action === 'steer') &&
1847
- event.id !== command.message.id
1848
- )
1849
- throw new A2Error(
1850
- 'INVALID_PAYLOAD',
1851
- 'AI input event ID must equal message.id',
1852
- )
1853
- return 'a2.ai.control'
1854
- },
1855
- handler: control.handler,
1856
- },
1857
- 'ai.work.reported': { lane: 'a2.ai.control', handler: control.handler },
1858
- 'ai.message.completed': {
1859
- handler: async (ctx) => {
1860
- clearPromptCache(ctx.event.sessionId)
1861
- },
1862
- },
1863
- 'ai.message.interrupted': {
1864
- handler: async (ctx) => {
1865
- clearPromptCache(ctx.event.sessionId)
1866
- },
1867
- },
1868
- 'ai.session.closed': {
1869
- lane: 'a2.ai.control',
1870
- handler: async (ctx) => {
1871
- clearPromptCache(ctx.event.sessionId)
1872
- return control.handler(ctx)
1873
- },
1874
- },
1875
- 'ai.generation.failed': {
1876
- handler: (ctx) => {
1877
- promptCache.delete(
1878
- promptCacheKey(ctx.event.sessionId, ctx.event.payload.generationId),
1879
- )
1880
- return Promise.resolve()
1881
- },
1882
- },
1883
- 'ai.generation.requested': {
1884
- lane: 'a2.ai.model',
1885
- abortOn: {
1886
- 'ai.control.requested': (event, trigger) =>
1887
- event.payload.action === 'stop' &&
1888
- event.payload.turnId === trigger.payload.control?.turnId,
1889
- 'ai.control.committed': (event, trigger) => {
1890
- const active = (event.payload as ControlCommit).view
1891
- return (
1892
- active?.turnId !== trigger.payload.control?.turnId ||
1893
- active?.version !== trigger.payload.control?.version
1894
- )
1895
- },
1896
- 'ai.session.closed': true,
1897
- },
1898
- handler: async (ctx) => {
1899
- try {
1900
- return control.settled({ ctx, events: await generationHandler(ctx) })
1901
- } catch (error) {
1902
- if (control.cancelled({ error, signal: ctx.signal }))
1903
- return control.settled({ ctx, events: undefined })
1904
- throw error
1905
- }
1906
- },
1907
- },
1908
- 'ai.tool.execution.requested': {
1909
- abortOn: {
1910
- 'ai.control.requested': (event, trigger) =>
1911
- event.payload.action === 'stop' &&
1912
- event.payload.turnId === trigger.payload.turnId,
1913
- 'ai.generation.failed': (event, trigger) =>
1914
- event.payload.generationId === trigger.payload.call.generationId,
1915
- 'ai.control.committed': (event, trigger) => {
1916
- const active = (event.payload as ControlCommit).view
1917
- return (
1918
- active?.turnId !== trigger.payload.turnId ||
1919
- active?.version !== trigger.payload.version
1920
- )
1921
- },
1922
- 'ai.session.closed': true,
1923
- },
1924
- handler: handleToolExecution,
1925
- },
1926
- } as NonNullable<ServerOptions<D>['handlers']>
1927
- }
1928
-
1929
- /** Assemble an A2 server with the built-in agent handlers and app extensions. */
1930
- export function createAgentServer<
1931
- M extends UIMessage,
1932
- D extends AIEventDefs<M> & EventDefs,
1933
- T extends ToolSet = ToolSet,
1934
- >(options: CreateAgentServerOptions<M, D, T>): A2Server<D> {
1935
- const {
1936
- agent: definition,
1937
- model,
1938
- tools,
1939
- instructions,
1940
- generation,
1941
- maxSteps,
1942
- messageMetadata,
1943
- generate,
1944
- compaction,
1945
- progress,
1946
- handlers,
1947
- ...serverOptions
1948
- } = options
1949
- const builtIns = createHandlers({
1950
- agent: definition,
1951
- model,
1952
- ...(tools === undefined ? {} : { tools }),
1953
- ...(instructions === undefined ? {} : { instructions }),
1954
- ...(generation === undefined ? {} : { generation }),
1955
- ...(maxSteps === undefined ? {} : { maxSteps }),
1956
- ...(messageMetadata === undefined ? {} : { messageMetadata }),
1957
- ...(generate === undefined ? {} : { generate }),
1958
- ...(compaction === undefined ? {} : { compaction }),
1959
- ...(progress === undefined ? {} : { progress }),
1960
- })
1961
- const server = createServer({
1962
- ...serverOptions,
1963
- contract: definition.contract,
1964
- handlers: { ...builtIns, ...handlers },
1965
- })
1966
- setServerFetchHooks(server, { validateIngress: validateAgentIngress })
1967
- return server
1968
- }
1969
-
1970
- export type { Instructions, LanguageModel, ToolSet }
26
+ } from './ai-agent.ts'