@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
package/src/agent.ts CHANGED
@@ -1,3 +1,4 @@
1
+ import { NOTICE_TAG } from './notice-tags.js';
1
2
  import { heartbeatToolAlias } from './heartbeat/heartbeat.js';
2
3
  import type { Membrane, NormalizedMessage, NormalizedRequest, ContentBlock, ThinkingBindingEntry, YieldingStream } from '@animalabs/membrane';
3
4
  import { isAbortedResponse } from '@animalabs/membrane';
@@ -320,6 +321,11 @@ export class Agent {
320
321
  /** Provider-specific request parameters forwarded unchanged by Membrane. */
321
322
  readonly providerParams?: Record<string, unknown>;
322
323
  readonly sameRoundThinkTextPolicy: AgentConfig['sameRoundThinkTextPolicy'];
324
+ /** The participant name this agent's turns are presented under (see AgentConfig.presentAs). */
325
+ readonly presentAs: string;
326
+ /** Stored participants presented as this agent's own turns: its name plus AgentConfig.selfParticipants. */
327
+ private readonly ownParticipants: ReadonlySet<string>;
328
+ private readonly deniedAtDispatch: ReadonlySet<string>;
323
329
 
324
330
  private _state: AgentState = { status: 'idle' };
325
331
  private _inferenceStartedAt = 0;
@@ -449,6 +455,9 @@ export class Agent {
449
455
  this.prefillUserMessage = config.prefillUserMessage;
450
456
  this.providerParams = config.providerParams;
451
457
  this.sameRoundThinkTextPolicy = config.sameRoundThinkTextPolicy;
458
+ this.presentAs = config.presentAs ?? config.name;
459
+ this.ownParticipants = new Set([config.name, ...(config.selfParticipants ?? [])]);
460
+ this.deniedAtDispatch = new Set(config.denyToolsAtDispatch ?? []);
452
461
  this.maxStreamTokens = config.maxStreamTokens ?? 150_000;
453
462
  // The window size converted as the projection's comparison (`projected > window`) converts it, so a string such
454
463
  // as "200000" from a JSON or YAML config is its number, for every rule that reads it. A value whose number isn't
@@ -508,6 +517,36 @@ export class Agent {
508
517
  return alias !== undefined && this.allowedTools.includes(alias);
509
518
  }
510
519
 
520
+ /**
521
+ * Whether a call to an advertised tool is refused at dispatch
522
+ * (see AgentConfig.denyToolsAtDispatch).
523
+ */
524
+ deniesAtDispatch(toolName: string): boolean {
525
+ return this.deniedAtDispatch.has(toolName);
526
+ }
527
+
528
+ /** Whether a stored participant is this agent's own voice. */
529
+ isOwnParticipant(participant: string): boolean {
530
+ return this.ownParticipants.has(participant);
531
+ }
532
+
533
+ /**
534
+ * Role assignment for a request: every participant that is this agent's
535
+ * own voice — its name, and for a derived agent the agent it inherited
536
+ * from — is presented under one name, which the formatter maps to the
537
+ * assistant role. Stored authorship is untouched; this shapes the request
538
+ * only. For a plain agent (no selfParticipants, presentAs = name) the
539
+ * input is returned as is.
540
+ */
541
+ private presentOwnTurns(messages: NormalizedMessage[]): NormalizedMessage[] {
542
+ if (this.ownParticipants.size === 1 && this.presentAs === this.name) return messages;
543
+ return messages.map((m) =>
544
+ this.ownParticipants.has(m.participant) && m.participant !== this.presentAs
545
+ ? { ...m, participant: this.presentAs }
546
+ : m,
547
+ );
548
+ }
549
+
511
550
  /**
512
551
  * Check if a source can trigger inference for this agent.
513
552
  */
@@ -1642,8 +1681,9 @@ export class Agent {
1642
1681
  // What it holds of the stored injections is what the agent last saw.
1643
1682
  this.noteRenderedInjections(compiled);
1644
1683
  const { systemInjections } = compiled;
1645
- // The framework's own pricing stamp never goes out (thinking-token-stamp.ts).
1646
- const messages = withoutTokenEstimates(compiled.messages);
1684
+ // The framework's own pricing stamp never goes out (thinking-token-stamp.ts);
1685
+ // the agent's own turns are then presented under its name (presentOwnTurns).
1686
+ const messages = this.presentOwnTurns(withoutTokenEstimates(compiled.messages));
1647
1687
  // Read once, after the first compile, since results may have become
1648
1688
  // ready while it ran; the refusal retry, assembled while the agent is
1649
1689
  // inferring, sends the same ones.
@@ -1655,6 +1695,7 @@ export class Agent {
1655
1695
  system: this.buildSystemPrompt(systemInjections),
1656
1696
  tools: tools.length > 0 ? tools : undefined,
1657
1697
  ...this.requestSettings(),
1698
+ assistantParticipant: this.presentAs,
1658
1699
  };
1659
1700
  };
1660
1701
  const request = await assemble();
@@ -1890,6 +1931,8 @@ export class Agent {
1890
1931
  // The framework's own pricing stamp never goes out (thinking-token-stamp.ts).
1891
1932
  messages = withoutTokenEstimates(messages);
1892
1933
 
1934
+ messages = this.presentOwnTurns(messages);
1935
+
1893
1936
  // Silent heartbeat ticks and replies to a [Continue] turn store no
1894
1937
  // prompt row. Render each request-only turn before its response's first
1895
1938
  // surviving row, so the wire formatter cannot merge the response into the
@@ -1900,7 +1943,7 @@ export class Agent {
1900
1943
  // renders. "First" is per agent: a broadcast tick shares its eventId with
1901
1944
  // every resident.
1902
1945
  const index = this.indexRequestOnlyRows();
1903
- const rendered = renderRequestOnlyRows(messages, index, this.name);
1946
+ const rendered = renderRequestOnlyRows(messages, index, this.presentAs);
1904
1947
  messages = rendered.messages;
1905
1948
  // A tick opens wherever its separator isn't already rendered: on its first
1906
1949
  // request, and on a restart whose compile left none of the tick's rows,
@@ -1919,7 +1962,7 @@ export class Agent {
1919
1962
  let continuePrompt: RequestOnlyPrompt | undefined;
1920
1963
  if (openingTick) {
1921
1964
  messages = [...messages, silentHeartbeatSeparatorTurn()];
1922
- } else if (endsOnOwnMessage(messages, this.name, injectionTail)) {
1965
+ } else if (endsOnOwnMessage(messages, this.presentAs, injectionTail)) {
1923
1966
  // Safety: ensure messages don't end with an assistant message.
1924
1967
  // Some models reject trailing assistant messages ("prefill not supported"),
1925
1968
  // and after context compression a stale assistant turn can end up last.
@@ -1969,7 +2012,7 @@ export class Agent {
1969
2012
  cacheTtl: this.cacheTtl,
1970
2013
  ...(this.prefillUserMessage && { prefillUserMessage: this.prefillUserMessage }),
1971
2014
  ...(this.providerParams && { providerParams: this.providerParams }),
1972
- assistantParticipant: this.name,
2015
+ assistantParticipant: this.presentAs,
1973
2016
  };
1974
2017
  }
1975
2018
 
@@ -2062,7 +2105,7 @@ export class Agent {
2062
2105
  try {
2063
2106
  const stored = cm.getAllMessages() as unknown as readonly StoredRowLike[];
2064
2107
  if (this.requestOnlyIndex?.source === stored) return this.requestOnlyIndex.index;
2065
- const index = indexRequestOnlyRows(stored, this.name);
2108
+ const index = indexRequestOnlyRows(stored, this.ownParticipants);
2066
2109
  this.requestOnlyIndex = { source: stored, index };
2067
2110
  return index;
2068
2111
  } catch (error) {
@@ -2398,7 +2441,9 @@ export class Agent {
2398
2441
  }
2399
2442
 
2400
2443
  const injectedText = systemInjections.map((block) =>
2401
- block.type === 'text' ? block.text : `[${block.type} block not shown: the system prompt carries text only.]`);
2444
+ block.type === 'text'
2445
+ ? block.text
2446
+ : `[${NOTICE_TAG.blockNotShown}: ${/^[aeiou]/i.test(block.type) ? 'an' : 'a'} ${block.type} block; the system prompt carries text only.]`);
2402
2447
 
2403
2448
  return this.systemPrompt + '\n' + injectedText.join('\n');
2404
2449
  }
@@ -12,7 +12,7 @@
12
12
  */
13
13
 
14
14
  import WebSocket from 'ws';
15
- import { createInterface } from 'node:readline';
15
+ import { escapeLineSeparators, readLines } from '../line-reader.js';
16
16
  import type { ApiRequest, ApiResponse, ApiEvent } from './types.js';
17
17
 
18
18
  // ============================================================================
@@ -86,18 +86,7 @@ export class McpServer {
86
86
  // Connect to WebSocket API
87
87
  await this.connectWs();
88
88
 
89
- // Set up stdio interface
90
- const rl = createInterface({
91
- input: process.stdin,
92
- output: process.stdout,
93
- terminal: false,
94
- });
95
-
96
- rl.on('line', (line) => {
97
- this.handleInput(line);
98
- });
99
-
100
- rl.on('close', () => {
89
+ readLines(process.stdin, (line) => this.handleInput(line), () => {
101
90
  this.close();
102
91
  process.exit(0);
103
92
  });
@@ -667,7 +656,7 @@ export class McpServer {
667
656
  }
668
657
 
669
658
  private send(message: JsonRpcResponse | JsonRpcNotification): void {
670
- process.stdout.write(JSON.stringify(message) + '\n');
659
+ process.stdout.write(escapeLineSeparators(JSON.stringify(message)) + '\n');
671
660
  }
672
661
 
673
662
  close(): void {
package/src/api/server.ts CHANGED
@@ -489,9 +489,13 @@ export class ApiServer {
489
489
  if (!params.participant || !params.content) {
490
490
  throw new Error('participant and content are required');
491
491
  }
492
- // ApiModule stores the message as one text block, which context-manager
493
- // refuses unless its text is a string, so content that isn't text is
494
- // refused here, where the caller hears it, rather than at dispatch.
492
+ // ApiModule stores the participant as a name, and the message as one text
493
+ // block, which context-manager refuses unless its text is a string. So a
494
+ // participant or content that isn't text is refused here, where the
495
+ // caller hears it, rather than at dispatch.
496
+ if (typeof params.participant !== 'string') {
497
+ throw new Error('participant must be text');
498
+ }
495
499
  if (typeof params.content !== 'string') {
496
500
  throw new Error('content must be text');
497
501
  }
@@ -0,0 +1,81 @@
1
+ /**
2
+ * The author of a channel message, presented to the model.
3
+ *
4
+ * MCPL `channels/incoming` carries the author as a field, `author: {id,
5
+ * name}`, and the body alone as content (SPEC §14); the framework stores
6
+ * the message under the anonymous participant `user` with the author in
7
+ * `metadata.author`. The wire formatter writes the participant, so without
8
+ * this step the model reads `user: <text>` and who spoke is lost. Servers
9
+ * that pre-render `Name: body` into the text themselves (portal-mcpl) are
10
+ * left as they are: the rule is idempotent, so the name appears once
11
+ * whichever shape a server sends.
12
+ *
13
+ * Applied where the framework stores a `channels/incoming` message — the
14
+ * compiled view carries no metadata, so the name has to be in the stored
15
+ * text to reach the wire — and to a tune-out backlog line, which reads the
16
+ * metadata and must not name an author twice. The author stays in
17
+ * `metadata.author` as the structured record.
18
+ */
19
+ import type { ContentBlock, MessageMetadata } from '@animalabs/membrane';
20
+ import { suppliedValue } from './supplied-text.js';
21
+
22
+ /** The participant the framework stores channel traffic under: a role, not a name. */
23
+ export const ANONYMOUS_PARTICIPANT = 'user';
24
+
25
+ type TextBlock = Extract<ContentBlock, { type: 'text' }>;
26
+
27
+ /** The author a stored message names, when it names one. */
28
+ export function authorNamed(metadata: Record<string, unknown> | MessageMetadata | undefined): string | undefined {
29
+ const name = (metadata?.author as { name?: unknown } | undefined)?.name;
30
+ return typeof name === 'string' && name.length > 0 ? name : undefined;
31
+ }
32
+
33
+ /** Whether `text` already begins with the author's own line: `Name:` then a space or the end. */
34
+ export function beginsWithAuthor(author: string, text: string): boolean {
35
+ if (!text.startsWith(author) || text.charAt(author.length) !== ':') return false;
36
+ const after = text.charAt(author.length + 1);
37
+ return after === '' || after === ' ' || after === '\n';
38
+ }
39
+
40
+ /**
41
+ * The author's name as a line of the framework's writes it: a value someone
42
+ * supplied, kept to one line with its notice openings marked (main's #852,
43
+ * suppliedValue), so a name can't open a line that reads as the framework's
44
+ * own notice.
45
+ */
46
+ export function authorShown(author: string): string {
47
+ return suppliedValue(author);
48
+ }
49
+
50
+ /** Whether `text` begins with the author's line, as the author wrote their
51
+ * name or as the framework shows it (authorShown). */
52
+ export function beginsWithAuthorLine(author: string, text: string): boolean {
53
+ return beginsWithAuthor(author, text) || beginsWithAuthor(authorShown(author), text);
54
+ }
55
+
56
+ /** `author: text`, unless `text` begins that way already. */
57
+ export function authorLine(author: string, text: string): string {
58
+ return beginsWithAuthorLine(author, text) ? text : `${authorShown(author)}: ${text}`;
59
+ }
60
+
61
+ /**
62
+ * A message stored under the anonymous participant that names an author is
63
+ * presented with the author before its first text block — or, with no text
64
+ * block, as a leading `Name:` line before its media. Any other message, and
65
+ * one whose text already begins with the author's line, is returned as is.
66
+ * Never mutates its input.
67
+ */
68
+ export function presentAuthor<M extends { participant: string; content: ContentBlock[]; metadata?: MessageMetadata | Record<string, unknown> }>(message: M): M {
69
+ if (message.participant !== ANONYMOUS_PARTICIPANT) return message;
70
+ const author = authorNamed(message.metadata);
71
+ if (author === undefined) return message;
72
+ const index = message.content.findIndex((b) => b.type === 'text');
73
+ if (index === -1) {
74
+ return { ...message, content: [{ type: 'text', text: `${authorShown(author)}:` } as TextBlock, ...message.content] };
75
+ }
76
+ const block = message.content[index] as TextBlock;
77
+ if (typeof block.text !== 'string' || beginsWithAuthorLine(author, block.text)) return message;
78
+ const content = [...message.content];
79
+ content[index] = { ...block, text: `${authorShown(author)}: ${block.text}` };
80
+ return { ...message, content };
81
+ }
@@ -5,6 +5,8 @@
5
5
  * row the next turn reads.
6
6
  */
7
7
 
8
+ import { NOTICE_TAG } from './notice-tags.js';
9
+
8
10
  /** The reason on a continuation's wake (InferenceRequest.reason). */
9
11
  export const AUTO_CONTINUE_REASON = 'auto-continue';
10
12
 
@@ -33,11 +35,15 @@ export function autoContinueNotice(opts: {
33
35
  stop: string;
34
36
  last: boolean;
35
37
  heldAfterSend?: boolean;
38
+ /** The run began with a heartbeat's turn, whose plain speech is held as drafts. */
39
+ heartbeat?: boolean;
36
40
  }): string {
37
- return `[continue] ${opts.agentName}, another turn starts now.` +
41
+ return `[${NOTICE_TAG.continue}] ${opts.agentName}, another turn starts now.` +
38
42
  (opts.heldAfterSend
39
43
  ? ' Some or all of your closing words last turn were held, not sent: a send that turn may already have posted where they were going.'
40
44
  : '') +
41
- ` If you're done, ${opts.stop}; anything you write without it is sent as usual.` +
45
+ (opts.heartbeat
46
+ ? ` If you're done, ${opts.stop}; plain words you write without it and with no destination are held as drafts, as on the heartbeat's turn, and an explicit send reaches a channel.`
47
+ : ` If you're done, ${opts.stop}; anything you write without it is sent as usual.`) +
42
48
  (opts.last ? ' This is the last continuation in a row: after this turn you wait for the next event either way.' : '');
43
49
  }
@@ -4,11 +4,13 @@
4
4
  * which ends with it, so nothing a script held stays after it.
5
5
  *
6
6
  * Every frame from the child is decoded and checked before the parent acts on
7
- * it, since the child is untrusted. A frame that breaks the protocol, a child
8
- * that exits without saying how the script ended, or one that doesn't stop
9
- * when asked, ends the run as crashed. A tool call goes to `onToolCall`, the
10
- * bridge to the framework's dispatcher, and its outcome goes into the run's
11
- * call record at dispatch, as running until it's answered.
7
+ * it, since the child is untrusted. A frame that breaks the protocol, or a
8
+ * child that exits without saying how the script ended when no stop was
9
+ * asked, ends the run as crashed. One that doesn't stop when asked is ended
10
+ * once the stop's grace has passed, and the run ends stopped, marked short.
11
+ * A tool call goes to `onToolCall`, the bridge to the framework's dispatcher,
12
+ * and its outcome goes into the run's call record at dispatch, as running
13
+ * until it's answered.
12
14
  *
13
15
  * What the host holds for a script is bounded: at most MAX_CALLS_IN_FLIGHT of
14
16
  * its calls wait for answers at once, and at most MAX_CALLS_IN_FLIGHT are out
@@ -26,6 +26,7 @@
26
26
  * dispatcher a script's calls go through, and how to wake an agent.
27
27
  */
28
28
 
29
+ import { NOTICE_TAG } from '../../notice-tags.js';
29
30
  import { ChildCapacity, ScriptRun, type CallAnswer, type Ending, type ScriptRunOptions, type ToolAnswer } from './runner.js';
30
31
  import type { CallSummary } from './calls.js';
31
32
  import type { OutputBounds, OutputWindow } from './output.js';
@@ -478,10 +479,10 @@ function leftEndingText(scriptId: string, ending: Ending): string {
478
479
  : ending.how === 'stopped'
479
480
  ? ending.why ? `was stopped: ${ending.why}` : 'was stopped at its limit'
480
481
  : 'ended';
481
- return `[script ${scriptId}] Your script ${how}. {"action": "wait", "script_id": "${scriptId}"} gives its result.`;
482
+ return `[${NOTICE_TAG.script} ${scriptId}] Your script ${how}. {"action": "wait", "script_id": "${scriptId}"} gives its result.`;
482
483
  }
483
484
 
484
485
  function wakeText(scriptId: string, payloadJson: string, n: number, max: number, ageMs: number): string {
485
486
  const minutes = Math.round(ageMs / 60_000);
486
- return `[script ${scriptId}] Woke you with wake_agent(), ${minutes} min after it started (wake ${n} of ${max}): ${payloadJson}`;
487
+ return `[${NOTICE_TAG.script} ${scriptId}] Woke you with wake_agent(), ${minutes} min after it started (wake ${n} of ${max}): ${payloadJson}`;
487
488
  }
@@ -0,0 +1,32 @@
1
+ export { AgentRegistry, AgentSpecError } from './registry.js';
2
+ export type { AgentRegistryOptions, RestoreOutcome, ReconcileOutcome } from './registry.js';
3
+ export {
4
+ residentSpec,
5
+ subconsciousSpec,
6
+ conversationForkSpec,
7
+ workerSpec,
8
+ taskForkSpec,
9
+ DEFAULT_TASK_IDLE_TIMEOUT_MS,
10
+ } from './presets.js';
11
+ export type { BoundedJobOptions } from './presets.js';
12
+ export type {
13
+ AgentName,
14
+ AgentKind,
15
+ AgentRoles,
16
+ AgentLifetime,
17
+ ActivationBounds,
18
+ ParentEndPolicy,
19
+ ResultRoute,
20
+ ObserveEdge,
21
+ AgentRelationships,
22
+ ContextInheritance,
23
+ AgentSpec,
24
+ AgentEndReason,
25
+ AgentRef,
26
+ ActivationRecord,
27
+ AgentRecord,
28
+ HeldMail,
29
+ RegistrySnapshot,
30
+ EndOutcome,
31
+ PolicyEpoch,
32
+ } from './types.js';
@@ -0,0 +1,149 @@
1
+ /**
2
+ * Presets — the historical agent kinds expressed as {@link AgentSpec}
3
+ * values. They are configurations of the common machinery, not engines:
4
+ * everything a preset decides is visible in the spec it returns, and a host
5
+ * may register a spec that matches none of them.
6
+ */
7
+
8
+ import type { AgentName, AgentSpec, ContextInheritance, ResultRoute } from './types.js';
9
+
10
+ /** Default idle bound for a bounded job with no explicit deadline (matches the run watchdog). */
11
+ export const DEFAULT_TASK_IDLE_TIMEOUT_MS = 15 * 60_000;
12
+
13
+ /**
14
+ * A resident: persistent, declared in configuration, reads the residents'
15
+ * shared message slot. The first one declared owns default delivery.
16
+ */
17
+ export function residentSpec(name: AgentName, options: { primary: boolean }): AgentSpec {
18
+ return {
19
+ name,
20
+ kind: 'resident',
21
+ roles: {
22
+ defaultDelivery: options.primary,
23
+ receivesUntargeted: true,
24
+ receivesGateWakes: true,
25
+ ownsProviderScheduling: true,
26
+ },
27
+ lifetime: { kind: 'persistent' },
28
+ readsSharedSlot: true,
29
+ namespace: `agents/${name}`,
30
+ };
31
+ }
32
+
33
+ /**
34
+ * The subconscious: a persistent attention tenant of the resident it
35
+ * serves. It persists across policy epochs (cancelling a tune-out ends a
36
+ * channel's cadence, not this agent), observes the residents' slot with
37
+ * held traffic included, is never a broadcast recipient, and ends with its
38
+ * resident.
39
+ */
40
+ export function subconsciousSpec(name: AgentName, serves: AgentName): AgentSpec {
41
+ return {
42
+ name,
43
+ kind: 'subconscious',
44
+ roles: {
45
+ defaultDelivery: false,
46
+ receivesUntargeted: false,
47
+ receivesGateWakes: false,
48
+ ownsProviderScheduling: true,
49
+ },
50
+ lifetime: { kind: 'persistent' },
51
+ spawnedBy: serves,
52
+ onParentEnd: 'end',
53
+ observes: [{ includeHeld: true }],
54
+ resultTo: { to: serves, as: 'message' },
55
+ readsSharedSlot: true,
56
+ namespace: `subconscious/${serves}`,
57
+ };
58
+ }
59
+
60
+ /**
61
+ * A conversation fork: derived from a template agent's context at bind
62
+ * time, bound to one channel, independent continuation, ends on idle.
63
+ */
64
+ export function conversationForkSpec(
65
+ name: AgentName,
66
+ options: {
67
+ template: AgentName;
68
+ /** The channel it's bound to, on its server. */
69
+ homeChannel: { serverId: string; channelId: string };
70
+ idleTtlMs: number;
71
+ inherit?: Partial<ContextInheritance>;
72
+ },
73
+ ): AgentSpec {
74
+ return {
75
+ name,
76
+ kind: 'conversation-fork',
77
+ roles: {
78
+ defaultDelivery: false,
79
+ receivesUntargeted: false,
80
+ // Woken by its own channel's debounced traffic through the host-wide gate.
81
+ receivesGateWakes: true,
82
+ ownsProviderScheduling: false,
83
+ },
84
+ lifetime: { kind: 'idle', idleTtlMs: options.idleTtlMs },
85
+ spawnedBy: options.template,
86
+ onParentEnd: 'orphan',
87
+ inherit: { from: options.template, mode: 'copy', ...options.inherit },
88
+ // A copy renames the template's turns as it copies them. A shared
89
+ // inheritance has no copy step, so the mapping is declared instead.
90
+ ...(options.inherit?.mode === 'shared' ? { selfParticipants: [options.template] } : {}),
91
+ homeChannel: { serverId: options.homeChannel.serverId, channelId: options.homeChannel.channelId },
92
+ namespace: `conversations/${name}`,
93
+ };
94
+ }
95
+
96
+ export interface BoundedJobOptions {
97
+ /** Who created it. Absent for an independent job driven by the host. */
98
+ spawnedBy?: AgentName;
99
+ /** Where its completion returns. */
100
+ resultTo?: ResultRoute;
101
+ deadlineMs?: number;
102
+ idleTimeoutMs?: number;
103
+ maxTurns?: number;
104
+ namespace?: string;
105
+ metadata?: Record<string, unknown>;
106
+ }
107
+
108
+ function boundedJob(name: AgentName, options: BoundedJobOptions): Omit<AgentSpec, 'kind'> {
109
+ return {
110
+ name,
111
+ roles: {
112
+ defaultDelivery: false,
113
+ receivesUntargeted: false,
114
+ receivesGateWakes: false,
115
+ ownsProviderScheduling: false,
116
+ },
117
+ lifetime: {
118
+ kind: 'task',
119
+ ...(options.deadlineMs !== undefined ? { deadlineMs: options.deadlineMs } : {}),
120
+ idleTimeoutMs: options.idleTimeoutMs ?? DEFAULT_TASK_IDLE_TIMEOUT_MS,
121
+ ...(options.maxTurns !== undefined ? { maxTurns: options.maxTurns } : {}),
122
+ },
123
+ ...(options.spawnedBy ? { spawnedBy: options.spawnedBy, onParentEnd: 'orphan' as const } : {}),
124
+ ...(options.resultTo ? { resultTo: options.resultTo } : {}),
125
+ namespace: options.namespace ?? `subagent/${name}`,
126
+ ...(options.metadata ? { metadata: options.metadata } : {}),
127
+ };
128
+ }
129
+
130
+ /** A fresh worker: new identity and context, one bounded job, a result route. */
131
+ export function workerSpec(name: AgentName, options: BoundedJobOptions = {}): AgentSpec {
132
+ return { ...boundedJob(name, options), kind: 'worker' };
133
+ }
134
+
135
+ /**
136
+ * A task fork: the parent's context at a checkpoint, a private
137
+ * continuation, one bounded job, a return.
138
+ */
139
+ export function taskForkSpec(
140
+ name: AgentName,
141
+ options: BoundedJobOptions & { parent: AgentName; inherit?: Partial<ContextInheritance> },
142
+ ): AgentSpec {
143
+ return {
144
+ ...boundedJob(name, { ...options, spawnedBy: options.spawnedBy ?? options.parent }),
145
+ kind: 'task-fork',
146
+ inherit: { from: options.parent, mode: 'copy', ...options.inherit },
147
+ ...(options.inherit?.mode === 'shared' ? { selfParticipants: [options.parent] } : {}),
148
+ };
149
+ }