@genesislcap/ai-assistant 15.19.0 → 15.19.1
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/package.json +17 -18
- package/api-extractor.json +0 -11
- package/dist/dts/components/chat-driver/chat-driver.compact.test.d.ts +0 -2
- package/dist/dts/components/chat-driver/chat-driver.compact.test.d.ts.map +0 -1
- package/dist/dts/components/chat-driver/chat-driver.invocation-scope.test.d.ts +0 -2
- package/dist/dts/components/chat-driver/chat-driver.invocation-scope.test.d.ts.map +0 -1
- package/dist/dts/components/chat-driver/chat-driver.test.d.ts +0 -2
- package/dist/dts/components/chat-driver/chat-driver.test.d.ts.map +0 -1
- package/dist/dts/components/chat-driver/chat-driver.thinking-policy.test.d.ts +0 -2
- package/dist/dts/components/chat-driver/chat-driver.thinking-policy.test.d.ts.map +0 -1
- package/dist/dts/components/chat-driver/chat-driver.trace-capture.test.d.ts +0 -2
- package/dist/dts/components/chat-driver/chat-driver.trace-capture.test.d.ts.map +0 -1
- package/dist/dts/components/chat-driver/chat-driver.turn-usage.test.d.ts +0 -2
- package/dist/dts/components/chat-driver/chat-driver.turn-usage.test.d.ts.map +0 -1
- package/dist/dts/components/chat-interaction-wrapper/chat-interaction-wrapper.test.d.ts +0 -2
- package/dist/dts/components/chat-interaction-wrapper/chat-interaction-wrapper.test.d.ts.map +0 -1
- package/dist/dts/components/orchestrating-driver/orchestrating-driver.budget.test.d.ts +0 -2
- package/dist/dts/components/orchestrating-driver/orchestrating-driver.budget.test.d.ts.map +0 -1
- package/dist/dts/components/orchestrating-driver/orchestrating-driver.cost.test.d.ts +0 -2
- package/dist/dts/components/orchestrating-driver/orchestrating-driver.cost.test.d.ts.map +0 -1
- package/dist/dts/components/orchestrating-driver/orchestrating-driver.pin.test.d.ts +0 -2
- package/dist/dts/components/orchestrating-driver/orchestrating-driver.pin.test.d.ts.map +0 -1
- package/dist/dts/components/settings-modal/settings-modal.styles.test.d.ts +0 -2
- package/dist/dts/components/settings-modal/settings-modal.styles.test.d.ts.map +0 -1
- package/dist/dts/components/settings-modal/settings-modal.template.test.d.ts +0 -2
- package/dist/dts/components/settings-modal/settings-modal.template.test.d.ts.map +0 -1
- package/dist/dts/config/define-stateful-agent.test.d.ts +0 -2
- package/dist/dts/config/define-stateful-agent.test.d.ts.map +0 -1
- package/dist/dts/config/validate-providers.test.d.ts +0 -2
- package/dist/dts/config/validate-providers.test.d.ts.map +0 -1
- package/dist/dts/main/blocked-state.test.d.ts +0 -2
- package/dist/dts/main/blocked-state.test.d.ts.map +0 -1
- package/dist/dts/main/budget-meter.test.d.ts +0 -2
- package/dist/dts/main/budget-meter.test.d.ts.map +0 -1
- package/dist/dts/main/cost-session-banking.test.d.ts +0 -2
- package/dist/dts/main/cost-session-banking.test.d.ts.map +0 -1
- package/dist/dts/main/file-attachments.test.d.ts +0 -2
- package/dist/dts/main/file-attachments.test.d.ts.map +0 -1
- package/dist/dts/main/interaction-cost.test.d.ts +0 -2
- package/dist/dts/main/interaction-cost.test.d.ts.map +0 -1
- package/dist/dts/main/main.styles.test.d.ts +0 -2
- package/dist/dts/main/main.styles.test.d.ts.map +0 -1
- package/dist/dts/main/persistence-broken-sources.test.d.ts +0 -2
- package/dist/dts/main/persistence-broken-sources.test.d.ts.map +0 -1
- package/dist/dts/main/popout-interaction-gate.test.d.ts +0 -2
- package/dist/dts/main/popout-interaction-gate.test.d.ts.map +0 -1
- package/dist/dts/state/ai-assistant-slice.test.d.ts +0 -2
- package/dist/dts/state/ai-assistant-slice.test.d.ts.map +0 -1
- package/dist/dts/state/debug-event-log.test.d.ts +0 -2
- package/dist/dts/state/debug-event-log.test.d.ts.map +0 -1
- package/dist/dts/state/driver-registry.test.d.ts +0 -2
- package/dist/dts/state/driver-registry.test.d.ts.map +0 -1
- package/dist/dts/state/interaction-context.test.d.ts +0 -2
- package/dist/dts/state/interaction-context.test.d.ts.map +0 -1
- package/dist/dts/state/persistence/diagnostics-cursors.test.d.ts +0 -2
- package/dist/dts/state/persistence/diagnostics-cursors.test.d.ts.map +0 -1
- package/dist/dts/state/persistence/diagnostics.test.d.ts +0 -2
- package/dist/dts/state/persistence/diagnostics.test.d.ts.map +0 -1
- package/dist/dts/state/persistence/session-persistence-provider.test.d.ts +0 -2
- package/dist/dts/state/persistence/session-persistence-provider.test.d.ts.map +0 -1
- package/dist/dts/state/persistence/session-persistence.integration.test.d.ts +0 -2
- package/dist/dts/state/persistence/session-persistence.integration.test.d.ts.map +0 -1
- package/dist/dts/state/persistence/session-persister.test.d.ts +0 -2
- package/dist/dts/state/persistence/session-persister.test.d.ts.map +0 -1
- package/dist/dts/state/persistence/session-snapshot.test.d.ts +0 -2
- package/dist/dts/state/persistence/session-snapshot.test.d.ts.map +0 -1
- package/dist/dts/state/persistence/stateful-restore.e2e.test.d.ts +0 -2
- package/dist/dts/state/persistence/stateful-restore.e2e.test.d.ts.map +0 -1
- package/dist/dts/utils/animation-exclusivity.test.d.ts +0 -2
- package/dist/dts/utils/animation-exclusivity.test.d.ts.map +0 -1
- package/dist/dts/utils/collect-session-models.test.d.ts +0 -2
- package/dist/dts/utils/collect-session-models.test.d.ts.map +0 -1
- package/dist/dts/utils/condense-history.test.d.ts +0 -2
- package/dist/dts/utils/condense-history.test.d.ts.map +0 -1
- package/dist/dts/utils/cost-session-history.test.d.ts +0 -2
- package/dist/dts/utils/cost-session-history.test.d.ts.map +0 -1
- package/dist/dts/utils/derive-cost-session-title.test.d.ts +0 -2
- package/dist/dts/utils/derive-cost-session-title.test.d.ts.map +0 -1
- package/dist/dts/utils/flatten-sub-agent-messages.test.d.ts +0 -2
- package/dist/dts/utils/flatten-sub-agent-messages.test.d.ts.map +0 -1
- package/dist/dts/utils/history-transform.test.d.ts +0 -2
- package/dist/dts/utils/history-transform.test.d.ts.map +0 -1
- package/dist/dts/utils/message-partition.test.d.ts +0 -2
- package/dist/dts/utils/message-partition.test.d.ts.map +0 -1
- package/dist/dts/utils/resolve-cost-history-config.test.d.ts +0 -2
- package/dist/dts/utils/resolve-cost-history-config.test.d.ts.map +0 -1
- package/dist/dts/utils/resolve-preference-baseline.test.d.ts +0 -2
- package/dist/dts/utils/resolve-preference-baseline.test.d.ts.map +0 -1
- package/dist/dts/utils/strip-agent-handlers.test.d.ts +0 -2
- package/dist/dts/utils/strip-agent-handlers.test.d.ts.map +0 -1
- package/dist/dts/utils/sum-costs.test.d.ts +0 -2
- package/dist/dts/utils/sum-costs.test.d.ts.map +0 -1
- package/dist/dts/utils/sum-tokens.test.d.ts +0 -2
- package/dist/dts/utils/sum-tokens.test.d.ts.map +0 -1
- package/dist/dts/utils/sum-usage.test.d.ts +0 -2
- package/dist/dts/utils/sum-usage.test.d.ts.map +0 -1
- package/dist/dts/utils/usage-rows.test.d.ts +0 -2
- package/dist/dts/utils/usage-rows.test.d.ts.map +0 -1
- package/dist/esm/components/chat-driver/chat-driver.compact.test.js +0 -93
- package/dist/esm/components/chat-driver/chat-driver.invocation-scope.test.js +0 -238
- package/dist/esm/components/chat-driver/chat-driver.test.js +0 -2770
- package/dist/esm/components/chat-driver/chat-driver.thinking-policy.test.js +0 -137
- package/dist/esm/components/chat-driver/chat-driver.trace-capture.test.js +0 -200
- package/dist/esm/components/chat-driver/chat-driver.turn-usage.test.js +0 -268
- package/dist/esm/components/chat-interaction-wrapper/chat-interaction-wrapper.test.js +0 -104
- package/dist/esm/components/orchestrating-driver/orchestrating-driver.budget.test.js +0 -312
- package/dist/esm/components/orchestrating-driver/orchestrating-driver.cost.test.js +0 -76
- package/dist/esm/components/orchestrating-driver/orchestrating-driver.pin.test.js +0 -61
- package/dist/esm/components/settings-modal/settings-modal.styles.test.js +0 -80
- package/dist/esm/components/settings-modal/settings-modal.template.test.js +0 -91
- package/dist/esm/config/define-stateful-agent.test.js +0 -53
- package/dist/esm/config/validate-providers.test.js +0 -100
- package/dist/esm/main/blocked-state.test.js +0 -1065
- package/dist/esm/main/budget-meter.test.js +0 -359
- package/dist/esm/main/cost-session-banking.test.js +0 -538
- package/dist/esm/main/file-attachments.test.js +0 -164
- package/dist/esm/main/interaction-cost.test.js +0 -107
- package/dist/esm/main/main.styles.test.js +0 -161
- package/dist/esm/main/persistence-broken-sources.test.js +0 -180
- package/dist/esm/main/popout-interaction-gate.test.js +0 -54
- package/dist/esm/state/ai-assistant-slice.test.js +0 -229
- package/dist/esm/state/debug-event-log.test.js +0 -117
- package/dist/esm/state/driver-registry.test.js +0 -55
- package/dist/esm/state/interaction-context.test.js +0 -78
- package/dist/esm/state/persistence/diagnostics-cursors.test.js +0 -30
- package/dist/esm/state/persistence/diagnostics.test.js +0 -280
- package/dist/esm/state/persistence/session-persistence-provider.test.js +0 -110
- package/dist/esm/state/persistence/session-persistence.integration.test.js +0 -113
- package/dist/esm/state/persistence/session-persister.test.js +0 -613
- package/dist/esm/state/persistence/session-snapshot.test.js +0 -256
- package/dist/esm/state/persistence/stateful-restore.e2e.test.js +0 -158
- package/dist/esm/utils/animation-exclusivity.test.js +0 -52
- package/dist/esm/utils/collect-session-models.test.js +0 -53
- package/dist/esm/utils/condense-history.test.js +0 -547
- package/dist/esm/utils/cost-session-history.test.js +0 -174
- package/dist/esm/utils/derive-cost-session-title.test.js +0 -37
- package/dist/esm/utils/flatten-sub-agent-messages.test.js +0 -139
- package/dist/esm/utils/history-transform.test.js +0 -158
- package/dist/esm/utils/message-partition.test.js +0 -90
- package/dist/esm/utils/resolve-cost-history-config.test.js +0 -32
- package/dist/esm/utils/resolve-preference-baseline.test.js +0 -69
- package/dist/esm/utils/strip-agent-handlers.test.js +0 -81
- package/dist/esm/utils/sum-costs.test.js +0 -114
- package/dist/esm/utils/sum-tokens.test.js +0 -51
- package/dist/esm/utils/sum-usage.test.js +0 -164
- package/dist/esm/utils/usage-rows.test.js +0 -189
- package/dist/tsconfig.tsbuildinfo +0 -1
- package/license.txt +0 -46
- package/sandbox/tsconfig.json +0 -8
- package/src/channel/ai-activity-bus.ts +0 -209
- package/src/channel/ai-activity-channel.ts +0 -146
- package/src/chat-driver-node.ts +0 -159
- package/src/components/activity-halo/activity-halo.ts +0 -106
- package/src/components/agent-picker/agent-picker.constants.ts +0 -2
- package/src/components/agent-picker/agent-picker.styles.ts +0 -24
- package/src/components/agent-picker/agent-picker.template.ts +0 -54
- package/src/components/agent-picker/agent-picker.ts +0 -99
- package/src/components/agent-picker/index.ts +0 -1
- package/src/components/ai-driver/ai-driver.ts +0 -166
- package/src/components/ai-driver/index.ts +0 -1
- package/src/components/chat-bubble/chat-bubble.styles.ts +0 -194
- package/src/components/chat-bubble/chat-bubble.template.ts +0 -67
- package/src/components/chat-bubble/chat-bubble.ts +0 -436
- package/src/components/chat-bubble/index.ts +0 -1
- package/src/components/chat-driver/align-event-globals.ts +0 -23
- package/src/components/chat-driver/chat-driver.compact.test.ts +0 -131
- package/src/components/chat-driver/chat-driver.invocation-scope.test.ts +0 -285
- package/src/components/chat-driver/chat-driver.test.ts +0 -3679
- package/src/components/chat-driver/chat-driver.thinking-policy.test.ts +0 -185
- package/src/components/chat-driver/chat-driver.trace-capture.test.ts +0 -251
- package/src/components/chat-driver/chat-driver.ts +0 -3925
- package/src/components/chat-driver/chat-driver.turn-usage.test.ts +0 -362
- package/src/components/chat-driver/index.ts +0 -1
- package/src/components/chat-interaction-wrapper/chat-interaction-wrapper.styles.ts +0 -7
- package/src/components/chat-interaction-wrapper/chat-interaction-wrapper.template.ts +0 -6
- package/src/components/chat-interaction-wrapper/chat-interaction-wrapper.test.ts +0 -144
- package/src/components/chat-interaction-wrapper/chat-interaction-wrapper.ts +0 -243
- package/src/components/chat-interaction-wrapper/index.ts +0 -3
- package/src/components/chat-markdown/chat-markdown.ts +0 -89
- package/src/components/chat-markdown/index.ts +0 -1
- package/src/components/flowing-waves-indicator.ts +0 -286
- package/src/components/halo-overlay.ts +0 -184
- package/src/components/orchestrating-driver/index.ts +0 -1
- package/src/components/orchestrating-driver/orchestrating-driver.budget.test.ts +0 -438
- package/src/components/orchestrating-driver/orchestrating-driver.cost.test.ts +0 -107
- package/src/components/orchestrating-driver/orchestrating-driver.pin.test.ts +0 -96
- package/src/components/orchestrating-driver/orchestrating-driver.ts +0 -813
- package/src/components/plasma-orb-indicator.ts +0 -281
- package/src/components/popout-manager/index.ts +0 -1
- package/src/components/popout-manager/popout-manager.ts +0 -212
- package/src/components/settings-modal/index.ts +0 -2
- package/src/components/settings-modal/settings-modal.styles.test.ts +0 -94
- package/src/components/settings-modal/settings-modal.styles.ts +0 -853
- package/src/components/settings-modal/settings-modal.template.test.ts +0 -124
- package/src/components/settings-modal/settings-modal.template.ts +0 -943
- package/src/components/waves-indicator.ts +0 -241
- package/src/config/config.ts +0 -608
- package/src/config/define-stateful-agent.test.ts +0 -64
- package/src/config/define-stateful-agent.ts +0 -642
- package/src/config/fallback-agents.ts +0 -29
- package/src/config/index.ts +0 -2
- package/src/config/validate-providers.test.ts +0 -148
- package/src/config/validate-providers.ts +0 -58
- package/src/index.ts +0 -29
- package/src/main/blocked-state.test.ts +0 -1444
- package/src/main/budget-meter.test.ts +0 -477
- package/src/main/cost-session-banking.test.ts +0 -679
- package/src/main/file-attachments.test.ts +0 -215
- package/src/main/index.ts +0 -3
- package/src/main/interaction-cost.test.ts +0 -140
- package/src/main/main.styles.test.ts +0 -216
- package/src/main/main.styles.ts +0 -995
- package/src/main/main.template.ts +0 -1119
- package/src/main/main.ts +0 -5432
- package/src/main/main.types.ts +0 -225
- package/src/main/persistence-broken-sources.test.ts +0 -219
- package/src/main/popout-interaction-gate.test.ts +0 -87
- package/src/provider/ai-provider-switcher.ts +0 -40
- package/src/provider/assistant-app-settings.ts +0 -84
- package/src/state/ai-assistant-slice.test.ts +0 -336
- package/src/state/ai-assistant-slice.ts +0 -604
- package/src/state/debug-event-log.test.ts +0 -154
- package/src/state/debug-event-log.ts +0 -358
- package/src/state/driver-registry.test.ts +0 -70
- package/src/state/driver-registry.ts +0 -79
- package/src/state/interaction-context.test.ts +0 -90
- package/src/state/interaction-context.ts +0 -101
- package/src/state/persistence/build-timeline-entries.ts +0 -66
- package/src/state/persistence/diagnostics-cursors.test.ts +0 -53
- package/src/state/persistence/diagnostics-cursors.ts +0 -80
- package/src/state/persistence/diagnostics.test.ts +0 -346
- package/src/state/persistence/diagnostics.ts +0 -220
- package/src/state/persistence/index.ts +0 -10
- package/src/state/persistence/persister-registry.ts +0 -55
- package/src/state/persistence/session-persistence-provider.test.ts +0 -136
- package/src/state/persistence/session-persistence-provider.ts +0 -226
- package/src/state/persistence/session-persistence.integration.test.ts +0 -141
- package/src/state/persistence/session-persister.test.ts +0 -787
- package/src/state/persistence/session-persister.ts +0 -917
- package/src/state/persistence/session-snapshot.test.ts +0 -352
- package/src/state/persistence/session-snapshot.ts +0 -344
- package/src/state/persistence/stateful-restore.e2e.test.ts +0 -229
- package/src/state/session-store.ts +0 -56
- package/src/styles/ai-colours.ts +0 -30
- package/src/styles/menu.ts +0 -187
- package/src/styles/settings-section.ts +0 -40
- package/src/suggestions/chat-suggestions.ts +0 -186
- package/src/tags/index.ts +0 -1
- package/src/types/ai-chat-widget.ts +0 -67
- package/src/types/interaction-context.ts +0 -82
- package/src/utils/animated-panel-toggle.ts +0 -62
- package/src/utils/animation-exclusivity.test.ts +0 -72
- package/src/utils/animation-exclusivity.ts +0 -40
- package/src/utils/banked-usage-baselines.ts +0 -90
- package/src/utils/collect-session-models.test.ts +0 -82
- package/src/utils/collect-session-models.ts +0 -65
- package/src/utils/condense-history.test.ts +0 -746
- package/src/utils/condense-history.ts +0 -448
- package/src/utils/cost-session-history.test.ts +0 -226
- package/src/utils/cost-session-history.ts +0 -218
- package/src/utils/derive-cost-session-title.test.ts +0 -63
- package/src/utils/derive-cost-session-title.ts +0 -76
- package/src/utils/flatten-sub-agent-messages.test.ts +0 -163
- package/src/utils/flatten-sub-agent-messages.ts +0 -88
- package/src/utils/format-usd.ts +0 -26
- package/src/utils/history-transform.test.ts +0 -228
- package/src/utils/history-transform.ts +0 -163
- package/src/utils/index.ts +0 -3
- package/src/utils/logger.ts +0 -3
- package/src/utils/message-partition.test.ts +0 -129
- package/src/utils/message-partition.ts +0 -115
- package/src/utils/resolve-cost-history-config.test.ts +0 -38
- package/src/utils/resolve-cost-history-config.ts +0 -38
- package/src/utils/resolve-preference-baseline.test.ts +0 -84
- package/src/utils/resolve-preference-baseline.ts +0 -80
- package/src/utils/strip-agent-handlers.test.ts +0 -99
- package/src/utils/strip-agent-handlers.ts +0 -53
- package/src/utils/sum-costs.test.ts +0 -136
- package/src/utils/sum-costs.ts +0 -18
- package/src/utils/sum-tokens.test.ts +0 -93
- package/src/utils/sum-tokens.ts +0 -68
- package/src/utils/sum-usage.test.ts +0 -191
- package/src/utils/sum-usage.ts +0 -208
- package/src/utils/tool-fold.ts +0 -181
- package/src/utils/usage-rows.test.ts +0 -237
- package/src/utils/usage-rows.ts +0 -187
- package/src/utils/with-timeout.ts +0 -24
- package/tsconfig.json +0 -9
|
@@ -1,237 +0,0 @@
|
|
|
1
|
-
import type { AggregateUsage, ChatMessage } from '@genesislcap/foundation-ai';
|
|
2
|
-
import { assert, createLogicSuite } from '@genesislcap/foundation-testing';
|
|
3
|
-
import { sumUsage } from './sum-usage';
|
|
4
|
-
import { usageRows } from './usage-rows';
|
|
5
|
-
|
|
6
|
-
const msg = (over: Partial<ChatMessage>): ChatMessage =>
|
|
7
|
-
({ role: 'assistant', content: '', ...over }) as ChatMessage;
|
|
8
|
-
|
|
9
|
-
const rolled = (over: Partial<AggregateUsage>): AggregateUsage => ({
|
|
10
|
-
costUsd: 0,
|
|
11
|
-
uncachedInputTokens: 0,
|
|
12
|
-
cacheReadTokens: 0,
|
|
13
|
-
cacheWriteTokens: 0,
|
|
14
|
-
outputTokens: 0,
|
|
15
|
-
...over,
|
|
16
|
-
});
|
|
17
|
-
|
|
18
|
-
/** Every place spend can hide, in one transcript: a delegation and a compaction. */
|
|
19
|
-
const richHistory = (): ChatMessage[] => [
|
|
20
|
-
msg({
|
|
21
|
-
role: 'compacted-summary',
|
|
22
|
-
content: 'summary',
|
|
23
|
-
compaction: {
|
|
24
|
-
compactedCount: 4,
|
|
25
|
-
rolledUpUsage: rolled({ costUsd: 0.05, uncachedInputTokens: 400, outputTokens: 60 }),
|
|
26
|
-
// The summariser's model — NOT what the banked spend ran on.
|
|
27
|
-
model: 'claude-haiku-4-5-20251001',
|
|
28
|
-
},
|
|
29
|
-
} as Partial<ChatMessage>),
|
|
30
|
-
msg({ role: 'user', content: 'go' }),
|
|
31
|
-
msg({
|
|
32
|
-
model: 'claude-sonnet-5',
|
|
33
|
-
agentName: 'boss',
|
|
34
|
-
cost: 0.01,
|
|
35
|
-
inputTokens: 1_000,
|
|
36
|
-
cacheReadTokens: 900,
|
|
37
|
-
outputTokens: 50,
|
|
38
|
-
toolCalls: [
|
|
39
|
-
{
|
|
40
|
-
id: 'tc1',
|
|
41
|
-
name: 'delegate',
|
|
42
|
-
args: {},
|
|
43
|
-
subAgentTrace: [
|
|
44
|
-
msg({
|
|
45
|
-
model: 'gemini-2.5-flash',
|
|
46
|
-
agentName: 'worker',
|
|
47
|
-
cost: 0.12,
|
|
48
|
-
inputTokens: 500,
|
|
49
|
-
outputTokens: 200,
|
|
50
|
-
}),
|
|
51
|
-
],
|
|
52
|
-
},
|
|
53
|
-
],
|
|
54
|
-
}),
|
|
55
|
-
];
|
|
56
|
-
|
|
57
|
-
const suite = createLogicSuite('usageRows');
|
|
58
|
-
|
|
59
|
-
suite('reconciles exactly with sumUsage over the same input', () => {
|
|
60
|
-
// THE guarantee. A per-call view that disagrees with the total is worse than none,
|
|
61
|
-
// because both look right in isolation — which is how the consumer's own copy
|
|
62
|
-
// under-reported for a month.
|
|
63
|
-
const history = richHistory();
|
|
64
|
-
const rows = usageRows(history);
|
|
65
|
-
const total = sumUsage(history);
|
|
66
|
-
|
|
67
|
-
const rowCost = rows.reduce((n, r) => n + (r.costUsd ?? 0) + (r.externalCostUsd ?? 0), 0);
|
|
68
|
-
assert.ok(Math.abs(rowCost - total.costUsd) < 1e-12, `${rowCost} vs ${total.costUsd}`);
|
|
69
|
-
|
|
70
|
-
const bucket = (
|
|
71
|
-
k: 'uncachedInputTokens' | 'cacheReadTokens' | 'cacheWriteTokens' | 'outputTokens',
|
|
72
|
-
) => rows.reduce((n, r) => n + r[k], 0);
|
|
73
|
-
assert.is(bucket('uncachedInputTokens'), total.uncachedInputTokens, 'uncached input');
|
|
74
|
-
assert.is(bucket('cacheReadTokens'), total.cacheReadTokens, 'cache read');
|
|
75
|
-
assert.is(bucket('cacheWriteTokens'), total.cacheWriteTokens, 'cache write');
|
|
76
|
-
assert.is(bucket('outputTokens'), total.outputTokens, 'output');
|
|
77
|
-
});
|
|
78
|
-
|
|
79
|
-
suite('emits a compaction row so banked spend is not lost from a per-call view', () => {
|
|
80
|
-
// The shortfall this function exists to close: the summary message carries no
|
|
81
|
-
// `cost` and no token fields of its own, so a naive per-message walk drops the
|
|
82
|
-
// spend of every turn the compaction deleted.
|
|
83
|
-
const rows = usageRows(richHistory());
|
|
84
|
-
const compactionRows = rows.filter((r) => r.source === 'compaction');
|
|
85
|
-
assert.is(compactionRows.length, 1);
|
|
86
|
-
assert.is(compactionRows[0].costUsd, 0.05);
|
|
87
|
-
assert.is(compactionRows[0].uncachedInputTokens, 400);
|
|
88
|
-
assert.is(
|
|
89
|
-
compactionRows[0].model,
|
|
90
|
-
undefined,
|
|
91
|
-
'no model — the banked spend may span several, and compaction.model is the summariser',
|
|
92
|
-
);
|
|
93
|
-
});
|
|
94
|
-
|
|
95
|
-
suite('recurses into sub-agent traces, tagging depth and the spawning call', () => {
|
|
96
|
-
const rows = usageRows(richHistory());
|
|
97
|
-
const child = rows.find((r) => r.model === 'gemini-2.5-flash');
|
|
98
|
-
assert.ok(child, 'the sub-agent turn produced a row');
|
|
99
|
-
assert.is(child!.subAgentDepth, 1);
|
|
100
|
-
assert.is(child!.subAgentOf, 'tc1');
|
|
101
|
-
assert.is(child!.agentName, 'worker');
|
|
102
|
-
const parent = rows.find((r) => r.model === 'claude-sonnet-5');
|
|
103
|
-
assert.is(parent!.subAgentDepth, 0);
|
|
104
|
-
assert.is(parent!.subAgentOf, undefined);
|
|
105
|
-
});
|
|
106
|
-
|
|
107
|
-
suite('prefers the provider the driver stamped over re-deriving it from the model', () => {
|
|
108
|
-
// `ChatMessage.provider` comes from the resolved provider's own status, so it is
|
|
109
|
-
// authoritative and knows vendors the model allowlists do not. Re-deriving from the
|
|
110
|
-
// model id alone drops attribution for a server-proxied turn — exactly the rows a
|
|
111
|
-
// usage ledger cares most about getting right. PR review.
|
|
112
|
-
const rows = usageRows([
|
|
113
|
-
msg({ model: 'gpt-5', provider: 'openai', cost: 1, inputTokens: 1 } as Partial<ChatMessage>),
|
|
114
|
-
// Stamped provider disagrees with what the model id would derive: the stamp still wins.
|
|
115
|
-
msg({
|
|
116
|
-
model: 'claude-sonnet-5',
|
|
117
|
-
provider: 'chrome',
|
|
118
|
-
cost: 1,
|
|
119
|
-
inputTokens: 1,
|
|
120
|
-
} as Partial<ChatMessage>),
|
|
121
|
-
]);
|
|
122
|
-
assert.equal(
|
|
123
|
-
rows.map((r) => r.provider),
|
|
124
|
-
['openai', 'chrome'],
|
|
125
|
-
);
|
|
126
|
-
});
|
|
127
|
-
|
|
128
|
-
suite('resolves the provider from the model, and leaves it unset when unknown', () => {
|
|
129
|
-
const rows = usageRows([
|
|
130
|
-
msg({ model: 'claude-sonnet-5', cost: 1, inputTokens: 1 }),
|
|
131
|
-
msg({ model: 'gemini-2.5-flash', cost: 1, inputTokens: 1 }),
|
|
132
|
-
msg({ model: 'gpt-5', cost: 1, inputTokens: 1 }),
|
|
133
|
-
msg({ cost: 1, inputTokens: 1 }),
|
|
134
|
-
]);
|
|
135
|
-
assert.equal(
|
|
136
|
-
rows.map((r) => r.provider),
|
|
137
|
-
['anthropic', 'gemini', undefined, undefined],
|
|
138
|
-
'an unrecognised or absent model leaves provider unset rather than guessing',
|
|
139
|
-
);
|
|
140
|
-
});
|
|
141
|
-
|
|
142
|
-
suite('splits the prompt into buckets the same way sumUsage does', () => {
|
|
143
|
-
const [row] = usageRows([
|
|
144
|
-
msg({
|
|
145
|
-
model: 'claude-sonnet-5',
|
|
146
|
-
inputTokens: 1_000,
|
|
147
|
-
cacheReadTokens: 600,
|
|
148
|
-
cacheWriteTokens: 300,
|
|
149
|
-
}),
|
|
150
|
-
]);
|
|
151
|
-
assert.is(row.uncachedInputTokens, 100, 'the remainder, not an addition');
|
|
152
|
-
assert.is(row.cacheReadTokens, 600);
|
|
153
|
-
assert.is(row.cacheWriteTokens, 300);
|
|
154
|
-
});
|
|
155
|
-
|
|
156
|
-
suite('clamps a prompt whose cache buckets exceed the total', () => {
|
|
157
|
-
const [row] = usageRows([msg({ inputTokens: 100, cacheReadTokens: 900 })]);
|
|
158
|
-
assert.is(row.uncachedInputTokens, 0, 'never negative');
|
|
159
|
-
});
|
|
160
|
-
|
|
161
|
-
suite('leaves costUsd undefined when nothing was reported, rather than zero', () => {
|
|
162
|
-
// What the "carried usage but no cost" audit keys off. A zero here would read as a
|
|
163
|
-
// free call and the alarm would never fire.
|
|
164
|
-
const [row] = usageRows([msg({ model: 'claude-sonnet-5', inputTokens: 500, outputTokens: 10 })]);
|
|
165
|
-
assert.is(row.costUsd, undefined);
|
|
166
|
-
assert.is(row.source, 'request');
|
|
167
|
-
});
|
|
168
|
-
|
|
169
|
-
suite('records external (non-LLM) cost and counts it in the reconciliation', () => {
|
|
170
|
-
const history = [msg({ externalCostUsd: 0.4 }), msg({ model: 'claude-sonnet-5', cost: 0.1 })];
|
|
171
|
-
const rows = usageRows(history);
|
|
172
|
-
assert.is(rows.find((r) => r.externalCostUsd != null)!.externalCostUsd, 0.4);
|
|
173
|
-
const rowCost = rows.reduce((n, r) => n + (r.costUsd ?? 0) + (r.externalCostUsd ?? 0), 0);
|
|
174
|
-
assert.ok(Math.abs(rowCost - sumUsage(history).costUsd) < 1e-12);
|
|
175
|
-
});
|
|
176
|
-
|
|
177
|
-
suite('produces no row for a message carrying no usage', () => {
|
|
178
|
-
assert.equal(
|
|
179
|
-
usageRows([
|
|
180
|
-
msg({ role: 'user', content: 'hello' }),
|
|
181
|
-
msg({ content: 'thinking out loud', category: 'reasoning' }),
|
|
182
|
-
]),
|
|
183
|
-
[],
|
|
184
|
-
);
|
|
185
|
-
});
|
|
186
|
-
|
|
187
|
-
suite('is empty for an empty transcript', () => {
|
|
188
|
-
assert.equal(usageRows([]), []);
|
|
189
|
-
});
|
|
190
|
-
|
|
191
|
-
suite('reconciles on a nested delegation two levels deep', () => {
|
|
192
|
-
const history = [
|
|
193
|
-
msg({
|
|
194
|
-
model: 'claude-sonnet-5',
|
|
195
|
-
cost: 0.01,
|
|
196
|
-
inputTokens: 100,
|
|
197
|
-
toolCalls: [
|
|
198
|
-
{
|
|
199
|
-
id: 'a',
|
|
200
|
-
name: 'delegate',
|
|
201
|
-
args: {},
|
|
202
|
-
subAgentTrace: [
|
|
203
|
-
msg({
|
|
204
|
-
model: 'claude-sonnet-5',
|
|
205
|
-
cost: 0.02,
|
|
206
|
-
inputTokens: 200,
|
|
207
|
-
toolCalls: [
|
|
208
|
-
{
|
|
209
|
-
id: 'b',
|
|
210
|
-
name: 'delegate',
|
|
211
|
-
args: {},
|
|
212
|
-
subAgentTrace: [msg({ model: 'gemini-2.5-flash', cost: 0.04, inputTokens: 400 })],
|
|
213
|
-
},
|
|
214
|
-
],
|
|
215
|
-
}),
|
|
216
|
-
],
|
|
217
|
-
},
|
|
218
|
-
],
|
|
219
|
-
}),
|
|
220
|
-
];
|
|
221
|
-
const rows = usageRows(history);
|
|
222
|
-
assert.equal(
|
|
223
|
-
rows.map((r) => r.subAgentDepth),
|
|
224
|
-
[0, 1, 2],
|
|
225
|
-
);
|
|
226
|
-
const rowCost = rows.reduce((n, r) => n + (r.costUsd ?? 0), 0);
|
|
227
|
-
assert.ok(Math.abs(rowCost - sumUsage(history).costUsd) < 1e-12, 'still reconciles');
|
|
228
|
-
});
|
|
229
|
-
|
|
230
|
-
suite('does not mutate the transcript', () => {
|
|
231
|
-
const history = richHistory();
|
|
232
|
-
const before = JSON.stringify(history);
|
|
233
|
-
usageRows(history);
|
|
234
|
-
assert.is(JSON.stringify(history), before, 'a projection, not a transform');
|
|
235
|
-
});
|
|
236
|
-
|
|
237
|
-
suite.run();
|
package/src/utils/usage-rows.ts
DELETED
|
@@ -1,187 +0,0 @@
|
|
|
1
|
-
import type { AggregateUsage, AIProviderType, ChatMessage } from '@genesislcap/foundation-ai';
|
|
2
|
-
import { vendorOfModel } from '@genesislcap/foundation-ai';
|
|
3
|
-
|
|
4
|
-
/**
|
|
5
|
-
* One billable unit of spend, projected out of a transcript.
|
|
6
|
-
*
|
|
7
|
-
* @remarks
|
|
8
|
-
* Deliberately **not** a `ChatMessage`, and not an extension of one. A row is a
|
|
9
|
-
* derived record for a ledger, and keeping the types distinct makes the one
|
|
10
|
-
* dangerous mistake — feeding rows back into `sumUsage`, which does its own
|
|
11
|
-
* recursion and would count everything twice — a compile error rather than a
|
|
12
|
-
* plausible wrong number.
|
|
13
|
-
*
|
|
14
|
-
* @beta
|
|
15
|
-
*/
|
|
16
|
-
export interface UsageRow {
|
|
17
|
-
/**
|
|
18
|
-
* Where the spend came from.
|
|
19
|
-
*
|
|
20
|
-
* - `'request'` — one model call, with a message behind it.
|
|
21
|
-
* - `'compaction'` — spend **banked** by a compaction. The turns that incurred it
|
|
22
|
-
* were summarised and deleted, so there is no single call to attribute it to;
|
|
23
|
-
* the amount survives on the summary's `compaction.rolledUpUsage`. Emitted as
|
|
24
|
-
* its own row because otherwise this money is simply missing from a per-call
|
|
25
|
-
* view while remaining present in the total, and the two disagree silently.
|
|
26
|
-
*/
|
|
27
|
-
source: 'request' | 'compaction';
|
|
28
|
-
/**
|
|
29
|
-
* Model that produced the call. `undefined` on a `'compaction'` row: the banked
|
|
30
|
-
* spend covers however many turns were summarised, which may have run on
|
|
31
|
-
* different models, so attributing it to one would invent a fact. (The summary's
|
|
32
|
-
* `compaction.model` is the model that did the *summarising* — a different thing,
|
|
33
|
-
* and not what this spend was.)
|
|
34
|
-
*/
|
|
35
|
-
model?: string;
|
|
36
|
-
/**
|
|
37
|
-
* Vendor that served the call. Taken from the provider the driver stamped on the message,
|
|
38
|
-
* which is authoritative and covers vendors the model allowlists do not — a server-proxied
|
|
39
|
-
* `gpt-5` turn is `'openai'` here and unknown to `vendorOfModel`. Falls back to resolving
|
|
40
|
-
* {@link UsageRow.model} through `vendorOfModel` only for messages persisted before that
|
|
41
|
-
* field existed, and is left unset when neither knows rather than guessed at.
|
|
42
|
-
*/
|
|
43
|
-
provider?: AIProviderType;
|
|
44
|
-
/**
|
|
45
|
-
* USD for this row, as reported by the provider and stamped by the transport.
|
|
46
|
-
*
|
|
47
|
-
* `undefined` means no cost was reported — **not** zero. Nothing here estimates
|
|
48
|
-
* one from the token counts: an estimate is plausible, unremarkable and quietly
|
|
49
|
-
* wrong, whereas a total that is visibly too low gets investigated. A row with
|
|
50
|
-
* tokens but no `costUsd` is worth alarming on; that is the shape that check needs.
|
|
51
|
-
*/
|
|
52
|
-
costUsd?: number;
|
|
53
|
-
/** Non-LLM spend a host reported for this turn (an external service it called). */
|
|
54
|
-
externalCostUsd?: number;
|
|
55
|
-
/** Prompt tokens billed at the full uncached rate — the remainder after the cache buckets. */
|
|
56
|
-
uncachedInputTokens: number;
|
|
57
|
-
/** Prompt tokens served from the provider's cache. */
|
|
58
|
-
cacheReadTokens: number;
|
|
59
|
-
/** Prompt tokens written to the provider's cache. */
|
|
60
|
-
cacheWriteTokens: number;
|
|
61
|
-
/** Generated tokens, including reasoning tokens the provider bills as output. */
|
|
62
|
-
outputTokens: number;
|
|
63
|
-
/** Agent that produced the call, when known. */
|
|
64
|
-
agentName?: string;
|
|
65
|
-
/** Delegation depth: 0 for the top-level conversation, 1 for a sub-agent, 2 for its sub-agent, … */
|
|
66
|
-
subAgentDepth: number;
|
|
67
|
-
/** Id of the tool call that spawned this row's conversation — correlates a row back to its delegation. */
|
|
68
|
-
subAgentOf?: string;
|
|
69
|
-
}
|
|
70
|
-
|
|
71
|
-
/** Whether a message carries anything a ledger would record. */
|
|
72
|
-
function hasUsage(m: ChatMessage): boolean {
|
|
73
|
-
return (
|
|
74
|
-
m.cost != null ||
|
|
75
|
-
m.externalCostUsd != null ||
|
|
76
|
-
m.inputTokens != null ||
|
|
77
|
-
m.outputTokens != null ||
|
|
78
|
-
m.cacheReadTokens != null ||
|
|
79
|
-
m.cacheWriteTokens != null
|
|
80
|
-
);
|
|
81
|
-
}
|
|
82
|
-
|
|
83
|
-
/**
|
|
84
|
-
* Project a transcript into one row per billable unit of spend.
|
|
85
|
-
*
|
|
86
|
-
* @remarks
|
|
87
|
-
* Reads the messages and returns a table; it stores nothing, mutates nothing, and
|
|
88
|
-
* adds nothing to history. Use it where an **aggregate is not enough** — writing
|
|
89
|
-
* usage rows for per-project attribution, a cost dashboard, or auditing which turns
|
|
90
|
-
* came back unpriced. For a single total, `sumUsage` is the answer and this is the
|
|
91
|
-
* wrong tool.
|
|
92
|
-
*
|
|
93
|
-
* Walks the same shape `sumUsage` does, and is guaranteed to agree with it: summing
|
|
94
|
-
* `costUsd` and `externalCostUsd` across these rows equals `sumUsage(...).costUsd`
|
|
95
|
-
* over the same input, and the four token buckets reconcile likewise. That property
|
|
96
|
-
* is the point of the function — a per-call view that quietly disagrees with the
|
|
97
|
-
* total is worse than no per-call view, because both look right in isolation.
|
|
98
|
-
*
|
|
99
|
-
* Covers the two places spend hides:
|
|
100
|
-
*
|
|
101
|
-
* - **Sub-agent conversations.** A delegating tool call carries the child's entire
|
|
102
|
-
* conversation on `toolCall.subAgentTrace`, so a walk of top-level messages alone
|
|
103
|
-
* misses everything a delegating agent spent. A consumer that reimplemented this
|
|
104
|
-
* and omitted the branch reported $0.106 on a run that cost $0.234.
|
|
105
|
-
* - **Compactions.** A compaction deletes the messages it summarises, banking their
|
|
106
|
-
* spend on the summary. Those turns produce a single `source: 'compaction'` row.
|
|
107
|
-
*
|
|
108
|
-
* Messages carrying no usage at all (a user turn, a narration) produce no row.
|
|
109
|
-
*
|
|
110
|
-
* **Do not pass the result to `sumUsage`** — the types prevent it, and the reason is
|
|
111
|
-
* that both recurse, so a pre-flattened list would be counted twice. Both functions
|
|
112
|
-
* take raw history.
|
|
113
|
-
*
|
|
114
|
-
* @beta
|
|
115
|
-
*/
|
|
116
|
-
export function usageRows(messages: readonly ChatMessage[]): UsageRow[] {
|
|
117
|
-
return collectRows(messages, 0, undefined);
|
|
118
|
-
}
|
|
119
|
-
|
|
120
|
-
/** Map an already-bucketed {@link AggregateUsage} onto a row's token fields. */
|
|
121
|
-
function bucketsOf(
|
|
122
|
-
usage: AggregateUsage,
|
|
123
|
-
): Pick<UsageRow, 'uncachedInputTokens' | 'cacheReadTokens' | 'cacheWriteTokens' | 'outputTokens'> {
|
|
124
|
-
return {
|
|
125
|
-
uncachedInputTokens: usage.uncachedInputTokens,
|
|
126
|
-
cacheReadTokens: usage.cacheReadTokens,
|
|
127
|
-
cacheWriteTokens: usage.cacheWriteTokens,
|
|
128
|
-
outputTokens: usage.outputTokens,
|
|
129
|
-
};
|
|
130
|
-
}
|
|
131
|
-
|
|
132
|
-
function collectRows(
|
|
133
|
-
messages: readonly ChatMessage[],
|
|
134
|
-
depth: number,
|
|
135
|
-
subAgentOf: string | undefined,
|
|
136
|
-
): UsageRow[] {
|
|
137
|
-
const rows: UsageRow[] = [];
|
|
138
|
-
for (const m of messages) {
|
|
139
|
-
if (hasUsage(m)) {
|
|
140
|
-
const cacheReadTokens = m.cacheReadTokens ?? 0;
|
|
141
|
-
const cacheWriteTokens = m.cacheWriteTokens ?? 0;
|
|
142
|
-
rows.push({
|
|
143
|
-
source: 'request',
|
|
144
|
-
model: m.model,
|
|
145
|
-
// The driver stamps `ChatMessage.provider` from the resolved provider's own
|
|
146
|
-
// status, so it is AUTHORITATIVE and covers vendors the model allowlists do
|
|
147
|
-
// not — a server-proxied `gpt-5` turn is `provider: 'openai'` on the message
|
|
148
|
-
// and unknown to `vendorOfModel`. Prefer it; fall back to resolving the model
|
|
149
|
-
// id only for messages persisted before the field existed. Still left
|
|
150
|
-
// undefined when neither knows, rather than guessed at.
|
|
151
|
-
provider: m.provider ?? (m.model ? vendorOfModel(m.model) : undefined),
|
|
152
|
-
...(m.cost != null && { costUsd: m.cost }),
|
|
153
|
-
...(m.externalCostUsd != null && { externalCostUsd: m.externalCostUsd }),
|
|
154
|
-
// Uncached input is the REMAINDER, matching `sumUsage`: `inputTokens` is the
|
|
155
|
-
// whole prompt and the cache fields break it down. Clamped for the same
|
|
156
|
-
// reason — hand-edited history must not drive a total negative.
|
|
157
|
-
uncachedInputTokens: Math.max(0, (m.inputTokens ?? 0) - cacheReadTokens - cacheWriteTokens),
|
|
158
|
-
cacheReadTokens,
|
|
159
|
-
cacheWriteTokens,
|
|
160
|
-
outputTokens: m.outputTokens ?? 0,
|
|
161
|
-
...(m.agentName != null && { agentName: m.agentName }),
|
|
162
|
-
subAgentDepth: depth,
|
|
163
|
-
...(subAgentOf != null && { subAgentOf }),
|
|
164
|
-
});
|
|
165
|
-
}
|
|
166
|
-
|
|
167
|
-
// Spend the compaction banked on this summary's behalf. Its buckets are already
|
|
168
|
-
// disjoint (it is an `AggregateUsage`), so they map across without the
|
|
169
|
-
// subtraction a per-message record needs.
|
|
170
|
-
const rolled = m.compaction?.rolledUpUsage;
|
|
171
|
-
if (rolled) {
|
|
172
|
-
rows.push({
|
|
173
|
-
source: 'compaction',
|
|
174
|
-
...(rolled.costUsd != null && { costUsd: rolled.costUsd }),
|
|
175
|
-
...bucketsOf(rolled),
|
|
176
|
-
...(m.agentName != null && { agentName: m.agentName }),
|
|
177
|
-
subAgentDepth: depth,
|
|
178
|
-
...(subAgentOf != null && { subAgentOf }),
|
|
179
|
-
});
|
|
180
|
-
}
|
|
181
|
-
|
|
182
|
-
for (const tc of m.toolCalls ?? []) {
|
|
183
|
-
if (tc.subAgentTrace) rows.push(...collectRows(tc.subAgentTrace, depth + 1, tc.id));
|
|
184
|
-
}
|
|
185
|
-
}
|
|
186
|
-
return rows;
|
|
187
|
-
}
|
|
@@ -1,24 +0,0 @@
|
|
|
1
|
-
/**
|
|
2
|
-
* Resolve `promise`, or **reject** if it doesn't settle within `ms`. Rejecting
|
|
3
|
-
* (rather than resolving to a fallback) lets restore tell a genuine timeout apart
|
|
4
|
-
* from a clean "nothing stored" result, so it can surface a notice for the former
|
|
5
|
-
* and stay silent for the latter. Because the loser of the race is abandoned, a
|
|
6
|
-
* late-resolving `load()` is discarded and can never clobber a now-live session;
|
|
7
|
-
* a late *rejection* is swallowed so it doesn't surface as an unhandled rejection.
|
|
8
|
-
*
|
|
9
|
-
* @internal
|
|
10
|
-
*/
|
|
11
|
-
export async function withTimeout<T>(promise: Promise<T>, ms: number): Promise<T> {
|
|
12
|
-
let timer: ReturnType<typeof setTimeout> | undefined;
|
|
13
|
-
promise.catch(() => {});
|
|
14
|
-
try {
|
|
15
|
-
return await Promise.race([
|
|
16
|
-
promise,
|
|
17
|
-
new Promise<T>((_resolve, reject) => {
|
|
18
|
-
timer = setTimeout(() => reject(new Error(`withTimeout: exceeded ${ms}ms`)), ms);
|
|
19
|
-
}),
|
|
20
|
-
]);
|
|
21
|
-
} finally {
|
|
22
|
-
clearTimeout(timer);
|
|
23
|
-
}
|
|
24
|
-
}
|