@animalabs/agent-framework 0.28.0 → 0.29.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (240) hide show
  1. package/README.md +88 -25
  2. package/dist/src/agent.d.ts.map +1 -1
  3. package/dist/src/agent.js +4 -1
  4. package/dist/src/agent.js.map +1 -1
  5. package/dist/src/api/mcp-server-core.d.ts.map +1 -1
  6. package/dist/src/api/mcp-server-core.js +3 -12
  7. package/dist/src/api/mcp-server-core.js.map +1 -1
  8. package/dist/src/api/server.d.ts.map +1 -1
  9. package/dist/src/api/server.js +7 -3
  10. package/dist/src/api/server.js.map +1 -1
  11. package/dist/src/auto-continue.d.ts +2 -0
  12. package/dist/src/auto-continue.d.ts.map +1 -1
  13. package/dist/src/auto-continue.js +5 -2
  14. package/dist/src/auto-continue.js.map +1 -1
  15. package/dist/src/code-execution/isolate/runner.d.ts +7 -5
  16. package/dist/src/code-execution/isolate/runner.d.ts.map +1 -1
  17. package/dist/src/code-execution/isolate/runner.js +7 -5
  18. package/dist/src/code-execution/isolate/runner.js.map +1 -1
  19. package/dist/src/code-execution/isolate/scripts.d.ts.map +1 -1
  20. package/dist/src/code-execution/isolate/scripts.js +3 -2
  21. package/dist/src/code-execution/isolate/scripts.js.map +1 -1
  22. package/dist/src/framework.d.ts +164 -36
  23. package/dist/src/framework.d.ts.map +1 -1
  24. package/dist/src/framework.js +1120 -479
  25. package/dist/src/framework.js.map +1 -1
  26. package/dist/src/gate/event-gate.d.ts +34 -10
  27. package/dist/src/gate/event-gate.d.ts.map +1 -1
  28. package/dist/src/gate/event-gate.js +118 -48
  29. package/dist/src/gate/event-gate.js.map +1 -1
  30. package/dist/src/gate/index.d.ts +1 -1
  31. package/dist/src/gate/index.d.ts.map +1 -1
  32. package/dist/src/gate/index.js +1 -1
  33. package/dist/src/gate/index.js.map +1 -1
  34. package/dist/src/gate/types.d.ts +12 -2
  35. package/dist/src/gate/types.d.ts.map +1 -1
  36. package/dist/src/heartbeat/heartbeat.d.ts +18 -0
  37. package/dist/src/heartbeat/heartbeat.d.ts.map +1 -1
  38. package/dist/src/heartbeat/heartbeat.js +34 -8
  39. package/dist/src/heartbeat/heartbeat.js.map +1 -1
  40. package/dist/src/held-writes.d.ts +3 -3
  41. package/dist/src/held-writes.js +3 -3
  42. package/dist/src/host-presentation.d.ts +170 -0
  43. package/dist/src/host-presentation.d.ts.map +1 -0
  44. package/dist/src/host-presentation.js +561 -0
  45. package/dist/src/host-presentation.js.map +1 -0
  46. package/dist/src/index.d.ts +2 -2
  47. package/dist/src/index.d.ts.map +1 -1
  48. package/dist/src/index.js +1 -1
  49. package/dist/src/index.js.map +1 -1
  50. package/dist/src/line-reader.d.ts +4 -0
  51. package/dist/src/line-reader.d.ts.map +1 -0
  52. package/dist/src/line-reader.js +59 -0
  53. package/dist/src/line-reader.js.map +1 -0
  54. package/dist/src/mcpl/channel-registry.d.ts +34 -6
  55. package/dist/src/mcpl/channel-registry.d.ts.map +1 -1
  56. package/dist/src/mcpl/channel-registry.js +131 -53
  57. package/dist/src/mcpl/channel-registry.js.map +1 -1
  58. package/dist/src/mcpl/conversation-router.d.ts.map +1 -1
  59. package/dist/src/mcpl/conversation-router.js +2 -1
  60. package/dist/src/mcpl/conversation-router.js.map +1 -1
  61. package/dist/src/mcpl/convert-block.d.ts +1 -0
  62. package/dist/src/mcpl/convert-block.d.ts.map +1 -1
  63. package/dist/src/mcpl/convert-block.js +16 -4
  64. package/dist/src/mcpl/convert-block.js.map +1 -1
  65. package/dist/src/mcpl/hook-orchestrator.d.ts.map +1 -1
  66. package/dist/src/mcpl/hook-orchestrator.js +4 -3
  67. package/dist/src/mcpl/hook-orchestrator.js.map +1 -1
  68. package/dist/src/mcpl/inbound-source.d.ts +3 -1
  69. package/dist/src/mcpl/inbound-source.d.ts.map +1 -1
  70. package/dist/src/mcpl/inbound-source.js +3 -1
  71. package/dist/src/mcpl/inbound-source.js.map +1 -1
  72. package/dist/src/mcpl/modern-connection.d.ts.map +1 -1
  73. package/dist/src/mcpl/modern-connection.js +11 -7
  74. package/dist/src/mcpl/modern-connection.js.map +1 -1
  75. package/dist/src/mcpl/protocol-family.d.ts +3 -2
  76. package/dist/src/mcpl/protocol-family.d.ts.map +1 -1
  77. package/dist/src/mcpl/protocol-family.js +25 -3
  78. package/dist/src/mcpl/protocol-family.js.map +1 -1
  79. package/dist/src/mcpl/push-handler.js +2 -2
  80. package/dist/src/mcpl/push-handler.js.map +1 -1
  81. package/dist/src/mcpl/references.d.ts +12 -40
  82. package/dist/src/mcpl/references.d.ts.map +1 -1
  83. package/dist/src/mcpl/references.js +29 -23
  84. package/dist/src/mcpl/references.js.map +1 -1
  85. package/dist/src/mcpl/server-connection.d.ts.map +1 -1
  86. package/dist/src/mcpl/server-connection.js +20 -20
  87. package/dist/src/mcpl/server-connection.js.map +1 -1
  88. package/dist/src/mcpl/skip-reply-wake.d.ts +25 -0
  89. package/dist/src/mcpl/skip-reply-wake.d.ts.map +1 -1
  90. package/dist/src/mcpl/skip-reply-wake.js +25 -0
  91. package/dist/src/mcpl/skip-reply-wake.js.map +1 -1
  92. package/dist/src/mcpl/tool-presence.d.ts.map +1 -1
  93. package/dist/src/mcpl/tool-presence.js +18 -17
  94. package/dist/src/mcpl/tool-presence.js.map +1 -1
  95. package/dist/src/mcpl/tool-signature.d.ts.map +1 -1
  96. package/dist/src/mcpl/tool-signature.js +17 -16
  97. package/dist/src/mcpl/tool-signature.js.map +1 -1
  98. package/dist/src/mcpl/toolset-host.d.ts +8 -26
  99. package/dist/src/mcpl/toolset-host.d.ts.map +1 -1
  100. package/dist/src/mcpl/toolset-host.js +16 -6
  101. package/dist/src/mcpl/toolset-host.js.map +1 -1
  102. package/dist/src/mcpl/toolsets-module.d.ts +14 -25
  103. package/dist/src/mcpl/toolsets-module.d.ts.map +1 -1
  104. package/dist/src/mcpl/toolsets-module.js +180 -50
  105. package/dist/src/mcpl/toolsets-module.js.map +1 -1
  106. package/dist/src/mcpl/transport.d.ts +1 -1
  107. package/dist/src/mcpl/transport.d.ts.map +1 -1
  108. package/dist/src/mcpl/transport.js +16 -16
  109. package/dist/src/mcpl/transport.js.map +1 -1
  110. package/dist/src/mcpl/types.d.ts +28 -12
  111. package/dist/src/mcpl/types.d.ts.map +1 -1
  112. package/dist/src/mcpl/types.js.map +1 -1
  113. package/dist/src/module-registry.d.ts +6 -7
  114. package/dist/src/module-registry.d.ts.map +1 -1
  115. package/dist/src/module-registry.js +6 -7
  116. package/dist/src/module-registry.js.map +1 -1
  117. package/dist/src/modules/api/index.d.ts.map +1 -1
  118. package/dist/src/modules/api/index.js +4 -2
  119. package/dist/src/modules/api/index.js.map +1 -1
  120. package/dist/src/modules/discord/index.d.ts.map +1 -1
  121. package/dist/src/modules/discord/index.js +33 -25
  122. package/dist/src/modules/discord/index.js.map +1 -1
  123. package/dist/src/modules/history/index.js +8 -6
  124. package/dist/src/modules/history/index.js.map +1 -1
  125. package/dist/src/modules/sender-name.d.ts +7 -0
  126. package/dist/src/modules/sender-name.d.ts.map +1 -0
  127. package/dist/src/modules/sender-name.js +10 -0
  128. package/dist/src/modules/sender-name.js.map +1 -0
  129. package/dist/src/modules/workspace/answers.d.ts +5 -1
  130. package/dist/src/modules/workspace/answers.d.ts.map +1 -1
  131. package/dist/src/modules/workspace/answers.js +16 -1
  132. package/dist/src/modules/workspace/answers.js.map +1 -1
  133. package/dist/src/modules/workspace/direct-write.d.ts.map +1 -1
  134. package/dist/src/modules/workspace/direct-write.js +6 -4
  135. package/dist/src/modules/workspace/direct-write.js.map +1 -1
  136. package/dist/src/modules/workspace/effects.d.ts +4 -2
  137. package/dist/src/modules/workspace/effects.d.ts.map +1 -1
  138. package/dist/src/modules/workspace/effects.js +50 -40
  139. package/dist/src/modules/workspace/effects.js.map +1 -1
  140. package/dist/src/modules/workspace/index.d.ts +1 -0
  141. package/dist/src/modules/workspace/index.d.ts.map +1 -1
  142. package/dist/src/modules/workspace/index.js +66 -36
  143. package/dist/src/modules/workspace/index.js.map +1 -1
  144. package/dist/src/modules/workspace/observe.d.ts +51 -20
  145. package/dist/src/modules/workspace/observe.d.ts.map +1 -1
  146. package/dist/src/modules/workspace/observe.js +120 -82
  147. package/dist/src/modules/workspace/observe.js.map +1 -1
  148. package/dist/src/modules/workspace/reconcile.d.ts +7 -1
  149. package/dist/src/modules/workspace/reconcile.d.ts.map +1 -1
  150. package/dist/src/modules/workspace/reconcile.js +133 -37
  151. package/dist/src/modules/workspace/reconcile.js.map +1 -1
  152. package/dist/src/notice-tags.d.ts +79 -0
  153. package/dist/src/notice-tags.d.ts.map +1 -0
  154. package/dist/src/notice-tags.js +83 -0
  155. package/dist/src/notice-tags.js.map +1 -0
  156. package/dist/src/reasoning-loss-notice.d.ts.map +1 -1
  157. package/dist/src/reasoning-loss-notice.js +5 -3
  158. package/dist/src/reasoning-loss-notice.js.map +1 -1
  159. package/dist/src/silent-heartbeat.d.ts +1 -53
  160. package/dist/src/silent-heartbeat.d.ts.map +1 -1
  161. package/dist/src/silent-heartbeat.js +56 -3
  162. package/dist/src/silent-heartbeat.js.map +1 -1
  163. package/dist/src/supplied-text.d.ts +35 -0
  164. package/dist/src/supplied-text.d.ts.map +1 -0
  165. package/dist/src/supplied-text.js +141 -0
  166. package/dist/src/supplied-text.js.map +1 -0
  167. package/dist/src/tool-args.d.ts.map +1 -1
  168. package/dist/src/tool-args.js +9 -8
  169. package/dist/src/tool-args.js.map +1 -1
  170. package/dist/src/tool-image-ledger.d.ts.map +1 -1
  171. package/dist/src/tool-image-ledger.js +2 -1
  172. package/dist/src/tool-image-ledger.js.map +1 -1
  173. package/dist/src/tool-presentation.d.ts.map +1 -1
  174. package/dist/src/tool-presentation.js +5 -4
  175. package/dist/src/tool-presentation.js.map +1 -1
  176. package/dist/src/tune-out/coordinator.d.ts +3 -2
  177. package/dist/src/tune-out/coordinator.d.ts.map +1 -1
  178. package/dist/src/tune-out/coordinator.js +24 -26
  179. package/dist/src/tune-out/coordinator.js.map +1 -1
  180. package/dist/src/types/agent.d.ts +23 -9
  181. package/dist/src/types/agent.d.ts.map +1 -1
  182. package/dist/src/types/framework.d.ts +43 -0
  183. package/dist/src/types/framework.d.ts.map +1 -1
  184. package/dist/src/types/module.d.ts +22 -11
  185. package/dist/src/types/module.d.ts.map +1 -1
  186. package/package.json +2 -2
  187. package/src/agent.ts +4 -1
  188. package/src/api/mcp-server-core.ts +3 -14
  189. package/src/api/server.ts +7 -3
  190. package/src/auto-continue.ts +8 -2
  191. package/src/code-execution/isolate/runner.ts +7 -5
  192. package/src/code-execution/isolate/scripts.ts +3 -2
  193. package/src/framework.ts +1132 -492
  194. package/src/gate/event-gate.ts +128 -47
  195. package/src/gate/index.ts +1 -1
  196. package/src/gate/types.ts +12 -2
  197. package/src/heartbeat/heartbeat.ts +48 -9
  198. package/src/held-writes.ts +3 -3
  199. package/src/host-presentation.ts +605 -0
  200. package/src/index.ts +2 -2
  201. package/src/line-reader.ts +58 -0
  202. package/src/mcpl/channel-registry.ts +137 -52
  203. package/src/mcpl/conversation-router.ts +2 -1
  204. package/src/mcpl/convert-block.ts +17 -4
  205. package/src/mcpl/hook-orchestrator.ts +4 -3
  206. package/src/mcpl/inbound-source.ts +3 -1
  207. package/src/mcpl/modern-connection.ts +11 -7
  208. package/src/mcpl/protocol-family.ts +24 -3
  209. package/src/mcpl/push-handler.ts +2 -2
  210. package/src/mcpl/references.ts +31 -23
  211. package/src/mcpl/server-connection.ts +20 -18
  212. package/src/mcpl/skip-reply-wake.ts +35 -0
  213. package/src/mcpl/tool-presence.ts +18 -17
  214. package/src/mcpl/tool-signature.ts +17 -16
  215. package/src/mcpl/toolset-host.ts +18 -6
  216. package/src/mcpl/toolsets-module.ts +142 -50
  217. package/src/mcpl/transport.ts +18 -14
  218. package/src/mcpl/types.ts +29 -12
  219. package/src/module-registry.ts +6 -7
  220. package/src/modules/api/index.ts +4 -2
  221. package/src/modules/discord/index.ts +37 -25
  222. package/src/modules/history/index.ts +8 -6
  223. package/src/modules/sender-name.ts +9 -0
  224. package/src/modules/workspace/answers.ts +20 -2
  225. package/src/modules/workspace/direct-write.ts +4 -3
  226. package/src/modules/workspace/effects.ts +50 -40
  227. package/src/modules/workspace/index.ts +58 -34
  228. package/src/modules/workspace/observe.ts +139 -88
  229. package/src/modules/workspace/reconcile.ts +159 -36
  230. package/src/notice-tags.ts +84 -0
  231. package/src/reasoning-loss-notice.ts +5 -3
  232. package/src/silent-heartbeat.ts +4 -3
  233. package/src/supplied-text.ts +154 -0
  234. package/src/tool-args.ts +9 -8
  235. package/src/tool-image-ledger.ts +2 -1
  236. package/src/tool-presentation.ts +5 -4
  237. package/src/tune-out/coordinator.ts +25 -27
  238. package/src/types/agent.ts +23 -9
  239. package/src/types/framework.ts +44 -0
  240. package/src/types/module.ts +23 -11
@@ -12,7 +12,9 @@
12
12
  * - Expose synthesized tools: channel_list, channel_open, channel_close, channel_publish
13
13
  * - Build channel context for beforeInference params
14
14
  */
15
- import { convertBlock } from './convert-block.js';
15
+ import { NOTICE_TAG } from '../notice-tags.js';
16
+ import { convertBlocks } from './convert-block.js';
17
+ import { cutToFit, markedText, markNoticeOpenings, nameToTypeBack, quotedValue, showsAsItself, suppliedValue } from '../supplied-text.js';
16
18
  import { isDeepStrictEqual } from 'node:util';
17
19
  import { ensureStateRegistered } from '../module-registry.js';
18
20
  import { expandCoreTags, isMarkerTagged } from './tags.js';
@@ -22,7 +24,7 @@ import { logDetail, logValue } from './log-value.js';
22
24
  import { RefusalLines } from './refusal-lines.js';
23
25
  import { CapabilityGrant } from './capability-grant.js';
24
26
  import { McplRequestError } from './server-connection.js';
25
- import { readSkipReplyWake } from './skip-reply-wake.js';
27
+ import { readSkipReplyWake, skipReplyNote } from './skip-reply-wake.js';
26
28
  import { isProseTargetToken } from './prose-grammar.js';
27
29
  // ============================================================================
28
30
  // Typing indicator interval (Discord typing lasts ~10s, so 7s keeps it alive)
@@ -410,20 +412,22 @@ const CHANNEL_TOOL_DEFINITIONS = [
410
412
  // ============================================================================
411
413
  // Constructor Options
412
414
  // ============================================================================
413
- /** Bound connector values for notices, logs and an outcome's reason. A
414
- * PublishOutcome keeps the connector's structured data (`detail`) as the
415
- * connection gives it, its strings redacted (redactedData). */
415
+ /** Bound connector values for notices, logs and an outcome's reason, on one
416
+ * line with their notice openings marked (suppliedValue). A PublishOutcome
417
+ * keeps the connector's structured data (`detail`) as the connection gives
418
+ * it, its strings redacted (redactedData). */
416
419
  export function connectorText(value) {
417
420
  let text;
418
421
  try {
419
422
  text = typeof value === 'string' ? value : JSON.stringify(value) ?? String(value);
420
423
  }
421
424
  catch {
422
- return '[unserializable connector value]';
425
+ return `[${NOTICE_TAG.unserializableConnectorValue}]`;
423
426
  }
424
427
  const limit = 2000;
425
- const suffix = '… [truncated]';
426
- return text.length > limit ? text.slice(0, limit - suffix.length) + suffix : text;
428
+ const suffix = `… [${NOTICE_TAG.truncated}]`;
429
+ const shown = suppliedValue(text);
430
+ return shown.length > limit ? cutToFit(shown, limit - suffix.length) + suffix : shown;
427
431
  }
428
432
  /** One key per channel, its server and id: unlike joining the two with a
429
433
  * separator, no two channels share one, whatever their ids hold. */
@@ -437,6 +441,8 @@ export function sameChannel(a, b) {
437
441
  return !a && !b;
438
442
  return a.serverId === b.serverId && a.channelId === b.channelId;
439
443
  }
444
+ export const NO_CHANNEL_EXAMPLE = '#<channel>';
445
+ export const NO_PERSON_EXAMPLE = '@<person>';
440
446
  /**
441
447
  * A channel's server-qualified name, `serverId:channelId`. A name given as a
442
448
  * string (a tool's channelId, a `>>` target, a host's call) may take this
@@ -447,11 +453,32 @@ export function sameChannel(a, b) {
447
453
  export function qualifiedChannelName(ref) {
448
454
  return `${ref.serverId}:${ref.channelId}`;
449
455
  }
456
+ function markedHistory(history) {
457
+ return history.map((item) => {
458
+ if (item === null || typeof item !== 'object' || Array.isArray(item) || !('content' in item))
459
+ return item;
460
+ const { content } = item;
461
+ if (Array.isArray(content))
462
+ return { ...item, content: markNoticeOpenings(content.map(withEmbeddedTextMarked)) };
463
+ return typeof content === 'string' ? { ...item, content: markedText(content) } : item;
464
+ });
465
+ }
466
+ function withEmbeddedTextMarked(block) {
467
+ if (block === null || typeof block !== 'object' || block.type !== 'resource')
468
+ return block;
469
+ const { resource } = block;
470
+ if (resource === null || typeof resource !== 'object' || typeof resource.text !== 'string')
471
+ return block;
472
+ return { ...block, resource: { ...resource, text: markedText(resource.text) } };
473
+ }
450
474
  /** A channel told apart by its id and its server, for one no string picks
451
- * alone: `"b:room" on server "a"`. No input takes it for a name. */
475
+ * alone or whose name wouldn't show as itself: `"b:room" on server "a"`,
476
+ * each a JSON literal a channel tool's channelId and serverId take back
477
+ * (quotedValue). No input takes the whole form for a name. */
452
478
  function channelOnServer(ref) {
453
- return `${JSON.stringify(ref.channelId)} on server ${JSON.stringify(ref.serverId)}`;
479
+ return `${quotedValue(ref.channelId)} on server ${quotedValue(ref.serverId)}`;
454
480
  }
481
+ const CHANNEL_ON_SERVER_AT_END = /("(?:[^"\\]|\\.)*") on server ("(?:[^"\\]|\\.)*")$/;
455
482
  /**
456
483
  * A failed request's words, bounded, for a reason that a caller composes.
457
484
  * This host's own words, for a request that wasn't sent or wasn't answered
@@ -1328,7 +1355,7 @@ export class ChannelRegistry {
1328
1355
  }
1329
1356
  if (message.tags)
1330
1357
  message.tags = expandCoreTags(message.tags);
1331
- const convertedContent = message.content.map((block) => convertBlock(block, 'attachment on channel message', serverId));
1358
+ const convertedContent = convertBlocks(message.content, 'attachment on channel message', serverId);
1332
1359
  if (!emptyAllowed && isVisiblyEmptyConversion(message.content, convertedContent)) {
1333
1360
  rejectEmpty();
1334
1361
  continue;
@@ -1764,7 +1791,7 @@ export class ChannelRegistry {
1764
1791
  case 'journal':
1765
1792
  return this.handleToolJournal(input);
1766
1793
  case 'skip_reply':
1767
- return this.handleToolSkipReply(input);
1794
+ return this.handleToolSkipReply(input, origin?.kind === 'agent' ? origin.earlierWake : undefined);
1768
1795
  default:
1769
1796
  // Not every agent is given every channel tool, so the refusal points
1770
1797
  // to the ones this agent has rather than listing them all.
@@ -1817,7 +1844,8 @@ export class ChannelRegistry {
1817
1844
  // declared it inbound-only, since speech isn't posted there): a conversation
1818
1845
  // fork's home channel, else this turn's channel (single trunk agent — item-3
1819
1846
  // redux: its locus, else its triggering channel), else the global default
1820
- // (heartbeats). Without this, a fork
1847
+ // (a no-trigger turn's, but not a heartbeat's, whose turn takes none).
1848
+ // Without this, a fork
1821
1849
  // was advertised the global locus but published somewhere else; and a trunk
1822
1850
  // agent was told the wrong channel under concurrency.
1823
1851
  // Each lookup is keyed on whether a name was given, not its truthiness:
@@ -3215,13 +3243,20 @@ export class ChannelRegistry {
3215
3243
  * name, only where no registered channel takes that name, as a channel
3216
3244
  * tool reads it or a `>>` target does (by label or raw id too), so the
3217
3245
  * name can't send anything to another channel; else by its id and server.
3246
+ * A name that wouldn't show as itself (showsAsItself: one holding a line
3247
+ * break, a notice opening, a quote, a backslash or a lone surrogate, or
3248
+ * whitespace at its edges) is shown by its id and its server too, which
3249
+ * the channel tools take exactly and the prose resolver doesn't, so no name
3250
+ * shown can be another channel's.
3218
3251
  */
3219
3252
  nameShown(ref) {
3220
3253
  const entry = this.entryAt(ref);
3221
- if (entry)
3222
- return this.nameFor(entry) ?? channelOnServer(ref);
3254
+ if (entry) {
3255
+ const name = this.nameFor(entry);
3256
+ return name !== undefined && showsAsItself(name) ? name : channelOnServer(ref);
3257
+ }
3223
3258
  for (const name of [ref.channelId, qualifiedChannelName(ref)]) {
3224
- if (this.entriesNamed(name).length === 0 && !('channelId' in this.resolveProseTarget(name)))
3259
+ if (showsAsItself(name) && this.entriesNamed(name).length === 0 && !('channelId' in this.resolveProseTarget(name)))
3225
3260
  return name;
3226
3261
  }
3227
3262
  return channelOnServer(ref);
@@ -3258,7 +3293,7 @@ export class ChannelRegistry {
3258
3293
  tuneOutEndText(entry, tool) {
3259
3294
  const close = tool === 'channel_close';
3260
3295
  return this.nameFor(entry) !== undefined
3261
- ? `Call tune_out with channelId ${this.quotedShown(refOf(entry))} and mode "cancel" for it to resume normal ` +
3296
+ ? `Call tune_out with channelId ${quotedValue(this.nameFor(entry))} and mode "cancel" for it to resume normal ` +
3262
3297
  `attention; cancelling delivers the diverted backlog${close ? ', and channel_close closes it after that.' : '.'}`
3263
3298
  : 'No channelId names it alone, so tune_out can\'t cancel it: its tune-out ends by itself when its duration ' +
3264
3299
  'runs out, or at the first wake past its wake budget, and its backlog is delivered then.' +
@@ -3270,7 +3305,7 @@ export class ChannelRegistry {
3270
3305
  */
3271
3306
  quotedShown(ref) {
3272
3307
  const shown = this.nameShown(ref);
3273
- return shown === channelOnServer(ref) ? shown : JSON.stringify(shown);
3308
+ return shown === channelOnServer(ref) ? shown : quotedValue(shown);
3274
3309
  }
3275
3310
  /**
3276
3311
  * The string that names exactly this entry by entriesNamed: its id when no
@@ -3303,7 +3338,7 @@ export class ChannelRegistry {
3303
3338
  const nothing = opts.nothing ?? 'nothing ran';
3304
3339
  const names = entries.map((e) => this.nameFor(e));
3305
3340
  if (names.every((n) => n !== undefined)) {
3306
- const listed = names.map((n) => JSON.stringify(n)).join(', ');
3341
+ const listed = names.map((n) => quotedValue(n)).join(', ');
3307
3342
  const servers = [...new Set(entries.map((e) => JSON.stringify(e.serverId)))];
3308
3343
  // One server's two channels can answer to one name too, as `s`'s
3309
3344
  // `s:room` and `room` both do to `s:room`; its server picks neither.
@@ -3325,8 +3360,8 @@ export class ChannelRegistry {
3325
3360
  if (own === undefined && !opts.takesServerId)
3326
3361
  return `- ${where}: no channelId reaches it alone.`;
3327
3362
  return own !== undefined
3328
- ? `- ${where}: call again with channelId ${JSON.stringify(own)}.`
3329
- : `- ${where}: call again with channelId ${JSON.stringify(e.descriptor.id)} and serverId ${JSON.stringify(e.serverId)}.`;
3363
+ ? `- ${where}: call again with channelId ${quotedValue(own)}.`
3364
+ : `- ${where}: call again with channelId ${quotedValue(e.descriptor.id)} and serverId ${JSON.stringify(e.serverId)}.`;
3330
3365
  });
3331
3366
  const count = entries.length === 2 ? 'either of two channels' : `any of ${entries.length} channels`;
3332
3367
  return `Channel ${JSON.stringify(name)} could mean ${count}, so ${nothing}:\n${lines.join('\n')}`;
@@ -3364,7 +3399,7 @@ export class ChannelRegistry {
3364
3399
  if (!alone)
3365
3400
  return 'channel_list shows the channels and their servers.';
3366
3401
  const one = named[0];
3367
- const names = `${JSON.stringify(channelId)} alone names channel ${JSON.stringify(one.descriptor.id)} on server `
3402
+ const names = `${JSON.stringify(channelId)} alone names channel ${quotedValue(one.descriptor.id)} on server `
3368
3403
  + JSON.stringify(one.serverId);
3369
3404
  return takesServerId ? `${names}: for that one, call again with serverId left out.` : `${names}.`;
3370
3405
  }
@@ -3376,7 +3411,7 @@ export class ChannelRegistry {
3376
3411
  const places = elsewhere.map((e) => {
3377
3412
  const at = `on server ${JSON.stringify(e.serverId)}`;
3378
3413
  const name = this.nameFor(e);
3379
- return name === undefined ? `${at} (no channelId reaches it alone)` : `${at} as ${JSON.stringify(name)}`;
3414
+ return name === undefined ? `${at} (no channelId reaches it alone)` : `${at} as ${quotedValue(name)}`;
3380
3415
  });
3381
3416
  return `It's ${places.length === 1 ? places[0] : `${places.slice(0, -1).join(', ')} and ${places.at(-1)}`}.`;
3382
3417
  }
@@ -3639,19 +3674,22 @@ export class ChannelRegistry {
3639
3674
  * before any other spelling (ownChannelNamed).
3640
3675
  */
3641
3676
  resolveProseTarget(spec, opts) {
3642
- const own = this.ownChannelNamed(spec.trim(), opts?.own);
3677
+ const name = spec.trim();
3678
+ if (name === NO_CHANNEL_EXAMPLE || name === NO_PERSON_EXAMPLE)
3679
+ return { error: `${name} is the example's placeholder: write the name it stands for in its place` };
3680
+ const own = this.ownChannelNamed(name, opts?.own);
3643
3681
  let resolved;
3644
3682
  if (own === undefined) {
3645
- resolved = this.resolveTargetByName(spec);
3683
+ resolved = this.resolveTargetByName(name);
3646
3684
  }
3647
3685
  else if ('neither' in own) {
3648
- resolved = { error: `"${spec.trim()}" is the id of this turn's channels on more than one server`, candidates: this.ownCandidates(own.neither).map((c) => c.text) };
3686
+ resolved = { error: `"${name}" is the id of this turn's channels on more than one server`, candidates: this.ownCandidates(own.neither).map((c) => c.prose) };
3649
3687
  }
3650
3688
  else {
3651
3689
  const entry = this.entryAt(own.channel);
3652
3690
  resolved = entry
3653
3691
  ? { channelId: entry.descriptor.id, serverId: entry.serverId, label: entry.descriptor.label }
3654
- : { error: `${this.ownChannelGoneText(spec.trim(), own.channel)}, and nothing can be sent there until that server registers it again` };
3692
+ : { error: `${this.ownChannelGoneText(name, own.channel)}, and nothing can be sent there until that server registers it again` };
3655
3693
  }
3656
3694
  if (!opts?.forSpeech || 'error' in resolved)
3657
3695
  return resolved;
@@ -3704,7 +3742,7 @@ export class ChannelRegistry {
3704
3742
  return undefined;
3705
3743
  if ('neither' in mine) {
3706
3744
  const done = tool === 'publish' ? 'was posted' : tool === 'deliver' ? 'was delivered' : 'ran';
3707
- return { error: this.ownNeitherRefusal(name, mine.neither, done) };
3745
+ return { error: this.ownNeitherRefusal(name, mine.neither, done, false) };
3708
3746
  }
3709
3747
  return this.entryAt(mine.channel) ? { channel: mine.channel } : { error: this.ownChannelGoneRefusal(name, mine.channel, tool) };
3710
3748
  }
@@ -3721,32 +3759,73 @@ export class ChannelRegistry {
3721
3759
  /**
3722
3760
  * The turn's channels a name means neither of (ownChannelNamed), each by a
3723
3761
  * string that reaches it outside the turn's rule: its server-qualified
3724
- * name, where that picks it alone, since its id is the name refused. One
3725
- * no string picks alone, and one its server no longer registers, is named
3726
- * by its id and server, saying so; `reaches` says which a name reaches.
3762
+ * name, where that picks it alone, since its id is the name refused. `text`
3763
+ * is that name, which a refusal writes as JSON; `prose`, which a `>>`
3764
+ * target reads, is that name where it shows as itself, else its id and
3765
+ * server. One no string picks alone, and one its server no longer
3766
+ * registers, are named by id and server in both, saying so. `reaches` says
3767
+ * which a name reaches.
3727
3768
  */
3728
3769
  ownCandidates(channels) {
3729
3770
  return channels.map((c) => {
3730
3771
  const entry = this.entryAt(c);
3731
- if (!entry)
3732
- return { text: `${channelOnServer(c)} (its server no longer registers it)`, reaches: false };
3772
+ if (!entry) {
3773
+ const text = `${channelOnServer(c)} (its server no longer registers it)`;
3774
+ return { text, prose: text, reaches: false, byServer: false };
3775
+ }
3733
3776
  const qualified = qualifiedChannelName(c);
3734
3777
  const picked = this.entriesNamed(qualified);
3735
- return picked.length === 1 && picked[0] === entry
3736
- ? { text: qualified, reaches: true }
3737
- : { text: `${channelOnServer(c)} (no channelId reaches it alone)`, reaches: false };
3778
+ if (picked.length !== 1 || picked[0] !== entry) {
3779
+ const text = `${channelOnServer(c)} (no channelId reaches it alone)`;
3780
+ return { text, prose: text, reaches: false, byServer: true };
3781
+ }
3782
+ return { text: qualified, prose: showsAsItself(qualified) ? qualified : channelOnServer(c), reaches: true, byServer: false };
3738
3783
  });
3739
3784
  }
3785
+ /**
3786
+ * What a refusal listing these candidates says of each one that is a
3787
+ * channel shown by its id and server: the channelId channel_publish reaches
3788
+ * it by, or that no channelId reaches it alone. That is its server-qualified
3789
+ * name where that picks it alone, since a bare id is what a turn's own
3790
+ * channels answer to first, whether or not their server still registers
3791
+ * them (ownChannelNamed), and else its id where that picks it alone and
3792
+ * isn't empty (channel_publish refuses an empty one). It leaves out a channel no
3793
+ * longer registered and a candidate that already says no channelId reaches
3794
+ * it alone, and is empty when nothing is left.
3795
+ */
3796
+ publishRouteFor(candidates) {
3797
+ const parts = new Set();
3798
+ for (const candidate of candidates) {
3799
+ const shown = CHANNEL_ON_SERVER_AT_END.exec(candidate);
3800
+ if (!shown)
3801
+ continue;
3802
+ const ref = { channelId: JSON.parse(shown[1]), serverId: JSON.parse(shown[2]) };
3803
+ const entry = this.entryAt(ref);
3804
+ if (!entry)
3805
+ continue;
3806
+ const name = [qualifiedChannelName(ref), ref.channelId].find((n) => {
3807
+ const named = n === '' ? [] : this.entriesNamed(n);
3808
+ return named.length === 1 && named[0] === entry;
3809
+ });
3810
+ parts.add(name !== undefined
3811
+ ? `channel_publish reaches ${channelOnServer(ref)} with channelId ${quotedValue(name)}`
3812
+ : `no channelId reaches ${channelOnServer(ref)} alone`);
3813
+ }
3814
+ return parts.size ? ` For a channel shown by its id and server: ${[...parts].join('; ')}.` : '';
3815
+ }
3740
3816
  /** A tool's refusal of a name the turn's channels on more than one server
3741
3817
  * answer to by their id (ownChannelNamed's neither). */
3742
- ownNeitherRefusal(name, channels, done) {
3818
+ ownNeitherRefusal(name, channels, done, takesServer = true) {
3743
3819
  const candidates = this.ownCandidates(channels);
3744
- const named = candidates.filter((c) => c.reaches).map((c) => JSON.stringify(c.text));
3820
+ const named = candidates.filter((c) => c.reaches).map((c) => quotedValue(c.text));
3745
3821
  const pick = named.length === candidates.length ? ' Call again with channelId set to one of them.'
3746
3822
  : named.length === 1 ? ` Call again with channelId ${named[0]}.`
3747
3823
  : named.length > 1 ? ` Call again with channelId set to one of ${named.join(', ')}.` : '';
3748
- const listed = candidates.map((c) => (c.reaches ? JSON.stringify(c.text) : c.text)).join(', ');
3749
- return `"${name}" is the id of this turn's channels on more than one server (${listed}), so nothing ${done}.${pick}`;
3824
+ const listed = candidates.map((c) => (c.reaches ? quotedValue(c.text) : c.text)).join(', ');
3825
+ const byServer = takesServer && candidates.some((c) => c.byServer)
3826
+ ? ' For a channel shown by its id and server, call again with that channelId and that serverId.'
3827
+ : '';
3828
+ return `"${name}" is the id of this turn's channels on more than one server (${listed}), so nothing ${done}.${pick}${byServer}`;
3750
3829
  }
3751
3830
  resolveTargetByName(spec) {
3752
3831
  const entries = [...this.channels.values()];
@@ -3757,7 +3836,7 @@ export class ChannelRegistry {
3757
3836
  // A candidate is named by the token that picks it alone (nameFor), so
3758
3837
  // two servers' channels with one id or one label can be told apart, or,
3759
3838
  // where no string picks it alone, by its id and its server.
3760
- const token = (e) => this.nameFor(e) ?? channelOnServer(refOf(e));
3839
+ const token = (e) => this.nameShown(refOf(e));
3761
3840
  // A DM is shown by its label, the person, where that label, given as a
3762
3841
  // target, picks that DM. It doesn't where another channel answers to that
3763
3842
  // label too, as two servers' DMs with one person do (a label is matched
@@ -3774,7 +3853,7 @@ export class ChannelRegistry {
3774
3853
  };
3775
3854
  const dmShown = (list) => list.map((e) => {
3776
3855
  const label = e.descriptor.label ?? e.descriptor.id;
3777
- return labelPicks(e, label) ? label : token(e);
3856
+ return showsAsItself(label) && labelPicks(e, label) ? label : token(e);
3778
3857
  });
3779
3858
  // A channel id, or its server-qualified name (entriesNamed).
3780
3859
  const exact = this.entriesNamed(trimmed);
@@ -3879,7 +3958,7 @@ export class ChannelRegistry {
3879
3958
  if (byName.length > 1) {
3880
3959
  return {
3881
3960
  error: `"${trimmed}" matches several channels`,
3882
- candidates: byName.map((e) => `${e.descriptor.label} = ${token(e)}`),
3961
+ candidates: byName.map((e) => `${nameToTypeBack(e.descriptor.label ?? '')} = ${token(e)}`),
3883
3962
  };
3884
3963
  }
3885
3964
  // No match: offer near-candidates (labels containing the name) so the
@@ -3887,7 +3966,7 @@ export class ChannelRegistry {
3887
3966
  const near = entries
3888
3967
  .filter((e) => norm(e.descriptor.label ?? '').includes(norm(trimmed)))
3889
3968
  .slice(0, 5)
3890
- .map((e) => `${e.descriptor.label} = ${token(e)}`);
3969
+ .map((e) => `${nameToTypeBack(e.descriptor.label ?? '')} = ${token(e)}`);
3891
3970
  return { error: `no channel matches "${trimmed}"`, ...(near.length ? { candidates: near } : {}) };
3892
3971
  }
3893
3972
  /**
@@ -3924,7 +4003,7 @@ export class ChannelRegistry {
3924
4003
  qualifiedChannelName(ref),
3925
4004
  ];
3926
4005
  for (const c of candidates) {
3927
- if (!isProseTargetToken(c) || c === '#')
4006
+ if (!isProseTargetToken(c) || c === '#' || !showsAsItself(c))
3928
4007
  continue;
3929
4008
  const r = this.resolveProseTarget(c, { forSpeech: true });
3930
4009
  if ('channelId' in r && sameChannel(r, ref))
@@ -4192,7 +4271,7 @@ export class ChannelRegistry {
4192
4271
  channelId: input.channelId,
4193
4272
  serverId: entry.serverId,
4194
4273
  status: alreadyDesiredOpen ? 'reconciled' : 'opened',
4195
- ...(result.history ? { history: result.history } : {}),
4274
+ ...(result.history ? { history: markedHistory(result.history) } : {}),
4196
4275
  ...(result.historyTruncated ? { historyTruncated: true } : {}),
4197
4276
  },
4198
4277
  };
@@ -4649,13 +4728,14 @@ export class ChannelRegistry {
4649
4728
  * tool, so any trailing prose this turn is NOT posted. Replaces the old
4650
4729
  * overloaded use of `think` for staying silent.
4651
4730
  */
4652
- handleToolSkipReply(input) {
4731
+ handleToolSkipReply(input, earlierWake) {
4653
4732
  // The self-wake itself is armed by the framework (which owns the
4654
4733
  // EventGate) BEFORE its dispatch reaches this handler, from the same
4655
4734
  // reading, and the framework refuses a wake the host can't set before
4656
4735
  // dispatching. A caller of this registry's handleChannelToolCall
4657
4736
  // (AgentFramework.channels) goes past both, so its note names a wake
4658
- // nothing armed.
4737
+ // nothing armed. The framework, which also owns the heartbeat, says
4738
+ // which of its wakes is due before the self-wake (`earlierWake`).
4659
4739
  const wake = readSkipReplyWake(input);
4660
4740
  const selfWake = wake.kind === 'wake' ? wake.seconds : undefined;
4661
4741
  return {
@@ -4671,9 +4751,7 @@ export class ChannelRegistry {
4671
4751
  // on Fable: ~60% of skip_reply result bytes were verbatim input echo).
4672
4752
  data: {
4673
4753
  skipped: true,
4674
- note: selfWake !== undefined
4675
- ? `Turn ended; nothing sent. Self-wake in ~${selfWake}s unless something wakes you first.`
4676
- : 'Turn ended; nothing sent.',
4754
+ note: skipReplyNote(selfWake, selfWake !== undefined ? earlierWake : undefined),
4677
4755
  },
4678
4756
  };
4679
4757
  }
@@ -5228,7 +5306,7 @@ export class ChannelRegistry {
5228
5306
  else if (own && 'neither' in own) {
5229
5307
  if (seen)
5230
5308
  seen.refusal = 'shared-id';
5231
- return { success: false, error: this.ownNeitherRefusal(input.channelId, own.neither, 'was posted'), isError: true };
5309
+ return { success: false, error: this.ownNeitherRefusal(input.channelId, own.neither, 'was posted', false), isError: true };
5232
5310
  }
5233
5311
  else if (own) {
5234
5312
  entry = this.entryAt(own.channel);
@@ -5265,7 +5343,7 @@ export class ChannelRegistry {
5265
5343
  // channel with its id isn't where its message came from.
5266
5344
  error: input.channelId === undefined
5267
5345
  ? serverGiven
5268
- ? `Server "${input.serverId}" has no channel "${fallback.channelId}", the id of the channel this call would default to, so nothing was posted.`
5346
+ ? `Server "${input.serverId}" has no channel ${quotedValue(fallback.channelId)}, the id of the channel this call would default to, so nothing was posted.`
5269
5347
  : `The channel this would default to, ${this.quotedShown(fallback)}, isn't registered now, so nothing was posted.`
5270
5348
  // This tool's schema has no serverId, so one given here is a host's
5271
5349
  // ref (publishForAgent), or one an agent added unasked, and the