@genesislcap/ai-assistant 14.444.1 → 14.445.1
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/dts/utils/sum-costs.d.ts +13 -0
- package/dist/dts/utils/sum-costs.d.ts.map +1 -0
- package/dist/dts/utils/sum-costs.test.d.ts +2 -0
- package/dist/dts/utils/sum-costs.test.d.ts.map +1 -0
- package/dist/esm/components/chat-driver/chat-driver.js +93 -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 +76 -21
- 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/esm/utils/sum-costs.js +23 -0
- package/dist/esm/utils/sum-costs.test.js +88 -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 +107 -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 +69 -14
- package/src/state/ai-assistant-slice.ts +24 -1
- package/src/utils/sum-costs.test.ts +108 -0
- package/src/utils/sum-costs.ts +22 -0
|
@@ -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.1",
|
|
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.1",
|
|
68
|
+
"@genesislcap/genx": "14.445.1",
|
|
69
|
+
"@genesislcap/rollup-builder": "14.445.1",
|
|
70
|
+
"@genesislcap/ts-builder": "14.445.1",
|
|
71
|
+
"@genesislcap/uvu-playwright-builder": "14.445.1",
|
|
72
|
+
"@genesislcap/vite-builder": "14.445.1",
|
|
73
|
+
"@genesislcap/webpack-builder": "14.445.1",
|
|
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.1",
|
|
79
|
+
"@genesislcap/foundation-logger": "14.445.1",
|
|
80
|
+
"@genesislcap/foundation-redux": "14.445.1",
|
|
81
|
+
"@genesislcap/foundation-ui": "14.445.1",
|
|
82
|
+
"@genesislcap/foundation-utils": "14.445.1",
|
|
83
|
+
"@genesislcap/rapid-design-system": "14.445.1",
|
|
84
|
+
"@genesislcap/web-core": "14.445.1",
|
|
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": "893e8b2ffadacaedb39f1935c1979278191b6730"
|
|
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
|
|
@@ -715,7 +795,17 @@ export class ChatDriver extends EventTarget implements AiDriver {
|
|
|
715
795
|
}),
|
|
716
796
|
);
|
|
717
797
|
};
|
|
798
|
+
// Re-dispatch the child's `provider-changed` so the UI reflects whichever
|
|
799
|
+
// provider is *actually* running right now (the sub-agent may use a
|
|
800
|
+
// different provider than the parent). Restoration of the parent's
|
|
801
|
+
// provider on `sub-agent-stop` is handled by the listener in main.ts.
|
|
802
|
+
const forwardProviderChanged = (e: Event) => {
|
|
803
|
+
this.dispatchEvent(
|
|
804
|
+
new CustomEvent('provider-changed', { detail: (e as CustomEvent).detail }),
|
|
805
|
+
);
|
|
806
|
+
};
|
|
718
807
|
child.addEventListener('history-updated', forwardTrace);
|
|
808
|
+
child.addEventListener('provider-changed', forwardProviderChanged);
|
|
719
809
|
|
|
720
810
|
// Unique per-invocation id so listeners can pair start/stop reliably even
|
|
721
811
|
// when the same sub-agent runs multiple times in parallel.
|
|
@@ -728,6 +818,7 @@ export class ChatDriver extends EventTarget implements AiDriver {
|
|
|
728
818
|
await child.sendMessage(task ?? '');
|
|
729
819
|
} finally {
|
|
730
820
|
child.removeEventListener('history-updated', forwardTrace);
|
|
821
|
+
child.removeEventListener('provider-changed', forwardProviderChanged);
|
|
731
822
|
this.dispatchEvent(new CustomEvent('sub-agent-stop', { detail: lifecycleDetail }));
|
|
732
823
|
}
|
|
733
824
|
|
|
@@ -750,10 +841,6 @@ export class ChatDriver extends EventTarget implements AiDriver {
|
|
|
750
841
|
*/
|
|
751
842
|
async continueFromHistory(transientPrimer?: ChatMessage[]): Promise<ChatDriverResult> {
|
|
752
843
|
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
844
|
|
|
758
845
|
this.busy = true;
|
|
759
846
|
this.subAgentCompletion = undefined;
|
|
@@ -1032,10 +1119,16 @@ export class ChatDriver extends EventTarget implements AiDriver {
|
|
|
1032
1119
|
attachments: attachmentsForCall,
|
|
1033
1120
|
};
|
|
1034
1121
|
|
|
1122
|
+
// Resolve the active provider for this turn. Static names were validated
|
|
1123
|
+
// in `applyAgent`; function-form names are validated on first resolution
|
|
1124
|
+
// here and cached for the agent's lifetime.
|
|
1125
|
+
// oxlint-disable-next-line no-await-in-loop
|
|
1126
|
+
const activeProvider = await this.resolveProviderForTurn(promptCtx);
|
|
1127
|
+
|
|
1035
1128
|
let response: ChatMessage;
|
|
1036
1129
|
try {
|
|
1037
1130
|
// oxlint-disable-next-line no-await-in-loop
|
|
1038
|
-
response = await
|
|
1131
|
+
response = await activeProvider.chat!(historyForCall, userInputForCall, options);
|
|
1039
1132
|
} catch (e) {
|
|
1040
1133
|
if (e instanceof MalformedFunctionCallError) {
|
|
1041
1134
|
malformedAttempts += 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.
|