@animalabs/agent-framework 0.23.1 → 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 +1036 -440
  71. package/dist/src/framework.d.ts.map +1 -1
  72. package/dist/src/framework.js +4131 -2228
  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 +4507 -2406
  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
@@ -12,7 +12,9 @@
12
12
  * - StdioTransport — spawns `config.command` and speaks over stdin/stdout;
13
13
  * child stderr is surfaced as 'stderr' lines.
14
14
  * - WebSocketTransport — dials `config.url` (ws:// or wss://) with the `ws`
15
- * library, appending `?token=` when `config.token` is
15
+ * library, sending the credential as `?token=` (or
16
+ * as an Authorization header, `tokenTransport:
17
+ * 'header'`) when `config.token` is
16
18
  * set; each WS text frame is one JSON-RPC line.
17
19
  */
18
20
 
@@ -23,7 +25,7 @@ import { StringDecoder } from 'node:string_decoder';
23
25
  import WebSocket from 'ws';
24
26
 
25
27
  import type { McplServerConfig } from './types.js';
26
- import { describeCredentialError, noteDialSent, redactUrl, resolveServerBinding, urlAsDialed } from './protocol-family.js';
28
+ import { WITHHELD_TEXT, describeCredentialError, headerCarries, maskedError, noteDialSent, redactServerText, redactUrl, resolveServerBinding, serverTextRedactor, tokenTransportProblem, urlAsDialed, type Redaction } from './protocol-family.js';
27
29
  import { assertBooleanSetting } from '../config-checks.js';
28
30
  import { logDetail } from './log-value.js';
29
31
 
@@ -94,6 +96,21 @@ export abstract class McplTransport extends EventEmitter {
94
96
  return '';
95
97
  }
96
98
 
99
+ /**
100
+ * {@link stderrTail} as an error a host gets carries it: the same
101
+ * characters, redacted with `redact`. The stream's last
102
+ * {@link STDERR_WINDOW_CHARS} are redacted together and the tail is taken
103
+ * from where it begins in them, so a credential that begins before the
104
+ * tail and reaches into it is matched whole and replaced whole
105
+ * ({@link Redaction}'s `from`). That holds for a credential of up to
106
+ * 14,336 characters, the window less the tail: a longer one that began
107
+ * before the window can't be matched, and the tail keeps what it holds of
108
+ * it. '' for a transport that has no diagnostic stream.
109
+ */
110
+ stderrTailRedacted(_redact: Redaction): string {
111
+ return '';
112
+ }
113
+
97
114
  /**
98
115
  * The error a stdio child's stdin or stdout failed with, once one has. The
99
116
  * transport is ending the child then, and stays open until it exits, but
@@ -230,9 +247,19 @@ export function buildChildEnv(
230
247
  return merged;
231
248
  }
232
249
 
233
- /** The most of a stdio child's stderr a transport keeps for {@link McplTransport.stderrTail}. */
250
+ /** The most of a stdio child's stderr that {@link McplTransport.stderrTail} gives. */
234
251
  export const STDERR_TAIL_CHARS = 2_048;
235
252
 
253
+ /** The most of a stdio child's stderr a transport keeps, for
254
+ * {@link McplTransport.stderrTail} and
255
+ * {@link McplTransport.stderrTailRedacted}: eight tails, so a credential of
256
+ * up to 14,336 characters that reaches into the tail, a private key's PEM
257
+ * of 8,192 bits (about 6,500 characters) included, is whole in it. */
258
+ export const STDERR_WINDOW_CHARS = 8 * STDERR_TAIL_CHARS;
259
+
260
+ /** A redaction that replaces nothing: the raw tail. */
261
+ const unredacted: Redaction = (text, from = 0) => String(text).slice(from);
262
+
236
263
  /**
237
264
  * How long a refusal at a stdio child's exit waits for the child's stderr to
238
265
  * end, so that its `stderrTail` holds what the child wrote. Node can emit a
@@ -251,13 +278,28 @@ export class StdioTransport extends McplTransport {
251
278
  readonly kind = 'stdio' as const;
252
279
  /** The first error on the child's stdin or stdout, named in the close reason. */
253
280
  private pipeError: Error | undefined;
254
- private stderrEnd = '';
255
- private stderrCut = false;
281
+ /** The stream's last STDERR_WINDOW_CHARS, raw. */
282
+ private stderrWindow = '';
256
283
  /** True once the child's stderr has ended or closed. */
257
284
  private stderrDone = false;
258
285
 
259
286
  override get stderrTail(): string {
260
- return (this.stderrCut ? '…' : '') + this.stderrEnd;
287
+ return this.stderrTailRedacted(unredacted);
288
+ }
289
+
290
+ override stderrTailRedacted(redact: Redaction): string {
291
+ // A child that wrote nothing has no tail, whatever its redaction.
292
+ if (this.stderrWindow === '') return '';
293
+ // The tail begins at its raw place in the window, so a redaction that
294
+ // shortens the window can't move its edge back toward the window's own
295
+ // cut. Something before the tail was dropped whenever the window is
296
+ // longer than the tail, which it always is once the window was cut.
297
+ const from = Math.max(0, this.stderrWindow.length - STDERR_TAIL_CHARS);
298
+ const cut = from > 0;
299
+ let kept = redact(this.stderrWindow, from);
300
+ // A cut between a surrogate pair's halves drops the low half too.
301
+ if (cut && /^[\uDC00-\uDFFF]/.test(kept)) kept = kept.slice(1);
302
+ return (cut ? '…' : '') + kept;
261
303
  }
262
304
 
263
305
  override get pipeFailure(): Error | undefined {
@@ -300,12 +342,8 @@ export class StdioTransport extends McplTransport {
300
342
  let carry = '';
301
343
  const decoder = new StringDecoder('utf8');
302
344
  const received = (text: string) => {
303
- const end = this.stderrEnd + text;
304
- this.stderrCut ||= end.length > STDERR_TAIL_CHARS;
305
- let kept = end.slice(-STDERR_TAIL_CHARS);
306
- // A cut between a surrogate pair's halves drops the low half too.
307
- if (/^[\uDC00-\uDFFF]/.test(kept) && end.length > STDERR_TAIL_CHARS) kept = kept.slice(1);
308
- this.stderrEnd = kept;
345
+ const window = this.stderrWindow + text;
346
+ this.stderrWindow = window.slice(-STDERR_WINDOW_CHARS);
309
347
  const lines = (carry + text).split('\n');
310
348
  carry = lines.pop() ?? '';
311
349
  for (const l of lines) if (l.length > 0) this.pushStderr(l);
@@ -374,10 +412,33 @@ export class StdioTransport extends McplTransport {
374
412
  `MCPL server "${config.id}": stdio transport requires "command" (none provided)`,
375
413
  );
376
414
  }
377
- const child = spawn(config.command, config.args ?? [], {
378
- stdio: ['pipe', 'pipe', 'pipe'],
379
- env: buildChildEnv(config),
380
- });
415
+ // Node's error for a launch that failed reaches the host, and its words
416
+ // and its other properties (`spawnargs`, `path`) can repeat a credential
417
+ // the configuration holds. So it's handed on as its name and redacted
418
+ // words (maskedError), with its code, which says what failed. What
419
+ // spawn() refuses at once is one of the configuration's own arguments or
420
+ // options, which Node quotes after " Received", cut to 128 characters: a
421
+ // credential across that cut couldn't be matched, so the words stop
422
+ // before the quote, still saying which value Node refused and why.
423
+ const failed = (err: NodeJS.ErrnoException, refusedAtOnce: boolean): Error => {
424
+ const redact = serverTextRedactor(config).now();
425
+ const message = err instanceof Error ? err.message : String(err);
426
+ const words = refusedAtOnce ? message.split(' Received')[0] : message;
427
+ const shown = Object.assign(new Error(words), { name: err instanceof Error ? err.name : 'Error' });
428
+ const cause = Object.assign(maskedError(shown, redact), err.code === undefined ? {} : { code: err.code });
429
+ return new Error(`MCPL server "${config.id}" could not be started: ${redact(words)}`, { cause });
430
+ };
431
+ // The child's environment is read first, outside that: a configuration
432
+ // whose `env` can't be read throws the framework's own error, such as
433
+ // its awareness ledger's, which the framework acts on: its startup ends
434
+ // on it, and a reconnect reports it.
435
+ const env = buildChildEnv(config);
436
+ let child: ChildProcess;
437
+ try {
438
+ child = spawn(config.command, config.args ?? [], { stdio: ['pipe', 'pipe', 'pipe'], env });
439
+ } catch (error) {
440
+ throw failed(error as NodeJS.ErrnoException, true);
441
+ }
381
442
  // Node reports a failed spawn (a command that doesn't exist, say) as an
382
443
  // 'error' on a tick after spawn() returns, and no 'exit' follows. It is
383
444
  // settled here, where it is listened for. A caller that listens only once
@@ -385,12 +446,10 @@ export class StdioTransport extends McplTransport {
385
446
  // from a timer callback, as a reconnect is: Node runs that tick before the
386
447
  // caller's promise continuation, and throws an 'error' nothing listens for.
387
448
  await new Promise<void>((resolve, reject) => {
388
- const failed = (err: Error) => {
389
- reject(new Error(`MCPL server "${config.id}" could not be started: ${err.message}`, { cause: err }));
390
- };
391
- child.once('error', failed);
449
+ const fail = (err: NodeJS.ErrnoException) => reject(failed(err, false));
450
+ child.once('error', fail);
392
451
  child.once('spawn', () => {
393
- child.off('error', failed);
452
+ child.off('error', fail);
394
453
  resolve();
395
454
  });
396
455
  });
@@ -504,7 +563,8 @@ function raceAbort<T>(work: Promise<T>, signal: AbortSignal): Promise<T> {
504
563
  export class WebSocketTransport extends McplTransport {
505
564
  readonly kind = 'websocket' as const;
506
565
 
507
- private constructor(private readonly ws: WebSocket, serverId: string) {
566
+ private constructor(private readonly ws: WebSocket, config: McplServerConfig, dialed: string) {
567
+ const serverId = config.id;
508
568
  super(serverId);
509
569
 
510
570
  this.ws.on('message', (data: WebSocket.RawData, isBinary: boolean) => {
@@ -517,7 +577,9 @@ export class WebSocketTransport extends McplTransport {
517
577
  }
518
578
  });
519
579
 
520
- this.ws.on('error', (err: Error) => this.reportError(err));
580
+ // The client's text can quote the url as dialed (Bun's does): reported
581
+ // masked, as a new error with no cause (clientErrorText).
582
+ this.ws.on('error', (err: Error) => this.reportError(new Error(clientErrorText(err, config, dialed))));
521
583
  this.ws.on('close', (code: number, reason: Buffer) => {
522
584
  const text = reason?.toString('utf8');
523
585
  this.markClosed(text ? { code, reason: text, reasonFromPeer: true } : { code, reason: 'websocket closed' });
@@ -525,8 +587,11 @@ export class WebSocketTransport extends McplTransport {
525
587
  }
526
588
 
527
589
  /**
528
- * Dial `config.url` and resolve once the socket is OPEN. Appends `?token=`
529
- * when `config.token` is set. Rejects on connect error / timeout (and tears
590
+ * Dial `config.url` and resolve once the socket is OPEN. Sends the
591
+ * credential (`config.token`, or the access provider's) as a `?token=`
592
+ * query parameter, or with `tokenTransport: 'header'` as an
593
+ * `Authorization: Bearer` header on the upgrade, refusing before the dial
594
+ * one a header can't carry as configured. Rejects on connect error / timeout (and tears
530
595
  * the socket down) so a failed dial never leaks an open handle. `signal`
531
596
  * ends a dial whose connection is closed meanwhile, at once, rather than
532
597
  * at the open timeout.
@@ -557,14 +622,32 @@ export class WebSocketTransport extends McplTransport {
557
622
  }
558
623
  }
559
624
  const url = buildWebSocketUrl(config, token);
560
- // A provider's token is in this url, and the query may be re-encoded:
561
- // the server's text is recorded with what the dial sent, the token in
562
- // each form a token is replaced in.
625
+ // A provider's token is in this url when the dial sends it in the query,
626
+ // which may then be written again, and in the header alone in header
627
+ // mode: the server's text is recorded with what the dial sent, the token
628
+ // in each form a token is replaced in.
563
629
  noteDialSent(config, { url, token: token ?? undefined });
564
630
  // Error strings travel: traces, logs, tool results the model reads. They
565
631
  // get the endpoint, never the credential.
566
632
  const redacted = redactUrl(config.url);
567
- const ws = new WebSocket(url);
633
+ // In header mode the url carries no credential (buildWebSocketUrl): the
634
+ // upgrade request does, if a header can carry it as configured
635
+ // (headerCarries). A static token is refused earlier, by
636
+ // serverConfigProblems; this refuses an access provider's, and a static
637
+ // one dialed past that check. A client that throws as the socket is
638
+ // constructed can quote the url, so its text is masked (clientErrorText).
639
+ const headerMode = config.tokenTransport === 'header' && !!token;
640
+ if (headerMode && token && !headerCarries(token)) {
641
+ throw new Error(`MCPL server "${config.id}" WebSocket connect failed: its credential isn't printable ASCII with no space at either end, which is all a header carries as configured (${redacted})`);
642
+ }
643
+ let ws: WebSocket;
644
+ try {
645
+ ws = headerMode
646
+ ? new WebSocket(url, { headers: { Authorization: `Bearer ${token}` } })
647
+ : new WebSocket(url);
648
+ } catch (err) {
649
+ throw new Error(`MCPL server "${config.id}" WebSocket connect failed: ${clientErrorText(err, config, url)} (${redacted})`);
650
+ }
568
651
 
569
652
  // A failed dial can emit 'error' MORE THAN ONCE on the raw socket (seen
570
653
  // live 2026-08-03: an HTTP 502 upgrade-reject emits the "Expected 101"
@@ -602,11 +685,11 @@ export class WebSocketTransport extends McplTransport {
602
685
  clearTimeout(timer);
603
686
  signal?.removeEventListener('abort', onAbort);
604
687
  ws.removeAllListeners('error'); // the instance re-attaches its own handlers
605
- resolve(new WebSocketTransport(ws, config.id));
688
+ resolve(new WebSocketTransport(ws, config, url));
606
689
  });
607
690
 
608
691
  ws.once('error', (err: Error) => {
609
- abandon(new Error(`MCPL server "${config.id}" WebSocket connect failed: ${err.message} (${redacted})`));
692
+ abandon(new Error(`MCPL server "${config.id}" WebSocket connect failed: ${clientErrorText(err, config, url)} (${redacted})`));
610
693
  });
611
694
  });
612
695
  }
@@ -635,10 +718,53 @@ export class WebSocketTransport extends McplTransport {
635
718
  }
636
719
  }
637
720
 
721
+ /** A ws:// or wss:// url in a WebSocket client's text, to the next space or
722
+ * quote: brackets (an IPv6 host) and parentheses can be part of one. */
723
+ const WS_URL_IN_TEXT = /\bwss?:\/\/[^\s"'<>`]+/gi;
724
+
725
+ /**
726
+ * A WebSocket client's error text (an Error under Node's ws, an ErrorEvent
727
+ * under Bun's), as the transport's own errors carry it: a failed dial's, and
728
+ * one after the socket opened. A client can quote the url as dialed: under
729
+ * Bun 1.3.13 and 1.4.3 a refused or unreachable dial's message holds it
730
+ * whole, its user, password, path and query, and the token, a static one or
731
+ * an access provider's. In order, so a credential is replaced while it's
732
+ * whole: the url as dialed and as configured, wherever they occur, are
733
+ * shown as {@link redactUrl} shows them; then the strings the configuration
734
+ * and its dials know to be credentials (the dial has noted what it sent by
735
+ * now) are replaced, in their encoded and decoded forms
736
+ * ({@link redactServerText}); then any other ws(s) url left is shown as
737
+ * {@link redactUrl} shows it. The caller makes a new error of it with no
738
+ * `cause`, so the client's own error, message and stack, isn't reachable.
739
+ * A configuration that can't be read withholds the text
740
+ * ({@link WITHHELD_TEXT}) rather than throw: this runs in a socket's
741
+ * 'error' listener, where a throw has nowhere to go.
742
+ */
743
+ export function clientErrorText(err: unknown, config: McplServerConfig, dialed: string): string {
744
+ // Bun's ws emits an ErrorEvent, not an Error, its text in `message` too.
745
+ try {
746
+ const said = (err as { message?: unknown } | null)?.message;
747
+ let text = typeof said === 'string' ? said : String(err);
748
+ const configured = config.url;
749
+ const shown = redactUrl(configured);
750
+ for (const url of [dialed, configured]) {
751
+ if (url) text = text.split(url).join(shown);
752
+ }
753
+ text = redactServerText(text, config);
754
+ return text.replace(WS_URL_IN_TEXT, (found) => redactUrl(found));
755
+ } catch {
756
+ return WITHHELD_TEXT;
757
+ }
758
+ }
759
+
638
760
  /**
639
761
  * Build the dial URL: validate it's a ws(s):// URL the ws library will dial,
640
762
  * which refuses one with a fragment, and append the auth token as a `token`
641
- * query param when provided (preserving any existing query).
763
+ * query param when provided (preserving any existing query). With
764
+ * `tokenTransport: 'header'` no token is appended: the dial sends it as a
765
+ * header, and a url that carries a `token` parameter of its own is refused,
766
+ * as is a `tokenTransport` that is neither mode, so a dial that skipped
767
+ * serverConfigProblems still can't fall back to the query.
642
768
  */
643
769
  export function buildWebSocketUrl(config: McplServerConfig, tokenOverride?: string | null): string {
644
770
  if (!config.url) {
@@ -659,7 +785,10 @@ export function buildWebSocketUrl(config: McplServerConfig, tokenOverride?: stri
659
785
  if (parsed.hash) {
660
786
  throw new Error(`MCPL server "${config.id}": websocket url must not have a fragment, got "${parsed.hash}"`);
661
787
  }
662
- const token = tokenOverride !== undefined ? tokenOverride : config.token;
788
+ const problem = tokenTransportProblem(config, parsed);
789
+ if (problem) throw new Error(problem);
790
+ const token = config.tokenTransport === 'header' ? null
791
+ : tokenOverride !== undefined ? tokenOverride : config.token;
663
792
  return urlAsDialed(parsed, token).toString();
664
793
  }
665
794
 
package/src/mcpl/types.ts CHANGED
@@ -285,12 +285,28 @@ export interface McplServerConfig {
285
285
  /**
286
286
  * Bearer token for a network server. Where it travels depends on the
287
287
  * transport: on a ws(s):// url it is appended as a `token` query parameter
288
- * (`?token=…`) on connect; to an http(s):// server it is sent as an
289
- * `Authorization: Bearer` header and never put in the url. A legacy stdio
290
- * server ignores it, and a modern stdio server refuses it.
288
+ * (`?token=…`) on connect, unless `tokenTransport` is `'header'`; to an
289
+ * http(s):// server it is sent as an `Authorization: Bearer` header and
290
+ * never put in the url. A legacy stdio server ignores it, and a modern
291
+ * stdio server refuses it.
291
292
  */
292
293
  token?: string;
293
294
 
295
+ /**
296
+ * How a ws(s):// server's credential, `token` or what `accessProvider`
297
+ * returns for the dial, is sent on the WebSocket upgrade. `'query'`, the
298
+ * default: as a `token` query parameter (`?token=…`). `'header'`: as an
299
+ * `Authorization: Bearer …` header, with no query parameter, so the
300
+ * credential stays out of the url and of the access logs that record it.
301
+ * A header carries a credential as configured only if it is printable
302
+ * ASCII (U+0020 to U+007E) with no space at either end, so header mode
303
+ * refuses any other: a static `token` when the server is added, an access
304
+ * provider's at the dial. Set only on a WebSocket server; an http(s)://
305
+ * server always sends the header, and a stdio server sends no credential.
306
+ * `null` counts as not set.
307
+ */
308
+ tokenTransport?: 'query' | 'header';
309
+
294
310
  /**
295
311
  * Lazily resolves the connection credential at EVERY dial — first connect
296
312
  * and each background reconnect — overriding `token` when it returns a
@@ -414,9 +430,10 @@ export interface McplServerConfig {
414
430
  * are also the only bound for one that closes its stdin, since Bun emits no
415
431
  * 'error' event for the writes to it. requestTimeoutMs is the timeout of
416
432
  * every request that names none; the handshake's initialize has its own.
417
- * Set 0 to disable, except for channels/publish and the channels/open a
418
- * delivery may send first, which keep the 60 s default: a publish's outcome
419
- * is reported to the resident and recorded for its draft, so it must end.
433
+ * Set 0 to disable, except for channels/publish, channels/open and
434
+ * channels/close, which keep the 60 s default: a publish's outcome is
435
+ * reported to the resident and recorded for its draft, so it must end, and
436
+ * a delivery waits for a channel's queued opens and closes.
420
437
  * Default: 60000 (60 seconds). A value above 2^31−1 is refused: Node runs a
421
438
  * longer timer after about 1 ms, so every request would time out at once.
422
439
  *
@@ -310,7 +310,8 @@ export class ModuleRegistry {
310
310
  * Handle a tool call by routing to the appropriate module: the module's own
311
311
  * result, unchecked, as it returned it, for a caller that uses the result
312
312
  * in process (`ModuleContext.callTool`, `executeToolCall`, or a host
313
- * calling the registry). A module's throw is answered as an error result.
313
+ * calling the registry), less a refusal it claims (withoutRefusal). A
314
+ * module's throw is answered as an error result.
314
315
  * A call whose input isn't an object, or lacks a key the tool's schema
315
316
  * requires, is answered before the module runs, with an error result that
316
317
  * opens "Nothing was done:" and names what to send (toolArgsProblems).
@@ -324,7 +325,7 @@ export class ModuleRegistry {
324
325
  if (sepIndex === -1) {
325
326
  return {
326
327
  success: false,
327
- error: `Invalid tool name format: ${call.name}`,
328
+ error: `Invalid tool name format: ${call.name}, so nothing was done`,
328
329
  isError: true,
329
330
  };
330
331
  }
@@ -336,19 +337,14 @@ export class ModuleRegistry {
336
337
  if (!module) {
337
338
  return {
338
339
  success: false,
339
- error: `Module not found: ${moduleName}`,
340
+ error: `Module not found: ${moduleName}, so nothing was done`,
340
341
  isError: true,
341
342
  };
342
343
  }
343
344
 
344
- // Create a call with the un-prefixed name (preserving caller identity and turn)
345
- const moduleCall: ToolCall = {
346
- id: call.id,
347
- name: toolName,
348
- input: call.input,
349
- callerAgentName: call.callerAgentName,
350
- ...(call.callerTurnId !== undefined ? { callerTurnId: call.callerTurnId } : {}),
351
- };
345
+ // The call as it came, with the un-prefixed name: the caller's identity
346
+ // and everything else the dispatch set on it reach the module.
347
+ const moduleCall: ToolCall = { ...call, name: toolName };
352
348
 
353
349
  try {
354
350
  // The call against the tool's declared schema, before the module runs:
@@ -372,7 +368,9 @@ export class ModuleRegistry {
372
368
  isError: true,
373
369
  };
374
370
  }
375
- return await module.handleToolCall(moduleCall);
371
+ // A refusal is the dispatcher's to say (RFC-007), so a module's result
372
+ // can't claim one on this route either.
373
+ return withoutRefusal(call, await module.handleToolCall(moduleCall));
376
374
  } catch (error) {
377
375
  return thrownResult(error);
378
376
  }
@@ -900,6 +898,83 @@ class ProcessStateImpl implements ProcessState {
900
898
  }
901
899
  }
902
900
 
901
+ /** The fields ToolResult declares, read by name, so a result class's getters count: endTurn first and the error next, so a failure keeps them. */
902
+ const TOOL_RESULT_FIELDS = ['endTurn', 'error', 'success', 'isError', 'data', 'structured', 'keepWhenWithheld'] as const;
903
+
904
+ /**
905
+ * A module's result for an in-process caller: the module's own object, unless
906
+ * it claims a refusal, which is the dispatcher's to say (RFC-007). Then it's a
907
+ * copy, less the claim: a plain object of every field ToolResult declares,
908
+ * read by name, so a result class's getters count, each read once, and every
909
+ * other own enumerable field, string or symbol, as a spread takes them, so
910
+ * non-enumerable own fields and inherited undeclared ones are left out. A
911
+ * result that can't be read, its claim included, is a failure saying so, as
912
+ * on the stored route (checkedToolResult),
913
+ * keeping the turn's end and the module's error if those could be read: the
914
+ * module's call completed, and the claim never reaches the caller.
915
+ */
916
+ function withoutRefusal(call: ToolCall, result: ToolResult): ToolResult {
917
+ // Not an object: nothing to claim, and the caller gets it as it came.
918
+ if (!result || (typeof result !== 'object' && typeof result !== 'function')) return result;
919
+ const source = result as unknown as Record<PropertyKey, unknown>;
920
+ const copy: Record<PropertyKey, unknown> = {};
921
+ // Each field is read once: one whose getter threw isn't asked again.
922
+ const read = new Set<PropertyKey>();
923
+ // Defined, not assigned, so a field named __proto__ is a field.
924
+ const keep = (key: PropertyKey): void => {
925
+ read.add(key);
926
+ Object.defineProperty(copy, key, { value: source[key], enumerable: true, writable: true, configurable: true });
927
+ };
928
+ const once = (key: string): unknown => {
929
+ if (key in copy) return copy[key];
930
+ if (read.has(key)) return undefined;
931
+ return source[key];
932
+ };
933
+ // A failure keeps what could be read of the turn's end and the module's
934
+ // error, each read on its own, whatever else couldn't be.
935
+ const unreadable = (error: unknown): ToolResult => {
936
+ let endTurn: unknown;
937
+ let own: string | undefined;
938
+ try {
939
+ endTurn = once('endTurn');
940
+ } catch {
941
+ // Not readable: the failure doesn't end the turn.
942
+ }
943
+ try {
944
+ own = errorText(once('error'));
945
+ } catch {
946
+ // An error whose own text can't be read: the failure says only why.
947
+ }
948
+ return {
949
+ success: false,
950
+ isError: true,
951
+ error: `Tool '${call.name}' returned a result that couldn't be read (${thrownText(error).split('\n')[0] || 'no reason given'}). ` +
952
+ `The call itself completed${own ? `, with this error: ${own}` : '.'}`,
953
+ ...(endTurn ? { endTurn: true } : {}),
954
+ };
955
+ };
956
+ let claim: unknown;
957
+ try {
958
+ claim = source.refused;
959
+ } catch (error) {
960
+ return unreadable(error);
961
+ }
962
+ if (!claim) return result;
963
+ try {
964
+ const copied = new Set<PropertyKey>(['refused']);
965
+ for (const key of TOOL_RESULT_FIELDS) {
966
+ copied.add(key);
967
+ if (key in source) keep(key);
968
+ }
969
+ for (const key of Reflect.ownKeys(source)) {
970
+ if (!copied.has(key) && Object.prototype.propertyIsEnumerable.call(source, key)) keep(key);
971
+ }
972
+ return copy as unknown as ToolResult;
973
+ } catch (error) {
974
+ return unreadable(error);
975
+ }
976
+ }
977
+
903
978
  /** A result's flags, which consumers read for their truth: booleans here. */
904
979
  const RESULT_FLAGS = ['success', 'isError', 'endTurn'] as const;
905
980
 
@@ -921,7 +996,8 @@ const RESULT_FLAGS = ['success', 'isError', 'endTurn'] as const;
921
996
  * runs again. It's taken where the framework stores or sends a result
922
997
  * ({@link ModuleRegistry.storableToolCall}). A caller that uses the result
923
998
  * in process (`ModuleContext.callTool`, `executeToolCall`) gets the module's
924
- * own result, as the module returned it ({@link ModuleRegistry.handleToolCall}).
999
+ * own result, as the module returned it, less a refusal it claims
1000
+ * ({@link ModuleRegistry.handleToolCall}).
925
1001
  * The flags are booleans, read for their truth as consumers read them.
926
1002
  * `error` is its text form, which is all a consumer reads of it, so an error
927
1003
  * value JSON can't hold costs nothing. An error that says nothing (null,
@@ -977,6 +1053,9 @@ export function checkedToolResult(call: ToolCall, result: unknown): { result: To
977
1053
  }
978
1054
  // The error is kept as its text, which consumers read; its value isn't.
979
1055
  delete fields.error;
1056
+ // A refusal is the dispatcher's to say, before the call opens (RFC-007):
1057
+ // a module answers after that, so its result can't claim one.
1058
+ delete fields.refused;
980
1059
  // The flags are kept as their truth, which is all consumers read of them,
981
1060
  // before anything is serialized, so a flag's own value (a BigInt, a
982
1061
  // function, an object with a toJSON) neither breaks nor runs anything.
@@ -8,7 +8,7 @@
8
8
  */
9
9
 
10
10
  import type { ContentBlock } from '@animalabs/membrane';
11
- import type { StoredMessage } from '@animalabs/context-manager';
11
+ import { continuesBody, type StoredMessage } from '@animalabs/context-manager';
12
12
  import type {
13
13
  Module,
14
14
  ModuleContext,
@@ -322,19 +322,14 @@ function withoutGoneAttachments(content: ContentBlock[], attachments: readonly D
322
322
  return textFirst(first.text, kept);
323
323
  }
324
324
 
325
- /** The ids to remove for a message's rows: each row once, and a body stored
326
- * in shards once, since removing one shard takes out its whole body. */
325
+ /** The ids to remove for a message's rows, in store order: each row once,
326
+ * and a body stored in shards once, by its first shard, since removing one
327
+ * shard takes out its whole body. Which rows make up a body is
328
+ * context-manager's rule (continuesBody), read pairwise: a group id names
329
+ * the text, so a message stored twice in shards is two bodies under one id,
330
+ * and each is removed. */
327
331
  function oncePerBody(rows: readonly StoredMessage[]): string[] {
328
- const bodies = new Set<string>();
329
- const ids: string[] = [];
330
- for (const row of rows) {
331
- if (row.bodyGroupId) {
332
- if (bodies.has(row.bodyGroupId)) continue;
333
- bodies.add(row.bodyGroupId);
334
- }
335
- ids.push(row.id);
336
- }
337
- return ids;
332
+ return rows.filter((row, i) => !continuesBody(rows[i - 1], row)).map((row) => row.id);
338
333
  }
339
334
 
340
335
  /** A live create on its way to the store (DiscordModule.creating). */
@@ -49,7 +49,7 @@ import type { ToolDefinition, ToolCall, ToolResult, ProcessEvent } from '../../t
49
49
  import type { EventResponse, ProcessState } from '../../types/module.js';
50
50
  import type { SearchWorkerMessage, SearchWorkerMatch } from './search-regex-worker.js';
51
51
  import type { ChannelRegistry } from '../../mcpl/channel-registry.js';
52
- import { EmbedServiceError, SemanticIndexClient, SemanticIndexer, messageIndexText, type SearchResponse, type SemanticIndexConfig, type SyncReport, type PendingChanges } from './semantic.js';
52
+ import { EmbedServiceError, SemanticIndexClient, SemanticIndexer, messageIndexText, summaryIndexText, type SearchResponse, type SemanticIndexConfig, type SyncReport, type PendingChanges } from './semantic.js';
53
53
  import { unknownToolMessage } from '../unknown-tool.js';
54
54
  import { safeSlice } from '../../safe-slice.js';
55
55
 
@@ -1614,6 +1614,9 @@ export class HistoryModule implements Module {
1614
1614
  try { sync = await this.indexer.catchUp(this.semanticCfg.maxSyncBeforeSearch ?? 256); } catch { /* reported via indexer.lastError */ }
1615
1615
  }
1616
1616
 
1617
+ // A message's or summary's snippet is built here, from the local text.
1618
+ // `snippet` still bounds what a service that keeps text sends with each
1619
+ // hit, since what it sends without one isn't specified here.
1617
1620
  const runSearch = (k: number) => this.semanticClient!.search({
1618
1621
  query: input.query, k,
1619
1622
  ts_from: fromMs === undefined ? undefined : fromMs / 1000,
@@ -1667,7 +1670,10 @@ export class HistoryModule implements Module {
1667
1670
  // /newtopic keeps its id (a new message gets a fresh one) and stays
1668
1671
  // indexed. Check every hit against the CURRENT branch before it goes back
1669
1672
  // to the model — getMessage/getSummary rebuild on branch switch, so an
1670
- // undone message or a summary minted on another branch answers null.
1673
+ // undone message, or a summary this branch doesn't hold, answers null.
1674
+ // A branch made from an earlier point, as an undo makes one, can mint a
1675
+ // summary id another branch already used, so a summary hit's score,
1676
+ // timestamp and date filtering can still be that other summary's.
1671
1677
  // k is small, so this is at most `limit` local lookups.
1672
1678
  // A message hit whose current text has nothing left to index (edited
1673
1679
  // blank, delete not yet applied) is dropped too, so its stale snippet is
@@ -1679,6 +1685,10 @@ export class HistoryModule implements Module {
1679
1685
  // A message hit's snippet is rebuilt from the CURRENT local text, never
1680
1686
  // the remote copy: an edit not yet synced (or one the index never got)
1681
1687
  // must not leak a passage the edit removed. Only the ranking can be stale.
1688
+ // A summary hit's snippet is rebuilt from the local summary too: a
1689
+ // service that keeps only vectors and ids (no text) still yields one, and
1690
+ // a summary whose id another branch also minted shows this branch's text
1691
+ // in its snippet and chars.
1682
1692
  const currentText = new Map<string, string>();
1683
1693
  const keep = (h: { id: string }): 'ok' | 'offBranch' | 'stale' => {
1684
1694
  const msgId = /^msg:(.+)$/.exec(h.id)?.[1];
@@ -1691,7 +1701,19 @@ export class HistoryModule implements Module {
1691
1701
  currentText.set(h.id, text);
1692
1702
  return 'ok';
1693
1703
  }
1694
- if (sumId !== undefined) return cm.getSummary(sumId) !== null ? 'ok' : 'offBranch';
1704
+ if (sumId !== undefined) {
1705
+ const s = cm.getSummary(sumId);
1706
+ if (!s) return 'offBranch';
1707
+ const text = summaryIndexText(s, this.semanticCfg!);
1708
+ // An empty text here is mostly defensive: context-manager drops a
1709
+ // summary that's empty after trimming when it loads, and refuses to
1710
+ // store one. Short of that, only a maxChars that cuts the text to
1711
+ // nothing gives one. The hit is dropped rather than show the index's
1712
+ // text.
1713
+ if (!text) return 'stale';
1714
+ currentText.set(h.id, text);
1715
+ return 'ok';
1716
+ }
1695
1717
  return 'ok';
1696
1718
  };
1697
1719
  let k = limit;