@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.
Files changed (55) hide show
  1. package/dist/ai-assistant.api.json +312 -41
  2. package/dist/ai-assistant.d.ts +105 -8
  3. package/dist/dts/components/ai-driver/ai-driver.d.ts +7 -0
  4. package/dist/dts/components/ai-driver/ai-driver.d.ts.map +1 -1
  5. package/dist/dts/components/chat-driver/chat-driver.d.ts +37 -3
  6. package/dist/dts/components/chat-driver/chat-driver.d.ts.map +1 -1
  7. package/dist/dts/components/orchestrating-driver/orchestrating-driver.d.ts +5 -3
  8. package/dist/dts/components/orchestrating-driver/orchestrating-driver.d.ts.map +1 -1
  9. package/dist/dts/config/config.d.ts +31 -0
  10. package/dist/dts/config/config.d.ts.map +1 -1
  11. package/dist/dts/config/define-stateful-agent.d.ts +9 -0
  12. package/dist/dts/config/define-stateful-agent.d.ts.map +1 -1
  13. package/dist/dts/config/validate-providers.d.ts +25 -0
  14. package/dist/dts/config/validate-providers.d.ts.map +1 -0
  15. package/dist/dts/config/validate-providers.test.d.ts +2 -0
  16. package/dist/dts/config/validate-providers.test.d.ts.map +1 -0
  17. package/dist/dts/main/main.d.ts +17 -5
  18. package/dist/dts/main/main.d.ts.map +1 -1
  19. package/dist/dts/main/main.styles.d.ts.map +1 -1
  20. package/dist/dts/main/main.template.d.ts.map +1 -1
  21. package/dist/dts/state/ai-assistant-slice.d.ts +14 -1
  22. package/dist/dts/state/ai-assistant-slice.d.ts.map +1 -1
  23. package/dist/dts/state/session-store.d.ts +2 -0
  24. package/dist/dts/state/session-store.d.ts.map +1 -1
  25. package/dist/dts/utils/sum-costs.d.ts +13 -0
  26. package/dist/dts/utils/sum-costs.d.ts.map +1 -0
  27. package/dist/dts/utils/sum-costs.test.d.ts +2 -0
  28. package/dist/dts/utils/sum-costs.test.d.ts.map +1 -0
  29. package/dist/esm/components/chat-driver/chat-driver.js +93 -15
  30. package/dist/esm/components/orchestrating-driver/orchestrating-driver.js +23 -4
  31. package/dist/esm/config/define-stateful-agent.js +12 -0
  32. package/dist/esm/config/validate-providers.js +47 -0
  33. package/dist/esm/config/validate-providers.test.js +100 -0
  34. package/dist/esm/main/main.js +76 -21
  35. package/dist/esm/main/main.styles.js +52 -0
  36. package/dist/esm/main/main.template.js +36 -1
  37. package/dist/esm/state/ai-assistant-slice.js +8 -0
  38. package/dist/esm/utils/sum-costs.js +23 -0
  39. package/dist/esm/utils/sum-costs.test.js +88 -0
  40. package/dist/tsconfig.tsbuildinfo +1 -1
  41. package/docs/migration-GENC-1262.md +219 -0
  42. package/package.json +16 -16
  43. package/src/components/ai-driver/ai-driver.ts +8 -0
  44. package/src/components/chat-driver/chat-driver.ts +107 -14
  45. package/src/components/orchestrating-driver/orchestrating-driver.ts +29 -4
  46. package/src/config/config.ts +32 -0
  47. package/src/config/define-stateful-agent.ts +28 -0
  48. package/src/config/validate-providers.test.ts +148 -0
  49. package/src/config/validate-providers.ts +58 -0
  50. package/src/main/main.styles.ts +52 -0
  51. package/src/main/main.template.ts +50 -2
  52. package/src/main/main.ts +69 -14
  53. package/src/state/ai-assistant-slice.ts +24 -1
  54. package/src/utils/sum-costs.test.ts +108 -0
  55. package/src/utils/sum-costs.ts +22 -0
@@ -22,7 +22,7 @@
22
22
  // =============================================================================
23
23
  var FoundationAiAssistant_1;
24
24
  import { __awaiter, __decorate, __rest } from "tslib";
25
- import { AIProvider } from '@genesislcap/foundation-ai';
25
+ import { AIProviderRegistry } 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 { agenticActivityBus } from '../channel/ai-activity-bus';
@@ -40,6 +40,7 @@ import { AI_COLOUR_AMBER, AI_COLOUR_CYAN, AI_COLOUR_PINK, AI_COLOUR_VIOLET, } fr
40
40
  import { ChatSuggestions } from '../suggestions/chat-suggestions';
41
41
  import { AnimatedPanelToggle } from '../utils/animated-panel-toggle';
42
42
  import { logger } from '../utils/logger';
43
+ import { sumCosts } from '../utils/sum-costs';
43
44
  import { expandToolTree } from '../utils/tool-fold';
44
45
  import { styles } from './main.styles';
45
46
  import { FoundationAiAssistantTemplate } from './main.template';
@@ -67,13 +68,13 @@ avoidTreeShaking(AiChatMarkdown, AiChatInteractionWrapper, AiHaloOverlay, AiChat
67
68
  * - `toolHandlers` (functions),
68
69
  * - `onActivate` / `onDeactivate` (lifecycle hooks, functions),
69
70
  * - `getDebugSnapshot` (function),
70
- * - function-form `systemPrompt` / `toolDefinitions` / `displayName` (downgraded
71
- * to `undefined` in the snapshot — the live config on the driver is still
72
- * the source of truth; the slice only stores a serializable projection).
71
+ * - function-form `systemPrompt` / `toolDefinitions` / `displayName` / `provider`
72
+ * (downgraded to `undefined` in the snapshot — the live config on the driver
73
+ * is still the source of truth; the slice only stores a serializable projection).
73
74
  */
74
75
  function stripHandlers(agent) {
75
- const { toolHandlers: _h, onActivate: _on, onDeactivate: _off, getDebugSnapshot: _g, subAgents, systemPrompt, toolDefinitions, displayName } = agent, rest = __rest(agent, ["toolHandlers", "onActivate", "onDeactivate", "getDebugSnapshot", "subAgents", "systemPrompt", "toolDefinitions", "displayName"]);
76
- const stripped = Object.assign(Object.assign({}, rest), { systemPrompt: typeof systemPrompt === 'function' ? undefined : systemPrompt, toolDefinitions: typeof toolDefinitions === 'function' ? undefined : toolDefinitions, displayName: typeof displayName === 'function' ? undefined : displayName });
76
+ const { toolHandlers: _h, onActivate: _on, onDeactivate: _off, getDebugSnapshot: _g, subAgents, systemPrompt, toolDefinitions, displayName, provider } = agent, rest = __rest(agent, ["toolHandlers", "onActivate", "onDeactivate", "getDebugSnapshot", "subAgents", "systemPrompt", "toolDefinitions", "displayName", "provider"]);
77
+ const stripped = Object.assign(Object.assign({}, rest), { systemPrompt: typeof systemPrompt === 'function' ? undefined : systemPrompt, toolDefinitions: typeof toolDefinitions === 'function' ? undefined : toolDefinitions, displayName: typeof displayName === 'function' ? undefined : displayName, provider: typeof provider === 'function' ? undefined : provider });
77
78
  return (subAgents === null || subAgents === void 0 ? void 0 : subAgents.length)
78
79
  ? Object.assign(Object.assign({}, stripped), { subAgents: subAgents.map(stripHandlers) }) : stripped;
79
80
  }
@@ -81,8 +82,11 @@ function stripHandlers(agent) {
81
82
  * Foundation AI Assistant component.
82
83
  *
83
84
  * @remarks
84
- * Inject an `AIProvider` through the DI container. Pass agent configuration via the `agents`
85
- * property. The component creates a `ChatDriver` (single agent) or `OrchestratingDriver`
85
+ * Register one or more AI providers via `registerAIProviders` from
86
+ * `@genesislcap/foundation-ai`; this element resolves the `AIProviderRegistry`
87
+ * through the DI container. Pass agent configuration via the `agents` property —
88
+ * each agent can override which registered provider it uses via `provider:`.
89
+ * The component creates a `ChatDriver` (single agent) or `OrchestratingDriver`
86
90
  * (multiple agents) to manage the conversation loop.
87
91
  *
88
92
  * Popout/collapse coordination uses `agenticActivityBus` topics `chat-popout` and `chat-popin` — not DOM `CustomEvent`s on this element.
@@ -422,6 +426,24 @@ let FoundationAiAssistant = FoundationAiAssistant_1 = class FoundationAiAssistan
422
426
  var _a;
423
427
  (_a = this._sessionRef) === null || _a === void 0 ? void 0 : _a.actions.aiAssistant.setActiveModel(value);
424
428
  }
429
+ /** Name of the AI provider used on the most recent turn. */
430
+ get activeProviderName() {
431
+ var _a;
432
+ return (_a = this._sessionRef) === null || _a === void 0 ? void 0 : _a.store.aiAssistant.activeProviderName;
433
+ }
434
+ set activeProviderName(value) {
435
+ var _a;
436
+ (_a = this._sessionRef) === null || _a === void 0 ? void 0 : _a.actions.aiAssistant.setActiveProviderName(value);
437
+ }
438
+ /** Per-provider status snapshot used by the settings panel. */
439
+ get providerStatuses() {
440
+ var _a, _b;
441
+ return (_b = (_a = this._sessionRef) === null || _a === void 0 ? void 0 : _a.store.aiAssistant.providerStatuses) !== null && _b !== void 0 ? _b : [];
442
+ }
443
+ set providerStatuses(value) {
444
+ var _a;
445
+ (_a = this._sessionRef) === null || _a === void 0 ? void 0 : _a.actions.aiAssistant.setProviderStatuses(value);
446
+ }
425
447
  get inputValue() {
426
448
  var _a, _b;
427
449
  return (_b = (_a = this._sessionRef) === null || _a === void 0 ? void 0 : _a.store.aiAssistant.inputValue) !== null && _b !== void 0 ? _b : '';
@@ -627,7 +649,7 @@ let FoundationAiAssistant = FoundationAiAssistant_1 = class FoundationAiAssistan
627
649
  // this via `agent-changed` once applyAgent fires on first sendMessage.
628
650
  this.activeAgent = agents[0];
629
651
  }
630
- return new OrchestratingDriver(this.aiProvider, agents, {
652
+ return new OrchestratingDriver(this.providerRegistry, agents, {
631
653
  sessionKey: (_b = this.getStateKey()) !== null && _b !== void 0 ? _b : '',
632
654
  maxHandoffs: agent.maxHandoffs,
633
655
  classifierHistoryLength: agent.classifierHistoryLength,
@@ -637,7 +659,7 @@ let FoundationAiAssistant = FoundationAiAssistant_1 = class FoundationAiAssistan
637
659
  maxTurnSnapshots: agent.maxTurnSnapshots,
638
660
  });
639
661
  }
640
- return new ChatDriver(this.aiProvider, {}, [], undefined, undefined, agent.maxToolIterations, agent.maxFoldOperations, agent.maxTurnSnapshots);
662
+ return new ChatDriver(this.providerRegistry, {}, [], undefined, undefined, agent.maxToolIterations, agent.maxFoldOperations, agent.maxTurnSnapshots);
641
663
  }
642
664
  /**
643
665
  * Attaches event listeners to the current driver. Stores a cleanup function
@@ -672,13 +694,22 @@ let FoundationAiAssistant = FoundationAiAssistant_1 = class FoundationAiAssistan
672
694
  });
673
695
  };
674
696
  const onSubAgentStop = (e) => {
675
- var _a;
697
+ var _a, _b;
676
698
  this.liveSubAgentTrace = [];
677
699
  this.liveSubAgentName = null;
678
700
  const { invocationId } = e.detail;
679
701
  if (invocationId) {
680
702
  (_a = this._sessionRef) === null || _a === void 0 ? void 0 : _a.actions.aiAssistant.removeInputOverride({ id: invocationId });
681
703
  }
704
+ // Sub-agents may have flipped the displayed provider while running;
705
+ // restore the parent agent's provider so the cog reflects the
706
+ // conversational driver again. Status (model / context-limit) is
707
+ // refreshed alongside so they don't disagree.
708
+ const restored = (_b = this.driver) === null || _b === void 0 ? void 0 : _b.getActiveProviderName();
709
+ if (restored && restored !== this.activeProviderName) {
710
+ this.activeProviderName = restored;
711
+ void this.resolveContextLimit();
712
+ }
682
713
  };
683
714
  const onInteractionStart = (e) => {
684
715
  var _a;
@@ -697,11 +728,19 @@ let FoundationAiAssistant = FoundationAiAssistant_1 = class FoundationAiAssistan
697
728
  (_a = this._sessionRef) === null || _a === void 0 ? void 0 : _a.actions.aiAssistant.removeInputOverride({ id: interactionId });
698
729
  }
699
730
  };
731
+ const onProviderChanged = (e) => {
732
+ const { name } = e.detail;
733
+ this.activeProviderName = name;
734
+ // Status (model id / context limit) belongs to the active provider — refresh
735
+ // the displayed values so the header reflects the provider that just took over.
736
+ void this.resolveContextLimit();
737
+ };
700
738
  driver.addEventListener('sub-agent-history-updated', onSubAgentHistoryUpdated);
701
739
  driver.addEventListener('sub-agent-start', onSubAgentStart);
702
740
  driver.addEventListener('sub-agent-stop', onSubAgentStop);
703
741
  driver.addEventListener('interaction-start', onInteractionStart);
704
742
  driver.addEventListener('interaction-stop', onInteractionStop);
743
+ driver.addEventListener('provider-changed', onProviderChanged);
705
744
  const cleanups = [
706
745
  () => driver.removeEventListener('history-updated', onHistoryUpdated),
707
746
  () => driver.removeEventListener('sub-agent-history-updated', onSubAgentHistoryUpdated),
@@ -709,6 +748,7 @@ let FoundationAiAssistant = FoundationAiAssistant_1 = class FoundationAiAssistan
709
748
  () => driver.removeEventListener('sub-agent-stop', onSubAgentStop),
710
749
  () => driver.removeEventListener('interaction-start', onInteractionStart),
711
750
  () => driver.removeEventListener('interaction-stop', onInteractionStop),
751
+ () => driver.removeEventListener('provider-changed', onProviderChanged),
712
752
  ];
713
753
  if (driver instanceof OrchestratingDriver) {
714
754
  // Restore the user pin and flow-owner lock from the session store onto
@@ -837,6 +877,7 @@ let FoundationAiAssistant = FoundationAiAssistant_1 = class FoundationAiAssistan
837
877
  }
838
878
  this.fetchSuggestions();
839
879
  void this.resolveContextLimit();
880
+ void this.loadProviderStatuses();
840
881
  if (this.messagesEl) {
841
882
  this._scrollListener = () => {
842
883
  this._userScrolledAway =
@@ -885,17 +926,33 @@ let FoundationAiAssistant = FoundationAiAssistant_1 = class FoundationAiAssistan
885
926
  }
886
927
  resolveContextLimit() {
887
928
  return __awaiter(this, void 0, void 0, function* () {
888
- var _a, _b;
929
+ var _a;
889
930
  try {
890
- const status = yield ((_b = (_a = this.aiProvider).getStatus) === null || _b === void 0 ? void 0 : _b.call(_a));
931
+ // Resolve status for the currently active provider when a driver is up
932
+ // (so per-agent provider swaps update the displayed model/limit); fall
933
+ // back to the registry default at startup before any turn has run.
934
+ const activeName = (_a = this.driver) === null || _a === void 0 ? void 0 : _a.getActiveProviderName();
935
+ const status = activeName
936
+ ? yield this.providerRegistry.getStatus(activeName)
937
+ : yield this.providerRegistry.getStatus();
891
938
  this.contextLimit = status === null || status === void 0 ? void 0 : status.contextLimit;
892
939
  this.activeModel = status === null || status === void 0 ? void 0 : status.model;
893
940
  }
894
- catch (_c) {
941
+ catch (_b) {
895
942
  // Non-fatal — context limit / model display simply won't show
896
943
  }
897
944
  });
898
945
  }
946
+ loadProviderStatuses() {
947
+ return __awaiter(this, void 0, void 0, function* () {
948
+ try {
949
+ this.providerStatuses = yield this.providerRegistry.listStatuses();
950
+ }
951
+ catch (_a) {
952
+ // Non-fatal — settings panel just won't list providers
953
+ }
954
+ });
955
+ }
899
956
  chatConfigChanged() {
900
957
  this.syncShowingSplash();
901
958
  }
@@ -945,11 +1002,9 @@ let FoundationAiAssistant = FoundationAiAssistant_1 = class FoundationAiAssistan
945
1002
  // Recompute aggregated session cost from per-message `cost` fields. Recomputing
946
1003
  // (rather than incrementing on append) keeps the total correct under any
947
1004
  // mutation of the message list — including future clear-chat / re-render flows.
948
- let runningCost = 0;
949
- for (const m of this.messages) {
950
- if (m.cost != null)
951
- runningCost += m.cost;
952
- }
1005
+ // `sumCosts` recurses into `toolCall.subAgentTrace` so cost incurred by
1006
+ // sub-agents (potentially on different providers) is included.
1007
+ const runningCost = sumCosts(this.messages);
953
1008
  if (runningCost !== this.sessionCostUsd) {
954
1009
  this.sessionCostUsd = runningCost;
955
1010
  }
@@ -1493,8 +1548,8 @@ FoundationAiAssistant.DEFAULT_LOADING_DELAY_S = 5;
1493
1548
  FoundationAiAssistant.DEFAULT_SUGGESTION_COUNT = 3;
1494
1549
  FoundationAiAssistant.MS_PER_SECOND = 1000;
1495
1550
  __decorate([
1496
- AIProvider
1497
- ], FoundationAiAssistant.prototype, "aiProvider", void 0);
1551
+ AIProviderRegistry
1552
+ ], FoundationAiAssistant.prototype, "providerRegistry", void 0);
1498
1553
  __decorate([
1499
1554
  observable
1500
1555
  ], FoundationAiAssistant.prototype, "designSystemPrefix", void 0);
@@ -251,6 +251,58 @@ export const styles = css `
251
251
  font-family: var(--monospace-font, ui-monospace, monospace);
252
252
  }
253
253
 
254
+ .provider-list {
255
+ animation: settings-slide-in 0.2s ease-out;
256
+ padding: calc(var(--design-unit) * 1.5px) calc(var(--design-unit) * 3px);
257
+ border-bottom: 1px solid var(--neutral-stroke-rest);
258
+ background-color: var(--neutral-layer-2);
259
+ display: flex;
260
+ flex-direction: column;
261
+ gap: calc(var(--design-unit) * 1px);
262
+ }
263
+
264
+ .provider-list-label {
265
+ font-size: 0.85em;
266
+ font-weight: 600;
267
+ color: var(--neutral-foreground-rest);
268
+ }
269
+
270
+ .provider-list-row {
271
+ display: flex;
272
+ align-items: baseline;
273
+ gap: calc(var(--design-unit) * 1.5px);
274
+ font-size: 0.8em;
275
+ color: var(--neutral-foreground-hint, var(--neutral-foreground-rest));
276
+ opacity: 80%;
277
+ }
278
+
279
+ .provider-list-row.is-current {
280
+ opacity: 100%;
281
+ color: var(--neutral-foreground-rest);
282
+ }
283
+
284
+ .provider-list-name {
285
+ font-weight: 600;
286
+ }
287
+
288
+ .provider-list-model {
289
+ font-family: var(--monospace-font, ui-monospace, monospace);
290
+ flex: 1;
291
+ overflow: hidden;
292
+ text-overflow: ellipsis;
293
+ white-space: nowrap;
294
+ }
295
+
296
+ .provider-list-badge {
297
+ font-size: 0.7em;
298
+ text-transform: uppercase;
299
+ letter-spacing: 0.05em;
300
+ padding: 0 calc(var(--design-unit) * 0.75px);
301
+ border-radius: calc(var(--design-unit) * 0.5px);
302
+ background-color: var(--neutral-layer-3);
303
+ color: var(--neutral-foreground-rest);
304
+ }
305
+
254
306
  .splash-wrapper {
255
307
  position: absolute;
256
308
  inset: 0;
@@ -376,12 +376,47 @@ ${(tc) => { var _a; return (((_a = tc.foldPath) === null || _a === void 0 ? void
376
376
  ></${progressTag}>
377
377
  </div>
378
378
  `)}
379
- ${when((x) => { var _a; return x.settingsOpen && ((_a = x.chatConfig.ui) === null || _a === void 0 ? void 0 : _a.showActiveModel) !== false && x.activeModel != null; }, html `
379
+ ${when((x) => {
380
+ var _a;
381
+ return x.settingsOpen &&
382
+ ((_a = x.chatConfig.ui) === null || _a === void 0 ? void 0 : _a.showActiveModel) !== false &&
383
+ x.providerStatuses.length <= 1 &&
384
+ x.activeModel != null;
385
+ }, html `
380
386
  <div class="session-stat" part="active-model">
381
387
  <span class="session-stat-label">Model</span>
382
388
  <span class="session-stat-value">${(x) => x.activeModel}</span>
383
389
  </div>
384
390
  `)}
391
+ ${when((x) => {
392
+ var _a;
393
+ return x.settingsOpen &&
394
+ ((_a = x.chatConfig.ui) === null || _a === void 0 ? void 0 : _a.showActiveModel) !== false &&
395
+ x.providerStatuses.length > 1;
396
+ }, html `
397
+ <div class="provider-list" part="provider-list">
398
+ <div class="provider-list-label">Providers</div>
399
+ ${repeat((x) => x.providerStatuses, html `
400
+ <div
401
+ class="provider-list-row ${(entry, c) => entry.name === c.parent.activeProviderName ? 'is-current' : ''}"
402
+ part="provider-list-row"
403
+ >
404
+ <span class="provider-list-name">${(entry) => entry.name}</span>
405
+ <span class="provider-list-model">${(entry) => { var _a, _b; return (_b = (_a = entry.status) === null || _a === void 0 ? void 0 : _a.model) !== null && _b !== void 0 ? _b : '—'; }}</span>
406
+ ${when((entry) => entry.isDefault, html `
407
+ <span class="provider-list-badge" part="provider-list-default-badge">
408
+ default
409
+ </span>
410
+ `)}
411
+ ${when((entry, c) => entry.name === c.parent.activeProviderName, html `
412
+ <span class="provider-list-badge" part="provider-list-current-badge">
413
+ current
414
+ </span>
415
+ `)}
416
+ </div>
417
+ `)}
418
+ </div>
419
+ `)}
385
420
  ${when((x) => { var _a; return x.settingsOpen && ((_a = x.chatConfig.ui) === null || _a === void 0 ? void 0 : _a.showSessionCost) !== false && x.sessionCostUsd > 0; }, html `
386
421
  <div class="session-stat" part="session-cost">
387
422
  <span class="session-stat-label">Session cost</span>
@@ -11,6 +11,8 @@ export const defaultSessionState = {
11
11
  contextLimit: undefined,
12
12
  sessionCostUsd: 0,
13
13
  activeModel: undefined,
14
+ activeProviderName: undefined,
15
+ providerStatuses: [],
14
16
  activeAgent: undefined,
15
17
  pinnedAgentName: null,
16
18
  flowOwnerAgentName: null,
@@ -57,6 +59,12 @@ export const aiAssistantSlice = createSlice({
57
59
  setActiveModel(state, action) {
58
60
  state.activeModel = action.payload;
59
61
  },
62
+ setActiveProviderName(state, action) {
63
+ state.activeProviderName = action.payload;
64
+ },
65
+ setProviderStatuses(state, action) {
66
+ state.providerStatuses = action.payload;
67
+ },
60
68
  setActiveAgent(state, action) {
61
69
  state.activeAgent = action.payload;
62
70
  },
@@ -0,0 +1,23 @@
1
+ /**
2
+ * Sum the `cost` field across a message list, recursing into each tool call's
3
+ * `subAgentTrace` so LLM calls made by sub-agents (potentially on a different
4
+ * provider with different rates) contribute to the total.
5
+ *
6
+ * Returns 0 when no messages carry a cost — providers that don't report usage
7
+ * metadata (e.g. Chrome built-in) are skipped silently.
8
+ *
9
+ * @internal
10
+ */
11
+ export function sumCosts(messages) {
12
+ var _a;
13
+ let total = 0;
14
+ for (const m of messages) {
15
+ if (m.cost != null)
16
+ total += m.cost;
17
+ for (const tc of (_a = m.toolCalls) !== null && _a !== void 0 ? _a : []) {
18
+ if (tc.subAgentTrace)
19
+ total += sumCosts(tc.subAgentTrace);
20
+ }
21
+ }
22
+ return total;
23
+ }
@@ -0,0 +1,88 @@
1
+ import { assert, createLogicSuite } from '@genesislcap/foundation-testing';
2
+ import { sumCosts } from './sum-costs';
3
+ const userMsg = (overrides = {}) => (Object.assign({ role: 'user', content: '' }, overrides));
4
+ const assistantMsg = (overrides = {}) => (Object.assign({ role: 'assistant', content: '' }, overrides));
5
+ const suite = createLogicSuite('sumCosts');
6
+ suite('returns 0 for an empty message list', () => {
7
+ assert.is(sumCosts([]), 0);
8
+ });
9
+ suite('returns 0 when no message carries cost (e.g. all from a no-usage provider)', () => {
10
+ assert.is(sumCosts([userMsg(), assistantMsg({ content: 'hi' })]), 0);
11
+ });
12
+ suite('sums top-level message costs', () => {
13
+ // Use integer-cents arithmetic to avoid float-equality flake; the production
14
+ // path only ever adds floats here, so any rounding lives in display, not sum.
15
+ const cost = sumCosts([
16
+ assistantMsg({ cost: 0.5 }),
17
+ assistantMsg({ cost: 1.5 }),
18
+ userMsg(), // no cost — ignored
19
+ ]);
20
+ assert.is(cost, 2);
21
+ });
22
+ suite('includes sub-agent trace costs attached to a tool call', () => {
23
+ const parentToolCallMsg = assistantMsg({
24
+ cost: 0.1,
25
+ toolCalls: [
26
+ {
27
+ id: 'tc1',
28
+ name: 'spawn_sub',
29
+ args: {},
30
+ subAgentTrace: [assistantMsg({ cost: 0.5 }), assistantMsg({ cost: 0.4 })],
31
+ },
32
+ ],
33
+ });
34
+ // 0.1 (parent) + 0.5 + 0.4 (sub) = 1.0
35
+ assert.is(sumCosts([parentToolCallMsg]), 1);
36
+ });
37
+ suite('recurses into nested sub-agent traces', () => {
38
+ const innerTrace = [assistantMsg({ cost: 0.25 })];
39
+ const middleTrace = [
40
+ assistantMsg({
41
+ cost: 0.5,
42
+ toolCalls: [{ id: 'inner', name: 'spawn_inner', args: {}, subAgentTrace: innerTrace }],
43
+ }),
44
+ ];
45
+ const top = assistantMsg({
46
+ cost: 1,
47
+ toolCalls: [{ id: 'middle', name: 'spawn_middle', args: {}, subAgentTrace: middleTrace }],
48
+ });
49
+ // 1 + 0.5 + 0.25 = 1.75
50
+ assert.is(sumCosts([top]), 1.75);
51
+ });
52
+ suite('handles a mix of tool calls with and without subAgentTrace', () => {
53
+ const cost = sumCosts([
54
+ assistantMsg({
55
+ cost: 1,
56
+ toolCalls: [
57
+ { id: 'a', name: 'plain_tool', args: {} }, // no trace — skipped
58
+ {
59
+ id: 'b',
60
+ name: 'sub_tool',
61
+ args: {},
62
+ subAgentTrace: [assistantMsg({ cost: 2 })],
63
+ },
64
+ ],
65
+ }),
66
+ ]);
67
+ assert.is(cost, 3);
68
+ });
69
+ suite('ignores missing cost fields inside an otherwise-populated trace', () => {
70
+ const cost = sumCosts([
71
+ assistantMsg({
72
+ toolCalls: [
73
+ {
74
+ id: 'tc',
75
+ name: 'sub',
76
+ args: {},
77
+ subAgentTrace: [
78
+ userMsg(), // role=user, no cost
79
+ assistantMsg({ content: 'partial', cost: 0.3 }),
80
+ assistantMsg({ content: 'unbilled' }), // no cost field
81
+ ],
82
+ },
83
+ ],
84
+ }),
85
+ ]);
86
+ assert.is(cost, 0.3);
87
+ });
88
+ suite.run();
@@ -1 +1 @@
1
- {"root":["../src/index.ts","../src/channel/ai-activity-bus.ts","../src/channel/ai-activity-channel.ts","../src/components/halo-overlay.ts","../src/components/activity-halo/activity-halo.ts","../src/components/agent-picker/agent-picker.constants.ts","../src/components/agent-picker/agent-picker.styles.ts","../src/components/agent-picker/agent-picker.template.ts","../src/components/agent-picker/agent-picker.ts","../src/components/agent-picker/index.ts","../src/components/ai-driver/ai-driver.ts","../src/components/ai-driver/index.ts","../src/components/chat-bubble/chat-bubble.styles.ts","../src/components/chat-bubble/chat-bubble.template.ts","../src/components/chat-bubble/chat-bubble.ts","../src/components/chat-bubble/index.ts","../src/components/chat-driver/chat-driver.ts","../src/components/chat-driver/index.ts","../src/components/chat-interaction-wrapper/chat-interaction-wrapper.styles.ts","../src/components/chat-interaction-wrapper/chat-interaction-wrapper.template.ts","../src/components/chat-interaction-wrapper/chat-interaction-wrapper.ts","../src/components/chat-interaction-wrapper/index.ts","../src/components/chat-markdown/chat-markdown.ts","../src/components/chat-markdown/index.ts","../src/components/orchestrating-driver/index.ts","../src/components/orchestrating-driver/orchestrating-driver.ts","../src/components/popout-manager/index.ts","../src/components/popout-manager/popout-manager.ts","../src/config/config.ts","../src/config/define-stateful-agent.ts","../src/config/fallback-agents.ts","../src/config/index.ts","../src/main/index.ts","../src/main/main.styles.ts","../src/main/main.template.ts","../src/main/main.ts","../src/main/main.types.ts","../src/state/ai-assistant-slice.ts","../src/state/driver-registry.ts","../src/state/session-store.ts","../src/styles/ai-colours.ts","../src/styles/index.ts","../src/styles/styles.ts","../src/suggestions/chat-suggestions.ts","../src/tags/index.ts","../src/types/ai-chat-widget.ts","../src/utils/animated-panel-toggle.ts","../src/utils/history-transform.ts","../src/utils/index.ts","../src/utils/logger.ts","../src/utils/tool-fold.ts"],"version":"5.9.2"}
1
+ {"root":["../src/index.ts","../src/channel/ai-activity-bus.ts","../src/channel/ai-activity-channel.ts","../src/components/halo-overlay.ts","../src/components/activity-halo/activity-halo.ts","../src/components/agent-picker/agent-picker.constants.ts","../src/components/agent-picker/agent-picker.styles.ts","../src/components/agent-picker/agent-picker.template.ts","../src/components/agent-picker/agent-picker.ts","../src/components/agent-picker/index.ts","../src/components/ai-driver/ai-driver.ts","../src/components/ai-driver/index.ts","../src/components/chat-bubble/chat-bubble.styles.ts","../src/components/chat-bubble/chat-bubble.template.ts","../src/components/chat-bubble/chat-bubble.ts","../src/components/chat-bubble/index.ts","../src/components/chat-driver/chat-driver.ts","../src/components/chat-driver/index.ts","../src/components/chat-interaction-wrapper/chat-interaction-wrapper.styles.ts","../src/components/chat-interaction-wrapper/chat-interaction-wrapper.template.ts","../src/components/chat-interaction-wrapper/chat-interaction-wrapper.ts","../src/components/chat-interaction-wrapper/index.ts","../src/components/chat-markdown/chat-markdown.ts","../src/components/chat-markdown/index.ts","../src/components/orchestrating-driver/index.ts","../src/components/orchestrating-driver/orchestrating-driver.ts","../src/components/popout-manager/index.ts","../src/components/popout-manager/popout-manager.ts","../src/config/config.ts","../src/config/define-stateful-agent.ts","../src/config/fallback-agents.ts","../src/config/index.ts","../src/config/validate-providers.test.ts","../src/config/validate-providers.ts","../src/main/index.ts","../src/main/main.styles.ts","../src/main/main.template.ts","../src/main/main.ts","../src/main/main.types.ts","../src/state/ai-assistant-slice.ts","../src/state/driver-registry.ts","../src/state/session-store.ts","../src/styles/ai-colours.ts","../src/styles/index.ts","../src/styles/styles.ts","../src/suggestions/chat-suggestions.ts","../src/tags/index.ts","../src/types/ai-chat-widget.ts","../src/utils/animated-panel-toggle.ts","../src/utils/history-transform.ts","../src/utils/index.ts","../src/utils/logger.ts","../src/utils/sum-costs.test.ts","../src/utils/sum-costs.ts","../src/utils/tool-fold.ts"],"version":"5.9.2"}