@animalabs/agent-framework 0.33.0 → 0.34.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 (176) hide show
  1. package/README.md +136 -8
  2. package/dist/src/agent.d.ts +25 -4
  3. package/dist/src/agent.d.ts.map +1 -1
  4. package/dist/src/agent.js +68 -13
  5. package/dist/src/agent.js.map +1 -1
  6. package/dist/src/append-all.d.ts +5 -0
  7. package/dist/src/append-all.d.ts.map +1 -0
  8. package/dist/src/append-all.js +8 -0
  9. package/dist/src/append-all.js.map +1 -0
  10. package/dist/src/context-options.d.ts +9 -6
  11. package/dist/src/context-options.d.ts.map +1 -1
  12. package/dist/src/context-options.js +13 -8
  13. package/dist/src/context-options.js.map +1 -1
  14. package/dist/src/cut-off-round.js +4 -4
  15. package/dist/src/dendrite/registry.d.ts.map +1 -1
  16. package/dist/src/dendrite/registry.js +4 -2
  17. package/dist/src/dendrite/registry.js.map +1 -1
  18. package/dist/src/failure-text.d.ts +36 -0
  19. package/dist/src/failure-text.d.ts.map +1 -0
  20. package/dist/src/failure-text.js +89 -0
  21. package/dist/src/failure-text.js.map +1 -0
  22. package/dist/src/framework.d.ts +94 -30
  23. package/dist/src/framework.d.ts.map +1 -1
  24. package/dist/src/framework.js +987 -284
  25. package/dist/src/framework.js.map +1 -1
  26. package/dist/src/gate/event-gate.d.ts +40 -6
  27. package/dist/src/gate/event-gate.d.ts.map +1 -1
  28. package/dist/src/gate/event-gate.js +66 -25
  29. package/dist/src/gate/event-gate.js.map +1 -1
  30. package/dist/src/gate/reply-judge.d.ts +4 -0
  31. package/dist/src/gate/reply-judge.d.ts.map +1 -1
  32. package/dist/src/gate/reply-judge.js +12 -3
  33. package/dist/src/gate/reply-judge.js.map +1 -1
  34. package/dist/src/gate/suppression-review.js +2 -2
  35. package/dist/src/gate/suppression-review.js.map +1 -1
  36. package/dist/src/gate/types.d.ts +7 -0
  37. package/dist/src/gate/types.d.ts.map +1 -1
  38. package/dist/src/heartbeat/heartbeat.js +1 -1
  39. package/dist/src/heartbeat/heartbeat.js.map +1 -1
  40. package/dist/src/index.d.ts +1 -0
  41. package/dist/src/index.d.ts.map +1 -1
  42. package/dist/src/index.js +1 -0
  43. package/dist/src/index.js.map +1 -1
  44. package/dist/src/inference-bounds.d.ts +2 -1
  45. package/dist/src/inference-bounds.d.ts.map +1 -1
  46. package/dist/src/inference-bounds.js +9 -6
  47. package/dist/src/inference-bounds.js.map +1 -1
  48. package/dist/src/mcpl/channel-registry.d.ts +57 -13
  49. package/dist/src/mcpl/channel-registry.d.ts.map +1 -1
  50. package/dist/src/mcpl/channel-registry.js +136 -24
  51. package/dist/src/mcpl/channel-registry.js.map +1 -1
  52. package/dist/src/mcpl/modern-connection.d.ts +109 -15
  53. package/dist/src/mcpl/modern-connection.d.ts.map +1 -1
  54. package/dist/src/mcpl/modern-connection.js +593 -86
  55. package/dist/src/mcpl/modern-connection.js.map +1 -1
  56. package/dist/src/mcpl/protocol-family.d.ts +3 -1
  57. package/dist/src/mcpl/protocol-family.d.ts.map +1 -1
  58. package/dist/src/mcpl/protocol-family.js +15 -9
  59. package/dist/src/mcpl/protocol-family.js.map +1 -1
  60. package/dist/src/mcpl/server-connection.d.ts +37 -18
  61. package/dist/src/mcpl/server-connection.d.ts.map +1 -1
  62. package/dist/src/mcpl/server-connection.js +31 -16
  63. package/dist/src/mcpl/server-connection.js.map +1 -1
  64. package/dist/src/mcpl/tool-presence.d.ts.map +1 -1
  65. package/dist/src/mcpl/tool-presence.js +10 -6
  66. package/dist/src/mcpl/tool-presence.js.map +1 -1
  67. package/dist/src/mcpl/toolset-host.d.ts.map +1 -1
  68. package/dist/src/mcpl/toolset-host.js +7 -6
  69. package/dist/src/mcpl/toolset-host.js.map +1 -1
  70. package/dist/src/mcpl/toolsets-module.d.ts.map +1 -1
  71. package/dist/src/mcpl/toolsets-module.js +2 -1
  72. package/dist/src/mcpl/toolsets-module.js.map +1 -1
  73. package/dist/src/mcpl/types.d.ts +25 -4
  74. package/dist/src/mcpl/types.d.ts.map +1 -1
  75. package/dist/src/mcpl/types.js.map +1 -1
  76. package/dist/src/modules/discord/index.js +1 -1
  77. package/dist/src/modules/discord/index.js.map +1 -1
  78. package/dist/src/modules/health/index.js +2 -2
  79. package/dist/src/modules/health/index.js.map +1 -1
  80. package/dist/src/modules/history/semantic.js +3 -3
  81. package/dist/src/modules/history/semantic.js.map +1 -1
  82. package/dist/src/modules/workspace/index.d.ts.map +1 -1
  83. package/dist/src/modules/workspace/index.js +4 -3
  84. package/dist/src/modules/workspace/index.js.map +1 -1
  85. package/dist/src/modules/workspace/observe.d.ts.map +1 -1
  86. package/dist/src/modules/workspace/observe.js +2 -1
  87. package/dist/src/modules/workspace/observe.js.map +1 -1
  88. package/dist/src/modules/workspace/reconcile.d.ts.map +1 -1
  89. package/dist/src/modules/workspace/reconcile.js +3 -2
  90. package/dist/src/modules/workspace/reconcile.js.map +1 -1
  91. package/dist/src/notice-tags.d.ts +10 -8
  92. package/dist/src/notice-tags.d.ts.map +1 -1
  93. package/dist/src/notice-tags.js +10 -8
  94. package/dist/src/notice-tags.js.map +1 -1
  95. package/dist/src/presentation.d.ts +2 -1
  96. package/dist/src/presentation.d.ts.map +1 -1
  97. package/dist/src/presentation.js +4 -2
  98. package/dist/src/presentation.js.map +1 -1
  99. package/dist/src/reasoning-loss.d.ts +5 -0
  100. package/dist/src/reasoning-loss.d.ts.map +1 -0
  101. package/dist/src/reasoning-loss.js +8 -0
  102. package/dist/src/reasoning-loss.js.map +1 -0
  103. package/dist/src/recovery/offline-branch.d.ts.map +1 -1
  104. package/dist/src/recovery/offline-branch.js +5 -4
  105. package/dist/src/recovery/offline-branch.js.map +1 -1
  106. package/dist/src/recovery/recover-cli.js +2 -1
  107. package/dist/src/recovery/recover-cli.js.map +1 -1
  108. package/dist/src/sent-rows.js +1 -1
  109. package/dist/src/supplied-text.d.ts +1 -1
  110. package/dist/src/supplied-text.js +3 -3
  111. package/dist/src/supplied-text.js.map +1 -1
  112. package/dist/src/thinking-drops.d.ts +2 -0
  113. package/dist/src/thinking-drops.d.ts.map +1 -1
  114. package/dist/src/thinking-drops.js.map +1 -1
  115. package/dist/src/tool-presentation.d.ts.map +1 -1
  116. package/dist/src/tool-presentation.js +38 -9
  117. package/dist/src/tool-presentation.js.map +1 -1
  118. package/dist/src/tune-out/coordinator.d.ts +13 -4
  119. package/dist/src/tune-out/coordinator.d.ts.map +1 -1
  120. package/dist/src/tune-out/coordinator.js +33 -16
  121. package/dist/src/tune-out/coordinator.js.map +1 -1
  122. package/dist/src/tune-out/tools.d.ts.map +1 -1
  123. package/dist/src/tune-out/tools.js +2 -1
  124. package/dist/src/tune-out/tools.js.map +1 -1
  125. package/dist/src/types/agent.d.ts +39 -5
  126. package/dist/src/types/agent.d.ts.map +1 -1
  127. package/dist/src/types/framework.d.ts +43 -8
  128. package/dist/src/types/framework.d.ts.map +1 -1
  129. package/dist/src/types/inference-log.d.ts +1 -0
  130. package/dist/src/types/inference-log.d.ts.map +1 -1
  131. package/dist/src/types/trace.d.ts +64 -4
  132. package/dist/src/types/trace.d.ts.map +1 -1
  133. package/package.json +3 -3
  134. package/src/agent.ts +71 -14
  135. package/src/append-all.ts +6 -0
  136. package/src/context-options.ts +18 -8
  137. package/src/cut-off-round.ts +4 -4
  138. package/src/dendrite/registry.ts +4 -2
  139. package/src/failure-text.ts +88 -0
  140. package/src/framework.ts +1018 -293
  141. package/src/gate/event-gate.ts +90 -26
  142. package/src/gate/reply-judge.ts +12 -3
  143. package/src/gate/suppression-review.ts +2 -2
  144. package/src/gate/types.ts +7 -0
  145. package/src/heartbeat/heartbeat.ts +1 -1
  146. package/src/index.ts +1 -0
  147. package/src/inference-bounds.ts +11 -6
  148. package/src/mcpl/channel-registry.ts +160 -31
  149. package/src/mcpl/modern-connection.ts +629 -91
  150. package/src/mcpl/protocol-family.ts +15 -9
  151. package/src/mcpl/server-connection.ts +28 -15
  152. package/src/mcpl/tool-presence.ts +10 -5
  153. package/src/mcpl/toolset-host.ts +7 -6
  154. package/src/mcpl/toolsets-module.ts +2 -1
  155. package/src/mcpl/types.ts +25 -4
  156. package/src/modules/discord/index.ts +1 -1
  157. package/src/modules/health/index.ts +2 -2
  158. package/src/modules/history/semantic.ts +3 -3
  159. package/src/modules/workspace/index.ts +4 -3
  160. package/src/modules/workspace/observe.ts +2 -1
  161. package/src/modules/workspace/reconcile.ts +3 -2
  162. package/src/notice-tags.ts +11 -8
  163. package/src/presentation.ts +4 -2
  164. package/src/reasoning-loss.ts +7 -0
  165. package/src/recovery/offline-branch.ts +5 -4
  166. package/src/recovery/recover-cli.ts +2 -1
  167. package/src/sent-rows.ts +1 -1
  168. package/src/supplied-text.ts +3 -3
  169. package/src/thinking-drops.ts +2 -0
  170. package/src/tool-presentation.ts +34 -9
  171. package/src/tune-out/coordinator.ts +32 -15
  172. package/src/tune-out/tools.ts +2 -1
  173. package/src/types/agent.ts +36 -5
  174. package/src/types/framework.ts +44 -8
  175. package/src/types/inference-log.ts +1 -0
  176. package/src/types/trace.ts +66 -4
package/src/agent.ts CHANGED
@@ -35,6 +35,7 @@ import {
35
35
  truncateForHistory,
36
36
  DEFAULT_TOOL_RESULT_INLINE_MAX_CHARS,
37
37
  } from './tool-result-history.js';
38
+ import { appendAll } from './append-all.js';
38
39
 
39
40
  export interface ActivationRequestOptions {
40
41
  /** The silent heartbeat tick this request answers, if any. */
@@ -112,6 +113,11 @@ export interface StartStreamResult {
112
113
  * far in sent order, with this request's head (context-manager's `noteRequestSent`). Present only where the
113
114
  * model binds its thinking and the context manager keeps a record it can be told of. */
114
115
  noteRequestSent?: (rows: readonly SentRow[]) => boolean;
116
+ /** Tells the context manager which stored bodies' thinking the provider dropped from a call of this stream, by the
117
+ * ids `placeDrops` gives for a `reply` drop, with the compile the stream was built from (context-manager's
118
+ * `noteThinkingDropped`), so, once the context manager takes the note, its record leaves them without their
119
+ * thinking from the next compile on. Present only where `noteRequestSent` is. */
120
+ noteThinkingDropped?: (messageIds: readonly MessageId[]) => boolean;
115
121
  /** The stored bodies the request's compile carried, by each body's first message id (its provenance's raw bodies).
116
122
  * Absent when the compile reports no provenance. */
117
123
  carriedBodies?: ReadonlySet<MessageId>;
@@ -435,6 +441,7 @@ export class Agent {
435
441
  * the next change.
436
442
  */
437
443
  private liveBudgetEpoch = 0;
444
+ private contextSettingsChangeCount = 0;
438
445
  /**
439
446
  * Counts changes of the descent (each arm of one, the same target included,
440
447
  * a reset, a cancel, a settle). A settle proves its target only to a
@@ -696,7 +703,19 @@ export class Agent {
696
703
  if (typeof manager.updateHotContextSettings !== 'function') {
697
704
  throw new Error('The active context strategy does not support live context settings');
698
705
  }
699
- return manager.updateHotContextSettings(update);
706
+ const settings = (s: HotContextSettingsStatus | null) =>
707
+ JSON.stringify([s?.tailTokens, s?.transitionPaceTokens, s?.preparedWindowTokens]);
708
+ const before = settings(this.getHotContextSettings());
709
+ const status = manager.updateHotContextSettings(update);
710
+ if (settings(status) !== before) this.contextSettingsChangeCount++;
711
+ return status;
712
+ }
713
+
714
+ /** How many times the live budget, its descent target or the hot context
715
+ * settings have changed.
716
+ * @internal For the framework's budget holds; not a host API. */
717
+ contextSettingsChanges(): number {
718
+ return this.contextSettingsChangeCount;
700
719
  }
701
720
 
702
721
  getRuntimeSettings(): AgentRuntimeSettingsSnapshot {
@@ -763,6 +782,7 @@ export class Agent {
763
782
  private setContextBudget(budget: number | undefined, target: number | undefined, armed = false): void {
764
783
  if (budget !== this.contextBudgetTokens) this.liveBudgetEpoch++;
765
784
  if (armed || target !== this.contextBudgetTargetTokens) this.descentEpoch++;
785
+ if (budget !== this.contextBudgetTokens || target !== this.contextBudgetTargetTokens) this.contextSettingsChangeCount++;
766
786
  this.contextBudgetTokens = budget;
767
787
  this.contextBudgetTargetTokens = target;
768
788
  }
@@ -802,7 +822,7 @@ export class Agent {
802
822
  return await this.compileAtLiveBudgetNoting(compile, started);
803
823
  } finally {
804
824
  this.compilesRunning.delete(started);
805
- const earliest = Math.min(...this.compilesRunning);
825
+ const earliest = [...this.compilesRunning].reduce((min, start) => Math.min(min, start), Infinity);
806
826
  for (const [budget, start] of this.latestEndedStart) {
807
827
  if (start < earliest) this.latestEndedStart.delete(budget);
808
828
  }
@@ -1582,8 +1602,8 @@ export class Agent {
1582
1602
  /**
1583
1603
  * A host's hook, called with each committing compile's `thinkingStrip` (never a dry run's) and the key of the
1584
1604
  * branch it read (`branchKey`), as the compile returns, whether or not its request is then sent, since the
1585
- * context manager's binding record takes the strip either way. One that throws is logged. The framework sets none
1586
- * of its own, and tells the agent nothing of a strip.
1605
+ * context manager's binding record takes the strip either way. One that throws is logged. It is observational only;
1606
+ * the framework does not write agent-facing reasoning-loss notices.
1587
1607
  */
1588
1608
  onThinkingStrip?: (strip: ThinkingStrip, branch: string) => void;
1589
1609
 
@@ -1611,12 +1631,9 @@ export class Agent {
1611
1631
  unsentInjection(namespace: string): { contentHash: string; whole: boolean } | undefined {
1612
1632
  let newest: { contentHash: string; parts: number; held: Set<number> } | undefined;
1613
1633
  try {
1614
- const branch = branchKey(this.contextManager.currentBranchRef());
1615
- const since = this.unsentSince.get(branch);
1634
+ const since = this.unsentSince.get(branchKey(this.contextManager.currentBranchRef()));
1616
1635
  if (since === undefined) {
1617
- const count = this.contextManager.getMessageCount();
1618
- const top = count > 0 ? this.contextManager.getMessageWindow(count - 1, 1, { resolveBlobs: false }).messages[0]?.sequence ?? 0 : 0;
1619
- this.unsentSince.set(branch, top);
1636
+ this.markUnsentPoint();
1620
1637
  return undefined;
1621
1638
  }
1622
1639
  scan: for (let end = this.contextManager.getMessageCount(); end > 0;) {
@@ -1652,6 +1669,42 @@ export class Agent {
1652
1669
  return newest && { contentHash: newest.contentHash, whole: newest.held.size === newest.parts };
1653
1670
  }
1654
1671
 
1672
+ /** Set the unsent point where it isn't set, as a start's plan does: a row
1673
+ * stored after it counts as in no request until a compile built to send.
1674
+ * @internal For the framework's budget holds; not a host API. */
1675
+ markUnsentPoint(): void {
1676
+ const branch = branchKey(this.contextManager.currentBranchRef());
1677
+ if (this.unsentSince.has(branch)) return;
1678
+ const count = this.contextManager.getMessageCount();
1679
+ const top = count > 0 ? this.contextManager.getMessageWindow(count - 1, 1, { resolveBlobs: false }).messages[0]?.sequence ?? 0 : 0;
1680
+ this.unsentSince.set(branch, top);
1681
+ }
1682
+
1683
+ /** The ids of this agent's stored injections, of every namespace, stored
1684
+ * on its branch since its last compile built to send and in its window
1685
+ * where it's presented, in the record's order, a sharded body's first
1686
+ * shard first: the rows unsentInjection counts, which no request has
1687
+ * carried. Reads only, so an unset point gives none. */
1688
+ unsentInjectionRows(): MessageId[] {
1689
+ const ids: MessageId[] = [];
1690
+ const since = this.unsentSince.get(branchKey(this.contextManager.currentBranchRef()));
1691
+ if (since === undefined) return ids;
1692
+ const presented = this.contextManager.windowMode?.() === 'presented';
1693
+ scan: for (let end = this.contextManager.getMessageCount(); end > 0;) {
1694
+ const span = Math.min(end, 64);
1695
+ const { messages } = this.contextManager.getMessageWindow(end - span, span, { resolveBlobs: false });
1696
+ for (let i = messages.length - 1; i >= 0; i--) {
1697
+ const row = messages[i]!;
1698
+ if (row.sequence <= since) break scan;
1699
+ if (storedInjectionMarkOf(row.metadata)?.agent !== this.name) continue;
1700
+ if (presented && this.contextManager.windowStatusOf(row.id) !== 'in') continue;
1701
+ ids.push(row.id);
1702
+ }
1703
+ end -= span;
1704
+ }
1705
+ return ids.reverse();
1706
+ }
1707
+
1655
1708
  /** Record which of this agent's stored injections a compile built to send
1656
1709
  * rendered. Another agent's stored rows never reach this agent's compile,
1657
1710
  * since every compile of a shared store leaves them out by its view
@@ -1925,7 +1978,7 @@ export class Agent {
1925
1978
  // inferring, sends the same ones.
1926
1979
  unguardedResults ??= this._state.status === 'ready' && !guardedResults
1927
1980
  ? this.buildToolResultMessages(this._state.toolResults) : [];
1928
- messages.push(...unguardedResults);
1981
+ appendAll(messages, unguardedResults);
1929
1982
  return {
1930
1983
  messages: this.toolResultGuard.prepareRequest(messages, true),
1931
1984
  system: this.buildSystemPrompt(systemInjections),
@@ -2268,9 +2321,10 @@ export class Agent {
2268
2321
  return { ...own, blockBinding: { prefixMismatchBehavior: 'drop_block' } };
2269
2322
  }
2270
2323
 
2271
- /** The stream's way to tell the context manager what its later rounds sent, with the head its compile was given:
2272
- * where the agent's model binds its thinking (context-manager's `noteRequestSent`). */
2273
- private requestNotes(binding: BindingCompileOptions, compiled: CompiledForRequest): Pick<StartStreamResult, 'noteRequestSent'> {
2324
+ /** The stream's way to tell the context manager what its later rounds sent, with the head its compile was given,
2325
+ * and which stored bodies' thinking the provider dropped: where the agent's model binds its thinking
2326
+ * (context-manager's `noteRequestSent` and `noteThinkingDropped`). */
2327
+ private requestNotes(binding: BindingCompileOptions, compiled: CompiledForRequest): Pick<StartStreamResult, 'noteRequestSent' | 'noteThinkingDropped'> {
2274
2328
  const identity = binding.identity;
2275
2329
  // Each note names the compile the stream was built from, so a note can't vouch for this stream under a record a
2276
2330
  // later committing compile with binds wrote. A compile that reports no provenance gives the stream nothing to name, and the
@@ -2278,7 +2332,10 @@ export class Agent {
2278
2332
  const compileId = compiled.provenance?.compileId;
2279
2333
  if (!identity || compileId === undefined) return {};
2280
2334
  const cm = this.contextManager;
2281
- return { noteRequestSent: (rows) => cm.noteRequestSent(rows, identity, compileId) };
2335
+ return {
2336
+ noteRequestSent: (rows) => cm.noteRequestSent(rows, identity, compileId),
2337
+ noteThinkingDropped: (messageIds) => cm.noteThinkingDropped(messageIds, compileId),
2338
+ };
2282
2339
  }
2283
2340
 
2284
2341
  /**
@@ -0,0 +1,6 @@
1
+ /** `into.push(...items)` passes each item as a call argument, and V8 throws a
2
+ * RangeError past about 125,000 arguments, so this pushes them one at a
3
+ * time. */
4
+ export function appendAll<T>(into: T[], items: readonly T[]): void {
5
+ for (let i = 0, n = items.length; i < n; i++) into.push(items[i]!);
6
+ }
@@ -19,7 +19,7 @@
19
19
  * (heldFromResidents), what a suppress rule hid (suppressedRow), and the
20
20
  * other residents' stored context injections (isAnotherAgentsInjection):
21
21
  * their context, which they share the slot with it but never its
22
- * conversation.
22
+ * conversation. Both views also omit legacy reasoning-loss notices.
23
23
  * - The subconscious keeps its rows in a slot of its own,
24
24
  * `subconscious/<serves>`, and reads the residents' slot beside it
25
25
  * (`auxiliaryMessageViews: [{}]`), held traffic included, which is what it
@@ -27,7 +27,7 @@
27
27
  * window by its observe edge's rule, and the merged slot is still the
28
28
  * record its window is presented from. Its view leaves out only the
29
29
  * residents' stored context injections and what a suppress rule hid,
30
- * which renders for no reader, this one included. Its strategy is a windowed
30
+ * in this shared view, along with legacy notices. Its strategy is a windowed
31
31
  * passthrough, anchored at the oldest active tune-out's start, or at an
32
32
  * earlier start kept in force: a cancelled tune-out's, kept while another
33
33
  * stays active and across a restart, until the next enter sets the oldest
@@ -37,6 +37,7 @@
37
37
  * a strategy keeps state for the manager it is opened with.
38
38
  */
39
39
  import { WindowedPassthroughStrategy, type ContextManagerConfig, type StoredMessage } from '@animalabs/context-manager';
40
+ import { isReasoningLoss } from './reasoning-loss.js';
40
41
  import { heldFromResidents, suppressedRow } from './dendrite/audience.js';
41
42
  import { persistentReaderName } from './tune-out/tools.js';
42
43
  import { isAnotherAgentsInjection } from './stored-injections.js';
@@ -44,17 +45,26 @@ import { isAnotherAgentsInjection } from './stored-injections.js';
44
45
  /** ContextManager.open's options over a store the caller gives: everything but the store. */
45
46
  export type ContextManagerOpenOptions = Omit<Extract<ContextManagerConfig, { store: unknown }>, 'store'>;
46
47
 
47
- /** What a resident's view leaves out: held traffic, suppressed rows, and another agent's stored context injections. */
48
- export function residentViewFilter(name: string): (message: StoredMessage) => boolean {
48
+ /** A resident's view: no held traffic, suppressed rows, foreign injections or legacy notices.
49
+ * Supports both a reusable named predicate and a direct metadata-only probe. */
50
+ export function residentViewFilter(message: Pick<StoredMessage, 'metadata'>, name: string): boolean;
51
+ export function residentViewFilter(name: string): (message: Pick<StoredMessage, 'metadata'>) => boolean;
52
+ export function residentViewFilter(messageOrName: Pick<StoredMessage, 'metadata'> | string, name?: string): boolean | ((message: Pick<StoredMessage, 'metadata'>) => boolean) {
53
+ if (typeof messageOrName !== 'string') return residentViewFilter(name!)(messageOrName);
49
54
  return (message) => !heldFromResidents(message.metadata as Record<string, unknown> | undefined)
50
55
  && !suppressedRow(message.metadata as Record<string, unknown> | undefined)
51
- && !isAnotherAgentsInjection(message.metadata, name);
56
+ && !isAnotherAgentsInjection(message.metadata, messageOrName)
57
+ && !isReasoningLoss(message.metadata);
52
58
  }
53
59
 
54
- /** What the subconscious's view leaves out: the residents' stored context injections and suppressed rows, and nothing it reads for. */
55
- export function subconsciousViewFilter(name: string): (message: StoredMessage) => boolean {
60
+ /** The subconscious includes held traffic, but not suppressed rows, foreign injections or legacy notices. */
61
+ export function subconsciousViewFilter(message: Pick<StoredMessage, 'metadata'>, name: string): boolean;
62
+ export function subconsciousViewFilter(name: string): (message: Pick<StoredMessage, 'metadata'>) => boolean;
63
+ export function subconsciousViewFilter(messageOrName: Pick<StoredMessage, 'metadata'> | string, name?: string): boolean | ((message: Pick<StoredMessage, 'metadata'>) => boolean) {
64
+ if (typeof messageOrName !== 'string') return subconsciousViewFilter(name!)(messageOrName);
56
65
  return (message) => !suppressedRow(message.metadata as Record<string, unknown> | undefined)
57
- && !isAnotherAgentsInjection(message.metadata, name);
66
+ && !isAnotherAgentsInjection(message.metadata, messageOrName)
67
+ && !isReasoningLoss(message.metadata);
58
68
  }
59
69
 
60
70
  /** The options a resident named `agentName` is opened with: the residents' slot, through its view. The caller gives its strategy. */
@@ -45,10 +45,10 @@ export function cutOffRound(response: NormalizedResponse): CutOffRound | undefin
45
45
  }
46
46
 
47
47
  /**
48
- * Membrane 0.8.0's grammar (unchanged from 0.7.2) for the anthropic-xml
49
- * formatter's XML tool mode. Membrane exports two of its predicates,
50
- * hasUnclosedToolBlock and endsWithPartialToolBlock, but not the grammar
51
- * itself (dist/utils/tool-parser.js, INVOKE_REGEX): a block opens
48
+ * Membrane 0.8.0's and 0.9.0's grammar (unchanged from 0.7.2) for the
49
+ * anthropic-xml formatter's XML tool mode. Membrane exports two of its
50
+ * predicates, hasUnclosedToolBlock and endsWithPartialToolBlock, but not the
51
+ * grammar itself (dist/utils/tool-parser.js, INVOKE_REGEX): a block opens
52
52
  * with `<function_calls>` and each call with `<invoke name="…">`, either tag
53
53
  * optionally `antml:` prefixed, and a closer ends the innermost block still
54
54
  * open. Its parser takes spans out of an unclosed block's text, which leaves
@@ -23,6 +23,7 @@ import type {
23
23
  ObserveEdge,
24
24
  RegistrySnapshot,
25
25
  } from './types.js';
26
+ import { appendAll } from '../append-all.js';
26
27
 
27
28
  /** Thrown for a spec the registry refuses to load. */
28
29
  export class AgentSpecError extends Error {
@@ -619,8 +620,9 @@ export class AgentRegistry {
619
620
  if (!this.live.has(child.name)) continue; // ended by an earlier cascade
620
621
  if (child.onParentEnd === 'end') {
621
622
  const inner = this.endRecursive(child, 'parent-ended', record.name);
622
- cascaded.push(...inner.cascaded, inner.ended);
623
- orphaned.push(...inner.orphaned);
623
+ appendAll(cascaded, inner.cascaded);
624
+ cascaded.push(inner.ended);
625
+ appendAll(orphaned, inner.orphaned);
624
626
  } else {
625
627
  const candidates = adopters.filter((candidate) => candidate !== child.name);
626
628
  child.orphaned = { since: this.now(), formerParent, reason, candidates };
@@ -0,0 +1,88 @@
1
+ import { MembraneError } from '@animalabs/membrane';
2
+ import { OverBudgetError, UncoveredDropError } from '@animalabs/context-manager';
3
+ import { isDeadlineCut } from './inference-bounds.js';
4
+
5
+ /**
6
+ * Longest failure text the inference-failure pipeline writes to one sink: the
7
+ * reason in the exhaustion stderr line, a field of a logs/failures.log record,
8
+ * an ops alert's message, the inference log's `error`, and a trace event's
9
+ * `error` and `stack`. A failure's text is whatever its source put in
10
+ * `Error.message`, and a provider can echo the entire rejected request there
11
+ * (one OpenAI-compatible 400 echoed ~1.7 MB). The sinks with shorter bounds
12
+ * name their own, and every cut goes through boundFailureText. This bound
13
+ * doesn't depend on membrane bounding its own errors.
14
+ */
15
+ export const FAILURE_TEXT_MAX_CHARS = 2_000;
16
+
17
+ /** Longest provider error code recorded verbatim; real codes are short tokens.
18
+ * A failure's name and type are bounded the same way. */
19
+ export const PROVIDER_ERROR_CODE_MAX_CHARS = 128;
20
+
21
+ const isHighSurrogate = (text: string, index: number): boolean => {
22
+ const code = text.charCodeAt(index);
23
+ return code >= 0xd800 && code <= 0xdbff;
24
+ };
25
+ const isLowSurrogate = (text: string, index: number): boolean => {
26
+ const code = text.charCodeAt(index);
27
+ return code >= 0xdc00 && code <= 0xdfff;
28
+ };
29
+ /** Whether a cut before `index` would separate the halves of a surrogate pair. */
30
+ const splitsPair = (text: string, index: number): boolean =>
31
+ index > 0 && index < text.length && isHighSurrogate(text, index - 1) && isLowSurrogate(text, index);
32
+
33
+ /**
34
+ * `text` when it fits within `max` characters; otherwise its head and tail
35
+ * around a marker stating how many of how many characters were omitted, the
36
+ * whole within `max`. A cut that would separate a surrogate pair moves toward
37
+ * omission, so the result never holds half of a pair the text held whole, and
38
+ * bounding bounded text returns it as is. Each sink cuts the whole text, so
39
+ * its marker states the whole length.
40
+ */
41
+ export function boundFailureText(text: string, max: number = FAILURE_TEXT_MAX_CHARS): string {
42
+ if (text.length <= max) return text;
43
+ const markerLength = ` …[${text.length} of ${text.length} characters omitted]… `.length;
44
+ const budget = Math.max(0, max - markerLength);
45
+ const tailLength = Math.floor(budget / 4);
46
+ let headEnd = budget - tailLength;
47
+ if (splitsPair(text, headEnd)) headEnd--;
48
+ let tailStart = text.length - tailLength;
49
+ if (splitsPair(text, tailStart)) tailStart++;
50
+ return `${text.slice(0, headEnd)} …[${tailStart - headEnd} of ${text.length} characters omitted]… ${text.slice(tailStart)}`;
51
+ }
52
+
53
+ /**
54
+ * A MembraneError, from this framework's copy of membrane or from another: the
55
+ * host supplies the Membrane instance (config.membrane), so with two copies
56
+ * installed its errors are instances of a class this framework's `instanceof`
57
+ * doesn't match. Recognised by name as well, as context-manager's errors are;
58
+ * its fields are the same, so callers still check each one's type.
59
+ */
60
+ export function isMembraneError(error: unknown): error is MembraneError {
61
+ return error instanceof MembraneError || (error instanceof Error && error.name === 'MembraneError');
62
+ }
63
+
64
+ export function compileRefusalType(err: Error): 'over_budget' | 'context_refusal' | undefined {
65
+ if (err instanceof OverBudgetError || err.name === 'OverBudgetError') return 'over_budget';
66
+ // context-manager's fatal coverage invariant (fix/coverage-invariant-fatal,
67
+ // 2026-07-26): a compile REFUSES rather than shipping a context with
68
+ // silently-dropped messages. Recovery is identical to over_budget — kick
69
+ // the compression drain so summaries cover the un-represented span.
70
+ if (err instanceof UncoveredDropError || err.name === 'UncoveredDropError') return 'context_refusal';
71
+ return undefined;
72
+ }
73
+
74
+ export function houseReason(err: Error): string | undefined {
75
+ return isDeadlineCut(err) || compileRefusalType(err) !== undefined ? String(err.message) : undefined;
76
+ }
77
+
78
+ export function failureWords(err: Error, otherwise = 'the turn failed'): string {
79
+ const own = houseReason(err);
80
+ if (own !== undefined) return own;
81
+ if (!isMembraneError(err)) return otherwise;
82
+ if (err.type === 'safety') return "the request couldn't be completed";
83
+ const status = typeof err.httpStatus === 'number' ? `HTTP ${err.httpStatus}` : undefined;
84
+ if (err.type === 'timeout') return `the model call timed out${status ? ` (${status})` : ''}`;
85
+ const type = typeof err.type === 'string' ? boundFailureText(err.type, PROVIDER_ERROR_CODE_MAX_CHARS) : undefined;
86
+ const parts = [type, status].filter((part): part is string => part !== undefined);
87
+ return `the model call failed${parts.length > 0 ? ` (${parts.join(', ')})` : ''}`;
88
+ }