@genesislcap/ai-assistant 14.467.1 → 14.467.2-GENC-1369.2

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 (49) hide show
  1. package/dist/ai-assistant.api.json +39 -53
  2. package/dist/ai-assistant.d.ts +59 -26
  3. package/dist/dts/components/chat-driver/chat-driver.d.ts +38 -0
  4. package/dist/dts/components/chat-driver/chat-driver.d.ts.map +1 -1
  5. package/dist/dts/index.d.ts +1 -0
  6. package/dist/dts/index.d.ts.map +1 -1
  7. package/dist/dts/main/main.d.ts +1 -20
  8. package/dist/dts/main/main.d.ts.map +1 -1
  9. package/dist/dts/state/debug-event-log.d.ts +17 -1
  10. package/dist/dts/state/debug-event-log.d.ts.map +1 -1
  11. package/dist/dts/state/debug-event-log.test.d.ts +2 -0
  12. package/dist/dts/state/debug-event-log.test.d.ts.map +1 -0
  13. package/dist/dts/utils/condense-history.d.ts +115 -0
  14. package/dist/dts/utils/condense-history.d.ts.map +1 -0
  15. package/dist/dts/utils/condense-history.test.d.ts +2 -0
  16. package/dist/dts/utils/condense-history.test.d.ts.map +1 -0
  17. package/dist/dts/utils/flatten-sub-agent-messages.d.ts +51 -0
  18. package/dist/dts/utils/flatten-sub-agent-messages.d.ts.map +1 -0
  19. package/dist/dts/utils/flatten-sub-agent-messages.test.d.ts +2 -0
  20. package/dist/dts/utils/flatten-sub-agent-messages.test.d.ts.map +1 -0
  21. package/dist/dts/utils/strip-agent-handlers.d.ts +29 -0
  22. package/dist/dts/utils/strip-agent-handlers.d.ts.map +1 -0
  23. package/dist/dts/utils/strip-agent-handlers.test.d.ts +2 -0
  24. package/dist/dts/utils/strip-agent-handlers.test.d.ts.map +1 -0
  25. package/dist/esm/components/chat-driver/chat-driver.js +156 -19
  26. package/dist/esm/components/chat-driver/chat-driver.test.js +225 -1
  27. package/dist/esm/main/main.js +14 -38
  28. package/dist/esm/state/debug-event-log.js +50 -2
  29. package/dist/esm/state/debug-event-log.test.js +67 -0
  30. package/dist/esm/utils/condense-history.js +218 -0
  31. package/dist/esm/utils/condense-history.test.js +297 -0
  32. package/dist/esm/utils/flatten-sub-agent-messages.js +49 -0
  33. package/dist/esm/utils/flatten-sub-agent-messages.test.js +139 -0
  34. package/dist/esm/utils/strip-agent-handlers.js +51 -0
  35. package/dist/esm/utils/strip-agent-handlers.test.js +81 -0
  36. package/dist/tsconfig.tsbuildinfo +1 -1
  37. package/package.json +16 -16
  38. package/src/components/chat-driver/chat-driver.test.ts +276 -1
  39. package/src/components/chat-driver/chat-driver.ts +184 -16
  40. package/src/index.ts +1 -0
  41. package/src/main/main.ts +16 -37
  42. package/src/state/debug-event-log.test.ts +89 -0
  43. package/src/state/debug-event-log.ts +52 -2
  44. package/src/utils/condense-history.test.ts +373 -0
  45. package/src/utils/condense-history.ts +306 -0
  46. package/src/utils/flatten-sub-agent-messages.test.ts +163 -0
  47. package/src/utils/flatten-sub-agent-messages.ts +88 -0
  48. package/src/utils/strip-agent-handlers.test.ts +99 -0
  49. package/src/utils/strip-agent-handlers.ts +52 -0
@@ -0,0 +1,51 @@
1
+ import type { ChatMessage } from '@genesislcap/foundation-ai';
2
+ /**
3
+ * A chat message lifted into the debug-log timeline as a `kind: 'message'` entry.
4
+ *
5
+ * @beta
6
+ */
7
+ export type TimelineMessage = ChatMessage & {
8
+ kind: 'message';
9
+ /**
10
+ * Set only on entries hoisted out of a `subAgentTrace`: the sub-agent's own
11
+ * (un-breadcrumbed) `agentName`, kept for filtering since the displayed
12
+ * `agentName` is rewritten to a `"<parent> › <sub-agent>"` breadcrumb.
13
+ */
14
+ subAgentName?: string;
15
+ /** Delegation depth — absent/0 for top-level, 1 for a sub-agent, 2 for a sub-agent's sub-agent, … */
16
+ subAgentDepth?: number;
17
+ /** Id of the parent tool call that spawned this run — correlates a hoisted message back to its delegation. */
18
+ subAgentOf?: string;
19
+ };
20
+ /**
21
+ * Flatten sub-agent conversations into a single top-level message stream for the
22
+ * debug-log timeline.
23
+ *
24
+ * A tool call that delegates to a sub-agent carries the sub-agent's entire
25
+ * conversation on `toolCall.subAgentTrace`. The live UI reads that nested shape,
26
+ * but a top-to-bottom timeline reads far better with those messages inline next to
27
+ * the turn that produced them. This walks the list and, for every tool call with a
28
+ * `subAgentTrace`, hoists the sub-agent's messages up as their own timeline
29
+ * entries while **removing** the nested copy from the emitted parent tool call — a
30
+ * move, not a copy. The data is relocated, never duplicated: the export stays the
31
+ * same size and a naive `sum(timeline[].cost)` does not double-count (the
32
+ * canonical `sumCosts` total still reads the un-flattened history).
33
+ *
34
+ * Hoisted entries are:
35
+ * - **breadcrumbed**: `agentName` becomes `"<parent> › <sub-agent>"`, composing for
36
+ * nested sub-agents (`"<parent> › <sub> › <subsub>"`); the raw sub-agent name is
37
+ * preserved on `subAgentName`.
38
+ * - **depth-tagged** (`subAgentDepth`) and **correlated** to the spawning tool call
39
+ * (`subAgentOf`), so the delegation tree is reconstructable even when two
40
+ * sub-agents run within the same parent turn.
41
+ *
42
+ * Top-level messages (depth 0) are emitted unchanged apart from the trace strip.
43
+ * Order is preserved within each list; the caller sorts the whole timeline by
44
+ * timestamp afterwards, and every sub-agent message carries its own timestamp, so
45
+ * hoisted entries interleave chronologically between the parent's tool-call and
46
+ * tool-result messages.
47
+ *
48
+ * @internal
49
+ */
50
+ export declare function flattenSubAgentMessages(messages: readonly ChatMessage[], prefix?: string, depth?: number, subAgentOf?: string): TimelineMessage[];
51
+ //# sourceMappingURL=flatten-sub-agent-messages.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"flatten-sub-agent-messages.d.ts","sourceRoot":"","sources":["../../../src/utils/flatten-sub-agent-messages.ts"],"names":[],"mappings":"AAAA,OAAO,KAAK,EAAE,WAAW,EAAE,MAAM,4BAA4B,CAAC;AAE9D;;;;GAIG;AACH,MAAM,MAAM,eAAe,GAAG,WAAW,GAAG;IAC1C,IAAI,EAAE,SAAS,CAAC;IAChB;;;;OAIG;IACH,YAAY,CAAC,EAAE,MAAM,CAAC;IACtB,qGAAqG;IACrG,aAAa,CAAC,EAAE,MAAM,CAAC;IACvB,8GAA8G;IAC9G,UAAU,CAAC,EAAE,MAAM,CAAC;CACrB,CAAC;AAEF;;;;;;;;;;;;;;;;;;;;;;;;;;;;;GA6BG;AACH,wBAAgB,uBAAuB,CACrC,QAAQ,EAAE,SAAS,WAAW,EAAE,EAChC,MAAM,SAAK,EACX,KAAK,SAAI,EACT,UAAU,CAAC,EAAE,MAAM,GAClB,eAAe,EAAE,CA+BnB"}
@@ -0,0 +1,2 @@
1
+ export {};
2
+ //# sourceMappingURL=flatten-sub-agent-messages.test.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"flatten-sub-agent-messages.test.d.ts","sourceRoot":"","sources":["../../../src/utils/flatten-sub-agent-messages.test.ts"],"names":[],"mappings":""}
@@ -0,0 +1,29 @@
1
+ import type { AgentConfig } from '../config/config';
2
+ /**
3
+ * Project an `AgentConfig` down to a JSON-serializable shape for the redux
4
+ * session store. Drops two kinds of field:
5
+ *
6
+ * 1. **Directly function-valued** — the lifecycle/dispatch hooks (`onActivate`,
7
+ * `onDeactivate`, `getDebugSnapshot`, `onUnresolvedTool`) and the function
8
+ * form of the per-turn resolvers (`systemPrompt`, `toolDefinitions`,
9
+ * `displayName`, `provider`, `temperature`, `toolChoice`, `toolHandlers`).
10
+ * 2. **Object "handler bags" whose *values* are functions** — `toolHandlers` in
11
+ * its object form is `{ name: handler }`, so `typeof` is `'object'`, not
12
+ * `'function'`. A by-value check on the field alone misses it, leaking a live
13
+ * handler into store state and tripping redux's serializability check (the
14
+ * regression this guards against — top-level agents tend to use the factory
15
+ * function form, but sub-agents commonly declare an object literal).
16
+ *
17
+ * `subAgents` are projected recursively. Static forms (string / number / array /
18
+ * plain data object) pass through unchanged.
19
+ *
20
+ * Filtering by value rather than an explicit field list means a new
21
+ * function-valued field on `AgentConfig` is handled automatically — no denylist
22
+ * to maintain. The live config on the driver stays the source of truth; the
23
+ * slice only holds this serializable projection and functions are never read
24
+ * back from it.
25
+ *
26
+ * @internal
27
+ */
28
+ export declare function stripAgentHandlers(agent: AgentConfig): Omit<AgentConfig, 'toolHandlers'>;
29
+ //# sourceMappingURL=strip-agent-handlers.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"strip-agent-handlers.d.ts","sourceRoot":"","sources":["../../../src/utils/strip-agent-handlers.ts"],"names":[],"mappings":"AAAA,OAAO,KAAK,EAAE,WAAW,EAAE,MAAM,kBAAkB,CAAC;AAEpD;;;;;;;;;;;;;;;;;;;;;;;;;GAyBG;AACH,wBAAgB,kBAAkB,CAAC,KAAK,EAAE,WAAW,GAAG,IAAI,CAAC,WAAW,EAAE,cAAc,CAAC,CAuBxF"}
@@ -0,0 +1,2 @@
1
+ export {};
2
+ //# sourceMappingURL=strip-agent-handlers.test.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"strip-agent-handlers.test.d.ts","sourceRoot":"","sources":["../../../src/utils/strip-agent-handlers.test.ts"],"names":[],"mappings":""}
@@ -2,7 +2,8 @@ import { __awaiter } from "tslib";
2
2
  import { isObservableAIProviderRegistry, MalformedFunctionCallError, } from '@genesislcap/foundation-ai';
3
3
  import { agenticActivityBus } from '../../channel/ai-activity-bus';
4
4
  import { resolveChatProvider } from '../../config/validate-providers';
5
- import { recordMetaEvent, recordTurnError, recordTurnRetry } from '../../state/debug-event-log';
5
+ import { clearSession, getMetaEvents, mergeMetaEvents, recordMetaEvent, recordTurnError, recordTurnRetry, } from '../../state/debug-event-log';
6
+ import { applyCondensation } from '../../utils/condense-history';
6
7
  import { applyHistoryCap } from '../../utils/history-transform';
7
8
  import { logger } from '../../utils/logger';
8
9
  import { TOOL_FOLD_SYMBOL } from '../../utils/tool-fold';
@@ -32,6 +33,22 @@ const MAX_EMPTY_RESPONSE_RETRIES = 3;
32
33
  // SAME iteration up to this many times before propagating, rather than tearing down the turn.
33
34
  const MAX_SETUP_TRANSPORT_RETRIES = 3;
34
35
  const SUGGESTIONS_HISTORY_WINDOW = 8;
36
+ /**
37
+ * Sub-agent meta events worth folding into the parent's debug timeline: the
38
+ * per-attempt and per-failure signals that do NOT otherwise surface in the
39
+ * sub-agent's (now hoisted) messages — a retried-away malformed/empty attempt
40
+ * produces no message, and the stale-vs-hallucinated/streak diagnostics live only
41
+ * on the event. High-volume, message-derivable events (turn.start/turn.end,
42
+ * provider.selected, context.updated) are intentionally excluded: read the
43
+ * sub-agent's hoisted messages for model/tokens/cost and turn-by-turn activity.
44
+ * See `ChatDriver.invokeSubAgent`.
45
+ */
46
+ const HARVESTED_SUBAGENT_EVENTS = new Set([
47
+ 'turn.retry',
48
+ 'turn.error',
49
+ 'tool.failed',
50
+ 'tool.unresolved',
51
+ ]);
35
52
  /** Name reserved for the cross-agent handoff tool — injected by OrchestratingDriver. */
36
53
  export const REQUEST_CONTINUATION_TOOL = 'request_continuation';
37
54
  /** Paired in history for each `request_continuation` so tool_calls stay balanced for the provider. */
@@ -59,6 +76,40 @@ export class ChatDriver extends EventTarget {
59
76
  /** Epoch ms when the current turn loop began — drives the `turn.end` duration. */
60
77
  this.turnStartedAt = 0;
61
78
  this.pendingInteractions = new Map();
79
+ /**
80
+ * Tool-declared condensation policies, keyed by tool-call id. Populated by
81
+ * `condenseWhen` (first-wins per call); read by `applyCondensation` before each
82
+ * provider call to collapse stale payloads from the model-bound history only.
83
+ * Accumulates across agents on a shared driver (a superseded read collapses no
84
+ * matter which agent made it) and is never cleared — it dies with the driver.
85
+ */
86
+ this.condensePolicies = new Map();
87
+ /**
88
+ * Monotonic model-call counter for the driver's whole lifetime — the age clock
89
+ * for `condenseWhen({ on: { kind: 'age' } })`. The per-`sendMessage` `iterations`
90
+ * loop counter resets to 0 every turn, so it can only measure age WITHIN a
91
+ * single turn; this never resets, so `age` counts model-calls since the result
92
+ * appeared across turn boundaries (each short turn still advances it ≥ 1).
93
+ * Bumped once per tool-loop iteration (provider call).
94
+ */
95
+ this.modelCallSeq = 0;
96
+ /**
97
+ * Monotonic turn counter — the `turnEnd` clock. Bumped once per `sendMessage`
98
+ * (a user turn; NOT per handoff continuation, which is the same request), never
99
+ * reset. `turnEnd` collapses a payload once `turnSeq` exceeds the turn it was
100
+ * created in.
101
+ */
102
+ this.turnSeq = 0;
103
+ /**
104
+ * Monotonic agent-activation counter — the `agentEnd` clock. Advances when the
105
+ * active flow ends: a swap to a different-named agent (`applyAgent`) OR an
106
+ * explicit `releaseAgent` / `completeSubAgent`. A stateful agent re-resolving
107
+ * the same name across turns keeps one activation. `agentEnd` fires for a
108
+ * payload once a LATER activation is current (`callActivation < currentActivation`)
109
+ * — so a re-run of a released agent gets a fresh activation and is NOT
110
+ * collapsed until IT ends.
111
+ */
112
+ this.currentActivation = 0;
62
113
  /** Stack of fold frames — grows when a fold opens, shrinks when it closes. */
63
114
  this.foldStack = [];
64
115
  /** Consecutive fold open/close ops without a real tool call. Reset on real tool execution. */
@@ -264,6 +315,12 @@ export class ChatDriver extends EventTarget {
264
315
  */
265
316
  applyAgent(config) {
266
317
  var _a, _b, _c;
318
+ // A real swap to a different agent begins a NEW activation — the prior agent's
319
+ // flow is over, so its `agentEnd`-tagged payloads become collapsible. Guarded
320
+ // on a name change so re-applying the same agent (e.g. a per-turn re-resolve)
321
+ // does not spuriously advance the clock and prematurely drop its payloads.
322
+ if (config.name !== this.activeAgentName)
323
+ this.currentActivation += 1;
267
324
  this.systemPrompt = config.systemPrompt;
268
325
  if (typeof config.toolDefinitions === 'function') {
269
326
  this.toolDefinitionsFactory = config.toolDefinitions;
@@ -857,6 +914,9 @@ export class ChatDriver extends EventTarget {
857
914
  return { reason: 'done' };
858
915
  this.busy = true;
859
916
  this.beginTurn();
917
+ // A new user turn — advances the `turnEnd` clock (continuations after a
918
+ // handoff stay on the same turn; they go through `continueFromHistory`).
919
+ this.turnSeq += 1;
860
920
  this.subAgentCompletion = undefined;
861
921
  this.subAgentFailure = undefined;
862
922
  this.agentReleaseRequested = false;
@@ -902,11 +962,15 @@ export class ChatDriver extends EventTarget {
902
962
  * Centralised here so fold shortcut dispatch and the main tool loop use the
903
963
  * same context without duplication.
904
964
  *
965
+ * @param activeToolCallId - The id of the tool call this context belongs to, so
966
+ * `condenseWhen` can register against the right call (it stamps the clocks
967
+ * straight off the driver). Absent for dispatch paths with no addressable tool
968
+ * call (e.g. the fold-close handler), where `condenseWhen` is a no-op.
905
969
  * @param traceCapture - Optional per-invocation slot. When provided, the trace
906
970
  * from any sub-agent call is written here rather than to shared instance state,
907
971
  * so parallel tool calls each capture their own trace independently.
908
972
  */
909
- buildHandlerContext(traceCapture) {
973
+ buildHandlerContext(activeToolCallId, traceCapture) {
910
974
  return Object.assign(Object.assign({ requestInteraction: (componentName, data, options) => this.requestInteraction(componentName, data, options) }, (this.subAgentsMap.size > 0 && {
911
975
  requestSubAgent: (name, options) => this.invokeSubAgent(name, options).then(({ outcome, trace }) => {
912
976
  if (traceCapture)
@@ -920,6 +984,9 @@ export class ChatDriver extends EventTarget {
920
984
  return;
921
985
  }
922
986
  this.subAgentCompletion = { result };
987
+ // This activation's flow has ended — advance the clock so its
988
+ // `agentEnd` payloads collapse and a re-run starts a fresh activation.
989
+ this.currentActivation += 1;
923
990
  }, releaseAgent: () => {
924
991
  var _a;
925
992
  if (this.agentReleaseRequested) {
@@ -927,6 +994,42 @@ export class ChatDriver extends EventTarget {
927
994
  return;
928
995
  }
929
996
  this.agentReleaseRequested = true;
997
+ // The stateful flow has wrapped up — advance the clock so its `agentEnd`
998
+ // payloads collapse and a re-run starts a fresh activation.
999
+ this.currentActivation += 1;
1000
+ }, condenseWhen: (policy) => {
1001
+ var _a, _b, _c;
1002
+ // No addressable tool call (e.g. fold-close handler) — nothing to attach to.
1003
+ if (!activeToolCallId)
1004
+ return;
1005
+ if (!(policy === null || policy === void 0 ? void 0 : policy.args) && !(policy === null || policy === void 0 ? void 0 : policy.response)) {
1006
+ logger.warn(`ChatDriver(${(_a = this.activeAgentName) !== null && _a !== void 0 ? _a : 'unknown'}): condenseWhen called with neither args nor response — ignoring`);
1007
+ return;
1008
+ }
1009
+ const triggers = Array.isArray(policy.on) ? policy.on : [policy.on];
1010
+ const validTrigger = (t) => (t === null || t === void 0 ? void 0 : t.kind) === 'superseded'
1011
+ ? typeof t.by === 'string' && t.by.length > 0
1012
+ : (t === null || t === void 0 ? void 0 : t.kind) === 'age'
1013
+ ? typeof t.turns === 'number' && t.turns >= 1
1014
+ : (t === null || t === void 0 ? void 0 : t.kind) === 'turnEnd' || (t === null || t === void 0 ? void 0 : t.kind) === 'agentEnd';
1015
+ if (triggers.length === 0 || !triggers.every(validTrigger)) {
1016
+ logger.warn(`ChatDriver(${(_b = this.activeAgentName) !== null && _b !== void 0 ? _b : 'unknown'}): condenseWhen called with an invalid trigger — ignoring`);
1017
+ return;
1018
+ }
1019
+ // First-wins: a handler declares its policy once per call. A second
1020
+ // declaration for the same tool call is a handler bug, not a refinement.
1021
+ if (this.condensePolicies.has(activeToolCallId)) {
1022
+ logger.error(`ChatDriver(${(_c = this.activeAgentName) !== null && _c !== void 0 ? _c : 'unknown'}): condenseWhen called more than once for the same tool call — keeping the first policy, ignoring this one`);
1023
+ return;
1024
+ }
1025
+ // Stamp the three clocks NOW (read straight off the driver — the handler
1026
+ // runs during the current model-call's tool batch).
1027
+ this.condensePolicies.set(activeToolCallId, {
1028
+ policy,
1029
+ iteration: this.modelCallSeq,
1030
+ turn: this.turnSeq,
1031
+ activation: this.currentActivation,
1032
+ });
930
1033
  } });
931
1034
  }
932
1035
  /**
@@ -937,7 +1040,7 @@ export class ChatDriver extends EventTarget {
937
1040
  */
938
1041
  invokeSubAgent(name, options) {
939
1042
  return __awaiter(this, void 0, void 0, function* () {
940
- var _a, _b, _c, _d;
1043
+ var _a, _b, _c, _d, _e;
941
1044
  const subConfig = this.subAgentsMap.get(name);
942
1045
  if (!subConfig) {
943
1046
  const available = [...this.subAgentsMap.keys()].join(', ') || '(none)';
@@ -960,7 +1063,13 @@ export class ChatDriver extends EventTarget {
960
1063
  ...contextMessages,
961
1064
  ...((_b = subConfig.primerHistory) !== null && _b !== void 0 ? _b : []),
962
1065
  ];
963
- const child = new ChatDriver(this.providerRegistry);
1066
+ // Unique per-invocation id — reused for the lifecycle event bracket below —
1067
+ // and a child session key derived from it. The child files its meta events
1068
+ // under this own bucket (rather than the shared empty-key sink), so they can
1069
+ // be harvested into THIS session on completion and then discarded.
1070
+ const invocationId = crypto.randomUUID();
1071
+ const childSessionKey = `${this.sessionKey}::sub:${invocationId}`;
1072
+ const child = new ChatDriver(this.providerRegistry, {}, [], undefined, undefined, undefined, undefined, undefined, childSessionKey);
964
1073
  // Mark before the first turn so the child forces tool use and reports a
965
1074
  // typed failure (rather than user-facing text) if it never completes.
966
1075
  child.markAsSubAgent();
@@ -994,9 +1103,6 @@ export class ChatDriver extends EventTarget {
994
1103
  };
995
1104
  child.addEventListener('history-updated', forwardTrace);
996
1105
  child.addEventListener('provider-changed', forwardProviderChanged);
997
- // Unique per-invocation id so listeners can pair start/stop reliably even
998
- // when the same sub-agent runs multiple times in parallel.
999
- const invocationId = crypto.randomUUID();
1000
1106
  const chatInputDuringExecution = options === null || options === void 0 ? void 0 : options.chatInputDuringExecution;
1001
1107
  const lifecycleDetail = { name, invocationId, chatInputDuringExecution };
1002
1108
  this.dispatchEvent(new CustomEvent('sub-agent-start', { detail: lifecycleDetail }));
@@ -1056,13 +1162,30 @@ export class ChatDriver extends EventTarget {
1056
1162
  // settled) lifecycle, so the snapshot/completion reads below still work.
1057
1163
  child.dispose();
1058
1164
  this.dispatchEvent(new CustomEvent('sub-agent-stop', { detail: lifecycleDetail }));
1165
+ // Capture the child's diagnostics into THIS session, then ALWAYS discard its
1166
+ // transient bucket — done in the `finally` so an unexpected `sendMessage`
1167
+ // rejection (which propagates out of this method) can't orphan the bucket in
1168
+ // the registry, and a crashed sub-agent still leaves its turns/events behind.
1169
+ // Forward the child's per-LLM-call snapshots so they show as `kind:'turn'`
1170
+ // entries in the exported debug log, re-numbered under the activating parent turn.
1171
+ this.forwardSubAgentSnapshots(child.getTurnSnapshots());
1172
+ // Fold the sub-agent's high-value meta events (retries/errors/tool failures —
1173
+ // see HARVESTED_SUBAGENT_EVENTS) into THIS session, preserving their original
1174
+ // timestamps so they interleave within the subagent.started→completed/failed
1175
+ // bracket. Each is breadcrumbed `"<parent> › <sub-agent>"`, composing for a
1176
+ // nested sub-agent whose own breadcrumb the child already merged.
1177
+ const parentName = (_c = this.activeAgentName) !== null && _c !== void 0 ? _c : '?';
1178
+ const harvested = getMetaEvents(childSessionKey)
1179
+ .filter((e) => HARVESTED_SUBAGENT_EVENTS.has(e.type))
1180
+ .map((e) => {
1181
+ var _a;
1182
+ const existing = (_a = e.detail) === null || _a === void 0 ? void 0 : _a.subAgent;
1183
+ return Object.assign(Object.assign({}, e), { detail: Object.assign(Object.assign({}, e.detail), { subAgent: `${parentName} › ${existing !== null && existing !== void 0 ? existing : name}` }) });
1184
+ });
1185
+ mergeMetaEvents(this.sessionKey, harvested);
1186
+ clearSession(childSessionKey);
1059
1187
  }
1060
1188
  const trace = child.getHistory();
1061
- // Forward the child's per-LLM-call snapshots onto this (parent) driver's
1062
- // buffer so they show as `kind:'turn'` entries in the exported debug log,
1063
- // re-numbered under the activating parent turn. Runs for both success and
1064
- // failure so the sub-agent's turns are always visible.
1065
- this.forwardSubAgentSnapshots(child.getTurnSnapshots());
1066
1189
  if (timedOut) {
1067
1190
  // Same failure shape as any other non-completion — the parent handler
1068
1191
  // recovers on its existing `{ ok: false }` branch. Recorded under THIS
@@ -1081,7 +1204,7 @@ export class ChatDriver extends EventTarget {
1081
1204
  // provider ignored forced tool use and returned text). The previous
1082
1205
  // final-text fallback is intentionally gone — sub-agents return a
1083
1206
  // structured outcome only, and the parent handler decides how to recover.
1084
- const reason = (_d = (_c = child.getSubAgentFailure()) === null || _c === void 0 ? void 0 : _c.reason) !== null && _d !== void 0 ? _d : 'max_iterations';
1207
+ const reason = (_e = (_d = child.getSubAgentFailure()) === null || _d === void 0 ? void 0 : _d.reason) !== null && _e !== void 0 ? _e : 'max_iterations';
1085
1208
  // Record under THIS (parent) driver's session so the failure lands on the
1086
1209
  // user-visible debug-log timeline — the child ran under its own session key.
1087
1210
  // This is also the only telemetry for the defensive default above, where the
@@ -1215,7 +1338,7 @@ export class ChatDriver extends EventTarget {
1215
1338
  this.toolHandlers = newHandlers;
1216
1339
  }
1217
1340
  /** Open a fold: push a stack frame, swap the tool set, return the response message. */
1218
- openFold(foldName, fold, args) {
1341
+ openFold(foldName, fold, args, activeToolCallId) {
1219
1342
  // Shortcut dispatch: model passed inner tool args directly, e.g.
1220
1343
  // trading_tools({ search_trades: { side: "BUY" } })
1221
1344
  for (const key of Object.keys(args)) {
@@ -1229,7 +1352,7 @@ export class ChatDriver extends EventTarget {
1229
1352
  const innerArgs = typeof args[key] === 'object' && args[key] !== null
1230
1353
  ? args[key]
1231
1354
  : {};
1232
- return innerHandler(innerArgs, this.buildHandlerContext()).then((r) => typeof r === 'string' ? r : JSON.stringify(r));
1355
+ return innerHandler(innerArgs, this.buildHandlerContext(activeToolCallId)).then((r) => typeof r === 'string' ? r : JSON.stringify(r));
1233
1356
  }
1234
1357
  }
1235
1358
  // Normal two-step open
@@ -1308,6 +1431,10 @@ export class ChatDriver extends EventTarget {
1308
1431
  let firstLlmCall = !!currentInput;
1309
1432
  while (iterations < this.maxToolIterations) {
1310
1433
  iterations += 1;
1434
+ // Monotonic across the driver's life — the age clock. Unlike `iterations`,
1435
+ // it is NOT reset per turn and NOT decremented for folds, so `age` keeps
1436
+ // counting model-calls once a turn ends. (See the `modelCallSeq` field.)
1437
+ this.modelCallSeq += 1;
1311
1438
  // A cancel (or dispose) that landed while the previous iteration's tool
1312
1439
  // batch was running takes effect here — before issuing another LLM call.
1313
1440
  // An abort during the call itself is handled by the catch below.
@@ -1376,9 +1503,19 @@ export class ChatDriver extends EventTarget {
1376
1503
  const primer = [...((_b = this.primerHistory) !== null && _b !== void 0 ? _b : []), ...(transientPrimer !== null && transientPrimer !== void 0 ? transientPrimer : [])];
1377
1504
  const baseHistory = firstLlmCall ? this.history.slice(0, -1) : this.history;
1378
1505
  firstLlmCall = false;
1506
+ // Collapse stale tool payloads this agent declared via `condenseWhen`, then
1507
+ // pipe through any externally-set transform (e.g. multi-agent masking).
1508
+ // Both run on a copy — stored history is untouched.
1509
+ const condensedHistory = applyCondensation([...baseHistory], this.condensePolicies, {
1510
+ modelCall: this.modelCallSeq,
1511
+ turn: this.turnSeq,
1512
+ // A call's activation has ended once a later activation is current — a
1513
+ // swap or release/complete both advance the counter.
1514
+ activationEnded: (activation) => activation < this.currentActivation,
1515
+ }, (detail) => recordMetaEvent(this.sessionKey, 'context.condensed', detail));
1379
1516
  const historyForProvider = this.providerHistoryTransform
1380
- ? this.providerHistoryTransform([...baseHistory])
1381
- : baseHistory;
1517
+ ? this.providerHistoryTransform(condensedHistory)
1518
+ : condensedHistory;
1382
1519
  const historyForCall = [...primer, ...historyForProvider];
1383
1520
  const systemPrompt = malformedAttempts > 0
1384
1521
  ? `${baseSystemPrompt !== null && baseSystemPrompt !== void 0 ? baseSystemPrompt : ''}\n\nIMPORTANT: Use only the structured function-call API to invoke tools. Do not write Python code or use Python-style syntax to call tools.`
@@ -1589,7 +1726,7 @@ export class ChatDriver extends EventTarget {
1589
1726
  });
1590
1727
  return;
1591
1728
  }
1592
- const content = yield this.openFold(tc.name, fold, tc.args);
1729
+ const content = yield this.openFold(tc.name, fold, tc.args, tc.id);
1593
1730
  executedById.set(tc.id, { toolCallId: tc.id, content });
1594
1731
  // Fold open/close does NOT count as a real iteration — decrement to compensate
1595
1732
  iterations -= 1;
@@ -1700,7 +1837,7 @@ export class ChatDriver extends EventTarget {
1700
1837
  // Real tool execution
1701
1838
  try {
1702
1839
  const traceCapture = {};
1703
- const result = yield handler(tc.args, this.buildHandlerContext(traceCapture));
1840
+ const result = yield handler(tc.args, this.buildHandlerContext(tc.id, traceCapture));
1704
1841
  const content = typeof result === 'string' ? result : JSON.stringify(result);
1705
1842
  executedById.set(tc.id, {
1706
1843
  toolCallId: tc.id,