@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,218 +0,0 @@
|
|
|
1
|
-
import type { AggregateUsage, AIProviderType } from '@genesislcap/foundation-ai';
|
|
2
|
-
import { emptyUsage } from './sum-usage';
|
|
3
|
-
|
|
4
|
-
/** A model used during a recorded cost session. */
|
|
5
|
-
export interface CostSessionModelEntry {
|
|
6
|
-
model: string;
|
|
7
|
-
/** Vendor id from `getStatus()` (e.g. `'anthropic'`, `'gemini'`). */
|
|
8
|
-
provider?: AIProviderType;
|
|
9
|
-
}
|
|
10
|
-
|
|
11
|
-
/** Persisted usage record for one project's assistant work. */
|
|
12
|
-
export interface CostSessionRecord {
|
|
13
|
-
id: string;
|
|
14
|
-
/**
|
|
15
|
-
* Stable identity of the work this row stands for — the assistant's session key
|
|
16
|
-
* (a project id, for hosts that key sessions per project). The upsert key: a row
|
|
17
|
-
* is one project's lifetime usage, so re-reporting the same key REPLACES the row
|
|
18
|
-
* rather than adding another.
|
|
19
|
-
*/
|
|
20
|
-
projectKey: string;
|
|
21
|
-
title: string;
|
|
22
|
-
/**
|
|
23
|
-
* ISO-8601 timestamp of the most recent usage recorded for this row.
|
|
24
|
-
*
|
|
25
|
-
* Named for what it is: the row is upserted as spend accrues, so this moves. It is
|
|
26
|
-
* NOT an end time — a row has no terminal state, because a project can always be
|
|
27
|
-
* worked on again.
|
|
28
|
-
*/
|
|
29
|
-
updatedAt: string;
|
|
30
|
-
/**
|
|
31
|
-
* Cost plus the four token buckets — the same shape the live session tile renders, so
|
|
32
|
-
* a history row can show the identical breakdown instead of a lone merged total that
|
|
33
|
-
* cannot explain a large token count at a small cost.
|
|
34
|
-
*
|
|
35
|
-
* This is the project's **lifetime** usage: `banked` plus whatever the live transcript
|
|
36
|
-
* currently proves.
|
|
37
|
-
*/
|
|
38
|
-
usage: AggregateUsage;
|
|
39
|
-
/**
|
|
40
|
-
* Spend that predates the current transcript, and so cannot be re-derived from it.
|
|
41
|
-
*
|
|
42
|
-
* Without this a row could only ever report what the open transcript proves. That is
|
|
43
|
-
* fine when the transcript is restored on load (it *is* the project's history), but
|
|
44
|
-
* where it starts empty every refresh the row would be rewritten downwards on the next
|
|
45
|
-
* turn and the project's earlier spend lost. Banking it keeps `usage` a lifetime figure
|
|
46
|
-
* in both cases.
|
|
47
|
-
*
|
|
48
|
-
* Deliberately part of the row rather than element state: two assistant instances (a
|
|
49
|
-
* docked bubble and a popped-out panel) share one session, and both write this row. A
|
|
50
|
-
* value derived from the row keeps every write idempotent, whereas an element-held
|
|
51
|
-
* accumulator would be applied once per instance.
|
|
52
|
-
*
|
|
53
|
-
* Optional: rows written before this existed have none, which reads as "nothing banked".
|
|
54
|
-
*/
|
|
55
|
-
banked?: AggregateUsage;
|
|
56
|
-
models: CostSessionModelEntry[];
|
|
57
|
-
}
|
|
58
|
-
|
|
59
|
-
const STORAGE_PREFIX = 'genesis-ai-assistant:cost-history:';
|
|
60
|
-
|
|
61
|
-
/**
|
|
62
|
-
* Every bucket must be a number before a row is trusted. Checked per field rather than
|
|
63
|
-
* just testing for an object, because a partially-shaped `usage` would sum as `NaN` and
|
|
64
|
-
* silently poison every total on the Usage tab.
|
|
65
|
-
*/
|
|
66
|
-
function isAggregateUsage(value: unknown): value is AggregateUsage {
|
|
67
|
-
if (typeof value !== 'object' || value == null) return false;
|
|
68
|
-
const u = value as AggregateUsage;
|
|
69
|
-
return (
|
|
70
|
-
typeof u.costUsd === 'number' &&
|
|
71
|
-
typeof u.uncachedInputTokens === 'number' &&
|
|
72
|
-
typeof u.cacheReadTokens === 'number' &&
|
|
73
|
-
typeof u.cacheWriteTokens === 'number' &&
|
|
74
|
-
typeof u.outputTokens === 'number'
|
|
75
|
-
);
|
|
76
|
-
}
|
|
77
|
-
|
|
78
|
-
/** localStorage key for a host-scoped cost history list. */
|
|
79
|
-
export function costHistoryStorageKey(scope: string): string {
|
|
80
|
-
return `${STORAGE_PREFIX}${scope}`;
|
|
81
|
-
}
|
|
82
|
-
|
|
83
|
-
/**
|
|
84
|
-
* Load persisted cost sessions for a scope, in whatever order they were stored — this applies no
|
|
85
|
-
* ordering of its own. Display order is the caller's business; see `sortRecordsByRecency`.
|
|
86
|
-
*/
|
|
87
|
-
export function loadCostSessionHistory(scope: string): CostSessionRecord[] {
|
|
88
|
-
if (typeof localStorage === 'undefined') return [];
|
|
89
|
-
try {
|
|
90
|
-
const raw = localStorage.getItem(costHistoryStorageKey(scope));
|
|
91
|
-
if (!raw) return [];
|
|
92
|
-
const parsed: unknown = JSON.parse(raw);
|
|
93
|
-
if (!Array.isArray(parsed)) return [];
|
|
94
|
-
return parsed.filter(isCostSessionRecord);
|
|
95
|
-
} catch {
|
|
96
|
-
return [];
|
|
97
|
-
}
|
|
98
|
-
}
|
|
99
|
-
|
|
100
|
-
/** Replace the persisted list for a scope. */
|
|
101
|
-
export function saveCostSessionHistory(scope: string, records: CostSessionRecord[]): void {
|
|
102
|
-
if (typeof localStorage === 'undefined') return;
|
|
103
|
-
try {
|
|
104
|
-
localStorage.setItem(costHistoryStorageKey(scope), JSON.stringify(records));
|
|
105
|
-
} catch {
|
|
106
|
-
// Quota exceeded or private browsing — non-fatal.
|
|
107
|
-
}
|
|
108
|
-
}
|
|
109
|
-
|
|
110
|
-
/**
|
|
111
|
-
* Pure upsert — `records` with `record` replacing any row sharing its `projectKey`, or
|
|
112
|
-
* prepended when there is none.
|
|
113
|
-
*
|
|
114
|
-
* An upsert, not an append, because a row stands for a **project's lifetime usage** rather
|
|
115
|
-
* than a visit to it. Appending cut a fresh row per page-hide, so one project accumulated
|
|
116
|
-
* a stack of near-identical rows and its spend was counted once per refresh by anything
|
|
117
|
-
* summing the list.
|
|
118
|
-
*
|
|
119
|
-
* The resulting array order is incidental — it is insertion order, so a project returned to
|
|
120
|
-
* stays where it first landed. Do not read it as recency; `sortRecordsByRecency` is what the
|
|
121
|
-
* Usage tab renders through.
|
|
122
|
-
*
|
|
123
|
-
* Storage-agnostic on purpose: the same merge serves the synchronous `localStorage` path
|
|
124
|
-
* and the async provider-backed one, so the two can never drift on what "upsert" means.
|
|
125
|
-
* The caller owns persistence — see the assistant's `persistCostHistory`.
|
|
126
|
-
*/
|
|
127
|
-
export function upsertRecord(
|
|
128
|
-
records: readonly CostSessionRecord[],
|
|
129
|
-
record: CostSessionRecord,
|
|
130
|
-
): CostSessionRecord[] {
|
|
131
|
-
const at = records.findIndex((r) => r.projectKey === record.projectKey);
|
|
132
|
-
return at === -1 ? [record, ...records] : records.map((r, i) => (i === at ? record : r));
|
|
133
|
-
}
|
|
134
|
-
|
|
135
|
-
/**
|
|
136
|
-
* Rows most-recently-worked-on first, as a new array.
|
|
137
|
-
*
|
|
138
|
-
* Applied at read time rather than baked into the stored order, so it covers rows that came from
|
|
139
|
-
* a host provider as well as from `localStorage`, and so no write path has to maintain it.
|
|
140
|
-
*
|
|
141
|
-
* `updatedAt` is ISO-8601, which sorts lexicographically — no date parsing, and no dependence on
|
|
142
|
-
* the strings being valid dates. Sorting by it rather than by insertion order is what makes the
|
|
143
|
-
* list track use: the active project moves to the top on its first upsert and stays there, since
|
|
144
|
-
* its row is rewritten on every transcript change.
|
|
145
|
-
*/
|
|
146
|
-
export function sortRecordsByRecency(records: readonly CostSessionRecord[]): CostSessionRecord[] {
|
|
147
|
-
return [...records].sort((a, b) => b.updatedAt.localeCompare(a.updatedAt));
|
|
148
|
-
}
|
|
149
|
-
|
|
150
|
-
/**
|
|
151
|
-
* The spend a project's next row must carry over from before its current transcript.
|
|
152
|
-
*
|
|
153
|
-
* `transcriptIsAuthoritative` means the open transcript already accounts for this project's
|
|
154
|
-
* history — true exactly when a session snapshot is restored on load. The two branches are
|
|
155
|
-
* not interchangeable, and picking the wrong one is silently destructive:
|
|
156
|
-
*
|
|
157
|
-
* - Authoritative: carry the previous row's own `banked` forward. Taking `usage` instead would
|
|
158
|
-
* re-add the restored history on every reload, the compounding double-count this replaced.
|
|
159
|
-
* - Not authoritative: the whole previous total becomes banked, because nothing in the fresh
|
|
160
|
-
* transcript accounts for it. Taking `banked` instead would rewrite the row down to the
|
|
161
|
-
* current page-load and drop the project's earlier spend.
|
|
162
|
-
*
|
|
163
|
-
* Note both branches are idempotent — the result never includes the current transcript — which
|
|
164
|
-
* is what lets two assistant instances sharing a session write the same row.
|
|
165
|
-
*/
|
|
166
|
-
export function resolveBankedUsage(
|
|
167
|
-
existing: CostSessionRecord | undefined,
|
|
168
|
-
transcriptIsAuthoritative: boolean,
|
|
169
|
-
): AggregateUsage {
|
|
170
|
-
if (!existing) return emptyUsage();
|
|
171
|
-
const carried = transcriptIsAuthoritative ? existing.banked : existing.usage;
|
|
172
|
-
return carried ?? emptyUsage();
|
|
173
|
-
}
|
|
174
|
-
|
|
175
|
-
/** Clear all persisted records for a scope. */
|
|
176
|
-
export function clearCostSessionHistory(scope: string): void {
|
|
177
|
-
if (typeof localStorage === 'undefined') return;
|
|
178
|
-
try {
|
|
179
|
-
localStorage.removeItem(costHistoryStorageKey(scope));
|
|
180
|
-
} catch {
|
|
181
|
-
// Non-fatal.
|
|
182
|
-
}
|
|
183
|
-
}
|
|
184
|
-
|
|
185
|
-
/** Display date for a history row (e.g. "Jun 14, 2026"). */
|
|
186
|
-
export function formatCostSessionDate(iso: string): string {
|
|
187
|
-
return new Intl.DateTimeFormat('en-US', {
|
|
188
|
-
month: 'short',
|
|
189
|
-
day: 'numeric',
|
|
190
|
-
year: 'numeric',
|
|
191
|
-
}).format(new Date(iso));
|
|
192
|
-
}
|
|
193
|
-
|
|
194
|
-
/**
|
|
195
|
-
* Whether a value is a usable row. Exported because rows arrive by two routes and BOTH need it:
|
|
196
|
-
* `loadCostSessionHistory` filters the `localStorage` path here, and the assistant filters what a
|
|
197
|
-
* host provider returns. A row that skips this check is not merely ignored downstream — a
|
|
198
|
-
* half-shaped `usage` sums to `NaN` through every total, and a missing `updatedAt` throws inside
|
|
199
|
-
* the sort that orders the list, during render.
|
|
200
|
-
*/
|
|
201
|
-
export function isCostSessionRecord(value: unknown): value is CostSessionRecord {
|
|
202
|
-
if (typeof value !== 'object' || value == null) return false;
|
|
203
|
-
const r = value as CostSessionRecord;
|
|
204
|
-
return (
|
|
205
|
-
typeof r.id === 'string' &&
|
|
206
|
-
// Rows written before the upsert model have no `projectKey` and cannot be keyed
|
|
207
|
-
// to a project, so they are discarded on load rather than silently pooled with
|
|
208
|
-
// the new per-project rows (where they would double-count that project).
|
|
209
|
-
typeof r.projectKey === 'string' &&
|
|
210
|
-
typeof r.title === 'string' &&
|
|
211
|
-
typeof r.updatedAt === 'string' &&
|
|
212
|
-
isAggregateUsage(r.usage) &&
|
|
213
|
-
// Absent is valid (nothing banked / pre-dates the field); present must be whole, since
|
|
214
|
-
// a half-shaped `banked` would poison the lifetime total exactly as a bad `usage` would.
|
|
215
|
-
(r.banked === undefined || isAggregateUsage(r.banked)) &&
|
|
216
|
-
Array.isArray(r.models)
|
|
217
|
-
);
|
|
218
|
-
}
|
|
@@ -1,63 +0,0 @@
|
|
|
1
|
-
import type { ChatMessage } from '@genesislcap/foundation-ai';
|
|
2
|
-
import { assert, createLogicSuite } from '@genesislcap/foundation-testing';
|
|
3
|
-
import {
|
|
4
|
-
deriveCostSessionTitleFromMessages,
|
|
5
|
-
resolveCostSessionTitle,
|
|
6
|
-
truncateCostSessionTitle,
|
|
7
|
-
} from './derive-cost-session-title';
|
|
8
|
-
|
|
9
|
-
const suite = createLogicSuite('deriveCostSessionTitle');
|
|
10
|
-
|
|
11
|
-
const userMsg = (content: string): ChatMessage => ({ role: 'user', content });
|
|
12
|
-
|
|
13
|
-
suite('truncateCostSessionTitle keeps first sentence only', () => {
|
|
14
|
-
assert.is(
|
|
15
|
-
truncateCostSessionTitle('Build a trade blotter. With FX options and limits.'),
|
|
16
|
-
'Build a trade blotter',
|
|
17
|
-
);
|
|
18
|
-
});
|
|
19
|
-
|
|
20
|
-
suite('truncateCostSessionTitle caps word count', () => {
|
|
21
|
-
const long = 'one two three four five six seven eight nine ten eleven twelve thirteen fourteen';
|
|
22
|
-
assert.is(truncateCostSessionTitle(long), 'one two three four five six seven eight nine ten…');
|
|
23
|
-
});
|
|
24
|
-
|
|
25
|
-
suite('truncateCostSessionTitle caps character count on one long word', () => {
|
|
26
|
-
const word = 'a'.repeat(80);
|
|
27
|
-
assert.is(truncateCostSessionTitle(word).length, 60);
|
|
28
|
-
assert.ok(truncateCostSessionTitle(word).endsWith('…'));
|
|
29
|
-
});
|
|
30
|
-
|
|
31
|
-
suite('deriveCostSessionTitleFromMessages skips empty and uses first user turn', () => {
|
|
32
|
-
assert.is(
|
|
33
|
-
deriveCostSessionTitleFromMessages([
|
|
34
|
-
{ role: 'assistant', content: 'Hi' },
|
|
35
|
-
{ role: 'user', content: ' ' },
|
|
36
|
-
{ role: 'user', content: 'Equity options pricer for desk' },
|
|
37
|
-
]),
|
|
38
|
-
'Equity options pricer for desk',
|
|
39
|
-
);
|
|
40
|
-
});
|
|
41
|
-
|
|
42
|
-
suite('resolveCostSessionTitle prefers app title over messages', () => {
|
|
43
|
-
assert.is(
|
|
44
|
-
resolveCostSessionTitle({
|
|
45
|
-
appTitle: 'fx-trading',
|
|
46
|
-
messages: [userMsg('Something else entirely')],
|
|
47
|
-
headerTitle: 'Genesis Assistant',
|
|
48
|
-
}),
|
|
49
|
-
'fx-trading',
|
|
50
|
-
);
|
|
51
|
-
});
|
|
52
|
-
|
|
53
|
-
suite('resolveCostSessionTitle falls back to first user message', () => {
|
|
54
|
-
assert.is(
|
|
55
|
-
resolveCostSessionTitle({
|
|
56
|
-
messages: [userMsg('Search for recent trades in APAC')],
|
|
57
|
-
headerTitle: 'Genesis Assistant',
|
|
58
|
-
}),
|
|
59
|
-
'Search for recent trades in APAC',
|
|
60
|
-
);
|
|
61
|
-
});
|
|
62
|
-
|
|
63
|
-
suite.run();
|
|
@@ -1,76 +0,0 @@
|
|
|
1
|
-
import type { ChatMessage } from '@genesislcap/foundation-ai';
|
|
2
|
-
|
|
3
|
-
/** Max words kept from the first user turn when deriving a session title. */
|
|
4
|
-
export const COST_SESSION_TITLE_MAX_WORDS = 10;
|
|
5
|
-
|
|
6
|
-
/** Hard cap on derived title length (ellipsis included). */
|
|
7
|
-
export const COST_SESSION_TITLE_MAX_CHARS = 60;
|
|
8
|
-
|
|
9
|
-
const ELLIPSIS = '…';
|
|
10
|
-
|
|
11
|
-
/** Plain text from a chat message `content` field. */
|
|
12
|
-
export function getChatMessageText(content: ChatMessage['content']): string {
|
|
13
|
-
return typeof content === 'string' ? content.trim() : '';
|
|
14
|
-
}
|
|
15
|
-
|
|
16
|
-
/**
|
|
17
|
-
* Trim a raw user phrase to a display-friendly session title: first sentence,
|
|
18
|
-
* then word cap, then character cap.
|
|
19
|
-
*/
|
|
20
|
-
export function truncateCostSessionTitle(text: string): string {
|
|
21
|
-
const normalized = text.replace(/\s+/g, ' ').trim();
|
|
22
|
-
if (!normalized) return '';
|
|
23
|
-
|
|
24
|
-
const firstSentence = normalized.split(/\n|[.!?](?:\s|$)/u)[0]?.trim() || normalized;
|
|
25
|
-
|
|
26
|
-
const words = firstSentence.split(/\s+/).filter(Boolean);
|
|
27
|
-
let title = words.slice(0, COST_SESSION_TITLE_MAX_WORDS).join(' ');
|
|
28
|
-
if (words.length > COST_SESSION_TITLE_MAX_WORDS) {
|
|
29
|
-
title = `${title}${ELLIPSIS}`;
|
|
30
|
-
}
|
|
31
|
-
|
|
32
|
-
if (title.length > COST_SESSION_TITLE_MAX_CHARS) {
|
|
33
|
-
const sliceEnd = COST_SESSION_TITLE_MAX_CHARS - ELLIPSIS.length;
|
|
34
|
-
title = `${title.slice(0, Math.max(0, sliceEnd)).trimEnd()}${ELLIPSIS}`;
|
|
35
|
-
}
|
|
36
|
-
|
|
37
|
-
return title;
|
|
38
|
-
}
|
|
39
|
-
|
|
40
|
-
/** First non-empty user message in the thread, shortened for cost history. */
|
|
41
|
-
export function deriveCostSessionTitleFromMessages(
|
|
42
|
-
messages: readonly ChatMessage[],
|
|
43
|
-
): string | undefined {
|
|
44
|
-
for (const message of messages) {
|
|
45
|
-
if (message.role !== 'user') continue;
|
|
46
|
-
const title = truncateCostSessionTitle(message.content);
|
|
47
|
-
if (title) return title;
|
|
48
|
-
}
|
|
49
|
-
return undefined;
|
|
50
|
-
}
|
|
51
|
-
|
|
52
|
-
export interface ResolveCostSessionTitleInput {
|
|
53
|
-
explicit?: string;
|
|
54
|
-
/** Host-provided label (e.g. Create `projectName`). */
|
|
55
|
-
appTitle?: string;
|
|
56
|
-
messages: readonly ChatMessage[];
|
|
57
|
-
headerTitle?: string;
|
|
58
|
-
fallback?: string;
|
|
59
|
-
}
|
|
60
|
-
|
|
61
|
-
/** Resolve the label stored on a finalized cost session record. */
|
|
62
|
-
export function resolveCostSessionTitle(input: ResolveCostSessionTitleInput): string {
|
|
63
|
-
const explicit = input.explicit?.trim();
|
|
64
|
-
if (explicit) return explicit;
|
|
65
|
-
|
|
66
|
-
const appTitle = input.appTitle?.trim();
|
|
67
|
-
if (appTitle) return appTitle;
|
|
68
|
-
|
|
69
|
-
const fromMessages = deriveCostSessionTitleFromMessages(input.messages);
|
|
70
|
-
if (fromMessages) return fromMessages;
|
|
71
|
-
|
|
72
|
-
const header = input.headerTitle?.trim();
|
|
73
|
-
if (header) return header;
|
|
74
|
-
|
|
75
|
-
return input.fallback?.trim() || 'Untitled session';
|
|
76
|
-
}
|
|
@@ -1,163 +0,0 @@
|
|
|
1
|
-
import type { ChatMessage } from '@genesislcap/foundation-ai';
|
|
2
|
-
import { assert, createLogicSuite } from '@genesislcap/foundation-testing';
|
|
3
|
-
import { flattenSubAgentMessages } from './flatten-sub-agent-messages';
|
|
4
|
-
|
|
5
|
-
const msg = (overrides: Partial<ChatMessage> = {}): ChatMessage => ({
|
|
6
|
-
role: 'assistant',
|
|
7
|
-
content: '',
|
|
8
|
-
...overrides,
|
|
9
|
-
});
|
|
10
|
-
|
|
11
|
-
/** A parent assistant message whose single tool call delegated to a sub-agent. */
|
|
12
|
-
const delegating = (id: string, agentName: string, trace: ChatMessage[]): ChatMessage =>
|
|
13
|
-
msg({
|
|
14
|
-
agentName,
|
|
15
|
-
toolCalls: [{ id, name: 'requestSubAgent', args: {}, subAgentTrace: trace }],
|
|
16
|
-
});
|
|
17
|
-
|
|
18
|
-
const suite = createLogicSuite('flattenSubAgentMessages');
|
|
19
|
-
|
|
20
|
-
suite('returns an empty array for an empty list', () => {
|
|
21
|
-
assert.equal(flattenSubAgentMessages([]), []);
|
|
22
|
-
});
|
|
23
|
-
|
|
24
|
-
suite('emits top-level messages unchanged and tags none as sub-agent', () => {
|
|
25
|
-
const out = flattenSubAgentMessages([
|
|
26
|
-
msg({ role: 'user', content: 'hi', agentName: 'UI Builder' }),
|
|
27
|
-
msg({ content: 'hello', agentName: 'UI Builder' }),
|
|
28
|
-
]);
|
|
29
|
-
assert.is(out.length, 2);
|
|
30
|
-
assert.is(out[0].kind, 'message');
|
|
31
|
-
assert.is(out[0].agentName, 'UI Builder');
|
|
32
|
-
// No depth/marker fields on top-level entries.
|
|
33
|
-
assert.is(out[0].subAgentDepth, undefined);
|
|
34
|
-
assert.is(out[0].subAgentName, undefined);
|
|
35
|
-
assert.is(out[1].subAgentOf, undefined);
|
|
36
|
-
});
|
|
37
|
-
|
|
38
|
-
suite('hoists a sub-agent trace to top-level entries after the spawning message', () => {
|
|
39
|
-
const trace = [
|
|
40
|
-
msg({
|
|
41
|
-
agentName: 'UI Architecture Planner',
|
|
42
|
-
content: '',
|
|
43
|
-
toolCalls: [{ id: 'g', name: 'grep_source', args: {} }],
|
|
44
|
-
}),
|
|
45
|
-
msg({
|
|
46
|
-
role: 'tool',
|
|
47
|
-
agentName: 'UI Architecture Planner',
|
|
48
|
-
toolResult: { toolCallId: 'g', content: 'result' },
|
|
49
|
-
}),
|
|
50
|
-
];
|
|
51
|
-
const out = flattenSubAgentMessages([
|
|
52
|
-
delegating('tc1', 'UI Builder', trace),
|
|
53
|
-
msg({
|
|
54
|
-
role: 'tool',
|
|
55
|
-
agentName: 'UI Builder',
|
|
56
|
-
toolResult: { toolCallId: 'tc1', content: 'plan done' },
|
|
57
|
-
}),
|
|
58
|
-
]);
|
|
59
|
-
|
|
60
|
-
// parent tool-call msg, then 2 hoisted children, then the parent tool-result msg.
|
|
61
|
-
assert.is(out.length, 4);
|
|
62
|
-
assert.is(out[0].agentName, 'UI Builder'); // parent unchanged
|
|
63
|
-
assert.is(out[1].agentName, 'UI Builder › UI Architecture Planner');
|
|
64
|
-
assert.is(out[2].agentName, 'UI Builder › UI Architecture Planner');
|
|
65
|
-
assert.is(out[3].agentName, 'UI Builder'); // parent tool-result, back at top level
|
|
66
|
-
});
|
|
67
|
-
|
|
68
|
-
suite('marks hoisted entries with depth, raw name, and the spawning tool-call id', () => {
|
|
69
|
-
const out = flattenSubAgentMessages([
|
|
70
|
-
delegating('tc1', 'UI Builder', [msg({ agentName: 'UI Architecture Planner', content: 'x' })]),
|
|
71
|
-
]);
|
|
72
|
-
const child = out[1];
|
|
73
|
-
assert.is(child.subAgentDepth, 1);
|
|
74
|
-
assert.is(child.subAgentName, 'UI Architecture Planner');
|
|
75
|
-
assert.is(child.subAgentOf, 'tc1');
|
|
76
|
-
});
|
|
77
|
-
|
|
78
|
-
suite(
|
|
79
|
-
'moves the trace out — the emitted parent tool call no longer carries it (no duplication)',
|
|
80
|
-
() => {
|
|
81
|
-
const trace = [msg({ agentName: 'Planner', content: 'only once' })];
|
|
82
|
-
const out = flattenSubAgentMessages([delegating('tc1', 'UI Builder', trace)]);
|
|
83
|
-
// The trace is stripped from the emitted tool call ...
|
|
84
|
-
assert.is(out[0].toolCalls![0].subAgentTrace, undefined);
|
|
85
|
-
// ... and surfaces exactly once, as a hoisted top-level entry.
|
|
86
|
-
const hoisted = out.filter((e) => e.subAgentDepth === 1);
|
|
87
|
-
assert.is(hoisted.length, 1);
|
|
88
|
-
assert.is(hoisted[0].content, 'only once');
|
|
89
|
-
},
|
|
90
|
-
);
|
|
91
|
-
|
|
92
|
-
suite('preserves per-message usage fields on hoisted entries', () => {
|
|
93
|
-
const out = flattenSubAgentMessages([
|
|
94
|
-
delegating('tc1', 'UI Builder', [
|
|
95
|
-
msg({
|
|
96
|
-
agentName: 'Planner',
|
|
97
|
-
model: 'claude-sonnet-4-6',
|
|
98
|
-
inputTokens: 100,
|
|
99
|
-
outputTokens: 20,
|
|
100
|
-
cost: 0.5,
|
|
101
|
-
}),
|
|
102
|
-
]),
|
|
103
|
-
]);
|
|
104
|
-
const child = out[1];
|
|
105
|
-
assert.is(child.model, 'claude-sonnet-4-6');
|
|
106
|
-
assert.is(child.inputTokens, 100);
|
|
107
|
-
assert.is(child.outputTokens, 20);
|
|
108
|
-
assert.is(child.cost, 0.5);
|
|
109
|
-
});
|
|
110
|
-
|
|
111
|
-
suite('composes the breadcrumb for nested sub-agents', () => {
|
|
112
|
-
const grandchild = [msg({ agentName: 'Grounding', content: 'deep' })];
|
|
113
|
-
const childTrace = [delegating('tc2', 'Planner', grandchild)];
|
|
114
|
-
const out = flattenSubAgentMessages([delegating('tc1', 'UI Builder', childTrace)]);
|
|
115
|
-
|
|
116
|
-
// top: UI Builder | depth1: Planner | depth2: Grounding
|
|
117
|
-
const depth1 = out.find((e) => e.subAgentDepth === 1)!;
|
|
118
|
-
const depth2 = out.find((e) => e.subAgentDepth === 2)!;
|
|
119
|
-
assert.is(depth1.agentName, 'UI Builder › Planner');
|
|
120
|
-
assert.is(depth2.agentName, 'UI Builder › Planner › Grounding');
|
|
121
|
-
assert.is(depth2.subAgentName, 'Grounding');
|
|
122
|
-
assert.is(depth2.subAgentOf, 'tc2');
|
|
123
|
-
});
|
|
124
|
-
|
|
125
|
-
suite('correlates two sub-agents spawned in the same parent turn', () => {
|
|
126
|
-
const parent = msg({
|
|
127
|
-
agentName: 'UI Builder',
|
|
128
|
-
toolCalls: [
|
|
129
|
-
{
|
|
130
|
-
id: 'a',
|
|
131
|
-
name: 'requestSubAgent',
|
|
132
|
-
args: {},
|
|
133
|
-
subAgentTrace: [msg({ agentName: 'Planner', content: 'p' })],
|
|
134
|
-
},
|
|
135
|
-
{
|
|
136
|
-
id: 'b',
|
|
137
|
-
name: 'requestSubAgent',
|
|
138
|
-
args: {},
|
|
139
|
-
subAgentTrace: [msg({ agentName: 'Reviewer', content: 'r' })],
|
|
140
|
-
},
|
|
141
|
-
],
|
|
142
|
-
});
|
|
143
|
-
const out = flattenSubAgentMessages([parent]);
|
|
144
|
-
const planner = out.find((e) => e.subAgentName === 'Planner')!;
|
|
145
|
-
const reviewer = out.find((e) => e.subAgentName === 'Reviewer')!;
|
|
146
|
-
assert.is(planner.subAgentOf, 'a');
|
|
147
|
-
assert.is(reviewer.subAgentOf, 'b');
|
|
148
|
-
assert.is(planner.agentName, 'UI Builder › Planner');
|
|
149
|
-
assert.is(reviewer.agentName, 'UI Builder › Reviewer');
|
|
150
|
-
});
|
|
151
|
-
|
|
152
|
-
suite('falls back gracefully when the spawning message has no agentName', () => {
|
|
153
|
-
const out = flattenSubAgentMessages([
|
|
154
|
-
delegating('tc1', undefined as unknown as string, [
|
|
155
|
-
msg({ agentName: 'Planner', content: 'x' }),
|
|
156
|
-
]),
|
|
157
|
-
]);
|
|
158
|
-
// No parent name to prefix → show the sub-agent's own name unbreadcrumbed.
|
|
159
|
-
assert.is(out[1].agentName, 'Planner');
|
|
160
|
-
assert.is(out[1].subAgentName, 'Planner');
|
|
161
|
-
});
|
|
162
|
-
|
|
163
|
-
suite.run();
|
|
@@ -1,88 +0,0 @@
|
|
|
1
|
-
import type { ChatMessage } from '@genesislcap/foundation-ai';
|
|
2
|
-
|
|
3
|
-
/**
|
|
4
|
-
* A chat message lifted into the debug-log timeline as a `kind: 'message'` entry.
|
|
5
|
-
*
|
|
6
|
-
* @beta
|
|
7
|
-
*/
|
|
8
|
-
export type TimelineMessage = ChatMessage & {
|
|
9
|
-
kind: 'message';
|
|
10
|
-
/**
|
|
11
|
-
* Set only on entries hoisted out of a `subAgentTrace`: the sub-agent's own
|
|
12
|
-
* (un-breadcrumbed) `agentName`, kept for filtering since the displayed
|
|
13
|
-
* `agentName` is rewritten to a `"<parent> › <sub-agent>"` breadcrumb.
|
|
14
|
-
*/
|
|
15
|
-
subAgentName?: string;
|
|
16
|
-
/** Delegation depth — absent/0 for top-level, 1 for a sub-agent, 2 for a sub-agent's sub-agent, … */
|
|
17
|
-
subAgentDepth?: number;
|
|
18
|
-
/** Id of the parent tool call that spawned this run — correlates a hoisted message back to its delegation. */
|
|
19
|
-
subAgentOf?: string;
|
|
20
|
-
};
|
|
21
|
-
|
|
22
|
-
/**
|
|
23
|
-
* Flatten sub-agent conversations into a single top-level message stream for the
|
|
24
|
-
* debug-log timeline.
|
|
25
|
-
*
|
|
26
|
-
* A tool call that delegates to a sub-agent carries the sub-agent's entire
|
|
27
|
-
* conversation on `toolCall.subAgentTrace`. The live UI reads that nested shape,
|
|
28
|
-
* but a top-to-bottom timeline reads far better with those messages inline next to
|
|
29
|
-
* the turn that produced them. This walks the list and, for every tool call with a
|
|
30
|
-
* `subAgentTrace`, hoists the sub-agent's messages up as their own timeline
|
|
31
|
-
* entries while **removing** the nested copy from the emitted parent tool call — a
|
|
32
|
-
* move, not a copy. The data is relocated, never duplicated: the export stays the
|
|
33
|
-
* same size and a naive `sum(timeline[].cost)` does not double-count (the
|
|
34
|
-
* canonical `sumCosts` total still reads the un-flattened history).
|
|
35
|
-
*
|
|
36
|
-
* Hoisted entries are:
|
|
37
|
-
* - **breadcrumbed**: `agentName` becomes `"<parent> › <sub-agent>"`, composing for
|
|
38
|
-
* nested sub-agents (`"<parent> › <sub> › <subsub>"`); the raw sub-agent name is
|
|
39
|
-
* preserved on `subAgentName`.
|
|
40
|
-
* - **depth-tagged** (`subAgentDepth`) and **correlated** to the spawning tool call
|
|
41
|
-
* (`subAgentOf`), so the delegation tree is reconstructable even when two
|
|
42
|
-
* sub-agents run within the same parent turn.
|
|
43
|
-
*
|
|
44
|
-
* Top-level messages (depth 0) are emitted unchanged apart from the trace strip.
|
|
45
|
-
* Order is preserved within each list; the caller sorts the whole timeline by
|
|
46
|
-
* timestamp afterwards, and every sub-agent message carries its own timestamp, so
|
|
47
|
-
* hoisted entries interleave chronologically between the parent's tool-call and
|
|
48
|
-
* tool-result messages.
|
|
49
|
-
*
|
|
50
|
-
* @internal
|
|
51
|
-
*/
|
|
52
|
-
export function flattenSubAgentMessages(
|
|
53
|
-
messages: readonly ChatMessage[],
|
|
54
|
-
prefix = '',
|
|
55
|
-
depth = 0,
|
|
56
|
-
subAgentOf?: string,
|
|
57
|
-
): TimelineMessage[] {
|
|
58
|
-
return messages.flatMap((m) => {
|
|
59
|
-
const breadcrumb = prefix ? `${prefix} › ${m.agentName ?? '?'}` : m.agentName;
|
|
60
|
-
const traceCalls = m.toolCalls?.filter((tc) => tc.subAgentTrace?.length) ?? [];
|
|
61
|
-
|
|
62
|
-
const entry: TimelineMessage = {
|
|
63
|
-
kind: 'message',
|
|
64
|
-
...m,
|
|
65
|
-
// Move the (about-to-be-hoisted) child conversations out of the emitted tool
|
|
66
|
-
// calls. Setting it to `undefined` drops the key from the serialized export.
|
|
67
|
-
...(traceCalls.length > 0 && {
|
|
68
|
-
toolCalls: m.toolCalls!.map((tc) =>
|
|
69
|
-
tc.subAgentTrace ? { ...tc, subAgentTrace: undefined } : tc,
|
|
70
|
-
),
|
|
71
|
-
}),
|
|
72
|
-
// Mark + breadcrumb hoisted sub-agent messages; top-level (depth 0) stays as-is.
|
|
73
|
-
...(depth > 0 && {
|
|
74
|
-
agentName: breadcrumb,
|
|
75
|
-
subAgentName: m.agentName,
|
|
76
|
-
subAgentDepth: depth,
|
|
77
|
-
subAgentOf,
|
|
78
|
-
}),
|
|
79
|
-
};
|
|
80
|
-
|
|
81
|
-
if (traceCalls.length === 0) return [entry];
|
|
82
|
-
|
|
83
|
-
const hoisted = traceCalls.flatMap((tc) =>
|
|
84
|
-
flattenSubAgentMessages(tc.subAgentTrace!, breadcrumb ?? '', depth + 1, tc.id),
|
|
85
|
-
);
|
|
86
|
-
return [entry, ...hoisted];
|
|
87
|
-
});
|
|
88
|
-
}
|
package/src/utils/format-usd.ts
DELETED
|
@@ -1,26 +0,0 @@
|
|
|
1
|
-
/**
|
|
2
|
-
* The ONE place this package renders a USD amount as text.
|
|
3
|
-
*
|
|
4
|
-
* Extracted (GENC-1464 budget meter) from the ad-hoc `.toFixed(2)` call sites
|
|
5
|
-
* that had grown in `formatBlockedReason` and the settings modal's cost blocks,
|
|
6
|
-
* so the blocked banner, the session-cost figures and the budget meter cannot
|
|
7
|
-
* drift to different precisions. Two decimals always — money columns jitter
|
|
8
|
-
* when "$5.1" sits above "$5.10".
|
|
9
|
-
*
|
|
10
|
-
* @internal
|
|
11
|
-
*/
|
|
12
|
-
|
|
13
|
-
/** Decimal places for every USD figure this package renders. */
|
|
14
|
-
export const USD_DECIMALS = 2;
|
|
15
|
-
|
|
16
|
-
/**
|
|
17
|
-
* The bare amount, no currency sign — `(12.3) => "12.30"`.
|
|
18
|
-
*
|
|
19
|
-
* Exists for the settings modal's cost blocks, where the `$` is a separately
|
|
20
|
-
* styled `<span class="prefix">` and folding it into the text would change the
|
|
21
|
-
* DOM those styles target.
|
|
22
|
-
*/
|
|
23
|
-
export const formatUsdAmount = (value: number): string => value.toFixed(USD_DECIMALS);
|
|
24
|
-
|
|
25
|
-
/** A signed USD figure — `(12.3) => "$12.30"`. */
|
|
26
|
-
export const formatUsd = (value: number): string => `$${formatUsdAmount(value)}`;
|