@genesislcap/ai-assistant 14.461.1 → 14.461.2-GENC-1346.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 +1 -1
- package/dist/ai-assistant.d.ts +32 -0
- package/dist/dts/components/chat-driver/chat-driver.d.ts +28 -0
- package/dist/dts/components/chat-driver/chat-driver.d.ts.map +1 -1
- package/dist/dts/main/main.d.ts +4 -0
- package/dist/dts/main/main.d.ts.map +1 -1
- package/dist/esm/components/chat-driver/chat-driver.js +78 -1
- package/dist/esm/components/chat-driver/chat-driver.test.js +177 -0
- package/dist/esm/main/main.js +23 -6
- package/dist/esm/state/debug-event-log.js +1 -1
- package/package.json +16 -16
- package/src/components/chat-driver/chat-driver.test.ts +240 -0
- package/src/components/chat-driver/chat-driver.ts +93 -1
- package/src/main/main.ts +20 -1
- package/src/state/debug-event-log.ts +1 -1
|
@@ -5394,7 +5394,7 @@
|
|
|
5394
5394
|
},
|
|
5395
5395
|
{
|
|
5396
5396
|
"kind": "Content",
|
|
5397
|
-
"text": ";\n kind: \"message\";\n } | {\n index: number;\n timestamp: string;\n type: "
|
|
5397
|
+
"text": ";\n model?: string;\n providerName?: string;\n kind: \"message\";\n } | {\n index: number;\n timestamp: string;\n type: "
|
|
5398
5398
|
},
|
|
5399
5399
|
{
|
|
5400
5400
|
"kind": "Reference",
|
package/dist/ai-assistant.d.ts
CHANGED
|
@@ -803,6 +803,27 @@ export declare class ChatDriver extends EventTarget implements AiDriver {
|
|
|
803
803
|
private lastResolvedProviderName?;
|
|
804
804
|
/** Last dispatched `provider-changed` name; avoids duplicate events on stable turns. */
|
|
805
805
|
private lastDispatchedProviderName?;
|
|
806
|
+
/**
|
|
807
|
+
* Concrete model id of the provider resolved for the current turn, read from
|
|
808
|
+
* its `getStatus()` and stamped onto the resulting assistant message so the
|
|
809
|
+
* debug log carries per-message model lineage. Re-resolved each turn in
|
|
810
|
+
* `resolveProviderForTurn`.
|
|
811
|
+
*/
|
|
812
|
+
private lastResolvedModel?;
|
|
813
|
+
/**
|
|
814
|
+
* Memoised `name → model id` lookups, so the per-turn model resolution doesn't
|
|
815
|
+
* re-await `getStatus()` every turn. Cleared (with `resolvedProviderCache`) on
|
|
816
|
+
* an observable-registry change, so a model swapped behind a stable name is
|
|
817
|
+
* picked up on the next turn.
|
|
818
|
+
*/
|
|
819
|
+
private resolvedModelCache;
|
|
820
|
+
/**
|
|
821
|
+
* Unsubscribe handle for the provider-registry change listener (only set when
|
|
822
|
+
* the injected registry is observable). Called in `dispose` so the long-lived
|
|
823
|
+
* registry doesn't retain this driver — see the constructor and the sub-agent
|
|
824
|
+
* teardown in `runSubAgent`.
|
|
825
|
+
*/
|
|
826
|
+
private unsubscribeRegistry?;
|
|
806
827
|
constructor(providerRegistry: AIProviderRegistry, toolHandlers?: ToolHandlersInput, toolDefinitions?: ToolDefinitionsInput, systemPrompt?: SystemPromptInput, primerHistory?: ChatMessage[], maxToolIterations?: number, maxFoldOperations?: number, maxTurnSnapshots?: number,
|
|
807
828
|
/** Session identity used to file meta events onto the shared debug-log timeline. */
|
|
808
829
|
sessionKey?: string);
|
|
@@ -866,6 +887,13 @@ export declare class ChatDriver extends EventTarget implements AiDriver {
|
|
|
866
887
|
* differs from the last dispatched value.
|
|
867
888
|
*/
|
|
868
889
|
private resolveProviderForTurn;
|
|
890
|
+
/**
|
|
891
|
+
* Resolve the concrete model id for a provider via its optional `getStatus()`,
|
|
892
|
+
* memoised by registry name. Used to stamp `model` onto outgoing messages.
|
|
893
|
+
* Best-effort: a provider without `getStatus`, a null status, or a throw all
|
|
894
|
+
* yield `undefined` — model attribution is diagnostic, never load-bearing.
|
|
895
|
+
*/
|
|
896
|
+
private resolveModelForProvider;
|
|
869
897
|
/**
|
|
870
898
|
* Resolve a per-turn config input that is either a static value or a function
|
|
871
899
|
* of the turn context — the value-or-resolver shape shared by `provider`,
|
|
@@ -1402,6 +1430,8 @@ export declare class FoundationAiAssistant extends GenesisElement {
|
|
|
1402
1430
|
private driverCleanup?;
|
|
1403
1431
|
private loadingTimer;
|
|
1404
1432
|
private unsubBus?;
|
|
1433
|
+
/** Unsubscribe handle for the provider-registry change listener (observable registries only). */
|
|
1434
|
+
private unsubProviderRegistry?;
|
|
1405
1435
|
private haloStartPublished;
|
|
1406
1436
|
/** Fingerprint of the agents array used to build the current driver. Used by agentsChanged to skip spurious rebuilds. */
|
|
1407
1437
|
private _driverAgentsKey?;
|
|
@@ -1629,6 +1659,8 @@ export declare class FoundationAiAssistant extends GenesisElement {
|
|
|
1629
1659
|
cost?: number;
|
|
1630
1660
|
externalCostUsd?: number;
|
|
1631
1661
|
responseMeta?: ChatResponseMeta;
|
|
1662
|
+
model?: string;
|
|
1663
|
+
providerName?: string;
|
|
1632
1664
|
kind: "message";
|
|
1633
1665
|
} | {
|
|
1634
1666
|
index: number;
|
|
@@ -247,6 +247,27 @@ export declare class ChatDriver extends EventTarget implements AiDriver {
|
|
|
247
247
|
private lastResolvedProviderName?;
|
|
248
248
|
/** Last dispatched `provider-changed` name; avoids duplicate events on stable turns. */
|
|
249
249
|
private lastDispatchedProviderName?;
|
|
250
|
+
/**
|
|
251
|
+
* Concrete model id of the provider resolved for the current turn, read from
|
|
252
|
+
* its `getStatus()` and stamped onto the resulting assistant message so the
|
|
253
|
+
* debug log carries per-message model lineage. Re-resolved each turn in
|
|
254
|
+
* `resolveProviderForTurn`.
|
|
255
|
+
*/
|
|
256
|
+
private lastResolvedModel?;
|
|
257
|
+
/**
|
|
258
|
+
* Memoised `name → model id` lookups, so the per-turn model resolution doesn't
|
|
259
|
+
* re-await `getStatus()` every turn. Cleared (with `resolvedProviderCache`) on
|
|
260
|
+
* an observable-registry change, so a model swapped behind a stable name is
|
|
261
|
+
* picked up on the next turn.
|
|
262
|
+
*/
|
|
263
|
+
private resolvedModelCache;
|
|
264
|
+
/**
|
|
265
|
+
* Unsubscribe handle for the provider-registry change listener (only set when
|
|
266
|
+
* the injected registry is observable). Called in `dispose` so the long-lived
|
|
267
|
+
* registry doesn't retain this driver — see the constructor and the sub-agent
|
|
268
|
+
* teardown in `runSubAgent`.
|
|
269
|
+
*/
|
|
270
|
+
private unsubscribeRegistry?;
|
|
250
271
|
constructor(providerRegistry: AIProviderRegistry, toolHandlers?: ToolHandlersInput, toolDefinitions?: ToolDefinitionsInput, systemPrompt?: SystemPromptInput, primerHistory?: ChatMessage[], maxToolIterations?: number, maxFoldOperations?: number, maxTurnSnapshots?: number,
|
|
251
272
|
/** Session identity used to file meta events onto the shared debug-log timeline. */
|
|
252
273
|
sessionKey?: string);
|
|
@@ -310,6 +331,13 @@ export declare class ChatDriver extends EventTarget implements AiDriver {
|
|
|
310
331
|
* differs from the last dispatched value.
|
|
311
332
|
*/
|
|
312
333
|
private resolveProviderForTurn;
|
|
334
|
+
/**
|
|
335
|
+
* Resolve the concrete model id for a provider via its optional `getStatus()`,
|
|
336
|
+
* memoised by registry name. Used to stamp `model` onto outgoing messages.
|
|
337
|
+
* Best-effort: a provider without `getStatus`, a null status, or a throw all
|
|
338
|
+
* yield `undefined` — model attribution is diagnostic, never load-bearing.
|
|
339
|
+
*/
|
|
340
|
+
private resolveModelForProvider;
|
|
313
341
|
/**
|
|
314
342
|
* Resolve a per-turn config input that is either a static value or a function
|
|
315
343
|
* of the turn context — the value-or-resolver shape shared by `provider`,
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"chat-driver.d.ts","sourceRoot":"","sources":["../../../../src/components/chat-driver/chat-driver.ts"],"names":[],"mappings":"AAAA,OAAO,KAAK,EAEV,kBAAkB,EAClB,cAAc,EACd,gBAAgB,EAChB,WAAW,EAGX,cAAc,EAGd,yBAAyB,EAEzB,qBAAqB,EAEtB,MAAM,4BAA4B,CAAC;
|
|
1
|
+
{"version":3,"file":"chat-driver.d.ts","sourceRoot":"","sources":["../../../../src/components/chat-driver/chat-driver.ts"],"names":[],"mappings":"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"}
|
package/dist/dts/main/main.d.ts
CHANGED
|
@@ -210,6 +210,8 @@ export declare class FoundationAiAssistant extends GenesisElement {
|
|
|
210
210
|
private driverCleanup?;
|
|
211
211
|
private loadingTimer;
|
|
212
212
|
private unsubBus?;
|
|
213
|
+
/** Unsubscribe handle for the provider-registry change listener (observable registries only). */
|
|
214
|
+
private unsubProviderRegistry?;
|
|
213
215
|
private haloStartPublished;
|
|
214
216
|
/** Fingerprint of the agents array used to build the current driver. Used by agentsChanged to skip spurious rebuilds. */
|
|
215
217
|
private _driverAgentsKey?;
|
|
@@ -437,6 +439,8 @@ export declare class FoundationAiAssistant extends GenesisElement {
|
|
|
437
439
|
cost?: number;
|
|
438
440
|
externalCostUsd?: number;
|
|
439
441
|
responseMeta?: import("@genesislcap/foundation-ai").ChatResponseMeta;
|
|
442
|
+
model?: string;
|
|
443
|
+
providerName?: string;
|
|
440
444
|
kind: "message";
|
|
441
445
|
} | {
|
|
442
446
|
index: number;
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"main.d.ts","sourceRoot":"","sources":["../../../src/main/main.ts"],"names":[],"mappings":"AAuBA,OAAO,KAAK,EACV,6BAA6B,EAC7B,cAAc,EACd,UAAU,EACV,4BAA4B,EAC5B,WAAW,EACZ,MAAM,4BAA4B,CAAC;AACpC,OAAO,EAAE,kBAAkB,
|
|
1
|
+
{"version":3,"file":"main.d.ts","sourceRoot":"","sources":["../../../src/main/main.ts"],"names":[],"mappings":"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"}
|
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
import { __awaiter } from "tslib";
|
|
2
|
-
import { MalformedFunctionCallError } from '@genesislcap/foundation-ai';
|
|
2
|
+
import { isObservableAIProviderRegistry, MalformedFunctionCallError, } from '@genesislcap/foundation-ai';
|
|
3
3
|
import { agenticActivityBus } from '../../channel/ai-activity-bus';
|
|
4
4
|
import { resolveChatProvider } from '../../config/validate-providers';
|
|
5
5
|
import { recordMetaEvent, recordTurnError, recordTurnRetry } from '../../state/debug-event-log';
|
|
@@ -138,6 +138,13 @@ export class ChatDriver extends EventTarget {
|
|
|
138
138
|
* validated fresh.
|
|
139
139
|
*/
|
|
140
140
|
this.resolvedProviderCache = new Map();
|
|
141
|
+
/**
|
|
142
|
+
* Memoised `name → model id` lookups, so the per-turn model resolution doesn't
|
|
143
|
+
* re-await `getStatus()` every turn. Cleared (with `resolvedProviderCache`) on
|
|
144
|
+
* an observable-registry change, so a model swapped behind a stable name is
|
|
145
|
+
* picked up on the next turn.
|
|
146
|
+
*/
|
|
147
|
+
this.resolvedModelCache = new Map();
|
|
141
148
|
if (typeof toolHandlers === 'function') {
|
|
142
149
|
this.toolHandlersFactory = toolHandlers;
|
|
143
150
|
this.toolHandlers = {};
|
|
@@ -158,6 +165,24 @@ export class ChatDriver extends EventTarget {
|
|
|
158
165
|
this.primerHistory = primerHistory;
|
|
159
166
|
this.maxFoldOperations = maxFoldOperations;
|
|
160
167
|
this.maxTurnSnapshots = maxTurnSnapshots;
|
|
168
|
+
// Runtime provider switching: when the host registered an observable
|
|
169
|
+
// registry, drop our memoised provider lookups whenever its mapping/default
|
|
170
|
+
// changes so the next turn re-resolves against the new providers. Resetting
|
|
171
|
+
// `lastDispatchedProviderName` forces the next `resolveProviderForTurn` to
|
|
172
|
+
// re-emit `provider-changed` even when the resolved *name* is unchanged
|
|
173
|
+
// (e.g. a tier name like 'high' kept, but the vendor underneath swapped) —
|
|
174
|
+
// that's what lets status UI refresh on a same-name switch. Feature-detect
|
|
175
|
+
// means immutable/empty registries are a no-op. Child sub-agent drivers get
|
|
176
|
+
// the same registry and so subscribe here too; each cleans up in `dispose`.
|
|
177
|
+
if (isObservableAIProviderRegistry(this.providerRegistry)) {
|
|
178
|
+
this.unsubscribeRegistry = this.providerRegistry.subscribe(() => {
|
|
179
|
+
this.resolvedProviderCache.clear();
|
|
180
|
+
this.resolvedModelCache.clear();
|
|
181
|
+
this.lastResolvedProviderName = undefined;
|
|
182
|
+
this.lastResolvedModel = undefined;
|
|
183
|
+
this.lastDispatchedProviderName = undefined;
|
|
184
|
+
});
|
|
185
|
+
}
|
|
161
186
|
}
|
|
162
187
|
/**
|
|
163
188
|
* Tear down the driver: aborts the lifecycle signal so any in-flight provider
|
|
@@ -175,7 +200,13 @@ export class ChatDriver extends EventTarget {
|
|
|
175
200
|
* timeout), which also stamps history and reverts the chat-input override.
|
|
176
201
|
*/
|
|
177
202
|
dispose() {
|
|
203
|
+
var _a;
|
|
178
204
|
this.lifecycleController.abort(new DOMException('AI assistant driver disposed', 'AbortError'));
|
|
205
|
+
// Detach from the provider registry so the long-lived registry doesn't pin
|
|
206
|
+
// this driver (and its closure) after teardown. Guard-cleared so a second
|
|
207
|
+
// dispose is a no-op, matching this method's idempotent contract.
|
|
208
|
+
(_a = this.unsubscribeRegistry) === null || _a === void 0 ? void 0 : _a.call(this);
|
|
209
|
+
this.unsubscribeRegistry = undefined;
|
|
179
210
|
}
|
|
180
211
|
/**
|
|
181
212
|
* Stop the current turn (user "stop" button). Aborts the in-flight provider
|
|
@@ -328,6 +359,7 @@ export class ChatDriver extends EventTarget {
|
|
|
328
359
|
resolvedName = name;
|
|
329
360
|
}
|
|
330
361
|
this.lastResolvedProviderName = resolvedName;
|
|
362
|
+
this.lastResolvedModel = yield this.resolveModelForProvider(resolvedName, provider);
|
|
331
363
|
if (resolvedName !== this.lastDispatchedProviderName) {
|
|
332
364
|
this.lastDispatchedProviderName = resolvedName;
|
|
333
365
|
recordMetaEvent(this.sessionKey, 'provider.selected', {
|
|
@@ -339,6 +371,28 @@ export class ChatDriver extends EventTarget {
|
|
|
339
371
|
return provider;
|
|
340
372
|
});
|
|
341
373
|
}
|
|
374
|
+
/**
|
|
375
|
+
* Resolve the concrete model id for a provider via its optional `getStatus()`,
|
|
376
|
+
* memoised by registry name. Used to stamp `model` onto outgoing messages.
|
|
377
|
+
* Best-effort: a provider without `getStatus`, a null status, or a throw all
|
|
378
|
+
* yield `undefined` — model attribution is diagnostic, never load-bearing.
|
|
379
|
+
*/
|
|
380
|
+
resolveModelForProvider(name, provider) {
|
|
381
|
+
return __awaiter(this, void 0, void 0, function* () {
|
|
382
|
+
var _a, _b;
|
|
383
|
+
if (this.resolvedModelCache.has(name))
|
|
384
|
+
return this.resolvedModelCache.get(name);
|
|
385
|
+
let model;
|
|
386
|
+
try {
|
|
387
|
+
model = (_b = (yield ((_a = provider.getStatus) === null || _a === void 0 ? void 0 : _a.call(provider)))) === null || _b === void 0 ? void 0 : _b.model;
|
|
388
|
+
}
|
|
389
|
+
catch (_c) {
|
|
390
|
+
model = undefined;
|
|
391
|
+
}
|
|
392
|
+
this.resolvedModelCache.set(name, model);
|
|
393
|
+
return model;
|
|
394
|
+
});
|
|
395
|
+
}
|
|
342
396
|
/**
|
|
343
397
|
* Resolve a per-turn config input that is either a static value or a function
|
|
344
398
|
* of the turn context — the value-or-resolver shape shared by `provider`,
|
|
@@ -957,6 +1011,13 @@ export class ChatDriver extends EventTarget {
|
|
|
957
1011
|
this.lifecycleController.signal.removeEventListener('abort', disposeChild);
|
|
958
1012
|
child.removeEventListener('history-updated', forwardTrace);
|
|
959
1013
|
child.removeEventListener('provider-changed', forwardProviderChanged);
|
|
1014
|
+
// Tear the child down on every exit path, not just timeout/parent-abort.
|
|
1015
|
+
// A child that completes normally is otherwise never disposed, so its
|
|
1016
|
+
// provider-registry subscription (wired in the ChatDriver constructor)
|
|
1017
|
+
// would leak — the long-lived registry would retain every completed
|
|
1018
|
+
// sub-agent driver. dispose() is idempotent and only aborts the (already
|
|
1019
|
+
// settled) lifecycle, so the snapshot/completion reads below still work.
|
|
1020
|
+
child.dispose();
|
|
960
1021
|
this.dispatchEvent(new CustomEvent('sub-agent-stop', { detail: lifecycleDetail }));
|
|
961
1022
|
}
|
|
962
1023
|
const trace = child.getHistory();
|
|
@@ -1408,6 +1469,22 @@ export class ChatDriver extends EventTarget {
|
|
|
1408
1469
|
}
|
|
1409
1470
|
throw e;
|
|
1410
1471
|
}
|
|
1472
|
+
// Attribute the response to the concrete model + registry slot that
|
|
1473
|
+
// produced it (resolved for this turn in `resolveProviderForTurn`). Carried
|
|
1474
|
+
// on the assistant message so the debug-log timeline shows per-message
|
|
1475
|
+
// model lineage — and thus per-tool-call, since tool calls ride on the
|
|
1476
|
+
// assistant message that requests them. Harmless on a response later
|
|
1477
|
+
// discarded as empty/retried; only kept copies reach history.
|
|
1478
|
+
//
|
|
1479
|
+
// Attach each key only when resolved: a provider with no `getStatus` (etc.)
|
|
1480
|
+
// leaves the key off entirely rather than carrying it as `undefined`.
|
|
1481
|
+
// JSON.stringify already drops undefined from the exported log, so this is
|
|
1482
|
+
// chiefly about keeping the in-memory message shape honest.
|
|
1483
|
+
if (this.lastResolvedModel !== undefined)
|
|
1484
|
+
response.model = this.lastResolvedModel;
|
|
1485
|
+
if (this.lastResolvedProviderName !== undefined) {
|
|
1486
|
+
response.providerName = this.lastResolvedProviderName;
|
|
1487
|
+
}
|
|
1411
1488
|
const isThinkingStep = response.content && ((_c = response.toolCalls) === null || _c === void 0 ? void 0 : _c.length);
|
|
1412
1489
|
const isEmptyResponse = !((_d = response.content) === null || _d === void 0 ? void 0 : _d.trim()) && !((_e = response.toolCalls) === null || _e === void 0 ? void 0 : _e.length);
|
|
1413
1490
|
if (isEmptyResponse) {
|
|
@@ -863,3 +863,180 @@ interactionCost('leaves externalCostUsd unset for a missing, zero, or negative c
|
|
|
863
863
|
assert.not.ok(driver.getHistory().some((m) => m.externalCostUsd != null));
|
|
864
864
|
}));
|
|
865
865
|
interactionCost.run();
|
|
866
|
+
/**
|
|
867
|
+
* A single-name (`'high'`) observable registry. `get`/`default` always return
|
|
868
|
+
* the current provider, so swapping it mid-session models a same-name vendor
|
|
869
|
+
* switch (the tier name stays `'high'`, the provider underneath changes).
|
|
870
|
+
*/
|
|
871
|
+
const makeObservableRegistry = (initial) => {
|
|
872
|
+
let current = initial;
|
|
873
|
+
const listeners = new Set();
|
|
874
|
+
return {
|
|
875
|
+
get: () => current,
|
|
876
|
+
default: () => current,
|
|
877
|
+
defaultName: () => 'high',
|
|
878
|
+
names: () => ['high'],
|
|
879
|
+
getStatus: () => __awaiter(void 0, void 0, void 0, function* () { return null; }),
|
|
880
|
+
listStatuses: () => __awaiter(void 0, void 0, void 0, function* () { return []; }),
|
|
881
|
+
subscribe(listener) {
|
|
882
|
+
listeners.add(listener);
|
|
883
|
+
return () => {
|
|
884
|
+
listeners.delete(listener);
|
|
885
|
+
};
|
|
886
|
+
},
|
|
887
|
+
swap(provider) {
|
|
888
|
+
current = provider;
|
|
889
|
+
for (const l of Array.from(listeners))
|
|
890
|
+
l();
|
|
891
|
+
},
|
|
892
|
+
listenerCount: () => listeners.size,
|
|
893
|
+
};
|
|
894
|
+
};
|
|
895
|
+
const makeDriverWithRegistry = (config, registry) => {
|
|
896
|
+
const driver = new ChatDriver(registry, {}, [], undefined, undefined, 50, 5, undefined, '');
|
|
897
|
+
driver.applyAgent(config);
|
|
898
|
+
return driver;
|
|
899
|
+
};
|
|
900
|
+
const observable = createLogicSuite('ChatDriver observable provider registry');
|
|
901
|
+
observable.after(() => {
|
|
902
|
+
agenticActivityBus.close();
|
|
903
|
+
});
|
|
904
|
+
observable('a registry change clears the resolved-provider cache so the next turn uses the new provider', () => __awaiter(void 0, void 0, void 0, function* () {
|
|
905
|
+
const providerA = scriptedProvider([{ role: 'assistant', content: 'A' }]);
|
|
906
|
+
const providerB = scriptedProvider([{ role: 'assistant', content: 'B' }]);
|
|
907
|
+
const registry = makeObservableRegistry(providerA);
|
|
908
|
+
// A static provider name means lookups go through `resolvedProviderCache` —
|
|
909
|
+
// the cache that must self-invalidate on a registry change.
|
|
910
|
+
const driver = makeDriverWithRegistry(agent({ name: 'tiered', provider: 'high' }), registry);
|
|
911
|
+
yield driver.sendMessage('first');
|
|
912
|
+
assert.is(providerA.advertisedPerCall.length, 1, 'turn 1 resolves the original provider');
|
|
913
|
+
assert.is(providerB.advertisedPerCall.length, 0);
|
|
914
|
+
registry.swap(providerB); // notify → cache cleared
|
|
915
|
+
yield driver.sendMessage('second');
|
|
916
|
+
assert.is(providerB.advertisedPerCall.length, 1, 'turn 2 resolves the swapped-in provider');
|
|
917
|
+
assert.is(providerA.advertisedPerCall.length, 1, 'the stale provider is not reused');
|
|
918
|
+
driver.dispose();
|
|
919
|
+
}));
|
|
920
|
+
observable('re-emits provider-changed on a same-name swap', () => __awaiter(void 0, void 0, void 0, function* () {
|
|
921
|
+
const providerA = scriptedProvider([{ role: 'assistant', content: 'A' }]);
|
|
922
|
+
const providerB = scriptedProvider([{ role: 'assistant', content: 'B' }]);
|
|
923
|
+
const registry = makeObservableRegistry(providerA);
|
|
924
|
+
const driver = makeDriverWithRegistry(agent({ name: 'tiered', provider: 'high' }), registry);
|
|
925
|
+
const names = [];
|
|
926
|
+
driver.addEventListener('provider-changed', (e) => {
|
|
927
|
+
names.push(e.detail.name);
|
|
928
|
+
});
|
|
929
|
+
yield driver.sendMessage('first');
|
|
930
|
+
registry.swap(providerB);
|
|
931
|
+
yield driver.sendMessage('second');
|
|
932
|
+
// The resolved name ('high') never changes, but the swap resets the
|
|
933
|
+
// last-dispatched name so the cog can refresh — two events, not one.
|
|
934
|
+
assert.equal(names, ['high', 'high']);
|
|
935
|
+
driver.dispose();
|
|
936
|
+
}));
|
|
937
|
+
observable('a non-observable registry is a no-op — turn runs, dispose does not throw', () => __awaiter(void 0, void 0, void 0, function* () {
|
|
938
|
+
const provider = scriptedProvider([{ role: 'assistant', content: 'hi' }]);
|
|
939
|
+
const driver = makeDriverWithRegistry(agent({ name: 'plain' }), makeRegistry(provider));
|
|
940
|
+
yield driver.sendMessage('go');
|
|
941
|
+
assert.is(provider.advertisedPerCall.length, 1);
|
|
942
|
+
driver.dispose(); // no subscription was wired — must still be safe
|
|
943
|
+
}));
|
|
944
|
+
observable('a child sub-agent driver unsubscribes on completion — no listener leak', () => __awaiter(void 0, void 0, void 0, function* () {
|
|
945
|
+
const provider = scriptedProvider([callsTool('delegate', 'd1'), callsTool('finish', 'f1')]);
|
|
946
|
+
const registry = makeObservableRegistry(provider);
|
|
947
|
+
const parent = delegatingParent(completingWorker({ ok: true }), () => { });
|
|
948
|
+
const driver = makeDriverWithRegistry(parent, registry);
|
|
949
|
+
assert.is(registry.listenerCount(), 1, 'the parent driver subscribed on construction');
|
|
950
|
+
yield driver.sendMessage('go');
|
|
951
|
+
// The child subscribed during the run; if it didn't clean up on its (normal)
|
|
952
|
+
// completion the registry would now hold two listeners.
|
|
953
|
+
assert.is(registry.listenerCount(), 1, 'the completed child unsubscribed');
|
|
954
|
+
driver.dispose();
|
|
955
|
+
assert.is(registry.listenerCount(), 0, 'the parent unsubscribed on dispose');
|
|
956
|
+
}));
|
|
957
|
+
observable.run();
|
|
958
|
+
// ---------------------------------------------------------------------------
|
|
959
|
+
// per-message model attribution (GENC-1346)
|
|
960
|
+
//
|
|
961
|
+
// Each model-produced assistant message carries `model` (the concrete model id
|
|
962
|
+
// the active provider's getStatus reports) and `providerName` (the registry slot
|
|
963
|
+
// it resolved under), so the exported debug log shows which model produced each
|
|
964
|
+
// message — and, since tool calls ride on the assistant message, each tool call.
|
|
965
|
+
// ---------------------------------------------------------------------------
|
|
966
|
+
/** A provider that replays scripted replies and reports `model` via getStatus
|
|
967
|
+
* (omitted entirely when `model` is undefined, to model a provider with no
|
|
968
|
+
* status). */
|
|
969
|
+
const modelProvider = (model, responses) => {
|
|
970
|
+
const queue = [...responses];
|
|
971
|
+
const provider = {
|
|
972
|
+
chat: () => __awaiter(void 0, void 0, void 0, function* () { var _a; return (_a = queue.shift()) !== null && _a !== void 0 ? _a : { role: 'assistant', content: 'done' }; }),
|
|
973
|
+
};
|
|
974
|
+
if (model !== undefined) {
|
|
975
|
+
provider.getStatus = () => __awaiter(void 0, void 0, void 0, function* () { return ({ provider: 'gemini', model }); });
|
|
976
|
+
}
|
|
977
|
+
return provider;
|
|
978
|
+
};
|
|
979
|
+
const modelAttr = createLogicSuite('ChatDriver per-message model attribution');
|
|
980
|
+
modelAttr.after(() => {
|
|
981
|
+
agenticActivityBus.close();
|
|
982
|
+
});
|
|
983
|
+
modelAttr('stamps the resolved model and registry name onto an assistant reply', () => __awaiter(void 0, void 0, void 0, function* () {
|
|
984
|
+
const provider = modelProvider('gemini-2.5-flash-lite', [
|
|
985
|
+
{ role: 'assistant', content: 'hi there' },
|
|
986
|
+
]);
|
|
987
|
+
const driver = makeDriver(agent({ name: 'plain' }), provider);
|
|
988
|
+
yield driver.sendMessage('hello');
|
|
989
|
+
const reply = driver.getHistory().find((m) => m.role === 'assistant');
|
|
990
|
+
assert.ok(reply, 'assistant reply present');
|
|
991
|
+
assert.is(reply.model, 'gemini-2.5-flash-lite', 'model id read from provider getStatus');
|
|
992
|
+
// makeRegistry registers a single provider under the name 'test'.
|
|
993
|
+
assert.is(reply.providerName, 'test', 'registry slot the turn resolved under');
|
|
994
|
+
driver.dispose();
|
|
995
|
+
}));
|
|
996
|
+
modelAttr('attributes a tool-calling assistant message (and so its tool calls) to the model', () => __awaiter(void 0, void 0, void 0, function* () {
|
|
997
|
+
const provider = modelProvider('claude-haiku-4-5-20251001', [
|
|
998
|
+
callsTool('noop', 't1'),
|
|
999
|
+
{ role: 'assistant', content: 'finished' },
|
|
1000
|
+
]);
|
|
1001
|
+
const config = agent({
|
|
1002
|
+
name: 'withTool',
|
|
1003
|
+
toolDefinitions: [def('noop')],
|
|
1004
|
+
toolHandlers: { noop: () => __awaiter(void 0, void 0, void 0, function* () { return 'ok'; }) },
|
|
1005
|
+
});
|
|
1006
|
+
const driver = makeDriver(config, provider);
|
|
1007
|
+
yield driver.sendMessage('go');
|
|
1008
|
+
const toolCallMsg = driver.getHistory().find((m) => { var _a, _b; return ((_b = (_a = m.toolCalls) === null || _a === void 0 ? void 0 : _a.length) !== null && _b !== void 0 ? _b : 0) > 0; });
|
|
1009
|
+
assert.ok(toolCallMsg, 'an assistant message with tool calls is present');
|
|
1010
|
+
assert.is(toolCallMsg.model, 'claude-haiku-4-5-20251001');
|
|
1011
|
+
assert.is(toolCallMsg.providerName, 'test');
|
|
1012
|
+
driver.dispose();
|
|
1013
|
+
}));
|
|
1014
|
+
modelAttr('picks up a model swapped behind a stable name', () => __awaiter(void 0, void 0, void 0, function* () {
|
|
1015
|
+
var _a, _b, _c;
|
|
1016
|
+
const before = modelProvider('gemini-2.5-flash-lite', [{ role: 'assistant', content: 'A' }]);
|
|
1017
|
+
const after = modelProvider('gemini-2.5-pro', [{ role: 'assistant', content: 'B' }]);
|
|
1018
|
+
const registry = makeObservableRegistry(before);
|
|
1019
|
+
const driver = makeDriverWithRegistry(agent({ name: 'tiered', provider: 'high' }), registry);
|
|
1020
|
+
yield driver.sendMessage('first');
|
|
1021
|
+
registry.swap(after); // notify → model cache cleared, next turn re-resolves
|
|
1022
|
+
yield driver.sendMessage('second');
|
|
1023
|
+
const replies = driver.getHistory().filter((m) => m.role === 'assistant');
|
|
1024
|
+
assert.is((_a = replies.at(-2)) === null || _a === void 0 ? void 0 : _a.model, 'gemini-2.5-flash-lite', 'first turn keeps the original model');
|
|
1025
|
+
assert.is((_b = replies.at(-1)) === null || _b === void 0 ? void 0 : _b.model, 'gemini-2.5-pro', 'after the swap the new model is stamped');
|
|
1026
|
+
// The tier name ('high') never changed across the swap — only the model behind it.
|
|
1027
|
+
assert.is((_c = replies.at(-1)) === null || _c === void 0 ? void 0 : _c.providerName, 'high');
|
|
1028
|
+
driver.dispose();
|
|
1029
|
+
}));
|
|
1030
|
+
modelAttr('omits the model key entirely when the provider reports no status', () => __awaiter(void 0, void 0, void 0, function* () {
|
|
1031
|
+
const provider = modelProvider(undefined, [{ role: 'assistant', content: 'no status here' }]);
|
|
1032
|
+
const driver = makeDriver(agent({ name: 'plain' }), provider);
|
|
1033
|
+
yield driver.sendMessage('hi');
|
|
1034
|
+
const reply = driver.getHistory().find((m) => m.role === 'assistant');
|
|
1035
|
+
assert.ok(reply, 'assistant reply present');
|
|
1036
|
+
// No getStatus → no model. The key is left off, not set to undefined, so the
|
|
1037
|
+
// exported log carries no dead `model` line for this message.
|
|
1038
|
+
assert.not.ok('model' in reply, 'model key is absent, not present-as-undefined');
|
|
1039
|
+
assert.is(reply.providerName, 'test', 'provider name is still recorded');
|
|
1040
|
+
driver.dispose();
|
|
1041
|
+
}));
|
|
1042
|
+
modelAttr.run();
|
package/dist/esm/main/main.js
CHANGED
|
@@ -22,7 +22,7 @@
|
|
|
22
22
|
// =============================================================================
|
|
23
23
|
var FoundationAiAssistant_1;
|
|
24
24
|
import { __awaiter, __decorate, __rest } from "tslib";
|
|
25
|
-
import { AIProviderRegistry } from '@genesislcap/foundation-ai';
|
|
25
|
+
import { AIProviderRegistry, isObservableAIProviderRegistry } from '@genesislcap/foundation-ai';
|
|
26
26
|
import { avoidTreeShaking } from '@genesislcap/foundation-utils';
|
|
27
27
|
import { customElement, html, GenesisElement, observable, volatile, attr, } from '@genesislcap/web-core';
|
|
28
28
|
import DOMPurify from 'dompurify';
|
|
@@ -900,7 +900,7 @@ let FoundationAiAssistant = FoundationAiAssistant_1 = class FoundationAiAssistan
|
|
|
900
900
|
this.driverCleanup = undefined;
|
|
901
901
|
}
|
|
902
902
|
connectedCallback() {
|
|
903
|
-
var _a, _b, _c, _d, _e, _f, _j, _k, _l;
|
|
903
|
+
var _a, _b, _c, _d, _e, _f, _j, _k, _l, _m;
|
|
904
904
|
// Initialise the store reference BEFORE super.connectedCallback() so that
|
|
905
905
|
// the first FAST render has access to the store. The store Proxy calls
|
|
906
906
|
// Observable.track(observableStore, sliceName) whenever a slice is read,
|
|
@@ -969,6 +969,21 @@ let FoundationAiAssistant = FoundationAiAssistant_1 = class FoundationAiAssistan
|
|
|
969
969
|
this.fetchSuggestions();
|
|
970
970
|
void this.resolveContextLimit();
|
|
971
971
|
void this.loadProviderStatuses();
|
|
972
|
+
// When the host registered an observable registry (runtime provider
|
|
973
|
+
// switching), refresh the displayed model/limit and the provider list the
|
|
974
|
+
// instant its contents change — so the header reflects the new provider
|
|
975
|
+
// immediately on switch, not only when the next turn re-emits
|
|
976
|
+
// `provider-changed`. Feature-detected: a no-op for immutable registries.
|
|
977
|
+
// Re-subscribed per connect (docking/popout remounts); balanced in
|
|
978
|
+
// disconnectedCallback.
|
|
979
|
+
(_k = this.unsubProviderRegistry) === null || _k === void 0 ? void 0 : _k.call(this);
|
|
980
|
+
this.unsubProviderRegistry = undefined;
|
|
981
|
+
if (isObservableAIProviderRegistry(this.providerRegistry)) {
|
|
982
|
+
this.unsubProviderRegistry = this.providerRegistry.subscribe(() => {
|
|
983
|
+
void this.resolveContextLimit();
|
|
984
|
+
void this.loadProviderStatuses();
|
|
985
|
+
});
|
|
986
|
+
}
|
|
972
987
|
if (this.messagesEl) {
|
|
973
988
|
this._scrollListener = () => {
|
|
974
989
|
this._userScrolledAway =
|
|
@@ -992,18 +1007,20 @@ let FoundationAiAssistant = FoundationAiAssistant_1 = class FoundationAiAssistan
|
|
|
992
1007
|
restoredMessages: this.messages.length,
|
|
993
1008
|
driver: driverExisted ? 'reused' : 'created',
|
|
994
1009
|
driverKind: this.driver instanceof OrchestratingDriver ? 'orchestrating' : 'chat',
|
|
995
|
-
driverBusy: (
|
|
1010
|
+
driverBusy: (_m = (_l = this.driver) === null || _l === void 0 ? void 0 : _l.isBusy()) !== null && _m !== void 0 ? _m : false,
|
|
996
1011
|
});
|
|
997
1012
|
}
|
|
998
1013
|
disconnectedCallback() {
|
|
999
|
-
var _a, _b, _c, _d;
|
|
1014
|
+
var _a, _b, _c, _d, _e;
|
|
1000
1015
|
super.disconnectedCallback();
|
|
1001
1016
|
this.stopLoadingTimer();
|
|
1002
1017
|
this.state = 'idle';
|
|
1003
1018
|
this.unwireDriver();
|
|
1004
1019
|
(_a = this.unsubBus) === null || _a === void 0 ? void 0 : _a.call(this);
|
|
1005
1020
|
this.unsubBus = undefined;
|
|
1006
|
-
(_b = this.
|
|
1021
|
+
(_b = this.unsubProviderRegistry) === null || _b === void 0 ? void 0 : _b.call(this);
|
|
1022
|
+
this.unsubProviderRegistry = undefined;
|
|
1023
|
+
(_c = this._executionCompletionUnsub) === null || _c === void 0 ? void 0 : _c.call(this);
|
|
1007
1024
|
this._executionCompletionUnsub = undefined;
|
|
1008
1025
|
if (this.messagesEl && this._scrollListener) {
|
|
1009
1026
|
this.messagesEl.removeEventListener('scroll', this._scrollListener);
|
|
@@ -1019,7 +1036,7 @@ let FoundationAiAssistant = FoundationAiAssistant_1 = class FoundationAiAssistan
|
|
|
1019
1036
|
this._agentPickerToggle.finalize();
|
|
1020
1037
|
this._settingsToggle.finalize();
|
|
1021
1038
|
// Capture before clearing — `wasBusy` reads the driver, which is dropped below.
|
|
1022
|
-
this.logMeta('assistant.disconnected', { wasBusy: (
|
|
1039
|
+
this.logMeta('assistant.disconnected', { wasBusy: (_e = (_d = this.driver) === null || _d === void 0 ? void 0 : _d.isBusy()) !== null && _e !== void 0 ? _e : false });
|
|
1023
1040
|
// Clear local references only — driver and store stay in their registries.
|
|
1024
1041
|
this.driver = undefined;
|
|
1025
1042
|
this._sessionRef = undefined;
|
|
@@ -138,7 +138,7 @@ export const DEBUG_LOG_README = [
|
|
|
138
138
|
'This is an exported debug log for the Genesis AI assistant. Read it top-to-bottom.',
|
|
139
139
|
'`timeline` is the entire session as one array, already sorted chronologically by `timestamp` (ISO 8601). Every entry has a `kind`.',
|
|
140
140
|
'Timestamps are millisecond-resolution; entries that share the same millisecond are ordered by a fixed kind rank (event, then turn, then message), which is a heuristic and may not reflect exact causal order within that millisecond — e.g. a user message and the turn it triggered, or a final assistant message and its turn.end event, can appear in either order depending on whether they landed in the same millisecond. Read the logical structure of a turn rather than over-interpreting the micro-ordering of co-timestamped entries of different kinds.',
|
|
141
|
-
"kind:'message' — the conversation. `role` is user/assistant/tool/system-event/synthetic-user; `agentName` says which agent produced it; `toolCalls`/`toolResult`/`interaction` carry tool and widget activity; `inputTokens`/`outputTokens`/`cost` are per-message LLM usage, and `externalCostUsd` is any non-LLM cost a widget reported for its own external service calls (folded into the session cost total alongside `cost`). A 'synthetic-user' message is a display-only echo of an interaction outcome (e.g. the answer a widget reported): it renders on the user's side of the chat and `agentName` is the agent that created it, but it is never sent to the LLM — so it has no matching 'turn' and the model learns the outcome only from the corresponding tool result.",
|
|
141
|
+
"kind:'message' — the conversation. `role` is user/assistant/tool/system-event/synthetic-user; `agentName` says which agent produced it; `toolCalls`/`toolResult`/`interaction` carry tool and widget activity; `inputTokens`/`outputTokens`/`cost` are per-message LLM usage, and `externalCostUsd` is any non-LLM cost a widget reported for its own external service calls (folded into the session cost total alongside `cost`). On model-produced assistant messages, `model` is the concrete model id that generated it (e.g. 'gemini-2.5-flash-lite') and `providerName` is the registry slot it resolved under (e.g. a tier name like 'high'/'low', or the default); together they attribute the message — and any tool calls it carries — to an exact model even across a mid-session vendor/tier switch, where one slot name can map to different models before and after the switch. Both are undefined on any entry that is NOT an LLM response: non-assistant roles (user/tool/system-event) and 'synthetic-user' echoes; assistant interaction/widget entries (empty content carrying an `interaction` — a rendered widget, not a model turn); driver-authored assistant fallbacks (the timeout, repeated-malformed-call, and empty-response apology messages); and messages restored from a session persisted before these fields existed. One partial case: on a genuine model turn whose provider exposes no `getStatus` (or reports no model), `providerName` is still set but `model` alone is undefined. A 'synthetic-user' message is a display-only echo of an interaction outcome (e.g. the answer a widget reported): it renders on the user's side of the chat and `agentName` is the agent that created it, but it is never sent to the LLM — so it has no matching 'turn' and the model learns the outcome only from the corresponding tool result.",
|
|
142
142
|
"kind:'turn' — one LLM call. `turnIndex` is a string: a top-level turn is the bare counter ('0', '1', …); a sub-agent's turns are numbered under the parent turn that activated them ('3-1', '3-2', …, and a nested sub-agent contributes '3-2-1', …), and `agentName` names the agent that ran the turn. `systemPrompt` and `toolNames` are what the model saw. A systemPrompt of '<repeated — identical to turn N>' was byte-identical to turn N and de-duplicated; the full prompt is shown whenever it changes (often because a stateful agent advanced), so prompt evolution is visible.",
|
|
143
143
|
"kind:'turn'.`agentSnapshot` — the active agent's own view of its internal state, captured at that turn. An agent opts into this by exposing a `getDebugSnapshot()` that returns JSON-serializable per-state info; stateful/flow agents wire it automatically, so you can watch a flow advance turn-by-turn (e.g. current step, cursor, collected fields, pending changes). Absent for agents that don't expose one.",
|
|
144
144
|
"kind:'event' — a meta/lifecycle event. `type` names it (see below); `detail` carries structured data. `detail.placement` is the emitting UI instance: 'bubble' (collapsed), 'panel' (popped-out), or 'standalone'.",
|
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.461.1",
|
|
4
|
+
"version": "14.461.2-GENC-1346.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.461.1",
|
|
68
|
-
"@genesislcap/genx": "14.461.1",
|
|
69
|
-
"@genesislcap/rollup-builder": "14.461.1",
|
|
70
|
-
"@genesislcap/ts-builder": "14.461.1",
|
|
71
|
-
"@genesislcap/uvu-playwright-builder": "14.461.1",
|
|
72
|
-
"@genesislcap/vite-builder": "14.461.1",
|
|
73
|
-
"@genesislcap/webpack-builder": "14.461.1",
|
|
67
|
+
"@genesislcap/foundation-testing": "14.461.2-GENC-1346.1",
|
|
68
|
+
"@genesislcap/genx": "14.461.2-GENC-1346.1",
|
|
69
|
+
"@genesislcap/rollup-builder": "14.461.2-GENC-1346.1",
|
|
70
|
+
"@genesislcap/ts-builder": "14.461.2-GENC-1346.1",
|
|
71
|
+
"@genesislcap/uvu-playwright-builder": "14.461.2-GENC-1346.1",
|
|
72
|
+
"@genesislcap/vite-builder": "14.461.2-GENC-1346.1",
|
|
73
|
+
"@genesislcap/webpack-builder": "14.461.2-GENC-1346.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.461.1",
|
|
79
|
-
"@genesislcap/foundation-logger": "14.461.1",
|
|
80
|
-
"@genesislcap/foundation-redux": "14.461.1",
|
|
81
|
-
"@genesislcap/foundation-ui": "14.461.1",
|
|
82
|
-
"@genesislcap/foundation-utils": "14.461.1",
|
|
83
|
-
"@genesislcap/rapid-design-system": "14.461.1",
|
|
84
|
-
"@genesislcap/web-core": "14.461.1",
|
|
78
|
+
"@genesislcap/foundation-ai": "14.461.2-GENC-1346.1",
|
|
79
|
+
"@genesislcap/foundation-logger": "14.461.2-GENC-1346.1",
|
|
80
|
+
"@genesislcap/foundation-redux": "14.461.2-GENC-1346.1",
|
|
81
|
+
"@genesislcap/foundation-ui": "14.461.2-GENC-1346.1",
|
|
82
|
+
"@genesislcap/foundation-utils": "14.461.2-GENC-1346.1",
|
|
83
|
+
"@genesislcap/rapid-design-system": "14.461.2-GENC-1346.1",
|
|
84
|
+
"@genesislcap/web-core": "14.461.2-GENC-1346.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": "0729266a735c8d8d74192119552f0774c6038436"
|
|
97
97
|
}
|
|
@@ -1187,3 +1187,243 @@ interactionCost(
|
|
|
1187
1187
|
);
|
|
1188
1188
|
|
|
1189
1189
|
interactionCost.run();
|
|
1190
|
+
|
|
1191
|
+
// ---------------------------------------------------------------------------
|
|
1192
|
+
// observable provider registry — runtime provider switching (GENC-1346)
|
|
1193
|
+
//
|
|
1194
|
+
// When the host registers an observable registry and swaps providers at
|
|
1195
|
+
// runtime, the driver must drop its memoised lookups so the next turn resolves
|
|
1196
|
+
// the new provider, re-emit `provider-changed` even when the resolved *name* is
|
|
1197
|
+
// unchanged, and (critically) never leak its subscription — including for child
|
|
1198
|
+
// sub-agent drivers that complete normally.
|
|
1199
|
+
// ---------------------------------------------------------------------------
|
|
1200
|
+
|
|
1201
|
+
interface ObservableTestRegistry extends AIProviderRegistry {
|
|
1202
|
+
subscribe(listener: () => void): () => void;
|
|
1203
|
+
/** Swap the provider returned by `get`/`default` and notify subscribers. */
|
|
1204
|
+
swap(provider: AIProvider): void;
|
|
1205
|
+
/** Live subscriber count — lets tests assert there's no listener leak. */
|
|
1206
|
+
listenerCount(): number;
|
|
1207
|
+
}
|
|
1208
|
+
|
|
1209
|
+
/**
|
|
1210
|
+
* A single-name (`'high'`) observable registry. `get`/`default` always return
|
|
1211
|
+
* the current provider, so swapping it mid-session models a same-name vendor
|
|
1212
|
+
* switch (the tier name stays `'high'`, the provider underneath changes).
|
|
1213
|
+
*/
|
|
1214
|
+
const makeObservableRegistry = (initial: AIProvider): ObservableTestRegistry => {
|
|
1215
|
+
let current = initial;
|
|
1216
|
+
const listeners = new Set<() => void>();
|
|
1217
|
+
return {
|
|
1218
|
+
get: () => current,
|
|
1219
|
+
default: () => current,
|
|
1220
|
+
defaultName: () => 'high',
|
|
1221
|
+
names: () => ['high'],
|
|
1222
|
+
getStatus: async () => null,
|
|
1223
|
+
listStatuses: async () => [],
|
|
1224
|
+
subscribe(listener: () => void) {
|
|
1225
|
+
listeners.add(listener);
|
|
1226
|
+
return () => {
|
|
1227
|
+
listeners.delete(listener);
|
|
1228
|
+
};
|
|
1229
|
+
},
|
|
1230
|
+
swap(provider: AIProvider) {
|
|
1231
|
+
current = provider;
|
|
1232
|
+
for (const l of Array.from(listeners)) l();
|
|
1233
|
+
},
|
|
1234
|
+
listenerCount: () => listeners.size,
|
|
1235
|
+
};
|
|
1236
|
+
};
|
|
1237
|
+
|
|
1238
|
+
const makeDriverWithRegistry = (config: AgentConfig, registry: AIProviderRegistry): ChatDriver => {
|
|
1239
|
+
const driver = new ChatDriver(registry, {}, [], undefined, undefined, 50, 5, undefined, '');
|
|
1240
|
+
driver.applyAgent(config);
|
|
1241
|
+
return driver;
|
|
1242
|
+
};
|
|
1243
|
+
|
|
1244
|
+
const observable = createLogicSuite('ChatDriver observable provider registry');
|
|
1245
|
+
|
|
1246
|
+
observable.after(() => {
|
|
1247
|
+
agenticActivityBus.close();
|
|
1248
|
+
});
|
|
1249
|
+
|
|
1250
|
+
observable(
|
|
1251
|
+
'a registry change clears the resolved-provider cache so the next turn uses the new provider',
|
|
1252
|
+
async () => {
|
|
1253
|
+
const providerA = scriptedProvider([{ role: 'assistant', content: 'A' }]);
|
|
1254
|
+
const providerB = scriptedProvider([{ role: 'assistant', content: 'B' }]);
|
|
1255
|
+
const registry = makeObservableRegistry(providerA);
|
|
1256
|
+
// A static provider name means lookups go through `resolvedProviderCache` —
|
|
1257
|
+
// the cache that must self-invalidate on a registry change.
|
|
1258
|
+
const driver = makeDriverWithRegistry(agent({ name: 'tiered', provider: 'high' }), registry);
|
|
1259
|
+
|
|
1260
|
+
await driver.sendMessage('first');
|
|
1261
|
+
assert.is(providerA.advertisedPerCall.length, 1, 'turn 1 resolves the original provider');
|
|
1262
|
+
assert.is(providerB.advertisedPerCall.length, 0);
|
|
1263
|
+
|
|
1264
|
+
registry.swap(providerB); // notify → cache cleared
|
|
1265
|
+
|
|
1266
|
+
await driver.sendMessage('second');
|
|
1267
|
+
assert.is(providerB.advertisedPerCall.length, 1, 'turn 2 resolves the swapped-in provider');
|
|
1268
|
+
assert.is(providerA.advertisedPerCall.length, 1, 'the stale provider is not reused');
|
|
1269
|
+
|
|
1270
|
+
driver.dispose();
|
|
1271
|
+
},
|
|
1272
|
+
);
|
|
1273
|
+
|
|
1274
|
+
observable('re-emits provider-changed on a same-name swap', async () => {
|
|
1275
|
+
const providerA = scriptedProvider([{ role: 'assistant', content: 'A' }]);
|
|
1276
|
+
const providerB = scriptedProvider([{ role: 'assistant', content: 'B' }]);
|
|
1277
|
+
const registry = makeObservableRegistry(providerA);
|
|
1278
|
+
const driver = makeDriverWithRegistry(agent({ name: 'tiered', provider: 'high' }), registry);
|
|
1279
|
+
|
|
1280
|
+
const names: string[] = [];
|
|
1281
|
+
driver.addEventListener('provider-changed', (e) => {
|
|
1282
|
+
names.push((e as CustomEvent<{ name: string }>).detail.name);
|
|
1283
|
+
});
|
|
1284
|
+
|
|
1285
|
+
await driver.sendMessage('first');
|
|
1286
|
+
registry.swap(providerB);
|
|
1287
|
+
await driver.sendMessage('second');
|
|
1288
|
+
|
|
1289
|
+
// The resolved name ('high') never changes, but the swap resets the
|
|
1290
|
+
// last-dispatched name so the cog can refresh — two events, not one.
|
|
1291
|
+
assert.equal(names, ['high', 'high']);
|
|
1292
|
+
|
|
1293
|
+
driver.dispose();
|
|
1294
|
+
});
|
|
1295
|
+
|
|
1296
|
+
observable('a non-observable registry is a no-op — turn runs, dispose does not throw', async () => {
|
|
1297
|
+
const provider = scriptedProvider([{ role: 'assistant', content: 'hi' }]);
|
|
1298
|
+
const driver = makeDriverWithRegistry(agent({ name: 'plain' }), makeRegistry(provider));
|
|
1299
|
+
await driver.sendMessage('go');
|
|
1300
|
+
assert.is(provider.advertisedPerCall.length, 1);
|
|
1301
|
+
driver.dispose(); // no subscription was wired — must still be safe
|
|
1302
|
+
});
|
|
1303
|
+
|
|
1304
|
+
observable('a child sub-agent driver unsubscribes on completion — no listener leak', async () => {
|
|
1305
|
+
const provider = scriptedProvider([callsTool('delegate', 'd1'), callsTool('finish', 'f1')]);
|
|
1306
|
+
const registry = makeObservableRegistry(provider);
|
|
1307
|
+
const parent = delegatingParent(completingWorker({ ok: true }), () => {});
|
|
1308
|
+
const driver = makeDriverWithRegistry(parent, registry);
|
|
1309
|
+
|
|
1310
|
+
assert.is(registry.listenerCount(), 1, 'the parent driver subscribed on construction');
|
|
1311
|
+
|
|
1312
|
+
await driver.sendMessage('go');
|
|
1313
|
+
// The child subscribed during the run; if it didn't clean up on its (normal)
|
|
1314
|
+
// completion the registry would now hold two listeners.
|
|
1315
|
+
assert.is(registry.listenerCount(), 1, 'the completed child unsubscribed');
|
|
1316
|
+
|
|
1317
|
+
driver.dispose();
|
|
1318
|
+
assert.is(registry.listenerCount(), 0, 'the parent unsubscribed on dispose');
|
|
1319
|
+
});
|
|
1320
|
+
|
|
1321
|
+
observable.run();
|
|
1322
|
+
|
|
1323
|
+
// ---------------------------------------------------------------------------
|
|
1324
|
+
// per-message model attribution (GENC-1346)
|
|
1325
|
+
//
|
|
1326
|
+
// Each model-produced assistant message carries `model` (the concrete model id
|
|
1327
|
+
// the active provider's getStatus reports) and `providerName` (the registry slot
|
|
1328
|
+
// it resolved under), so the exported debug log shows which model produced each
|
|
1329
|
+
// message — and, since tool calls ride on the assistant message, each tool call.
|
|
1330
|
+
// ---------------------------------------------------------------------------
|
|
1331
|
+
|
|
1332
|
+
/** A provider that replays scripted replies and reports `model` via getStatus
|
|
1333
|
+
* (omitted entirely when `model` is undefined, to model a provider with no
|
|
1334
|
+
* status). */
|
|
1335
|
+
const modelProvider = (model: string | undefined, responses: ChatMessage[]): AIProvider => {
|
|
1336
|
+
const queue = [...responses];
|
|
1337
|
+
const provider: AIProvider = {
|
|
1338
|
+
chat: async (): Promise<ChatMessage> => queue.shift() ?? { role: 'assistant', content: 'done' },
|
|
1339
|
+
};
|
|
1340
|
+
if (model !== undefined) {
|
|
1341
|
+
provider.getStatus = async () => ({ provider: 'gemini', model });
|
|
1342
|
+
}
|
|
1343
|
+
return provider;
|
|
1344
|
+
};
|
|
1345
|
+
|
|
1346
|
+
const modelAttr = createLogicSuite('ChatDriver per-message model attribution');
|
|
1347
|
+
|
|
1348
|
+
modelAttr.after(() => {
|
|
1349
|
+
agenticActivityBus.close();
|
|
1350
|
+
});
|
|
1351
|
+
|
|
1352
|
+
modelAttr('stamps the resolved model and registry name onto an assistant reply', async () => {
|
|
1353
|
+
const provider = modelProvider('gemini-2.5-flash-lite', [
|
|
1354
|
+
{ role: 'assistant', content: 'hi there' },
|
|
1355
|
+
]);
|
|
1356
|
+
const driver = makeDriver(agent({ name: 'plain' }), provider);
|
|
1357
|
+
|
|
1358
|
+
await driver.sendMessage('hello');
|
|
1359
|
+
|
|
1360
|
+
const reply = driver.getHistory().find((m) => m.role === 'assistant');
|
|
1361
|
+
assert.ok(reply, 'assistant reply present');
|
|
1362
|
+
assert.is(reply!.model, 'gemini-2.5-flash-lite', 'model id read from provider getStatus');
|
|
1363
|
+
// makeRegistry registers a single provider under the name 'test'.
|
|
1364
|
+
assert.is(reply!.providerName, 'test', 'registry slot the turn resolved under');
|
|
1365
|
+
|
|
1366
|
+
driver.dispose();
|
|
1367
|
+
});
|
|
1368
|
+
|
|
1369
|
+
modelAttr(
|
|
1370
|
+
'attributes a tool-calling assistant message (and so its tool calls) to the model',
|
|
1371
|
+
async () => {
|
|
1372
|
+
const provider = modelProvider('claude-haiku-4-5-20251001', [
|
|
1373
|
+
callsTool('noop', 't1'),
|
|
1374
|
+
{ role: 'assistant', content: 'finished' },
|
|
1375
|
+
]);
|
|
1376
|
+
const config = agent({
|
|
1377
|
+
name: 'withTool',
|
|
1378
|
+
toolDefinitions: [def('noop')],
|
|
1379
|
+
toolHandlers: { noop: async () => 'ok' },
|
|
1380
|
+
});
|
|
1381
|
+
const driver = makeDriver(config, provider);
|
|
1382
|
+
|
|
1383
|
+
await driver.sendMessage('go');
|
|
1384
|
+
|
|
1385
|
+
const toolCallMsg = driver.getHistory().find((m) => (m.toolCalls?.length ?? 0) > 0);
|
|
1386
|
+
assert.ok(toolCallMsg, 'an assistant message with tool calls is present');
|
|
1387
|
+
assert.is(toolCallMsg!.model, 'claude-haiku-4-5-20251001');
|
|
1388
|
+
assert.is(toolCallMsg!.providerName, 'test');
|
|
1389
|
+
|
|
1390
|
+
driver.dispose();
|
|
1391
|
+
},
|
|
1392
|
+
);
|
|
1393
|
+
|
|
1394
|
+
modelAttr('picks up a model swapped behind a stable name', async () => {
|
|
1395
|
+
const before = modelProvider('gemini-2.5-flash-lite', [{ role: 'assistant', content: 'A' }]);
|
|
1396
|
+
const after = modelProvider('gemini-2.5-pro', [{ role: 'assistant', content: 'B' }]);
|
|
1397
|
+
const registry = makeObservableRegistry(before);
|
|
1398
|
+
const driver = makeDriverWithRegistry(agent({ name: 'tiered', provider: 'high' }), registry);
|
|
1399
|
+
|
|
1400
|
+
await driver.sendMessage('first');
|
|
1401
|
+
registry.swap(after); // notify → model cache cleared, next turn re-resolves
|
|
1402
|
+
await driver.sendMessage('second');
|
|
1403
|
+
|
|
1404
|
+
const replies = driver.getHistory().filter((m) => m.role === 'assistant');
|
|
1405
|
+
assert.is(replies.at(-2)?.model, 'gemini-2.5-flash-lite', 'first turn keeps the original model');
|
|
1406
|
+
assert.is(replies.at(-1)?.model, 'gemini-2.5-pro', 'after the swap the new model is stamped');
|
|
1407
|
+
// The tier name ('high') never changed across the swap — only the model behind it.
|
|
1408
|
+
assert.is(replies.at(-1)?.providerName, 'high');
|
|
1409
|
+
|
|
1410
|
+
driver.dispose();
|
|
1411
|
+
});
|
|
1412
|
+
|
|
1413
|
+
modelAttr('omits the model key entirely when the provider reports no status', async () => {
|
|
1414
|
+
const provider = modelProvider(undefined, [{ role: 'assistant', content: 'no status here' }]);
|
|
1415
|
+
const driver = makeDriver(agent({ name: 'plain' }), provider);
|
|
1416
|
+
|
|
1417
|
+
await driver.sendMessage('hi');
|
|
1418
|
+
|
|
1419
|
+
const reply = driver.getHistory().find((m) => m.role === 'assistant');
|
|
1420
|
+
assert.ok(reply, 'assistant reply present');
|
|
1421
|
+
// No getStatus → no model. The key is left off, not set to undefined, so the
|
|
1422
|
+
// exported log carries no dead `model` line for this message.
|
|
1423
|
+
assert.not.ok('model' in reply!, 'model key is absent, not present-as-undefined');
|
|
1424
|
+
assert.is(reply!.providerName, 'test', 'provider name is still recorded');
|
|
1425
|
+
|
|
1426
|
+
driver.dispose();
|
|
1427
|
+
});
|
|
1428
|
+
|
|
1429
|
+
modelAttr.run();
|
|
@@ -14,7 +14,10 @@ import type {
|
|
|
14
14
|
SubAgentFailureReason,
|
|
15
15
|
SubAgentRequestOptions,
|
|
16
16
|
} from '@genesislcap/foundation-ai';
|
|
17
|
-
import {
|
|
17
|
+
import {
|
|
18
|
+
isObservableAIProviderRegistry,
|
|
19
|
+
MalformedFunctionCallError,
|
|
20
|
+
} from '@genesislcap/foundation-ai';
|
|
18
21
|
import { agenticActivityBus } from '../../channel/ai-activity-bus';
|
|
19
22
|
import type {
|
|
20
23
|
AgentConfig,
|
|
@@ -339,6 +342,27 @@ export class ChatDriver extends EventTarget implements AiDriver {
|
|
|
339
342
|
private lastResolvedProviderName?: string;
|
|
340
343
|
/** Last dispatched `provider-changed` name; avoids duplicate events on stable turns. */
|
|
341
344
|
private lastDispatchedProviderName?: string;
|
|
345
|
+
/**
|
|
346
|
+
* Concrete model id of the provider resolved for the current turn, read from
|
|
347
|
+
* its `getStatus()` and stamped onto the resulting assistant message so the
|
|
348
|
+
* debug log carries per-message model lineage. Re-resolved each turn in
|
|
349
|
+
* `resolveProviderForTurn`.
|
|
350
|
+
*/
|
|
351
|
+
private lastResolvedModel?: string;
|
|
352
|
+
/**
|
|
353
|
+
* Memoised `name → model id` lookups, so the per-turn model resolution doesn't
|
|
354
|
+
* re-await `getStatus()` every turn. Cleared (with `resolvedProviderCache`) on
|
|
355
|
+
* an observable-registry change, so a model swapped behind a stable name is
|
|
356
|
+
* picked up on the next turn.
|
|
357
|
+
*/
|
|
358
|
+
private resolvedModelCache = new Map<string, string | undefined>();
|
|
359
|
+
/**
|
|
360
|
+
* Unsubscribe handle for the provider-registry change listener (only set when
|
|
361
|
+
* the injected registry is observable). Called in `dispose` so the long-lived
|
|
362
|
+
* registry doesn't retain this driver — see the constructor and the sub-agent
|
|
363
|
+
* teardown in `runSubAgent`.
|
|
364
|
+
*/
|
|
365
|
+
private unsubscribeRegistry?: () => void;
|
|
342
366
|
|
|
343
367
|
constructor(
|
|
344
368
|
private readonly providerRegistry: AIProviderRegistry,
|
|
@@ -371,6 +395,24 @@ export class ChatDriver extends EventTarget implements AiDriver {
|
|
|
371
395
|
this.primerHistory = primerHistory;
|
|
372
396
|
this.maxFoldOperations = maxFoldOperations;
|
|
373
397
|
this.maxTurnSnapshots = maxTurnSnapshots;
|
|
398
|
+
// Runtime provider switching: when the host registered an observable
|
|
399
|
+
// registry, drop our memoised provider lookups whenever its mapping/default
|
|
400
|
+
// changes so the next turn re-resolves against the new providers. Resetting
|
|
401
|
+
// `lastDispatchedProviderName` forces the next `resolveProviderForTurn` to
|
|
402
|
+
// re-emit `provider-changed` even when the resolved *name* is unchanged
|
|
403
|
+
// (e.g. a tier name like 'high' kept, but the vendor underneath swapped) —
|
|
404
|
+
// that's what lets status UI refresh on a same-name switch. Feature-detect
|
|
405
|
+
// means immutable/empty registries are a no-op. Child sub-agent drivers get
|
|
406
|
+
// the same registry and so subscribe here too; each cleans up in `dispose`.
|
|
407
|
+
if (isObservableAIProviderRegistry(this.providerRegistry)) {
|
|
408
|
+
this.unsubscribeRegistry = this.providerRegistry.subscribe(() => {
|
|
409
|
+
this.resolvedProviderCache.clear();
|
|
410
|
+
this.resolvedModelCache.clear();
|
|
411
|
+
this.lastResolvedProviderName = undefined;
|
|
412
|
+
this.lastResolvedModel = undefined;
|
|
413
|
+
this.lastDispatchedProviderName = undefined;
|
|
414
|
+
});
|
|
415
|
+
}
|
|
374
416
|
}
|
|
375
417
|
|
|
376
418
|
/**
|
|
@@ -390,6 +432,11 @@ export class ChatDriver extends EventTarget implements AiDriver {
|
|
|
390
432
|
*/
|
|
391
433
|
dispose(): void {
|
|
392
434
|
this.lifecycleController.abort(new DOMException('AI assistant driver disposed', 'AbortError'));
|
|
435
|
+
// Detach from the provider registry so the long-lived registry doesn't pin
|
|
436
|
+
// this driver (and its closure) after teardown. Guard-cleared so a second
|
|
437
|
+
// dispose is a no-op, matching this method's idempotent contract.
|
|
438
|
+
this.unsubscribeRegistry?.();
|
|
439
|
+
this.unsubscribeRegistry = undefined;
|
|
393
440
|
}
|
|
394
441
|
|
|
395
442
|
/**
|
|
@@ -540,6 +587,7 @@ export class ChatDriver extends EventTarget implements AiDriver {
|
|
|
540
587
|
resolvedName = name;
|
|
541
588
|
}
|
|
542
589
|
this.lastResolvedProviderName = resolvedName;
|
|
590
|
+
this.lastResolvedModel = await this.resolveModelForProvider(resolvedName, provider);
|
|
543
591
|
if (resolvedName !== this.lastDispatchedProviderName) {
|
|
544
592
|
this.lastDispatchedProviderName = resolvedName;
|
|
545
593
|
recordMetaEvent(this.sessionKey, 'provider.selected', {
|
|
@@ -553,6 +601,27 @@ export class ChatDriver extends EventTarget implements AiDriver {
|
|
|
553
601
|
return provider;
|
|
554
602
|
}
|
|
555
603
|
|
|
604
|
+
/**
|
|
605
|
+
* Resolve the concrete model id for a provider via its optional `getStatus()`,
|
|
606
|
+
* memoised by registry name. Used to stamp `model` onto outgoing messages.
|
|
607
|
+
* Best-effort: a provider without `getStatus`, a null status, or a throw all
|
|
608
|
+
* yield `undefined` — model attribution is diagnostic, never load-bearing.
|
|
609
|
+
*/
|
|
610
|
+
private async resolveModelForProvider(
|
|
611
|
+
name: string,
|
|
612
|
+
provider: AIProvider,
|
|
613
|
+
): Promise<string | undefined> {
|
|
614
|
+
if (this.resolvedModelCache.has(name)) return this.resolvedModelCache.get(name);
|
|
615
|
+
let model: string | undefined;
|
|
616
|
+
try {
|
|
617
|
+
model = (await provider.getStatus?.())?.model;
|
|
618
|
+
} catch {
|
|
619
|
+
model = undefined;
|
|
620
|
+
}
|
|
621
|
+
this.resolvedModelCache.set(name, model);
|
|
622
|
+
return model;
|
|
623
|
+
}
|
|
624
|
+
|
|
556
625
|
/**
|
|
557
626
|
* Resolve a per-turn config input that is either a static value or a function
|
|
558
627
|
* of the turn context — the value-or-resolver shape shared by `provider`,
|
|
@@ -1262,6 +1331,13 @@ export class ChatDriver extends EventTarget implements AiDriver {
|
|
|
1262
1331
|
this.lifecycleController.signal.removeEventListener('abort', disposeChild);
|
|
1263
1332
|
child.removeEventListener('history-updated', forwardTrace);
|
|
1264
1333
|
child.removeEventListener('provider-changed', forwardProviderChanged);
|
|
1334
|
+
// Tear the child down on every exit path, not just timeout/parent-abort.
|
|
1335
|
+
// A child that completes normally is otherwise never disposed, so its
|
|
1336
|
+
// provider-registry subscription (wired in the ChatDriver constructor)
|
|
1337
|
+
// would leak — the long-lived registry would retain every completed
|
|
1338
|
+
// sub-agent driver. dispose() is idempotent and only aborts the (already
|
|
1339
|
+
// settled) lifecycle, so the snapshot/completion reads below still work.
|
|
1340
|
+
child.dispose();
|
|
1265
1341
|
this.dispatchEvent(new CustomEvent('sub-agent-stop', { detail: lifecycleDetail }));
|
|
1266
1342
|
}
|
|
1267
1343
|
|
|
@@ -1767,6 +1843,22 @@ export class ChatDriver extends EventTarget implements AiDriver {
|
|
|
1767
1843
|
throw e;
|
|
1768
1844
|
}
|
|
1769
1845
|
|
|
1846
|
+
// Attribute the response to the concrete model + registry slot that
|
|
1847
|
+
// produced it (resolved for this turn in `resolveProviderForTurn`). Carried
|
|
1848
|
+
// on the assistant message so the debug-log timeline shows per-message
|
|
1849
|
+
// model lineage — and thus per-tool-call, since tool calls ride on the
|
|
1850
|
+
// assistant message that requests them. Harmless on a response later
|
|
1851
|
+
// discarded as empty/retried; only kept copies reach history.
|
|
1852
|
+
//
|
|
1853
|
+
// Attach each key only when resolved: a provider with no `getStatus` (etc.)
|
|
1854
|
+
// leaves the key off entirely rather than carrying it as `undefined`.
|
|
1855
|
+
// JSON.stringify already drops undefined from the exported log, so this is
|
|
1856
|
+
// chiefly about keeping the in-memory message shape honest.
|
|
1857
|
+
if (this.lastResolvedModel !== undefined) response.model = this.lastResolvedModel;
|
|
1858
|
+
if (this.lastResolvedProviderName !== undefined) {
|
|
1859
|
+
response.providerName = this.lastResolvedProviderName;
|
|
1860
|
+
}
|
|
1861
|
+
|
|
1770
1862
|
const isThinkingStep = response.content && response.toolCalls?.length;
|
|
1771
1863
|
const isEmptyResponse = !response.content?.trim() && !response.toolCalls?.length;
|
|
1772
1864
|
|
package/src/main/main.ts
CHANGED
|
@@ -28,7 +28,7 @@ import type {
|
|
|
28
28
|
ChatInputDuringExecutionMode,
|
|
29
29
|
ChatMessage,
|
|
30
30
|
} from '@genesislcap/foundation-ai';
|
|
31
|
-
import { AIProviderRegistry } from '@genesislcap/foundation-ai';
|
|
31
|
+
import { AIProviderRegistry, isObservableAIProviderRegistry } from '@genesislcap/foundation-ai';
|
|
32
32
|
import { avoidTreeShaking } from '@genesislcap/foundation-utils';
|
|
33
33
|
import {
|
|
34
34
|
customElement,
|
|
@@ -592,6 +592,8 @@ export class FoundationAiAssistant extends GenesisElement {
|
|
|
592
592
|
private driverCleanup?: () => void;
|
|
593
593
|
private loadingTimer: ReturnType<typeof setTimeout> | undefined;
|
|
594
594
|
private unsubBus?: () => void;
|
|
595
|
+
/** Unsubscribe handle for the provider-registry change listener (observable registries only). */
|
|
596
|
+
private unsubProviderRegistry?: () => void;
|
|
595
597
|
private haloStartPublished = false;
|
|
596
598
|
/** Fingerprint of the agents array used to build the current driver. Used by agentsChanged to skip spurious rebuilds. */
|
|
597
599
|
private _driverAgentsKey?: string;
|
|
@@ -1117,6 +1119,21 @@ export class FoundationAiAssistant extends GenesisElement {
|
|
|
1117
1119
|
this.fetchSuggestions();
|
|
1118
1120
|
void this.resolveContextLimit();
|
|
1119
1121
|
void this.loadProviderStatuses();
|
|
1122
|
+
// When the host registered an observable registry (runtime provider
|
|
1123
|
+
// switching), refresh the displayed model/limit and the provider list the
|
|
1124
|
+
// instant its contents change — so the header reflects the new provider
|
|
1125
|
+
// immediately on switch, not only when the next turn re-emits
|
|
1126
|
+
// `provider-changed`. Feature-detected: a no-op for immutable registries.
|
|
1127
|
+
// Re-subscribed per connect (docking/popout remounts); balanced in
|
|
1128
|
+
// disconnectedCallback.
|
|
1129
|
+
this.unsubProviderRegistry?.();
|
|
1130
|
+
this.unsubProviderRegistry = undefined;
|
|
1131
|
+
if (isObservableAIProviderRegistry(this.providerRegistry)) {
|
|
1132
|
+
this.unsubProviderRegistry = this.providerRegistry.subscribe(() => {
|
|
1133
|
+
void this.resolveContextLimit();
|
|
1134
|
+
void this.loadProviderStatuses();
|
|
1135
|
+
});
|
|
1136
|
+
}
|
|
1120
1137
|
if (this.messagesEl) {
|
|
1121
1138
|
this._scrollListener = () => {
|
|
1122
1139
|
this._userScrolledAway =
|
|
@@ -1150,6 +1167,8 @@ export class FoundationAiAssistant extends GenesisElement {
|
|
|
1150
1167
|
this.unwireDriver();
|
|
1151
1168
|
this.unsubBus?.();
|
|
1152
1169
|
this.unsubBus = undefined;
|
|
1170
|
+
this.unsubProviderRegistry?.();
|
|
1171
|
+
this.unsubProviderRegistry = undefined;
|
|
1153
1172
|
this._executionCompletionUnsub?.();
|
|
1154
1173
|
this._executionCompletionUnsub = undefined;
|
|
1155
1174
|
if (this.messagesEl && this._scrollListener) {
|
|
@@ -247,7 +247,7 @@ export const DEBUG_LOG_README: readonly string[] = [
|
|
|
247
247
|
'This is an exported debug log for the Genesis AI assistant. Read it top-to-bottom.',
|
|
248
248
|
'`timeline` is the entire session as one array, already sorted chronologically by `timestamp` (ISO 8601). Every entry has a `kind`.',
|
|
249
249
|
'Timestamps are millisecond-resolution; entries that share the same millisecond are ordered by a fixed kind rank (event, then turn, then message), which is a heuristic and may not reflect exact causal order within that millisecond — e.g. a user message and the turn it triggered, or a final assistant message and its turn.end event, can appear in either order depending on whether they landed in the same millisecond. Read the logical structure of a turn rather than over-interpreting the micro-ordering of co-timestamped entries of different kinds.',
|
|
250
|
-
"kind:'message' — the conversation. `role` is user/assistant/tool/system-event/synthetic-user; `agentName` says which agent produced it; `toolCalls`/`toolResult`/`interaction` carry tool and widget activity; `inputTokens`/`outputTokens`/`cost` are per-message LLM usage, and `externalCostUsd` is any non-LLM cost a widget reported for its own external service calls (folded into the session cost total alongside `cost`). A 'synthetic-user' message is a display-only echo of an interaction outcome (e.g. the answer a widget reported): it renders on the user's side of the chat and `agentName` is the agent that created it, but it is never sent to the LLM — so it has no matching 'turn' and the model learns the outcome only from the corresponding tool result.",
|
|
250
|
+
"kind:'message' — the conversation. `role` is user/assistant/tool/system-event/synthetic-user; `agentName` says which agent produced it; `toolCalls`/`toolResult`/`interaction` carry tool and widget activity; `inputTokens`/`outputTokens`/`cost` are per-message LLM usage, and `externalCostUsd` is any non-LLM cost a widget reported for its own external service calls (folded into the session cost total alongside `cost`). On model-produced assistant messages, `model` is the concrete model id that generated it (e.g. 'gemini-2.5-flash-lite') and `providerName` is the registry slot it resolved under (e.g. a tier name like 'high'/'low', or the default); together they attribute the message — and any tool calls it carries — to an exact model even across a mid-session vendor/tier switch, where one slot name can map to different models before and after the switch. Both are undefined on any entry that is NOT an LLM response: non-assistant roles (user/tool/system-event) and 'synthetic-user' echoes; assistant interaction/widget entries (empty content carrying an `interaction` — a rendered widget, not a model turn); driver-authored assistant fallbacks (the timeout, repeated-malformed-call, and empty-response apology messages); and messages restored from a session persisted before these fields existed. One partial case: on a genuine model turn whose provider exposes no `getStatus` (or reports no model), `providerName` is still set but `model` alone is undefined. A 'synthetic-user' message is a display-only echo of an interaction outcome (e.g. the answer a widget reported): it renders on the user's side of the chat and `agentName` is the agent that created it, but it is never sent to the LLM — so it has no matching 'turn' and the model learns the outcome only from the corresponding tool result.",
|
|
251
251
|
"kind:'turn' — one LLM call. `turnIndex` is a string: a top-level turn is the bare counter ('0', '1', …); a sub-agent's turns are numbered under the parent turn that activated them ('3-1', '3-2', …, and a nested sub-agent contributes '3-2-1', …), and `agentName` names the agent that ran the turn. `systemPrompt` and `toolNames` are what the model saw. A systemPrompt of '<repeated — identical to turn N>' was byte-identical to turn N and de-duplicated; the full prompt is shown whenever it changes (often because a stateful agent advanced), so prompt evolution is visible.",
|
|
252
252
|
"kind:'turn'.`agentSnapshot` — the active agent's own view of its internal state, captured at that turn. An agent opts into this by exposing a `getDebugSnapshot()` that returns JSON-serializable per-state info; stateful/flow agents wire it automatically, so you can watch a flow advance turn-by-turn (e.g. current step, cursor, collected fields, pending changes). Absent for agents that don't expose one.",
|
|
253
253
|
"kind:'event' — a meta/lifecycle event. `type` names it (see below); `detail` carries structured data. `detail.placement` is the emitting UI instance: 'bubble' (collapsed), 'panel' (popped-out), or 'standalone'.",
|