experimental-a2 0.16.1 → 0.18.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 (406) hide show
  1. package/CHANGELOG.md +373 -0
  2. package/README.md +1 -1
  3. package/dist/{actor-ohPC-81x.d.ts → actor-Bjce9_5U.d.ts} +21 -11
  4. package/dist/actor-Bjce9_5U.d.ts.map +1 -0
  5. package/dist/actor-client.d.ts +2 -5
  6. package/dist/actor-client.d.ts.map +1 -1
  7. package/dist/actor-client.js +13 -4
  8. package/dist/actor-client.js.map +1 -1
  9. package/dist/actor-react.d.ts +5 -3
  10. package/dist/actor-react.d.ts.map +1 -1
  11. package/dist/actor-react.js +20 -10
  12. package/dist/actor-react.js.map +1 -1
  13. package/dist/{actor-shared-B5tJfzt-.js → actor-shared-t7w8xIKP.js} +12 -7
  14. package/dist/actor-shared-t7w8xIKP.js.map +1 -0
  15. package/dist/actor.d.ts +1 -1
  16. package/dist/actor.js +42 -23
  17. package/dist/actor.js.map +1 -1
  18. package/dist/{ai-DgOBltJ_.js → ai-BpOGm-zB.js} +621 -216
  19. package/dist/ai-BpOGm-zB.js.map +1 -0
  20. package/dist/{ai-CrEf6p_W.d.ts → ai-DrUaMyRt.d.ts} +114 -100
  21. package/dist/ai-DrUaMyRt.d.ts.map +1 -0
  22. package/dist/ai-client.d.ts +27 -0
  23. package/dist/ai-client.d.ts.map +1 -0
  24. package/dist/ai-client.js +29 -0
  25. package/dist/ai-client.js.map +1 -0
  26. package/dist/ai-fx.browser.d.ts +1 -0
  27. package/dist/ai-fx.browser.js +6 -0
  28. package/dist/ai-fx.browser.js.map +1 -0
  29. package/dist/ai-fx.d.ts +10 -0
  30. package/dist/ai-fx.d.ts.map +1 -0
  31. package/dist/ai-fx.js +634 -0
  32. package/dist/ai-fx.js.map +1 -0
  33. package/dist/ai-harness-BuLh_EYi.js +273 -0
  34. package/dist/ai-harness-BuLh_EYi.js.map +1 -0
  35. package/dist/ai-harness-DvsPeJeB.d.ts +26 -0
  36. package/dist/ai-harness-DvsPeJeB.d.ts.map +1 -0
  37. package/dist/ai-pi.browser.d.ts +1 -0
  38. package/dist/ai-pi.browser.js +6 -0
  39. package/dist/ai-pi.browser.js.map +1 -0
  40. package/dist/ai-pi.d.ts +17 -0
  41. package/dist/ai-pi.d.ts.map +1 -0
  42. package/dist/ai-pi.js +904 -0
  43. package/dist/ai-pi.js.map +1 -0
  44. package/dist/ai-react.d.ts +96 -0
  45. package/dist/ai-react.d.ts.map +1 -0
  46. package/dist/ai-react.js +142 -0
  47. package/dist/ai-react.js.map +1 -0
  48. package/dist/ai-server.browser.js +2 -2
  49. package/dist/ai-server.browser.js.map +1 -1
  50. package/dist/ai-server.d.ts +109 -43
  51. package/dist/ai-server.d.ts.map +1 -1
  52. package/dist/ai-server.js +526 -283
  53. package/dist/ai-server.js.map +1 -1
  54. package/dist/ai.d.ts +2 -2
  55. package/dist/ai.js +2 -2
  56. package/dist/bounded-cache-CPHWF-8h.js +41 -0
  57. package/dist/bounded-cache-CPHWF-8h.js.map +1 -0
  58. package/dist/{client-BYzHjkwU.js → client-BReKqwZC.js} +447 -80
  59. package/dist/client-BReKqwZC.js.map +1 -0
  60. package/dist/{client-CzyacQpJ.d.ts → client-h_DKFAjj.d.ts} +14 -13
  61. package/dist/client-h_DKFAjj.d.ts.map +1 -0
  62. package/dist/client.d.ts +4 -2
  63. package/dist/client.js +1 -1
  64. package/dist/{contract-CKRg_E4q.js → contract-BYKASlCV.js} +12 -21
  65. package/dist/contract-BYKASlCV.js.map +1 -0
  66. package/dist/{idempotent-replay-DVOlyYbx.js → idempotent-replay-JdgMQRRm.js} +2 -2
  67. package/dist/{idempotent-replay-DVOlyYbx.js.map → idempotent-replay-JdgMQRRm.js.map} +1 -1
  68. package/dist/index.d.ts +6 -5
  69. package/dist/index.d.ts.map +1 -1
  70. package/dist/index.js +4 -3
  71. package/dist/{errors-DvhSXnxk.d.ts → initial-snapshot-Bx-c0pDP.d.ts} +7 -3
  72. package/dist/initial-snapshot-Bx-c0pDP.d.ts.map +1 -0
  73. package/dist/{internal-Dq2qYxou.js → internal-B2T3U4Rq.js} +29 -2
  74. package/dist/internal-B2T3U4Rq.js.map +1 -0
  75. package/dist/otel.d.ts +1 -1
  76. package/dist/react-BBXgHWFc.d.ts +86 -0
  77. package/dist/react-BBXgHWFc.d.ts.map +1 -0
  78. package/dist/react.d.ts +2 -122
  79. package/dist/react.js +107 -100
  80. package/dist/react.js.map +1 -1
  81. package/dist/reducer-BAXc8JyF.js +87 -0
  82. package/dist/reducer-BAXc8JyF.js.map +1 -0
  83. package/dist/{reducer-otzHjuJj.d.ts → reducer-CqA59Q6U.d.ts} +46 -30
  84. package/dist/reducer-CqA59Q6U.d.ts.map +1 -0
  85. package/dist/reducer-runtime-DQwz_J59.js +28 -0
  86. package/dist/reducer-runtime-DQwz_J59.js.map +1 -0
  87. package/dist/scheduler-qstash.d.ts +3 -3
  88. package/dist/scheduler-qstash.d.ts.map +1 -1
  89. package/dist/scheduler-qstash.js +33 -10
  90. package/dist/scheduler-qstash.js.map +1 -1
  91. package/dist/{scheduler-task-BpzhPnRS.js → scheduler-task-X1SToDjP.js} +18 -3
  92. package/dist/scheduler-task-X1SToDjP.js.map +1 -0
  93. package/dist/scheduler-vercel.d.ts +4 -9
  94. package/dist/scheduler-vercel.d.ts.map +1 -1
  95. package/dist/scheduler-vercel.js +25 -44
  96. package/dist/scheduler-vercel.js.map +1 -1
  97. package/dist/{server-BD5ckxZb.js → server-Dw_k_7kO.js} +904 -219
  98. package/dist/server-Dw_k_7kO.js.map +1 -0
  99. package/dist/{server-DbD7IJVK.d.ts → server-En7cbi3v.d.ts} +116 -26
  100. package/dist/server-En7cbi3v.d.ts.map +1 -0
  101. package/dist/server.d.ts +4 -4
  102. package/dist/server.js +2 -2
  103. package/dist/session-clear-Bo-XTFEy.d.ts +12 -0
  104. package/dist/session-clear-Bo-XTFEy.d.ts.map +1 -0
  105. package/dist/{errors-DCk6ch5n.js → session-clear-DGI4jTQ5.js} +17 -2
  106. package/dist/session-clear-DGI4jTQ5.js.map +1 -0
  107. package/dist/{store-DyZM6fS5.d.ts → store-Djdv0bnZ.d.ts} +73 -3
  108. package/dist/store-Djdv0bnZ.d.ts.map +1 -0
  109. package/dist/store-N8PXxDAS.js.map +1 -1
  110. package/dist/store-memory.d.ts +1 -1
  111. package/dist/store-memory.d.ts.map +1 -1
  112. package/dist/store-memory.js +205 -66
  113. package/dist/store-memory.js.map +1 -1
  114. package/dist/{store-polling-CmxUbV93.js → store-polling-B5FcwJfo.js} +21 -4
  115. package/dist/store-polling-B5FcwJfo.js.map +1 -0
  116. package/dist/store-postgres.d.ts +1 -1
  117. package/dist/store-postgres.d.ts.map +1 -1
  118. package/dist/store-postgres.js +450 -108
  119. package/dist/store-postgres.js.map +1 -1
  120. package/dist/{store-presence-polling-C7-XZyW9.js → store-presence-polling-BAwS8RWn.js} +3 -3
  121. package/dist/{store-presence-polling-C7-XZyW9.js.map → store-presence-polling-BAwS8RWn.js.map} +1 -1
  122. package/dist/store-redis-http.d.ts +1 -1
  123. package/dist/store-redis-http.d.ts.map +1 -1
  124. package/dist/store-redis-http.js +293 -11
  125. package/dist/store-redis-http.js.map +1 -1
  126. package/dist/store-redis-notify-BRITti_o.js +2493 -0
  127. package/dist/store-redis-notify-BRITti_o.js.map +1 -0
  128. package/dist/store-redis.d.ts +1 -1
  129. package/dist/store-redis.d.ts.map +1 -1
  130. package/dist/store-redis.js +11 -11
  131. package/dist/store-redis.js.map +1 -1
  132. package/dist/store-sqlite.d.ts +1 -1
  133. package/dist/store-sqlite.d.ts.map +1 -1
  134. package/dist/store-sqlite.js +197 -33
  135. package/dist/store-sqlite.js.map +1 -1
  136. package/dist/stream-buffer-Cx7AAPGq.js +23 -0
  137. package/dist/stream-buffer-Cx7AAPGq.js.map +1 -0
  138. package/dist/{telemetry-B5jzpy6y.d.ts → telemetry-Cyfqo6Qt.d.ts} +2 -2
  139. package/dist/telemetry-Cyfqo6Qt.d.ts.map +1 -0
  140. package/dist/{wire-BO5wWCb1.js → wire-rSUglYo8.js} +132 -10
  141. package/dist/wire-rSUglYo8.js.map +1 -0
  142. package/docs/01-quickstart.mdx +50 -84
  143. package/docs/{guides/07-examples.mdx → 02-examples.mdx} +10 -20
  144. package/docs/actors/01-introduction.mdx +48 -82
  145. package/docs/actors/02-concurrency.mdx +43 -70
  146. package/docs/actors/03-timers.mdx +28 -42
  147. package/docs/actors/04-routes.mdx +110 -137
  148. package/docs/actors/meta.ts +1 -1
  149. package/docs/agents/01-introduction.mdx +257 -0
  150. package/docs/agents/02-tools.mdx +247 -0
  151. package/docs/agents/03-conversations.mdx +538 -0
  152. package/docs/agents/04-harnesses.mdx +136 -0
  153. package/docs/agents/meta.ts +1 -0
  154. package/docs/client/01-react.mdx +294 -0
  155. package/docs/client/02-transports.mdx +86 -0
  156. package/docs/client/03-presence.mdx +224 -0
  157. package/docs/client/meta.ts +1 -0
  158. package/docs/contract/01-events.mdx +57 -0
  159. package/docs/contract/02-state.mdx +142 -0
  160. package/docs/contract/03-sessions.mdx +175 -0
  161. package/docs/index.mdx +112 -156
  162. package/docs/reference/01-core.mdx +139 -0
  163. package/docs/reference/02-server.mdx +855 -0
  164. package/docs/reference/03-client.mdx +301 -0
  165. package/docs/reference/04-ai.mdx +722 -0
  166. package/docs/reference/05-adapters.mdx +351 -0
  167. package/docs/reference/06-errors.mdx +138 -0
  168. package/docs/reference/meta.ts +1 -1
  169. package/docs/server/01-handlers.mdx +200 -0
  170. package/docs/server/02-timers.mdx +130 -0
  171. package/docs/server/03-cancellation.mdx +141 -0
  172. package/docs/server/04-routes.mdx +243 -0
  173. package/docs/server/05-production.mdx +322 -0
  174. package/docs/server/06-guarantees.mdx +123 -0
  175. package/examples/playground/DEPLOY.md +53 -7
  176. package/examples/playground/app/a0/[appId]/loading.tsx +16 -0
  177. package/examples/playground/app/a0/[appId]/page.tsx +53 -0
  178. package/examples/playground/app/a0/a0.css +818 -0
  179. package/examples/playground/app/a0/app-list.tsx +218 -0
  180. package/examples/playground/app/a0/app-view.tsx +63 -0
  181. package/examples/playground/app/a0/apps/[appId]/preview/route.ts +27 -0
  182. package/examples/playground/app/a0/apps/[appId]/route.ts +28 -0
  183. package/examples/playground/app/a0/apps.test.ts +128 -0
  184. package/examples/playground/app/a0/apps.ts +102 -0
  185. package/examples/playground/app/a0/chat.tsx +410 -0
  186. package/examples/playground/app/a0/columns.tsx +109 -0
  187. package/examples/playground/app/a0/context-menu.tsx +116 -0
  188. package/examples/playground/app/a0/cookies.ts +46 -0
  189. package/examples/playground/app/a0/crash-on-request.ts +72 -0
  190. package/examples/playground/app/a0/crashes/route.ts +3 -0
  191. package/examples/playground/app/a0/crashes.test.ts +33 -0
  192. package/examples/playground/app/a0/crashes.ts +28 -0
  193. package/examples/playground/app/a0/devtools.tsx +164 -0
  194. package/examples/playground/app/a0/events/route.ts +6 -0
  195. package/examples/playground/app/a0/flags.ts +30 -0
  196. package/examples/playground/app/a0/fx/events/route.ts +9 -0
  197. package/examples/playground/app/a0/fx/scheduler/route.ts +7 -0
  198. package/examples/playground/app/a0/fx/short/events/route.ts +11 -0
  199. package/examples/playground/app/a0/layout.tsx +38 -0
  200. package/examples/playground/app/a0/list/route.ts +10 -0
  201. package/examples/playground/app/a0/model.test.ts +73 -0
  202. package/examples/playground/app/a0/model.ts +95 -0
  203. package/examples/playground/app/a0/no-photon.cjs +4 -0
  204. package/examples/playground/app/a0/no-quickjs.cjs +3 -0
  205. package/examples/playground/app/a0/page.tsx +14 -0
  206. package/examples/playground/app/a0/pi/events/route.ts +9 -0
  207. package/examples/playground/app/a0/pi/scheduler/route.ts +7 -0
  208. package/examples/playground/app/a0/pi/short/events/route.ts +11 -0
  209. package/examples/playground/app/a0/preview-pane.tsx +145 -0
  210. package/examples/playground/app/a0/preview.test.ts +53 -0
  211. package/examples/playground/app/a0/preview.ts +26 -0
  212. package/examples/playground/app/a0/sandbox-log.ts +75 -0
  213. package/examples/playground/app/a0/sandbox-session.ts +152 -0
  214. package/examples/playground/app/a0/sandbox.ts +191 -0
  215. package/examples/playground/app/a0/sandboxes/scheduler/route.ts +6 -0
  216. package/examples/playground/app/a0/scheduler/route.ts +7 -0
  217. package/examples/playground/app/a0/server.ts +213 -0
  218. package/examples/playground/app/a0/session.ts +59 -0
  219. package/examples/playground/app/a0/shell.tsx +158 -0
  220. package/examples/playground/app/a0/short/events/route.ts +9 -0
  221. package/examples/playground/app/a0/splitter.tsx +120 -0
  222. package/examples/playground/app/agent/[agentId]/agent-client.tsx +144 -174
  223. package/examples/playground/app/agent/[agentId]/command-receipt.test.tsx +65 -0
  224. package/examples/playground/app/agent/[agentId]/command-receipt.tsx +27 -0
  225. package/examples/playground/app/agent/[agentId]/compaction-event.tsx +25 -24
  226. package/examples/playground/app/agent/[agentId]/compaction-panel.test.tsx +119 -0
  227. package/examples/playground/app/agent/[agentId]/compaction-panel.tsx +208 -165
  228. package/examples/playground/app/agent/[agentId]/page.tsx +2 -16
  229. package/examples/playground/app/agent/events/route.ts +5 -2
  230. package/examples/playground/app/agent/ingress.test.ts +101 -0
  231. package/examples/playground/app/agent/model-input.test.tsx +58 -0
  232. package/examples/playground/app/agent/model-input.tsx +115 -0
  233. package/examples/playground/app/agent/model.test.ts +73 -0
  234. package/examples/playground/app/agent/model.ts +58 -26
  235. package/examples/playground/app/agent/new-agent-session.tsx +37 -19
  236. package/examples/playground/app/agent/page.tsx +4 -4
  237. package/examples/playground/app/agent/server.ts +19 -15
  238. package/examples/playground/app/agent/session.ts +5 -6
  239. package/examples/playground/app/agent/telemetry.test.ts +189 -0
  240. package/examples/playground/app/agent/telemetry.ts +30 -0
  241. package/examples/playground/app/agent/trace.test.ts +23 -0
  242. package/examples/playground/app/agent/trace.ts +18 -0
  243. package/examples/playground/app/canvas/[canvasId]/canvas-client.tsx +12 -12
  244. package/examples/playground/app/canvas/[canvasId]/page.tsx +1 -2
  245. package/examples/playground/app/canvas/[canvasId]/session.ts +2 -2
  246. package/examples/playground/app/canvas/model.ts +53 -53
  247. package/examples/playground/app/chat/[chatId]/agent-state.test.ts +176 -0
  248. package/examples/playground/app/chat/[chatId]/agent-stream-drawer.tsx +16 -1
  249. package/examples/playground/app/chat/[chatId]/chat-client.tsx +55 -77
  250. package/examples/playground/app/chat/[chatId]/chat-view.ts +37 -3
  251. package/examples/playground/app/chat/[chatId]/page.tsx +7 -15
  252. package/examples/playground/app/chat/[chatId]/session.ts +2 -19
  253. package/examples/playground/app/chat/agent/route.ts +9 -0
  254. package/examples/playground/app/chat/model.test.ts +46 -119
  255. package/examples/playground/app/chat/model.ts +82 -128
  256. package/examples/playground/app/chat/scheduler/route.ts +2 -2
  257. package/examples/playground/app/chat/server.test.ts +112 -0
  258. package/examples/playground/app/chat/server.ts +113 -117
  259. package/examples/playground/app/components/activity-feed.tsx +30 -1
  260. package/examples/playground/app/components/activity-history.test.tsx +35 -0
  261. package/examples/playground/app/components/demo-icons.tsx +89 -0
  262. package/examples/playground/app/components/demos.tsx +33 -0
  263. package/examples/playground/app/components/fast-link.test.tsx +58 -0
  264. package/examples/playground/app/components/fast-link.tsx +55 -0
  265. package/examples/playground/app/components/site-header.tsx +96 -0
  266. package/examples/playground/app/counter/counter-client.tsx +3 -3
  267. package/examples/playground/app/counter/model.test.ts +3 -3
  268. package/examples/playground/app/counter/model.ts +11 -21
  269. package/examples/playground/app/counter/page.tsx +2 -15
  270. package/examples/playground/app/counter/session.ts +2 -2
  271. package/examples/playground/app/documents/[documentId]/code-editor.tsx +12 -1
  272. package/examples/playground/app/documents/[documentId]/document-client.tsx +10 -11
  273. package/examples/playground/app/documents/[documentId]/page.tsx +1 -2
  274. package/examples/playground/app/documents/[documentId]/session.ts +2 -2
  275. package/examples/playground/app/documents/model.ts +27 -27
  276. package/examples/playground/app/globals.css +420 -207
  277. package/examples/playground/app/initial-state.test.tsx +54 -0
  278. package/examples/playground/app/layout.tsx +7 -23
  279. package/examples/playground/app/orders/[orderId]/page.tsx +1 -2
  280. package/examples/playground/app/orders/[orderId]/session.ts +2 -2
  281. package/examples/playground/app/orders/model.ts +49 -49
  282. package/examples/playground/app/page.tsx +13 -102
  283. package/examples/playground/app/recovery/[recoveryId]/page.tsx +1 -2
  284. package/examples/playground/app/recovery/[recoveryId]/session.ts +2 -2
  285. package/examples/playground/app/recovery/model.ts +30 -30
  286. package/examples/playground/instrumentation.ts +5 -0
  287. package/examples/playground/next.config.ts +17 -5
  288. package/examples/playground/package.json +13 -2
  289. package/examples/playground/scripts/a0-template.ts +77 -0
  290. package/examples/playground/vercel.json +36 -0
  291. package/package.json +29 -3
  292. package/src/actor-client.ts +16 -4
  293. package/src/actor-react.ts +25 -11
  294. package/src/actor-shared.ts +21 -6
  295. package/src/actor.ts +76 -31
  296. package/src/ai-agent-actions.ts +219 -0
  297. package/src/ai-agent.ts +2517 -0
  298. package/src/ai-app-state.ts +91 -0
  299. package/src/ai-client-state.ts +6 -3
  300. package/src/ai-client.ts +62 -0
  301. package/src/ai-context-schema.ts +25 -5
  302. package/src/ai-context.ts +90 -40
  303. package/src/ai-control-server.ts +92 -72
  304. package/src/ai-coordinator.ts +23 -22
  305. package/src/ai-fx.browser.ts +5 -0
  306. package/src/ai-fx.ts +833 -0
  307. package/src/ai-harness.ts +518 -0
  308. package/src/ai-message-base.ts +209 -0
  309. package/src/ai-message-projection.ts +262 -64
  310. package/src/ai-model-metadata.ts +11 -6
  311. package/src/ai-pi.browser.ts +5 -0
  312. package/src/ai-pi.ts +1263 -0
  313. package/src/ai-progress-batches.ts +26 -0
  314. package/src/ai-projector.ts +95 -14
  315. package/src/ai-react.ts +146 -0
  316. package/src/ai-sdk-step.ts +2 -2
  317. package/src/ai-server.browser.ts +2 -2
  318. package/src/ai-server.ts +23 -1965
  319. package/src/ai-stored-state.ts +48 -163
  320. package/src/ai.ts +339 -155
  321. package/src/bootstrap-stream.ts +72 -0
  322. package/src/bounded-cache.ts +55 -0
  323. package/src/client-render-session.ts +49 -0
  324. package/src/client.ts +688 -151
  325. package/src/contract.ts +92 -42
  326. package/src/deterministic-id.ts +37 -0
  327. package/src/equal-json.ts +32 -0
  328. package/src/errors.ts +13 -1
  329. package/src/index.ts +5 -1
  330. package/src/initial-snapshot.ts +39 -0
  331. package/src/internal.ts +52 -0
  332. package/src/libfx.d.ts +34 -0
  333. package/src/postgres-notification-scope.ts +14 -3
  334. package/src/react.ts +171 -206
  335. package/src/redis-http-reply.ts +256 -0
  336. package/src/redis-http-stream.ts +73 -0
  337. package/src/redis-http-subscriptions.ts +13 -5
  338. package/src/reducer-runtime.ts +52 -0
  339. package/src/reducer.ts +93 -11
  340. package/src/scheduler-qstash.ts +49 -20
  341. package/src/scheduler-task.ts +34 -2
  342. package/src/scheduler-vercel.ts +32 -84
  343. package/src/server-fetch.ts +139 -35
  344. package/src/server.ts +1133 -255
  345. package/src/session-clear.ts +23 -0
  346. package/src/session-socket.ts +154 -49
  347. package/src/sse.ts +71 -4
  348. package/src/store-memory.ts +252 -75
  349. package/src/store-polling.ts +18 -4
  350. package/src/store-postgres.ts +541 -114
  351. package/src/store-redis-core.ts +1450 -493
  352. package/src/store-redis-http.ts +18 -2
  353. package/src/store-redis-notify.ts +262 -85
  354. package/src/store-redis.ts +19 -9
  355. package/src/store-sqlite.ts +319 -45
  356. package/src/store.ts +79 -1
  357. package/src/stream-buffer.ts +43 -0
  358. package/src/telemetry.ts +14 -1
  359. package/src/wire.ts +241 -13
  360. package/dist/actor-ohPC-81x.d.ts.map +0 -1
  361. package/dist/actor-shared-B5tJfzt-.js.map +0 -1
  362. package/dist/ai-CrEf6p_W.d.ts.map +0 -1
  363. package/dist/ai-DgOBltJ_.js.map +0 -1
  364. package/dist/client-BYzHjkwU.js.map +0 -1
  365. package/dist/client-CzyacQpJ.d.ts.map +0 -1
  366. package/dist/contract-CKRg_E4q.js.map +0 -1
  367. package/dist/errors-DCk6ch5n.js.map +0 -1
  368. package/dist/errors-DvhSXnxk.d.ts.map +0 -1
  369. package/dist/internal-Dq2qYxou.js.map +0 -1
  370. package/dist/react.d.ts.map +0 -1
  371. package/dist/reducer-DEMjEY_O.js +0 -29
  372. package/dist/reducer-DEMjEY_O.js.map +0 -1
  373. package/dist/reducer-otzHjuJj.d.ts.map +0 -1
  374. package/dist/scheduler-task-BpzhPnRS.js.map +0 -1
  375. package/dist/server-BD5ckxZb.js.map +0 -1
  376. package/dist/server-DbD7IJVK.d.ts.map +0 -1
  377. package/dist/store-DyZM6fS5.d.ts.map +0 -1
  378. package/dist/store-polling-CmxUbV93.js.map +0 -1
  379. package/dist/store-redis-notify-BVLUfI3j.js +0 -1583
  380. package/dist/store-redis-notify-BVLUfI3j.js.map +0 -1
  381. package/dist/telemetry-B5jzpy6y.d.ts.map +0 -1
  382. package/dist/validate-XKT4FSNn.js +0 -30
  383. package/dist/validate-XKT4FSNn.js.map +0 -1
  384. package/dist/wire-BO5wWCb1.js.map +0 -1
  385. package/docs/concepts/01-contracts.mdx +0 -142
  386. package/docs/concepts/02-handlers.mdx +0 -284
  387. package/docs/concepts/03-durability.mdx +0 -314
  388. package/docs/concepts/04-state.mdx +0 -209
  389. package/docs/guides/01-timers.mdx +0 -185
  390. package/docs/guides/02-cancellation.mdx +0 -161
  391. package/docs/guides/03-react.mdx +0 -384
  392. package/docs/guides/04-local-first.mdx +0 -88
  393. package/docs/guides/05-production.mdx +0 -521
  394. package/docs/guides/06-ai-agents.mdx +0 -1318
  395. package/docs/guides/08-application-data.mdx +0 -113
  396. package/docs/guides/09-presence.mdx +0 -266
  397. package/docs/guides/10-transports.mdx +0 -221
  398. package/docs/reference/01-api.mdx +0 -1896
  399. package/docs/reference/02-errors.mdx +0 -149
  400. package/examples/playground/app/agent/[agentId]/compaction/route.ts +0 -14
  401. package/examples/playground/app/agent/compaction-settings.test.ts +0 -144
  402. package/examples/playground/app/agent/compaction-settings.ts +0 -49
  403. package/examples/playground/app/agent/compaction-timeline.test.ts +0 -377
  404. package/examples/playground/app/agent/compaction-timeline.ts +0 -208
  405. /package/docs/{concepts → contract}/meta.ts +0 -0
  406. /package/docs/{guides → server}/meta.ts +0 -0
@@ -0,0 +1,2517 @@
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.
4
+ */
5
+
6
+ // oxlint-disable no-await-in-loop -- stream chunks and durable appends are
7
+ // ordered; parallel consumption would corrupt progress sequence and state.
8
+
9
+ import { createControlRuntime, ControlCancelled } from './ai-control-server.ts'
10
+ import {
11
+ createContextReducer,
12
+ contextForAttempt,
13
+ generationBase,
14
+ type AIContextState,
15
+ type AIModelContext,
16
+ } from './ai-context.ts'
17
+ import { isAgentContract } from './ai-app-state.ts'
18
+ import { boundedCache } from './bounded-cache.ts'
19
+ import type { Reducer } from './reducer.ts'
20
+ import { aiEventId, AI_EVENT_ID_PREFIX } from './ai-id.ts'
21
+ import type { ControlCommit, ControlCommand } from './ai-control.ts'
22
+ import { AsyncLocalStorage } from 'node:async_hooks'
23
+ import { createHash } from 'node:crypto'
24
+ import { convertToModelMessages, isToolUIPart } from 'ai'
25
+ import type {
26
+ Experimental_SandboxSession,
27
+ FinishReason,
28
+ Instructions,
29
+ LanguageModelUsage,
30
+ ModelMessage,
31
+ TextStreamPart,
32
+ ToolSet,
33
+ UIMessage,
34
+ UIMessageChunk,
35
+ } from 'ai'
36
+ import { generateAISDKStep, type AISDKStepSettings } from './ai-sdk-step.ts'
37
+ import {
38
+ harnessDriver,
39
+ harnessLog,
40
+ harnessStates,
41
+ registerHarnessStates,
42
+ type Harness,
43
+ type HarnessDriver,
44
+ type HarnessStates,
45
+ } from './ai-harness.ts'
46
+ import { gatewayModelId, readModelLimits } from './ai-model-metadata.ts'
47
+ import { estimateInputTokens, measuredInputTokens } from './ai-input-tokens.ts'
48
+ import { AI_STATE_REDUCER, agentContract, events as aiEvents } from './ai.ts'
49
+ import type {
50
+ AIEventDefs,
51
+ AIMessageOf,
52
+ AIState,
53
+ AgentToolContext,
54
+ AgentToolEventDefs,
55
+ ApprovalRequestedPayload,
56
+ CompactionCompletedPayload,
57
+ CompactionRequestedPayload,
58
+ GenerationCompletedPayload,
59
+ GenerationFailedPayload,
60
+ GenerationProgressPayload,
61
+ GenerationStartedPayload,
62
+ MessageCompletedPayload,
63
+ ReasoningLevel,
64
+ ToolCalledPayload,
65
+ ToolResultPayload,
66
+ } from './ai.ts'
67
+ import type { StandardSchemaV1 } from './standard-schema.ts'
68
+ import type { AICoordinatorState } from './ai-coordinator.ts'
69
+ import type {
70
+ AppendInput,
71
+ Contract,
72
+ ContractEvent,
73
+ EventDefs,
74
+ } from './contract.ts'
75
+ import { A2Error } from './errors.ts'
76
+ import {
77
+ consumeSchedulerSendFailure,
78
+ failedPreviousAttempt,
79
+ installAmbientToolScopeStorage,
80
+ type AmbientToolScope,
81
+ } from './internal.ts'
82
+ import {
83
+ createServer,
84
+ type A2Server,
85
+ type HandlerContext,
86
+ type ServerOptions,
87
+ } from './server.ts'
88
+ import {
89
+ setServerFetchHooks,
90
+ type ServerIngressContext,
91
+ } from './server-fetch.ts'
92
+ import { CONSOLE_TELEMETRY, type A2Telemetry } from './telemetry.ts'
93
+
94
+ const isAgentIngressEvent = (event: {
95
+ type: string
96
+ payload: unknown
97
+ }): boolean => {
98
+ if (event.type !== 'ai.control.requested') return false
99
+ const command = event.payload
100
+ if (typeof command !== 'object' || command === null) return false
101
+ const action = (command as { action?: unknown }).action
102
+ if (action === 'request-input' || action === 'tool-result') return false
103
+ if (action === 'send' || action === 'edit' || action === 'steer')
104
+ return (
105
+ (command as { message?: { role?: unknown } }).message?.role === 'user'
106
+ )
107
+ return true
108
+ }
109
+
110
+ // With a shared contract, browsers push application events like any A2
111
+ // client, under the route's `authorize`. Events declared with `name` have no
112
+ // client that can push them, so they stay server-only.
113
+ const validateAgentIngress = ({
114
+ context,
115
+ applicationEvents,
116
+ }: {
117
+ context: ServerIngressContext
118
+ applicationEvents: boolean
119
+ }): void => {
120
+ const rejected = context.events.find((event) =>
121
+ Object.hasOwn(aiEvents, event.type)
122
+ ? !isAgentIngressEvent(event)
123
+ : !applicationEvents,
124
+ )
125
+ if (rejected !== undefined) {
126
+ throw new A2Error(
127
+ 'INVALID_PAYLOAD',
128
+ `event '${rejected.type}' is server-only for an AI agent`,
129
+ )
130
+ }
131
+ }
132
+
133
+ type AgentGenerationProgress = {
134
+ type: 'progress'
135
+ chunks: UIMessageChunk[]
136
+ }
137
+
138
+ type AgentGenerationFinish = {
139
+ type: 'finish'
140
+ finishReason?: FinishReason
141
+ usage?: LanguageModelUsage
142
+ }
143
+
144
+ type AgentGenerationUpdate = AgentGenerationProgress | AgentGenerationFinish
145
+
146
+ export type AgentGenerateContext<
147
+ M extends UIMessage,
148
+ D extends AIEventDefs<M> & EventDefs,
149
+ T extends ToolSet,
150
+ X extends object = Record<never, never>,
151
+ > = AgentResolverContext<M, D, X> & {
152
+ generationId: string
153
+ modelMessages: ModelMessage[]
154
+ /** The resolved AI Gateway model ID. */
155
+ model: string
156
+ tools: T
157
+ instructions?: Instructions
158
+ generation: AgentGenerationSettings<T>
159
+ }
160
+
161
+ export type AgentGenerate<
162
+ M extends UIMessage,
163
+ D extends AIEventDefs<M> & EventDefs,
164
+ T extends ToolSet,
165
+ X extends object = Record<never, never>,
166
+ > = (
167
+ context: AgentGenerateContext<M, D, T, X>,
168
+ ) => ReadableStream<UIMessageChunk> | Promise<ReadableStream<UIMessageChunk>>
169
+
170
+ type UIMessageMetadata<M extends UIMessage> =
171
+ M extends UIMessage<infer Metadata> ? Metadata : unknown
172
+
173
+ export type AgentMessageMetadata<
174
+ M extends UIMessage,
175
+ D extends AIEventDefs<M> & EventDefs,
176
+ T extends ToolSet,
177
+ X extends object = Record<never, never>,
178
+ > = (
179
+ context: AgentGenerateContext<M, D, T, X> & { part: TextStreamPart<T> },
180
+ ) => UIMessageMetadata<M> | undefined
181
+
182
+ export type AgentResolverContext<
183
+ M extends UIMessage,
184
+ D extends AIEventDefs<M> & EventDefs,
185
+ X extends object = Record<never, never>,
186
+ > = {
187
+ event: ContractEvent<D, 'ai.generation.requested'>
188
+ messages: M[]
189
+ /**
190
+ * The contract's application state, as of the captured snapshot. It rides
191
+ * in the checkpoint the generation already reads, so it costs no I/O.
192
+ */
193
+ state: X
194
+ session: Pick<HandlerContext<D>['session'], 'state'>
195
+ signal: AbortSignal
196
+ }
197
+
198
+ export type Resolvable<T, C> = T | ((context: C) => T | Promise<T>)
199
+
200
+ /** A model with its reasoning level and speed, chosen together. */
201
+ export type ModelSettings = {
202
+ /** An AI Gateway model ID, such as `'anthropic/claude-sonnet-5.5'`. */
203
+ model: string
204
+ /** Overrides `generation.reasoning`. */
205
+ reasoning?: ReasoningLevel
206
+ /** `fast` prefers AI Gateway's faster tier when the model offers one. */
207
+ speed?: 'fast' | 'standard'
208
+ }
209
+
210
+ export type CompactionPolicy<
211
+ M extends UIMessage,
212
+ D extends AIEventDefs<M> & EventDefs,
213
+ X extends object = Record<never, never>,
214
+ > = {
215
+ shouldCompact(
216
+ context: AgentResolverContext<M, D, X> & {
217
+ messages: M[]
218
+ modelMessages: ModelMessage[]
219
+ },
220
+ ): boolean | Promise<boolean>
221
+ compact(
222
+ context: AgentResolverContext<M, D, X> & {
223
+ messages: M[]
224
+ modelMessages: ModelMessage[]
225
+ },
226
+ ): M[] | Promise<M[]>
227
+ }
228
+
229
+ export type AutomaticCompaction = {
230
+ thresholdTokens?: number
231
+ instructions?: string
232
+ }
233
+
234
+ export type CompactionOptions<
235
+ M extends UIMessage,
236
+ D extends AIEventDefs<M> & EventDefs,
237
+ X extends object = Record<never, never>,
238
+ > = false | AutomaticCompaction | CompactionPolicy<M, D, X>
239
+
240
+ export type AgentGenerationSettings<T extends ToolSet> = AISDKStepSettings<T>
241
+
242
+ export type HandlersOptions<
243
+ M extends UIMessage,
244
+ D extends AIEventDefs<M> & EventDefs,
245
+ T extends ToolSet = ToolSet,
246
+ X extends object = Record<never, never>,
247
+ > = AgentModelOptions<M, D, T, X> & {
248
+ contract: Contract<D, Record<never, never>, unknown>
249
+ telemetry?: A2Telemetry
250
+ /** Where harness steps keep their saved states; one per agent. */
251
+ harnessStates?: HarnessStates
252
+ }
253
+
254
+ /** What a tool call does when a crash or a cancellation may have cut it short. */
255
+ export type ToolRecovery = 'rerun' | 'report'
256
+
257
+ /**
258
+ * The AI SDK harness, the default: one AI SDK `streamText()` step per
259
+ * durable generation, with A2 running tools, approvals, and compaction.
260
+ */
261
+ export type AISDKHarness<
262
+ M extends UIMessage = UIMessage,
263
+ D extends AIEventDefs<M> & EventDefs = AIEventDefs<M> & EventDefs,
264
+ T extends ToolSet = ToolSet,
265
+ X extends object = Record<never, never>,
266
+ > = {
267
+ type: 'ai-sdk'
268
+ /** Settings for each one-step AI SDK streamText call, resolved per generation. */
269
+ generation?: Resolvable<
270
+ AgentGenerationSettings<T>,
271
+ AgentResolverContext<M, D, X>
272
+ >
273
+ /** Map default AI SDK stream parts to typed assistant message metadata. */
274
+ messageMetadata?: AgentMessageMetadata<M, D, T, X>
275
+ /**
276
+ * Replace model generation with an AI SDK UI message chunk stream.
277
+ * The custom stream owns its metadata chunks.
278
+ */
279
+ generate?: AgentGenerate<M, D, T, X>
280
+ compaction?: Resolvable<
281
+ CompactionOptions<M, D, X>,
282
+ AgentResolverContext<M, D, X>
283
+ >
284
+ }
285
+
286
+ type AgentLoopOptions<
287
+ M extends UIMessage,
288
+ D extends AIEventDefs<M> & EventDefs,
289
+ T extends ToolSet,
290
+ X extends object,
291
+ H extends string,
292
+ > =
293
+ | {
294
+ /** The agent's loop: the AI SDK's (the default), or `fx()` or `pi()`. */
295
+ harness?: AISDKHarness<M, D, T, X>
296
+ /** Per tool name: rerun an interrupted call (the default) or report it to the model. */
297
+ toolRecovery?: Readonly<
298
+ Partial<Record<NoInfer<keyof T & string>, ToolRecovery>>
299
+ >
300
+ }
301
+ | {
302
+ harness: Harness<'fx' | 'pi', H>
303
+ /** Per tool name, the harness's own tools included: rerun an interrupted call (the default) or report it. */
304
+ toolRecovery?: Readonly<
305
+ Partial<Record<NoInfer<(keyof T & string) | H>, ToolRecovery>>
306
+ >
307
+ }
308
+
309
+ export type AgentModelOptions<
310
+ M extends UIMessage,
311
+ D extends AIEventDefs<M> & EventDefs,
312
+ T extends ToolSet = ToolSet,
313
+ X extends object = Record<never, never>,
314
+ /** The tool names a harness from `fx()` or `pi()` brings. */
315
+ H extends string = string,
316
+ > = {
317
+ /**
318
+ * An AI Gateway model ID, or `ModelSettings` to choose its reasoning level
319
+ * and speed with it, optionally resolved per generation.
320
+ */
321
+ model: Resolvable<string | ModelSettings, AgentResolverContext<M, D, X>>
322
+ tools?: T
323
+ instructions?: Resolvable<
324
+ Instructions | undefined,
325
+ AgentResolverContext<M, D, X>
326
+ >
327
+ /**
328
+ * Maximum model steps in one assistant response. Defaults to Infinity.
329
+ */
330
+ maxSteps?: number
331
+ /** Progress is durably flushed at either limit, whichever is reached first. */
332
+ progress?: { maxChunks?: number; maxDelayMs?: number }
333
+ /**
334
+ * Run local tool executions one at a time on this lane. By default,
335
+ * independent tools run concurrently.
336
+ */
337
+ toolLane?: string
338
+ } & AgentLoopOptions<M, D, T, X, H>
339
+
340
+ type AgentServerOptions<
341
+ M extends UIMessage,
342
+ D extends AIEventDefs<M> & EventDefs,
343
+ S,
344
+ T extends ToolSet,
345
+ X extends object = Record<never, never>,
346
+ H extends string = string,
347
+ > = 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
+ }
355
+
356
+ export type AgentOptions<
357
+ M extends UIMessage,
358
+ E extends EventDefs,
359
+ T extends ToolSet,
360
+ H extends string = string,
361
+ > = AgentServerOptions<
362
+ NoInfer<M>,
363
+ AIEventDefs<NoInfer<M>> & E,
364
+ AIState<NoInfer<M>>,
365
+ T,
366
+ Record<never, never>,
367
+ H
368
+ > & {
369
+ /** Identity: prefixes storage keys and queue messages. */
370
+ name: string
371
+ /**
372
+ * Server-only application events. Browsers push application events through
373
+ * a shared `agentContract()`.
374
+ */
375
+ events?: E
376
+ /** Optional stricter schema for custom UIMessage metadata/data/tool types. */
377
+ messageSchema?: StandardSchemaV1<M>
378
+ }
379
+
380
+ export type AgentFromContractOptions<
381
+ D extends AIEventDefs<AIMessageOf<D>> & EventDefs,
382
+ S,
383
+ T extends ToolSet,
384
+ Name extends string = string,
385
+ X extends object = Record<never, never>,
386
+ H extends string = string,
387
+ > = AgentServerOptions<AIMessageOf<D>, D, S, T, X, H> & {
388
+ /**
389
+ * A shared `agentContract()`. Its application state reaches every resolver
390
+ * as `state`; a fully custom contract is core A2, served by `createServer()`.
391
+ */
392
+ contract: Contract<
393
+ D,
394
+ Record<never, never>,
395
+ Reducer<D, S> & { readonly name: Name; readonly '~appState': X }
396
+ >
397
+ }
398
+
399
+ /**
400
+ * An A2 server for the AI protocol; `typeof` it types `useAgent`. `Name` is
401
+ * its reducer's name: the built-in one lets contract-free clients fold state.
402
+ */
403
+ export type Agent<
404
+ D extends EventDefs,
405
+ S,
406
+ Name extends string = string,
407
+ > = A2Server<D, Record<never, never>, Reducer<D, S> & { readonly name: Name }>
408
+
409
+ const checkAbort = (signal: AbortSignal): boolean => {
410
+ if (!signal.aborted) return false
411
+ const reason: unknown = signal.reason
412
+ if (
413
+ reason instanceof A2Error &&
414
+ (reason.code === 'CLAIM_EXPIRED' || reason.code === 'SUPERSEDED_ATTEMPT')
415
+ ) {
416
+ throw reason
417
+ }
418
+ return true
419
+ }
420
+
421
+ const resolve = async <T, C extends { signal: AbortSignal }>(
422
+ value: Resolvable<T, C>,
423
+ context: C,
424
+ ): Promise<T> => {
425
+ let result: T
426
+ try {
427
+ result =
428
+ typeof value === 'function'
429
+ ? await (value as (context: C) => T | Promise<T>)(context)
430
+ : value
431
+ } catch (error) {
432
+ checkAbort(context.signal)
433
+ throw error
434
+ }
435
+ checkAbort(context.signal)
436
+ return result
437
+ }
438
+
439
+ const withModelSettings = <T extends ToolSet>(options: {
440
+ generation: AgentGenerationSettings<T>
441
+ settings: ModelSettings
442
+ }): AgentGenerationSettings<T> => {
443
+ const { generation, settings } = options
444
+ if (settings.reasoning === undefined && settings.speed === undefined)
445
+ return generation
446
+ const { speed: _speed, ...gateway } =
447
+ generation.providerOptions?.['gateway'] ?? {}
448
+ return {
449
+ ...generation,
450
+ ...(settings.reasoning === undefined
451
+ ? {}
452
+ : { reasoning: settings.reasoning }),
453
+ ...(settings.speed === undefined
454
+ ? {}
455
+ : {
456
+ providerOptions: {
457
+ ...generation.providerOptions,
458
+ gateway:
459
+ settings.speed === 'fast'
460
+ ? { ...gateway, speed: 'fast' }
461
+ : gateway,
462
+ },
463
+ }),
464
+ }
465
+ }
466
+
467
+ const instructionTexts = (instructions: Instructions | undefined): string[] =>
468
+ instructions === undefined
469
+ ? []
470
+ : typeof instructions === 'string'
471
+ ? [instructions]
472
+ : (Array.isArray(instructions) ? instructions : [instructions]).map(
473
+ (message) => message.content,
474
+ )
475
+
476
+ const errorMessage = (error: unknown): string =>
477
+ error instanceof Error ? error.message : String(error)
478
+
479
+ const toolScopeStorage = installAmbientToolScopeStorage(
480
+ () => new AsyncLocalStorage<AmbientToolScope>(),
481
+ )
482
+
483
+ /** Run `fn` with `ctx` as the ambient tool handler context, for direct tool tests. */
484
+ export function runWithHandlerContext<D extends AgentToolEventDefs, R>(
485
+ ctx: AgentToolContext<D>,
486
+ fn: () => R,
487
+ ): R {
488
+ return toolScopeStorage.run({ context: ctx }, fn)
489
+ }
490
+
491
+ /** Sessions whose latest step prompt one process keeps for that step's tools. */
492
+ const PROMPT_SESSIONS = 64
493
+
494
+ const asyncIterator = (value: unknown): AsyncIterator<unknown> | undefined => {
495
+ if (typeof value !== 'object' || value === null) return undefined
496
+ const factory = Reflect.get(value, Symbol.asyncIterator)
497
+ if (typeof factory !== 'function') return undefined
498
+ return (factory as (this: object) => AsyncIterator<unknown>).call(value)
499
+ }
500
+
501
+ const isBoundaryChunk = (chunk: UIMessageChunk | undefined): boolean =>
502
+ chunk !== undefined &&
503
+ (chunk.type === 'text-end' ||
504
+ chunk.type === 'reasoning-end' ||
505
+ chunk.type === 'tool-input-available' ||
506
+ chunk.type === 'tool-input-error' ||
507
+ chunk.type === 'tool-output-available' ||
508
+ chunk.type === 'tool-output-error' ||
509
+ chunk.type === 'tool-output-denied' ||
510
+ chunk.type === 'tool-approval-request' ||
511
+ chunk.type === 'finish' ||
512
+ chunk.type === 'abort' ||
513
+ chunk.type === 'error')
514
+
515
+ type DeltaChunk = Extract<
516
+ UIMessageChunk,
517
+ { type: 'text-delta' | 'reasoning-delta' | 'tool-input-delta' }
518
+ >
519
+
520
+ const DELTA_KEYS: Record<DeltaChunk['type'], ReadonlySet<string>> = {
521
+ 'text-delta': new Set(['type', 'id', 'delta', 'providerMetadata']),
522
+ 'reasoning-delta': new Set(['type', 'id', 'delta', 'providerMetadata']),
523
+ 'tool-input-delta': new Set(['type', 'toolCallId', 'inputTextDelta']),
524
+ }
525
+
526
+ const isDeltaChunk = (chunk: UIMessageChunk): chunk is DeltaChunk =>
527
+ Object.hasOwn(DELTA_KEYS, chunk.type) &&
528
+ Object.keys(chunk).every((key) =>
529
+ DELTA_KEYS[chunk.type as DeltaChunk['type']].has(key),
530
+ ) &&
531
+ typeof (chunk.type === 'tool-input-delta'
532
+ ? chunk.inputTextDelta
533
+ : (chunk as { delta?: unknown }).delta) === 'string'
534
+
535
+ // Adjacent deltas of one part project exactly as their concatenation: the text
536
+ // appends and the last defined `providerMetadata` wins.
537
+ const joinDeltas = (
538
+ left: DeltaChunk,
539
+ right: DeltaChunk,
540
+ ): DeltaChunk | undefined => {
541
+ if (left.type === 'tool-input-delta')
542
+ return right.type === 'tool-input-delta' &&
543
+ right.toolCallId === left.toolCallId
544
+ ? { ...left, inputTextDelta: left.inputTextDelta + right.inputTextDelta }
545
+ : undefined
546
+ if (
547
+ right.type === 'tool-input-delta' ||
548
+ right.type !== left.type ||
549
+ right.id !== left.id
550
+ )
551
+ return undefined
552
+ const providerMetadata =
553
+ right.providerMetadata !== undefined
554
+ ? right.providerMetadata
555
+ : left.providerMetadata
556
+ return {
557
+ ...left,
558
+ delta: left.delta + right.delta,
559
+ ...(providerMetadata === undefined ? {} : { providerMetadata }),
560
+ }
561
+ }
562
+
563
+ const mergeDeltas = (chunks: UIMessageChunk[]): UIMessageChunk[] => {
564
+ const merged: UIMessageChunk[] = []
565
+ for (const chunk of chunks) {
566
+ const previous = merged.at(-1)
567
+ const joined =
568
+ previous !== undefined && isDeltaChunk(previous) && isDeltaChunk(chunk)
569
+ ? joinDeltas(previous, chunk)
570
+ : undefined
571
+ if (joined === undefined) merged.push(chunk)
572
+ else merged[merged.length - 1] = joined
573
+ }
574
+ return merged
575
+ }
576
+
577
+ type StreamRead<T> = Awaited<ReturnType<ReadableStreamDefaultReader<T>['read']>>
578
+
579
+ const nextOrTimer = async <T>(
580
+ next: Promise<StreamRead<T>>,
581
+ delayMs: number,
582
+ ): Promise<{ type: 'next'; result: StreamRead<T> } | { type: 'timer' }> => {
583
+ let timer: ReturnType<typeof setTimeout> | undefined
584
+ try {
585
+ return await Promise.race([
586
+ next.then((result) => ({ type: 'next' as const, result })),
587
+ new Promise<{ type: 'timer' }>((finishTimer) => {
588
+ timer = setTimeout(() => finishTimer({ type: 'timer' }), delayMs)
589
+ ;(timer as { unref?: () => void }).unref?.()
590
+ }),
591
+ ])
592
+ } finally {
593
+ if (timer !== undefined) clearTimeout(timer)
594
+ }
595
+ }
596
+
597
+ type GenerationCompletion = {
598
+ finishReason?: FinishReason
599
+ usage?: LanguageModelUsage
600
+ }
601
+
602
+ type GenerationSource = {
603
+ stream: ReadableStream<UIMessageChunk>
604
+ completion?: () => GenerationCompletion
605
+ }
606
+
607
+ async function generateWithAISDK<
608
+ M extends UIMessage,
609
+ D extends AIEventDefs<M> & EventDefs,
610
+ T extends ToolSet,
611
+ X extends object,
612
+ >(options: {
613
+ agentName: string
614
+ context: AgentGenerateContext<M, D, T, X>
615
+ messageMetadata: AgentMessageMetadata<M, D, T, X> | undefined
616
+ }): Promise<GenerationSource> {
617
+ const { agentName, context, messageMetadata } = options
618
+ const headers = context.generation.headers ?? {}
619
+ const generation =
620
+ gatewayModelId(context.model) === undefined
621
+ ? context.generation
622
+ : {
623
+ ...context.generation,
624
+ headers: Object.keys(headers).some(
625
+ (key) => key.toLowerCase() === 'x-session-affinity',
626
+ )
627
+ ? headers
628
+ : {
629
+ ...headers,
630
+ 'x-session-affinity': createHash('sha256')
631
+ .update(JSON.stringify([agentName, context.event.sessionId]))
632
+ .digest('hex'),
633
+ },
634
+ providerOptions: {
635
+ ...context.generation.providerOptions,
636
+ gateway: {
637
+ caching: 'auto',
638
+ ...context.generation.providerOptions?.['gateway'],
639
+ },
640
+ },
641
+ }
642
+ const step = await generateAISDKStep<M, T>({
643
+ model: context.model,
644
+ tools: context.tools,
645
+ messages: context.messages,
646
+ modelMessages: context.modelMessages,
647
+ responseMessageId: context.event.payload.responseMessageId,
648
+ abortSignal: context.signal,
649
+ ...(context.instructions === undefined
650
+ ? {}
651
+ : { instructions: context.instructions }),
652
+ settings: generation,
653
+ ...(messageMetadata === undefined
654
+ ? {}
655
+ : {
656
+ messageMetadata: ({ part }) =>
657
+ messageMetadata({ ...context, generation, part }),
658
+ }),
659
+ })
660
+ return {
661
+ stream: step.stream as ReadableStream<UIMessageChunk>,
662
+ completion: step.completion,
663
+ }
664
+ }
665
+
666
+ async function* consumeGeneration(options: {
667
+ source: GenerationSource
668
+ progress?: { maxChunks?: number; maxDelayMs?: number }
669
+ }): AsyncGenerator<AgentGenerationUpdate> {
670
+ const pending: UIMessageChunk[] = []
671
+ let streamedFinishReason: FinishReason | undefined
672
+ let streamedError: string | undefined
673
+ // Flush as soon as the previous write finishes: chunks that arrive while
674
+ // it is in flight wait in the stream and share the next batch.
675
+ const maxChunks = options.progress?.maxChunks ?? 128
676
+ const maxDelayMs = options.progress?.maxDelayMs ?? 0
677
+ let lastFlush = Date.now()
678
+ const batch = (): AgentGenerationProgress => ({
679
+ type: 'progress',
680
+ chunks: mergeDeltas(pending.splice(0)),
681
+ })
682
+ const reader = options.source.stream.getReader()
683
+ let pendingChunk = reader.read()
684
+ try {
685
+ for (;;) {
686
+ let result: StreamRead<UIMessageChunk>
687
+ if (pending.length > 0) {
688
+ const remaining = Math.max(0, maxDelayMs - (Date.now() - lastFlush))
689
+ const outcome = await nextOrTimer(pendingChunk, remaining)
690
+ if (outcome.type === 'timer') {
691
+ yield batch()
692
+ lastFlush = Date.now()
693
+ continue
694
+ }
695
+ result = outcome.result
696
+ } else {
697
+ result = await pendingChunk
698
+ }
699
+ if (result.done) break
700
+ const chunk = result.value
701
+ pending.push(chunk)
702
+ if (chunk.type === 'finish') streamedFinishReason = chunk.finishReason
703
+ if (chunk.type === 'error') streamedError = chunk.errorText
704
+ pendingChunk = reader.read()
705
+
706
+ if (pending.length >= maxChunks || isBoundaryChunk(pending.at(-1))) {
707
+ yield batch()
708
+ lastFlush = Date.now()
709
+ }
710
+ }
711
+ } catch (error) {
712
+ if (pending.length > 0) yield batch()
713
+ throw error
714
+ } finally {
715
+ void pendingChunk.catch(() => {})
716
+ try {
717
+ await reader.cancel()
718
+ } catch {
719
+ // Cleanup must not replace the error that ended generation.
720
+ } finally {
721
+ reader.releaseLock()
722
+ }
723
+ }
724
+
725
+ let completion: GenerationCompletion | undefined
726
+ try {
727
+ completion = options.source.completion?.()
728
+ } catch (error) {
729
+ if (pending.length > 0) {
730
+ yield batch()
731
+ }
732
+ throw error
733
+ }
734
+ if (pending.length > 0) {
735
+ yield batch()
736
+ }
737
+ if (streamedError !== undefined) throw new Error(streamedError)
738
+ const finishReason = completion?.finishReason ?? streamedFinishReason
739
+ yield {
740
+ type: 'finish',
741
+ ...(finishReason === undefined ? {} : { finishReason }),
742
+ ...(completion?.usage === undefined ? {} : { usage: completion.usage }),
743
+ }
744
+ }
745
+
746
+ const foldAIEvents = <
747
+ M extends UIMessage,
748
+ D extends AIEventDefs<M> & EventDefs,
749
+ >(options: {
750
+ reducer: Reducer<D, AIContextState<M>>
751
+ state: AIContextState<M>
752
+ events: ContractEvent<D>[]
753
+ }): AIContextState<M> => {
754
+ let state = options.state
755
+ for (const event of options.events) state = options.reducer.fold(state, event)
756
+ return state
757
+ }
758
+
759
+ const summarize = async <
760
+ M extends UIMessage,
761
+ D extends AIEventDefs<M> & EventDefs,
762
+ T extends ToolSet,
763
+ >(options: {
764
+ agentName: string
765
+ context: AgentGenerateContext<M, D, T>
766
+ instructions: string | undefined
767
+ }): Promise<{ summary: string; usage?: LanguageModelUsage }> => {
768
+ const instruction = [
769
+ 'Summarize this conversation so you can continue the task after the earlier context is replaced by your summary.',
770
+ 'Preserve the objective, constraints, decisions, exact identifiers, completed actions and their results, unresolved questions, and next steps. Distinguish observations from assumptions.',
771
+ 'Respond only with the summary as text. Do not call any tools or continue working on the task.',
772
+ options.instructions,
773
+ ]
774
+ .filter(Boolean)
775
+ .join('\n')
776
+ const generation = { ...options.context.generation }
777
+ for (const key of Object.keys(generation)) {
778
+ if (
779
+ key.startsWith('on') ||
780
+ key.startsWith('experimental_on') ||
781
+ [
782
+ 'repairToolCall',
783
+ 'experimental_repairToolCall',
784
+ 'experimental_refineToolInput',
785
+ 'experimental_transform',
786
+ 'toolApproval',
787
+ ].includes(key)
788
+ ) {
789
+ Reflect.deleteProperty(generation, key)
790
+ }
791
+ }
792
+ const tools = Object.fromEntries(
793
+ Object.entries(options.context.tools).map(([name, tool]) => {
794
+ const definition = { ...tool, needsApproval: false }
795
+ for (const key of [
796
+ 'execute',
797
+ 'onInputStart',
798
+ 'onInputDelta',
799
+ 'onInputAvailable',
800
+ ]) {
801
+ Reflect.deleteProperty(definition, key)
802
+ }
803
+ return [name, definition]
804
+ }),
805
+ ) as T
806
+ const source = await generateWithAISDK({
807
+ agentName: options.agentName,
808
+ context: {
809
+ ...options.context,
810
+ generation,
811
+ tools,
812
+ modelMessages: [
813
+ ...options.context.modelMessages,
814
+ { role: 'user', content: instruction },
815
+ ],
816
+ },
817
+ messageMetadata: undefined,
818
+ })
819
+ let summary = ''
820
+ let calledTool = false
821
+ let finish: AgentGenerationFinish | undefined
822
+ for await (const update of consumeGeneration({ source })) {
823
+ options.context.signal.throwIfAborted()
824
+ if (update.type === 'finish') {
825
+ finish = update
826
+ continue
827
+ }
828
+ for (const chunk of update.chunks) {
829
+ if (chunk.type === 'text-delta') summary += chunk.delta
830
+ if (chunk.type.startsWith('tool-')) calledTool = true
831
+ }
832
+ }
833
+ if (
834
+ calledTool ||
835
+ summary.trim().length === 0 ||
836
+ finish?.finishReason !== 'stop'
837
+ ) {
838
+ throw new Error(
839
+ 'compaction must produce a complete text summary without calling tools',
840
+ )
841
+ }
842
+ return {
843
+ summary,
844
+ ...(finish.usage === undefined ? {} : { usage: finish.usage }),
845
+ }
846
+ }
847
+
848
+ const activeCompaction = <M extends UIMessage>(
849
+ state: AIModelContext<M>,
850
+ ): AIModelContext<M>['compaction'] => {
851
+ const compaction = state.compaction
852
+ return compaction?.status === 'completed' ? compaction : null
853
+ }
854
+
855
+ const contextMessages = <M extends UIMessage>({
856
+ state,
857
+ coordinator,
858
+ }: {
859
+ state: AIModelContext<M>
860
+ coordinator: AICoordinatorState
861
+ }): M[] => {
862
+ const queued = new Set(coordinator.queued.map((item) => item.messageId))
863
+ return state.messages.filter((message) => !queued.has(message.id))
864
+ }
865
+
866
+ const modelContext = async <M extends UIMessage>(options: {
867
+ messages: M[]
868
+ state: AIModelContext<M>
869
+ tools: ToolSet
870
+ }): Promise<ModelMessage[]> => {
871
+ const messages = await convertToModelMessages(options.messages, {
872
+ tools: options.tools,
873
+ })
874
+ const summary = activeCompaction(options.state)?.summary
875
+ return summary === undefined
876
+ ? messages
877
+ : [{ role: 'user', content: summary }, ...messages]
878
+ }
879
+
880
+ type ToolCallClassification = 'automatic' | 'approval' | 'unknown'
881
+
882
+ type PendingToolCall = {
883
+ call: ToolCalledPayload
884
+ classification: ToolCallClassification
885
+ }
886
+
887
+ const toolCalledEvent = async <D extends EventDefs>(
888
+ payload: ToolCalledPayload,
889
+ ): Promise<AppendInput<D>> =>
890
+ ({
891
+ type: 'ai.tool.called',
892
+ id: await aiEventId(
893
+ 'a2.ai.tool.called',
894
+ payload.generationId,
895
+ payload.toolCallId,
896
+ ),
897
+ payload,
898
+ }) as AppendInput<D>
899
+
900
+ const approvalClassification = <T extends ToolSet>(
901
+ tools: T,
902
+ generation: AgentGenerationSettings<T>,
903
+ toolName: string,
904
+ ): ToolCallClassification => {
905
+ const configured = generation.toolApproval
906
+ if (typeof configured === 'function') return 'unknown'
907
+ const policy = configured?.[toolName]
908
+ if (typeof policy === 'function') return 'unknown'
909
+ if (
910
+ policy === 'user-approval' ||
911
+ (typeof policy === 'object' && policy?.type === 'user-approval')
912
+ ) {
913
+ return 'approval'
914
+ }
915
+ if (
916
+ policy === 'denied' ||
917
+ (typeof policy === 'object' && policy?.type === 'denied')
918
+ ) {
919
+ return 'approval'
920
+ }
921
+ const tool = tools[toolName] as { needsApproval?: unknown } | undefined
922
+ if (typeof tool?.needsApproval === 'function') return 'unknown'
923
+ return tool?.needsApproval === true ? 'approval' : 'automatic'
924
+ }
925
+
926
+ const lifecycleEvents = async <
927
+ D extends EventDefs,
928
+ T extends ToolSet,
929
+ >(options: {
930
+ requestId: string
931
+ messageId: string
932
+ generationId: string
933
+ sequence: number
934
+ chunks: UIMessageChunk[]
935
+ pending: PendingToolCall[]
936
+ tools: T
937
+ generation: AgentGenerationSettings<T>
938
+ /** A harness's tool calls run without approval. */
939
+ approvals?: boolean
940
+ }): Promise<{ events: AppendInput<D>[]; pending: PendingToolCall[] }> => {
941
+ const result: AppendInput<D>[] = []
942
+ const pending = [...options.pending]
943
+ const take = (toolCallId: string): PendingToolCall | undefined => {
944
+ const index = pending.findIndex(
945
+ (candidate) => candidate.call.toolCallId === toolCallId,
946
+ )
947
+ if (index === -1) return undefined
948
+ return pending.splice(index, 1)[0]
949
+ }
950
+ const flush = async (toolCallId: string): Promise<void> => {
951
+ const pendingCall = take(toolCallId)
952
+ if (pendingCall) result.push(await toolCalledEvent<D>(pendingCall.call))
953
+ }
954
+ for (const [chunkIndex, chunk] of options.chunks.entries()) {
955
+ const resultId = (): Promise<string> =>
956
+ aiEventId(
957
+ 'a2.ai.tool.stream-result',
958
+ options.generationId,
959
+ String(options.sequence),
960
+ String(chunkIndex),
961
+ )
962
+ if (chunk.type === 'tool-input-available') {
963
+ const payload: ToolCalledPayload = {
964
+ requestId: options.requestId,
965
+ messageId: options.messageId,
966
+ generationId: options.generationId,
967
+ toolCallId: chunk.toolCallId,
968
+ toolName: chunk.toolName,
969
+ input: chunk.input,
970
+ ...(chunk.dynamic === undefined ? {} : { dynamic: chunk.dynamic }),
971
+ ...(chunk.providerExecuted === undefined
972
+ ? {}
973
+ : { providerExecuted: chunk.providerExecuted }),
974
+ ...((
975
+ options.tools[chunk.toolName] as
976
+ { supportsDeferredResults?: boolean } | undefined
977
+ )?.supportsDeferredResults === true
978
+ ? { supportsDeferredResults: true }
979
+ : {}),
980
+ ...(chunk.providerMetadata === undefined
981
+ ? {}
982
+ : { providerMetadata: chunk.providerMetadata }),
983
+ ...(chunk.toolMetadata === undefined
984
+ ? {}
985
+ : { toolMetadata: chunk.toolMetadata }),
986
+ ...(chunk.title === undefined ? {} : { title: chunk.title }),
987
+ }
988
+ take(payload.toolCallId)
989
+ const classification =
990
+ options.approvals === false
991
+ ? 'automatic'
992
+ : approvalClassification(
993
+ options.tools,
994
+ options.generation,
995
+ chunk.toolName,
996
+ )
997
+ if (classification === 'automatic') {
998
+ result.push(await toolCalledEvent<D>(payload))
999
+ } else {
1000
+ pending.push({ call: payload, classification })
1001
+ }
1002
+ } else if (chunk.type === 'tool-input-error') {
1003
+ const payload: ToolResultPayload = {
1004
+ requestId: options.requestId,
1005
+ messageId: options.messageId,
1006
+ generationId: options.generationId,
1007
+ toolCallId: chunk.toolCallId,
1008
+ toolName: chunk.toolName,
1009
+ ...(chunk.dynamic ? { input: chunk.input } : { rawInput: chunk.input }),
1010
+ error: chunk.errorText,
1011
+ phase: 'input',
1012
+ ...(chunk.dynamic === undefined ? {} : { dynamic: chunk.dynamic }),
1013
+ ...(chunk.providerExecuted === undefined
1014
+ ? {}
1015
+ : { providerExecuted: chunk.providerExecuted }),
1016
+ ...(chunk.providerMetadata === undefined
1017
+ ? {}
1018
+ : { providerMetadata: chunk.providerMetadata }),
1019
+ ...(chunk.toolMetadata === undefined
1020
+ ? {}
1021
+ : { toolMetadata: chunk.toolMetadata }),
1022
+ }
1023
+ const called: ToolCalledPayload = {
1024
+ requestId: options.requestId,
1025
+ messageId: options.messageId,
1026
+ generationId: options.generationId,
1027
+ toolCallId: chunk.toolCallId,
1028
+ toolName: chunk.toolName,
1029
+ input: chunk.input,
1030
+ }
1031
+ take(chunk.toolCallId)
1032
+ result.push(await toolCalledEvent<D>(called), {
1033
+ type: 'ai.tool.result',
1034
+ id: await resultId(),
1035
+ payload,
1036
+ } as AppendInput<D>)
1037
+ } else if (chunk.type === 'tool-output-available') {
1038
+ await flush(chunk.toolCallId)
1039
+ const payload: ToolResultPayload = {
1040
+ requestId: options.requestId,
1041
+ messageId: options.messageId,
1042
+ generationId: options.generationId,
1043
+ toolCallId: chunk.toolCallId,
1044
+ output: chunk.output,
1045
+ phase: 'execution',
1046
+ ...(chunk.preliminary === undefined
1047
+ ? {}
1048
+ : { preliminary: chunk.preliminary }),
1049
+ ...(chunk.dynamic === undefined ? {} : { dynamic: chunk.dynamic }),
1050
+ ...(chunk.providerExecuted === undefined
1051
+ ? {}
1052
+ : { providerExecuted: chunk.providerExecuted }),
1053
+ ...(chunk.providerMetadata === undefined
1054
+ ? {}
1055
+ : { providerMetadata: chunk.providerMetadata }),
1056
+ ...(chunk.toolMetadata === undefined
1057
+ ? {}
1058
+ : { toolMetadata: chunk.toolMetadata }),
1059
+ }
1060
+ result.push({
1061
+ type: 'ai.tool.result',
1062
+ id: await resultId(),
1063
+ payload,
1064
+ } as AppendInput<D>)
1065
+ } else if (chunk.type === 'tool-output-error') {
1066
+ await flush(chunk.toolCallId)
1067
+ const payload: ToolResultPayload = {
1068
+ requestId: options.requestId,
1069
+ messageId: options.messageId,
1070
+ generationId: options.generationId,
1071
+ toolCallId: chunk.toolCallId,
1072
+ error: chunk.errorText,
1073
+ phase: 'execution',
1074
+ ...(chunk.dynamic === undefined ? {} : { dynamic: chunk.dynamic }),
1075
+ ...(chunk.providerExecuted === undefined
1076
+ ? {}
1077
+ : { providerExecuted: chunk.providerExecuted }),
1078
+ ...(chunk.providerMetadata === undefined
1079
+ ? {}
1080
+ : { providerMetadata: chunk.providerMetadata }),
1081
+ ...(chunk.toolMetadata === undefined
1082
+ ? {}
1083
+ : { toolMetadata: chunk.toolMetadata }),
1084
+ }
1085
+ result.push({
1086
+ type: 'ai.tool.result',
1087
+ id: await resultId(),
1088
+ payload,
1089
+ } as AppendInput<D>)
1090
+ } else if (chunk.type === 'tool-output-denied') {
1091
+ await flush(chunk.toolCallId)
1092
+ const payload: ToolResultPayload = {
1093
+ requestId: options.requestId,
1094
+ messageId: options.messageId,
1095
+ generationId: options.generationId,
1096
+ toolCallId: chunk.toolCallId,
1097
+ denied: true,
1098
+ phase: 'execution',
1099
+ }
1100
+ result.push({
1101
+ type: 'ai.tool.result',
1102
+ id: await resultId(),
1103
+ payload,
1104
+ } as AppendInput<D>)
1105
+ } else if (chunk.type === 'tool-approval-request') {
1106
+ await flush(chunk.toolCallId)
1107
+ const payload: ApprovalRequestedPayload = {
1108
+ requestId: options.requestId,
1109
+ messageId: options.messageId,
1110
+ generationId: options.generationId,
1111
+ approvalId: chunk.approvalId,
1112
+ toolCallId: chunk.toolCallId,
1113
+ ...(chunk.isAutomatic === undefined
1114
+ ? {}
1115
+ : { isAutomatic: chunk.isAutomatic }),
1116
+ ...(chunk.signature === undefined
1117
+ ? {}
1118
+ : { signature: chunk.signature }),
1119
+ }
1120
+ result.push({
1121
+ type: 'ai.approval.requested',
1122
+ id: await aiEventId(
1123
+ 'a2.ai.approval.requested',
1124
+ options.generationId,
1125
+ chunk.approvalId,
1126
+ ),
1127
+ payload,
1128
+ } as AppendInput<D>)
1129
+ } else if (chunk.type === 'tool-approval-response') {
1130
+ result.push({
1131
+ type: 'ai.approval.responded',
1132
+ id: await aiEventId(
1133
+ 'a2.ai.approval.automatic-response',
1134
+ options.generationId,
1135
+ chunk.approvalId,
1136
+ ),
1137
+ payload: {
1138
+ requestId: options.requestId,
1139
+ messageId: options.messageId,
1140
+ generationId: options.generationId,
1141
+ approvalId: chunk.approvalId,
1142
+ approved: chunk.approved,
1143
+ ...(chunk.reason === undefined ? {} : { reason: chunk.reason }),
1144
+ },
1145
+ } as AppendInput<D>)
1146
+ }
1147
+ }
1148
+ return { events: result, pending }
1149
+ }
1150
+
1151
+ const validateCompaction = <
1152
+ M extends UIMessage,
1153
+ D extends AIEventDefs<M> & EventDefs,
1154
+ T extends ToolSet,
1155
+ >(options: {
1156
+ compaction: CompactionOptions<M, D>
1157
+ explicit: boolean
1158
+ generation: AgentGenerationSettings<T> | undefined
1159
+ tools: T | undefined
1160
+ }): CompactionOptions<M, D> => {
1161
+ let compaction = options.compaction
1162
+ if (
1163
+ compaction !== false &&
1164
+ (typeof compaction !== 'object' || compaction === null)
1165
+ ) {
1166
+ throw new TypeError('compaction must resolve to false or a policy object')
1167
+ }
1168
+ if (compaction !== false && 'then' in compaction) {
1169
+ void Promise.resolve(compaction).catch(() => {})
1170
+ throw new TypeError('compaction options must be a policy or a resolver')
1171
+ }
1172
+ if (compaction !== false && !('shouldCompact' in compaction)) {
1173
+ if (
1174
+ compaction.thresholdTokens !== undefined &&
1175
+ (!Number.isSafeInteger(compaction.thresholdTokens) ||
1176
+ compaction.thresholdTokens < 1)
1177
+ ) {
1178
+ throw new TypeError(
1179
+ 'compaction.thresholdTokens must be a positive safe integer',
1180
+ )
1181
+ }
1182
+ const choice = options.generation?.toolChoice
1183
+ const unsupported =
1184
+ options.generation?.output !== undefined
1185
+ ? 'structured output'
1186
+ : choice !== undefined && choice !== 'auto' && choice !== 'none'
1187
+ ? 'forced tool choice'
1188
+ : Object.values(options.tools ?? {}).some(
1189
+ (tool) =>
1190
+ tool.type === 'provider' && tool.isProviderExecuted === true,
1191
+ )
1192
+ ? 'provider-executed tools'
1193
+ : undefined
1194
+ if (unsupported !== undefined) {
1195
+ if (options.explicit) {
1196
+ throw new TypeError(
1197
+ `automatic compaction does not support ${unsupported}; use a custom policy or compaction: false`,
1198
+ )
1199
+ }
1200
+ compaction = false
1201
+ }
1202
+ }
1203
+
1204
+ return compaction
1205
+ }
1206
+
1207
+ // Options of the AI SDK harness, which live in it, not on agent().
1208
+ const AI_SDK_HARNESS_OPTIONS = [
1209
+ 'generation',
1210
+ 'generate',
1211
+ 'compaction',
1212
+ 'messageMetadata',
1213
+ ] as const
1214
+
1215
+ const assertNoLoopOptions = (options: { harness?: { type: string } }): void => {
1216
+ const harness = options.harness?.type ?? 'ai-sdk'
1217
+ for (const key of AI_SDK_HARNESS_OPTIONS) {
1218
+ if (!Object.hasOwn(options, key)) continue
1219
+ throw new TypeError(
1220
+ harness === 'ai-sdk'
1221
+ ? `${key} belongs to the harness: agent({ harness: { type: 'ai-sdk', ${key} } })`
1222
+ : `${key} is an option of the AI SDK harness; ${harness}() runs its own loop`,
1223
+ )
1224
+ }
1225
+ }
1226
+
1227
+ export function createHandlers<
1228
+ M extends UIMessage,
1229
+ D extends AIEventDefs<M> & EventDefs,
1230
+ T extends ToolSet = ToolSet,
1231
+ X extends object = Record<never, never>,
1232
+ >(
1233
+ options: HandlersOptions<M, D, T, X>,
1234
+ ): NonNullable<ServerOptions<D>['handlers']> {
1235
+ if (options.model === undefined) {
1236
+ throw new TypeError('agent requires model')
1237
+ }
1238
+ if (
1239
+ options.progress?.maxChunks !== undefined &&
1240
+ (!Number.isInteger(options.progress.maxChunks) ||
1241
+ options.progress.maxChunks < 1)
1242
+ ) {
1243
+ throw new TypeError('progress.maxChunks must be a positive integer')
1244
+ }
1245
+ if (
1246
+ options.progress?.maxDelayMs !== undefined &&
1247
+ (!Number.isFinite(options.progress.maxDelayMs) ||
1248
+ options.progress.maxDelayMs < 0)
1249
+ ) {
1250
+ throw new TypeError('progress.maxDelayMs must be a non-negative number')
1251
+ }
1252
+ if (
1253
+ options.maxSteps !== undefined &&
1254
+ options.maxSteps !== Number.POSITIVE_INFINITY &&
1255
+ (!Number.isInteger(options.maxSteps) || options.maxSteps < 1)
1256
+ ) {
1257
+ throw new TypeError('maxSteps must be a positive integer or Infinity')
1258
+ }
1259
+
1260
+ assertNoLoopOptions(options)
1261
+ const configured = options.harness
1262
+ const loop: Omit<AISDKHarness<M, D, T, X>, 'type'> = configured?.type ===
1263
+ 'ai-sdk'
1264
+ ? configured
1265
+ : {}
1266
+ const harness: HarnessDriver | undefined =
1267
+ configured === undefined || configured.type === 'ai-sdk'
1268
+ ? undefined
1269
+ : harnessDriver(configured)
1270
+ const states = options.harnessStates ?? harnessStates()
1271
+ if (harness !== undefined) {
1272
+ for (const [name, tool] of Object.entries(options.tools ?? {})) {
1273
+ const needsApproval = (tool as { needsApproval?: unknown }).needsApproval
1274
+ if (needsApproval !== undefined && needsApproval !== false)
1275
+ throw new TypeError(
1276
+ `a harness runs tools without approval; '${name}' sets needsApproval`,
1277
+ )
1278
+ }
1279
+ }
1280
+
1281
+ const configuredCompaction =
1282
+ loop.compaction ??
1283
+ (loop.generate === undefined && harness === undefined ? {} : false)
1284
+ const staticCompaction =
1285
+ typeof configuredCompaction === 'function'
1286
+ ? undefined
1287
+ : validateCompaction({
1288
+ compaction: configuredCompaction,
1289
+ explicit: loop.compaction !== undefined,
1290
+ generation:
1291
+ typeof loop.generation === 'function' ? undefined : loop.generation,
1292
+ tools: options.tools,
1293
+ })
1294
+
1295
+ // The harness's own tools come first, so an agent tool with a name replaces them.
1296
+ const tools = (
1297
+ harness === undefined
1298
+ ? (options.tools ?? {})
1299
+ : { ...harness.tools, ...options.tools }
1300
+ ) as T
1301
+ const toolRecovery: Readonly<Record<string, ToolRecovery | undefined>> =
1302
+ options.toolRecovery ?? {}
1303
+ for (const [name, recovery] of Object.entries(toolRecovery)) {
1304
+ if (!Object.hasOwn(tools, name))
1305
+ throw new TypeError(
1306
+ `toolRecovery names '${name}', which is not one of the agent's tools`,
1307
+ )
1308
+ if (typeof (tools[name] as RuntimeTool).execute !== 'function')
1309
+ throw new TypeError(
1310
+ `toolRecovery names '${name}', which has no execute function`,
1311
+ )
1312
+ if (recovery !== 'rerun' && recovery !== 'report')
1313
+ throw new TypeError(
1314
+ `toolRecovery for '${name}' must be 'rerun' or 'report'`,
1315
+ )
1316
+ }
1317
+ const telemetry = options.telemetry ?? CONSOLE_TELEMETRY
1318
+ const maxSteps = options.maxSteps ?? Number.POSITIVE_INFINITY
1319
+ const control = createControlRuntime({
1320
+ contract: options.contract,
1321
+ telemetry,
1322
+ })
1323
+ const controlHandler: typeof control.handler = (ctx) =>
1324
+ telemetry.span(
1325
+ 'a2.ai.control',
1326
+ {
1327
+ 'a2.contract': options.contract.name,
1328
+ 'a2.session_id': ctx.event.sessionId,
1329
+ 'a2.ai.control.type': ctx.event.type,
1330
+ 'a2.ai.control.id': ctx.event.id,
1331
+ 'a2.ai.control.attempt': ctx.attempt,
1332
+ },
1333
+ () => control.handler(ctx),
1334
+ )
1335
+ const coordinator = control.coordinator
1336
+ const contextReducer = createContextReducer<M, D>({
1337
+ contract: options.contract,
1338
+ })
1339
+ const readContext = async (
1340
+ session: HandlerContext<D>['session'],
1341
+ through: number | 'latest',
1342
+ ) => {
1343
+ const snapshot = await session.state(contextReducer, { through })
1344
+ return snapshot
1345
+ }
1346
+ // A session's next step starts only after its tools settle, so only its
1347
+ // latest step's prompt is ever reused. A miss rebuilds the prompt from the
1348
+ // context checkpoint, so eviction is memory policy, never correctness.
1349
+ const prompts = boundedCache<
1350
+ string,
1351
+ { generationId: string; messages: Promise<ModelMessage[]> }
1352
+ >({ maxEntries: PROMPT_SESSIONS, maxBytes: PROMPT_SESSIONS })
1353
+ const cachePrompt = ({
1354
+ sessionId,
1355
+ generationId,
1356
+ messages,
1357
+ }: {
1358
+ sessionId: string
1359
+ generationId: string
1360
+ messages: Promise<ModelMessage[]>
1361
+ }): void =>
1362
+ // Entries count as one byte, so the bound is the session count.
1363
+ prompts.set(sessionId, { generationId, messages }, 1)
1364
+ const forgetPrompt = (sessionId: string, generationId: string): void => {
1365
+ if (prompts.get(sessionId)?.generationId === generationId)
1366
+ prompts.delete(sessionId)
1367
+ }
1368
+
1369
+ type RuntimeTool = {
1370
+ execute?: (
1371
+ input: unknown,
1372
+ options: {
1373
+ toolCallId: string
1374
+ messages: ModelMessage[]
1375
+ abortSignal?: AbortSignal
1376
+ experimental_sandbox?: Experimental_SandboxSession
1377
+ },
1378
+ ) => unknown
1379
+ }
1380
+
1381
+ type ToolHandlerContext = HandlerContext<D, 'ai.tool.execution.requested'>
1382
+
1383
+ const resultEvent = async (
1384
+ call: ToolCalledPayload,
1385
+ suffix: string,
1386
+ result: {
1387
+ output?: unknown
1388
+ error?: string
1389
+ denied?: boolean
1390
+ preliminary?: boolean
1391
+ },
1392
+ ): Promise<AppendInput<D>> =>
1393
+ ({
1394
+ type: 'ai.tool.result',
1395
+ id: await aiEventId(
1396
+ 'a2.ai.tool.result',
1397
+ call.generationId,
1398
+ call.toolCallId,
1399
+ suffix,
1400
+ ),
1401
+ payload: {
1402
+ requestId: call.requestId,
1403
+ messageId: call.messageId,
1404
+ generationId: call.generationId,
1405
+ toolCallId: call.toolCallId,
1406
+ toolName: call.toolName,
1407
+ input: call.input,
1408
+ phase: 'execution',
1409
+ ...(result.output === undefined ? {} : { output: result.output }),
1410
+ ...(result.error === undefined ? {} : { error: result.error }),
1411
+ ...(result.denied === undefined ? {} : { denied: result.denied }),
1412
+ ...(result.preliminary === undefined
1413
+ ? {}
1414
+ : { preliminary: result.preliminary }),
1415
+ ...(call.dynamic === undefined ? {} : { dynamic: call.dynamic }),
1416
+ ...(call.providerExecuted === undefined
1417
+ ? {}
1418
+ : { providerExecuted: call.providerExecuted }),
1419
+ ...(call.toolMetadata === undefined
1420
+ ? {}
1421
+ : { toolMetadata: call.toolMetadata }),
1422
+ },
1423
+ }) as AppendInput<D>
1424
+
1425
+ const promptMessages = (input: {
1426
+ ctx: ToolHandlerContext
1427
+ call: ToolCalledPayload
1428
+ generation: GenerationStartedPayload
1429
+ }): Promise<ModelMessage[]> => {
1430
+ const { ctx, call, generation: owner } = input
1431
+ const sessionId = ctx.event.sessionId
1432
+ const cached = prompts.get(sessionId)
1433
+ if (cached?.generationId === call.generationId) return cached.messages
1434
+ const computation = (async () => {
1435
+ const frontier = owner.promptThroughIndex
1436
+ if (frontier === undefined)
1437
+ throw new TypeError('AI work requires a prompt checkpoint')
1438
+ const snapshot = await readContext(ctx.session, ctx.event.index)
1439
+ if (
1440
+ snapshot.state.generation?.owner.generationId !== owner.generationId ||
1441
+ snapshot.state.generation.owner.requestId !== owner.requestId ||
1442
+ snapshot.state.activeProjection?.generationId !== owner.generationId
1443
+ )
1444
+ throw new TypeError(
1445
+ 'Tool prompt checkpoint does not own its generation',
1446
+ )
1447
+ const state = generationBase(snapshot.state)
1448
+ return telemetry.span(
1449
+ 'a2.ai.context',
1450
+ {
1451
+ 'a2.contract': options.contract.name,
1452
+ 'a2.session_id': ctx.event.sessionId,
1453
+ 'a2.ai.context.request_id': call.requestId,
1454
+ 'a2.ai.context.phase': 'tool',
1455
+ },
1456
+ () =>
1457
+ modelContext({
1458
+ messages: state.messages.filter(
1459
+ (message) => state.messageIndexes[message.id]! <= frontier,
1460
+ ),
1461
+ state,
1462
+ tools,
1463
+ }),
1464
+ )
1465
+ })()
1466
+ cachePrompt({
1467
+ sessionId,
1468
+ generationId: call.generationId,
1469
+ messages: computation,
1470
+ })
1471
+ void computation.catch(() => forgetPrompt(sessionId, call.generationId))
1472
+ return computation
1473
+ }
1474
+
1475
+ const toolExecutionFailure = async (
1476
+ ctx: ToolHandlerContext,
1477
+ call: ToolCalledPayload,
1478
+ error: unknown,
1479
+ ): Promise<AppendInput<D> | void> => {
1480
+ const schedulerFailure = consumeSchedulerSendFailure(error)
1481
+ if (checkAbort(ctx.signal)) return
1482
+ if (schedulerFailure === 'retryable') throw error
1483
+ return resultEvent(call, `${ctx.event.id}:execution:error`, {
1484
+ error: errorMessage(error),
1485
+ })
1486
+ }
1487
+
1488
+ const runToolExecution = async (
1489
+ ctx: ToolHandlerContext,
1490
+ call: ToolCalledPayload,
1491
+ execute: NonNullable<RuntimeTool['execute']>,
1492
+ messages: ModelMessage[],
1493
+ sandbox: Experimental_SandboxSession | undefined,
1494
+ ): Promise<AppendInput<D> | void> => {
1495
+ let output: unknown
1496
+ try {
1497
+ output = await execute(call.input, {
1498
+ toolCallId: call.toolCallId,
1499
+ messages,
1500
+ abortSignal: ctx.signal,
1501
+ ...(sandbox === undefined ? {} : { experimental_sandbox: sandbox }),
1502
+ })
1503
+ } catch (error) {
1504
+ return toolExecutionFailure(ctx, call, error)
1505
+ }
1506
+ let iterator: AsyncIterator<unknown> | undefined
1507
+ try {
1508
+ iterator = asyncIterator(output)
1509
+ } catch (error) {
1510
+ return toolExecutionFailure(ctx, call, error)
1511
+ }
1512
+ if (iterator === undefined) {
1513
+ return resultEvent(call, `${ctx.event.id}:execution:0`, { output })
1514
+ }
1515
+ let last: unknown
1516
+ let sequence = 0
1517
+ let done = false
1518
+ try {
1519
+ for (;;) {
1520
+ let result: IteratorResult<unknown>
1521
+ try {
1522
+ result = await iterator.next()
1523
+ } catch (error) {
1524
+ return toolExecutionFailure(ctx, call, error)
1525
+ }
1526
+ if (result.done) {
1527
+ if (ctx.signal.reason instanceof A2Error) throw ctx.signal.reason
1528
+ done = true
1529
+ break
1530
+ }
1531
+ if (checkAbort(ctx.signal)) return
1532
+ last = result.value
1533
+ await control.append({
1534
+ ctx,
1535
+ name: `tool:${call.toolCallId}:preliminary:${sequence}`,
1536
+ events: [
1537
+ await resultEvent(
1538
+ call,
1539
+ `${ctx.event.id}:execution:${ctx.attempt}:${sequence}:preliminary`,
1540
+ {
1541
+ output: result.value,
1542
+ preliminary: true,
1543
+ },
1544
+ ),
1545
+ ],
1546
+ })
1547
+ sequence += 1
1548
+ }
1549
+ } finally {
1550
+ if (!done) {
1551
+ try {
1552
+ await iterator.return?.()
1553
+ } catch {
1554
+ // Preserve the failure or cancellation that ended execution.
1555
+ }
1556
+ }
1557
+ }
1558
+ return resultEvent(
1559
+ call,
1560
+ `${ctx.event.id}:execution:${sequence}:final`,
1561
+ sequence === 0 ? {} : { output: last },
1562
+ )
1563
+ }
1564
+
1565
+ const executeTool = async (input: {
1566
+ ctx: ToolHandlerContext
1567
+ call: ToolCalledPayload
1568
+ generation: GenerationStartedPayload
1569
+ }): Promise<AppendInput<D> | void> => {
1570
+ const { ctx, call } = input
1571
+ const tool = tools[call.toolName] as RuntimeTool | undefined
1572
+ const execute = tool?.execute
1573
+ if (execute === undefined) {
1574
+ return resultEvent(call, `${ctx.event.id}:execution:error`, {
1575
+ error: `Tool '${call.toolName}' has no server executor`,
1576
+ })
1577
+ }
1578
+ const report = toolRecovery[call.toolName] === 'report'
1579
+ const interrupted = () =>
1580
+ resultEvent(call, `${ctx.event.id}:execution:error`, {
1581
+ error: `Tool '${call.toolName}' was interrupted and may have partly run.`,
1582
+ })
1583
+ // A thrown failure never follows `execute` for a reported tool (see
1584
+ // below), so only a lapsed attempt may have reached it.
1585
+ if (report && ctx.attempt > 1 && !failedPreviousAttempt.has(ctx))
1586
+ return interrupted()
1587
+ const messages = await promptMessages(input)
1588
+ // A sandbox that fails to resolve fails the attempt, which A2 retries;
1589
+ // it is not the tool's failure to report to the model.
1590
+ const sandbox =
1591
+ harness?.sandbox === undefined
1592
+ ? undefined
1593
+ : await resolve(harness.sandbox, {
1594
+ session: { id: ctx.session.id },
1595
+ signal: ctx.signal,
1596
+ })
1597
+ if (checkAbort(ctx.signal)) return
1598
+ const scope: AmbientToolScope = {
1599
+ contract: options.contract,
1600
+ context: ctx,
1601
+ }
1602
+ // Async iteration resumes in the caller's context, so the whole
1603
+ // execution, not just the execute call, runs inside the scope.
1604
+ const run = () =>
1605
+ toolScopeStorage.run(scope, () =>
1606
+ runToolExecution(ctx, call, execute, messages, sandbox),
1607
+ )
1608
+ if (!report) return run()
1609
+ try {
1610
+ // Undefined means a user cancellation after `execute` started.
1611
+ return (await run()) ?? (await interrupted())
1612
+ } catch {
1613
+ // A lapsed claim surrenders; the next attempt reports the call.
1614
+ checkAbort(ctx.signal)
1615
+ return interrupted()
1616
+ }
1617
+ }
1618
+
1619
+ const handleToolExecution = async (
1620
+ ctx: ToolHandlerContext,
1621
+ ): Promise<AppendInput<D>> => {
1622
+ try {
1623
+ const state = (
1624
+ await ctx.session.state(control.reducer, { through: 'latest' })
1625
+ ).state
1626
+ const request = ctx.event.payload
1627
+ const call = state.coordinator.response?.calls.find(
1628
+ (candidate) => candidate.work?.id === ctx.event.id,
1629
+ )
1630
+ if (
1631
+ state.active?.turnId !== request.turnId ||
1632
+ Object.values(state.pendingStops).includes(request.turnId) ||
1633
+ state.active.suspended ||
1634
+ state.active.version !== request.version ||
1635
+ state.coordinator.response?.failure !== undefined ||
1636
+ !call ||
1637
+ call.terminal ||
1638
+ call.work?.settled
1639
+ )
1640
+ return control.settled({ ctx, events: undefined })
1641
+ return control.settled({
1642
+ ctx,
1643
+ events: await executeTool({
1644
+ ctx,
1645
+ call: request.call,
1646
+ generation: request.generation,
1647
+ }),
1648
+ })
1649
+ } catch (error) {
1650
+ if (control.cancelled({ error, signal: ctx.signal }))
1651
+ return control.settled({ ctx, events: undefined })
1652
+ throw error
1653
+ }
1654
+ }
1655
+
1656
+ const generationHandler = async (
1657
+ ctx: HandlerContext<D, 'ai.generation.requested'>,
1658
+ ): Promise<void | AppendInput<D> | readonly AppendInput<D>[]> => {
1659
+ if (checkAbort(ctx.signal)) return
1660
+ const requestId = ctx.event.id
1661
+ const request = ctx.event.payload
1662
+ const snapshot = await readContext(ctx.session, 'latest')
1663
+ if (
1664
+ snapshot.state.closed ||
1665
+ snapshot.state.authority?.requestId !== requestId ||
1666
+ snapshot.state.authority?.phase === 'paused' ||
1667
+ snapshot.state.authority?.phase === 'pausing'
1668
+ )
1669
+ return
1670
+ const controlState = (
1671
+ await ctx.session.state(control.reducer, { through: snapshot.index })
1672
+ ).state
1673
+ if (
1674
+ request.control !== undefined &&
1675
+ Object.values(controlState.pendingStops).includes(request.control.turnId)
1676
+ )
1677
+ return
1678
+ const coordinatorState = controlState.coordinator
1679
+ if (
1680
+ coordinatorState.closed ||
1681
+ coordinatorState.response?.activeRequestId !== requestId
1682
+ )
1683
+ return
1684
+ const response = coordinatorState.response
1685
+ const current =
1686
+ response.generation?.requestId === requestId
1687
+ ? response.generation
1688
+ : undefined
1689
+ if (
1690
+ current !== undefined &&
1691
+ (response.completion !== undefined ||
1692
+ (response.failure !== undefined &&
1693
+ response.failure.superseded !== true))
1694
+ )
1695
+ return
1696
+ if (current !== undefined && current.attempt >= ctx.attempt) return
1697
+ const attempt = ctx.attempt
1698
+ const generationId = await aiEventId(
1699
+ 'a2.ai.generation',
1700
+ requestId,
1701
+ String(attempt),
1702
+ )
1703
+ const responseMessageId = request.responseMessageId
1704
+ const responseStepCount = response.stepCount
1705
+ if (
1706
+ request.reason === 'tool' &&
1707
+ (request.sourceGenerationId !==
1708
+ response.source?.generation.generationId ||
1709
+ response.responseMessageId !== request.messageId ||
1710
+ response.responseMessageId !== request.responseMessageId)
1711
+ )
1712
+ return
1713
+ const previous = current
1714
+ const state =
1715
+ previous === undefined
1716
+ ? snapshot.state
1717
+ : contextForAttempt(snapshot.state)
1718
+ const resolverContext: AgentResolverContext<M, D, X> = {
1719
+ event: ctx.event,
1720
+ messages: contextMessages({ state, coordinator: coordinatorState }),
1721
+ // The contract registered this reducer with its declared state type.
1722
+ state: (snapshot.state.app ?? {}) as X,
1723
+ session: {
1724
+ state: (reducer, readOptions) =>
1725
+ ctx.session.state(reducer, {
1726
+ ...readOptions,
1727
+ through: readOptions?.through ?? snapshot.index,
1728
+ }),
1729
+ },
1730
+ signal: ctx.signal,
1731
+ }
1732
+
1733
+ if (request.reason === 'tool' && responseStepCount >= maxSteps) {
1734
+ const source = response.source?.generation
1735
+ if (source === undefined) return
1736
+ return {
1737
+ type: 'ai.generation.failed',
1738
+ id: await aiEventId('a2.ai.step-limit', requestId),
1739
+ payload: {
1740
+ requestId: source.requestId,
1741
+ messageId: source.messageId,
1742
+ generationId: source.generationId,
1743
+ responseMessageId,
1744
+ error: `agent exceeded the ${maxSteps}-step limit`,
1745
+ stepLimit: true,
1746
+ },
1747
+ } as AppendInput<D>
1748
+ }
1749
+
1750
+ const startEvents = async (
1751
+ started: GenerationStartedPayload,
1752
+ metadataModelId?: string,
1753
+ ): Promise<AppendInput<D>[]> => {
1754
+ const events: AppendInput<D>[] = []
1755
+ if (metadataModelId !== undefined) {
1756
+ events.push({
1757
+ type: 'ai.model.metadata.requested',
1758
+ id: await aiEventId(
1759
+ 'a2.ai.model.metadata',
1760
+ options.contract.name,
1761
+ ctx.event.sessionId,
1762
+ metadataModelId,
1763
+ ),
1764
+ payload: { modelId: metadataModelId },
1765
+ } as AppendInput<D>)
1766
+ }
1767
+ if (previous) {
1768
+ const superseded: GenerationFailedPayload = {
1769
+ requestId,
1770
+ messageId: previous.messageId,
1771
+ generationId: previous.generationId,
1772
+ responseMessageId: previous.responseMessageId,
1773
+ error: 'generation attempt was superseded after an incomplete run',
1774
+ superseded: true,
1775
+ }
1776
+ events.push({
1777
+ type: 'ai.generation.failed',
1778
+ id: await aiEventId('a2.ai.superseded', previous.generationId),
1779
+ payload: superseded,
1780
+ } as AppendInput<D>)
1781
+ }
1782
+ events.push({
1783
+ type: 'ai.generation.started',
1784
+ id: generationId,
1785
+ payload: started,
1786
+ } as AppendInput<D>)
1787
+ return events
1788
+ }
1789
+ const startedPayload = (model?: string): GenerationStartedPayload => ({
1790
+ requestId,
1791
+ messageId: request.messageId,
1792
+ generationId,
1793
+ responseMessageId,
1794
+ attempt,
1795
+ ...(model === undefined ? {} : { model }),
1796
+ promptThroughIndex: snapshot.index,
1797
+ })
1798
+
1799
+ let resolvedModel: string | undefined
1800
+ let resolving = true
1801
+ let generationStarted = false
1802
+ try {
1803
+ const choice = await resolve(options.model, resolverContext)
1804
+ if (choice === undefined) {
1805
+ throw new TypeError('the model resolver returned undefined')
1806
+ }
1807
+ const settings: ModelSettings =
1808
+ typeof choice === 'string' ? { model: choice } : choice
1809
+ const model = settings.model
1810
+ if (typeof model !== 'string' || model === '')
1811
+ throw new TypeError('the model must be an AI Gateway model ID')
1812
+ resolvedModel = model
1813
+ if (checkAbort(ctx.signal)) return
1814
+ const resolvedInstructions =
1815
+ options.instructions === undefined
1816
+ ? undefined
1817
+ : await resolve(options.instructions, resolverContext)
1818
+ if (checkAbort(ctx.signal)) return
1819
+ const resolvedGeneration: AgentGenerationSettings<T> = await resolve(
1820
+ loop.generation ?? {},
1821
+ resolverContext,
1822
+ )
1823
+ if (checkAbort(ctx.signal)) return
1824
+ if (
1825
+ resolvedGeneration === null ||
1826
+ typeof resolvedGeneration !== 'object' ||
1827
+ Array.isArray(resolvedGeneration)
1828
+ )
1829
+ throw new TypeError('the generation resolver must return settings')
1830
+ const generation = withModelSettings({
1831
+ generation: resolvedGeneration,
1832
+ settings,
1833
+ })
1834
+ const compaction =
1835
+ typeof configuredCompaction === 'function' ||
1836
+ typeof loop.generation === 'function'
1837
+ ? validateCompaction({
1838
+ compaction:
1839
+ typeof configuredCompaction === 'function'
1840
+ ? await resolve(configuredCompaction, resolverContext)
1841
+ : staticCompaction!,
1842
+ explicit: loop.compaction !== undefined,
1843
+ generation,
1844
+ tools: options.tools,
1845
+ })
1846
+ : staticCompaction!
1847
+ if (checkAbort(ctx.signal)) return
1848
+ resolving = false
1849
+
1850
+ const catalogModelId = gatewayModelId(model)
1851
+ const gatewayOptions = generation.providerOptions?.['gateway']
1852
+ const usesFallbackModels =
1853
+ Array.isArray(gatewayOptions?.['models']) &&
1854
+ gatewayOptions['models'].length > 0
1855
+ const discoversMetadata =
1856
+ compaction !== false &&
1857
+ !('shouldCompact' in compaction) &&
1858
+ compaction.thresholdTokens === undefined &&
1859
+ !usesFallbackModels &&
1860
+ catalogModelId !== undefined
1861
+ const generationEvents = await control.append({
1862
+ ctx,
1863
+ name: 'generation-start',
1864
+ events: await startEvents(
1865
+ startedPayload(model),
1866
+ discoversMetadata &&
1867
+ !Object.hasOwn(state.modelMetadata, catalogModelId)
1868
+ ? catalogModelId
1869
+ : undefined,
1870
+ ),
1871
+ })
1872
+ const startIndex = generationEvents.find(
1873
+ (event) => event.id === generationId,
1874
+ )!.index
1875
+ generationStarted = true
1876
+
1877
+ if (checkAbort(ctx.signal)) return
1878
+
1879
+ const promptCoordinatorState = coordinatorState
1880
+ const { messages } = resolverContext
1881
+ let generationMessages = messages
1882
+ let modelMessages = await telemetry.span(
1883
+ 'a2.ai.context',
1884
+ {
1885
+ 'a2.contract': options.contract.name,
1886
+ 'a2.session_id': ctx.event.sessionId,
1887
+ 'a2.ai.context.request_id': requestId,
1888
+ 'a2.ai.context.phase': 'generation',
1889
+ },
1890
+ () => modelContext({ messages, state, tools }),
1891
+ )
1892
+ const baseContext: AgentGenerateContext<M, D, T, X> = {
1893
+ ...resolverContext,
1894
+ generationId,
1895
+ modelMessages,
1896
+ model: resolvedModel,
1897
+ tools,
1898
+ ...(resolvedInstructions === undefined
1899
+ ? {}
1900
+ : { instructions: resolvedInstructions }),
1901
+ generation,
1902
+ }
1903
+ const policy = compaction
1904
+ const tracksInput =
1905
+ policy !== false &&
1906
+ !('shouldCompact' in policy) &&
1907
+ (policy.thresholdTokens !== undefined || discoversMetadata)
1908
+ const metadata =
1909
+ catalogModelId === undefined
1910
+ ? undefined
1911
+ : state.modelMetadata[catalogModelId]
1912
+ const limits =
1913
+ metadata?.status === 'resolved' && !usesFallbackModels
1914
+ ? metadata.limits
1915
+ : undefined
1916
+ let inputTokenEstimate: number | undefined
1917
+ let compacted = false
1918
+ let compactionEstimate:
1919
+ | Pick<
1920
+ CompactionRequestedPayload,
1921
+ 'inputTokenEstimate' | 'inputTokens' | 'thresholdTokens'
1922
+ >
1923
+ | undefined
1924
+ const hasPendingTools = messages.some((message) =>
1925
+ message.parts.some(
1926
+ (part) =>
1927
+ isToolUIPart(part) &&
1928
+ !(
1929
+ part.state === 'output-error' ||
1930
+ part.state === 'output-denied' ||
1931
+ (part.state === 'output-available' && part.preliminary !== true)
1932
+ ),
1933
+ ),
1934
+ )
1935
+ if (policy && !hasPendingTools) {
1936
+ const compactionContext = {
1937
+ ...resolverContext,
1938
+ messages,
1939
+ modelMessages,
1940
+ }
1941
+ let shouldCompact: boolean
1942
+ if ('shouldCompact' in policy) {
1943
+ shouldCompact = await policy.shouldCompact(compactionContext)
1944
+ } else if (tracksInput) {
1945
+ inputTokenEstimate = await estimateInputTokens({
1946
+ messages: modelMessages,
1947
+ instructions: resolvedInstructions,
1948
+ tools,
1949
+ })
1950
+ const inputTokens = measuredInputTokens({
1951
+ estimate: inputTokenEstimate,
1952
+ model: resolvedModel,
1953
+ calibration: coordinatorState.calibration,
1954
+ })
1955
+ const threshold =
1956
+ policy.thresholdTokens ??
1957
+ (limits === undefined
1958
+ ? undefined
1959
+ : Math.floor(
1960
+ Math.min(
1961
+ limits.contextWindow * 0.75,
1962
+ limits.contextWindow - (generation.maxOutputTokens ?? 0),
1963
+ ),
1964
+ ))
1965
+ if (
1966
+ limits !== undefined &&
1967
+ threshold !== undefined &&
1968
+ threshold <= 0
1969
+ ) {
1970
+ throw new Error(
1971
+ `output allowance exhausts the ${limits.contextWindow}-token context window`,
1972
+ )
1973
+ }
1974
+ shouldCompact = threshold !== undefined && inputTokens >= threshold
1975
+ if (threshold !== undefined && shouldCompact) {
1976
+ compactionEstimate = {
1977
+ inputTokenEstimate,
1978
+ inputTokens,
1979
+ thresholdTokens: threshold,
1980
+ }
1981
+ }
1982
+ } else {
1983
+ shouldCompact = false
1984
+ }
1985
+ if (checkAbort(ctx.signal)) return
1986
+ if (shouldCompact) {
1987
+ const latest = (
1988
+ await ctx.session.state(coordinator, { through: 'latest' })
1989
+ ).state
1990
+ shouldCompact = Object.keys(latest.unsettledTools ?? {}).length === 0
1991
+ }
1992
+ if (checkAbort(ctx.signal)) return
1993
+ if (shouldCompact) {
1994
+ const throughMessageId =
1995
+ state.messages.findLast(
1996
+ (message) =>
1997
+ !promptCoordinatorState.queued.some(
1998
+ (queued) => queued.messageId === message.id,
1999
+ ),
2000
+ )?.id ?? request.messageId
2001
+ const throughIndex = snapshot.index
2002
+ generationEvents.push(
2003
+ ...(await control.append({
2004
+ ctx,
2005
+ name: 'compaction-requested',
2006
+ events: [
2007
+ {
2008
+ type: 'ai.compaction.requested',
2009
+ id: await aiEventId(
2010
+ 'a2.ai.compaction.requested',
2011
+ generationId,
2012
+ ),
2013
+ payload: {
2014
+ generationId,
2015
+ throughMessageId,
2016
+ throughIndex,
2017
+ ...compactionEstimate,
2018
+ },
2019
+ } as AppendInput<D>,
2020
+ ],
2021
+ })),
2022
+ )
2023
+ const result = await telemetry.span(
2024
+ 'a2.ai.compaction',
2025
+ {
2026
+ 'a2.contract': options.contract.name,
2027
+ 'a2.session_id': ctx.event.sessionId,
2028
+ 'a2.ai.compaction.request_id': requestId,
2029
+ 'a2.ai.compaction.generation_id': generationId,
2030
+ 'a2.ai.compaction.policy':
2031
+ 'shouldCompact' in policy ? 'custom' : 'automatic',
2032
+ },
2033
+ async () =>
2034
+ !('shouldCompact' in policy)
2035
+ ? {
2036
+ messages: [] as M[],
2037
+ ...(await summarize({
2038
+ agentName: options.contract.name,
2039
+ context: baseContext,
2040
+ instructions: policy.instructions,
2041
+ })),
2042
+ }
2043
+ : { messages: await policy.compact(compactionContext) },
2044
+ )
2045
+ if (checkAbort(ctx.signal)) return
2046
+ const completed: CompactionCompletedPayload<M> = {
2047
+ generationId,
2048
+ throughMessageId,
2049
+ throughIndex,
2050
+ ...result,
2051
+ ...(promptCoordinatorState.queued.length === 0
2052
+ ? {}
2053
+ : {
2054
+ retainedMessageIds: promptCoordinatorState.queued.map(
2055
+ (item) => item.messageId,
2056
+ ),
2057
+ }),
2058
+ }
2059
+ generationEvents.push(
2060
+ ...(await control.append({
2061
+ ctx,
2062
+ name: 'compaction-completed',
2063
+ events: [
2064
+ {
2065
+ type: 'ai.compaction.completed',
2066
+ id: await aiEventId(
2067
+ 'a2.ai.compaction.completed',
2068
+ generationId,
2069
+ ),
2070
+ payload: completed,
2071
+ } as AppendInput<D>,
2072
+ ],
2073
+ })),
2074
+ )
2075
+ const queued = new Set(
2076
+ promptCoordinatorState.queued.map((item) => item.messageId),
2077
+ )
2078
+ generationMessages = result.messages.filter(
2079
+ (message) => !queued.has(message.id),
2080
+ )
2081
+ compacted = true
2082
+ }
2083
+ }
2084
+
2085
+ const currentState = foldAIEvents({
2086
+ reducer: contextReducer,
2087
+ events: generationEvents,
2088
+ state,
2089
+ })
2090
+ if (request.reason === 'retry' || previous !== undefined) {
2091
+ generationMessages = contextMessages({
2092
+ state: currentState,
2093
+ coordinator: promptCoordinatorState,
2094
+ })
2095
+ }
2096
+ if (
2097
+ (policy && 'shouldCompact' in policy) ||
2098
+ generationMessages !== messages ||
2099
+ activeCompaction(currentState)?.summary !==
2100
+ activeCompaction(state)?.summary
2101
+ ) {
2102
+ modelMessages = await telemetry.span(
2103
+ 'a2.ai.context',
2104
+ {
2105
+ 'a2.contract': options.contract.name,
2106
+ 'a2.session_id': ctx.event.sessionId,
2107
+ 'a2.ai.context.request_id': requestId,
2108
+ 'a2.ai.context.phase': 'refresh',
2109
+ },
2110
+ () =>
2111
+ modelContext({
2112
+ messages: generationMessages,
2113
+ state: currentState,
2114
+ tools,
2115
+ }),
2116
+ )
2117
+ }
2118
+ cachePrompt({
2119
+ sessionId: ctx.event.sessionId,
2120
+ generationId,
2121
+ messages: Promise.resolve(modelMessages),
2122
+ })
2123
+ if (tracksInput && (inputTokenEstimate === undefined || compacted)) {
2124
+ inputTokenEstimate = await estimateInputTokens({
2125
+ messages: modelMessages,
2126
+ instructions: resolvedInstructions,
2127
+ tools,
2128
+ })
2129
+ }
2130
+
2131
+ let pendingToolCalls: PendingToolCall[] = []
2132
+ if (checkAbort(ctx.signal)) return
2133
+ const generateContext: AgentGenerateContext<M, D, T, X> = {
2134
+ ...baseContext,
2135
+ messages: generationMessages,
2136
+ modelMessages,
2137
+ }
2138
+ const source =
2139
+ harness !== undefined
2140
+ ? await harness.step({
2141
+ agentName: options.contract.name,
2142
+ sessionId: ctx.event.sessionId,
2143
+ requestIndex: ctx.event.index,
2144
+ generationId,
2145
+ responseMessageId,
2146
+ throughIndex: snapshot.index,
2147
+ messages: generationMessages,
2148
+ model,
2149
+ ...(settings.reasoning === undefined
2150
+ ? {}
2151
+ : { reasoning: settings.reasoning }),
2152
+ ...(settings.speed === undefined
2153
+ ? {}
2154
+ : { speed: settings.speed }),
2155
+ instructions: instructionTexts(resolvedInstructions),
2156
+ tools,
2157
+ log: harnessLog({
2158
+ states,
2159
+ session: ctx.session as Parameters<
2160
+ typeof harnessLog
2161
+ >[0]['session'],
2162
+ }),
2163
+ signal: ctx.signal,
2164
+ })
2165
+ : loop.generate === undefined
2166
+ ? await generateWithAISDK({
2167
+ agentName: options.contract.name,
2168
+ context: generateContext,
2169
+ messageMetadata: loop.messageMetadata,
2170
+ })
2171
+ : { stream: await loop.generate(generateContext) }
2172
+ const updates = consumeGeneration({
2173
+ source,
2174
+ ...(options.progress === undefined
2175
+ ? {}
2176
+ : { progress: options.progress }),
2177
+ })
2178
+
2179
+ let sequence = 0
2180
+ let finish: AgentGenerationFinish | undefined
2181
+ for await (const update of updates) {
2182
+ ctx.signal.throwIfAborted()
2183
+ if (update.type === 'finish') {
2184
+ finish = update
2185
+ continue
2186
+ }
2187
+ const progress: GenerationProgressPayload = {
2188
+ startIndex,
2189
+ chunks: update.chunks,
2190
+ }
2191
+ const lifecycle = await lifecycleEvents<D, T>({
2192
+ requestId,
2193
+ messageId: responseMessageId,
2194
+ generationId,
2195
+ sequence,
2196
+ chunks: update.chunks,
2197
+ pending: pendingToolCalls,
2198
+ tools,
2199
+ generation,
2200
+ approvals: harness === undefined,
2201
+ })
2202
+ pendingToolCalls = lifecycle.pending
2203
+ await control.append({
2204
+ ctx,
2205
+ name: `generation-progress:${sequence}`,
2206
+ events: [
2207
+ {
2208
+ type: 'ai.generation.progress',
2209
+ id: await aiEventId(
2210
+ 'a2.ai.progress',
2211
+ generationId,
2212
+ String(sequence),
2213
+ ),
2214
+ payload: progress,
2215
+ } as AppendInput<D>,
2216
+ ...lifecycle.events,
2217
+ ],
2218
+ })
2219
+ sequence += 1
2220
+ }
2221
+
2222
+ if (!finish) throw new Error('agent generation finished without output')
2223
+ const completed: GenerationCompletedPayload = {
2224
+ requestId,
2225
+ messageId: request.messageId,
2226
+ generationId,
2227
+ responseMessageId,
2228
+ ...(inputTokenEstimate === undefined ? {} : { inputTokenEstimate }),
2229
+ ...(finish.finishReason === undefined
2230
+ ? {}
2231
+ : { finishReason: finish.finishReason }),
2232
+ ...(finish.usage === undefined ? {} : { usage: finish.usage }),
2233
+ }
2234
+ const completionEvent = {
2235
+ type: 'ai.generation.completed',
2236
+ id: await aiEventId('a2.ai.completed', generationId),
2237
+ payload: completed,
2238
+ } as AppendInput<D>
2239
+ const unresolved = loop.generate
2240
+ ? pendingToolCalls.filter(
2241
+ (pending) => pending.classification !== 'automatic',
2242
+ )
2243
+ : []
2244
+ if (unresolved.length > 0) {
2245
+ throw new Error(
2246
+ `generation ended with unresolved tool authorization for '${unresolved[0]!.call.toolName}'`,
2247
+ )
2248
+ }
2249
+ const pendingEvents = await Promise.all(
2250
+ pendingToolCalls
2251
+ .filter(
2252
+ (pending) =>
2253
+ pending.classification === 'automatic' ||
2254
+ (loop.generate === undefined &&
2255
+ pending.classification === 'unknown'),
2256
+ )
2257
+ .map((pending) => toolCalledEvent<D>(pending.call)),
2258
+ )
2259
+ if (finish.finishReason === 'tool-calls') {
2260
+ return [...pendingEvents, completionEvent]
2261
+ }
2262
+ forgetPrompt(ctx.event.sessionId, generationId)
2263
+ return [
2264
+ ...pendingEvents,
2265
+ completionEvent,
2266
+ {
2267
+ type: 'ai.message.completed',
2268
+ id: await aiEventId('a2.ai.message.completed', generationId),
2269
+ payload: {
2270
+ messageId: responseMessageId,
2271
+ } satisfies MessageCompletedPayload,
2272
+ } as AppendInput<D>,
2273
+ ]
2274
+ } catch (error) {
2275
+ if (error instanceof ControlCancelled) throw error
2276
+ if (error instanceof A2Error) {
2277
+ checkAbort(ctx.signal)
2278
+ throw error
2279
+ }
2280
+ if (checkAbort(ctx.signal)) return
2281
+ if (!generationStarted && !resolving) throw error
2282
+ // A resolver or its result failed before the model ran: record the
2283
+ // attempt and fail it once, like a provider error, rather than retrying.
2284
+ if (!generationStarted)
2285
+ await control.append({
2286
+ ctx,
2287
+ name: 'generation-start',
2288
+ events: await startEvents(startedPayload(resolvedModel)),
2289
+ })
2290
+ forgetPrompt(ctx.event.sessionId, generationId)
2291
+ const failed: GenerationFailedPayload = {
2292
+ requestId,
2293
+ messageId: request.messageId,
2294
+ generationId,
2295
+ responseMessageId,
2296
+ error: errorMessage(error),
2297
+ }
2298
+ return {
2299
+ type: 'ai.generation.failed',
2300
+ id: await aiEventId('a2.ai.failed', generationId),
2301
+ payload: failed,
2302
+ } as AppendInput<D>
2303
+ }
2304
+ }
2305
+
2306
+ return {
2307
+ 'ai.model.metadata.requested': {
2308
+ handler: async (ctx) => {
2309
+ const limits = await readModelLimits({
2310
+ modelId: ctx.event.payload.modelId,
2311
+ signal: ctx.signal,
2312
+ })
2313
+ ctx.signal.throwIfAborted()
2314
+ return {
2315
+ type: 'ai.model.metadata.resolved',
2316
+ id: await aiEventId('a2.ai.model.metadata.resolved', ctx.event.id),
2317
+ payload: { modelId: ctx.event.payload.modelId, limits },
2318
+ } as AppendInput<D>
2319
+ },
2320
+ },
2321
+ 'ai.control.requested': {
2322
+ lane: ({ event }) => {
2323
+ const command = event.payload as ControlCommand<M>
2324
+ if (event.id === '')
2325
+ throw new A2Error(
2326
+ 'INVALID_PAYLOAD',
2327
+ 'AI command IDs must be nonempty',
2328
+ )
2329
+ if (event.id?.startsWith(AI_EVENT_ID_PREFIX))
2330
+ throw new A2Error(
2331
+ 'INVALID_PAYLOAD',
2332
+ `AI command IDs cannot use the reserved '${AI_EVENT_ID_PREFIX}' prefix`,
2333
+ )
2334
+ if (
2335
+ (command.action === 'send' || command.action === 'steer') &&
2336
+ event.id !== command.message.id
2337
+ )
2338
+ throw new A2Error(
2339
+ 'INVALID_PAYLOAD',
2340
+ 'AI input event ID must equal message.id',
2341
+ )
2342
+ return 'a2.ai.control'
2343
+ },
2344
+ handler: controlHandler,
2345
+ },
2346
+ 'ai.work.reported': { lane: 'a2.ai.control', handler: controlHandler },
2347
+ 'ai.session.closed': { lane: 'a2.ai.control', handler: controlHandler },
2348
+ 'ai.generation.requested': {
2349
+ lane: 'a2.ai.model',
2350
+ abortOn: {
2351
+ 'ai.control.requested': (event, trigger) =>
2352
+ event.payload.action === 'stop' &&
2353
+ event.payload.turnId === trigger.payload.control?.turnId,
2354
+ 'ai.control.committed': (event, trigger) => {
2355
+ const active = (event.payload as ControlCommit).view
2356
+ return (
2357
+ active?.turnId !== trigger.payload.control?.turnId ||
2358
+ active?.version !== trigger.payload.control?.version
2359
+ )
2360
+ },
2361
+ 'ai.session.closed': true,
2362
+ },
2363
+ handler: (ctx) =>
2364
+ telemetry.span(
2365
+ 'a2.ai.generate',
2366
+ {
2367
+ 'a2.contract': options.contract.name,
2368
+ 'a2.session_id': ctx.event.sessionId,
2369
+ 'a2.ai.generate.request_id': ctx.event.id,
2370
+ 'a2.ai.generate.attempt': ctx.attempt,
2371
+ },
2372
+ async () => {
2373
+ try {
2374
+ return control.settled({
2375
+ ctx,
2376
+ events: await generationHandler(ctx),
2377
+ })
2378
+ } catch (error) {
2379
+ if (control.cancelled({ error, signal: ctx.signal }))
2380
+ return control.settled({ ctx, events: undefined })
2381
+ throw error
2382
+ }
2383
+ },
2384
+ ),
2385
+ },
2386
+ 'ai.tool.execution.requested': {
2387
+ ...(options.toolLane === undefined ? {} : { lane: options.toolLane }),
2388
+ abortOn: {
2389
+ 'ai.control.requested': (event, trigger) =>
2390
+ event.payload.action === 'stop' &&
2391
+ event.payload.turnId === trigger.payload.turnId,
2392
+ 'ai.generation.failed': (event, trigger) =>
2393
+ event.payload.generationId === trigger.payload.call.generationId,
2394
+ 'ai.control.committed': (event, trigger) => {
2395
+ const active = (event.payload as ControlCommit).view
2396
+ return (
2397
+ active?.turnId !== trigger.payload.turnId ||
2398
+ active?.version !== trigger.payload.version
2399
+ )
2400
+ },
2401
+ 'ai.session.closed': true,
2402
+ },
2403
+ handler: (ctx) =>
2404
+ telemetry.span(
2405
+ 'a2.ai.tool',
2406
+ {
2407
+ 'a2.contract': options.contract.name,
2408
+ 'a2.session_id': ctx.event.sessionId,
2409
+ 'a2.ai.tool.request_id': ctx.event.payload.call.requestId,
2410
+ 'a2.ai.tool.call_id': ctx.event.payload.call.toolCallId,
2411
+ 'a2.ai.tool.name': ctx.event.payload.call.toolName,
2412
+ 'a2.ai.tool.attempt': ctx.attempt,
2413
+ },
2414
+ () => handleToolExecution(ctx),
2415
+ ),
2416
+ },
2417
+ } as NonNullable<ServerOptions<D>['handlers']>
2418
+ }
2419
+
2420
+ /**
2421
+ * Serve an AI agent: the built-in protocol, its handlers, and A2's store,
2422
+ * scheduler, and telemetry, as one ordinary A2 server. Pass `name` for the
2423
+ * built-in protocol, or a shared `agentContract()` when clients fold
2424
+ * application state.
2425
+ */
2426
+ export function agent<
2427
+ T extends ToolSet = ToolSet,
2428
+ M extends UIMessage = UIMessage,
2429
+ E extends EventDefs = Record<never, never>,
2430
+ H extends string = never,
2431
+ >(
2432
+ options: AgentOptions<M, E, T, H>,
2433
+ ): Agent<AIEventDefs<M> & E, AIState<M>, typeof AI_STATE_REDUCER>
2434
+ export function agent<
2435
+ D extends AIEventDefs<AIMessageOf<D>> & EventDefs,
2436
+ S,
2437
+ T extends ToolSet = ToolSet,
2438
+ Name extends string = string,
2439
+ X extends object = Record<never, never>,
2440
+ H extends string = never,
2441
+ >(options: AgentFromContractOptions<D, S, T, Name, X, H>): Agent<D, S, Name>
2442
+ export function agent(
2443
+ options:
2444
+ | AgentOptions<UIMessage, EventDefs, ToolSet>
2445
+ | AgentFromContractOptions<AIEventDefs & EventDefs, AIState, ToolSet>,
2446
+ ): Agent<AIEventDefs & EventDefs, AIState> {
2447
+ type D = AIEventDefs & EventDefs
2448
+ assertNoLoopOptions(options)
2449
+ const {
2450
+ model,
2451
+ tools,
2452
+ instructions,
2453
+ maxSteps,
2454
+ harness,
2455
+ toolRecovery,
2456
+ progress,
2457
+ toolLane,
2458
+ handlers,
2459
+ ...rest
2460
+ } = options
2461
+ let contract: Contract<D, Record<never, never>, Reducer<D, AIState>>
2462
+ let serverOptions: Omit<
2463
+ ServerOptions<D, Record<never, never>, Reducer<D, AIState>>,
2464
+ 'contract' | 'handlers'
2465
+ >
2466
+ if ('contract' in rest) {
2467
+ const { contract: shared, ...remaining } = rest
2468
+ if ('name' in remaining)
2469
+ throw new TypeError('agent takes either name or contract, not both')
2470
+ if (!isAgentContract(shared))
2471
+ throw new TypeError('agent({ contract }) takes an agentContract()')
2472
+ contract = shared
2473
+ serverOptions = remaining
2474
+ } else {
2475
+ const { name, events, messageSchema, ...remaining } = rest
2476
+ contract = agentContract({
2477
+ name,
2478
+ ...(events === undefined ? {} : { events }),
2479
+ ...(messageSchema === undefined ? {} : { messageSchema }),
2480
+ })
2481
+ serverOptions = remaining
2482
+ }
2483
+ const states = harnessStates()
2484
+ const builtIns = createHandlers<UIMessage, D>({
2485
+ contract,
2486
+ model,
2487
+ harnessStates: states,
2488
+ ...(serverOptions.telemetry === undefined
2489
+ ? {}
2490
+ : { telemetry: serverOptions.telemetry }),
2491
+ ...(tools === undefined ? {} : { tools }),
2492
+ ...(instructions === undefined ? {} : { instructions }),
2493
+ ...(maxSteps === undefined ? {} : { maxSteps }),
2494
+ ...(harness === undefined ? {} : { harness }),
2495
+ ...(toolRecovery === undefined ? {} : { toolRecovery }),
2496
+ ...(progress === undefined ? {} : { progress }),
2497
+ ...(toolLane === undefined ? {} : { toolLane }),
2498
+ } as HandlersOptions<UIMessage, D>)
2499
+ for (const type of Object.keys(handlers ?? {})) {
2500
+ if (Object.hasOwn(builtIns, type))
2501
+ throw new TypeError(`agent handles '${type}' itself`)
2502
+ }
2503
+ const server = createServer({
2504
+ ...serverOptions,
2505
+ contract,
2506
+ handlers: { ...builtIns, ...handlers },
2507
+ })
2508
+ registerHarnessStates(server, states)
2509
+ const applicationEvents = 'contract' in rest
2510
+ setServerFetchHooks(server, {
2511
+ validateIngress: (context) =>
2512
+ validateAgentIngress({ context, applicationEvents }),
2513
+ })
2514
+ return server
2515
+ }
2516
+
2517
+ export type { Instructions, ToolSet }