@genesislcap/ai-assistant 14.434.0 → 14.436.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 (33) hide show
  1. package/dist/ai-assistant.api.json +1513 -70
  2. package/dist/ai-assistant.d.ts +367 -7
  3. package/dist/dts/components/ai-driver/ai-driver.d.ts +8 -0
  4. package/dist/dts/components/ai-driver/ai-driver.d.ts.map +1 -1
  5. package/dist/dts/components/chat-driver/chat-driver.d.ts +79 -3
  6. package/dist/dts/components/chat-driver/chat-driver.d.ts.map +1 -1
  7. package/dist/dts/components/orchestrating-driver/orchestrating-driver.d.ts +23 -0
  8. package/dist/dts/components/orchestrating-driver/orchestrating-driver.d.ts.map +1 -1
  9. package/dist/dts/config/config.d.ts +106 -2
  10. package/dist/dts/config/config.d.ts.map +1 -1
  11. package/dist/dts/config/define-stateful-agent.d.ts +115 -0
  12. package/dist/dts/config/define-stateful-agent.d.ts.map +1 -0
  13. package/dist/dts/index.d.ts +1 -0
  14. package/dist/dts/index.d.ts.map +1 -1
  15. package/dist/dts/main/main.d.ts +36 -4
  16. package/dist/dts/main/main.d.ts.map +1 -1
  17. package/dist/dts/main/main.template.d.ts.map +1 -1
  18. package/dist/esm/components/chat-driver/chat-driver.js +126 -11
  19. package/dist/esm/components/orchestrating-driver/orchestrating-driver.js +192 -33
  20. package/dist/esm/config/define-stateful-agent.js +174 -0
  21. package/dist/esm/index.js +1 -0
  22. package/dist/esm/main/main.js +164 -21
  23. package/dist/esm/main/main.template.js +2 -11
  24. package/dist/tsconfig.tsbuildinfo +1 -1
  25. package/package.json +16 -16
  26. package/src/components/ai-driver/ai-driver.ts +9 -0
  27. package/src/components/chat-driver/chat-driver.ts +178 -8
  28. package/src/components/orchestrating-driver/orchestrating-driver.ts +191 -17
  29. package/src/config/config.ts +112 -2
  30. package/src/config/define-stateful-agent.ts +293 -0
  31. package/src/index.ts +1 -0
  32. package/src/main/main.template.ts +2 -9
  33. package/src/main/main.ts +167 -14
@@ -5,6 +5,7 @@ import { logger } from '../../utils/logger';
5
5
  import { TOOL_FOLD_SYMBOL } from '../../utils/tool-fold';
6
6
  const DEFAULT_MAX_TOOL_ITERATIONS = 50;
7
7
  const DEFAULT_MAX_FOLD_OPERATIONS = 5;
8
+ const DEFAULT_MAX_TURN_SNAPSHOTS = 40;
8
9
  const DEFAULT_MAX_UNKNOWN_TOOL_CALLS = 5;
9
10
  const MAX_MALFORMED_RETRIES = 2;
10
11
  const MAX_EMPTY_RESPONSE_RETRIES = 3;
@@ -24,7 +25,7 @@ const HANDOFF_TOOL_RESULT_PLACEHOLDER = 'Handoff to another specialist — routi
24
25
  * @beta
25
26
  */
26
27
  export class ChatDriver extends EventTarget {
27
- constructor(aiProvider, toolHandlers = {}, toolDefinitions = [], systemPrompt, primerHistory, maxToolIterations = DEFAULT_MAX_TOOL_ITERATIONS, maxFoldOperations = DEFAULT_MAX_FOLD_OPERATIONS) {
28
+ constructor(aiProvider, toolHandlers = {}, toolDefinitions = [], systemPrompt, primerHistory, maxToolIterations = DEFAULT_MAX_TOOL_ITERATIONS, maxFoldOperations = DEFAULT_MAX_FOLD_OPERATIONS, maxTurnSnapshots = DEFAULT_MAX_TURN_SNAPSHOTS) {
28
29
  super();
29
30
  this.aiProvider = aiProvider;
30
31
  this.maxToolIterations = maxToolIterations;
@@ -39,11 +40,37 @@ export class ChatDriver extends EventTarget {
39
40
  this.consecutiveUnknownToolCalls = 0;
40
41
  /** Sub-agents declared on the active agent config, keyed by name. */
41
42
  this.subAgentsMap = new Map();
43
+ /**
44
+ * Set by `releaseAgent` inside a top-level tool handler — typically a stateful
45
+ * agent's terminal-state handler signalling that its flow is complete and the
46
+ * auto-pin lock can release. Checked by the orchestrator after `sendMessage`
47
+ * returns; the orchestrator fires `onDeactivate` and clears the pin.
48
+ *
49
+ * Reset at the start of each `sendMessage` so a release from a previous turn
50
+ * doesn't leak forward.
51
+ */
52
+ this.agentReleaseRequested = false;
53
+ /**
54
+ * Ring buffer of per-LLM-call snapshots. Cap is configurable via
55
+ * `chatConfig.agent.maxTurnSnapshots`; older entries drop off as new ones
56
+ * arrive. See {@link TurnSnapshot} for the captured shape.
57
+ */
58
+ this.turnSnapshots = [];
59
+ /** Monotonic counter that survives agent swaps — useful for cross-referencing with history. */
60
+ this.globalTurnIndex = 0;
42
61
  this.toolHandlers = toolHandlers;
43
- this.toolDefinitions = toolDefinitions;
62
+ if (typeof toolDefinitions === 'function') {
63
+ this.toolDefinitionsFactory = toolDefinitions;
64
+ this.toolDefinitions = [];
65
+ }
66
+ else {
67
+ this.toolDefinitionsFactory = undefined;
68
+ this.toolDefinitions = toolDefinitions;
69
+ }
44
70
  this.systemPrompt = systemPrompt;
45
71
  this.primerHistory = primerHistory;
46
72
  this.maxFoldOperations = maxFoldOperations;
73
+ this.maxTurnSnapshots = maxTurnSnapshots;
47
74
  }
48
75
  /**
49
76
  * Swap in a new agent's configuration. Called by OrchestratingDriver before
@@ -52,10 +79,20 @@ export class ChatDriver extends EventTarget {
52
79
  applyAgent(config) {
53
80
  var _a, _b, _c;
54
81
  this.systemPrompt = config.systemPrompt;
55
- this.toolDefinitions = (_a = config.toolDefinitions) !== null && _a !== void 0 ? _a : [];
82
+ if (typeof config.toolDefinitions === 'function') {
83
+ this.toolDefinitionsFactory = config.toolDefinitions;
84
+ // Cleared each turn by the factory in runToolLoop; empty is safe in the
85
+ // meantime (no LLM call happens before resolution).
86
+ this.toolDefinitions = [];
87
+ }
88
+ else {
89
+ this.toolDefinitionsFactory = undefined;
90
+ this.toolDefinitions = (_a = config.toolDefinitions) !== null && _a !== void 0 ? _a : [];
91
+ }
56
92
  this.toolHandlers = (_b = config.toolHandlers) !== null && _b !== void 0 ? _b : {};
57
93
  this.primerHistory = config.primerHistory;
58
94
  this.activeAgentName = config.name;
95
+ this.debugSnapshotter = config.getDebugSnapshot;
59
96
  this.subAgentsMap = new Map(((_c = config.subAgents) !== null && _c !== void 0 ? _c : []).map((s) => [s.name, s]));
60
97
  // Reset fold state when agent changes — each specialist starts fresh
61
98
  this.foldStack = [];
@@ -68,6 +105,55 @@ export class ChatDriver extends EventTarget {
68
105
  getSubAgentCompletion() {
69
106
  return this.subAgentCompletion;
70
107
  }
108
+ /**
109
+ * Returns true if `releaseAgent` was called during the most recent turn.
110
+ * Consumed by the orchestrator to trigger the auto-pin release path.
111
+ */
112
+ getAgentReleaseRequested() {
113
+ return this.agentReleaseRequested;
114
+ }
115
+ /**
116
+ * Return the per-turn snapshots captured so far. Used by the host's debug
117
+ * log exporter to show what the LLM saw on each turn — system prompt, tool
118
+ * surface, and agent-supplied state (e.g. a machine snapshot).
119
+ *
120
+ * Ring-buffered at `MAX_TURN_SNAPSHOTS`; older entries are dropped.
121
+ */
122
+ getTurnSnapshots() {
123
+ return this.turnSnapshots;
124
+ }
125
+ /**
126
+ * Push one snapshot to the ring buffer. Called inside `runToolLoop` just
127
+ * before each LLM call — that's the latest point where the prompt, tool
128
+ * surface, and agent state line up with what the model is about to see.
129
+ */
130
+ recordTurnSnapshot(resolvedSystemPrompt) {
131
+ let agentSnapshot;
132
+ if (this.debugSnapshotter) {
133
+ try {
134
+ agentSnapshot = this.debugSnapshotter();
135
+ }
136
+ catch (e) {
137
+ // A snapshotter throwing must not derail the LLM call — capture the
138
+ // error string in place of the snapshot so the export still shows
139
+ // *something* happened.
140
+ agentSnapshot = `<getDebugSnapshot threw: ${e instanceof Error ? e.message : String(e)}>`;
141
+ }
142
+ }
143
+ const turnIndex = this.globalTurnIndex;
144
+ this.globalTurnIndex += 1;
145
+ this.turnSnapshots.push({
146
+ turnIndex,
147
+ timestamp: new Date().toISOString(),
148
+ agentName: this.activeAgentName,
149
+ systemPrompt: resolvedSystemPrompt,
150
+ toolNames: this.toolDefinitions.map((t) => t.name),
151
+ agentSnapshot,
152
+ });
153
+ if (this.turnSnapshots.length > this.maxTurnSnapshots) {
154
+ this.turnSnapshots.shift();
155
+ }
156
+ }
71
157
  /**
72
158
  * Optional transform applied to conversation history immediately before each LLM request.
73
159
  * Cleared when `undefined`. Does not alter stored history.
@@ -249,6 +335,7 @@ export class ChatDriver extends EventTarget {
249
335
  }
250
336
  this.busy = true;
251
337
  this.subAgentCompletion = undefined;
338
+ this.agentReleaseRequested = false;
252
339
  this.appendToHistory({ role: 'user', content: userInput, attachments });
253
340
  try {
254
341
  return yield this.runToolLoop(userInput, attachments);
@@ -286,6 +373,13 @@ export class ChatDriver extends EventTarget {
286
373
  return;
287
374
  }
288
375
  this.subAgentCompletion = { result };
376
+ }, releaseAgent: () => {
377
+ var _a;
378
+ if (this.agentReleaseRequested) {
379
+ logger.warn(`ChatDriver(${(_a = this.activeAgentName) !== null && _a !== void 0 ? _a : 'unknown'}): releaseAgent called more than once — ignoring`);
380
+ return;
381
+ }
382
+ this.agentReleaseRequested = true;
289
383
  } });
290
384
  }
291
385
  /**
@@ -516,7 +610,7 @@ export class ChatDriver extends EventTarget {
516
610
  // ---------------------------------------------------------------------------
517
611
  runToolLoop(userInput, attachments, transientPrimer) {
518
612
  return __awaiter(this, void 0, void 0, function* () {
519
- var _a, _b, _c, _d, _e, _f;
613
+ var _a, _b, _c, _d, _e, _f, _g;
520
614
  if (!this.systemPrompt) {
521
615
  logger.warn('ChatDriver: no systemPrompt set. The assistant will have no instructions — provide a systemPrompt via agents config or the foundation-ai-assistant property.');
522
616
  }
@@ -532,11 +626,31 @@ export class ChatDriver extends EventTarget {
532
626
  let firstLlmCall = !!currentInput;
533
627
  while (iterations < this.maxToolIterations) {
534
628
  iterations += 1;
629
+ const promptCtx = {
630
+ agentName: (_a = this.activeAgentName) !== null && _a !== void 0 ? _a : '',
631
+ history: this.history,
632
+ turnIndex: iterations - 1,
633
+ signal: new AbortController().signal,
634
+ };
635
+ // Re-resolve dynamic tool definitions before each LLM call. The static
636
+ // case is a no-op (factory is undefined and `this.toolDefinitions` was
637
+ // set by applyAgent). Folds operate on `this.toolDefinitions` and are
638
+ // forbidden when a factory is set, so the array form is always valid.
639
+ // Sequential await is required — each iteration must see fresh values
640
+ // before constructing the LLM request.
641
+ if (this.toolDefinitionsFactory) {
642
+ // eslint-disable-next-line no-await-in-loop
643
+ this.toolDefinitions = yield this.toolDefinitionsFactory(promptCtx);
644
+ }
645
+ const resolvedSystemPrompt = typeof this.systemPrompt === 'function'
646
+ ? // eslint-disable-next-line no-await-in-loop
647
+ yield this.systemPrompt(promptCtx)
648
+ : this.systemPrompt;
535
649
  const foldSuffix = this.buildFoldSystemPromptSuffix();
536
- const baseSystemPrompt = this.systemPrompt
537
- ? `${this.systemPrompt}${foldSuffix}`
650
+ const baseSystemPrompt = resolvedSystemPrompt
651
+ ? `${resolvedSystemPrompt}${foldSuffix}`
538
652
  : foldSuffix || undefined;
539
- const primer = [...((_a = this.primerHistory) !== null && _a !== void 0 ? _a : []), ...(transientPrimer !== null && transientPrimer !== void 0 ? transientPrimer : [])];
653
+ const primer = [...((_b = this.primerHistory) !== null && _b !== void 0 ? _b : []), ...(transientPrimer !== null && transientPrimer !== void 0 ? transientPrimer : [])];
540
654
  const baseHistory = firstLlmCall ? this.history.slice(0, -1) : this.history;
541
655
  firstLlmCall = false;
542
656
  const historyForProvider = this.providerHistoryTransform
@@ -548,6 +662,7 @@ export class ChatDriver extends EventTarget {
548
662
  : emptyResponseAttempts > 0
549
663
  ? `${baseSystemPrompt !== null && baseSystemPrompt !== void 0 ? baseSystemPrompt : ''}\n\nIMPORTANT: You must respond to the user's message. Call the appropriate tool or provide a text response — do not return an empty response.`
550
664
  : baseSystemPrompt;
665
+ this.recordTurnSnapshot(systemPrompt);
551
666
  // Capture the pending user input, then clear the slots BEFORE the chat
552
667
  // call. `sendMessage` already appended the user message to `this.history`,
553
668
  // so on retries (empty / malformed) we must rely on history alone —
@@ -586,8 +701,8 @@ export class ChatDriver extends EventTarget {
586
701
  }
587
702
  throw e;
588
703
  }
589
- const isThinkingStep = response.content && ((_b = response.toolCalls) === null || _b === void 0 ? void 0 : _b.length);
590
- const isEmptyResponse = !((_c = response.content) === null || _c === void 0 ? void 0 : _c.trim()) && !((_d = response.toolCalls) === null || _d === void 0 ? void 0 : _d.length);
704
+ const isThinkingStep = response.content && ((_c = response.toolCalls) === null || _c === void 0 ? void 0 : _c.length);
705
+ const isEmptyResponse = !((_d = response.content) === null || _d === void 0 ? void 0 : _d.trim()) && !((_e = response.toolCalls) === null || _e === void 0 ? void 0 : _e.length);
591
706
  if (isEmptyResponse) {
592
707
  emptyResponseAttempts += 1;
593
708
  if (emptyResponseAttempts < MAX_EMPTY_RESPONSE_RETRIES) {
@@ -609,7 +724,7 @@ export class ChatDriver extends EventTarget {
609
724
  else {
610
725
  this.appendToHistory(response);
611
726
  }
612
- if (!((_e = response.toolCalls) === null || _e === void 0 ? void 0 : _e.length)) {
727
+ if (!((_f = response.toolCalls) === null || _f === void 0 ? void 0 : _f.length)) {
613
728
  break;
614
729
  }
615
730
  const [toolCalls, systemCalls] = response.toolCalls.reduce((acc, tc) => {
@@ -734,7 +849,7 @@ export class ChatDriver extends EventTarget {
734
849
  // The response was appended before execution — find it and annotate.
735
850
  let tcMsgIdx = -1;
736
851
  for (let i = this.history.length - 1; i >= 0; i -= 1) {
737
- if (this.history[i].role === 'assistant' && ((_f = this.history[i].toolCalls) === null || _f === void 0 ? void 0 : _f.length)) {
852
+ if (this.history[i].role === 'assistant' && ((_g = this.history[i].toolCalls) === null || _g === void 0 ? void 0 : _g.length)) {
738
853
  tcMsgIdx = i;
739
854
  break;
740
855
  }
@@ -1,7 +1,7 @@
1
1
  import { __awaiter } from "tslib";
2
2
  import { transformHistoryForAgent } from '../../utils/history-transform';
3
3
  import { logger } from '../../utils/logger';
4
- import { ChatDriver, REQUEST_CONTINUATION_TOOL } from '../chat-driver/chat-driver';
4
+ import { ChatDriver, REQUEST_CONTINUATION_TOOL, } from '../chat-driver/chat-driver';
5
5
  const DEFAULT_MAX_HANDOFFS = 3;
6
6
  const DEFAULT_CLASSIFIER_HISTORY_LENGTH = 4;
7
7
  const DEFAULT_CLASSIFIER_RETRIES = 2;
@@ -31,9 +31,15 @@ function isFallback(agent) {
31
31
  }
32
32
  function buildFallbackSystemPrompt(fallback, specialists) {
33
33
  const agentList = specialists.map((s) => `- ${s.name}: ${s.description}`).join('\n');
34
- if (fallback.systemPrompt) {
34
+ if (typeof fallback.systemPrompt === 'string') {
35
35
  return fallback.systemPrompt.replace('{{agents}}', agentList);
36
36
  }
37
+ if (typeof fallback.systemPrompt === 'function') {
38
+ // Function-form fallback prompt — pass through unchanged. The `{{agents}}`
39
+ // substitution is a string-template convenience; consumers using the
40
+ // function form can compose the agent list themselves if they want it.
41
+ return fallback.systemPrompt;
42
+ }
37
43
  return `You are a helpful assistant. You cannot directly help with the user's request, but the following specialists are available:\n\n${agentList}\n\nPolitely let the user know what you can help with and invite them to rephrase their request.`;
38
44
  }
39
45
  /**
@@ -45,16 +51,24 @@ function buildFallbackSystemPrompt(fallback, specialists) {
45
51
  */
46
52
  export class OrchestratingDriver extends EventTarget {
47
53
  constructor(aiProvider, agents, options = {}) {
48
- var _a, _b, _c;
54
+ var _a, _b, _c, _d;
49
55
  super();
50
56
  this.aiProvider = aiProvider;
51
57
  this.agents = agents;
58
+ /**
59
+ * Aborted on driver disposal. Threaded into `AgentLifecycleContext.signal`
60
+ * so long-running `onActivate` work can bail if the session disconnects.
61
+ */
62
+ this.lifecycleAbortController = new AbortController();
52
63
  this.pinnedAgentName = null;
53
- this.maxHandoffs = (_a = options.maxHandoffs) !== null && _a !== void 0 ? _a : DEFAULT_MAX_HANDOFFS;
64
+ this.sessionKey = (_a = options.sessionKey) !== null && _a !== void 0 ? _a : '';
65
+ this.maxHandoffs = (_b = options.maxHandoffs) !== null && _b !== void 0 ? _b : DEFAULT_MAX_HANDOFFS;
54
66
  this.classifierHistoryLength =
55
- (_b = options.classifierHistoryLength) !== null && _b !== void 0 ? _b : DEFAULT_CLASSIFIER_HISTORY_LENGTH;
56
- this.classifierRetries = (_c = options.classifierRetries) !== null && _c !== void 0 ? _c : DEFAULT_CLASSIFIER_RETRIES;
57
- this.specialists = agents.filter(isSpecialist);
67
+ (_c = options.classifierHistoryLength) !== null && _c !== void 0 ? _c : DEFAULT_CLASSIFIER_HISTORY_LENGTH;
68
+ this.classifierRetries = (_d = options.classifierRetries) !== null && _d !== void 0 ? _d : DEFAULT_CLASSIFIER_RETRIES;
69
+ // Specialists drive the classifier. `excludeFromClassifier` agents are still
70
+ // resolvable by name (so manual pinning works) but never auto-routed.
71
+ this.specialists = agents.filter(isSpecialist).filter((a) => !a.excludeFromClassifier);
58
72
  const fallbacks = agents.filter(isFallback);
59
73
  if (fallbacks.length > 1) {
60
74
  logger.warn('OrchestratingDriver: multiple fallback agents found — only the first will be used.');
@@ -62,7 +76,7 @@ export class OrchestratingDriver extends EventTarget {
62
76
  const rawFallback = fallbacks[0];
63
77
  this.fallback = rawFallback
64
78
  ? Object.assign(Object.assign({}, rawFallback), { systemPrompt: buildFallbackSystemPrompt(rawFallback, this.specialists) }) : undefined;
65
- this.chatDriver = new ChatDriver(aiProvider, {}, [], undefined, undefined, options.maxToolIterations, options.maxFoldOperations);
79
+ this.chatDriver = new ChatDriver(aiProvider, {}, [], undefined, undefined, options.maxToolIterations, options.maxFoldOperations, options.maxTurnSnapshots);
66
80
  // Proxy events from the shared driver
67
81
  this.chatDriver.addEventListener('history-updated', (e) => {
68
82
  this.dispatchEvent(new CustomEvent('history-updated', { detail: e.detail }));
@@ -98,20 +112,24 @@ export class OrchestratingDriver extends EventTarget {
98
112
  getRawHistory() {
99
113
  return this.chatDriver.getHistory();
100
114
  }
115
+ /** Delegates to the inner {@link ChatDriver} — turns are captured there. */
116
+ getTurnSnapshots() {
117
+ return this.chatDriver.getTurnSnapshots();
118
+ }
101
119
  getSuggestions(history, prompt, count, allAgentInfo) {
102
120
  return __awaiter(this, void 0, void 0, function* () {
103
121
  // When pinned to a specialist, scope suggestions to that agent so the
104
122
  // proposed prompts reflect the active routing instead of the full panel.
105
123
  const pinned = this.resolvePinnedAgent();
106
124
  const candidates = pinned && isSpecialist(pinned) ? [pinned] : this.specialists;
107
- const agentInfo = candidates.map((s) => {
108
- var _a;
109
- return ({
110
- name: s.name,
111
- description: s.description,
112
- tools: (_a = s.toolDefinitions) !== null && _a !== void 0 ? _a : [],
113
- });
114
- });
125
+ const agentInfo = candidates.map((s) => ({
126
+ name: s.name,
127
+ description: s.description,
128
+ // Suggestions use tool names for prompt hints. Dynamic agents resolve
129
+ // their tools per-turn against a SystemPromptContext we don't have here
130
+ // pass an empty list rather than invoke the factory with a fake one.
131
+ tools: Array.isArray(s.toolDefinitions) ? s.toolDefinitions : [],
132
+ }));
115
133
  return this.chatDriver.getSuggestions(history, prompt, count, agentInfo);
116
134
  });
117
135
  }
@@ -132,7 +150,8 @@ export class OrchestratingDriver extends EventTarget {
132
150
  let handoffSummary = '';
133
151
  let remainingTask = '';
134
152
  while (true) {
135
- this.applyAgent(currentAgent);
153
+ // eslint-disable-next-line no-await-in-loop
154
+ yield this.applyAgent(currentAgent);
136
155
  let result;
137
156
  if (isHandoff) {
138
157
  const contextPrimer = handoffSummary
@@ -145,6 +164,15 @@ export class OrchestratingDriver extends EventTarget {
145
164
  // eslint-disable-next-line no-await-in-loop
146
165
  result = yield this.chatDriver.sendMessage(input, attachments);
147
166
  }
167
+ // Release check: a stateful agent called `releaseAgent` from a terminal
168
+ // tool handler. Fire onDeactivate, clear the pin, drop the user back to
169
+ // classifier-mode. The LLM has already emitted its final wrap-up message
170
+ // by the time we get here — release is purely a teardown.
171
+ if (this.chatDriver.getAgentReleaseRequested()) {
172
+ // eslint-disable-next-line no-await-in-loop
173
+ yield this.releaseActiveAgent();
174
+ break;
175
+ }
148
176
  // Pinned agents never hand off — the continuation tool is filtered out in
149
177
  // applyAgent, but this guards against a model hallucinating a handoff result.
150
178
  if (result.reason !== 'agent-handoff' || isFallback(currentAgent) || pinned) {
@@ -172,27 +200,158 @@ export class OrchestratingDriver extends EventTarget {
172
200
  });
173
201
  }
174
202
  applyAgent(agent) {
175
- var _a;
176
- // Fallback and pinned agents are terminal — neither should hand off.
177
- const isTerminal = isFallback(agent) || this.pinnedAgentName !== null;
178
- const agentToApply = isTerminal
179
- ? agent
180
- : Object.assign(Object.assign({}, agent), { toolDefinitions: [...((_a = agent.toolDefinitions) !== null && _a !== void 0 ? _a : []), REQUEST_CONTINUATION_DEFINITION] });
181
- const previousAgent = this.activeAgent;
182
- if (previousAgent && previousAgent.name !== agent.name) {
183
- const rawHistory = this.chatDriver.getHistory();
184
- this.chatDriver.loadHistory([...rawHistory, { role: 'system-event', content: agent.name }]);
185
- }
186
- this.chatDriver.setProviderHistoryTransform((h) => transformHistoryForAgent(h, agent.name));
187
- this.chatDriver.applyAgent(agentToApply);
188
- this.activeAgent = agent;
189
- this.dispatchEvent(new CustomEvent('orchestrating-stop'));
190
- this.dispatchEvent(new CustomEvent('agent-changed', { detail: agent }));
203
+ return __awaiter(this, void 0, void 0, function* () {
204
+ const previousAgent = this.activeAgent;
205
+ const isSwitch = !previousAgent || previousAgent.name !== agent.name;
206
+ // Fire lifecycle hooks around the swap — outgoing first, then incoming.
207
+ // Both are awaited so a heavy `onActivate` (e.g. machine restore) completes
208
+ // before the agent's first turn runs.
209
+ if (isSwitch && (previousAgent === null || previousAgent === void 0 ? void 0 : previousAgent.onDeactivate)) {
210
+ try {
211
+ yield previousAgent.onDeactivate({
212
+ agentName: previousAgent.name,
213
+ sessionKey: this.sessionKey,
214
+ signal: this.lifecycleAbortController.signal,
215
+ });
216
+ }
217
+ catch (e) {
218
+ logger.warn(`OrchestratingDriver: onDeactivate("${previousAgent.name}") threw:`, e);
219
+ }
220
+ }
221
+ if (isSwitch && agent.onActivate) {
222
+ try {
223
+ yield agent.onActivate({
224
+ agentName: agent.name,
225
+ sessionKey: this.sessionKey,
226
+ previousAgentName: previousAgent === null || previousAgent === void 0 ? void 0 : previousAgent.name,
227
+ signal: this.lifecycleAbortController.signal,
228
+ });
229
+ }
230
+ catch (e) {
231
+ logger.warn(`OrchestratingDriver: onActivate("${agent.name}") threw:`, e);
232
+ }
233
+ }
234
+ const hasLifecycleHooks = !!(agent.onActivate || agent.onDeactivate);
235
+ // Stateful agents auto-pin on activation. The pin guarantees the machine
236
+ // survives subsequent turns (the classifier would otherwise be free to
237
+ // route away mid-flow, tearing the machine down). Release happens when the
238
+ // agent calls `releaseAgent` from a terminal-state tool handler — see the
239
+ // post-sendMessage check below.
240
+ if (isSwitch && hasLifecycleHooks && this.pinnedAgentName !== agent.name) {
241
+ this.pinnedAgentName = agent.name;
242
+ this.dispatchEvent(new CustomEvent('pinned-changed', { detail: agent.name }));
243
+ }
244
+ // Terminal agents do not get the cross-agent handoff tool. Three cases:
245
+ // • fallback — already a leaf; handoff would loop
246
+ // • pinned — user explicitly selected this agent; do not auto-route away
247
+ // • stateful — agents with lifecycle hooks own state for the duration of
248
+ // their flow. Initiating a handoff mid-flow would abandon
249
+ // that state with no clean exit and dump the user into the
250
+ // classifier mid-machine. Capture the tool loop until the
251
+ // user (or the agent itself, via `releaseAgent`) releases.
252
+ const isTerminal = isFallback(agent) || this.pinnedAgentName !== null || hasLifecycleHooks;
253
+ let agentToApply = agent;
254
+ if (!isTerminal) {
255
+ const declaredTools = agent.toolDefinitions;
256
+ agentToApply = Object.assign(Object.assign({}, agent), { toolDefinitions: typeof declaredTools === 'function'
257
+ ? (ctx) => __awaiter(this, void 0, void 0, function* () {
258
+ return [
259
+ ...(yield declaredTools(ctx)),
260
+ REQUEST_CONTINUATION_DEFINITION,
261
+ ];
262
+ })
263
+ : [...(declaredTools !== null && declaredTools !== void 0 ? declaredTools : []), REQUEST_CONTINUATION_DEFINITION] });
264
+ }
265
+ if (previousAgent && previousAgent.name !== agent.name) {
266
+ const rawHistory = this.chatDriver.getHistory();
267
+ this.chatDriver.loadHistory([...rawHistory, { role: 'system-event', content: agent.name }]);
268
+ }
269
+ this.chatDriver.setProviderHistoryTransform((h) => transformHistoryForAgent(h, agent.name));
270
+ this.chatDriver.applyAgent(agentToApply);
271
+ this.activeAgent = agent;
272
+ this.dispatchEvent(new CustomEvent('orchestrating-stop'));
273
+ this.dispatchEvent(new CustomEvent('agent-changed', { detail: agent }));
274
+ });
275
+ }
276
+ /**
277
+ * Release the current stateful agent: fire `onDeactivate`, clear the pin,
278
+ * dispatch events so the host (and Redux) reflect the unpinned state. Called
279
+ * automatically when a tool handler invokes `context.releaseAgent`.
280
+ */
281
+ releaseActiveAgent() {
282
+ return __awaiter(this, void 0, void 0, function* () {
283
+ const agent = this.activeAgent;
284
+ if (!agent)
285
+ return;
286
+ if (agent.onDeactivate) {
287
+ try {
288
+ yield agent.onDeactivate({
289
+ agentName: agent.name,
290
+ sessionKey: this.sessionKey,
291
+ signal: this.lifecycleAbortController.signal,
292
+ });
293
+ }
294
+ catch (e) {
295
+ logger.warn(`OrchestratingDriver: release onDeactivate("${agent.name}") threw:`, e);
296
+ }
297
+ }
298
+ this.activeAgent = undefined;
299
+ if (this.pinnedAgentName !== null) {
300
+ this.pinnedAgentName = null;
301
+ this.dispatchEvent(new CustomEvent('pinned-changed', { detail: null }));
302
+ }
303
+ this.dispatchEvent(new CustomEvent('agent-released', { detail: agent }));
304
+ this.dispatchEvent(new CustomEvent('agent-changed', { detail: undefined }));
305
+ });
306
+ }
307
+ /**
308
+ * Fire `onDeactivate` on the current active agent and abort any pending
309
+ * lifecycle work. Called by the host on session teardown so machines can
310
+ * release resources cleanly.
311
+ */
312
+ dispose() {
313
+ return __awaiter(this, void 0, void 0, function* () {
314
+ const previousAgent = this.activeAgent;
315
+ if (previousAgent === null || previousAgent === void 0 ? void 0 : previousAgent.onDeactivate) {
316
+ try {
317
+ yield previousAgent.onDeactivate({
318
+ agentName: previousAgent.name,
319
+ sessionKey: this.sessionKey,
320
+ signal: this.lifecycleAbortController.signal,
321
+ });
322
+ }
323
+ catch (e) {
324
+ logger.warn(`OrchestratingDriver: dispose onDeactivate("${previousAgent.name}") threw:`, e);
325
+ }
326
+ }
327
+ this.lifecycleAbortController.abort();
328
+ this.activeAgent = undefined;
329
+ });
191
330
  }
192
331
  classify(input, history, contextAgent) {
193
332
  return __awaiter(this, void 0, void 0, function* () {
194
333
  var _a, _b;
334
+ // Single-candidate short-circuits. No point asking the LLM to route
335
+ // when there's only one viable choice. Skipped if a fallback is
336
+ // configured — that's an explicit "escape hatch" signal from the
337
+ // consumer, and we preserve the LLM's ability to send unrelated
338
+ // messages there by returning -1 from select_agent.
339
+ if (this.specialists.length === 1 && !this.fallback) {
340
+ return this.specialists[0];
341
+ }
195
342
  if (this.specialists.length === 0) {
343
+ // No classifier-eligible specialists. If exactly one non-fallback
344
+ // agent exists (typically a stateful agent flagged
345
+ // `excludeFromClassifier`) and there's no fallback to preserve as an
346
+ // escape hatch, route to it — this is what fixes the previously-
347
+ // silent single-stateful-agent case. Otherwise drop to the fallback;
348
+ // excluded specialists remain reachable via manual pin.
349
+ if (!this.fallback) {
350
+ const routable = this.agents.filter((a) => !isFallback(a));
351
+ if (routable.length === 1) {
352
+ return routable[0];
353
+ }
354
+ }
196
355
  return (_a = this.fallback) !== null && _a !== void 0 ? _a : { name: 'Assistant', fallback: true };
197
356
  }
198
357
  const agentList = this.specialists