@animalabs/agent-framework 0.26.2 → 0.27.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 (276) hide show
  1. package/README.md +56 -6
  2. package/dist/src/agent.d.ts +263 -22
  3. package/dist/src/agent.d.ts.map +1 -1
  4. package/dist/src/agent.js +531 -54
  5. package/dist/src/agent.js.map +1 -1
  6. package/dist/src/api/server.d.ts.map +1 -1
  7. package/dist/src/api/server.js +1 -2
  8. package/dist/src/api/server.js.map +1 -1
  9. package/dist/src/auto-continue.d.ts +33 -0
  10. package/dist/src/auto-continue.d.ts.map +1 -0
  11. package/dist/src/auto-continue.js +35 -0
  12. package/dist/src/auto-continue.js.map +1 -0
  13. package/dist/src/code-execution/isolate/scripts.d.ts +11 -0
  14. package/dist/src/code-execution/isolate/scripts.d.ts.map +1 -1
  15. package/dist/src/code-execution/isolate/scripts.js +5 -1
  16. package/dist/src/code-execution/isolate/scripts.js.map +1 -1
  17. package/dist/src/framework.d.ts +759 -129
  18. package/dist/src/framework.d.ts.map +1 -1
  19. package/dist/src/framework.js +3482 -928
  20. package/dist/src/framework.js.map +1 -1
  21. package/dist/src/gate/event-gate.d.ts +309 -46
  22. package/dist/src/gate/event-gate.d.ts.map +1 -1
  23. package/dist/src/gate/event-gate.js +2117 -395
  24. package/dist/src/gate/event-gate.js.map +1 -1
  25. package/dist/src/gate/index.d.ts +2 -2
  26. package/dist/src/gate/index.d.ts.map +1 -1
  27. package/dist/src/gate/index.js +1 -1
  28. package/dist/src/gate/index.js.map +1 -1
  29. package/dist/src/gate/recipe-reconcile.d.ts +103 -0
  30. package/dist/src/gate/recipe-reconcile.d.ts.map +1 -0
  31. package/dist/src/gate/recipe-reconcile.js +325 -0
  32. package/dist/src/gate/recipe-reconcile.js.map +1 -0
  33. package/dist/src/gate/types.d.ts +68 -8
  34. package/dist/src/gate/types.d.ts.map +1 -1
  35. package/dist/src/index.d.ts +6 -4
  36. package/dist/src/index.d.ts.map +1 -1
  37. package/dist/src/index.js +5 -2
  38. package/dist/src/index.js.map +1 -1
  39. package/dist/src/mcpl/channel-registry.d.ts +433 -113
  40. package/dist/src/mcpl/channel-registry.d.ts.map +1 -1
  41. package/dist/src/mcpl/channel-registry.js +1152 -434
  42. package/dist/src/mcpl/channel-registry.js.map +1 -1
  43. package/dist/src/mcpl/conversation-router.d.ts +16 -8
  44. package/dist/src/mcpl/conversation-router.d.ts.map +1 -1
  45. package/dist/src/mcpl/conversation-router.js +29 -15
  46. package/dist/src/mcpl/conversation-router.js.map +1 -1
  47. package/dist/src/mcpl/convert-block.d.ts.map +1 -1
  48. package/dist/src/mcpl/convert-block.js +14 -2
  49. package/dist/src/mcpl/convert-block.js.map +1 -1
  50. package/dist/src/mcpl/hook-orchestrator.d.ts +7 -1
  51. package/dist/src/mcpl/hook-orchestrator.d.ts.map +1 -1
  52. package/dist/src/mcpl/hook-orchestrator.js +32 -7
  53. package/dist/src/mcpl/hook-orchestrator.js.map +1 -1
  54. package/dist/src/mcpl/index.d.ts +5 -1
  55. package/dist/src/mcpl/index.d.ts.map +1 -1
  56. package/dist/src/mcpl/index.js +4 -0
  57. package/dist/src/mcpl/index.js.map +1 -1
  58. package/dist/src/mcpl/inference-router.d.ts +14 -2
  59. package/dist/src/mcpl/inference-router.d.ts.map +1 -1
  60. package/dist/src/mcpl/inference-router.js +73 -28
  61. package/dist/src/mcpl/inference-router.js.map +1 -1
  62. package/dist/src/mcpl/prose-grammar.d.ts +4 -0
  63. package/dist/src/mcpl/prose-grammar.d.ts.map +1 -1
  64. package/dist/src/mcpl/prose-grammar.js +6 -0
  65. package/dist/src/mcpl/prose-grammar.js.map +1 -1
  66. package/dist/src/mcpl/protocol-family.d.ts.map +1 -1
  67. package/dist/src/mcpl/protocol-family.js +3 -0
  68. package/dist/src/mcpl/protocol-family.js.map +1 -1
  69. package/dist/src/mcpl/push-coalescer.d.ts +20 -2
  70. package/dist/src/mcpl/push-coalescer.d.ts.map +1 -1
  71. package/dist/src/mcpl/push-coalescer.js +33 -11
  72. package/dist/src/mcpl/push-coalescer.js.map +1 -1
  73. package/dist/src/mcpl/reference-fetcher.d.ts +63 -42
  74. package/dist/src/mcpl/reference-fetcher.d.ts.map +1 -1
  75. package/dist/src/mcpl/reference-fetcher.js +177 -145
  76. package/dist/src/mcpl/reference-fetcher.js.map +1 -1
  77. package/dist/src/mcpl/references.d.ts +380 -46
  78. package/dist/src/mcpl/references.d.ts.map +1 -1
  79. package/dist/src/mcpl/references.js +736 -77
  80. package/dist/src/mcpl/references.js.map +1 -1
  81. package/dist/src/mcpl/server-connection.d.ts +82 -3
  82. package/dist/src/mcpl/server-connection.d.ts.map +1 -1
  83. package/dist/src/mcpl/server-connection.js +132 -7
  84. package/dist/src/mcpl/server-connection.js.map +1 -1
  85. package/dist/src/mcpl/tool-lifecycle.d.ts +11 -0
  86. package/dist/src/mcpl/tool-lifecycle.d.ts.map +1 -1
  87. package/dist/src/mcpl/tool-lifecycle.js +21 -0
  88. package/dist/src/mcpl/tool-lifecycle.js.map +1 -1
  89. package/dist/src/mcpl/tool-presence.d.ts +259 -0
  90. package/dist/src/mcpl/tool-presence.d.ts.map +1 -0
  91. package/dist/src/mcpl/tool-presence.js +628 -0
  92. package/dist/src/mcpl/tool-presence.js.map +1 -0
  93. package/dist/src/mcpl/tool-result-normalize.d.ts +9 -3
  94. package/dist/src/mcpl/tool-result-normalize.d.ts.map +1 -1
  95. package/dist/src/mcpl/tool-result-normalize.js +14 -6
  96. package/dist/src/mcpl/tool-result-normalize.js.map +1 -1
  97. package/dist/src/mcpl/tool-signature.d.ts +39 -0
  98. package/dist/src/mcpl/tool-signature.d.ts.map +1 -0
  99. package/dist/src/mcpl/tool-signature.js +292 -0
  100. package/dist/src/mcpl/tool-signature.js.map +1 -0
  101. package/dist/src/mcpl/toolset-host.d.ts +200 -0
  102. package/dist/src/mcpl/toolset-host.d.ts.map +1 -0
  103. package/dist/src/mcpl/toolset-host.js +621 -0
  104. package/dist/src/mcpl/toolset-host.js.map +1 -0
  105. package/dist/src/mcpl/toolsets-module.d.ts +80 -0
  106. package/dist/src/mcpl/toolsets-module.d.ts.map +1 -0
  107. package/dist/src/mcpl/toolsets-module.js +431 -0
  108. package/dist/src/mcpl/toolsets-module.js.map +1 -0
  109. package/dist/src/mcpl/transport.d.ts +2 -2
  110. package/dist/src/mcpl/transport.d.ts.map +1 -1
  111. package/dist/src/mcpl/transport.js +3 -2
  112. package/dist/src/mcpl/transport.js.map +1 -1
  113. package/dist/src/mcpl/types.d.ts +21 -1
  114. package/dist/src/mcpl/types.d.ts.map +1 -1
  115. package/dist/src/mcpl/types.js.map +1 -1
  116. package/dist/src/mcpl/visible-content.d.ts +7 -9
  117. package/dist/src/mcpl/visible-content.d.ts.map +1 -1
  118. package/dist/src/mcpl/visible-content.js +21 -9
  119. package/dist/src/mcpl/visible-content.js.map +1 -1
  120. package/dist/src/module-registry.d.ts +23 -8
  121. package/dist/src/module-registry.d.ts.map +1 -1
  122. package/dist/src/module-registry.js +68 -15
  123. package/dist/src/module-registry.js.map +1 -1
  124. package/dist/src/modules/discord/index.js +1 -1
  125. package/dist/src/modules/discord/index.js.map +1 -1
  126. package/dist/src/modules/history/index.d.ts +2 -1
  127. package/dist/src/modules/history/index.d.ts.map +1 -1
  128. package/dist/src/modules/history/index.js +51 -17
  129. package/dist/src/modules/history/index.js.map +1 -1
  130. package/dist/src/modules/history/semantic.d.ts +108 -6
  131. package/dist/src/modules/history/semantic.d.ts.map +1 -1
  132. package/dist/src/modules/history/semantic.js +273 -57
  133. package/dist/src/modules/history/semantic.js.map +1 -1
  134. package/dist/src/modules/workspace/direct-write.d.ts +181 -0
  135. package/dist/src/modules/workspace/direct-write.d.ts.map +1 -0
  136. package/dist/src/modules/workspace/direct-write.js +384 -0
  137. package/dist/src/modules/workspace/direct-write.js.map +1 -0
  138. package/dist/src/modules/workspace/edit-text.d.ts +26 -12
  139. package/dist/src/modules/workspace/edit-text.d.ts.map +1 -1
  140. package/dist/src/modules/workspace/edit-text.js +58 -28
  141. package/dist/src/modules/workspace/edit-text.js.map +1 -1
  142. package/dist/src/modules/workspace/effects.d.ts +10 -0
  143. package/dist/src/modules/workspace/effects.d.ts.map +1 -1
  144. package/dist/src/modules/workspace/effects.js +1 -1
  145. package/dist/src/modules/workspace/effects.js.map +1 -1
  146. package/dist/src/modules/workspace/index.d.ts +246 -27
  147. package/dist/src/modules/workspace/index.d.ts.map +1 -1
  148. package/dist/src/modules/workspace/index.js +1216 -236
  149. package/dist/src/modules/workspace/index.js.map +1 -1
  150. package/dist/src/modules/workspace/read-page.d.ts +41 -0
  151. package/dist/src/modules/workspace/read-page.d.ts.map +1 -0
  152. package/dist/src/modules/workspace/read-page.js +213 -0
  153. package/dist/src/modules/workspace/read-page.js.map +1 -0
  154. package/dist/src/modules/workspace/result-text.d.ts +67 -0
  155. package/dist/src/modules/workspace/result-text.d.ts.map +1 -0
  156. package/dist/src/modules/workspace/result-text.js +94 -0
  157. package/dist/src/modules/workspace/result-text.js.map +1 -0
  158. package/dist/src/modules/workspace/text.d.ts +29 -12
  159. package/dist/src/modules/workspace/text.d.ts.map +1 -1
  160. package/dist/src/modules/workspace/text.js +35 -33
  161. package/dist/src/modules/workspace/text.js.map +1 -1
  162. package/dist/src/modules/workspace/types.d.ts +41 -17
  163. package/dist/src/modules/workspace/types.d.ts.map +1 -1
  164. package/dist/src/modules/workspace/types.js.map +1 -1
  165. package/dist/src/sent-rows.d.ts +59 -0
  166. package/dist/src/sent-rows.d.ts.map +1 -0
  167. package/dist/src/sent-rows.js +145 -0
  168. package/dist/src/sent-rows.js.map +1 -0
  169. package/dist/src/silent-heartbeat.d.ts +17 -10
  170. package/dist/src/silent-heartbeat.d.ts.map +1 -1
  171. package/dist/src/silent-heartbeat.js.map +1 -1
  172. package/dist/src/stable-hash.d.ts +3 -0
  173. package/dist/src/stable-hash.d.ts.map +1 -0
  174. package/dist/src/stable-hash.js +15 -0
  175. package/dist/src/stable-hash.js.map +1 -0
  176. package/dist/src/stored-injections.d.ts +99 -0
  177. package/dist/src/stored-injections.d.ts.map +1 -0
  178. package/dist/src/stored-injections.js +106 -0
  179. package/dist/src/stored-injections.js.map +1 -0
  180. package/dist/src/text-as-stored.d.ts +4 -3
  181. package/dist/src/text-as-stored.d.ts.map +1 -1
  182. package/dist/src/text-as-stored.js +4 -3
  183. package/dist/src/text-as-stored.js.map +1 -1
  184. package/dist/src/thinking-drops.d.ts +81 -0
  185. package/dist/src/thinking-drops.d.ts.map +1 -0
  186. package/dist/src/thinking-drops.js +103 -0
  187. package/dist/src/thinking-drops.js.map +1 -0
  188. package/dist/src/tool-image-ledger.d.ts +9 -8
  189. package/dist/src/tool-image-ledger.d.ts.map +1 -1
  190. package/dist/src/tool-image-ledger.js +9 -8
  191. package/dist/src/tool-image-ledger.js.map +1 -1
  192. package/dist/src/tool-presentation.d.ts.map +1 -1
  193. package/dist/src/tool-presentation.js +3 -1
  194. package/dist/src/tool-presentation.js.map +1 -1
  195. package/dist/src/tool-result-guard.d.ts +4 -2
  196. package/dist/src/tool-result-guard.d.ts.map +1 -1
  197. package/dist/src/tool-result-guard.js +21 -6
  198. package/dist/src/tool-result-guard.js.map +1 -1
  199. package/dist/src/tool-result-history.d.ts +176 -24
  200. package/dist/src/tool-result-history.d.ts.map +1 -1
  201. package/dist/src/tool-result-history.js +411 -91
  202. package/dist/src/tool-result-history.js.map +1 -1
  203. package/dist/src/tune-out/coordinator.d.ts +17 -9
  204. package/dist/src/tune-out/coordinator.d.ts.map +1 -1
  205. package/dist/src/tune-out/coordinator.js +84 -37
  206. package/dist/src/tune-out/coordinator.js.map +1 -1
  207. package/dist/src/types/agent.d.ts +186 -7
  208. package/dist/src/types/agent.d.ts.map +1 -1
  209. package/dist/src/types/events.d.ts +15 -1
  210. package/dist/src/types/events.d.ts.map +1 -1
  211. package/dist/src/types/framework.d.ts +24 -3
  212. package/dist/src/types/framework.d.ts.map +1 -1
  213. package/dist/src/types/module.d.ts +90 -7
  214. package/dist/src/types/module.d.ts.map +1 -1
  215. package/dist/src/types/trace.d.ts +57 -2
  216. package/dist/src/types/trace.d.ts.map +1 -1
  217. package/package.json +4 -3
  218. package/src/agent.ts +593 -60
  219. package/src/api/server.ts +1 -2
  220. package/src/auto-continue.ts +42 -0
  221. package/src/code-execution/isolate/scripts.ts +11 -3
  222. package/src/framework.ts +3601 -967
  223. package/src/gate/event-gate.ts +2315 -389
  224. package/src/gate/index.ts +2 -1
  225. package/src/gate/recipe-reconcile.ts +355 -0
  226. package/src/gate/types.ts +70 -8
  227. package/src/index.ts +7 -4
  228. package/src/mcpl/channel-registry.ts +1220 -467
  229. package/src/mcpl/conversation-router.ts +34 -16
  230. package/src/mcpl/convert-block.ts +14 -1
  231. package/src/mcpl/hook-orchestrator.ts +33 -6
  232. package/src/mcpl/index.ts +6 -1
  233. package/src/mcpl/inference-router.ts +79 -34
  234. package/src/mcpl/prose-grammar.ts +7 -0
  235. package/src/mcpl/protocol-family.ts +2 -0
  236. package/src/mcpl/push-coalescer.ts +51 -13
  237. package/src/mcpl/reference-fetcher.ts +201 -162
  238. package/src/mcpl/references.ts +851 -77
  239. package/src/mcpl/server-connection.ts +145 -7
  240. package/src/mcpl/tool-lifecycle.ts +21 -0
  241. package/src/mcpl/tool-presence.ts +650 -0
  242. package/src/mcpl/tool-result-normalize.ts +18 -6
  243. package/src/mcpl/tool-signature.ts +285 -0
  244. package/src/mcpl/toolset-host.ts +637 -0
  245. package/src/mcpl/toolsets-module.ts +451 -0
  246. package/src/mcpl/transport.ts +3 -2
  247. package/src/mcpl/types.ts +22 -1
  248. package/src/mcpl/visible-content.ts +22 -9
  249. package/src/module-registry.ts +77 -24
  250. package/src/modules/discord/index.ts +1 -1
  251. package/src/modules/history/index.ts +48 -18
  252. package/src/modules/history/semantic.ts +308 -54
  253. package/src/modules/workspace/direct-write.ts +425 -0
  254. package/src/modules/workspace/edit-text.ts +82 -35
  255. package/src/modules/workspace/effects.ts +1 -1
  256. package/src/modules/workspace/index.ts +1274 -241
  257. package/src/modules/workspace/read-page.ts +222 -0
  258. package/src/modules/workspace/result-text.ts +119 -0
  259. package/src/modules/workspace/text.ts +58 -32
  260. package/src/modules/workspace/types.ts +41 -17
  261. package/src/sent-rows.ts +162 -0
  262. package/src/silent-heartbeat.ts +17 -10
  263. package/src/stable-hash.ts +14 -0
  264. package/src/stored-injections.ts +131 -0
  265. package/src/text-as-stored.ts +4 -3
  266. package/src/thinking-drops.ts +141 -0
  267. package/src/tool-image-ledger.ts +9 -8
  268. package/src/tool-presentation.ts +3 -1
  269. package/src/tool-result-guard.ts +19 -6
  270. package/src/tool-result-history.ts +471 -90
  271. package/src/tune-out/coordinator.ts +80 -37
  272. package/src/types/agent.ts +189 -7
  273. package/src/types/events.ts +15 -1
  274. package/src/types/framework.ts +21 -3
  275. package/src/types/module.ts +86 -7
  276. package/src/types/trace.ts +58 -2
@@ -0,0 +1,451 @@
1
+ /**
2
+ * The host's own interface to MCPL dynamic tools (RFC-014 §8.2): four tools,
3
+ * constant and static, aggregated across every server that presents
4
+ * toolsets.
5
+ *
6
+ * toolsets--open {server?, toolset?, instance?, options?}
7
+ * toolsets--act {server?, tool, instance?, args?}
8
+ * toolsets--look {server?, toolset?, instance?, tool?}
9
+ * toolsets--close {server?, toolset?, instance?}
10
+ *
11
+ * Dynamic tools never enter the API tool list: changing it rebuilds the
12
+ * prompt cache. They're announced in the conversation (in these tools'
13
+ * results, in the results of calls that changed them, in push events and in
14
+ * notices) and called through `toolsets--act`, which this module only
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.
18
+ *
19
+ * Its gatherContext publishes the RFC §8.3 reminder, as a stored `afterUser`
20
+ * injection, when an agent's earlier announcements of an open toolset have
21
+ * left its context (ToolsetHub.observeCompile).
22
+ *
23
+ * Ported from mcpl-cc-bridge's meta-tools.ts, the reference host.
24
+ */
25
+ import type { EventResponse, Module, ModuleContext, ModuleContextInjection, ProcessState } from '../types/module.js';
26
+ import type { ProcessEvent, ToolCall, ToolDefinition, ToolResult } from '../types/events.js';
27
+ import { unknownToolMessage } from '../modules/unknown-tool.js';
28
+ 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
+
32
+ /** The namespace a reminder's stored injection carries. */
33
+ export const TOOLSETS_REMINDER_NAMESPACE = 'mcpl-toolsets';
34
+
35
+ const META_TOOLS: ToolDefinition[] = [
36
+ {
37
+ name: 'open',
38
+ 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); ' +
42
+ 'options as toolsets--look lists them.',
43
+ inputSchema: {
44
+ type: 'object',
45
+ properties: {
46
+ server: { type: 'string', description: 'the MCPL server id' },
47
+ toolset: { type: 'string' },
48
+ instance: { type: 'string', description: 'a name for this copy (toolsets with instances only)' },
49
+ options: { type: 'object', description: 'toolset options, as toolsets--look shows them' },
50
+ },
51
+ },
52
+ },
53
+ {
54
+ name: 'act',
55
+ 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. ' +
57
+ '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.',
59
+ inputSchema: {
60
+ type: 'object',
61
+ properties: {
62
+ server: { type: 'string', description: 'the MCPL server id (only needed when two servers have the tool)' },
63
+ tool: { type: 'string' },
64
+ instance: { type: 'string', description: 'which open copy (only needed when several are open)' },
65
+ args: { type: 'object' },
66
+ },
67
+ required: ['tool'],
68
+ },
69
+ },
70
+ {
71
+ name: 'look',
72
+ 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. ' +
74
+ 'With tool: that tool\'s full description and input schema. With toolset or instance: just that one.',
75
+ inputSchema: {
76
+ type: 'object',
77
+ properties: {
78
+ server: { type: 'string' },
79
+ toolset: { type: 'string' },
80
+ instance: { type: 'string' },
81
+ tool: { type: 'string' },
82
+ },
83
+ },
84
+ },
85
+ {
86
+ 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.',
88
+ inputSchema: {
89
+ type: 'object',
90
+ properties: {
91
+ 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' },
93
+ instance: { type: 'string' },
94
+ },
95
+ },
96
+ },
97
+ ];
98
+
99
+ const str = (v: unknown): string | undefined => (typeof v === 'string' && v.trim() ? v.trim() : undefined);
100
+ const isObj = (v: unknown): v is Record<string, unknown> => typeof v === 'object' && v !== null && !Array.isArray(v);
101
+
102
+ type Block = { type: string; text?: string; [k: string]: unknown };
103
+
104
+ class MetaError extends Error {}
105
+ const fail = (message: string): never => {
106
+ throw new MetaError(message);
107
+ };
108
+
109
+ /** A text-only result. */
110
+ const textResult = (text: string): ToolResult => ({ success: true, data: text });
111
+
112
+ /** Adjacent text blocks merged, so a result reads as one message. */
113
+ function mergeText(blocks: Block[]): Block[] {
114
+ const out: Block[] = [];
115
+ for (const b of blocks) {
116
+ if (b.type === 'text' && !b.text) continue;
117
+ const prev = out[out.length - 1];
118
+ if (b.type === 'text' && prev?.type === 'text') out[out.length - 1] = { type: 'text', text: `${prev.text}\n${b.text}` };
119
+ else out.push(b);
120
+ }
121
+ return out;
122
+ }
123
+
124
+ /** Content blocks as a result: content when anything isn't text, else its text. */
125
+ function blocksResult(blocks: Block[], isError = false): ToolResult {
126
+ const merged = mergeText(blocks);
127
+ if (isError) {
128
+ const text = merged.map((b) => (b.type === 'text' ? b.text ?? '' : `[${b.type}]`)).join('\n');
129
+ return { success: false, isError: true, error: text || 'the server answered with an error' };
130
+ }
131
+ if (merged.every((b) => b.type === 'text')) return textResult(merged.map((b) => b.text ?? '').join('\n'));
132
+ return { success: true, data: merged, dataIsContent: true };
133
+ }
134
+
135
+ const asBlocks = (content: unknown[]): Block[] => content.filter((b): b is Block => isObj(b) && typeof b.type === 'string');
136
+
137
+
138
+ // ── choosing the server ──
139
+
140
+ function pick(hub: ToolsetHub, server: unknown): ToolsetController {
141
+ const id = str(server);
142
+ const ids = hub.presenting().map((c) => c.serverId);
143
+ if (!id) {
144
+ if (ids.length === 1) return hub.get(ids[0]!)!;
145
+ return fail(`name a server: ${ids.join(', ') || '(no MCPL server presents toolsets now)'}`);
146
+ }
147
+ const c = hub.get(id);
148
+ 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`);
150
+ return c;
151
+ }
152
+
153
+ /**
154
+ * RFC §8.2: `server` may be left out; the name decides, when exactly one
155
+ * presenting server has it. Two or more: name the server.
156
+ */
157
+ function owner(hub: ToolsetHub, server: unknown, has: (c: ToolsetController) => boolean, what: string): ToolsetController {
158
+ if (str(server)) return pick(hub, server);
159
+ const owners = hub.presenting().filter(has);
160
+ 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}`);
164
+ }
165
+
166
+ /** What a `toolsets--act` call resolves to: the server's tool, its instance and arguments. */
167
+ export type ActTarget = { ok: true; serverId: string; tool: string; instance?: string; args: Record<string, unknown> } | { ok: false; error: string };
168
+
169
+ /**
170
+ * Resolve a `toolsets--act` call (RFC §8.2) to the dynamic tool it calls,
171
+ * or the reason it can't be sent. The framework then dispatches it exactly as
172
+ * a direct call of that tool, with `_meta["mcpl/instance"]` added (RFC §5.5).
173
+ * 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
175
+ * several are open, a group the host has closed (RFC §5.4), and arguments
176
+ * that don't fit the definition in force for that instance (RFC §6.1).
177
+ */
178
+ export function resolveAct(hub: ToolsetHub, input: unknown): ActTarget {
179
+ try {
180
+ const args = isObj(input) ? input : {};
181
+ const tool = str(args.tool) ?? fail('name the tool');
182
+ const c = owner(hub, args.server, (x) => !!x.presence.tool(tool), `a tool "${tool}"`);
183
+ const p = c.presence;
184
+ 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.`);
186
+ if (args.args !== undefined && !isObj(args.args)) fail('args must be an object of the tool\'s arguments');
187
+ const callArgs = (args.args ?? {}) as Record<string, unknown>;
188
+ let instance = str(args.instance);
189
+ const ts = p.toolsetOf(tool);
190
+ const declared = ts !== undefined && p.isDeclared(ts);
191
+ const withInstances = declared && !!p.declared[ts!]!.instances;
192
+ if (withInstances && !instance) {
193
+ // RFC §5.5: the instance rides in _meta; with exactly one open, it's that one.
194
+ const open = p.openRecords(ts);
195
+ 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`);
198
+ }
199
+ if (instance && !withInstances) fail(`${tool} ${ts ? `belongs to ${ts}, which has no instances` : 'belongs to no toolset'}: call it without instance`);
200
+ // 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)}`);
202
+ // RFC §6.1: arguments are checked against the definition in force for
203
+ // that instance: its own from hints (the legal moves of one game), else
204
+ // the toolset's, else the listed one.
205
+ const key = ts ? keyOf(ts, withInstances ? instance : undefined) : keyOf('');
206
+ const problem = checkArgs(p.defOf(key, tool).inputSchema, callArgs);
207
+ 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.`);
210
+ }
211
+ return { ok: true, serverId: c.serverId, tool, ...(instance ? { instance } : {}), args: callArgs };
212
+ } catch (error) {
213
+ if (error instanceof MetaError) return { ok: false, error: error.message };
214
+ throw error;
215
+ }
216
+ }
217
+
218
+ export class ToolsetsModule implements Module {
219
+ readonly name = TOOLSETS_MODULE;
220
+
221
+ constructor(private readonly hub: ToolsetHub) {}
222
+
223
+ async start(_ctx: ModuleContext): Promise<void> {}
224
+ async stop(): Promise<void> {}
225
+
226
+ getTools(): ToolDefinition[] {
227
+ return META_TOOLS;
228
+ }
229
+
230
+ async onProcess(_event: ProcessEvent, _state: ProcessState): Promise<EventResponse> {
231
+ return {};
232
+ }
233
+
234
+ /** RFC §8.3: the reminder due for this agent, stored where compile() would place it. */
235
+ async gatherContext(agentName: string): Promise<ModuleContextInjection[]> {
236
+ const reminder = this.hub.reminderFor(agentName);
237
+ if (!reminder) return [];
238
+ return [{ namespace: TOOLSETS_REMINDER_NAMESPACE, position: 'afterUser', content: [{ type: 'text', text: reminder }] }];
239
+ }
240
+
241
+ async handleToolCall(call: ToolCall): Promise<ToolResult> {
242
+ const args = isObj(call.input) ? call.input : {};
243
+ try {
244
+ switch (call.name) {
245
+ case 'open': return await this.open(args);
246
+ case 'act': return this.act(args);
247
+ case 'look': return await this.look(args);
248
+ case 'close': return await this.close(args);
249
+ default: return { success: false, isError: true, error: unknownToolMessage(this.name, call.name, META_TOOLS) };
250
+ }
251
+ } catch (error) {
252
+ const message = error instanceof MetaError
253
+ ? error.message
254
+ : `${META_TOOL_NAMES[call.name as keyof typeof META_TOOL_NAMES] ?? call.name}: ${error instanceof Error ? error.message : String(error)}`;
255
+ return { success: false, isError: true, error: message };
256
+ }
257
+ }
258
+
259
+ private toolsAt(c: ToolsetController, key: string): HintedTool[] {
260
+ return [...(c.presence.view().get(key)?.tools.values() ?? [])];
261
+ }
262
+
263
+ /** 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) });
266
+ }
267
+
268
+ // ── the tools ──
269
+
270
+ private async open(args: Record<string, unknown>): Promise<ToolResult> {
271
+ let toolset = str(args.toolset);
272
+ const c = toolset && !str(args.server)
273
+ ? owner(this.hub, undefined, (x) => x.presence.isDeclared(toolset!) || x.presence.undeclaredGroups.includes(toolset!), `a toolset "${toolset}"`)
274
+ : pick(this.hub, args.server);
275
+ const p = c.presence;
276
+ const openable = [...Object.keys(p.declared), ...p.undeclaredGroups];
277
+ if (!toolset) {
278
+ if (openable.length !== 1) fail(`${c.serverId} has ${openable.length ? `several toolsets: name one of ${openable.join(', ')}` : 'no toolsets'}`);
279
+ toolset = openable[0]!;
280
+ }
281
+ const instance = str(args.instance);
282
+ if (args.options !== undefined && !isObj(args.options)) fail('options must be an object');
283
+ const options = args.options as Record<string, unknown> | undefined;
284
+
285
+ if (p.isDeclared(toolset)) {
286
+ if (instance && !p.declared[toolset]!.instances) fail(`${c.serverId}/${toolset} has no instances: open it without one`);
287
+ let opened: Awaited<ReturnType<ToolsetController['open']>>;
288
+ try {
289
+ opened = await c.open(toolset, instance, options);
290
+ } catch (error) {
291
+ return fail(`${c.serverId} did not open ${toolset}: ${error instanceof Error ? error.message : String(error)}`);
292
+ }
293
+ // RFC §5.2: opening what is open adopts it; its content says which options weren't applied.
294
+ const blocks: Block[] = [
295
+ { type: 'text', text: `${p.label(opened.key)}${opened.title ? ` ${opened.title}` : ''}${opened.adopted ? ' (already open)' : ''}` },
296
+ ...asBlocks(opened.content),
297
+ ];
298
+ const tail = [...p.toolBlock(opened.key, this.toolsAt(c, opened.key), { headed: false })];
299
+ 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);
302
+ if (others) tail.push(others);
303
+ const close = p.closeHint(opened.key);
304
+ if (close) tail.push(`${close} closes it.`);
305
+ blocks.push({ type: 'text', text: tail.join('\n') });
306
+ return blocksResult(blocks);
307
+ }
308
+ if (p.undeclaredGroups.includes(toolset)) {
309
+ // 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)`);
311
+ const cs = c.setGroupOpen(toolset, true);
312
+ 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);
315
+ if (others) lines.push(others);
316
+ lines.push(`${p.closeHint(key)} closes it.`);
317
+ return textResult(lines.join('\n'));
318
+ }
319
+ return fail(`${c.serverId} has no toolset "${toolset}"${openable.length ? `; it has ${openable.join(', ')}` : ''}`);
320
+ }
321
+
322
+ /**
323
+ * act is dispatched by the framework, not here: it resolves the call
324
+ * (resolveAct) and sends it down the same path as a direct call of the
325
+ * server's tool, so state, results, refusals and lifecycle are the same.
326
+ * A caller that reaches the module itself gets the resolution's refusal,
327
+ * or this.
328
+ */
329
+ private act(args: Record<string, unknown>): ToolResult {
330
+ const target = resolveAct(this.hub, args);
331
+ if (!target.ok) return { success: false, isError: true, error: target.error };
332
+ return { success: false, isError: true, error: `${META_TOOL_NAMES.act} is dispatched by the framework; nothing was sent` };
333
+ }
334
+
335
+ /** 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> {
337
+ const server = str(args.server);
338
+ 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;
342
+ }
343
+
344
+ private lookNow(args: Record<string, unknown>, server: string | undefined, tool: string | undefined): ToolResult {
345
+ 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);
349
+ if (ts || inst) {
350
+ const c = owner(this.hub,
351
+ undefined,
352
+ (x) => (ts ? x.presence.isDeclared(ts) || x.presence.undeclaredGroups.includes(ts) : true) && (!inst || x.presence.openRecords().some((o) => o.instance === inst)),
353
+ ts ? `a toolset "${ts}"${inst ? ` with instance "${inst}"` : ''}` : `an open instance "${inst}"`,
354
+ );
355
+ return this.lookNow(args, c.serverId, tool);
356
+ }
357
+ 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'));
360
+ }
361
+ const c = pick(this.hub, server);
362
+ if (tool) return this.lookTool(c, tool, str(args.instance));
363
+ 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());
368
+ if (p.isOpen(toolset, instance) || (!instance && p.openRecords(toolset).length === 1)) {
369
+ const rec = p.isDeclared(toolset) ? (instance ? p.openRecords(toolset).find((o) => o.instance === instance) : p.openRecords(toolset)[0]) : undefined;
370
+ const key = keyOf(toolset, rec?.instance);
371
+ const lines = p.toolBlock(key, this.toolsAt(c, key), { describeAll: true, title: rec?.title });
372
+ const close = p.closeHint(key);
373
+ if (close) lines.push(`${close} closes it.`);
374
+ return textResult(lines.join('\n'));
375
+ }
376
+ const d = p.declared[toolset];
377
+ if (d) {
378
+ const open = p.openRecords(toolset);
379
+ 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',
382
+ ...(d.instances ? ['several can be open at once; name each with instance'] : []),
383
+ ...(d.options ? [`options: ${JSON.stringify(d.options)}`] : []),
384
+ `${p.openHint(toolset)} opens it.`,
385
+ ].join('\n'));
386
+ }
387
+ if (p.undeclaredGroups.includes(toolset)) {
388
+ 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.`);
390
+ }
391
+ return fail(`${c.serverId} has no toolset "${toolset}"`);
392
+ }
393
+
394
+ private lookTool(c: ToolsetController, tool: string, instance: string | undefined): ToolResult {
395
+ const p = c.presence;
396
+ if (!p.tool(tool)) fail(`${c.serverId} has no tool "${tool}"`);
397
+ 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];
400
+ const def = key ? p.defOf(key, tool) : p.tool(tool)!;
401
+ return textResult(`${renderToolDefinition(def)}\n${p.toolStatus(tool)}`);
402
+ }
403
+
404
+ private async close(args: Record<string, unknown>): Promise<ToolResult> {
405
+ const named = str(args.toolset);
406
+ 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}"`)
408
+ : pick(this.hub, args.server);
409
+ const p = c.presence;
410
+ let toolset = named;
411
+ let instance = str(args.instance);
412
+ if (!toolset) {
413
+ const open = [
414
+ ...p.openRecords().filter((o) => !instance || o.instance === instance),
415
+ ...(instance ? [] : p.openGroups.map((g) => ({ toolset: g, instance: undefined as string | undefined }))),
416
+ ];
417
+ if (open.length !== 1) {
418
+ fail(open.length
419
+ ? `several are open on ${c.serverId}: name one of ${open.map((o) => `${o.toolset}${o.instance ? `:${o.instance}` : ''}`).join(', ')}`
420
+ : `nothing is open on ${c.serverId}`);
421
+ }
422
+ toolset = open[0]!.toolset;
423
+ instance = open[0]!.instance;
424
+ }
425
+ if (p.isDeclared(toolset!)) {
426
+ // 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`);
428
+ if (p.declared[toolset!]!.instances && !instance) {
429
+ 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`);
431
+ instance = open[0]!.instance;
432
+ }
433
+ let closed: Awaited<ReturnType<ToolsetController['close']>>;
434
+ try {
435
+ closed = await c.close(toolset!, instance);
436
+ } catch (error) {
437
+ return fail(`${c.serverId} did not close ${toolset}${instance ? `:${instance}` : ''}: ${error instanceof Error ? error.message : String(error)}`);
438
+ }
439
+ const key = keyOf(toolset!, instance);
440
+ const change = p.render(closed.change) || `${p.label(key)} closed.`;
441
+ return blocksResult([...asBlocks(closed.content), { type: 'text', text: change }]);
442
+ }
443
+ 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`);
446
+ const cs = c.setGroupOpen(toolset!, false);
447
+ return textResult(p.render(cs) || `${p.label(keyOf(toolset!))} closed.`);
448
+ }
449
+ return fail(`${c.serverId} has no toolset "${toolset}"`);
450
+ }
451
+ }
@@ -830,11 +830,12 @@ const SERVER_CONFIG_BOOLEANS: Record<BooleanSetting, string> = {
830
830
  inheritEnv: 'true passes the server the host\'s whole environment, and false or leaving it out passes only the allowlisted and LC_* host variables',
831
831
  reconnect: 'true retries in the background when the server can\'t be reached or its connection drops, and false or leaving it out doesn\'t',
832
832
  allowHostCommands: 'true lets the server run the host\'s admin commands, such as undo, hide, unstick and quiesce, and false or leaving it out refuses them',
833
+ toolHints: 'true advertises MCPL tool hints (RFC-014), so the server\'s dynamic tools are announced in the conversation and called through toolsets--act, and false or leaving it out lists every tool as before',
833
834
  };
834
835
 
835
836
  /**
836
- * Refuse a server config whose `inheritEnv`, `reconnect` or `allowHostCommands`
837
- * holds anything but true or false. A config read from JSON can carry a quoted
837
+ * Refuse a server config whose `inheritEnv`, `reconnect`, `allowHostCommands`
838
+ * or `toolHints` holds anything but true or false. A config read from JSON can carry a quoted
838
839
  * "false" where the type says boolean, and every reader of these settings takes
839
840
  * only `true` as on, so the quotes would hide what the value does. null counts
840
841
  * as not set, so a generated config that writes an unset setting as null
package/src/mcpl/types.ts CHANGED
@@ -96,6 +96,9 @@ export interface McplAudioContent {
96
96
  disposition?: 'never' | 'ref';
97
97
  }
98
98
 
99
+ /** MCPL's reference block (RFC-005 §3). A `resource` block with no `uri` of
100
+ * its own may be MCP's embedded resource instead, its contents under
101
+ * `resource`, which classifyBlock tells apart and shows as its contents. */
99
102
  export interface McplResourceContent {
100
103
  type: 'resource';
101
104
  /** Resource URI (e.g., "memory://facts/12345") */
@@ -190,6 +193,9 @@ export interface McplHostCapabilities {
190
193
  modelInfo?: boolean;
191
194
  featureSets?: boolean;
192
195
  channels?: boolean | McplChannelCapabilities;
196
+ /** RFC-014 §6.3: this host presents tool hints. Sent only to a server
197
+ * whose config sets `toolHints: true`. */
198
+ toolHints?: boolean;
193
199
  }
194
200
 
195
201
  /** Channel capability leaves (§14.1). `observe` was the pre-0.5 name for
@@ -498,6 +504,20 @@ export interface McplServerConfig {
498
504
  * See src/mcpl/tool-lifecycle.ts.
499
505
  */
500
506
  toolLifecycle?: import('./tool-lifecycle.js').ToolLifecycleConfig;
507
+
508
+ /**
509
+ * MCPL RFC-014 (tool presence, toolsets and surfaces). Default false: the
510
+ * host advertises nothing and treats every tool as static, byte for byte as
511
+ * before. When true, the host advertises `experimental.mcpl.toolHints`, and
512
+ * honours hints only from a server that advertised `toolHints` too. Then
513
+ * the server's static tools stay in the tool list as before, and its
514
+ * dynamic tools (toolset tools, `mcpl/presence: "dynamic"`) leave it: they
515
+ * are announced in the conversation as they become visible and invisible,
516
+ * and called through the `toolsets` module's `toolsets--act`, opened with
517
+ * `toolsets--open`, inspected with `toolsets--look` and closed with
518
+ * `toolsets--close`. Only `true` turns it on, and null counts as not set.
519
+ */
520
+ toolHints?: boolean;
501
521
  }
502
522
 
503
523
  // ============================================================================
@@ -1299,7 +1319,8 @@ export interface ChannelIncomingMessageResult {
1299
1319
  * Non-normative; host-defined. See Spec Section 11.5.
1300
1320
  */
1301
1321
  export interface InferenceRoutingPolicy {
1302
- /** Default model for all inference requests */
1322
+ /** Model for a request no override, byFeature or wildcards entry names a
1323
+ * model for. Left empty, such a request is refused with -32603. */
1303
1324
  default: string;
1304
1325
 
1305
1326
  /** Model override per feature set name */
@@ -14,31 +14,44 @@
14
14
  * a stored message.
15
15
  */
16
16
 
17
+ import { classifyBlock, NON_STRING_TEXT_PLACEHOLDER } from './references.js';
18
+
17
19
  /**
18
20
  * True when `content` holds nothing the model would see: no non-text block,
19
21
  * and no text block whose text has a non-whitespace character. Mirrors the
20
22
  * request sanitizer, which drops text that is not a string or trims to ''.
21
- * Accepts MCPL wire blocks and converted membrane blocks alike.
23
+ * Accepts MCPL wire blocks and converted membrane blocks alike. A wire
24
+ * block is judged as the block the converter makes of it where that's text:
25
+ * MCP's embedded resource shows as the block its contents make
26
+ * (classifyBlock's `embedded`), so one whose contents are blank text holds
27
+ * nothing to see.
22
28
  */
23
29
  export function isVisiblyEmptyContent(content: readonly unknown[] | null | undefined): boolean {
24
30
  if (!Array.isArray(content)) return true;
25
31
  for (const block of content) {
26
32
  if (!block || typeof block !== 'object') continue;
27
33
  const b = block as { type?: unknown; text?: unknown };
34
+ const shown = b.type === 'resource' ? embeddedText(b) : undefined;
35
+ if (shown !== undefined) {
36
+ if (shown.trim() !== '') return false;
37
+ continue;
38
+ }
28
39
  if (b.type !== 'text') return false;
29
40
  if (typeof b.text === 'string' && b.text.trim() !== '') return false;
30
41
  }
31
42
  return true;
32
43
  }
33
44
 
34
- /**
35
- * What the converter (convertBlock, on every lane) shows for a text
36
- * block whose text isn't a string, which only a nonconformant sender writes,
37
- * so the agent learns that a block was there. It is the host's word, not the
38
- * sender's, so it never makes content visible (isVisiblyEmptyConversion).
39
- * The other blocks the converters can't show have theirs in references.ts.
40
- */
41
- export const NON_STRING_TEXT_PLACEHOLDER = '[Text not shown: a text part arrived without readable text.]';
45
+ /** The text an embedded resource's contents make, or undefined for any other
46
+ * block, an embedded resource shown as anything but text among them. */
47
+ function embeddedText(block: unknown): string | undefined {
48
+ const c = classifyBlock(block);
49
+ return c.kind === 'embedded' && c.contents.kind === 'block' && c.contents.block.type === 'text' ? c.contents.block.text : undefined;
50
+ }
51
+
52
+ // What the converter shows for a text block whose text isn't a string,
53
+ // beside the converters' other markers in references.ts.
54
+ export { NON_STRING_TEXT_PLACEHOLDER };
42
55
 
43
56
  /** A wire text block whose text isn't a string, which the converter shows as
44
57
  * NON_STRING_TEXT_PLACEHOLDER. */