@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
@@ -1,5 +1,35 @@
1
+ /**
2
+ * MCPL RFC-014, host side: per server, the controller that feeds the
3
+ * server's tool presence (tool-presence.ts) from what its connection reads,
4
+ * sends toolsets/open, close and list, recovers from a lost rev, and renders
5
+ * what changed; and the hub that holds every server's controller for the
6
+ * meta-tools (toolsets-module.ts) and for reminders.
7
+ *
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.
16
+ * What a message's changes did to the model's view is kept by the object
17
+ * that carried them (`readChanges`), so whoever later handles that object
18
+ * (the tool result, the push, the meta-tool) renders it next to the content.
19
+ *
20
+ * Reminders (RFC §8.3): after each compile built to send, the hub reads
21
+ * which open toolsets' markers (`[server/toolset:instance]`) the request
22
+ * carried. A marker an agent has had in view, gone from a later request
23
+ * (compressed, or out of the window), makes a reminder due for that agent,
24
+ * which the toolsets module gathers at its next start as a stored
25
+ * `afterUser` injection.
26
+ */
27
+ import { nameToTypeBack, quotedValue } from '../supplied-text.js';
28
+ import { McplRequestError } from './server-connection.js';
1
29
  import { isToolAllowed } from './tool-policy.js';
2
- import { ToolPresence, emptyChange, isEmptyChange, joinChanges, keyOf, } from './tool-presence.js';
30
+ import { redactServerText } from './protocol-family.js';
31
+ import { logDetail, logValueRedacted } from './log-value.js';
32
+ import { ToolPresence, emptyChange, isEmptyChange, keyOf, } from './tool-presence.js';
3
33
  /** The meta-tools' module, whose tools are `toolsets--open` and so on. */
4
34
  export const TOOLSETS_MODULE = 'toolsets';
5
35
  export const META_TOOL_NAMES = {
@@ -8,7 +38,7 @@ export const META_TOOL_NAMES = {
8
38
  look: `${TOOLSETS_MODULE}--look`,
9
39
  close: `${TOOLSETS_MODULE}--close`,
10
40
  };
11
- /** How long an open, close or call is waited for. A late result still applies when it's read. */
41
+ /** How long an open or close is waited for. A late result still applies when it's read. */
12
42
  export const TOOLSET_REQUEST_TIMEOUT_MS = 60_000;
13
43
  /** A toolsets/list after a (re)connect or in a recovery. */
14
44
  const LIST_TIMEOUT_MS = 15_000;
@@ -19,8 +49,8 @@ const RECOVERY_DEBOUNCE_MS = 200;
19
49
  const isObj = (v) => typeof v === 'object' && v !== null && !Array.isArray(v);
20
50
  const metaHints = (res) => (isObj(res._meta) ? res._meta['mcpl/hints'] : undefined);
21
51
  /** One server's RFC-014 state and traffic. Lives as long as the server's
22
- * configuration, across reconnects and restarts (its `ended` ids and what
23
- * the model was shown carry over). */
52
+ * configuration, across reconnects and the server's restarts (its `ended`
53
+ * ids and what the model was shown carry over), in memory only. */
24
54
  export class ToolsetController {
25
55
  serverId;
26
56
  config;
@@ -31,16 +61,10 @@ export class ToolsetController {
31
61
  epoch = -1;
32
62
  /** What applying a message's hints changed, by the object that carried them. */
33
63
  readChanges = new WeakMap();
34
- /** Results whose change no one has rendered yet (tellUnlessClaimed). */
35
- unclaimed = new Set();
64
+ /** Results whose change no one has taken yet, in the order they were read, with their method (tellUnlessClaimed). */
65
+ unclaimed = new Map();
36
66
  /** eventIds of pushes read on this transport (bounded): a repeat's hints are ignored (RFC §6.1). */
37
67
  seenEvents = new Set();
38
- /** A paged tools/list being read. */
39
- listPages = null;
40
- /** Lists this controller asked for itself, whose change it delivers. */
41
- ownLists = 0;
42
- /** What lists read while one of its own was out changed, for it to tell. */
43
- ownListChange = null;
44
68
  recoveryTimer = null;
45
69
  recovering = false;
46
70
  recoverAgain = false;
@@ -48,11 +72,17 @@ export class ToolsetController {
48
72
  this.serverId = serverId;
49
73
  this.config = config;
50
74
  this.deps = deps;
51
- this.presence = new ToolPresence(serverId, META_TOOL_NAMES, (tool) => isToolAllowed(tool, this.config), (tool) => deps.listedName(serverId, tool));
75
+ this.presence = new ToolPresence(serverId, META_TOOL_NAMES, (tool) => isToolAllowed(tool, this.config), (tool) => deps.listedName(serverId, tool), (tool) => deps.published?.(serverId, tool));
52
76
  }
53
77
  log(line) {
54
78
  this.deps.log(`${this.serverId}: ${line}`);
55
79
  }
80
+ field(value) {
81
+ return logValueRedacted(value, (text) => redactServerText(text, this.config));
82
+ }
83
+ failure(error) {
84
+ return logDetail(redactServerText(error instanceof Error ? error.message : String(error), this.config));
85
+ }
56
86
  /** A restart can bring a new configuration under the same id. */
57
87
  setConfig(config) {
58
88
  this.config = config;
@@ -67,6 +97,10 @@ export class ToolsetController {
67
97
  this.connection = connection;
68
98
  connection.setReadTap((read, epoch) => this.onRead(connection, read, epoch));
69
99
  }
100
+ release() {
101
+ if (this.connection)
102
+ this.detach(this.connection);
103
+ }
70
104
  /** The server's connection is gone for good (disconnected, removed). */
71
105
  detach(connection) {
72
106
  if (this.connection !== connection)
@@ -89,7 +123,7 @@ export class ToolsetController {
89
123
  live() {
90
124
  const c = this.connection;
91
125
  if (!c || !c.isConnected)
92
- throw new Error(`${this.serverId} is not connected right now; try again later`);
126
+ throw new McplRequestError(`${this.serverId} is not connected right now; try again later`, 'not-sent');
93
127
  this.sync(c.transportEpoch, c);
94
128
  return c;
95
129
  }
@@ -103,7 +137,6 @@ export class ToolsetController {
103
137
  return;
104
138
  this.epoch = epoch;
105
139
  this.seenEvents = new Set();
106
- this.listPages = null;
107
140
  this.presence.beginConnection(connection.toolsetDeclarations, true);
108
141
  }
109
142
  // ── RFC §6.1: apply on arrival ──
@@ -112,7 +145,7 @@ export class ToolsetController {
112
145
  return;
113
146
  this.sync(epoch, connection);
114
147
  if (read.kind === 'result') {
115
- this.readResult(read.method, read.params, read.result, read.awaited);
148
+ this.readResult(read.method, read.params, read.result, read.awaited, read.straddled === true);
116
149
  return;
117
150
  }
118
151
  if (!this.presence.active || !isObj(read.params))
@@ -129,7 +162,7 @@ export class ToolsetController {
129
162
  // RFC §7: a push carrying hints is never coalesced; when its changes
130
163
  // would apply is undefined, so they're ignored and the state re-read.
131
164
  if (p.coalesce !== undefined && p.coalesce !== null) {
132
- this.log(`push ${String(p.eventId ?? '')} carries hints and coalesce (RFC-014 §7 forbids it): its hints are ignored; recovering`);
165
+ this.log(`push ${this.field(p.eventId ?? '')} carries hints and coalesce (RFC-014 §7 forbids it): its hints are ignored; recovering`);
133
166
  this.scheduleRecovery();
134
167
  return;
135
168
  }
@@ -137,7 +170,7 @@ export class ToolsetController {
137
170
  const eventId = typeof p.eventId === 'string' || typeof p.eventId === 'number' ? String(p.eventId) : '';
138
171
  if (eventId) {
139
172
  if (this.seenEvents.has(eventId)) {
140
- this.log(`push ${eventId} was read before: its hints are ignored`);
173
+ this.log(`push ${this.field(eventId)} was read before: its hints are ignored`);
141
174
  return;
142
175
  }
143
176
  this.seenEvents.add(eventId);
@@ -145,28 +178,22 @@ export class ToolsetController {
145
178
  this.seenEvents.delete(this.seenEvents.values().next().value);
146
179
  }
147
180
  // Applied now, before (and whatever) the host decides about the push.
148
- this.readChanges.set(p, this.applyHints(p.hints, 'push'));
181
+ const cs = this.applyHints(p.hints, 'push');
182
+ this.readChanges.set(p, cs);
183
+ this.tellUnlessClaimed(p, cs, 'push/event');
149
184
  }
150
- readResult(method, params, res, awaited) {
185
+ readResult(method, params, res, awaited, straddled) {
151
186
  let cs;
152
187
  switch (method) {
153
188
  case 'tools/list': {
154
- const page = Array.isArray(res.tools) ? res.tools : [];
155
- // A first page (no cursor) starts the list; a page with no next cursor ends it.
156
- if (params.cursor === undefined || this.listPages === null)
157
- this.listPages = [];
158
- this.listPages.push(...page);
159
- if (res.nextCursor !== undefined && res.nextCursor !== null)
189
+ if (!Array.isArray(res.tools))
160
190
  return;
161
- const tools = this.listPages;
162
- this.listPages = null;
163
- cs = this.readToolList(tools, res);
164
- // The framework's own refresh (a list change, a reconnect) told here;
165
- // a list this controller asked for is told by whoever asked.
166
- if (this.ownLists === 0)
167
- this.deliver(cs);
168
- else
169
- this.ownListChange = joinChanges(this.ownListChange ?? emptyChange(), cs);
191
+ cs = this.readToolList(res.tools, res);
192
+ if (straddled && !this.recovering) {
193
+ this.log('tools/list pages carried different revs, so its earlier pages are older than changes read between them: recovering');
194
+ this.scheduleRecovery();
195
+ }
196
+ this.deliver(cs);
170
197
  return;
171
198
  }
172
199
  case 'toolsets/list':
@@ -218,9 +245,9 @@ export class ToolsetController {
218
245
  if (h === undefined || !this.presence.active)
219
246
  return emptyChange();
220
247
  const before = this.presence.view();
221
- const last = this.presence.rev;
248
+ const last = this.field(this.presence.rev);
222
249
  const r = this.presence.apply(h);
223
- const rev = String(h?.rev);
250
+ const rev = this.field(h?.rev);
224
251
  if (r.behind) {
225
252
  this.log(`hint change rev ${rev} (${source}) is not after rev ${last}: ignored; recovering from tools/list and toolsets/list`);
226
253
  this.scheduleRecovery();
@@ -232,6 +259,10 @@ export class ToolsetController {
232
259
  else if (!r.applied) {
233
260
  this.log(`a malformed hint change (${source}) was ignored`);
234
261
  }
262
+ if (r.unlisted) {
263
+ this.log(`hint change rev ${rev} (${source}) names a tool tools/list hasn't listed: recovering from tools/list and toolsets/list`);
264
+ this.scheduleRecovery();
265
+ }
235
266
  const cs = this.presence.diff(before, this.presence.view());
236
267
  cs.suggest.push(...r.suggest);
237
268
  return cs;
@@ -241,7 +272,7 @@ export class ToolsetController {
241
272
  const before = this.presence.view();
242
273
  this.presence.setTools(tools);
243
274
  if (this.presence.active && this.presence.noteListRev(isObj(last._meta) ? last._meta['mcpl/rev'] : undefined)) {
244
- 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`);
275
+ 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`);
245
276
  this.scheduleRecovery();
246
277
  }
247
278
  return this.presence.diff(before, this.presence.view());
@@ -250,40 +281,82 @@ export class ToolsetController {
250
281
  readToolsetList(res) {
251
282
  if (!this.presence.active)
252
283
  return emptyChange();
284
+ if (!Array.isArray(res.open)) {
285
+ this.log('a toolsets/list result with no open list was ignored');
286
+ return emptyChange();
287
+ }
253
288
  const before = this.presence.view();
254
- const { lost, ended, suggest, skipped } = this.presence.adopt(res);
289
+ const { lost, ended, suggest, skipped, unlisted } = this.presence.adopt(res);
255
290
  if (skipped && !this.recovering) {
256
291
  // Per-instance state is restated in full; what else was lost (presence) comes from tools/list.
257
292
  this.log('toolsets/list skipped a rev: re-reading tools/list');
258
293
  this.scheduleRecovery();
259
294
  }
295
+ if (unlisted && !this.recovering) {
296
+ this.log('toolsets/list names a tool tools/list hasn\'t listed: re-reading tools/list');
297
+ this.scheduleRecovery();
298
+ }
260
299
  const cs = this.presence.diff(before, this.presence.view());
261
300
  cs.suggest.push(...suggest);
262
301
  // A lost toolset gets its note, not also a "closed" line.
263
302
  cs.changes = cs.changes.filter((c) => !lost.some((o) => keyOf(o.toolset, o.instance) === c.key));
264
- cs.notes.push(...this.presence.adoptionNotes(lost, ended));
303
+ const told = this.presence.adoptionNotes(lost, ended);
304
+ cs.notes.push(...told.notes);
305
+ if (told.ended.length)
306
+ cs.ended = [...(cs.ended ?? []), ...told.ended];
265
307
  return cs;
266
308
  }
267
- /** A change with nothing else to ride on, told as a notice. */
309
+ /** A change with nothing else to ride on, told as a notice, after every result's change still unclaimed. */
268
310
  deliver(cs) {
311
+ this.tellUnclaimed();
312
+ this.notice(cs);
313
+ }
314
+ /**
315
+ * Unclaimed carriers' changes, told as notices in the order they were read,
316
+ * through `upTo` when given. When no notice can be written, a push keeps its
317
+ * change for the framework's push path, which takes it when the push gets
318
+ * there; a result's is dropped, whether or not its caller is still to
319
+ * resume.
320
+ */
321
+ tellUnclaimed(upTo) {
322
+ for (const [res, method] of [...this.unclaimed]) {
323
+ this.unclaimed.delete(res);
324
+ const cs = this.readChanges.get(res) ?? emptyChange();
325
+ const sent = this.notice(cs);
326
+ if (sent !== 'refused' || method !== 'push/event')
327
+ this.readChanges.delete(res);
328
+ if (sent === 'told')
329
+ this.log(`${method}'s tool change wasn't shown with it: told as a notice`);
330
+ if (res === upTo)
331
+ return;
332
+ }
333
+ }
334
+ /** Whether a notice was written, there was nothing left to tell (`empty`), or one couldn't be written (`refused`). */
335
+ notice(cs) {
269
336
  if (isEmptyChange(cs))
270
- return;
271
- const text = this.presence.render(cs);
337
+ return 'empty';
338
+ const text = this.presence.render(cs, { mark: false });
272
339
  if (!text)
273
- return;
274
- this.deps.notify(this.serverId, text, { invite: cs.suggest.some((s) => s.action === 'open') });
340
+ return 'empty';
341
+ if (!this.deps.notify(this.serverId, text, { invite: cs.suggest.some((s) => s.action === 'open') }))
342
+ return 'refused';
343
+ this.presence.markTold(cs.ended);
344
+ this.presence.markShown(cs);
345
+ return 'told';
275
346
  }
276
347
  /**
277
348
  * A dynamic tool called by its namespaced name, as if it were listed (a
278
349
  * model imitating an earlier call, a module): refused, so every call into
279
350
  * a toolset goes through act, which knows its instance and gates closed
280
- * groups (RFC §5.4, §5.5). Null for any other tool.
351
+ * groups (RFC §5.4, §5.5). Null for any other tool, including one the tool
352
+ * list last published or the server's latest tools/list puts as static
353
+ * (shownAs: RFC §4.2, until its move applies).
281
354
  */
282
355
  dynamicCallRefusal(tool, calledAs) {
283
- if (!this.presence.active || !this.presence.tool(tool) || this.presence.presenceOf(tool) !== 'dynamic')
356
+ if (!this.presence.active || !this.presence.tool(tool) || this.presence.shownAs(tool, 'static'))
284
357
  return null;
285
- return `There's no tool "${calledAs}" in your tool list, so nothing ran: ${tool} is a dynamic tool of ${this.serverId}, ` +
286
- `called with ${META_TOOL_NAMES.act} {server:"${this.serverId}", tool:"${tool}", args}. ${this.presence.toolStatus(tool)}.`;
358
+ return `There's no tool "${calledAs}" in your tool list, so nothing ran: ${nameToTypeBack(tool)} is a dynamic tool of ${this.serverId}, ` +
359
+ `called with ${META_TOOL_NAMES.act} {server:"${this.serverId}", tool:${quotedValue(tool)}, args}. ${this.presence.toolStatus(tool)}.`;
287
360
  }
288
361
  /**
289
362
  * An awaited result's change is told by whoever renders that result, when
@@ -292,34 +365,54 @@ export class ToolsetController {
292
365
  * run (the next macrotask), such as the result of a call the framework makes
293
366
  * itself (a reaction, a send, an accounted side effect), is told as a notice
294
367
  * instead, as a standalone tools/hints is (RFC §6.1: every change reaches the
295
- * agent once).
368
+ * agent once), or dropped as tellUnclaimed says when none can be written.
296
369
  */
297
370
  tellUnlessClaimed(res, cs, method) {
298
371
  if (isEmptyChange(cs))
299
372
  return;
300
- this.unclaimed.add(res);
373
+ this.unclaimed.set(res, method);
301
374
  setImmediate(() => {
302
- if (!this.unclaimed.delete(res))
303
- return;
304
- this.log(`${method} result's tool change wasn't shown with it: told as a notice`);
305
- this.deliver(cs);
375
+ if (this.unclaimed.has(res))
376
+ this.tellUnclaimed(res);
306
377
  });
307
378
  }
308
- /** What applying `carrier`'s hints changed (a tool result, a push's params),
309
- * rendered, for its handler to show with it; '' for nothing. Taking it
310
- * means it's told there, not as a notice. */
311
- changeText(carrier) {
379
+ /**
380
+ * The change a carrier's hints made (a tool result, a push's params), taken
381
+ * by a caller that shows it to an agent with the carrier: it isn't told as a
382
+ * notice, and the caller says when it has reached an agent (told). Call it
383
+ * synchronously as the carrier is read, before the next macrotask, when an
384
+ * unclaimed carrier's change goes out as a notice instead (tellUnclaimed
385
+ * says what happens when none can be written). A change already gone that
386
+ * way is empty here. Unclaimed carriers read before this one
387
+ * are told first, as notices, so no later change is shown before them.
388
+ */
389
+ takeChange(carrier) {
312
390
  if (!isObj(carrier))
313
- return '';
391
+ return emptyChange();
392
+ if (this.unclaimed.has(carrier)) {
393
+ let before;
394
+ for (const res of this.unclaimed.keys()) {
395
+ if (res === carrier)
396
+ break;
397
+ before = res;
398
+ }
399
+ if (before)
400
+ this.tellUnclaimed(before);
401
+ }
314
402
  this.unclaimed.delete(carrier);
315
- const cs = this.readChanges.get(carrier);
316
- return cs ? this.presence.render(cs) : '';
403
+ return this.readChanges.get(carrier) ?? emptyChange();
404
+ }
405
+ /** 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`). */
406
+ told(cs) {
407
+ this.presence.markTold(cs.ended);
408
+ if (cs.blocksShown !== false)
409
+ this.presence.markShown(cs);
410
+ }
411
+ /** A change its taker couldn't show, told as a notice instead, or dropped when none can be written. */
412
+ tell(cs) {
413
+ this.notice(cs);
317
414
  }
318
- /** The change a carrier's hints made, or an empty one; taken as changeText's is. */
319
415
  changeOf(carrier) {
320
- if (!isObj(carrier))
321
- return emptyChange();
322
- this.unclaimed.delete(carrier);
323
416
  return this.readChanges.get(carrier) ?? emptyChange();
324
417
  }
325
418
  // ── recovery (RFC §6.1) ──
@@ -341,8 +434,11 @@ export class ToolsetController {
341
434
  /**
342
435
  * Recovering: re-read tools/list (presence, declared definitions), then
343
436
  * toolsets/list (dynamic-tool state in full), so every tool the statement
344
- * names is already known; each applies where it's read. What changed is
345
- * told. (stdio and WebSocket deliver in order, so no early rev is held.)
437
+ * names is already known, unless tools/list failed: a tool the last list
438
+ * lacks then stays unknown until a later change or list starts a recovery.
439
+ * Each applies where it's read. What
440
+ * changed is told. (stdio and WebSocket deliver in order, so no early rev is
441
+ * held.)
346
442
  */
347
443
  async recover() {
348
444
  const connection = this.connection;
@@ -350,13 +446,14 @@ export class ToolsetController {
350
446
  return;
351
447
  this.recovering = true;
352
448
  try {
353
- const tools = await this.ownToolList(connection);
449
+ await this.ownToolList(connection);
450
+ // RFC §6.1: toolsets/list after tools/list, so every tool it names is known, unless tools/list failed.
354
451
  if (connection !== this.connection)
355
452
  return;
356
453
  const lists = await this.ownToolsetList(connection, LIST_TIMEOUT_MS, 'recovery');
357
454
  if (connection !== this.connection)
358
455
  return;
359
- this.deliver(joinChanges(tools, lists));
456
+ this.deliver(lists);
360
457
  }
361
458
  finally {
362
459
  this.recovering = false;
@@ -366,34 +463,22 @@ export class ToolsetController {
366
463
  }
367
464
  }
368
465
  }
466
+ /** The tap applies the list and tells its change as it's read. */
369
467
  async ownToolList(connection) {
370
- if (this.ownLists++ === 0)
371
- this.ownListChange = null;
372
468
  try {
373
- // The tap applied each page as it was read, and kept what the whole
374
- // list changed (ownListChange).
375
469
  await connection.sendToolsList();
376
470
  }
377
471
  catch (error) {
378
- this.log(`tools/list failed: ${error instanceof Error ? error.message : String(error)}`);
379
- }
380
- finally {
381
- this.ownLists--;
472
+ this.log(`tools/list failed: ${this.failure(error)}`);
382
473
  }
383
- const collected = this.ownListChange ?? emptyChange();
384
- if (this.ownLists === 0)
385
- this.ownListChange = null;
386
- else
387
- this.ownListChange = emptyChange();
388
- return collected;
389
474
  }
390
475
  async ownToolsetList(connection, timeoutMs, why) {
391
476
  try {
392
477
  const r = await connection.sendToolsetsList(timeoutMs);
393
- return this.changeOf(r);
478
+ return this.takeChange(r);
394
479
  }
395
480
  catch (error) {
396
- this.log(`toolsets/list (${why}) failed: ${error instanceof Error ? error.message : String(error)}`);
481
+ this.log(`toolsets/list (${why}) failed: ${this.failure(error)}`);
397
482
  return emptyChange();
398
483
  }
399
484
  }
@@ -411,60 +496,67 @@ export class ToolsetController {
411
496
  if (!this.presence.active)
412
497
  return;
413
498
  const epoch = connection.transportEpoch;
414
- const tools = await this.ownToolList(connection);
499
+ await this.ownToolList(connection);
415
500
  if (connection !== this.connection || connection.transportEpoch !== epoch)
416
501
  return;
417
502
  const lists = await this.ownToolsetList(connection, LIST_TIMEOUT_MS, 'after connecting');
418
503
  if (connection !== this.connection || connection.transportEpoch !== epoch)
419
504
  return;
420
- this.deliver(joinChanges(tools, lists));
505
+ this.deliver(lists);
421
506
  }
422
507
  /**
423
508
  * Re-read what the server holds open (titles, visibility) without telling
424
- * it: for look, which restates everything anyway. Returns notes for what
425
- * the server no longer holds.
509
+ * it, for look. Returns what the re-read changed, taken from the notice it
510
+ * would otherwise get when `shown` (its result reaches an agent): look then
511
+ * either shows it (told) or hands it back (tell). Otherwise the change is
512
+ * left to its notice, and returned to be shown to the caller alone. Null
513
+ * when nothing was read.
426
514
  */
427
- async refreshOpen() {
515
+ async refreshOpen(opts) {
428
516
  if (!this.presence.active)
429
- return [];
517
+ return null;
430
518
  const connection = this.connection;
431
519
  if (!connection || !connection.isConnected)
432
- return [];
520
+ return null;
433
521
  try {
434
522
  const r = await connection.sendToolsetsList(REFRESH_TIMEOUT_MS);
435
- return this.changeOf(r).notes;
523
+ if (!isObj(r))
524
+ return null;
525
+ return opts.shown ? this.takeChange(r) : this.changeOf(r);
436
526
  }
437
527
  catch (error) {
438
- this.log(`toolsets/list (look) failed: ${error instanceof Error ? error.message : String(error)}`);
439
- return [];
528
+ this.log(`toolsets/list (look) failed: ${this.failure(error)}`);
529
+ return null;
440
530
  }
441
531
  }
442
532
  // ── the host's acts ──
443
- /** toolsets/open (RFC §5.2). The server's refusal rejects. */
444
- async open(toolset, instance, options) {
533
+ /** toolsets/open (RFC §5.2). The server's refusal rejects. Its change is
534
+ * taken when `shown` (its result reaches an agent), else left to its
535
+ * notice and returned for the caller alone; close's is the same. */
536
+ async open(toolset, instance, options, opts) {
445
537
  const connection = this.live();
446
538
  const r = await connection.sendToolsetsOpen({ toolset, ...(instance ? { instance } : {}), ...(options ? { options } : {}) }, TOOLSET_REQUEST_TIMEOUT_MS);
447
539
  const res = isObj(r) ? r : {};
448
540
  const inst = typeof res.instance === 'string' && res.instance ? res.instance : instance;
449
541
  // RFC §5.2: a proposed name is used as given or the open refused.
450
542
  if (instance && inst !== instance)
451
- this.log(`proposed instance "${instance}" came back as "${inst}" (RFC-014 §5.2 forbids renaming): using the server's`);
543
+ this.log(`proposed instance ${this.field(instance)} came back as ${this.field(inst)} (RFC-014 §5.2 forbids renaming): using the server's`);
452
544
  const key = keyOf(toolset, this.presence.declared[toolset]?.instances ? inst : undefined);
453
545
  const title = typeof res.title === 'string' && res.title ? res.title : this.presence.openRecords().find((o) => keyOf(o.toolset, o.instance) === key)?.title;
454
546
  return {
455
547
  key,
456
548
  ...(title ? { title } : {}),
457
549
  content: Array.isArray(res.content) ? res.content : [],
458
- change: this.changeOf(res),
550
+ change: opts.shown ? this.takeChange(res) : this.changeOf(res),
459
551
  adopted: res.adopted === true,
460
552
  };
461
553
  }
462
554
  /** toolsets/close (RFC §5.2). */
463
- async close(toolset, instance) {
555
+ async close(toolset, instance, opts) {
464
556
  const connection = this.live();
465
557
  const r = await connection.sendToolsetsClose({ toolset, ...(instance ? { instance } : {}) }, TOOLSET_REQUEST_TIMEOUT_MS);
466
558
  const res = isObj(r) ? r : {};
467
- return { content: Array.isArray(res.content) ? res.content : [], change: this.changeOf(res) };
559
+ return { content: Array.isArray(res.content) ? res.content : [], change: opts.shown ? this.takeChange(res) : this.changeOf(res) };
468
560
  }
469
561
  /** Open or close an undeclared group (RFC §5.4): the host's own act; the server isn't told. */
470
562
  setGroupOpen(group, open) {
@@ -489,7 +581,7 @@ export class ToolsetController {
489
581
  this.presence.deactivate();
490
582
  this.log('the server withdrew toolHints: every tool is static from now on');
491
583
  const cs = this.presence.diff(before, this.presence.view());
492
- cs.notes.push(`${this.serverId} no longer presents toolsets: its tools are all in your tool list now.`);
584
+ cs.notes.push(`${this.serverId} no longer presents toolsets: its tools are called directly now, by name, not through ${META_TOOL_NAMES.act}.`);
493
585
  this.deliver(cs);
494
586
  return;
495
587
  }
@@ -502,11 +594,16 @@ export class ToolsetController {
502
594
  this.presence.setManifest(declarations);
503
595
  this.deliver(this.presence.diff(before, this.presence.view()));
504
596
  }
505
- /** Apply pending static↔dynamic moves (RFC §4.2) at a tool-list boundary, and say where they went. */
506
- applyMoves() {
507
- return this.presence.applyMoves().map(({ name, to }) => to === 'dynamic'
508
- ? `${name} left your tool list: it's announced when available now, and called with ${META_TOOL_NAMES.act} {server:"${this.serverId}", tool:"${name}", args}.`
509
- : `${name} is in your tool list now, as ${this.deps.listedName(this.serverId, name)}.`);
597
+ /** 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. */
598
+ applyMoves(moves) {
599
+ const moved = this.presence.applyMoves(moves);
600
+ if (!moved.length)
601
+ return;
602
+ const cs = emptyChange();
603
+ cs.notes.push(...moved.map(({ name, to }) => to === 'dynamic'
604
+ ? `${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}.`
605
+ : `${nameToTypeBack(name)} is in your tool list now, as ${nameToTypeBack(this.deps.listedName(this.serverId, name))}.`));
606
+ this.deliver(cs);
510
607
  }
511
608
  }
512
609
  /** Every server's controller, for the meta-tools and reminders. */
@@ -534,8 +631,11 @@ export class ToolsetHub {
534
631
  get(serverId) {
535
632
  return this.controllers.get(serverId);
536
633
  }
537
- /** A server's configuration went (removed): forget its controller. */
634
+ /** A server's configuration went (removed): its controller lets go of the
635
+ * connection it holds, and is forgotten, so a server added later under
636
+ * the same id starts from nothing. */
538
637
  forget(serverId) {
638
+ this.controllers.get(serverId)?.release();
539
639
  this.controllers.delete(serverId);
540
640
  }
541
641
  all() {
@@ -556,8 +656,9 @@ export class ToolsetHub {
556
656
  * reminder due.
557
657
  */
558
658
  observeCompile(agentName, messages) {
659
+ const host = this.deps.hostOpen?.(agentName) ?? [];
559
660
  const open = this.openMarkers();
560
- if (open.length === 0) {
661
+ if (open.length === 0 && host.length === 0) {
561
662
  this.seen.delete(agentName);
562
663
  this.due.delete(agentName);
563
664
  return;
@@ -575,6 +676,11 @@ export class ToolsetHub {
575
676
  if (!open.includes(marker))
576
677
  seen.delete(marker);
577
678
  this.seen.set(agentName, seen);
679
+ // A host toolset open across a restart was never seen here: it's due
680
+ // whenever a request doesn't carry its marker.
681
+ for (const h of host)
682
+ if (h.dueWhenAbsent && !text.includes(h.marker))
683
+ missing = true;
578
684
  // Due until a request carries every marker the agent has seen again: a
579
685
  // gather that doesn't send (a preview) leaves it due for the next start.
580
686
  if (missing)
@@ -586,7 +692,10 @@ export class ToolsetHub {
586
692
  reminderFor(agentName) {
587
693
  if (!this.due.has(agentName))
588
694
  return null;
589
- const parts = this.presenting().map((c) => c.presence.reminder()).filter((r) => !!r);
695
+ const parts = [
696
+ ...(this.deps.hostOpen?.(agentName) ?? []).map((h) => h.reminder()),
697
+ ...this.presenting().filter((c) => this.deps.canNotify(c.serverId)).map((c) => c.presence.reminder()).filter((r) => !!r),
698
+ ];
590
699
  if (parts.length === 0)
591
700
  return null;
592
701
  return `Open toolsets (a reminder: their earlier announcements have left your context):\n${parts.join('\n')}`;