@genesislcap/ai-assistant 15.11.0 → 15.12.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.
Files changed (50) hide show
  1. package/dist/ai-assistant.api.json +135 -0
  2. package/dist/ai-assistant.d.ts +75 -3
  3. package/dist/chat-driver.cjs +540 -117
  4. package/dist/chat-driver.cjs.map +4 -4
  5. package/dist/chat-driver.mjs +524 -116
  6. package/dist/chat-driver.mjs.map +4 -4
  7. package/dist/custom-elements.json +1822 -1483
  8. package/dist/dts/chat-driver-node.d.ts +5 -2
  9. package/dist/dts/chat-driver-node.d.ts.map +1 -1
  10. package/dist/dts/components/chat-driver/chat-driver.d.ts +20 -3
  11. package/dist/dts/components/chat-driver/chat-driver.d.ts.map +1 -1
  12. package/dist/dts/components/chat-driver/chat-driver.thinking-policy.test.d.ts +2 -0
  13. package/dist/dts/components/chat-driver/chat-driver.thinking-policy.test.d.ts.map +1 -0
  14. package/dist/dts/components/chat-driver/chat-driver.trace-capture.test.d.ts +2 -0
  15. package/dist/dts/components/chat-driver/chat-driver.trace-capture.test.d.ts.map +1 -0
  16. package/dist/dts/config/config.d.ts +39 -2
  17. package/dist/dts/config/config.d.ts.map +1 -1
  18. package/dist/dts/config/define-stateful-agent.d.ts +15 -1
  19. package/dist/dts/config/define-stateful-agent.d.ts.map +1 -1
  20. package/dist/dts/main/main.template.d.ts.map +1 -1
  21. package/dist/dts/utils/strip-agent-handlers.d.ts +1 -1
  22. package/dist/dts/utils/sum-usage.d.ts +37 -4
  23. package/dist/dts/utils/sum-usage.d.ts.map +1 -1
  24. package/dist/dts/utils/usage-rows.d.ts +102 -0
  25. package/dist/dts/utils/usage-rows.d.ts.map +1 -0
  26. package/dist/dts/utils/usage-rows.test.d.ts +2 -0
  27. package/dist/dts/utils/usage-rows.test.d.ts.map +1 -0
  28. package/dist/esm/chat-driver-node.js +36 -1
  29. package/dist/esm/components/chat-driver/chat-driver.js +73 -10
  30. package/dist/esm/components/chat-driver/chat-driver.thinking-policy.test.js +137 -0
  31. package/dist/esm/components/chat-driver/chat-driver.trace-capture.test.js +200 -0
  32. package/dist/esm/config/define-stateful-agent.js +11 -0
  33. package/dist/esm/main/main.template.js +20 -1
  34. package/dist/esm/utils/strip-agent-handlers.js +1 -1
  35. package/dist/esm/utils/sum-usage.js +37 -4
  36. package/dist/esm/utils/usage-rows.js +90 -0
  37. package/dist/esm/utils/usage-rows.test.js +189 -0
  38. package/dist/tsconfig.tsbuildinfo +1 -1
  39. package/package.json +17 -17
  40. package/src/chat-driver-node.ts +58 -0
  41. package/src/components/chat-driver/chat-driver.thinking-policy.test.ts +185 -0
  42. package/src/components/chat-driver/chat-driver.trace-capture.test.ts +251 -0
  43. package/src/components/chat-driver/chat-driver.ts +90 -10
  44. package/src/config/config.ts +50 -1
  45. package/src/config/define-stateful-agent.ts +37 -0
  46. package/src/main/main.template.ts +19 -1
  47. package/src/utils/strip-agent-handlers.ts +1 -1
  48. package/src/utils/sum-usage.ts +37 -4
  49. package/src/utils/usage-rows.test.ts +237 -0
  50. package/src/utils/usage-rows.ts +187 -0
@@ -0,0 +1,90 @@
1
+ import { vendorOfModel } from '@genesislcap/foundation-ai';
2
+ /** Whether a message carries anything a ledger would record. */
3
+ function hasUsage(m) {
4
+ return (m.cost != null ||
5
+ m.externalCostUsd != null ||
6
+ m.inputTokens != null ||
7
+ m.outputTokens != null ||
8
+ m.cacheReadTokens != null ||
9
+ m.cacheWriteTokens != null);
10
+ }
11
+ /**
12
+ * Project a transcript into one row per billable unit of spend.
13
+ *
14
+ * @remarks
15
+ * Reads the messages and returns a table; it stores nothing, mutates nothing, and
16
+ * adds nothing to history. Use it where an **aggregate is not enough** — writing
17
+ * usage rows for per-project attribution, a cost dashboard, or auditing which turns
18
+ * came back unpriced. For a single total, `sumUsage` is the answer and this is the
19
+ * wrong tool.
20
+ *
21
+ * Walks the same shape `sumUsage` does, and is guaranteed to agree with it: summing
22
+ * `costUsd` and `externalCostUsd` across these rows equals `sumUsage(...).costUsd`
23
+ * over the same input, and the four token buckets reconcile likewise. That property
24
+ * is the point of the function — a per-call view that quietly disagrees with the
25
+ * total is worse than no per-call view, because both look right in isolation.
26
+ *
27
+ * Covers the two places spend hides:
28
+ *
29
+ * - **Sub-agent conversations.** A delegating tool call carries the child's entire
30
+ * conversation on `toolCall.subAgentTrace`, so a walk of top-level messages alone
31
+ * misses everything a delegating agent spent. A consumer that reimplemented this
32
+ * and omitted the branch reported $0.106 on a run that cost $0.234.
33
+ * - **Compactions.** A compaction deletes the messages it summarises, banking their
34
+ * spend on the summary. Those turns produce a single `source: 'compaction'` row.
35
+ *
36
+ * Messages carrying no usage at all (a user turn, a narration) produce no row.
37
+ *
38
+ * **Do not pass the result to `sumUsage`** — the types prevent it, and the reason is
39
+ * that both recurse, so a pre-flattened list would be counted twice. Both functions
40
+ * take raw history.
41
+ *
42
+ * @beta
43
+ */
44
+ export function usageRows(messages) {
45
+ return collectRows(messages, 0, undefined);
46
+ }
47
+ /** Map an already-bucketed {@link AggregateUsage} onto a row's token fields. */
48
+ function bucketsOf(usage) {
49
+ return {
50
+ uncachedInputTokens: usage.uncachedInputTokens,
51
+ cacheReadTokens: usage.cacheReadTokens,
52
+ cacheWriteTokens: usage.cacheWriteTokens,
53
+ outputTokens: usage.outputTokens,
54
+ };
55
+ }
56
+ function collectRows(messages, depth, subAgentOf) {
57
+ var _a, _b, _c, _d, _e, _f, _g;
58
+ const rows = [];
59
+ for (const m of messages) {
60
+ if (hasUsage(m)) {
61
+ const cacheReadTokens = (_a = m.cacheReadTokens) !== null && _a !== void 0 ? _a : 0;
62
+ const cacheWriteTokens = (_b = m.cacheWriteTokens) !== null && _b !== void 0 ? _b : 0;
63
+ rows.push(Object.assign(Object.assign(Object.assign(Object.assign(Object.assign(Object.assign({ source: 'request', model: m.model,
64
+ // The driver stamps `ChatMessage.provider` from the resolved provider's own
65
+ // status, so it is AUTHORITATIVE and covers vendors the model allowlists do
66
+ // not — a server-proxied `gpt-5` turn is `provider: 'openai'` on the message
67
+ // and unknown to `vendorOfModel`. Prefer it; fall back to resolving the model
68
+ // id only for messages persisted before the field existed. Still left
69
+ // undefined when neither knows, rather than guessed at.
70
+ provider: (_c = m.provider) !== null && _c !== void 0 ? _c : (m.model ? vendorOfModel(m.model) : undefined) }, (m.cost != null && { costUsd: m.cost })), (m.externalCostUsd != null && { externalCostUsd: m.externalCostUsd })), {
71
+ // Uncached input is the REMAINDER, matching `sumUsage`: `inputTokens` is the
72
+ // whole prompt and the cache fields break it down. Clamped for the same
73
+ // reason — hand-edited history must not drive a total negative.
74
+ uncachedInputTokens: Math.max(0, ((_d = m.inputTokens) !== null && _d !== void 0 ? _d : 0) - cacheReadTokens - cacheWriteTokens), cacheReadTokens,
75
+ cacheWriteTokens, outputTokens: (_e = m.outputTokens) !== null && _e !== void 0 ? _e : 0 }), (m.agentName != null && { agentName: m.agentName })), { subAgentDepth: depth }), (subAgentOf != null && { subAgentOf })));
76
+ }
77
+ // Spend the compaction banked on this summary's behalf. Its buckets are already
78
+ // disjoint (it is an `AggregateUsage`), so they map across without the
79
+ // subtraction a per-message record needs.
80
+ const rolled = (_f = m.compaction) === null || _f === void 0 ? void 0 : _f.rolledUpUsage;
81
+ if (rolled) {
82
+ rows.push(Object.assign(Object.assign(Object.assign(Object.assign(Object.assign({ source: 'compaction' }, (rolled.costUsd != null && { costUsd: rolled.costUsd })), bucketsOf(rolled)), (m.agentName != null && { agentName: m.agentName })), { subAgentDepth: depth }), (subAgentOf != null && { subAgentOf })));
83
+ }
84
+ for (const tc of (_g = m.toolCalls) !== null && _g !== void 0 ? _g : []) {
85
+ if (tc.subAgentTrace)
86
+ rows.push(...collectRows(tc.subAgentTrace, depth + 1, tc.id));
87
+ }
88
+ }
89
+ return rows;
90
+ }
@@ -0,0 +1,189 @@
1
+ import { assert, createLogicSuite } from '@genesislcap/foundation-testing';
2
+ import { sumUsage } from './sum-usage';
3
+ import { usageRows } from './usage-rows';
4
+ const msg = (over) => (Object.assign({ role: 'assistant', content: '' }, over));
5
+ const rolled = (over) => (Object.assign({ costUsd: 0, uncachedInputTokens: 0, cacheReadTokens: 0, cacheWriteTokens: 0, outputTokens: 0 }, over));
6
+ /** Every place spend can hide, in one transcript: a delegation and a compaction. */
7
+ const richHistory = () => [
8
+ msg({
9
+ role: 'compacted-summary',
10
+ content: 'summary',
11
+ compaction: {
12
+ compactedCount: 4,
13
+ rolledUpUsage: rolled({ costUsd: 0.05, uncachedInputTokens: 400, outputTokens: 60 }),
14
+ // The summariser's model — NOT what the banked spend ran on.
15
+ model: 'claude-haiku-4-5-20251001',
16
+ },
17
+ }),
18
+ msg({ role: 'user', content: 'go' }),
19
+ msg({
20
+ model: 'claude-sonnet-5',
21
+ agentName: 'boss',
22
+ cost: 0.01,
23
+ inputTokens: 1000,
24
+ cacheReadTokens: 900,
25
+ outputTokens: 50,
26
+ toolCalls: [
27
+ {
28
+ id: 'tc1',
29
+ name: 'delegate',
30
+ args: {},
31
+ subAgentTrace: [
32
+ msg({
33
+ model: 'gemini-2.5-flash',
34
+ agentName: 'worker',
35
+ cost: 0.12,
36
+ inputTokens: 500,
37
+ outputTokens: 200,
38
+ }),
39
+ ],
40
+ },
41
+ ],
42
+ }),
43
+ ];
44
+ const suite = createLogicSuite('usageRows');
45
+ suite('reconciles exactly with sumUsage over the same input', () => {
46
+ // THE guarantee. A per-call view that disagrees with the total is worse than none,
47
+ // because both look right in isolation — which is how the consumer's own copy
48
+ // under-reported for a month.
49
+ const history = richHistory();
50
+ const rows = usageRows(history);
51
+ const total = sumUsage(history);
52
+ const rowCost = rows.reduce((n, r) => { var _a, _b; return n + ((_a = r.costUsd) !== null && _a !== void 0 ? _a : 0) + ((_b = r.externalCostUsd) !== null && _b !== void 0 ? _b : 0); }, 0);
53
+ assert.ok(Math.abs(rowCost - total.costUsd) < 1e-12, `${rowCost} vs ${total.costUsd}`);
54
+ const bucket = (k) => rows.reduce((n, r) => n + r[k], 0);
55
+ assert.is(bucket('uncachedInputTokens'), total.uncachedInputTokens, 'uncached input');
56
+ assert.is(bucket('cacheReadTokens'), total.cacheReadTokens, 'cache read');
57
+ assert.is(bucket('cacheWriteTokens'), total.cacheWriteTokens, 'cache write');
58
+ assert.is(bucket('outputTokens'), total.outputTokens, 'output');
59
+ });
60
+ suite('emits a compaction row so banked spend is not lost from a per-call view', () => {
61
+ // The shortfall this function exists to close: the summary message carries no
62
+ // `cost` and no token fields of its own, so a naive per-message walk drops the
63
+ // spend of every turn the compaction deleted.
64
+ const rows = usageRows(richHistory());
65
+ const compactionRows = rows.filter((r) => r.source === 'compaction');
66
+ assert.is(compactionRows.length, 1);
67
+ assert.is(compactionRows[0].costUsd, 0.05);
68
+ assert.is(compactionRows[0].uncachedInputTokens, 400);
69
+ assert.is(compactionRows[0].model, undefined, 'no model — the banked spend may span several, and compaction.model is the summariser');
70
+ });
71
+ suite('recurses into sub-agent traces, tagging depth and the spawning call', () => {
72
+ const rows = usageRows(richHistory());
73
+ const child = rows.find((r) => r.model === 'gemini-2.5-flash');
74
+ assert.ok(child, 'the sub-agent turn produced a row');
75
+ assert.is(child.subAgentDepth, 1);
76
+ assert.is(child.subAgentOf, 'tc1');
77
+ assert.is(child.agentName, 'worker');
78
+ const parent = rows.find((r) => r.model === 'claude-sonnet-5');
79
+ assert.is(parent.subAgentDepth, 0);
80
+ assert.is(parent.subAgentOf, undefined);
81
+ });
82
+ suite('prefers the provider the driver stamped over re-deriving it from the model', () => {
83
+ // `ChatMessage.provider` comes from the resolved provider's own status, so it is
84
+ // authoritative and knows vendors the model allowlists do not. Re-deriving from the
85
+ // model id alone drops attribution for a server-proxied turn — exactly the rows a
86
+ // usage ledger cares most about getting right. PR review.
87
+ const rows = usageRows([
88
+ msg({ model: 'gpt-5', provider: 'openai', cost: 1, inputTokens: 1 }),
89
+ // Stamped provider disagrees with what the model id would derive: the stamp still wins.
90
+ msg({
91
+ model: 'claude-sonnet-5',
92
+ provider: 'chrome',
93
+ cost: 1,
94
+ inputTokens: 1,
95
+ }),
96
+ ]);
97
+ assert.equal(rows.map((r) => r.provider), ['openai', 'chrome']);
98
+ });
99
+ suite('resolves the provider from the model, and leaves it unset when unknown', () => {
100
+ const rows = usageRows([
101
+ msg({ model: 'claude-sonnet-5', cost: 1, inputTokens: 1 }),
102
+ msg({ model: 'gemini-2.5-flash', cost: 1, inputTokens: 1 }),
103
+ msg({ model: 'gpt-5', cost: 1, inputTokens: 1 }),
104
+ msg({ cost: 1, inputTokens: 1 }),
105
+ ]);
106
+ assert.equal(rows.map((r) => r.provider), ['anthropic', 'gemini', undefined, undefined], 'an unrecognised or absent model leaves provider unset rather than guessing');
107
+ });
108
+ suite('splits the prompt into buckets the same way sumUsage does', () => {
109
+ const [row] = usageRows([
110
+ msg({
111
+ model: 'claude-sonnet-5',
112
+ inputTokens: 1000,
113
+ cacheReadTokens: 600,
114
+ cacheWriteTokens: 300,
115
+ }),
116
+ ]);
117
+ assert.is(row.uncachedInputTokens, 100, 'the remainder, not an addition');
118
+ assert.is(row.cacheReadTokens, 600);
119
+ assert.is(row.cacheWriteTokens, 300);
120
+ });
121
+ suite('clamps a prompt whose cache buckets exceed the total', () => {
122
+ const [row] = usageRows([msg({ inputTokens: 100, cacheReadTokens: 900 })]);
123
+ assert.is(row.uncachedInputTokens, 0, 'never negative');
124
+ });
125
+ suite('leaves costUsd undefined when nothing was reported, rather than zero', () => {
126
+ // What the "carried usage but no cost" audit keys off. A zero here would read as a
127
+ // free call and the alarm would never fire.
128
+ const [row] = usageRows([msg({ model: 'claude-sonnet-5', inputTokens: 500, outputTokens: 10 })]);
129
+ assert.is(row.costUsd, undefined);
130
+ assert.is(row.source, 'request');
131
+ });
132
+ suite('records external (non-LLM) cost and counts it in the reconciliation', () => {
133
+ const history = [msg({ externalCostUsd: 0.4 }), msg({ model: 'claude-sonnet-5', cost: 0.1 })];
134
+ const rows = usageRows(history);
135
+ assert.is(rows.find((r) => r.externalCostUsd != null).externalCostUsd, 0.4);
136
+ const rowCost = rows.reduce((n, r) => { var _a, _b; return n + ((_a = r.costUsd) !== null && _a !== void 0 ? _a : 0) + ((_b = r.externalCostUsd) !== null && _b !== void 0 ? _b : 0); }, 0);
137
+ assert.ok(Math.abs(rowCost - sumUsage(history).costUsd) < 1e-12);
138
+ });
139
+ suite('produces no row for a message carrying no usage', () => {
140
+ assert.equal(usageRows([
141
+ msg({ role: 'user', content: 'hello' }),
142
+ msg({ content: 'thinking out loud', category: 'reasoning' }),
143
+ ]), []);
144
+ });
145
+ suite('is empty for an empty transcript', () => {
146
+ assert.equal(usageRows([]), []);
147
+ });
148
+ suite('reconciles on a nested delegation two levels deep', () => {
149
+ const history = [
150
+ msg({
151
+ model: 'claude-sonnet-5',
152
+ cost: 0.01,
153
+ inputTokens: 100,
154
+ toolCalls: [
155
+ {
156
+ id: 'a',
157
+ name: 'delegate',
158
+ args: {},
159
+ subAgentTrace: [
160
+ msg({
161
+ model: 'claude-sonnet-5',
162
+ cost: 0.02,
163
+ inputTokens: 200,
164
+ toolCalls: [
165
+ {
166
+ id: 'b',
167
+ name: 'delegate',
168
+ args: {},
169
+ subAgentTrace: [msg({ model: 'gemini-2.5-flash', cost: 0.04, inputTokens: 400 })],
170
+ },
171
+ ],
172
+ }),
173
+ ],
174
+ },
175
+ ],
176
+ }),
177
+ ];
178
+ const rows = usageRows(history);
179
+ assert.equal(rows.map((r) => r.subAgentDepth), [0, 1, 2]);
180
+ const rowCost = rows.reduce((n, r) => { var _a; return n + ((_a = r.costUsd) !== null && _a !== void 0 ? _a : 0); }, 0);
181
+ assert.ok(Math.abs(rowCost - sumUsage(history).costUsd) < 1e-12, 'still reconciles');
182
+ });
183
+ suite('does not mutate the transcript', () => {
184
+ const history = richHistory();
185
+ const before = JSON.stringify(history);
186
+ usageRows(history);
187
+ assert.is(JSON.stringify(history), before, 'a projection, not a transform');
188
+ });
189
+ 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.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/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/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.11.0",
4
+ "version": "15.12.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.11.0",
77
- "@genesislcap/genx": "15.11.0",
78
- "@genesislcap/rollup-builder": "15.11.0",
79
- "@genesislcap/ts-builder": "15.11.0",
80
- "@genesislcap/uvu-playwright-builder": "15.11.0",
81
- "@genesislcap/vite-builder": "15.11.0",
82
- "@genesislcap/webpack-builder": "15.11.0",
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",
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.11.0",
89
- "@genesislcap/foundation-logger": "15.11.0",
90
- "@genesislcap/foundation-notifications": "15.11.0",
91
- "@genesislcap/foundation-redux": "15.11.0",
92
- "@genesislcap/foundation-ui": "15.11.0",
93
- "@genesislcap/foundation-utils": "15.11.0",
94
- "@genesislcap/rapid-design-system": "15.11.0",
95
- "@genesislcap/web-core": "15.11.0",
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",
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": "261d900881ea1ca2b613033af497ffd449868eb0"
108
+ "gitHead": "06f5ab51086c6616c6b96a7a1aebccddb3af7881"
109
109
  }
@@ -44,8 +44,33 @@ export {
44
44
  SUPPORTED_ANTHROPIC_MODEL_IDS,
45
45
  SUPPORTED_GEMINI_MODEL_IDS,
46
46
  isObservableAIProviderRegistry,
47
+ // Request pricing. Re-exported HERE, not left to a direct `@genesislcap/foundation-ai`
48
+ // import, for the same reason as everything above it: a headless host that adds its own
49
+ // foundation-ai dependency gets a second copy, and the driver's malformed/truncated
50
+ // handling does `instanceof` on the transports' error classes. Reaching the pricing
51
+ // should not cost a consumer that guarantee.
52
+ //
53
+ // These take RAW provider usage rather than a `ChatMessage` — see the foundation-ai
54
+ // module docs. They are for the path where nothing stamped a cost (a proxied or
55
+ // server-side call holding a usage block); when a message already carries `cost`, use
56
+ // it, because it was computed with the per-TTL cache split the message no longer has.
57
+ ANTHROPIC_CACHE_READ_MULTIPLIER,
58
+ ANTHROPIC_CACHE_WRITE_1H_MULTIPLIER,
59
+ ANTHROPIC_CACHE_WRITE_5M_MULTIPLIER,
60
+ anthropicRatesFor,
61
+ anthropicTokenCost,
62
+ GEMINI_CACHED_INPUT_MULTIPLIER,
63
+ GEMINI_LONG_CONTEXT_THRESHOLD,
64
+ geminiRatesFor,
65
+ geminiTokenCost,
66
+ // Resolves a model id to its vendor, so a caller holding only `message.model` can pick
67
+ // between the two cost functions above without maintaining its own model→vendor map.
68
+ vendorOfModel,
47
69
  } from '@genesislcap/foundation-ai';
48
70
  export type {
71
+ // Re-exported alongside `sumUsage` below — a consumer that can call the function but cannot NAME
72
+ // its return type has to restate the shape by hand, which is how the buckets get mis-summed.
73
+ AggregateUsage,
49
74
  AIProvider,
50
75
  AIProviderRegistry,
51
76
  AnthropicModelId,
@@ -53,12 +78,45 @@ export type {
53
78
  ChatFallback,
54
79
  ChatMessage,
55
80
  ChatRequestOptions,
81
+ ChatThinkingPolicy,
56
82
  ChatToolChoice,
57
83
  ChatToolDefinition,
58
84
  ChatToolHandlers,
59
85
  GeminiModelId,
86
+ // Pricing input/output shapes, alongside the functions re-exported above — a caller that
87
+ // can invoke them but cannot name their argument or result has to restate the shape by
88
+ // hand, which is how the token buckets get mixed up between the two providers.
89
+ AnthropicUsageRecord,
90
+ GeminiUsageRecord,
91
+ TokenCost,
92
+ TokenCostBreakdown,
93
+ TokenRates,
60
94
  } from '@genesislcap/foundation-ai';
61
95
 
96
+ // Usage/cost summation. A headless consumer attributing spend to a run has no other route to it:
97
+ // the transports' `getLifetimeCost()` is per-instance and lifetime-scoped, so a shared registry
98
+ // makes it unattributable, and re-deriving cost from token counts is wrong in the one direction
99
+ // that matters (`inputTokens` is total prompt size INCLUDING the cached part, so re-pricing charges
100
+ // every cached token at full rate). `sumUsage` reads the per-request cost the transports already
101
+ // stamped, and recurses into `toolCalls[].subAgentTrace` — where the entire spend of a delegating
102
+ // agent lives — and into `compaction.rolledUpUsage`, which is spend whose messages no longer exist.
103
+ // Both are invisible when omitted: the total is merely too low, which reads as a cheap run.
104
+ //
105
+ // `addUsage`/`emptyUsage` are the composition primitives for a conversation that CONTINUES across
106
+ // rounds — see the guidance on `sumUsage` itself. `totalTokens` exists because the four buckets of
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';
109
+
110
+ // Per-call projection, for the cases an aggregate cannot serve: usage rows for
111
+ // per-project attribution, a cost dashboard, or auditing which turns came back
112
+ // unpriced. `usageRows` is guaranteed to reconcile with `sumUsage` — including spend a
113
+ // compaction banked, which has no message of its own and which a naive per-message walk
114
+ // therefore drops while the total keeps it. Both take RAW history; `UsageRow` is
115
+ // deliberately not a `ChatMessage`, so passing rows back into `sumUsage` (which recurses
116
+ // itself, and would double-count) does not compile.
117
+ export { usageRows } from './utils/usage-rows';
118
+ export type { UsageRow } from './utils/usage-rows';
119
+
62
120
  // Debug-log harvesting (GENC-1461 unified diagnostics). A headless consumer collates its driver's
63
121
  // log — `buildTimelineEntries({ turnSnapshots: driver.getTurnSnapshots(), messages:
64
122
  // driver.getHistory(), metaEvents: getMetaEvents(sessionKey) })` → `DiagnosticEntry[]` — ships it to
@@ -0,0 +1,185 @@
1
+ import type {
2
+ AIProvider,
3
+ AIProviderRegistry,
4
+ ChatMessage,
5
+ ChatRequestOptions,
6
+ ChatThinkingPolicy,
7
+ } from '@genesislcap/foundation-ai';
8
+ import { assert, createLogicSuite } from '@genesislcap/foundation-testing';
9
+ import type { AgentConfig } from '../../config/config';
10
+ // Side-effect import — MUST come before `./chat-driver` so the driver subclasses
11
+ // jsdom's EventTarget rather than Node's native one. Mirrors chat-driver.test.ts.
12
+ import './align-event-globals';
13
+ import { ChatDriver } from './chat-driver';
14
+
15
+ // ---------------------------------------------------------------------------
16
+ // Per-turn `thinkingPolicy` resolution.
17
+ //
18
+ // The cost case for this option is the SHAPE of a tool loop, not a single call: the
19
+ // opening turn is a real decision and worth reasoning over, while the iterations that
20
+ // follow mostly pick the next tool from a narrow set — and on a thinking model each of
21
+ // those bills reasoning at the full output rate. That saving only exists if the
22
+ // resolver runs per iteration, so these tests assert the sequence across a loop, not
23
+ // just that one value arrives.
24
+ //
25
+ // The other half is the undefined case. Every agent written before this option leaves
26
+ // it unset, and a resolver may answer `undefined` on any given turn; both must reach
27
+ // the transport as `undefined` so the model keeps its own default. `undefined` reaching
28
+ // the wire as `'off'` would be a silent capability regression, and as `'auto'` a silent
29
+ // bill increase — so it is asserted explicitly rather than assumed.
30
+ // ---------------------------------------------------------------------------
31
+
32
+ /** Captures the policy seen on each model call; calls one tool, then finishes. */
33
+ const capturingProvider = (): AIProvider & { seen: Array<ChatThinkingPolicy | undefined> } => {
34
+ const seen: Array<ChatThinkingPolicy | undefined> = [];
35
+ let turns = 0;
36
+ return {
37
+ seen,
38
+ chat: async (
39
+ _history: ChatMessage[],
40
+ _userMessage: string,
41
+ options?: ChatRequestOptions,
42
+ ): Promise<ChatMessage> => {
43
+ seen.push(options?.thinkingPolicy);
44
+ turns += 1;
45
+ // Two tool calls, so the loop runs three model calls in total — enough for a
46
+ // per-turn resolver to say something different on the later ones.
47
+ if (turns <= 2) {
48
+ return {
49
+ role: 'assistant',
50
+ content: '',
51
+ toolCalls: [{ id: `t${turns}`, name: 'step', args: {} }],
52
+ };
53
+ }
54
+ return { role: 'assistant', content: 'done' };
55
+ },
56
+ };
57
+ };
58
+
59
+ const makeRegistry = (provider: AIProvider): AIProviderRegistry => ({
60
+ get: () => provider,
61
+ default: () => provider,
62
+ defaultName: () => 'test',
63
+ names: () => ['test'],
64
+ getStatus: async () => null,
65
+ listStatuses: async () => [],
66
+ });
67
+
68
+ const agent = (overrides: Partial<AgentConfig>): AgentConfig =>
69
+ ({
70
+ name: 'worker',
71
+ description: 'test agent',
72
+ toolDefinitions: [
73
+ { name: 'step', description: 'step', parameters: { type: 'object', properties: {} } },
74
+ ],
75
+ toolHandlers: { step: async () => 'stepped' },
76
+ ...overrides,
77
+ }) as AgentConfig;
78
+
79
+ /** Run one user turn through a driver carrying `config`, and return the policies seen. */
80
+ const policiesFor = async (
81
+ config: Partial<AgentConfig>,
82
+ ): Promise<Array<ChatThinkingPolicy | undefined>> => {
83
+ const provider = capturingProvider();
84
+ const driver = new ChatDriver(makeRegistry(provider), {
85
+ maxToolIterations: 10,
86
+ maxFoldOperations: 5,
87
+ sessionKey: '',
88
+ });
89
+ driver.applyAgent(agent(config));
90
+ await driver.sendMessage('go');
91
+ return provider.seen;
92
+ };
93
+
94
+ const suite = createLogicSuite('ChatDriver thinkingPolicy');
95
+
96
+ suite('leaves the policy undefined when the agent does not set one', async () => {
97
+ // The compatibility case: every existing agent. Undefined must reach the transport
98
+ // as undefined so each model keeps its own default posture — not silently coerced
99
+ // to 'off' (a capability regression) or 'auto' (a bill increase).
100
+ const seen = await policiesFor({});
101
+ assert.ok(seen.length >= 3, `expected a multi-call loop, got ${seen.length}`);
102
+ assert.equal(
103
+ seen.filter((p) => p !== undefined),
104
+ [],
105
+ 'no turn invents a policy',
106
+ );
107
+ });
108
+
109
+ suite('applies a static policy to every turn of the loop', async () => {
110
+ const seen = await policiesFor({ thinkingPolicy: 'off' });
111
+ assert.ok(seen.length >= 3);
112
+ assert.equal([...new Set(seen)], ['off'], 'a static value is not just a first-turn setting');
113
+ });
114
+
115
+ suite('pins the resolved policy for the whole tool loop', async () => {
116
+ // A tool-use loop is ONE assistant turn and Anthropic requires a single thinking mode
117
+ // for its duration. Toggling part-way does not error — the API silently disables
118
+ // thinking for that request and strips blocks that would leave the turn structure
119
+ // invalid, so an 'auto' -> 'off' switch loses the continuity the opening call
120
+ // established and 'off' -> 'auto' never delivers the reasoning asked for. It also
121
+ // invalidates the prompt cache, costing more than the reasoning it meant to save.
122
+ //
123
+ // So a resolver that changes its mind mid-loop must NOT be honoured mid-loop. This
124
+ // asserts the opposite of what it looks like it should: the later values are ignored.
125
+ let call = 0;
126
+ const seen = await policiesFor({
127
+ thinkingPolicy: () => {
128
+ call += 1;
129
+ return call === 1 ? 'auto' : 'off';
130
+ },
131
+ });
132
+ assert.ok(seen.length >= 3, `expected a multi-call loop, got ${seen.length}`);
133
+ assert.equal(
134
+ [...new Set(seen)],
135
+ ['auto'],
136
+ 'the first call decides; later resolutions do not take effect until the next user turn',
137
+ );
138
+ });
139
+
140
+ suite('re-resolves on the next user turn', async () => {
141
+ // The flip side: pinning is per turn, not for the driver's lifetime, so a state change
142
+ // between turns still lands.
143
+ const provider = capturingProvider();
144
+ const driver = new ChatDriver(makeRegistry(provider), {
145
+ maxToolIterations: 10,
146
+ maxFoldOperations: 5,
147
+ sessionKey: '',
148
+ });
149
+ let turn = 0;
150
+ driver.applyAgent(
151
+ agent({
152
+ thinkingPolicy: () => (turn === 0 ? 'auto' : 'off'),
153
+ }),
154
+ );
155
+ await driver.sendMessage('go');
156
+ const firstTurn = [...provider.seen];
157
+ turn = 1;
158
+ await driver.sendMessage('again');
159
+ const secondTurn = provider.seen.slice(firstTurn.length);
160
+
161
+ assert.equal([...new Set(firstTurn)], ['auto'], 'turn one holds its posture');
162
+ assert.equal([...new Set(secondTurn)], ['off'], 'turn two picks up the new one');
163
+ });
164
+
165
+ suite('passes undefined through when the resolver declines to choose', async () => {
166
+ // A resolver may answer on some turns and not others. `undefined` is its third answer —
167
+ // "leave this model alone" — and must not be normalised into a value. Asserted across
168
+ // the whole loop because the first call's answer is the one that gets pinned.
169
+ const seen = await policiesFor({ thinkingPolicy: () => undefined });
170
+ assert.ok(seen.length >= 3);
171
+ assert.equal(
172
+ seen.filter((p) => p !== undefined),
173
+ [],
174
+ 'declining is not the same as choosing',
175
+ );
176
+ });
177
+
178
+ suite('awaits an async resolver', async () => {
179
+ const seen = await policiesFor({
180
+ thinkingPolicy: async () => 'off' as ChatThinkingPolicy,
181
+ });
182
+ assert.equal([...new Set(seen)], ['off'], 'a promise is resolved, not passed through');
183
+ });
184
+
185
+ suite.run();