@genesislcap/ai-assistant 14.444.1 → 14.445.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 +312 -41
- package/dist/ai-assistant.d.ts +105 -8
- package/dist/dts/components/ai-driver/ai-driver.d.ts +7 -0
- package/dist/dts/components/ai-driver/ai-driver.d.ts.map +1 -1
- package/dist/dts/components/chat-driver/chat-driver.d.ts +37 -3
- package/dist/dts/components/chat-driver/chat-driver.d.ts.map +1 -1
- package/dist/dts/components/orchestrating-driver/orchestrating-driver.d.ts +5 -3
- package/dist/dts/components/orchestrating-driver/orchestrating-driver.d.ts.map +1 -1
- package/dist/dts/config/config.d.ts +31 -0
- package/dist/dts/config/config.d.ts.map +1 -1
- package/dist/dts/config/define-stateful-agent.d.ts +9 -0
- package/dist/dts/config/define-stateful-agent.d.ts.map +1 -1
- package/dist/dts/config/validate-providers.d.ts +25 -0
- package/dist/dts/config/validate-providers.d.ts.map +1 -0
- package/dist/dts/config/validate-providers.test.d.ts +2 -0
- package/dist/dts/config/validate-providers.test.d.ts.map +1 -0
- package/dist/dts/main/main.d.ts +17 -5
- 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/state/ai-assistant-slice.d.ts +14 -1
- package/dist/dts/state/ai-assistant-slice.d.ts.map +1 -1
- package/dist/dts/state/session-store.d.ts +2 -0
- package/dist/dts/state/session-store.d.ts.map +1 -1
- package/dist/esm/components/chat-driver/chat-driver.js +84 -15
- package/dist/esm/components/orchestrating-driver/orchestrating-driver.js +23 -4
- package/dist/esm/config/define-stateful-agent.js +12 -0
- package/dist/esm/config/validate-providers.js +47 -0
- package/dist/esm/config/validate-providers.test.js +100 -0
- package/dist/esm/main/main.js +57 -10
- package/dist/esm/main/main.styles.js +52 -0
- package/dist/esm/main/main.template.js +36 -1
- package/dist/esm/state/ai-assistant-slice.js +8 -0
- package/dist/tsconfig.tsbuildinfo +1 -1
- package/docs/migration-GENC-1262.md +219 -0
- package/package.json +16 -16
- package/src/components/ai-driver/ai-driver.ts +8 -0
- package/src/components/chat-driver/chat-driver.ts +96 -14
- package/src/components/orchestrating-driver/orchestrating-driver.ts +29 -4
- package/src/config/config.ts +32 -0
- package/src/config/define-stateful-agent.ts +28 -0
- package/src/config/validate-providers.test.ts +148 -0
- package/src/config/validate-providers.ts +58 -0
- package/src/main/main.styles.ts +52 -0
- package/src/main/main.template.ts +50 -2
- package/src/main/main.ts +51 -7
- package/src/state/ai-assistant-slice.ts +24 -1
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
import type {
|
|
2
|
-
|
|
2
|
+
AIProviderRegistry,
|
|
3
3
|
ChatAttachment,
|
|
4
4
|
ChatDriverResult,
|
|
5
5
|
ChatMessage,
|
|
@@ -12,6 +12,7 @@ import type {
|
|
|
12
12
|
SystemPromptContext,
|
|
13
13
|
SystemPromptInput,
|
|
14
14
|
} from '../../config/config';
|
|
15
|
+
import { validateStaticAgentProviders } from '../../config/validate-providers';
|
|
15
16
|
import { transformHistoryForAgent } from '../../utils/history-transform';
|
|
16
17
|
import { logger } from '../../utils/logger';
|
|
17
18
|
import type { AiDriver, AllAgentSummary } from '../ai-driver/ai-driver';
|
|
@@ -110,7 +111,7 @@ export class OrchestratingDriver extends EventTarget implements AiDriver {
|
|
|
110
111
|
activeAgent?: AgentConfig;
|
|
111
112
|
|
|
112
113
|
constructor(
|
|
113
|
-
private readonly
|
|
114
|
+
private readonly providerRegistry: AIProviderRegistry,
|
|
114
115
|
private readonly agents: AgentConfig[],
|
|
115
116
|
options: {
|
|
116
117
|
sessionKey?: string;
|
|
@@ -129,6 +130,11 @@ export class OrchestratingDriver extends EventTarget implements AiDriver {
|
|
|
129
130
|
options.classifierHistoryLength ?? DEFAULT_CLASSIFIER_HISTORY_LENGTH;
|
|
130
131
|
this.classifierRetries = options.classifierRetries ?? DEFAULT_CLASSIFIER_RETRIES;
|
|
131
132
|
|
|
133
|
+
// Static-name validation: walk every agent (and nested sub-agents). Any
|
|
134
|
+
// `provider: '<name>'` that isn't registered, or resolves to a provider
|
|
135
|
+
// without `chat()`, throws now rather than at first turn.
|
|
136
|
+
validateStaticAgentProviders(agents, providerRegistry);
|
|
137
|
+
|
|
132
138
|
// Specialists drive the classifier. `excludeFromClassifier` agents are still
|
|
133
139
|
// resolvable by name (so manual pinning works) but never auto-routed.
|
|
134
140
|
this.specialists = agents.filter(isSpecialist).filter((a) => !a.excludeFromClassifier);
|
|
@@ -145,7 +151,7 @@ export class OrchestratingDriver extends EventTarget implements AiDriver {
|
|
|
145
151
|
: undefined;
|
|
146
152
|
|
|
147
153
|
this.chatDriver = new ChatDriver(
|
|
148
|
-
|
|
154
|
+
providerRegistry,
|
|
149
155
|
{},
|
|
150
156
|
[],
|
|
151
157
|
undefined,
|
|
@@ -180,6 +186,11 @@ export class OrchestratingDriver extends EventTarget implements AiDriver {
|
|
|
180
186
|
new CustomEvent('interaction-stop', { detail: (e as CustomEvent).detail }),
|
|
181
187
|
);
|
|
182
188
|
});
|
|
189
|
+
this.chatDriver.addEventListener('provider-changed', (e: Event) => {
|
|
190
|
+
this.dispatchEvent(
|
|
191
|
+
new CustomEvent('provider-changed', { detail: (e as CustomEvent).detail }),
|
|
192
|
+
);
|
|
193
|
+
});
|
|
183
194
|
}
|
|
184
195
|
|
|
185
196
|
resolveInteraction(interactionId: string, result: unknown): void {
|
|
@@ -190,6 +201,11 @@ export class OrchestratingDriver extends EventTarget implements AiDriver {
|
|
|
190
201
|
return this.chatDriver.isBusy();
|
|
191
202
|
}
|
|
192
203
|
|
|
204
|
+
/** Currently active provider name from the underlying ChatDriver. */
|
|
205
|
+
getActiveProviderName(): string {
|
|
206
|
+
return this.chatDriver.getActiveProviderName();
|
|
207
|
+
}
|
|
208
|
+
|
|
193
209
|
/**
|
|
194
210
|
* Pins routing to a specific agent by name. While pinned, the classifier is
|
|
195
211
|
* skipped and the continuation tool is hidden from the agent's tool list, so
|
|
@@ -530,8 +546,17 @@ export class OrchestratingDriver extends EventTarget implements AiDriver {
|
|
|
530
546
|
systemPrompt: classifierPrompt,
|
|
531
547
|
tools: [routingTool],
|
|
532
548
|
};
|
|
549
|
+
// Classification is orchestrator-level, not bound to any single agent —
|
|
550
|
+
// always run against the registry default.
|
|
551
|
+
const classifierProvider = this.providerRegistry.default();
|
|
552
|
+
if (!classifierProvider.chat) {
|
|
553
|
+
logger.warn(
|
|
554
|
+
'OrchestratingDriver: default AI provider does not implement chat() — cannot classify.',
|
|
555
|
+
);
|
|
556
|
+
break;
|
|
557
|
+
}
|
|
533
558
|
// oxlint-disable-next-line no-await-in-loop
|
|
534
|
-
const response = await
|
|
559
|
+
const response = await classifierProvider.chat([], input, options);
|
|
535
560
|
const tc = response.toolCalls?.[0];
|
|
536
561
|
const index = tc?.name === 'select_agent' ? (tc.args.agent_index as number) : -1;
|
|
537
562
|
|
package/src/config/config.ts
CHANGED
|
@@ -75,6 +75,20 @@ export type ToolHandlersInput =
|
|
|
75
75
|
| ChatToolHandlers
|
|
76
76
|
| ((ctx: SystemPromptContext) => ChatToolHandlers | Promise<ChatToolHandlers>);
|
|
77
77
|
|
|
78
|
+
/**
|
|
79
|
+
* Names which registered `AIProviderRegistry` entry an agent uses. Either
|
|
80
|
+
* a static string (resolved once at agent registration) or a function resolved
|
|
81
|
+
* each tool-loop iteration — pick the function form to vary the provider by
|
|
82
|
+
* current state (e.g. a stateful agent that switches providers between
|
|
83
|
+
* machine states).
|
|
84
|
+
*
|
|
85
|
+
* Unknown provider names hard-error: static names at agent registration time,
|
|
86
|
+
* function-resolved names on first resolution.
|
|
87
|
+
*
|
|
88
|
+
* @beta
|
|
89
|
+
*/
|
|
90
|
+
export type ProviderInput = string | ((ctx: SystemPromptContext) => string | Promise<string>);
|
|
91
|
+
|
|
78
92
|
/**
|
|
79
93
|
* Opts an agent in to manual selection from the assistant's agent picker.
|
|
80
94
|
*
|
|
@@ -138,6 +152,24 @@ interface BaseAgentConfig {
|
|
|
138
152
|
* See {@link ToolHandlersInput}.
|
|
139
153
|
*/
|
|
140
154
|
toolHandlers?: ToolHandlersInput;
|
|
155
|
+
/**
|
|
156
|
+
* Which registered AI provider this agent uses, by name. Either a static
|
|
157
|
+
* string (resolved once at agent registration) or a function resolved each
|
|
158
|
+
* tool-loop iteration — pick the function form to vary the provider by
|
|
159
|
+
* current state (e.g. a stateful agent that switches providers between
|
|
160
|
+
* machine states).
|
|
161
|
+
*
|
|
162
|
+
* Omit to use the registry's default provider. Unknown names hard-error:
|
|
163
|
+
* static names at registration time, function-resolved names on first
|
|
164
|
+
* resolution.
|
|
165
|
+
*
|
|
166
|
+
* Known limitation: switching provider mid-conversation can blow the
|
|
167
|
+
* smaller provider's context window if history accumulated under a
|
|
168
|
+
* larger one. No automatic trimming is performed.
|
|
169
|
+
*
|
|
170
|
+
* @beta
|
|
171
|
+
*/
|
|
172
|
+
provider?: ProviderInput;
|
|
141
173
|
/**
|
|
142
174
|
* Optional primer history prepended to every call (not visible to the user).
|
|
143
175
|
* Used to establish agent identity and behavioural rules.
|
|
@@ -5,6 +5,7 @@ import type {
|
|
|
5
5
|
AgentLifecycleContext,
|
|
6
6
|
ChatInputDuringExecutionMode,
|
|
7
7
|
ManualSelectionConfig,
|
|
8
|
+
ProviderInput,
|
|
8
9
|
SystemPromptContext,
|
|
9
10
|
SystemPromptInput,
|
|
10
11
|
ToolDefinitionsInput,
|
|
@@ -116,6 +117,16 @@ export interface StatefulAgentInit<S> {
|
|
|
116
117
|
*/
|
|
117
118
|
toolHandlers?: (state: S) => ChatToolHandlers | Promise<ChatToolHandlers>;
|
|
118
119
|
|
|
120
|
+
/**
|
|
121
|
+
* Which registered AI provider this agent uses, by name. Either a static
|
|
122
|
+
* string or a function resolved each tool-loop iteration with the current
|
|
123
|
+
* `state` — pick the function form to vary provider per machine state
|
|
124
|
+
* (e.g. swap to a heavier model only for the planning step).
|
|
125
|
+
*
|
|
126
|
+
* Omit to use the registry's default provider. Unknown names hard-error.
|
|
127
|
+
*/
|
|
128
|
+
provider?: string | ((ctx: StatefulAgentContext<S>) => string | Promise<string>);
|
|
129
|
+
|
|
119
130
|
/**
|
|
120
131
|
* Optional getter for the debug-log snapshot. Defaults to auto-snapshotting
|
|
121
132
|
* any property on `state` that looks like a foundation-state-machine
|
|
@@ -246,6 +257,22 @@ export function defineStatefulAgent<S>(opts: StatefulAgentInit<S>): AgentConfig
|
|
|
246
257
|
}
|
|
247
258
|
: undefined;
|
|
248
259
|
|
|
260
|
+
// Function-form `provider` needs `state` injected the same way the other
|
|
261
|
+
// per-turn resolvers do; the static-string form is passed through unchanged
|
|
262
|
+
// and validated up-front by OrchestratingDriver.
|
|
263
|
+
const wrappedProvider: ProviderInput | undefined =
|
|
264
|
+
typeof opts.provider === 'function'
|
|
265
|
+
? async (ctx: SystemPromptContext) => {
|
|
266
|
+
if (!state) {
|
|
267
|
+
throw new Error(`Stateful agent "${opts.name}" provider called before init`);
|
|
268
|
+
}
|
|
269
|
+
return (opts.provider as (ctx: StatefulAgentContext<S>) => string | Promise<string>)({
|
|
270
|
+
...ctx,
|
|
271
|
+
state,
|
|
272
|
+
});
|
|
273
|
+
}
|
|
274
|
+
: opts.provider;
|
|
275
|
+
|
|
249
276
|
const base = {
|
|
250
277
|
name: opts.name,
|
|
251
278
|
displayName: wrappedDisplayName,
|
|
@@ -255,6 +282,7 @@ export function defineStatefulAgent<S>(opts: StatefulAgentInit<S>): AgentConfig
|
|
|
255
282
|
chatInputDuringExecution: opts.chatInputDuringExecution,
|
|
256
283
|
toolDefinitions: wrappedTools,
|
|
257
284
|
toolHandlers: wrappedHandlers,
|
|
285
|
+
provider: wrappedProvider,
|
|
258
286
|
|
|
259
287
|
onActivate: async (ctx: AgentLifecycleContext) => {
|
|
260
288
|
state = await opts.init(ctx);
|
|
@@ -0,0 +1,148 @@
|
|
|
1
|
+
import type { AIProvider, AIProviderRegistry } from '@genesislcap/foundation-ai';
|
|
2
|
+
import { assert, createLogicSuite } from '@genesislcap/foundation-testing';
|
|
3
|
+
import type { AgentConfig } from './config';
|
|
4
|
+
import { resolveChatProvider, validateStaticAgentProviders } from './validate-providers';
|
|
5
|
+
|
|
6
|
+
const chatProvider = (): AIProvider => ({
|
|
7
|
+
chat: async () => ({ role: 'assistant', content: '' }),
|
|
8
|
+
});
|
|
9
|
+
|
|
10
|
+
const promptOnlyProvider = (): AIProvider => ({
|
|
11
|
+
prompt: async () => '',
|
|
12
|
+
});
|
|
13
|
+
|
|
14
|
+
/**
|
|
15
|
+
* Minimal in-test registry — implements only what the validation helpers
|
|
16
|
+
* touch (`get`, `names`). Avoids depending on foundation-ai's concrete
|
|
17
|
+
* registry impl, and keeps these tests Node-runnable.
|
|
18
|
+
*/
|
|
19
|
+
const makeRegistry = (providers: Record<string, AIProvider>): AIProviderRegistry => {
|
|
20
|
+
const map = new Map(Object.entries(providers));
|
|
21
|
+
return {
|
|
22
|
+
get: (name) => map.get(name),
|
|
23
|
+
default: () => map.values().next().value as AIProvider,
|
|
24
|
+
defaultName: () => map.keys().next().value as string,
|
|
25
|
+
names: () => [...map.keys()],
|
|
26
|
+
getStatus: async () => null,
|
|
27
|
+
listStatuses: async () => [],
|
|
28
|
+
};
|
|
29
|
+
};
|
|
30
|
+
|
|
31
|
+
const specialist = (overrides: Partial<AgentConfig> & { name: string }): AgentConfig =>
|
|
32
|
+
({
|
|
33
|
+
name: overrides.name,
|
|
34
|
+
description: 'test specialist',
|
|
35
|
+
...overrides,
|
|
36
|
+
}) as AgentConfig;
|
|
37
|
+
|
|
38
|
+
// ---------------------------------------------------------------------------
|
|
39
|
+
// resolveChatProvider
|
|
40
|
+
// ---------------------------------------------------------------------------
|
|
41
|
+
|
|
42
|
+
const resolve = createLogicSuite('resolveChatProvider');
|
|
43
|
+
|
|
44
|
+
resolve('returns the provider when the name is registered and has chat()', () => {
|
|
45
|
+
const fast = chatProvider();
|
|
46
|
+
const registry = makeRegistry({ fast });
|
|
47
|
+
assert.is(resolveChatProvider(registry, 'fast', 'A'), fast);
|
|
48
|
+
});
|
|
49
|
+
|
|
50
|
+
resolve('throws on an unknown provider name with the agent name in the message', () => {
|
|
51
|
+
const registry = makeRegistry({ fast: chatProvider() });
|
|
52
|
+
assert.throws(
|
|
53
|
+
() => resolveChatProvider(registry, 'deep', 'A'),
|
|
54
|
+
/Agent "A" references unknown provider "deep"/,
|
|
55
|
+
);
|
|
56
|
+
});
|
|
57
|
+
|
|
58
|
+
resolve('throws including the registered names so the misconfiguration is debuggable', () => {
|
|
59
|
+
const registry = makeRegistry({
|
|
60
|
+
fast: chatProvider(),
|
|
61
|
+
deep: chatProvider(),
|
|
62
|
+
cheap: chatProvider(),
|
|
63
|
+
});
|
|
64
|
+
assert.throws(() => resolveChatProvider(registry, 'typo', 'A'), /Registered: fast, deep, cheap/);
|
|
65
|
+
});
|
|
66
|
+
|
|
67
|
+
resolve('throws when the resolved provider does not implement chat()', () => {
|
|
68
|
+
const registry = makeRegistry({ chrome: promptOnlyProvider() });
|
|
69
|
+
assert.throws(
|
|
70
|
+
() => resolveChatProvider(registry, 'chrome', 'A'),
|
|
71
|
+
/Agent "A" resolved to provider "chrome" which does not implement chat\(\)/,
|
|
72
|
+
);
|
|
73
|
+
});
|
|
74
|
+
|
|
75
|
+
resolve.run();
|
|
76
|
+
|
|
77
|
+
// ---------------------------------------------------------------------------
|
|
78
|
+
// validateStaticAgentProviders
|
|
79
|
+
// ---------------------------------------------------------------------------
|
|
80
|
+
|
|
81
|
+
const validate = createLogicSuite('validateStaticAgentProviders');
|
|
82
|
+
|
|
83
|
+
validate('accepts agents with no `provider` field (back-compat path)', () => {
|
|
84
|
+
const registry = makeRegistry({ default: chatProvider() });
|
|
85
|
+
assert.not.throws(() => validateStaticAgentProviders([specialist({ name: 'A' })], registry));
|
|
86
|
+
});
|
|
87
|
+
|
|
88
|
+
validate('accepts agents that name a registered chat-capable provider', () => {
|
|
89
|
+
const registry = makeRegistry({ fast: chatProvider(), deep: chatProvider() });
|
|
90
|
+
assert.not.throws(() =>
|
|
91
|
+
validateStaticAgentProviders([specialist({ name: 'A', provider: 'deep' })], registry),
|
|
92
|
+
);
|
|
93
|
+
});
|
|
94
|
+
|
|
95
|
+
validate('throws when a top-level agent references an unknown provider', () => {
|
|
96
|
+
const registry = makeRegistry({ fast: chatProvider() });
|
|
97
|
+
assert.throws(
|
|
98
|
+
() => validateStaticAgentProviders([specialist({ name: 'A', provider: 'deep' })], registry),
|
|
99
|
+
/Agent "A" references unknown provider "deep"/,
|
|
100
|
+
);
|
|
101
|
+
});
|
|
102
|
+
|
|
103
|
+
validate('recurses into subAgents — bad provider on a sub-agent throws', () => {
|
|
104
|
+
const registry = makeRegistry({ fast: chatProvider() });
|
|
105
|
+
const sub = specialist({ name: 'Sub', provider: 'missing' });
|
|
106
|
+
assert.throws(
|
|
107
|
+
() => validateStaticAgentProviders([specialist({ name: 'A', subAgents: [sub] })], registry),
|
|
108
|
+
/Agent "Sub" references unknown provider "missing"/,
|
|
109
|
+
);
|
|
110
|
+
});
|
|
111
|
+
|
|
112
|
+
validate(
|
|
113
|
+
'recurses through multiple levels — bad provider on a deeply-nested sub-agent throws',
|
|
114
|
+
() => {
|
|
115
|
+
const registry = makeRegistry({ fast: chatProvider() });
|
|
116
|
+
const deepest = specialist({ name: 'Deepest', provider: 'missing' });
|
|
117
|
+
const middle = specialist({ name: 'Middle', subAgents: [deepest] });
|
|
118
|
+
assert.throws(
|
|
119
|
+
() =>
|
|
120
|
+
validateStaticAgentProviders([specialist({ name: 'Top', subAgents: [middle] })], registry),
|
|
121
|
+
/Agent "Deepest" references unknown provider "missing"/,
|
|
122
|
+
);
|
|
123
|
+
},
|
|
124
|
+
);
|
|
125
|
+
|
|
126
|
+
validate('defers function-form `provider` — it is not invoked at validation time', () => {
|
|
127
|
+
const registry = makeRegistry({ fast: chatProvider() });
|
|
128
|
+
const agent = specialist({
|
|
129
|
+
name: 'A',
|
|
130
|
+
// If validateStaticAgentProviders eagerly resolved this, it would throw
|
|
131
|
+
// here. The contract is that function-form is deferred to the driver's
|
|
132
|
+
// per-turn resolver.
|
|
133
|
+
provider: () => {
|
|
134
|
+
throw new Error('function-form provider should not be invoked at validation time');
|
|
135
|
+
},
|
|
136
|
+
});
|
|
137
|
+
assert.not.throws(() => validateStaticAgentProviders([agent], registry));
|
|
138
|
+
});
|
|
139
|
+
|
|
140
|
+
validate('shared sub-agent across two parents validates cleanly', () => {
|
|
141
|
+
const registry = makeRegistry({ fast: chatProvider() });
|
|
142
|
+
const shared = specialist({ name: 'Shared', provider: 'fast' });
|
|
143
|
+
const parentA = specialist({ name: 'ParentA', subAgents: [shared] });
|
|
144
|
+
const parentB = specialist({ name: 'ParentB', subAgents: [shared] });
|
|
145
|
+
assert.not.throws(() => validateStaticAgentProviders([parentA, parentB], registry));
|
|
146
|
+
});
|
|
147
|
+
|
|
148
|
+
validate.run();
|
|
@@ -0,0 +1,58 @@
|
|
|
1
|
+
import type { AIProvider, AIProviderRegistry } from '@genesislcap/foundation-ai';
|
|
2
|
+
import type { AgentConfig } from './config';
|
|
3
|
+
|
|
4
|
+
/**
|
|
5
|
+
* Resolve a provider name against the registry and assert it implements
|
|
6
|
+
* `chat()` — the capability the chat tool-loop depends on. Throws with a
|
|
7
|
+
* message naming the agent so misconfigured agents are easy to track down.
|
|
8
|
+
*
|
|
9
|
+
* Used by both the up-front static walk in `OrchestratingDriver` and by
|
|
10
|
+
* `ChatDriver`'s per-turn resolver for function-form `provider` values.
|
|
11
|
+
*
|
|
12
|
+
* @internal
|
|
13
|
+
*/
|
|
14
|
+
export function resolveChatProvider(
|
|
15
|
+
registry: AIProviderRegistry,
|
|
16
|
+
providerName: string,
|
|
17
|
+
agentName: string,
|
|
18
|
+
): AIProvider {
|
|
19
|
+
const provider = registry.get(providerName);
|
|
20
|
+
if (!provider) {
|
|
21
|
+
const available = registry.names().join(', ') || '(none)';
|
|
22
|
+
throw new Error(
|
|
23
|
+
`Agent "${agentName}" references unknown provider "${providerName}". Registered: ${available}`,
|
|
24
|
+
);
|
|
25
|
+
}
|
|
26
|
+
if (!provider.chat) {
|
|
27
|
+
throw new Error(
|
|
28
|
+
`Agent "${agentName}" resolved to provider "${providerName}" which does not implement chat().`,
|
|
29
|
+
);
|
|
30
|
+
}
|
|
31
|
+
return provider;
|
|
32
|
+
}
|
|
33
|
+
|
|
34
|
+
/**
|
|
35
|
+
* Walk an agent tree (including nested `subAgents`) and validate every static
|
|
36
|
+
* `provider: string` against the registry. Function-form `provider` values are
|
|
37
|
+
* deferred — they validate on first resolution inside the driver.
|
|
38
|
+
*
|
|
39
|
+
* Throws on the first failure, with the same message shape produced by
|
|
40
|
+
* {@link resolveChatProvider}.
|
|
41
|
+
*
|
|
42
|
+
* @internal
|
|
43
|
+
*/
|
|
44
|
+
export function validateStaticAgentProviders(
|
|
45
|
+
agents: AgentConfig[],
|
|
46
|
+
registry: AIProviderRegistry,
|
|
47
|
+
): void {
|
|
48
|
+
const visited = new Set<AgentConfig>();
|
|
49
|
+
const walk = (agent: AgentConfig): void => {
|
|
50
|
+
if (visited.has(agent)) return;
|
|
51
|
+
visited.add(agent);
|
|
52
|
+
if (typeof agent.provider === 'string') {
|
|
53
|
+
resolveChatProvider(registry, agent.provider, agent.name);
|
|
54
|
+
}
|
|
55
|
+
for (const sub of agent.subAgents ?? []) walk(sub);
|
|
56
|
+
};
|
|
57
|
+
for (const root of agents) walk(root);
|
|
58
|
+
}
|
package/src/main/main.styles.ts
CHANGED
|
@@ -257,6 +257,58 @@ export const styles = css`
|
|
|
257
257
|
font-family: var(--monospace-font, ui-monospace, monospace);
|
|
258
258
|
}
|
|
259
259
|
|
|
260
|
+
.provider-list {
|
|
261
|
+
animation: settings-slide-in 0.2s ease-out;
|
|
262
|
+
padding: calc(var(--design-unit) * 1.5px) calc(var(--design-unit) * 3px);
|
|
263
|
+
border-bottom: 1px solid var(--neutral-stroke-rest);
|
|
264
|
+
background-color: var(--neutral-layer-2);
|
|
265
|
+
display: flex;
|
|
266
|
+
flex-direction: column;
|
|
267
|
+
gap: calc(var(--design-unit) * 1px);
|
|
268
|
+
}
|
|
269
|
+
|
|
270
|
+
.provider-list-label {
|
|
271
|
+
font-size: 0.85em;
|
|
272
|
+
font-weight: 600;
|
|
273
|
+
color: var(--neutral-foreground-rest);
|
|
274
|
+
}
|
|
275
|
+
|
|
276
|
+
.provider-list-row {
|
|
277
|
+
display: flex;
|
|
278
|
+
align-items: baseline;
|
|
279
|
+
gap: calc(var(--design-unit) * 1.5px);
|
|
280
|
+
font-size: 0.8em;
|
|
281
|
+
color: var(--neutral-foreground-hint, var(--neutral-foreground-rest));
|
|
282
|
+
opacity: 80%;
|
|
283
|
+
}
|
|
284
|
+
|
|
285
|
+
.provider-list-row.is-current {
|
|
286
|
+
opacity: 100%;
|
|
287
|
+
color: var(--neutral-foreground-rest);
|
|
288
|
+
}
|
|
289
|
+
|
|
290
|
+
.provider-list-name {
|
|
291
|
+
font-weight: 600;
|
|
292
|
+
}
|
|
293
|
+
|
|
294
|
+
.provider-list-model {
|
|
295
|
+
font-family: var(--monospace-font, ui-monospace, monospace);
|
|
296
|
+
flex: 1;
|
|
297
|
+
overflow: hidden;
|
|
298
|
+
text-overflow: ellipsis;
|
|
299
|
+
white-space: nowrap;
|
|
300
|
+
}
|
|
301
|
+
|
|
302
|
+
.provider-list-badge {
|
|
303
|
+
font-size: 0.7em;
|
|
304
|
+
text-transform: uppercase;
|
|
305
|
+
letter-spacing: 0.05em;
|
|
306
|
+
padding: 0 calc(var(--design-unit) * 0.75px);
|
|
307
|
+
border-radius: calc(var(--design-unit) * 0.5px);
|
|
308
|
+
background-color: var(--neutral-layer-3);
|
|
309
|
+
color: var(--neutral-foreground-rest);
|
|
310
|
+
}
|
|
311
|
+
|
|
260
312
|
.splash-wrapper {
|
|
261
313
|
position: absolute;
|
|
262
314
|
inset: 0;
|
|
@@ -15,7 +15,12 @@
|
|
|
15
15
|
* lines let the formatter inject whitespace between them.
|
|
16
16
|
*/
|
|
17
17
|
|
|
18
|
-
import type {
|
|
18
|
+
import type {
|
|
19
|
+
AIProviderRegistryStatusEntry,
|
|
20
|
+
ChatAttachment,
|
|
21
|
+
ChatMessage,
|
|
22
|
+
ChatToolCall,
|
|
23
|
+
} from '@genesislcap/foundation-ai';
|
|
19
24
|
import { isChatToolCallUnknown } from '@genesislcap/foundation-ai';
|
|
20
25
|
import { html, ref, repeat, when, ViewTemplate } from '@genesislcap/web-core';
|
|
21
26
|
import type { FoundationAiAssistant } from './main';
|
|
@@ -500,7 +505,10 @@ ${(tc) => (tc.foldPath?.length ? `${tc.foldPath.join(' › ')} › ` : '')}<stro
|
|
|
500
505
|
)}
|
|
501
506
|
${when(
|
|
502
507
|
(x) =>
|
|
503
|
-
x.settingsOpen &&
|
|
508
|
+
x.settingsOpen &&
|
|
509
|
+
x.chatConfig.ui?.showActiveModel !== false &&
|
|
510
|
+
x.providerStatuses.length <= 1 &&
|
|
511
|
+
x.activeModel != null,
|
|
504
512
|
html<FoundationAiAssistant>`
|
|
505
513
|
<div class="session-stat" part="active-model">
|
|
506
514
|
<span class="session-stat-label">Model</span>
|
|
@@ -508,6 +516,46 @@ ${(tc) => (tc.foldPath?.length ? `${tc.foldPath.join(' › ')} › ` : '')}<stro
|
|
|
508
516
|
</div>
|
|
509
517
|
`,
|
|
510
518
|
)}
|
|
519
|
+
${when(
|
|
520
|
+
(x) =>
|
|
521
|
+
x.settingsOpen &&
|
|
522
|
+
x.chatConfig.ui?.showActiveModel !== false &&
|
|
523
|
+
x.providerStatuses.length > 1,
|
|
524
|
+
html<FoundationAiAssistant>`
|
|
525
|
+
<div class="provider-list" part="provider-list">
|
|
526
|
+
<div class="provider-list-label">Providers</div>
|
|
527
|
+
${repeat(
|
|
528
|
+
(x) => x.providerStatuses,
|
|
529
|
+
html<AIProviderRegistryStatusEntry, FoundationAiAssistant>`
|
|
530
|
+
<div
|
|
531
|
+
class="provider-list-row ${(entry, c) =>
|
|
532
|
+
entry.name === c.parent.activeProviderName ? 'is-current' : ''}"
|
|
533
|
+
part="provider-list-row"
|
|
534
|
+
>
|
|
535
|
+
<span class="provider-list-name">${(entry) => entry.name}</span>
|
|
536
|
+
<span class="provider-list-model">${(entry) => entry.status?.model ?? '—'}</span>
|
|
537
|
+
${when(
|
|
538
|
+
(entry) => entry.isDefault,
|
|
539
|
+
html<AIProviderRegistryStatusEntry>`
|
|
540
|
+
<span class="provider-list-badge" part="provider-list-default-badge">
|
|
541
|
+
default
|
|
542
|
+
</span>
|
|
543
|
+
`,
|
|
544
|
+
)}
|
|
545
|
+
${when(
|
|
546
|
+
(entry, c) => entry.name === c.parent.activeProviderName,
|
|
547
|
+
html<AIProviderRegistryStatusEntry>`
|
|
548
|
+
<span class="provider-list-badge" part="provider-list-current-badge">
|
|
549
|
+
current
|
|
550
|
+
</span>
|
|
551
|
+
`,
|
|
552
|
+
)}
|
|
553
|
+
</div>
|
|
554
|
+
`,
|
|
555
|
+
)}
|
|
556
|
+
</div>
|
|
557
|
+
`,
|
|
558
|
+
)}
|
|
511
559
|
${when(
|
|
512
560
|
(x) => x.settingsOpen && x.chatConfig.ui?.showSessionCost !== false && x.sessionCostUsd > 0,
|
|
513
561
|
html<FoundationAiAssistant>`
|
package/src/main/main.ts
CHANGED
|
@@ -22,12 +22,13 @@
|
|
|
22
22
|
// =============================================================================
|
|
23
23
|
|
|
24
24
|
import type {
|
|
25
|
+
AIProviderRegistryStatusEntry,
|
|
25
26
|
ChatAttachment,
|
|
26
27
|
ChatConfig,
|
|
27
28
|
ChatInputDuringExecutionMode,
|
|
28
29
|
ChatMessage,
|
|
29
30
|
} from '@genesislcap/foundation-ai';
|
|
30
|
-
import {
|
|
31
|
+
import { AIProviderRegistry } from '@genesislcap/foundation-ai';
|
|
31
32
|
import { avoidTreeShaking } from '@genesislcap/foundation-utils';
|
|
32
33
|
import {
|
|
33
34
|
customElement,
|
|
@@ -138,8 +139,11 @@ function stripHandlers(agent: AgentConfig): Omit<AgentConfig, 'toolHandlers'> {
|
|
|
138
139
|
* Foundation AI Assistant component.
|
|
139
140
|
*
|
|
140
141
|
* @remarks
|
|
141
|
-
*
|
|
142
|
-
*
|
|
142
|
+
* Register one or more AI providers via `registerAIProviders` from
|
|
143
|
+
* `@genesislcap/foundation-ai`; this element resolves the `AIProviderRegistry`
|
|
144
|
+
* through the DI container. Pass agent configuration via the `agents` property —
|
|
145
|
+
* each agent can override which registered provider it uses via `provider:`.
|
|
146
|
+
* The component creates a `ChatDriver` (single agent) or `OrchestratingDriver`
|
|
143
147
|
* (multiple agents) to manage the conversation loop.
|
|
144
148
|
*
|
|
145
149
|
* Popout/collapse coordination uses `agenticActivityBus` topics `chat-popout` and `chat-popin` — not DOM `CustomEvent`s on this element.
|
|
@@ -156,7 +160,7 @@ function stripHandlers(agent: AgentConfig): Omit<AgentConfig, 'toolHandlers'> {
|
|
|
156
160
|
styles,
|
|
157
161
|
})
|
|
158
162
|
export class FoundationAiAssistant extends GenesisElement {
|
|
159
|
-
@
|
|
163
|
+
@AIProviderRegistry providerRegistry!: AIProviderRegistry;
|
|
160
164
|
|
|
161
165
|
@observable designSystemPrefix: string = 'rapid';
|
|
162
166
|
@attr({ attribute: 'header-title' }) headerTitle: string = 'Genesis Assistant';
|
|
@@ -439,6 +443,22 @@ export class FoundationAiAssistant extends GenesisElement {
|
|
|
439
443
|
this._sessionRef?.actions.aiAssistant.setActiveModel(value);
|
|
440
444
|
}
|
|
441
445
|
|
|
446
|
+
/** Name of the AI provider used on the most recent turn. */
|
|
447
|
+
get activeProviderName(): string | undefined {
|
|
448
|
+
return this._sessionRef?.store.aiAssistant.activeProviderName;
|
|
449
|
+
}
|
|
450
|
+
set activeProviderName(value: string | undefined) {
|
|
451
|
+
this._sessionRef?.actions.aiAssistant.setActiveProviderName(value);
|
|
452
|
+
}
|
|
453
|
+
|
|
454
|
+
/** Per-provider status snapshot used by the settings panel. */
|
|
455
|
+
get providerStatuses(): AIProviderRegistryStatusEntry[] {
|
|
456
|
+
return this._sessionRef?.store.aiAssistant.providerStatuses ?? [];
|
|
457
|
+
}
|
|
458
|
+
set providerStatuses(value: AIProviderRegistryStatusEntry[]) {
|
|
459
|
+
this._sessionRef?.actions.aiAssistant.setProviderStatuses(value);
|
|
460
|
+
}
|
|
461
|
+
|
|
442
462
|
// ---- Transient UI state (stays as @observable on the component) ----
|
|
443
463
|
|
|
444
464
|
private _suggestionsGeneration = 0;
|
|
@@ -698,7 +718,7 @@ export class FoundationAiAssistant extends GenesisElement {
|
|
|
698
718
|
// this via `agent-changed` once applyAgent fires on first sendMessage.
|
|
699
719
|
this.activeAgent = agents[0];
|
|
700
720
|
}
|
|
701
|
-
return new OrchestratingDriver(this.
|
|
721
|
+
return new OrchestratingDriver(this.providerRegistry, agents, {
|
|
702
722
|
sessionKey: this.getStateKey() ?? '',
|
|
703
723
|
maxHandoffs: agent.maxHandoffs,
|
|
704
724
|
classifierHistoryLength: agent.classifierHistoryLength,
|
|
@@ -710,7 +730,7 @@ export class FoundationAiAssistant extends GenesisElement {
|
|
|
710
730
|
}
|
|
711
731
|
|
|
712
732
|
return new ChatDriver(
|
|
713
|
-
this.
|
|
733
|
+
this.providerRegistry,
|
|
714
734
|
{},
|
|
715
735
|
[],
|
|
716
736
|
undefined,
|
|
@@ -781,11 +801,19 @@ export class FoundationAiAssistant extends GenesisElement {
|
|
|
781
801
|
this._sessionRef?.actions.aiAssistant.removeInputOverride({ id: interactionId });
|
|
782
802
|
}
|
|
783
803
|
};
|
|
804
|
+
const onProviderChanged = (e: Event) => {
|
|
805
|
+
const { name } = (e as CustomEvent<{ name: string }>).detail;
|
|
806
|
+
this.activeProviderName = name;
|
|
807
|
+
// Status (model id / context limit) belongs to the active provider — refresh
|
|
808
|
+
// the displayed values so the header reflects the provider that just took over.
|
|
809
|
+
void this.resolveContextLimit();
|
|
810
|
+
};
|
|
784
811
|
driver.addEventListener('sub-agent-history-updated', onSubAgentHistoryUpdated);
|
|
785
812
|
driver.addEventListener('sub-agent-start', onSubAgentStart);
|
|
786
813
|
driver.addEventListener('sub-agent-stop', onSubAgentStop);
|
|
787
814
|
driver.addEventListener('interaction-start', onInteractionStart);
|
|
788
815
|
driver.addEventListener('interaction-stop', onInteractionStop);
|
|
816
|
+
driver.addEventListener('provider-changed', onProviderChanged);
|
|
789
817
|
|
|
790
818
|
const cleanups: (() => void)[] = [
|
|
791
819
|
() => driver.removeEventListener('history-updated', onHistoryUpdated),
|
|
@@ -794,6 +822,7 @@ export class FoundationAiAssistant extends GenesisElement {
|
|
|
794
822
|
() => driver.removeEventListener('sub-agent-stop', onSubAgentStop),
|
|
795
823
|
() => driver.removeEventListener('interaction-start', onInteractionStart),
|
|
796
824
|
() => driver.removeEventListener('interaction-stop', onInteractionStop),
|
|
825
|
+
() => driver.removeEventListener('provider-changed', onProviderChanged),
|
|
797
826
|
];
|
|
798
827
|
|
|
799
828
|
if (driver instanceof OrchestratingDriver) {
|
|
@@ -935,6 +964,7 @@ export class FoundationAiAssistant extends GenesisElement {
|
|
|
935
964
|
}
|
|
936
965
|
this.fetchSuggestions();
|
|
937
966
|
void this.resolveContextLimit();
|
|
967
|
+
void this.loadProviderStatuses();
|
|
938
968
|
if (this.messagesEl) {
|
|
939
969
|
this._scrollListener = () => {
|
|
940
970
|
this._userScrolledAway =
|
|
@@ -983,7 +1013,13 @@ export class FoundationAiAssistant extends GenesisElement {
|
|
|
983
1013
|
|
|
984
1014
|
private async resolveContextLimit(): Promise<void> {
|
|
985
1015
|
try {
|
|
986
|
-
|
|
1016
|
+
// Resolve status for the currently active provider when a driver is up
|
|
1017
|
+
// (so per-agent provider swaps update the displayed model/limit); fall
|
|
1018
|
+
// back to the registry default at startup before any turn has run.
|
|
1019
|
+
const activeName = this.driver?.getActiveProviderName();
|
|
1020
|
+
const status = activeName
|
|
1021
|
+
? await this.providerRegistry.getStatus(activeName)
|
|
1022
|
+
: await this.providerRegistry.getStatus();
|
|
987
1023
|
this.contextLimit = status?.contextLimit;
|
|
988
1024
|
this.activeModel = status?.model;
|
|
989
1025
|
} catch {
|
|
@@ -991,6 +1027,14 @@ export class FoundationAiAssistant extends GenesisElement {
|
|
|
991
1027
|
}
|
|
992
1028
|
}
|
|
993
1029
|
|
|
1030
|
+
private async loadProviderStatuses(): Promise<void> {
|
|
1031
|
+
try {
|
|
1032
|
+
this.providerStatuses = await this.providerRegistry.listStatuses();
|
|
1033
|
+
} catch {
|
|
1034
|
+
// Non-fatal — settings panel just won't list providers
|
|
1035
|
+
}
|
|
1036
|
+
}
|
|
1037
|
+
|
|
994
1038
|
chatConfigChanged() {
|
|
995
1039
|
this.syncShowingSplash();
|
|
996
1040
|
}
|