@animalabs/agent-framework 0.26.2 → 0.28.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 (371) hide show
  1. package/README.md +99 -8
  2. package/dist/src/agent.d.ts +291 -23
  3. package/dist/src/agent.d.ts.map +1 -1
  4. package/dist/src/agent.js +596 -61
  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 +5 -4
  8. package/dist/src/api/server.js.map +1 -1
  9. package/dist/src/auto-continue.d.ts +34 -0
  10. package/dist/src/auto-continue.d.ts.map +1 -0
  11. package/dist/src/auto-continue.js +36 -0
  12. package/dist/src/auto-continue.js.map +1 -0
  13. package/dist/src/code-execution/isolate/scripts.d.ts +11 -0
  14. package/dist/src/code-execution/isolate/scripts.d.ts.map +1 -1
  15. package/dist/src/code-execution/isolate/scripts.js +5 -1
  16. package/dist/src/code-execution/isolate/scripts.js.map +1 -1
  17. package/dist/src/cut-off-round.d.ts +24 -0
  18. package/dist/src/cut-off-round.d.ts.map +1 -0
  19. package/dist/src/cut-off-round.js +87 -0
  20. package/dist/src/cut-off-round.js.map +1 -0
  21. package/dist/src/framework.d.ts +1038 -193
  22. package/dist/src/framework.d.ts.map +1 -1
  23. package/dist/src/framework.js +5967 -1514
  24. package/dist/src/framework.js.map +1 -1
  25. package/dist/src/gate/event-gate.d.ts +405 -46
  26. package/dist/src/gate/event-gate.d.ts.map +1 -1
  27. package/dist/src/gate/event-gate.js +2646 -399
  28. package/dist/src/gate/event-gate.js.map +1 -1
  29. package/dist/src/gate/index.d.ts +3 -2
  30. package/dist/src/gate/index.d.ts.map +1 -1
  31. package/dist/src/gate/index.js +2 -1
  32. package/dist/src/gate/index.js.map +1 -1
  33. package/dist/src/gate/recipe-reconcile.d.ts +103 -0
  34. package/dist/src/gate/recipe-reconcile.d.ts.map +1 -0
  35. package/dist/src/gate/recipe-reconcile.js +325 -0
  36. package/dist/src/gate/recipe-reconcile.js.map +1 -0
  37. package/dist/src/gate/reply-judge.d.ts +116 -0
  38. package/dist/src/gate/reply-judge.d.ts.map +1 -0
  39. package/dist/src/gate/reply-judge.js +467 -0
  40. package/dist/src/gate/reply-judge.js.map +1 -0
  41. package/dist/src/gate/types.d.ts +229 -8
  42. package/dist/src/gate/types.d.ts.map +1 -1
  43. package/dist/src/heartbeat/heartbeat.d.ts +184 -0
  44. package/dist/src/heartbeat/heartbeat.d.ts.map +1 -0
  45. package/dist/src/heartbeat/heartbeat.js +752 -0
  46. package/dist/src/heartbeat/heartbeat.js.map +1 -0
  47. package/dist/src/heartbeat/reminder-time.d.ts +5 -0
  48. package/dist/src/heartbeat/reminder-time.d.ts.map +1 -0
  49. package/dist/src/heartbeat/reminder-time.js +107 -0
  50. package/dist/src/heartbeat/reminder-time.js.map +1 -0
  51. package/dist/src/index.d.ts +10 -4
  52. package/dist/src/index.d.ts.map +1 -1
  53. package/dist/src/index.js +7 -2
  54. package/dist/src/index.js.map +1 -1
  55. package/dist/src/mcpl/capability-mask.d.ts.map +1 -1
  56. package/dist/src/mcpl/capability-mask.js +3 -2
  57. package/dist/src/mcpl/capability-mask.js.map +1 -1
  58. package/dist/src/mcpl/channel-registry.d.ts +433 -113
  59. package/dist/src/mcpl/channel-registry.d.ts.map +1 -1
  60. package/dist/src/mcpl/channel-registry.js +1154 -435
  61. package/dist/src/mcpl/channel-registry.js.map +1 -1
  62. package/dist/src/mcpl/conversation-router.d.ts +16 -8
  63. package/dist/src/mcpl/conversation-router.d.ts.map +1 -1
  64. package/dist/src/mcpl/conversation-router.js +29 -15
  65. package/dist/src/mcpl/conversation-router.js.map +1 -1
  66. package/dist/src/mcpl/convert-block.d.ts.map +1 -1
  67. package/dist/src/mcpl/convert-block.js +14 -2
  68. package/dist/src/mcpl/convert-block.js.map +1 -1
  69. package/dist/src/mcpl/hook-orchestrator.d.ts +7 -1
  70. package/dist/src/mcpl/hook-orchestrator.d.ts.map +1 -1
  71. package/dist/src/mcpl/hook-orchestrator.js +32 -7
  72. package/dist/src/mcpl/hook-orchestrator.js.map +1 -1
  73. package/dist/src/mcpl/inbound-source.d.ts +26 -10
  74. package/dist/src/mcpl/inbound-source.d.ts.map +1 -1
  75. package/dist/src/mcpl/inbound-source.js +40 -69
  76. package/dist/src/mcpl/inbound-source.js.map +1 -1
  77. package/dist/src/mcpl/index.d.ts +5 -1
  78. package/dist/src/mcpl/index.d.ts.map +1 -1
  79. package/dist/src/mcpl/index.js +4 -0
  80. package/dist/src/mcpl/index.js.map +1 -1
  81. package/dist/src/mcpl/inference-router.d.ts +14 -2
  82. package/dist/src/mcpl/inference-router.d.ts.map +1 -1
  83. package/dist/src/mcpl/inference-router.js +73 -28
  84. package/dist/src/mcpl/inference-router.js.map +1 -1
  85. package/dist/src/mcpl/prose-grammar.d.ts +4 -0
  86. package/dist/src/mcpl/prose-grammar.d.ts.map +1 -1
  87. package/dist/src/mcpl/prose-grammar.js +6 -0
  88. package/dist/src/mcpl/prose-grammar.js.map +1 -1
  89. package/dist/src/mcpl/protocol-family.d.ts.map +1 -1
  90. package/dist/src/mcpl/protocol-family.js +3 -0
  91. package/dist/src/mcpl/protocol-family.js.map +1 -1
  92. package/dist/src/mcpl/push-coalescer.d.ts +20 -2
  93. package/dist/src/mcpl/push-coalescer.d.ts.map +1 -1
  94. package/dist/src/mcpl/push-coalescer.js +33 -11
  95. package/dist/src/mcpl/push-coalescer.js.map +1 -1
  96. package/dist/src/mcpl/reference-fetcher.d.ts +63 -42
  97. package/dist/src/mcpl/reference-fetcher.d.ts.map +1 -1
  98. package/dist/src/mcpl/reference-fetcher.js +177 -145
  99. package/dist/src/mcpl/reference-fetcher.js.map +1 -1
  100. package/dist/src/mcpl/references.d.ts +380 -46
  101. package/dist/src/mcpl/references.d.ts.map +1 -1
  102. package/dist/src/mcpl/references.js +736 -77
  103. package/dist/src/mcpl/references.js.map +1 -1
  104. package/dist/src/mcpl/server-connection.d.ts +93 -4
  105. package/dist/src/mcpl/server-connection.d.ts.map +1 -1
  106. package/dist/src/mcpl/server-connection.js +176 -11
  107. package/dist/src/mcpl/server-connection.js.map +1 -1
  108. package/dist/src/mcpl/tags.d.ts +9 -2
  109. package/dist/src/mcpl/tags.d.ts.map +1 -1
  110. package/dist/src/mcpl/tags.js +12 -2
  111. package/dist/src/mcpl/tags.js.map +1 -1
  112. package/dist/src/mcpl/tool-lifecycle.d.ts +11 -0
  113. package/dist/src/mcpl/tool-lifecycle.d.ts.map +1 -1
  114. package/dist/src/mcpl/tool-lifecycle.js +21 -0
  115. package/dist/src/mcpl/tool-lifecycle.js.map +1 -1
  116. package/dist/src/mcpl/tool-presence.d.ts +311 -0
  117. package/dist/src/mcpl/tool-presence.d.ts.map +1 -0
  118. package/dist/src/mcpl/tool-presence.js +697 -0
  119. package/dist/src/mcpl/tool-presence.js.map +1 -0
  120. package/dist/src/mcpl/tool-result-normalize.d.ts +9 -3
  121. package/dist/src/mcpl/tool-result-normalize.d.ts.map +1 -1
  122. package/dist/src/mcpl/tool-result-normalize.js +14 -6
  123. package/dist/src/mcpl/tool-result-normalize.js.map +1 -1
  124. package/dist/src/mcpl/tool-signature.d.ts +46 -0
  125. package/dist/src/mcpl/tool-signature.d.ts.map +1 -0
  126. package/dist/src/mcpl/tool-signature.js +321 -0
  127. package/dist/src/mcpl/tool-signature.js.map +1 -0
  128. package/dist/src/mcpl/toolset-host.d.ts +251 -0
  129. package/dist/src/mcpl/toolset-host.d.ts.map +1 -0
  130. package/dist/src/mcpl/toolset-host.js +720 -0
  131. package/dist/src/mcpl/toolset-host.js.map +1 -0
  132. package/dist/src/mcpl/toolsets-module.d.ts +83 -0
  133. package/dist/src/mcpl/toolsets-module.d.ts.map +1 -0
  134. package/dist/src/mcpl/toolsets-module.js +523 -0
  135. package/dist/src/mcpl/toolsets-module.js.map +1 -0
  136. package/dist/src/mcpl/transport.d.ts +2 -2
  137. package/dist/src/mcpl/transport.d.ts.map +1 -1
  138. package/dist/src/mcpl/transport.js +3 -2
  139. package/dist/src/mcpl/transport.js.map +1 -1
  140. package/dist/src/mcpl/types.d.ts +24 -2
  141. package/dist/src/mcpl/types.d.ts.map +1 -1
  142. package/dist/src/mcpl/types.js.map +1 -1
  143. package/dist/src/mcpl/visible-content.d.ts +7 -9
  144. package/dist/src/mcpl/visible-content.d.ts.map +1 -1
  145. package/dist/src/mcpl/visible-content.js +21 -9
  146. package/dist/src/mcpl/visible-content.js.map +1 -1
  147. package/dist/src/module-registry.d.ts +27 -9
  148. package/dist/src/module-registry.d.ts.map +1 -1
  149. package/dist/src/module-registry.js +77 -17
  150. package/dist/src/module-registry.js.map +1 -1
  151. package/dist/src/modules/discord/index.js +1 -1
  152. package/dist/src/modules/discord/index.js.map +1 -1
  153. package/dist/src/modules/history/index.d.ts +2 -1
  154. package/dist/src/modules/history/index.d.ts.map +1 -1
  155. package/dist/src/modules/history/index.js +73 -28
  156. package/dist/src/modules/history/index.js.map +1 -1
  157. package/dist/src/modules/history/semantic.d.ts +108 -6
  158. package/dist/src/modules/history/semantic.d.ts.map +1 -1
  159. package/dist/src/modules/history/semantic.js +273 -57
  160. package/dist/src/modules/history/semantic.js.map +1 -1
  161. package/dist/src/modules/workspace/answers.d.ts +22 -0
  162. package/dist/src/modules/workspace/answers.d.ts.map +1 -0
  163. package/dist/src/modules/workspace/answers.js +123 -0
  164. package/dist/src/modules/workspace/answers.js.map +1 -0
  165. package/dist/src/modules/workspace/branch-intent.d.ts +5 -3
  166. package/dist/src/modules/workspace/branch-intent.d.ts.map +1 -1
  167. package/dist/src/modules/workspace/branch-intent.js +6 -2
  168. package/dist/src/modules/workspace/branch-intent.js.map +1 -1
  169. package/dist/src/modules/workspace/direct-write.d.ts +184 -0
  170. package/dist/src/modules/workspace/direct-write.d.ts.map +1 -0
  171. package/dist/src/modules/workspace/direct-write.js +417 -0
  172. package/dist/src/modules/workspace/direct-write.js.map +1 -0
  173. package/dist/src/modules/workspace/disk-agreement.d.ts +33 -11
  174. package/dist/src/modules/workspace/disk-agreement.d.ts.map +1 -1
  175. package/dist/src/modules/workspace/disk-agreement.js +48 -17
  176. package/dist/src/modules/workspace/disk-agreement.js.map +1 -1
  177. package/dist/src/modules/workspace/edit-text.d.ts +27 -12
  178. package/dist/src/modules/workspace/edit-text.d.ts.map +1 -1
  179. package/dist/src/modules/workspace/edit-text.js +61 -29
  180. package/dist/src/modules/workspace/edit-text.js.map +1 -1
  181. package/dist/src/modules/workspace/effects.d.ts +10 -0
  182. package/dist/src/modules/workspace/effects.d.ts.map +1 -1
  183. package/dist/src/modules/workspace/effects.js +21 -14
  184. package/dist/src/modules/workspace/effects.js.map +1 -1
  185. package/dist/src/modules/workspace/index.d.ts +308 -78
  186. package/dist/src/modules/workspace/index.d.ts.map +1 -1
  187. package/dist/src/modules/workspace/index.js +2103 -720
  188. package/dist/src/modules/workspace/index.js.map +1 -1
  189. package/dist/src/modules/workspace/observe.d.ts +75 -63
  190. package/dist/src/modules/workspace/observe.d.ts.map +1 -1
  191. package/dist/src/modules/workspace/observe.js +366 -180
  192. package/dist/src/modules/workspace/observe.js.map +1 -1
  193. package/dist/src/modules/workspace/read-page.d.ts +41 -0
  194. package/dist/src/modules/workspace/read-page.d.ts.map +1 -0
  195. package/dist/src/modules/workspace/read-page.js +219 -0
  196. package/dist/src/modules/workspace/read-page.js.map +1 -0
  197. package/dist/src/modules/workspace/reconcile.d.ts.map +1 -1
  198. package/dist/src/modules/workspace/reconcile.js +26 -16
  199. package/dist/src/modules/workspace/reconcile.js.map +1 -1
  200. package/dist/src/modules/workspace/result-text.d.ts +68 -0
  201. package/dist/src/modules/workspace/result-text.d.ts.map +1 -0
  202. package/dist/src/modules/workspace/result-text.js +94 -0
  203. package/dist/src/modules/workspace/result-text.js.map +1 -0
  204. package/dist/src/modules/workspace/sync.d.ts +9 -0
  205. package/dist/src/modules/workspace/sync.d.ts.map +1 -1
  206. package/dist/src/modules/workspace/sync.js +14 -0
  207. package/dist/src/modules/workspace/sync.js.map +1 -1
  208. package/dist/src/modules/workspace/text.d.ts +29 -12
  209. package/dist/src/modules/workspace/text.d.ts.map +1 -1
  210. package/dist/src/modules/workspace/text.js +35 -33
  211. package/dist/src/modules/workspace/text.js.map +1 -1
  212. package/dist/src/modules/workspace/types.d.ts +137 -60
  213. package/dist/src/modules/workspace/types.d.ts.map +1 -1
  214. package/dist/src/modules/workspace/types.js.map +1 -1
  215. package/dist/src/modules/workspace/watcher.d.ts.map +1 -1
  216. package/dist/src/modules/workspace/watcher.js +8 -3
  217. package/dist/src/modules/workspace/watcher.js.map +1 -1
  218. package/dist/src/prose-drafts.d.ts +4 -3
  219. package/dist/src/prose-drafts.d.ts.map +1 -1
  220. package/dist/src/prose-drafts.js +3 -2
  221. package/dist/src/prose-drafts.js.map +1 -1
  222. package/dist/src/reasoning-loss-notice.d.ts +23 -0
  223. package/dist/src/reasoning-loss-notice.d.ts.map +1 -0
  224. package/dist/src/reasoning-loss-notice.js +50 -0
  225. package/dist/src/reasoning-loss-notice.js.map +1 -0
  226. package/dist/src/reasoning-loss.d.ts +24 -0
  227. package/dist/src/reasoning-loss.d.ts.map +1 -0
  228. package/dist/src/reasoning-loss.js +102 -0
  229. package/dist/src/reasoning-loss.js.map +1 -0
  230. package/dist/src/send-tools.d.ts +3 -2
  231. package/dist/src/send-tools.d.ts.map +1 -1
  232. package/dist/src/send-tools.js +3 -2
  233. package/dist/src/send-tools.js.map +1 -1
  234. package/dist/src/sent-rows.d.ts +59 -0
  235. package/dist/src/sent-rows.d.ts.map +1 -0
  236. package/dist/src/sent-rows.js +145 -0
  237. package/dist/src/sent-rows.js.map +1 -0
  238. package/dist/src/silent-heartbeat.d.ts +17 -10
  239. package/dist/src/silent-heartbeat.d.ts.map +1 -1
  240. package/dist/src/silent-heartbeat.js.map +1 -1
  241. package/dist/src/stable-hash.d.ts +3 -0
  242. package/dist/src/stable-hash.d.ts.map +1 -0
  243. package/dist/src/stable-hash.js +15 -0
  244. package/dist/src/stable-hash.js.map +1 -0
  245. package/dist/src/stored-injections.d.ts +99 -0
  246. package/dist/src/stored-injections.d.ts.map +1 -0
  247. package/dist/src/stored-injections.js +106 -0
  248. package/dist/src/stored-injections.js.map +1 -0
  249. package/dist/src/text-as-stored.d.ts +4 -3
  250. package/dist/src/text-as-stored.d.ts.map +1 -1
  251. package/dist/src/text-as-stored.js +4 -3
  252. package/dist/src/text-as-stored.js.map +1 -1
  253. package/dist/src/thinking-drops.d.ts +74 -0
  254. package/dist/src/thinking-drops.d.ts.map +1 -0
  255. package/dist/src/thinking-drops.js +96 -0
  256. package/dist/src/thinking-drops.js.map +1 -0
  257. package/dist/src/tool-args.d.ts +12 -0
  258. package/dist/src/tool-args.d.ts.map +1 -1
  259. package/dist/src/tool-args.js +35 -1
  260. package/dist/src/tool-args.js.map +1 -1
  261. package/dist/src/tool-image-ledger.d.ts +9 -8
  262. package/dist/src/tool-image-ledger.d.ts.map +1 -1
  263. package/dist/src/tool-image-ledger.js +9 -8
  264. package/dist/src/tool-image-ledger.js.map +1 -1
  265. package/dist/src/tool-presentation.d.ts.map +1 -1
  266. package/dist/src/tool-presentation.js +3 -1
  267. package/dist/src/tool-presentation.js.map +1 -1
  268. package/dist/src/tool-result-guard.d.ts +12 -6
  269. package/dist/src/tool-result-guard.d.ts.map +1 -1
  270. package/dist/src/tool-result-guard.js +44 -17
  271. package/dist/src/tool-result-guard.js.map +1 -1
  272. package/dist/src/tool-result-history.d.ts +176 -24
  273. package/dist/src/tool-result-history.d.ts.map +1 -1
  274. package/dist/src/tool-result-history.js +411 -91
  275. package/dist/src/tool-result-history.js.map +1 -1
  276. package/dist/src/tune-out/coordinator.d.ts +17 -9
  277. package/dist/src/tune-out/coordinator.d.ts.map +1 -1
  278. package/dist/src/tune-out/coordinator.js +84 -37
  279. package/dist/src/tune-out/coordinator.js.map +1 -1
  280. package/dist/src/tune-out/tools.d.ts +4 -1
  281. package/dist/src/tune-out/tools.d.ts.map +1 -1
  282. package/dist/src/types/agent.d.ts +217 -7
  283. package/dist/src/types/agent.d.ts.map +1 -1
  284. package/dist/src/types/events.d.ts +33 -9
  285. package/dist/src/types/events.d.ts.map +1 -1
  286. package/dist/src/types/framework.d.ts +39 -3
  287. package/dist/src/types/framework.d.ts.map +1 -1
  288. package/dist/src/types/module.d.ts +95 -9
  289. package/dist/src/types/module.d.ts.map +1 -1
  290. package/dist/src/types/trace.d.ts +91 -3
  291. package/dist/src/types/trace.d.ts.map +1 -1
  292. package/package.json +5 -4
  293. package/src/agent.ts +661 -67
  294. package/src/api/server.ts +5 -4
  295. package/src/auto-continue.ts +43 -0
  296. package/src/code-execution/isolate/scripts.ts +11 -3
  297. package/src/cut-off-round.ts +98 -0
  298. package/src/framework.ts +6168 -1540
  299. package/src/gate/event-gate.ts +2895 -405
  300. package/src/gate/index.ts +10 -1
  301. package/src/gate/recipe-reconcile.ts +355 -0
  302. package/src/gate/reply-judge.ts +512 -0
  303. package/src/gate/types.ts +234 -9
  304. package/src/heartbeat/heartbeat.ts +804 -0
  305. package/src/heartbeat/reminder-time.ts +107 -0
  306. package/src/index.ts +11 -4
  307. package/src/mcpl/capability-mask.ts +3 -2
  308. package/src/mcpl/channel-registry.ts +1222 -468
  309. package/src/mcpl/conversation-router.ts +34 -16
  310. package/src/mcpl/convert-block.ts +14 -1
  311. package/src/mcpl/hook-orchestrator.ts +33 -6
  312. package/src/mcpl/inbound-source.ts +42 -69
  313. package/src/mcpl/index.ts +6 -1
  314. package/src/mcpl/inference-router.ts +79 -34
  315. package/src/mcpl/prose-grammar.ts +7 -0
  316. package/src/mcpl/protocol-family.ts +2 -0
  317. package/src/mcpl/push-coalescer.ts +51 -13
  318. package/src/mcpl/reference-fetcher.ts +201 -162
  319. package/src/mcpl/references.ts +851 -77
  320. package/src/mcpl/server-connection.ts +189 -11
  321. package/src/mcpl/tags.ts +13 -2
  322. package/src/mcpl/tool-lifecycle.ts +21 -0
  323. package/src/mcpl/tool-presence.ts +716 -0
  324. package/src/mcpl/tool-result-normalize.ts +18 -6
  325. package/src/mcpl/tool-signature.ts +316 -0
  326. package/src/mcpl/toolset-host.ts +727 -0
  327. package/src/mcpl/toolsets-module.ts +545 -0
  328. package/src/mcpl/transport.ts +3 -2
  329. package/src/mcpl/types.ts +25 -2
  330. package/src/mcpl/visible-content.ts +22 -9
  331. package/src/module-registry.ts +85 -26
  332. package/src/modules/discord/index.ts +1 -1
  333. package/src/modules/history/index.ts +70 -30
  334. package/src/modules/history/semantic.ts +308 -54
  335. package/src/modules/workspace/answers.ts +133 -0
  336. package/src/modules/workspace/branch-intent.ts +10 -5
  337. package/src/modules/workspace/direct-write.ts +460 -0
  338. package/src/modules/workspace/disk-agreement.ts +52 -21
  339. package/src/modules/workspace/edit-text.ts +84 -35
  340. package/src/modules/workspace/effects.ts +22 -13
  341. package/src/modules/workspace/index.ts +2142 -723
  342. package/src/modules/workspace/observe.ts +381 -177
  343. package/src/modules/workspace/read-page.ts +228 -0
  344. package/src/modules/workspace/reconcile.ts +27 -15
  345. package/src/modules/workspace/result-text.ts +121 -0
  346. package/src/modules/workspace/sync.ts +16 -0
  347. package/src/modules/workspace/text.ts +58 -32
  348. package/src/modules/workspace/types.ts +137 -60
  349. package/src/modules/workspace/watcher.ts +8 -3
  350. package/src/prose-drafts.ts +4 -3
  351. package/src/reasoning-loss-notice.ts +62 -0
  352. package/src/reasoning-loss.ts +120 -0
  353. package/src/send-tools.ts +3 -2
  354. package/src/sent-rows.ts +162 -0
  355. package/src/silent-heartbeat.ts +17 -10
  356. package/src/stable-hash.ts +14 -0
  357. package/src/stored-injections.ts +131 -0
  358. package/src/text-as-stored.ts +4 -3
  359. package/src/thinking-drops.ts +135 -0
  360. package/src/tool-args.ts +34 -1
  361. package/src/tool-image-ledger.ts +9 -8
  362. package/src/tool-presentation.ts +3 -1
  363. package/src/tool-result-guard.ts +45 -17
  364. package/src/tool-result-history.ts +471 -90
  365. package/src/tune-out/coordinator.ts +80 -37
  366. package/src/tune-out/tools.ts +4 -1
  367. package/src/types/agent.ts +220 -7
  368. package/src/types/events.ts +33 -9
  369. package/src/types/framework.ts +36 -3
  370. package/src/types/module.ts +91 -9
  371. package/src/types/trace.ts +94 -3
@@ -37,6 +37,7 @@
37
37
  import { createHash } from 'node:crypto';
38
38
  import type { ContextStrategy } from '@animalabs/context-manager';
39
39
  import type { ChannelDescriptor } from './types.js';
40
+ import { destinationKey, type ChannelRef } from './channel-registry.js';
40
41
 
41
42
  // ============================================================================
42
43
  // Config & types
@@ -108,6 +109,9 @@ export interface ConversationRouterConfig {
108
109
 
109
110
  export interface ConversationBinding {
110
111
  channelId: string;
112
+ /** The channel's server: channel ids are unique only within one server's
113
+ * connection (MCPL §14.2), so each server's channel has its own binding. */
114
+ serverId: string;
111
115
  agentName: string;
112
116
  /** Engagement counter per channel — fresh forks get a new generation. */
113
117
  generation: number;
@@ -157,11 +161,15 @@ export const DEFAULT_CLOSURE_PROMPT =
157
161
  export class ConversationRouter {
158
162
  private config: ConversationRouterConfig;
159
163
 
160
- /** Active bindings, keyed by channelId. */
164
+ /** Active bindings, keyed by the channel's server and id (destinationKey):
165
+ * two servers' channels with one id are two conversations. */
161
166
  private bindings = new Map<string, ConversationBinding>();
162
167
 
163
- /** Engagement counter per channel — survives unbind, so a re-bound channel
164
- * gets a distinct fork agent name. */
168
+ /** Engagement counter per channel id — survives unbind, so a re-bound
169
+ * channel gets a distinct fork agent name. Kept by bare id, as it's
170
+ * persisted: two servers sharing an id share a counter, which still never
171
+ * repeats a name, and their forks' names differ anyway, since each hashes
172
+ * its server (forkAgentName). */
165
173
  private generations = new Map<string, number>();
166
174
 
167
175
  constructor(config: ConversationRouterConfig) {
@@ -191,7 +199,7 @@ export class ConversationRouter {
191
199
  * unchanged; a failed spawn doesn't leave a dangling route.
192
200
  */
193
201
  route(facts: IncomingMessageFacts): RoutingDecision {
194
- const existing = this.bindings.get(facts.channelId);
202
+ const existing = this.bindings.get(bindingKey({ serverId: facts.serverId ?? '', channelId: facts.channelId }));
195
203
 
196
204
  if (existing) {
197
205
  return {
@@ -223,39 +231,44 @@ export class ConversationRouter {
223
231
  * Record a binding after the framework successfully spawned the fork.
224
232
  * Returns the new binding.
225
233
  */
226
- bind(channelId: string, agentName: string, generation: number, now = Date.now()): ConversationBinding {
227
- this.generations.set(channelId, generation);
234
+ bind(channel: ChannelRef, agentName: string, generation: number, now = Date.now()): ConversationBinding {
235
+ // The counter is kept by bare id, and two servers' spawns on one id run
236
+ // beside each other, so a bind can finish after a later spawn's: the
237
+ // counter keeps the higher, or the next spawn would propose a fork name
238
+ // already used.
239
+ this.generations.set(channel.channelId, Math.max(this.generations.get(channel.channelId) ?? 0, generation));
228
240
  const binding: ConversationBinding = {
229
- channelId,
241
+ channelId: channel.channelId,
242
+ serverId: channel.serverId,
230
243
  agentName,
231
244
  generation,
232
245
  boundAt: now,
233
246
  lastActivity: now,
234
247
  };
235
- this.bindings.set(channelId, binding);
248
+ this.bindings.set(bindingKey(channel), binding);
236
249
  return binding;
237
250
  }
238
251
 
239
252
  /** Record delivered activity on an existing binding. Does not bind a
240
253
  * channel or advance its generation. Call after successful delivery, not
241
254
  * when previewing a route. The optional clock supports deterministic use. */
242
- touch(channelId: string, now = Date.now()): void {
243
- const binding = this.bindings.get(channelId);
255
+ touch(channel: ChannelRef, now = Date.now()): void {
256
+ const binding = this.bindings.get(bindingKey(channel));
244
257
  if (binding) binding.lastActivity = now;
245
258
  }
246
259
 
247
- unbind(channelId: string): void {
248
- this.bindings.delete(channelId);
260
+ unbind(channel: ChannelRef): void {
261
+ this.bindings.delete(bindingKey(channel));
249
262
  }
250
263
 
251
- getBinding(channelId: string): ConversationBinding | undefined {
252
- return this.bindings.get(channelId);
264
+ getBinding(channel: ChannelRef): ConversationBinding | undefined {
265
+ return this.bindings.get(bindingKey(channel));
253
266
  }
254
267
 
255
268
  /** Reverse lookup: the channel an agent is bound to, if any. */
256
- channelForAgent(agentName: string): string | undefined {
269
+ channelForAgent(agentName: string): ChannelRef | undefined {
257
270
  for (const binding of this.bindings.values()) {
258
- if (binding.agentName === agentName) return binding.channelId;
271
+ if (binding.agentName === agentName) return { serverId: binding.serverId, channelId: binding.channelId };
259
272
  }
260
273
  return undefined;
261
274
  }
@@ -325,6 +338,11 @@ export class ConversationRouter {
325
338
  /** The readable part of a fork's name stops here: uniqueness rides on the hash. */
326
339
  const FORK_NAME_READABLE_MAX = 64;
327
340
 
341
+ /** A binding's key: its channel's server and id (destinationKey). */
342
+ function bindingKey(channel: ChannelRef): string {
343
+ return destinationKey(channel);
344
+ }
345
+
328
346
  /**
329
347
  * A fork's agent name: `<prefix>-<readable>-g<generation>-<hash>`, such as
330
348
  * `conversation-C123-g2-e08e564271867d16` for Slack's channel `C123` on the
@@ -11,6 +11,7 @@ import {
11
11
  IMAGE_NO_DATA_TEXT,
12
12
  IMAGE_NO_MEDIA_TYPE_TEXT,
13
13
  INLINE_WITHHELD_TEXT,
14
+ classifyBlock,
14
15
  isInlineContradiction,
15
16
  referenceStubOrNull,
16
17
  unknownBlockText,
@@ -85,12 +86,24 @@ export function convertBlock(block: McplContentBlock, provenance: string, server
85
86
  }
86
87
  return { type: 'text', text: isFilledString(block.data) ? AUDIO_NO_MEDIA_TYPE_TEXT : AUDIO_NO_DATA_TEXT };
87
88
 
88
- case 'resource':
89
+ case 'resource': {
90
+ // MCP's embedded resource shows as the block its contents make would
91
+ // show here (RFC-005 §8.1), its uri never.
92
+ const c = classifyBlock(block);
93
+ if (c.kind === 'embedded' && c.contents.kind === 'block') {
94
+ return convertBlock(c.contents.block as McplContentBlock, provenance, serverId);
95
+ }
89
96
  // RFC-005: reference blocks become bounded stubs — never raw URIs
90
97
  // (a signed URL is a bearer credential that looks like a location).
91
98
  return { type: 'text', text: referenceStubOrNull(block, provenance, serverId) ?? '[reference]' };
99
+ }
92
100
 
93
101
  default: {
102
+ // MCP's link to a resource isn't one of MCPL's own blocks, but a server
103
+ // can send one, and RFC-005 §8.1 makes it a reference, shown as one.
104
+ if ((block as { type?: unknown }).type === 'resource_link') {
105
+ return { type: 'text', text: referenceStubOrNull(block, provenance, serverId) ?? '[reference]' };
106
+ }
94
107
  // Unknown wire block types previously fell off the exhaustive switch
95
108
  // and propagated `undefined` into ContentBlock[]. Fail visibly. A type
96
109
  // that isn't a string is no type, and is never converted to one: an
@@ -71,7 +71,9 @@ function convertContent(content: string | McplContentBlock[], serverId: string):
71
71
 
72
72
  /**
73
73
  * Convert an MCPL wire-format context injection, from the server `serverId`,
74
- * to a context-manager ContextInjection.
74
+ * to a context-manager ContextInjection. It builds only these four fields,
75
+ * so a server can't opt an injection out of being stored
76
+ * (ModuleContextInjection.placedEachCompile, which only a module may set).
75
77
  */
76
78
  function convertMcplInjection(mcplInj: McplContextInjection, serverId: string): ContextInjection {
77
79
  return {
@@ -114,8 +116,17 @@ export class HookOrchestrator {
114
116
  *
115
117
  * Returns aggregated ContextInjection[] ready for context-manager's compile().
116
118
  * Fail-open: servers that time out or error are silently skipped.
119
+ *
120
+ * `channelsFor` gives each server its own channel context (§14.4), in
121
+ * place of `params.channels`: channel ids are unique only within one
122
+ * server's connection (§14.2), so each server is told only of its own
123
+ * channels (ChannelRegistry.buildChannelContext). Undefined from it leaves
124
+ * the field out for that server.
117
125
  */
118
- async beforeInference(params: BeforeInferenceParams): Promise<ContextInjection[]> {
126
+ async beforeInference(
127
+ params: BeforeInferenceParams,
128
+ channelsFor?: (serverId: string) => BeforeInferenceParams['channels'],
129
+ ): Promise<ContextInjection[]> {
119
130
  // §5.4: the GRANT decides fan-out, not the raw advertisement — which
120
131
  // also fixes selection for the recursive §5.1 shape, where
121
132
  // `beforeInference: {observe: true, …}` is an object, not `true`. A
@@ -132,7 +143,7 @@ export class HookOrchestrator {
132
143
 
133
144
  this._isInHook = true;
134
145
  try {
135
- return await this.fanOutBeforeInference(servers, params);
146
+ return await this.fanOutBeforeInference(servers, params, channelsFor);
136
147
  } finally {
137
148
  this._isInHook = false;
138
149
  }
@@ -163,17 +174,33 @@ export class HookOrchestrator {
163
174
  private async fanOutBeforeInference(
164
175
  servers: McplServerConnection[],
165
176
  params: BeforeInferenceParams,
177
+ channelsFor?: (serverId: string) => BeforeInferenceParams['channels'],
166
178
  ): Promise<ContextInjection[]> {
167
179
  const results = await Promise.allSettled(
168
- servers.map((server) => {
180
+ // Async, so a throw building one server's channel context fails that
181
+ // server alone, as its timeout or transport error would.
182
+ servers.map(async (server) => {
183
+ let base = params;
184
+ if (channelsFor) {
185
+ const { channels: _shared, ...rest } = params;
186
+ let channels: BeforeInferenceParams['channels'];
187
+ try {
188
+ channels = channelsFor(server.id);
189
+ } catch (err) {
190
+ // A bug, not a server's failure: said, then this server is skipped.
191
+ console.error(`[beforeInference] the channel context for "${server.id}" couldn't be built, so it isn't called:`, err);
192
+ throw err;
193
+ }
194
+ base = channels ? { ...rest, channels } : rest;
195
+ }
169
196
  // §10.1: a server not granted observe MUST receive
170
197
  // `userMessage: null` — the field is ABSENT authority, not merely
171
198
  // discouraged — while the hook is still invoked so granted
172
199
  // injection positions keep working (write-without-read).
173
200
  const perServer: BeforeInferenceParams =
174
201
  CapabilityGrant.of(server).has('contextHooks.beforeInference.observe')
175
- ? params
176
- : { ...params, userMessage: null };
202
+ ? base
203
+ : { ...base, userMessage: null };
177
204
  return withTimeout(
178
205
  server.sendBeforeInference(perServer),
179
206
  BEFORE_INFERENCE_TIMEOUT_MS,
@@ -22,6 +22,8 @@
22
22
  */
23
23
 
24
24
  import { createHash } from 'node:crypto';
25
+ import { blockAsStored, MalformedContentError } from '@animalabs/context-manager';
26
+ import { logDetail, logValue } from './log-value.js';
25
27
 
26
28
  /** The MCPL admission lane that accepted an item. Its contract decides what
27
29
  * an `eventId` is worth: `push/event` deduplicates by eventId, and RFC-006
@@ -192,68 +194,23 @@ export function canonicalJson(value: unknown): string {
192
194
  }
193
195
 
194
196
  /**
195
- * A block as the store hands it back. Context-manager keeps inline media as
196
- * blobs (blob-manager.ts): an image whose source isn't a URL, and every
197
- * document, audio or video block, comes back as `{ type, source: { type:
198
- * 'base64', data, mediaType } }` and nothing else, its data re-encoded from
199
- * the decoded bytes, and an image's media type taken from its bytes'
200
- * signature where they have one. Any other block comes back as it was given,
201
- * except a store-internal `blob_ref` from an unresolved read, which resolves
202
- * to inline media by default. It is digested as given here, so an unresolved
203
- * read and its resolved form differ. Context-manager refuses caller-supplied
204
- * blob_ref writes; the MCPL lanes never store one.
197
+ * blockAsStored throws a `MalformedContentError` for a block the store
198
+ * refuses on write, a caller-supplied `blob_ref` among them. Such a block
199
+ * has no stored form, and whatever is thrown, it is digested as given.
205
200
  */
206
201
  function storedBlock(block: unknown): unknown {
207
- const media = block as { type?: unknown; source?: { type?: unknown; data?: unknown; mediaType?: unknown } } | null;
208
- const blob = media?.type === 'image'
209
- ? media.source?.type !== 'url'
210
- : media?.type === 'document' || media?.type === 'audio' || media?.type === 'video';
211
- const source = media?.source;
212
- if (!blob || typeof source?.data !== 'string') return block;
213
- const data = canonicalBase64(source.data);
214
- const mediaType = media!.type === 'image'
215
- ? sniffRasterImageMediaType(Buffer.from(data.slice(0, 32), 'base64')) ?? source.mediaType
216
- : source.mediaType;
217
- return { type: media!.type, source: { type: 'base64', data, mediaType } };
218
- }
219
-
220
- /**
221
- * Base64 as the store writes it back from the decoded bytes: padded, with no
222
- * whitespace, the standard alphabet and zero unused bits. Data already in
223
- * that form, as every stored copy's is, is returned without decoding it all.
224
- */
225
- function canonicalBase64(data: string): string {
226
- if (data.length % 4 === 0 && /^[A-Za-z0-9+/]*={0,2}$/.test(data)) {
227
- const last = data.slice(-4);
228
- if (Buffer.from(last, 'base64').toString('base64') === last) return data;
229
- }
230
- return Buffer.from(data, 'base64').toString('base64');
231
- }
232
-
233
- /**
234
- * The store's own test for raster image types with an unambiguous byte
235
- * signature, copied from context-manager's blob-manager.ts (it isn't
236
- * exported): the store relabels an image whose bytes match one of these.
237
- */
238
- function sniffRasterImageMediaType(bytes: Uint8Array): string | null {
239
- if (bytes.length >= 8 &&
240
- bytes[0] === 0x89 && bytes[1] === 0x50 && bytes[2] === 0x4e && bytes[3] === 0x47 &&
241
- bytes[4] === 0x0d && bytes[5] === 0x0a && bytes[6] === 0x1a && bytes[7] === 0x0a) {
242
- return 'image/png';
243
- }
244
- if (bytes.length >= 3 && bytes[0] === 0xff && bytes[1] === 0xd8 && bytes[2] === 0xff) {
245
- return 'image/jpeg';
246
- }
247
- if (bytes.length >= 6) {
248
- const signature = Buffer.from(bytes.subarray(0, 6)).toString('ascii');
249
- if (signature === 'GIF87a' || signature === 'GIF89a') return 'image/gif';
250
- }
251
- if (bytes.length >= 12 &&
252
- Buffer.from(bytes.subarray(0, 4)).toString('ascii') === 'RIFF' &&
253
- Buffer.from(bytes.subarray(8, 12)).toString('ascii') === 'WEBP') {
254
- return 'image/webp';
202
+ try {
203
+ return blockAsStored(block as Parameters<typeof blockAsStored>[0]);
204
+ } catch (err) {
205
+ if (!(err instanceof MalformedContentError)) {
206
+ const { name, message } = (err ?? {}) as { name?: unknown; message?: unknown };
207
+ console.error(
208
+ `[stored-digest-as-given] error=${logValue(typeof name === 'string' ? name : typeof err)}: ` +
209
+ logDetail(typeof message === 'string' ? message : ''),
210
+ );
211
+ }
212
+ return block;
255
213
  }
256
- return null;
257
214
  }
258
215
 
259
216
  /**
@@ -261,23 +218,39 @@ function sniffRasterImageMediaType(bytes: Uint8Array): string | null {
261
218
  * room-220 #46752–#47210): SHA-256 hex of `canonicalJson([blocks])` over the
262
219
  * ContentBlock[] the framework stores for the body — after MCPL conversion,
263
220
  * before any host decoration is added and before storage shards it — with
264
- * each block as the store keeps it (storedBlock, canonicalJson). So a body
265
- * hashes alike as delivered, as handed to storage, and as read back from
266
- * the store. The one-element array keeps the framing an undecorated,
267
- * unsharded stored copy has always hashed to, so a stamped copy and an
268
- * older copy of the same body share a version.
221
+ * each block as the store keeps it, which context-manager's `blockAsStored`
222
+ * gives. So a body hashes alike as delivered, as handed to storage, and as
223
+ * read back unsharded from the store with its blobs resolved, except one
224
+ * whose blocks the store changes together: when it mends a `tool_use` whose
225
+ * input isn't an object, it also releases a raw replay form (`rawXml`,
226
+ * `rawItem`) that the message's other blocks share, which no one block's
227
+ * stored form shows. A body an MCPL lane stamps is never one. A block the
228
+ * store refuses on write, a `blob_ref` among them, is digested as given. The
229
+ * one-element array keeps the framing an undecorated, unsharded stored copy
230
+ * has always hashed to, so a stamped copy and an older copy of the same body
231
+ * share a version where the store keeps the body as it did then.
232
+ *
233
+ * A digest is a digest under the context-manager release that computed it: a
234
+ * release that changes what the store keeps, such as its set of image
235
+ * signatures, changes the digest of a body it touches.
269
236
  *
270
237
  * Ingestion stamps it as `metadata.sourceBodyDigest`, and the same function
271
238
  * over exactly the blocks handed to storage (decorations included) as
272
239
  * `metadata.storedBodyDigest`, on both MCPL lanes. Both live outside the
273
240
  * frozen admission envelope: they describe the body actually delivered (a
274
241
  * materialization, a correction), not the admission. Neither proves later
275
- * presence or completeness; an unsharded stored copy that no longer hashes
242
+ * presence or completeness. An unsharded stored copy that no longer hashes
276
243
  * to its storedBodyDigest was changed after delivery (editMessage keeps
277
- * metadata). A body the store shards carries the whole body's two digests
278
- * on every shard, and no shard hashes to them. Check a copy read back with
279
- * its blobs resolved (getAllMessages, getMessage, or getMessageWindow
280
- * without `resolveBlobs: false`): a blob reference never matches.
244
+ * metadata), or holds blocks the store changes together, or its digest was
245
+ * taken under a release that kept some block of it differently: a
246
+ * context-manager release that changed a stored form, or an agent-framework
247
+ * release from 0.24.0 up to the one that brought in blockAsStored, which
248
+ * took stored forms from a copy of the store's transform. Digests already
249
+ * stamped are never recomputed. A body the store shards carries the whole
250
+ * body's two digests on every shard, and no shard hashes to them. Check a
251
+ * copy read back with its blobs resolved (getAllMessages, getMessage, or
252
+ * getMessageWindow without `resolveBlobs: false`): a blob reference never
253
+ * matches.
281
254
  *
282
255
  * Exported from the package root, so a consumer checks a copy against either
283
256
  * field with this same function rather than a copy of it.
package/src/mcpl/index.ts CHANGED
@@ -88,6 +88,11 @@ export type {
88
88
  } from './types.js';
89
89
 
90
90
  export { McplMethod } from './types.js';
91
+ // RFC-014 tool presence and toolsets (host side).
92
+ export { ToolsetHub, ToolsetController, TOOLSETS_MODULE, META_TOOL_NAMES } from './toolset-host.js';
93
+ export { ToolsetsModule, TOOLSETS_REMINDER_NAMESPACE } from './toolsets-module.js';
94
+ export { ToolPresence } from './tool-presence.js';
95
+ export type { ChangeSet, MetaToolNames, ToolsetDecl } from './tool-presence.js';
91
96
 
92
97
  // Error codes and factories
93
98
  export {
@@ -171,7 +176,7 @@ export { InferenceRouter } from './inference-router.js';
171
176
 
172
177
  // Channel registry (channel lifecycle, incoming messages, synthesized tools)
173
178
  export { ChannelRegistry } from './channel-registry.js';
174
- export type { PublishRefusalReason } from './channel-registry.js';
179
+ export type { ChannelRef, PublishRefusalReason } from './channel-registry.js';
175
180
 
176
181
  // Inbound source envelope: where each accepted item came from, stamped once;
177
182
  // and the digest of a delivered or stored body
@@ -26,6 +26,7 @@ import type {
26
26
  import type { FeatureSetManager } from './feature-set-manager.js';
27
27
  import { McplFeatureSetError } from './feature-set-manager.js';
28
28
  import type { HookOrchestrator } from './hook-orchestrator.js';
29
+ import { logValue } from './log-value.js';
29
30
 
30
31
  // ============================================================================
31
32
  // Responder interface
@@ -181,6 +182,10 @@ export class InferenceRouter {
181
182
  private featureSetManager: FeatureSetManager;
182
183
  private routingPolicy: InferenceRoutingPolicy | null;
183
184
  private emitTraceFn: (event: { type: string; [key: string]: unknown }) => void;
185
+ /** The refusals for want of a model that have been logged: one line per
186
+ * server with no policy, and per server and feature set with one, so a
187
+ * server that keeps asking doesn't flood the operator's log. */
188
+ private loggedRefusals = new Set<string>();
184
189
 
185
190
  constructor(
186
191
  membrane: Membrane,
@@ -201,7 +206,7 @@ export class InferenceRouter {
201
206
  *
202
207
  * 1. Validate feature set
203
208
  * 2. Check loop prevention (reject if inside a hook)
204
- * 3. Resolve model from routing policy
209
+ * 3. Resolve model from routing policy, refusing when it names none
205
210
  * 4. Build membrane request
206
211
  * 5. Run inference, streaming its visible text as chunks when asked
207
212
  * 6. Answer with the result, or with the error for any ending that isn't
@@ -233,11 +238,62 @@ export class InferenceRouter {
233
238
  return;
234
239
  }
235
240
 
236
- // 3. Resolve model
241
+ // Streaming needs a way to send chunks and an id to tag them with;
242
+ // without either, the request is answered whole.
243
+ const sendChunk = params.stream ? responder.sendChunk?.bind(responder) : undefined;
244
+ const chunkRequestId = sendChunk ? responder.requestId : undefined;
245
+ const streamed = chunkRequestId != null;
246
+
247
+ // 3. Resolve model. With no routing policy, or one that names no model
248
+ // for this feature set, the operator has chosen no model for this
249
+ // server's inference: refused rather than guessed, since a model picked
250
+ // here would run, on whatever credentials and at whatever price it
251
+ // carries, for a server the operator never chose it for. The answer is
252
+ // -32603, the code the router answers a failed inference with, as it
253
+ // answered this request when it reached the provider with no model; the
254
+ // server's grant can still list inferenceRequest, so not -32002.
237
255
  const model = this.resolveModel(
238
256
  params.featureSet,
239
257
  params.conversationId,
240
258
  );
259
+ if (model === undefined) {
260
+ const policy = Boolean(this.routingPolicy);
261
+ // One reason, quoted for where it goes: JSON for the server, logValue
262
+ // for the host log.
263
+ const reason = (quote: (value: string) => string) => policy
264
+ ? `the inference routing policy (inferenceRouting) names no model for feature set ${quote(params.featureSet)}`
265
+ : 'the host sets no inference routing policy (inferenceRouting)';
266
+ const message = `inference/request refused: ${reason(JSON.stringify)}, so the host has no model to run it on; ` +
267
+ "it stays refused until the host's operator configures one";
268
+ // Logged once per server with no policy, and once per server and
269
+ // feature set with one, since each feature set the policy misses is
270
+ // its own entry to add; validateInbound (step 1) bounds feature sets
271
+ // to those the server declares.
272
+ const key = JSON.stringify(policy ? [serverId, params.featureSet] : [serverId]);
273
+ if (!this.loggedRefusals.has(key)) {
274
+ this.loggedRefusals.add(key);
275
+ console.error(
276
+ `[mcpl] ${logValue(serverId)}: inference/request refused: ${reason(logValue)}; ` +
277
+ (policy
278
+ ? 'give it a default, or a byFeature or wildcards entry for that feature set. ' +
279
+ 'This line prints once per server and feature set'
280
+ : 'set inferenceRouting in the framework config to choose the model server-initiated inference runs on. ' +
281
+ 'This line prints once per server') +
282
+ '; later refusals are answered and traced but not logged.',
283
+ );
284
+ }
285
+ // The failure a trace watcher saw when this request failed at the
286
+ // provider, still seen; no inference_start, since none started.
287
+ this.emitTraceFn({
288
+ type: 'mcpl:inference_fail',
289
+ serverId,
290
+ model: null,
291
+ error: message,
292
+ streamed,
293
+ });
294
+ responder.respondError(-32603, message);
295
+ return;
296
+ }
241
297
 
242
298
  // Emit trace: inference start
243
299
  this.emitTraceFn({
@@ -263,12 +319,6 @@ export class InferenceRouter {
263
319
  },
264
320
  };
265
321
 
266
- // Streaming needs a way to send chunks and an id to tag them with;
267
- // without either, the request is answered whole.
268
- const sendChunk = params.stream ? responder.sendChunk?.bind(responder) : undefined;
269
- const chunkRequestId = sendChunk ? responder.requestId : undefined;
270
- const streamed = chunkRequestId != null;
271
-
272
322
  try {
273
323
  // 5. Run inference. Both modes end in membrane's response, and one
274
324
  // builder makes the result from it.
@@ -325,37 +375,32 @@ export class InferenceRouter {
325
375
  * 2. Exact feature set match
326
376
  * 3. Wildcard pattern match (first match wins)
327
377
  * 4. Default from policy
328
- * 5. Fallback string 'default'
378
+ *
379
+ * Only an entry naming a model counts: one that is empty or not a string
380
+ * names none and is passed over, as a missing one is. That includes what a
381
+ * server-sent conversation id or feature set such as `constructor` or
382
+ * `__proto__` finds on Object.prototype, which is never a string. A
383
+ * conversation id that is falsy reads no override.
384
+ *
385
+ * Undefined with no policy, or when none of these names a model: the
386
+ * request is then refused.
329
387
  */
330
388
  private resolveModel(
331
389
  featureSet: string,
332
390
  conversationId?: string,
333
- ): string {
334
- if (!this.routingPolicy) {
335
- return 'default';
391
+ ): string | undefined {
392
+ const policy = this.routingPolicy;
393
+ if (!policy) {
394
+ return undefined;
336
395
  }
337
-
338
- // Check per-conversation override
339
- if (conversationId && this.routingPolicy.overrides?.[conversationId]) {
340
- return this.routingPolicy.overrides[conversationId];
341
- }
342
-
343
- // Check exact feature set match
344
- if (this.routingPolicy.byFeature?.[featureSet]) {
345
- return this.routingPolicy.byFeature[featureSet];
346
- }
347
-
348
- // Check wildcard patterns
349
- if (this.routingPolicy.wildcards) {
350
- for (const [pattern, matchedModel] of Object.entries(this.routingPolicy.wildcards)) {
351
- if (wildcardMatch(pattern, featureSet)) {
352
- return matchedModel;
353
- }
354
- }
355
- }
356
-
357
- // Default
358
- return this.routingPolicy.default;
396
+ const named = (value: unknown): string | undefined =>
397
+ typeof value === 'string' && value !== '' ? value : undefined;
398
+
399
+ return (conversationId ? named(policy.overrides?.[conversationId]) : undefined)
400
+ ?? named(policy.byFeature?.[featureSet])
401
+ // First wildcard that matches and names a model.
402
+ ?? Object.entries(policy.wildcards ?? {}).find(([pattern, m]) => named(m) && wildcardMatch(pattern, featureSet))?.[1]
403
+ ?? named(policy.default);
359
404
  }
360
405
 
361
406
  // ==========================================================================
@@ -19,6 +19,13 @@ export interface ProsePrefix {
19
19
  body: string;
20
20
  }
21
21
 
22
+ /** Whether a `>>` (or `>>>`) prefix line reads `name` as a target: one token,
23
+ * and not `skip_reply`, which the line reads as private speech. For a name
24
+ * offered to an agent to write after `>>`. */
25
+ export function isProseTargetToken(name: string): boolean {
26
+ return /^\S+$/.test(name) && name !== 'skip_reply';
27
+ }
28
+
22
29
  /** A REAL prefix line: `>>` immediately followed by a target token. */
23
30
  export const PROSE_PREFIX_LINE = /^>>\S/;
24
31
 
@@ -641,6 +641,8 @@ function isPlainRecord(value: unknown): value is Record<string, unknown> {
641
641
  export function mcplPolicyGrants(config: McplServerConfig): string[] {
642
642
  const grants: string[] = [];
643
643
  if (isEffective(config.allowHostCommands)) grants.push('allowHostCommands');
644
+ // RFC-014 tool hints are an MCPL advertisement.
645
+ if (config.toolHints === true) grants.push('toolHints');
644
646
  // An allow-list, except that a pattern matching a path denied by default
645
647
  // grants it (computeGrant's explicitlyEnabled), named by the path.
646
648
  const enabled = config.enabledCapabilities as unknown;