@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
|
@@ -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,
|
|
@@ -59,6 +60,7 @@ import {
|
|
|
59
60
|
import { ChatSuggestions } from '../suggestions/chat-suggestions';
|
|
60
61
|
import { AnimatedPanelToggle } from '../utils/animated-panel-toggle';
|
|
61
62
|
import { logger } from '../utils/logger';
|
|
63
|
+
import { sumCosts } from '../utils/sum-costs';
|
|
62
64
|
import { expandToolTree } from '../utils/tool-fold';
|
|
63
65
|
import { styles } from './main.styles';
|
|
64
66
|
import { FoundationAiAssistantTemplate } from './main.template';
|
|
@@ -107,9 +109,9 @@ avoidTreeShaking(
|
|
|
107
109
|
* - `toolHandlers` (functions),
|
|
108
110
|
* - `onActivate` / `onDeactivate` (lifecycle hooks, functions),
|
|
109
111
|
* - `getDebugSnapshot` (function),
|
|
110
|
-
* - function-form `systemPrompt` / `toolDefinitions` / `displayName`
|
|
111
|
-
* to `undefined` in the snapshot — the live config on the driver
|
|
112
|
-
* the source of truth; the slice only stores a serializable projection).
|
|
112
|
+
* - function-form `systemPrompt` / `toolDefinitions` / `displayName` / `provider`
|
|
113
|
+
* (downgraded to `undefined` in the snapshot — the live config on the driver
|
|
114
|
+
* is still the source of truth; the slice only stores a serializable projection).
|
|
113
115
|
*/
|
|
114
116
|
function stripHandlers(agent: AgentConfig): Omit<AgentConfig, 'toolHandlers'> {
|
|
115
117
|
const {
|
|
@@ -121,6 +123,7 @@ function stripHandlers(agent: AgentConfig): Omit<AgentConfig, 'toolHandlers'> {
|
|
|
121
123
|
systemPrompt,
|
|
122
124
|
toolDefinitions,
|
|
123
125
|
displayName,
|
|
126
|
+
provider,
|
|
124
127
|
...rest
|
|
125
128
|
} = agent;
|
|
126
129
|
const stripped = {
|
|
@@ -128,6 +131,7 @@ function stripHandlers(agent: AgentConfig): Omit<AgentConfig, 'toolHandlers'> {
|
|
|
128
131
|
systemPrompt: typeof systemPrompt === 'function' ? undefined : systemPrompt,
|
|
129
132
|
toolDefinitions: typeof toolDefinitions === 'function' ? undefined : toolDefinitions,
|
|
130
133
|
displayName: typeof displayName === 'function' ? undefined : displayName,
|
|
134
|
+
provider: typeof provider === 'function' ? undefined : provider,
|
|
131
135
|
};
|
|
132
136
|
return subAgents?.length
|
|
133
137
|
? { ...stripped, subAgents: subAgents.map(stripHandlers) as AgentConfig[] }
|
|
@@ -138,8 +142,11 @@ function stripHandlers(agent: AgentConfig): Omit<AgentConfig, 'toolHandlers'> {
|
|
|
138
142
|
* Foundation AI Assistant component.
|
|
139
143
|
*
|
|
140
144
|
* @remarks
|
|
141
|
-
*
|
|
142
|
-
*
|
|
145
|
+
* Register one or more AI providers via `registerAIProviders` from
|
|
146
|
+
* `@genesislcap/foundation-ai`; this element resolves the `AIProviderRegistry`
|
|
147
|
+
* through the DI container. Pass agent configuration via the `agents` property —
|
|
148
|
+
* each agent can override which registered provider it uses via `provider:`.
|
|
149
|
+
* The component creates a `ChatDriver` (single agent) or `OrchestratingDriver`
|
|
143
150
|
* (multiple agents) to manage the conversation loop.
|
|
144
151
|
*
|
|
145
152
|
* Popout/collapse coordination uses `agenticActivityBus` topics `chat-popout` and `chat-popin` — not DOM `CustomEvent`s on this element.
|
|
@@ -156,7 +163,7 @@ function stripHandlers(agent: AgentConfig): Omit<AgentConfig, 'toolHandlers'> {
|
|
|
156
163
|
styles,
|
|
157
164
|
})
|
|
158
165
|
export class FoundationAiAssistant extends GenesisElement {
|
|
159
|
-
@
|
|
166
|
+
@AIProviderRegistry providerRegistry!: AIProviderRegistry;
|
|
160
167
|
|
|
161
168
|
@observable designSystemPrefix: string = 'rapid';
|
|
162
169
|
@attr({ attribute: 'header-title' }) headerTitle: string = 'Genesis Assistant';
|
|
@@ -439,6 +446,22 @@ export class FoundationAiAssistant extends GenesisElement {
|
|
|
439
446
|
this._sessionRef?.actions.aiAssistant.setActiveModel(value);
|
|
440
447
|
}
|
|
441
448
|
|
|
449
|
+
/** Name of the AI provider used on the most recent turn. */
|
|
450
|
+
get activeProviderName(): string | undefined {
|
|
451
|
+
return this._sessionRef?.store.aiAssistant.activeProviderName;
|
|
452
|
+
}
|
|
453
|
+
set activeProviderName(value: string | undefined) {
|
|
454
|
+
this._sessionRef?.actions.aiAssistant.setActiveProviderName(value);
|
|
455
|
+
}
|
|
456
|
+
|
|
457
|
+
/** Per-provider status snapshot used by the settings panel. */
|
|
458
|
+
get providerStatuses(): AIProviderRegistryStatusEntry[] {
|
|
459
|
+
return this._sessionRef?.store.aiAssistant.providerStatuses ?? [];
|
|
460
|
+
}
|
|
461
|
+
set providerStatuses(value: AIProviderRegistryStatusEntry[]) {
|
|
462
|
+
this._sessionRef?.actions.aiAssistant.setProviderStatuses(value);
|
|
463
|
+
}
|
|
464
|
+
|
|
442
465
|
// ---- Transient UI state (stays as @observable on the component) ----
|
|
443
466
|
|
|
444
467
|
private _suggestionsGeneration = 0;
|
|
@@ -698,7 +721,7 @@ export class FoundationAiAssistant extends GenesisElement {
|
|
|
698
721
|
// this via `agent-changed` once applyAgent fires on first sendMessage.
|
|
699
722
|
this.activeAgent = agents[0];
|
|
700
723
|
}
|
|
701
|
-
return new OrchestratingDriver(this.
|
|
724
|
+
return new OrchestratingDriver(this.providerRegistry, agents, {
|
|
702
725
|
sessionKey: this.getStateKey() ?? '',
|
|
703
726
|
maxHandoffs: agent.maxHandoffs,
|
|
704
727
|
classifierHistoryLength: agent.classifierHistoryLength,
|
|
@@ -710,7 +733,7 @@ export class FoundationAiAssistant extends GenesisElement {
|
|
|
710
733
|
}
|
|
711
734
|
|
|
712
735
|
return new ChatDriver(
|
|
713
|
-
this.
|
|
736
|
+
this.providerRegistry,
|
|
714
737
|
{},
|
|
715
738
|
[],
|
|
716
739
|
undefined,
|
|
@@ -763,6 +786,15 @@ export class FoundationAiAssistant extends GenesisElement {
|
|
|
763
786
|
if (invocationId) {
|
|
764
787
|
this._sessionRef?.actions.aiAssistant.removeInputOverride({ id: invocationId });
|
|
765
788
|
}
|
|
789
|
+
// Sub-agents may have flipped the displayed provider while running;
|
|
790
|
+
// restore the parent agent's provider so the cog reflects the
|
|
791
|
+
// conversational driver again. Status (model / context-limit) is
|
|
792
|
+
// refreshed alongside so they don't disagree.
|
|
793
|
+
const restored = this.driver?.getActiveProviderName();
|
|
794
|
+
if (restored && restored !== this.activeProviderName) {
|
|
795
|
+
this.activeProviderName = restored;
|
|
796
|
+
void this.resolveContextLimit();
|
|
797
|
+
}
|
|
766
798
|
};
|
|
767
799
|
const onInteractionStart = (e: Event) => {
|
|
768
800
|
const { interactionId, chatInputDuringExecution } = (e as CustomEvent).detail as {
|
|
@@ -781,11 +813,19 @@ export class FoundationAiAssistant extends GenesisElement {
|
|
|
781
813
|
this._sessionRef?.actions.aiAssistant.removeInputOverride({ id: interactionId });
|
|
782
814
|
}
|
|
783
815
|
};
|
|
816
|
+
const onProviderChanged = (e: Event) => {
|
|
817
|
+
const { name } = (e as CustomEvent<{ name: string }>).detail;
|
|
818
|
+
this.activeProviderName = name;
|
|
819
|
+
// Status (model id / context limit) belongs to the active provider — refresh
|
|
820
|
+
// the displayed values so the header reflects the provider that just took over.
|
|
821
|
+
void this.resolveContextLimit();
|
|
822
|
+
};
|
|
784
823
|
driver.addEventListener('sub-agent-history-updated', onSubAgentHistoryUpdated);
|
|
785
824
|
driver.addEventListener('sub-agent-start', onSubAgentStart);
|
|
786
825
|
driver.addEventListener('sub-agent-stop', onSubAgentStop);
|
|
787
826
|
driver.addEventListener('interaction-start', onInteractionStart);
|
|
788
827
|
driver.addEventListener('interaction-stop', onInteractionStop);
|
|
828
|
+
driver.addEventListener('provider-changed', onProviderChanged);
|
|
789
829
|
|
|
790
830
|
const cleanups: (() => void)[] = [
|
|
791
831
|
() => driver.removeEventListener('history-updated', onHistoryUpdated),
|
|
@@ -794,6 +834,7 @@ export class FoundationAiAssistant extends GenesisElement {
|
|
|
794
834
|
() => driver.removeEventListener('sub-agent-stop', onSubAgentStop),
|
|
795
835
|
() => driver.removeEventListener('interaction-start', onInteractionStart),
|
|
796
836
|
() => driver.removeEventListener('interaction-stop', onInteractionStop),
|
|
837
|
+
() => driver.removeEventListener('provider-changed', onProviderChanged),
|
|
797
838
|
];
|
|
798
839
|
|
|
799
840
|
if (driver instanceof OrchestratingDriver) {
|
|
@@ -935,6 +976,7 @@ export class FoundationAiAssistant extends GenesisElement {
|
|
|
935
976
|
}
|
|
936
977
|
this.fetchSuggestions();
|
|
937
978
|
void this.resolveContextLimit();
|
|
979
|
+
void this.loadProviderStatuses();
|
|
938
980
|
if (this.messagesEl) {
|
|
939
981
|
this._scrollListener = () => {
|
|
940
982
|
this._userScrolledAway =
|
|
@@ -983,7 +1025,13 @@ export class FoundationAiAssistant extends GenesisElement {
|
|
|
983
1025
|
|
|
984
1026
|
private async resolveContextLimit(): Promise<void> {
|
|
985
1027
|
try {
|
|
986
|
-
|
|
1028
|
+
// Resolve status for the currently active provider when a driver is up
|
|
1029
|
+
// (so per-agent provider swaps update the displayed model/limit); fall
|
|
1030
|
+
// back to the registry default at startup before any turn has run.
|
|
1031
|
+
const activeName = this.driver?.getActiveProviderName();
|
|
1032
|
+
const status = activeName
|
|
1033
|
+
? await this.providerRegistry.getStatus(activeName)
|
|
1034
|
+
: await this.providerRegistry.getStatus();
|
|
987
1035
|
this.contextLimit = status?.contextLimit;
|
|
988
1036
|
this.activeModel = status?.model;
|
|
989
1037
|
} catch {
|
|
@@ -991,6 +1039,14 @@ export class FoundationAiAssistant extends GenesisElement {
|
|
|
991
1039
|
}
|
|
992
1040
|
}
|
|
993
1041
|
|
|
1042
|
+
private async loadProviderStatuses(): Promise<void> {
|
|
1043
|
+
try {
|
|
1044
|
+
this.providerStatuses = await this.providerRegistry.listStatuses();
|
|
1045
|
+
} catch {
|
|
1046
|
+
// Non-fatal — settings panel just won't list providers
|
|
1047
|
+
}
|
|
1048
|
+
}
|
|
1049
|
+
|
|
994
1050
|
chatConfigChanged() {
|
|
995
1051
|
this.syncShowingSplash();
|
|
996
1052
|
}
|
|
@@ -1039,10 +1095,9 @@ export class FoundationAiAssistant extends GenesisElement {
|
|
|
1039
1095
|
// Recompute aggregated session cost from per-message `cost` fields. Recomputing
|
|
1040
1096
|
// (rather than incrementing on append) keeps the total correct under any
|
|
1041
1097
|
// mutation of the message list — including future clear-chat / re-render flows.
|
|
1042
|
-
|
|
1043
|
-
|
|
1044
|
-
|
|
1045
|
-
}
|
|
1098
|
+
// `sumCosts` recurses into `toolCall.subAgentTrace` so cost incurred by
|
|
1099
|
+
// sub-agents (potentially on different providers) is included.
|
|
1100
|
+
const runningCost = sumCosts(this.messages);
|
|
1046
1101
|
if (runningCost !== this.sessionCostUsd) {
|
|
1047
1102
|
this.sessionCostUsd = runningCost;
|
|
1048
1103
|
}
|
|
@@ -1,4 +1,8 @@
|
|
|
1
|
-
import type {
|
|
1
|
+
import type {
|
|
2
|
+
AIProviderRegistryStatusEntry,
|
|
3
|
+
ChatInputDuringExecutionMode,
|
|
4
|
+
ChatMessage,
|
|
5
|
+
} from '@genesislcap/foundation-ai';
|
|
2
6
|
import type { PayloadAction } from '@genesislcap/foundation-redux';
|
|
3
7
|
import { createSlice } from '@genesislcap/foundation-redux';
|
|
4
8
|
import type { AgentConfig } from '../config/config';
|
|
@@ -39,6 +43,17 @@ export interface AiAssistantSessionState {
|
|
|
39
43
|
sessionCostUsd: number;
|
|
40
44
|
/** Active model id (e.g. `claude-sonnet-4-6`), resolved on connect. */
|
|
41
45
|
activeModel: string | undefined;
|
|
46
|
+
/**
|
|
47
|
+
* Name of the AI provider used on the most recent turn — drives "current"
|
|
48
|
+
* marker in the settings panel. `undefined` until the first turn runs.
|
|
49
|
+
*/
|
|
50
|
+
activeProviderName: string | undefined;
|
|
51
|
+
/**
|
|
52
|
+
* Snapshot of every registered provider's status, in registration order.
|
|
53
|
+
* Populated on connect (or on settings-open if not yet loaded) and reused
|
|
54
|
+
* for the lifetime of the session — provider registration is static.
|
|
55
|
+
*/
|
|
56
|
+
providerStatuses: AIProviderRegistryStatusEntry[];
|
|
42
57
|
activeAgent: Omit<AgentConfig, 'toolHandlers'> | undefined;
|
|
43
58
|
/**
|
|
44
59
|
* Name of the agent the user has pinned via the picker. `null` means the
|
|
@@ -90,6 +105,8 @@ export const defaultSessionState: AiAssistantSessionState = {
|
|
|
90
105
|
contextLimit: undefined,
|
|
91
106
|
sessionCostUsd: 0,
|
|
92
107
|
activeModel: undefined,
|
|
108
|
+
activeProviderName: undefined,
|
|
109
|
+
providerStatuses: [],
|
|
93
110
|
activeAgent: undefined,
|
|
94
111
|
pinnedAgentName: null,
|
|
95
112
|
flowOwnerAgentName: null,
|
|
@@ -137,6 +154,12 @@ export const aiAssistantSlice = createSlice({
|
|
|
137
154
|
setActiveModel(state, action: PayloadAction<string | undefined>) {
|
|
138
155
|
state.activeModel = action.payload;
|
|
139
156
|
},
|
|
157
|
+
setActiveProviderName(state, action: PayloadAction<string | undefined>) {
|
|
158
|
+
state.activeProviderName = action.payload;
|
|
159
|
+
},
|
|
160
|
+
setProviderStatuses(state, action: PayloadAction<AIProviderRegistryStatusEntry[]>) {
|
|
161
|
+
state.providerStatuses = action.payload;
|
|
162
|
+
},
|
|
140
163
|
setActiveAgent(state, action: PayloadAction<Omit<AgentConfig, 'toolHandlers'> | undefined>) {
|
|
141
164
|
state.activeAgent = action.payload;
|
|
142
165
|
},
|