@animalabs/agent-framework 0.27.0 → 0.29.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 (344) hide show
  1. package/README.md +145 -41
  2. package/dist/src/agent.d.ts +29 -2
  3. package/dist/src/agent.d.ts.map +1 -1
  4. package/dist/src/agent.js +73 -12
  5. package/dist/src/agent.js.map +1 -1
  6. package/dist/src/api/mcp-server-core.d.ts.map +1 -1
  7. package/dist/src/api/mcp-server-core.js +3 -12
  8. package/dist/src/api/mcp-server-core.js.map +1 -1
  9. package/dist/src/api/server.d.ts.map +1 -1
  10. package/dist/src/api/server.js +11 -5
  11. package/dist/src/api/server.js.map +1 -1
  12. package/dist/src/auto-continue.d.ts +5 -2
  13. package/dist/src/auto-continue.d.ts.map +1 -1
  14. package/dist/src/auto-continue.js +9 -5
  15. package/dist/src/auto-continue.js.map +1 -1
  16. package/dist/src/code-execution/isolate/runner.d.ts +7 -5
  17. package/dist/src/code-execution/isolate/runner.d.ts.map +1 -1
  18. package/dist/src/code-execution/isolate/runner.js +7 -5
  19. package/dist/src/code-execution/isolate/runner.js.map +1 -1
  20. package/dist/src/code-execution/isolate/scripts.d.ts.map +1 -1
  21. package/dist/src/code-execution/isolate/scripts.js +3 -2
  22. package/dist/src/code-execution/isolate/scripts.js.map +1 -1
  23. package/dist/src/cut-off-round.d.ts +24 -0
  24. package/dist/src/cut-off-round.d.ts.map +1 -0
  25. package/dist/src/cut-off-round.js +87 -0
  26. package/dist/src/cut-off-round.js.map +1 -0
  27. package/dist/src/framework.d.ts +468 -125
  28. package/dist/src/framework.d.ts.map +1 -1
  29. package/dist/src/framework.js +3651 -1111
  30. package/dist/src/framework.js.map +1 -1
  31. package/dist/src/gate/event-gate.d.ts +133 -13
  32. package/dist/src/gate/event-gate.d.ts.map +1 -1
  33. package/dist/src/gate/event-gate.js +654 -59
  34. package/dist/src/gate/event-gate.js.map +1 -1
  35. package/dist/src/gate/index.d.ts +3 -2
  36. package/dist/src/gate/index.d.ts.map +1 -1
  37. package/dist/src/gate/index.js +2 -1
  38. package/dist/src/gate/index.js.map +1 -1
  39. package/dist/src/gate/reply-judge.d.ts +116 -0
  40. package/dist/src/gate/reply-judge.d.ts.map +1 -0
  41. package/dist/src/gate/reply-judge.js +467 -0
  42. package/dist/src/gate/reply-judge.js.map +1 -0
  43. package/dist/src/gate/types.d.ts +173 -2
  44. package/dist/src/gate/types.d.ts.map +1 -1
  45. package/dist/src/heartbeat/heartbeat.d.ts +202 -0
  46. package/dist/src/heartbeat/heartbeat.d.ts.map +1 -0
  47. package/dist/src/heartbeat/heartbeat.js +778 -0
  48. package/dist/src/heartbeat/heartbeat.js.map +1 -0
  49. package/dist/src/heartbeat/reminder-time.d.ts +5 -0
  50. package/dist/src/heartbeat/reminder-time.d.ts.map +1 -0
  51. package/dist/src/heartbeat/reminder-time.js +107 -0
  52. package/dist/src/heartbeat/reminder-time.js.map +1 -0
  53. package/dist/src/held-writes.d.ts +3 -3
  54. package/dist/src/held-writes.js +3 -3
  55. package/dist/src/host-presentation.d.ts +170 -0
  56. package/dist/src/host-presentation.d.ts.map +1 -0
  57. package/dist/src/host-presentation.js +561 -0
  58. package/dist/src/host-presentation.js.map +1 -0
  59. package/dist/src/index.d.ts +5 -1
  60. package/dist/src/index.d.ts.map +1 -1
  61. package/dist/src/index.js +3 -1
  62. package/dist/src/index.js.map +1 -1
  63. package/dist/src/line-reader.d.ts +4 -0
  64. package/dist/src/line-reader.d.ts.map +1 -0
  65. package/dist/src/line-reader.js +59 -0
  66. package/dist/src/line-reader.js.map +1 -0
  67. package/dist/src/mcpl/capability-mask.d.ts.map +1 -1
  68. package/dist/src/mcpl/capability-mask.js +3 -2
  69. package/dist/src/mcpl/capability-mask.js.map +1 -1
  70. package/dist/src/mcpl/channel-registry.d.ts +34 -6
  71. package/dist/src/mcpl/channel-registry.d.ts.map +1 -1
  72. package/dist/src/mcpl/channel-registry.js +133 -54
  73. package/dist/src/mcpl/channel-registry.js.map +1 -1
  74. package/dist/src/mcpl/conversation-router.d.ts.map +1 -1
  75. package/dist/src/mcpl/conversation-router.js +2 -1
  76. package/dist/src/mcpl/conversation-router.js.map +1 -1
  77. package/dist/src/mcpl/convert-block.d.ts +1 -0
  78. package/dist/src/mcpl/convert-block.d.ts.map +1 -1
  79. package/dist/src/mcpl/convert-block.js +16 -4
  80. package/dist/src/mcpl/convert-block.js.map +1 -1
  81. package/dist/src/mcpl/hook-orchestrator.d.ts.map +1 -1
  82. package/dist/src/mcpl/hook-orchestrator.js +4 -3
  83. package/dist/src/mcpl/hook-orchestrator.js.map +1 -1
  84. package/dist/src/mcpl/inbound-source.d.ts +29 -11
  85. package/dist/src/mcpl/inbound-source.d.ts.map +1 -1
  86. package/dist/src/mcpl/inbound-source.js +43 -70
  87. package/dist/src/mcpl/inbound-source.js.map +1 -1
  88. package/dist/src/mcpl/modern-connection.d.ts.map +1 -1
  89. package/dist/src/mcpl/modern-connection.js +11 -7
  90. package/dist/src/mcpl/modern-connection.js.map +1 -1
  91. package/dist/src/mcpl/protocol-family.d.ts +3 -2
  92. package/dist/src/mcpl/protocol-family.d.ts.map +1 -1
  93. package/dist/src/mcpl/protocol-family.js +25 -3
  94. package/dist/src/mcpl/protocol-family.js.map +1 -1
  95. package/dist/src/mcpl/push-handler.js +2 -2
  96. package/dist/src/mcpl/push-handler.js.map +1 -1
  97. package/dist/src/mcpl/references.d.ts +12 -40
  98. package/dist/src/mcpl/references.d.ts.map +1 -1
  99. package/dist/src/mcpl/references.js +29 -23
  100. package/dist/src/mcpl/references.js.map +1 -1
  101. package/dist/src/mcpl/server-connection.d.ts +18 -8
  102. package/dist/src/mcpl/server-connection.d.ts.map +1 -1
  103. package/dist/src/mcpl/server-connection.js +70 -30
  104. package/dist/src/mcpl/server-connection.js.map +1 -1
  105. package/dist/src/mcpl/skip-reply-wake.d.ts +25 -0
  106. package/dist/src/mcpl/skip-reply-wake.d.ts.map +1 -1
  107. package/dist/src/mcpl/skip-reply-wake.js +25 -0
  108. package/dist/src/mcpl/skip-reply-wake.js.map +1 -1
  109. package/dist/src/mcpl/tags.d.ts +9 -2
  110. package/dist/src/mcpl/tags.d.ts.map +1 -1
  111. package/dist/src/mcpl/tags.js +12 -2
  112. package/dist/src/mcpl/tags.js.map +1 -1
  113. package/dist/src/mcpl/tool-presence.d.ts +66 -14
  114. package/dist/src/mcpl/tool-presence.d.ts.map +1 -1
  115. package/dist/src/mcpl/tool-presence.js +150 -80
  116. package/dist/src/mcpl/tool-presence.js.map +1 -1
  117. package/dist/src/mcpl/tool-signature.d.ts +10 -3
  118. package/dist/src/mcpl/tool-signature.d.ts.map +1 -1
  119. package/dist/src/mcpl/tool-signature.js +60 -30
  120. package/dist/src/mcpl/tool-signature.js.map +1 -1
  121. package/dist/src/mcpl/toolset-host.d.ts +88 -55
  122. package/dist/src/mcpl/toolset-host.d.ts.map +1 -1
  123. package/dist/src/mcpl/toolset-host.js +220 -111
  124. package/dist/src/mcpl/toolset-host.js.map +1 -1
  125. package/dist/src/mcpl/toolsets-module.d.ts +27 -35
  126. package/dist/src/mcpl/toolsets-module.d.ts.map +1 -1
  127. package/dist/src/mcpl/toolsets-module.js +313 -91
  128. package/dist/src/mcpl/toolsets-module.js.map +1 -1
  129. package/dist/src/mcpl/transport.d.ts +1 -1
  130. package/dist/src/mcpl/transport.d.ts.map +1 -1
  131. package/dist/src/mcpl/transport.js +16 -16
  132. package/dist/src/mcpl/transport.js.map +1 -1
  133. package/dist/src/mcpl/types.d.ts +31 -13
  134. package/dist/src/mcpl/types.d.ts.map +1 -1
  135. package/dist/src/mcpl/types.js.map +1 -1
  136. package/dist/src/module-registry.d.ts +10 -8
  137. package/dist/src/module-registry.d.ts.map +1 -1
  138. package/dist/src/module-registry.js +15 -9
  139. package/dist/src/module-registry.js.map +1 -1
  140. package/dist/src/modules/api/index.d.ts.map +1 -1
  141. package/dist/src/modules/api/index.js +4 -2
  142. package/dist/src/modules/api/index.js.map +1 -1
  143. package/dist/src/modules/discord/index.d.ts.map +1 -1
  144. package/dist/src/modules/discord/index.js +33 -25
  145. package/dist/src/modules/discord/index.js.map +1 -1
  146. package/dist/src/modules/history/index.d.ts.map +1 -1
  147. package/dist/src/modules/history/index.js +30 -17
  148. package/dist/src/modules/history/index.js.map +1 -1
  149. package/dist/src/modules/sender-name.d.ts +7 -0
  150. package/dist/src/modules/sender-name.d.ts.map +1 -0
  151. package/dist/src/modules/sender-name.js +10 -0
  152. package/dist/src/modules/sender-name.js.map +1 -0
  153. package/dist/src/modules/workspace/answers.d.ts +26 -0
  154. package/dist/src/modules/workspace/answers.d.ts.map +1 -0
  155. package/dist/src/modules/workspace/answers.js +138 -0
  156. package/dist/src/modules/workspace/answers.js.map +1 -0
  157. package/dist/src/modules/workspace/branch-intent.d.ts +5 -3
  158. package/dist/src/modules/workspace/branch-intent.d.ts.map +1 -1
  159. package/dist/src/modules/workspace/branch-intent.js +6 -2
  160. package/dist/src/modules/workspace/branch-intent.js.map +1 -1
  161. package/dist/src/modules/workspace/direct-write.d.ts +3 -0
  162. package/dist/src/modules/workspace/direct-write.d.ts.map +1 -1
  163. package/dist/src/modules/workspace/direct-write.js +59 -24
  164. package/dist/src/modules/workspace/direct-write.js.map +1 -1
  165. package/dist/src/modules/workspace/disk-agreement.d.ts +33 -11
  166. package/dist/src/modules/workspace/disk-agreement.d.ts.map +1 -1
  167. package/dist/src/modules/workspace/disk-agreement.js +48 -17
  168. package/dist/src/modules/workspace/disk-agreement.js.map +1 -1
  169. package/dist/src/modules/workspace/edit-text.d.ts +2 -1
  170. package/dist/src/modules/workspace/edit-text.d.ts.map +1 -1
  171. package/dist/src/modules/workspace/edit-text.js +4 -2
  172. package/dist/src/modules/workspace/edit-text.js.map +1 -1
  173. package/dist/src/modules/workspace/effects.d.ts +4 -2
  174. package/dist/src/modules/workspace/effects.d.ts.map +1 -1
  175. package/dist/src/modules/workspace/effects.js +67 -50
  176. package/dist/src/modules/workspace/effects.js.map +1 -1
  177. package/dist/src/modules/workspace/index.d.ts +63 -51
  178. package/dist/src/modules/workspace/index.d.ts.map +1 -1
  179. package/dist/src/modules/workspace/index.js +980 -547
  180. package/dist/src/modules/workspace/index.js.map +1 -1
  181. package/dist/src/modules/workspace/observe.d.ts +125 -82
  182. package/dist/src/modules/workspace/observe.d.ts.map +1 -1
  183. package/dist/src/modules/workspace/observe.js +465 -241
  184. package/dist/src/modules/workspace/observe.js.map +1 -1
  185. package/dist/src/modules/workspace/read-page.d.ts.map +1 -1
  186. package/dist/src/modules/workspace/read-page.js +7 -1
  187. package/dist/src/modules/workspace/read-page.js.map +1 -1
  188. package/dist/src/modules/workspace/reconcile.d.ts +7 -1
  189. package/dist/src/modules/workspace/reconcile.d.ts.map +1 -1
  190. package/dist/src/modules/workspace/reconcile.js +156 -50
  191. package/dist/src/modules/workspace/reconcile.js.map +1 -1
  192. package/dist/src/modules/workspace/result-text.d.ts +1 -0
  193. package/dist/src/modules/workspace/result-text.d.ts.map +1 -1
  194. package/dist/src/modules/workspace/result-text.js.map +1 -1
  195. package/dist/src/modules/workspace/sync.d.ts +9 -0
  196. package/dist/src/modules/workspace/sync.d.ts.map +1 -1
  197. package/dist/src/modules/workspace/sync.js +14 -0
  198. package/dist/src/modules/workspace/sync.js.map +1 -1
  199. package/dist/src/modules/workspace/types.d.ts +96 -43
  200. package/dist/src/modules/workspace/types.d.ts.map +1 -1
  201. package/dist/src/modules/workspace/types.js.map +1 -1
  202. package/dist/src/modules/workspace/watcher.d.ts.map +1 -1
  203. package/dist/src/modules/workspace/watcher.js +8 -3
  204. package/dist/src/modules/workspace/watcher.js.map +1 -1
  205. package/dist/src/notice-tags.d.ts +79 -0
  206. package/dist/src/notice-tags.d.ts.map +1 -0
  207. package/dist/src/notice-tags.js +83 -0
  208. package/dist/src/notice-tags.js.map +1 -0
  209. package/dist/src/prose-drafts.d.ts +4 -3
  210. package/dist/src/prose-drafts.d.ts.map +1 -1
  211. package/dist/src/prose-drafts.js +3 -2
  212. package/dist/src/prose-drafts.js.map +1 -1
  213. package/dist/src/reasoning-loss-notice.d.ts +23 -0
  214. package/dist/src/reasoning-loss-notice.d.ts.map +1 -0
  215. package/dist/src/reasoning-loss-notice.js +52 -0
  216. package/dist/src/reasoning-loss-notice.js.map +1 -0
  217. package/dist/src/reasoning-loss.d.ts +24 -0
  218. package/dist/src/reasoning-loss.d.ts.map +1 -0
  219. package/dist/src/reasoning-loss.js +102 -0
  220. package/dist/src/reasoning-loss.js.map +1 -0
  221. package/dist/src/send-tools.d.ts +3 -2
  222. package/dist/src/send-tools.d.ts.map +1 -1
  223. package/dist/src/send-tools.js +3 -2
  224. package/dist/src/send-tools.js.map +1 -1
  225. package/dist/src/sent-rows.js +1 -1
  226. package/dist/src/silent-heartbeat.d.ts +1 -53
  227. package/dist/src/silent-heartbeat.d.ts.map +1 -1
  228. package/dist/src/silent-heartbeat.js +56 -3
  229. package/dist/src/silent-heartbeat.js.map +1 -1
  230. package/dist/src/supplied-text.d.ts +35 -0
  231. package/dist/src/supplied-text.d.ts.map +1 -0
  232. package/dist/src/supplied-text.js +141 -0
  233. package/dist/src/supplied-text.js.map +1 -0
  234. package/dist/src/thinking-drops.d.ts +4 -11
  235. package/dist/src/thinking-drops.d.ts.map +1 -1
  236. package/dist/src/thinking-drops.js +7 -14
  237. package/dist/src/thinking-drops.js.map +1 -1
  238. package/dist/src/tool-args.d.ts +12 -0
  239. package/dist/src/tool-args.d.ts.map +1 -1
  240. package/dist/src/tool-args.js +39 -4
  241. package/dist/src/tool-args.js.map +1 -1
  242. package/dist/src/tool-image-ledger.d.ts.map +1 -1
  243. package/dist/src/tool-image-ledger.js +2 -1
  244. package/dist/src/tool-image-ledger.js.map +1 -1
  245. package/dist/src/tool-presentation.d.ts.map +1 -1
  246. package/dist/src/tool-presentation.js +5 -4
  247. package/dist/src/tool-presentation.js.map +1 -1
  248. package/dist/src/tool-result-guard.d.ts +8 -4
  249. package/dist/src/tool-result-guard.d.ts.map +1 -1
  250. package/dist/src/tool-result-guard.js +23 -11
  251. package/dist/src/tool-result-guard.js.map +1 -1
  252. package/dist/src/tune-out/coordinator.d.ts +3 -2
  253. package/dist/src/tune-out/coordinator.d.ts.map +1 -1
  254. package/dist/src/tune-out/coordinator.js +24 -26
  255. package/dist/src/tune-out/coordinator.js.map +1 -1
  256. package/dist/src/tune-out/tools.d.ts +4 -1
  257. package/dist/src/tune-out/tools.d.ts.map +1 -1
  258. package/dist/src/types/agent.d.ts +63 -18
  259. package/dist/src/types/agent.d.ts.map +1 -1
  260. package/dist/src/types/events.d.ts +18 -8
  261. package/dist/src/types/events.d.ts.map +1 -1
  262. package/dist/src/types/framework.d.ts +58 -0
  263. package/dist/src/types/framework.d.ts.map +1 -1
  264. package/dist/src/types/module.d.ts +27 -13
  265. package/dist/src/types/module.d.ts.map +1 -1
  266. package/dist/src/types/trace.d.ts +34 -1
  267. package/dist/src/types/trace.d.ts.map +1 -1
  268. package/package.json +3 -3
  269. package/src/agent.ts +76 -12
  270. package/src/api/mcp-server-core.ts +3 -14
  271. package/src/api/server.ts +11 -5
  272. package/src/auto-continue.ts +12 -5
  273. package/src/code-execution/isolate/runner.ts +7 -5
  274. package/src/code-execution/isolate/scripts.ts +3 -2
  275. package/src/cut-off-round.ts +98 -0
  276. package/src/framework.ts +3772 -1138
  277. package/src/gate/event-gate.ts +705 -60
  278. package/src/gate/index.ts +9 -1
  279. package/src/gate/reply-judge.ts +512 -0
  280. package/src/gate/types.ts +176 -3
  281. package/src/heartbeat/heartbeat.ts +843 -0
  282. package/src/heartbeat/reminder-time.ts +107 -0
  283. package/src/held-writes.ts +3 -3
  284. package/src/host-presentation.ts +605 -0
  285. package/src/index.ts +5 -1
  286. package/src/line-reader.ts +58 -0
  287. package/src/mcpl/capability-mask.ts +3 -2
  288. package/src/mcpl/channel-registry.ts +139 -53
  289. package/src/mcpl/conversation-router.ts +2 -1
  290. package/src/mcpl/convert-block.ts +17 -4
  291. package/src/mcpl/hook-orchestrator.ts +4 -3
  292. package/src/mcpl/inbound-source.ts +45 -70
  293. package/src/mcpl/modern-connection.ts +11 -7
  294. package/src/mcpl/protocol-family.ts +24 -3
  295. package/src/mcpl/push-handler.ts +2 -2
  296. package/src/mcpl/references.ts +31 -23
  297. package/src/mcpl/server-connection.ts +76 -34
  298. package/src/mcpl/skip-reply-wake.ts +35 -0
  299. package/src/mcpl/tags.ts +13 -2
  300. package/src/mcpl/tool-presence.ts +149 -82
  301. package/src/mcpl/tool-signature.ts +64 -32
  302. package/src/mcpl/toolset-host.ts +212 -110
  303. package/src/mcpl/toolsets-module.ts +277 -91
  304. package/src/mcpl/transport.ts +18 -14
  305. package/src/mcpl/types.ts +32 -13
  306. package/src/module-registry.ts +14 -9
  307. package/src/modules/api/index.ts +4 -2
  308. package/src/modules/discord/index.ts +37 -25
  309. package/src/modules/history/index.ts +30 -18
  310. package/src/modules/sender-name.ts +9 -0
  311. package/src/modules/workspace/answers.ts +151 -0
  312. package/src/modules/workspace/branch-intent.ts +10 -5
  313. package/src/modules/workspace/direct-write.ts +59 -23
  314. package/src/modules/workspace/disk-agreement.ts +52 -21
  315. package/src/modules/workspace/edit-text.ts +7 -5
  316. package/src/modules/workspace/effects.ts +69 -50
  317. package/src/modules/workspace/index.ts +966 -556
  318. package/src/modules/workspace/observe.ts +497 -242
  319. package/src/modules/workspace/read-page.ts +7 -1
  320. package/src/modules/workspace/reconcile.ts +184 -49
  321. package/src/modules/workspace/result-text.ts +2 -0
  322. package/src/modules/workspace/sync.ts +16 -0
  323. package/src/modules/workspace/types.ts +96 -43
  324. package/src/modules/workspace/watcher.ts +8 -3
  325. package/src/notice-tags.ts +84 -0
  326. package/src/prose-drafts.ts +4 -3
  327. package/src/reasoning-loss-notice.ts +64 -0
  328. package/src/reasoning-loss.ts +120 -0
  329. package/src/send-tools.ts +3 -2
  330. package/src/sent-rows.ts +1 -1
  331. package/src/silent-heartbeat.ts +4 -3
  332. package/src/supplied-text.ts +154 -0
  333. package/src/thinking-drops.ts +10 -16
  334. package/src/tool-args.ts +38 -4
  335. package/src/tool-image-ledger.ts +2 -1
  336. package/src/tool-presentation.ts +5 -4
  337. package/src/tool-result-guard.ts +26 -11
  338. package/src/tune-out/coordinator.ts +25 -27
  339. package/src/tune-out/tools.ts +4 -1
  340. package/src/types/agent.ts +63 -18
  341. package/src/types/events.ts +18 -8
  342. package/src/types/framework.ts +59 -0
  343. package/src/types/module.ts +28 -13
  344. package/src/types/trace.ts +36 -1
@@ -5,10 +5,14 @@
5
5
  * what changed; and the hub that holds every server's controller for the
6
6
  * meta-tools (toolsets-module.ts) and for reminders.
7
7
  *
8
- * Hint changes apply as their message is read (RFC §6.1): the connection's
9
- * read tap hands the controller every result of a hinted request (awaited or
10
- * not), every push/event and every tools/hints, of the current transport,
11
- * synchronously and in arrival order, before anything routes the message.
8
+ * Hint changes apply as their message is read (RFC §6.1); a static↔dynamic
9
+ * move takes effect when a tools/list states it (§4.1), and the model's tool
10
+ * list shows it at its next rebuild; until then the tool is called at either
11
+ * place (§4.2). The connection's read tap hands
12
+ * the controller every result of a hinted request (awaited or not, while no
13
+ * more than 1,000 hinted requests await an answer), each tools/list whole, every push/event and every tools/hints, of the
14
+ * current transport, synchronously and in arrival order, before anything
15
+ * routes the message or anything read after it is handled.
12
16
  * What a message's changes did to the model's view is kept by the object
13
17
  * that carried them (`readChanges`), so whoever later handles that object
14
18
  * (the tool result, the push, the meta-tool) renders it next to the content.
@@ -20,12 +24,15 @@
20
24
  * which the toolsets module gathers at its next start as a stored
21
25
  * `afterUser` injection.
22
26
  */
23
- import type { McplServerConnection, HintRead } from './server-connection.js';
27
+ import { nameToTypeBack, quotedValue } from '../supplied-text.js';
28
+ import { McplRequestError, type McplServerConnection, type HintRead } from './server-connection.js';
24
29
  import type { McplServerConfig } from './types.js';
25
30
  import { isToolAllowed } from './tool-policy.js';
31
+ import { redactServerText } from './protocol-family.js';
32
+ import { logDetail, logValueRedacted } from './log-value.js';
26
33
  import {
27
- ToolPresence, emptyChange, isEmptyChange, joinChanges, keyOf,
28
- type ChangeSet, type MetaToolNames,
34
+ ToolPresence, emptyChange, isEmptyChange, keyOf,
35
+ type ChangeSet, type MetaToolNames, type Presence,
29
36
  } from './tool-presence.js';
30
37
  import type { HintedTool } from './tool-signature.js';
31
38
 
@@ -38,7 +45,7 @@ export const META_TOOL_NAMES: MetaToolNames = {
38
45
  close: `${TOOLSETS_MODULE}--close`,
39
46
  };
40
47
 
41
- /** How long an open, close or call is waited for. A late result still applies when it's read. */
48
+ /** How long an open or close is waited for. A late result still applies when it's read. */
42
49
  export const TOOLSET_REQUEST_TIMEOUT_MS = 60_000;
43
50
  /** A toolsets/list after a (re)connect or in a recovery. */
44
51
  const LIST_TIMEOUT_MS = 15_000;
@@ -54,16 +61,28 @@ export interface ToolsetHostDeps {
54
61
  /** A standalone notice into the conversation: a tools/hints change, a late
55
62
  * result's, a reconnect's adoption, a recovery's. `invite`: it suggests an
56
63
  * open. The host applies its usual rules for events. */
57
- notify(serverId: string, text: string, opts: { invite: boolean }): void;
64
+ notify(serverId: string, text: string, opts: { invite: boolean }): boolean;
65
+ /** Whether the server's changes may be put into the conversation unasked
66
+ * (its `pushEvents` grant): notices and reminders. */
67
+ canNotify(serverId: string): boolean;
58
68
  /** The operator's log. */
59
69
  log(line: string): void;
60
70
  /** How a static tool is named in the tool list (`prefix--tool`). */
61
71
  listedName(serverId: string, tool: string): string;
72
+ /** Where the model's published tool list last put a server's tool:
73
+ * offered in it (static), listed but not offered (dynamic), or undefined
74
+ * where it isn't published. RFC §4.2: until its move applies, a tool
75
+ * keeps working where it is. */
76
+ published?(serverId: string, tool: string): Presence | undefined;
77
+ /** The host's own toolsets an agent has open (the built-in one, server
78
+ * tabs): each one's marker and reminder, for RFC §8.3 reminders beside the
79
+ * servers' toolsets. */
80
+ hostOpen?(agentName: string): Array<{ marker: string; reminder: () => string; dueWhenAbsent?: boolean }>;
62
81
  }
63
82
 
64
83
  /** One server's RFC-014 state and traffic. Lives as long as the server's
65
- * configuration, across reconnects and restarts (its `ended` ids and what
66
- * the model was shown carry over). */
84
+ * configuration, across reconnects and the server's restarts (its `ended`
85
+ * ids and what the model was shown carry over), in memory only. */
67
86
  export class ToolsetController {
68
87
  readonly presence: ToolPresence;
69
88
  private connection: McplServerConnection | null = null;
@@ -71,16 +90,10 @@ export class ToolsetController {
71
90
  private epoch = -1;
72
91
  /** What applying a message's hints changed, by the object that carried them. */
73
92
  private readonly readChanges = new WeakMap<object, ChangeSet>();
74
- /** Results whose change no one has rendered yet (tellUnlessClaimed). */
75
- private readonly unclaimed = new Set<object>();
93
+ /** Results whose change no one has taken yet, in the order they were read, with their method (tellUnlessClaimed). */
94
+ private readonly unclaimed = new Map<object, string>();
76
95
  /** eventIds of pushes read on this transport (bounded): a repeat's hints are ignored (RFC §6.1). */
77
96
  private seenEvents = new Set<string>();
78
- /** A paged tools/list being read. */
79
- private listPages: HintedTool[] | null = null;
80
- /** Lists this controller asked for itself, whose change it delivers. */
81
- private ownLists = 0;
82
- /** What lists read while one of its own was out changed, for it to tell. */
83
- private ownListChange: ChangeSet | null = null;
84
97
  private recoveryTimer: ReturnType<typeof setTimeout> | null = null;
85
98
  private recovering = false;
86
99
  private recoverAgain = false;
@@ -95,6 +108,7 @@ export class ToolsetController {
95
108
  META_TOOL_NAMES,
96
109
  (tool) => isToolAllowed(tool, this.config),
97
110
  (tool) => deps.listedName(serverId, tool),
111
+ (tool) => deps.published?.(serverId, tool),
98
112
  );
99
113
  }
100
114
 
@@ -102,6 +116,14 @@ export class ToolsetController {
102
116
  this.deps.log(`${this.serverId}: ${line}`);
103
117
  }
104
118
 
119
+ private field(value: unknown): string {
120
+ return logValueRedacted(value, (text) => redactServerText(text, this.config));
121
+ }
122
+
123
+ private failure(error: unknown): string {
124
+ return logDetail(redactServerText(error instanceof Error ? error.message : String(error), this.config));
125
+ }
126
+
105
127
  /** A restart can bring a new configuration under the same id. */
106
128
  setConfig(config: McplServerConfig): void {
107
129
  this.config = config;
@@ -118,6 +140,10 @@ export class ToolsetController {
118
140
  connection.setReadTap((read, epoch) => this.onRead(connection, read, epoch));
119
141
  }
120
142
 
143
+ release(): void {
144
+ if (this.connection) this.detach(this.connection);
145
+ }
146
+
121
147
  /** The server's connection is gone for good (disconnected, removed). */
122
148
  detach(connection: McplServerConnection): void {
123
149
  if (this.connection !== connection) return;
@@ -139,7 +165,7 @@ export class ToolsetController {
139
165
  /** The live connection, for a request. */
140
166
  private live(): McplServerConnection {
141
167
  const c = this.connection;
142
- if (!c || !c.isConnected) throw new Error(`${this.serverId} is not connected right now; try again later`);
168
+ if (!c || !c.isConnected) throw new McplRequestError(`${this.serverId} is not connected right now; try again later`, 'not-sent');
143
169
  this.sync(c.transportEpoch, c);
144
170
  return c;
145
171
  }
@@ -153,7 +179,6 @@ export class ToolsetController {
153
179
  if (epoch === this.epoch) return;
154
180
  this.epoch = epoch;
155
181
  this.seenEvents = new Set();
156
- this.listPages = null;
157
182
  this.presence.beginConnection(connection.toolsetDeclarations, true);
158
183
  }
159
184
 
@@ -163,7 +188,7 @@ export class ToolsetController {
163
188
  if (connection !== this.connection) return;
164
189
  this.sync(epoch, connection);
165
190
  if (read.kind === 'result') {
166
- this.readResult(read.method, read.params, read.result as Record<string, unknown>, read.awaited);
191
+ this.readResult(read.method, read.params, read.result as Record<string, unknown>, read.awaited, read.straddled === true);
167
192
  return;
168
193
  }
169
194
  if (!this.presence.active || !isObj(read.params)) return;
@@ -178,7 +203,7 @@ export class ToolsetController {
178
203
  // RFC §7: a push carrying hints is never coalesced; when its changes
179
204
  // would apply is undefined, so they're ignored and the state re-read.
180
205
  if (p.coalesce !== undefined && p.coalesce !== null) {
181
- this.log(`push ${String(p.eventId ?? '')} carries hints and coalesce (RFC-014 §7 forbids it): its hints are ignored; recovering`);
206
+ this.log(`push ${this.field(p.eventId ?? '')} carries hints and coalesce (RFC-014 §7 forbids it): its hints are ignored; recovering`);
182
207
  this.scheduleRecovery();
183
208
  return;
184
209
  }
@@ -186,32 +211,29 @@ export class ToolsetController {
186
211
  const eventId = typeof p.eventId === 'string' || typeof p.eventId === 'number' ? String(p.eventId) : '';
187
212
  if (eventId) {
188
213
  if (this.seenEvents.has(eventId)) {
189
- this.log(`push ${eventId} was read before: its hints are ignored`);
214
+ this.log(`push ${this.field(eventId)} was read before: its hints are ignored`);
190
215
  return;
191
216
  }
192
217
  this.seenEvents.add(eventId);
193
218
  if (this.seenEvents.size > 1000) this.seenEvents.delete(this.seenEvents.values().next().value!);
194
219
  }
195
220
  // Applied now, before (and whatever) the host decides about the push.
196
- this.readChanges.set(p, this.applyHints(p.hints, 'push'));
221
+ const cs = this.applyHints(p.hints, 'push');
222
+ this.readChanges.set(p, cs);
223
+ this.tellUnlessClaimed(p, cs, 'push/event');
197
224
  }
198
225
 
199
- private readResult(method: string, params: Record<string, unknown>, res: Record<string, unknown>, awaited: boolean): void {
226
+ private readResult(method: string, params: Record<string, unknown>, res: Record<string, unknown>, awaited: boolean, straddled: boolean): void {
200
227
  let cs: ChangeSet;
201
228
  switch (method) {
202
229
  case 'tools/list': {
203
- const page = Array.isArray(res.tools) ? (res.tools as HintedTool[]) : [];
204
- // A first page (no cursor) starts the list; a page with no next cursor ends it.
205
- if (params.cursor === undefined || this.listPages === null) this.listPages = [];
206
- this.listPages.push(...page);
207
- if (res.nextCursor !== undefined && res.nextCursor !== null) return;
208
- const tools = this.listPages;
209
- this.listPages = null;
210
- cs = this.readToolList(tools, res);
211
- // The framework's own refresh (a list change, a reconnect) told here;
212
- // a list this controller asked for is told by whoever asked.
213
- if (this.ownLists === 0) this.deliver(cs);
214
- else this.ownListChange = joinChanges(this.ownListChange ?? emptyChange(), cs);
230
+ if (!Array.isArray(res.tools)) return;
231
+ cs = this.readToolList(res.tools as HintedTool[], res);
232
+ if (straddled && !this.recovering) {
233
+ this.log('tools/list pages carried different revs, so its earlier pages are older than changes read between them: recovering');
234
+ this.scheduleRecovery();
235
+ }
236
+ this.deliver(cs);
215
237
  return;
216
238
  }
217
239
  case 'toolsets/list':
@@ -259,9 +281,9 @@ export class ToolsetController {
259
281
  private applyHints(h: unknown, source: string): ChangeSet {
260
282
  if (h === undefined || !this.presence.active) return emptyChange();
261
283
  const before = this.presence.view();
262
- const last = this.presence.rev;
284
+ const last = this.field(this.presence.rev);
263
285
  const r = this.presence.apply(h);
264
- const rev = String((h as { rev?: unknown } | null)?.rev);
286
+ const rev = this.field((h as { rev?: unknown } | null)?.rev);
265
287
  if (r.behind) {
266
288
  this.log(`hint change rev ${rev} (${source}) is not after rev ${last}: ignored; recovering from tools/list and toolsets/list`);
267
289
  this.scheduleRecovery();
@@ -271,6 +293,10 @@ export class ToolsetController {
271
293
  } else if (!r.applied) {
272
294
  this.log(`a malformed hint change (${source}) was ignored`);
273
295
  }
296
+ if (r.unlisted) {
297
+ this.log(`hint change rev ${rev} (${source}) names a tool tools/list hasn't listed: recovering from tools/list and toolsets/list`);
298
+ this.scheduleRecovery();
299
+ }
274
300
  const cs = this.presence.diff(before, this.presence.view());
275
301
  cs.suggest.push(...r.suggest);
276
302
  return cs;
@@ -281,7 +307,7 @@ export class ToolsetController {
281
307
  const before = this.presence.view();
282
308
  this.presence.setTools(tools);
283
309
  if (this.presence.active && this.presence.noteListRev(isObj(last._meta) ? last._meta['mcpl/rev'] : undefined)) {
284
- this.log(`tools/list was written after rev ${String(isObj(last._meta) ? last._meta['mcpl/rev'] : '')}, past the last rev read: a message was lost; recovering`);
310
+ this.log(`tools/list was written after rev ${this.field(isObj(last._meta) ? last._meta['mcpl/rev'] : '')}, past the last rev read: a message was lost; recovering`);
285
311
  this.scheduleRecovery();
286
312
  }
287
313
  return this.presence.diff(before, this.presence.view());
@@ -290,39 +316,78 @@ export class ToolsetController {
290
316
  /** toolsets/list, as read: a full statement of dynamic-tool state under its own rev (RFC §5.3, §6.1). */
291
317
  private readToolsetList(res: Record<string, unknown>): ChangeSet {
292
318
  if (!this.presence.active) return emptyChange();
319
+ if (!Array.isArray(res.open)) {
320
+ this.log('a toolsets/list result with no open list was ignored');
321
+ return emptyChange();
322
+ }
293
323
  const before = this.presence.view();
294
- const { lost, ended, suggest, skipped } = this.presence.adopt(res);
324
+ const { lost, ended, suggest, skipped, unlisted } = this.presence.adopt(res);
295
325
  if (skipped && !this.recovering) {
296
326
  // Per-instance state is restated in full; what else was lost (presence) comes from tools/list.
297
327
  this.log('toolsets/list skipped a rev: re-reading tools/list');
298
328
  this.scheduleRecovery();
299
329
  }
330
+ if (unlisted && !this.recovering) {
331
+ this.log('toolsets/list names a tool tools/list hasn\'t listed: re-reading tools/list');
332
+ this.scheduleRecovery();
333
+ }
300
334
  const cs = this.presence.diff(before, this.presence.view());
301
335
  cs.suggest.push(...suggest);
302
336
  // A lost toolset gets its note, not also a "closed" line.
303
337
  cs.changes = cs.changes.filter((c) => !lost.some((o) => keyOf(o.toolset, o.instance) === c.key));
304
- cs.notes.push(...this.presence.adoptionNotes(lost, ended));
338
+ const told = this.presence.adoptionNotes(lost, ended);
339
+ cs.notes.push(...told.notes);
340
+ if (told.ended.length) cs.ended = [...(cs.ended ?? []), ...told.ended];
305
341
  return cs;
306
342
  }
307
343
 
308
- /** A change with nothing else to ride on, told as a notice. */
344
+ /** A change with nothing else to ride on, told as a notice, after every result's change still unclaimed. */
309
345
  private deliver(cs: ChangeSet): void {
310
- if (isEmptyChange(cs)) return;
311
- const text = this.presence.render(cs);
312
- if (!text) return;
313
- this.deps.notify(this.serverId, text, { invite: cs.suggest.some((s) => s.action === 'open') });
346
+ this.tellUnclaimed();
347
+ this.notice(cs);
348
+ }
349
+
350
+ /**
351
+ * Unclaimed carriers' changes, told as notices in the order they were read,
352
+ * through `upTo` when given. When no notice can be written, a push keeps its
353
+ * change for the framework's push path, which takes it when the push gets
354
+ * there; a result's is dropped, whether or not its caller is still to
355
+ * resume.
356
+ */
357
+ private tellUnclaimed(upTo?: object): void {
358
+ for (const [res, method] of [...this.unclaimed]) {
359
+ this.unclaimed.delete(res);
360
+ const cs = this.readChanges.get(res) ?? emptyChange();
361
+ const sent = this.notice(cs);
362
+ if (sent !== 'refused' || method !== 'push/event') this.readChanges.delete(res);
363
+ if (sent === 'told') this.log(`${method}'s tool change wasn't shown with it: told as a notice`);
364
+ if (res === upTo) return;
365
+ }
366
+ }
367
+
368
+ /** Whether a notice was written, there was nothing left to tell (`empty`), or one couldn't be written (`refused`). */
369
+ private notice(cs: ChangeSet): 'told' | 'empty' | 'refused' {
370
+ if (isEmptyChange(cs)) return 'empty';
371
+ const text = this.presence.render(cs, { mark: false });
372
+ if (!text) return 'empty';
373
+ if (!this.deps.notify(this.serverId, text, { invite: cs.suggest.some((s) => s.action === 'open') })) return 'refused';
374
+ this.presence.markTold(cs.ended);
375
+ this.presence.markShown(cs);
376
+ return 'told';
314
377
  }
315
378
 
316
379
  /**
317
380
  * A dynamic tool called by its namespaced name, as if it were listed (a
318
381
  * model imitating an earlier call, a module): refused, so every call into
319
382
  * a toolset goes through act, which knows its instance and gates closed
320
- * groups (RFC §5.4, §5.5). Null for any other tool.
383
+ * groups (RFC §5.4, §5.5). Null for any other tool, including one the tool
384
+ * list last published or the server's latest tools/list puts as static
385
+ * (shownAs: RFC §4.2, until its move applies).
321
386
  */
322
387
  dynamicCallRefusal(tool: string, calledAs: string): string | null {
323
- if (!this.presence.active || !this.presence.tool(tool) || this.presence.presenceOf(tool) !== 'dynamic') return null;
324
- return `There's no tool "${calledAs}" in your tool list, so nothing ran: ${tool} is a dynamic tool of ${this.serverId}, ` +
325
- `called with ${META_TOOL_NAMES.act} {server:"${this.serverId}", tool:"${tool}", args}. ${this.presence.toolStatus(tool)}.`;
388
+ if (!this.presence.active || !this.presence.tool(tool) || this.presence.shownAs(tool, 'static')) return null;
389
+ return `There's no tool "${calledAs}" in your tool list, so nothing ran: ${nameToTypeBack(tool)} is a dynamic tool of ${this.serverId}, ` +
390
+ `called with ${META_TOOL_NAMES.act} {server:"${this.serverId}", tool:${quotedValue(tool)}, args}. ${this.presence.toolStatus(tool)}.`;
326
391
  }
327
392
 
328
393
  /**
@@ -332,35 +397,56 @@ export class ToolsetController {
332
397
  * run (the next macrotask), such as the result of a call the framework makes
333
398
  * itself (a reaction, a send, an accounted side effect), is told as a notice
334
399
  * instead, as a standalone tools/hints is (RFC §6.1: every change reaches the
335
- * agent once).
400
+ * agent once), or dropped as tellUnclaimed says when none can be written.
336
401
  */
337
402
  private tellUnlessClaimed(res: object, cs: ChangeSet, method: string): void {
338
403
  if (isEmptyChange(cs)) return;
339
- this.unclaimed.add(res);
404
+ this.unclaimed.set(res, method);
340
405
  setImmediate(() => {
341
- if (!this.unclaimed.delete(res)) return;
342
- this.log(`${method} result's tool change wasn't shown with it: told as a notice`);
343
- this.deliver(cs);
406
+ if (this.unclaimed.has(res)) this.tellUnclaimed(res);
344
407
  });
345
408
  }
346
409
 
347
- /** What applying `carrier`'s hints changed (a tool result, a push's params),
348
- * rendered, for its handler to show with it; '' for nothing. Taking it
349
- * means it's told there, not as a notice. */
350
- changeText(carrier: unknown): string {
351
- if (!isObj(carrier)) return '';
352
- this.unclaimed.delete(carrier);
353
- const cs = this.readChanges.get(carrier);
354
- return cs ? this.presence.render(cs) : '';
355
- }
356
410
 
357
- /** The change a carrier's hints made, or an empty one; taken as changeText's is. */
358
- changeOf(carrier: unknown): ChangeSet {
411
+ /**
412
+ * The change a carrier's hints made (a tool result, a push's params), taken
413
+ * by a caller that shows it to an agent with the carrier: it isn't told as a
414
+ * notice, and the caller says when it has reached an agent (told). Call it
415
+ * synchronously as the carrier is read, before the next macrotask, when an
416
+ * unclaimed carrier's change goes out as a notice instead (tellUnclaimed
417
+ * says what happens when none can be written). A change already gone that
418
+ * way is empty here. Unclaimed carriers read before this one
419
+ * are told first, as notices, so no later change is shown before them.
420
+ */
421
+ takeChange(carrier: unknown): ChangeSet {
359
422
  if (!isObj(carrier)) return emptyChange();
423
+ if (this.unclaimed.has(carrier)) {
424
+ let before: object | undefined;
425
+ for (const res of this.unclaimed.keys()) {
426
+ if (res === carrier) break;
427
+ before = res;
428
+ }
429
+ if (before) this.tellUnclaimed(before);
430
+ }
360
431
  this.unclaimed.delete(carrier);
361
432
  return this.readChanges.get(carrier) ?? emptyChange();
362
433
  }
363
434
 
435
+ /** A change its taker showed to an agent: its ended records count as told, and the definitions it describes as shown, unless the taker showed it without its blocks (`blocksShown: false`). */
436
+ told(cs: ChangeSet): void {
437
+ this.presence.markTold(cs.ended);
438
+ if (cs.blocksShown !== false) this.presence.markShown(cs);
439
+ }
440
+
441
+ /** A change its taker couldn't show, told as a notice instead, or dropped when none can be written. */
442
+ tell(cs: ChangeSet): void {
443
+ this.notice(cs);
444
+ }
445
+
446
+ private changeOf(carrier: object): ChangeSet {
447
+ return this.readChanges.get(carrier) ?? emptyChange();
448
+ }
449
+
364
450
  // ── recovery (RFC §6.1) ──
365
451
 
366
452
  private scheduleRecovery(): void {
@@ -380,19 +466,23 @@ export class ToolsetController {
380
466
  /**
381
467
  * Recovering: re-read tools/list (presence, declared definitions), then
382
468
  * toolsets/list (dynamic-tool state in full), so every tool the statement
383
- * names is already known; each applies where it's read. What changed is
384
- * told. (stdio and WebSocket deliver in order, so no early rev is held.)
469
+ * names is already known, unless tools/list failed: a tool the last list
470
+ * lacks then stays unknown until a later change or list starts a recovery.
471
+ * Each applies where it's read. What
472
+ * changed is told. (stdio and WebSocket deliver in order, so no early rev is
473
+ * held.)
385
474
  */
386
475
  private async recover(): Promise<void> {
387
476
  const connection = this.connection;
388
477
  if (!connection || !connection.isConnected) return;
389
478
  this.recovering = true;
390
479
  try {
391
- const tools = await this.ownToolList(connection);
480
+ await this.ownToolList(connection);
481
+ // RFC §6.1: toolsets/list after tools/list, so every tool it names is known, unless tools/list failed.
392
482
  if (connection !== this.connection) return;
393
483
  const lists = await this.ownToolsetList(connection, LIST_TIMEOUT_MS, 'recovery');
394
484
  if (connection !== this.connection) return;
395
- this.deliver(joinChanges(tools, lists));
485
+ this.deliver(lists);
396
486
  } finally {
397
487
  this.recovering = false;
398
488
  if (this.recoverAgain) {
@@ -402,29 +492,21 @@ export class ToolsetController {
402
492
  }
403
493
  }
404
494
 
405
- private async ownToolList(connection: McplServerConnection): Promise<ChangeSet> {
406
- if (this.ownLists++ === 0) this.ownListChange = null;
495
+ /** The tap applies the list and tells its change as it's read. */
496
+ private async ownToolList(connection: McplServerConnection): Promise<void> {
407
497
  try {
408
- // The tap applied each page as it was read, and kept what the whole
409
- // list changed (ownListChange).
410
498
  await connection.sendToolsList();
411
499
  } catch (error) {
412
- this.log(`tools/list failed: ${error instanceof Error ? error.message : String(error)}`);
413
- } finally {
414
- this.ownLists--;
500
+ this.log(`tools/list failed: ${this.failure(error)}`);
415
501
  }
416
- const collected = this.ownListChange ?? emptyChange();
417
- if (this.ownLists === 0) this.ownListChange = null;
418
- else this.ownListChange = emptyChange();
419
- return collected;
420
502
  }
421
503
 
422
504
  private async ownToolsetList(connection: McplServerConnection, timeoutMs: number, why: string): Promise<ChangeSet> {
423
505
  try {
424
506
  const r = await connection.sendToolsetsList(timeoutMs);
425
- return this.changeOf(r);
507
+ return this.takeChange(r);
426
508
  } catch (error) {
427
- this.log(`toolsets/list (${why}) failed: ${error instanceof Error ? error.message : String(error)}`);
509
+ this.log(`toolsets/list (${why}) failed: ${this.failure(error)}`);
428
510
  return emptyChange();
429
511
  }
430
512
  }
@@ -441,35 +523,41 @@ export class ToolsetController {
441
523
  this.sync(connection.transportEpoch, connection);
442
524
  if (!this.presence.active) return;
443
525
  const epoch = connection.transportEpoch;
444
- const tools = await this.ownToolList(connection);
526
+ await this.ownToolList(connection);
445
527
  if (connection !== this.connection || connection.transportEpoch !== epoch) return;
446
528
  const lists = await this.ownToolsetList(connection, LIST_TIMEOUT_MS, 'after connecting');
447
529
  if (connection !== this.connection || connection.transportEpoch !== epoch) return;
448
- this.deliver(joinChanges(tools, lists));
530
+ this.deliver(lists);
449
531
  }
450
532
 
451
533
  /**
452
534
  * Re-read what the server holds open (titles, visibility) without telling
453
- * it: for look, which restates everything anyway. Returns notes for what
454
- * the server no longer holds.
535
+ * it, for look. Returns what the re-read changed, taken from the notice it
536
+ * would otherwise get when `shown` (its result reaches an agent): look then
537
+ * either shows it (told) or hands it back (tell). Otherwise the change is
538
+ * left to its notice, and returned to be shown to the caller alone. Null
539
+ * when nothing was read.
455
540
  */
456
- async refreshOpen(): Promise<string[]> {
457
- if (!this.presence.active) return [];
541
+ async refreshOpen(opts: { shown: boolean }): Promise<ChangeSet | null> {
542
+ if (!this.presence.active) return null;
458
543
  const connection = this.connection;
459
- if (!connection || !connection.isConnected) return [];
544
+ if (!connection || !connection.isConnected) return null;
460
545
  try {
461
546
  const r = await connection.sendToolsetsList(REFRESH_TIMEOUT_MS);
462
- return this.changeOf(r).notes;
547
+ if (!isObj(r)) return null;
548
+ return opts.shown ? this.takeChange(r) : this.changeOf(r);
463
549
  } catch (error) {
464
- this.log(`toolsets/list (look) failed: ${error instanceof Error ? error.message : String(error)}`);
465
- return [];
550
+ this.log(`toolsets/list (look) failed: ${this.failure(error)}`);
551
+ return null;
466
552
  }
467
553
  }
468
554
 
469
555
  // ── the host's acts ──
470
556
 
471
- /** toolsets/open (RFC §5.2). The server's refusal rejects. */
472
- async open(toolset: string, instance: string | undefined, options: Record<string, unknown> | undefined): Promise<{
557
+ /** toolsets/open (RFC §5.2). The server's refusal rejects. Its change is
558
+ * taken when `shown` (its result reaches an agent), else left to its
559
+ * notice and returned for the caller alone; close's is the same. */
560
+ async open(toolset: string, instance: string | undefined, options: Record<string, unknown> | undefined, opts: { shown: boolean }): Promise<{
473
561
  key: string; title?: string; content: unknown[]; change: ChangeSet; adopted: boolean;
474
562
  }> {
475
563
  const connection = this.live();
@@ -477,24 +565,24 @@ export class ToolsetController {
477
565
  const res = isObj(r) ? r : {};
478
566
  const inst = typeof res.instance === 'string' && res.instance ? res.instance : instance;
479
567
  // RFC §5.2: a proposed name is used as given or the open refused.
480
- if (instance && inst !== instance) this.log(`proposed instance "${instance}" came back as "${inst}" (RFC-014 §5.2 forbids renaming): using the server's`);
568
+ if (instance && inst !== instance) this.log(`proposed instance ${this.field(instance)} came back as ${this.field(inst)} (RFC-014 §5.2 forbids renaming): using the server's`);
481
569
  const key = keyOf(toolset, this.presence.declared[toolset]?.instances ? inst : undefined);
482
570
  const title = typeof res.title === 'string' && res.title ? res.title : this.presence.openRecords().find((o) => keyOf(o.toolset, o.instance) === key)?.title;
483
571
  return {
484
572
  key,
485
573
  ...(title ? { title } : {}),
486
574
  content: Array.isArray(res.content) ? res.content : [],
487
- change: this.changeOf(res),
575
+ change: opts.shown ? this.takeChange(res) : this.changeOf(res),
488
576
  adopted: res.adopted === true,
489
577
  };
490
578
  }
491
579
 
492
580
  /** toolsets/close (RFC §5.2). */
493
- async close(toolset: string, instance: string | undefined): Promise<{ content: unknown[]; change: ChangeSet }> {
581
+ async close(toolset: string, instance: string | undefined, opts: { shown: boolean }): Promise<{ content: unknown[]; change: ChangeSet }> {
494
582
  const connection = this.live();
495
583
  const r = await connection.sendToolsetsClose({ toolset, ...(instance ? { instance } : {}) }, TOOLSET_REQUEST_TIMEOUT_MS);
496
584
  const res = isObj(r) ? r : {};
497
- return { content: Array.isArray(res.content) ? res.content : [], change: this.changeOf(res) };
585
+ return { content: Array.isArray(res.content) ? res.content : [], change: opts.shown ? this.takeChange(res) : this.changeOf(res) };
498
586
  }
499
587
 
500
588
  /** Open or close an undeclared group (RFC §5.4): the host's own act; the server isn't told. */
@@ -518,7 +606,7 @@ export class ToolsetController {
518
606
  this.presence.deactivate();
519
607
  this.log('the server withdrew toolHints: every tool is static from now on');
520
608
  const cs = this.presence.diff(before, this.presence.view());
521
- cs.notes.push(`${this.serverId} no longer presents toolsets: its tools are all in your tool list now.`);
609
+ cs.notes.push(`${this.serverId} no longer presents toolsets: its tools are called directly now, by name, not through ${META_TOOL_NAMES.act}.`);
522
610
  this.deliver(cs);
523
611
  return;
524
612
  }
@@ -532,11 +620,15 @@ export class ToolsetController {
532
620
  this.deliver(this.presence.diff(before, this.presence.view()));
533
621
  }
534
622
 
535
- /** Apply pending static↔dynamic moves (RFC §4.2) at a tool-list boundary, and say where they went. */
536
- applyMoves(): string[] {
537
- return this.presence.applyMoves().map(({ name, to }) => to === 'dynamic'
538
- ? `${name} left your tool list: it's announced when available now, and called with ${META_TOOL_NAMES.act} {server:"${this.serverId}", tool:"${name}", args}.`
539
- : `${name} is in your tool list now, as ${this.deps.listedName(this.serverId, name)}.`);
623
+ /** At a rebuild of the model's tool list (RFC §4.2), tell where the tools it moved went: `moves`, those still pending, or every one pending. A move a list has undone since the rebuild listed the server isn't told; the next rebuild publishes the undoing. */
624
+ applyMoves(moves?: Array<{ name: string; to: Presence }>): void {
625
+ const moved = this.presence.applyMoves(moves);
626
+ if (!moved.length) return;
627
+ const cs = emptyChange();
628
+ cs.notes.push(...moved.map(({ name, to }) => to === 'dynamic'
629
+ ? `${nameToTypeBack(name)} left your tool list: it's announced when available, and called with ${META_TOOL_NAMES.act} {server:"${this.serverId}", tool:${quotedValue(name)}, args}.`
630
+ : `${nameToTypeBack(name)} is in your tool list now, as ${nameToTypeBack(this.deps.listedName(this.serverId, name))}.`));
631
+ this.deliver(cs);
540
632
  }
541
633
  }
542
634
 
@@ -565,8 +657,11 @@ export class ToolsetHub {
565
657
  return this.controllers.get(serverId);
566
658
  }
567
659
 
568
- /** A server's configuration went (removed): forget its controller. */
660
+ /** A server's configuration went (removed): its controller lets go of the
661
+ * connection it holds, and is forgotten, so a server added later under
662
+ * the same id starts from nothing. */
569
663
  forget(serverId: string): void {
664
+ this.controllers.get(serverId)?.release();
570
665
  this.controllers.delete(serverId);
571
666
  }
572
667
 
@@ -591,8 +686,9 @@ export class ToolsetHub {
591
686
  * reminder due.
592
687
  */
593
688
  observeCompile(agentName: string, messages: readonly unknown[]): void {
689
+ const host = this.deps.hostOpen?.(agentName) ?? [];
594
690
  const open = this.openMarkers();
595
- if (open.length === 0) {
691
+ if (open.length === 0 && host.length === 0) {
596
692
  this.seen.delete(agentName);
597
693
  this.due.delete(agentName);
598
694
  return;
@@ -606,6 +702,9 @@ export class ToolsetHub {
606
702
  }
607
703
  for (const marker of [...seen]) if (!open.includes(marker)) seen.delete(marker);
608
704
  this.seen.set(agentName, seen);
705
+ // A host toolset open across a restart was never seen here: it's due
706
+ // whenever a request doesn't carry its marker.
707
+ for (const h of host) if (h.dueWhenAbsent && !text.includes(h.marker)) missing = true;
609
708
  // Due until a request carries every marker the agent has seen again: a
610
709
  // gather that doesn't send (a preview) leaves it due for the next start.
611
710
  if (missing) this.due.add(agentName);
@@ -615,7 +714,10 @@ export class ToolsetHub {
615
714
  /** The reminder due for `agentName`, or null when none is. */
616
715
  reminderFor(agentName: string): string | null {
617
716
  if (!this.due.has(agentName)) return null;
618
- const parts = this.presenting().map((c) => c.presence.reminder()).filter((r): r is string => !!r);
717
+ const parts = [
718
+ ...(this.deps.hostOpen?.(agentName) ?? []).map((h) => h.reminder()),
719
+ ...this.presenting().filter((c) => this.deps.canNotify(c.serverId)).map((c) => c.presence.reminder()).filter((r): r is string => !!r),
720
+ ];
619
721
  if (parts.length === 0) return null;
620
722
  return `Open toolsets (a reminder: their earlier announcements have left your context):\n${parts.join('\n')}`;
621
723
  }