@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
@@ -34,6 +34,7 @@
34
34
  * follow-up.
35
35
  */
36
36
 
37
+ import { NOTICE_TAG } from '../notice-tags.js';
37
38
  import { createHash } from 'node:crypto';
38
39
  import type { ContextStrategy } from '@animalabs/context-manager';
39
40
  import type { ChannelDescriptor } from './types.js';
@@ -144,7 +145,7 @@ export type RoutingDecision =
144
145
  const DEFAULT_IDLE_TTL_MS = 12 * 60 * 60 * 1000;
145
146
 
146
147
  export const DEFAULT_CLOSURE_PROMPT =
147
- '[system] This engagement is closing due to inactivity. Finalize your ' +
148
+ `[${NOTICE_TAG.system}] This engagement is closing due to inactivity. Finalize your ` +
148
149
  'work: post any promised results to the channel and record outstanding ' +
149
150
  'threads as unresolved. Do not ask follow-up questions.';
150
151
 
@@ -4,6 +4,7 @@
4
4
  * about a malformed block holds on all three.
5
5
  */
6
6
 
7
+ import { NOTICE_TAG } from '../notice-tags.js';
7
8
  import type { ContentBlock } from '@animalabs/membrane';
8
9
  import {
9
10
  AUDIO_NO_DATA_TEXT,
@@ -18,6 +19,7 @@ import {
18
19
  } from './references.js';
19
20
  import { NON_STRING_TEXT_PLACEHOLDER } from './visible-content.js';
20
21
  import type { McplContentBlock } from './types.js';
22
+ import { markNoticeOpenings } from '../supplied-text.js';
21
23
 
22
24
  /**
23
25
  * Convert a single MCPL wire-format content block to a membrane ContentBlock.
@@ -50,7 +52,7 @@ export function convertBlock(block: McplContentBlock, provenance: string, server
50
52
  // is a claim, one the reference rejects included, as classifyBlock
51
53
  // and isInlineContradiction read it: a falsy one such as 0, false
52
54
  // or '' stubs too, rather than sending the URI as the image's source.
53
- return { type: 'text', text: referenceStubOrNull(block, provenance, serverId) ?? '[reference]' };
55
+ return { type: 'text', text: referenceStubOrNull(block, provenance, serverId) ?? `[${NOTICE_TAG.reference}]` };
54
56
  }
55
57
  if (isFilledString(block.data) && isFilledString(block.mimeType)) {
56
58
  return {
@@ -76,7 +78,7 @@ export function convertBlock(block: McplContentBlock, provenance: string, server
76
78
  // RFC-005 uri-form media with a disposition claim, a falsy one
77
79
  // included, as for an image (above): stub, do not hand the URI to
78
80
  // the provider or inline it.
79
- return { type: 'text', text: referenceStubOrNull(block, provenance, serverId) ?? '[reference]' };
81
+ return { type: 'text', text: referenceStubOrNull(block, provenance, serverId) ?? `[${NOTICE_TAG.reference}]` };
80
82
  }
81
83
  if (isFilledString(block.data) && isFilledString(block.mimeType)) {
82
84
  return {
@@ -95,14 +97,14 @@ export function convertBlock(block: McplContentBlock, provenance: string, server
95
97
  }
96
98
  // RFC-005: reference blocks become bounded stubs — never raw URIs
97
99
  // (a signed URL is a bearer credential that looks like a location).
98
- return { type: 'text', text: referenceStubOrNull(block, provenance, serverId) ?? '[reference]' };
100
+ return { type: 'text', text: referenceStubOrNull(block, provenance, serverId) ?? `[${NOTICE_TAG.reference}]` };
99
101
  }
100
102
 
101
103
  default: {
102
104
  // MCP's link to a resource isn't one of MCPL's own blocks, but a server
103
105
  // can send one, and RFC-005 §8.1 makes it a reference, shown as one.
104
106
  if ((block as { type?: unknown }).type === 'resource_link') {
105
- return { type: 'text', text: referenceStubOrNull(block, provenance, serverId) ?? '[reference]' };
107
+ return { type: 'text', text: referenceStubOrNull(block, provenance, serverId) ?? `[${NOTICE_TAG.reference}]` };
106
108
  }
107
109
  // Unknown wire block types previously fell off the exhaustive switch
108
110
  // and propagated `undefined` into ContentBlock[]. Fail visibly. A type
@@ -113,6 +115,17 @@ export function convertBlock(block: McplContentBlock, provenance: string, server
113
115
  }
114
116
  }
115
117
 
118
+ export function convertBlocks(blocks: readonly McplContentBlock[], provenance: string, serverId: string): ContentBlock[] {
119
+ const converted = blocks.map((block) => convertBlock(block, provenance, serverId));
120
+ return markNoticeOpenings(converted, (index) => sendersText(blocks[index]));
121
+ }
122
+
123
+ function sendersText(block: McplContentBlock | undefined): boolean {
124
+ if (block?.type === 'text') return typeof block.text === 'string';
125
+ const c = classifyBlock(block);
126
+ return c.kind === 'embedded' && c.contents.kind === 'block' && c.contents.block.type === 'text';
127
+ }
128
+
116
129
  /** A string with something in it: the only data, mimeType or uri a block's
117
130
  * source is built from, and the only uri that makes it a reference. Anything
118
131
  * else a sender puts there leaves the block without it, so conversion never
@@ -14,7 +14,8 @@
14
14
  */
15
15
 
16
16
  import type { ContentBlock } from '@animalabs/membrane';
17
- import { convertBlock } from './convert-block.js';
17
+ import { convertBlocks } from './convert-block.js';
18
+ import { markedText } from '../supplied-text.js';
18
19
  import type { ContextInjection } from '@animalabs/context-manager';
19
20
 
20
21
  import type {
@@ -64,9 +65,9 @@ function withTimeout<T>(promise: Promise<T>, ms: number, label: string): Promise
64
65
  */
65
66
  function convertContent(content: string | McplContentBlock[], serverId: string): ContentBlock[] {
66
67
  if (typeof content === 'string') {
67
- return [{ type: 'text', text: content }];
68
+ return [{ type: 'text', text: markedText(content) }];
68
69
  }
69
- return content.map((block) => convertBlock(block, 'attachment on context injection', serverId));
70
+ return convertBlocks(content, 'attachment on context injection', serverId);
70
71
  }
71
72
 
72
73
  /**
@@ -217,7 +217,9 @@ function storedBlock(block: unknown): unknown {
217
217
  * The version identity of a delivered body (agreed with the receipts lane,
218
218
  * room-220 #46752–#47210): SHA-256 hex of `canonicalJson([blocks])` over the
219
219
  * ContentBlock[] the framework stores for the body — after MCPL conversion,
220
- * before any host decoration is added and before storage shards it — with
220
+ * which marks the server's text at the framework's notice openings
221
+ * (supplied-text.ts), before any host decoration is added and before storage
222
+ * shards it — with
221
223
  * each block as the store keeps it, which context-manager's `blockAsStored`
222
224
  * gives. So a body hashes alike as delivered, as handed to storage, and as
223
225
  * read back unsharded from the store with its blobs resolved, except one
@@ -68,7 +68,7 @@ import {
68
68
 
69
69
  import { StdioTransport, type McplTransport } from './transport.js';
70
70
  import { McplProtocolVersionError, McplRequestError, McplUnreapedLaunchError } from './server-connection.js';
71
- import { LEGACY_MCP_PROTOCOL_VERSION, MAX_TIMER_MS, MODERN_MCP_PROTOCOL_VERSION, checkServerConfig, describeCredentialError, maskedError, noteDialSent, redactServerText, redactedData, serverTextRedactor, type ServerTextRedactor } from './protocol-family.js';
71
+ import { LEGACY_MCP_PROTOCOL_VERSION, MAX_TIMER_MS, MODERN_MCP_PROTOCOL_VERSION, checkServerConfig, describeCredentialError, logDroppedLine, maskedError, noteDialSent, redactServerText, redactedData, serverTextRedactor, type ServerTextRedactor } from './protocol-family.js';
72
72
  import type { McplServerConfig, McpToolDefinition } from './types.js';
73
73
  import { boundedForLog } from './errors.js';
74
74
  import { logDetail, logValue, logValueRedacted, loggedCode } from './log-value.js';
@@ -297,8 +297,9 @@ export function malformedLineDiagnostic(
297
297
 
298
298
  /**
299
299
  * SDK `Transport` over Connectome's stdio spawner. Framing is the same
300
- * newline-delimited JSON-RPC as legacy. Lines that are not JSON are ignored,
301
- * as the legacy engine ignores them. Because this is a custom transport, the
300
+ * newline-delimited JSON-RPC as legacy. Lines that are not JSON are dropped,
301
+ * and the first three on a connection that aren't blank are logged by their
302
+ * length, as the legacy engine does. Because this is a custom transport, the
302
303
  * SDK runs `server/discover` on this very process instead of starting a
303
304
  * throwaway copy, and cancels with `notifications/cancelled`, which is the
304
305
  * stdio binding's mechanism.
@@ -332,14 +333,17 @@ class SpawnerStdioTransport implements Transport {
332
333
  const line = await this.spawning;
333
334
  this.line = line;
334
335
  line.on('line', (text: string) => {
335
- // A line that isn't JSON is ignored, as the legacy engine ignores it.
336
- // A line that is JSON but not a JSON-RPC message (`null`, an array, a
337
- // bare value) is a diagnostic. It must never reach a handler that
338
- // assumes the shape: thrown from this callback, it would end the host.
336
+ // A line that isn't JSON is dropped, and the first three on a
337
+ // connection that aren't blank are logged by their length, as the
338
+ // legacy engine does. A line that is JSON but not a JSON-RPC message
339
+ // (`null`, an array, a bare value) is a diagnostic. It must never reach
340
+ // a handler that assumes the shape: thrown from this callback, it would
341
+ // end the host.
339
342
  let parsed: unknown;
340
343
  try {
341
344
  parsed = JSON.parse(text);
342
345
  } catch {
346
+ logDroppedLine(this.config.id, text, 'unparsed', line);
343
347
  return;
344
348
  }
345
349
  let message: JSONRPCMessage;
@@ -22,6 +22,7 @@
22
22
  * isn't `initialize`), the SDK's stdio probe launches the server twice, and a
23
23
  * server's era is a stable fact its configuration can state.
24
24
  */
25
+ import { NOTICE_TAG } from '../notice-tags.js';
25
26
  import { DENY_BY_DEFAULT, capabilityPatternMatches } from './capability-grant.js';
26
27
  import { DEFAULT_EAGER_FETCH_MAX_BYTES, DEFAULT_MAX_TOTAL_BYTES } from './reference-fetcher.js';
27
28
  import { DEFAULT_MAX_INPUT_BYTES } from './tool-lifecycle.js';
@@ -198,11 +199,11 @@ export function serverTextRedactor(config: Pick<McplServerConfig, 'url' | 'token
198
199
 
199
200
  /** What a redaction gives in place of text when the configuration it
200
201
  * redacts with can't be read ({@link redactServerText}). */
201
- export const WITHHELD_TEXT = "[withheld: the server's configuration couldn't be read to redact this]";
202
+ export const WITHHELD_TEXT = `[${NOTICE_TAG.withheld}: the server's configuration couldn't be read to redact this]`;
202
203
 
203
204
  /** What {@link redactedData} gives in place of data nested deeper than it
204
205
  * walks. */
205
- export const TOO_DEEP_TEXT = '[withheld: nested too deep to redact]';
206
+ export const TOO_DEEP_TEXT = `[${NOTICE_TAG.withheld}: nested too deep to redact]`;
206
207
 
207
208
  /** How deep {@link redactedData} walks. A server's JSON can nest deeper than
208
209
  * a recursive walk's stack holds (10,000 levels overflow it), and the walk
@@ -528,7 +529,7 @@ export function credentialRedactor(patterns: readonly CredentialPattern[]): Reda
528
529
  /** Any value as text: a value that can't be printed reads as a placeholder. */
529
530
  export function asText(text: unknown): string {
530
531
  if (typeof text === 'string') return text;
531
- try { return String(text); } catch { return '[a value that cannot be printed]'; }
532
+ try { return String(text); } catch { return `[${NOTICE_TAG.unprintableValue}]`; }
532
533
  }
533
534
 
534
535
  /** A part of an environment variable's name that ends with a credential word, in the singular or plural, with any digits after it. */
@@ -869,3 +870,23 @@ export function checkServerConfig(config: McplServerConfig): ServerBinding {
869
870
  for (const warning of serverConfigWarnings(config)) console.warn(`[mcp] ${warning}`);
870
871
  return resolveServerBinding(config);
871
872
  }
873
+
874
+ const DROPPED_LINES_LOGGED = 3;
875
+ const droppedLinesLogged = new WeakMap<object, number>();
876
+
877
+ export function logDroppedLine(serverId: string, line: string, reason: 'unparsed' | 'not-object', connection: object): void {
878
+ if (/^[ \t]*$/.test(line)) return;
879
+ const logged = droppedLinesLogged.get(connection) ?? 0;
880
+ if (logged > DROPPED_LINES_LOGGED) return;
881
+ droppedLinesLogged.set(connection, logged + 1);
882
+ if (logged === DROPPED_LINES_LOGGED) {
883
+ console.error(`MCP server "${serverId}" sent another line that isn't a JSON object, so it was dropped; more such lines on this connection are dropped without a log line`);
884
+ return;
885
+ }
886
+ let length = 0;
887
+ for (const _ of line) length++;
888
+ const why = reason === 'not-object' ? 'is JSON but not an object'
889
+ : /^[ \t]*\{/.test(line) ? "starts like a JSON object but doesn't parse"
890
+ : "isn't JSON";
891
+ console.error(`MCP server "${serverId}" sent a line of length ${length} that ${why}, so it was dropped`);
892
+ }
@@ -1000,6 +1000,25 @@ export class PushCoalescer<E = unknown> {
1000
1000
  return result;
1001
1001
  }
1002
1002
 
1003
+ /**
1004
+ * Render and materialize one subject's pending batch for `agentName` now,
1005
+ * outside a turn's assembly: a batch whose wake a hold keeps back, stored
1006
+ * as it renders (held, as a plain push under the hold is), so whoever reads
1007
+ * held traffic sees it and the turn after the hold doesn't take it again.
1008
+ * A batch that opened while another render ran is rendered once that
1009
+ * render settles. Resolves whether it materialized.
1010
+ */
1011
+ async materializeNow(agentName: string, subject: string): Promise<boolean> {
1012
+ for (;;) {
1013
+ if (this.suspended) return false;
1014
+ const started = await this.locked(() => this.freeze(subject, agentName));
1015
+ if (!started || started === 'declined') return false;
1016
+ await started.done;
1017
+ this.persist();
1018
+ if (!started.joined) return started.rendering.materialized === true;
1019
+ }
1020
+ }
1021
+
1003
1022
  /**
1004
1023
  * Under the lock: decide whether `subject`'s batch renders for `agentName`,
1005
1024
  * freeze it, make the render-start durable, re-check authority, and start
@@ -8,7 +8,7 @@
8
8
  */
9
9
 
10
10
  import type { ContentBlock } from '@animalabs/membrane';
11
- import { convertBlock } from './convert-block.js';
11
+ import { convertBlocks } from './convert-block.js';
12
12
 
13
13
  import type {
14
14
  PushEventParams,
@@ -204,7 +204,7 @@ export class PushHandler {
204
204
 
205
205
  // 2. Convert content blocks, and refuse content with nothing visible in
206
206
  // it — before dedup, so a refused eventId is not burned for a retry.
207
- const content: ContentBlock[] = params.payload.content.map((block) => convertBlock(block, 'attachment on push event', serverId));
207
+ const content: ContentBlock[] = convertBlocks(params.payload.content, 'attachment on push event', serverId);
208
208
  if (!emptyAllowed && isVisiblyEmptyConversion(params.payload.content, content)) {
209
209
  const err = new EmptyContentError();
210
210
  this.traceEmptyRejection(serverId, params);
@@ -26,6 +26,9 @@
26
26
  * the registry is what makes a future host-mediated fetcher and the
27
27
  * code-execution materialization hook possible.
28
28
  */
29
+ import { cutToFit, suppliedValue } from '../supplied-text.js';
30
+ import { NOTICE_TAG } from '../notice-tags.js';
31
+
29
32
 
30
33
  export const REFERENCE_LIMITS = { uri: 4096, mimeType: 255, name: 255, expiresAt: 64 } as const;
31
34
  export const DIGEST_PATTERN = /^sha256:[A-Za-z0-9_-]{43}$/;
@@ -210,7 +213,7 @@ export function isInlineContradiction(block: unknown): boolean {
210
213
  }
211
214
 
212
215
  export const INLINE_WITHHELD_TEXT =
213
- '[Content not shown: its inline data arrived marked as a reference too, so this host withheld it.]';
216
+ `[${NOTICE_TAG.contentNotShown}: its inline data arrived marked as a reference too, so this host withheld it.]`;
214
217
 
215
218
  /*
216
219
  * What the model-visible lanes (push, channel, context injection and tool
@@ -220,28 +223,28 @@ export const INLINE_WITHHELD_TEXT =
220
223
  * agent to do about any of them. The text marker is NON_STRING_TEXT_PLACEHOLDER
221
224
  * (visible-content.ts).
222
225
  */
223
- export const IMAGE_NO_MEDIA_TYPE_TEXT = '[Image not shown: its data arrived without a media type.]';
224
- export const IMAGE_NO_DATA_TEXT = '[Image not shown: it arrived with no usable data or link.]';
225
- export const AUDIO_NO_MEDIA_TYPE_TEXT = '[Audio not shown: its data arrived without a media type.]';
226
+ export const IMAGE_NO_MEDIA_TYPE_TEXT = `[${NOTICE_TAG.imageNotShown}: its data arrived without a media type.]`;
227
+ export const IMAGE_NO_DATA_TEXT = `[${NOTICE_TAG.imageNotShown}: it arrived with no usable data or link.]`;
228
+ export const AUDIO_NO_MEDIA_TYPE_TEXT = `[${NOTICE_TAG.audioNotShown}: its data arrived without a media type.]`;
226
229
  /** On the push, channel and context-injection lanes, audio shows only as
227
230
  * inline data, so a plain link isn't enough either. (A tool result's audio
228
231
  * with a link is a reference, as any block with one is there.) */
229
- export const AUDIO_NO_DATA_TEXT = '[Audio not shown: it arrived with no usable inline data.]';
232
+ export const AUDIO_NO_DATA_TEXT = `[${NOTICE_TAG.audioNotShown}: it arrived with no usable inline data.]`;
230
233
  /** MCP's embedded resource whose contents are neither text nor a blob, or
231
234
  * both. */
232
- export const EMBEDDED_RESOURCE_UNREADABLE_TEXT = '[Resource not shown: its contents arrived as neither text nor data, or as both.]';
235
+ export const EMBEDDED_RESOURCE_UNREADABLE_TEXT = `[${NOTICE_TAG.resourceNotShown}: its contents arrived as neither text nor data, or as both.]`;
233
236
  /** MCP's embedded resource whose blob has no media type, so it can't be
234
237
  * shown as an image or audio. */
235
- export const EMBEDDED_RESOURCE_NO_MEDIA_TYPE_TEXT = '[Resource not shown: its data arrived without a media type.]';
238
+ export const EMBEDDED_RESOURCE_NO_MEDIA_TYPE_TEXT = `[${NOTICE_TAG.resourceNotShown}: its data arrived without a media type.]`;
236
239
 
237
240
  /** MCP's embedded resource whose blob is of a type no lane can show: what
238
241
  * it was, by its type and size. Made well formed (a lone UTF-16 surrogate
239
242
  * becomes U+FFFD), as a stub is (#695), since it names the block's media
240
- * type, which sanitizeLabel leaves as it is: every lane then shows the same
241
- * text. */
243
+ * type, whose lone surrogates referenceTypeLabel leaves as they are: every
244
+ * lane then shows the same text. */
242
245
  export function embeddedBinaryText(mimeType: string, blob: string): string {
243
246
  const what = [referenceTypeLabel(mimeType), formatSizeBytes(Math.floor(blob.length * 3 / 4))].filter(Boolean).join(', ');
244
- return `[Resource not shown: this host can't display its data (${what}).]`.toWellFormed();
247
+ return `[${NOTICE_TAG.resourceNotShown}: this host can't display its data (${what}).]`.toWellFormed();
245
248
  }
246
249
  /**
247
250
  * What the converter (convertBlock, on every lane) shows for a text
@@ -250,10 +253,10 @@ export function embeddedBinaryText(mimeType: string, blob: string): string {
250
253
  * sender's, so it never makes content visible (isVisiblyEmptyConversion in
251
254
  * visible-content.ts, which re-exports it).
252
255
  */
253
- export const NON_STRING_TEXT_PLACEHOLDER = '[Text not shown: a text part arrived without readable text.]';
254
- export const REFERENCE_NO_LINK_TEXT = '[Reference not shown: it arrived without a link.]';
255
- export const REFERENCE_LINK_TOO_LONG_TEXT = '[Reference not shown: its link is longer than this host accepts.]';
256
- const UNREADABLE_BLOCK_TEXT = "[Content not shown: a part arrived in a form this host can't read.]";
256
+ export const NON_STRING_TEXT_PLACEHOLDER = `[${NOTICE_TAG.textNotShown}: a text part arrived without readable text.]`;
257
+ export const REFERENCE_NO_LINK_TEXT = `[${NOTICE_TAG.referenceNotShown}: it arrived without a link.]`;
258
+ export const REFERENCE_LINK_TOO_LONG_TEXT = `[${NOTICE_TAG.referenceNotShown}: its link is longer than this host accepts.]`;
259
+ const UNREADABLE_BLOCK_TEXT = `[${NOTICE_TAG.contentNotShown}: a part arrived in a form this host can't read.]`;
257
260
 
258
261
  /** A block of a type the host doesn't show, of no type at all, or no block.
259
262
  * The type is the sender's word, so it's shown as a label is: bounded, with
@@ -261,10 +264,10 @@ const UNREADABLE_BLOCK_TEXT = "[Content not shown: a part arrived in a form this
261
264
  * converted to one. */
262
265
  export function unknownBlockText(block: unknown): string {
263
266
  if (!isRecord(block)) return UNREADABLE_BLOCK_TEXT;
264
- const label = typeof block.type === 'string' ? sanitizeLabel(block.type, STUB_FIELD_CHARS) : '';
267
+ const label = typeof block.type === 'string' ? suppliedLabel(block.type, STUB_FIELD_CHARS) : '';
265
268
  return label
266
- ? `[Content not shown: this host can't display "${label}" content.]`
267
- : '[Content not shown: a part arrived with no type.]';
269
+ ? `[${NOTICE_TAG.contentNotShown}: this host can't display "${label}" content.]`
270
+ : `[${NOTICE_TAG.contentNotShown}: a part arrived with no type.]`;
268
271
  }
269
272
 
270
273
  /**
@@ -654,6 +657,11 @@ export function sanitizeLabel(s: string, maxChars: number): string {
654
657
  return cut + '…';
655
658
  }
656
659
 
660
+ function suppliedLabel(s: string, maxChars: number): string {
661
+ const shown = suppliedValue(s).replace(/[\p{Cc}\p{Bidi_Control}]/gu, '').trim();
662
+ return shown.length <= maxChars ? shown : `${cutToFit(shown, maxChars - 1)}…`;
663
+ }
664
+
657
665
  export function formatSizeBytes(n: number): string {
658
666
  if (n < 1024) return `${n}B`;
659
667
  if (n < 1024 * 1024) return `~${Math.round(n / 1024)}KB`;
@@ -786,7 +794,7 @@ export class ReferenceRegistry {
786
794
  return existing;
787
795
  }
788
796
  }
789
- const refId = `ref_${(++this.counter).toString(36)}_${Math.random().toString(36).slice(2, 2 + REF_ID_SUFFIX_CHARS)}`;
797
+ const refId = `${NOTICE_TAG.referenceId}_${(++this.counter).toString(36)}_${Math.random().toString(36).slice(2, 2 + REF_ID_SUFFIX_CHARS)}`;
790
798
  const record: ReferenceRecord = { refId, serverIds: serverId === undefined ? [] : [serverId], testimony, receivedAt: Date.now() };
791
799
  this.records.set(refId, record);
792
800
  this.byKey.set(testimony.uri, refId);
@@ -871,7 +879,7 @@ export interface ScriptLine {
871
879
  /** A reference's type as its stub shows one, or '' when nothing is left of
872
880
  * it once sanitized (the stub then leaves it out). */
873
881
  export function referenceTypeLabel(mimeType: string | undefined): string {
874
- return mimeType ? sanitizeLabel(mimeType, STUB_FIELD_CHARS) : '';
882
+ return mimeType ? suppliedLabel(mimeType, STUB_FIELD_CHARS) : '';
875
883
  }
876
884
 
877
885
  /**
@@ -886,7 +894,7 @@ export function referenceTypeLabel(mimeType: string | undefined): string {
886
894
  * stub, so it's no slot whatever its length.
887
895
  */
888
896
  export const REFERENCE_STUB_MAX_CHARS =
889
- `[ref_${MAX_SAFE.toString(36)}_] `.length + REF_ID_SUFFIX_CHARS
897
+ `[${NOTICE_TAG.referenceId}_${MAX_SAFE.toString(36)}_] `.length + REF_ID_SUFFIX_CHARS
890
898
  + STUB_FIELD_CHARS
891
899
  + ' — '.length + STUB_FIELD_CHARS + ', '.length + `${formatSizeBytes(MAX_SAFE)} claimed`.length
892
900
  + ' — '.length + STUB_FIELD_CHARS
@@ -1099,7 +1107,7 @@ export function buildReferenceStub(t: ReferenceTestimony, provenance?: string, s
1099
1107
  const nowMs = Date.now();
1100
1108
  const parts: string[] = [];
1101
1109
  // A label is shown only when something is left of it once sanitized.
1102
- const name = t.name ? sanitizeLabel(t.name, STUB_FIELD_CHARS) : '';
1110
+ const name = t.name ? suppliedLabel(t.name, STUB_FIELD_CHARS) : '';
1103
1111
  if (name) parts.push(name);
1104
1112
  const meta: string[] = [];
1105
1113
  const mimeType = referenceTypeLabel(t.mimeType);
@@ -1130,9 +1138,9 @@ export function buildReferenceStub(t: ReferenceTestimony, provenance?: string, s
1130
1138
  * server's link shows (StubOptions.unfetchedLinkUri).
1131
1139
  */
1132
1140
  function unfetchedLinkText(t: ReferenceTestimony, uri: string): string {
1133
- const name = t.name ? sanitizeLabel(t.name, STUB_FIELD_CHARS) : '';
1141
+ const name = t.name ? suppliedLabel(t.name, STUB_FIELD_CHARS) : '';
1134
1142
  const mimeType = referenceTypeLabel(t.mimeType);
1135
- return `[resource link${name ? ` "${name}"` : ''}: ${uri}${mimeType ? `, ${mimeType}` : ''}; not fetched]`;
1143
+ return `[${NOTICE_TAG.resourceLink}${name ? ` "${name}"` : ''}: ${uri}${mimeType ? `, ${mimeType}` : ''}; not fetched]`;
1136
1144
  }
1137
1145
 
1138
1146
  /**
@@ -20,6 +20,7 @@ import {
20
20
  LEGACY_MCP_PROTOCOL_VERSION,
21
21
  MODERN_MCP_PROTOCOL_VERSION,
22
22
  checkServerConfig,
23
+ logDroppedLine,
23
24
  redactServerText,
24
25
  redactedData,
25
26
  serverTextRedactor,
@@ -1330,13 +1331,8 @@ export class McplServerConnection extends EventEmitter {
1330
1331
  reject(closedWhileConnecting());
1331
1332
  };
1332
1333
  const onLine = (line: string) => {
1333
- let msg: JsonRpcResponse;
1334
- try {
1335
- msg = JSON.parse(line) as JsonRpcResponse;
1336
- } catch {
1337
- return; // Ignore non-JSON lines (e.g. logback output from Java servers)
1338
- }
1339
- if (msg === null || typeof msg !== 'object' || Array.isArray(msg)) return;
1334
+ const msg = messageFromLine(config.id, line, transport) as JsonRpcResponse | undefined;
1335
+ if (!msg) return;
1340
1336
  // A request or a notification from the server is never the
1341
1337
  // initialize response, whatever its id: a server's own ids can start
1342
1338
  // at 0, this handshake's. MCP lets a server ping before
@@ -2376,17 +2372,8 @@ export class McplServerConnection extends EventEmitter {
2376
2372
  // passed is the dead process's (afterBoundary).
2377
2373
  const epoch = this.transportEpoch;
2378
2374
  transport.on('line', (line: string) => {
2379
- let msg: JsonRpcRequest | JsonRpcResponse;
2380
- try {
2381
- msg = JSON.parse(line);
2382
- } catch {
2383
- // Ignore non-JSON lines
2384
- return;
2385
- }
2386
-
2387
- // JSON-RPC messages must be objects. Ignore unrelated stdout/log values
2388
- // before property access, just as malformed JSON lines are ignored.
2389
- if (msg === null || typeof msg !== 'object' || Array.isArray(msg)) return;
2375
+ const msg = messageFromLine(this.id, line, transport) as JsonRpcRequest | JsonRpcResponse | undefined;
2376
+ if (!msg) return;
2390
2377
  try {
2391
2378
  this.dispatchMessage(msg, epoch);
2392
2379
  } catch (error) {
@@ -2829,3 +2816,18 @@ export class McplServerConnection extends EventEmitter {
2829
2816
  });
2830
2817
  }
2831
2818
  }
2819
+
2820
+ function messageFromLine(serverId: string, line: string, transport: McplTransport): object | undefined {
2821
+ let value: unknown;
2822
+ try {
2823
+ value = JSON.parse(line);
2824
+ } catch {
2825
+ logDroppedLine(serverId, line, 'unparsed', transport);
2826
+ return undefined;
2827
+ }
2828
+ if (value === null || typeof value !== 'object' || Array.isArray(value)) {
2829
+ logDroppedLine(serverId, line, 'not-object', transport);
2830
+ return undefined;
2831
+ }
2832
+ return value;
2833
+ }
@@ -43,6 +43,41 @@ export function readSkipReplyWake(input: unknown): SkipReplyWake {
43
43
  return { kind: 'wake', seconds: Math.floor(Math.min(SKIP_REPLY_WAKE_MAX_SECONDS, seconds)) };
44
44
  }
45
45
 
46
+ /**
47
+ * A wake the heartbeat has due before a skip_reply's self-wake: its next tick
48
+ * or a reminder, one the agent's rules would let wake it as they stand when
49
+ * the self-wake is armed. `what` names it to the agent ("your heartbeat
50
+ * (…)"); `inSeconds` is how far off it is then.
51
+ */
52
+ export interface SkipReplyEarlierWake {
53
+ inSeconds: number;
54
+ what: string;
55
+ }
56
+
57
+ /**
58
+ * The note a skip_reply's result carries: that the turn ended, and, when the
59
+ * agent set a wake, when it fires unless something wakes the agent first.
60
+ * When the heartbeat has a wake due sooner, the note adds it, and what it
61
+ * would mean: any turn start cancels a pending self-wake
62
+ * (EventGate.onInferenceStarted), so a timer the agent still wants after the
63
+ * heartbeat's turn is one it sets again then, for the time that was left.
64
+ *
65
+ * It says when the heartbeat is due, not that it will wake the agent: the
66
+ * rules can change before then, a rate limit can have no token left, a
67
+ * gate.js or a host's inference policy can hold the wake back, and a batch or
68
+ * a queued request can wake the agent sooner. Each of those leaves the note
69
+ * true.
70
+ */
71
+ export function skipReplyNote(selfWakeSeconds: number | undefined, earlier?: SkipReplyEarlierWake): string {
72
+ if (selfWakeSeconds === undefined) return 'Turn ended; nothing sent.';
73
+ const note = `Turn ended; nothing sent. Self-wake in ~${selfWakeSeconds}s unless something wakes you first.`;
74
+ if (earlier === undefined) return note;
75
+ const what = earlier.what.charAt(0).toUpperCase() + earlier.what.slice(1);
76
+ const left = Math.max(1, selfWakeSeconds - earlier.inSeconds);
77
+ return `${note} ${what} is due sooner, in ~${earlier.inSeconds}s; if it wakes you, the self-wake is cancelled, `
78
+ + `so set it again for the ~${left}s left if you still want it.`;
79
+ }
80
+
46
81
  /** The refusal the model reads when its host has no EventGate to set a timer. */
47
82
  export const SKIP_REPLY_WAKE_NO_GATE_REFUSAL =
48
83
  "Your turn didn't end, and no wake was set: this host can't wake you on a timer, so skip_reply can't take wake_in_seconds here. To end the turn and wait for the next message, call skip_reply without it. Anything you meant to come back to, do now.";
@@ -26,6 +26,7 @@
26
26
  * Ported from mcpl-cc-bridge's presence.ts, the reference host.
27
27
  */
28
28
  import { objectSignature, renderToolLines, toolDigest, type HintedTool, type Shown } from './tool-signature.js';
29
+ import { nameToTypeBack, quotedValue, suppliedValue } from '../supplied-text.js';
29
30
 
30
31
  export interface ToolsetDecl {
31
32
  title?: string;
@@ -531,7 +532,7 @@ export class ToolPresence {
531
532
  const endedKeys = new Set<string>();
532
533
  for (const e of ended) {
533
534
  if (!e.id || !this.toldEnded.has(e.id)) endedKeys.add(keyOf(e.toolset, e.instance));
534
- const note = `${this.label(keyOf(e.toolset, e.instance))} ended${e.reason ? `: ${e.reason}` : ''}.`;
535
+ const note = `${this.label(keyOf(e.toolset, e.instance))} ended${e.reason ? `: ${suppliedValue(e.reason)}` : ''}.`;
535
536
  if (e.id) {
536
537
  if (!this.toldEnded.has(e.id)) told.push({ id: e.id, note });
537
538
  } else notes.push(note);
@@ -588,27 +589,27 @@ export class ToolPresence {
588
589
  /** The marker a key's announcements begin with, `[server/toolset:instance]`, which a reminder looks for. */
589
590
  label(key: string): string {
590
591
  const { toolset, instance } = splitKey(key);
591
- return `[${this.server}${toolset ? `/${toolset}` : ''}${instance ? `:${instance}` : ''}]`;
592
+ return `[${this.server}${toolset ? `/${nameToTypeBack(toolset)}` : ''}${instance ? `:${nameToTypeBack(instance)}` : ''}]`;
592
593
  }
593
594
 
594
595
  callHint(key: string): string {
595
596
  const { instance } = splitKey(key);
596
- return `${this.names.act} {server:"${this.server}"${instance ? `, instance:"${instance}"` : ''}, tool, args}`;
597
+ return `${this.names.act} {server:"${this.server}"${instance ? `, instance:${quotedValue(instance)}` : ''}, tool, args}`;
597
598
  }
598
599
 
599
600
  closeHint(key: string): string | null {
600
601
  const { toolset, instance } = splitKey(key);
601
602
  if (!toolset) return null;
602
- return `${this.names.close} {server:"${this.server}", toolset:"${toolset}"${instance ? `, instance:"${instance}"` : ''}}`;
603
+ return `${this.names.close} {server:"${this.server}", toolset:${quotedValue(toolset)}${instance ? `, instance:${quotedValue(instance)}` : ''}}`;
603
604
  }
604
605
 
605
606
  openHint(toolset: string, instance?: string): string {
606
- return `${this.names.open} {server:"${this.server}", toolset:"${toolset}"${instance ? `, instance:"${instance}"` : ''}}`;
607
+ return `${this.names.open} {server:"${this.server}", toolset:${quotedValue(toolset)}${instance ? `, instance:${quotedValue(instance)}` : ''}}`;
607
608
  }
608
609
 
609
610
  /** The tool block for one key: its label, how to call, and the visible tools (new or changed ones described). */
610
611
  toolBlock(key: string, tools: HintedTool[], opts: { describeAll?: boolean; headed?: boolean; title?: string; seen?: Shown } = {}): string[] {
611
- const head = opts.headed === false ? '' : `${this.label(key)}${opts.title ? ` ${opts.title}` : ''} `;
612
+ const head = opts.headed === false ? '' : `${this.label(key)}${opts.title ? ` ${suppliedValue(opts.title)}` : ''} `;
612
613
  if (!tools.length) return [`${head}no tools available right now.`];
613
614
  return [`${head}tools now — ${this.callHint(key)}:`, ...renderToolLines(tools, opts.seen ?? this.shown, { describeAll: opts.describeAll })];
614
615
  }
@@ -627,13 +628,13 @@ export class ToolPresence {
627
628
  for (const c of cs.changes) {
628
629
  const bare = opts.bare === c.key;
629
630
  if (c.closed) {
630
- lines.push(`${this.label(c.key)} closed.${c.gone.length ? ` Now unavailable: ${c.gone.join(', ')}.` : ''}`);
631
+ lines.push(`${this.label(c.key)} closed.${c.gone.length ? ` Now unavailable: ${c.gone.map(suppliedValue).join(', ')}.` : ''}`);
631
632
  continue;
632
633
  }
633
634
  const title = (c.opened || c.retitled) && !bare ? c.title : undefined;
634
635
  if (showsBlock(c)) lines.push(...this.toolBlock(c.key, c.now, { headed: !bare, title, seen, describeAll: opts.describeAll }));
635
- else if (title) lines.push(`${this.label(c.key)} ${title}`);
636
- if (c.gone.length) lines.push(`now unavailable: ${c.gone.join(', ')}`);
636
+ else if (title) lines.push(`${this.label(c.key)} ${suppliedValue(title)}`);
637
+ if (c.gone.length) lines.push(`now unavailable: ${c.gone.map(suppliedValue).join(', ')}`);
637
638
  if (c.opened && !bare) {
638
639
  const close = this.closeHint(c.key);
639
640
  if (close) lines.push(`${close} closes it.`);
@@ -646,9 +647,9 @@ export class ToolPresence {
646
647
 
647
648
  /** RFC §6.2: a suggestion is shown, never acted on. */
648
649
  renderSuggestion(s: Suggestion): string {
649
- const what = `${s.toolset}${s.instance ? `:${s.instance}` : ''}`;
650
+ const what = suppliedValue(`${s.toolset}${s.instance ? `:${s.instance}` : ''}`);
650
651
  const how = s.action === 'open' ? this.openHint(s.toolset, s.instance) : this.closeHint(keyOf(s.toolset, s.instance));
651
- return `${this.server} suggests ${s.action === 'open' ? 'opening' : 'closing'} ${what}${s.reason ? `: ${s.reason}` : ''} — ${how} if you want to (nothing was done).`;
652
+ return `${this.server} suggests ${s.action === 'open' ? 'opening' : 'closing'} ${what}${s.reason ? `: ${suppliedValue(s.reason)}` : ''} — ${how} if you want to (nothing was done).`;
652
653
  }
653
654
 
654
655
  /** The keys a reminder restates: every open key of this server's view. */
@@ -674,7 +675,7 @@ export class ToolPresence {
674
675
  * reach no agent. */
675
676
  catalog(opts: { mark?: boolean } = {}): string {
676
677
  const lines: string[] = [];
677
- lines.push(this.surface ? `${this.server} — ${[this.surface.title, this.surface.summary].filter(Boolean).join(': ')}` : this.server);
678
+ lines.push(this.surface ? `${this.server} — ${[this.surface.title, this.surface.summary].filter(Boolean).map(suppliedValue).join(': ')}` : this.server);
678
679
  const v = this.view();
679
680
  if (v.size) {
680
681
  for (const [key, e] of v) {
@@ -689,7 +690,7 @@ export class ToolPresence {
689
690
  for (const [name, d] of Object.entries(this.declared)) {
690
691
  if (!d.instances && this.isOpen(name)) continue;
691
692
  openable.push(
692
- ` ${name}${d.title ? ` — ${d.title}` : ''}${d.summary ? `: ${d.summary}` : ''}` +
693
+ ` ${nameToTypeBack(name)}${d.title ? ` — ${suppliedValue(d.title)}` : ''}${d.summary ? `: ${suppliedValue(d.summary)}` : ''}` +
693
694
  (d.instances ? ' (several can be open at once; name each with instance)' : '') +
694
695
  (d.options ? ` · options ${objectSignature(d.options)}` : ''),
695
696
  );
@@ -697,7 +698,7 @@ export class ToolPresence {
697
698
  for (const g of this.undeclaredGroups) {
698
699
  if (this.groups.has(g)) continue;
699
700
  const names = this.allTools.filter((t) => this.toolsetOf(t.name) === g).map((t) => t.name);
700
- openable.push(` ${g}: ${names.join(', ')}`);
701
+ openable.push(` ${nameToTypeBack(g)}: ${names.map(nameToTypeBack).join(', ')}`);
701
702
  }
702
703
  if (openable.length) lines.push(`can open — ${this.names.open} {server:"${this.server}", toolset, instance?, options?}:`, ...openable);
703
704
  return lines.join('\n');
@@ -705,12 +706,12 @@ export class ToolPresence {
705
706
 
706
707
  /** Where a tool can be called from now, for `look` naming it. */
707
708
  toolStatus(tool: string): string {
708
- if (this.presenceOf(tool) === 'static') return `static — called by name, as ${this.listedName(tool)}`;
709
+ if (this.presenceOf(tool) === 'static') return `static — called by name, as ${nameToTypeBack(this.listedName(tool))}`;
709
710
  const where = [...this.view().entries()].filter(([, e]) => e.tools.has(tool)).map(([k]) => k);
710
711
  const ts = this.toolsetOf(tool);
711
712
  return where.length
712
- ? `available now in ${where.map((k) => this.label(k)).join(', ')} — ${this.names.act} {server:"${this.server}", tool:"${tool}"${where.length > 1 ? ', instance' : ''}, args}`
713
- : `not available right now${ts ? ` (toolset ${ts})` : ''}`;
713
+ ? `available now in ${where.map((k) => this.label(k)).join(', ')} — ${this.names.act} {server:"${this.server}", tool:${quotedValue(tool)}${where.length > 1 ? ', instance' : ''}, args}`
714
+ : `not available right now${ts ? ` (toolset ${nameToTypeBack(ts)})` : ''}`;
714
715
  }
715
716
 
716
717
  }