@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
|
@@ -251,6 +251,58 @@ export const styles = css `
|
|
|
251
251
|
font-family: var(--monospace-font, ui-monospace, monospace);
|
|
252
252
|
}
|
|
253
253
|
|
|
254
|
+
.provider-list {
|
|
255
|
+
animation: settings-slide-in 0.2s ease-out;
|
|
256
|
+
padding: calc(var(--design-unit) * 1.5px) calc(var(--design-unit) * 3px);
|
|
257
|
+
border-bottom: 1px solid var(--neutral-stroke-rest);
|
|
258
|
+
background-color: var(--neutral-layer-2);
|
|
259
|
+
display: flex;
|
|
260
|
+
flex-direction: column;
|
|
261
|
+
gap: calc(var(--design-unit) * 1px);
|
|
262
|
+
}
|
|
263
|
+
|
|
264
|
+
.provider-list-label {
|
|
265
|
+
font-size: 0.85em;
|
|
266
|
+
font-weight: 600;
|
|
267
|
+
color: var(--neutral-foreground-rest);
|
|
268
|
+
}
|
|
269
|
+
|
|
270
|
+
.provider-list-row {
|
|
271
|
+
display: flex;
|
|
272
|
+
align-items: baseline;
|
|
273
|
+
gap: calc(var(--design-unit) * 1.5px);
|
|
274
|
+
font-size: 0.8em;
|
|
275
|
+
color: var(--neutral-foreground-hint, var(--neutral-foreground-rest));
|
|
276
|
+
opacity: 80%;
|
|
277
|
+
}
|
|
278
|
+
|
|
279
|
+
.provider-list-row.is-current {
|
|
280
|
+
opacity: 100%;
|
|
281
|
+
color: var(--neutral-foreground-rest);
|
|
282
|
+
}
|
|
283
|
+
|
|
284
|
+
.provider-list-name {
|
|
285
|
+
font-weight: 600;
|
|
286
|
+
}
|
|
287
|
+
|
|
288
|
+
.provider-list-model {
|
|
289
|
+
font-family: var(--monospace-font, ui-monospace, monospace);
|
|
290
|
+
flex: 1;
|
|
291
|
+
overflow: hidden;
|
|
292
|
+
text-overflow: ellipsis;
|
|
293
|
+
white-space: nowrap;
|
|
294
|
+
}
|
|
295
|
+
|
|
296
|
+
.provider-list-badge {
|
|
297
|
+
font-size: 0.7em;
|
|
298
|
+
text-transform: uppercase;
|
|
299
|
+
letter-spacing: 0.05em;
|
|
300
|
+
padding: 0 calc(var(--design-unit) * 0.75px);
|
|
301
|
+
border-radius: calc(var(--design-unit) * 0.5px);
|
|
302
|
+
background-color: var(--neutral-layer-3);
|
|
303
|
+
color: var(--neutral-foreground-rest);
|
|
304
|
+
}
|
|
305
|
+
|
|
254
306
|
.splash-wrapper {
|
|
255
307
|
position: absolute;
|
|
256
308
|
inset: 0;
|
|
@@ -376,12 +376,47 @@ ${(tc) => { var _a; return (((_a = tc.foldPath) === null || _a === void 0 ? void
|
|
|
376
376
|
></${progressTag}>
|
|
377
377
|
</div>
|
|
378
378
|
`)}
|
|
379
|
-
${when((x) => {
|
|
379
|
+
${when((x) => {
|
|
380
|
+
var _a;
|
|
381
|
+
return x.settingsOpen &&
|
|
382
|
+
((_a = x.chatConfig.ui) === null || _a === void 0 ? void 0 : _a.showActiveModel) !== false &&
|
|
383
|
+
x.providerStatuses.length <= 1 &&
|
|
384
|
+
x.activeModel != null;
|
|
385
|
+
}, html `
|
|
380
386
|
<div class="session-stat" part="active-model">
|
|
381
387
|
<span class="session-stat-label">Model</span>
|
|
382
388
|
<span class="session-stat-value">${(x) => x.activeModel}</span>
|
|
383
389
|
</div>
|
|
384
390
|
`)}
|
|
391
|
+
${when((x) => {
|
|
392
|
+
var _a;
|
|
393
|
+
return x.settingsOpen &&
|
|
394
|
+
((_a = x.chatConfig.ui) === null || _a === void 0 ? void 0 : _a.showActiveModel) !== false &&
|
|
395
|
+
x.providerStatuses.length > 1;
|
|
396
|
+
}, html `
|
|
397
|
+
<div class="provider-list" part="provider-list">
|
|
398
|
+
<div class="provider-list-label">Providers</div>
|
|
399
|
+
${repeat((x) => x.providerStatuses, html `
|
|
400
|
+
<div
|
|
401
|
+
class="provider-list-row ${(entry, c) => entry.name === c.parent.activeProviderName ? 'is-current' : ''}"
|
|
402
|
+
part="provider-list-row"
|
|
403
|
+
>
|
|
404
|
+
<span class="provider-list-name">${(entry) => entry.name}</span>
|
|
405
|
+
<span class="provider-list-model">${(entry) => { var _a, _b; return (_b = (_a = entry.status) === null || _a === void 0 ? void 0 : _a.model) !== null && _b !== void 0 ? _b : '—'; }}</span>
|
|
406
|
+
${when((entry) => entry.isDefault, html `
|
|
407
|
+
<span class="provider-list-badge" part="provider-list-default-badge">
|
|
408
|
+
default
|
|
409
|
+
</span>
|
|
410
|
+
`)}
|
|
411
|
+
${when((entry, c) => entry.name === c.parent.activeProviderName, html `
|
|
412
|
+
<span class="provider-list-badge" part="provider-list-current-badge">
|
|
413
|
+
current
|
|
414
|
+
</span>
|
|
415
|
+
`)}
|
|
416
|
+
</div>
|
|
417
|
+
`)}
|
|
418
|
+
</div>
|
|
419
|
+
`)}
|
|
385
420
|
${when((x) => { var _a; return x.settingsOpen && ((_a = x.chatConfig.ui) === null || _a === void 0 ? void 0 : _a.showSessionCost) !== false && x.sessionCostUsd > 0; }, html `
|
|
386
421
|
<div class="session-stat" part="session-cost">
|
|
387
422
|
<span class="session-stat-label">Session cost</span>
|
|
@@ -11,6 +11,8 @@ export const defaultSessionState = {
|
|
|
11
11
|
contextLimit: undefined,
|
|
12
12
|
sessionCostUsd: 0,
|
|
13
13
|
activeModel: undefined,
|
|
14
|
+
activeProviderName: undefined,
|
|
15
|
+
providerStatuses: [],
|
|
14
16
|
activeAgent: undefined,
|
|
15
17
|
pinnedAgentName: null,
|
|
16
18
|
flowOwnerAgentName: null,
|
|
@@ -57,6 +59,12 @@ export const aiAssistantSlice = createSlice({
|
|
|
57
59
|
setActiveModel(state, action) {
|
|
58
60
|
state.activeModel = action.payload;
|
|
59
61
|
},
|
|
62
|
+
setActiveProviderName(state, action) {
|
|
63
|
+
state.activeProviderName = action.payload;
|
|
64
|
+
},
|
|
65
|
+
setProviderStatuses(state, action) {
|
|
66
|
+
state.providerStatuses = action.payload;
|
|
67
|
+
},
|
|
60
68
|
setActiveAgent(state, action) {
|
|
61
69
|
state.activeAgent = action.payload;
|
|
62
70
|
},
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"root":["../src/index.ts","../src/channel/ai-activity-bus.ts","../src/channel/ai-activity-channel.ts","../src/components/halo-overlay.ts","../src/components/activity-halo/activity-halo.ts","../src/components/agent-picker/agent-picker.constants.ts","../src/components/agent-picker/agent-picker.styles.ts","../src/components/agent-picker/agent-picker.template.ts","../src/components/agent-picker/agent-picker.ts","../src/components/agent-picker/index.ts","../src/components/ai-driver/ai-driver.ts","../src/components/ai-driver/index.ts","../src/components/chat-bubble/chat-bubble.styles.ts","../src/components/chat-bubble/chat-bubble.template.ts","../src/components/chat-bubble/chat-bubble.ts","../src/components/chat-bubble/index.ts","../src/components/chat-driver/chat-driver.ts","../src/components/chat-driver/index.ts","../src/components/chat-interaction-wrapper/chat-interaction-wrapper.styles.ts","../src/components/chat-interaction-wrapper/chat-interaction-wrapper.template.ts","../src/components/chat-interaction-wrapper/chat-interaction-wrapper.ts","../src/components/chat-interaction-wrapper/index.ts","../src/components/chat-markdown/chat-markdown.ts","../src/components/chat-markdown/index.ts","../src/components/orchestrating-driver/index.ts","../src/components/orchestrating-driver/orchestrating-driver.ts","../src/components/popout-manager/index.ts","../src/components/popout-manager/popout-manager.ts","../src/config/config.ts","../src/config/define-stateful-agent.ts","../src/config/fallback-agents.ts","../src/config/index.ts","../src/main/index.ts","../src/main/main.styles.ts","../src/main/main.template.ts","../src/main/main.ts","../src/main/main.types.ts","../src/state/ai-assistant-slice.ts","../src/state/driver-registry.ts","../src/state/session-store.ts","../src/styles/ai-colours.ts","../src/styles/index.ts","../src/styles/styles.ts","../src/suggestions/chat-suggestions.ts","../src/tags/index.ts","../src/types/ai-chat-widget.ts","../src/utils/animated-panel-toggle.ts","../src/utils/history-transform.ts","../src/utils/index.ts","../src/utils/logger.ts","../src/utils/tool-fold.ts"],"version":"5.9.2"}
|
|
1
|
+
{"root":["../src/index.ts","../src/channel/ai-activity-bus.ts","../src/channel/ai-activity-channel.ts","../src/components/halo-overlay.ts","../src/components/activity-halo/activity-halo.ts","../src/components/agent-picker/agent-picker.constants.ts","../src/components/agent-picker/agent-picker.styles.ts","../src/components/agent-picker/agent-picker.template.ts","../src/components/agent-picker/agent-picker.ts","../src/components/agent-picker/index.ts","../src/components/ai-driver/ai-driver.ts","../src/components/ai-driver/index.ts","../src/components/chat-bubble/chat-bubble.styles.ts","../src/components/chat-bubble/chat-bubble.template.ts","../src/components/chat-bubble/chat-bubble.ts","../src/components/chat-bubble/index.ts","../src/components/chat-driver/chat-driver.ts","../src/components/chat-driver/index.ts","../src/components/chat-interaction-wrapper/chat-interaction-wrapper.styles.ts","../src/components/chat-interaction-wrapper/chat-interaction-wrapper.template.ts","../src/components/chat-interaction-wrapper/chat-interaction-wrapper.ts","../src/components/chat-interaction-wrapper/index.ts","../src/components/chat-markdown/chat-markdown.ts","../src/components/chat-markdown/index.ts","../src/components/orchestrating-driver/index.ts","../src/components/orchestrating-driver/orchestrating-driver.ts","../src/components/popout-manager/index.ts","../src/components/popout-manager/popout-manager.ts","../src/config/config.ts","../src/config/define-stateful-agent.ts","../src/config/fallback-agents.ts","../src/config/index.ts","../src/config/validate-providers.test.ts","../src/config/validate-providers.ts","../src/main/index.ts","../src/main/main.styles.ts","../src/main/main.template.ts","../src/main/main.ts","../src/main/main.types.ts","../src/state/ai-assistant-slice.ts","../src/state/driver-registry.ts","../src/state/session-store.ts","../src/styles/ai-colours.ts","../src/styles/index.ts","../src/styles/styles.ts","../src/suggestions/chat-suggestions.ts","../src/tags/index.ts","../src/types/ai-chat-widget.ts","../src/utils/animated-panel-toggle.ts","../src/utils/history-transform.ts","../src/utils/index.ts","../src/utils/logger.ts","../src/utils/tool-fold.ts"],"version":"5.9.2"}
|
|
@@ -0,0 +1,219 @@
|
|
|
1
|
+
# Migration Guide — GENC-1262 (Per-Agent AI Provider)
|
|
2
|
+
|
|
3
|
+
This document covers the breaking changes introduced by the `mw/GENC-1262-per-agent-model` branch and the new per-agent provider capability that motivated them.
|
|
4
|
+
|
|
5
|
+
The old single-provider DI token (`AIProvider`) is gone. Hosts now register a **named registry of providers** (`AIProviderRegistry`), and each agent can declare which named provider it wants.
|
|
6
|
+
|
|
7
|
+
---
|
|
8
|
+
|
|
9
|
+
## 1. Single-provider DI token replaced by a registry
|
|
10
|
+
|
|
11
|
+
The `AIProvider` DI token (a const exported by `@genesislcap/foundation-ai`) has been **removed**. So has the `getAIProvider()` helper. They're replaced by a new token `AIProviderRegistry` and a registration helper `registerAIProviders`.
|
|
12
|
+
|
|
13
|
+
The `AIProvider` **interface** still exists and is still exported (concrete provider classes implement it). Only the DI-token const and `getAIProvider` helper are gone.
|
|
14
|
+
|
|
15
|
+
### Bootstrap: single-provider host
|
|
16
|
+
|
|
17
|
+
**Before**
|
|
18
|
+
|
|
19
|
+
```ts
|
|
20
|
+
import { AIProvider, createAIProvider, resolveAIConfig } from '@genesislcap/foundation-ai';
|
|
21
|
+
import { Registration } from '@microsoft/fast-foundation';
|
|
22
|
+
|
|
23
|
+
const aiConfig = await resolveAIConfig({ provider: 'gemini', model: 'gemini-2.5-flash' });
|
|
24
|
+
|
|
25
|
+
this.container.register(
|
|
26
|
+
Registration.instance(AIProvider, createAIProvider(aiConfig)),
|
|
27
|
+
);
|
|
28
|
+
```
|
|
29
|
+
|
|
30
|
+
**After**
|
|
31
|
+
|
|
32
|
+
```ts
|
|
33
|
+
import { createAIProvider, registerAIProviders, resolveAIConfig } from '@genesislcap/foundation-ai';
|
|
34
|
+
|
|
35
|
+
const aiConfig = await resolveAIConfig({ provider: 'gemini', model: 'gemini-2.5-flash' });
|
|
36
|
+
|
|
37
|
+
registerAIProviders(this.container, { gemini: createAIProvider(aiConfig) });
|
|
38
|
+
```
|
|
39
|
+
|
|
40
|
+
Notes:
|
|
41
|
+
|
|
42
|
+
- `registerAIProviders` is a function — call it directly with the container; **do not** wrap it in `container.register(...)`.
|
|
43
|
+
- With one provider in the map, the default is inferred automatically. The key (`'gemini'` here) becomes both the registered name and the default name.
|
|
44
|
+
- Pick a stable, descriptive key. It's how agents will reference this provider in their config, and it's what the settings panel will show.
|
|
45
|
+
|
|
46
|
+
### Bootstrap: multi-provider host
|
|
47
|
+
|
|
48
|
+
```ts
|
|
49
|
+
import {
|
|
50
|
+
AnthropicProvider,
|
|
51
|
+
AnthropicTransport,
|
|
52
|
+
GeminiProvider,
|
|
53
|
+
GeminiTransport,
|
|
54
|
+
registerAIProviders,
|
|
55
|
+
} from '@genesislcap/foundation-ai';
|
|
56
|
+
|
|
57
|
+
const fast = new GeminiProvider({}, new GeminiTransport({ model: 'gemini-2.5-flash-lite' }));
|
|
58
|
+
const deep = new AnthropicProvider({}, new AnthropicTransport({ model: 'claude-sonnet-4-6' }));
|
|
59
|
+
|
|
60
|
+
registerAIProviders(this.container, { fast, deep }, { default: 'fast' });
|
|
61
|
+
```
|
|
62
|
+
|
|
63
|
+
- With **more than one** entry, `options.default` is **required**. Omitting it throws — there's no implicit ordering.
|
|
64
|
+
- Unknown `options.default` (a name not present in `providers`) throws.
|
|
65
|
+
- Empty `providers` map throws.
|
|
66
|
+
|
|
67
|
+
### What happens if you register nothing?
|
|
68
|
+
|
|
69
|
+
A built-in empty registry resolves from the DI container. Every lookup returns a no-op provider; the assistant renders inert. This preserves today's "AI feature flag off / nothing configured" behaviour — no need to special-case it.
|
|
70
|
+
|
|
71
|
+
---
|
|
72
|
+
|
|
73
|
+
## 2. New `BaseAgentConfig.provider` — per-agent provider override
|
|
74
|
+
|
|
75
|
+
Every agent (specialist, fallback, or stateful) can now declare which registered provider it uses by name. Omit the field to use the registry's default.
|
|
76
|
+
|
|
77
|
+
```ts
|
|
78
|
+
import { defineAgent } from '@genesislcap/ai-assistant';
|
|
79
|
+
|
|
80
|
+
const classifier = defineAgent({
|
|
81
|
+
name: 'classifier',
|
|
82
|
+
description: 'Quick triage and routing.',
|
|
83
|
+
provider: 'fast', // resolves against the AIProviderRegistry
|
|
84
|
+
});
|
|
85
|
+
|
|
86
|
+
const analyst = defineAgent({
|
|
87
|
+
name: 'analyst',
|
|
88
|
+
description: 'Deep analysis with long context.',
|
|
89
|
+
provider: 'deep',
|
|
90
|
+
});
|
|
91
|
+
```
|
|
92
|
+
|
|
93
|
+
Type: `provider?: string | ((ctx: SystemPromptContext) => string | Promise<string>)`.
|
|
94
|
+
|
|
95
|
+
### Function form (per-turn resolution)
|
|
96
|
+
|
|
97
|
+
The function form mirrors `systemPrompt`, `toolDefinitions`, `toolHandlers` — resolved each tool-loop iteration with the same `SystemPromptContext`. On a plain `defineAgent` config this looks like:
|
|
98
|
+
|
|
99
|
+
```ts
|
|
100
|
+
const triage = defineAgent({
|
|
101
|
+
name: 'triage',
|
|
102
|
+
description: 'Routes intents.',
|
|
103
|
+
provider: (ctx) => (ctx.history.length > 8 ? 'deep' : 'fast'),
|
|
104
|
+
});
|
|
105
|
+
```
|
|
106
|
+
|
|
107
|
+
The argument is just `SystemPromptContext` — `agentName`, `history`, `turnIndex`, `signal`. No `state` field, because plain agents don't own per-turn state.
|
|
108
|
+
|
|
109
|
+
### Stateful agents (`defineStatefulAgent`)
|
|
110
|
+
|
|
111
|
+
For agents built with `defineStatefulAgent<S>`, the `provider` resolver receives `StatefulAgentContext<S>` — the same context plus the live `state` value. This lets the agent flip provider based on whatever it owns (a state machine, an accumulator, anything in `S`):
|
|
112
|
+
|
|
113
|
+
```ts
|
|
114
|
+
const guidedBooking = defineStatefulAgent<{ machine: GuidedBookingMachine }>({
|
|
115
|
+
name: 'guided_booking',
|
|
116
|
+
description: 'Walks the user through a multi-step booking flow.',
|
|
117
|
+
init: ({ /* ... */ }) => ({ machine: createMachine(...) }),
|
|
118
|
+
|
|
119
|
+
provider: ({ state }) =>
|
|
120
|
+
state.machine.state === GuidedBookingStates.EnterDetails ? 'deep' : 'fast',
|
|
121
|
+
|
|
122
|
+
systemPrompt: ({ state }) => state.machine.meta?.systemPrompt ?? '...',
|
|
123
|
+
// ...
|
|
124
|
+
});
|
|
125
|
+
```
|
|
126
|
+
|
|
127
|
+
`defineStatefulAgent` wraps the function so `state` is threaded in before each call — you don't write the wrapping yourself.
|
|
128
|
+
|
|
129
|
+
**Use typed constants for state names.** Comparing against a string literal like `'enterDetails'` is fragile — renames in the machine don't break the resolver. Export a typed const map from the machine file and reference it from the agent:
|
|
130
|
+
|
|
131
|
+
```ts
|
|
132
|
+
// guided-booking.machine.ts
|
|
133
|
+
export const GuidedBookingStates = {
|
|
134
|
+
PickCounterparty: 'pickCounterparty',
|
|
135
|
+
PickInstrument: 'pickInstrument',
|
|
136
|
+
EnterDetails: 'enterDetails',
|
|
137
|
+
Confirm: 'confirm',
|
|
138
|
+
Done: 'done',
|
|
139
|
+
Cancelled: 'cancelled',
|
|
140
|
+
} as const satisfies Record<string, GuidedBookingState>;
|
|
141
|
+
```
|
|
142
|
+
|
|
143
|
+
Renaming a state now requires updating the map, which surfaces every consumer in one place. The `satisfies Record<string, GuidedBookingState>` keeps the values constrained to the state-name union.
|
|
144
|
+
|
|
145
|
+
For dynamic stateful agents (no machine — see [`trade-operations.agent.ts`](../../../../../showcase/client-app/client/src/routes/ai/trade-operations/trade-operations.agent.ts)), the same pattern applies but the predicate is whatever derives "current phase" from the state shape — e.g. `provider: ({ state }) => isPlanning(state) ? 'deep' : 'fast'`.
|
|
146
|
+
|
|
147
|
+
### Sub-agents
|
|
148
|
+
|
|
149
|
+
`BaseAgentConfig.provider` is inherited by sub-agents. A sub-agent declared in `subAgents: [...]` can have its own `provider` field; it's validated alongside its parent.
|
|
150
|
+
|
|
151
|
+
---
|
|
152
|
+
|
|
153
|
+
## 3. Validation — unknown names and missing capabilities hard-error
|
|
154
|
+
|
|
155
|
+
There are no silent fallbacks for misconfigured providers. The validation runs at two points:
|
|
156
|
+
|
|
157
|
+
- **Static names** (`provider: 'fast'`) — validated at `OrchestratingDriver` construction time. Walks every agent and every nested sub-agent. Unknown names throw with the list of registered names. Providers missing `chat()` throw too.
|
|
158
|
+
- **Function-form names** (`provider: ({ state }) => '...'`) — validated on first resolution per name (cached after that). Subsequent state transitions that return a different name validate again.
|
|
159
|
+
|
|
160
|
+
Sample errors:
|
|
161
|
+
|
|
162
|
+
```
|
|
163
|
+
OrchestratingDriver: agent "analyst" references unknown provider "deep". Registered: fast
|
|
164
|
+
ChatDriver: agent "analyst" resolved to provider "deep" which does not implement chat() — required by ChatDriver.
|
|
165
|
+
```
|
|
166
|
+
|
|
167
|
+
If you see one of these, the fix is in either the registration (`registerAIProviders(...)`) or the agent config (`provider: '...'`), not in this code.
|
|
168
|
+
|
|
169
|
+
---
|
|
170
|
+
|
|
171
|
+
## 4. Settings panel — multi-provider display
|
|
172
|
+
|
|
173
|
+
The settings cog inside the assistant displays providers in two modes:
|
|
174
|
+
|
|
175
|
+
- **1 provider** — same single "Model" row as before. No visible change for single-provider hosts.
|
|
176
|
+
- **2+ providers** — a list of every registered provider with the model id from `getStatus()`, plus `default` and `current` badges. `current` follows the active agent's resolved provider.
|
|
177
|
+
|
|
178
|
+
Status is fetched once on connect via `AIProviderRegistry.listStatuses()` and refreshed when the active provider changes.
|
|
179
|
+
|
|
180
|
+
Nothing about the multi-provider list leaks outside the cog — the header, halo, and message rows are unchanged.
|
|
181
|
+
|
|
182
|
+
---
|
|
183
|
+
|
|
184
|
+
## 5. Consumer behaviour outside the assistant
|
|
185
|
+
|
|
186
|
+
Other foundation-ui components that consume the AI layer use **the registry's default provider only** — they're not bound to any agent, so per-agent overrides don't apply:
|
|
187
|
+
|
|
188
|
+
- `ai-criteria-search` (the natural-language criteria filter, including when embedded in `foundation-entity-management`) — calls `interpretCriteria` on the default provider.
|
|
189
|
+
- `ai-indicator` (chrome status indicator) — reads status / triggers Chrome model download on the default provider.
|
|
190
|
+
|
|
191
|
+
If your host needs criteria search backed by a specific provider, make sure that provider is your default.
|
|
192
|
+
|
|
193
|
+
---
|
|
194
|
+
|
|
195
|
+
## 6. Things that always use the default provider
|
|
196
|
+
|
|
197
|
+
Inside the assistant itself, a few non-turn paths always run against the registry default rather than the active agent's provider:
|
|
198
|
+
|
|
199
|
+
- **Suggestion generation** (`ChatDriver.getSuggestions`) — UI helper not bound to any agent.
|
|
200
|
+
- **Classifier** (`OrchestratingDriver`'s intent routing) — orchestrator-level decision, runs before any specialist is picked.
|
|
201
|
+
|
|
202
|
+
This is intentional: keep those paths cheap and predictable, and pick a default provider that's cheap enough for them.
|
|
203
|
+
|
|
204
|
+
---
|
|
205
|
+
|
|
206
|
+
## 7. Known limitations
|
|
207
|
+
|
|
208
|
+
- **Context-window flip.** If an agent switches provider mid-conversation (e.g. a stateful agent moves from a long-window provider to a short-window one), history accumulated under the larger window can blow the smaller window. The driver does not auto-trim. Either keep providers compatible in context size, or design state transitions that reset / summarise history.
|
|
209
|
+
- **Capability check is `chat()` only.** `ChatDriver` validates that the resolved provider implements `chat()`. It does **not** validate `streamChat()` or `prompt()`. If an agent's flow specifically needs streaming, ensure the provider implements it; otherwise calls will fall through to non-streaming.
|
|
210
|
+
|
|
211
|
+
---
|
|
212
|
+
|
|
213
|
+
## 8. Checklist for migrating a host app
|
|
214
|
+
|
|
215
|
+
1. Replace `Registration.instance(AIProvider, createAIProvider(...))` with `registerAIProviders(container, { '<name>': createAIProvider(...) })`. Call it **outside** `container.register(...)`.
|
|
216
|
+
2. Drop `import { AIProvider }` value imports; keep `import type { AIProvider }` if you still need the interface.
|
|
217
|
+
3. Drop any `getAIProvider()` calls — pull the registry from DI (`@AIProviderRegistry providerRegistry!: AIProviderRegistry`) and use `providerRegistry.default()` or `providerRegistry.get(name)`.
|
|
218
|
+
4. If you want per-agent providers, register multiple under distinct names with an explicit `default`, then add `provider: '<name>'` to the relevant `AgentConfig` entries.
|
|
219
|
+
5. Re-run typecheck. The breakage surface is narrow: only sites that used the removed const or helper. Type-only uses of the `AIProvider` interface continue to work unchanged.
|
package/package.json
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@genesislcap/ai-assistant",
|
|
3
3
|
"description": "Genesis AI Assistant micro-frontend",
|
|
4
|
-
"version": "14.
|
|
4
|
+
"version": "14.445.0",
|
|
5
5
|
"license": "SEE LICENSE IN license.txt",
|
|
6
6
|
"main": "dist/esm/index.js",
|
|
7
7
|
"types": "dist/ai-assistant.d.ts",
|
|
@@ -64,24 +64,24 @@
|
|
|
64
64
|
}
|
|
65
65
|
},
|
|
66
66
|
"devDependencies": {
|
|
67
|
-
"@genesislcap/foundation-testing": "14.
|
|
68
|
-
"@genesislcap/genx": "14.
|
|
69
|
-
"@genesislcap/rollup-builder": "14.
|
|
70
|
-
"@genesislcap/ts-builder": "14.
|
|
71
|
-
"@genesislcap/uvu-playwright-builder": "14.
|
|
72
|
-
"@genesislcap/vite-builder": "14.
|
|
73
|
-
"@genesislcap/webpack-builder": "14.
|
|
67
|
+
"@genesislcap/foundation-testing": "14.445.0",
|
|
68
|
+
"@genesislcap/genx": "14.445.0",
|
|
69
|
+
"@genesislcap/rollup-builder": "14.445.0",
|
|
70
|
+
"@genesislcap/ts-builder": "14.445.0",
|
|
71
|
+
"@genesislcap/uvu-playwright-builder": "14.445.0",
|
|
72
|
+
"@genesislcap/vite-builder": "14.445.0",
|
|
73
|
+
"@genesislcap/webpack-builder": "14.445.0",
|
|
74
74
|
"@types/dompurify": "^3.0.5",
|
|
75
75
|
"@types/marked": "^5.0.2"
|
|
76
76
|
},
|
|
77
77
|
"dependencies": {
|
|
78
|
-
"@genesislcap/foundation-ai": "14.
|
|
79
|
-
"@genesislcap/foundation-logger": "14.
|
|
80
|
-
"@genesislcap/foundation-redux": "14.
|
|
81
|
-
"@genesislcap/foundation-ui": "14.
|
|
82
|
-
"@genesislcap/foundation-utils": "14.
|
|
83
|
-
"@genesislcap/rapid-design-system": "14.
|
|
84
|
-
"@genesislcap/web-core": "14.
|
|
78
|
+
"@genesislcap/foundation-ai": "14.445.0",
|
|
79
|
+
"@genesislcap/foundation-logger": "14.445.0",
|
|
80
|
+
"@genesislcap/foundation-redux": "14.445.0",
|
|
81
|
+
"@genesislcap/foundation-ui": "14.445.0",
|
|
82
|
+
"@genesislcap/foundation-utils": "14.445.0",
|
|
83
|
+
"@genesislcap/rapid-design-system": "14.445.0",
|
|
84
|
+
"@genesislcap/web-core": "14.445.0",
|
|
85
85
|
"dompurify": "^3.3.1",
|
|
86
86
|
"marked": "^17.0.3"
|
|
87
87
|
},
|
|
@@ -93,5 +93,5 @@
|
|
|
93
93
|
"publishConfig": {
|
|
94
94
|
"access": "public"
|
|
95
95
|
},
|
|
96
|
-
"gitHead": "
|
|
96
|
+
"gitHead": "ab4f2ce242dedb41dcd82795abf7e0acaf14979c"
|
|
97
97
|
}
|
|
@@ -75,4 +75,12 @@ export interface AiDriver extends EventTarget {
|
|
|
75
75
|
* orchestrator just delegates to its inner `ChatDriver`.
|
|
76
76
|
*/
|
|
77
77
|
getTurnSnapshots?(): ReadonlyArray<TurnSnapshot>;
|
|
78
|
+
|
|
79
|
+
/**
|
|
80
|
+
* Name of the AI provider used on the most recent turn. Falls back to the
|
|
81
|
+
* registry's default name when no turn has run yet.
|
|
82
|
+
*
|
|
83
|
+
* @beta
|
|
84
|
+
*/
|
|
85
|
+
getActiveProviderName(): string;
|
|
78
86
|
}
|
|
@@ -1,5 +1,6 @@
|
|
|
1
1
|
import type {
|
|
2
2
|
AIProvider,
|
|
3
|
+
AIProviderRegistry,
|
|
3
4
|
ChatAttachment,
|
|
4
5
|
ChatDriverResult,
|
|
5
6
|
ChatMessage,
|
|
@@ -14,11 +15,13 @@ import { MalformedFunctionCallError } from '@genesislcap/foundation-ai';
|
|
|
14
15
|
import { agenticActivityBus } from '../../channel/ai-activity-bus';
|
|
15
16
|
import type {
|
|
16
17
|
AgentConfig,
|
|
18
|
+
ProviderInput,
|
|
17
19
|
SystemPromptContext,
|
|
18
20
|
SystemPromptInput,
|
|
19
21
|
ToolDefinitionsInput,
|
|
20
22
|
ToolHandlersInput,
|
|
21
23
|
} from '../../config/config';
|
|
24
|
+
import { resolveChatProvider } from '../../config/validate-providers';
|
|
22
25
|
import { applyHistoryCap } from '../../utils/history-transform';
|
|
23
26
|
import { logger } from '../../utils/logger';
|
|
24
27
|
import { TOOL_FOLD_SYMBOL, type ToolFold } from '../../utils/tool-fold';
|
|
@@ -205,8 +208,24 @@ export class ChatDriver extends EventTarget implements AiDriver {
|
|
|
205
208
|
private debugSnapshotter?: () => unknown;
|
|
206
209
|
private readonly maxTurnSnapshots: number;
|
|
207
210
|
|
|
211
|
+
/**
|
|
212
|
+
* Active agent's provider selector (static name or per-turn resolver).
|
|
213
|
+
* `undefined` means "use the registry default".
|
|
214
|
+
*/
|
|
215
|
+
private activeProviderInput?: ProviderInput;
|
|
216
|
+
/**
|
|
217
|
+
* Caches validated provider lookups per name within the current agent. Cleared
|
|
218
|
+
* by `applyAgent` so each new agent's static/function-resolved names are
|
|
219
|
+
* validated fresh.
|
|
220
|
+
*/
|
|
221
|
+
private resolvedProviderCache = new Map<string, AIProvider>();
|
|
222
|
+
/** Last successfully resolved provider name — drives `getActiveProviderName`. */
|
|
223
|
+
private lastResolvedProviderName?: string;
|
|
224
|
+
/** Last dispatched `provider-changed` name; avoids duplicate events on stable turns. */
|
|
225
|
+
private lastDispatchedProviderName?: string;
|
|
226
|
+
|
|
208
227
|
constructor(
|
|
209
|
-
private readonly
|
|
228
|
+
private readonly providerRegistry: AIProviderRegistry,
|
|
210
229
|
toolHandlers: ToolHandlersInput = {},
|
|
211
230
|
toolDefinitions: ToolDefinitionsInput = [],
|
|
212
231
|
systemPrompt?: SystemPromptInput,
|
|
@@ -270,11 +289,72 @@ export class ChatDriver extends EventTarget implements AiDriver {
|
|
|
270
289
|
typeof config.displayName === 'string' ? config.displayName : config.name;
|
|
271
290
|
this.debugSnapshotter = config.getDebugSnapshot;
|
|
272
291
|
this.subAgentsMap = new Map((config.subAgents ?? []).map((s) => [s.name, s]));
|
|
292
|
+
this.activeProviderInput = config.provider;
|
|
293
|
+
this.resolvedProviderCache.clear();
|
|
294
|
+
this.lastResolvedProviderName = undefined;
|
|
295
|
+
// Static validation: resolve the name now so unknown-provider and missing-
|
|
296
|
+
// capability errors fire at agent swap rather than on the first LLM call.
|
|
297
|
+
// Function-form `provider` is validated lazily inside `resolveProviderForTurn`.
|
|
298
|
+
if (typeof config.provider === 'string') {
|
|
299
|
+
this.resolveProviderByName(config.provider, config.name);
|
|
300
|
+
}
|
|
273
301
|
// Reset fold state when agent changes — each specialist starts fresh
|
|
274
302
|
this.foldStack = [];
|
|
275
303
|
this.consecutiveFoldOps = 0;
|
|
276
304
|
}
|
|
277
305
|
|
|
306
|
+
/**
|
|
307
|
+
* Returns the most recently resolved provider name. Falls back to the
|
|
308
|
+
* registry's default when no per-turn resolution has happened yet.
|
|
309
|
+
*/
|
|
310
|
+
getActiveProviderName(): string {
|
|
311
|
+
return this.lastResolvedProviderName ?? this.providerRegistry.defaultName();
|
|
312
|
+
}
|
|
313
|
+
|
|
314
|
+
/**
|
|
315
|
+
* Resolve a named provider against the registry. Cached per-agent so
|
|
316
|
+
* repeated lookups during one agent's lifetime don't re-validate.
|
|
317
|
+
* Validation lives in `resolveChatProvider`; this wrapper just adds the
|
|
318
|
+
* cache.
|
|
319
|
+
*/
|
|
320
|
+
private resolveProviderByName(name: string, agentName: string): AIProvider {
|
|
321
|
+
const cached = this.resolvedProviderCache.get(name);
|
|
322
|
+
if (cached) return cached;
|
|
323
|
+
const provider = resolveChatProvider(this.providerRegistry, name, agentName);
|
|
324
|
+
this.resolvedProviderCache.set(name, provider);
|
|
325
|
+
return provider;
|
|
326
|
+
}
|
|
327
|
+
|
|
328
|
+
/**
|
|
329
|
+
* Resolve the provider to use for the current turn. Walks the agent's
|
|
330
|
+
* `provider` selector (static or function form) or falls back to the
|
|
331
|
+
* registry default. Dispatches `provider-changed` when the resolved name
|
|
332
|
+
* differs from the last dispatched value.
|
|
333
|
+
*/
|
|
334
|
+
private async resolveProviderForTurn(ctx: SystemPromptContext): Promise<AIProvider> {
|
|
335
|
+
let provider: AIProvider;
|
|
336
|
+
let resolvedName: string;
|
|
337
|
+
if (this.activeProviderInput === undefined) {
|
|
338
|
+
provider = this.providerRegistry.default();
|
|
339
|
+
resolvedName = this.providerRegistry.defaultName();
|
|
340
|
+
} else {
|
|
341
|
+
const name =
|
|
342
|
+
typeof this.activeProviderInput === 'function'
|
|
343
|
+
? await this.activeProviderInput(ctx)
|
|
344
|
+
: this.activeProviderInput;
|
|
345
|
+
provider = this.resolveProviderByName(name, this.activeAgentName ?? '<unknown>');
|
|
346
|
+
resolvedName = name;
|
|
347
|
+
}
|
|
348
|
+
this.lastResolvedProviderName = resolvedName;
|
|
349
|
+
if (resolvedName !== this.lastDispatchedProviderName) {
|
|
350
|
+
this.lastDispatchedProviderName = resolvedName;
|
|
351
|
+
this.dispatchEvent(
|
|
352
|
+
new CustomEvent<{ name: string }>('provider-changed', { detail: { name: resolvedName } }),
|
|
353
|
+
);
|
|
354
|
+
}
|
|
355
|
+
return provider;
|
|
356
|
+
}
|
|
357
|
+
|
|
278
358
|
/**
|
|
279
359
|
* Returns the early-stop result set by `completeSubAgent`, if any.
|
|
280
360
|
* Called by a parent `ChatDriver` after running this instance as a sub-agent.
|
|
@@ -362,8 +442,12 @@ export class ChatDriver extends EventTarget implements AiDriver {
|
|
|
362
442
|
count: number,
|
|
363
443
|
allAgentInfo?: AllAgentSummary[],
|
|
364
444
|
): Promise<string[]> {
|
|
365
|
-
|
|
366
|
-
|
|
445
|
+
// Suggestions are an out-of-turn UI helper, not bound to any single agent —
|
|
446
|
+
// always run against the registry default. Best-effort: a default with no
|
|
447
|
+
// `prompt()` just means no suggestions, not a hard error.
|
|
448
|
+
const defaultProvider = this.providerRegistry.default();
|
|
449
|
+
if (!defaultProvider.prompt) {
|
|
450
|
+
logger.warn('ChatDriver: default AI provider does not implement prompt()');
|
|
367
451
|
return [];
|
|
368
452
|
}
|
|
369
453
|
|
|
@@ -449,7 +533,7 @@ export class ChatDriver extends EventTarget implements AiDriver {
|
|
|
449
533
|
`- No preamble, headings, summary, or commentary before or after the list.`;
|
|
450
534
|
}
|
|
451
535
|
|
|
452
|
-
const text = await
|
|
536
|
+
const text = await defaultProvider.prompt!(userMessage, { systemPrompt });
|
|
453
537
|
|
|
454
538
|
// Lenient parsing as a defensive backstop: even with the strict prompt,
|
|
455
539
|
// models occasionally slip in numbering, bullets, or surrounding markdown.
|
|
@@ -585,10 +669,6 @@ export class ChatDriver extends EventTarget implements AiDriver {
|
|
|
585
669
|
|
|
586
670
|
async sendMessage(userInput: string, attachments?: ChatAttachment[]): Promise<ChatDriverResult> {
|
|
587
671
|
if (this.busy || (!userInput.trim() && !attachments?.length)) return { reason: 'done' };
|
|
588
|
-
if (!this.aiProvider.chat) {
|
|
589
|
-
logger.warn('ChatDriver: AIProvider does not implement chat()');
|
|
590
|
-
return { reason: 'done' };
|
|
591
|
-
}
|
|
592
672
|
|
|
593
673
|
this.busy = true;
|
|
594
674
|
this.subAgentCompletion = undefined;
|
|
@@ -697,7 +777,7 @@ export class ChatDriver extends EventTarget implements AiDriver {
|
|
|
697
777
|
...(subConfig.primerHistory ?? []),
|
|
698
778
|
];
|
|
699
779
|
|
|
700
|
-
const child = new ChatDriver(this.
|
|
780
|
+
const child = new ChatDriver(this.providerRegistry);
|
|
701
781
|
child.applyAgent({ ...subConfig, primerHistory: effectivePrimer });
|
|
702
782
|
// Route interactions back through this driver so widgets render in the
|
|
703
783
|
// parent's (ultimately the root's) history and resolve via the same
|
|
@@ -750,10 +830,6 @@ export class ChatDriver extends EventTarget implements AiDriver {
|
|
|
750
830
|
*/
|
|
751
831
|
async continueFromHistory(transientPrimer?: ChatMessage[]): Promise<ChatDriverResult> {
|
|
752
832
|
if (this.busy) return { reason: 'done' };
|
|
753
|
-
if (!this.aiProvider.chat) {
|
|
754
|
-
logger.warn('ChatDriver: AIProvider does not implement chat()');
|
|
755
|
-
return { reason: 'done' };
|
|
756
|
-
}
|
|
757
833
|
|
|
758
834
|
this.busy = true;
|
|
759
835
|
this.subAgentCompletion = undefined;
|
|
@@ -1032,10 +1108,16 @@ export class ChatDriver extends EventTarget implements AiDriver {
|
|
|
1032
1108
|
attachments: attachmentsForCall,
|
|
1033
1109
|
};
|
|
1034
1110
|
|
|
1111
|
+
// Resolve the active provider for this turn. Static names were validated
|
|
1112
|
+
// in `applyAgent`; function-form names are validated on first resolution
|
|
1113
|
+
// here and cached for the agent's lifetime.
|
|
1114
|
+
// oxlint-disable-next-line no-await-in-loop
|
|
1115
|
+
const activeProvider = await this.resolveProviderForTurn(promptCtx);
|
|
1116
|
+
|
|
1035
1117
|
let response: ChatMessage;
|
|
1036
1118
|
try {
|
|
1037
1119
|
// oxlint-disable-next-line no-await-in-loop
|
|
1038
|
-
response = await
|
|
1120
|
+
response = await activeProvider.chat!(historyForCall, userInputForCall, options);
|
|
1039
1121
|
} catch (e) {
|
|
1040
1122
|
if (e instanceof MalformedFunctionCallError) {
|
|
1041
1123
|
malformedAttempts += 1;
|