@animalabs/agent-framework 0.28.0 → 0.30.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 (289) hide show
  1. package/README.md +88 -25
  2. package/dist/src/admission/admit.d.ts +90 -0
  3. package/dist/src/admission/admit.d.ts.map +1 -0
  4. package/dist/src/admission/admit.js +43 -0
  5. package/dist/src/admission/admit.js.map +1 -0
  6. package/dist/src/agent.d.ts +21 -0
  7. package/dist/src/agent.d.ts.map +1 -1
  8. package/dist/src/agent.js +47 -7
  9. package/dist/src/agent.js.map +1 -1
  10. package/dist/src/api/mcp-server-core.d.ts.map +1 -1
  11. package/dist/src/api/mcp-server-core.js +3 -12
  12. package/dist/src/api/mcp-server-core.js.map +1 -1
  13. package/dist/src/api/server.d.ts.map +1 -1
  14. package/dist/src/api/server.js +7 -3
  15. package/dist/src/api/server.js.map +1 -1
  16. package/dist/src/author-presentation.d.ts +50 -0
  17. package/dist/src/author-presentation.d.ts.map +1 -0
  18. package/dist/src/author-presentation.js +58 -0
  19. package/dist/src/author-presentation.js.map +1 -0
  20. package/dist/src/auto-continue.d.ts +2 -0
  21. package/dist/src/auto-continue.d.ts.map +1 -1
  22. package/dist/src/auto-continue.js +5 -2
  23. package/dist/src/auto-continue.js.map +1 -1
  24. package/dist/src/code-execution/isolate/runner.d.ts +7 -5
  25. package/dist/src/code-execution/isolate/runner.d.ts.map +1 -1
  26. package/dist/src/code-execution/isolate/runner.js +7 -5
  27. package/dist/src/code-execution/isolate/runner.js.map +1 -1
  28. package/dist/src/code-execution/isolate/scripts.d.ts.map +1 -1
  29. package/dist/src/code-execution/isolate/scripts.js +3 -2
  30. package/dist/src/code-execution/isolate/scripts.js.map +1 -1
  31. package/dist/src/dendrite/index.d.ts +6 -0
  32. package/dist/src/dendrite/index.d.ts.map +1 -0
  33. package/dist/src/dendrite/index.js +3 -0
  34. package/dist/src/dendrite/index.js.map +1 -0
  35. package/dist/src/dendrite/presets.d.ts +60 -0
  36. package/dist/src/dendrite/presets.d.ts.map +1 -0
  37. package/dist/src/dendrite/presets.js +117 -0
  38. package/dist/src/dendrite/presets.js.map +1 -0
  39. package/dist/src/dendrite/registry.d.ts +260 -0
  40. package/dist/src/dendrite/registry.d.ts.map +1 -0
  41. package/dist/src/dendrite/registry.js +742 -0
  42. package/dist/src/dendrite/registry.js.map +1 -0
  43. package/dist/src/dendrite/types.d.ts +392 -0
  44. package/dist/src/dendrite/types.d.ts.map +1 -0
  45. package/dist/src/dendrite/types.js +24 -0
  46. package/dist/src/dendrite/types.js.map +1 -0
  47. package/dist/src/framework.d.ts +846 -67
  48. package/dist/src/framework.d.ts.map +1 -1
  49. package/dist/src/framework.js +4213 -924
  50. package/dist/src/framework.js.map +1 -1
  51. package/dist/src/gate/event-gate.d.ts +46 -14
  52. package/dist/src/gate/event-gate.d.ts.map +1 -1
  53. package/dist/src/gate/event-gate.js +143 -53
  54. package/dist/src/gate/event-gate.js.map +1 -1
  55. package/dist/src/gate/index.d.ts +1 -1
  56. package/dist/src/gate/index.d.ts.map +1 -1
  57. package/dist/src/gate/index.js +1 -1
  58. package/dist/src/gate/index.js.map +1 -1
  59. package/dist/src/gate/types.d.ts +12 -2
  60. package/dist/src/gate/types.d.ts.map +1 -1
  61. package/dist/src/heartbeat/heartbeat.d.ts +18 -0
  62. package/dist/src/heartbeat/heartbeat.d.ts.map +1 -1
  63. package/dist/src/heartbeat/heartbeat.js +34 -8
  64. package/dist/src/heartbeat/heartbeat.js.map +1 -1
  65. package/dist/src/held-writes.d.ts +3 -3
  66. package/dist/src/held-writes.js +3 -3
  67. package/dist/src/host-presentation.d.ts +174 -0
  68. package/dist/src/host-presentation.d.ts.map +1 -0
  69. package/dist/src/host-presentation.js +565 -0
  70. package/dist/src/host-presentation.js.map +1 -0
  71. package/dist/src/index.d.ts +7 -4
  72. package/dist/src/index.d.ts.map +1 -1
  73. package/dist/src/index.js +7 -2
  74. package/dist/src/index.js.map +1 -1
  75. package/dist/src/line-reader.d.ts +4 -0
  76. package/dist/src/line-reader.d.ts.map +1 -0
  77. package/dist/src/line-reader.js +59 -0
  78. package/dist/src/line-reader.js.map +1 -0
  79. package/dist/src/mcpl/channel-registry.d.ts +118 -15
  80. package/dist/src/mcpl/channel-registry.d.ts.map +1 -1
  81. package/dist/src/mcpl/channel-registry.js +266 -110
  82. package/dist/src/mcpl/channel-registry.js.map +1 -1
  83. package/dist/src/mcpl/conversation-router.d.ts.map +1 -1
  84. package/dist/src/mcpl/conversation-router.js +2 -1
  85. package/dist/src/mcpl/conversation-router.js.map +1 -1
  86. package/dist/src/mcpl/convert-block.d.ts +1 -0
  87. package/dist/src/mcpl/convert-block.d.ts.map +1 -1
  88. package/dist/src/mcpl/convert-block.js +16 -4
  89. package/dist/src/mcpl/convert-block.js.map +1 -1
  90. package/dist/src/mcpl/hook-orchestrator.d.ts.map +1 -1
  91. package/dist/src/mcpl/hook-orchestrator.js +4 -3
  92. package/dist/src/mcpl/hook-orchestrator.js.map +1 -1
  93. package/dist/src/mcpl/inbound-source.d.ts +3 -1
  94. package/dist/src/mcpl/inbound-source.d.ts.map +1 -1
  95. package/dist/src/mcpl/inbound-source.js +3 -1
  96. package/dist/src/mcpl/inbound-source.js.map +1 -1
  97. package/dist/src/mcpl/modern-connection.d.ts.map +1 -1
  98. package/dist/src/mcpl/modern-connection.js +11 -7
  99. package/dist/src/mcpl/modern-connection.js.map +1 -1
  100. package/dist/src/mcpl/protocol-family.d.ts +3 -2
  101. package/dist/src/mcpl/protocol-family.d.ts.map +1 -1
  102. package/dist/src/mcpl/protocol-family.js +25 -3
  103. package/dist/src/mcpl/protocol-family.js.map +1 -1
  104. package/dist/src/mcpl/push-coalescer.d.ts +9 -0
  105. package/dist/src/mcpl/push-coalescer.d.ts.map +1 -1
  106. package/dist/src/mcpl/push-coalescer.js +21 -0
  107. package/dist/src/mcpl/push-coalescer.js.map +1 -1
  108. package/dist/src/mcpl/push-handler.js +2 -2
  109. package/dist/src/mcpl/push-handler.js.map +1 -1
  110. package/dist/src/mcpl/references.d.ts +12 -40
  111. package/dist/src/mcpl/references.d.ts.map +1 -1
  112. package/dist/src/mcpl/references.js +29 -23
  113. package/dist/src/mcpl/references.js.map +1 -1
  114. package/dist/src/mcpl/server-connection.d.ts.map +1 -1
  115. package/dist/src/mcpl/server-connection.js +20 -20
  116. package/dist/src/mcpl/server-connection.js.map +1 -1
  117. package/dist/src/mcpl/skip-reply-wake.d.ts +25 -0
  118. package/dist/src/mcpl/skip-reply-wake.d.ts.map +1 -1
  119. package/dist/src/mcpl/skip-reply-wake.js +25 -0
  120. package/dist/src/mcpl/skip-reply-wake.js.map +1 -1
  121. package/dist/src/mcpl/tool-presence.d.ts.map +1 -1
  122. package/dist/src/mcpl/tool-presence.js +18 -17
  123. package/dist/src/mcpl/tool-presence.js.map +1 -1
  124. package/dist/src/mcpl/tool-signature.d.ts.map +1 -1
  125. package/dist/src/mcpl/tool-signature.js +17 -16
  126. package/dist/src/mcpl/tool-signature.js.map +1 -1
  127. package/dist/src/mcpl/toolset-host.d.ts +8 -26
  128. package/dist/src/mcpl/toolset-host.d.ts.map +1 -1
  129. package/dist/src/mcpl/toolset-host.js +16 -6
  130. package/dist/src/mcpl/toolset-host.js.map +1 -1
  131. package/dist/src/mcpl/toolsets-module.d.ts +14 -25
  132. package/dist/src/mcpl/toolsets-module.d.ts.map +1 -1
  133. package/dist/src/mcpl/toolsets-module.js +187 -50
  134. package/dist/src/mcpl/toolsets-module.js.map +1 -1
  135. package/dist/src/mcpl/transport.d.ts +1 -1
  136. package/dist/src/mcpl/transport.d.ts.map +1 -1
  137. package/dist/src/mcpl/transport.js +16 -16
  138. package/dist/src/mcpl/transport.js.map +1 -1
  139. package/dist/src/mcpl/types.d.ts +28 -12
  140. package/dist/src/mcpl/types.d.ts.map +1 -1
  141. package/dist/src/mcpl/types.js.map +1 -1
  142. package/dist/src/module-registry.d.ts +6 -7
  143. package/dist/src/module-registry.d.ts.map +1 -1
  144. package/dist/src/module-registry.js +6 -7
  145. package/dist/src/module-registry.js.map +1 -1
  146. package/dist/src/modules/api/index.d.ts.map +1 -1
  147. package/dist/src/modules/api/index.js +4 -2
  148. package/dist/src/modules/api/index.js.map +1 -1
  149. package/dist/src/modules/discord/index.d.ts.map +1 -1
  150. package/dist/src/modules/discord/index.js +33 -25
  151. package/dist/src/modules/discord/index.js.map +1 -1
  152. package/dist/src/modules/history/index.js +8 -6
  153. package/dist/src/modules/history/index.js.map +1 -1
  154. package/dist/src/modules/sender-name.d.ts +7 -0
  155. package/dist/src/modules/sender-name.d.ts.map +1 -0
  156. package/dist/src/modules/sender-name.js +10 -0
  157. package/dist/src/modules/sender-name.js.map +1 -0
  158. package/dist/src/modules/workspace/answers.d.ts +5 -1
  159. package/dist/src/modules/workspace/answers.d.ts.map +1 -1
  160. package/dist/src/modules/workspace/answers.js +16 -1
  161. package/dist/src/modules/workspace/answers.js.map +1 -1
  162. package/dist/src/modules/workspace/direct-write.d.ts.map +1 -1
  163. package/dist/src/modules/workspace/direct-write.js +6 -4
  164. package/dist/src/modules/workspace/direct-write.js.map +1 -1
  165. package/dist/src/modules/workspace/effects.d.ts +4 -2
  166. package/dist/src/modules/workspace/effects.d.ts.map +1 -1
  167. package/dist/src/modules/workspace/effects.js +50 -40
  168. package/dist/src/modules/workspace/effects.js.map +1 -1
  169. package/dist/src/modules/workspace/index.d.ts +1 -0
  170. package/dist/src/modules/workspace/index.d.ts.map +1 -1
  171. package/dist/src/modules/workspace/index.js +66 -36
  172. package/dist/src/modules/workspace/index.js.map +1 -1
  173. package/dist/src/modules/workspace/observe.d.ts +51 -20
  174. package/dist/src/modules/workspace/observe.d.ts.map +1 -1
  175. package/dist/src/modules/workspace/observe.js +120 -82
  176. package/dist/src/modules/workspace/observe.js.map +1 -1
  177. package/dist/src/modules/workspace/reconcile.d.ts +7 -1
  178. package/dist/src/modules/workspace/reconcile.d.ts.map +1 -1
  179. package/dist/src/modules/workspace/reconcile.js +133 -37
  180. package/dist/src/modules/workspace/reconcile.js.map +1 -1
  181. package/dist/src/notice-tags.d.ts +81 -0
  182. package/dist/src/notice-tags.d.ts.map +1 -0
  183. package/dist/src/notice-tags.js +85 -0
  184. package/dist/src/notice-tags.js.map +1 -0
  185. package/dist/src/reasoning-loss-notice.d.ts.map +1 -1
  186. package/dist/src/reasoning-loss-notice.js +5 -3
  187. package/dist/src/reasoning-loss-notice.js.map +1 -1
  188. package/dist/src/silent-heartbeat.d.ts +5 -55
  189. package/dist/src/silent-heartbeat.d.ts.map +1 -1
  190. package/dist/src/silent-heartbeat.js +63 -7
  191. package/dist/src/silent-heartbeat.js.map +1 -1
  192. package/dist/src/stored-injections.d.ts +4 -0
  193. package/dist/src/stored-injections.d.ts.map +1 -1
  194. package/dist/src/stored-injections.js +6 -0
  195. package/dist/src/stored-injections.js.map +1 -1
  196. package/dist/src/supplied-text.d.ts +35 -0
  197. package/dist/src/supplied-text.d.ts.map +1 -0
  198. package/dist/src/supplied-text.js +141 -0
  199. package/dist/src/supplied-text.js.map +1 -0
  200. package/dist/src/tool-args.d.ts.map +1 -1
  201. package/dist/src/tool-args.js +9 -8
  202. package/dist/src/tool-args.js.map +1 -1
  203. package/dist/src/tool-image-ledger.d.ts.map +1 -1
  204. package/dist/src/tool-image-ledger.js +2 -1
  205. package/dist/src/tool-image-ledger.js.map +1 -1
  206. package/dist/src/tool-presentation.d.ts.map +1 -1
  207. package/dist/src/tool-presentation.js +5 -4
  208. package/dist/src/tool-presentation.js.map +1 -1
  209. package/dist/src/tune-out/coordinator.d.ts +382 -11
  210. package/dist/src/tune-out/coordinator.d.ts.map +1 -1
  211. package/dist/src/tune-out/coordinator.js +1100 -119
  212. package/dist/src/tune-out/coordinator.js.map +1 -1
  213. package/dist/src/tune-out/tools.d.ts +101 -5
  214. package/dist/src/tune-out/tools.d.ts.map +1 -1
  215. package/dist/src/tune-out/tools.js +55 -18
  216. package/dist/src/tune-out/tools.js.map +1 -1
  217. package/dist/src/types/agent.d.ts +48 -9
  218. package/dist/src/types/agent.d.ts.map +1 -1
  219. package/dist/src/types/framework.d.ts +142 -0
  220. package/dist/src/types/framework.d.ts.map +1 -1
  221. package/dist/src/types/module.d.ts +22 -11
  222. package/dist/src/types/module.d.ts.map +1 -1
  223. package/dist/src/types/trace.d.ts +136 -0
  224. package/dist/src/types/trace.d.ts.map +1 -1
  225. package/package.json +3 -3
  226. package/src/admission/admit.ts +113 -0
  227. package/src/agent.ts +52 -7
  228. package/src/api/mcp-server-core.ts +3 -14
  229. package/src/api/server.ts +7 -3
  230. package/src/author-presentation.ts +81 -0
  231. package/src/auto-continue.ts +8 -2
  232. package/src/code-execution/isolate/runner.ts +7 -5
  233. package/src/code-execution/isolate/scripts.ts +3 -2
  234. package/src/dendrite/index.ts +32 -0
  235. package/src/dendrite/presets.ts +149 -0
  236. package/src/dendrite/registry.ts +808 -0
  237. package/src/dendrite/types.ts +397 -0
  238. package/src/framework.ts +4362 -852
  239. package/src/gate/event-gate.ts +159 -52
  240. package/src/gate/index.ts +1 -1
  241. package/src/gate/types.ts +12 -2
  242. package/src/heartbeat/heartbeat.ts +48 -9
  243. package/src/held-writes.ts +3 -3
  244. package/src/host-presentation.ts +612 -0
  245. package/src/index.ts +17 -4
  246. package/src/line-reader.ts +58 -0
  247. package/src/mcpl/channel-registry.ts +306 -111
  248. package/src/mcpl/conversation-router.ts +2 -1
  249. package/src/mcpl/convert-block.ts +17 -4
  250. package/src/mcpl/hook-orchestrator.ts +4 -3
  251. package/src/mcpl/inbound-source.ts +3 -1
  252. package/src/mcpl/modern-connection.ts +11 -7
  253. package/src/mcpl/protocol-family.ts +24 -3
  254. package/src/mcpl/push-coalescer.ts +19 -0
  255. package/src/mcpl/push-handler.ts +2 -2
  256. package/src/mcpl/references.ts +31 -23
  257. package/src/mcpl/server-connection.ts +20 -18
  258. package/src/mcpl/skip-reply-wake.ts +35 -0
  259. package/src/mcpl/tool-presence.ts +18 -17
  260. package/src/mcpl/tool-signature.ts +17 -16
  261. package/src/mcpl/toolset-host.ts +18 -6
  262. package/src/mcpl/toolsets-module.ts +149 -50
  263. package/src/mcpl/transport.ts +18 -14
  264. package/src/mcpl/types.ts +29 -12
  265. package/src/module-registry.ts +6 -7
  266. package/src/modules/api/index.ts +4 -2
  267. package/src/modules/discord/index.ts +37 -25
  268. package/src/modules/history/index.ts +8 -6
  269. package/src/modules/sender-name.ts +9 -0
  270. package/src/modules/workspace/answers.ts +20 -2
  271. package/src/modules/workspace/direct-write.ts +4 -3
  272. package/src/modules/workspace/effects.ts +50 -40
  273. package/src/modules/workspace/index.ts +58 -34
  274. package/src/modules/workspace/observe.ts +139 -88
  275. package/src/modules/workspace/reconcile.ts +159 -36
  276. package/src/notice-tags.ts +86 -0
  277. package/src/reasoning-loss-notice.ts +5 -3
  278. package/src/silent-heartbeat.ts +11 -7
  279. package/src/stored-injections.ts +7 -0
  280. package/src/supplied-text.ts +154 -0
  281. package/src/tool-args.ts +9 -8
  282. package/src/tool-image-ledger.ts +2 -1
  283. package/src/tool-presentation.ts +5 -4
  284. package/src/tune-out/coordinator.ts +1252 -123
  285. package/src/tune-out/tools.ts +152 -11
  286. package/src/types/agent.ts +51 -9
  287. package/src/types/framework.ts +146 -0
  288. package/src/types/module.ts +23 -11
  289. package/src/types/trace.ts +150 -0
@@ -10,6 +10,7 @@
10
10
  * reference host of RFC-014. Dependency-free.
11
11
  */
12
12
  import { createHash } from 'node:crypto';
13
+ import { nameToTypeBack, quotedValue, suppliedValue } from '../supplied-text.js';
13
14
 
14
15
  export type JsonSchema = Record<string, unknown>;
15
16
 
@@ -34,7 +35,7 @@ function nestsWithin(value: unknown, depth = 0): boolean {
34
35
 
35
36
  /** A schema's literal value (an enum's, a const or a default) as JSON, or … where it's nested too deeply to write. */
36
37
  function literal(value: unknown): string {
37
- return nestsWithin(value) ? JSON.stringify(value) ?? 'null' : '…';
38
+ return nestsWithin(value) ? quotedValue(value) : '…';
38
39
  }
39
40
 
40
41
  function typeSig(schema: JsonSchema | undefined, depth = 0): string {
@@ -80,12 +81,12 @@ function typeSig(schema: JsonSchema | undefined, depth = 0): string {
80
81
  if (!isObj(props) || Object.keys(props).length === 0) return 'object';
81
82
  if (depth >= 3) return '{…}';
82
83
  const req = new Set(Array.isArray(schema.required) ? (schema.required as string[]) : []);
83
- return `{${Object.entries(props).map(([k, v]) => `${k}${req.has(k) ? '' : '?'}: ${typeSig(v, depth + 1)}`).join(', ')}}`;
84
+ return `{${Object.entries(props).map(([k, v]) => `${nameToTypeBack(k)}${req.has(k) ? '' : '?'}: ${typeSig(v, depth + 1)}`).join(', ')}}`;
84
85
  }
85
86
  case undefined:
86
87
  return 'any';
87
88
  default:
88
- return t;
89
+ return suppliedValue(t);
89
90
  }
90
91
  }
91
92
 
@@ -98,9 +99,9 @@ export function signature(tool: HintedTool): string {
98
99
  const req = new Set(Array.isArray(s.required) ? (s.required as string[]) : []);
99
100
  const params = Object.entries(props).map(([k, v]) => {
100
101
  const def = isObj(v) && v.default !== undefined ? ` =${literal(v.default)}` : '';
101
- return `${k}${req.has(k) ? '' : '?'}: ${typeSig(v, 1)}${def}`;
102
+ return `${nameToTypeBack(k)}${req.has(k) ? '' : '?'}: ${typeSig(v, 1)}${def}`;
102
103
  });
103
- return `${tool.name}(${params.join(', ')})`;
104
+ return `${nameToTypeBack(String(tool.name))}(${params.join(', ')})`;
104
105
  }
105
106
 
106
107
  /** An options schema as `{size?: 9|13|19 =9, …}` (a toolset's options). */
@@ -153,7 +154,7 @@ export function renderToolLines(tools: HintedTool[], shown: Shown, opts: { descr
153
154
  const describe = opts.describeAll || !shown.has(d);
154
155
  shown.add(d);
155
156
  const sentence = describe ? firstSentence(t.description) : '';
156
- return `${indent}${signature(t)}${sentence ? ` — ${sentence}` : ''}`;
157
+ return `${indent}${signature(t)}${sentence ? ` — ${suppliedValue(sentence)}` : ''}`;
157
158
  });
158
159
  }
159
160
 
@@ -161,7 +162,7 @@ export function renderToolLines(tools: HintedTool[], shown: Shown, opts: { descr
161
162
  export function jsonText(value: unknown, space?: number): string {
162
163
  if (!nestsWithin(value)) return '(nested too deeply to show)';
163
164
  try {
164
- return JSON.stringify(value, null, space) ?? '';
165
+ return JSON.stringify(value) === undefined ? '' : quotedValue(value, space);
165
166
  } catch {
166
167
  return '(too long to show)';
167
168
  }
@@ -173,8 +174,8 @@ export function renderToolDefinition(tool: HintedTool): string {
173
174
  const classes = Array.isArray(cls) ? cls.filter((c): c is string => typeof c === 'string') : [];
174
175
  return [
175
176
  signature(tool),
176
- typeof tool.description === 'string' ? tool.description : '',
177
- ...(classes.length ? [`class: ${classes.join(', ')}`] : []),
177
+ typeof tool.description === 'string' ? suppliedValue(tool.description) : '',
178
+ ...(classes.length ? [`class: ${classes.map(suppliedValue).join(', ')}`] : []),
178
179
  'inputSchema:',
179
180
  jsonText(tool.inputSchema ?? {}, 2),
180
181
  ]
@@ -252,15 +253,15 @@ function check(schema: unknown, value: unknown, at: string, depth: number, budge
252
253
  const s = schema;
253
254
  if (s.type !== undefined) {
254
255
  const types = Array.isArray(s.type) ? (s.type as string[]) : [s.type as string];
255
- if (!types.some((t) => typeOk(t, value))) return `${at}: must be ${types.join(' or ')}`;
256
+ if (!types.some((t) => typeOk(t, value))) return `${at}: must be ${types.map(suppliedValue).join(' or ')}`;
256
257
  }
257
258
  if (Array.isArray(s.enum) || s.const !== undefined) {
258
259
  const own = canonicalJson(value);
259
260
  if (Array.isArray(s.enum)) {
260
261
  spend(budget, s.enum.length);
261
- if (!s.enum.some((e) => canonicalJson(e) === own)) return `${at}: must be one of ${s.enum.map((e) => JSON.stringify(e)).join('|')}`;
262
+ if (!s.enum.some((e) => canonicalJson(e) === own)) return `${at}: must be one of ${s.enum.map((e) => quotedValue(e)).join('|')}`;
262
263
  }
263
- if (s.const !== undefined && canonicalJson(s.const) !== own) return `${at}: must be ${JSON.stringify(s.const)}`;
264
+ if (s.const !== undefined && canonicalJson(s.const) !== own) return `${at}: must be ${quotedValue(s.const)}`;
264
265
  }
265
266
  if (typeof value === 'string') {
266
267
  const n = [...value].length;
@@ -286,16 +287,16 @@ function check(schema: unknown, value: unknown, at: string, depth: number, budge
286
287
  if (isObj(value)) {
287
288
  const props = isObj(s.properties) ? s.properties : {};
288
289
  for (const r of Array.isArray(s.required) ? (s.required as unknown[]) : []) {
289
- if (typeof r === 'string' && !Object.hasOwn(value, r)) return `${at}.${r}: required`;
290
+ if (typeof r === 'string' && !Object.hasOwn(value, r)) return `${at}.${nameToTypeBack(r)}: required`;
290
291
  }
291
292
  for (const [k, v] of Object.entries(value)) {
292
293
  if (Object.hasOwn(props, k)) {
293
- const e = check(props[k], v, `${at}.${k}`, depth + 1, budget);
294
+ const e = check(props[k], v, `${at}.${nameToTypeBack(k)}`, depth + 1, budget);
294
295
  if (e) return e;
295
296
  } else if (s.additionalProperties === false) {
296
- return `${at}.${k}: not an argument of this tool`;
297
+ return `${at}.${nameToTypeBack(k)}: not an argument of this tool`;
297
298
  } else if (isObj(s.additionalProperties)) {
298
- const e = check(s.additionalProperties, v, `${at}.${k}`, depth + 1, budget);
299
+ const e = check(s.additionalProperties, v, `${at}.${nameToTypeBack(k)}`, depth + 1, budget);
299
300
  if (e) return e;
300
301
  }
301
302
  }
@@ -24,6 +24,7 @@
24
24
  * which the toolsets module gathers at its next start as a stored
25
25
  * `afterUser` injection.
26
26
  */
27
+ import { nameToTypeBack, quotedValue } from '../supplied-text.js';
27
28
  import { McplRequestError, type McplServerConnection, type HintRead } from './server-connection.js';
28
29
  import type { McplServerConfig } from './types.js';
29
30
  import { isToolAllowed } from './tool-policy.js';
@@ -73,6 +74,10 @@ export interface ToolsetHostDeps {
73
74
  * where it isn't published. RFC §4.2: until its move applies, a tool
74
75
  * keeps working where it is. */
75
76
  published?(serverId: string, tool: string): Presence | undefined;
77
+ /** The host's own toolsets an agent has open (the built-in one, server
78
+ * tabs): each one's marker and reminder, for RFC §8.3 reminders beside the
79
+ * servers' toolsets. */
80
+ hostOpen?(agentName: string): Array<{ marker: string; reminder: () => string; dueWhenAbsent?: boolean }>;
76
81
  }
77
82
 
78
83
  /** One server's RFC-014 state and traffic. Lives as long as the server's
@@ -381,8 +386,8 @@ export class ToolsetController {
381
386
  */
382
387
  dynamicCallRefusal(tool: string, calledAs: string): string | null {
383
388
  if (!this.presence.active || !this.presence.tool(tool) || this.presence.shownAs(tool, 'static')) return null;
384
- return `There's no tool "${calledAs}" in your tool list, so nothing ran: ${tool} is a dynamic tool of ${this.serverId}, ` +
385
- `called with ${META_TOOL_NAMES.act} {server:"${this.serverId}", tool:"${tool}", args}. ${this.presence.toolStatus(tool)}.`;
389
+ return `There's no tool "${calledAs}" in your tool list, so nothing ran: ${nameToTypeBack(tool)} is a dynamic tool of ${this.serverId}, ` +
390
+ `called with ${META_TOOL_NAMES.act} {server:"${this.serverId}", tool:${quotedValue(tool)}, args}. ${this.presence.toolStatus(tool)}.`;
386
391
  }
387
392
 
388
393
  /**
@@ -621,8 +626,8 @@ export class ToolsetController {
621
626
  if (!moved.length) return;
622
627
  const cs = emptyChange();
623
628
  cs.notes.push(...moved.map(({ name, to }) => to === 'dynamic'
624
- ? `${name} left your tool list: it's announced when available, and called with ${META_TOOL_NAMES.act} {server:"${this.serverId}", tool:"${name}", args}.`
625
- : `${name} is in your tool list now, as ${this.deps.listedName(this.serverId, name)}.`));
629
+ ? `${nameToTypeBack(name)} left your tool list: it's announced when available, and called with ${META_TOOL_NAMES.act} {server:"${this.serverId}", tool:${quotedValue(name)}, args}.`
630
+ : `${nameToTypeBack(name)} is in your tool list now, as ${nameToTypeBack(this.deps.listedName(this.serverId, name))}.`));
626
631
  this.deliver(cs);
627
632
  }
628
633
  }
@@ -681,8 +686,9 @@ export class ToolsetHub {
681
686
  * reminder due.
682
687
  */
683
688
  observeCompile(agentName: string, messages: readonly unknown[]): void {
689
+ const host = this.deps.hostOpen?.(agentName) ?? [];
684
690
  const open = this.openMarkers();
685
- if (open.length === 0) {
691
+ if (open.length === 0 && host.length === 0) {
686
692
  this.seen.delete(agentName);
687
693
  this.due.delete(agentName);
688
694
  return;
@@ -696,6 +702,9 @@ export class ToolsetHub {
696
702
  }
697
703
  for (const marker of [...seen]) if (!open.includes(marker)) seen.delete(marker);
698
704
  this.seen.set(agentName, seen);
705
+ // A host toolset open across a restart was never seen here: it's due
706
+ // whenever a request doesn't carry its marker.
707
+ for (const h of host) if (h.dueWhenAbsent && !text.includes(h.marker)) missing = true;
699
708
  // Due until a request carries every marker the agent has seen again: a
700
709
  // gather that doesn't send (a preview) leaves it due for the next start.
701
710
  if (missing) this.due.add(agentName);
@@ -705,7 +714,10 @@ export class ToolsetHub {
705
714
  /** The reminder due for `agentName`, or null when none is. */
706
715
  reminderFor(agentName: string): string | null {
707
716
  if (!this.due.has(agentName)) return null;
708
- const parts = this.presenting().filter((c) => this.deps.canNotify(c.serverId)).map((c) => c.presence.reminder()).filter((r): r is string => !!r);
717
+ const parts = [
718
+ ...(this.deps.hostOpen?.(agentName) ?? []).map((h) => h.reminder()),
719
+ ...this.presenting().filter((c) => this.deps.canNotify(c.serverId)).map((c) => c.presence.reminder()).filter((r): r is string => !!r),
720
+ ];
709
721
  if (parts.length === 0) return null;
710
722
  return `Open toolsets (a reminder: their earlier announcements have left your context):\n${parts.join('\n')}`;
711
723
  }
@@ -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,6 +23,7 @@
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';
@@ -29,6 +31,7 @@ import { META_TOOL_NAMES, TOOLSETS_MODULE, type ToolsetController, type ToolsetH
29
31
  import { keyOf, type ChangeSet, type ToolPresence } from './tool-presence.js';
30
32
  import { checkArgs, jsonText, renderToolDefinition, type HintedTool } from './tool-signature.js';
31
33
  import { McplRequestError } from './server-connection.js';
34
+ import { BUILTIN_TOOLSET, type HostPresentation } from '../host-presentation.js';
32
35
 
33
36
  /** The namespace a reminder's stored injection carries. */
34
37
  export const TOOLSETS_REMINDER_NAMESPACE = 'mcpl-toolsets';
@@ -37,24 +40,26 @@ const META_TOOLS: ToolDefinition[] = [
37
40
  {
38
41
  name: 'open',
39
42
  description:
40
- 'Open a toolset of an MCPL server (a game, a building mode, a card table) so its tools can be called with toolsets--act. ' +
41
- 'Returns what the server shows on opening and the tools available now. server may be left out when one server has the toolset, ' +
42
- '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; ' +
43
47
  'options as toolsets--look lists them.',
44
48
  inputSchema: {
45
49
  type: 'object',
46
50
  properties: {
47
51
  server: { type: 'string', description: 'the MCPL server id' },
48
- toolset: { type: 'string' },
52
+ toolset: { type: 'string', description: '"settings" for the host\'s own; a server\'s toolset with server' },
49
53
  instance: { type: 'string', description: 'a name for this copy (toolsets with instances only)' },
50
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' },
51
56
  },
52
57
  },
53
58
  },
54
59
  {
55
60
  name: 'act',
56
61
  description:
57
- '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. ' +
58
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. ' +
59
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.',
60
65
  inputSchema: {
@@ -71,7 +76,7 @@ const META_TOOLS: ToolDefinition[] = [
71
76
  {
72
77
  name: 'look',
73
78
  description:
74
- '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. ' +
75
80
  'With tool: that tool\'s full description and input schema. With toolset or instance: just that one.',
76
81
  inputSchema: {
77
82
  type: 'object',
@@ -85,13 +90,15 @@ const META_TOOLS: ToolDefinition[] = [
85
90
  },
86
91
  {
87
92
  name: 'close',
88
- 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.',
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.',
89
95
  inputSchema: {
90
96
  type: 'object',
91
97
  properties: {
92
98
  server: { type: 'string', description: 'the MCPL server id (may be left out when one server has the toolset open)' },
93
- 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' },
94
100
  instance: { type: 'string' },
101
+ disconnect: { type: 'boolean', description: 'with server alone: disconnect it as a closed tab until it is opened again' },
95
102
  },
96
103
  },
97
104
  },
@@ -99,6 +106,21 @@ const META_TOOLS: ToolDefinition[] = [
99
106
 
100
107
  const str = (v: unknown): string | undefined => (typeof v === 'string' && v.trim() ? v.trim() : undefined);
101
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
+
102
124
  const SELECTORS = ['server', 'toolset', 'instance', 'tool'] as const;
103
125
 
104
126
  function checkSelectors(args: Record<string, unknown>): void {
@@ -117,7 +139,7 @@ export function withLookHint(serverId: string, text: string): string {
117
139
  }
118
140
 
119
141
  function requestFailure(serverId: string, did: string, unconfirmed: string, error: unknown): never {
120
- const message = error instanceof Error ? error.message : String(error);
142
+ const message = suppliedValue(error instanceof Error ? error.message : String(error));
121
143
  if (error instanceof McplRequestError && error.outcome === 'not-sent') return fail(`${serverId} ${did}: ${message}`);
122
144
  return fail(withLookHint(serverId, `${serverId} ${unconfirmed}: ${message}`));
123
145
  }
@@ -194,7 +216,7 @@ const NONE_PRESENTS = 'no MCPL server presents toolsets now: MCPL tools are call
194
216
 
195
217
  function noToolset(c: ToolsetController, toolset: string): string {
196
218
  const names = [...Object.keys(c.presence.declared), ...c.presence.undeclaredGroups];
197
- return `${c.serverId} has no toolset "${toolset}"; ${names.length ? `it has ${names.join(', ')}` : 'it has none'}`;
219
+ return `${c.serverId} has no toolset "${toolset}"; ${names.length ? `it has ${names.map(nameToTypeBack).join(', ')}` : 'it has none'}`;
198
220
  }
199
221
 
200
222
  /** What a `toolsets--act` call resolves to: the server's tool, its instance and arguments. */
@@ -221,35 +243,35 @@ export function resolveAct(hub: ToolsetHub, input: unknown): ActTarget {
221
243
  try {
222
244
  const args = isObj(input) ? input : {};
223
245
  checkSelectors(args);
224
- const tool = str(args.tool) ?? fail('name the tool');
246
+ const tool = toolNamed(hub, args.server, args.tool) ?? fail('name the tool');
225
247
  const c = owner(hub, args.server, (x) => !!x.presence.tool(tool), `a tool "${tool}"`);
226
248
  const p = c.presence;
227
249
  if (!p.tool(tool)) fail(`${c.serverId} has no tool "${tool}": ${META_TOOL_NAMES.look} {server:"${c.serverId}"} shows what's available`);
228
- if (!p.shownAs(tool, 'dynamic')) 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.`);
229
251
  if (args.args !== undefined && !isObj(args.args)) fail('args must be an object of the tool\'s arguments');
230
252
  const callArgs = (args.args ?? {}) as Record<string, unknown>;
231
- let instance = str(args.instance);
232
253
  const ts = p.toolsetOf(tool);
254
+ let instance = instanceNamed([c], ts, args.instance);
233
255
  const declared = ts !== undefined && p.isDeclared(ts);
234
256
  const withInstances = hasInstances(p, ts);
235
257
  if (withInstances && !instance) {
236
258
  // RFC §5.5: the instance rides in _meta; with exactly one open, it's that one.
237
259
  const open = p.openRecords(ts);
238
260
  if (open.length === 1) instance = open[0]!.instance;
239
- else if (!open.length) fail(`${tool} belongs to ${ts}, which isn't open: ${p.openHint(ts!)}`);
240
- 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`);
241
263
  }
242
- 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`);
243
265
  // RFC §5.4: the host gates calls into an undeclared group it has closed.
244
- 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)}`);
245
267
  // RFC §6.1: arguments are checked against the definition in force for
246
268
  // that instance: its own from hints (the legal moves of one game), else
247
269
  // the toolset's, else the listed one.
248
270
  const key = actKey(p, tool, instance);
249
271
  const problem = checkArgs(p.defOf(key, tool).inputSchema, callArgs);
250
272
  if (problem) {
251
- fail(`Invalid arguments for ${tool} in ${p.label(key)}: ${problem}. Nothing was sent; ` +
252
- `${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.`);
253
275
  }
254
276
  return { ok: true, serverId: c.serverId, tool, ...(instance ? { instance } : {}), args: callArgs };
255
277
  } catch (error) {
@@ -267,6 +289,8 @@ export class ToolsetsModule implements Module {
267
289
  constructor(
268
290
  private readonly hub: ToolsetHub,
269
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,
270
294
  ) {}
271
295
 
272
296
  async start(_ctx: ModuleContext): Promise<void> {}
@@ -291,7 +315,23 @@ export class ToolsetsModule implements Module {
291
315
  const args = isObj(call.input) ? call.input : {};
292
316
  try {
293
317
  checkSelectors(args);
318
+ // An agent shown another's tool list can't switch it: what it opened
319
+ // or closed would change its owner's list, or nothing it is shown.
320
+ const caller = call.callerAgentName;
321
+ const owner = caller && this.host ? this.host.ownerOf(caller) : caller;
322
+ if ((call.name === 'open' || call.name === 'close') && caller && owner !== caller) {
323
+ fail(`${caller} is shown ${owner}'s tools and can't ${call.name} a toolset; it reaches the toolsets ${owner} has open. Nothing changed.`);
324
+ }
325
+ const hosted = await this.hostCall(call.name, args, call.callerAgentName);
326
+ if (hosted) return hosted;
294
327
  const take = this.takerFor(call);
328
+ const lead = call.name === 'look' ? this.hostLookLead(args, call.callerAgentName) : null;
329
+ if (lead !== null) {
330
+ if (!this.hub.presenting().length) return textResult(lead);
331
+ const theirs = await this.look(args, take);
332
+ if (theirs.isError || typeof theirs.data !== 'string') return theirs;
333
+ return textResult(`${lead}\n\n${theirs.data}`);
334
+ }
295
335
  switch (call.name) {
296
336
  case 'open': return await this.open(args, take);
297
337
  case 'act': return this.act(args);
@@ -307,6 +347,62 @@ export class ToolsetsModule implements Module {
307
347
  }
308
348
  }
309
349
 
350
+ /**
351
+ * The host's own toolsets: `toolset: "settings"` alone, or `server` alone
352
+ * (a server's tab), for open, close and look; a tool a host toolset has, for
353
+ * look. Null for everything else, which the servers' toolsets answer.
354
+ */
355
+ private async hostCall(name: string, args: Record<string, unknown>, agentName: string | undefined): Promise<ToolResult | null> {
356
+ const host = this.host;
357
+ if (!host || !agentName || !host.active(agentName)) return null;
358
+ const server = str(args.server);
359
+ const toolset = str(args.toolset);
360
+ const instance = str(args.instance);
361
+ const tool = str(args.tool);
362
+ const builtin = toolset === BUILTIN_TOOLSET && !server && !instance;
363
+ const tab = !!server && !toolset && !instance && args.options === undefined;
364
+ if (args.full !== undefined && typeof args.full !== 'boolean') fail('full must be true or false');
365
+ if (args.disconnect !== undefined && typeof args.disconnect !== 'boolean') fail('disconnect must be true or false');
366
+ if (tab && !host.knows(server!) && !this.hub.get(server!)) fail(`no MCPL server "${server}" is configured here`);
367
+ switch (name) {
368
+ case 'open':
369
+ if (builtin) return textResult(host.openBuiltin(agentName));
370
+ if (tab) return textResult(await host.openServer(agentName, server!, { full: args.full === true }));
371
+ if (args.full !== undefined) fail('full goes with server alone');
372
+ return null;
373
+ case 'close':
374
+ if (builtin) return textResult(host.closeBuiltin(agentName));
375
+ if (tab) return textResult(await host.closeServer(agentName, server!, { disconnect: args.disconnect === true }));
376
+ if (args.disconnect !== undefined) fail('disconnect goes with server alone');
377
+ return null;
378
+ case 'look': {
379
+ // A host toolset's tool, with or without its server named (as act takes it).
380
+ if (tool && !toolset && !instance) {
381
+ const shown = host.lookTool(agentName, tool, server);
382
+ if (shown) return textResult(shown);
383
+ return null;
384
+ }
385
+ if (builtin) return textResult(host.lookToolset(agentName, { builtin: true }));
386
+ // A server's tab, and every host toolset with no selector, go first in
387
+ // what the servers' own look says (handleToolCall adds them).
388
+ if (tab && !tool && !this.hub.get(server!)?.presents) return textResult(host.lookToolset(agentName, { serverId: server! }));
389
+ return null;
390
+ }
391
+ default:
392
+ return null;
393
+ }
394
+ }
395
+
396
+ /** What the host's own toolsets add ahead of the servers' look: everything, or one presenting server's tab. */
397
+ private hostLookLead(args: Record<string, unknown>, agentName: string | undefined): string | null {
398
+ const host = this.host;
399
+ if (!host || !agentName || !host.active(agentName)) return null;
400
+ const server = str(args.server);
401
+ if (str(args.toolset) || str(args.instance) || str(args.tool)) return null;
402
+ if (!server) return host.catalog(agentName).join('\n');
403
+ return host.knows(server) ? host.lookToolset(agentName, { serverId: server }) : null;
404
+ }
405
+
310
406
  private toolsAt(c: ToolsetController, key: string): HintedTool[] {
311
407
  return [...(c.presence.view().get(key)?.tools.values() ?? [])];
312
408
  }
@@ -320,36 +416,36 @@ export class ToolsetsModule implements Module {
320
416
 
321
417
  private async open(args: Record<string, unknown>, take: ((taken: TakenChange) => void) | null): Promise<ToolResult> {
322
418
  const shown = take !== null;
323
- let toolset = str(args.toolset);
419
+ let toolset = toolsetNamed(this.hub, args.server, args.toolset);
324
420
  const c = toolset && !str(args.server)
325
421
  ? owner(this.hub, undefined, (x) => x.presence.isDeclared(toolset!) || x.presence.undeclaredGroups.includes(toolset!), `a toolset "${toolset}"`)
326
422
  : pick(this.hub, args.server);
327
423
  const p = c.presence;
328
424
  const openable = [...Object.keys(p.declared), ...p.undeclaredGroups];
329
425
  if (!toolset) {
330
- if (openable.length !== 1) fail(`${c.serverId} has ${openable.length ? `several toolsets: name one of ${openable.join(', ')}` : 'no toolsets'}`);
426
+ if (openable.length !== 1) fail(`${c.serverId} has ${openable.length ? `several toolsets: name one of ${openable.map(nameToTypeBack).join(', ')}` : 'no toolsets'}`);
331
427
  toolset = openable[0]!;
332
428
  }
333
- const instance = str(args.instance);
429
+ const instance = instanceNamed([c], toolset, args.instance);
334
430
  if (args.options !== undefined && !isObj(args.options)) fail('options must be an object');
335
431
  const options = args.options as Record<string, unknown> | undefined;
336
432
 
337
433
  if (p.isDeclared(toolset)) {
338
- if (instance && !p.declared[toolset]!.instances) fail(`${c.serverId}/${toolset} has no instances: open it without one`);
434
+ if (instance && !p.declared[toolset]!.instances) fail(`${c.serverId}/${nameToTypeBack(toolset)} has no instances: open it without one`);
339
435
  let opened: Awaited<ReturnType<ToolsetController['open']>>;
340
436
  try {
341
437
  opened = await c.open(toolset, instance, options, { shown });
342
438
  } catch (error) {
343
- return requestFailure(c.serverId, `did not open ${toolset}`, `didn't confirm opening ${toolset}`, error);
439
+ return requestFailure(c.serverId, `did not open ${nameToTypeBack(toolset)}`, `didn't confirm opening ${nameToTypeBack(toolset)}`, error);
344
440
  }
345
441
  // RFC §5.2: opening what is open adopts it; its content says which options weren't applied.
346
442
  const blocks: Block[] = [
347
- { type: 'text', text: `${p.label(opened.key)}${opened.title ? ` ${opened.title}` : ''}${opened.adopted ? ' (already open)' : ''}` },
443
+ { type: 'text', text: `${p.label(opened.key)}${opened.title ? ` ${suppliedValue(opened.title)}` : ''}${opened.adopted ? ' (already open)' : ''}` },
348
444
  ...asBlocks(opened.content),
349
445
  ];
350
446
  const tail = [...p.toolBlock(opened.key, this.toolsAt(c, opened.key), { headed: false, describeAll: !shown, seen: shown ? undefined : new Set<string>() })];
351
447
  const gone = opened.change.changes.find((ch) => ch.key === opened.key)?.gone ?? [];
352
- if (gone.length) tail.push(`now unavailable: ${gone.join(', ')}`);
448
+ if (gone.length) tail.push(`now unavailable: ${gone.map(suppliedValue).join(', ')}`);
353
449
  const others = this.renderOthers(c, opened.change, opened.key, shown);
354
450
  if (others) tail.push(others);
355
451
  const close = p.closeHint(opened.key);
@@ -360,7 +456,7 @@ export class ToolsetsModule implements Module {
360
456
  }
361
457
  if (p.undeclaredGroups.includes(toolset)) {
362
458
  // RFC §5.4: a grouping the server offers but doesn't track: the host opens it, the server isn't told.
363
- if (instance || options) fail(`${c.serverId}/${toolset} takes no instance or options (the server doesn't declare it)`);
459
+ if (instance || options) fail(`${c.serverId}/${nameToTypeBack(toolset)} takes no instance or options (the server doesn't declare it)`);
364
460
  const cs = c.setGroupOpen(toolset, true);
365
461
  const key = keyOf(toolset);
366
462
  const lines = [`${p.label(key)} open.`, ...p.toolBlock(key, this.toolsAt(c, key), { headed: false, describeAll: !shown, seen: shown ? undefined : new Set<string>() })];
@@ -400,7 +496,7 @@ export class ToolsetsModule implements Module {
400
496
  };
401
497
  let result: ToolResult;
402
498
  try {
403
- result = this.lookNow(args, server, str(args.tool), shown);
499
+ result = this.lookNow(args, server, toolNamed(this.hub, server, args.tool), shown);
404
500
  } catch (error) {
405
501
  giveBack();
406
502
  if (error instanceof MetaError && unread.length) fail([error.message, ...unread].join('\n'));
@@ -426,13 +522,16 @@ export class ToolsetsModule implements Module {
426
522
 
427
523
  private lookNow(args: Record<string, unknown>, server: string | undefined, tool: string | undefined, mark: boolean): ToolResult {
428
524
  if (!server) {
429
- if (tool) return this.lookTool(owner(this.hub, undefined, (c) => !!c.presence.tool(tool), `a tool "${tool}"`), tool, str(args.instance));
430
- const ts = str(args.toolset);
431
- const inst = str(args.instance);
525
+ if (tool) {
526
+ const c = owner(this.hub, undefined, (x) => !!x.presence.tool(tool), `a tool "${tool}"`);
527
+ return this.lookTool(c, tool, instanceNamed([c], c.presence.toolsetOf(tool), args.instance));
528
+ }
529
+ const ts = toolsetNamed(this.hub, undefined, args.toolset);
530
+ const inst = instanceNamed(this.hub.presenting(), ts, args.instance);
432
531
  if (ts || inst) {
433
532
  const c = owner(this.hub,
434
533
  undefined,
435
- (x) => (ts ? x.presence.isDeclared(ts) || x.presence.undeclaredGroups.includes(ts) : true) && (!inst || x.presence.openRecords().some((o) => o.instance === inst)),
534
+ (x) => (ts ? x.presence.isDeclared(ts) || x.presence.undeclaredGroups.includes(ts) : true) && (!inst || x.presence.openRecords(ts).some((o) => o.instance === inst)),
436
535
  ts ? `a toolset "${ts}"${inst ? ` with instance "${inst}"` : ''}` : `an open instance "${inst}"`,
437
536
  );
438
537
  return this.lookNow(args, c.serverId, tool, mark);
@@ -442,14 +541,14 @@ export class ToolsetsModule implements Module {
442
541
  return textResult(all.map((c) => c.presence.catalog({ mark })).join('\n\n'));
443
542
  }
444
543
  const c = pick(this.hub, server);
445
- if (tool) return this.lookTool(c, tool, str(args.instance));
544
+ if (tool) return this.lookTool(c, tool, instanceNamed([c], c.presence.toolsetOf(tool), args.instance));
446
545
  const p = c.presence;
447
- let toolset = str(args.toolset);
448
- const instance = str(args.instance);
546
+ let toolset = toolsetNamed(this.hub, server, args.toolset);
547
+ const instance = instanceNamed([c], toolset, args.instance);
449
548
  if (!toolset && instance) {
450
549
  const open = p.openRecords().filter((o) => o.instance !== undefined);
451
550
  toolset = open.find((o) => o.instance === instance)?.toolset
452
- ?? fail(`no open instance "${instance}" on ${c.serverId}; ${open.length ? `open: ${open.map((o) => `${o.toolset}:${o.instance}`).join(', ')}` : 'no instance is open there'}`);
551
+ ?? 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'}`);
453
552
  }
454
553
  if (!toolset) return textResult(p.catalog({ mark }));
455
554
  if (p.isOpen(toolset, instance) || (!instance && p.openRecords(toolset).length === 1)) {
@@ -464,8 +563,8 @@ export class ToolsetsModule implements Module {
464
563
  if (d) {
465
564
  const open = p.openRecords(toolset);
466
565
  return textResult([
467
- `${c.serverId}/${toolset}${d.title ? ` — ${d.title}` : ''}${d.summary ? `: ${d.summary}` : ''}`,
468
- open.length ? `open: ${open.map((o) => o.instance ?? toolset).join(', ')}` : 'not open',
566
+ `${c.serverId}/${nameToTypeBack(toolset)}${d.title ? ` — ${suppliedValue(d.title)}` : ''}${d.summary ? `: ${suppliedValue(d.summary)}` : ''}`,
567
+ open.length ? `open: ${open.map((o) => nameToTypeBack(o.instance ?? toolset)).join(', ')}` : 'not open',
469
568
  ...(d.instances ? ['several can be open at once; name each with instance'] : []),
470
569
  ...(d.options ? [`options: ${jsonText(d.options)}`] : []),
471
570
  `${p.openHint(toolset)} opens it.`,
@@ -473,7 +572,7 @@ export class ToolsetsModule implements Module {
473
572
  }
474
573
  if (p.undeclaredGroups.includes(toolset)) {
475
574
  const names = p.allTools.filter((t) => p.toolsetOf(t.name) === toolset).map((t) => t.name);
476
- return textResult(`${c.serverId}/${toolset}: ${names.join(', ')}; not open: ${p.openHint(toolset)} opens it.`);
575
+ return textResult(`${c.serverId}/${nameToTypeBack(toolset)}: ${names.map(nameToTypeBack).join(', ')}; not open: ${p.openHint(toolset)} opens it.`);
477
576
  }
478
577
  return fail(noToolset(c, toolset!));
479
578
  }
@@ -491,13 +590,13 @@ export class ToolsetsModule implements Module {
491
590
 
492
591
  private async close(args: Record<string, unknown>, take: ((taken: TakenChange) => void) | null): Promise<ToolResult> {
493
592
  const shown = take !== null;
494
- const named = str(args.toolset);
593
+ const named = toolsetNamed(this.hub, args.server, args.toolset);
495
594
  const c = named && !str(args.server)
496
- ? owner(this.hub, undefined, (x) => x.presence.isOpen(named, str(args.instance)) || x.presence.openRecords(named).length > 0, `an open toolset "${named}"`)
595
+ ? owner(this.hub, undefined, (x) => x.presence.isOpen(named, instanceNamed([x], named, args.instance)) || x.presence.openRecords(named).length > 0, `an open toolset "${named}"`)
497
596
  : pick(this.hub, args.server);
498
597
  const p = c.presence;
499
598
  let toolset = named;
500
- let instance = str(args.instance);
599
+ let instance = instanceNamed([c], named, args.instance);
501
600
  if (!toolset) {
502
601
  const open = [
503
602
  ...p.openRecords().filter((o) => !instance || o.instance === instance),
@@ -505,7 +604,7 @@ export class ToolsetsModule implements Module {
505
604
  ];
506
605
  if (open.length !== 1) {
507
606
  fail(open.length
508
- ? `several are open on ${c.serverId}: name one of ${open.map((o) => `${o.toolset}${o.instance ? `:${o.instance}` : ''}`).join(', ')}`
607
+ ? `several are open on ${c.serverId}: name one of ${open.map((o) => `${nameToTypeBack(o.toolset)}${o.instance ? `:${nameToTypeBack(o.instance)}` : ''}`).join(', ')}`
509
608
  : `nothing is open on ${c.serverId}`);
510
609
  }
511
610
  toolset = open[0]!.toolset;
@@ -513,17 +612,17 @@ export class ToolsetsModule implements Module {
513
612
  }
514
613
  if (p.isDeclared(toolset!)) {
515
614
  // As open refuses one: a toolset without instances is closed without one.
516
- if (instance && !p.declared[toolset!]!.instances) fail(`${c.serverId}/${toolset} has no instances: close it without one`);
615
+ if (instance && !p.declared[toolset!]!.instances) fail(`${c.serverId}/${nameToTypeBack(toolset!)} has no instances: close it without one`);
517
616
  if (p.declared[toolset!]!.instances && !instance) {
518
617
  const open = p.openRecords(toolset);
519
- if (open.length !== 1) fail(open.length ? `several ${toolset} instances are open (${open.map((o) => o.instance).join(', ')}): say which` : `${toolset} is not open`);
618
+ 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`);
520
619
  instance = open[0]!.instance;
521
620
  }
522
621
  let closed: Awaited<ReturnType<ToolsetController['close']>>;
523
622
  try {
524
623
  closed = await c.close(toolset!, instance, { shown });
525
624
  } catch (error) {
526
- const what = `${toolset}${instance ? `:${instance}` : ''}`;
625
+ const what = `${nameToTypeBack(toolset!)}${instance ? `:${nameToTypeBack(instance)}` : ''}`;
527
626
  return requestFailure(c.serverId, `did not close ${what}`, `didn't confirm closing ${what}`, error);
528
627
  }
529
628
  const key = keyOf(toolset!, instance);
@@ -532,8 +631,8 @@ export class ToolsetsModule implements Module {
532
631
  return blocksResult([...asBlocks(closed.content), { type: 'text', text: change }]);
533
632
  }
534
633
  if (p.undeclaredGroups.includes(toolset!)) {
535
- if (instance) fail(`${c.serverId}/${toolset} takes no instance (the server doesn't declare it)`);
536
- if (!p.isOpen(toolset!)) fail(`${c.serverId}/${toolset} is not open`);
634
+ if (instance) fail(`${c.serverId}/${nameToTypeBack(toolset!)} takes no instance (the server doesn't declare it)`);
635
+ if (!p.isOpen(toolset!)) fail(`${c.serverId}/${nameToTypeBack(toolset!)} is not open`);
537
636
  const cs = c.setGroupOpen(toolset!, false);
538
637
  const text = p.render(cs, { mark: shown, describeAll: !shown }) || `${p.label(keyOf(toolset!))} closed.`;
539
638
  if (take) take({ toolsets: c, cs });