@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
@@ -13,8 +13,9 @@
13
13
  * results, in the results of calls that changed them, in push events and in
14
14
  * notices) and called through `toolsets--act`, which this module only
15
15
  * resolves (resolveAct): the framework dispatches the call it resolves to
16
- * down the same path as a direct call of the server's tool. The module is registered when a server is
17
- * configured with `toolHints: true`; its tools never change after that.
16
+ * down the same path as a direct call of the server's tool. The module is registered at create(),
17
+ * for every agent (the host's own toolsets use it too), unless a module given to create() holds
18
+ * the name; its tools never change after that.
18
19
  *
19
20
  * Its gatherContext publishes the RFC §8.3 reminder, as a stored `afterUser`
20
21
  * injection, when an agent's earlier announcements of an open toolset have
@@ -22,12 +23,15 @@
22
23
  *
23
24
  * Ported from mcpl-cc-bridge's meta-tools.ts, the reference host.
24
25
  */
26
+ import { nameToTypeBack, quotedValue, suppliedValue } from '../supplied-text.js';
25
27
  import type { EventResponse, Module, ModuleContext, ModuleContextInjection, ProcessState } from '../types/module.js';
26
28
  import type { ProcessEvent, ToolCall, ToolDefinition, ToolResult } from '../types/events.js';
27
29
  import { unknownToolMessage } from '../modules/unknown-tool.js';
28
30
  import { META_TOOL_NAMES, TOOLSETS_MODULE, type ToolsetController, type ToolsetHub } from './toolset-host.js';
29
- import { keyOf, type ChangeSet } from './tool-presence.js';
30
- import { checkArgs, renderToolDefinition, type HintedTool } from './tool-signature.js';
31
+ import { keyOf, type ChangeSet, type ToolPresence } from './tool-presence.js';
32
+ import { checkArgs, jsonText, renderToolDefinition, type HintedTool } from './tool-signature.js';
33
+ import { McplRequestError } from './server-connection.js';
34
+ import { BUILTIN_TOOLSET, type HostPresentation } from '../host-presentation.js';
31
35
 
32
36
  /** The namespace a reminder's stored injection carries. */
33
37
  export const TOOLSETS_REMINDER_NAMESPACE = 'mcpl-toolsets';
@@ -36,26 +40,28 @@ const META_TOOLS: ToolDefinition[] = [
36
40
  {
37
41
  name: 'open',
38
42
  description:
39
- 'Open a toolset of an MCPL server (a game, a building mode, a card table) so its tools can be called with toolsets--act. ' +
40
- 'Returns what the server shows on opening and the tools available now. server may be left out when one server has the toolset, ' +
41
- 'and toolset when the server has only one; instance names a separate copy where a toolset allows several (two games at once); ' +
43
+ 'Open a toolset so its tools can be called with toolsets--act: the host\'s own settings and controls (toolset "settings"), ' +
44
+ 'an MCPL server\'s tab (server alone: its tools without putting them in your tool list; a closed tab is connected again; ' +
45
+ 'full: true puts the server back in your tool list), or a toolset an MCPL server declares (a game, a building mode, a card table: ' +
46
+ 'server and toolset). Returns the tools available now. instance names a separate copy where a toolset allows several; ' +
42
47
  'options as toolsets--look lists them.',
43
48
  inputSchema: {
44
49
  type: 'object',
45
50
  properties: {
46
51
  server: { type: 'string', description: 'the MCPL server id' },
47
- toolset: { type: 'string' },
52
+ toolset: { type: 'string', description: '"settings" for the host\'s own; a server\'s toolset with server' },
48
53
  instance: { type: 'string', description: 'a name for this copy (toolsets with instances only)' },
49
54
  options: { type: 'object', description: 'toolset options, as toolsets--look shows them' },
55
+ full: { type: 'boolean', description: 'with server alone: put the server\'s tools back in your tool list' },
50
56
  },
51
57
  },
52
58
  },
53
59
  {
54
60
  name: 'act',
55
61
  description:
56
- 'Call a tool that was announced in the conversation (a tool of an open toolset, or another dynamic tool); they are not in your tool list. ' +
62
+ 'Call a tool of an open toolset (the host\'s "settings", a server\'s tab, or a server\'s own toolset) or another dynamic tool, as announced in the conversation; they are not in your tool list. ' +
57
63
  'args are the arguments its signature shows; instance picks the open copy when several are open; server is needed only when two servers have the tool. ' +
58
- 'Returns the server\'s result, then any change in which tools are available.',
64
+ 'Returns the server\'s result, then any change it made in which tools are available, unless that change already went out as a notice. toolsets--look shows what is available now.',
59
65
  inputSchema: {
60
66
  type: 'object',
61
67
  properties: {
@@ -70,7 +76,7 @@ const META_TOOLS: ToolDefinition[] = [
70
76
  {
71
77
  name: 'look',
72
78
  description:
73
- 'Show what is open and what can be opened on MCPL servers, with the tools usable now: announcements earlier in the conversation may be out of date. ' +
79
+ 'Show what is open and what can be opened (the host\'s "settings", each MCPL server\'s tab, servers\' own toolsets), with the tools usable now: announcements earlier in the conversation may be out of date. ' +
74
80
  'With tool: that tool\'s full description and input schema. With toolset or instance: just that one.',
75
81
  inputSchema: {
76
82
  type: 'object',
@@ -84,19 +90,63 @@ const META_TOOLS: ToolDefinition[] = [
84
90
  },
85
91
  {
86
92
  name: 'close',
87
- description: 'Close an open toolset or instance: its tools and its events stop. Closing a game in progress may count as leaving it.',
93
+ description: 'Close an open toolset or instance: its tools stop, and for a toolset the server declares, the server is told and stops its events too. Closing a game in progress may count as leaving it. ' +
94
+ 'server alone moves a server out of your tool list into a tab (connected, its events still come), or hides its open tab; with disconnect: true it is disconnected until you open it again.',
88
95
  inputSchema: {
89
96
  type: 'object',
90
97
  properties: {
91
98
  server: { type: 'string', description: 'the MCPL server id (may be left out when one server has the toolset open)' },
92
- toolset: { type: 'string', description: 'may be left out when only one thing is open on the server' },
99
+ toolset: { type: 'string', description: '"settings" for the host\'s own; may be left out when only one thing is open on the server' },
93
100
  instance: { type: 'string' },
101
+ disconnect: { type: 'boolean', description: 'with server alone: disconnect it as a closed tab until it is opened again' },
94
102
  },
95
103
  },
96
104
  },
97
105
  ];
98
106
 
99
107
  const str = (v: unknown): string | undefined => (typeof v === 'string' && v.trim() ? v.trim() : undefined);
108
+
109
+ const selector = (v: unknown, has: (name: string) => boolean): string | undefined =>
110
+ typeof v === 'string' && v !== v.trim() && v.trim() !== '' && has(v) ? v : str(v);
111
+ const presentersOf = (hub: ToolsetHub, server: unknown): ToolsetController[] => {
112
+ const id = str(server);
113
+ if (!id) return hub.presenting();
114
+ const c = hub.get(id);
115
+ return c ? [c] : [];
116
+ };
117
+ const toolNamed = (hub: ToolsetHub, server: unknown, v: unknown): string | undefined =>
118
+ selector(v, (name) => presentersOf(hub, server).some((c) => !!c.presence.tool(name)));
119
+ const toolsetNamed = (hub: ToolsetHub, server: unknown, v: unknown): string | undefined =>
120
+ selector(v, (name) => presentersOf(hub, server).some((c) => c.presence.isDeclared(name) || c.presence.undeclaredGroups.includes(name)));
121
+ const instanceNamed = (presenters: readonly ToolsetController[], toolset: string | undefined, v: unknown): string | undefined =>
122
+ selector(v, (name) => presenters.some((c) => c.presence.openRecords(toolset).some((o) => o.instance === name)));
123
+
124
+ const SELECTORS = ['server', 'toolset', 'instance', 'tool'] as const;
125
+
126
+ function checkSelectors(args: Record<string, unknown>): void {
127
+ for (const key of SELECTORS) {
128
+ const v = args[key];
129
+ if (v === undefined || v === null || str(v) !== undefined) continue;
130
+ const got = typeof v === 'string' ? 'a blank string' : Array.isArray(v) ? 'a list' : typeof v === 'object' ? 'an object' : `a ${typeof v}`;
131
+ fail(`${key} must be a name, or left out; got ${got}, so nothing was done`);
132
+ }
133
+ }
134
+
135
+ /** A failure whose outcome is unknown, with how to find it out. */
136
+ export function withLookHint(serverId: string, text: string): string {
137
+ const said = text.trimEnd();
138
+ return `${said}${/[.!?]$/.test(said) ? '' : '.'} ${META_TOOL_NAMES.look} {server:"${serverId}"} asks it what's open, or says it couldn't.`;
139
+ }
140
+
141
+ function requestFailure(serverId: string, did: string, unconfirmed: string, error: unknown): never {
142
+ const message = suppliedValue(error instanceof Error ? error.message : String(error));
143
+ if (error instanceof McplRequestError && error.outcome === 'not-sent') return fail(`${serverId} ${did}: ${message}`);
144
+ return fail(withLookHint(serverId, `${serverId} ${unconfirmed}: ${message}`));
145
+ }
146
+
147
+ /** A result's tool change, for whoever shows it with the result (ToolsetsModule's takerFor). */
148
+ export type TakenChange = { toolsets: ToolsetController; cs: ChangeSet };
149
+
100
150
  const isObj = (v: unknown): v is Record<string, unknown> => typeof v === 'object' && v !== null && !Array.isArray(v);
101
151
 
102
152
  type Block = { type: string; text?: string; [k: string]: unknown };
@@ -142,11 +192,11 @@ function pick(hub: ToolsetHub, server: unknown): ToolsetController {
142
192
  const ids = hub.presenting().map((c) => c.serverId);
143
193
  if (!id) {
144
194
  if (ids.length === 1) return hub.get(ids[0]!)!;
145
- return fail(`name a server: ${ids.join(', ') || '(no MCPL server presents toolsets now)'}`);
195
+ return fail(ids.length ? `name a server: ${ids.join(', ')}` : NONE_PRESENTS);
146
196
  }
147
197
  const c = hub.get(id);
148
198
  if (!c) return fail(`no server "${id}" presents toolsets; the ones that do: ${ids.join(', ') || 'none'}`);
149
- if (!c.presence.active) return fail(`${id} sends no tool hints now: its tools are in your tool list`);
199
+ if (!c.presence.active) return fail(`${id} sends no tool hints now: its tools are called directly, by name, not through ${META_TOOL_NAMES.act}`);
150
200
  return c;
151
201
  }
152
202
 
@@ -158,9 +208,15 @@ function owner(hub: ToolsetHub, server: unknown, has: (c: ToolsetController) =>
158
208
  if (str(server)) return pick(hub, server);
159
209
  const owners = hub.presenting().filter(has);
160
210
  if (owners.length === 1) return owners[0]!;
161
- return fail(owners.length
162
- ? `${what} is on several servers (${owners.map((c) => c.serverId).join(', ')}): add server`
163
- : `no server that presents toolsets has ${what}`);
211
+ if (owners.length) return fail(`${what} is on several servers (${owners.map((c) => c.serverId).join(', ')}): add server`);
212
+ return fail(hub.presenting().length ? `no server that presents toolsets has ${what}: ${META_TOOL_NAMES.look} shows what they have` : NONE_PRESENTS);
213
+ }
214
+
215
+ const NONE_PRESENTS = 'no MCPL server presents toolsets now: MCPL tools are called directly, by name';
216
+
217
+ function noToolset(c: ToolsetController, toolset: string): string {
218
+ const names = [...Object.keys(c.presence.declared), ...c.presence.undeclaredGroups];
219
+ return `${c.serverId} has no toolset "${toolset}"; ${names.length ? `it has ${names.map(nameToTypeBack).join(', ')}` : 'it has none'}`;
164
220
  }
165
221
 
166
222
  /** What a `toolsets--act` call resolves to: the server's tool, its instance and arguments. */
@@ -171,42 +227,51 @@ export type ActTarget = { ok: true; serverId: string; tool: string; instance?: s
171
227
  * or the reason it can't be sent. The framework then dispatches it exactly as
172
228
  * a direct call of that tool, with `_meta["mcpl/instance"]` added (RFC §5.5).
173
229
  * Refused here, before anything is sent: a tool no presenting server has, a
174
- * static tool (it's in the tool list), an instance that isn't named where
230
+ * tool neither the published list nor the server's latest tools/list puts as dynamic (RFC §4.2), an instance that isn't named where
175
231
  * several are open, a group the host has closed (RFC §5.4), and arguments
176
232
  * that don't fit the definition in force for that instance (RFC §6.1).
177
233
  */
234
+ const hasInstances = (p: ToolPresence, ts: string | undefined): boolean => ts !== undefined && p.isDeclared(ts) && !!p.declared[ts]!.instances;
235
+
236
+ /** The key whose definition act checks a call of `tool` against (RFC §6.1): an instance's own, else the toolset's, else the listed one. */
237
+ function actKey(p: ToolPresence, tool: string, instance: string | undefined): string {
238
+ const ts = p.toolsetOf(tool);
239
+ return ts ? keyOf(ts, hasInstances(p, ts) ? instance : undefined) : keyOf('');
240
+ }
241
+
178
242
  export function resolveAct(hub: ToolsetHub, input: unknown): ActTarget {
179
243
  try {
180
244
  const args = isObj(input) ? input : {};
181
- const tool = str(args.tool) ?? fail('name the tool');
245
+ checkSelectors(args);
246
+ const tool = toolNamed(hub, args.server, args.tool) ?? fail('name the tool');
182
247
  const c = owner(hub, args.server, (x) => !!x.presence.tool(tool), `a tool "${tool}"`);
183
248
  const p = c.presence;
184
249
  if (!p.tool(tool)) fail(`${c.serverId} has no tool "${tool}": ${META_TOOL_NAMES.look} {server:"${c.serverId}"} shows what's available`);
185
- if (p.presenceOf(tool) === 'static') fail(`${tool} is not a dynamic tool: ${p.toolStatus(tool)}. Call it there.`);
250
+ if (!p.shownAs(tool, 'dynamic')) fail(`${nameToTypeBack(tool)} is not a dynamic tool: ${p.toolStatus(tool)}. Call it there.`);
186
251
  if (args.args !== undefined && !isObj(args.args)) fail('args must be an object of the tool\'s arguments');
187
252
  const callArgs = (args.args ?? {}) as Record<string, unknown>;
188
- let instance = str(args.instance);
189
253
  const ts = p.toolsetOf(tool);
254
+ let instance = instanceNamed([c], ts, args.instance);
190
255
  const declared = ts !== undefined && p.isDeclared(ts);
191
- const withInstances = declared && !!p.declared[ts!]!.instances;
256
+ const withInstances = hasInstances(p, ts);
192
257
  if (withInstances && !instance) {
193
258
  // RFC §5.5: the instance rides in _meta; with exactly one open, it's that one.
194
259
  const open = p.openRecords(ts);
195
260
  if (open.length === 1) instance = open[0]!.instance;
196
- else if (!open.length) fail(`${tool} belongs to ${ts}, which isn't open: ${p.openHint(ts!)}`);
197
- else fail(`several ${ts} instances are open (${open.map((o) => o.instance).join(', ')}): say which with instance`);
261
+ else if (!open.length) fail(`${nameToTypeBack(tool)} belongs to ${nameToTypeBack(ts!)}, which isn't open: ${p.openHint(ts!)}`);
262
+ else fail(`several ${nameToTypeBack(ts!)} instances are open (${open.map((o) => nameToTypeBack(o.instance!)).join(', ')}): say which with instance`);
198
263
  }
199
- if (instance && !withInstances) fail(`${tool} ${ts ? `belongs to ${ts}, which has no instances` : 'belongs to no toolset'}: call it without instance`);
264
+ if (instance && !withInstances) fail(`${nameToTypeBack(tool)} ${ts ? `belongs to ${nameToTypeBack(ts)}, which has no instances` : 'belongs to no toolset'}: call it without instance`);
200
265
  // RFC §5.4: the host gates calls into an undeclared group it has closed.
201
- if (ts && !declared && !p.isOpen(ts)) fail(`${tool} is in ${ts}, which isn't open: ${p.openHint(ts)}`);
266
+ if (ts && !declared && !p.isOpen(ts)) fail(`${nameToTypeBack(tool)} is in ${nameToTypeBack(ts)}, which isn't open: ${p.openHint(ts)}`);
202
267
  // RFC §6.1: arguments are checked against the definition in force for
203
268
  // that instance: its own from hints (the legal moves of one game), else
204
269
  // the toolset's, else the listed one.
205
- const key = ts ? keyOf(ts, withInstances ? instance : undefined) : keyOf('');
270
+ const key = actKey(p, tool, instance);
206
271
  const problem = checkArgs(p.defOf(key, tool).inputSchema, callArgs);
207
272
  if (problem) {
208
- fail(`Invalid arguments for ${tool} in ${p.label(key)}: ${problem}. Nothing was sent; ` +
209
- `${META_TOOL_NAMES.look} {server:"${c.serverId}", tool:"${tool}"${instance ? `, instance:"${instance}"` : ''}} shows its current definition.`);
273
+ fail(`Invalid arguments for ${nameToTypeBack(tool)} in ${p.label(key)}: ${problem}. Nothing was sent; ` +
274
+ `${META_TOOL_NAMES.look} {server:"${c.serverId}", tool:${quotedValue(tool)}${instance ? `, instance:${quotedValue(instance)}` : ''}} shows its current definition.`);
210
275
  }
211
276
  return { ok: true, serverId: c.serverId, tool, ...(instance ? { instance } : {}), args: callArgs };
212
277
  } catch (error) {
@@ -218,7 +283,15 @@ export function resolveAct(hub: ToolsetHub, input: unknown): ActTarget {
218
283
  export class ToolsetsModule implements Module {
219
284
  readonly name = TOOLSETS_MODULE;
220
285
 
221
- constructor(private readonly hub: ToolsetHub) {}
286
+ /** `takerFor`: who shows a call's result to an agent, taking the tool
287
+ * change rendered into it; null when the result may reach none, and its
288
+ * change is left to a notice (RFC-014 §6.1: told once). */
289
+ constructor(
290
+ private readonly hub: ToolsetHub,
291
+ private readonly takerFor: (call: ToolCall) => ((taken: TakenChange) => void) | null,
292
+ /** The host's own toolsets (the built-in "settings" and server tabs), when the framework presents them. */
293
+ private readonly host: HostPresentation | null = null,
294
+ ) {}
222
295
 
223
296
  async start(_ctx: ModuleContext): Promise<void> {}
224
297
  async stop(): Promise<void> {}
@@ -241,11 +314,22 @@ export class ToolsetsModule implements Module {
241
314
  async handleToolCall(call: ToolCall): Promise<ToolResult> {
242
315
  const args = isObj(call.input) ? call.input : {};
243
316
  try {
317
+ checkSelectors(args);
318
+ const hosted = await this.hostCall(call.name, args, call.callerAgentName);
319
+ if (hosted) return hosted;
320
+ const take = this.takerFor(call);
321
+ const lead = call.name === 'look' ? this.hostLookLead(args, call.callerAgentName) : null;
322
+ if (lead !== null) {
323
+ if (!this.hub.presenting().length) return textResult(lead);
324
+ const theirs = await this.look(args, take);
325
+ if (theirs.isError || typeof theirs.data !== 'string') return theirs;
326
+ return textResult(`${lead}\n\n${theirs.data}`);
327
+ }
244
328
  switch (call.name) {
245
- case 'open': return await this.open(args);
329
+ case 'open': return await this.open(args, take);
246
330
  case 'act': return this.act(args);
247
- case 'look': return await this.look(args);
248
- case 'close': return await this.close(args);
331
+ case 'look': return await this.look(args, take);
332
+ case 'close': return await this.close(args, take);
249
333
  default: return { success: false, isError: true, error: unknownToolMessage(this.name, call.name, META_TOOLS) };
250
334
  }
251
335
  } catch (error) {
@@ -256,67 +340,127 @@ export class ToolsetsModule implements Module {
256
340
  }
257
341
  }
258
342
 
343
+ /**
344
+ * The host's own toolsets: `toolset: "settings"` alone, or `server` alone
345
+ * (a server's tab), for open, close and look; a tool a host toolset has, for
346
+ * look. Null for everything else, which the servers' toolsets answer.
347
+ */
348
+ private async hostCall(name: string, args: Record<string, unknown>, agentName: string | undefined): Promise<ToolResult | null> {
349
+ const host = this.host;
350
+ if (!host || !agentName || !host.active(agentName)) return null;
351
+ const server = str(args.server);
352
+ const toolset = str(args.toolset);
353
+ const instance = str(args.instance);
354
+ const tool = str(args.tool);
355
+ const builtin = toolset === BUILTIN_TOOLSET && !server && !instance;
356
+ const tab = !!server && !toolset && !instance && args.options === undefined;
357
+ if (args.full !== undefined && typeof args.full !== 'boolean') fail('full must be true or false');
358
+ if (args.disconnect !== undefined && typeof args.disconnect !== 'boolean') fail('disconnect must be true or false');
359
+ if (tab && !host.knows(server!) && !this.hub.get(server!)) fail(`no MCPL server "${server}" is configured here`);
360
+ switch (name) {
361
+ case 'open':
362
+ if (builtin) return textResult(host.openBuiltin(agentName));
363
+ if (tab) return textResult(await host.openServer(agentName, server!, { full: args.full === true }));
364
+ if (args.full !== undefined) fail('full goes with server alone');
365
+ return null;
366
+ case 'close':
367
+ if (builtin) return textResult(host.closeBuiltin(agentName));
368
+ if (tab) return textResult(await host.closeServer(agentName, server!, { disconnect: args.disconnect === true }));
369
+ if (args.disconnect !== undefined) fail('disconnect goes with server alone');
370
+ return null;
371
+ case 'look': {
372
+ // A host toolset's tool, with or without its server named (as act takes it).
373
+ if (tool && !toolset && !instance) {
374
+ const shown = host.lookTool(agentName, tool, server);
375
+ if (shown) return textResult(shown);
376
+ return null;
377
+ }
378
+ if (builtin) return textResult(host.lookToolset(agentName, { builtin: true }));
379
+ // A server's tab, and every host toolset with no selector, go first in
380
+ // what the servers' own look says (handleToolCall adds them).
381
+ if (tab && !tool && !this.hub.get(server!)?.presents) return textResult(host.lookToolset(agentName, { serverId: server! }));
382
+ return null;
383
+ }
384
+ default:
385
+ return null;
386
+ }
387
+ }
388
+
389
+ /** What the host's own toolsets add ahead of the servers' look: everything, or one presenting server's tab. */
390
+ private hostLookLead(args: Record<string, unknown>, agentName: string | undefined): string | null {
391
+ const host = this.host;
392
+ if (!host || !agentName || !host.active(agentName)) return null;
393
+ const server = str(args.server);
394
+ if (str(args.toolset) || str(args.instance) || str(args.tool)) return null;
395
+ if (!server) return host.catalog(agentName).join('\n');
396
+ return host.knows(server) ? host.lookToolset(agentName, { serverId: server }) : null;
397
+ }
398
+
259
399
  private toolsAt(c: ToolsetController, key: string): HintedTool[] {
260
400
  return [...(c.presence.view().get(key)?.tools.values() ?? [])];
261
401
  }
262
402
 
263
403
  /** A change set rendered without the key the caller already showed whole. */
264
- private renderOthers(c: ToolsetController, cs: ChangeSet, key: string): string {
265
- return c.presence.render({ ...cs, changes: cs.changes.filter((ch) => ch.key !== key) });
404
+ private renderOthers(c: ToolsetController, cs: ChangeSet, key: string, shown: boolean): string {
405
+ return c.presence.render({ ...cs, changes: cs.changes.filter((ch) => ch.key !== key) }, { mark: shown, describeAll: !shown });
266
406
  }
267
407
 
268
408
  // ── the tools ──
269
409
 
270
- private async open(args: Record<string, unknown>): Promise<ToolResult> {
271
- let toolset = str(args.toolset);
410
+ private async open(args: Record<string, unknown>, take: ((taken: TakenChange) => void) | null): Promise<ToolResult> {
411
+ const shown = take !== null;
412
+ let toolset = toolsetNamed(this.hub, args.server, args.toolset);
272
413
  const c = toolset && !str(args.server)
273
414
  ? owner(this.hub, undefined, (x) => x.presence.isDeclared(toolset!) || x.presence.undeclaredGroups.includes(toolset!), `a toolset "${toolset}"`)
274
415
  : pick(this.hub, args.server);
275
416
  const p = c.presence;
276
417
  const openable = [...Object.keys(p.declared), ...p.undeclaredGroups];
277
418
  if (!toolset) {
278
- if (openable.length !== 1) fail(`${c.serverId} has ${openable.length ? `several toolsets: name one of ${openable.join(', ')}` : 'no toolsets'}`);
419
+ if (openable.length !== 1) fail(`${c.serverId} has ${openable.length ? `several toolsets: name one of ${openable.map(nameToTypeBack).join(', ')}` : 'no toolsets'}`);
279
420
  toolset = openable[0]!;
280
421
  }
281
- const instance = str(args.instance);
422
+ const instance = instanceNamed([c], toolset, args.instance);
282
423
  if (args.options !== undefined && !isObj(args.options)) fail('options must be an object');
283
424
  const options = args.options as Record<string, unknown> | undefined;
284
425
 
285
426
  if (p.isDeclared(toolset)) {
286
- if (instance && !p.declared[toolset]!.instances) fail(`${c.serverId}/${toolset} has no instances: open it without one`);
427
+ if (instance && !p.declared[toolset]!.instances) fail(`${c.serverId}/${nameToTypeBack(toolset)} has no instances: open it without one`);
287
428
  let opened: Awaited<ReturnType<ToolsetController['open']>>;
288
429
  try {
289
- opened = await c.open(toolset, instance, options);
430
+ opened = await c.open(toolset, instance, options, { shown });
290
431
  } catch (error) {
291
- return fail(`${c.serverId} did not open ${toolset}: ${error instanceof Error ? error.message : String(error)}`);
432
+ return requestFailure(c.serverId, `did not open ${nameToTypeBack(toolset)}`, `didn't confirm opening ${nameToTypeBack(toolset)}`, error);
292
433
  }
293
434
  // RFC §5.2: opening what is open adopts it; its content says which options weren't applied.
294
435
  const blocks: Block[] = [
295
- { type: 'text', text: `${p.label(opened.key)}${opened.title ? ` ${opened.title}` : ''}${opened.adopted ? ' (already open)' : ''}` },
436
+ { type: 'text', text: `${p.label(opened.key)}${opened.title ? ` ${suppliedValue(opened.title)}` : ''}${opened.adopted ? ' (already open)' : ''}` },
296
437
  ...asBlocks(opened.content),
297
438
  ];
298
- const tail = [...p.toolBlock(opened.key, this.toolsAt(c, opened.key), { headed: false })];
439
+ const tail = [...p.toolBlock(opened.key, this.toolsAt(c, opened.key), { headed: false, describeAll: !shown, seen: shown ? undefined : new Set<string>() })];
299
440
  const gone = opened.change.changes.find((ch) => ch.key === opened.key)?.gone ?? [];
300
- if (gone.length) tail.push(`now unavailable: ${gone.join(', ')}`);
301
- const others = this.renderOthers(c, opened.change, opened.key);
441
+ if (gone.length) tail.push(`now unavailable: ${gone.map(suppliedValue).join(', ')}`);
442
+ const others = this.renderOthers(c, opened.change, opened.key, shown);
302
443
  if (others) tail.push(others);
303
444
  const close = p.closeHint(opened.key);
304
445
  if (close) tail.push(`${close} closes it.`);
305
446
  blocks.push({ type: 'text', text: tail.join('\n') });
447
+ take?.({ toolsets: c, cs: opened.change });
306
448
  return blocksResult(blocks);
307
449
  }
308
450
  if (p.undeclaredGroups.includes(toolset)) {
309
451
  // RFC §5.4: a grouping the server offers but doesn't track: the host opens it, the server isn't told.
310
- if (instance || options) fail(`${c.serverId}/${toolset} takes no instance or options (the server doesn't declare it)`);
452
+ if (instance || options) fail(`${c.serverId}/${nameToTypeBack(toolset)} takes no instance or options (the server doesn't declare it)`);
311
453
  const cs = c.setGroupOpen(toolset, true);
312
454
  const key = keyOf(toolset);
313
- const lines = [`${p.label(key)} open.`, ...p.toolBlock(key, this.toolsAt(c, key), { headed: false })];
314
- const others = this.renderOthers(c, cs, key);
455
+ const lines = [`${p.label(key)} open.`, ...p.toolBlock(key, this.toolsAt(c, key), { headed: false, describeAll: !shown, seen: shown ? undefined : new Set<string>() })];
456
+ const others = this.renderOthers(c, cs, key, shown);
315
457
  if (others) lines.push(others);
316
458
  lines.push(`${p.closeHint(key)} closes it.`);
459
+ if (take) take({ toolsets: c, cs });
460
+ else c.tell(cs);
317
461
  return textResult(lines.join('\n'));
318
462
  }
319
- return fail(`${c.serverId} has no toolset "${toolset}"${openable.length ? `; it has ${openable.join(', ')}` : ''}`);
463
+ return fail(noToolset(c, toolset));
320
464
  }
321
465
 
322
466
  /**
@@ -333,42 +477,77 @@ export class ToolsetsModule implements Module {
333
477
  }
334
478
 
335
479
  /** look restates the present: titles and visibility are re-read from the servers first (toolsets/list). */
336
- private async look(args: Record<string, unknown>): Promise<ToolResult> {
480
+ private async look(args: Record<string, unknown>, take: ((taken: TakenChange) => void) | null): Promise<ToolResult> {
337
481
  const server = str(args.server);
338
482
  const targets = server ? [this.hub.get(server)].filter((c): c is ToolsetController => !!c && c.presents) : this.hub.presenting();
339
- const notes = (await Promise.all(targets.map((c) => c.refreshOpen().catch(() => [] as string[])))).flat();
340
- const result = this.lookNow(args, server, str(args.tool));
341
- return notes.length && !result.isError && typeof result.data === 'string' ? textResult(`${result.data}\n${notes.join('\n')}`) : result;
483
+ const shown = take !== null;
484
+ const reads = await Promise.all(targets.map(async (c) => ({ c, cs: await c.refreshOpen({ shown }).catch(() => null) })));
485
+ const read = reads.filter((r): r is { c: ToolsetController; cs: ChangeSet } => r.cs !== null);
486
+ const unread = reads.filter((r) => r.cs === null).map(({ c }) => `${c.serverId} couldn't be asked just now: this is what the host last knew of it.`);
487
+ const giveBack = () => {
488
+ if (shown) for (const { c, cs } of read) c.tell(cs);
489
+ };
490
+ let result: ToolResult;
491
+ try {
492
+ result = this.lookNow(args, server, toolNamed(this.hub, server, args.tool), shown);
493
+ } catch (error) {
494
+ giveBack();
495
+ if (error instanceof MetaError && unread.length) fail([error.message, ...unread].join('\n'));
496
+ throw error;
497
+ }
498
+ if (result.isError || typeof result.data !== 'string') {
499
+ giveBack();
500
+ return result;
501
+ }
502
+ const restates = !str(args.tool) && !str(args.toolset) && !str(args.instance);
503
+ if (!restates) giveBack();
504
+ const lines = restates
505
+ ? read.flatMap(({ c, cs }) => {
506
+ const said = [...cs.suggest.map((s) => c.presence.renderSuggestion(s)), ...c.presence.untoldNotes(cs), ...cs.notes];
507
+ // The catalog above counted what it showed; the change's own blocks weren't shown, but stay for a notice if its taker can't show the result.
508
+ take?.({ toolsets: c, cs: { ...cs, blocksShown: false } });
509
+ return said;
510
+ })
511
+ : [];
512
+ lines.push(...unread);
513
+ return lines.length ? textResult(`${result.data}\n${lines.join('\n')}`) : result;
342
514
  }
343
515
 
344
- private lookNow(args: Record<string, unknown>, server: string | undefined, tool: string | undefined): ToolResult {
516
+ private lookNow(args: Record<string, unknown>, server: string | undefined, tool: string | undefined, mark: boolean): ToolResult {
345
517
  if (!server) {
346
- if (tool) return this.lookTool(owner(this.hub, undefined, (c) => !!c.presence.tool(tool), `a tool "${tool}"`), tool, str(args.instance));
347
- const ts = str(args.toolset);
348
- const inst = str(args.instance);
518
+ if (tool) {
519
+ const c = owner(this.hub, undefined, (x) => !!x.presence.tool(tool), `a tool "${tool}"`);
520
+ return this.lookTool(c, tool, instanceNamed([c], c.presence.toolsetOf(tool), args.instance));
521
+ }
522
+ const ts = toolsetNamed(this.hub, undefined, args.toolset);
523
+ const inst = instanceNamed(this.hub.presenting(), ts, args.instance);
349
524
  if (ts || inst) {
350
525
  const c = owner(this.hub,
351
526
  undefined,
352
- (x) => (ts ? x.presence.isDeclared(ts) || x.presence.undeclaredGroups.includes(ts) : true) && (!inst || x.presence.openRecords().some((o) => o.instance === inst)),
527
+ (x) => (ts ? x.presence.isDeclared(ts) || x.presence.undeclaredGroups.includes(ts) : true) && (!inst || x.presence.openRecords(ts).some((o) => o.instance === inst)),
353
528
  ts ? `a toolset "${ts}"${inst ? ` with instance "${inst}"` : ''}` : `an open instance "${inst}"`,
354
529
  );
355
- return this.lookNow(args, c.serverId, tool);
530
+ return this.lookNow(args, c.serverId, tool, mark);
356
531
  }
357
532
  const all = this.hub.presenting();
358
- if (!all.length) return textResult('No MCPL server presents toolsets or dynamic tools now; every tool is in your tool list.');
359
- return textResult(all.map((c) => c.presence.catalog()).join('\n\n'));
533
+ if (!all.length) return textResult('No MCPL server presents toolsets or dynamic tools now; MCPL tools are called directly, by name.');
534
+ return textResult(all.map((c) => c.presence.catalog({ mark })).join('\n\n'));
360
535
  }
361
536
  const c = pick(this.hub, server);
362
- if (tool) return this.lookTool(c, tool, str(args.instance));
537
+ if (tool) return this.lookTool(c, tool, instanceNamed([c], c.presence.toolsetOf(tool), args.instance));
363
538
  const p = c.presence;
364
- let toolset = str(args.toolset);
365
- const instance = str(args.instance);
366
- if (!toolset && instance) toolset = p.openRecords().find((o) => o.instance === instance)?.toolset ?? fail(`no open instance "${instance}" on ${c.serverId}`);
367
- if (!toolset) return textResult(p.catalog());
539
+ let toolset = toolsetNamed(this.hub, server, args.toolset);
540
+ const instance = instanceNamed([c], toolset, args.instance);
541
+ if (!toolset && instance) {
542
+ const open = p.openRecords().filter((o) => o.instance !== undefined);
543
+ toolset = open.find((o) => o.instance === instance)?.toolset
544
+ ?? fail(`no open instance "${instance}" on ${c.serverId}; ${open.length ? `open: ${open.map((o) => `${nameToTypeBack(o.toolset)}:${nameToTypeBack(String(o.instance))}`).join(', ')}` : 'no instance is open there'}`);
545
+ }
546
+ if (!toolset) return textResult(p.catalog({ mark }));
368
547
  if (p.isOpen(toolset, instance) || (!instance && p.openRecords(toolset).length === 1)) {
369
548
  const rec = p.isDeclared(toolset) ? (instance ? p.openRecords(toolset).find((o) => o.instance === instance) : p.openRecords(toolset)[0]) : undefined;
370
549
  const key = keyOf(toolset, rec?.instance);
371
- const lines = p.toolBlock(key, this.toolsAt(c, key), { describeAll: true, title: rec?.title });
550
+ const lines = p.toolBlock(key, this.toolsAt(c, key), { describeAll: true, title: rec?.title, seen: mark ? undefined : new Set<string>() });
372
551
  const close = p.closeHint(key);
373
552
  if (close) lines.push(`${close} closes it.`);
374
553
  return textResult(lines.join('\n'));
@@ -377,38 +556,40 @@ export class ToolsetsModule implements Module {
377
556
  if (d) {
378
557
  const open = p.openRecords(toolset);
379
558
  return textResult([
380
- `${c.serverId}/${toolset}${d.title ? ` — ${d.title}` : ''}${d.summary ? `: ${d.summary}` : ''}`,
381
- open.length ? `open: ${open.map((o) => o.instance ?? toolset).join(', ')}` : 'not open',
559
+ `${c.serverId}/${nameToTypeBack(toolset)}${d.title ? ` — ${suppliedValue(d.title)}` : ''}${d.summary ? `: ${suppliedValue(d.summary)}` : ''}`,
560
+ open.length ? `open: ${open.map((o) => nameToTypeBack(o.instance ?? toolset)).join(', ')}` : 'not open',
382
561
  ...(d.instances ? ['several can be open at once; name each with instance'] : []),
383
- ...(d.options ? [`options: ${JSON.stringify(d.options)}`] : []),
562
+ ...(d.options ? [`options: ${jsonText(d.options)}`] : []),
384
563
  `${p.openHint(toolset)} opens it.`,
385
564
  ].join('\n'));
386
565
  }
387
566
  if (p.undeclaredGroups.includes(toolset)) {
388
567
  const names = p.allTools.filter((t) => p.toolsetOf(t.name) === toolset).map((t) => t.name);
389
- return textResult(`${c.serverId}/${toolset}: ${names.join(', ')}; not open: ${p.openHint(toolset)} opens it.`);
568
+ return textResult(`${c.serverId}/${nameToTypeBack(toolset)}: ${names.map(nameToTypeBack).join(', ')}; not open: ${p.openHint(toolset)} opens it.`);
390
569
  }
391
- return fail(`${c.serverId} has no toolset "${toolset}"`);
570
+ return fail(noToolset(c, toolset!));
392
571
  }
393
572
 
394
573
  private lookTool(c: ToolsetController, tool: string, instance: string | undefined): ToolResult {
395
574
  const p = c.presence;
396
- if (!p.tool(tool)) fail(`${c.serverId} has no tool "${tool}"`);
575
+ if (!p.tool(tool)) fail(`${c.serverId} has no tool "${tool}": ${META_TOOL_NAMES.look} {server:"${c.serverId}"} shows what's available`);
397
576
  const ts = p.toolsetOf(tool);
398
- const where = [...p.view().entries()].filter(([, e]) => e.tools.has(tool)).map(([k]) => k);
399
- const key = instance && ts ? keyOf(ts, instance) : where[0];
577
+ // The definition act would check, wherever act would take the tool.
578
+ const open = !instance && hasInstances(p, ts) ? p.openRecords(ts) : [];
579
+ const key = !p.shownAs(tool, 'dynamic') ? undefined : actKey(p, tool, instance ?? (open.length === 1 ? open[0]!.instance : undefined));
400
580
  const def = key ? p.defOf(key, tool) : p.tool(tool)!;
401
581
  return textResult(`${renderToolDefinition(def)}\n${p.toolStatus(tool)}`);
402
582
  }
403
583
 
404
- private async close(args: Record<string, unknown>): Promise<ToolResult> {
405
- const named = str(args.toolset);
584
+ private async close(args: Record<string, unknown>, take: ((taken: TakenChange) => void) | null): Promise<ToolResult> {
585
+ const shown = take !== null;
586
+ const named = toolsetNamed(this.hub, args.server, args.toolset);
406
587
  const c = named && !str(args.server)
407
- ? owner(this.hub, undefined, (x) => x.presence.isOpen(named, str(args.instance)) || x.presence.openRecords(named).length > 0, `an open toolset "${named}"`)
588
+ ? owner(this.hub, undefined, (x) => x.presence.isOpen(named, instanceNamed([x], named, args.instance)) || x.presence.openRecords(named).length > 0, `an open toolset "${named}"`)
408
589
  : pick(this.hub, args.server);
409
590
  const p = c.presence;
410
591
  let toolset = named;
411
- let instance = str(args.instance);
592
+ let instance = instanceNamed([c], named, args.instance);
412
593
  if (!toolset) {
413
594
  const open = [
414
595
  ...p.openRecords().filter((o) => !instance || o.instance === instance),
@@ -416,7 +597,7 @@ export class ToolsetsModule implements Module {
416
597
  ];
417
598
  if (open.length !== 1) {
418
599
  fail(open.length
419
- ? `several are open on ${c.serverId}: name one of ${open.map((o) => `${o.toolset}${o.instance ? `:${o.instance}` : ''}`).join(', ')}`
600
+ ? `several are open on ${c.serverId}: name one of ${open.map((o) => `${nameToTypeBack(o.toolset)}${o.instance ? `:${nameToTypeBack(o.instance)}` : ''}`).join(', ')}`
420
601
  : `nothing is open on ${c.serverId}`);
421
602
  }
422
603
  toolset = open[0]!.toolset;
@@ -424,28 +605,33 @@ export class ToolsetsModule implements Module {
424
605
  }
425
606
  if (p.isDeclared(toolset!)) {
426
607
  // As open refuses one: a toolset without instances is closed without one.
427
- if (instance && !p.declared[toolset!]!.instances) fail(`${c.serverId}/${toolset} has no instances: close it without one`);
608
+ if (instance && !p.declared[toolset!]!.instances) fail(`${c.serverId}/${nameToTypeBack(toolset!)} has no instances: close it without one`);
428
609
  if (p.declared[toolset!]!.instances && !instance) {
429
610
  const open = p.openRecords(toolset);
430
- if (open.length !== 1) fail(open.length ? `several ${toolset} instances are open (${open.map((o) => o.instance).join(', ')}): say which` : `${toolset} is not open`);
611
+ if (open.length !== 1) fail(open.length ? `several ${nameToTypeBack(toolset!)} instances are open (${open.map((o) => nameToTypeBack(String(o.instance))).join(', ')}): say which` : `${nameToTypeBack(toolset!)} is not open`);
431
612
  instance = open[0]!.instance;
432
613
  }
433
614
  let closed: Awaited<ReturnType<ToolsetController['close']>>;
434
615
  try {
435
- closed = await c.close(toolset!, instance);
616
+ closed = await c.close(toolset!, instance, { shown });
436
617
  } catch (error) {
437
- return fail(`${c.serverId} did not close ${toolset}${instance ? `:${instance}` : ''}: ${error instanceof Error ? error.message : String(error)}`);
618
+ const what = `${nameToTypeBack(toolset!)}${instance ? `:${nameToTypeBack(instance)}` : ''}`;
619
+ return requestFailure(c.serverId, `did not close ${what}`, `didn't confirm closing ${what}`, error);
438
620
  }
439
621
  const key = keyOf(toolset!, instance);
440
- const change = p.render(closed.change) || `${p.label(key)} closed.`;
622
+ const change = p.render(closed.change, { mark: shown, describeAll: !shown }) || `${p.label(key)} closed.`;
623
+ take?.({ toolsets: c, cs: closed.change });
441
624
  return blocksResult([...asBlocks(closed.content), { type: 'text', text: change }]);
442
625
  }
443
626
  if (p.undeclaredGroups.includes(toolset!)) {
444
- if (instance) fail(`${c.serverId}/${toolset} takes no instance (the server doesn't declare it)`);
445
- if (!p.isOpen(toolset!)) fail(`${c.serverId}/${toolset} is not open`);
627
+ if (instance) fail(`${c.serverId}/${nameToTypeBack(toolset!)} takes no instance (the server doesn't declare it)`);
628
+ if (!p.isOpen(toolset!)) fail(`${c.serverId}/${nameToTypeBack(toolset!)} is not open`);
446
629
  const cs = c.setGroupOpen(toolset!, false);
447
- return textResult(p.render(cs) || `${p.label(keyOf(toolset!))} closed.`);
630
+ const text = p.render(cs, { mark: shown, describeAll: !shown }) || `${p.label(keyOf(toolset!))} closed.`;
631
+ if (take) take({ toolsets: c, cs });
632
+ else c.tell(cs);
633
+ return textResult(text);
448
634
  }
449
- return fail(`${c.serverId} has no toolset "${toolset}"`);
635
+ return fail(noToolset(c, toolset!));
450
636
  }
451
637
  }