@animalabs/agent-framework 0.23.3 → 0.24.1

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (355) hide show
  1. package/README.md +212 -42
  2. package/dist/src/agent.d.ts +65 -9
  3. package/dist/src/agent.d.ts.map +1 -1
  4. package/dist/src/agent.js +98 -12
  5. package/dist/src/agent.js.map +1 -1
  6. package/dist/src/api/server.d.ts.map +1 -1
  7. package/dist/src/api/server.js +14 -12
  8. package/dist/src/api/server.js.map +1 -1
  9. package/dist/src/clock-rows.d.ts +115 -0
  10. package/dist/src/clock-rows.d.ts.map +1 -0
  11. package/dist/src/clock-rows.js +222 -0
  12. package/dist/src/clock-rows.js.map +1 -0
  13. package/dist/src/code-execution/engine.d.ts +177 -0
  14. package/dist/src/code-execution/engine.d.ts.map +1 -0
  15. package/dist/src/code-execution/engine.js +494 -0
  16. package/dist/src/code-execution/engine.js.map +1 -0
  17. package/dist/src/code-execution/index.d.ts +5 -10
  18. package/dist/src/code-execution/index.d.ts.map +1 -1
  19. package/dist/src/code-execution/index.js +5 -9
  20. package/dist/src/code-execution/index.js.map +1 -1
  21. package/dist/src/code-execution/isolate/calls.d.ts +114 -0
  22. package/dist/src/code-execution/isolate/calls.d.ts.map +1 -0
  23. package/dist/src/code-execution/isolate/calls.js +145 -0
  24. package/dist/src/code-execution/isolate/calls.js.map +1 -0
  25. package/dist/src/code-execution/isolate/child-entry.d.ts +2 -0
  26. package/dist/src/code-execution/isolate/child-entry.d.ts.map +1 -0
  27. package/dist/src/code-execution/isolate/child-entry.js +5 -0
  28. package/dist/src/code-execution/isolate/child-entry.js.map +1 -0
  29. package/dist/src/code-execution/isolate/child-main.d.ts +26 -0
  30. package/dist/src/code-execution/isolate/child-main.d.ts.map +1 -0
  31. package/dist/src/code-execution/isolate/child-main.js +132 -0
  32. package/dist/src/code-execution/isolate/child-main.js.map +1 -0
  33. package/dist/src/code-execution/isolate/child.d.ts +44 -0
  34. package/dist/src/code-execution/isolate/child.d.ts.map +1 -0
  35. package/dist/src/code-execution/isolate/child.js +315 -0
  36. package/dist/src/code-execution/isolate/child.js.map +1 -0
  37. package/dist/src/code-execution/isolate/frames.d.ts +80 -0
  38. package/dist/src/code-execution/isolate/frames.d.ts.map +1 -0
  39. package/dist/src/code-execution/isolate/frames.js +210 -0
  40. package/dist/src/code-execution/isolate/frames.js.map +1 -0
  41. package/dist/src/code-execution/isolate/output.d.ts +68 -0
  42. package/dist/src/code-execution/isolate/output.d.ts.map +1 -0
  43. package/dist/src/code-execution/isolate/output.js +143 -0
  44. package/dist/src/code-execution/isolate/output.js.map +1 -0
  45. package/dist/src/code-execution/isolate/prelude.d.ts +109 -0
  46. package/dist/src/code-execution/isolate/prelude.d.ts.map +1 -0
  47. package/dist/src/code-execution/isolate/prelude.js +401 -0
  48. package/dist/src/code-execution/isolate/prelude.js.map +1 -0
  49. package/dist/src/code-execution/isolate/protocol.d.ts +151 -0
  50. package/dist/src/code-execution/isolate/protocol.d.ts.map +1 -0
  51. package/dist/src/code-execution/isolate/protocol.js +150 -0
  52. package/dist/src/code-execution/isolate/protocol.js.map +1 -0
  53. package/dist/src/code-execution/isolate/render.d.ts +57 -0
  54. package/dist/src/code-execution/isolate/render.d.ts.map +1 -0
  55. package/dist/src/code-execution/isolate/render.js +214 -0
  56. package/dist/src/code-execution/isolate/render.js.map +1 -0
  57. package/dist/src/code-execution/isolate/runner.d.ts +250 -0
  58. package/dist/src/code-execution/isolate/runner.d.ts.map +1 -0
  59. package/dist/src/code-execution/isolate/runner.js +854 -0
  60. package/dist/src/code-execution/isolate/runner.js.map +1 -0
  61. package/dist/src/code-execution/isolate/scripts.d.ts +175 -0
  62. package/dist/src/code-execution/isolate/scripts.d.ts.map +1 -0
  63. package/dist/src/code-execution/isolate/scripts.js +357 -0
  64. package/dist/src/code-execution/isolate/scripts.js.map +1 -0
  65. package/dist/src/code-execution/quickjs.wasm +0 -0
  66. package/dist/src/code-execution/tool-definition.d.ts +93 -43
  67. package/dist/src/code-execution/tool-definition.d.ts.map +1 -1
  68. package/dist/src/code-execution/tool-definition.js +176 -142
  69. package/dist/src/code-execution/tool-definition.js.map +1 -1
  70. package/dist/src/deadline-timer.d.ts +3 -0
  71. package/dist/src/deadline-timer.d.ts.map +1 -1
  72. package/dist/src/deadline-timer.js +1 -1
  73. package/dist/src/deadline-timer.js.map +1 -1
  74. package/dist/src/framework.d.ts +1120 -457
  75. package/dist/src/framework.d.ts.map +1 -1
  76. package/dist/src/framework.js +4635 -2505
  77. package/dist/src/framework.js.map +1 -1
  78. package/dist/src/gate/event-gate.d.ts +28 -4
  79. package/dist/src/gate/event-gate.d.ts.map +1 -1
  80. package/dist/src/gate/event-gate.js +89 -13
  81. package/dist/src/gate/event-gate.js.map +1 -1
  82. package/dist/src/held-writes.d.ts +16 -3
  83. package/dist/src/held-writes.d.ts.map +1 -1
  84. package/dist/src/held-writes.js +16 -3
  85. package/dist/src/held-writes.js.map +1 -1
  86. package/dist/src/index.d.ts +8 -6
  87. package/dist/src/index.d.ts.map +1 -1
  88. package/dist/src/index.js +10 -8
  89. package/dist/src/index.js.map +1 -1
  90. package/dist/src/mcpl/channel-registry.d.ts +483 -115
  91. package/dist/src/mcpl/channel-registry.d.ts.map +1 -1
  92. package/dist/src/mcpl/channel-registry.js +1735 -425
  93. package/dist/src/mcpl/channel-registry.js.map +1 -1
  94. package/dist/src/mcpl/conversation-router.d.ts +19 -39
  95. package/dist/src/mcpl/conversation-router.d.ts.map +1 -1
  96. package/dist/src/mcpl/conversation-router.js +27 -59
  97. package/dist/src/mcpl/conversation-router.js.map +1 -1
  98. package/dist/src/mcpl/convert-block.d.ts.map +1 -1
  99. package/dist/src/mcpl/convert-block.js +9 -5
  100. package/dist/src/mcpl/convert-block.js.map +1 -1
  101. package/dist/src/mcpl/inbound-source.d.ts +50 -0
  102. package/dist/src/mcpl/inbound-source.d.ts.map +1 -1
  103. package/dist/src/mcpl/inbound-source.js +130 -0
  104. package/dist/src/mcpl/inbound-source.js.map +1 -1
  105. package/dist/src/mcpl/index.d.ts +2 -1
  106. package/dist/src/mcpl/index.d.ts.map +1 -1
  107. package/dist/src/mcpl/index.js +3 -2
  108. package/dist/src/mcpl/index.js.map +1 -1
  109. package/dist/src/mcpl/log-value.d.ts +6 -0
  110. package/dist/src/mcpl/log-value.d.ts.map +1 -1
  111. package/dist/src/mcpl/log-value.js +8 -0
  112. package/dist/src/mcpl/log-value.js.map +1 -1
  113. package/dist/src/mcpl/modern-connection.d.ts +15 -6
  114. package/dist/src/mcpl/modern-connection.d.ts.map +1 -1
  115. package/dist/src/mcpl/modern-connection.js +168 -53
  116. package/dist/src/mcpl/modern-connection.js.map +1 -1
  117. package/dist/src/mcpl/prose-grammar.d.ts +4 -5
  118. package/dist/src/mcpl/prose-grammar.d.ts.map +1 -1
  119. package/dist/src/mcpl/prose-grammar.js +4 -5
  120. package/dist/src/mcpl/prose-grammar.js.map +1 -1
  121. package/dist/src/mcpl/protocol-family.d.ts +97 -10
  122. package/dist/src/mcpl/protocol-family.d.ts.map +1 -1
  123. package/dist/src/mcpl/protocol-family.js +174 -17
  124. package/dist/src/mcpl/protocol-family.js.map +1 -1
  125. package/dist/src/mcpl/push-coalescer.d.ts +132 -14
  126. package/dist/src/mcpl/push-coalescer.d.ts.map +1 -1
  127. package/dist/src/mcpl/push-coalescer.js +227 -50
  128. package/dist/src/mcpl/push-coalescer.js.map +1 -1
  129. package/dist/src/mcpl/push-handler.d.ts +9 -20
  130. package/dist/src/mcpl/push-handler.d.ts.map +1 -1
  131. package/dist/src/mcpl/push-handler.js +7 -49
  132. package/dist/src/mcpl/push-handler.js.map +1 -1
  133. package/dist/src/mcpl/reference-fetcher.d.ts +2 -2
  134. package/dist/src/mcpl/reference-fetcher.d.ts.map +1 -1
  135. package/dist/src/mcpl/reference-fetcher.js +4 -4
  136. package/dist/src/mcpl/reference-fetcher.js.map +1 -1
  137. package/dist/src/mcpl/server-connection.d.ts +50 -21
  138. package/dist/src/mcpl/server-connection.d.ts.map +1 -1
  139. package/dist/src/mcpl/server-connection.js +158 -74
  140. package/dist/src/mcpl/server-connection.js.map +1 -1
  141. package/dist/src/mcpl/tags.d.ts +24 -0
  142. package/dist/src/mcpl/tags.d.ts.map +1 -1
  143. package/dist/src/mcpl/tags.js +33 -0
  144. package/dist/src/mcpl/tags.js.map +1 -1
  145. package/dist/src/mcpl/tool-result-normalize.d.ts +2 -1
  146. package/dist/src/mcpl/tool-result-normalize.d.ts.map +1 -1
  147. package/dist/src/mcpl/tool-result-normalize.js.map +1 -1
  148. package/dist/src/mcpl/transport.d.ts +55 -7
  149. package/dist/src/mcpl/transport.d.ts.map +1 -1
  150. package/dist/src/mcpl/transport.js +163 -34
  151. package/dist/src/mcpl/transport.js.map +1 -1
  152. package/dist/src/mcpl/types.d.ts +22 -6
  153. package/dist/src/mcpl/types.d.ts.map +1 -1
  154. package/dist/src/mcpl/types.js.map +1 -1
  155. package/dist/src/module-registry.d.ts +4 -2
  156. package/dist/src/module-registry.d.ts.map +1 -1
  157. package/dist/src/module-registry.js +100 -13
  158. package/dist/src/module-registry.js.map +1 -1
  159. package/dist/src/modules/discord/index.d.ts.map +1 -1
  160. package/dist/src/modules/discord/index.js +8 -13
  161. package/dist/src/modules/discord/index.js.map +1 -1
  162. package/dist/src/modules/history/index.d.ts.map +1 -1
  163. package/dist/src/modules/history/index.js +27 -4
  164. package/dist/src/modules/history/index.js.map +1 -1
  165. package/dist/src/modules/history/semantic.d.ts +15 -3
  166. package/dist/src/modules/history/semantic.d.ts.map +1 -1
  167. package/dist/src/modules/history/semantic.js +27 -5
  168. package/dist/src/modules/history/semantic.js.map +1 -1
  169. package/dist/src/modules/index.d.ts +1 -1
  170. package/dist/src/modules/index.d.ts.map +1 -1
  171. package/dist/src/modules/index.js.map +1 -1
  172. package/dist/src/modules/unknown-tool.d.ts +4 -1
  173. package/dist/src/modules/unknown-tool.d.ts.map +1 -1
  174. package/dist/src/modules/unknown-tool.js +4 -1
  175. package/dist/src/modules/unknown-tool.js.map +1 -1
  176. package/dist/src/modules/workspace/branch-intent.d.ts +3 -1
  177. package/dist/src/modules/workspace/branch-intent.d.ts.map +1 -1
  178. package/dist/src/modules/workspace/branch-intent.js +3 -1
  179. package/dist/src/modules/workspace/branch-intent.js.map +1 -1
  180. package/dist/src/modules/workspace/edit-plan.d.ts +265 -0
  181. package/dist/src/modules/workspace/edit-plan.d.ts.map +1 -0
  182. package/dist/src/modules/workspace/edit-plan.js +794 -0
  183. package/dist/src/modules/workspace/edit-plan.js.map +1 -0
  184. package/dist/src/modules/workspace/edit-text.d.ts +117 -0
  185. package/dist/src/modules/workspace/edit-text.d.ts.map +1 -0
  186. package/dist/src/modules/workspace/edit-text.js +584 -0
  187. package/dist/src/modules/workspace/edit-text.js.map +1 -0
  188. package/dist/src/modules/workspace/effects.d.ts +5 -4
  189. package/dist/src/modules/workspace/effects.d.ts.map +1 -1
  190. package/dist/src/modules/workspace/effects.js +78 -25
  191. package/dist/src/modules/workspace/effects.js.map +1 -1
  192. package/dist/src/modules/workspace/index.d.ts +493 -62
  193. package/dist/src/modules/workspace/index.d.ts.map +1 -1
  194. package/dist/src/modules/workspace/index.js +2216 -652
  195. package/dist/src/modules/workspace/index.js.map +1 -1
  196. package/dist/src/modules/workspace/observe.d.ts +213 -38
  197. package/dist/src/modules/workspace/observe.d.ts.map +1 -1
  198. package/dist/src/modules/workspace/observe.js +416 -36
  199. package/dist/src/modules/workspace/observe.js.map +1 -1
  200. package/dist/src/modules/workspace/reconcile.d.ts.map +1 -1
  201. package/dist/src/modules/workspace/reconcile.js +15 -8
  202. package/dist/src/modules/workspace/reconcile.js.map +1 -1
  203. package/dist/src/modules/workspace/text.d.ts +32 -0
  204. package/dist/src/modules/workspace/text.d.ts.map +1 -0
  205. package/dist/src/modules/workspace/text.js +67 -0
  206. package/dist/src/modules/workspace/text.js.map +1 -0
  207. package/dist/src/modules/workspace/types.d.ts +210 -6
  208. package/dist/src/modules/workspace/types.d.ts.map +1 -1
  209. package/dist/src/modules/workspace/types.js.map +1 -1
  210. package/dist/src/modules/workspace/watcher.d.ts +3 -2
  211. package/dist/src/modules/workspace/watcher.d.ts.map +1 -1
  212. package/dist/src/modules/workspace/watcher.js.map +1 -1
  213. package/dist/src/operator-log.d.ts +52 -24
  214. package/dist/src/operator-log.d.ts.map +1 -1
  215. package/dist/src/operator-log.js.map +1 -1
  216. package/dist/src/recovery/discord-awareness-outbox.d.ts +496 -72
  217. package/dist/src/recovery/discord-awareness-outbox.d.ts.map +1 -1
  218. package/dist/src/recovery/discord-awareness-outbox.js +1196 -289
  219. package/dist/src/recovery/discord-awareness-outbox.js.map +1 -1
  220. package/dist/src/recovery/offline-branch.d.ts +26 -1
  221. package/dist/src/recovery/offline-branch.d.ts.map +1 -1
  222. package/dist/src/recovery/offline-branch.js +164 -39
  223. package/dist/src/recovery/offline-branch.js.map +1 -1
  224. package/dist/src/recovery/recover-cli.js +23 -4
  225. package/dist/src/recovery/recover-cli.js.map +1 -1
  226. package/dist/src/send-tools.d.ts +11 -0
  227. package/dist/src/send-tools.d.ts.map +1 -0
  228. package/dist/src/send-tools.js +11 -0
  229. package/dist/src/send-tools.js.map +1 -0
  230. package/dist/src/timezone.d.ts +4 -2
  231. package/dist/src/timezone.d.ts.map +1 -1
  232. package/dist/src/timezone.js +4 -2
  233. package/dist/src/timezone.js.map +1 -1
  234. package/dist/src/tool-image-ledger.d.ts +46 -7
  235. package/dist/src/tool-image-ledger.d.ts.map +1 -1
  236. package/dist/src/tool-image-ledger.js +60 -7
  237. package/dist/src/tool-image-ledger.js.map +1 -1
  238. package/dist/src/tool-result-guard.d.ts +14 -11
  239. package/dist/src/tool-result-guard.d.ts.map +1 -1
  240. package/dist/src/tool-result-guard.js +17 -14
  241. package/dist/src/tool-result-guard.js.map +1 -1
  242. package/dist/src/tool-result-history.d.ts +3 -2
  243. package/dist/src/tool-result-history.d.ts.map +1 -1
  244. package/dist/src/tool-result-history.js +3 -2
  245. package/dist/src/tool-result-history.js.map +1 -1
  246. package/dist/src/tune-out/coordinator.d.ts +21 -0
  247. package/dist/src/tune-out/coordinator.d.ts.map +1 -1
  248. package/dist/src/tune-out/coordinator.js +142 -40
  249. package/dist/src/tune-out/coordinator.js.map +1 -1
  250. package/dist/src/types/agent.d.ts +31 -6
  251. package/dist/src/types/agent.d.ts.map +1 -1
  252. package/dist/src/types/events.d.ts +66 -9
  253. package/dist/src/types/events.d.ts.map +1 -1
  254. package/dist/src/types/framework.d.ts +69 -62
  255. package/dist/src/types/framework.d.ts.map +1 -1
  256. package/dist/src/types/module.d.ts +40 -8
  257. package/dist/src/types/module.d.ts.map +1 -1
  258. package/dist/src/types/trace.d.ts +111 -10
  259. package/dist/src/types/trace.d.ts.map +1 -1
  260. package/dist/src/usage/types.d.ts +16 -0
  261. package/dist/src/usage/types.d.ts.map +1 -1
  262. package/dist/src/usage/usage-tracker.d.ts +14 -1
  263. package/dist/src/usage/usage-tracker.d.ts.map +1 -1
  264. package/dist/src/usage/usage-tracker.js +81 -41
  265. package/dist/src/usage/usage-tracker.js.map +1 -1
  266. package/package.json +6 -8
  267. package/src/agent.ts +99 -12
  268. package/src/api/server.ts +14 -12
  269. package/src/clock-rows.ts +302 -0
  270. package/src/code-execution/engine.ts +595 -0
  271. package/src/code-execution/index.ts +5 -17
  272. package/src/code-execution/isolate/calls.ts +199 -0
  273. package/src/code-execution/isolate/child-entry.ts +5 -0
  274. package/src/code-execution/isolate/child-main.ts +132 -0
  275. package/src/code-execution/isolate/child.ts +327 -0
  276. package/src/code-execution/isolate/frames.ts +209 -0
  277. package/src/code-execution/isolate/output.ts +161 -0
  278. package/src/code-execution/isolate/prelude.ts +409 -0
  279. package/src/code-execution/isolate/protocol.ts +240 -0
  280. package/src/code-execution/isolate/render.ts +230 -0
  281. package/src/code-execution/isolate/runner.ts +911 -0
  282. package/src/code-execution/isolate/scripts.ts +479 -0
  283. package/src/code-execution/quickjs.wasm +0 -0
  284. package/src/code-execution/quickjs.wasm.LICENSES +149 -0
  285. package/src/code-execution/tool-definition.ts +236 -167
  286. package/src/deadline-timer.ts +1 -1
  287. package/src/framework.ts +4978 -2671
  288. package/src/gate/event-gate.ts +79 -17
  289. package/src/held-writes.ts +17 -3
  290. package/src/index.ts +32 -22
  291. package/src/mcpl/channel-registry.ts +1892 -471
  292. package/src/mcpl/conversation-router.ts +26 -72
  293. package/src/mcpl/convert-block.ts +9 -5
  294. package/src/mcpl/inbound-source.ts +131 -0
  295. package/src/mcpl/index.ts +4 -2
  296. package/src/mcpl/log-value.ts +9 -0
  297. package/src/mcpl/modern-connection.ts +162 -53
  298. package/src/mcpl/prose-grammar.ts +4 -5
  299. package/src/mcpl/protocol-family.ts +200 -22
  300. package/src/mcpl/push-coalescer.ts +306 -62
  301. package/src/mcpl/push-handler.ts +12 -54
  302. package/src/mcpl/reference-fetcher.ts +4 -4
  303. package/src/mcpl/server-connection.ts +180 -79
  304. package/src/mcpl/tags.ts +36 -0
  305. package/src/mcpl/tool-result-normalize.ts +2 -1
  306. package/src/mcpl/transport.ts +162 -33
  307. package/src/mcpl/types.ts +23 -6
  308. package/src/module-registry.ts +92 -13
  309. package/src/modules/discord/index.ts +8 -13
  310. package/src/modules/history/index.ts +25 -3
  311. package/src/modules/history/semantic.ts +29 -6
  312. package/src/modules/index.ts +1 -0
  313. package/src/modules/unknown-tool.ts +4 -1
  314. package/src/modules/workspace/branch-intent.ts +3 -1
  315. package/src/modules/workspace/edit-plan.ts +954 -0
  316. package/src/modules/workspace/edit-text.ts +618 -0
  317. package/src/modules/workspace/effects.ts +70 -19
  318. package/src/modules/workspace/index.ts +2293 -650
  319. package/src/modules/workspace/observe.ts +465 -56
  320. package/src/modules/workspace/reconcile.ts +14 -7
  321. package/src/modules/workspace/text.ts +68 -0
  322. package/src/modules/workspace/types.ts +211 -6
  323. package/src/modules/workspace/watcher.ts +3 -2
  324. package/src/operator-log.ts +47 -19
  325. package/src/recovery/discord-awareness-outbox.ts +1500 -356
  326. package/src/recovery/offline-branch.ts +189 -55
  327. package/src/recovery/recover-cli.ts +25 -4
  328. package/src/send-tools.ts +11 -0
  329. package/src/timezone.ts +4 -2
  330. package/src/tool-image-ledger.ts +62 -8
  331. package/src/tool-result-guard.ts +18 -15
  332. package/src/tool-result-history.ts +3 -2
  333. package/src/tune-out/coordinator.ts +166 -45
  334. package/src/types/agent.ts +32 -6
  335. package/src/types/events.ts +66 -9
  336. package/src/types/framework.ts +69 -62
  337. package/src/types/module.ts +40 -8
  338. package/src/types/trace.ts +121 -15
  339. package/src/usage/types.ts +16 -0
  340. package/src/usage/usage-tracker.ts +86 -43
  341. package/dist/src/code-execution/py-runner.d.ts +0 -191
  342. package/dist/src/code-execution/py-runner.d.ts.map +0 -1
  343. package/dist/src/code-execution/py-runner.js +0 -655
  344. package/dist/src/code-execution/py-runner.js.map +0 -1
  345. package/dist/src/code-execution/runtime-py.d.ts +0 -50
  346. package/dist/src/code-execution/runtime-py.d.ts.map +0 -1
  347. package/dist/src/code-execution/runtime-py.js +0 -677
  348. package/dist/src/code-execution/runtime-py.js.map +0 -1
  349. package/dist/src/mcpl/prose-stream-router.d.ts +0 -88
  350. package/dist/src/mcpl/prose-stream-router.d.ts.map +0 -1
  351. package/dist/src/mcpl/prose-stream-router.js +0 -291
  352. package/dist/src/mcpl/prose-stream-router.js.map +0 -1
  353. package/src/code-execution/py-runner.ts +0 -791
  354. package/src/code-execution/runtime-py.ts +0 -677
  355. package/src/mcpl/prose-stream-router.ts +0 -268
@@ -0,0 +1,854 @@
1
+ /**
2
+ * The parent's side of one script: its child process, the calls it makes, its
3
+ * output, and how it ends. One `ScriptRun` per script, and one child per run,
4
+ * which ends with it, so nothing a script held stays after it.
5
+ *
6
+ * Every frame from the child is decoded and checked before the parent acts on
7
+ * it, since the child is untrusted. A frame that breaks the protocol, a child
8
+ * that exits without saying how the script ended, or one that doesn't stop
9
+ * when asked, ends the run as crashed. A tool call goes to `onToolCall`, the
10
+ * bridge to the framework's dispatcher, and its outcome goes into the run's
11
+ * call record at dispatch, as running until it's answered.
12
+ *
13
+ * What the host holds for a script is bounded: at most MAX_CALLS_IN_FLIGHT of
14
+ * its calls wait for answers at once, and at most MAX_CALLS_IN_FLIGHT are out
15
+ * in the host, timed-out ones whose tools still run included, with any past
16
+ * those waiting here as their text. Each call's arguments are within
17
+ * MAX_ARGUMENT_BYTES and MAX_ARGUMENT_VALUES, and each answer is at most
18
+ * `maxResultBytes`. Answers wait for the child as their frames, written while
19
+ * its stdin takes them and after its 'drain' once it hasn't, so each is held
20
+ * once, in bytes.
21
+ */
22
+ import { spawn } from 'node:child_process';
23
+ import { mkdtempSync, rmSync, writeFileSync } from 'node:fs';
24
+ import { tmpdir } from 'node:os';
25
+ import { extname, join, resolve } from 'node:path';
26
+ import { fileURLToPath } from 'node:url';
27
+ import { encodeFrame, encodeResultFrame, FrameDecoder, RESULT_FRAME_OVERHEAD } from './frames.js';
28
+ import { MAX_BATCH_CHARS, WindowOutput } from './output.js';
29
+ import { WAKE_CALL_NAME } from './prelude.js';
30
+ import { CallLog, callBrief } from './calls.js';
31
+ import { argumentsRefusal, capText, MAX_CHILD_FRAME_BYTES, decodeChildMessage, MAX_ARGUMENT_BYTES, MAX_ARGUMENT_VALUES, MAX_CALLS_IN_FLIGHT, MAX_WAKE_PAYLOAD_BYTES, RESULT_TOO_LARGE, wakeRefusal, } from './protocol.js';
32
+ import { bounded, CUT_MARK_CHARS, formatMs, glimpse, NAME_CHARS, VALUE_CHARS } from './render.js';
33
+ /** How many script children a host runs at once, counted until each child's process has exited. */
34
+ export class ChildCapacity {
35
+ max;
36
+ live = 0;
37
+ constructor(max) {
38
+ this.max = max;
39
+ }
40
+ get running() {
41
+ return this.live;
42
+ }
43
+ /** Take a place, or false when the host is full. */
44
+ take() {
45
+ if (this.live >= this.max)
46
+ return false;
47
+ this.live += 1;
48
+ return true;
49
+ }
50
+ release() {
51
+ this.live = Math.max(0, this.live - 1);
52
+ }
53
+ }
54
+ /**
55
+ * The child's entry, beside this module and with its extension: `.ts` where
56
+ * the host runs this package's source, as a Bun host does through the
57
+ * package's `bun` export, and `.js` where it runs the build. So the runtime
58
+ * is given a file that exists: given a `.js` path that it maps to the `.ts`
59
+ * beside it, Bun runs it as `bun run` would, with npm_* variables and a PATH
60
+ * of its own in the child's environment.
61
+ */
62
+ export const CHILD_ENTRY = fileURLToPath(new URL(`./child-entry${extname(fileURLToPath(import.meta.url))}`, import.meta.url));
63
+ /** The most of a child's stderr kept, for a crash's detail. */
64
+ const STDERR_KEEP_CHARS = 4_000;
65
+ /**
66
+ * The most values a frame from the child may hold. Its largest, a failure's
67
+ * ending, holds eight, since arguments, output and values travel as strings.
68
+ */
69
+ const MAX_CHILD_FRAME_VALUES = 32;
70
+ /** How much of a refusal too long for the script's memory reaches it. */
71
+ const REFUSAL_GLIMPSE_CHARS = 2000;
72
+ /** Remove a run's directory. A failure leaves only an empty directory behind, which mustn't stop the run's ending. */
73
+ function removeDir(dir) {
74
+ try {
75
+ rmSync(dir, { recursive: true, force: true });
76
+ }
77
+ catch {
78
+ // Nothing else to do: the directory is left.
79
+ }
80
+ }
81
+ export { MAX_CALLS_IN_FLIGHT };
82
+ /**
83
+ * The largest result the parent sends a script: a third of its linear memory,
84
+ * since parsing one takes about three times its size there (a heap run under
85
+ * Bun 1.4.3 and Node 24.19.0). A larger one would fail in the guest anyway,
86
+ * after growing the child past its budget, so it's refused before it's sent,
87
+ * and the script's ResultTooLarge says the call took effect.
88
+ */
89
+ export function maxResultBytes(memoryMaxBytes) {
90
+ return Math.max(0, Math.floor(memoryMaxBytes / 3));
91
+ }
92
+ /** A refusal's own text, from its JSON, for a refusal that came with no brief. */
93
+ function refusalText(json) {
94
+ try {
95
+ const message = JSON.parse(json).message;
96
+ if (typeof message === 'string')
97
+ return message;
98
+ }
99
+ catch {
100
+ // Not a refusal's JSON: its text is what there is.
101
+ }
102
+ return json;
103
+ }
104
+ export class ScriptRun {
105
+ options;
106
+ child = null;
107
+ dir = null;
108
+ output;
109
+ log = new CallLog();
110
+ /** The ids of the guest's calls the child named, reserved until it says the script has their answer. */
111
+ pendingIds = new Set();
112
+ /** The calls answered and not yet seen: only these may the child say the script has. */
113
+ answeredIds = new Set();
114
+ /** The script's tools, the only names its calls may have. */
115
+ toolNames = new Set();
116
+ /** Results sent to the guest and not yet seen: each is counted once the child says the script has it, and whether it fit. */
117
+ delivered = new Map();
118
+ ending = null;
119
+ waiters = new Set();
120
+ stderr = '';
121
+ lifetimeTimer = null;
122
+ killTimer = null;
123
+ processExited = false;
124
+ stopAsked = null;
125
+ /** Each unanswered call's timeout, cleared when it's answered or the run ends. */
126
+ callTimers = new Set();
127
+ /** The calls holding a place among those in flight, by id, until the child says the script has their answer. */
128
+ inFlight = new Set();
129
+ /**
130
+ * How many calls the bridge holds, until it answers each: a call that timed
131
+ * out in the script, and so left its place there, may still hold its
132
+ * arguments in the host. Counted, not keyed by id, since the child may name
133
+ * a timed-out call's id again once the script has seen its TimeoutError.
134
+ * At most MAX_CALLS_IN_FLIGHT; a call past them waits in `dispatchWaiting`,
135
+ * as its text, until one is answered. One the bridge answered for (`held`)
136
+ * keeps its place until the run ends.
137
+ */
138
+ hostCalls = 0;
139
+ /** Calls waiting for the bridge to hold fewer: each resolved with true when its turn comes, false when it won't be sent. */
140
+ dispatchWaiting = new Map();
141
+ /** Wakes the child has sent, which the cap counts: those refused once a stop was asked included, so a child that ignores the stop is still bounded. */
142
+ wakeFrames = 0;
143
+ /** Wakes handed to the host to deliver, whatever it answered: a wake's number, which the host is given. */
144
+ wakes = 0;
145
+ /** Wakes the host delivered, its answer neither a refusal nor a failure. */
146
+ delivered_wakes = 0;
147
+ /** Wakes waiting for their turn, which the run's end counts as never sent. */
148
+ wakesWaiting = 0;
149
+ /** When the next wake may go out, on the monotonic clock: the spacing counts from when the last wake's delivery ended. */
150
+ nextWakeAt = 0;
151
+ /** Settles once the latest wake has gone out, or won't: wakes go out in the order they were made. */
152
+ wakeChain = Promise.resolve();
153
+ /** Wakes waiting out the spacing, each released at its time or when the run ends. */
154
+ wakeWaiters = new Set();
155
+ /**
156
+ * Results waiting for the child's stdin to drain, as their frames: each holds
157
+ * its JSON's bytes, which the result bound counts. A result's id becomes one
158
+ * the child may say it has seen once its frame is written.
159
+ */
160
+ resultQueue = [];
161
+ draining = false;
162
+ exitResolve;
163
+ endedResolve;
164
+ /** Resolves when the run ends: a wake waiting behind another's delivery stops waiting then. */
165
+ endedSignal = new Promise((resolve) => (this.endedResolve = resolve));
166
+ /** Resolves when the child's process has exited, or at once if it never started. */
167
+ exited = new Promise((resolve) => (this.exitResolve = resolve));
168
+ constructor(options) {
169
+ this.options = options;
170
+ this.output = new WindowOutput(options.output);
171
+ }
172
+ /**
173
+ * Spawn the child and send it the script. A start frame too long for a frame
174
+ * throws before anything is spawned, with `exited` resolved, so nothing is
175
+ * left running and the place it took frees at once. A child that can't be
176
+ * spawned crashes the run.
177
+ */
178
+ start(script, tools) {
179
+ let dir = null;
180
+ let startFrame;
181
+ try {
182
+ this.toolNames = new Set(tools.map((t) => t.name));
183
+ // Absolute, since the child starts in it: a relative TMPDIR would name
184
+ // the stop file from two different places.
185
+ dir = resolve(mkdtempSync(join(tmpdir(), 'af-script-')));
186
+ startFrame = encodeFrame({
187
+ t: 'start',
188
+ script,
189
+ tools: tools.map((t) => ({ name: t.name, description: t.description, ...(t.inputSchema === undefined ? {} : { inputSchema: t.inputSchema }) })),
190
+ memoryMaxBytes: this.options.memoryMaxBytes,
191
+ ...(this.options.maxStackBytes === undefined ? {} : { maxStackBytes: this.options.maxStackBytes }),
192
+ stopFile: join(dir, 'stop'),
193
+ parentPid: process.pid,
194
+ flushMs: this.options.flushMs,
195
+ maxWakes: this.options.maxWakes,
196
+ });
197
+ }
198
+ catch (err) {
199
+ if (dir)
200
+ removeDir(dir);
201
+ this.exitResolve();
202
+ throw err;
203
+ }
204
+ this.dir = dir;
205
+ const runDir = dir;
206
+ // A process that never started never exits: the run crashes, and its place
207
+ // is free now. It counts as exited, so the ending arms no kill and holds no child.
208
+ const neverStarted = (why) => {
209
+ this.processExited = true;
210
+ this.crash(`its process could not run: ${why instanceof Error ? why.message : String(why)}`);
211
+ removeDir(runDir);
212
+ this.exitResolve();
213
+ };
214
+ let child;
215
+ try {
216
+ child = spawn(this.options.execPath ?? process.execPath, [this.options.childEntry, ...(this.options.childArgs ?? [])], {
217
+ // The parent's frames go in on stdin, and the child's come back on fd 3 (child-main.ts says why).
218
+ stdio: ['pipe', 'ignore', 'pipe', 'pipe'],
219
+ // The child is ours and reads nothing from its environment; the guest has none at all.
220
+ env: {},
221
+ // Its own directory, which holds nothing but the stop file: Bun reads a
222
+ // .env file, and its bunfig.toml, from the directory it starts in, so
223
+ // in the host's it would load the host's .env into the child.
224
+ cwd: runDir,
225
+ });
226
+ }
227
+ catch (err) {
228
+ // Node throws here for some arguments it refuses at once.
229
+ neverStarted(err);
230
+ return;
231
+ }
232
+ // Node emits a spawn's 'error' on a later tick, which this takes. Bun's can
233
+ // come while spawn runs, before this is attached: spawn then throws it into
234
+ // the catch above, or returns with its pipes null, which the check below
235
+ // takes.
236
+ child.on('error', (err) => {
237
+ if (child.pid === undefined)
238
+ neverStarted(err);
239
+ else
240
+ this.crash(`its process could not run: ${err.message}`);
241
+ });
242
+ this.child = child;
243
+ const toChild = child.stdin;
244
+ const fromChild = child.stdio[3];
245
+ if (!toChild || !fromChild) {
246
+ // Bun can return with no pipes and emit the spawn's 'error', which names
247
+ // the cause, just after: a turn of the loop lets it come first.
248
+ setImmediate(() => neverStarted(new Error('its pipes were never opened')));
249
+ return;
250
+ }
251
+ // A child's frames are small flat objects, its arguments and output
252
+ // carried as strings, so one holding more values isn't following the
253
+ // protocol, and is refused before it's parsed.
254
+ const decoder = new FrameDecoder(MAX_CHILD_FRAME_BYTES, false, MAX_CHILD_FRAME_VALUES);
255
+ fromChild.on('data', (chunk) => {
256
+ let values;
257
+ try {
258
+ values = decoder.push(chunk);
259
+ }
260
+ catch (err) {
261
+ this.crash(`its process sent the host a message it couldn't read (${err.message})`);
262
+ return;
263
+ }
264
+ for (const value of values) {
265
+ const message = decodeChildMessage(value);
266
+ if ('invalid' in message) {
267
+ this.crash(`its process sent the host a message it couldn't read (${message.invalid})`);
268
+ return;
269
+ }
270
+ this.handle(message);
271
+ }
272
+ });
273
+ // Whether the child said how the script ended is known once its process has
274
+ // exited and fd 3 has ended, so every frame it wrote has been read: Node can
275
+ // emit 'exit' before it has read a child's last frames (1 in 2,700 runs of a
276
+ // probe under Node 20.20.2), and Bun 1.4.3 emits 'close' before an extra
277
+ // pipe like fd 3 has ended. A crash's detail waits for stderr's end too, for
278
+ // the same reason.
279
+ let exit = null;
280
+ let channelEnded = false;
281
+ let stderrEnded = child.stderr === null;
282
+ const decide = () => {
283
+ if (!exit || !channelEnded || !stderrEnded || this.ending !== null)
284
+ return;
285
+ const how = exit.signal ? `was ended by ${exit.signal}` : `exited with code ${exit.code}`;
286
+ if (this.stopAsked)
287
+ this.end({ how: 'stopped', reason: this.stopAsked.reason, short: true, ...(this.stopAsked.why ? { why: this.stopAsked.why } : {}) });
288
+ else
289
+ this.crash(`its process ${how} before the script ended${this.stderr ? `: ${this.stderr.trim()}` : ''}`);
290
+ };
291
+ const channelEnd = () => {
292
+ if (channelEnded)
293
+ return;
294
+ channelEnded = true;
295
+ // A frame cut off at the channel's end can't be read: the child broke off
296
+ // mid-message. Once a stop was asked that's the stop's doing, since the
297
+ // grace's kill can cut a frame, and the run ends as stopped.
298
+ try {
299
+ decoder.end();
300
+ }
301
+ catch (err) {
302
+ if (!this.stopAsked)
303
+ this.crash(`its process sent the host a message it couldn't read (${err.message})`);
304
+ }
305
+ decide();
306
+ };
307
+ fromChild.on('end', channelEnd);
308
+ fromChild.on('close', channelEnd);
309
+ fromChild.on('error', () => { });
310
+ toChild.on('error', () => { });
311
+ const stderrEnd = () => {
312
+ stderrEnded = true;
313
+ decide();
314
+ };
315
+ child.stderr?.on('data', (chunk) => {
316
+ this.stderr = (this.stderr + chunk.toString('utf8')).slice(-STDERR_KEEP_CHARS);
317
+ });
318
+ child.stderr?.on('end', stderrEnd);
319
+ child.stderr?.on('close', stderrEnd);
320
+ child.stderr?.on('error', () => { });
321
+ // The host's place for the child frees when its process exits. fd 3 and
322
+ // stderr have the grace to end after it, which only a process the child
323
+ // left holding them could stretch.
324
+ child.on('exit', (code, signal) => {
325
+ exit = { code, signal };
326
+ this.processExited = true;
327
+ if (this.killTimer)
328
+ clearTimeout(this.killTimer);
329
+ removeDir(runDir);
330
+ this.exitResolve();
331
+ if (!channelEnded || !stderrEnded) {
332
+ setTimeout(() => {
333
+ // After the next poll, so what the child wrote before it exited is
334
+ // read first, even when this loop was too busy to read it in the grace.
335
+ setImmediate(() => {
336
+ fromChild.destroy();
337
+ child.stderr?.destroy();
338
+ channelEnd();
339
+ stderrEnd();
340
+ });
341
+ }, this.options.stopGraceMs).unref();
342
+ }
343
+ decide();
344
+ if (this.ending !== null)
345
+ this.release();
346
+ });
347
+ this.write(startFrame);
348
+ this.lifetimeTimer = setTimeout(() => this.stop('limit'), this.options.lifetimeMs);
349
+ }
350
+ /** The run as it stands, its output since the last return included. */
351
+ snapshot() {
352
+ // Calls waiting for room in the host, and wakes waiting for their turn, are the script's too.
353
+ return { output: this.output.render(), calls: this.log.summary(this.dispatchWaiting.size + this.wakesWaiting), ending: this.ending };
354
+ }
355
+ /** The output since the last return, as it stands. */
356
+ outputWindow() {
357
+ return this.output.window();
358
+ }
359
+ /** The output since the last return, starting a fresh stretch from here. */
360
+ takeOutput() {
361
+ return this.output.take();
362
+ }
363
+ get ended() {
364
+ return this.ending !== null;
365
+ }
366
+ /** End the child's process now if it's still alive, whatever the run's state. Resolves once it has exited. */
367
+ kill() {
368
+ if (!this.processExited)
369
+ this.child?.kill('SIGKILL');
370
+ return this.exited;
371
+ }
372
+ /** The wakes the host delivered to the script's agent: those it answered ok and not as an error. A refused wake isn't one, nor a failed one, though a failure may have taken effect. */
373
+ get wakesDelivered() {
374
+ return this.delivered_wakes;
375
+ }
376
+ /** Resolves when the script has ended, or after `ms`, whichever comes first. */
377
+ wait(ms) {
378
+ if (this.ending)
379
+ return Promise.resolve(this.snapshot());
380
+ return new Promise((resolve) => {
381
+ const done = () => {
382
+ clearTimeout(timer);
383
+ this.waiters.delete(done);
384
+ resolve(this.snapshot());
385
+ };
386
+ const timer = setTimeout(done, ms);
387
+ this.waiters.add(done);
388
+ });
389
+ }
390
+ /**
391
+ * Ask the script to stop: the stop file for a computing guest, a frame for an
392
+ * idle one. If the child hasn't said how it ended within the grace, it's
393
+ * ended, and the ending says its output so far may be short. The reason is
394
+ * the one asked here, whatever the child says.
395
+ */
396
+ stop(reason, why) {
397
+ if (this.ending || !this.child)
398
+ return Promise.resolve(this.snapshot());
399
+ // A process that has exited has no stop to take: its run ends as its last
400
+ // frames say, or as a crash, which a stop asked now mustn't take for its own.
401
+ if (!this.stopAsked && !this.processExited) {
402
+ this.stopAsked = { reason, ...(why ? { why } : {}) };
403
+ try {
404
+ if (this.dir)
405
+ writeFileSync(join(this.dir, 'stop'), '');
406
+ }
407
+ catch {
408
+ // The frame below still reaches an idle child, and the grace ends a computing one.
409
+ }
410
+ this.send({ t: 'stop' });
411
+ this.killTimer = setTimeout(() => {
412
+ this.child?.kill('SIGKILL');
413
+ }, this.options.stopGraceMs);
414
+ this.killTimer.unref?.();
415
+ }
416
+ return new Promise((resolve) => {
417
+ const done = () => {
418
+ this.waiters.delete(done);
419
+ resolve(this.snapshot());
420
+ };
421
+ if (this.ending)
422
+ done();
423
+ else
424
+ this.waiters.add(done);
425
+ });
426
+ }
427
+ send(message) {
428
+ this.write(encodeFrame(message));
429
+ }
430
+ /** A call's result, as a result frame once its turn comes: the JSON's bytes go to the engine as they are. */
431
+ sendResult(id, ok, json) {
432
+ this.resultQueue.push({ id, frame: encodeResultFrame(id, ok, json, maxResultBytes(this.options.memoryMaxBytes) + RESULT_FRAME_OVERHEAD) });
433
+ this.pump();
434
+ }
435
+ /** Write waiting results, one frame at a time: after a write the pipe didn't take whole, the next waits for 'drain'. */
436
+ pump() {
437
+ const pipe = this.child?.stdin ?? undefined;
438
+ if (!pipe || pipe.destroyed) {
439
+ this.resultQueue.length = 0;
440
+ return;
441
+ }
442
+ while (!this.draining && this.resultQueue.length > 0) {
443
+ const { id, frame } = this.resultQueue.shift();
444
+ this.answeredIds.add(id);
445
+ if (!pipe.write(frame)) {
446
+ this.draining = true;
447
+ pipe.once('drain', () => {
448
+ this.draining = false;
449
+ this.pump();
450
+ });
451
+ }
452
+ }
453
+ }
454
+ write(frame) {
455
+ const pipe = this.child?.stdin ?? undefined;
456
+ if (!pipe || pipe.destroyed)
457
+ return;
458
+ pipe.write(frame);
459
+ }
460
+ handle(message) {
461
+ if (this.ending && message.t !== 'out')
462
+ return;
463
+ switch (message.t) {
464
+ case 'out':
465
+ // The child flushes output in pieces of at most a batch.
466
+ if (message.text.length > MAX_BATCH_CHARS) {
467
+ this.crash(`its process sent the host ${message.text.length.toLocaleString('en-US')} characters of output in one frame, past the ${MAX_BATCH_CHARS.toLocaleString('en-US')} it sends at most`);
468
+ return;
469
+ }
470
+ this.output.apply(message.text);
471
+ return;
472
+ case 'call':
473
+ void this.call(message.id, message.name, message.args);
474
+ return;
475
+ case 'seen': {
476
+ // The script has the answer the parent sent, or got ResultTooLarge in
477
+ // its place: the call's id and its place are free. A seen for a call
478
+ // whose answer the parent hasn't written isn't following the protocol.
479
+ if (!this.answeredIds.delete(message.id)) {
480
+ this.crash(`its process said the script had the answer to call ${message.id}, which the host never sent`);
481
+ return;
482
+ }
483
+ this.pendingIds.delete(message.id);
484
+ this.inFlight.delete(message.id);
485
+ const sent = this.delivered.get(message.id);
486
+ if (!sent)
487
+ return;
488
+ this.delivered.delete(message.id);
489
+ // A refusal stays one though the script couldn't receive it: nothing was done either way.
490
+ this.log.answered(sent.entry, message.unfit && sent.outcome !== 'refused' ? 'too-large' : sent.outcome, sent.brief);
491
+ return;
492
+ }
493
+ case 'end': {
494
+ const { t: _t, ...said } = message;
495
+ // The child cuts the value and a failure's parts as a return shows them.
496
+ // The parent bounds them too, since an ended run is kept for a later
497
+ // wait, with room for the child's cut and its marker, so an honest
498
+ // child's cut, and the count it gives, pass as they are.
499
+ if (said.how === 'finished') {
500
+ this.end(said.value === undefined ? { how: 'finished' } : { how: 'finished', value: bounded(said.value, VALUE_CHARS + CUT_MARK_CHARS) });
501
+ return;
502
+ }
503
+ if (said.how === 'failed') {
504
+ const error = {
505
+ name: bounded(said.error.name, NAME_CHARS + CUT_MARK_CHARS),
506
+ message: bounded(said.error.message, VALUE_CHARS + CUT_MARK_CHARS),
507
+ ...(said.error.stack === undefined ? {} : { stack: bounded(said.error.stack, VALUE_CHARS + CUT_MARK_CHARS) }),
508
+ };
509
+ this.end({ how: 'failed', error, ...(said.stalled ? { stalled: true } : {}) });
510
+ return;
511
+ }
512
+ if (!this.stopAsked) {
513
+ this.crash("its process sent the host a message it couldn't read (the script said it was stopped, and no stop was asked)");
514
+ return;
515
+ }
516
+ this.end({ how: 'stopped', reason: this.stopAsked.reason, ...(this.stopAsked.why ? { why: this.stopAsked.why } : {}) });
517
+ return;
518
+ }
519
+ }
520
+ }
521
+ async call(id, name, args) {
522
+ const wake = name === WAKE_CALL_NAME;
523
+ // The prelude offers only the script's tools, so a call to another name
524
+ // isn't following it. The name is the child's, bounded by the protocol.
525
+ if (!wake && !this.toolNames.has(name)) {
526
+ this.crash(`its process sent the host a call to ${JSON.stringify(name)}, which isn't one of the script's tools`);
527
+ return;
528
+ }
529
+ if (this.pendingIds.has(id)) {
530
+ this.crash(`its process sent the host a message it couldn't read (call ${id} was named twice)`);
531
+ return;
532
+ }
533
+ this.pendingIds.add(id);
534
+ const tool = wake ? 'wake_agent' : name;
535
+ // Refused here, before anything is sent: the record counts it, and nothing was done.
536
+ const refuse = (refusal) => {
537
+ this.delivered.set(id, { entry: this.log.start(tool, args), outcome: 'refused', brief: callBrief(refusal.message) });
538
+ this.answer(id, false, JSON.stringify(refusal));
539
+ };
540
+ let n = 0;
541
+ let wakeSent = () => { };
542
+ if (wake) {
543
+ // The child refuses a wake past the cap itself, and counts every wake it
544
+ // sends, one refused here included, so a child that sends more isn't
545
+ // following it. Counted before a stop's refusal, so wakes sent after a
546
+ // stop are bounded too.
547
+ if (this.wakeFrames >= this.options.maxWakes) {
548
+ this.crash(`its process sent the host more than ${this.options.maxWakes} wakes, the most a script may send`);
549
+ return;
550
+ }
551
+ // The child refuses a payload past its cap before a frame is built.
552
+ const tooLarge = wakeRefusal(args);
553
+ if (tooLarge) {
554
+ this.crash(`its process sent the host a wake of ${tooLarge.bytes.toLocaleString('en-US')} bytes, past the ${capText(MAX_WAKE_PAYLOAD_BYTES)} the script refuses itself`);
555
+ return;
556
+ }
557
+ this.wakeFrames += 1;
558
+ const stopping = this.stopRefusal('WakeRefused', tool);
559
+ if (stopping) {
560
+ refuse(stopping);
561
+ return;
562
+ }
563
+ // An early wake waits for its turn rather than being dropped: once the
564
+ // one before it has gone out, and the spacing since that one's delivery
565
+ // has passed. The run's end stops both waits.
566
+ const previous = this.wakeChain;
567
+ this.wakeChain = new Promise((resolve) => (wakeSent = resolve));
568
+ this.wakesWaiting += 1;
569
+ await Promise.race([previous, this.endedSignal]);
570
+ await this.wakeTurn();
571
+ // A wake still waiting for its turn when the script ended, or once a
572
+ // stop was asked or its process exited, is never sent: the ending is the
573
+ // script's last word, and the endings that should wake its agent already
574
+ // do. The record counts it as never sent: the run's end counts those
575
+ // still waiting when it comes, and one dropped before it is counted here.
576
+ // (A call waiting in the script for a place is likewise never sent when a
577
+ // stop or a crash ends it; one whose script returns goes out, since the
578
+ // child waits for every call it made.)
579
+ if (this.ending) {
580
+ wakeSent();
581
+ return;
582
+ }
583
+ this.wakesWaiting -= 1;
584
+ if (this.stopAsked || this.processExited) {
585
+ wakeSent();
586
+ this.log.unsent();
587
+ return;
588
+ }
589
+ this.wakes += 1;
590
+ n = this.wakes;
591
+ }
592
+ else {
593
+ // The prelude sends at most MAX_CALLS_IN_FLIGHT at once and keeps the
594
+ // rest waiting in the script, so a child that sends more isn't following it.
595
+ if (this.inFlight.size >= MAX_CALLS_IN_FLIGHT) {
596
+ this.crash(`its process sent the host more than ${MAX_CALLS_IN_FLIGHT} calls at once, the most a script may have out`);
597
+ return;
598
+ }
599
+ this.inFlight.add(id);
600
+ // The child refuses arguments past their caps before a frame is built.
601
+ const tooLarge = argumentsRefusal(name, args);
602
+ if (tooLarge) {
603
+ this.crash(`its process sent the host a call to ${name} whose arguments pass their caps of ${capText(MAX_ARGUMENT_BYTES)} and ` +
604
+ `${MAX_ARGUMENT_VALUES.toLocaleString('en-US')} values, which the script refuses itself`);
605
+ return;
606
+ }
607
+ const refusal = this.stopRefusal('ToolRefused', tool);
608
+ if (refusal) {
609
+ refuse(refusal);
610
+ return;
611
+ }
612
+ }
613
+ // The guest has its answer: a result, or a TimeoutError in its place.
614
+ let settled = false;
615
+ const settle = (ok, json) => {
616
+ settled = true;
617
+ this.answer(id, ok, json);
618
+ };
619
+ let entry = null;
620
+ // A call's time runs from when the script made it, a wait for room in the host included.
621
+ const timer = wake ? null : setTimeout(() => {
622
+ if (timer)
623
+ this.callTimers.delete(timer);
624
+ if (settled || this.ending)
625
+ return;
626
+ const waiting = this.dispatchWaiting.get(id);
627
+ if (waiting) {
628
+ // It never went out: no place in the host came free for it in time.
629
+ this.dispatchWaiting.delete(id);
630
+ waiting(false);
631
+ this.log.unsent();
632
+ settle(false, JSON.stringify({
633
+ name: 'TimeoutError',
634
+ message: `${name} was never sent: ${MAX_CALLS_IN_FLIGHT} of this script's earlier calls were still running in the host, timed-out ones ` +
635
+ `included, and its turn didn't come within ${formatMs(this.options.toolCallTimeoutMs)}, so nothing was done; calling it again may go through once one of them gets its result, ` +
636
+ 'though a call whose tool the host stopped waiting for stays counted until this script ends, and a new script starts with none',
637
+ }));
638
+ return;
639
+ }
640
+ if (!entry)
641
+ return;
642
+ this.log.timedOut(entry);
643
+ settle(false, JSON.stringify({
644
+ name: 'TimeoutError',
645
+ message: `${name} was sent, and no answer came within ${formatMs(this.options.toolCallTimeoutMs)}; it may still take effect`,
646
+ }));
647
+ }, this.options.toolCallTimeoutMs);
648
+ if (timer)
649
+ this.callTimers.add(timer);
650
+ const clearTimer = () => {
651
+ if (!timer)
652
+ return;
653
+ clearTimeout(timer);
654
+ this.callTimers.delete(timer);
655
+ };
656
+ if (!wake) {
657
+ // Past the calls the host holds, it waits as its text, and its place is
658
+ // taken for it when its turn comes (releasePlace).
659
+ if (this.hostCalls < MAX_CALLS_IN_FLIGHT)
660
+ this.hostCalls += 1;
661
+ else if (!(await new Promise((resolve) => this.dispatchWaiting.set(id, resolve))))
662
+ return;
663
+ // Once the run has ended or its process has exited, nothing it asks goes
664
+ // out: a frame the child wrote before it exited can be read after the
665
+ // exit, and a call waiting for room can be given its place after it. The
666
+ // record counts such a call as never sent: the script made it, and
667
+ // nothing was done. A stop asked while it waited refuses it, as it would
668
+ // have refused it then.
669
+ if (this.ending || this.processExited) {
670
+ clearTimer();
671
+ this.releasePlace();
672
+ this.log.unsent();
673
+ return;
674
+ }
675
+ const stopping = this.stopRefusal('ToolRefused', tool);
676
+ if (stopping) {
677
+ clearTimer();
678
+ this.releasePlace();
679
+ refuse(stopping);
680
+ return;
681
+ }
682
+ }
683
+ entry = this.log.start(tool, args);
684
+ let answer;
685
+ try {
686
+ answer = wake ? await this.options.onWake(args, n) : await this.options.onToolCall(name, args);
687
+ }
688
+ catch (err) {
689
+ // The bridge never rejects. One that does is answered as a failure,
690
+ // which may have taken effect, never as an unhandled rejection.
691
+ const text = `${tool} failed in the host: ${err instanceof Error ? err.message : String(err)}; it may have taken effect`;
692
+ answer = { ok: true, isError: true, brief: text, json: JSON.stringify({ content: [{ type: 'text', text }], structuredContent: null, isError: true }) };
693
+ }
694
+ if (wake) {
695
+ this.nextWakeAt = performance.now() + this.options.wakeMinIntervalMs;
696
+ if (answer.ok && !answer.isError)
697
+ this.delivered_wakes += 1;
698
+ wakeSent();
699
+ }
700
+ else if (!answer.held)
701
+ this.releasePlace();
702
+ // No result came, and the bridge answered for it, after the script's own
703
+ // timeout (the bridge waits longer): the call stays as the record had it
704
+ // (timed out, or uncertain at an ending), and its place stays taken. Had
705
+ // the script's timeout not come yet, its timer would still give it one.
706
+ if (answer.held)
707
+ return;
708
+ clearTimer();
709
+ const bytes = Buffer.byteLength(answer.json, 'utf8');
710
+ const limit = maxResultBytes(this.options.memoryMaxBytes);
711
+ const tooLarge = bytes > limit;
712
+ // A refusal stays one however long its text: nothing was done.
713
+ const outcome = !answer.ok ? 'refused' : tooLarge ? 'too-large' : answer.isError ? 'failed' : 'answered';
714
+ // Only its start is kept, as a copy: the record names a call by that much.
715
+ const brief = callBrief(answer.brief ?? answer.json);
716
+ if (settled || this.ending) {
717
+ // The script stopped waiting for it, or ended: the record takes the answer as it comes.
718
+ this.log.answered(entry, outcome, brief);
719
+ return;
720
+ }
721
+ this.delivered.set(id, { entry, outcome, brief });
722
+ if (tooLarge) {
723
+ settle(false, JSON.stringify(outcome === 'refused'
724
+ ? {
725
+ name: wake ? 'WakeRefused' : 'ToolRefused',
726
+ message: `${glimpse(answer.brief ?? refusalText(answer.json), REFUSAL_GLIMPSE_CHARS)} (the refusal's text, ${bytes} bytes, is too large for this script's memory, so only its start is here; nothing was done)`,
727
+ }
728
+ : {
729
+ name: RESULT_TOO_LARGE,
730
+ message: `${name}'s result, ${bytes} bytes, is too large for this script's memory; the call took effect, and none of its result reached the script`,
731
+ bytes,
732
+ }));
733
+ return;
734
+ }
735
+ settle(answer.ok, answer.json);
736
+ }
737
+ /**
738
+ * A call no longer holds a place in the host: the bridge answered it, or it
739
+ * won't be sent. The first call waiting for room takes the place, in the
740
+ * same step, so no later call can take it first. Whatever lets a waiting
741
+ * call go takes it off the queue. One given its place once the run has
742
+ * ended or its process has exited isn't sent (the call's own check), and
743
+ * the end lets the calls still waiting go.
744
+ */
745
+ releasePlace() {
746
+ this.hostCalls -= 1;
747
+ const next = this.dispatchWaiting.entries().next();
748
+ if (next.done)
749
+ return;
750
+ const [nextId, go] = next.value;
751
+ this.dispatchWaiting.delete(nextId);
752
+ this.hostCalls += 1;
753
+ go(true);
754
+ }
755
+ /** An answer to the call the child named `id`, sent once its turn comes. The id stays reserved until the child says the script has it. */
756
+ answer(id, ok, json) {
757
+ this.sendResult(id, ok, json);
758
+ }
759
+ /** A call or wake that comes once a stop has been asked isn't sent: the script is ending. */
760
+ stopRefusal(name, tool) {
761
+ return this.stopAsked ? { name, message: `${tool}: the script is being stopped, so nothing was sent` } : null;
762
+ }
763
+ /** Resolves once the spacing since the last wake has passed, or at once when the run has ended. */
764
+ wakeTurn() {
765
+ const wait = this.nextWakeAt - performance.now();
766
+ if (wait <= 0 || this.ending)
767
+ return Promise.resolve();
768
+ return new Promise((resolve) => {
769
+ const release = () => {
770
+ clearTimeout(timer);
771
+ this.wakeWaiters.delete(release);
772
+ resolve();
773
+ };
774
+ const timer = setTimeout(release, wait);
775
+ this.wakeWaiters.add(release);
776
+ });
777
+ }
778
+ /**
779
+ * Once the process has exited and the run has ended, nothing more goes to
780
+ * the child: an ended run kept for a later wait holds none of it. Whichever
781
+ * of the two comes second calls this.
782
+ */
783
+ release() {
784
+ this.child?.stdin?.destroy();
785
+ this.child = null;
786
+ }
787
+ crash(detail) {
788
+ if (this.ending)
789
+ return;
790
+ this.end({ how: 'crashed', detail });
791
+ this.child?.kill('SIGKILL');
792
+ }
793
+ end(ending) {
794
+ if (this.ending)
795
+ return;
796
+ // Results sent and not yet seen were answered, and the script ended
797
+ // before it read them: each is named, a refusal counted. Calls still
798
+ // running may or may not take effect. A stop or a crash can leave either,
799
+ // whatever the ending: a script that had returned or thrown when a stop
800
+ // came ends as it did, with the calls its stop cut the wait for. So can a
801
+ // failure that escaped the script's promises, which ends the run at once,
802
+ // and a result the engine dropped as a stop came, which gets no seen. A
803
+ // TimeoutError the engine dropped so leaves its call named timed-out, as
804
+ // the host's timer had it, with no answer to name. Otherwise the child
805
+ // reports a finish or a failure only once every call it made is settled
806
+ // and seen. The host's count of the calls the bridge holds stays as it
807
+ // is, since those tools may still run: each frees its place as it
808
+ // answers, and one the bridge answered for with no result (held) never
809
+ // does.
810
+ for (const sent of this.delivered.values())
811
+ this.log.unseen(sent.entry, sent.outcome);
812
+ this.delivered.clear();
813
+ this.pendingIds.clear();
814
+ this.answeredIds.clear();
815
+ this.inFlight.clear();
816
+ this.log.ended();
817
+ this.ending = ending;
818
+ if (this.lifetimeTimer)
819
+ clearTimeout(this.lifetimeTimer);
820
+ // A child exits on its own once it has said how its script ended. One that
821
+ // doesn't, such as one stuck in its cleanup, is ended after the stop's
822
+ // grace, so it can't hold its place or outlive its run.
823
+ if (this.child && !this.processExited && !this.killTimer) {
824
+ this.killTimer = setTimeout(() => this.child?.kill('SIGKILL'), this.options.stopGraceMs);
825
+ this.killTimer.unref?.();
826
+ }
827
+ for (const timer of this.callTimers)
828
+ clearTimeout(timer);
829
+ this.callTimers.clear();
830
+ // Nothing more goes to the child: waiting results are dropped, and calls
831
+ // waiting for a place go nowhere, so an ended run that's kept for a later
832
+ // wait holds neither.
833
+ this.resultQueue.length = 0;
834
+ for (const release of [...this.wakeWaiters])
835
+ release();
836
+ this.endedResolve();
837
+ // Calls waiting for room in the host are never sent, and the record counts
838
+ // them so: the script made them, and nothing was done.
839
+ for (const [waitingId, go] of [...this.dispatchWaiting]) {
840
+ this.dispatchWaiting.delete(waitingId);
841
+ this.log.unsent();
842
+ go(false);
843
+ }
844
+ // So are wakes waiting for their turn: each sees the ending and goes.
845
+ for (; this.wakesWaiting > 0; this.wakesWaiting -= 1)
846
+ this.log.unsent();
847
+ for (const waiter of [...this.waiters])
848
+ waiter();
849
+ if (this.processExited)
850
+ this.release();
851
+ this.options.onEnd?.(ending);
852
+ }
853
+ }
854
+ //# sourceMappingURL=runner.js.map