@genesislcap/ai-assistant 14.432.2 → 14.433.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/api-extractor.json +8 -1
- package/dist/ai-assistant.api.json +1216 -141
- package/dist/ai-assistant.d.ts +216 -15
- package/dist/dts/components/agent-picker/agent-picker.d.ts +69 -0
- package/dist/dts/components/agent-picker/agent-picker.d.ts.map +1 -0
- package/dist/dts/components/agent-picker/agent-picker.styles.d.ts +2 -0
- package/dist/dts/components/agent-picker/agent-picker.styles.d.ts.map +1 -0
- package/dist/dts/components/agent-picker/agent-picker.template.d.ts +5 -0
- package/dist/dts/components/agent-picker/agent-picker.template.d.ts.map +1 -0
- package/dist/dts/components/agent-picker/index.d.ts +2 -0
- package/dist/dts/components/agent-picker/index.d.ts.map +1 -0
- package/dist/dts/components/chat-driver/chat-driver.d.ts +21 -0
- package/dist/dts/components/chat-driver/chat-driver.d.ts.map +1 -1
- package/dist/dts/components/orchestrating-driver/orchestrating-driver.d.ts +14 -0
- package/dist/dts/components/orchestrating-driver/orchestrating-driver.d.ts.map +1 -1
- package/dist/dts/config/config.d.ts +22 -12
- package/dist/dts/config/config.d.ts.map +1 -1
- package/dist/dts/index.d.ts +1 -0
- package/dist/dts/index.d.ts.map +1 -1
- package/dist/dts/main/main.d.ts +72 -4
- package/dist/dts/main/main.d.ts.map +1 -1
- package/dist/dts/main/main.styles.d.ts.map +1 -1
- package/dist/dts/main/main.template.d.ts.map +1 -1
- package/dist/dts/main/main.types.d.ts +1 -0
- package/dist/dts/main/main.types.d.ts.map +1 -1
- package/dist/dts/state/ai-assistant-slice.d.ts +39 -1
- package/dist/dts/state/ai-assistant-slice.d.ts.map +1 -1
- package/dist/dts/state/session-store.d.ts +6 -0
- package/dist/dts/state/session-store.d.ts.map +1 -1
- package/dist/dts/utils/animated-panel-toggle.d.ts +26 -0
- package/dist/dts/utils/animated-panel-toggle.d.ts.map +1 -0
- package/dist/dts/utils/index.d.ts +1 -0
- package/dist/dts/utils/index.d.ts.map +1 -1
- package/dist/esm/components/agent-picker/agent-picker.js +157 -0
- package/dist/esm/components/agent-picker/agent-picker.styles.js +73 -0
- package/dist/esm/components/agent-picker/agent-picker.template.js +72 -0
- package/dist/esm/components/agent-picker/index.js +1 -0
- package/dist/esm/components/chat-driver/chat-driver.js +48 -6
- package/dist/esm/components/orchestrating-driver/orchestrating-driver.js +43 -6
- package/dist/esm/index.js +1 -0
- package/dist/esm/main/main.js +215 -21
- package/dist/esm/main/main.styles.js +59 -0
- package/dist/esm/main/main.template.js +66 -12
- package/dist/esm/state/ai-assistant-slice.js +15 -0
- package/dist/esm/utils/animated-panel-toggle.js +62 -0
- package/dist/esm/utils/index.js +1 -0
- package/dist/tsconfig.tsbuildinfo +1 -1
- package/docs/gemini-empty-response.md +110 -0
- package/package.json +16 -16
- package/src/components/agent-picker/agent-picker.styles.ts +74 -0
- package/src/components/agent-picker/agent-picker.template.ts +88 -0
- package/src/components/agent-picker/agent-picker.ts +148 -0
- package/src/components/agent-picker/index.ts +1 -0
- package/src/components/chat-driver/chat-driver.ts +65 -8
- package/src/components/orchestrating-driver/orchestrating-driver.ts +45 -6
- package/src/config/config.ts +28 -11
- package/src/index.ts +1 -0
- package/src/main/main.styles.ts +59 -0
- package/src/main/main.template.ts +79 -13
- package/src/main/main.ts +220 -19
- package/src/main/main.types.ts +2 -0
- package/src/state/ai-assistant-slice.ts +51 -1
- package/src/utils/animated-panel-toggle.ts +62 -0
- package/src/utils/index.ts +1 -0
|
@@ -0,0 +1,148 @@
|
|
|
1
|
+
import {
|
|
2
|
+
attr,
|
|
3
|
+
customElement,
|
|
4
|
+
GenesisElement,
|
|
5
|
+
html,
|
|
6
|
+
observable,
|
|
7
|
+
volatile,
|
|
8
|
+
} from '@genesislcap/web-core';
|
|
9
|
+
import type { AgentConfig } from '../../config/config';
|
|
10
|
+
import type { AgentPickerMode } from '../../main/main.types';
|
|
11
|
+
import { styles } from './agent-picker.styles';
|
|
12
|
+
import { AgentPickerTemplate } from './agent-picker.template';
|
|
13
|
+
|
|
14
|
+
/** Sentinel value used by the segmented control / select to represent "Auto". */
|
|
15
|
+
export const AGENT_PICKER_AUTO_VALUE = '__auto__';
|
|
16
|
+
|
|
17
|
+
/** Tolerance for `scrollWidth > clientWidth` overflow detection (sub-pixel rounding). */
|
|
18
|
+
const OVERFLOW_TOLERANCE_PX = 1;
|
|
19
|
+
|
|
20
|
+
/**
|
|
21
|
+
* User-facing agent picker rendered above the chat input.
|
|
22
|
+
*
|
|
23
|
+
* Renders `Auto` plus each top-level agent that has `manualSelection.enabled`.
|
|
24
|
+
* Sub-agents and non-selectable specialists are excluded. Hidden when fewer
|
|
25
|
+
* than two agents are configured or none are manually selectable.
|
|
26
|
+
*
|
|
27
|
+
* @fires agent-pinned - Fired when the user changes selection. `detail` is the
|
|
28
|
+
* agent name (string) or `null` for Auto.
|
|
29
|
+
*
|
|
30
|
+
* @beta
|
|
31
|
+
*/
|
|
32
|
+
@customElement({
|
|
33
|
+
name: 'foundation-ai-agent-picker',
|
|
34
|
+
template: html`
|
|
35
|
+
${(x: AgentPicker) => AgentPickerTemplate(x.designSystemPrefix)}
|
|
36
|
+
`,
|
|
37
|
+
styles,
|
|
38
|
+
})
|
|
39
|
+
export class AgentPicker extends GenesisElement {
|
|
40
|
+
/** Design-system tag prefix, e.g. `'rapid'` for `rapid-segmented-control`. */
|
|
41
|
+
@attr({ attribute: 'design-system-prefix' }) designSystemPrefix: string = 'rapid';
|
|
42
|
+
/** Picker variant, sourced from the assistant's `chatConfig.agentPicker`. */
|
|
43
|
+
@observable mode: AgentPickerMode = 'disabled';
|
|
44
|
+
/** Top-level agents passed to the assistant. */
|
|
45
|
+
@observable agents: AgentConfig[] = [];
|
|
46
|
+
/** Currently pinned agent name, or `null` for Auto. */
|
|
47
|
+
@observable pinnedAgentName: string | null = null;
|
|
48
|
+
|
|
49
|
+
/** @internal — set via the template `${ref(...)}` binding. */
|
|
50
|
+
segmentedRowEl?: HTMLElement;
|
|
51
|
+
|
|
52
|
+
/**
|
|
53
|
+
* @internal — true when the segmented row's content overflows its container.
|
|
54
|
+
* Drives the fallback to `select` rendering. Only meaningful when
|
|
55
|
+
* `mode === 'segmented-control'`.
|
|
56
|
+
*/
|
|
57
|
+
@observable overflowing: boolean = false;
|
|
58
|
+
|
|
59
|
+
private resizeObserver?: ResizeObserver;
|
|
60
|
+
|
|
61
|
+
/** Top-level agents that opted in to manual selection. */
|
|
62
|
+
@volatile
|
|
63
|
+
get selectableAgents(): AgentConfig[] {
|
|
64
|
+
return (this.agents ?? []).filter((a) => a.manualSelection?.enabled);
|
|
65
|
+
}
|
|
66
|
+
|
|
67
|
+
/** Whether the picker should render at all. */
|
|
68
|
+
@volatile
|
|
69
|
+
get visible(): boolean {
|
|
70
|
+
if (this.mode === 'disabled') return false;
|
|
71
|
+
if ((this.agents?.length ?? 0) <= 1) return false;
|
|
72
|
+
if (this.selectableAgents.length === 0) return false;
|
|
73
|
+
return true;
|
|
74
|
+
}
|
|
75
|
+
|
|
76
|
+
/**
|
|
77
|
+
* The variant actually rendered: `'select'` if user-configured or if the
|
|
78
|
+
* segmented row currently overflows; `'segmented-control'` otherwise.
|
|
79
|
+
*/
|
|
80
|
+
@volatile
|
|
81
|
+
get effectiveMode(): 'select' | 'segmented-control' {
|
|
82
|
+
if (this.mode === 'segmented-control') {
|
|
83
|
+
return this.overflowing ? 'select' : 'segmented-control';
|
|
84
|
+
}
|
|
85
|
+
return 'select';
|
|
86
|
+
}
|
|
87
|
+
|
|
88
|
+
override connectedCallback(): void {
|
|
89
|
+
super.connectedCallback();
|
|
90
|
+
if (typeof ResizeObserver === 'undefined') return;
|
|
91
|
+
this.resizeObserver = new ResizeObserver(() => this.measureOverflow());
|
|
92
|
+
this.resizeObserver.observe(this);
|
|
93
|
+
}
|
|
94
|
+
|
|
95
|
+
override disconnectedCallback(): void {
|
|
96
|
+
super.disconnectedCallback();
|
|
97
|
+
this.resizeObserver?.disconnect();
|
|
98
|
+
this.resizeObserver = undefined;
|
|
99
|
+
}
|
|
100
|
+
|
|
101
|
+
/**
|
|
102
|
+
* Re-measure when agents change — newly added selectable agents may push the
|
|
103
|
+
* segmented row past the container's width.
|
|
104
|
+
*/
|
|
105
|
+
agentsChanged(): void {
|
|
106
|
+
queueMicrotask(() => this.measureOverflow());
|
|
107
|
+
}
|
|
108
|
+
|
|
109
|
+
modeChanged(): void {
|
|
110
|
+
queueMicrotask(() => this.measureOverflow());
|
|
111
|
+
}
|
|
112
|
+
|
|
113
|
+
/**
|
|
114
|
+
* Measures whether the segmented row's natural content width exceeds the
|
|
115
|
+
* container's visible width. Bails when the row isn't currently rendered
|
|
116
|
+
* (e.g. when `mode !== 'segmented-control'`).
|
|
117
|
+
*/
|
|
118
|
+
private measureOverflow(): void {
|
|
119
|
+
const row = this.segmentedRowEl;
|
|
120
|
+
if (!row) {
|
|
121
|
+
this.overflowing = false;
|
|
122
|
+
return;
|
|
123
|
+
}
|
|
124
|
+
this.overflowing = row.scrollWidth > row.clientWidth + OVERFLOW_TOLERANCE_PX;
|
|
125
|
+
}
|
|
126
|
+
|
|
127
|
+
selectAgent(name: string | null): void {
|
|
128
|
+
this.pinnedAgentName = name;
|
|
129
|
+
// Always emit — the host may close a slide-out panel on click even when
|
|
130
|
+
// the selection didn't change.
|
|
131
|
+
this.$emit('agent-pinned', name);
|
|
132
|
+
}
|
|
133
|
+
|
|
134
|
+
/**
|
|
135
|
+
* Maps a value from the segmented-control or select (which uses
|
|
136
|
+
* {@link AGENT_PICKER_AUTO_VALUE} for Auto) back to the `string | null` form
|
|
137
|
+
* expected by `selectAgent`.
|
|
138
|
+
*/
|
|
139
|
+
selectByValue(value: string): void {
|
|
140
|
+
this.selectAgent(value === AGENT_PICKER_AUTO_VALUE ? null : value);
|
|
141
|
+
}
|
|
142
|
+
|
|
143
|
+
/** Current pinned agent name as a value the segmented-control / select can bind to. */
|
|
144
|
+
@volatile
|
|
145
|
+
get currentValue(): string {
|
|
146
|
+
return this.pinnedAgentName ?? AGENT_PICKER_AUTO_VALUE;
|
|
147
|
+
}
|
|
148
|
+
}
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
export * from './agent-picker';
|
|
@@ -66,6 +66,13 @@ export class ChatDriver extends EventTarget implements AiDriver {
|
|
|
66
66
|
private toolHandlers: ChatToolHandlers;
|
|
67
67
|
private primerHistory?: ChatMessage[];
|
|
68
68
|
private activeAgentName?: string;
|
|
69
|
+
/**
|
|
70
|
+
* When set, `requestInteraction` delegates to this callback instead of using
|
|
71
|
+
* this driver's own pending map. Wired by `invokeSubAgent` so a sub-agent's
|
|
72
|
+
* widget renders in — and resolves through — the parent (ultimately the
|
|
73
|
+
* root) driver, where the main UI is listening.
|
|
74
|
+
*/
|
|
75
|
+
private hostInteractionRequester?: <T>(componentName: string, data: any) => Promise<T>;
|
|
69
76
|
/**
|
|
70
77
|
* When set (e.g. by OrchestratingDriver), applied only to the conversation slice
|
|
71
78
|
* sent to the model — stored `history` stays unchanged for UI and logging.
|
|
@@ -237,14 +244,43 @@ export class ChatDriver extends EventTarget implements AiDriver {
|
|
|
237
244
|
return this.busy;
|
|
238
245
|
}
|
|
239
246
|
|
|
247
|
+
/**
|
|
248
|
+
* Wire a parent driver as the host for this driver's interactions. When set,
|
|
249
|
+
* `requestInteraction` delegates upward so the widget renders in (and
|
|
250
|
+
* resolves through) the parent's history and pending map. Calls chain
|
|
251
|
+
* naturally: a grandchild → child → root.
|
|
252
|
+
*/
|
|
253
|
+
public setHostInteractionRequester(
|
|
254
|
+
fn: <T>(componentName: string, data: any) => Promise<T>,
|
|
255
|
+
): void {
|
|
256
|
+
this.hostInteractionRequester = fn;
|
|
257
|
+
}
|
|
258
|
+
|
|
240
259
|
/**
|
|
241
260
|
* Request a custom UI interaction. Emits a new message with the interaction.
|
|
242
261
|
* Tool handlers can call this to pause execution until the user completes the UI interaction.
|
|
243
262
|
*
|
|
263
|
+
* If a host requester is wired (sub-agent case), the call delegates upward
|
|
264
|
+
* so the interaction lives on the parent — the main UI is only listening to
|
|
265
|
+
* the root driver. Only one interaction may be in flight at any time on a
|
|
266
|
+
* given root: concurrent calls (e.g. two parallel sub-agents both spawning a
|
|
267
|
+
* widget) throw. Parallel sub-agents are for parallel work, not for user
|
|
268
|
+
* interaction, which is inherently sequential.
|
|
269
|
+
*
|
|
244
270
|
* @param componentName - The custom element name to render.
|
|
245
271
|
* @param data - Data to pass to the component.
|
|
246
272
|
*/
|
|
247
273
|
public async requestInteraction<T>(componentName: string, data: any): Promise<T> {
|
|
274
|
+
if (this.hostInteractionRequester) {
|
|
275
|
+
return this.hostInteractionRequester<T>(componentName, data);
|
|
276
|
+
}
|
|
277
|
+
if (this.pendingInteractions.size > 0) {
|
|
278
|
+
throw new Error(
|
|
279
|
+
'requestInteraction: another user interaction is already in flight. ' +
|
|
280
|
+
'Only one interaction may be active at a time — sequence them in a single tool handler ' +
|
|
281
|
+
'rather than spawning widgets from parallel sub-agents or parallel tool calls.',
|
|
282
|
+
);
|
|
283
|
+
}
|
|
248
284
|
const interactionId = crypto.randomUUID();
|
|
249
285
|
return new Promise((resolve, reject) => {
|
|
250
286
|
this.pendingInteractions.set(interactionId, { resolve, reject });
|
|
@@ -396,6 +432,14 @@ export class ChatDriver extends EventTarget implements AiDriver {
|
|
|
396
432
|
|
|
397
433
|
const child = new ChatDriver(this.aiProvider);
|
|
398
434
|
child.applyAgent({ ...subConfig, primerHistory: effectivePrimer });
|
|
435
|
+
// Route interactions back through this driver so widgets render in the
|
|
436
|
+
// parent's (ultimately the root's) history and resolve via the same
|
|
437
|
+
// pending map the main UI is wired to. Recurses naturally for nested
|
|
438
|
+
// sub-agents.
|
|
439
|
+
child.setHostInteractionRequester(
|
|
440
|
+
<R>(componentName: string, data: any): Promise<R> =>
|
|
441
|
+
this.requestInteraction<R>(componentName, data),
|
|
442
|
+
);
|
|
399
443
|
|
|
400
444
|
const forwardTrace = (e: Event) => {
|
|
401
445
|
this.dispatchEvent(
|
|
@@ -406,12 +450,18 @@ export class ChatDriver extends EventTarget implements AiDriver {
|
|
|
406
450
|
};
|
|
407
451
|
child.addEventListener('history-updated', forwardTrace);
|
|
408
452
|
|
|
409
|
-
|
|
453
|
+
// Unique per-invocation id so listeners can pair start/stop reliably even
|
|
454
|
+
// when the same sub-agent runs multiple times in parallel.
|
|
455
|
+
const invocationId = crypto.randomUUID();
|
|
456
|
+
const chatInputDuringExecution = options?.chatInputDuringExecution;
|
|
457
|
+
const lifecycleDetail = { name, invocationId, chatInputDuringExecution };
|
|
458
|
+
|
|
459
|
+
this.dispatchEvent(new CustomEvent('sub-agent-start', { detail: lifecycleDetail }));
|
|
410
460
|
try {
|
|
411
461
|
await child.sendMessage(task ?? '');
|
|
412
462
|
} finally {
|
|
413
463
|
child.removeEventListener('history-updated', forwardTrace);
|
|
414
|
-
this.dispatchEvent(new CustomEvent('sub-agent-stop', { detail:
|
|
464
|
+
this.dispatchEvent(new CustomEvent('sub-agent-stop', { detail: lifecycleDetail }));
|
|
415
465
|
}
|
|
416
466
|
|
|
417
467
|
const trace = child.getHistory() as ChatMessage[];
|
|
@@ -651,17 +701,28 @@ export class ChatDriver extends EventTarget implements AiDriver {
|
|
|
651
701
|
? `${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.`
|
|
652
702
|
: baseSystemPrompt;
|
|
653
703
|
|
|
704
|
+
// Capture the pending user input, then clear the slots BEFORE the chat
|
|
705
|
+
// call. `sendMessage` already appended the user message to `this.history`,
|
|
706
|
+
// so on retries (empty / malformed) we must rely on history alone —
|
|
707
|
+
// otherwise the message gets sent twice (once via history, once via
|
|
708
|
+
// `currentInput`), which Gemini answers with an empty response and then
|
|
709
|
+
// we retry forever.
|
|
710
|
+
const userInputForCall = currentInput;
|
|
711
|
+
const attachmentsForCall = currentAttachments;
|
|
712
|
+
currentInput = '';
|
|
713
|
+
currentAttachments = undefined;
|
|
714
|
+
|
|
654
715
|
const options: ChatRequestOptions = {
|
|
655
716
|
systemPrompt,
|
|
656
717
|
// Strip fold-only properties (foldEvent, foldPath) before sending to provider
|
|
657
718
|
tools: this.toolDefinitions.length ? this.toolDefinitions : undefined,
|
|
658
|
-
attachments:
|
|
719
|
+
attachments: attachmentsForCall,
|
|
659
720
|
};
|
|
660
721
|
|
|
661
722
|
let response: ChatMessage;
|
|
662
723
|
try {
|
|
663
724
|
// eslint-disable-next-line no-await-in-loop
|
|
664
|
-
response = await this.aiProvider.chat!(historyForCall,
|
|
725
|
+
response = await this.aiProvider.chat!(historyForCall, userInputForCall, options);
|
|
665
726
|
} catch (e) {
|
|
666
727
|
if (e instanceof MalformedFunctionCallError) {
|
|
667
728
|
malformedAttempts += 1;
|
|
@@ -683,8 +744,6 @@ export class ChatDriver extends EventTarget implements AiDriver {
|
|
|
683
744
|
throw e;
|
|
684
745
|
}
|
|
685
746
|
|
|
686
|
-
currentAttachments = undefined;
|
|
687
|
-
|
|
688
747
|
const isThinkingStep = response.content && response.toolCalls?.length;
|
|
689
748
|
const isEmptyResponse = !response.content?.trim() && !response.toolCalls?.length;
|
|
690
749
|
|
|
@@ -918,8 +977,6 @@ export class ChatDriver extends EventTarget implements AiDriver {
|
|
|
918
977
|
if (this.subAgentCompletion) {
|
|
919
978
|
return { reason: 'done' };
|
|
920
979
|
}
|
|
921
|
-
|
|
922
|
-
currentInput = '';
|
|
923
980
|
}
|
|
924
981
|
|
|
925
982
|
if (iterations >= this.maxToolIterations) {
|
|
@@ -69,6 +69,7 @@ export class OrchestratingDriver extends EventTarget implements AiDriver {
|
|
|
69
69
|
private readonly maxHandoffs: number;
|
|
70
70
|
private readonly classifierHistoryLength: number;
|
|
71
71
|
private readonly classifierRetries: number;
|
|
72
|
+
private pinnedAgentName: string | null = null;
|
|
72
73
|
|
|
73
74
|
activeAgent?: AgentConfig;
|
|
74
75
|
|
|
@@ -121,6 +122,9 @@ export class OrchestratingDriver extends EventTarget implements AiDriver {
|
|
|
121
122
|
new CustomEvent('sub-agent-history-updated', { detail: (e as CustomEvent).detail }),
|
|
122
123
|
);
|
|
123
124
|
});
|
|
125
|
+
this.chatDriver.addEventListener('sub-agent-start', (e: Event) => {
|
|
126
|
+
this.dispatchEvent(new CustomEvent('sub-agent-start', { detail: (e as CustomEvent).detail }));
|
|
127
|
+
});
|
|
124
128
|
this.chatDriver.addEventListener('sub-agent-stop', (e: Event) => {
|
|
125
129
|
this.dispatchEvent(new CustomEvent('sub-agent-stop', { detail: (e as CustomEvent).detail }));
|
|
126
130
|
});
|
|
@@ -134,6 +138,16 @@ export class OrchestratingDriver extends EventTarget implements AiDriver {
|
|
|
134
138
|
return this.chatDriver.isBusy();
|
|
135
139
|
}
|
|
136
140
|
|
|
141
|
+
/**
|
|
142
|
+
* Pins routing to a specific agent by name. While pinned, the classifier is
|
|
143
|
+
* skipped and the continuation tool is hidden from the agent's tool list, so
|
|
144
|
+
* the agent cannot quietly hand back to the orchestrator. Pass `null` to
|
|
145
|
+
* return to automatic routing.
|
|
146
|
+
*/
|
|
147
|
+
setPinnedAgent(name: string | null): void {
|
|
148
|
+
this.pinnedAgentName = name;
|
|
149
|
+
}
|
|
150
|
+
|
|
137
151
|
loadHistory(messages: ChatMessage[]): void {
|
|
138
152
|
this.chatDriver.loadHistory(messages);
|
|
139
153
|
}
|
|
@@ -148,7 +162,11 @@ export class OrchestratingDriver extends EventTarget implements AiDriver {
|
|
|
148
162
|
count: number,
|
|
149
163
|
allAgentInfo?: AllAgentSummary[],
|
|
150
164
|
): Promise<string[]> {
|
|
151
|
-
|
|
165
|
+
// When pinned to a specialist, scope suggestions to that agent so the
|
|
166
|
+
// proposed prompts reflect the active routing instead of the full panel.
|
|
167
|
+
const pinned = this.resolvePinnedAgent();
|
|
168
|
+
const candidates = pinned && isSpecialist(pinned) ? [pinned] : this.specialists;
|
|
169
|
+
const agentInfo = candidates.map((s) => ({
|
|
152
170
|
name: s.name,
|
|
153
171
|
description: s.description,
|
|
154
172
|
tools: s.toolDefinitions ?? [],
|
|
@@ -167,8 +185,9 @@ export class OrchestratingDriver extends EventTarget implements AiDriver {
|
|
|
167
185
|
}),
|
|
168
186
|
);
|
|
169
187
|
|
|
170
|
-
this.
|
|
171
|
-
|
|
188
|
+
const pinned = this.resolvePinnedAgent();
|
|
189
|
+
if (!pinned) this.dispatchEvent(new CustomEvent('orchestrating-start'));
|
|
190
|
+
let currentAgent = pinned ?? (await this.classify(input, history, this.activeAgent));
|
|
172
191
|
let isHandoff = false;
|
|
173
192
|
let handoffs = 0;
|
|
174
193
|
let handoffSummary = '';
|
|
@@ -189,7 +208,9 @@ export class OrchestratingDriver extends EventTarget implements AiDriver {
|
|
|
189
208
|
result = await this.chatDriver.sendMessage(input, attachments);
|
|
190
209
|
}
|
|
191
210
|
|
|
192
|
-
|
|
211
|
+
// Pinned agents never hand off — the continuation tool is filtered out in
|
|
212
|
+
// applyAgent, but this guards against a model hallucinating a handoff result.
|
|
213
|
+
if (result.reason !== 'agent-handoff' || isFallback(currentAgent) || pinned) {
|
|
193
214
|
break;
|
|
194
215
|
}
|
|
195
216
|
|
|
@@ -219,8 +240,9 @@ export class OrchestratingDriver extends EventTarget implements AiDriver {
|
|
|
219
240
|
}
|
|
220
241
|
|
|
221
242
|
private applyAgent(agent: AgentConfig): void {
|
|
222
|
-
// Fallback agents are terminal
|
|
223
|
-
const
|
|
243
|
+
// Fallback and pinned agents are terminal — neither should hand off.
|
|
244
|
+
const isTerminal = isFallback(agent) || this.pinnedAgentName !== null;
|
|
245
|
+
const agentToApply = isTerminal
|
|
224
246
|
? agent
|
|
225
247
|
: {
|
|
226
248
|
...agent,
|
|
@@ -315,6 +337,23 @@ export class OrchestratingDriver extends EventTarget implements AiDriver {
|
|
|
315
337
|
return this.specialists[0];
|
|
316
338
|
}
|
|
317
339
|
|
|
340
|
+
/**
|
|
341
|
+
* Returns the pinned agent if `pinnedAgentName` matches a known specialist or
|
|
342
|
+
* fallback. Logs and returns `undefined` if pinned to a name that no longer
|
|
343
|
+
* exists in the agents array — caller falls back to the classifier.
|
|
344
|
+
*/
|
|
345
|
+
private resolvePinnedAgent(): AgentConfig | undefined {
|
|
346
|
+
if (this.pinnedAgentName === null) return undefined;
|
|
347
|
+
const match = this.agents.find((a) => a.name === this.pinnedAgentName);
|
|
348
|
+
if (!match) {
|
|
349
|
+
logger.warn(
|
|
350
|
+
`OrchestratingDriver: pinned agent "${this.pinnedAgentName}" not found in agents array — falling back to classifier.`,
|
|
351
|
+
);
|
|
352
|
+
return undefined;
|
|
353
|
+
}
|
|
354
|
+
return match;
|
|
355
|
+
}
|
|
356
|
+
|
|
318
357
|
private appendInlineMessage(content: string): void {
|
|
319
358
|
const history = this.chatDriver.getHistory() as ChatMessage[];
|
|
320
359
|
this.chatDriver.loadHistory([...history, { role: 'assistant', content }]);
|
package/src/config/config.ts
CHANGED
|
@@ -1,20 +1,31 @@
|
|
|
1
|
-
import type {
|
|
1
|
+
import type {
|
|
2
|
+
ChatInputDuringExecutionMode,
|
|
3
|
+
ChatMessage,
|
|
4
|
+
ChatToolDefinition,
|
|
5
|
+
ChatToolHandlers,
|
|
6
|
+
} from '@genesislcap/foundation-ai';
|
|
7
|
+
|
|
8
|
+
export type { ChatInputDuringExecutionMode };
|
|
2
9
|
|
|
3
10
|
/**
|
|
4
|
-
*
|
|
5
|
-
* (i.e. while `state === 'loading'` — covers both LLM thinking and pending
|
|
6
|
-
* widget interactions).
|
|
11
|
+
* Opts an agent in to manual selection from the assistant's agent picker.
|
|
7
12
|
*
|
|
8
|
-
* -
|
|
9
|
-
*
|
|
10
|
-
* hidden. Useful for agents whose interaction widgets contain their own
|
|
11
|
-
* input affordances and where a second disabled chat input is redundant or
|
|
12
|
-
* confusing — typically long-running agents (planners) where one transition
|
|
13
|
-
* in/out is less jarring than mid-run flicker.
|
|
13
|
+
* Only applies to top-level agents — sub-agents are never user-selectable.
|
|
14
|
+
* Has no effect unless the assistant's `agentPicker` is also enabled.
|
|
14
15
|
*
|
|
15
16
|
* @beta
|
|
16
17
|
*/
|
|
17
|
-
export
|
|
18
|
+
export interface ManualSelectionConfig {
|
|
19
|
+
/**
|
|
20
|
+
* Whether this agent appears in the picker.
|
|
21
|
+
*/
|
|
22
|
+
enabled: boolean;
|
|
23
|
+
/**
|
|
24
|
+
* Optional short description shown as a tooltip on the picker entry, and as
|
|
25
|
+
* a secondary line in the dropdown variant.
|
|
26
|
+
*/
|
|
27
|
+
hint?: string;
|
|
28
|
+
}
|
|
18
29
|
|
|
19
30
|
interface BaseAgentConfig {
|
|
20
31
|
/**
|
|
@@ -47,6 +58,12 @@ interface BaseAgentConfig {
|
|
|
47
58
|
* Defaults to `'disabled'`. See {@link ChatInputDuringExecutionMode}.
|
|
48
59
|
*/
|
|
49
60
|
chatInputDuringExecution?: ChatInputDuringExecutionMode;
|
|
61
|
+
/**
|
|
62
|
+
* Opts this agent in to manual selection from the assistant's agent picker.
|
|
63
|
+
* Has no effect on sub-agents or when the assistant's `agentPicker` is
|
|
64
|
+
* disabled. See {@link ManualSelectionConfig}.
|
|
65
|
+
*/
|
|
66
|
+
manualSelection?: ManualSelectionConfig;
|
|
50
67
|
}
|
|
51
68
|
|
|
52
69
|
/**
|
package/src/index.ts
CHANGED
|
@@ -1,6 +1,7 @@
|
|
|
1
1
|
export * from './main/main';
|
|
2
2
|
export * from './main/main.types';
|
|
3
3
|
export * from './main/main.template';
|
|
4
|
+
export * from './components/agent-picker';
|
|
4
5
|
export * from './components/ai-driver';
|
|
5
6
|
export * from './components/chat-driver';
|
|
6
7
|
export * from './components/orchestrating-driver';
|
package/src/main/main.styles.ts
CHANGED
|
@@ -522,10 +522,69 @@ export const styles = css`
|
|
|
522
522
|
background-color: var(--neutral-layer-2);
|
|
523
523
|
}
|
|
524
524
|
|
|
525
|
+
.input-left-controls {
|
|
526
|
+
display: flex;
|
|
527
|
+
flex-direction: column;
|
|
528
|
+
align-items: stretch;
|
|
529
|
+
gap: calc(var(--design-unit) * 1px);
|
|
530
|
+
}
|
|
531
|
+
|
|
532
|
+
/* Lock the width so the column doesn't reflow when the button content
|
|
533
|
+
switches between the "Auto" label, the pin icon, and the chevron. */
|
|
534
|
+
.agent-toggle-button {
|
|
535
|
+
width: 56px;
|
|
536
|
+
min-width: 56px;
|
|
537
|
+
max-width: 56px;
|
|
538
|
+
box-sizing: border-box;
|
|
539
|
+
}
|
|
540
|
+
|
|
541
|
+
.agent-toggle-label {
|
|
542
|
+
font-size: 0.75em;
|
|
543
|
+
font-weight: 600;
|
|
544
|
+
letter-spacing: 0.02em;
|
|
545
|
+
}
|
|
546
|
+
|
|
547
|
+
@keyframes agent-picker-slide-in {
|
|
548
|
+
from {
|
|
549
|
+
opacity: 0%;
|
|
550
|
+
transform: translateY(8px);
|
|
551
|
+
}
|
|
552
|
+
|
|
553
|
+
to {
|
|
554
|
+
opacity: 100%;
|
|
555
|
+
transform: translateY(0);
|
|
556
|
+
}
|
|
557
|
+
}
|
|
558
|
+
|
|
559
|
+
@keyframes agent-picker-slide-out {
|
|
560
|
+
from {
|
|
561
|
+
opacity: 100%;
|
|
562
|
+
transform: translateY(0);
|
|
563
|
+
}
|
|
564
|
+
|
|
565
|
+
to {
|
|
566
|
+
opacity: 0%;
|
|
567
|
+
transform: translateY(8px);
|
|
568
|
+
}
|
|
569
|
+
}
|
|
570
|
+
|
|
571
|
+
.agent-picker-panel {
|
|
572
|
+
animation: agent-picker-slide-in 0.2s ease-out;
|
|
573
|
+
overflow: hidden;
|
|
574
|
+
}
|
|
575
|
+
|
|
576
|
+
.agent-picker-panel.closing {
|
|
577
|
+
animation: agent-picker-slide-out 0.2s ease-in forwards;
|
|
578
|
+
}
|
|
579
|
+
|
|
525
580
|
.chat-input {
|
|
526
581
|
flex: 1;
|
|
527
582
|
resize: none;
|
|
528
583
|
max-height: 150px;
|
|
584
|
+
|
|
585
|
+
/* Match the height of the left-controls column when it has stacked buttons,
|
|
586
|
+
so the textarea fills the row instead of sitting flush to the send button. */
|
|
587
|
+
align-self: stretch;
|
|
529
588
|
}
|
|
530
589
|
|
|
531
590
|
.file-input {
|
|
@@ -268,6 +268,80 @@ ${(tc) => (tc.foldPath?.length ? `${tc.foldPath.join(' › ')} › ` : '')}<stro
|
|
|
268
268
|
)}
|
|
269
269
|
`;
|
|
270
270
|
|
|
271
|
+
// ── Agent picker ────────────────────────────────────────────────────────────
|
|
272
|
+
|
|
273
|
+
const agentToggleButtonTemplate = html<FoundationAiAssistant>`
|
|
274
|
+
<${buttonTag}
|
|
275
|
+
class="agent-toggle-button"
|
|
276
|
+
part="agent-toggle-button"
|
|
277
|
+
appearance="stealth"
|
|
278
|
+
title=${(x) =>
|
|
279
|
+
x.agentPickerOpen
|
|
280
|
+
? 'Close agent picker'
|
|
281
|
+
: x.pinnedAgentName !== null
|
|
282
|
+
? x.pinnedAgentHint
|
|
283
|
+
? `${x.pinnedAgentName} — ${x.pinnedAgentHint}`
|
|
284
|
+
: (x.pinnedAgentName ?? '')
|
|
285
|
+
: 'Attempts to route messages to the correct available agent. Click to manually pin an agent.'}
|
|
286
|
+
?disabled=${(x) => x.state === 'loading'}
|
|
287
|
+
@click=${(x) => x.toggleAgentPicker()}
|
|
288
|
+
>
|
|
289
|
+
${when(
|
|
290
|
+
(x) => x.agentPickerOpen,
|
|
291
|
+
html<FoundationAiAssistant>`<${iconTag} name="chevron-down"></${iconTag}>`,
|
|
292
|
+
)}
|
|
293
|
+
${when(
|
|
294
|
+
(x) => !x.agentPickerOpen && x.pinnedAgentName !== null,
|
|
295
|
+
html<FoundationAiAssistant>`
|
|
296
|
+
<${iconTag}
|
|
297
|
+
name="thumbtack"
|
|
298
|
+
style="${(x) => (x.pinnedAgentColour ? `color: ${x.pinnedAgentColour}` : '')}"
|
|
299
|
+
></${iconTag}>
|
|
300
|
+
`,
|
|
301
|
+
)}
|
|
302
|
+
${when(
|
|
303
|
+
(x) => !x.agentPickerOpen && x.pinnedAgentName === null,
|
|
304
|
+
html<FoundationAiAssistant>`
|
|
305
|
+
<span class="agent-toggle-label">Auto</span>
|
|
306
|
+
`,
|
|
307
|
+
)}
|
|
308
|
+
</${buttonTag}>
|
|
309
|
+
`;
|
|
310
|
+
|
|
311
|
+
const attachButtonTemplate = html<FoundationAiAssistant>`
|
|
312
|
+
<${buttonTag}
|
|
313
|
+
class="attach-button"
|
|
314
|
+
part="attach-button"
|
|
315
|
+
appearance="stealth"
|
|
316
|
+
title=${(x) => `Attach file (${x.chatConfig.ui?.acceptedFiles})`}
|
|
317
|
+
?disabled=${(x) => x.state === 'loading'}
|
|
318
|
+
@click=${(x) => x.triggerFileInput()}
|
|
319
|
+
><${iconTag} name="paperclip"></${iconTag}></${buttonTag}>
|
|
320
|
+
`;
|
|
321
|
+
|
|
322
|
+
const inputLeftControlsTemplate = html<FoundationAiAssistant>`
|
|
323
|
+
<div class="input-left-controls" part="input-left-controls">
|
|
324
|
+
${when((x) => x.agentPickerEnabled, agentToggleButtonTemplate)}
|
|
325
|
+
${when((x) => !!x.chatConfig.ui?.acceptedFiles, attachButtonTemplate)}
|
|
326
|
+
</div>
|
|
327
|
+
`;
|
|
328
|
+
|
|
329
|
+
const agentPickerPanelTemplate = html<FoundationAiAssistant>`
|
|
330
|
+
<div class="agent-picker-panel" part="agent-picker-panel">
|
|
331
|
+
<foundation-ai-agent-picker
|
|
332
|
+
part="agent-picker"
|
|
333
|
+
:designSystemPrefix="${(x) => x.designSystemPrefix}"
|
|
334
|
+
:mode="${(x) => x.agentPicker}"
|
|
335
|
+
:agents="${(x) => x.agents ?? []}"
|
|
336
|
+
:pinnedAgentName="${(x) => x.pinnedAgentName}"
|
|
337
|
+
@agent-pinned="${(x, c) => {
|
|
338
|
+
x.pinnedAgentName = (c.event as CustomEvent<string | null>).detail;
|
|
339
|
+
x.toggleAgentPicker();
|
|
340
|
+
}}"
|
|
341
|
+
></foundation-ai-agent-picker>
|
|
342
|
+
</div>
|
|
343
|
+
`;
|
|
344
|
+
|
|
271
345
|
// ── Root template ───────────────────────────────────────────────────────────
|
|
272
346
|
|
|
273
347
|
return html<FoundationAiAssistant>`
|
|
@@ -509,28 +583,20 @@ ${(tc) => (tc.foldPath?.length ? `${tc.foldPath.join(' › ')} › ` : '')}<stro
|
|
|
509
583
|
></chat-suggestions>
|
|
510
584
|
`,
|
|
511
585
|
)}
|
|
586
|
+
${when((x) => x.agentPickerEnabled && x.agentPickerOpen, agentPickerPanelTemplate)}
|
|
512
587
|
${when(
|
|
513
|
-
(x) => !(x.state === 'loading' && x.
|
|
588
|
+
(x) => !(x.state === 'loading' && x.effectiveChatInputDuringExecution === 'hidden'),
|
|
514
589
|
html<FoundationAiAssistant>`
|
|
515
590
|
<div class="input-row" part="input-row">
|
|
516
591
|
${when(
|
|
517
|
-
(x) => !!x.chatConfig.ui?.acceptedFiles,
|
|
518
|
-
|
|
519
|
-
<${buttonTag}
|
|
520
|
-
class="attach-button"
|
|
521
|
-
part="attach-button"
|
|
522
|
-
appearance="stealth"
|
|
523
|
-
title=${(x) => `Attach file (${x.chatConfig.ui?.acceptedFiles})`}
|
|
524
|
-
?disabled=${(x) => x.state === 'loading'}
|
|
525
|
-
@click=${(x) => x.triggerFileInput()}
|
|
526
|
-
><${iconTag} name="paperclip"></${iconTag}></${buttonTag}>
|
|
527
|
-
`,
|
|
592
|
+
(x) => x.agentPickerEnabled || !!x.chatConfig.ui?.acceptedFiles,
|
|
593
|
+
inputLeftControlsTemplate,
|
|
528
594
|
)}
|
|
529
595
|
<${textareaTag}
|
|
530
596
|
${ref('chatInputEl')}
|
|
531
597
|
class="chat-input"
|
|
532
598
|
part="input"
|
|
533
|
-
placeholder=${(x) => x.
|
|
599
|
+
placeholder=${(x) => x.effectivePlaceholder}
|
|
534
600
|
:value=${(x) => x.inputValue}
|
|
535
601
|
?disabled=${(x) => x.state === 'loading'}
|
|
536
602
|
@input=${(x, c) => (x.inputValue = (c.event.target as any).value)}
|