@genesislcap/ai-assistant 15.84.2 → 15.85.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 (50) hide show
  1. package/dist/ai-assistant.api.json +2175 -138
  2. package/dist/ai-assistant.d.ts +420 -43
  3. package/dist/chat-driver.cjs +780 -210
  4. package/dist/chat-driver.cjs.map +4 -4
  5. package/dist/chat-driver.mjs +780 -209
  6. package/dist/chat-driver.mjs.map +4 -4
  7. package/dist/custom-elements.json +2902 -1812
  8. package/dist/dts/components/agent-session/agent-session.d.ts +50 -2
  9. package/dist/dts/components/agent-session/agent-session.d.ts.map +1 -1
  10. package/dist/dts/components/ai-driver/ai-driver.d.ts +59 -1
  11. package/dist/dts/components/ai-driver/ai-driver.d.ts.map +1 -1
  12. package/dist/dts/components/ai-driver/base-driver-decorator.d.ts +13 -1
  13. package/dist/dts/components/ai-driver/base-driver-decorator.d.ts.map +1 -1
  14. package/dist/dts/components/chat-driver/chat-driver.constants.d.ts +0 -8
  15. package/dist/dts/components/chat-driver/chat-driver.constants.d.ts.map +1 -1
  16. package/dist/dts/components/chat-driver/chat-driver.d.ts +74 -15
  17. package/dist/dts/components/chat-driver/chat-driver.d.ts.map +1 -1
  18. package/dist/dts/components/orchestrating-driver/hand-off.d.ts +20 -0
  19. package/dist/dts/components/orchestrating-driver/hand-off.d.ts.map +1 -0
  20. package/dist/dts/components/orchestrating-driver/orchestrating-driver.d.ts +121 -22
  21. package/dist/dts/components/orchestrating-driver/orchestrating-driver.d.ts.map +1 -1
  22. package/dist/dts/config/config.d.ts +66 -0
  23. package/dist/dts/config/config.d.ts.map +1 -1
  24. package/dist/dts/main/main.d.ts +3 -0
  25. package/dist/dts/main/main.d.ts.map +1 -1
  26. package/dist/dts/main/main.types.d.ts +6 -0
  27. package/dist/dts/main/main.types.d.ts.map +1 -1
  28. package/dist/dts/react.d.ts +4 -1
  29. package/dist/dts/state/persistence/session-persister.d.ts.map +1 -1
  30. package/dist/dts/state/persistence/session-snapshot.d.ts +33 -1
  31. package/dist/dts/state/persistence/session-snapshot.d.ts.map +1 -1
  32. package/dist/dts/utils/condense-history.d.ts +10 -3
  33. package/dist/dts/utils/condense-history.d.ts.map +1 -1
  34. package/dist/esm/components/agent-session/agent-session.js +128 -7
  35. package/dist/esm/components/ai-driver/ai-driver.js +3 -0
  36. package/dist/esm/components/ai-driver/base-driver-decorator.js +24 -0
  37. package/dist/esm/components/chat-driver/chat-driver.constants.js +0 -8
  38. package/dist/esm/components/chat-driver/chat-driver.js +247 -74
  39. package/dist/esm/components/orchestrating-driver/hand-off.js +59 -0
  40. package/dist/esm/components/orchestrating-driver/orchestrating-driver.js +480 -299
  41. package/dist/esm/main/main.js +32 -6
  42. package/dist/esm/main/main.types.js +6 -0
  43. package/dist/esm/state/debug-event-log.js +1 -1
  44. package/dist/esm/state/persistence/session-persister.js +55 -8
  45. package/dist/esm/state/persistence/session-snapshot.js +12 -6
  46. package/dist/esm/utils/condense-history.js +13 -4
  47. package/dist/react.cjs +16 -1
  48. package/dist/react.mjs +16 -1
  49. package/docs/sub_agent.md +1 -1
  50. package/package.json +17 -17
@@ -204,6 +204,9 @@ avoidTreeShaking(AiChatMarkdown, AiChatInteractionWrapper, AiHaloOverlay, AiWave
204
204
  *
205
205
  * @fires chat-header-mousedown - Fired when the user presses the chat header in `popout-mode="expand"` (for drag-to-popout hosts). Bubbles and composed. detail: `ChatHeaderMouseDownDetail`
206
206
  * @fires session-cleared - Fired after the session is wiped (the lifecycle menu's Clear). The payload is captured before teardown so the host can clean up any external state the active agent was working on. Bubbles and composed. detail: `SessionClearedDetail`
207
+ * @fires handoff-started - Fired when an agent's `handOff` takes effect and its target becomes active. Bubbles and composed. detail: `HandOffFrame`
208
+ * @fires handoff-returned - Fired when a hand-off ends and its caller is active again; `detail.outcome` says how it ended. Bubbles and composed. detail: `HandOffFrame`
209
+ * @fires handoff-unwound - Fired when a hand-off ends with no hand-back: the chat was cleared, the agents changed, or a restore found it could not resume. Bubbles and composed. detail: `HandOffFrame`
207
210
  *
208
211
  * @beta
209
212
  */
@@ -2059,7 +2062,7 @@ let FoundationAiAssistant = FoundationAiAssistant_1 = class FoundationAiAssistan
2059
2062
  return trailingInteractionRow(this.messages);
2060
2063
  }
2061
2064
  agentsChanged() {
2062
- var _a, _b, _c;
2065
+ var _a, _b, _c, _d, _e;
2063
2066
  // Guard: driver doesn't exist yet during connectedCallback — createDriver/wireDriver handle that.
2064
2067
  if (!this.driver)
2065
2068
  return;
@@ -2105,6 +2108,8 @@ let FoundationAiAssistant = FoundationAiAssistant_1 = class FoundationAiAssistan
2105
2108
  if (registryDriver && registryDriver !== this.driver && registryDriver.isBusy())
2106
2109
  return;
2107
2110
  const history = (_c = (_b = (_a = this.driver).getRawHistory) === null || _b === void 0 ? void 0 : _b.call(_a)) !== null && _c !== void 0 ? _c : [];
2111
+ // A hand-off cannot outlive its callers' machines, which the teardown disposes (GENC-1737).
2112
+ void ((_e = (_d = this.driver).unwindHandOffs) === null || _e === void 0 ? void 0 : _e.call(_d, 'state-lost'));
2108
2113
  // Fire the active agent's `onDeactivate` so machines / per-agent state get
2109
2114
  // torn down before the replacement driver instantiates them again.
2110
2115
  this.teardownDriver(key);
@@ -2178,6 +2183,7 @@ let FoundationAiAssistant = FoundationAiAssistant_1 = class FoundationAiAssistan
2178
2183
  const orchestrator = new OrchestratingDriver(this.providerRegistry, agents, {
2179
2184
  sessionKey: (_b = this.getStateKey()) !== null && _b !== void 0 ? _b : '',
2180
2185
  maxHandoffs: agent.maxHandoffs,
2186
+ maxHandOffDepth: agent.maxHandOffDepth,
2181
2187
  classifierHistoryLength: agent.classifierHistoryLength,
2182
2188
  classifierRetries: agent.classifierRetries,
2183
2189
  maxToolIterations: agent.maxToolIterations,
@@ -2426,6 +2432,24 @@ let FoundationAiAssistant = FoundationAiAssistant_1 = class FoundationAiAssistan
2426
2432
  driver.addEventListener('pinned-changed', onPinnedChanged);
2427
2433
  driver.addEventListener('flow-owner-changed', onFlowOwnerChanged);
2428
2434
  cleanups.push(() => driver.removeEventListener('orchestrating-start', onOrchStart), () => driver.removeEventListener('orchestrating-stop', onOrchStop), () => driver.removeEventListener('agent-changed', onAgentChanged), () => driver.removeEventListener('pinned-changed', onPinnedChanged), () => driver.removeEventListener('flow-owner-changed', onFlowOwnerChanged));
2435
+ // Hand-offs (GENC-1737): told to the host, and saved, since the stack is part of the session.
2436
+ // Not one ended as `cleared`: a Clear drops the session, and a save could land after it. (A
2437
+ // turn that ends an orphaned hand-off as `cleared` saves anyway.)
2438
+ const onHandOff = (e) => {
2439
+ var _a, _b;
2440
+ const detail = e.detail;
2441
+ if (((_a = detail === null || detail === void 0 ? void 0 : detail.outcome) === null || _a === void 0 ? void 0 : _a.kind) !== 'cleared')
2442
+ (_b = this.persister()) === null || _b === void 0 ? void 0 : _b.scheduleSave();
2443
+ this.dispatchEvent(new CustomEvent(e.type, { bubbles: true, composed: true, detail }));
2444
+ };
2445
+ for (const type of [
2446
+ AiAssistantEvent.HandOffStarted,
2447
+ AiAssistantEvent.HandOffReturned,
2448
+ AiAssistantEvent.HandOffUnwound,
2449
+ ]) {
2450
+ driver.addEventListener(type, onHandOff);
2451
+ cleanups.push(() => driver.removeEventListener(type, onHandOff));
2452
+ }
2429
2453
  this.driverCleanup = () => {
2430
2454
  for (const fn of cleanups)
2431
2455
  fn();
@@ -3496,7 +3520,7 @@ let FoundationAiAssistant = FoundationAiAssistant_1 = class FoundationAiAssistan
3496
3520
  */
3497
3521
  clearSession() {
3498
3522
  return __awaiter(this, void 0, void 0, function* () {
3499
- var _a, _b, _c, _d, _e, _f, _g, _h, _j;
3523
+ var _a, _b, _c, _d, _e, _f, _g, _h, _j, _k, _l;
3500
3524
  if ((_a = this.driver) === null || _a === void 0 ? void 0 : _a.isBusy())
3501
3525
  return; // the menu only opens when idle; defensive
3502
3526
  const key = this.getStateKey();
@@ -3516,9 +3540,11 @@ let FoundationAiAssistant = FoundationAiAssistant_1 = class FoundationAiAssistan
3516
3540
  // proved, and nothing else records it. Banking it is what stops the next turn rewriting the
3517
3541
  // project's row down to itself and losing the spend up to the clear (GENC-1475).
3518
3542
  const discarded = this.sessionUsage;
3543
+ // Each hand-off ends as `cleared`, telling its waiting caller while it is still live (GENC-1737).
3544
+ yield ((_g = (_f = this.driver) === null || _f === void 0 ? void 0 : _f.unwindHandOffs) === null || _g === void 0 ? void 0 : _g.call(_f, 'cleared'));
3519
3545
  if (this.driver)
3520
3546
  this.teardownDriver(key);
3521
- (_f = this._sessionRef) === null || _f === void 0 ? void 0 : _f.actions.aiAssistant.resetSession();
3547
+ (_h = this._sessionRef) === null || _h === void 0 ? void 0 : _h.actions.aiAssistant.resetSession();
3522
3548
  // Same reason as in `switchSession`: the compose box and its note were about the
3523
3549
  // conversation just discarded.
3524
3550
  this.resetDebugLogSubmitState();
@@ -3533,7 +3559,7 @@ let FoundationAiAssistant = FoundationAiAssistant_1 = class FoundationAiAssistan
3533
3559
  // Restore-core is owned by the shared per-session persister (GENC-1351 §5.10):
3534
3560
  // it drops the snapshot + resets the carry-forward cache. Diagnostics is
3535
3561
  // append-only and handled separately below.
3536
- yield ((_g = this.persister()) === null || _g === void 0 ? void 0 : _g.clearRestoreCore());
3562
+ yield ((_j = this.persister()) === null || _j === void 0 ? void 0 : _j.clearRestoreCore());
3537
3563
  this.confirmingClear = false;
3538
3564
  this.composerMenus.closeImmediate();
3539
3565
  this.logMeta('session.cleared', { agentName });
@@ -3542,8 +3568,8 @@ let FoundationAiAssistant = FoundationAiAssistant_1 = class FoundationAiAssistan
3542
3568
  // forward-capture cursors and flush, so the just-logged `session.cleared`
3543
3569
  // event becomes the stream's roll boundary: exactly like a page reload, the
3544
3570
  // new conversation begins a fresh section within the same lifetime log (§5.8).
3545
- resetDiagnosticsCursorsFor((_h = this.getStateKey()) !== null && _h !== void 0 ? _h : '');
3546
- (_j = this.persister()) === null || _j === void 0 ? void 0 : _j.scheduleDiagnostics();
3571
+ resetDiagnosticsCursorsFor((_k = this.getStateKey()) !== null && _k !== void 0 ? _k : '');
3572
+ (_l = this.persister()) === null || _l === void 0 ? void 0 : _l.scheduleDiagnostics();
3547
3573
  this.dispatchEvent(new CustomEvent(AiAssistantEvent.SessionCleared, {
3548
3574
  bubbles: true,
3549
3575
  composed: true,
@@ -24,6 +24,12 @@ export const AiAssistantEvent = {
24
24
  SessionCleared: 'session-cleared',
25
25
  /** Header pressed in `popout-mode="expand"` (drag-to-popout hosts). `detail`: `ChatHeaderMouseDownDetail`. */
26
26
  ChatHeaderMouseDown: 'chat-header-mousedown',
27
+ /** An agent's hand-off started (GENC-1737). `detail`: `HandOffFrame`. */
28
+ HandOffStarted: 'handoff-started',
29
+ /** A hand-off ended and its caller is active again. `detail`: `HandOffFrame`, with `outcome`. */
30
+ HandOffReturned: 'handoff-returned',
31
+ /** A hand-off ended with no hand-back. `detail`: `HandOffFrame`, with `outcome`. */
32
+ HandOffUnwound: 'handoff-unwound',
27
33
  };
28
34
  /**
29
35
  * Registry of all available animations with their display metadata.
@@ -266,7 +266,7 @@ export const DEBUG_LOG_README = [
266
266
  'Sub-agent meta events: a sub-agent\'s own turn.retry/turn.error/tool.failed/tool.unresolved events are merged into this same timeline, tagged with `detail.subAgent` — a `"<parent> › <sub-agent>"` breadcrumb that composes when nested (e.g. `"UI Builder › Planner › Grounding"`) — and interleaved by their original timestamps within the subagent.started→completed/failed bracket. These are the per-attempt/per-failure signals that do NOT appear among the sub-agent\'s (hoisted) messages: a malformed/empty attempt that gets retried produces no message, and the stale-vs-hallucinated split and streak counts live only on the event. A sub-agent\'s high-volume, message-derivable events (turn.start/turn.end, provider.selected, context.updated) are intentionally NOT merged — read its hoisted messages for model/tokens/cost and turn-by-turn activity, and the bracketing subagent.* events for the run\'s span.',
267
267
  "`meta` holds context captured at export time: agentSummary (full agent configs), context (active model, contextTokens/contextLimit/contextUsagePercent for the last call, and the session totals — sessionCostUsd, sessionTokensConsumed, and the four-bucket sessionUsage lifted to the top of this log), activeDebugSnapshot (the active agent's `getDebugSnapshot()` taken fresh at export — reflects state NOW, which may have advanced beyond the last turn's agentSnapshot), debug (optional host-supplied debug state), host, and the export timestamp.",
268
268
  'Note the two different scopes in `meta.context`: `contextTokens` is the prompt size of the LAST call (against `contextLimit`, the model context window), while `sessionUsage`/`sessionTokensConsumed` are cumulative BILLED throughput. Every turn resends the conversation, so the cumulative figure counts each turn’s prompt again in the next turn’s and is expected to dwarf the context size — that is not double-counting.',
269
- "kind:'event' type 'routing.decided' — how ONE turn was routed, emitted by the orchestrating driver before the agent runs (and again after every handoff). `decision` is why that agent was chosen: 'classified' (the classifier ran and picked one), 'no-match' (it ran and answered -1 — nothing listed covers the request), 'ballot-exhausted' (every eligible specialist had already handed off this turn, so the ballot was empty and NO call was made — distinct from 'no-match', where one ran and declined), 'pinned' (a user pin, an agent named for that one message by the send's `agent` option, or an active stateful flow owned the turn, so no call was made), 'single-candidate'/'no-specialists' (configuration left nothing to decide), 'provider-unavailable' (the registry default exposes no chat()), 'classifier-failed' (every attempt threw — `error` carries the last one). `to` names the agent and `fallback` says whether that agent is the fallback. `candidates` is what was actually ON THE BALLOT, in ballot order, so `index` — the classifier's own answer — indexes it directly; `excluded` names the specialists left off because they already handed off this turn, so the full eligible set is `candidates` ∪ `excluded`. `candidates` is absent on the four decisions that put nothing to a vote — 'pinned', 'single-candidate', 'no-specialists', 'ballot-exhausted'. It IS present on 'provider-unavailable', which is not an exception worth removing: the ballot was built there, it simply never got sent. `attempts` covers the retry ladder. `durationMs` is on EVERY routing.decided, and is the field to read when a decision looks wrong: the four decisions that can reach the fallback are told apart by the clock as much as by their name, because a fallback routing that resolved in tens of milliseconds made no network call at all whatever its `decision` says.",
269
+ "kind:'event' type 'routing.decided' — how ONE user turn was routed, emitted by the orchestrating driver before the agent runs. Only a user turn is routed: a hand-off names its target, so the agents a hand-off runs show up as agent.handoff events, not as more routing.decided. `decision` is why that agent was chosen: 'classified' (the classifier ran and picked one), 'no-match' (it ran and answered -1 — nothing listed covers the request), 'pinned' (a user pin, an agent named for that one message by the send's `agent` option, or an active stateful flow owned the turn, so no call was made), 'single-candidate'/'no-specialists' (configuration left nothing to decide), 'provider-unavailable' (the registry default exposes no chat()), 'classifier-failed' (every attempt threw — `error` carries the last one). `to` names the agent and `fallback` says whether that agent is the fallback. `candidates` is what was ON THE BALLOT — every classifier-eligible specialist — in ballot order, so `index` — the classifier's own answer — indexes it directly. `candidates` is absent on the three decisions that put nothing to a vote — 'pinned', 'single-candidate', 'no-specialists'. It IS present on 'provider-unavailable', which is not an exception worth removing: the ballot was built there, it simply never got sent. `attempts` covers the retry ladder. `durationMs` is on EVERY routing.decided, and is the field to read when a decision looks wrong: the decisions that can reach the fallback ('no-match', 'no-specialists', 'provider-unavailable', 'classifier-failed') are told apart by the clock as much as by their name, because a fallback routing that resolved in tens of milliseconds made no network call at all whatever its `decision` says.",
270
270
  "kind:'event' type 'routing.decided'.`usage` — what that classifier call cost, in the same four-bucket + costUsd shape as `sessionUsage`, plus `model`. Absent on the decisions that make no call (see 'pinned' and the configuration ones). THIS SPEND IS NOT IN `sessionUsage`: a classifier call never enters the transcript and `sessionUsage` is derived from the transcript, so classifier cost is missing from the session total by construction, not by accident. Sum these events to close that gap — it is the one component of a session's real spend that the totals cannot see.",
271
271
  'To debug a failure: find the last turn.error or tool.failed, then read upward for the user message, the turn(s), and the agent/provider/state events that led into it. For a turn that went to the wrong agent, read its routing.decided first — `decision` says whether anything was actually asked.',
272
272
  ];
@@ -28,6 +28,38 @@ function savedCheckpoints(snapshot) {
28
28
  return snapshot.checkpoints;
29
29
  return snapshot.checkpoint ? [snapshot.checkpoint] : [];
30
30
  }
31
+ /** The hand-offs in progress and their waiting callers' states, to save (GENC-1737). */
32
+ function handOffsOf(driver) {
33
+ var _a, _b, _c, _d, _e, _f;
34
+ return {
35
+ waitingCallers: (_b = (_a = driver === null || driver === void 0 ? void 0 : driver.serializeWaitingCallers) === null || _a === void 0 ? void 0 : _a.call(driver)) !== null && _b !== void 0 ? _b : {},
36
+ handoffStack: (_d = (_c = driver === null || driver === void 0 ? void 0 : driver.getHandOffStack) === null || _c === void 0 ? void 0 : _c.call(driver)) !== null && _d !== void 0 ? _d : [],
37
+ handOffData: (_f = (_e = driver === null || driver === void 0 ? void 0 : driver.getHandOffData) === null || _e === void 0 ? void 0 : _e.call(driver)) !== null && _f !== void 0 ? _f : {},
38
+ };
39
+ }
40
+ /**
41
+ * Restore the saved hand-offs (GENC-1737), and settle what the restore re-activates and locks:
42
+ * - the top frame's target had finished, or has gone: its caller resumes, and the target's lock
43
+ * goes. The hand-back happens either way; the note, which runs the caller's turn, comes only
44
+ * when its turn would run on any resume;
45
+ * - a plain target's hand-off could not be resumed: it held the lock only for that hand-off, so
46
+ * the lock goes, and nothing is re-activated.
47
+ */
48
+ function restoreHandOffs(driver, snapshot, restored) {
49
+ var _a, _b, _c, _d, _e, _f;
50
+ // The data first: the stack drops it for a caller that cannot resume.
51
+ (_a = driver === null || driver === void 0 ? void 0 : driver.restoreHandOffData) === null || _a === void 0 ? void 0 : _a.call(driver, snapshot.handOffData, !!snapshot.interrupted);
52
+ const { resume, droppedLocks } = (_d = (_b = driver === null || driver === void 0 ? void 0 : driver.restoreHandOffStack) === null || _b === void 0 ? void 0 : _b.call(driver, snapshot.handoffStack, (_c = snapshot.agentSnapshots) !== null && _c !== void 0 ? _c : {}, !!snapshot.interrupted)) !== null && _d !== void 0 ? _d : {};
53
+ if (resume) {
54
+ const note = ((_e = driver === null || driver === void 0 ? void 0 : driver.canResumeRestoredFlow) === null || _e === void 0 ? void 0 : _e.call(driver, resume.agent)) ? resume.note : undefined;
55
+ return { flowOwner: resume.agent, resumeNote: note, savedLock: null, handBack: true };
56
+ }
57
+ const savedLock = (_f = snapshot.flowOwnerAgentName) !== null && _f !== void 0 ? _f : null;
58
+ if (savedLock && (droppedLocks === null || droppedLocks === void 0 ? void 0 : droppedLocks.includes(savedLock))) {
59
+ return { flowOwner: null, savedLock: null, handBack: false };
60
+ }
61
+ return Object.assign(Object.assign({}, restored), { savedLock, handBack: false });
62
+ }
31
63
  /** Hand restored checkpoints back to the driver: none clears what it holds. */
32
64
  function restoreCheckpoints(driver, saved) {
33
65
  var _a;
@@ -347,7 +379,9 @@ export class SessionPersister {
347
379
  // `onActivate` ran), not via the host's `agents` — a host whose `agents` getter
348
380
  // mints fresh configs (the showcase) would otherwise hand us a never-activated
349
381
  // closure whose `serialize()` returns undefined.
350
- const liveSnapshots = {};
382
+ // Callers waiting in a hand-off are live too, and resume from their state (GENC-1737).
383
+ const { waitingCallers, handoffStack, handOffData } = handOffsOf(driver);
384
+ const liveSnapshots = Object.assign({}, waitingCallers);
351
385
  const liveActive = driver === null || driver === void 0 ? void 0 : driver.getActiveAgent();
352
386
  if (liveActive === null || liveActive === void 0 ? void 0 : liveActive.serialize) {
353
387
  try {
@@ -364,7 +398,7 @@ export class SessionPersister {
364
398
  // even though a flow is mid-stream. Writing `{}` would wipe the machine and
365
399
  // reset the flow on the next reload — so carry the flow-owner's last-known-good
366
400
  // image forward (GENC-1351 §6). Pure + unit-tested.
367
- const { snapshots: agentSnapshots, cache } = reconcileAgentSnapshots(liveSnapshots, s.flowOwnerAgentName, this._cache);
401
+ const { snapshots: agentSnapshots, cache } = reconcileAgentSnapshots(liveSnapshots, s.flowOwnerAgentName, this._cache, handoffStack.map((f) => f.caller));
368
402
  this._cache = cache;
369
403
  const resumable = yield this.resolveResumable(liveActive, key, agentSnapshots, messages, s);
370
404
  // Report a broken episode at the first SAFE moment, which is what `resumable` decides.
@@ -422,6 +456,9 @@ export class SessionPersister {
422
456
  condensePolicies: this.condensePoliciesToPersist(driver, frozenMessages, {
423
457
  flowDropped: true,
424
458
  }),
459
+ // Kept so a restore can say so: a frozen session drops every agent's state, and with it
460
+ // any hand-off's chance to resume (GENC-1737).
461
+ handoffStack,
425
462
  });
426
463
  // Skip the write when the frozen boundary has not moved — a flow autosaves on every
427
464
  // transcript change, and while it runs the last user message (and therefore the
@@ -496,6 +533,8 @@ export class SessionPersister {
496
533
  condensePolicies: this.condensePoliciesToPersist(driver, messages),
497
534
  autopilot: (_h = autopilotOf(driver)) === null || _h === void 0 ? void 0 : _h.getPersistedMode(),
498
535
  checkpoints: checkpointsOf(driver),
536
+ handoffStack,
537
+ handOffData,
499
538
  });
500
539
  // Observability (GENC-1351): record what each autosave captured for stateful
501
540
  // agents — `agents: []` here means no machine snapshot was taken (the bug to
@@ -567,7 +606,7 @@ export class SessionPersister {
567
606
  */
568
607
  restore() {
569
608
  return __awaiter(this, void 0, void 0, function* () {
570
- var _a, _b, _c, _d, _e, _f, _g, _h, _j;
609
+ var _a, _b, _c, _d, _e, _f, _g, _h;
571
610
  const { provider } = this.deps.getPersistence();
572
611
  if (!provider)
573
612
  return undefined;
@@ -607,8 +646,8 @@ export class SessionPersister {
607
646
  // whose owner this driver knows; resume it as well when the reload interrupted a turn and the
608
647
  // owner has not opted out. Otherwise the restore is exactly what it was.
609
648
  const savedOwner = snapshot.interrupted ? null : ((_a = snapshot.flowOwnerAgentName) !== null && _a !== void 0 ? _a : null);
610
- const flowOwner = savedOwner && ((_b = driver === null || driver === void 0 ? void 0 : driver.canRestoreFlowOwner) === null || _b === void 0 ? void 0 : _b.call(driver, savedOwner)) ? savedOwner : null;
611
- const resumeNote = flowOwner && ((_c = driver === null || driver === void 0 ? void 0 : driver.canResumeRestoredFlow) === null || _c === void 0 ? void 0 : _c.call(driver, flowOwner))
649
+ let flowOwner = savedOwner && ((_b = driver === null || driver === void 0 ? void 0 : driver.canRestoreFlowOwner) === null || _b === void 0 ? void 0 : _b.call(driver, savedOwner)) ? savedOwner : null;
650
+ let resumeNote = flowOwner && ((_c = driver === null || driver === void 0 ? void 0 : driver.canResumeRestoredFlow) === null || _c === void 0 ? void 0 : _c.call(driver, flowOwner))
612
651
  ? buildResumeNote(trimmed, snapshot.messages.slice(bounded.length))
613
652
  : undefined;
614
653
  const ref = this.deps.getStore();
@@ -625,13 +664,18 @@ export class SessionPersister {
625
664
  // matter when the agent re-activates. The flow-owner lock self-heals on
626
665
  // activation (OrchestratingDriver.installAgent re-sets it).
627
666
  driver === null || driver === void 0 ? void 0 : driver.primeRestoredAgentState(snapshot.agentSnapshots);
667
+ // Hand-offs (GENC-1737), before the condense policies, which have to know who is waiting. They
668
+ // can change who is re-activated and resumed, and the lock the session saved: see the helper.
669
+ const handOffs = restoreHandOffs(driver, snapshot, { flowOwner, resumeNote });
670
+ const { savedLock, handBack } = handOffs;
671
+ ({ flowOwner, resumeNote } = handOffs);
628
672
  // The autopilot's mode waits for its flow to re-activate; restoring it starts nothing.
629
673
  (_d = autopilotOf(driver)) === null || _d === void 0 ? void 0 : _d.restorePersistedMode(snapshot.autopilot);
630
674
  // Long actions the reload cut wait for requests of their widget types (§4.12) — but only when
631
675
  // their flow is re-activated above. A flow left behind (interrupted, or an owner this driver
632
676
  // does not know) never ends, so nothing would clear its checkpoints, and the next request of
633
677
  // such a widget type would take over the abandoned run's work.
634
- restoreCheckpoints(driver, flowOwner ? savedCheckpoints(snapshot) : []);
678
+ restoreCheckpoints(driver, flowOwner && !handBack ? savedCheckpoints(snapshot) : []);
635
679
  // Seed the carry-forward cache so the immediate post-restore autosave (fired
636
680
  // by the history change below, before any agent re-activates) preserves the
637
681
  // machine snapshot instead of overwriting it with an empty one (GENC-1351).
@@ -703,7 +747,9 @@ export class SessionPersister {
703
747
  ];
704
748
  }
705
749
  const messages = [...trimmed, ...trailers];
706
- ref.actions.aiAssistant.loadSession(toLoadSessionPayload(Object.assign(Object.assign({}, snapshot), { messages })));
750
+ ref.actions.aiAssistant.loadSession(toLoadSessionPayload(Object.assign(Object.assign({}, snapshot), { messages,
751
+ // A hand-back takes the lock from the target that finished; it sets its own.
752
+ flowOwnerAgentName: savedLock })));
707
753
  (_e = driver === null || driver === void 0 ? void 0 : driver.loadHistory) === null || _e === void 0 ? void 0 : _e.call(driver, [...messages]);
708
754
  // Re-arm the condense policies that had not fired at save time (GENC-1632) — AFTER
709
755
  // `loadHistory`, because the driver keeps only entries whose tool call is in the history it
@@ -754,11 +800,12 @@ export class SessionPersister {
754
800
  // On the driver itself, not a narrowed orchestrator (GENC-1660): both members are on
755
801
  // `AiDriver` and every decorator forwards them, so this reaches the routing whatever wraps it.
756
802
  driver === null || driver === void 0 ? void 0 : driver.setPinnedAgent((_h = snapshot.pinnedAgentName) !== null && _h !== void 0 ? _h : null);
757
- driver === null || driver === void 0 ? void 0 : driver.setFlowOwner((_j = snapshot.flowOwnerAgentName) !== null && _j !== void 0 ? _j : null);
803
+ driver === null || driver === void 0 ? void 0 : driver.setFlowOwner(savedLock);
758
804
  if (flowOwner) {
759
805
  this.logMeta('persistence.resume-planned', {
760
806
  flowOwner,
761
807
  resume: !!resumeNote,
808
+ handBack: !!handBack,
762
809
  dropped: snapshot.messages.length - bounded.length,
763
810
  });
764
811
  }
@@ -13,8 +13,10 @@ export const PERSISTED_SESSION_VERSION = 1;
13
13
  * @internal
14
14
  */
15
15
  export function createPersistedSession(input) {
16
- var _a, _b, _c;
17
- return Object.assign(Object.assign(Object.assign({ version: PERSISTED_SESSION_VERSION, savedAt: new Date().toISOString(), host: input.host, sessionKey: input.sessionKey, messages: input.messages, activeAgentName: input.activeAgentName, pinnedAgentName: input.pinnedAgentName, flowOwnerAgentName: input.flowOwnerAgentName, sessionCostUsd: input.sessionCostUsd, contextTokens: input.contextTokens, contextLimit: input.contextLimit, activeModel: input.activeModel, activeProviderName: input.activeProviderName, agentSnapshots: (_a = input.agentSnapshots) !== null && _a !== void 0 ? _a : {} }, (((_b = input.condensePolicies) === null || _b === void 0 ? void 0 : _b.length) ? { condensePolicies: input.condensePolicies } : {})), (input.autopilot ? { autopilot: input.autopilot } : {})), (((_c = input.checkpoints) === null || _c === void 0 ? void 0 : _c.length) ? { checkpoints: input.checkpoints } : {}));
16
+ var _a, _b, _c, _d;
17
+ return Object.assign(Object.assign(Object.assign(Object.assign(Object.assign({ version: PERSISTED_SESSION_VERSION, savedAt: new Date().toISOString(), host: input.host, sessionKey: input.sessionKey, messages: input.messages, activeAgentName: input.activeAgentName, pinnedAgentName: input.pinnedAgentName, flowOwnerAgentName: input.flowOwnerAgentName, sessionCostUsd: input.sessionCostUsd, contextTokens: input.contextTokens, contextLimit: input.contextLimit, activeModel: input.activeModel, activeProviderName: input.activeProviderName, agentSnapshots: (_a = input.agentSnapshots) !== null && _a !== void 0 ? _a : {} }, (((_b = input.condensePolicies) === null || _b === void 0 ? void 0 : _b.length) ? { condensePolicies: input.condensePolicies } : {})), (input.autopilot ? { autopilot: input.autopilot } : {})), (((_c = input.checkpoints) === null || _c === void 0 ? void 0 : _c.length) ? { checkpoints: input.checkpoints } : {})), (((_d = input.handoffStack) === null || _d === void 0 ? void 0 : _d.length) ? { handoffStack: input.handoffStack } : {})), (input.handOffData && Object.keys(input.handOffData).length
18
+ ? { handOffData: input.handOffData }
19
+ : {}));
18
20
  }
19
21
  /**
20
22
  * Resolve the notice text + pre-fill prompt for an interrupted (non-resumable) flow
@@ -280,12 +282,16 @@ export function unknownAgentSnapshots(snapshot, knownAgentNames) {
280
282
  *
281
283
  * @internal
282
284
  */
283
- export function reconcileAgentSnapshots(live, flowOwnerName, lastKnownGood) {
285
+ export function reconcileAgentSnapshots(live, flowOwnerName, lastKnownGood,
286
+ /** Callers waiting in a hand-off (GENC-1737): after a restore they are not live until it ends. */
287
+ waitingCallers = []) {
284
288
  const snapshots = Object.assign({}, live);
285
- if (flowOwnerName && snapshots[flowOwnerName] === undefined) {
286
- const carried = lastKnownGood[flowOwnerName];
289
+ for (const name of [flowOwnerName, ...waitingCallers]) {
290
+ if (!name || snapshots[name] !== undefined)
291
+ continue;
292
+ const carried = lastKnownGood[name];
287
293
  if (carried !== undefined)
288
- snapshots[flowOwnerName] = carried;
294
+ snapshots[name] = carried;
289
295
  }
290
296
  const cache = Object.keys(snapshots).length ? snapshots : lastKnownGood;
291
297
  return { snapshots, cache };
@@ -63,7 +63,13 @@ export function toolCallIdsIn(history) {
63
63
  *
64
64
  * Pure, no writes to the registry — safe on a save path that runs on every autosave.
65
65
  */
66
- export function snapshotCondensePolicies(policies, clocks, history) {
66
+ export function snapshotCondensePolicies(policies, clocks, history,
67
+ /**
68
+ * Activations suspended in a hand-off (GENC-1737), each with the phase epoch it was suspended
69
+ * at: still live, so written as unended, and their phases counted only up to the suspension.
70
+ */
71
+ suspended = new Map()) {
72
+ var _a;
67
73
  if (policies.size === 0)
68
74
  return [];
69
75
  const present = toolCallIdsIn(history);
@@ -76,8 +82,10 @@ export function snapshotCondensePolicies(policies, clocks, history) {
76
82
  // the clamp only stops a malformed entry from being written out as "the future".
77
83
  modelCalls: Math.max(0, clocks.modelCall - entry.iteration),
78
84
  turns: Math.max(0, clocks.turn - entry.turn),
79
- activations: Math.max(0, clocks.activation - entry.activation),
80
- phases: Math.max(0, clocks.phaseEpoch - entry.phaseEpoch),
85
+ activations: suspended.has(entry.activation)
86
+ ? 0
87
+ : Math.max(0, clocks.activation - entry.activation),
88
+ phases: Math.max(0, ((_a = suspended.get(entry.activation)) !== null && _a !== void 0 ? _a : clocks.phaseEpoch) - entry.phaseEpoch),
81
89
  } }));
82
90
  }
83
91
  return out;
@@ -337,7 +345,8 @@ export function applyCondensation(history, policies, ctx, onCondensed) {
337
345
  // backstop, when the whole activation has ended, so a phase boundary that
338
346
  // is never declared still collapses at `agentEnd` rather than never.
339
347
  case 'phaseEnd':
340
- return ctx.phaseEnded(entry.phaseEpoch) || ctx.activationEnded(entry.activation);
348
+ return (ctx.phaseEnded(entry.phaseEpoch, entry.activation) ||
349
+ ctx.activationEnded(entry.activation));
341
350
  default:
342
351
  return latestByKey.get(trig.by) !== toolCallId; // superseded by a newer call
343
352
  }
package/dist/react.cjs CHANGED
@@ -53,7 +53,7 @@ const AiWavesIndicator = React.forwardRef(function AiWavesIndicator(props, ref)
53
53
  });
54
54
 
55
55
  const FoundationAiAssistant = React.forwardRef(function FoundationAiAssistant(props, ref) {
56
- const { onBudgetRefreshRequested, onChatHeaderMousedown, onSessionCleared, children, ...rest } = props;
56
+ const { onBudgetRefreshRequested, onChatHeaderMousedown, onSessionCleared, onHandoffStarted, onHandoffReturned, onHandoffUnwound, children, ...rest } = props;
57
57
  const _innerRef = React.useRef(null);
58
58
  const _onBudgetRefreshRequestedRef = React.useRef(onBudgetRefreshRequested);
59
59
  _onBudgetRefreshRequestedRef.current = onBudgetRefreshRequested;
@@ -61,6 +61,12 @@ const FoundationAiAssistant = React.forwardRef(function FoundationAiAssistant(pr
61
61
  _onChatHeaderMousedownRef.current = onChatHeaderMousedown;
62
62
  const _onSessionClearedRef = React.useRef(onSessionCleared);
63
63
  _onSessionClearedRef.current = onSessionCleared;
64
+ const _onHandoffStartedRef = React.useRef(onHandoffStarted);
65
+ _onHandoffStartedRef.current = onHandoffStarted;
66
+ const _onHandoffReturnedRef = React.useRef(onHandoffReturned);
67
+ _onHandoffReturnedRef.current = onHandoffReturned;
68
+ const _onHandoffUnwoundRef = React.useRef(onHandoffUnwound);
69
+ _onHandoffUnwoundRef.current = onHandoffUnwound;
64
70
  React.useLayoutEffect(() => {
65
71
  const el = _innerRef.current;
66
72
  if (!el) return;
@@ -70,10 +76,19 @@ const FoundationAiAssistant = React.forwardRef(function FoundationAiAssistant(pr
70
76
  el.addEventListener('chat-header-mousedown', _onChatHeaderMousedownFn);
71
77
  const _onSessionClearedFn = (e) => _onSessionClearedRef.current?.(e);
72
78
  el.addEventListener('session-cleared', _onSessionClearedFn);
79
+ const _onHandoffStartedFn = (e) => _onHandoffStartedRef.current?.(e);
80
+ el.addEventListener('handoff-started', _onHandoffStartedFn);
81
+ const _onHandoffReturnedFn = (e) => _onHandoffReturnedRef.current?.(e);
82
+ el.addEventListener('handoff-returned', _onHandoffReturnedFn);
83
+ const _onHandoffUnwoundFn = (e) => _onHandoffUnwoundRef.current?.(e);
84
+ el.addEventListener('handoff-unwound', _onHandoffUnwoundFn);
73
85
  return () => {
74
86
  el.removeEventListener('budget-refresh-requested', _onBudgetRefreshRequestedFn);
75
87
  el.removeEventListener('chat-header-mousedown', _onChatHeaderMousedownFn);
76
88
  el.removeEventListener('session-cleared', _onSessionClearedFn);
89
+ el.removeEventListener('handoff-started', _onHandoffStartedFn);
90
+ el.removeEventListener('handoff-returned', _onHandoffReturnedFn);
91
+ el.removeEventListener('handoff-unwound', _onHandoffUnwoundFn);
77
92
  };
78
93
  }, []);
79
94
  return React.createElement(customElements.getName(FoundationAiAssistantWC) ?? 'foundation-ai-assistant', { ...rest, ref: _mergeRefs(_innerRef, ref) }, children);
package/dist/react.mjs CHANGED
@@ -51,7 +51,7 @@ export const AiWavesIndicator = React.forwardRef(function AiWavesIndicator(props
51
51
  });
52
52
 
53
53
  export const FoundationAiAssistant = React.forwardRef(function FoundationAiAssistant(props, ref) {
54
- const { onBudgetRefreshRequested, onChatHeaderMousedown, onSessionCleared, children, ...rest } = props;
54
+ const { onBudgetRefreshRequested, onChatHeaderMousedown, onSessionCleared, onHandoffStarted, onHandoffReturned, onHandoffUnwound, children, ...rest } = props;
55
55
  const _innerRef = React.useRef(null);
56
56
  const _onBudgetRefreshRequestedRef = React.useRef(onBudgetRefreshRequested);
57
57
  _onBudgetRefreshRequestedRef.current = onBudgetRefreshRequested;
@@ -59,6 +59,12 @@ export const FoundationAiAssistant = React.forwardRef(function FoundationAiAssis
59
59
  _onChatHeaderMousedownRef.current = onChatHeaderMousedown;
60
60
  const _onSessionClearedRef = React.useRef(onSessionCleared);
61
61
  _onSessionClearedRef.current = onSessionCleared;
62
+ const _onHandoffStartedRef = React.useRef(onHandoffStarted);
63
+ _onHandoffStartedRef.current = onHandoffStarted;
64
+ const _onHandoffReturnedRef = React.useRef(onHandoffReturned);
65
+ _onHandoffReturnedRef.current = onHandoffReturned;
66
+ const _onHandoffUnwoundRef = React.useRef(onHandoffUnwound);
67
+ _onHandoffUnwoundRef.current = onHandoffUnwound;
62
68
  React.useLayoutEffect(() => {
63
69
  const el = _innerRef.current;
64
70
  if (!el) return;
@@ -68,10 +74,19 @@ export const FoundationAiAssistant = React.forwardRef(function FoundationAiAssis
68
74
  el.addEventListener('chat-header-mousedown', _onChatHeaderMousedownFn);
69
75
  const _onSessionClearedFn = (e) => _onSessionClearedRef.current?.(e);
70
76
  el.addEventListener('session-cleared', _onSessionClearedFn);
77
+ const _onHandoffStartedFn = (e) => _onHandoffStartedRef.current?.(e);
78
+ el.addEventListener('handoff-started', _onHandoffStartedFn);
79
+ const _onHandoffReturnedFn = (e) => _onHandoffReturnedRef.current?.(e);
80
+ el.addEventListener('handoff-returned', _onHandoffReturnedFn);
81
+ const _onHandoffUnwoundFn = (e) => _onHandoffUnwoundRef.current?.(e);
82
+ el.addEventListener('handoff-unwound', _onHandoffUnwoundFn);
71
83
  return () => {
72
84
  el.removeEventListener('budget-refresh-requested', _onBudgetRefreshRequestedFn);
73
85
  el.removeEventListener('chat-header-mousedown', _onChatHeaderMousedownFn);
74
86
  el.removeEventListener('session-cleared', _onSessionClearedFn);
87
+ el.removeEventListener('handoff-started', _onHandoffStartedFn);
88
+ el.removeEventListener('handoff-returned', _onHandoffReturnedFn);
89
+ el.removeEventListener('handoff-unwound', _onHandoffUnwoundFn);
75
90
  };
76
91
  }, []);
77
92
  return React.createElement(customElements.getName(FoundationAiAssistantWC) ?? 'foundation-ai-assistant', { ...rest, ref: _mergeRefs(_innerRef, ref) }, children);
package/docs/sub_agent.md CHANGED
@@ -165,7 +165,7 @@ reason you got**, and the two groups are opposites:
165
165
  | `budget_exhausted` | **Terminal, and already handled for you.** The AI-spend budget is gone and the proxy is refusing every request. Retrying is another `402`, and "ask the user" is asking someone who cannot fix it. Return a terse error and let the turn end — the parent driver ends the turn as soon as this outcome comes back, so your handler will not be called again this turn. |
166
166
  | `provider_refused` | **Terminal, and already handled for you.** The vendor refused the account (a revoked key, a suspended organisation). Retrying gets the same refusal. Return a terse error: the parent driver ends the turn before its next model call, as for `budget_exhausted`. |
167
167
  | `context_exhausted` | **Terminal.** The sub-agent stopped because going on would overrun the context window. Re-invoking it with the same conversation stops it the same way, and your own next model call is likely to stop on the same guard. Return a terse error; a new conversation, or a smaller task, is the way on. |
168
- | `ai_service_error` | **Check `terminal`.** The app's AI service refused the sub-agent, and `code` says why (`MODEL_NOT_ALLOWED`, `NO_API_KEY`, …). When `terminal` is true, re-invoking the sub-agent with the same request gets the same refusal: return a terse error and do not re-invoke it. Only `VENDOR_UNREACHABLE` is not terminal, but a refusal from your own vendor that is about the service (for example `VENDOR_UNREACHABLE`) still ends this turn; retry on the user's next message, not in the handler. When the refusal is about the service itself and the sub-agent used your vendor, the parent turn ends on it, as for `budget_exhausted`, unless a completion or a handoff from the same batch goes on past it. The user is shown the sentence for a key, access, model or reachability refusal once per message, so your reply need not repeat it; a refusal about the request itself (`REQUEST_TOO_LARGE`, `BAD_REQUEST`, `FALLBACKS_NOT_SUPPORTED`) is left to you and the console. |
168
+ | `ai_service_error` | **Check `terminal`.** The app's AI service refused the sub-agent, and `code` says why (`MODEL_NOT_ALLOWED`, `NO_API_KEY`, …). When `terminal` is true, re-invoking the sub-agent with the same request gets the same refusal: return a terse error and do not re-invoke it. Only `VENDOR_UNREACHABLE` is not terminal, but a refusal from your own vendor that is about the service (for example `VENDOR_UNREACHABLE`) still ends this turn; retry on the user's next message, not in the handler. When the refusal is about the service itself and the sub-agent used your vendor, the parent turn ends on it, as for `budget_exhausted`, unless a completion from the same batch goes on past it. The user is shown the sentence for a key, access, model or reachability refusal once per message, so your reply need not repeat it; a refusal about the request itself (`REQUEST_TOO_LARGE`, `BAD_REQUEST`, `FALLBACKS_NOT_SUPPORTED`) is left to you and the console. |
169
169
 
170
170
  ```ts
171
171
  const outcome = await context.requestSubAgent<ExtractedTrades>('trade_file_extractor', {
package/package.json CHANGED
@@ -1,7 +1,7 @@
1
1
  {
2
2
  "name": "@genesislcap/ai-assistant",
3
3
  "description": "Genesis AI Assistant micro-frontend",
4
- "version": "15.84.2",
4
+ "version": "15.85.0",
5
5
  "license": "SEE LICENSE IN license.txt",
6
6
  "main": "dist/esm/index.js",
7
7
  "types": "dist/dts/index.d.ts",
@@ -92,31 +92,31 @@
92
92
  }
93
93
  },
94
94
  "devDependencies": {
95
- "@genesislcap/foundation-comms": "15.84.2",
96
- "@genesislcap/foundation-testing": "15.84.2",
97
- "@genesislcap/genx": "15.84.2",
98
- "@genesislcap/ts-builder": "15.84.2",
99
- "@genesislcap/uvu-playwright-builder": "15.84.2",
100
- "@genesislcap/vite-builder": "15.84.2",
101
- "@genesislcap/webpack-builder": "15.84.2",
95
+ "@genesislcap/foundation-comms": "15.85.0",
96
+ "@genesislcap/foundation-testing": "15.85.0",
97
+ "@genesislcap/genx": "15.85.0",
98
+ "@genesislcap/ts-builder": "15.85.0",
99
+ "@genesislcap/uvu-playwright-builder": "15.85.0",
100
+ "@genesislcap/vite-builder": "15.85.0",
101
+ "@genesislcap/webpack-builder": "15.85.0",
102
102
  "@types/dompurify": "^3.0.5",
103
103
  "@types/marked": "^5.0.2",
104
104
  "esbuild": "0.25.12"
105
105
  },
106
106
  "dependencies": {
107
- "@genesislcap/foundation-ai": "15.84.2",
108
- "@genesislcap/foundation-logger": "15.84.2",
109
- "@genesislcap/foundation-notifications": "15.84.2",
110
- "@genesislcap/foundation-redux": "15.84.2",
111
- "@genesislcap/foundation-ui": "15.84.2",
112
- "@genesislcap/foundation-utils": "15.84.2",
113
- "@genesislcap/rapid-design-system": "15.84.2",
114
- "@genesislcap/web-core": "15.84.2",
107
+ "@genesislcap/foundation-ai": "15.85.0",
108
+ "@genesislcap/foundation-logger": "15.85.0",
109
+ "@genesislcap/foundation-notifications": "15.85.0",
110
+ "@genesislcap/foundation-redux": "15.85.0",
111
+ "@genesislcap/foundation-ui": "15.85.0",
112
+ "@genesislcap/foundation-utils": "15.85.0",
113
+ "@genesislcap/rapid-design-system": "15.85.0",
114
+ "@genesislcap/web-core": "15.85.0",
115
115
  "dompurify": "^3.3.1",
116
116
  "marked": "^17.0.3"
117
117
  },
118
118
  "peerDependencies": {
119
- "@genesislcap/foundation-comms": "^15.84.2"
119
+ "@genesislcap/foundation-comms": "^15.85.0"
120
120
  },
121
121
  "peerDependenciesMeta": {
122
122
  "@genesislcap/foundation-comms": {