@genesislcap/ai-assistant 15.4.1 → 15.5.0
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 +544 -82
- package/dist/ai-assistant.d.ts +324 -36
- package/dist/chat-driver.cjs +94 -22
- package/dist/chat-driver.cjs.map +3 -3
- package/dist/chat-driver.mjs +94 -22
- package/dist/chat-driver.mjs.map +3 -3
- package/dist/custom-elements.json +303 -36
- package/dist/dts/components/chat-driver/chat-driver.d.ts.map +1 -1
- package/dist/dts/components/settings-modal/settings-modal.styles.d.ts.map +1 -1
- package/dist/dts/components/settings-modal/settings-modal.template.d.ts +9 -2
- package/dist/dts/components/settings-modal/settings-modal.template.d.ts.map +1 -1
- package/dist/dts/index.d.ts +1 -0
- package/dist/dts/index.d.ts.map +1 -1
- package/dist/dts/main/cost-session-banking.test.d.ts +2 -0
- package/dist/dts/main/cost-session-banking.test.d.ts.map +1 -0
- package/dist/dts/main/main.d.ts +184 -24
- package/dist/dts/main/main.d.ts.map +1 -1
- package/dist/dts/provider/assistant-app-settings.d.ts +30 -5
- package/dist/dts/provider/assistant-app-settings.d.ts.map +1 -1
- package/dist/dts/state/ai-assistant-slice.d.ts +11 -8
- package/dist/dts/state/ai-assistant-slice.d.ts.map +1 -1
- package/dist/dts/state/persistence/session-persistence-provider.d.ts +23 -0
- package/dist/dts/state/persistence/session-persistence-provider.d.ts.map +1 -1
- package/dist/dts/state/persistence/session-snapshot.d.ts.map +1 -1
- package/dist/dts/state/session-store.d.ts +1 -2
- package/dist/dts/state/session-store.d.ts.map +1 -1
- package/dist/dts/styles/settings-section.d.ts +29 -0
- package/dist/dts/styles/settings-section.d.ts.map +1 -0
- package/dist/dts/utils/cost-session-history.d.ts +103 -12
- package/dist/dts/utils/cost-session-history.d.ts.map +1 -1
- package/dist/dts/utils/resolve-cost-history-config.d.ts +9 -3
- package/dist/dts/utils/resolve-cost-history-config.d.ts.map +1 -1
- package/dist/dts/utils/sum-costs.d.ts.map +1 -1
- package/dist/dts/utils/sum-tokens.d.ts +8 -8
- package/dist/dts/utils/sum-tokens.d.ts.map +1 -1
- package/dist/dts/utils/sum-usage.d.ts +59 -0
- package/dist/dts/utils/sum-usage.d.ts.map +1 -0
- package/dist/dts/utils/sum-usage.test.d.ts +2 -0
- package/dist/dts/utils/sum-usage.test.d.ts.map +1 -0
- package/dist/esm/components/chat-driver/chat-driver.js +6 -0
- package/dist/esm/components/settings-modal/settings-modal.styles.js +237 -18
- package/dist/esm/components/settings-modal/settings-modal.template.js +229 -73
- package/dist/esm/index.js +1 -0
- package/dist/esm/main/cost-session-banking.test.js +308 -0
- package/dist/esm/main/main.js +424 -71
- package/dist/esm/state/ai-assistant-slice.js +11 -8
- package/dist/esm/state/ai-assistant-slice.test.js +12 -5
- package/dist/esm/state/debug-event-log.js +2 -2
- package/dist/esm/state/persistence/session-persistence.integration.test.js +5 -1
- package/dist/esm/state/persistence/session-persister.js +2 -2
- package/dist/esm/state/persistence/session-persister.test.js +10 -1
- package/dist/esm/state/persistence/session-snapshot.js +6 -2
- package/dist/esm/state/persistence/session-snapshot.test.js +4 -1
- package/dist/esm/state/persistence/stateful-restore.e2e.test.js +10 -1
- package/dist/esm/styles/settings-section.js +39 -0
- package/dist/esm/utils/cost-session-history.js +92 -15
- package/dist/esm/utils/cost-session-history.test.js +155 -13
- package/dist/esm/utils/resolve-cost-history-config.js +2 -1
- package/dist/esm/utils/sum-costs.js +2 -13
- package/dist/esm/utils/sum-tokens.js +10 -27
- package/dist/esm/utils/sum-tokens.test.js +1 -5
- package/dist/esm/utils/sum-usage.js +123 -0
- package/dist/esm/utils/sum-usage.test.js +120 -0
- package/dist/tsconfig.tsbuildinfo +1 -1
- package/package.json +17 -17
- package/src/components/chat-driver/chat-driver.ts +6 -0
- package/src/components/settings-modal/settings-modal.styles.ts +237 -18
- package/src/components/settings-modal/settings-modal.template.ts +270 -81
- package/src/index.ts +1 -0
- package/src/main/cost-session-banking.test.ts +407 -0
- package/src/main/main.ts +433 -68
- package/src/provider/assistant-app-settings.ts +31 -5
- package/src/state/ai-assistant-slice.test.ts +12 -5
- package/src/state/ai-assistant-slice.ts +21 -13
- package/src/state/debug-event-log.ts +2 -2
- package/src/state/persistence/session-persistence-provider.ts +24 -0
- package/src/state/persistence/session-persistence.integration.test.ts +8 -1
- package/src/state/persistence/session-persister.test.ts +10 -1
- package/src/state/persistence/session-persister.ts +2 -2
- package/src/state/persistence/session-snapshot.test.ts +4 -1
- package/src/state/persistence/session-snapshot.ts +5 -1
- package/src/state/persistence/stateful-restore.e2e.test.ts +9 -1
- package/src/styles/settings-section.ts +40 -0
- package/src/utils/cost-session-history.test.ts +187 -16
- package/src/utils/cost-session-history.ts +142 -23
- package/src/utils/resolve-cost-history-config.ts +10 -3
- package/src/utils/sum-costs.ts +2 -9
- package/src/utils/sum-tokens.test.ts +1 -11
- package/src/utils/sum-tokens.ts +10 -26
- package/src/utils/sum-usage.test.ts +140 -0
- package/src/utils/sum-usage.ts +130 -0
|
@@ -1,25 +1,21 @@
|
|
|
1
1
|
import DOMPurify from 'dompurify';
|
|
2
|
-
|
|
3
|
-
const messageTokenUsage = (m) => { var _a, _b; return ((_a = m.inputTokens) !== null && _a !== void 0 ? _a : 0) + ((_b = m.outputTokens) !== null && _b !== void 0 ? _b : 0); };
|
|
2
|
+
import { sumUsage, totalTokens } from './sum-usage';
|
|
4
3
|
/**
|
|
5
|
-
*
|
|
6
|
-
*
|
|
4
|
+
* Total tokens across a message list — the headline figure, with the per-bucket
|
|
5
|
+
* split available from {@link sumUsage} for anything that needs to show *why* a
|
|
6
|
+
* large token count cost little.
|
|
7
|
+
*
|
|
8
|
+
* Identical arithmetic to the old per-message `inputTokens + outputTokens` sum:
|
|
9
|
+
* the three input buckets partition `inputTokens`, so they re-add to it exactly.
|
|
10
|
+
* It does now additionally count usage banked by a compaction, which is the point
|
|
11
|
+
* — that spend used to vanish when the summarised turns left the transcript.
|
|
7
12
|
*
|
|
8
13
|
* Returns 0 when no message carries token metadata.
|
|
9
14
|
*
|
|
10
15
|
* @internal
|
|
11
16
|
*/
|
|
12
17
|
export function sumTokens(messages) {
|
|
13
|
-
|
|
14
|
-
let total = 0;
|
|
15
|
-
for (const m of messages) {
|
|
16
|
-
total += messageTokenUsage(m);
|
|
17
|
-
for (const tc of (_a = m.toolCalls) !== null && _a !== void 0 ? _a : []) {
|
|
18
|
-
if (tc.subAgentTrace)
|
|
19
|
-
total += sumTokens(tc.subAgentTrace);
|
|
20
|
-
}
|
|
21
|
-
}
|
|
22
|
-
return total;
|
|
18
|
+
return totalTokens(sumUsage(messages));
|
|
23
19
|
}
|
|
24
20
|
const TOKEN_SCALE_DECIMAL_FACTOR = 10;
|
|
25
21
|
const TOKENS_PER_MILLION = 1000000;
|
|
@@ -49,19 +45,6 @@ const scaledNumericValue = (tokens) => {
|
|
|
49
45
|
return roundScaled(tokens / TOKENS_PER_MILLION);
|
|
50
46
|
return roundScaled(tokens / TOKENS_PER_THOUSAND);
|
|
51
47
|
};
|
|
52
|
-
/**
|
|
53
|
-
* Compact plain-text token count (e.g. 1_400_000 → "1.4M").
|
|
54
|
-
*
|
|
55
|
-
* @internal
|
|
56
|
-
*/
|
|
57
|
-
export function formatCompactTokenCountPlain(tokens) {
|
|
58
|
-
const suffix = scaledSuffixLabel(tokens);
|
|
59
|
-
if (!suffix)
|
|
60
|
-
return tokens.toLocaleString();
|
|
61
|
-
const rounded = scaledNumericValue(tokens);
|
|
62
|
-
const value = Number.isInteger(rounded) ? String(rounded) : rounded.toFixed(1);
|
|
63
|
-
return `${value}${suffix}`;
|
|
64
|
-
}
|
|
65
48
|
/**
|
|
66
49
|
* Compact display for large token counts (e.g. 1_400_000 → value "1.4" + suffix "M").
|
|
67
50
|
* Returns HTML markup so the suffix can be styled separately in the settings modal.
|
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
import { assert, createLogicSuite } from '@genesislcap/foundation-testing';
|
|
2
|
-
import { formatCompactTokenCount,
|
|
2
|
+
import { formatCompactTokenCount, formatCompactTokenCountSafe, sumTokens } from './sum-tokens';
|
|
3
3
|
const assistantMsg = (overrides = {}) => (Object.assign({ role: 'assistant', content: '' }, overrides));
|
|
4
4
|
const suite = createLogicSuite('sumTokens');
|
|
5
5
|
suite('returns 0 for an empty message list', () => {
|
|
@@ -47,9 +47,5 @@ formatSuite('rolls K suffix up to M when rounding would yield 1000K', () => {
|
|
|
47
47
|
formatSuite('safe formatter preserves compact markup', () => {
|
|
48
48
|
assert.is(formatCompactTokenCountSafe(1400000), '<span class="value">1.4</span><span class="suffix">M</span>');
|
|
49
49
|
});
|
|
50
|
-
formatSuite('plain formatter returns compact text', () => {
|
|
51
|
-
assert.is(formatCompactTokenCountPlain(1400000), '1.4M');
|
|
52
|
-
assert.is(formatCompactTokenCountPlain(842), '842');
|
|
53
|
-
});
|
|
54
50
|
suite.run();
|
|
55
51
|
formatSuite.run();
|
|
@@ -0,0 +1,123 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Zeroed {@link AggregateUsage}. A factory, not a shared const, because callers
|
|
3
|
+
* accumulate into the returned object.
|
|
4
|
+
*
|
|
5
|
+
* @internal
|
|
6
|
+
*/
|
|
7
|
+
export function emptyUsage() {
|
|
8
|
+
return {
|
|
9
|
+
costUsd: 0,
|
|
10
|
+
uncachedInputTokens: 0,
|
|
11
|
+
cacheReadTokens: 0,
|
|
12
|
+
cacheWriteTokens: 0,
|
|
13
|
+
outputTokens: 0,
|
|
14
|
+
};
|
|
15
|
+
}
|
|
16
|
+
/**
|
|
17
|
+
* Total tokens processed — the four buckets added up. Safe to sum because the
|
|
18
|
+
* buckets are disjoint (see {@link AggregateUsage}); the equivalent sum over
|
|
19
|
+
* per-message fields would double-count the prompt.
|
|
20
|
+
*
|
|
21
|
+
* Bear in mind what this number is: because every turn resends the conversation,
|
|
22
|
+
* a turn's prompt is counted again in the next turn's. It is billed throughput,
|
|
23
|
+
* NOT the count of distinct tokens in the conversation, so it grows
|
|
24
|
+
* super-linearly with turn count and is expected to dwarf the context size.
|
|
25
|
+
*
|
|
26
|
+
* @internal
|
|
27
|
+
*/
|
|
28
|
+
export function totalTokens(usage) {
|
|
29
|
+
return (usage.uncachedInputTokens + usage.cacheReadTokens + usage.cacheWriteTokens + usage.outputTokens);
|
|
30
|
+
}
|
|
31
|
+
/**
|
|
32
|
+
* `a + b`, per bucket, as a new object.
|
|
33
|
+
*
|
|
34
|
+
* Used to lay a row's banked (pre-transcript) spend over what the live transcript
|
|
35
|
+
* proves. Pure rather than in-place because both operands are live state a caller
|
|
36
|
+
* must not mutate.
|
|
37
|
+
*
|
|
38
|
+
* @internal
|
|
39
|
+
*/
|
|
40
|
+
export function addUsage(a, b) {
|
|
41
|
+
return {
|
|
42
|
+
costUsd: a.costUsd + b.costUsd,
|
|
43
|
+
uncachedInputTokens: a.uncachedInputTokens + b.uncachedInputTokens,
|
|
44
|
+
cacheReadTokens: a.cacheReadTokens + b.cacheReadTokens,
|
|
45
|
+
cacheWriteTokens: a.cacheWriteTokens + b.cacheWriteTokens,
|
|
46
|
+
outputTokens: a.outputTokens + b.outputTokens,
|
|
47
|
+
};
|
|
48
|
+
}
|
|
49
|
+
/**
|
|
50
|
+
* Sum cost and per-bucket token usage across a message list.
|
|
51
|
+
*
|
|
52
|
+
* One walk produces everything a usage display needs, replacing separate cost and
|
|
53
|
+
* token passes over the same (potentially deep) transcript. Recurses into each
|
|
54
|
+
* tool call's `subAgentTrace`, so LLM work a sub-agent did — possibly on another
|
|
55
|
+
* provider at other rates — lands in the total; the driver embeds a whole
|
|
56
|
+
* sub-agent conversation in the parent's tool result, so this is the only place
|
|
57
|
+
* that spend is visible.
|
|
58
|
+
*
|
|
59
|
+
* Splitting the prompt into buckets is the point: `cost` already has the cache
|
|
60
|
+
* discount applied, but a single token total cannot show that a session was cheap
|
|
61
|
+
* because most of its prompt was a cache hit. The three input buckets are derived
|
|
62
|
+
* from the per-message fields, where `inputTokens` is the whole prompt and the
|
|
63
|
+
* cache fields break it down — so uncached input is the remainder, never an
|
|
64
|
+
* addition.
|
|
65
|
+
*
|
|
66
|
+
* Also counts `compaction.rolledUpUsage`: compaction destroys the messages it
|
|
67
|
+
* summarises, and without their banked usage every total re-derived from the
|
|
68
|
+
* transcript would drop by whatever those turns had already spent.
|
|
69
|
+
*
|
|
70
|
+
* Returns a zeroed total when no message carries usage — providers that report
|
|
71
|
+
* none (e.g. Chrome built-in) are skipped silently rather than guessed at.
|
|
72
|
+
*
|
|
73
|
+
* @internal
|
|
74
|
+
*/
|
|
75
|
+
export function sumUsage(messages) {
|
|
76
|
+
const total = emptyUsage();
|
|
77
|
+
accumulate(messages, total);
|
|
78
|
+
return total;
|
|
79
|
+
}
|
|
80
|
+
/** Add `from` into `into`, in place. */
|
|
81
|
+
function addInto(into, from) {
|
|
82
|
+
into.costUsd += from.costUsd;
|
|
83
|
+
into.uncachedInputTokens += from.uncachedInputTokens;
|
|
84
|
+
into.cacheReadTokens += from.cacheReadTokens;
|
|
85
|
+
into.cacheWriteTokens += from.cacheWriteTokens;
|
|
86
|
+
into.outputTokens += from.outputTokens;
|
|
87
|
+
}
|
|
88
|
+
/**
|
|
89
|
+
* Walk `messages`, accumulating into `into`. Mutates a single accumulator rather
|
|
90
|
+
* than merging returned objects — this runs on every transcript change, so the
|
|
91
|
+
* per-message allocation is worth avoiding.
|
|
92
|
+
*/
|
|
93
|
+
function accumulate(messages, into) {
|
|
94
|
+
var _a, _b, _c, _d, _e, _f;
|
|
95
|
+
for (const m of messages) {
|
|
96
|
+
// Provider/LLM cost plus any non-LLM cost a widget reported for its own
|
|
97
|
+
// external service calls. Both are real spend on this session.
|
|
98
|
+
if (m.cost != null)
|
|
99
|
+
into.costUsd += m.cost;
|
|
100
|
+
if (m.externalCostUsd != null)
|
|
101
|
+
into.costUsd += m.externalCostUsd;
|
|
102
|
+
const cacheRead = (_a = m.cacheReadTokens) !== null && _a !== void 0 ? _a : 0;
|
|
103
|
+
const cacheWrite = (_b = m.cacheWriteTokens) !== null && _b !== void 0 ? _b : 0;
|
|
104
|
+
into.cacheReadTokens += cacheRead;
|
|
105
|
+
into.cacheWriteTokens += cacheWrite;
|
|
106
|
+
// The uncached remainder of the prompt. Clamped at zero so malformed or
|
|
107
|
+
// hand-edited history whose buckets exceed the prompt total can't drive the
|
|
108
|
+
// session total negative — a wrong-but-plausible number is worse than a
|
|
109
|
+
// conservative one here, since this feeds a cost display.
|
|
110
|
+
into.uncachedInputTokens += Math.max(0, ((_c = m.inputTokens) !== null && _c !== void 0 ? _c : 0) - cacheRead - cacheWrite);
|
|
111
|
+
into.outputTokens += (_d = m.outputTokens) !== null && _d !== void 0 ? _d : 0;
|
|
112
|
+
// Spend banked by a compaction on this summary's behalf. NOTE: deliberately
|
|
113
|
+
// not `compaction.tokensBefore` — that is a one-turn context-window reading,
|
|
114
|
+
// not cumulative spend, and counting it here would inflate the total.
|
|
115
|
+
const rolled = (_e = m.compaction) === null || _e === void 0 ? void 0 : _e.rolledUpUsage;
|
|
116
|
+
if (rolled)
|
|
117
|
+
addInto(into, rolled);
|
|
118
|
+
for (const tc of (_f = m.toolCalls) !== null && _f !== void 0 ? _f : []) {
|
|
119
|
+
if (tc.subAgentTrace)
|
|
120
|
+
accumulate(tc.subAgentTrace, into);
|
|
121
|
+
}
|
|
122
|
+
}
|
|
123
|
+
}
|
|
@@ -0,0 +1,120 @@
|
|
|
1
|
+
import { assert, createLogicSuite } from '@genesislcap/foundation-testing';
|
|
2
|
+
import { addUsage, emptyUsage, sumUsage, totalTokens } from './sum-usage';
|
|
3
|
+
const suite = createLogicSuite('sumUsage');
|
|
4
|
+
const msg = (over = {}) => (Object.assign({ role: 'assistant', content: '' }, over));
|
|
5
|
+
suite('returns zeroes when no message carries usage', () => {
|
|
6
|
+
const usage = sumUsage([msg({ role: 'user', content: 'hi' })]);
|
|
7
|
+
assert.equal(usage, emptyUsage());
|
|
8
|
+
assert.is(totalTokens(usage), 0);
|
|
9
|
+
});
|
|
10
|
+
suite('splits the prompt into three disjoint input buckets', () => {
|
|
11
|
+
// `inputTokens` is the WHOLE prompt; the cache fields break it down. So uncached is
|
|
12
|
+
// the remainder, and the three must re-add to the prompt exactly — that identity is
|
|
13
|
+
// what stops the buckets double-counting the prompt when summed.
|
|
14
|
+
const usage = sumUsage([
|
|
15
|
+
msg({ inputTokens: 1000, cacheReadTokens: 700, cacheWriteTokens: 200, outputTokens: 50 }),
|
|
16
|
+
]);
|
|
17
|
+
assert.is(usage.uncachedInputTokens, 100);
|
|
18
|
+
assert.is(usage.cacheReadTokens, 700);
|
|
19
|
+
assert.is(usage.cacheWriteTokens, 200);
|
|
20
|
+
assert.is(usage.outputTokens, 50);
|
|
21
|
+
assert.is(usage.uncachedInputTokens + usage.cacheReadTokens + usage.cacheWriteTokens, 1000, 'input buckets re-add to the prompt total');
|
|
22
|
+
assert.is(totalTokens(usage), 1050);
|
|
23
|
+
});
|
|
24
|
+
suite('treats the whole prompt as uncached when no cache split is reported', () => {
|
|
25
|
+
// Providers that report no cache breakdown (and messages persisted before the fields
|
|
26
|
+
// existed) must still contribute their full prompt, not drop it.
|
|
27
|
+
const usage = sumUsage([msg({ inputTokens: 400, outputTokens: 60 })]);
|
|
28
|
+
assert.is(usage.uncachedInputTokens, 400);
|
|
29
|
+
assert.is(usage.cacheReadTokens, 0);
|
|
30
|
+
assert.is(usage.cacheWriteTokens, 0);
|
|
31
|
+
assert.is(totalTokens(usage), 460);
|
|
32
|
+
});
|
|
33
|
+
suite('clamps uncached input at zero when buckets exceed the prompt total', () => {
|
|
34
|
+
// Malformed or hand-edited history must not be able to drive a cost display negative.
|
|
35
|
+
const usage = sumUsage([
|
|
36
|
+
msg({ inputTokens: 100, cacheReadTokens: 90, cacheWriteTokens: 90, outputTokens: 0 }),
|
|
37
|
+
]);
|
|
38
|
+
assert.is(usage.uncachedInputTokens, 0);
|
|
39
|
+
});
|
|
40
|
+
suite('sums LLM cost and widget-reported external cost', () => {
|
|
41
|
+
const usage = sumUsage([msg({ cost: 0.25 }), msg({ externalCostUsd: 0.1, cost: 0.05 })]);
|
|
42
|
+
assert.is(Math.round(usage.costUsd * 100) / 100, 0.4);
|
|
43
|
+
});
|
|
44
|
+
suite('recurses into sub-agent traces', () => {
|
|
45
|
+
// A sub-agent's whole conversation is embedded in the parent's tool call, so this is
|
|
46
|
+
// the only place its spend is visible to a host total.
|
|
47
|
+
const usage = sumUsage([
|
|
48
|
+
msg({
|
|
49
|
+
toolCalls: [
|
|
50
|
+
{
|
|
51
|
+
id: 'tc-1',
|
|
52
|
+
name: 'delegate',
|
|
53
|
+
args: {},
|
|
54
|
+
subAgentTrace: [
|
|
55
|
+
msg({ cost: 0.2, inputTokens: 300, cacheReadTokens: 100, outputTokens: 40 }),
|
|
56
|
+
],
|
|
57
|
+
},
|
|
58
|
+
],
|
|
59
|
+
}),
|
|
60
|
+
]);
|
|
61
|
+
assert.is(usage.costUsd, 0.2);
|
|
62
|
+
assert.is(usage.uncachedInputTokens, 200);
|
|
63
|
+
assert.is(usage.cacheReadTokens, 100);
|
|
64
|
+
assert.is(usage.outputTokens, 40);
|
|
65
|
+
});
|
|
66
|
+
suite('counts usage banked onto a compaction summary', () => {
|
|
67
|
+
// Compaction destroys the messages it summarises. Without the banked rollup, a total
|
|
68
|
+
// re-derived from the transcript would fall by whatever those turns had spent.
|
|
69
|
+
const before = sumUsage([
|
|
70
|
+
msg({ cost: 0.5, inputTokens: 1000, cacheReadTokens: 400, outputTokens: 100 }),
|
|
71
|
+
msg({ cost: 0.3, inputTokens: 500, outputTokens: 50 }),
|
|
72
|
+
]);
|
|
73
|
+
const afterCompaction = sumUsage([
|
|
74
|
+
msg({
|
|
75
|
+
role: 'compacted-summary',
|
|
76
|
+
content: 'summary of the above',
|
|
77
|
+
compaction: {
|
|
78
|
+
compactedCount: 2,
|
|
79
|
+
createdAt: '2026-06-14T12:00:00.000Z',
|
|
80
|
+
rolledUpUsage: before,
|
|
81
|
+
},
|
|
82
|
+
}),
|
|
83
|
+
]);
|
|
84
|
+
assert.equal(afterCompaction, before, 'compaction preserves the running total');
|
|
85
|
+
});
|
|
86
|
+
suite('ignores tokensBefore, which is a context reading rather than spend', () => {
|
|
87
|
+
// `tokensBefore` is one turn's context-window size. Counting it as cumulative spend
|
|
88
|
+
// would silently inflate the session total on every compaction.
|
|
89
|
+
const usage = sumUsage([
|
|
90
|
+
msg({
|
|
91
|
+
role: 'compacted-summary',
|
|
92
|
+
content: 'summary',
|
|
93
|
+
compaction: {
|
|
94
|
+
compactedCount: 3,
|
|
95
|
+
createdAt: '2026-06-14T12:00:00.000Z',
|
|
96
|
+
tokensBefore: 90000,
|
|
97
|
+
},
|
|
98
|
+
}),
|
|
99
|
+
]);
|
|
100
|
+
assert.is(totalTokens(usage), 0);
|
|
101
|
+
});
|
|
102
|
+
suite('addUsage adds every bucket and mutates neither operand', () => {
|
|
103
|
+
const a = Object.assign(Object.assign({}, emptyUsage()), { costUsd: 1.5, uncachedInputTokens: 100, outputTokens: 10 });
|
|
104
|
+
const b = Object.assign(Object.assign({}, emptyUsage()), { costUsd: 0.25, cacheReadTokens: 900, outputTokens: 5 });
|
|
105
|
+
const sum = addUsage(a, b);
|
|
106
|
+
assert.is(sum.costUsd, 1.75);
|
|
107
|
+
assert.is(sum.uncachedInputTokens, 100);
|
|
108
|
+
assert.is(sum.cacheReadTokens, 900);
|
|
109
|
+
assert.is(sum.outputTokens, 15);
|
|
110
|
+
// Both operands are live state (a row's banked figure and the session total), so a
|
|
111
|
+
// caller must be able to add without either being altered underneath it.
|
|
112
|
+
assert.is(a.costUsd, 1.5);
|
|
113
|
+
assert.is(b.cacheReadTokens, 900);
|
|
114
|
+
});
|
|
115
|
+
suite('addUsage with an empty operand is the identity', () => {
|
|
116
|
+
const a = Object.assign(Object.assign({}, emptyUsage()), { costUsd: 3, cacheWriteTokens: 42 });
|
|
117
|
+
assert.equal(addUsage(a, emptyUsage()), a);
|
|
118
|
+
assert.equal(addUsage(emptyUsage(), a), a);
|
|
119
|
+
});
|
|
120
|
+
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.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.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.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/index.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/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/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/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/tool-fold.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.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.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.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/cost-session-banking.test.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/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/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/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/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.
|
|
4
|
+
"version": "15.5.0",
|
|
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.
|
|
77
|
-
"@genesislcap/genx": "15.
|
|
78
|
-
"@genesislcap/rollup-builder": "15.
|
|
79
|
-
"@genesislcap/ts-builder": "15.
|
|
80
|
-
"@genesislcap/uvu-playwright-builder": "15.
|
|
81
|
-
"@genesislcap/vite-builder": "15.
|
|
82
|
-
"@genesislcap/webpack-builder": "15.
|
|
76
|
+
"@genesislcap/foundation-testing": "15.5.0",
|
|
77
|
+
"@genesislcap/genx": "15.5.0",
|
|
78
|
+
"@genesislcap/rollup-builder": "15.5.0",
|
|
79
|
+
"@genesislcap/ts-builder": "15.5.0",
|
|
80
|
+
"@genesislcap/uvu-playwright-builder": "15.5.0",
|
|
81
|
+
"@genesislcap/vite-builder": "15.5.0",
|
|
82
|
+
"@genesislcap/webpack-builder": "15.5.0",
|
|
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.
|
|
89
|
-
"@genesislcap/foundation-logger": "15.
|
|
90
|
-
"@genesislcap/foundation-notifications": "15.
|
|
91
|
-
"@genesislcap/foundation-redux": "15.
|
|
92
|
-
"@genesislcap/foundation-ui": "15.
|
|
93
|
-
"@genesislcap/foundation-utils": "15.
|
|
94
|
-
"@genesislcap/rapid-design-system": "15.
|
|
95
|
-
"@genesislcap/web-core": "15.
|
|
88
|
+
"@genesislcap/foundation-ai": "15.5.0",
|
|
89
|
+
"@genesislcap/foundation-logger": "15.5.0",
|
|
90
|
+
"@genesislcap/foundation-notifications": "15.5.0",
|
|
91
|
+
"@genesislcap/foundation-redux": "15.5.0",
|
|
92
|
+
"@genesislcap/foundation-ui": "15.5.0",
|
|
93
|
+
"@genesislcap/foundation-utils": "15.5.0",
|
|
94
|
+
"@genesislcap/rapid-design-system": "15.5.0",
|
|
95
|
+
"@genesislcap/web-core": "15.5.0",
|
|
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": "
|
|
108
|
+
"gitHead": "be7121280e828345e79cca6a084b1025536813f2"
|
|
109
109
|
}
|
|
@@ -65,6 +65,7 @@ import {
|
|
|
65
65
|
normalizeForProvider,
|
|
66
66
|
} from '../../utils/history-transform';
|
|
67
67
|
import { logger } from '../../utils/logger';
|
|
68
|
+
import { sumUsage } from '../../utils/sum-usage';
|
|
68
69
|
import { TOOL_FOLD_SYMBOL, type ToolFold } from '../../utils/tool-fold';
|
|
69
70
|
import type { AiDriver, AllAgentSummary } from '../ai-driver/ai-driver';
|
|
70
71
|
|
|
@@ -1299,6 +1300,11 @@ export class ChatDriver extends EventTarget implements AiDriver {
|
|
|
1299
1300
|
tokensBefore,
|
|
1300
1301
|
model: status?.model,
|
|
1301
1302
|
createdAt,
|
|
1303
|
+
// Bank what the replaced turns spent. `toCompact` is dropped from history
|
|
1304
|
+
// below, taking its per-message cost/token fields with it — so a host total
|
|
1305
|
+
// re-derived from the transcript would otherwise fall by exactly this much
|
|
1306
|
+
// on every compaction. Summarising a turn does not refund what it cost.
|
|
1307
|
+
rolledUpUsage: sumUsage(toCompact),
|
|
1302
1308
|
},
|
|
1303
1309
|
};
|
|
1304
1310
|
|