@genesislcap/ai-assistant 15.24.2 → 15.26.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 (47) hide show
  1. package/dist/ai-assistant.api.json +1065 -33
  2. package/dist/ai-assistant.d.ts +288 -102
  3. package/dist/chat-driver.cjs +630 -378
  4. package/dist/chat-driver.cjs.map +4 -4
  5. package/dist/chat-driver.mjs +630 -378
  6. package/dist/chat-driver.mjs.map +4 -4
  7. package/dist/custom-elements.json +1198 -388
  8. package/dist/dts/components/agent-session/agent-session.d.ts +134 -0
  9. package/dist/dts/components/agent-session/agent-session.d.ts.map +1 -0
  10. package/dist/dts/components/agent-session/index.d.ts +2 -0
  11. package/dist/dts/components/agent-session/index.d.ts.map +1 -0
  12. package/dist/dts/components/ai-driver/ai-driver.d.ts +31 -4
  13. package/dist/dts/components/ai-driver/ai-driver.d.ts.map +1 -1
  14. package/dist/dts/components/chat-driver/chat-driver.constants.d.ts +52 -0
  15. package/dist/dts/components/chat-driver/chat-driver.constants.d.ts.map +1 -0
  16. package/dist/dts/components/chat-driver/chat-driver.d.ts +100 -82
  17. package/dist/dts/components/chat-driver/chat-driver.d.ts.map +1 -1
  18. package/dist/dts/components/chat-driver/fold-controller.d.ts +82 -0
  19. package/dist/dts/components/chat-driver/fold-controller.d.ts.map +1 -0
  20. package/dist/dts/components/chat-driver/turn-failures.d.ts +20 -1
  21. package/dist/dts/components/chat-driver/turn-failures.d.ts.map +1 -1
  22. package/dist/dts/components/orchestrating-driver/orchestrating-driver.d.ts +14 -24
  23. package/dist/dts/components/orchestrating-driver/orchestrating-driver.d.ts.map +1 -1
  24. package/dist/dts/index.d.ts +1 -0
  25. package/dist/dts/index.d.ts.map +1 -1
  26. package/dist/dts/main/main.d.ts +6 -16
  27. package/dist/dts/main/main.d.ts.map +1 -1
  28. package/dist/dts/suggestions/generate-suggestions.d.ts +35 -0
  29. package/dist/dts/suggestions/generate-suggestions.d.ts.map +1 -0
  30. package/dist/dts/utils/agents-key.d.ts +10 -0
  31. package/dist/dts/utils/agents-key.d.ts.map +1 -0
  32. package/dist/dts/utils/resolve-turn-input.d.ts +9 -0
  33. package/dist/dts/utils/resolve-turn-input.d.ts.map +1 -0
  34. package/dist/esm/components/agent-session/agent-session.js +208 -0
  35. package/dist/esm/components/agent-session/index.js +1 -0
  36. package/dist/esm/components/chat-driver/chat-driver.constants.js +72 -0
  37. package/dist/esm/components/chat-driver/chat-driver.js +190 -395
  38. package/dist/esm/components/chat-driver/fold-controller.js +184 -0
  39. package/dist/esm/components/chat-driver/turn-failures.js +14 -0
  40. package/dist/esm/components/orchestrating-driver/orchestrating-driver.js +48 -131
  41. package/dist/esm/index.js +1 -0
  42. package/dist/esm/main/main.js +11 -38
  43. package/dist/esm/suggestions/generate-suggestions.js +123 -0
  44. package/dist/esm/utils/agents-key.js +24 -0
  45. package/dist/esm/utils/resolve-turn-input.js +16 -0
  46. package/docs/migration-GENC-1580.md +44 -36
  47. package/package.json +16 -16
@@ -0,0 +1,184 @@
1
+ import { __awaiter } from "tslib";
2
+ import { logger } from '../../utils/logger';
3
+ import { TOOL_FOLD_SYMBOL } from '../../utils/tool-fold';
4
+ /** Read `ToolFold` metadata off a handler, or `undefined` if it is not a fold facade. */
5
+ const foldOf = (handler) => handler
6
+ ? handler[TOOL_FOLD_SYMBOL]
7
+ : undefined;
8
+ /**
9
+ * Tool folds: grouping tools behind a facade so the model sees a small surface, and swapping the
10
+ * surface as it opens and closes them.
11
+ *
12
+ * Owns the fold stack and the no-progress counter. Everything else it needs is the driver's tool
13
+ * surface, reached through {@link FoldDeps}.
14
+ */
15
+ export class FoldController {
16
+ constructor(deps) {
17
+ this.deps = deps;
18
+ this.stack = [];
19
+ /**
20
+ * Consecutive fold opens/closes with no tool call between them. A model that only opens and
21
+ * closes is making no progress, and the loop breaks out past a ceiling.
22
+ */
23
+ this.consecutiveOps = 0;
24
+ }
25
+ /** Drop all fold state. Called on an agent swap — each agent starts outside every fold. */
26
+ reset() {
27
+ this.stack = [];
28
+ this.consecutiveOps = 0;
29
+ }
30
+ /** Names of the currently open folds, outermost first. */
31
+ activeNames() {
32
+ return this.stack.map((f) => f.foldName);
33
+ }
34
+ get depth() {
35
+ return this.stack.length;
36
+ }
37
+ /** The innermost open fold's name, or `undefined` when none is open. */
38
+ get topName() {
39
+ var _a;
40
+ return (_a = this.stack[this.stack.length - 1]) === null || _a === void 0 ? void 0 : _a.foldName;
41
+ }
42
+ /** Count one fold operation and return the new consecutive total. */
43
+ noteOp() {
44
+ this.consecutiveOps += 1;
45
+ return this.consecutiveOps;
46
+ }
47
+ /** Real work happened, so the no-progress streak restarts. */
48
+ clearOps() {
49
+ this.consecutiveOps = 0;
50
+ }
51
+ /** Fold metadata for a tool name, or `undefined` if it is not a fold facade. */
52
+ foldFor(toolName) {
53
+ return foldOf(this.deps.getHandlers()[toolName]);
54
+ }
55
+ /**
56
+ * Which fold to **open** to reach `toolName`, or `null` if no registered fold contains it.
57
+ *
58
+ * Returns the outermost *registered* ancestor, not the innermost fold holding the tool — the one
59
+ * consumer uses the answer to tell the model what to call next, and a nested fold cannot be
60
+ * opened before its parent. The recursion therefore only descends through closed folds: once a
61
+ * fold is open its children are top-level in the live handler map and match directly.
62
+ */
63
+ findContaining(toolName, handlers) {
64
+ const source = handlers !== null && handlers !== void 0 ? handlers : this.deps.getHandlers();
65
+ for (const handler of Object.values(source)) {
66
+ const fold = foldOf(handler);
67
+ if (!fold)
68
+ continue;
69
+ if (fold.handlers[toolName])
70
+ return fold.name;
71
+ if (this.findContaining(toolName, fold.handlers))
72
+ return fold.name;
73
+ }
74
+ return null;
75
+ }
76
+ /**
77
+ * If an open fold is hiding a previously-available tool, the fold to close to start getting it
78
+ * back, else `null`.
79
+ *
80
+ * Only exclusive folds hide tools — they replace the tool set on open rather than extending it —
81
+ * so a base tool that was visible beforehand sits in a frame's `previousHandlers` but not in the
82
+ * live map. Only the top fold's `close_` tool is active, so that is always the actionable step:
83
+ * closing repeatedly walks back to a tool further down the stack.
84
+ */
85
+ hidingTool(toolName) {
86
+ if (this.stack.length === 0)
87
+ return null;
88
+ const hidden = this.stack.some((f) => f.previousHandlers[toolName]);
89
+ return hidden ? this.topName : null;
90
+ }
91
+ /** Open a fold: push a frame, swap the tool set, and return the response for the model. */
92
+ open(foldName, fold, args, activeToolCallId) {
93
+ // Shortcut dispatch: the model passed inner tool args directly, e.g.
94
+ // trading_tools({ search_trades: { side: "BUY" } })
95
+ for (const key of Object.keys(args)) {
96
+ const innerHandler = fold.handlers[key];
97
+ if (innerHandler) {
98
+ logger.debug(`ChatDriver: fold shortcut dispatch "${foldName}" → "${key}"`);
99
+ // Open first, so the tool set is correct for subsequent calls.
100
+ this.pushFrame(foldName);
101
+ this.applyToolSet(fold, foldName);
102
+ this.clearOps(); // shortcut dispatch counts as real work
103
+ const innerArgs = typeof args[key] === 'object' && args[key] !== null
104
+ ? args[key]
105
+ : {};
106
+ return innerHandler(innerArgs, this.deps.handlerContext(activeToolCallId)).then((r) => typeof r === 'string' ? r : JSON.stringify(r));
107
+ }
108
+ }
109
+ this.pushFrame(foldName);
110
+ this.applyToolSet(fold, foldName);
111
+ const closeToolName = `close_${foldName}`;
112
+ const innerToolNames = fold.tools.map((t) => t.name);
113
+ let message = `Fold opened: ${foldName}. Tools now available: ${[...innerToolNames, closeToolName].join(', ')}.`;
114
+ if (fold.usageNotes)
115
+ message += ` Notes: ${fold.usageNotes}`;
116
+ message += ` Call ${closeToolName} when done to return to the previous tools.`;
117
+ return Promise.resolve(message);
118
+ }
119
+ /** Close the top fold: pop the frame, restore the tool set it captured. */
120
+ close() {
121
+ const frame = this.stack.pop();
122
+ if (!frame)
123
+ return 'No fold is currently open.';
124
+ this.deps.setDefinitions(frame.previousDefinitions);
125
+ this.deps.setHandlers(frame.previousHandlers);
126
+ const toolNames = frame.previousDefinitions.map((t) => t.name);
127
+ return `Fold closed: ${frame.foldName}. Tools now available: ${toolNames.join(', ')}.`;
128
+ }
129
+ /** The fold-awareness suffix appended to the system prompt on each LLM call. */
130
+ systemPromptSuffix() {
131
+ const activeFolds = Object.values(this.deps.getHandlers())
132
+ .map(foldOf)
133
+ .filter((f) => !!f);
134
+ if (activeFolds.length === 0 && this.stack.length === 0)
135
+ return '';
136
+ const parts = [
137
+ '\n\n--- Tool Folds ---',
138
+ 'Some tools are grouped into folds. You may see tool calls in the conversation history for tools that are not currently available — they are inside a fold. To access them, invoke the fold tool first.',
139
+ ];
140
+ if (this.topName) {
141
+ parts.push(`You are currently inside the "${this.topName}" fold. Call close_${this.topName} when you are done with these tools.`);
142
+ }
143
+ for (const fold of activeFolds)
144
+ parts.push(`• ${fold.name}: ${fold.description}`);
145
+ return parts.join('\n');
146
+ }
147
+ pushFrame(foldName) {
148
+ this.stack.push({
149
+ foldName,
150
+ previousDefinitions: [...this.deps.getDefinitions()],
151
+ previousHandlers: Object.assign({}, this.deps.getHandlers()),
152
+ });
153
+ }
154
+ /**
155
+ * Install the fold's inner tool set, replacing it (exclusive) or extending it (non-exclusive),
156
+ * and inject the close tool. Does not touch the stack.
157
+ */
158
+ applyToolSet(fold, foldName) {
159
+ const closeToolName = `close_${foldName}`;
160
+ const newDefs = [];
161
+ const newHandlers = {};
162
+ if (!fold.exclusive) {
163
+ // Keep the existing tools, minus the facade just opened.
164
+ for (const def of this.deps.getDefinitions()) {
165
+ if (def.name !== foldName)
166
+ newDefs.push(def);
167
+ }
168
+ for (const [name, handler] of Object.entries(this.deps.getHandlers())) {
169
+ if (name !== foldName)
170
+ newHandlers[name] = handler;
171
+ }
172
+ }
173
+ newDefs.push(...fold.tools);
174
+ Object.assign(newHandlers, fold.handlers);
175
+ newDefs.push({
176
+ name: closeToolName,
177
+ description: `Close the ${foldName} fold and return to the previous set of tools.`,
178
+ parameters: { type: 'object', properties: {} },
179
+ });
180
+ newHandlers[closeToolName] = () => __awaiter(this, void 0, void 0, function* () { return this.close(); });
181
+ this.deps.setDefinitions(newDefs);
182
+ this.deps.setHandlers(newHandlers);
183
+ }
184
+ }
@@ -82,3 +82,17 @@ export const TURN_FAILURE_EXITS = {
82
82
  // `TurnFailureReason` and cost the literal types that let `turn-failures.test.ts` pin the
83
83
  // two deliberate divergences at compile time instead of at runtime.
84
84
  };
85
+ /**
86
+ * Build the `done` loop result, carrying the typed failure reason when the turn bailed (PTC-0).
87
+ *
88
+ * The discriminant stays `'done'` either way, so consumers matching on `reason === 'done'` are
89
+ * unchanged. Both payloads are omitted rather than set to `undefined`, which keeps a happy-path
90
+ * result byte-identical to `{ reason: 'done' }` for a consumer that structurally compares it.
91
+ *
92
+ * @beta
93
+ */
94
+ export function turnDone(failureReason, budget, providerRefused) {
95
+ if (!failureReason)
96
+ return { reason: 'done' };
97
+ return Object.assign(Object.assign({ reason: 'done', failureReason }, (budget ? { budget } : {})), (providerRefused ? { providerRefused } : {}));
98
+ }
@@ -149,16 +149,33 @@ function buildFallbackSystemPrompt(fallback, specialists) {
149
149
  * @beta
150
150
  */
151
151
  export class OrchestratingDriver extends EventTarget {
152
+ /**
153
+ * The stateful-agent contract (§7.1): active agent, user pin, flow lock, restored state, and
154
+ * the `onActivate` / `onDeactivate` hooks.
155
+ *
156
+ * **The inner driver's session, not one of our own.** One session is the invariant: two would
157
+ * let the outer and inner driver disagree about which agent is active, and the inner one would
158
+ * never be activated at all, because this driver installs a *decorated* config through
159
+ * `applyAgent` rather than going via `ChatDriver.activateAgent`. This driver keeps the routing
160
+ * decisions; the session owns the lifecycle.
161
+ */
162
+ get agentSession() {
163
+ return this.chatDriver.agentSession;
164
+ }
165
+ /**
166
+ * The active agent, or `undefined` before the first activation and after a release.
167
+ *
168
+ * Kept as an accessor over `agentSession` rather than removed: it is a public member of an
169
+ * exported class, so nothing in this package reading it is not the same as nothing reading it.
170
+ */
171
+ get activeAgent() {
172
+ return this.agentSession.getActive();
173
+ }
152
174
  constructor(providerRegistry, agents, options = {}) {
153
175
  var _a, _b, _c, _d;
154
176
  super();
155
177
  this.providerRegistry = providerRegistry;
156
178
  this.agents = agents;
157
- /**
158
- * Aborted on driver disposal. Threaded into `AgentLifecycleContext.signal`
159
- * so long-running `onActivate` work can bail if the session disconnects.
160
- */
161
- this.lifecycleAbortController = new AbortController();
162
179
  /**
163
180
  * Set by `cancel()` for the duration of a `sendMessage` run. Breaks the
164
181
  * handoff loop so we don't classify or start another agent turn after the
@@ -176,28 +193,6 @@ export class OrchestratingDriver extends EventTarget {
176
193
  * made it vanish. Reset at the top of each `runOrchestratedTurn`.
177
194
  */
178
195
  this.userMessageAppended = false;
179
- /**
180
- * Sticky user pick from the picker (or the host's `setAgent` API). Only
181
- * changes on explicit user action. Survives flow completion: when a stateful
182
- * agent releases, the user-pin stays put and routing falls back to it on
183
- * the next turn.
184
- */
185
- this.pinnedAgentName = null;
186
- /**
187
- * Transient flow lock — set when a stateful agent's `onActivate` fires,
188
- * cleared by `releaseActiveAgent`. While non-null, routing precedence goes
189
- * to this agent and the picker is locked. Independent of
190
- * `pinnedAgentName` so completing a flow doesn't wipe the user's sticky
191
- * choice.
192
- */
193
- this.flowOwnerAgentName = null;
194
- /**
195
- * Per-agent restored state for a rehydrated session, keyed by agent name.
196
- * Primed by the host on restore; each entry is passed to that agent's
197
- * `onActivate` as `ctx.restored` on its next activation, then deleted so a
198
- * later re-activation starts fresh.
199
- */
200
- this.restoredAgentStates = {};
201
196
  this.sessionKey = (_a = options.sessionKey) !== null && _a !== void 0 ? _a : '';
202
197
  this.maxHandoffs = (_b = options.maxHandoffs) !== null && _b !== void 0 ? _b : DEFAULT_MAX_HANDOFFS;
203
198
  this.classifierHistoryLength =
@@ -224,6 +219,8 @@ export class OrchestratingDriver extends EventTarget {
224
219
  sessionKey: this.sessionKey,
225
220
  activityBus: options.activityBus,
226
221
  budgetExhaustedMessage: options.budgetExhaustedMessage,
222
+ // This driver releases after the turn so it can fire the host-facing events; see the option.
223
+ deferAgentRelease: true,
227
224
  });
228
225
  // Proxy events from the shared driver
229
226
  this.chatDriver.addEventListener('history-updated', (e) => {
@@ -282,7 +279,7 @@ export class OrchestratingDriver extends EventTarget {
282
279
  * return to automatic routing.
283
280
  */
284
281
  setPinnedAgent(name) {
285
- this.pinnedAgentName = name;
282
+ this.agentSession.setPinned(name);
286
283
  }
287
284
  /**
288
285
  * Set the flow-owner lock — used to restore state from Redux on driver
@@ -291,7 +288,7 @@ export class OrchestratingDriver extends EventTarget {
291
288
  * for.
292
289
  */
293
290
  setFlowOwner(name) {
294
- this.flowOwnerAgentName = name;
291
+ this.agentSession.setFlowOwner(name);
295
292
  }
296
293
  loadHistory(messages) {
297
294
  this.chatDriver.loadHistory(messages);
@@ -535,7 +532,7 @@ export class OrchestratingDriver extends EventTarget {
535
532
  }
536
533
  /** {@inheritDoc AiDriver.primeRestoredAgentState} */
537
534
  primeRestoredAgentState(snapshots) {
538
- this.restoredAgentStates = Object.assign({}, snapshots);
535
+ this.agentSession.primeRestoredStates(snapshots);
539
536
  }
540
537
  /**
541
538
  * {@inheritDoc AiDriver.getContextReserve}
@@ -593,69 +590,16 @@ export class OrchestratingDriver extends EventTarget {
593
590
  }
594
591
  /** {@inheritDoc AiDriver.getActiveAgent} */
595
592
  getActiveAgent() {
596
- return this.activeAgent;
593
+ return this.agentSession.getActive();
597
594
  }
598
595
  applyAgent(agent) {
599
596
  return __awaiter(this, void 0, void 0, function* () {
600
- var _a;
601
- const previousAgent = this.activeAgent;
602
- const isSwitch = !previousAgent || previousAgent.name !== agent.name;
603
- // Record the agent transition on the debug-log timeline. A `null` `from`
604
- // marks the initial activation; a named `from` is a handoff between agents.
605
- if (isSwitch) {
606
- recordMetaEvent(this.sessionKey, 'agent.handoff', {
607
- from: (_a = previousAgent === null || previousAgent === void 0 ? void 0 : previousAgent.name) !== null && _a !== void 0 ? _a : null,
608
- to: agent.name,
609
- // GENC-1351: was a restored snapshot waiting for this agent at activation?
610
- restored: this.restoredAgentStates[agent.name] !== undefined,
611
- });
612
- }
613
- // Fire lifecycle hooks around the swap — outgoing first, then incoming.
614
- // Both are awaited so a heavy `onActivate` (e.g. machine restore) completes
615
- // before the agent's first turn runs.
616
- if (isSwitch && (previousAgent === null || previousAgent === void 0 ? void 0 : previousAgent.onDeactivate)) {
617
- try {
618
- yield previousAgent.onDeactivate({
619
- agentName: previousAgent.name,
620
- sessionKey: this.sessionKey,
621
- signal: this.lifecycleAbortController.signal,
622
- });
623
- }
624
- catch (e) {
625
- logger.warn(`OrchestratingDriver: onDeactivate("${previousAgent.name}") threw:`, e);
626
- }
627
- }
628
- if (isSwitch && agent.onActivate) {
629
- // On a session restore, hand this agent its persisted state exactly once
630
- // (consumed here) so a stateful machine resumes at the right step; a later
631
- // re-activation of the same agent then starts fresh.
632
- const restored = this.restoredAgentStates[agent.name];
633
- if (restored !== undefined)
634
- delete this.restoredAgentStates[agent.name];
635
- try {
636
- yield agent.onActivate({
637
- agentName: agent.name,
638
- sessionKey: this.sessionKey,
639
- previousAgentName: previousAgent === null || previousAgent === void 0 ? void 0 : previousAgent.name,
640
- signal: this.lifecycleAbortController.signal,
641
- restored,
642
- });
643
- }
644
- catch (e) {
645
- logger.warn(`OrchestratingDriver: onActivate("${agent.name}") threw:`, e);
646
- }
647
- }
648
- const hasLifecycleHooks = !!(agent.onActivate || agent.onDeactivate);
649
- // Stateful agents auto-lock the flow on activation. The lock guarantees
650
- // the machine survives subsequent turns (the classifier would otherwise be
651
- // free to route away mid-flow, tearing the machine down). Release happens
652
- // when the agent calls `releaseAgent` from a terminal-state tool handler
653
- // — see the post-sendMessage check below. We write `flowOwnerAgentName`
654
- // rather than `pinnedAgentName` so completing a flow doesn't wipe the
655
- // user's sticky picker choice.
656
- if (isSwitch && hasLifecycleHooks && this.flowOwnerAgentName !== agent.name) {
657
- this.flowOwnerAgentName = agent.name;
658
- this.dispatchEvent(new CustomEvent('flow-owner-changed', { detail: agent.name }));
597
+ // The transition — handoff record, lifecycle hooks, restored state, flow lock — belongs to
598
+ // the session. What is left below is this driver's own job: what the agent may route to, and
599
+ // what the transcript says happened.
600
+ const { previousAgent, flowOwnerChangedTo } = yield this.agentSession.activate(agent);
601
+ if (flowOwnerChangedTo !== undefined) {
602
+ this.dispatchEvent(new CustomEvent('flow-owner-changed', { detail: flowOwnerChangedTo }));
659
603
  }
660
604
  // Terminal agents do not get the cross-agent handoff tool. Four cases:
661
605
  // • fallback — already a leaf; handoff would loop
@@ -667,9 +611,9 @@ export class OrchestratingDriver extends EventTarget {
667
611
  // classifier mid-machine. Capture the tool loop until the
668
612
  // user (or the agent itself, via `releaseAgent`) releases.
669
613
  const isTerminal = isFallback(agent) ||
670
- this.pinnedAgentName !== null ||
671
- this.flowOwnerAgentName !== null ||
672
- hasLifecycleHooks;
614
+ this.agentSession.getPinned() !== null ||
615
+ this.agentSession.getFlowOwner() !== null ||
616
+ !!(agent.onActivate || agent.onDeactivate);
673
617
  let agentToApply = agent;
674
618
  if (!isTerminal) {
675
619
  const declaredTools = agent.toolDefinitions;
@@ -691,7 +635,6 @@ export class OrchestratingDriver extends EventTarget {
691
635
  }
692
636
  this.chatDriver.setProviderHistoryTransform((h) => transformHistoryForAgent(h, agent.name));
693
637
  this.chatDriver.applyAgent(agentToApply);
694
- this.activeAgent = agent;
695
638
  this.dispatchEvent(new CustomEvent('orchestrating-stop'));
696
639
  this.dispatchEvent(new CustomEvent('agent-changed', { detail: agent }));
697
640
  });
@@ -709,27 +652,13 @@ export class OrchestratingDriver extends EventTarget {
709
652
  */
710
653
  releaseActiveAgent() {
711
654
  return __awaiter(this, void 0, void 0, function* () {
712
- const agent = this.activeAgent;
713
- if (!agent)
655
+ const released = yield this.agentSession.release();
656
+ if (!released)
714
657
  return;
715
- if (agent.onDeactivate) {
716
- try {
717
- yield agent.onDeactivate({
718
- agentName: agent.name,
719
- sessionKey: this.sessionKey,
720
- signal: this.lifecycleAbortController.signal,
721
- });
722
- }
723
- catch (e) {
724
- logger.warn(`OrchestratingDriver: release onDeactivate("${agent.name}") threw:`, e);
725
- }
658
+ if (released.flowOwnerChangedTo !== undefined) {
659
+ this.dispatchEvent(new CustomEvent('flow-owner-changed', { detail: released.flowOwnerChangedTo }));
726
660
  }
727
- this.activeAgent = undefined;
728
- if (this.flowOwnerAgentName !== null) {
729
- this.flowOwnerAgentName = null;
730
- this.dispatchEvent(new CustomEvent('flow-owner-changed', { detail: null }));
731
- }
732
- this.dispatchEvent(new CustomEvent('agent-released', { detail: agent }));
661
+ this.dispatchEvent(new CustomEvent('agent-released', { detail: released.previousAgent }));
733
662
  this.dispatchEvent(new CustomEvent('agent-changed', { detail: undefined }));
734
663
  });
735
664
  }
@@ -749,23 +678,12 @@ export class OrchestratingDriver extends EventTarget {
749
678
  */
750
679
  dispose() {
751
680
  return __awaiter(this, void 0, void 0, function* () {
752
- const previousAgent = this.activeAgent;
753
- if (previousAgent === null || previousAgent === void 0 ? void 0 : previousAgent.onDeactivate) {
754
- try {
755
- yield previousAgent.onDeactivate({
756
- agentName: previousAgent.name,
757
- sessionKey: this.sessionKey,
758
- signal: this.lifecycleAbortController.signal,
759
- });
760
- }
761
- catch (e) {
762
- logger.warn(`OrchestratingDriver: dispose onDeactivate("${previousAgent.name}") threw:`, e);
763
- }
764
- }
765
- this.lifecycleAbortController.abort();
681
+ // Hooks first, then teardown — the order this method has always had, so an `onDeactivate`
682
+ // still runs against a live inner driver. The session lives on that driver, so disposing it
683
+ // below repeats this call and finds nothing left to do.
684
+ yield this.agentSession.dispose();
766
685
  // Tear down the inner driver too, so any in-flight provider request aborts.
767
- this.chatDriver.dispose();
768
- this.activeAgent = undefined;
686
+ yield this.chatDriver.dispose();
769
687
  });
770
688
  }
771
689
  /**
@@ -995,8 +913,7 @@ export class OrchestratingDriver extends EventTarget {
995
913
  * — protects against stale state after an agents-array reassignment.
996
914
  */
997
915
  resolvePinnedAgent() {
998
- var _a;
999
- const name = (_a = this.flowOwnerAgentName) !== null && _a !== void 0 ? _a : this.pinnedAgentName;
916
+ const name = this.agentSession.getLockedName();
1000
917
  if (name === null)
1001
918
  return undefined;
1002
919
  const match = this.agents.find((a) => a.name === name);
package/dist/esm/index.js CHANGED
@@ -2,6 +2,7 @@ export * from './main/main';
2
2
  export * from './main/main.types';
3
3
  export * from './main/main.template';
4
4
  export * from './components/agent-picker';
5
+ export * from './components/agent-session';
5
6
  export * from './components/ai-driver';
6
7
  export * from './components/chat-driver';
7
8
  export * from './components/orchestrating-driver';
@@ -52,6 +52,7 @@ import { lastUserMessageIndex } from '../state/persistence/session-snapshot';
52
52
  import { getSessionStore, hasSessionStore, deleteSessionStore, } from '../state/session-store';
53
53
  import { pinColourAt } from '../styles/ai-colours';
54
54
  import { ChatSuggestions } from '../suggestions/chat-suggestions';
55
+ import { agentsKey } from '../utils/agents-key';
55
56
  import { resolveExclusiveLoadingStyle } from '../utils/animation-exclusivity';
56
57
  import { deleteBankedBaseline } from '../utils/banked-usage-baselines';
57
58
  import { estimateContextTokens, isContextMeasurable, } from '../utils/context-tokens';
@@ -555,7 +556,7 @@ let FoundationAiAssistant = FoundationAiAssistant_1 = class FoundationAiAssistan
555
556
  applyDeferredAgentsSwap() {
556
557
  if (!this.driver)
557
558
  return;
558
- if (this.getAgentsKey(this.agents) === this._driverAgentsKey)
559
+ if (agentsKey(this.agents) === this._driverAgentsKey)
559
560
  return;
560
561
  this.agentsChanged();
561
562
  }
@@ -1153,8 +1154,8 @@ let FoundationAiAssistant = FoundationAiAssistant_1 = class FoundationAiAssistan
1153
1154
  /**
1154
1155
  * Aggregated USD cost across every chat turn in this session.
1155
1156
  *
1156
- * Read-only (unlike previously): it is a projection of `sessionUsage`, and a
1157
- * host assigning it would have set a figure the next transcript change overwrites.
1157
+ * Read-only: a projection of `sessionUsage`, so an assigned figure would be overwritten by
1158
+ * the next transcript change.
1158
1159
  */
1159
1160
  get sessionCostUsd() {
1160
1161
  return this.sessionUsage.costUsd;
@@ -1414,14 +1415,9 @@ let FoundationAiAssistant = FoundationAiAssistant_1 = class FoundationAiAssistan
1414
1415
  this.settings.costHistorySlotted = value;
1415
1416
  }
1416
1417
  /**
1417
- * This element's driver as an orchestrator, or `undefined` when the session is
1418
- * running a bare `ChatDriver`.
1419
- *
1420
- * The five call sites that used to test the type themselves now read this
1421
- * (§11.2 constraint 3 of `docs/code-health.md`): the agent pin, the flow-owner
1422
- * lock, the wire-up in `wireDriver`, and the two `driverKind` diagnostics. The
1423
- * type test itself lives in `asOrchestrator`, shared with the session
1424
- * persister.
1418
+ * This element's driver as an orchestrator, or `undefined` when the session is running a bare
1419
+ * `ChatDriver`. The only place in this element that narrows the type; the test itself lives in
1420
+ * `asOrchestrator`, shared with the session persister.
1425
1421
  */
1426
1422
  get orchestrator() {
1427
1423
  return asOrchestrator(this.driver);
@@ -1544,7 +1540,7 @@ let FoundationAiAssistant = FoundationAiAssistant_1 = class FoundationAiAssistan
1544
1540
  // This prevents spurious driver teardowns when the collapse-mode element
1545
1541
  // connects and its parent re-assigns the same agents array, which would
1546
1542
  // destroy pending interactions in the existing driver.
1547
- const newKey = this.getAgentsKey(this.agents);
1543
+ const newKey = agentsKey(this.agents);
1548
1544
  if (newKey === this._driverAgentsKey)
1549
1545
  return;
1550
1546
  // GENC-1388: a sibling component sharing this session key (e.g. the popped-out
@@ -1597,26 +1593,6 @@ let FoundationAiAssistant = FoundationAiAssistant_1 = class FoundationAiAssistan
1597
1593
  driverKind: this.orchestrator ? 'orchestrating' : 'chat',
1598
1594
  });
1599
1595
  }
1600
- /** Returns a stable fingerprint for an agents array based on agent names and tool handler keys. */
1601
- getAgentsKey(agents) {
1602
- if (!(agents === null || agents === void 0 ? void 0 : agents.length))
1603
- return '';
1604
- return agents
1605
- .map((a) => {
1606
- var _a;
1607
- // Function-form handlers are resolved per turn — their full key set
1608
- // isn't knowable at fingerprint time. A constant marker still
1609
- // distinguishes static-vs-dynamic shape changes; finer-grained
1610
- // detection isn't load-bearing here.
1611
- const toolNames = typeof a.toolHandlers === 'function'
1612
- ? '<fn>'
1613
- : Object.keys((_a = a.toolHandlers) !== null && _a !== void 0 ? _a : {})
1614
- .sort()
1615
- .join('+');
1616
- return `${a.name}[${toolNames}]`;
1617
- })
1618
- .join(',');
1619
- }
1620
1596
  /**
1621
1597
  * Pure factory — creates a ChatDriver or OrchestratingDriver based on the
1622
1598
  * current agent configuration. Does not wire event listeners or register in
@@ -1927,7 +1903,7 @@ let FoundationAiAssistant = FoundationAiAssistant_1 = class FoundationAiAssistan
1927
1903
  * `clearSession`.
1928
1904
  */
1929
1905
  rebuildDriver(key) {
1930
- this._driverAgentsKey = this.getAgentsKey(this.agents);
1906
+ this._driverAgentsKey = agentsKey(this.agents);
1931
1907
  this.driver = getOrCreateDriver(key, this._driverAgentsKey, () => this.createDriver());
1932
1908
  this.wireDriver();
1933
1909
  }
@@ -1967,7 +1943,7 @@ let FoundationAiAssistant = FoundationAiAssistant_1 = class FoundationAiAssistan
1967
1943
  }
1968
1944
  }
1969
1945
  const driverExisted = !!getDriver(key);
1970
- this._driverAgentsKey = this.getAgentsKey(this.agents);
1946
+ this._driverAgentsKey = agentsKey(this.agents);
1971
1947
  this.driver = getOrCreateDriver(key, this._driverAgentsKey, () => this.createDriver());
1972
1948
  this.wireDriver();
1973
1949
  // Auto-restore only for a freshly created store (the page-reload case); an
@@ -2345,10 +2321,7 @@ let FoundationAiAssistant = FoundationAiAssistant_1 = class FoundationAiAssistan
2345
2321
  this.finalizeCostSession();
2346
2322
  }
2347
2323
  }
2348
- /**
2349
- * Runs side effects that were previously in `messagesChanged()`.
2350
- * Called from the `messages` setter after dispatching to the store.
2351
- */
2324
+ /** Transcript side effects, called from the `messages` setter after it dispatches to the store. */
2352
2325
  handleMessagesUpdate() {
2353
2326
  var _a, _b;
2354
2327
  // Reset the loading timer when an assistant message arrives mid-loop so each