@animalabs/agent-framework 0.29.0 → 0.31.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 (236) hide show
  1. package/README.md +71 -8
  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 +50 -9
  7. package/dist/src/agent.d.ts.map +1 -1
  8. package/dist/src/agent.js +125 -23
  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 +10 -3
  12. package/dist/src/api/mcp-server-core.js.map +1 -1
  13. package/dist/src/author-presentation.d.ts +50 -0
  14. package/dist/src/author-presentation.d.ts.map +1 -0
  15. package/dist/src/author-presentation.js +58 -0
  16. package/dist/src/author-presentation.js.map +1 -0
  17. package/dist/src/code-execution/isolate/render.d.ts.map +1 -1
  18. package/dist/src/code-execution/isolate/render.js +7 -6
  19. package/dist/src/code-execution/isolate/render.js.map +1 -1
  20. package/dist/src/code-execution/isolate/scripts.d.ts.map +1 -1
  21. package/dist/src/code-execution/isolate/scripts.js +5 -4
  22. package/dist/src/code-execution/isolate/scripts.js.map +1 -1
  23. package/dist/src/dendrite/index.d.ts +6 -0
  24. package/dist/src/dendrite/index.d.ts.map +1 -0
  25. package/dist/src/dendrite/index.js +3 -0
  26. package/dist/src/dendrite/index.js.map +1 -0
  27. package/dist/src/dendrite/presets.d.ts +60 -0
  28. package/dist/src/dendrite/presets.d.ts.map +1 -0
  29. package/dist/src/dendrite/presets.js +117 -0
  30. package/dist/src/dendrite/presets.js.map +1 -0
  31. package/dist/src/dendrite/registry.d.ts +260 -0
  32. package/dist/src/dendrite/registry.d.ts.map +1 -0
  33. package/dist/src/dendrite/registry.js +742 -0
  34. package/dist/src/dendrite/registry.js.map +1 -0
  35. package/dist/src/dendrite/types.d.ts +392 -0
  36. package/dist/src/dendrite/types.d.ts.map +1 -0
  37. package/dist/src/dendrite/types.js +24 -0
  38. package/dist/src/dendrite/types.js.map +1 -0
  39. package/dist/src/framework.d.ts +806 -118
  40. package/dist/src/framework.d.ts.map +1 -1
  41. package/dist/src/framework.js +4180 -1121
  42. package/dist/src/framework.js.map +1 -1
  43. package/dist/src/gate/event-gate.d.ts +36 -21
  44. package/dist/src/gate/event-gate.d.ts.map +1 -1
  45. package/dist/src/gate/event-gate.js +223 -149
  46. package/dist/src/gate/event-gate.js.map +1 -1
  47. package/dist/src/gate/recipe-reconcile.d.ts.map +1 -1
  48. package/dist/src/gate/recipe-reconcile.js +6 -5
  49. package/dist/src/gate/recipe-reconcile.js.map +1 -1
  50. package/dist/src/host-presentation.d.ts +4 -0
  51. package/dist/src/host-presentation.d.ts.map +1 -1
  52. package/dist/src/host-presentation.js +12 -10
  53. package/dist/src/host-presentation.js.map +1 -1
  54. package/dist/src/index.d.ts +6 -2
  55. package/dist/src/index.d.ts.map +1 -1
  56. package/dist/src/index.js +7 -1
  57. package/dist/src/index.js.map +1 -1
  58. package/dist/src/inference-bounds.d.ts +37 -0
  59. package/dist/src/inference-bounds.d.ts.map +1 -0
  60. package/dist/src/inference-bounds.js +84 -0
  61. package/dist/src/inference-bounds.js.map +1 -0
  62. package/dist/src/line-reader.d.ts +7 -1
  63. package/dist/src/line-reader.d.ts.map +1 -1
  64. package/dist/src/line-reader.js +17 -6
  65. package/dist/src/line-reader.js.map +1 -1
  66. package/dist/src/mcpl/channel-registry.d.ts +84 -9
  67. package/dist/src/mcpl/channel-registry.d.ts.map +1 -1
  68. package/dist/src/mcpl/channel-registry.js +136 -58
  69. package/dist/src/mcpl/channel-registry.js.map +1 -1
  70. package/dist/src/mcpl/errors.d.ts +1 -0
  71. package/dist/src/mcpl/errors.d.ts.map +1 -1
  72. package/dist/src/mcpl/errors.js +3 -0
  73. package/dist/src/mcpl/errors.js.map +1 -1
  74. package/dist/src/mcpl/inbound-source.d.ts +2 -2
  75. package/dist/src/mcpl/inbound-source.js +2 -2
  76. package/dist/src/mcpl/inference-router.d.ts +11 -1
  77. package/dist/src/mcpl/inference-router.d.ts.map +1 -1
  78. package/dist/src/mcpl/inference-router.js +65 -9
  79. package/dist/src/mcpl/inference-router.js.map +1 -1
  80. package/dist/src/mcpl/modern-connection.d.ts +10 -7
  81. package/dist/src/mcpl/modern-connection.d.ts.map +1 -1
  82. package/dist/src/mcpl/modern-connection.js +85 -74
  83. package/dist/src/mcpl/modern-connection.js.map +1 -1
  84. package/dist/src/mcpl/protocol-family.d.ts +1 -1
  85. package/dist/src/mcpl/protocol-family.d.ts.map +1 -1
  86. package/dist/src/mcpl/protocol-family.js +45 -3
  87. package/dist/src/mcpl/protocol-family.js.map +1 -1
  88. package/dist/src/mcpl/push-coalescer.d.ts +11 -7
  89. package/dist/src/mcpl/push-coalescer.d.ts.map +1 -1
  90. package/dist/src/mcpl/push-coalescer.js +21 -0
  91. package/dist/src/mcpl/push-coalescer.js.map +1 -1
  92. package/dist/src/mcpl/references.d.ts +1 -2
  93. package/dist/src/mcpl/references.d.ts.map +1 -1
  94. package/dist/src/mcpl/references.js +3 -3
  95. package/dist/src/mcpl/references.js.map +1 -1
  96. package/dist/src/mcpl/server-connection.d.ts +14 -0
  97. package/dist/src/mcpl/server-connection.d.ts.map +1 -1
  98. package/dist/src/mcpl/server-connection.js +26 -22
  99. package/dist/src/mcpl/server-connection.js.map +1 -1
  100. package/dist/src/mcpl/tool-signature.d.ts +1 -0
  101. package/dist/src/mcpl/tool-signature.d.ts.map +1 -1
  102. package/dist/src/mcpl/tool-signature.js +16 -11
  103. package/dist/src/mcpl/tool-signature.js.map +1 -1
  104. package/dist/src/mcpl/toolsets-module.d.ts.map +1 -1
  105. package/dist/src/mcpl/toolsets-module.js +7 -0
  106. package/dist/src/mcpl/toolsets-module.js.map +1 -1
  107. package/dist/src/mcpl/transport.d.ts +44 -21
  108. package/dist/src/mcpl/transport.d.ts.map +1 -1
  109. package/dist/src/mcpl/transport.js +283 -64
  110. package/dist/src/mcpl/transport.js.map +1 -1
  111. package/dist/src/mcpl/types.d.ts +17 -1
  112. package/dist/src/mcpl/types.d.ts.map +1 -1
  113. package/dist/src/mcpl/types.js.map +1 -1
  114. package/dist/src/module-registry.d.ts +32 -4
  115. package/dist/src/module-registry.d.ts.map +1 -1
  116. package/dist/src/module-registry.js +189 -25
  117. package/dist/src/module-registry.js.map +1 -1
  118. package/dist/src/modules/discord/discord-js-client.d.ts.map +1 -1
  119. package/dist/src/modules/discord/discord-js-client.js +24 -16
  120. package/dist/src/modules/discord/discord-js-client.js.map +1 -1
  121. package/dist/src/modules/sender-name.d.ts +5 -4
  122. package/dist/src/modules/sender-name.d.ts.map +1 -1
  123. package/dist/src/modules/sender-name.js +7 -5
  124. package/dist/src/modules/sender-name.js.map +1 -1
  125. package/dist/src/modules/workspace/disk-agreement.d.ts +16 -8
  126. package/dist/src/modules/workspace/disk-agreement.d.ts.map +1 -1
  127. package/dist/src/modules/workspace/disk-agreement.js +33 -12
  128. package/dist/src/modules/workspace/disk-agreement.js.map +1 -1
  129. package/dist/src/modules/workspace/index.d.ts +6 -3
  130. package/dist/src/modules/workspace/index.d.ts.map +1 -1
  131. package/dist/src/modules/workspace/index.js +61 -15
  132. package/dist/src/modules/workspace/index.js.map +1 -1
  133. package/dist/src/modules/workspace/observe.d.ts +9 -1
  134. package/dist/src/modules/workspace/observe.d.ts.map +1 -1
  135. package/dist/src/modules/workspace/observe.js +50 -10
  136. package/dist/src/modules/workspace/observe.js.map +1 -1
  137. package/dist/src/modules/workspace/reconcile.d.ts +11 -1
  138. package/dist/src/modules/workspace/reconcile.d.ts.map +1 -1
  139. package/dist/src/modules/workspace/reconcile.js +19 -8
  140. package/dist/src/modules/workspace/reconcile.js.map +1 -1
  141. package/dist/src/modules/workspace/types.d.ts +8 -0
  142. package/dist/src/modules/workspace/types.d.ts.map +1 -1
  143. package/dist/src/modules/workspace/types.js.map +1 -1
  144. package/dist/src/modules/workspace/watcher.d.ts +7 -0
  145. package/dist/src/modules/workspace/watcher.d.ts.map +1 -1
  146. package/dist/src/modules/workspace/watcher.js +7 -5
  147. package/dist/src/modules/workspace/watcher.js.map +1 -1
  148. package/dist/src/notice-tags.d.ts +2 -0
  149. package/dist/src/notice-tags.d.ts.map +1 -1
  150. package/dist/src/notice-tags.js +2 -0
  151. package/dist/src/notice-tags.js.map +1 -1
  152. package/dist/src/recovery/discord-awareness-outbox.d.ts.map +1 -1
  153. package/dist/src/recovery/discord-awareness-outbox.js +1 -9
  154. package/dist/src/recovery/discord-awareness-outbox.js.map +1 -1
  155. package/dist/src/silent-heartbeat.d.ts +4 -2
  156. package/dist/src/silent-heartbeat.d.ts.map +1 -1
  157. package/dist/src/silent-heartbeat.js +7 -4
  158. package/dist/src/silent-heartbeat.js.map +1 -1
  159. package/dist/src/stored-injections.d.ts +4 -0
  160. package/dist/src/stored-injections.d.ts.map +1 -1
  161. package/dist/src/stored-injections.js +6 -0
  162. package/dist/src/stored-injections.js.map +1 -1
  163. package/dist/src/supplied-text.d.ts +2 -0
  164. package/dist/src/supplied-text.d.ts.map +1 -1
  165. package/dist/src/supplied-text.js +16 -7
  166. package/dist/src/supplied-text.js.map +1 -1
  167. package/dist/src/tool-result-guard.js +1 -1
  168. package/dist/src/tool-result-guard.js.map +1 -1
  169. package/dist/src/tune-out/coordinator.d.ts +368 -16
  170. package/dist/src/tune-out/coordinator.d.ts.map +1 -1
  171. package/dist/src/tune-out/coordinator.js +1224 -136
  172. package/dist/src/tune-out/coordinator.js.map +1 -1
  173. package/dist/src/tune-out/tools.d.ts +115 -7
  174. package/dist/src/tune-out/tools.d.ts.map +1 -1
  175. package/dist/src/tune-out/tools.js +55 -18
  176. package/dist/src/tune-out/tools.js.map +1 -1
  177. package/dist/src/types/agent.d.ts +27 -1
  178. package/dist/src/types/agent.d.ts.map +1 -1
  179. package/dist/src/types/framework.d.ts +153 -15
  180. package/dist/src/types/framework.d.ts.map +1 -1
  181. package/dist/src/types/module.d.ts +42 -3
  182. package/dist/src/types/module.d.ts.map +1 -1
  183. package/dist/src/types/trace.d.ts +140 -3
  184. package/dist/src/types/trace.d.ts.map +1 -1
  185. package/package.json +4 -4
  186. package/src/admission/admit.ts +113 -0
  187. package/src/agent.ts +138 -31
  188. package/src/api/mcp-server-core.ts +10 -3
  189. package/src/author-presentation.ts +81 -0
  190. package/src/code-execution/isolate/render.ts +7 -6
  191. package/src/code-execution/isolate/scripts.ts +5 -4
  192. package/src/dendrite/index.ts +32 -0
  193. package/src/dendrite/presets.ts +149 -0
  194. package/src/dendrite/registry.ts +808 -0
  195. package/src/dendrite/types.ts +397 -0
  196. package/src/framework.ts +4344 -1052
  197. package/src/gate/event-gate.ts +251 -150
  198. package/src/gate/recipe-reconcile.ts +7 -5
  199. package/src/host-presentation.ts +14 -10
  200. package/src/index.ts +16 -2
  201. package/src/inference-bounds.ts +111 -0
  202. package/src/line-reader.ts +22 -5
  203. package/src/mcpl/channel-registry.ts +170 -60
  204. package/src/mcpl/errors.ts +4 -0
  205. package/src/mcpl/inbound-source.ts +2 -2
  206. package/src/mcpl/inference-router.ts +67 -10
  207. package/src/mcpl/modern-connection.ts +83 -68
  208. package/src/mcpl/protocol-family.ts +37 -4
  209. package/src/mcpl/push-coalescer.ts +21 -7
  210. package/src/mcpl/references.ts +3 -4
  211. package/src/mcpl/server-connection.ts +30 -21
  212. package/src/mcpl/tool-signature.ts +18 -11
  213. package/src/mcpl/toolsets-module.ts +7 -0
  214. package/src/mcpl/transport.ts +261 -60
  215. package/src/mcpl/types.ts +18 -1
  216. package/src/module-registry.ts +178 -25
  217. package/src/modules/discord/discord-js-client.ts +33 -29
  218. package/src/modules/sender-name.ts +8 -5
  219. package/src/modules/workspace/disk-agreement.ts +39 -15
  220. package/src/modules/workspace/index.ts +65 -14
  221. package/src/modules/workspace/observe.ts +51 -10
  222. package/src/modules/workspace/reconcile.ts +22 -8
  223. package/src/modules/workspace/types.ts +5 -0
  224. package/src/modules/workspace/watcher.ts +8 -6
  225. package/src/notice-tags.ts +2 -0
  226. package/src/recovery/discord-awareness-outbox.ts +1 -11
  227. package/src/silent-heartbeat.ts +7 -4
  228. package/src/stored-injections.ts +7 -0
  229. package/src/supplied-text.ts +17 -6
  230. package/src/tool-result-guard.ts +1 -1
  231. package/src/tune-out/coordinator.ts +1374 -141
  232. package/src/tune-out/tools.ts +166 -13
  233. package/src/types/agent.ts +30 -1
  234. package/src/types/framework.ts +157 -15
  235. package/src/types/module.ts +42 -3
  236. package/src/types/trace.ts +154 -3
@@ -0,0 +1,113 @@
1
+ /**
2
+ * The admission stage (Dendrite §D; issue #232 §3.5).
3
+ *
4
+ * One decision per inbound event, taken before any side effect. Everything
5
+ * downstream — storage, the recipient's view, the wake, the speech locus,
6
+ * the gate's batching — consumes this decision instead of rediscovering the
7
+ * policy at its own stage. The function is pure over the policy inputs it is
8
+ * handed; the framework supplies them and applies the result.
9
+ *
10
+ * What is decided, in the proposal's words:
11
+ * 1. captured — always; an admitted event is stored where it arrived
12
+ * 2. recipients — who is eligible (explicit targets, else the untargeted set)
13
+ * 3. delivery — a body in the recipients' view, or a hold
14
+ * 4. wake — may it start a turn
15
+ * 5. locus — may it count as "the latest delivered inbound" (never
16
+ * under a hold); the channel registry records it under
17
+ * either `speech.locus` setting, and makes it the fallback
18
+ * locus only under `'latest-inbound'`
19
+ *
20
+ * A hold is a policy epoch over a channel (today: tune-out). Residents share
21
+ * one message slot, so a hold applies to the slot: every resident recipient
22
+ * gets the same disposition. A reader of the slot (the subconscious, or a
23
+ * reader fork) sees held traffic through its own view; that is not a
24
+ * delivery decision of this stage.
25
+ */
26
+
27
+ export interface InboundEnvelope {
28
+ /** Which MCPL lane carried it. */
29
+ lane: 'channel' | 'push';
30
+ serverId: string;
31
+ /** Endpoint-qualified channel id when the event has one (a push event
32
+ * without channel provenance — heartbeat, timer — has none). */
33
+ channelId?: string;
34
+ /** Platform message id, or the push event id. */
35
+ messageId: string;
36
+ authorId?: string;
37
+ tags?: string[];
38
+ /** `chat:addressed` (mention / reply / DM) or the server's equivalent. */
39
+ addressed: boolean;
40
+ /**
41
+ * The server-side veto: a per-server `shouldTriggerInference` said no, or
42
+ * the event is a materialized coalesced delivery whose wake already
43
+ * happened (`triggerInference: false`). A vetoed event wakes nobody and is
44
+ * not shown to the gate as a fresh event.
45
+ */
46
+ vetoed: boolean;
47
+ /** Explicit recipients named by the event, if any. */
48
+ targetAgents?: string[];
49
+ /** What the gate sees: the text and the metadata shape it has always matched on. */
50
+ gateContent: string;
51
+ gateMetadata: Record<string, unknown>;
52
+ }
53
+
54
+ export interface HoldRecord {
55
+ kind: 'tune-out';
56
+ epochId: string;
57
+ serverId: string;
58
+ channelId: string;
59
+ }
60
+
61
+ export interface AdmissionPolicy {
62
+ /** The recipients an untargeted event reaches. */
63
+ untargetedRecipients(): string[];
64
+ /** The hold in force over this channel, if any. */
65
+ holdFor(serverId: string, channelId: string): HoldRecord | null;
66
+ /**
67
+ * The wake gate. `evaluate` may have side effects (debounce batching,
68
+ * counters, traces) and is called only for an event that may reach the
69
+ * recipients; `probe` is side-effect-free and answers "would the gate have
70
+ * woken them" for a held event, which the hold's own policy needs.
71
+ */
72
+ gate?: {
73
+ evaluate(env: InboundEnvelope): boolean;
74
+ probe(env: InboundEnvelope): boolean;
75
+ };
76
+ }
77
+
78
+ export interface AdmissionDecision {
79
+ recipients: string[];
80
+ /** Null = delivered into the recipients' view; otherwise held, with the record to stamp. */
81
+ held: HoldRecord | null;
82
+ /** Whether the recipients may be woken by this event. */
83
+ wake: boolean;
84
+ /**
85
+ * The gate's verdict as a precondition for whatever reads the hold: true
86
+ * when the gate would (or did) wake the recipients. For a held event this
87
+ * comes from the side-effect-free probe; for a vetoed event it is false.
88
+ */
89
+ gatePass: boolean;
90
+ /** Whether this arrival may count as the latest delivered inbound message. */
91
+ locus: boolean;
92
+ }
93
+
94
+ export function admit(env: InboundEnvelope, policy: AdmissionPolicy): AdmissionDecision {
95
+ const recipients = env.targetAgents ?? policy.untargetedRecipients();
96
+ const held = env.channelId ? policy.holdFor(env.serverId, env.channelId) : null;
97
+
98
+ if (held) {
99
+ // Held: nothing downstream may act for the recipients. The gate is only
100
+ // asked what it would have done — no batch forms, no counter moves.
101
+ const gatePass = env.vetoed ? false : (policy.gate ? policy.gate.probe(env) : true);
102
+ return { recipients, held, wake: false, gatePass, locus: false };
103
+ }
104
+
105
+ if (env.vetoed) {
106
+ return { recipients, held: null, wake: false, gatePass: false, locus: true };
107
+ }
108
+
109
+ // Deliverable: now the gate may take its side effects (a debounce batch
110
+ // is the gate's own deferred wake and is consumed later by the same rule).
111
+ const gatePass = policy.gate ? policy.gate.evaluate(env) : true;
112
+ return { recipients, held: null, wake: gatePass, gatePass, locus: true };
113
+ }
package/src/agent.ts CHANGED
@@ -2,6 +2,7 @@ import { NOTICE_TAG } from './notice-tags.js';
2
2
  import { heartbeatToolAlias } from './heartbeat/heartbeat.js';
3
3
  import type { Membrane, NormalizedMessage, NormalizedRequest, ContentBlock, ThinkingBindingEntry, YieldingStream } from '@animalabs/membrane';
4
4
  import { isAbortedResponse } from '@animalabs/membrane';
5
+ import { streamBounds, type InferenceBounds } from './inference-bounds.js';
5
6
  import type { CacheWireReceipt, KvUnifiedRequestHooks } from './kv-unified-wire.js';
6
7
  import { ToolResultGuard } from './tool-result-guard.js';
7
8
  import { withoutTokenEstimates } from './thinking-token-stamp.js';
@@ -50,6 +51,9 @@ export interface ActivationRequestOptions {
50
51
  * pass it (its errorPolicy is their retry layer); a direct caller of
51
52
  * startStream keeps membrane's retries. */
52
53
  membraneRetries?: boolean;
54
+ /** The framework's inferenceBounds, resolved: the stream gets their phase
55
+ * bounds and flat bound. None when left out. */
56
+ inferenceBounds?: InferenceBounds | null;
53
57
  }
54
58
 
55
59
  /** A compile's render stats, as context-manager's `getRenderStats()` types
@@ -162,6 +166,24 @@ function wireDigest(value: unknown): string {
162
166
  const DEFAULT_CONTEXT_BUDGET_TOKENS = 100_000;
163
167
  const DEFAULT_TRANSITION_PACE_TOKENS = 16_000;
164
168
  const DEFAULT_SAME_ROUND_THINK_TEXT_POLICY: SameRoundThinkTextPolicy = 'public';
169
+ const TURN_INPUTS_PIN = 'agent-framework:turn-inputs';
170
+
171
+ export function assertThinkingBinding(setting: string, value: AgentConfig['thinkingBinding']): void {
172
+ if (holdsSettings(setting, value, '{ binds: true, dropBlock: true }')) {
173
+ assertBooleanSetting(
174
+ `${setting}.dropBlock`,
175
+ value.dropBlock,
176
+ 'true asks the provider to drop a signed thinking block whose earlier conversation changed rather than refuse the request, ' +
177
+ 'and false or leaving it out sends nothing, so the account\'s default applies',
178
+ );
179
+ assertBooleanSetting(
180
+ `${setting}.binds`,
181
+ value.binds,
182
+ 'true says the model binds its signed thinking to the conversation before it, so each compile tells the context manager so, ' +
183
+ 'and false or leaving it out compiles as before',
184
+ );
185
+ }
186
+ }
165
187
 
166
188
  /**
167
189
  * Refuse an agent config whose `promptCaching`, `toolResultGuard`,
@@ -190,20 +212,7 @@ export function assertAgentSwitches(config: AgentConfig): void {
190
212
  'true asks the provider for extended thinking, and false or leaving it out doesn\'t',
191
213
  );
192
214
  }
193
- if (holdsSettings(`${agent} thinkingBinding`, config.thinkingBinding, '{ binds: true, dropBlock: true }')) {
194
- assertBooleanSetting(
195
- `${agent} thinkingBinding.dropBlock`,
196
- config.thinkingBinding.dropBlock,
197
- 'true asks the provider to drop a signed thinking block whose earlier conversation changed rather than refuse the request, ' +
198
- 'and false or leaving it out sends nothing, so the account\'s default applies',
199
- );
200
- assertBooleanSetting(
201
- `${agent} thinkingBinding.binds`,
202
- config.thinkingBinding.binds,
203
- 'true says the model binds its signed thinking to the conversation before it, so each compile tells the context manager so, ' +
204
- 'and false or leaving it out compiles as before',
205
- );
206
- }
215
+ assertThinkingBinding(`${agent} thinkingBinding`, config.thinkingBinding);
207
216
  if (holdsSettings(`${agent} refusalHandling`, config.refusalHandling, '{ autoRewind: true }')) {
208
217
  assertBooleanSetting(
209
218
  `${agent} refusalHandling.autoRewind`,
@@ -321,6 +330,11 @@ export class Agent {
321
330
  /** Provider-specific request parameters forwarded unchanged by Membrane. */
322
331
  readonly providerParams?: Record<string, unknown>;
323
332
  readonly sameRoundThinkTextPolicy: AgentConfig['sameRoundThinkTextPolicy'];
333
+ /** The participant name this agent's turns are presented under (see AgentConfig.presentAs). */
334
+ readonly presentAs: string;
335
+ /** Stored participants presented as this agent's own turns: its name plus AgentConfig.selfParticipants. */
336
+ private readonly ownParticipants: ReadonlySet<string>;
337
+ private readonly deniedAtDispatch: ReadonlySet<string>;
324
338
 
325
339
  private _state: AgentState = { status: 'idle' };
326
340
  private _inferenceStartedAt = 0;
@@ -450,6 +464,9 @@ export class Agent {
450
464
  this.prefillUserMessage = config.prefillUserMessage;
451
465
  this.providerParams = config.providerParams;
452
466
  this.sameRoundThinkTextPolicy = config.sameRoundThinkTextPolicy;
467
+ this.presentAs = config.presentAs ?? config.name;
468
+ this.ownParticipants = new Set([config.name, ...(config.selfParticipants ?? [])]);
469
+ this.deniedAtDispatch = new Set(config.denyToolsAtDispatch ?? []);
453
470
  this.maxStreamTokens = config.maxStreamTokens ?? 150_000;
454
471
  // The window size converted as the projection's comparison (`projected > window`) converts it, so a string such
455
472
  // as "200000" from a JSON or YAML config is its number, for every rule that reads it. A value whose number isn't
@@ -509,6 +526,36 @@ export class Agent {
509
526
  return alias !== undefined && this.allowedTools.includes(alias);
510
527
  }
511
528
 
529
+ /**
530
+ * Whether a call to an advertised tool is refused at dispatch
531
+ * (see AgentConfig.denyToolsAtDispatch).
532
+ */
533
+ deniesAtDispatch(toolName: string): boolean {
534
+ return this.deniedAtDispatch.has(toolName);
535
+ }
536
+
537
+ /** Whether a stored participant is this agent's own voice. */
538
+ isOwnParticipant(participant: string): boolean {
539
+ return this.ownParticipants.has(participant);
540
+ }
541
+
542
+ /**
543
+ * Role assignment for a request: every participant that is this agent's
544
+ * own voice — its name, and for a derived agent the agent it inherited
545
+ * from — is presented under one name, which the formatter maps to the
546
+ * assistant role. Stored authorship is untouched; this shapes the request
547
+ * only. For a plain agent (no selfParticipants, presentAs = name) the
548
+ * input is returned as is.
549
+ */
550
+ private presentOwnTurns(messages: NormalizedMessage[]): NormalizedMessage[] {
551
+ if (this.ownParticipants.size === 1 && this.presentAs === this.name) return messages;
552
+ return messages.map((m) =>
553
+ this.ownParticipants.has(m.participant) && m.participant !== this.presentAs
554
+ ? { ...m, participant: this.presentAs }
555
+ : m,
556
+ );
557
+ }
558
+
512
559
  /**
513
560
  * Check if a source can trigger inference for this agent.
514
561
  */
@@ -1098,11 +1145,7 @@ export class Agent {
1098
1145
  * A message above it may still be replaced or withdrawn in place by its
1099
1146
  * sender, though it may already have been in one of the agent's requests.
1100
1147
  * A live stream carries each round's own content and results, which it
1101
- * stores at its tool boundaries, into its next request. It can also carry
1102
- * the messages injected at a boundary whose stream didn't say whether it
1103
- * carries injections, as no membrane 0.5.x stream does: its native path
1104
- * carries them and its XML path drops them, and either way they stay
1105
- * above it.
1148
+ * stores at its tool boundaries, into its next request.
1106
1149
  */
1107
1150
  private consumedWatermark: { branch: string; sequence: number } | null = null;
1108
1151
  /** Counts marks (markContextConsumed's and applyCompileWatermark's), so a
@@ -1112,8 +1155,7 @@ export class Agent {
1112
1155
  * The newest stored message a compile or a tool boundary may have put in
1113
1156
  * front of the agent, per branch. It moves where the consumed watermark
1114
1157
  * does, and sooner: when a committing compile takes its value, before it
1115
- * awaits, and at every tool boundary that injects, whether or not the
1116
- * stream says it carries injected messages. A message above it may still
1158
+ * awaits, and at every tool boundary that injects into its stream. A message above it may still
1117
1159
  * have been shown, though: for one, a live stream carries each round's own
1118
1160
  * content and results, stored at a boundary that injected nothing, into its
1119
1161
  * next request. A compile that throws assembled nothing, and gives its move
@@ -1152,6 +1194,63 @@ export class Agent {
1152
1194
  if (watermark) this.answeredWatermark = watermark;
1153
1195
  }
1154
1196
 
1197
+ /**
1198
+ * Keep the stored rows from `first` through `last` raw for the turn now
1199
+ * opening, with a raw pin under one name per agent. Any pin of that name
1200
+ * already held is removed first, one a crash left included. Under a
1201
+ * strategy without pins nothing is pinned. A committing compile that
1202
+ * refuses as over budget while a pin of that name is held removes it and
1203
+ * compiles once more, and `unpinTurnInputs` removes it. Returns the pin's
1204
+ * id, or undefined when nothing was pinned.
1205
+ *
1206
+ * @internal For the framework's turn starts; not a host API.
1207
+ */
1208
+ pinTurnInputs(first: MessageId, last: MessageId): string | undefined {
1209
+ if (!this.unpinTurnInputs()) return undefined;
1210
+ const cm = this.contextManager;
1211
+ if (typeof (cm.getStrategy() as { pinRange?: unknown }).pinRange !== 'function') return undefined;
1212
+ try {
1213
+ return cm.pinRange(first, last, { name: TURN_INPUTS_PIN });
1214
+ } catch (error) {
1215
+ console.error(`[turn-inputs] ${this.name}: could not pin the turn's inputs (${first}..${last}), so they may be compressed:`, error);
1216
+ return undefined;
1217
+ }
1218
+ }
1219
+
1220
+ /** Remove the turn's input pin `pinId`, or without one every pin of its
1221
+ * name. False when a removal threw.
1222
+ * @internal For the framework's turn starts and ends; not a host API. */
1223
+ unpinTurnInputs(pinId?: string): boolean {
1224
+ try {
1225
+ for (const pin of this.contextManager.listPins()) {
1226
+ if (pin.name === TURN_INPUTS_PIN && (pinId === undefined || pin.id === pinId)) this.contextManager.unpin(pin.id);
1227
+ }
1228
+ return true;
1229
+ } catch (error) {
1230
+ console.error(`[turn-inputs] ${this.name}: could not remove the turn's input pin:`, error);
1231
+ return false;
1232
+ }
1233
+ }
1234
+
1235
+ private async compileUnpinningOnRefusal(compile: () => Promise<CompileResult>): Promise<CompileResult> {
1236
+ const pinned = this.holdsTurnInputsPin();
1237
+ try {
1238
+ return await compile();
1239
+ } catch (error) {
1240
+ if ((error as { name?: unknown } | null)?.name !== 'OverBudgetError' || !pinned || !this.unpinTurnInputs()) throw error;
1241
+ console.error(`[turn-inputs] ${this.name}: the compile refused with the turn's inputs pinned, so they are unpinned and it compiles again`);
1242
+ return compile();
1243
+ }
1244
+ }
1245
+
1246
+ private holdsTurnInputsPin(): boolean {
1247
+ try {
1248
+ return this.contextManager.listPins().some((pin) => pin.name === TURN_INPUTS_PIN);
1249
+ } catch {
1250
+ return false;
1251
+ }
1252
+ }
1253
+
1155
1254
  /**
1156
1255
  * Per destination (a server's channel, by its key), the newest consumed
1157
1256
  * watermark the agent had read when it published there by a route the
@@ -1198,7 +1297,7 @@ export class Agent {
1198
1297
  }
1199
1298
 
1200
1299
  /** Mark everything currently stored as possibly shown (a tool boundary
1201
- * that injected messages, whatever its stream says it carries). Without
1300
+ * that injected messages into its stream). Without
1202
1301
  * a watermark the coalescer counts a message as shown, as it counts one
1203
1302
  * read without a consumed watermark. */
1204
1303
  markContextShown(): void {
@@ -1281,7 +1380,9 @@ export class Agent {
1281
1380
  * run. It isn't told the agent's thinking binding (`thinkingBinding.binds`),
1282
1381
  * so a context manager that keeps a thinking-binding record neither reads
1283
1382
  * nor writes the record here, and what it returns keeps thinking whose
1284
- * earlier conversation changed. */
1383
+ * earlier conversation changed. Called while a turn's input pin is held
1384
+ * (pinTurnInputs), a refusal as over budget removes the pin and compiles
1385
+ * once more, as the turn's own compiles do. */
1285
1386
  async compileContext(budget?: TokenBudget): Promise<CompileResult> {
1286
1387
  // RFC-006 §3.3: consumption is determined at assembly — a compile that
1287
1388
  // fails assembled nothing, so the watermark moves only on success.
@@ -1289,7 +1390,7 @@ export class Agent {
1289
1390
  const marksBefore = this.watermarkMarks;
1290
1391
  const shownBefore = this.shownWatermark;
1291
1392
  if (watermark) this.shownWatermark = watermark;
1292
- const compile = () => this.contextManager.compile(this.compileBudget(budget));
1393
+ const compile = () => this.compileUnpinningOnRefusal(() => this.contextManager.compile(this.compileBudget(budget)));
1293
1394
  let result: CompileResult;
1294
1395
  try {
1295
1396
  result = budget ? await compile() : await this.compileAtLiveBudget(compile);
@@ -1545,7 +1646,8 @@ export class Agent {
1545
1646
  // agent before the consumed watermark moves, once it returns.
1546
1647
  const shownBefore = this.shownWatermark;
1547
1648
  if (watermark) this.shownWatermark = watermark;
1548
- const compile = () => this.contextManager.compile(this.compileBudget(budget), injections, opts as never);
1649
+ const select = () => this.contextManager.compile(this.compileBudget(budget), injections, opts as never);
1650
+ const compile = opts?.dryRun ? select : () => this.compileUnpinningOnRefusal(select);
1549
1651
  let result: CompileResult;
1550
1652
  try {
1551
1653
  result = budget || opts?.dryRun ? await compile() : await this.compileAtLiveBudget(compile);
@@ -1643,8 +1745,9 @@ export class Agent {
1643
1745
  // What it holds of the stored injections is what the agent last saw.
1644
1746
  this.noteRenderedInjections(compiled);
1645
1747
  const { systemInjections } = compiled;
1646
- // The framework's own pricing stamp never goes out (thinking-token-stamp.ts).
1647
- const messages = withoutTokenEstimates(compiled.messages);
1748
+ // The framework's own pricing stamp never goes out (thinking-token-stamp.ts);
1749
+ // the agent's own turns are then presented under its name (presentOwnTurns).
1750
+ const messages = this.presentOwnTurns(withoutTokenEstimates(compiled.messages));
1648
1751
  // Read once, after the first compile, since results may have become
1649
1752
  // ready while it ran; the refusal retry, assembled while the agent is
1650
1753
  // inferring, sends the same ones.
@@ -1656,6 +1759,7 @@ export class Agent {
1656
1759
  system: this.buildSystemPrompt(systemInjections),
1657
1760
  tools: tools.length > 0 ? tools : undefined,
1658
1761
  ...this.requestSettings(),
1762
+ assistantParticipant: this.presentAs,
1659
1763
  };
1660
1764
  };
1661
1765
  const request = await assemble();
@@ -1891,6 +1995,8 @@ export class Agent {
1891
1995
  // The framework's own pricing stamp never goes out (thinking-token-stamp.ts).
1892
1996
  messages = withoutTokenEstimates(messages);
1893
1997
 
1998
+ messages = this.presentOwnTurns(messages);
1999
+
1894
2000
  // Silent heartbeat ticks and replies to a [Continue] turn store no
1895
2001
  // prompt row. Render each request-only turn before its response's first
1896
2002
  // surviving row, so the wire formatter cannot merge the response into the
@@ -1901,7 +2007,7 @@ export class Agent {
1901
2007
  // renders. "First" is per agent: a broadcast tick shares its eventId with
1902
2008
  // every resident.
1903
2009
  const index = this.indexRequestOnlyRows();
1904
- const rendered = renderRequestOnlyRows(messages, index, this.name);
2010
+ const rendered = renderRequestOnlyRows(messages, index, this.presentAs);
1905
2011
  messages = rendered.messages;
1906
2012
  // A tick opens wherever its separator isn't already rendered: on its first
1907
2013
  // request, and on a restart whose compile left none of the tick's rows,
@@ -1920,7 +2026,7 @@ export class Agent {
1920
2026
  let continuePrompt: RequestOnlyPrompt | undefined;
1921
2027
  if (openingTick) {
1922
2028
  messages = [...messages, silentHeartbeatSeparatorTurn()];
1923
- } else if (endsOnOwnMessage(messages, this.name, injectionTail)) {
2029
+ } else if (endsOnOwnMessage(messages, this.presentAs, injectionTail)) {
1924
2030
  // Safety: ensure messages don't end with an assistant message.
1925
2031
  // Some models reject trailing assistant messages ("prefill not supported"),
1926
2032
  // and after context compression a stale assistant turn can end up last.
@@ -1970,7 +2076,7 @@ export class Agent {
1970
2076
  cacheTtl: this.cacheTtl,
1971
2077
  ...(this.prefillUserMessage && { prefillUserMessage: this.prefillUserMessage }),
1972
2078
  ...(this.providerParams && { providerParams: this.providerParams }),
1973
- assistantParticipant: this.name,
2079
+ assistantParticipant: this.presentAs,
1974
2080
  };
1975
2081
  }
1976
2082
 
@@ -2063,7 +2169,7 @@ export class Agent {
2063
2169
  try {
2064
2170
  const stored = cm.getAllMessages() as unknown as readonly StoredRowLike[];
2065
2171
  if (this.requestOnlyIndex?.source === stored) return this.requestOnlyIndex.index;
2066
- const index = indexRequestOnlyRows(stored, this.name);
2172
+ const index = indexRequestOnlyRows(stored, this.ownParticipants);
2067
2173
  this.requestOnlyIndex = { source: stored, index };
2068
2174
  return index;
2069
2175
  } catch (error) {
@@ -2169,6 +2275,7 @@ export class Agent {
2169
2275
  // since membrane 0.7 its own schedules, ten minutes at a 529, would
2170
2276
  // otherwise run inside each of the caller's attempts, out of its sight.
2171
2277
  ...(options.membraneRetries === false ? { retry: false } : {}),
2278
+ ...streamBounds(options.inferenceBounds ?? null, request.config.maxTokens),
2172
2279
  // Retry a content-policy refusal at the provider seam rather than at
2173
2280
  // the framework level: membrane replays the SAME request immediately
2174
2281
  // (cache-warm), where a framework-level requeue would recompile and
@@ -86,9 +86,16 @@ export class McpServer {
86
86
  // Connect to WebSocket API
87
87
  await this.connectWs();
88
88
 
89
- readLines(process.stdin, (line) => this.handleInput(line), () => {
90
- this.close();
91
- process.exit(0);
89
+ readLines(process.stdin, {
90
+ line: (line) => this.handleInput(line),
91
+ end: () => {
92
+ this.close();
93
+ process.exit(0);
94
+ },
95
+ tooLong: (maxChars) => {
96
+ this.close();
97
+ process.stderr.write(`MCP server stopped: a line on stdin was longer than ${maxChars.toLocaleString('en-US')} UTF-16 code units\n`, () => process.exit(1));
98
+ },
92
99
  });
93
100
 
94
101
  // Handle SIGINT gracefully
@@ -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
+ }
@@ -9,6 +9,7 @@ import { fresh, toolRef, type CallSummary, type NamedCall } from './calls.js';
9
9
  import type { Ending } from './runner.js';
10
10
  import type { ScriptListing, ScriptReturn } from './scripts.js';
11
11
  import { renderOutput } from './output.js';
12
+ import { quotedValue, suppliedValue } from '../../supplied-text.js';
12
13
 
13
14
  /**
14
15
  * The most of a script's return value, or of a failure's message or stack,
@@ -67,18 +68,18 @@ function endingLine(id: string, ending: Ending, lifetimeMs: number): string {
67
68
  return `Script ${id} finished.`;
68
69
  case 'failed':
69
70
  // The run keeps the message cut to VALUE_CHARS, with room for the cut's marker.
70
- return `Script ${id} failed: ${ending.error.name ? `${ending.error.name}: ` : ''}${ending.error.message}`;
71
+ return `Script ${id} failed: ${ending.error.name ? `${suppliedValue(ending.error.name)}: ` : ''}${suppliedValue(ending.error.message)}`;
71
72
  case 'stopped': {
72
73
  const why = ending.reason === 'agent'
73
74
  ? `Script ${id} was stopped, as you asked.`
74
75
  : ending.why
75
- ? `Script ${id} was stopped: ${ending.why}.`
76
+ ? `Script ${id} was stopped: ${suppliedValue(glimpse(ending.why, 300))}.`
76
77
  : `Script ${id} was stopped at its limit of ${formatMs(lifetimeMs)}.`;
77
78
  return ending.short ? `${why} Its process didn't stop by itself and was ended, so its output may be short.` : why;
78
79
  }
79
80
  case 'crashed':
80
81
  // The detail is the host's account; what it means for the agent follows.
81
- return `Script ${id} crashed: ${ending.detail}. A crash is the script's process failing, not an error its code threw: a fault in the host or its runtime, or something the script's own data asked of the runtime that it couldn't do, such as reading values nested thousands deep, or code nested hundreds deep. Running it again may work; if the same script crashes again, change what it does with its data or its code.`;
82
+ return `Script ${id} crashed: ${suppliedValue(ending.detail)}. A crash is the script's process failing, not an error its code threw: a fault in the host or its runtime, or something the script's own data asked of the runtime that it couldn't do, such as reading values nested thousands deep, or code nested hundreds deep. Running it again may work; if the same script crashes again, change what it does with its data or its code.`;
82
83
  }
83
84
  }
84
85
 
@@ -107,7 +108,7 @@ function namedLine(call: NamedCall): string {
107
108
  case 'timed-out':
108
109
  return `- timed out in the script, and still running when the script ended, so it may still take effect: ${namedRef(call)}`;
109
110
  case 'timed-out-answered':
110
- return `- timed out in the script, and its answer came later: ${namedRef(call)}, answer ${JSON.stringify(call.answer ?? '')}`;
111
+ return `- timed out in the script, and its answer came later: ${namedRef(call)}, answer ${quotedValue(call.answer ?? '')}`;
111
112
  case 'too-large':
112
113
  return `- answered with a result the script couldn't receive, too large for its memory or nested too deep, so it got an error instead: ${namedRef(call)}`;
113
114
  case 'unseen':
@@ -208,11 +209,11 @@ function listingState(s: ScriptListing, now: number): string {
208
209
  case 'finished':
209
210
  return `finished${when}`;
210
211
  case 'failed':
211
- return `failed${when}: ${e.error.name ? `${e.error.name}: ` : ''}${glimpse(e.error.message, 300)}`;
212
+ return `failed${when}: ${e.error.name ? `${suppliedValue(e.error.name)}: ` : ''}${suppliedValue(glimpse(e.error.message, 300))}`;
212
213
  case 'crashed':
213
214
  return `crashed${when}`;
214
215
  case 'stopped':
215
- return e.reason === 'agent' ? `stopped by you${when}` : e.why ? `stopped${when}: ${e.why}` : `stopped at its limit${when}`;
216
+ return e.reason === 'agent' ? `stopped by you${when}` : e.why ? `stopped${when}: ${suppliedValue(glimpse(e.why, 300))}` : `stopped at its limit${when}`;
216
217
  default:
217
218
  return 'ended';
218
219
  }
@@ -32,6 +32,7 @@ import type { CallSummary } from './calls.js';
32
32
  import type { OutputBounds, OutputWindow } from './output.js';
33
33
  import type { PreludeTool } from './prelude.js';
34
34
  import { glimpse } from './render.js';
35
+ import { quotedJson, suppliedValue } from '../../supplied-text.js';
35
36
 
36
37
  /** The run or wait call watching a script: its turn, which the script's calls carry, and whether one of them ended that turn. */
37
38
  export interface ScriptWatch {
@@ -473,16 +474,16 @@ export class AgentScripts {
473
474
 
474
475
  function leftEndingText(scriptId: string, ending: Ending): string {
475
476
  const how = ending.how === 'failed'
476
- ? `failed: ${ending.error.name ? `${ending.error.name}: ` : ''}${glimpse(ending.error.message, 300)}`
477
+ ? `failed: ${ending.error.name ? `${suppliedValue(ending.error.name)}: ` : ''}${suppliedValue(glimpse(ending.error.message, 300))}`
477
478
  : ending.how === 'crashed'
478
- ? `crashed: ${ending.detail}`
479
+ ? `crashed: ${suppliedValue(ending.detail)}`
479
480
  : ending.how === 'stopped'
480
- ? ending.why ? `was stopped: ${ending.why}` : 'was stopped at its limit'
481
+ ? ending.why ? `was stopped: ${suppliedValue(glimpse(ending.why, 300))}` : 'was stopped at its limit'
481
482
  : 'ended';
482
483
  return `[${NOTICE_TAG.script} ${scriptId}] Your script ${how}. {"action": "wait", "script_id": "${scriptId}"} gives its result.`;
483
484
  }
484
485
 
485
486
  function wakeText(scriptId: string, payloadJson: string, n: number, max: number, ageMs: number): string {
486
487
  const minutes = Math.round(ageMs / 60_000);
487
- return `[${NOTICE_TAG.script} ${scriptId}] Woke you with wake_agent(), ${minutes} min after it started (wake ${n} of ${max}): ${payloadJson}`;
488
+ return `[${NOTICE_TAG.script} ${scriptId}] Woke you with wake_agent(), ${minutes} min after it started (wake ${n} of ${max}): ${quotedJson(payloadJson)}`;
488
489
  }
@@ -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';