@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.
- package/dist/ai-assistant.api.json +1513 -70
- package/dist/ai-assistant.d.ts +367 -7
- package/dist/dts/components/ai-driver/ai-driver.d.ts +8 -0
- package/dist/dts/components/ai-driver/ai-driver.d.ts.map +1 -1
- package/dist/dts/components/chat-driver/chat-driver.d.ts +79 -3
- package/dist/dts/components/chat-driver/chat-driver.d.ts.map +1 -1
- package/dist/dts/components/orchestrating-driver/orchestrating-driver.d.ts +23 -0
- package/dist/dts/components/orchestrating-driver/orchestrating-driver.d.ts.map +1 -1
- package/dist/dts/config/config.d.ts +106 -2
- package/dist/dts/config/config.d.ts.map +1 -1
- package/dist/dts/config/define-stateful-agent.d.ts +115 -0
- package/dist/dts/config/define-stateful-agent.d.ts.map +1 -0
- package/dist/dts/index.d.ts +1 -0
- package/dist/dts/index.d.ts.map +1 -1
- package/dist/dts/main/main.d.ts +36 -4
- package/dist/dts/main/main.d.ts.map +1 -1
- package/dist/dts/main/main.template.d.ts.map +1 -1
- package/dist/esm/components/chat-driver/chat-driver.js +126 -11
- package/dist/esm/components/orchestrating-driver/orchestrating-driver.js +192 -33
- package/dist/esm/config/define-stateful-agent.js +174 -0
- package/dist/esm/index.js +1 -0
- package/dist/esm/main/main.js +164 -21
- package/dist/esm/main/main.template.js +2 -11
- package/dist/tsconfig.tsbuildinfo +1 -1
- package/package.json +16 -16
- package/src/components/ai-driver/ai-driver.ts +9 -0
- package/src/components/chat-driver/chat-driver.ts +178 -8
- package/src/components/orchestrating-driver/orchestrating-driver.ts +191 -17
- package/src/config/config.ts +112 -2
- package/src/config/define-stateful-agent.ts +293 -0
- package/src/index.ts +1 -0
- package/src/main/main.template.ts +2 -9
- 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
|
-
|
|
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
|
-
|
|
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 =
|
|
537
|
-
? `${
|
|
650
|
+
const baseSystemPrompt = resolvedSystemPrompt
|
|
651
|
+
? `${resolvedSystemPrompt}${foldSuffix}`
|
|
538
652
|
: foldSuffix || undefined;
|
|
539
|
-
const primer = [...((
|
|
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 && ((
|
|
590
|
-
const isEmptyResponse = !((
|
|
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 (!((
|
|
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' && ((
|
|
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.
|
|
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
|
-
(
|
|
56
|
-
this.classifierRetries = (
|
|
57
|
-
|
|
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
|
-
|
|
109
|
-
|
|
110
|
-
|
|
111
|
-
|
|
112
|
-
|
|
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
|
-
|
|
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
|
-
|
|
176
|
-
|
|
177
|
-
|
|
178
|
-
|
|
179
|
-
|
|
180
|
-
|
|
181
|
-
|
|
182
|
-
|
|
183
|
-
|
|
184
|
-
|
|
185
|
-
|
|
186
|
-
|
|
187
|
-
|
|
188
|
-
|
|
189
|
-
|
|
190
|
-
|
|
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
|