@animalabs/agent-framework 0.23.2 → 0.24.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 (338) hide show
  1. package/README.md +212 -42
  2. package/dist/src/agent.d.ts +60 -9
  3. package/dist/src/agent.d.ts.map +1 -1
  4. package/dist/src/agent.js +92 -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/code-execution/engine.d.ts +177 -0
  10. package/dist/src/code-execution/engine.d.ts.map +1 -0
  11. package/dist/src/code-execution/engine.js +494 -0
  12. package/dist/src/code-execution/engine.js.map +1 -0
  13. package/dist/src/code-execution/index.d.ts +5 -10
  14. package/dist/src/code-execution/index.d.ts.map +1 -1
  15. package/dist/src/code-execution/index.js +5 -9
  16. package/dist/src/code-execution/index.js.map +1 -1
  17. package/dist/src/code-execution/isolate/calls.d.ts +114 -0
  18. package/dist/src/code-execution/isolate/calls.d.ts.map +1 -0
  19. package/dist/src/code-execution/isolate/calls.js +145 -0
  20. package/dist/src/code-execution/isolate/calls.js.map +1 -0
  21. package/dist/src/code-execution/isolate/child-entry.d.ts +2 -0
  22. package/dist/src/code-execution/isolate/child-entry.d.ts.map +1 -0
  23. package/dist/src/code-execution/isolate/child-entry.js +5 -0
  24. package/dist/src/code-execution/isolate/child-entry.js.map +1 -0
  25. package/dist/src/code-execution/isolate/child-main.d.ts +26 -0
  26. package/dist/src/code-execution/isolate/child-main.d.ts.map +1 -0
  27. package/dist/src/code-execution/isolate/child-main.js +132 -0
  28. package/dist/src/code-execution/isolate/child-main.js.map +1 -0
  29. package/dist/src/code-execution/isolate/child.d.ts +44 -0
  30. package/dist/src/code-execution/isolate/child.d.ts.map +1 -0
  31. package/dist/src/code-execution/isolate/child.js +315 -0
  32. package/dist/src/code-execution/isolate/child.js.map +1 -0
  33. package/dist/src/code-execution/isolate/frames.d.ts +80 -0
  34. package/dist/src/code-execution/isolate/frames.d.ts.map +1 -0
  35. package/dist/src/code-execution/isolate/frames.js +210 -0
  36. package/dist/src/code-execution/isolate/frames.js.map +1 -0
  37. package/dist/src/code-execution/isolate/output.d.ts +68 -0
  38. package/dist/src/code-execution/isolate/output.d.ts.map +1 -0
  39. package/dist/src/code-execution/isolate/output.js +143 -0
  40. package/dist/src/code-execution/isolate/output.js.map +1 -0
  41. package/dist/src/code-execution/isolate/prelude.d.ts +109 -0
  42. package/dist/src/code-execution/isolate/prelude.d.ts.map +1 -0
  43. package/dist/src/code-execution/isolate/prelude.js +401 -0
  44. package/dist/src/code-execution/isolate/prelude.js.map +1 -0
  45. package/dist/src/code-execution/isolate/protocol.d.ts +151 -0
  46. package/dist/src/code-execution/isolate/protocol.d.ts.map +1 -0
  47. package/dist/src/code-execution/isolate/protocol.js +150 -0
  48. package/dist/src/code-execution/isolate/protocol.js.map +1 -0
  49. package/dist/src/code-execution/isolate/render.d.ts +57 -0
  50. package/dist/src/code-execution/isolate/render.d.ts.map +1 -0
  51. package/dist/src/code-execution/isolate/render.js +214 -0
  52. package/dist/src/code-execution/isolate/render.js.map +1 -0
  53. package/dist/src/code-execution/isolate/runner.d.ts +250 -0
  54. package/dist/src/code-execution/isolate/runner.d.ts.map +1 -0
  55. package/dist/src/code-execution/isolate/runner.js +854 -0
  56. package/dist/src/code-execution/isolate/runner.js.map +1 -0
  57. package/dist/src/code-execution/isolate/scripts.d.ts +175 -0
  58. package/dist/src/code-execution/isolate/scripts.d.ts.map +1 -0
  59. package/dist/src/code-execution/isolate/scripts.js +357 -0
  60. package/dist/src/code-execution/isolate/scripts.js.map +1 -0
  61. package/dist/src/code-execution/quickjs.wasm +0 -0
  62. package/dist/src/code-execution/tool-definition.d.ts +93 -43
  63. package/dist/src/code-execution/tool-definition.d.ts.map +1 -1
  64. package/dist/src/code-execution/tool-definition.js +176 -142
  65. package/dist/src/code-execution/tool-definition.js.map +1 -1
  66. package/dist/src/deadline-timer.d.ts +3 -0
  67. package/dist/src/deadline-timer.d.ts.map +1 -1
  68. package/dist/src/deadline-timer.js +1 -1
  69. package/dist/src/deadline-timer.js.map +1 -1
  70. package/dist/src/framework.d.ts +1027 -440
  71. package/dist/src/framework.d.ts.map +1 -1
  72. package/dist/src/framework.js +4093 -2238
  73. package/dist/src/framework.js.map +1 -1
  74. package/dist/src/gate/event-gate.d.ts +28 -1
  75. package/dist/src/gate/event-gate.d.ts.map +1 -1
  76. package/dist/src/gate/event-gate.js +88 -10
  77. package/dist/src/gate/event-gate.js.map +1 -1
  78. package/dist/src/held-writes.d.ts +16 -3
  79. package/dist/src/held-writes.d.ts.map +1 -1
  80. package/dist/src/held-writes.js +16 -3
  81. package/dist/src/held-writes.js.map +1 -1
  82. package/dist/src/index.d.ts +6 -6
  83. package/dist/src/index.d.ts.map +1 -1
  84. package/dist/src/index.js +9 -8
  85. package/dist/src/index.js.map +1 -1
  86. package/dist/src/mcpl/channel-registry.d.ts +471 -75
  87. package/dist/src/mcpl/channel-registry.d.ts.map +1 -1
  88. package/dist/src/mcpl/channel-registry.js +1673 -337
  89. package/dist/src/mcpl/channel-registry.js.map +1 -1
  90. package/dist/src/mcpl/conversation-router.d.ts +12 -7
  91. package/dist/src/mcpl/conversation-router.d.ts.map +1 -1
  92. package/dist/src/mcpl/conversation-router.js +12 -7
  93. package/dist/src/mcpl/conversation-router.js.map +1 -1
  94. package/dist/src/mcpl/convert-block.d.ts.map +1 -1
  95. package/dist/src/mcpl/convert-block.js +9 -5
  96. package/dist/src/mcpl/convert-block.js.map +1 -1
  97. package/dist/src/mcpl/inbound-source.d.ts +50 -0
  98. package/dist/src/mcpl/inbound-source.d.ts.map +1 -1
  99. package/dist/src/mcpl/inbound-source.js +130 -0
  100. package/dist/src/mcpl/inbound-source.js.map +1 -1
  101. package/dist/src/mcpl/index.d.ts +2 -1
  102. package/dist/src/mcpl/index.d.ts.map +1 -1
  103. package/dist/src/mcpl/index.js +3 -2
  104. package/dist/src/mcpl/index.js.map +1 -1
  105. package/dist/src/mcpl/log-value.d.ts +6 -0
  106. package/dist/src/mcpl/log-value.d.ts.map +1 -1
  107. package/dist/src/mcpl/log-value.js +8 -0
  108. package/dist/src/mcpl/log-value.js.map +1 -1
  109. package/dist/src/mcpl/modern-connection.d.ts +15 -6
  110. package/dist/src/mcpl/modern-connection.d.ts.map +1 -1
  111. package/dist/src/mcpl/modern-connection.js +168 -53
  112. package/dist/src/mcpl/modern-connection.js.map +1 -1
  113. package/dist/src/mcpl/prose-grammar.d.ts +4 -5
  114. package/dist/src/mcpl/prose-grammar.d.ts.map +1 -1
  115. package/dist/src/mcpl/prose-grammar.js +4 -5
  116. package/dist/src/mcpl/prose-grammar.js.map +1 -1
  117. package/dist/src/mcpl/protocol-family.d.ts +97 -10
  118. package/dist/src/mcpl/protocol-family.d.ts.map +1 -1
  119. package/dist/src/mcpl/protocol-family.js +173 -16
  120. package/dist/src/mcpl/protocol-family.js.map +1 -1
  121. package/dist/src/mcpl/push-coalescer.d.ts +132 -14
  122. package/dist/src/mcpl/push-coalescer.d.ts.map +1 -1
  123. package/dist/src/mcpl/push-coalescer.js +227 -50
  124. package/dist/src/mcpl/push-coalescer.js.map +1 -1
  125. package/dist/src/mcpl/push-handler.d.ts +6 -1
  126. package/dist/src/mcpl/push-handler.d.ts.map +1 -1
  127. package/dist/src/mcpl/push-handler.js +4 -1
  128. package/dist/src/mcpl/push-handler.js.map +1 -1
  129. package/dist/src/mcpl/reference-fetcher.d.ts +2 -2
  130. package/dist/src/mcpl/reference-fetcher.d.ts.map +1 -1
  131. package/dist/src/mcpl/reference-fetcher.js +4 -4
  132. package/dist/src/mcpl/reference-fetcher.js.map +1 -1
  133. package/dist/src/mcpl/server-connection.d.ts +50 -21
  134. package/dist/src/mcpl/server-connection.d.ts.map +1 -1
  135. package/dist/src/mcpl/server-connection.js +158 -74
  136. package/dist/src/mcpl/server-connection.js.map +1 -1
  137. package/dist/src/mcpl/tags.d.ts +24 -0
  138. package/dist/src/mcpl/tags.d.ts.map +1 -1
  139. package/dist/src/mcpl/tags.js +33 -0
  140. package/dist/src/mcpl/tags.js.map +1 -1
  141. package/dist/src/mcpl/tool-result-normalize.d.ts +2 -1
  142. package/dist/src/mcpl/tool-result-normalize.d.ts.map +1 -1
  143. package/dist/src/mcpl/tool-result-normalize.js.map +1 -1
  144. package/dist/src/mcpl/transport.d.ts +55 -7
  145. package/dist/src/mcpl/transport.d.ts.map +1 -1
  146. package/dist/src/mcpl/transport.js +163 -34
  147. package/dist/src/mcpl/transport.js.map +1 -1
  148. package/dist/src/mcpl/types.d.ts +22 -6
  149. package/dist/src/mcpl/types.d.ts.map +1 -1
  150. package/dist/src/mcpl/types.js.map +1 -1
  151. package/dist/src/module-registry.d.ts +4 -2
  152. package/dist/src/module-registry.d.ts.map +1 -1
  153. package/dist/src/module-registry.js +100 -13
  154. package/dist/src/module-registry.js.map +1 -1
  155. package/dist/src/modules/discord/index.d.ts.map +1 -1
  156. package/dist/src/modules/discord/index.js +8 -13
  157. package/dist/src/modules/discord/index.js.map +1 -1
  158. package/dist/src/modules/history/index.d.ts.map +1 -1
  159. package/dist/src/modules/history/index.js +27 -4
  160. package/dist/src/modules/history/index.js.map +1 -1
  161. package/dist/src/modules/history/semantic.d.ts +15 -3
  162. package/dist/src/modules/history/semantic.d.ts.map +1 -1
  163. package/dist/src/modules/history/semantic.js +27 -5
  164. package/dist/src/modules/history/semantic.js.map +1 -1
  165. package/dist/src/modules/index.d.ts +1 -1
  166. package/dist/src/modules/index.d.ts.map +1 -1
  167. package/dist/src/modules/index.js.map +1 -1
  168. package/dist/src/modules/unknown-tool.d.ts +4 -1
  169. package/dist/src/modules/unknown-tool.d.ts.map +1 -1
  170. package/dist/src/modules/unknown-tool.js +4 -1
  171. package/dist/src/modules/unknown-tool.js.map +1 -1
  172. package/dist/src/modules/workspace/branch-intent.d.ts +3 -1
  173. package/dist/src/modules/workspace/branch-intent.d.ts.map +1 -1
  174. package/dist/src/modules/workspace/branch-intent.js +3 -1
  175. package/dist/src/modules/workspace/branch-intent.js.map +1 -1
  176. package/dist/src/modules/workspace/edit-plan.d.ts +265 -0
  177. package/dist/src/modules/workspace/edit-plan.d.ts.map +1 -0
  178. package/dist/src/modules/workspace/edit-plan.js +794 -0
  179. package/dist/src/modules/workspace/edit-plan.js.map +1 -0
  180. package/dist/src/modules/workspace/edit-text.d.ts +117 -0
  181. package/dist/src/modules/workspace/edit-text.d.ts.map +1 -0
  182. package/dist/src/modules/workspace/edit-text.js +584 -0
  183. package/dist/src/modules/workspace/edit-text.js.map +1 -0
  184. package/dist/src/modules/workspace/effects.d.ts +5 -4
  185. package/dist/src/modules/workspace/effects.d.ts.map +1 -1
  186. package/dist/src/modules/workspace/effects.js +78 -25
  187. package/dist/src/modules/workspace/effects.js.map +1 -1
  188. package/dist/src/modules/workspace/index.d.ts +484 -61
  189. package/dist/src/modules/workspace/index.d.ts.map +1 -1
  190. package/dist/src/modules/workspace/index.js +2173 -645
  191. package/dist/src/modules/workspace/index.js.map +1 -1
  192. package/dist/src/modules/workspace/observe.d.ts +213 -38
  193. package/dist/src/modules/workspace/observe.d.ts.map +1 -1
  194. package/dist/src/modules/workspace/observe.js +416 -36
  195. package/dist/src/modules/workspace/observe.js.map +1 -1
  196. package/dist/src/modules/workspace/reconcile.d.ts.map +1 -1
  197. package/dist/src/modules/workspace/reconcile.js +15 -8
  198. package/dist/src/modules/workspace/reconcile.js.map +1 -1
  199. package/dist/src/modules/workspace/text.d.ts +32 -0
  200. package/dist/src/modules/workspace/text.d.ts.map +1 -0
  201. package/dist/src/modules/workspace/text.js +67 -0
  202. package/dist/src/modules/workspace/text.js.map +1 -0
  203. package/dist/src/modules/workspace/types.d.ts +210 -6
  204. package/dist/src/modules/workspace/types.d.ts.map +1 -1
  205. package/dist/src/modules/workspace/types.js.map +1 -1
  206. package/dist/src/modules/workspace/watcher.d.ts +3 -2
  207. package/dist/src/modules/workspace/watcher.d.ts.map +1 -1
  208. package/dist/src/modules/workspace/watcher.js.map +1 -1
  209. package/dist/src/operator-log.d.ts +52 -24
  210. package/dist/src/operator-log.d.ts.map +1 -1
  211. package/dist/src/operator-log.js.map +1 -1
  212. package/dist/src/recovery/discord-awareness-outbox.d.ts +496 -72
  213. package/dist/src/recovery/discord-awareness-outbox.d.ts.map +1 -1
  214. package/dist/src/recovery/discord-awareness-outbox.js +1196 -289
  215. package/dist/src/recovery/discord-awareness-outbox.js.map +1 -1
  216. package/dist/src/recovery/offline-branch.d.ts +26 -1
  217. package/dist/src/recovery/offline-branch.d.ts.map +1 -1
  218. package/dist/src/recovery/offline-branch.js +164 -39
  219. package/dist/src/recovery/offline-branch.js.map +1 -1
  220. package/dist/src/recovery/recover-cli.js +23 -4
  221. package/dist/src/recovery/recover-cli.js.map +1 -1
  222. package/dist/src/send-tools.d.ts +11 -0
  223. package/dist/src/send-tools.d.ts.map +1 -0
  224. package/dist/src/send-tools.js +11 -0
  225. package/dist/src/send-tools.js.map +1 -0
  226. package/dist/src/tool-result-guard.d.ts +4 -3
  227. package/dist/src/tool-result-guard.d.ts.map +1 -1
  228. package/dist/src/tool-result-guard.js +5 -4
  229. package/dist/src/tool-result-guard.js.map +1 -1
  230. package/dist/src/tool-result-history.d.ts +1 -1
  231. package/dist/src/tool-result-history.js +1 -1
  232. package/dist/src/tune-out/coordinator.d.ts +21 -0
  233. package/dist/src/tune-out/coordinator.d.ts.map +1 -1
  234. package/dist/src/tune-out/coordinator.js +139 -37
  235. package/dist/src/tune-out/coordinator.js.map +1 -1
  236. package/dist/src/types/agent.d.ts +4 -0
  237. package/dist/src/types/agent.d.ts.map +1 -1
  238. package/dist/src/types/events.d.ts +62 -9
  239. package/dist/src/types/events.d.ts.map +1 -1
  240. package/dist/src/types/framework.d.ts +62 -58
  241. package/dist/src/types/framework.d.ts.map +1 -1
  242. package/dist/src/types/module.d.ts +40 -8
  243. package/dist/src/types/module.d.ts.map +1 -1
  244. package/dist/src/types/trace.d.ts +111 -10
  245. package/dist/src/types/trace.d.ts.map +1 -1
  246. package/dist/src/usage/types.d.ts +16 -0
  247. package/dist/src/usage/types.d.ts.map +1 -1
  248. package/dist/src/usage/usage-tracker.d.ts +14 -1
  249. package/dist/src/usage/usage-tracker.d.ts.map +1 -1
  250. package/dist/src/usage/usage-tracker.js +81 -41
  251. package/dist/src/usage/usage-tracker.js.map +1 -1
  252. package/package.json +6 -5
  253. package/src/agent.ts +93 -12
  254. package/src/api/server.ts +14 -12
  255. package/src/code-execution/engine.ts +595 -0
  256. package/src/code-execution/index.ts +5 -17
  257. package/src/code-execution/isolate/calls.ts +199 -0
  258. package/src/code-execution/isolate/child-entry.ts +5 -0
  259. package/src/code-execution/isolate/child-main.ts +132 -0
  260. package/src/code-execution/isolate/child.ts +327 -0
  261. package/src/code-execution/isolate/frames.ts +209 -0
  262. package/src/code-execution/isolate/output.ts +161 -0
  263. package/src/code-execution/isolate/prelude.ts +409 -0
  264. package/src/code-execution/isolate/protocol.ts +240 -0
  265. package/src/code-execution/isolate/render.ts +230 -0
  266. package/src/code-execution/isolate/runner.ts +911 -0
  267. package/src/code-execution/isolate/scripts.ts +479 -0
  268. package/src/code-execution/quickjs.wasm +0 -0
  269. package/src/code-execution/quickjs.wasm.LICENSES +149 -0
  270. package/src/code-execution/tool-definition.ts +236 -167
  271. package/src/deadline-timer.ts +1 -1
  272. package/src/framework.ts +4468 -2418
  273. package/src/gate/event-gate.ts +78 -11
  274. package/src/held-writes.ts +17 -3
  275. package/src/index.ts +30 -22
  276. package/src/mcpl/channel-registry.ts +1825 -368
  277. package/src/mcpl/conversation-router.ts +12 -7
  278. package/src/mcpl/convert-block.ts +9 -5
  279. package/src/mcpl/inbound-source.ts +131 -0
  280. package/src/mcpl/index.ts +4 -2
  281. package/src/mcpl/log-value.ts +9 -0
  282. package/src/mcpl/modern-connection.ts +162 -53
  283. package/src/mcpl/prose-grammar.ts +4 -5
  284. package/src/mcpl/protocol-family.ts +199 -21
  285. package/src/mcpl/push-coalescer.ts +306 -62
  286. package/src/mcpl/push-handler.ts +10 -2
  287. package/src/mcpl/reference-fetcher.ts +4 -4
  288. package/src/mcpl/server-connection.ts +180 -79
  289. package/src/mcpl/tags.ts +36 -0
  290. package/src/mcpl/tool-result-normalize.ts +2 -1
  291. package/src/mcpl/transport.ts +162 -33
  292. package/src/mcpl/types.ts +23 -6
  293. package/src/module-registry.ts +92 -13
  294. package/src/modules/discord/index.ts +8 -13
  295. package/src/modules/history/index.ts +25 -3
  296. package/src/modules/history/semantic.ts +29 -6
  297. package/src/modules/index.ts +1 -0
  298. package/src/modules/unknown-tool.ts +4 -1
  299. package/src/modules/workspace/branch-intent.ts +3 -1
  300. package/src/modules/workspace/edit-plan.ts +954 -0
  301. package/src/modules/workspace/edit-text.ts +618 -0
  302. package/src/modules/workspace/effects.ts +70 -19
  303. package/src/modules/workspace/index.ts +2247 -642
  304. package/src/modules/workspace/observe.ts +465 -56
  305. package/src/modules/workspace/reconcile.ts +14 -7
  306. package/src/modules/workspace/text.ts +68 -0
  307. package/src/modules/workspace/types.ts +211 -6
  308. package/src/modules/workspace/watcher.ts +3 -2
  309. package/src/operator-log.ts +47 -19
  310. package/src/recovery/discord-awareness-outbox.ts +1500 -356
  311. package/src/recovery/offline-branch.ts +189 -55
  312. package/src/recovery/recover-cli.ts +25 -4
  313. package/src/send-tools.ts +11 -0
  314. package/src/tool-result-guard.ts +6 -5
  315. package/src/tool-result-history.ts +1 -1
  316. package/src/tune-out/coordinator.ts +163 -42
  317. package/src/types/agent.ts +4 -0
  318. package/src/types/events.ts +62 -9
  319. package/src/types/framework.ts +62 -58
  320. package/src/types/module.ts +40 -8
  321. package/src/types/trace.ts +121 -15
  322. package/src/usage/types.ts +16 -0
  323. package/src/usage/usage-tracker.ts +86 -43
  324. package/dist/src/code-execution/py-runner.d.ts +0 -191
  325. package/dist/src/code-execution/py-runner.d.ts.map +0 -1
  326. package/dist/src/code-execution/py-runner.js +0 -655
  327. package/dist/src/code-execution/py-runner.js.map +0 -1
  328. package/dist/src/code-execution/runtime-py.d.ts +0 -50
  329. package/dist/src/code-execution/runtime-py.d.ts.map +0 -1
  330. package/dist/src/code-execution/runtime-py.js +0 -677
  331. package/dist/src/code-execution/runtime-py.js.map +0 -1
  332. package/dist/src/mcpl/prose-stream-router.d.ts +0 -88
  333. package/dist/src/mcpl/prose-stream-router.d.ts.map +0 -1
  334. package/dist/src/mcpl/prose-stream-router.js +0 -291
  335. package/dist/src/mcpl/prose-stream-router.js.map +0 -1
  336. package/src/code-execution/py-runner.ts +0 -791
  337. package/src/code-execution/runtime-py.ts +0 -677
  338. package/src/mcpl/prose-stream-router.ts +0 -268
@@ -0,0 +1,199 @@
1
+ /**
2
+ * A script's call record, kept in the parent, outside the child, so losing
3
+ * the child loses none of it. A call goes in at
4
+ * dispatch, as running until it's answered. Answered calls are counted; those
5
+ * whose answer the script didn't see are named, with their arguments, so an
6
+ * agent can tell which effects may have taken place. A script can live a day,
7
+ * so the record is bounded: it names at most CALLS_NAMED and counts the rest
8
+ * by tool, and it holds a counted call only while it runs. A named call stays
9
+ * until the script ends, and its state can still change after the call ends,
10
+ * as when a timed-out call's answer comes.
11
+ */
12
+
13
+ /** A call's arguments, or a later answer's start, as a return names it: at most this many characters. */
14
+ export const CALL_BRIEF_CHARS = 120;
15
+ /** How many calls whose answer the script didn't see a return names; it counts the rest by tool. */
16
+ export const CALLS_NAMED = 10;
17
+
18
+ /** How a return names a tool, as the agent writes it in a script. */
19
+ export const toolRef = (tool: string): string => (tool === 'wake_agent' ? 'wake_agent()' : `tools[${JSON.stringify(tool)}]`);
20
+
21
+ /**
22
+ * A copy of `text` that shares no storage with it. A slice of a string can
23
+ * keep the whole string it was cut from alive, in V8 and in JavaScriptCore, so
24
+ * a short piece kept from a long text is copied first.
25
+ */
26
+ export function fresh(text: string): string {
27
+ return JSON.parse(JSON.stringify(text)) as string;
28
+ }
29
+
30
+ /** `text` cut to CALL_BRIEF_CHARS with "…", never between a surrogate pair's halves, as a copy of its own. */
31
+ export function callBrief(text: string): string {
32
+ if (text.length <= CALL_BRIEF_CHARS) return fresh(text);
33
+ let cut = CALL_BRIEF_CHARS - 1;
34
+ const before = text.charCodeAt(cut - 1);
35
+ if (before >= 0xd800 && before <= 0xdbff) cut -= 1;
36
+ return fresh(`${text.slice(0, cut)}…`);
37
+ }
38
+
39
+ /** A call whose answer the script didn't see, as a return names it. */
40
+ export interface NamedCall {
41
+ tool: string;
42
+ /** Its arguments as the script sent them, cut to CALL_BRIEF_CHARS. */
43
+ args: string;
44
+ /**
45
+ * - `uncertain`: still running when the script ended, so it may or may not take effect.
46
+ * - `timed-out`: no answer came within the call's timeout, so it may still take effect. The script was given a TimeoutError, unless it ended before reading it.
47
+ * - `timed-out-answered`: no answer came within the call's timeout, and one came later; `answer` is its start.
48
+ * - `too-large`: answered, with a result the script couldn't receive (too large for its memory, or nested too deep), so it got ResultTooLarge in its place.
49
+ * - `unseen`: answered, and the script was stopped or crashed before it read the answer.
50
+ */
51
+ state: 'uncertain' | 'timed-out' | 'timed-out-answered' | 'too-large' | 'unseen';
52
+ /** For `timed-out-answered`: the answer's start, cut to CALL_BRIEF_CHARS. */
53
+ answer?: string;
54
+ }
55
+
56
+ /** The record as a return reports it. */
57
+ export interface CallSummary {
58
+ /** Calls the script made. */
59
+ made: number;
60
+ /** Answered while the script waited, with a result that wasn't an error. */
61
+ answered: number;
62
+ /** Answered while the script waited, with a result marked as an error. */
63
+ failed: number;
64
+ /** Refused before they ran: nothing was done. */
65
+ refused: number;
66
+ /** Never sent, so nothing was done: they timed out while they waited for room in the host, the run ended while they waited, or the host read them once the script's process had exited. */
67
+ unsent: number;
68
+ /** Still in flight, the script waiting on them. */
69
+ running: number;
70
+ /** Waiting their turn in the host, not sent yet: calls waiting for room, and wakes waiting for theirs. */
71
+ waiting: number;
72
+ /** Those whose answer the script didn't see, the first CALLS_NAMED of them, in the order each became so. */
73
+ named: NamedCall[];
74
+ /** The rest of those, counted by tool. */
75
+ more: Record<string, number>;
76
+ }
77
+
78
+ /** How a call that the script waited on was answered. */
79
+ export type CallOutcome = 'answered' | 'failed' | 'refused' | 'too-large';
80
+
81
+ /** One call the record still follows, held by the caller between its start and its end. */
82
+ export interface CallEntry {
83
+ readonly tool: string;
84
+ readonly args: string;
85
+ /** Set once the call has a place among the named, so a later answer can say what it was. */
86
+ named: NamedCall | null;
87
+ /** True once the call's timeout passed with no answer: the script is given a TimeoutError, unless it ended before reading it. */
88
+ timedOut: boolean;
89
+ /** True once the record holds the call's last word. */
90
+ done: boolean;
91
+ }
92
+
93
+ export class CallLog {
94
+ private made = 0;
95
+ private readonly counts = { answered: 0, failed: 0, refused: 0, unsent: 0 };
96
+ private readonly running = new Set<CallEntry>();
97
+ /** Timed-out calls among the named whose answer hasn't come: it can still be named, until the script ends. */
98
+ private readonly awaiting = new Set<CallEntry>();
99
+ private readonly named: NamedCall[] = [];
100
+ private readonly more = new Map<string, number>();
101
+
102
+ /** A call went out: it's running until it's answered. */
103
+ start(tool: string, argsJson: string): CallEntry {
104
+ this.made += 1;
105
+ const entry: CallEntry = { tool, args: callBrief(argsJson), named: null, timedOut: false, done: false };
106
+ this.running.add(entry);
107
+ return entry;
108
+ }
109
+
110
+ /**
111
+ * A call or wake the script made was never sent, so nothing was done: a
112
+ * call that timed out while it waited for room in the host, or that the run
113
+ * ended, or its process exited, before it went out, and one read after the
114
+ * process exited; a wake still waiting for its turn when the run ended, or
115
+ * once a stop was asked or the process exited.
116
+ */
117
+ unsent(): void {
118
+ this.made += 1;
119
+ this.counts.unsent += 1;
120
+ }
121
+
122
+ /** The call's timeout passed with no answer, and the script is given a TimeoutError. Its answer may still come. */
123
+ timedOut(entry: CallEntry): void {
124
+ if (entry.done || entry.timedOut) return;
125
+ entry.timedOut = true;
126
+ this.running.delete(entry);
127
+ entry.named = this.name(entry, 'timed-out');
128
+ if (entry.named) this.awaiting.add(entry);
129
+ else entry.done = true;
130
+ }
131
+
132
+ /** The call's answer came. `brief` is the start of what the tool answered, for a call the script stopped waiting for. */
133
+ answered(entry: CallEntry, outcome: CallOutcome, brief: string): void {
134
+ if (entry.done) return;
135
+ entry.done = true;
136
+ this.running.delete(entry);
137
+ this.awaiting.delete(entry);
138
+ if (entry.timedOut) {
139
+ if (entry.named) {
140
+ entry.named.state = 'timed-out-answered';
141
+ entry.named.answer = callBrief(brief);
142
+ }
143
+ return;
144
+ }
145
+ if (outcome === 'too-large') this.name(entry, 'too-large');
146
+ else this.counts[outcome] += 1;
147
+ }
148
+
149
+ /**
150
+ * The call was answered, and the script ended before it read the answer: a
151
+ * refusal did nothing, and is counted; any other answer is named.
152
+ */
153
+ unseen(entry: CallEntry, outcome: CallOutcome): void {
154
+ if (entry.done) return;
155
+ entry.done = true;
156
+ this.running.delete(entry);
157
+ this.awaiting.delete(entry);
158
+ if (outcome === 'refused') this.counts.refused += 1;
159
+ else this.name(entry, 'unseen');
160
+ }
161
+
162
+ /**
163
+ * The script ended: every call still running may or may not take effect,
164
+ * and the record stays as it stands, so it names the calls as the host saw
165
+ * them when the script ended.
166
+ */
167
+ ended(): void {
168
+ for (const entry of this.running) {
169
+ entry.done = true;
170
+ this.name(entry, 'uncertain');
171
+ }
172
+ this.running.clear();
173
+ for (const entry of this.awaiting) entry.done = true;
174
+ this.awaiting.clear();
175
+ }
176
+
177
+ /** The record, with `waiting` calls the host holds back, for room or a wake's turn, which the script has made too. */
178
+ summary(waiting = 0): CallSummary {
179
+ return {
180
+ made: this.made + waiting,
181
+ ...this.counts,
182
+ running: this.running.size,
183
+ waiting,
184
+ named: this.named.map((c) => ({ ...c })),
185
+ more: Object.fromEntries(this.more),
186
+ };
187
+ }
188
+
189
+ /** A place among the named, or a count by tool past CALLS_NAMED. */
190
+ private name(entry: CallEntry, state: NamedCall['state']): NamedCall | null {
191
+ if (this.named.length < CALLS_NAMED) {
192
+ const named: NamedCall = { tool: entry.tool, args: entry.args, state };
193
+ this.named.push(named);
194
+ return named;
195
+ }
196
+ this.more.set(entry.tool, (this.more.get(entry.tool) ?? 0) + 1);
197
+ return null;
198
+ }
199
+ }
@@ -0,0 +1,5 @@
1
+ /** The script the parent spawns for each script: the child, on the engine. */
2
+ import { loadEngine } from '../engine.js';
3
+ import { childMain } from './child-main.js';
4
+
5
+ childMain(loadEngine);
@@ -0,0 +1,132 @@
1
+ /**
2
+ * The child's process: reads the parent's frames on stdin, writes its own on
3
+ * fd 3, runs the script with the engine, and exits. Spawned by the parent with
4
+ * the parent's own runtime (`process.execPath`), Node's or Bun's.
5
+ *
6
+ * The channel is the one way that works the same under both runtimes, as
7
+ * probes under Bun 1.4.3, Node 24.19.0 and Node 20.20.2 showed: a socket on
8
+ * another fd reads nothing under Bun, and a file stream on one leaves Node's
9
+ * exit waiting on a blocked read. The child writes with `writeSync`, since its slots run while
10
+ * the engine is mid-call, and output has to reach the parent while a guest
11
+ * computes without queuing in memory.
12
+ *
13
+ * `runChildProcess` takes the engine loader so a test can spawn a child whose
14
+ * engine is a stand-in; the entry the parent spawns in production passes the
15
+ * engine's binding.
16
+ */
17
+
18
+ import { writeSync } from 'node:fs';
19
+ import { performance } from 'node:perf_hooks';
20
+ import { EngineError, MAX_MEMORY_BYTES } from '../engine.js';
21
+ import { runChild, type ChildIo, type LoadEngine } from './child.js';
22
+ import { encodeFrame, FrameDecoder, RESULT_FRAME_OVERHEAD } from './frames.js';
23
+ import { MAX_CHILD_FRAME_BYTES, type ParentMessage, type StartMessage } from './protocol.js';
24
+
25
+ const TO_PARENT_FD = 3;
26
+
27
+ /**
28
+ * The largest frame the child takes from its parent: a result as large as the
29
+ * largest memory can be given (a third of it, as the parent bounds a result),
30
+ * with its header. The parent is trusted; this only bounds a broken one.
31
+ */
32
+ const MAX_PARENT_FRAME_BYTES = Math.floor(MAX_MEMORY_BYTES / 3) + RESULT_FRAME_OVERHEAD;
33
+
34
+ /** A shared buffer to wait on, so a full pipe is retried after a short sleep without the event loop. */
35
+ const sleeper = new Int32Array(new SharedArrayBuffer(4));
36
+
37
+ /** Write all of `buf` to `fd`, synchronously, waiting a moment when the pipe is full. */
38
+ function writeAll(fd: number, buf: Buffer): void {
39
+ let offset = 0;
40
+ while (offset < buf.length) {
41
+ try {
42
+ offset += writeSync(fd, buf, offset, buf.length - offset);
43
+ } catch (err) {
44
+ const code = (err as NodeJS.ErrnoException).code;
45
+ if (code !== 'EAGAIN' && code !== 'EWOULDBLOCK') throw err;
46
+ Atomics.wait(sleeper, 0, 0, 1);
47
+ }
48
+ }
49
+ }
50
+
51
+ /** The parent's frames, as messages, from the child's stdin. The parent's frames are trusted: a bad one ends the child. */
52
+ function parentFrames(): () => Promise<ParentMessage | null> {
53
+ const decoder = new FrameDecoder(MAX_PARENT_FRAME_BYTES, true);
54
+ const queue: ParentMessage[] = [];
55
+ let ended = false;
56
+ let failure: Error | null = null;
57
+ let wake: (() => void) | null = null;
58
+ const stream = process.stdin;
59
+ stream.on('data', (chunk) => {
60
+ try {
61
+ for (const value of decoder.push(chunk as Buffer)) queue.push(value as ParentMessage);
62
+ } catch (err) {
63
+ failure = err as Error;
64
+ }
65
+ wake?.();
66
+ });
67
+ const finish = () => {
68
+ ended = true;
69
+ wake?.();
70
+ };
71
+ stream.on('end', finish);
72
+ stream.on('error', (err) => {
73
+ failure = err;
74
+ finish();
75
+ });
76
+ return async () => {
77
+ for (;;) {
78
+ if (failure) throw failure;
79
+ const next = queue.shift();
80
+ if (next) return next;
81
+ if (ended) return null;
82
+ await new Promise<void>((resolve) => {
83
+ wake = () => {
84
+ wake = null;
85
+ resolve();
86
+ };
87
+ });
88
+ }
89
+ };
90
+ }
91
+
92
+ export async function runChildProcess(loadEngine: LoadEngine): Promise<number> {
93
+ const next = parentFrames();
94
+ const first = await next();
95
+ if (first === null) return 0;
96
+ if (first.t !== 'start') throw new Error('the first frame from the parent was not a start');
97
+ const start = first as StartMessage;
98
+ const io: ChildIo = {
99
+ next,
100
+ // Against the parent's cap, so the two can't drift apart.
101
+ send: (message) => writeAll(TO_PARENT_FD, encodeFrame(message, MAX_CHILD_FRAME_BYTES)),
102
+ now: () => performance.now(),
103
+ // On Linux and macOS an orphan is reparented, so its parent's pid isn't
104
+ // the one the start named once its parent has gone, even if that went
105
+ // before this ran. Windows keeps the pid, so an orphan isn't seen there.
106
+ parentGone: () => process.ppid !== start.parentPid,
107
+ orphaned: () => process.exit(0),
108
+ };
109
+ await runChild(io, start, loadEngine);
110
+ return 0;
111
+ }
112
+
113
+ /**
114
+ * The child's process as the parent spawns it: run the script, and exit. An
115
+ * engine that fails, rather than the script, ends the child with its reason
116
+ * on one line of stderr, which the parent puts in the crash it reports; any
117
+ * other throw is a fault here, and its stack goes there instead.
118
+ */
119
+ export function childMain(loadEngine: LoadEngine): void {
120
+ runChildProcess(loadEngine).then(
121
+ (code) => process.exit(code),
122
+ (err: unknown) => {
123
+ // Written at once: a write to a pipe is asynchronous on macOS, and the exit would drop it.
124
+ try {
125
+ writeSync(2, err instanceof EngineError ? `${err.message}\n` : `${err instanceof Error ? err.stack : String(err)}\n`);
126
+ } catch {
127
+ // The parent learns of the crash from the exit either way.
128
+ }
129
+ process.exit(1);
130
+ },
131
+ );
132
+ }
@@ -0,0 +1,327 @@
1
+ /**
2
+ * A script's child: one process per script, which runs it in the engine and
3
+ * answers the engine's four imports. It takes its start from the parent's
4
+ * first frame, sends each tool call to the parent and settles it with the
5
+ * parent's answer, flushes the output in bounded batches, and reports how the
6
+ * script ended. A crash it can't report: the parent sees that the child ended
7
+ * without saying how.
8
+ *
9
+ * Stopping: the parent creates the stop file and sends a stop frame. A
10
+ * computing guest's interrupt import sees the file, checked at most every few
11
+ * milliseconds, so a stop reaches a spinning guest without its event loop; an
12
+ * idle guest's child reads the frame. Why the stop was asked is the parent's
13
+ * to say, so the child reports only that the script stopped.
14
+ */
15
+
16
+ import { existsSync } from 'node:fs';
17
+ import { MAX_RESULT_DEPTH, type Engine, type EngineLimits, type EngineSlots, type RunState, type SettleState } from '../engine.js';
18
+ import { argumentsRefusal, MAX_CALLS_IN_FLIGHT, RESULT_TOO_LARGE, wakeLimitRefusal, wakeRefusal } from './protocol.js';
19
+ import { OutputBatch } from './output.js';
20
+ import { bounded, glimpse, NAME_CHARS, VALUE_CHARS } from './render.js';
21
+ import { preludeSource, scriptCall, scriptFrames, WAKE_CALL_NAME } from './prelude.js';
22
+ import type { ParentMessage, StartMessage } from './protocol.js';
23
+
24
+ /** How often, at most, the interrupt import looks for the stop file. */
25
+ export const STOP_CHECK_MS = 3;
26
+
27
+ /**
28
+ * The filename of the one line that starts the script, `await __run(...)`.
29
+ * The script's own frames carry `<input>`, the source name QuickJS gives the
30
+ * async function it's compiled into (SCRIPT_LINE_OFFSET).
31
+ */
32
+ const SCRIPT_FILENAME = 'script.js';
33
+
34
+ export type LoadEngine = (limits: EngineLimits, slots: EngineSlots) => Promise<Engine>;
35
+
36
+ export interface ChildIo {
37
+ /** The parent's frames, in order, each already decoded; done when the parent closes its end. */
38
+ next(): Promise<ParentMessage | null>;
39
+ /**
40
+ * Send one message now, synchronously: the engine may be mid-call. A message
41
+ * too long for a frame throws its FrameError, and nothing is sent.
42
+ */
43
+ send(message: unknown): void;
44
+ /** Milliseconds from some fixed point. */
45
+ now(): number;
46
+ /** Whether the parent has gone, so no one is left to stop the script or read its ending. */
47
+ parentGone?(): boolean;
48
+ /**
49
+ * End the child's process at once, its parent having gone: nothing it does
50
+ * reaches anyone, its unsent output included, and nothing is left to kill
51
+ * it if its guest keeps a stop from ending it. The child looks for its
52
+ * parent at the engine's interrupt checks, at most once a stop check
53
+ * (`STOP_CHECK_MS`), after a stop as before one. Without this, the child
54
+ * stops its guest instead.
55
+ */
56
+ orphaned?(): never;
57
+ }
58
+
59
+ /** The value's JSON from the engine's JSON of the prelude's text: a string, so one parse gives it, and anything else is given as it came. */
60
+ function valueJson(json: string): string {
61
+ try {
62
+ const text: unknown = JSON.parse(json);
63
+ return typeof text === 'string' ? text : json;
64
+ } catch {
65
+ return json;
66
+ }
67
+ }
68
+
69
+ /** Run the script `start` names to its end. Resolves once its ending is sent, or when the parent closes the channel. */
70
+ export async function runChild(io: ChildIo, start: StartMessage, loadEngine: LoadEngine): Promise<void> {
71
+ const batch = new OutputBatch();
72
+ /** The calls the guest waits on, by id: their tools' names. */
73
+ const outstanding = new Map<number, string>();
74
+ /** Calls answered here, without going out: their arguments were too large to send. */
75
+ const local: { id: number; json: string }[] = [];
76
+ /**
77
+ * While a result settles, the calls the guest's jobs make are held, and go
78
+ * out after that result's `seen`, which says whether the result fit: the
79
+ * parent then frees the answered call's place before it counts the next.
80
+ * A result the engine drops reaches no script code, and what code QuickJS
81
+ * still runs as the stop lands, a stack hook building the stop's error,
82
+ * can't make a call, since the engine refuses `__host` once stopped: its
83
+ * settle holds none.
84
+ */
85
+ let held: unknown[] | null = null;
86
+ /** Wakes sent, which the script's cap counts. */
87
+ let wakesSent = 0;
88
+ let stopped = false;
89
+ let lastFlush = io.now();
90
+ let lastStopCheck = -Infinity;
91
+ let lastParentCheck = -Infinity;
92
+ /** Its parent has been seen gone, so the child has ended its process, or stopped its guest. */
93
+ let orphan = false;
94
+
95
+ const flush = (): void => {
96
+ lastFlush = io.now();
97
+ for (const text of batch.flush()) io.send({ t: 'out', text });
98
+ };
99
+
100
+ const slots: EngineSlots = {
101
+ hostCall(callId, name, argsJson) {
102
+ outstanding.set(callId, name);
103
+ // What the host won't take is refused here, before a frame is built,
104
+ // and by the parent again, since the child is untrusted. A call that
105
+ // passes always fits a frame, which carries its arguments escaped, at
106
+ // most six bytes for each of theirs.
107
+ const wake = name === WAKE_CALL_NAME;
108
+ // A wake past the script's cap is refused too, after its size, so one
109
+ // refused for its size uses none of the script's wakes.
110
+ const refusal = wake
111
+ ? (wakeRefusal(argsJson) ?? (wakesSent >= start.maxWakes ? wakeLimitRefusal(start.maxWakes) : null))
112
+ : argumentsRefusal(name, argsJson);
113
+ if (refusal) {
114
+ // The slot can't settle the call from inside the engine: it's answered
115
+ // as soon as the engine returns.
116
+ local.push({ id: callId, json: JSON.stringify(refusal) });
117
+ return;
118
+ }
119
+ if (wake) wakesSent += 1;
120
+ // What the script wrote before the call reaches the host before it does.
121
+ if (batch.unflushed > 0) flush();
122
+ const call = { t: 'call', id: callId, name, args: argsJson };
123
+ // A wake takes no place among the calls in flight, so it needn't wait for a seen.
124
+ if (held === null || wake) io.send(call);
125
+ else if (held.length < MAX_CALLS_IN_FLIGHT) held.push(call);
126
+ else {
127
+ // A guest held to the prelude has at most that many calls out. One
128
+ // that makes more, its promises replaced, would have them held
129
+ // without bound: the rest go out at once, which the parent takes as
130
+ // the protocol break it is, and the guest is stopped.
131
+ stopped = true;
132
+ io.send(call);
133
+ }
134
+ },
135
+ interrupt() {
136
+ const t = io.now();
137
+ if (!stopped && t - lastStopCheck >= STOP_CHECK_MS) {
138
+ lastStopCheck = t;
139
+ stopped = existsSync(start.stopFile);
140
+ }
141
+ if (batch.unflushed > 0 && t - lastFlush >= start.flushMs) flush();
142
+ return stopped;
143
+ },
144
+ supervise() {
145
+ // A guest that computes doesn't let the child see its parent go, so
146
+ // that's checked at the engine's interrupt checks, at most once a stop
147
+ // check, a stop's included: a guest can keep a stop from ending it,
148
+ // since QuickJS drops a stop it takes inside a stack hook or an
149
+ // iterator's return, and only the parent kills a child that runs on.
150
+ // The engine calls this first, so an orphan ends its process at once
151
+ // rather than stop its guest. Output waiting to be sent goes with the
152
+ // process: the parent that would read it is gone, and a write to it
153
+ // could fail or block.
154
+ const t = io.now();
155
+ if (orphan || t - lastParentCheck < STOP_CHECK_MS) return;
156
+ lastParentCheck = t;
157
+ if (!io.parentGone?.()) return;
158
+ orphan = true;
159
+ io.orphaned?.();
160
+ stopped = true;
161
+ },
162
+ output(_stream, text) {
163
+ if (batch.write(text) || io.now() - lastFlush >= start.flushMs) flush();
164
+ },
165
+ };
166
+
167
+ const engine: Engine = await loadEngine(
168
+ { memoryMaxBytes: start.memoryMaxBytes, ...(start.maxStackBytes === undefined ? {} : { maxStackBytes: start.maxStackBytes }) },
169
+ slots,
170
+ );
171
+ // The loop's own failure, if one is leaving it: dispose()'s, after it, is the lesser news.
172
+ let failing = false;
173
+ try {
174
+ // The script's own ending: the first terminal state. Calls still in flight
175
+ // when it ends are waited for, so each outcome reaches the record, and their
176
+ // later states don't change how the script ended: a stop then ends the
177
+ // wait, and the script's own ending is what's reported. A failure once the
178
+ // script ran (`failed`: an exception escaped its promises) ends the run at
179
+ // once, past an ending it kept: the engine takes nothing more.
180
+ const terminal = (s: RunState) => s.kind === 'fulfilled' || s.kind === 'rejected';
181
+ let ending = { kind: 'pending' } as RunState;
182
+ let interrupted = false;
183
+ const after = (next: RunState): void => {
184
+ if (next.kind === 'interrupted') interrupted = true;
185
+ if (next.kind === 'failed' || !terminal(ending)) ending = next;
186
+ };
187
+ const over = (): boolean => interrupted || ending.kind === 'failed';
188
+ const settleLocal = (): void => {
189
+ while (local.length > 0 && !over()) {
190
+ const { id, json } = local.shift()!;
191
+ outstanding.delete(id);
192
+ after(engine.settle(id, false, json, json));
193
+ }
194
+ };
195
+ after(engine.start(preludeSource(start.tools), scriptCall(start.script), SCRIPT_FILENAME));
196
+ settleLocal();
197
+ for (;;) {
198
+ if (over() || stopped || outstanding.size === 0) {
199
+ flush();
200
+ // The value, or a failure's message and stack, cut as a return shows
201
+ // them, so the ending's frame always fits whatever the script made.
202
+ if (ending.kind === 'fulfilled') {
203
+ // The prelude's run resolves to its value's JSON text, which the engine
204
+ // gives as a JSON string: one layer off is the value's JSON.
205
+ io.send(ending.json === undefined ? { t: 'end', how: 'finished' } : { t: 'end', how: 'finished', value: bounded(valueJson(ending.json), VALUE_CHARS) });
206
+ } else if (ending.kind === 'rejected' || ending.kind === 'failed') {
207
+ // The script's frames go apart from the name and message, each cut
208
+ // on its own, so a cut never breaks which is which. Every failure's
209
+ // stack is QuickJS's own, or one the script set, and is put as the
210
+ // agent reads it here (scriptFrames), whether the script threw it or
211
+ // it escaped the script's promises, and cut as it's read: the frames
212
+ // go as scriptFrames gives them, cut once.
213
+ const frames = ending.stack ? scriptFrames(ending.stack, VALUE_CHARS) : '';
214
+ io.send({
215
+ t: 'end',
216
+ how: 'failed',
217
+ error: {
218
+ name: bounded(ending.name, NAME_CHARS),
219
+ message: bounded(ending.message, VALUE_CHARS),
220
+ ...(frames ? { stack: frames } : {}),
221
+ },
222
+ });
223
+ } else if (interrupted || stopped) {
224
+ io.send({ t: 'end', how: 'stopped' });
225
+ } else {
226
+ io.send({
227
+ t: 'end',
228
+ how: 'failed',
229
+ stalled: true,
230
+ error: {
231
+ name: 'Error',
232
+ message: 'The script is waiting on a promise that nothing can settle: no tool call is pending, and there are no timers here.',
233
+ },
234
+ });
235
+ }
236
+ return;
237
+ }
238
+ // The guest is idle until a call is answered: send its output so far.
239
+ flush();
240
+ const message = await io.next();
241
+ if (message === null) return; // the parent is gone, and with it anyone to tell
242
+ if (message.t === 'stop') {
243
+ stopped = true;
244
+ continue;
245
+ }
246
+ if (message.t !== 'result' || !outstanding.has(message.id)) continue;
247
+ const name = outstanding.get(message.id)!;
248
+ outstanding.delete(message.id);
249
+ // A result the guest can't take, too large for its memory, nested too
250
+ // deep or not parsed, reaches the script as ResultTooLarge, made here,
251
+ // and the parent hears it wasn't seen. An answer that isn't ok is an
252
+ // error the host gave in the result's place, such as a refusal.
253
+ // The call as the script wrote it: a wake is wake_agent.
254
+ const called = name === WAKE_CALL_NAME ? 'wake_agent' : name;
255
+ // An error the host gave in the result's place keeps its name, such as
256
+ // a refusal's, and its message's start: whether the call ran is in
257
+ // them, read here, outside the guest.
258
+ const unfit = message.ok
259
+ ? JSON.stringify({
260
+ name: RESULT_TOO_LARGE,
261
+ message: `${called}'s result, ${message.json.length} bytes, couldn't be received by this script: it was ${unfitWhy(true)}; ` +
262
+ 'the call took effect, and none of its result reached the script. Calling it again would do the call again, its effect included, and likely bring the same result, so if the call only reads, ask the tool for less if it can',
263
+ bytes: message.json.length,
264
+ })
265
+ : unfitAnswer(called, message.json);
266
+ held = [];
267
+ let state: SettleState;
268
+ let calls: unknown[];
269
+ try {
270
+ state = engine.settle(message.id, message.ok, message.json, unfit);
271
+ } finally {
272
+ calls = held;
273
+ held = null;
274
+ }
275
+ // A result that reached the script freed its call's place, as the
276
+ // prelude has it, and its seen says so. One that never reached it
277
+ // (`dropped`) has no seen: the script never had it, so the prelude
278
+ // never freed its place, and what code QuickJS still runs as the stop
279
+ // lands can't reach the host, since the engine refuses `__host` once
280
+ // stopped. The run's end counts the answer as one the script didn't
281
+ // read (Gladys-2592, room-203 #159239, #162129).
282
+ if (!state.dropped) io.send(state.unfit ? { t: 'seen', id: message.id, unfit: true } : { t: 'seen', id: message.id });
283
+ for (const call of calls) io.send(call);
284
+ after(state);
285
+ settleLocal();
286
+ }
287
+ } catch (err) {
288
+ failing = true;
289
+ throw err;
290
+ } finally {
291
+ try {
292
+ engine.dispose();
293
+ } catch (err) {
294
+ if (!failing) throw err;
295
+ }
296
+ }
297
+ }
298
+
299
+ /** An error the host gave in a result's place, which the guest couldn't take: its name and its message's start, read in the child. */
300
+ function unfitAnswer(tool: string, json: Uint8Array): string {
301
+ let name = RESULT_TOO_LARGE;
302
+ let message = '';
303
+ let parsed = false;
304
+ try {
305
+ const answer = JSON.parse(new TextDecoder().decode(json)) as { name?: unknown; message?: unknown };
306
+ parsed = true;
307
+ if (typeof answer.name === 'string' && answer.name) name = bounded(answer.name, NAME_CHARS);
308
+ if (typeof answer.message === 'string') message = glimpse(answer.message, UNFIT_GLIMPSE_CHARS);
309
+ } catch {
310
+ // Not readable here either: only its size and the tool are known.
311
+ }
312
+ return JSON.stringify({
313
+ name,
314
+ message: `${message ? `${message} ` : `${tool}'s answer `}(the host's answer, ${json.length} bytes, couldn't be received by this script whole: it was ${unfitWhy(!parsed)})`,
315
+ bytes: json.length,
316
+ });
317
+ }
318
+
319
+ /** Why the guest couldn't take an answer: what the engine refuses, less what the child has ruled out. */
320
+ function unfitWhy(couldBeJson: boolean): string {
321
+ return couldBeJson
322
+ ? `too large for its memory, nested deeper than ${MAX_RESULT_DEPTH} levels, or not valid JSON`
323
+ : `too large for its memory, or nested deeper than ${MAX_RESULT_DEPTH} levels`;
324
+ }
325
+
326
+ /** How much of an unfit answer's message reaches the script. */
327
+ const UNFIT_GLIMPSE_CHARS = 2000;