experimental-a2 0.20.0 → 0.22.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (309) hide show
  1. package/CHANGELOG.md +55 -0
  2. package/README.md +1 -1
  3. package/dist/{actor-C7b-1arn.d.ts → actor-CqibLgjQ.d.ts} +15 -7
  4. package/dist/actor-CqibLgjQ.d.ts.map +1 -0
  5. package/dist/actor-client.d.ts +1 -1
  6. package/dist/actor-client.js +2 -2
  7. package/dist/actor-react.d.ts +3 -3
  8. package/dist/actor-react.js +5 -5
  9. package/dist/{actor-shared-t7w8xIKP.js → actor-shared-DFET5q16.js} +3 -3
  10. package/dist/actor-shared-DFET5q16.js.map +1 -0
  11. package/dist/actor.d.ts +1 -1
  12. package/dist/actor.js +54 -43
  13. package/dist/actor.js.map +1 -1
  14. package/dist/{ai-Cz3hqVhk.d.ts → ai-DAu77CDr.d.ts} +8 -8
  15. package/dist/{ai-Cz3hqVhk.d.ts.map → ai-DAu77CDr.d.ts.map} +1 -1
  16. package/dist/{ai-mMiWul1T.js → ai-EEfKyc-z.js} +9 -9
  17. package/dist/ai-EEfKyc-z.js.map +1 -0
  18. package/dist/ai-client.d.ts +2 -2
  19. package/dist/ai-client.js +2 -2
  20. package/dist/ai-fx.d.ts +4 -2
  21. package/dist/ai-fx.d.ts.map +1 -1
  22. package/dist/ai-fx.js +214 -67
  23. package/dist/ai-fx.js.map +1 -1
  24. package/dist/{ai-harness-CV6rXbB3.d.ts → ai-harness-DTjkAr2T.d.ts} +2 -2
  25. package/dist/{ai-harness-CV6rXbB3.d.ts.map → ai-harness-DTjkAr2T.d.ts.map} +1 -1
  26. package/dist/{ai-harness-Clbg5ei7.js → ai-harness-dQeJaR1t.js} +7 -3
  27. package/dist/ai-harness-dQeJaR1t.js.map +1 -0
  28. package/dist/ai-pi.d.ts +1 -1
  29. package/dist/ai-pi.js +3 -3
  30. package/dist/ai-pi.js.map +1 -1
  31. package/dist/ai-react.d.ts +5 -5
  32. package/dist/ai-react.js +2 -2
  33. package/dist/ai-server.d.ts +14 -17
  34. package/dist/ai-server.d.ts.map +1 -1
  35. package/dist/ai-server.js +86 -73
  36. package/dist/ai-server.js.map +1 -1
  37. package/dist/ai.d.ts +2 -2
  38. package/dist/ai.js +2 -2
  39. package/dist/{client-BhHLoNKF.js → client-BQS4i_V9.js} +29 -6
  40. package/dist/client-BQS4i_V9.js.map +1 -0
  41. package/dist/{client-h_DKFAjj.d.ts → client-CUMbS_38.d.ts} +3 -3
  42. package/dist/{client-h_DKFAjj.d.ts.map → client-CUMbS_38.d.ts.map} +1 -1
  43. package/dist/client.d.ts +3 -3
  44. package/dist/client.js +1 -1
  45. package/dist/{contract-BYKASlCV.js → contract-CWukt01d.js} +2 -2
  46. package/dist/contract-CWukt01d.js.map +1 -0
  47. package/dist/index.d.ts +5 -5
  48. package/dist/index.js +3 -3
  49. package/dist/{initial-snapshot-Bx-c0pDP.d.ts → initial-snapshot-D3Lve43o.d.ts} +4 -4
  50. package/dist/initial-snapshot-D3Lve43o.d.ts.map +1 -0
  51. package/dist/{internal-CscSY7K_.js → internal-CvsrprnU.js} +40 -16
  52. package/dist/internal-CvsrprnU.js.map +1 -0
  53. package/dist/otel.d.ts +1 -1
  54. package/dist/otel.js +1 -1
  55. package/dist/otel.js.map +1 -1
  56. package/dist/{react-BBXgHWFc.d.ts → react-BoHvC3v9.d.ts} +4 -4
  57. package/dist/{react-BBXgHWFc.d.ts.map → react-BoHvC3v9.d.ts.map} +1 -1
  58. package/dist/react.d.ts +1 -1
  59. package/dist/react.js +1 -1
  60. package/dist/{reducer-CqA59Q6U.d.ts → reducer-CYPHsSmd.d.ts} +2 -2
  61. package/dist/{reducer-CqA59Q6U.d.ts.map → reducer-CYPHsSmd.d.ts.map} +1 -1
  62. package/dist/{reducer-BAXc8JyF.js → reducer-CjmvZSAb.js} +2 -2
  63. package/dist/{reducer-BAXc8JyF.js.map → reducer-CjmvZSAb.js.map} +1 -1
  64. package/dist/runtime-consumer-CIeL33wj.js +12 -0
  65. package/dist/runtime-consumer-CIeL33wj.js.map +1 -0
  66. package/dist/scheduler-qstash.d.ts +2 -2
  67. package/dist/scheduler-qstash.js +3 -3
  68. package/dist/scheduler-task-X1SToDjP.js.map +1 -1
  69. package/dist/scheduler-vercel.d.ts +2 -2
  70. package/dist/scheduler-vercel.js +3 -3
  71. package/dist/scheduler-vercel.js.map +1 -1
  72. package/dist/{server-qyb5aJJG.js → server-DtFrWxi3.js} +412 -116
  73. package/dist/server-DtFrWxi3.js.map +1 -0
  74. package/dist/{server-DYAATFuT.d.ts → server-qgez0Xny.d.ts} +108 -41
  75. package/dist/server-qgez0Xny.d.ts.map +1 -0
  76. package/dist/server.browser.js +1 -1
  77. package/dist/server.browser.js.map +1 -1
  78. package/dist/server.d.ts +4 -4
  79. package/dist/server.js +2 -2
  80. package/dist/{session-clear-C-QwTXV1.d.ts → session-clear-D1PFe7xl.d.ts} +2 -2
  81. package/dist/session-clear-D1PFe7xl.d.ts.map +1 -0
  82. package/dist/{session-clear-DGI4jTQ5.js → session-clear-wHd5GCpZ.js} +5 -4
  83. package/dist/session-clear-wHd5GCpZ.js.map +1 -0
  84. package/dist/store-D0MhBvg6.d.ts +532 -0
  85. package/dist/store-D0MhBvg6.d.ts.map +1 -0
  86. package/dist/store-N8PXxDAS.js.map +1 -1
  87. package/dist/store-codec-DwrWlgQx.js +65 -0
  88. package/dist/store-codec-DwrWlgQx.js.map +1 -0
  89. package/dist/store-memory.d.ts +1 -1
  90. package/dist/store-memory.d.ts.map +1 -1
  91. package/dist/store-memory.js +406 -225
  92. package/dist/store-memory.js.map +1 -1
  93. package/dist/{store-polling-7WJV0YWP.js → store-polling-BZt2Rf3D.js} +2 -2
  94. package/dist/{store-polling-7WJV0YWP.js.map → store-polling-BZt2Rf3D.js.map} +1 -1
  95. package/dist/store-postgres.d.ts +1 -1
  96. package/dist/store-postgres.d.ts.map +1 -1
  97. package/dist/store-postgres.js +1598 -587
  98. package/dist/store-postgres.js.map +1 -1
  99. package/dist/store-presence-polling-JuAy2Ck2.js +111 -0
  100. package/dist/store-presence-polling-JuAy2Ck2.js.map +1 -0
  101. package/dist/store-redis-http.d.ts +1 -1
  102. package/dist/store-redis-http.js +4 -4
  103. package/dist/store-redis-notify-CsJw_hfD.js +3060 -0
  104. package/dist/store-redis-notify-CsJw_hfD.js.map +1 -0
  105. package/dist/store-redis.d.ts +1 -1
  106. package/dist/store-redis.d.ts.map +1 -1
  107. package/dist/store-redis.js +4 -5
  108. package/dist/store-redis.js.map +1 -1
  109. package/dist/store-sqlite.d.ts +1 -1
  110. package/dist/store-sqlite.d.ts.map +1 -1
  111. package/dist/store-sqlite.js +527 -299
  112. package/dist/store-sqlite.js.map +1 -1
  113. package/dist/stream-buffer-DrTyiIuo.js +57 -0
  114. package/dist/stream-buffer-DrTyiIuo.js.map +1 -0
  115. package/dist/{telemetry-Cyfqo6Qt.d.ts → telemetry-CWwsh-Gx.d.ts} +3 -3
  116. package/dist/telemetry-CWwsh-Gx.d.ts.map +1 -0
  117. package/dist/{wire-BKxH1vbP.js → wire-CV-Z7wz-.js} +6 -5
  118. package/dist/wire-CV-Z7wz-.js.map +1 -0
  119. package/docs/01-quickstart.mdx +21 -15
  120. package/docs/02-examples.mdx +1 -1
  121. package/docs/actors/01-introduction.mdx +13 -2
  122. package/docs/actors/02-concurrency.mdx +1 -1
  123. package/docs/agents/01-introduction.mdx +1 -1
  124. package/docs/agents/02-tools.mdx +12 -11
  125. package/docs/agents/03-conversations.mdx +36 -25
  126. package/docs/agents/04-harnesses.mdx +14 -7
  127. package/docs/client/03-presence.mdx +18 -14
  128. package/docs/contract/01-events.mdx +2 -2
  129. package/docs/contract/02-state.mdx +18 -14
  130. package/docs/contract/03-sessions.mdx +32 -25
  131. package/docs/index.mdx +29 -24
  132. package/docs/reference/01-core.mdx +7 -6
  133. package/docs/reference/02-server.mdx +250 -191
  134. package/docs/reference/04-ai.mdx +33 -23
  135. package/docs/reference/05-telemetry.mdx +27 -13
  136. package/docs/reference/06-errors.mdx +25 -19
  137. package/docs/reference/meta.ts +1 -1
  138. package/docs/schedulers/01-overview.mdx +8 -8
  139. package/docs/schedulers/02-vercel-queues.mdx +2 -2
  140. package/docs/schedulers/03-qstash.mdx +4 -4
  141. package/docs/server/01-consumers.mdx +418 -0
  142. package/docs/server/02-timers.mdx +31 -21
  143. package/docs/server/03-cancellation.mdx +52 -44
  144. package/docs/server/04-routes.mdx +4 -1
  145. package/docs/server/05-sql-projections.mdx +391 -0
  146. package/docs/server/06-guarantees.mdx +61 -51
  147. package/docs/stores/01-overview.mdx +3 -3
  148. package/docs/stores/02-redis.mdx +24 -10
  149. package/docs/stores/03-redis-http.mdx +8 -6
  150. package/docs/stores/04-postgres.mdx +65 -14
  151. package/docs/stores/05-sqlite.mdx +5 -3
  152. package/docs/upgrades/01-overview.mdx +31 -0
  153. package/docs/upgrades/78-0-22.mdx +115 -0
  154. package/docs/upgrades/79-0-21.mdx +165 -0
  155. package/docs/upgrades/80-0-20.mdx +43 -0
  156. package/docs/upgrades/81-0-19.mdx +33 -0
  157. package/docs/upgrades/82-0-18.mdx +33 -0
  158. package/docs/upgrades/83-0-17.mdx +69 -0
  159. package/docs/upgrades/meta.ts +3 -0
  160. package/examples/playground/DEPLOY.md +39 -7
  161. package/examples/playground/app/a0/chat.test.tsx +2 -2
  162. package/examples/playground/app/a0/sandbox-log.ts +17 -15
  163. package/examples/playground/app/a0/sandbox.ts +58 -13
  164. package/examples/playground/app/a0/server.ts +4 -4
  165. package/examples/playground/app/a2slack/a2slack.css +1067 -0
  166. package/examples/playground/app/a2slack/c/[channel]/page.tsx +20 -0
  167. package/examples/playground/app/a2slack/consumers/agent.ts +63 -0
  168. package/examples/playground/app/a2slack/consumers/feed.ts +164 -0
  169. package/examples/playground/app/a2slack/consumers/notify.ts +116 -0
  170. package/examples/playground/app/a2slack/consumers/search.ts +37 -0
  171. package/examples/playground/app/a2slack/consumers/triage.ts +31 -0
  172. package/examples/playground/app/a2slack/consumers/welcome.ts +25 -0
  173. package/examples/playground/app/a2slack/db/a2.ts +209 -0
  174. package/examples/playground/app/a2slack/db/index.ts +73 -0
  175. package/examples/playground/app/a2slack/db/migrations/0000_extensions.sql +4 -0
  176. package/examples/playground/app/a2slack/db/migrations/0001_workspace.sql +74 -0
  177. package/examples/playground/app/a2slack/db/migrations/0002_a2.sql +215 -0
  178. package/examples/playground/app/a2slack/db/migrations/0003_a2_batch.sql +67 -0
  179. package/examples/playground/app/a2slack/db/migrations/0004_a2_session_event_ids.sql +2 -0
  180. package/examples/playground/app/a2slack/db/migrations/meta/0000_snapshot.json +18 -0
  181. package/examples/playground/app/a2slack/db/migrations/meta/0001_snapshot.json +500 -0
  182. package/examples/playground/app/a2slack/db/migrations/meta/0002_snapshot.json +1159 -0
  183. package/examples/playground/app/a2slack/db/migrations/meta/0003_snapshot.json +1159 -0
  184. package/examples/playground/app/a2slack/db/migrations/meta/0004_snapshot.json +1165 -0
  185. package/examples/playground/app/a2slack/db/migrations/meta/_journal.json +41 -0
  186. package/examples/playground/app/a2slack/db/queries.ts +263 -0
  187. package/examples/playground/app/a2slack/db/schema.ts +145 -0
  188. package/examples/playground/app/a2slack/drills/a2slack.drill.ts +708 -0
  189. package/examples/playground/app/a2slack/drills/harness.ts +244 -0
  190. package/examples/playground/app/a2slack/events/route.ts +36 -0
  191. package/examples/playground/app/a2slack/identity.ts +44 -0
  192. package/examples/playground/app/a2slack/inbox/route.ts +8 -0
  193. package/examples/playground/app/a2slack/lag/data/route.ts +8 -0
  194. package/examples/playground/app/a2slack/lag/page.tsx +101 -0
  195. package/examples/playground/app/a2slack/lag.ts +73 -0
  196. package/examples/playground/app/a2slack/layout.tsx +43 -0
  197. package/examples/playground/app/a2slack/messages/route.ts +17 -0
  198. package/examples/playground/app/a2slack/model.test.ts +208 -0
  199. package/examples/playground/app/a2slack/model.ts +369 -0
  200. package/examples/playground/app/a2slack/models.test.ts +72 -0
  201. package/examples/playground/app/a2slack/page.tsx +10 -0
  202. package/examples/playground/app/a2slack/scheduler/route.ts +5 -0
  203. package/examples/playground/app/a2slack/search/route.ts +40 -0
  204. package/examples/playground/app/a2slack/search-index.ts +108 -0
  205. package/examples/playground/app/a2slack/server.test.ts +315 -0
  206. package/examples/playground/app/a2slack/server.ts +55 -0
  207. package/examples/playground/app/a2slack/sessions.ts +50 -0
  208. package/examples/playground/app/a2slack/thread/route.ts +8 -0
  209. package/examples/playground/app/a2slack/ui/ChannelView.tsx +87 -0
  210. package/examples/playground/app/a2slack/ui/Composer.tsx +117 -0
  211. package/examples/playground/app/a2slack/ui/DotsShadow.tsx +42 -0
  212. package/examples/playground/app/a2slack/ui/Feed.tsx +157 -0
  213. package/examples/playground/app/a2slack/ui/InboxContext.tsx +63 -0
  214. package/examples/playground/app/a2slack/ui/InboxView.tsx +82 -0
  215. package/examples/playground/app/a2slack/ui/Menu.tsx +128 -0
  216. package/examples/playground/app/a2slack/ui/MessageRow.tsx +314 -0
  217. package/examples/playground/app/a2slack/ui/NameGate.tsx +35 -0
  218. package/examples/playground/app/a2slack/ui/SearchContext.tsx +146 -0
  219. package/examples/playground/app/a2slack/ui/SearchResults.tsx +97 -0
  220. package/examples/playground/app/a2slack/ui/Shell.tsx +34 -0
  221. package/examples/playground/app/a2slack/ui/Sidebar.tsx +199 -0
  222. package/examples/playground/app/a2slack/ui/ThreadPanel.tsx +134 -0
  223. package/examples/playground/app/a2slack/ui/useInbox.ts +71 -0
  224. package/examples/playground/app/agent/server.ts +3 -3
  225. package/examples/playground/app/chat/[chatId]/chat-client.tsx +1 -1
  226. package/examples/playground/app/chat/server.ts +45 -35
  227. package/examples/playground/app/components/demo-icons.tsx +7 -0
  228. package/examples/playground/app/components/demos.tsx +7 -0
  229. package/examples/playground/app/orders/[orderId]/order-client.tsx +2 -2
  230. package/examples/playground/app/orders/page.tsx +1 -1
  231. package/examples/playground/app/orders/server.ts +49 -30
  232. package/examples/playground/app/recovery/[recoveryId]/recovery-client.tsx +14 -15
  233. package/examples/playground/app/recovery/page.tsx +4 -4
  234. package/examples/playground/app/recovery/server.ts +23 -32
  235. package/examples/playground/drizzle.config.ts +14 -0
  236. package/examples/playground/lib/store.ts +1 -1
  237. package/examples/playground/next.config.ts +6 -1
  238. package/examples/playground/package.json +12 -1
  239. package/examples/playground/scripts/migrate.mjs +31 -0
  240. package/examples/playground/vercel.json +9 -0
  241. package/examples/playground/vitest.drills.config.ts +32 -0
  242. package/package.json +5 -2
  243. package/sql/postgres/drizzle-schema.ts +209 -0
  244. package/sql/postgres/schema.sql +227 -67
  245. package/src/actor-shared.ts +10 -2
  246. package/src/actor.ts +101 -71
  247. package/src/ai-agent.ts +162 -111
  248. package/src/ai-control-server.ts +8 -8
  249. package/src/ai-fx.ts +324 -82
  250. package/src/ai-harness.ts +17 -2
  251. package/src/ai-pi.ts +2 -2
  252. package/src/ai-server.ts +2 -2
  253. package/src/ai.ts +7 -7
  254. package/src/client.ts +19 -2
  255. package/src/contract.ts +1 -1
  256. package/src/deterministic-id.ts +27 -4
  257. package/src/errors.ts +8 -5
  258. package/src/idempotent-replay.ts +0 -1
  259. package/src/index.ts +1 -1
  260. package/src/internal.ts +57 -16
  261. package/src/otel.ts +1 -1
  262. package/src/postgres-schema.ts +98 -57
  263. package/src/presence-recovery.ts +18 -11
  264. package/src/presence-tombstones.ts +52 -0
  265. package/src/push-envelope.ts +20 -0
  266. package/src/runtime-consumer.ts +10 -0
  267. package/src/scheduler-task.ts +2 -2
  268. package/src/scheduler-vercel.ts +1 -1
  269. package/src/server.browser.ts +1 -1
  270. package/src/server.ts +819 -218
  271. package/src/session-socket.ts +1 -1
  272. package/src/store-codec.ts +66 -0
  273. package/src/store-memory.ts +604 -286
  274. package/src/store-postgres.ts +2006 -857
  275. package/src/store-presence-polling.ts +59 -34
  276. package/src/store-redis-core.ts +1961 -1333
  277. package/src/store-redis-notify.ts +76 -26
  278. package/src/store-redis.ts +2 -3
  279. package/src/store-sqlite.ts +915 -516
  280. package/src/store.ts +241 -102
  281. package/src/telemetry.ts +2 -1
  282. package/src/wire.ts +1 -0
  283. package/dist/actor-C7b-1arn.d.ts.map +0 -1
  284. package/dist/actor-shared-t7w8xIKP.js.map +0 -1
  285. package/dist/ai-harness-Clbg5ei7.js.map +0 -1
  286. package/dist/ai-mMiWul1T.js.map +0 -1
  287. package/dist/client-BhHLoNKF.js.map +0 -1
  288. package/dist/contract-BYKASlCV.js.map +0 -1
  289. package/dist/idempotent-replay-JdgMQRRm.js +0 -21
  290. package/dist/idempotent-replay-JdgMQRRm.js.map +0 -1
  291. package/dist/initial-snapshot-Bx-c0pDP.d.ts.map +0 -1
  292. package/dist/internal-CscSY7K_.js.map +0 -1
  293. package/dist/server-DYAATFuT.d.ts.map +0 -1
  294. package/dist/server-qyb5aJJG.js.map +0 -1
  295. package/dist/session-clear-C-QwTXV1.d.ts.map +0 -1
  296. package/dist/session-clear-DGI4jTQ5.js.map +0 -1
  297. package/dist/store-CyLUy86v.d.ts +0 -404
  298. package/dist/store-CyLUy86v.d.ts.map +0 -1
  299. package/dist/store-codec-DTG0Ftek.js +0 -10
  300. package/dist/store-codec-DTG0Ftek.js.map +0 -1
  301. package/dist/store-presence-polling-B1ixeqIS.js +0 -94
  302. package/dist/store-presence-polling-B1ixeqIS.js.map +0 -1
  303. package/dist/store-redis-notify-CM0AfLY0.js +0 -2544
  304. package/dist/store-redis-notify-CM0AfLY0.js.map +0 -1
  305. package/dist/stream-buffer-Cx7AAPGq.js +0 -23
  306. package/dist/stream-buffer-Cx7AAPGq.js.map +0 -1
  307. package/dist/telemetry-Cyfqo6Qt.d.ts.map +0 -1
  308. package/dist/wire-BKxH1vbP.js.map +0 -1
  309. package/docs/server/01-handlers.mdx +0 -227
package/src/ai-fx.ts CHANGED
@@ -34,18 +34,35 @@ import {
34
34
  } from './ai-harness.ts'
35
35
  import type { CompactionPartData, GenerationCompletedPayload } from './ai.ts'
36
36
  import { schemaInput } from './ai-tool-input.ts'
37
+ import { HARNESS_PARKING } from './internal.ts'
37
38
  import { boundedCache } from './bounded-cache.ts'
38
39
 
39
40
  export type { Harness } from './ai-harness.ts'
40
41
 
41
42
  /**
42
43
  * The FX harness. Every model call FX makes is one durable step; A2 rebuilds
43
- * the kernel for each step from its latest checkpoint and the log since.
44
+ * the kernel for each step from its latest checkpoint and the log since,
45
+ * unless this process holds the kernel the step before left waiting on its
46
+ * tool calls.
44
47
  */
45
48
  export function fx(): Harness<'fx', never> {
49
+ // Per agent, as its saved states are: sessions of two agents never share one.
50
+ const lots = new WeakMap<object, ParkingLot>()
51
+ const lotOf = (scope: object): ParkingLot => {
52
+ let lot = lots.get(scope)
53
+ if (lot === undefined) {
54
+ lot = parkingLot()
55
+ lots.set(scope, lot)
56
+ }
57
+ return lot
58
+ }
46
59
  return defineHarness('fx', {
47
60
  tools: {},
48
- step: runStep,
61
+ step: (step) => runStep(step, lotOf(step.scope)),
62
+ release: ({ scope, sessionId }) => {
63
+ const entry = lots.get(scope)?.take(sessionId)
64
+ if (entry !== undefined) closeParked(entry)
65
+ },
49
66
  })
50
67
  }
51
68
 
@@ -72,6 +89,8 @@ type TurnPlan = {
72
89
  /** Steering to queue while serving the call at that index. */
73
90
  steers: Map<number, string[]>
74
91
  end: 'stop' | 'interrupted' | 'live'
92
+ /** The attempt that made its last committed model call. */
93
+ generationId?: string
75
94
  }
76
95
 
77
96
  type Completion = { finishReason?: FinishReason; usage?: LanguageModelUsage }
@@ -92,6 +111,12 @@ type TurnState = {
92
111
  parsed?: Promise<void>
93
112
  }
94
113
 
114
+ /** A model request as the kernel sends it to the Gateway. */
115
+ type GatewayRequest = {
116
+ toolChoice?: { type?: string }
117
+ providerOptions?: Record<string, Record<string, unknown> | undefined>
118
+ }
119
+
95
120
  function fail(state: TurnState, error: Error): never {
96
121
  state.failure ??= error
97
122
  state.fxTurn.cancel()
@@ -100,12 +125,102 @@ function fail(state: TurnState, error: Error): never {
100
125
 
101
126
  const diverged = (message: string): Divergence => new Divergence('FX', message)
102
127
 
128
+ // ------------------------------------------------------------- kernels
129
+
130
+ /** A tool call the kernel waits on while A2's tool consumer runs it. */
131
+ type Waiting = {
132
+ toolName: string
133
+ input: unknown
134
+ resolve: (output: unknown) => void
135
+ reject: (error: unknown) => void
136
+ }
137
+
138
+ /** What a kernel's fetch and tools call: the step driving it now. */
139
+ type KernelHooks = {
140
+ fetch: (
141
+ input: string | URL | Request,
142
+ init?: RequestInit,
143
+ ) => Promise<Response>
144
+ tool: (toolName: string, input: unknown) => unknown
145
+ }
146
+
147
+ type Kernel = { agent: FxAgent; hooks: KernelHooks; waiting: Waiting[] }
148
+
149
+ /**
150
+ * A kernel a step left waiting on its tool calls. The session's next step in
151
+ * this process, in the same response or one that steered it, continues it
152
+ * with their results instead of rebuilding one from the log.
153
+ */
154
+ type Parked = {
155
+ kernel: Kernel
156
+ fxTurn: FxTurn
157
+ drained: Promise<void>
158
+ /** The attempt whose model call the kernel made. */
159
+ generationId: string
160
+ settings: string
161
+ }
162
+
163
+ type ParkingLot = {
164
+ /** Removes the session's parked kernel; the caller then owns it. */
165
+ take(sessionId: string): Parked | undefined
166
+ park(sessionId: string, entry: Parked): void
167
+ }
168
+
169
+ function closeParked(entry: Parked): void {
170
+ entry.fxTurn.cancel()
171
+ void entry.kernel.agent.close().catch(() => {})
172
+ }
173
+
174
+ /**
175
+ * One agent's parked kernels: one per session, at most `maxKernels`. Each is
176
+ * closed when its session has nothing left to run in this process (the
177
+ * agent's release), when its turn ends, or when another takes its place.
178
+ */
179
+ function parkingLot(): ParkingLot {
180
+ const held = new Map<string, Parked>()
181
+ const take = (sessionId: string): Parked | undefined => {
182
+ const entry = held.get(sessionId)
183
+ held.delete(sessionId)
184
+ return entry
185
+ }
186
+ return {
187
+ take,
188
+ park(sessionId, entry) {
189
+ const replaced = take(sessionId)
190
+ if (replaced !== undefined) closeParked(replaced)
191
+ held.set(sessionId, entry)
192
+ // A turn that ends while it waits, as a cancelled one does, is done.
193
+ const ended = () => {
194
+ if (held.get(sessionId) !== entry) return
195
+ held.delete(sessionId)
196
+ closeParked(entry)
197
+ }
198
+ entry.fxTurn.result.then(ended, ended)
199
+ const oldest = held.keys().next().value
200
+ if (held.size > HARNESS_PARKING.maxKernels && oldest !== undefined) {
201
+ const evicted = take(oldest)
202
+ if (evicted !== undefined) closeParked(evicted)
203
+ }
204
+ },
205
+ }
206
+ }
207
+
103
208
  // ---------------------------------------------------------------- step
104
209
 
105
- async function runStep(step: HarnessStep): Promise<HarnessGeneration> {
210
+ async function runStep(
211
+ step: HarnessStep,
212
+ lot: ParkingLot,
213
+ ): Promise<HarnessGeneration> {
106
214
  const { createFxAgent } = await loadLibfx()
107
215
  const credential = gatewayCredential('fx')
108
216
  const name = `${step.agentName}.a2.harness.fx`
217
+ const definitions = await toolDefinitions(step.tools)
218
+ // What the kernel is made with: one made otherwise can't continue the step.
219
+ const settings = JSON.stringify([
220
+ kernelModel(step),
221
+ step.instructions,
222
+ definitions,
223
+ ])
109
224
  const { replayAll, ...first } = await readHarness({
110
225
  step,
111
226
  name,
@@ -119,17 +234,25 @@ async function runStep(step: HarnessStep): Promise<HarnessGeneration> {
119
234
  // has read a response's finish, while A2 may still be reading its copy.
120
235
  const released = new AbortController()
121
236
  const network = AbortSignal.any([step.signal, released.signal])
122
- let agent: FxAgent | undefined
237
+ let kernel: Kernel | undefined
238
+ /** Whether this step left its kernel for the next one. */
239
+ let parked = false
123
240
  let turn: TurnState | undefined
124
241
 
125
242
  const run = async (
126
243
  controller: ReadableStreamDefaultController<UIMessageChunk>,
127
244
  turns: TurnPlan[],
245
+ adopted: Parked | undefined,
128
246
  ): Promise<void> => {
129
247
  controller.enqueue({ type: 'start', messageId: step.responseMessageId })
248
+ if (adopted !== undefined) {
249
+ await resume(controller, turns.at(-1)!, adopted)
250
+ return
251
+ }
130
252
  for (const plan of turns) {
131
- if (stopped) return
132
- const fxTurn = agent!.prompt(plan.prompt, { signal: step.signal })
253
+ if (stopped || step.signal.aborted) return
254
+ // The step's abort cancels the turn; the turn outlives it when parked.
255
+ const fxTurn = kernel!.agent.prompt(plan.prompt)
133
256
  const state: TurnState = {
134
257
  plan,
135
258
  fxTurn,
@@ -161,37 +284,123 @@ async function runStep(step: HarnessStep): Promise<HarnessGeneration> {
161
284
  )
162
285
  continue
163
286
  }
164
- const outcome = await Promise.race([
165
- fxTurn.result.then((result) => ({ ended: true, result })),
166
- state.toolsRequested.promise.then(() => ({ ended: false as const })),
167
- ]).catch((error: unknown) => {
168
- throw state.failure ?? error
169
- })
170
- if (state.failure) throw state.failure
171
- await state.parsed
172
- if (outcome.ended) {
173
- await drained
174
- // Only a turn this attempt finished describes the log.
175
- if (outcome.result.stopReason === 'end_turn' && !step.signal.aborted)
176
- await saveCheckpoint()
177
- } else {
178
- // FX now waits on tools that A2's tool handlers run.
179
- fxTurn.cancel()
287
+ await finishLive(controller, state, drained)
288
+ }
289
+ }
290
+
291
+ /** Continues a parked kernel: the results it waits on are in the log. */
292
+ const resume = async (
293
+ controller: ReadableStreamDefaultController<UIMessageChunk>,
294
+ plan: TurnPlan,
295
+ adopted: Parked,
296
+ ): Promise<void> => {
297
+ const state: TurnState = {
298
+ plan,
299
+ fxTurn: adopted.fxTurn,
300
+ next: plan.calls.length,
301
+ served: plan.calls.at(-1)!.toolCalls,
302
+ answered: new Set(),
303
+ live: false,
304
+ liveStep: false,
305
+ toolsRequested: deferred(),
306
+ }
307
+ turn = state
308
+ // Steering goes in before any tool result, so FX's next model call can't
309
+ // start without it, as replay queues it before the recorded call ends.
310
+ for (const text of plan.steers.get(plan.calls.length - 1) ?? [])
311
+ await adopted.fxTurn.steer(text)
312
+ for (const waiting of kernel!.waiting.splice(0)) {
313
+ try {
314
+ const answered = answer(state, waiting.toolName, waiting.input)
315
+ if (answered === undefined)
316
+ fail(state, diverged(`no recorded result for ${waiting.toolName}`))
317
+ waiting.resolve(answered.output)
318
+ } catch (error) {
319
+ waiting.reject(error)
180
320
  }
181
- controller.enqueue({ type: 'finish-step' })
182
- controller.enqueue({
183
- type: 'finish',
184
- ...(completion.finishReason === undefined
185
- ? {}
186
- : { finishReason: completion.finishReason }),
321
+ }
322
+ await finishLive(controller, state, adopted.drained)
323
+ }
324
+
325
+ const finishLive = async (
326
+ controller: ReadableStreamDefaultController<UIMessageChunk>,
327
+ state: TurnState,
328
+ drained: Promise<void>,
329
+ ): Promise<void> => {
330
+ const outcome = await Promise.race([
331
+ state.fxTurn.result.then((result) => ({ ended: true, result })),
332
+ state.toolsRequested.promise.then(() => ({ ended: false as const })),
333
+ ]).catch((error: unknown) => {
334
+ throw state.failure ?? error
335
+ })
336
+ if (state.failure) throw state.failure
337
+ await state.parsed
338
+ if (outcome.ended) {
339
+ if (
340
+ outcome.result.stopReason === 'cancelled' &&
341
+ !stopped &&
342
+ !step.signal.aborted
343
+ )
344
+ throw diverged('the turn was cancelled while this step ran it')
345
+ await drained
346
+ // Only a turn this attempt finished describes the log.
347
+ if (outcome.result.stopReason === 'end_turn' && !step.signal.aborted)
348
+ await saveCheckpoint()
349
+ } else if (kernel!.waiting.length > 0 && !stopped && !closed) {
350
+ // FX now waits on tools that A2's tool consumer runs; the next step in
351
+ // this process can answer it with their results.
352
+ lot.park(step.sessionId, {
353
+ kernel: kernel!,
354
+ fxTurn: state.fxTurn,
355
+ drained,
356
+ generationId: step.generationId,
357
+ settings,
187
358
  })
359
+ parked = true
360
+ } else {
361
+ state.fxTurn.cancel()
188
362
  }
363
+ controller.enqueue({ type: 'finish-step' })
364
+ controller.enqueue({
365
+ type: 'finish',
366
+ ...(completion.finishReason === undefined
367
+ ? {}
368
+ : { finishReason: completion.finishReason }),
369
+ })
370
+ }
371
+
372
+ /**
373
+ * Whether a parked kernel stands where this step's log does: made the same
374
+ * way, its model call the last one the log's live turn committed, and a
375
+ * recorded result for each tool call it waits on. A steered response
376
+ * continues the same turn, so its steps qualify too.
377
+ */
378
+ const adoptable = (entry: Parked, turns: TurnPlan[]): boolean => {
379
+ const plan = turns.at(-1)
380
+ if (
381
+ entry.settings !== settings ||
382
+ plan?.end !== 'live' ||
383
+ plan.generationId !== entry.generationId
384
+ )
385
+ return false
386
+ const unanswered = [...(plan.calls.at(-1)?.toolCalls ?? [])]
387
+ return entry.kernel.waiting.every((waiting) => {
388
+ const at = unanswered.findIndex(
389
+ (call) =>
390
+ call.toolName === waiting.toolName &&
391
+ sameJson(call.input, waiting.input) &&
392
+ results.has(call.toolCallId),
393
+ )
394
+ if (at === -1) return false
395
+ unanswered.splice(at, 1)
396
+ return true
397
+ })
189
398
  }
190
399
 
191
400
  const saveCheckpoint = async (): Promise<void> => {
192
401
  let checkpoint: Uint8Array
193
402
  try {
194
- checkpoint = await agent!.checkpoint()
403
+ checkpoint = await kernel!.agent.checkpoint()
195
404
  } catch {
196
405
  // Later steps replay from the previous snapshot.
197
406
  return
@@ -218,7 +427,8 @@ async function runStep(step: HarnessStep): Promise<HarnessGeneration> {
218
427
  const state = turn!
219
428
  if (state.failure) throw state.failure
220
429
  const body = decodeBody(init.body)
221
- const compaction = isCompaction(body)
430
+ const request = JSON.parse(body) as GatewayRequest
431
+ const compaction = request.toolChoice?.type === 'none'
222
432
  const recorded = state.plan.calls[state.next]
223
433
  if (recorded !== undefined) {
224
434
  if ((recorded.kind === 'compaction') !== compaction)
@@ -245,7 +455,12 @@ async function runStep(step: HarnessStep): Promise<HarnessGeneration> {
245
455
  return pending(init.signal)
246
456
  }
247
457
  state.live = true
248
- return live(state, url, { ...init, body }, compaction)
458
+ return live(
459
+ state,
460
+ url,
461
+ { ...init, body: withGatewayCaching(body, request) },
462
+ compaction,
463
+ )
249
464
  }
250
465
 
251
466
  const affinity = gatewaySessionAffinity({
@@ -256,18 +471,13 @@ async function runStep(step: HarnessStep): Promise<HarnessGeneration> {
256
471
  const live = async (
257
472
  state: TurnState,
258
473
  url: string,
259
- init: RequestInit & { body: string },
474
+ init: RequestInit,
260
475
  compaction: boolean,
261
476
  ): Promise<Response> => {
262
477
  const headers = withAuth(init.headers, credential)
263
478
  // The kernel's own affinity is new with every rebuild; A2's lasts the session.
264
479
  headers.set('x-session-affinity', affinity)
265
- const response = await fetch(url, {
266
- ...init,
267
- body: withGatewayCaching(init.body),
268
- headers,
269
- signal: network,
270
- })
480
+ const response = await fetch(url, { ...init, headers, signal: network })
271
481
  // Abandoned before its headers came: the kernel has moved on without it.
272
482
  if (init.signal?.aborted) {
273
483
  void response.body?.cancel()
@@ -316,9 +526,12 @@ async function runStep(step: HarnessStep): Promise<HarnessGeneration> {
316
526
  })
317
527
  }
318
528
 
319
- const hostTools = await kernelTools(step.tools, (toolName, toolInput) => {
320
- const state = turn!
321
- if (state.failure) throw state.failure
529
+ /** The recorded result of a call from the step the kernel was served. */
530
+ const answer = (
531
+ state: TurnState,
532
+ toolName: string,
533
+ toolInput: unknown,
534
+ ): { output: unknown } | undefined => {
322
535
  const call = state.served.find(
323
536
  (candidate) =>
324
537
  candidate.toolName === toolName &&
@@ -326,24 +539,33 @@ async function runStep(step: HarnessStep): Promise<HarnessGeneration> {
326
539
  sameJson(candidate.input, toolInput),
327
540
  )
328
541
  const result = call === undefined ? undefined : results.get(call.toolCallId)
329
- if (call !== undefined && result !== undefined) {
330
- state.answered.add(call.toolCallId)
331
- if (result.error !== undefined) throw new Error(result.error)
332
- if (result.denied === true) throw new Error('The tool call was denied.')
333
- return result.output ?? null
334
- }
542
+ if (call === undefined || result === undefined) return undefined
543
+ state.answered.add(call.toolCallId)
544
+ if (result.error !== undefined) throw new Error(result.error)
545
+ if (result.denied === true) throw new Error('The tool call was denied.')
546
+ return { output: result.output ?? null }
547
+ }
548
+
549
+ const hostTool = (toolName: string, toolInput: unknown): unknown => {
550
+ const state = turn!
551
+ if (state.failure) throw state.failure
552
+ const answered = answer(state, toolName, toolInput)
553
+ if (answered !== undefined) return answered.output
335
554
  if (state.live) {
336
555
  state.toolsRequested.resolve()
337
- return pending(undefined)
556
+ return new Promise((resolve, reject) => {
557
+ kernel!.waiting.push({ toolName, input: toolInput, resolve, reject })
558
+ })
338
559
  }
339
560
  if (state.plan.end === 'interrupted') {
340
561
  state.fxTurn.cancel()
341
562
  return pending(undefined)
342
563
  }
343
564
  return fail(state, diverged(`no recorded result for ${toolName}`))
344
- })
565
+ }
345
566
 
346
- const plans = (turns: HarnessTurn[]): Promise<TurnPlan[]> =>
567
+ const hooks: KernelHooks = { fetch: hostFetch, tool: hostTool }
568
+ const planTurns = (turns: HarnessTurn[]): Promise<TurnPlan[]> =>
347
569
  Promise.all(turns.map((recorded) => turnPlan(recorded, step.signal)))
348
570
  let results = read.results
349
571
  let controller: ReadableStreamDefaultController<UIMessageChunk> | undefined
@@ -352,17 +574,26 @@ async function runStep(step: HarnessStep): Promise<HarnessGeneration> {
352
574
  if (!closed) controller!.enqueue(chunk)
353
575
  }
354
576
 
355
- const create = (checkpoint: Uint8Array | undefined) =>
356
- createFxAgent({
577
+ const create = async (
578
+ checkpoint: Uint8Array | undefined,
579
+ ): Promise<Kernel> => {
580
+ const created = { hooks, waiting: [] as Waiting[] }
581
+ const agent = await createFxAgent({
357
582
  apiKey: credential.token,
358
583
  model: kernelModel(step),
359
584
  ...(step.instructions.length === 0
360
585
  ? {}
361
586
  : { instructions: step.instructions }),
362
- tools: hostTools,
587
+ tools: definitions.map((definition): FxHostTool => ({
588
+ ...definition,
589
+ execute: (toolInput: unknown) =>
590
+ created.hooks.tool(definition.name, toolInput),
591
+ })),
363
592
  ...(checkpoint === undefined ? {} : { checkpoint }),
364
- fetch: hostFetch,
593
+ fetch: (input, init) => created.hooks.fetch(input, init),
365
594
  })
595
+ return Object.assign(created, { agent })
596
+ }
366
597
 
367
598
  const stream = new ReadableStream<UIMessageChunk>({
368
599
  start(streamController) {
@@ -374,16 +605,33 @@ async function runStep(step: HarnessStep): Promise<HarnessGeneration> {
374
605
  step.signal.addEventListener('abort', abort, { once: true })
375
606
  void (async () => {
376
607
  try {
377
- try {
378
- agent = await create(read.saved)
379
- } catch (error) {
380
- if (read.saved === undefined) throw error
381
- // An unusable checkpoint: replay the whole log instead.
382
- read = await replayAll()
383
- results = read.results
384
- agent = await create(undefined)
608
+ let turns = await planTurns(read.turns)
609
+ // An aborted step leaves a parked kernel for a later attempt.
610
+ const candidate = step.signal.aborted
611
+ ? undefined
612
+ : lot.take(step.sessionId)
613
+ const adopted =
614
+ candidate !== undefined && adoptable(candidate, turns)
615
+ ? candidate
616
+ : undefined
617
+ if (candidate !== undefined && adopted === undefined)
618
+ closeParked(candidate)
619
+ if (adopted !== undefined) {
620
+ kernel = adopted.kernel
621
+ kernel.hooks = hooks
622
+ } else {
623
+ try {
624
+ kernel = await create(read.saved)
625
+ } catch (error) {
626
+ if (read.saved === undefined) throw error
627
+ // An unusable checkpoint: replay the whole log instead.
628
+ read = await replayAll()
629
+ results = read.results
630
+ kernel = await create(undefined)
631
+ turns = await planTurns(read.turns)
632
+ }
385
633
  }
386
- await run(streamController, await plans(read.turns))
634
+ await run(streamController, turns, adopted)
387
635
  closed = true
388
636
  streamController.close()
389
637
  } catch (error) {
@@ -394,7 +642,7 @@ async function runStep(step: HarnessStep): Promise<HarnessGeneration> {
394
642
  } finally {
395
643
  released.abort()
396
644
  step.signal.removeEventListener('abort', abort)
397
- await agent?.close().catch(() => {})
645
+ if (!parked) await kernel?.agent.close().catch(() => {})
398
646
  }
399
647
  })()
400
648
  },
@@ -417,21 +665,21 @@ async function turnPlan(
417
665
  ): Promise<TurnPlan> {
418
666
  const calls: RecordedCall[] = []
419
667
  const steers = new Map<number, string[]>()
420
- const steer = (at: number, inputs: HarnessInput[] | undefined) => {
421
- if (inputs !== undefined) steers.set(Math.max(0, at), inputs.map(steerText))
422
- }
423
- steer(0, turn.generations.length === 0 ? turn.steers.get(0) : undefined)
424
668
  turn.generations.forEach((generation, index) => {
425
669
  calls.push(...recordedCalls(generation.chunks, generation.completed))
670
+ const inputs = turn.steers.get(index)
426
671
  // A generation's step is its last call.
427
- steer(calls.length - 1, turn.steers.get(index))
672
+ if (inputs !== undefined)
673
+ steers.set(calls.length - 1, inputs.map(steerText))
428
674
  })
429
675
  const images = await harnessImages({
430
676
  harness: 'fx()',
431
677
  input: turn.prompt,
432
678
  signal,
433
679
  })
680
+ const generationId = turn.generations.at(-1)?.completed.generationId
434
681
  return {
682
+ ...(generationId === undefined ? {} : { generationId }),
435
683
  prompt:
436
684
  images.length === 0
437
685
  ? turn.prompt.text
@@ -788,10 +1036,7 @@ async function loadLibfx(): Promise<typeof import('libfx')> {
788
1036
  * asks for it; options the request already has take precedence. Every other
789
1037
  * byte stays as the kernel wrote it, a tool input's spacing included.
790
1038
  */
791
- function withGatewayCaching(body: string): string {
792
- const request = JSON.parse(body) as {
793
- providerOptions?: Record<string, Record<string, unknown> | undefined>
794
- }
1039
+ function withGatewayCaching(body: string, request: GatewayRequest): string {
795
1040
  const options = request.providerOptions
796
1041
  if (options === undefined)
797
1042
  return body.replace(
@@ -814,6 +1059,9 @@ function withAuth(
814
1059
  ): Headers {
815
1060
  const result = new Headers(headers)
816
1061
  result.set('ai-gateway-auth-method', credential.method)
1062
+ // A parked kernel keeps the credential it was made with; the step's is current.
1063
+ if (result.has('authorization'))
1064
+ result.set('authorization', `Bearer ${credential.token}`)
817
1065
  return result
818
1066
  }
819
1067
 
@@ -833,17 +1081,16 @@ function kernelModel(
833
1081
  }
834
1082
  }
835
1083
 
836
- async function kernelTools(
1084
+ /** The agent's tools as the kernel takes them, without their execution. */
1085
+ async function toolDefinitions(
837
1086
  tools: ToolSet,
838
- execute: (toolName: string, input: unknown) => unknown,
839
- ): Promise<FxHostTool[]> {
1087
+ ): Promise<{ name: string; description: string; inputSchema: object }[]> {
840
1088
  return Promise.all(
841
1089
  Object.entries(tools).map(async ([toolName, tool]) => ({
842
1090
  name: toolName,
843
1091
  description:
844
1092
  typeof tool.description === 'string' ? tool.description : toolName,
845
1093
  inputSchema: (await asSchema(tool.inputSchema).jsonSchema) as object,
846
- execute: (input: unknown) => execute(toolName, input),
847
1094
  })),
848
1095
  )
849
1096
  }
@@ -891,11 +1138,6 @@ function savedCheckpoint(state: unknown): Uint8Array | undefined {
891
1138
  return new Uint8Array(Buffer.from(checkpoint, 'base64'))
892
1139
  }
893
1140
 
894
- function isCompaction(body: string): boolean {
895
- const request = JSON.parse(body) as { toolChoice?: { type?: string } }
896
- return request.toolChoice?.type === 'none'
897
- }
898
-
899
1141
  function decodeBody(body: BodyInit | null | undefined): string {
900
1142
  if (typeof body === 'string') return body
901
1143
  if (body instanceof Uint8Array || body instanceof ArrayBuffer)
package/src/ai-harness.ts CHANGED
@@ -69,6 +69,8 @@ export type HarnessLog = {
69
69
 
70
70
  /** Everything one durable generation hands its harness. */
71
71
  export type HarnessStep = {
72
+ /** The agent's own identity: what a harness keeps in memory is per scope. */
73
+ scope: object
72
74
  agentName: string
73
75
  sessionId: string
74
76
  requestIndex: number
@@ -95,6 +97,11 @@ export type HarnessDriver = {
95
97
  /** Tools the harness brings; A2 executes them like the agent's own. */
96
98
  readonly tools: ToolSet
97
99
  step(step: HarnessStep): Promise<HarnessGeneration>
100
+ /**
101
+ * The session has nothing left to run in this process. Lets go of anything
102
+ * kept in memory for its next step; it holds nothing the log needs.
103
+ */
104
+ release?(session: { scope: object; sessionId: string }): void
98
105
  }
99
106
 
100
107
  // A global symbol, so harness values work across separately bundled copies.
@@ -323,6 +330,7 @@ export type HarnessInput = { text: string; files: FileUIPart[] }
323
330
 
324
331
  /** One harness turn to rebuild: its prompt, completed generations, and end. */
325
332
  export type HarnessTurn = {
333
+ /** With any steering that entered before the turn's first completed generation. */
326
334
  prompt: HarnessInput
327
335
  generations: RecordedGeneration[]
328
336
  /** Steering that entered after the generation at that index. */
@@ -441,8 +449,15 @@ export function harnessTurns(
441
449
  files: response.prompt.flatMap((prompt) => prompt.files),
442
450
  }
443
451
  let turn = turns.at(-1)
444
- if (steered && turn !== undefined) {
445
- const at = Math.max(0, turn.generations.length - 1)
452
+ if (steered && turn?.generations.length === 0) {
453
+ // Nothing answered the prompt yet, so the steering joins it, as messages
454
+ // queued before a response join its prompt.
455
+ turn.prompt = {
456
+ text: `${turn.prompt.text}\n\n${input.text}`,
457
+ files: [...turn.prompt.files, ...input.files],
458
+ }
459
+ } else if (steered && turn !== undefined) {
460
+ const at = turn.generations.length - 1
446
461
  turn.steers.set(at, [...(turn.steers.get(at) ?? []), input])
447
462
  } else {
448
463
  turn = { prompt: input, generations: [], steers: new Map(), end: 'live' }
package/src/ai-pi.ts CHANGED
@@ -436,7 +436,7 @@ async function runStep(
436
436
  const result = results.get(toolCallId)
437
437
  if (result !== undefined) return answeredTool(result, shape)
438
438
  if (state.plan.end === 'live' && state.liveStep) {
439
- // Pi now waits on tools that A2's tool handlers run.
439
+ // Pi now waits on tools that A2's tool consumer run.
440
440
  endStep(state)
441
441
  return pending(signal)
442
442
  }
@@ -962,7 +962,7 @@ function builtinDefinition(
962
962
 
963
963
  /**
964
964
  * The tools Pi's session declares: Pi's own, and the agent's. Their results
965
- * come from A2's log; A2's tool handlers ran them.
965
+ * come from A2's log; A2's tool consumer ran them.
966
966
  */
967
967
  async function sessionTools(
968
968
  modules: PiModules,
package/src/ai-server.ts CHANGED
@@ -1,10 +1,10 @@
1
1
  /**
2
2
  * experimental-a2/ai/server — `agent()`: the experimental-a2/ai protocol,
3
- * its model and tool handlers, and A2's store, scheduler, and telemetry,
3
+ * its model and tool consumers, and A2's store, scheduler, and telemetry,
4
4
  * assembled as one ordinary A2 server.
5
5
  */
6
6
 
7
- export { agent, runWithHandlerContext } from './ai-agent.ts'
7
+ export { agent, runWithConsumerContext } from './ai-agent.ts'
8
8
  export type {
9
9
  AISDKHarness,
10
10
  Agent,