@genesislcap/ai-assistant 15.12.0 → 15.13.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 (50) hide show
  1. package/dist/ai-assistant.api.json +245 -3
  2. package/dist/ai-assistant.d.ts +127 -7
  3. package/dist/chat-driver.cjs +79 -13
  4. package/dist/chat-driver.cjs.map +2 -2
  5. package/dist/chat-driver.mjs +76 -12
  6. package/dist/chat-driver.mjs.map +2 -2
  7. package/dist/custom-elements.json +99 -6
  8. package/dist/dts/chat-driver-node.d.ts +2 -2
  9. package/dist/dts/chat-driver-node.d.ts.map +1 -1
  10. package/dist/dts/components/chat-driver/chat-driver.d.ts +45 -1
  11. package/dist/dts/components/chat-driver/chat-driver.d.ts.map +1 -1
  12. package/dist/dts/components/chat-driver/chat-driver.turn-usage.test.d.ts +2 -0
  13. package/dist/dts/components/chat-driver/chat-driver.turn-usage.test.d.ts.map +1 -0
  14. package/dist/dts/main/main.d.ts +20 -1
  15. package/dist/dts/main/main.d.ts.map +1 -1
  16. package/dist/dts/state/debug-event-log.d.ts.map +1 -1
  17. package/dist/dts/state/persistence/diagnostics.d.ts +65 -8
  18. package/dist/dts/state/persistence/diagnostics.d.ts.map +1 -1
  19. package/dist/dts/state/persistence/index.d.ts +1 -1
  20. package/dist/dts/state/persistence/index.d.ts.map +1 -1
  21. package/dist/dts/state/persistence/session-persister.d.ts +4 -3
  22. package/dist/dts/state/persistence/session-persister.d.ts.map +1 -1
  23. package/dist/dts/utils/sum-usage.d.ts +20 -0
  24. package/dist/dts/utils/sum-usage.d.ts.map +1 -1
  25. package/dist/esm/chat-driver-node.js +12 -2
  26. package/dist/esm/components/chat-driver/chat-driver.js +60 -5
  27. package/dist/esm/components/chat-driver/chat-driver.turn-usage.test.js +268 -0
  28. package/dist/esm/main/main.js +53 -28
  29. package/dist/esm/state/debug-event-log.js +7 -2
  30. package/dist/esm/state/persistence/diagnostics.js +79 -16
  31. package/dist/esm/state/persistence/diagnostics.test.js +174 -1
  32. package/dist/esm/state/persistence/index.js +1 -1
  33. package/dist/esm/state/persistence/session-persister.js +13 -5
  34. package/dist/esm/state/persistence/session-persister.test.js +31 -0
  35. package/dist/esm/utils/sum-usage.js +43 -0
  36. package/dist/esm/utils/sum-usage.test.js +45 -1
  37. package/dist/tsconfig.tsbuildinfo +1 -1
  38. package/package.json +17 -17
  39. package/src/chat-driver-node.ts +12 -2
  40. package/src/components/chat-driver/chat-driver.ts +107 -6
  41. package/src/components/chat-driver/chat-driver.turn-usage.test.ts +362 -0
  42. package/src/main/main.ts +52 -23
  43. package/src/state/debug-event-log.ts +7 -2
  44. package/src/state/persistence/diagnostics.test.ts +208 -1
  45. package/src/state/persistence/diagnostics.ts +117 -15
  46. package/src/state/persistence/index.ts +1 -1
  47. package/src/state/persistence/session-persister.test.ts +37 -0
  48. package/src/state/persistence/session-persister.ts +13 -5
  49. package/src/utils/sum-usage.test.ts +52 -1
  50. package/src/utils/sum-usage.ts +45 -0
@@ -46,6 +46,49 @@ export function addUsage(a, b) {
46
46
  outputTokens: a.outputTokens + b.outputTokens,
47
47
  };
48
48
  }
49
+ /**
50
+ * The four buckets plus USD for ONE message — the per-message fields projected into
51
+ * the disjoint, addable shape, or `undefined` when the message reports no usage at
52
+ * all (a user turn, a display-only reasoning/narration split, a provider that
53
+ * reports none).
54
+ *
55
+ * Shallow by design: it does NOT recurse into `toolCalls[].subAgentTrace` and does
56
+ * NOT count `compaction.rolledUpUsage`, so it answers "what did this one request
57
+ * cost" rather than "what did this message and everything under it cost". Use
58
+ * {@link sumUsage} for the latter — passing a whole transcript through this one
59
+ * message at a time would silently drop both.
60
+ *
61
+ * `costUsd` folds in `externalCostUsd` (non-LLM spend a widget reported), matching
62
+ * `sumUsage`. It is `0` when the provider priced nothing, which is NOT a claim that
63
+ * the call was free — see {@link AggregateUsage}; `UsageRow.costUsd` is the
64
+ * shape that keeps unpriced distinguishable, and is the right tool for a ledger.
65
+ *
66
+ * @beta
67
+ */
68
+ export function messageUsage(m) {
69
+ var _a, _b, _c, _d, _e, _f;
70
+ if (m.cost == null &&
71
+ m.externalCostUsd == null &&
72
+ m.inputTokens == null &&
73
+ m.outputTokens == null &&
74
+ m.cacheReadTokens == null &&
75
+ m.cacheWriteTokens == null) {
76
+ return undefined;
77
+ }
78
+ const cacheReadTokens = (_a = m.cacheReadTokens) !== null && _a !== void 0 ? _a : 0;
79
+ const cacheWriteTokens = (_b = m.cacheWriteTokens) !== null && _b !== void 0 ? _b : 0;
80
+ return {
81
+ costUsd: ((_c = m.cost) !== null && _c !== void 0 ? _c : 0) + ((_d = m.externalCostUsd) !== null && _d !== void 0 ? _d : 0),
82
+ // The uncached REMAINDER, clamped — the same arithmetic `accumulate` below applies,
83
+ // for the same reasons. The two are deliberately separate implementations (that walk
84
+ // mutates one accumulator rather than allocating per message) and must agree;
85
+ // `sum-usage.test.ts` pins them together.
86
+ uncachedInputTokens: Math.max(0, ((_e = m.inputTokens) !== null && _e !== void 0 ? _e : 0) - cacheReadTokens - cacheWriteTokens),
87
+ cacheReadTokens,
88
+ cacheWriteTokens,
89
+ outputTokens: (_f = m.outputTokens) !== null && _f !== void 0 ? _f : 0,
90
+ };
91
+ }
49
92
  /**
50
93
  * Sum cost and per-bucket token usage across a message list.
51
94
  *
@@ -1,5 +1,5 @@
1
1
  import { assert, createLogicSuite } from '@genesislcap/foundation-testing';
2
- import { addUsage, emptyUsage, sumUsage, totalTokens } from './sum-usage';
2
+ import { addUsage, emptyUsage, messageUsage, sumUsage, totalTokens } from './sum-usage';
3
3
  const suite = createLogicSuite('sumUsage');
4
4
  const msg = (over = {}) => (Object.assign({ role: 'assistant', content: '' }, over));
5
5
  suite('returns zeroes when no message carries usage', () => {
@@ -117,4 +117,48 @@ suite('addUsage with an empty operand is the identity', () => {
117
117
  assert.equal(addUsage(a, emptyUsage()), a);
118
118
  assert.equal(addUsage(emptyUsage(), a), a);
119
119
  });
120
+ suite('messageUsage is undefined for a message reporting nothing', () => {
121
+ // A user turn, or a display-only reasoning/narration split. Distinct from a zeroed
122
+ // usage: "this call reported nothing" is not "this call cost nothing".
123
+ assert.is(messageUsage(msg({ role: 'user', content: 'hi' })), undefined);
124
+ assert.is(messageUsage(msg({ content: 'thinking…', category: 'reasoning' })), undefined);
125
+ });
126
+ suite('messageUsage agrees with sumUsage over the same single message', () => {
127
+ // The two derivations are deliberately separate — `accumulate` mutates one
128
+ // accumulator rather than allocating per message — so pin them together. A drift
129
+ // here would put one figure on a turn entry and a different one in the session
130
+ // total, both looking plausible.
131
+ const cases = [
132
+ { inputTokens: 1000, cacheReadTokens: 700, cacheWriteTokens: 200, outputTokens: 50, cost: 0.4 },
133
+ { inputTokens: 400, outputTokens: 60 },
134
+ { inputTokens: 100, cacheReadTokens: 90, cacheWriteTokens: 90 },
135
+ { cost: 0.01, externalCostUsd: 0.02 },
136
+ ];
137
+ for (const over of cases) {
138
+ const m = msg(over);
139
+ assert.equal(messageUsage(m), sumUsage([m]), `messageUsage must match sumUsage for ${JSON.stringify(over)}`);
140
+ }
141
+ });
142
+ suite('messageUsage is shallow — it prices the one request, not the tree', () => {
143
+ // Deliberately blind to sub-agent traces and banked compaction usage: it answers
144
+ // "what did this call cost", which is what a turn snapshot needs. `sumUsage` is the
145
+ // function that walks everything underneath.
146
+ const m = msg({
147
+ inputTokens: 100,
148
+ outputTokens: 10,
149
+ cost: 0.05,
150
+ toolCalls: [
151
+ {
152
+ id: 't1',
153
+ name: 'delegate',
154
+ args: {},
155
+ subAgentTrace: [msg({ inputTokens: 900, outputTokens: 90, cost: 0.5 })],
156
+ },
157
+ ],
158
+ });
159
+ const usage = messageUsage(m);
160
+ assert.is(usage.costUsd, 0.05, 'the child conversation is not folded in');
161
+ assert.is(usage.uncachedInputTokens, 100);
162
+ assert.ok(sumUsage([m]).costUsd > usage.costUsd, 'sumUsage does fold it in');
163
+ });
120
164
  suite.run();
@@ -1 +1 @@
1
- {"root":["../src/chat-driver-node.ts","../src/index.ts","../src/channel/ai-activity-bus.ts","../src/channel/ai-activity-channel.ts","../src/components/flowing-waves-indicator.ts","../src/components/halo-overlay.ts","../src/components/plasma-orb-indicator.ts","../src/components/waves-indicator.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/align-event-globals.ts","../src/components/chat-driver/chat-driver.compact.test.ts","../src/components/chat-driver/chat-driver.invocation-scope.test.ts","../src/components/chat-driver/chat-driver.test.ts","../src/components/chat-driver/chat-driver.thinking-policy.test.ts","../src/components/chat-driver/chat-driver.trace-capture.test.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.test.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.budget.test.ts","../src/components/orchestrating-driver/orchestrating-driver.pin.test.ts","../src/components/orchestrating-driver/orchestrating-driver.ts","../src/components/popout-manager/index.ts","../src/components/popout-manager/popout-manager.ts","../src/components/settings-modal/index.ts","../src/components/settings-modal/settings-modal.styles.test.ts","../src/components/settings-modal/settings-modal.styles.ts","../src/components/settings-modal/settings-modal.template.test.ts","../src/components/settings-modal/settings-modal.template.ts","../src/config/config.ts","../src/config/define-stateful-agent.test.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/blocked-state.test.ts","../src/main/budget-meter.test.ts","../src/main/cost-session-banking.test.ts","../src/main/index.ts","../src/main/main.styles.test.ts","../src/main/main.styles.ts","../src/main/main.template.ts","../src/main/main.ts","../src/main/main.types.ts","../src/main/popout-interaction-gate.test.ts","../src/provider/ai-provider-switcher.ts","../src/provider/assistant-app-settings.ts","../src/state/ai-assistant-slice.test.ts","../src/state/ai-assistant-slice.ts","../src/state/debug-event-log.test.ts","../src/state/debug-event-log.ts","../src/state/driver-registry.test.ts","../src/state/driver-registry.ts","../src/state/interaction-context.test.ts","../src/state/interaction-context.ts","../src/state/session-store.ts","../src/state/persistence/build-timeline-entries.ts","../src/state/persistence/diagnostics-cursors.test.ts","../src/state/persistence/diagnostics-cursors.ts","../src/state/persistence/diagnostics.test.ts","../src/state/persistence/diagnostics.ts","../src/state/persistence/index.ts","../src/state/persistence/persister-registry.ts","../src/state/persistence/session-persistence-provider.test.ts","../src/state/persistence/session-persistence-provider.ts","../src/state/persistence/session-persistence.integration.test.ts","../src/state/persistence/session-persister.test.ts","../src/state/persistence/session-persister.ts","../src/state/persistence/session-snapshot.test.ts","../src/state/persistence/session-snapshot.ts","../src/state/persistence/stateful-restore.e2e.test.ts","../src/styles/ai-colours.ts","../src/styles/settings-section.ts","../src/suggestions/chat-suggestions.ts","../src/tags/index.ts","../src/types/ai-chat-widget.ts","../src/types/interaction-context.ts","../src/utils/animated-panel-toggle.ts","../src/utils/animation-exclusivity.test.ts","../src/utils/animation-exclusivity.ts","../src/utils/banked-usage-baselines.ts","../src/utils/collect-session-models.test.ts","../src/utils/collect-session-models.ts","../src/utils/condense-history.test.ts","../src/utils/condense-history.ts","../src/utils/cost-session-history.test.ts","../src/utils/cost-session-history.ts","../src/utils/derive-cost-session-title.test.ts","../src/utils/derive-cost-session-title.ts","../src/utils/flatten-sub-agent-messages.test.ts","../src/utils/flatten-sub-agent-messages.ts","../src/utils/format-usd.ts","../src/utils/history-transform.test.ts","../src/utils/history-transform.ts","../src/utils/index.ts","../src/utils/logger.ts","../src/utils/message-partition.test.ts","../src/utils/message-partition.ts","../src/utils/resolve-cost-history-config.test.ts","../src/utils/resolve-cost-history-config.ts","../src/utils/resolve-preference-baseline.test.ts","../src/utils/resolve-preference-baseline.ts","../src/utils/strip-agent-handlers.test.ts","../src/utils/strip-agent-handlers.ts","../src/utils/sum-costs.test.ts","../src/utils/sum-costs.ts","../src/utils/sum-tokens.test.ts","../src/utils/sum-tokens.ts","../src/utils/sum-usage.test.ts","../src/utils/sum-usage.ts","../src/utils/tool-fold.ts","../src/utils/usage-rows.test.ts","../src/utils/usage-rows.ts","../src/utils/with-timeout.ts"],"version":"5.9.2"}
1
+ {"root":["../src/chat-driver-node.ts","../src/index.ts","../src/channel/ai-activity-bus.ts","../src/channel/ai-activity-channel.ts","../src/components/flowing-waves-indicator.ts","../src/components/halo-overlay.ts","../src/components/plasma-orb-indicator.ts","../src/components/waves-indicator.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/align-event-globals.ts","../src/components/chat-driver/chat-driver.compact.test.ts","../src/components/chat-driver/chat-driver.invocation-scope.test.ts","../src/components/chat-driver/chat-driver.test.ts","../src/components/chat-driver/chat-driver.thinking-policy.test.ts","../src/components/chat-driver/chat-driver.trace-capture.test.ts","../src/components/chat-driver/chat-driver.ts","../src/components/chat-driver/chat-driver.turn-usage.test.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.test.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.budget.test.ts","../src/components/orchestrating-driver/orchestrating-driver.pin.test.ts","../src/components/orchestrating-driver/orchestrating-driver.ts","../src/components/popout-manager/index.ts","../src/components/popout-manager/popout-manager.ts","../src/components/settings-modal/index.ts","../src/components/settings-modal/settings-modal.styles.test.ts","../src/components/settings-modal/settings-modal.styles.ts","../src/components/settings-modal/settings-modal.template.test.ts","../src/components/settings-modal/settings-modal.template.ts","../src/config/config.ts","../src/config/define-stateful-agent.test.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/blocked-state.test.ts","../src/main/budget-meter.test.ts","../src/main/cost-session-banking.test.ts","../src/main/index.ts","../src/main/main.styles.test.ts","../src/main/main.styles.ts","../src/main/main.template.ts","../src/main/main.ts","../src/main/main.types.ts","../src/main/popout-interaction-gate.test.ts","../src/provider/ai-provider-switcher.ts","../src/provider/assistant-app-settings.ts","../src/state/ai-assistant-slice.test.ts","../src/state/ai-assistant-slice.ts","../src/state/debug-event-log.test.ts","../src/state/debug-event-log.ts","../src/state/driver-registry.test.ts","../src/state/driver-registry.ts","../src/state/interaction-context.test.ts","../src/state/interaction-context.ts","../src/state/session-store.ts","../src/state/persistence/build-timeline-entries.ts","../src/state/persistence/diagnostics-cursors.test.ts","../src/state/persistence/diagnostics-cursors.ts","../src/state/persistence/diagnostics.test.ts","../src/state/persistence/diagnostics.ts","../src/state/persistence/index.ts","../src/state/persistence/persister-registry.ts","../src/state/persistence/session-persistence-provider.test.ts","../src/state/persistence/session-persistence-provider.ts","../src/state/persistence/session-persistence.integration.test.ts","../src/state/persistence/session-persister.test.ts","../src/state/persistence/session-persister.ts","../src/state/persistence/session-snapshot.test.ts","../src/state/persistence/session-snapshot.ts","../src/state/persistence/stateful-restore.e2e.test.ts","../src/styles/ai-colours.ts","../src/styles/settings-section.ts","../src/suggestions/chat-suggestions.ts","../src/tags/index.ts","../src/types/ai-chat-widget.ts","../src/types/interaction-context.ts","../src/utils/animated-panel-toggle.ts","../src/utils/animation-exclusivity.test.ts","../src/utils/animation-exclusivity.ts","../src/utils/banked-usage-baselines.ts","../src/utils/collect-session-models.test.ts","../src/utils/collect-session-models.ts","../src/utils/condense-history.test.ts","../src/utils/condense-history.ts","../src/utils/cost-session-history.test.ts","../src/utils/cost-session-history.ts","../src/utils/derive-cost-session-title.test.ts","../src/utils/derive-cost-session-title.ts","../src/utils/flatten-sub-agent-messages.test.ts","../src/utils/flatten-sub-agent-messages.ts","../src/utils/format-usd.ts","../src/utils/history-transform.test.ts","../src/utils/history-transform.ts","../src/utils/index.ts","../src/utils/logger.ts","../src/utils/message-partition.test.ts","../src/utils/message-partition.ts","../src/utils/resolve-cost-history-config.test.ts","../src/utils/resolve-cost-history-config.ts","../src/utils/resolve-preference-baseline.test.ts","../src/utils/resolve-preference-baseline.ts","../src/utils/strip-agent-handlers.test.ts","../src/utils/strip-agent-handlers.ts","../src/utils/sum-costs.test.ts","../src/utils/sum-costs.ts","../src/utils/sum-tokens.test.ts","../src/utils/sum-tokens.ts","../src/utils/sum-usage.test.ts","../src/utils/sum-usage.ts","../src/utils/tool-fold.ts","../src/utils/usage-rows.test.ts","../src/utils/usage-rows.ts","../src/utils/with-timeout.ts"],"version":"5.9.2"}
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": "15.12.0",
4
+ "version": "15.13.1",
5
5
  "license": "SEE LICENSE IN license.txt",
6
6
  "main": "dist/esm/index.js",
7
7
  "types": "dist/ai-assistant.d.ts",
@@ -73,26 +73,26 @@
73
73
  }
74
74
  },
75
75
  "devDependencies": {
76
- "@genesislcap/foundation-testing": "15.12.0",
77
- "@genesislcap/genx": "15.12.0",
78
- "@genesislcap/rollup-builder": "15.12.0",
79
- "@genesislcap/ts-builder": "15.12.0",
80
- "@genesislcap/uvu-playwright-builder": "15.12.0",
81
- "@genesislcap/vite-builder": "15.12.0",
82
- "@genesislcap/webpack-builder": "15.12.0",
76
+ "@genesislcap/foundation-testing": "15.13.1",
77
+ "@genesislcap/genx": "15.13.1",
78
+ "@genesislcap/rollup-builder": "15.13.1",
79
+ "@genesislcap/ts-builder": "15.13.1",
80
+ "@genesislcap/uvu-playwright-builder": "15.13.1",
81
+ "@genesislcap/vite-builder": "15.13.1",
82
+ "@genesislcap/webpack-builder": "15.13.1",
83
83
  "@types/dompurify": "^3.0.5",
84
84
  "@types/marked": "^5.0.2",
85
85
  "esbuild": "0.25.12"
86
86
  },
87
87
  "dependencies": {
88
- "@genesislcap/foundation-ai": "15.12.0",
89
- "@genesislcap/foundation-logger": "15.12.0",
90
- "@genesislcap/foundation-notifications": "15.12.0",
91
- "@genesislcap/foundation-redux": "15.12.0",
92
- "@genesislcap/foundation-ui": "15.12.0",
93
- "@genesislcap/foundation-utils": "15.12.0",
94
- "@genesislcap/rapid-design-system": "15.12.0",
95
- "@genesislcap/web-core": "15.12.0",
88
+ "@genesislcap/foundation-ai": "15.13.1",
89
+ "@genesislcap/foundation-logger": "15.13.1",
90
+ "@genesislcap/foundation-notifications": "15.13.1",
91
+ "@genesislcap/foundation-redux": "15.13.1",
92
+ "@genesislcap/foundation-ui": "15.13.1",
93
+ "@genesislcap/foundation-utils": "15.13.1",
94
+ "@genesislcap/rapid-design-system": "15.13.1",
95
+ "@genesislcap/web-core": "15.13.1",
96
96
  "dompurify": "^3.3.1",
97
97
  "marked": "^17.0.3"
98
98
  },
@@ -105,5 +105,5 @@
105
105
  "access": "public"
106
106
  },
107
107
  "customElements": "dist/custom-elements.json",
108
- "gitHead": "06f5ab51086c6616c6b96a7a1aebccddb3af7881"
108
+ "gitHead": "4ac0cfb22051b17da81586baf5166c9b605fae10"
109
109
  }
@@ -105,7 +105,13 @@ export type {
105
105
  // `addUsage`/`emptyUsage` are the composition primitives for a conversation that CONTINUES across
106
106
  // rounds — see the guidance on `sumUsage` itself. `totalTokens` exists because the four buckets of
107
107
  // an `AggregateUsage` are disjoint and safe to add, which the per-message fields are NOT.
108
- export { addUsage, emptyUsage, sumUsage, totalTokens } from './utils/sum-usage';
108
+ //
109
+ // `messageUsage` is the single-message projection of the same arithmetic — what ONE request
110
+ // cost, in the same four-bucket shape. It is what prices a turn snapshot (see
111
+ // `TurnSnapshot.usage`), and the reason a consumer needs it separately is that the response
112
+ // to a *failed* attempt never reaches history, so `sumUsage` over the transcript cannot see
113
+ // that spend at all.
114
+ export { addUsage, emptyUsage, messageUsage, sumUsage, totalTokens } from './utils/sum-usage';
109
115
 
110
116
  // Per-call projection, for the cases an aggregate cannot serve: usage rows for
111
117
  // per-project attribution, a cost dashboard, or auditing which turns came back
@@ -128,5 +134,9 @@ export { clearSession, getMetaEvents } from './state/debug-event-log';
128
134
  export type { MetaEvent } from './state/debug-event-log';
129
135
  export { buildTimelineEntries } from './state/persistence/build-timeline-entries';
130
136
  export type { TurnSnapshotLike } from './state/persistence/build-timeline-entries';
131
- export { assembleDebugLog } from './state/persistence/diagnostics';
137
+ // `withFreshMetaSnapshot` matters to a headless consumer that stitches a STORED stream: the
138
+ // newest stored `meta-snapshot` is frozen at the last config-signature change, so its context
139
+ // and cost figures are as old as that. Pass a fresh snapshot of your own through this before
140
+ // `assembleDebugLog`, or state in your own output that the totals are historical.
141
+ export { assembleDebugLog, withFreshMetaSnapshot } from './state/persistence/diagnostics';
132
142
  export type { DebugLog, DiagnosticEntry } from './state/persistence/diagnostics';
@@ -1,4 +1,5 @@
1
1
  import type {
2
+ AggregateUsage,
2
3
  AIProvider,
3
4
  AIProviderRegistry,
4
5
  AIProviderType,
@@ -70,7 +71,7 @@ import {
70
71
  normalizeForProvider,
71
72
  } from '../../utils/history-transform';
72
73
  import { logger } from '../../utils/logger';
73
- import { sumUsage } from '../../utils/sum-usage';
74
+ import { messageUsage, sumUsage } from '../../utils/sum-usage';
74
75
  import { TOOL_FOLD_SYMBOL, type ToolFold } from '../../utils/tool-fold';
75
76
  import type { AiDriver, AllAgentSummary } from '../ai-driver/ai-driver';
76
77
 
@@ -241,6 +242,46 @@ export interface TurnSnapshot {
241
242
  agentLabel?: string;
242
243
  /** Agent-supplied snapshot — machine state/context for stateful agents, undefined otherwise. */
243
244
  agentSnapshot?: unknown;
245
+ /**
246
+ * Concrete model that ran this call (e.g. `'claude-sonnet-4-6'`) — the serving model
247
+ * where the provider reports one, else the model the resolved tier was configured
248
+ * with. Undefined when the provider exposes no `getStatus` and the transport stamped
249
+ * nothing.
250
+ *
251
+ * Recorded per call, so an agent whose `provider` selector varies by state (a tier
252
+ * switch between steps of a flow) has each step attributed to the model that actually
253
+ * ran it — without the reader having to join the turn to the message after it, which
254
+ * is impossible for a call that produced no message.
255
+ */
256
+ model?: string;
257
+ /**
258
+ * Registry slot the provider resolved under for this call — a tier name like
259
+ * `'high'`/`'low'`, or the registry default's name. Kept alongside {@link
260
+ * TurnSnapshot.model} because they answer different questions: the slot is what the
261
+ * agent asked for, the model is what served it, and repointing a slot at a new model
262
+ * mid-session is only visible when both are recorded.
263
+ */
264
+ providerName?: string;
265
+ /** Vendor behind the resolved slot (`'anthropic'`, `'gemini'`, …), when the provider reports it. */
266
+ provider?: AIProviderType;
267
+ /**
268
+ * What this one LLM call cost — the four disjoint token buckets plus USD, derived
269
+ * from the response's usage by `messageUsage`. Back-filled when the response lands
270
+ * (the rest of the snapshot is captured *before* the call), so it is `undefined`
271
+ * while the call is in flight, on providers that report no usage, and on a call that
272
+ * threw rather than returning — a malformed-call/truncation/402 error carries no usage
273
+ * block, so any tokens the provider billed for it are not recoverable here.
274
+ *
275
+ * A snapshot is one **model call**, not one user turn: every tool-loop iteration and
276
+ * every retried attempt records its own. That makes this the only record of spend on
277
+ * an attempt that produced no message — a blank or refused response is billed and
278
+ * then discarded (see the empty-response retries), so summing the transcript alone
279
+ * under-reports the turn.
280
+ *
281
+ * For a call that DID produce a message, this is the same money as that message's
282
+ * `cost`/token fields, not additional money — never add turn usage to message usage.
283
+ */
284
+ usage?: AggregateUsage;
244
285
  }
245
286
 
246
287
  interface FoldStackFrame {
@@ -1140,7 +1181,14 @@ export class ChatDriver extends EventTarget implements AiDriver {
1140
1181
  if (resolvedName !== this.lastDispatchedProviderName) {
1141
1182
  this.lastDispatchedProviderName = resolvedName;
1142
1183
  recordMetaEvent(this.sessionKey, 'provider.selected', {
1184
+ // `provider` is the registry SLOT (a tier name like 'high'), kept under that key
1185
+ // for compatibility; `model` and `vendor` are what it resolved to. Recording all
1186
+ // three is the difference between "the agent switched to its high tier" and
1187
+ // knowing which model that actually was — a tier can be repointed mid-session,
1188
+ // and a slot name alone cannot distinguish anthropic from gemini.
1143
1189
  provider: resolvedName,
1190
+ model: status.model,
1191
+ vendor: status.provider,
1144
1192
  agent: this.activeAgentName,
1145
1193
  });
1146
1194
  this.dispatchEvent(
@@ -1304,13 +1352,17 @@ export class ChatDriver extends EventTarget implements AiDriver {
1304
1352
  * Push one snapshot to the ring buffer. Called inside `runToolLoop` just
1305
1353
  * before each LLM call — that's the latest point where the prompt, tool
1306
1354
  * surface, and agent state line up with what the model is about to see.
1355
+ *
1356
+ * Returns the pushed object so the caller can back-fill what only the response
1357
+ * knows (`usage`). Mutating it after the fact is safe whether or not the ring
1358
+ * buffer has since evicted it — an evicted snapshot is simply no longer exported.
1307
1359
  */
1308
1360
  private recordTurnSnapshot(
1309
1361
  resolvedSystemPrompt: string | undefined,
1310
1362
  temperature: number | undefined,
1311
1363
  toolChoice: ChatToolChoice | undefined,
1312
1364
  tailContext: string | undefined,
1313
- ): void {
1365
+ ): TurnSnapshot {
1314
1366
  let agentSnapshot: unknown;
1315
1367
  if (this.debugSnapshotter) {
1316
1368
  try {
@@ -1324,7 +1376,7 @@ export class ChatDriver extends EventTarget implements AiDriver {
1324
1376
  }
1325
1377
  const turnIndex = String(this.globalTurnIndex);
1326
1378
  this.globalTurnIndex += 1;
1327
- this.turnSnapshots.push({
1379
+ const snapshot: TurnSnapshot = {
1328
1380
  turnIndex,
1329
1381
  timestamp: new Date().toISOString(),
1330
1382
  agentName: this.activeAgentName,
@@ -1335,10 +1387,12 @@ export class ChatDriver extends EventTarget implements AiDriver {
1335
1387
  temperature,
1336
1388
  toolChoice,
1337
1389
  agentSnapshot,
1338
- });
1390
+ };
1391
+ this.turnSnapshots.push(snapshot);
1339
1392
  if (this.turnSnapshots.length > this.maxTurnSnapshots) {
1340
1393
  this.turnSnapshots.shift();
1341
1394
  }
1395
+ return snapshot;
1342
1396
  }
1343
1397
 
1344
1398
  /**
@@ -2759,7 +2813,12 @@ export class ChatDriver extends EventTarget implements AiDriver {
2759
2813
  // force when no tools are advertised.)
2760
2814
  const effectiveToolChoice = resolvedToolChoice ?? (this.isSubAgent ? 'required' : undefined);
2761
2815
 
2762
- this.recordTurnSnapshot(systemPrompt, resolvedTemperature, effectiveToolChoice, tailContext);
2816
+ const turnSnapshot = this.recordTurnSnapshot(
2817
+ systemPrompt,
2818
+ resolvedTemperature,
2819
+ effectiveToolChoice,
2820
+ tailContext,
2821
+ );
2763
2822
 
2764
2823
  // Capture the pending user input, then clear the slots BEFORE the chat
2765
2824
  // call. `sendMessage` already appended the user message to `this.history`,
@@ -2809,6 +2868,20 @@ export class ChatDriver extends EventTarget implements AiDriver {
2809
2868
  // oxlint-disable-next-line no-await-in-loop
2810
2869
  const activeProvider = await this.resolveProviderForTurn(promptCtx);
2811
2870
 
2871
+ // Attribute the turn to the tier/model it resolved. Stamped HERE, not inside
2872
+ // `recordTurnSnapshot`: the snapshot is taken before this line runs, so reading
2873
+ // `lastResolved*` there yields the PREVIOUS call's model — wrong on precisely the
2874
+ // turn where an agent's per-state `provider` selector switches tier, which is the
2875
+ // turn a reader is looking for. `model` is refined to the serving model once the
2876
+ // response lands (see below); until then — and on a call that throws — it is the
2877
+ // model we ASKED for, which is the only thing knowable at that point.
2878
+ if (this.lastResolvedProviderName !== undefined) {
2879
+ turnSnapshot.providerName = this.lastResolvedProviderName;
2880
+ }
2881
+ if (this.lastResolvedProvider !== undefined)
2882
+ turnSnapshot.provider = this.lastResolvedProvider;
2883
+ if (this.lastResolvedModel !== undefined) turnSnapshot.model = this.lastResolvedModel;
2884
+
2812
2885
  let response: ChatMessage;
2813
2886
  try {
2814
2887
  // oxlint-disable-next-line no-await-in-loop
@@ -2995,6 +3068,19 @@ export class ChatDriver extends EventTarget implements AiDriver {
2995
3068
  response.providerName = this.lastResolvedProviderName;
2996
3069
  }
2997
3070
 
3071
+ // Back-fill what this call cost onto the snapshot taken just before it, so the
3072
+ // exported debug log prices each model call next to the prompt/tools/state that
3073
+ // produced it (GENC-1480 follow-up). Stamped BEFORE the empty-response branch
3074
+ // below deliberately: a blank or refused response is billed and then thrown away,
3075
+ // so the snapshot is the only place that spend is ever recorded.
3076
+ turnSnapshot.usage = messageUsage(response);
3077
+ // Take the SERVING model over the requested one, now that it is known. `response.model`
3078
+ // was just filled from `lastResolvedModel` if the transport left it unset, so this is
3079
+ // the same rule the message gets — which is the point: a turn and the message it
3080
+ // produced must never disagree about which model ran, including when a server-side
3081
+ // fallback chain answered on a different model than the one we asked for.
3082
+ if (response.model !== undefined) turnSnapshot.model = response.model;
3083
+
2998
3084
  const isThinkingStep = response.content && response.toolCalls?.length;
2999
3085
  const isEmptyResponse = !response.content?.trim() && !response.toolCalls?.length;
3000
3086
  // A pre-output refusal (safety-classifier decline, e.g. Fable 5 `stop_reason: 'refusal'`)
@@ -3060,8 +3146,23 @@ export class ChatDriver extends EventTarget implements AiDriver {
3060
3146
  // `sumCosts`/`sumTokens` don't double-count and `contextTokens` reads it. Reasoning/narration are
3061
3147
  // display-only (usage undefined) and are skipped when building the provider request. `model` /
3062
3148
  // `provider` / `providerName` stay on every split message so each is still attributed.
3149
+ //
3150
+ // EVERY usage field has to be cleared here, not just the three the invariant was
3151
+ // originally written against — keep this list in step with the usage fields on
3152
+ // `ChatMessage`. The cache buckets arrived later (GENC-1475) and were left riding
3153
+ // along on the copies, so a response carrying reasoning AND narration counted its
3154
+ // cache read/write volume three times in `sumUsage`/`usageRows` — invisible in the
3155
+ // cost total (which comes from `cost`) but wrong in every bucket display and in the
3156
+ // exported log.
3063
3157
  const { reasoning, ...rest } = response;
3064
- const displayOnly = { cost: undefined, inputTokens: undefined, outputTokens: undefined };
3158
+ const displayOnly = {
3159
+ cost: undefined,
3160
+ externalCostUsd: undefined,
3161
+ inputTokens: undefined,
3162
+ outputTokens: undefined,
3163
+ cacheReadTokens: undefined,
3164
+ cacheWriteTokens: undefined,
3165
+ };
3065
3166
  if (reasoning) {
3066
3167
  this.appendToHistory({
3067
3168
  ...rest,