@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,448 +0,0 @@
|
|
|
1
|
-
import type { ChatMessage, CondensePolicy, CondenseTrigger } from '@genesislcap/foundation-ai';
|
|
2
|
-
|
|
3
|
-
/**
|
|
4
|
-
* Tool-context condensation: collapse stale tool payloads out of the history
|
|
5
|
-
* sent to the model, while leaving stored history untouched.
|
|
6
|
-
*
|
|
7
|
-
* The driver owns the registry (tool handlers populate it via `condenseWhen`)
|
|
8
|
-
* and the cadence (this runs before every provider call). This module owns the
|
|
9
|
-
* pure application: given the registry + the current loop iteration, produce a
|
|
10
|
-
* rewritten copy of the history with stale args/results replaced by stubs. Kept
|
|
11
|
-
* pure (no driver/session state, no logging) so it unit-tests in isolation —
|
|
12
|
-
* the one-time `context.condensed` signal is delivered via an `onCondensed`
|
|
13
|
-
* callback rather than reaching into the debug-event log directly.
|
|
14
|
-
*
|
|
15
|
-
* @packageDocumentation
|
|
16
|
-
*/
|
|
17
|
-
|
|
18
|
-
/**
|
|
19
|
-
* Below this serialized size a tool payload isn't worth condensing — the stub
|
|
20
|
-
* would save little and cost a breadcrumb. A driver-level floor so the public
|
|
21
|
-
* `condenseWhen` API needs no per-policy threshold field.
|
|
22
|
-
*/
|
|
23
|
-
export const CONDENSE_MIN_CHARS = 1000;
|
|
24
|
-
|
|
25
|
-
/** Key the collapsed args are stored under — a tool-call's args must stay a Record. */
|
|
26
|
-
export const CONDENSED_ARGS_KEY = 'condensed';
|
|
27
|
-
|
|
28
|
-
/**
|
|
29
|
-
* Rough chars-per-token divisor for the `tokensSaved` estimate. No tokenizer is
|
|
30
|
-
* available in this stack; ~4 chars/token is the usual English approximation.
|
|
31
|
-
*/
|
|
32
|
-
const APPROX_CHARS_PER_TOKEN = 4;
|
|
33
|
-
|
|
34
|
-
/**
|
|
35
|
-
* A `condenseWhen` policy registered against a tool-call id, with its age clock
|
|
36
|
-
* and report-once flags. The driver holds these in a `Map<toolCallId, …>`;
|
|
37
|
-
* {@link applyCondensation} reads them and flips the `reported*` flags so the
|
|
38
|
-
* `context.condensed` signal fires once per payload, not once per provider call.
|
|
39
|
-
*/
|
|
40
|
-
export interface RegisteredCondensePolicy {
|
|
41
|
-
policy: CondensePolicy;
|
|
42
|
-
/**
|
|
43
|
-
* Monotonic model-call sequence (`modelCallSeq`) when `condenseWhen` was called
|
|
44
|
-
* — the origin of the age clock. Driver-lifetime and NOT reset per turn, so age
|
|
45
|
-
* is measured consistently across turn boundaries (the field name is historical;
|
|
46
|
-
* it is not the per-turn loop iteration).
|
|
47
|
-
*/
|
|
48
|
-
iteration: number;
|
|
49
|
-
/** Turn (`sendMessage`) sequence when the call was made — the `turnEnd` clock. */
|
|
50
|
-
turn: number;
|
|
51
|
-
/** Agent-activation sequence when the call was made — the `agentEnd` clock. */
|
|
52
|
-
activation: number;
|
|
53
|
-
/** App-controlled phase epoch when the call was made — the `phaseEnd` clock. */
|
|
54
|
-
phaseEpoch: number;
|
|
55
|
-
/** `context.condensed` already emitted for the args payload (fire-once). */
|
|
56
|
-
reportedArgs?: boolean;
|
|
57
|
-
/** `context.condensed` already emitted for the response payload (fire-once). */
|
|
58
|
-
reportedResponse?: boolean;
|
|
59
|
-
/**
|
|
60
|
-
* The first trigger to fire for this call, latched on the pass it fired.
|
|
61
|
-
*
|
|
62
|
-
* "Fired", not "collapsed": it is written during trigger resolution, before the
|
|
63
|
-
* per-payload size floor is applied, so an entry whose payloads are all below the floor
|
|
64
|
-
* can latch without anything ever rendering. That is deliberate and safe — both payloads
|
|
65
|
-
* of a call resolve off the same clocks in the same pass, so they can never disagree
|
|
66
|
-
* about which trigger fired, and a payload's size is fixed for the life of the call
|
|
67
|
-
* (condensation never touches stored history), so a below-floor payload can never later
|
|
68
|
-
* become a large one under a stale reason.
|
|
69
|
-
*
|
|
70
|
-
* Trigger resolution picks the first LISTED trigger that is *currently* true, so without
|
|
71
|
-
* this it re-evaluates on every provider call and can change after the collapse: with the
|
|
72
|
-
* usual `on: [{superseded}, {agentEnd}]`, a payload collapsed at `agentEnd` silently
|
|
73
|
-
* re-resolves to `superseded` as soon as a newer call on the same key lands. Any stub that
|
|
74
|
-
* embeds the reason (`pointer` does) is then rewritten — an in-place history mutation that
|
|
75
|
-
* invalidates the prompt cache from that position a second time, for a payload that had
|
|
76
|
-
* already collapsed. Latching keeps the rendered stub stable, which is sound because a
|
|
77
|
-
* collapse is monotonic and never reverts, so the first trigger to fire is the correct
|
|
78
|
-
* one to keep.
|
|
79
|
-
*/
|
|
80
|
-
firedTrigger?: CondenseTrigger;
|
|
81
|
-
}
|
|
82
|
-
|
|
83
|
-
/**
|
|
84
|
-
* The driver's live state at the moment of a provider call — the clocks every
|
|
85
|
-
* trigger is evaluated against. Passed by the driver; kept as an interface so the
|
|
86
|
-
* transform stays a pure function of (history, policies, clocks).
|
|
87
|
-
*/
|
|
88
|
-
export interface CondenseContext {
|
|
89
|
-
/** Monotonic model-call sequence now — the `age` clock. */
|
|
90
|
-
modelCall: number;
|
|
91
|
-
/** Monotonic turn (`sendMessage`) sequence now — the `turnEnd` clock. */
|
|
92
|
-
turn: number;
|
|
93
|
-
/** Whether the agent activation that made a call has since ended — the `agentEnd` clock. */
|
|
94
|
-
activationEnded: (activation: number) => boolean;
|
|
95
|
-
/**
|
|
96
|
-
* Whether a later phase epoch is current than the one a call was made in — the
|
|
97
|
-
* `phaseEnd` clock. True once `endPhase()` has been called since the call.
|
|
98
|
-
*/
|
|
99
|
-
phaseEnded: (phaseEpoch: number) => boolean;
|
|
100
|
-
/**
|
|
101
|
-
* Collapse in batches of this many model-calls instead of continuously. A positive
|
|
102
|
-
* integer; decimals are floored and anything below `1` becomes `1`. Omit (or `1`) to
|
|
103
|
-
* collapse as soon as a trigger fires, which is the historical behaviour.
|
|
104
|
-
*
|
|
105
|
-
* Condensation rewrites a payload *in place*, so the model-bound history is not
|
|
106
|
-
* append-only and every prompt-cache entry written before the rewrite is invalidated
|
|
107
|
-
* from that position on. Collapsing the moment each trigger fires therefore breaks the
|
|
108
|
-
* cache on nearly every call of a re-read loop — the case condensation exists to
|
|
109
|
-
* contain. Batching holds the collapse set still between boundaries so the prefix is
|
|
110
|
-
* append-only in between, trading up to `batchCalls` calls' worth of extra context for
|
|
111
|
-
* one cache break per batch instead of one per call.
|
|
112
|
-
*
|
|
113
|
-
* `agentEnd` and `phaseEnd` are exempt and still collapse as they fire: they mark a
|
|
114
|
-
* discontinuity that invalidates the cached prefix anyway, so deferring them would hold
|
|
115
|
-
* stale payloads longer for nothing.
|
|
116
|
-
*/
|
|
117
|
-
batchCalls?: number;
|
|
118
|
-
}
|
|
119
|
-
|
|
120
|
-
/**
|
|
121
|
-
* Structured detail for a `context.condensed` meta-event. The index signature
|
|
122
|
-
* lets it satisfy `recordMetaEvent`'s `Record<string, unknown>` detail param
|
|
123
|
-
* while the named fields stay strictly typed.
|
|
124
|
-
*/
|
|
125
|
-
export interface CondensedEventDetail {
|
|
126
|
-
[key: string]: unknown;
|
|
127
|
-
/** Tool-loop iteration the collapsed call was made on. */
|
|
128
|
-
turn: number;
|
|
129
|
-
toolCallId: string;
|
|
130
|
-
tool: string;
|
|
131
|
-
target: 'args' | 'response';
|
|
132
|
-
/** Human-readable trigger, e.g. `superseded:Foo.tsx` or `age:3`. */
|
|
133
|
-
trigger: string;
|
|
134
|
-
/** Length of the replacement stub. */
|
|
135
|
-
stubLen: number;
|
|
136
|
-
/** Estimated input tokens saved this collapse (char/4 — no tokenizer in-stack). */
|
|
137
|
-
tokensSaved: number;
|
|
138
|
-
}
|
|
139
|
-
|
|
140
|
-
/** Short human-readable reason for the stub text, per fired trigger. */
|
|
141
|
-
function triggerReason(trigger: CondenseTrigger): string {
|
|
142
|
-
switch (trigger.kind) {
|
|
143
|
-
case 'age':
|
|
144
|
-
return 'aged out';
|
|
145
|
-
case 'turnEnd':
|
|
146
|
-
return 'turn ended';
|
|
147
|
-
case 'agentEnd':
|
|
148
|
-
return 'agent finished';
|
|
149
|
-
case 'phaseEnd':
|
|
150
|
-
return 'phase ended';
|
|
151
|
-
default:
|
|
152
|
-
return 'superseded';
|
|
153
|
-
}
|
|
154
|
-
}
|
|
155
|
-
|
|
156
|
-
/**
|
|
157
|
-
* Framework-generated stub, shared by `pointer` and `drop`. Names the tool, the superseded key
|
|
158
|
-
* (the only surviving identifier once args are elided — the model cannot otherwise tell WHICH
|
|
159
|
-
* file a collapsed `vfs_write` wrote) and the original size.
|
|
160
|
-
*
|
|
161
|
-
* `restorable` is the ONLY difference between the two modes: `pointer` promises that re-calling
|
|
162
|
-
* reproduces the content and is valid solely for idempotent tools; `drop` makes no such promise
|
|
163
|
-
* and must not carry the clause. Identifying information is not what separates them — a stub too
|
|
164
|
-
* bare to say what the call did is how GENC-1494 started.
|
|
165
|
-
*/
|
|
166
|
-
function condenseStub(
|
|
167
|
-
target: 'args' | 'response',
|
|
168
|
-
tool: string,
|
|
169
|
-
trigger: CondenseTrigger,
|
|
170
|
-
origLen: number,
|
|
171
|
-
restorable: boolean,
|
|
172
|
-
/**
|
|
173
|
-
* Images elided alongside the text. Reported separately because the model reads this stub as
|
|
174
|
-
* fact: folding base64 bytes into the char count would tell it a 15KB render was "~15447
|
|
175
|
-
* chars" of prose. One variable cannot be both the size FLOOR (bytes, correctly) and a
|
|
176
|
-
* model-facing description (characters).
|
|
177
|
-
*/
|
|
178
|
-
imageCount = 0,
|
|
179
|
-
): string {
|
|
180
|
-
const what = target === 'args' ? 'args' : 'result';
|
|
181
|
-
const key = trigger.kind === 'superseded' ? ` ${trigger.by}` : '';
|
|
182
|
-
const restore = restorable ? '; re-call to restore' : '';
|
|
183
|
-
const images = imageCount > 0 ? ` + ${imageCount} image${imageCount === 1 ? '' : 's'}` : '';
|
|
184
|
-
return `[${tool}${key} — ${what} elided, ~${origLen} chars${images} (${triggerReason(trigger)})${restore}]`;
|
|
185
|
-
}
|
|
186
|
-
|
|
187
|
-
/** Stable label for the `context.condensed` meta-event's `trigger` field. */
|
|
188
|
-
function triggerLabel(trigger: CondenseTrigger): string {
|
|
189
|
-
switch (trigger.kind) {
|
|
190
|
-
case 'age':
|
|
191
|
-
return `age:${trigger.turns}`;
|
|
192
|
-
case 'superseded':
|
|
193
|
-
return `superseded:${trigger.by}`;
|
|
194
|
-
default:
|
|
195
|
-
return trigger.kind; // 'turnEnd' | 'agentEnd' | 'phaseEnd'
|
|
196
|
-
}
|
|
197
|
-
}
|
|
198
|
-
|
|
199
|
-
function estimateTokensSaved(origLen: number, stubLen: number): number {
|
|
200
|
-
return Math.max(0, Math.ceil((origLen - stubLen) / APPROX_CHARS_PER_TOKEN));
|
|
201
|
-
}
|
|
202
|
-
|
|
203
|
-
/**
|
|
204
|
-
* Whether a trigger marks a genuine discontinuity rather than firing continuously, and so is
|
|
205
|
-
* exempt from batching.
|
|
206
|
-
*
|
|
207
|
-
* `agentEnd` advances only when the active agent's name changes (or a sub-agent completes /
|
|
208
|
-
* the agent is released), which swaps the system prompt and tool definitions — those render
|
|
209
|
-
* before the messages, so the cached prefix is invalidated at that point regardless. `phaseEnd`
|
|
210
|
-
* is the app declaring the same kind of boundary explicitly, and falls back to `agentEnd`.
|
|
211
|
-
* Deferring either to the next batch would buy no cache and only hold stale payloads longer.
|
|
212
|
-
*
|
|
213
|
-
* The rest fire continuously (`superseded`, `age`) or at a point where the history is otherwise
|
|
214
|
-
* append-only (`turnEnd` — a new turn appends a user message, leaving the previous entry live),
|
|
215
|
-
* so batching them is exactly where the cache is won.
|
|
216
|
-
*/
|
|
217
|
-
function marksDiscontinuity(kind: CondenseTrigger['kind']): boolean {
|
|
218
|
-
return kind === 'agentEnd' || kind === 'phaseEnd';
|
|
219
|
-
}
|
|
220
|
-
|
|
221
|
-
/**
|
|
222
|
-
* Collapse stale tool payloads (declared via `condenseWhen`) out of the
|
|
223
|
-
* model-bound history. Pure over the `history` array — it emits new message
|
|
224
|
-
* objects and never mutates the input messages (mirroring the agent-masking
|
|
225
|
-
* transform). It DOES, however, write two latches onto the matching `policies`
|
|
226
|
-
* entry the first time a payload collapses, both because the collapse itself
|
|
227
|
-
* re-runs before every provider call: the `reported*` flag (the report-once gate
|
|
228
|
-
* that keeps `onCondensed` firing once per (tool call, payload)) and
|
|
229
|
-
* `firedTrigger` (which pins the reason so a reason-bearing stub is rendered
|
|
230
|
-
* once and never rewritten).
|
|
231
|
-
*
|
|
232
|
-
* Triggers (a policy may list several via `on: Trigger[]` — the FIRST to fire
|
|
233
|
-
* collapses the payload; all are monotonic, so the collapse never reverts):
|
|
234
|
-
* - `superseded` — among all registered calls sharing a `by` key, the LAST in
|
|
235
|
-
* history order survives; every earlier one's targeted payload collapses. A
|
|
236
|
-
* re-call with the same key becomes the new survivor and re-arms the rest.
|
|
237
|
-
* - `age` — the result is first visible one model-call after the call, so `turns`
|
|
238
|
-
* is how many model-calls may see the full result before it collapses
|
|
239
|
-
* (fires when `modelCall − callModelCall > turns`): `turns: 1` is seen once,
|
|
240
|
-
* then collapses. The clock is monotonic across turns.
|
|
241
|
-
* - `turnEnd` — collapses once the turn (`sendMessage`) that made the call has
|
|
242
|
-
* ended (fires when `turn > callTurn`): full for the rest of that request,
|
|
243
|
-
* gone on every later one.
|
|
244
|
-
* - `agentEnd` — collapses once the agent activation that made the call has ended
|
|
245
|
-
* (a swap to another agent, or `releaseAgent`/`completeSubAgent`): kept across
|
|
246
|
-
* all of the agent's turns, dropped when its flow finishes.
|
|
247
|
-
* - `phaseEnd` — collapses once the app advances its phase epoch via `endPhase()`
|
|
248
|
-
* after the call (a sub-`agentEnd` boundary the author declares), OR at
|
|
249
|
-
* `agentEnd` as a backstop if no boundary is ever declared.
|
|
250
|
-
*
|
|
251
|
-
* The tool-call/result envelope is never removed (providers reject orphaned
|
|
252
|
-
* calls/results) — only the args object or the result content is replaced.
|
|
253
|
-
*
|
|
254
|
-
* @param history - The to-model history copy to rewrite (not mutated).
|
|
255
|
-
* @param policies - The driver's registry, keyed by tool-call id. Its entries'
|
|
256
|
-
* `reported*` flags are flipped as payloads first collapse.
|
|
257
|
-
* @param ctx - The driver's live clocks (model-call / turn / agent-activation).
|
|
258
|
-
* @param onCondensed - Invoked once per payload at its full→stub transition.
|
|
259
|
-
*/
|
|
260
|
-
export function applyCondensation(
|
|
261
|
-
history: ChatMessage[],
|
|
262
|
-
policies: Map<string, RegisteredCondensePolicy>,
|
|
263
|
-
ctx: CondenseContext,
|
|
264
|
-
onCondensed: (detail: CondensedEventDetail) => void,
|
|
265
|
-
): ChatMessage[] {
|
|
266
|
-
if (policies.size === 0) return history;
|
|
267
|
-
|
|
268
|
-
const triggersOf = (entry: RegisteredCondensePolicy): CondenseTrigger[] =>
|
|
269
|
-
Array.isArray(entry.policy.on) ? entry.policy.on : [entry.policy.on];
|
|
270
|
-
|
|
271
|
-
// One pass: resolve supersession survivors and a toolCallId → tool-name
|
|
272
|
-
// lookup. For each `superseded` key the LAST call in history order survives.
|
|
273
|
-
// The name lookup lets a tool message (which carries only an id) name its tool
|
|
274
|
-
// in stubs and events.
|
|
275
|
-
// Batch boundary: the collapse set is resolved as of this model-call and stays put
|
|
276
|
-
// until the next boundary, so the prefix is append-only in between. `batchCalls` of 1
|
|
277
|
-
// (the default) makes this `ctx.modelCall` — i.e. no batching.
|
|
278
|
-
const batchCalls = Math.max(1, Math.floor(ctx.batchCalls ?? 1));
|
|
279
|
-
const asOf =
|
|
280
|
-
batchCalls === 1 ? ctx.modelCall : Math.floor(ctx.modelCall / batchCalls) * batchCalls;
|
|
281
|
-
// A call made after the boundary is invisible to this pass in BOTH directions: it
|
|
282
|
-
// cannot supersede an older call (which would collapse the older payload early), and
|
|
283
|
-
// it cannot itself collapse (which would elide the very result just fetched). Unbatched,
|
|
284
|
-
// the boundary is the live clock and this gate is off entirely — a call registered on
|
|
285
|
-
// the current model-call is still eligible, as it always was.
|
|
286
|
-
const withinBatch = (entry: RegisteredCondensePolicy): boolean =>
|
|
287
|
-
batchCalls === 1 || entry.iteration <= asOf;
|
|
288
|
-
|
|
289
|
-
const latestByKey = new Map<string, string>();
|
|
290
|
-
const nameById = new Map<string, string>();
|
|
291
|
-
for (const msg of history) {
|
|
292
|
-
if (!msg.toolCalls?.length) continue;
|
|
293
|
-
for (const tc of msg.toolCalls) {
|
|
294
|
-
nameById.set(tc.id, tc.name);
|
|
295
|
-
const entry = policies.get(tc.id);
|
|
296
|
-
if (!entry || !withinBatch(entry)) continue;
|
|
297
|
-
for (const t of triggersOf(entry)) {
|
|
298
|
-
if (t.kind === 'superseded') latestByKey.set(t.by, tc.id);
|
|
299
|
-
}
|
|
300
|
-
}
|
|
301
|
-
}
|
|
302
|
-
|
|
303
|
-
const triggerFires = (
|
|
304
|
-
toolCallId: string,
|
|
305
|
-
entry: RegisteredCondensePolicy,
|
|
306
|
-
trig: CondenseTrigger,
|
|
307
|
-
): boolean => {
|
|
308
|
-
switch (trig.kind) {
|
|
309
|
-
// Result first visible one model-call after the call, so collapse one call
|
|
310
|
-
// past the last allowed view: turns:1 → seen once, then gone. Monotonic
|
|
311
|
-
// clock, so this holds across turn boundaries too.
|
|
312
|
-
case 'age':
|
|
313
|
-
return asOf - entry.iteration > trig.turns;
|
|
314
|
-
case 'turnEnd':
|
|
315
|
-
return ctx.turn > entry.turn;
|
|
316
|
-
case 'agentEnd':
|
|
317
|
-
return ctx.activationEnded(entry.activation);
|
|
318
|
-
// Fires when the app advanced the phase epoch since the call — OR, as a
|
|
319
|
-
// backstop, when the whole activation has ended, so a phase boundary that
|
|
320
|
-
// is never declared still collapses at `agentEnd` rather than never.
|
|
321
|
-
case 'phaseEnd':
|
|
322
|
-
return ctx.phaseEnded(entry.phaseEpoch) || ctx.activationEnded(entry.activation);
|
|
323
|
-
default:
|
|
324
|
-
return latestByKey.get(trig.by) !== toolCallId; // superseded by a newer call
|
|
325
|
-
}
|
|
326
|
-
};
|
|
327
|
-
|
|
328
|
-
// The first listed trigger that fires — drives the collapse, the stub reason,
|
|
329
|
-
// and the event label. `undefined` means the payload stays full.
|
|
330
|
-
//
|
|
331
|
-
// Latched on first fire (see `firedTrigger`): re-resolving on every pass lets the reason
|
|
332
|
-
// change under a payload that has already collapsed, which rewrites any reason-bearing stub
|
|
333
|
-
// and costs a second cache break. Both payloads of a call share the latch — they are
|
|
334
|
-
// evaluated in the same pass off the same clocks, so they never disagree about which
|
|
335
|
-
// trigger fired; only the size floor decides whether each one renders.
|
|
336
|
-
const firstFired = (
|
|
337
|
-
toolCallId: string,
|
|
338
|
-
entry: RegisteredCondensePolicy,
|
|
339
|
-
): CondenseTrigger | undefined => {
|
|
340
|
-
if (entry.firedTrigger) return entry.firedTrigger;
|
|
341
|
-
const fired = triggersOf(entry).find(
|
|
342
|
-
(t) =>
|
|
343
|
-
(marksDiscontinuity(t.kind) || withinBatch(entry)) && triggerFires(toolCallId, entry, t),
|
|
344
|
-
);
|
|
345
|
-
if (fired) entry.firedTrigger = fired;
|
|
346
|
-
return fired;
|
|
347
|
-
};
|
|
348
|
-
|
|
349
|
-
return history.map((msg) => {
|
|
350
|
-
// Tool-call ARGS live on the assistant message.
|
|
351
|
-
if (msg.toolCalls?.length) {
|
|
352
|
-
let changed = false;
|
|
353
|
-
const toolCalls = msg.toolCalls.map((tc) => {
|
|
354
|
-
const entry = policies.get(tc.id);
|
|
355
|
-
if (!entry?.policy.args) return tc;
|
|
356
|
-
const fired = firstFired(tc.id, entry);
|
|
357
|
-
if (!fired) return tc;
|
|
358
|
-
const origLen = JSON.stringify(tc.args ?? {}).length;
|
|
359
|
-
if (origLen < CONDENSE_MIN_CHARS) return tc;
|
|
360
|
-
changed = true;
|
|
361
|
-
const result = entry.policy.args;
|
|
362
|
-
// Args must stay a Record, so every mode stashes its stub under a single key.
|
|
363
|
-
// Spread `tc` so providerMetadata (e.g. the Gemini reasoning signature that must
|
|
364
|
-
// round-trip) and UI fields survive.
|
|
365
|
-
//
|
|
366
|
-
// GENC-1494: `drop` used to collapse args to a bare `{}`. That is indistinguishable
|
|
367
|
-
// from a call the model made with NO arguments, and the model reads its own history
|
|
368
|
-
// as fact: a ui-builder run saw its elided `vfs_write` calls as `vfs_write {}` beside
|
|
369
|
-
// a live `{"status":"buffered","path":"…"}` result, concluded it was calling the tool
|
|
370
|
-
// without content, and burned two hours re-writing files that were already correct.
|
|
371
|
-
// A bare marker is not enough either — it still fails to say WHICH file the call
|
|
372
|
-
// wrote, since `path` went with the args. So `drop` renders the same identifying stub
|
|
373
|
-
// as `pointer`, minus only the restore promise it cannot honour.
|
|
374
|
-
const args: Record<string, unknown> = {
|
|
375
|
-
[CONDENSED_ARGS_KEY]:
|
|
376
|
-
result === 'drop' || result === 'pointer'
|
|
377
|
-
? condenseStub('args', tc.name, fired, origLen, result === 'pointer')
|
|
378
|
-
: result.replaceWith,
|
|
379
|
-
};
|
|
380
|
-
if (!entry.reportedArgs) {
|
|
381
|
-
entry.reportedArgs = true;
|
|
382
|
-
const stubLen = JSON.stringify(args).length;
|
|
383
|
-
onCondensed({
|
|
384
|
-
turn: entry.iteration,
|
|
385
|
-
toolCallId: tc.id,
|
|
386
|
-
tool: tc.name,
|
|
387
|
-
target: 'args',
|
|
388
|
-
trigger: triggerLabel(fired),
|
|
389
|
-
stubLen,
|
|
390
|
-
tokensSaved: estimateTokensSaved(origLen, stubLen),
|
|
391
|
-
});
|
|
392
|
-
}
|
|
393
|
-
return { ...tc, args };
|
|
394
|
-
});
|
|
395
|
-
return changed ? { ...msg, toolCalls } : msg;
|
|
396
|
-
}
|
|
397
|
-
|
|
398
|
-
// The tool RESULT lives on the tool message.
|
|
399
|
-
if (msg.toolResult) {
|
|
400
|
-
const entry = policies.get(msg.toolResult.toolCallId);
|
|
401
|
-
// Defensive `?? 0` for restored/malformed history — `content` is typed
|
|
402
|
-
// `string`, but a 0-length payload is simply below the floor and skipped.
|
|
403
|
-
//
|
|
404
|
-
// Attachment bytes COUNT toward the floor (GENC-1508). An image-bearing result is
|
|
405
|
-
// typically a few words of text — "rendered the mockup" — plus ~15KB of base64, so
|
|
406
|
-
// sizing on `content` alone leaves it permanently under CONDENSE_MIN_CHARS and the
|
|
407
|
-
// image rides every subsequent turn. That is the exact payload condensation exists
|
|
408
|
-
// to shed, so it must be what is measured.
|
|
409
|
-
const textLen = msg.toolResult.content?.length ?? 0;
|
|
410
|
-
const images = msg.toolResult.attachments ?? [];
|
|
411
|
-
// Bytes gate the FLOOR (an image result is a few words of text plus ~15KB of base64, so
|
|
412
|
-
// sizing on text alone would never condense it); `textLen` and the image count are what
|
|
413
|
-
// the model is told. See condenseStub.
|
|
414
|
-
const origLen = textLen + images.reduce((n, att) => n + (att.data?.length ?? 0), 0);
|
|
415
|
-
const fired = entry?.policy.response
|
|
416
|
-
? firstFired(msg.toolResult.toolCallId, entry)
|
|
417
|
-
: undefined;
|
|
418
|
-
if (entry && fired && origLen >= CONDENSE_MIN_CHARS) {
|
|
419
|
-
const tool = nameById.get(msg.toolResult.toolCallId) ?? msg.toolResult.toolCallId;
|
|
420
|
-
const result = entry.policy.response!; // `fired` is only set when response is declared
|
|
421
|
-
// Kept non-empty — Anthropic rejects empty tool_result content.
|
|
422
|
-
const content =
|
|
423
|
-
result === 'drop' || result === 'pointer'
|
|
424
|
-
? condenseStub('response', tool, fired, textLen, result === 'pointer', images.length)
|
|
425
|
-
: result.replaceWith;
|
|
426
|
-
if (!entry.reportedResponse) {
|
|
427
|
-
entry.reportedResponse = true;
|
|
428
|
-
onCondensed({
|
|
429
|
-
turn: entry.iteration,
|
|
430
|
-
toolCallId: msg.toolResult.toolCallId,
|
|
431
|
-
tool,
|
|
432
|
-
target: 'response',
|
|
433
|
-
trigger: triggerLabel(fired),
|
|
434
|
-
stubLen: content.length,
|
|
435
|
-
tokensSaved: estimateTokensSaved(origLen, content.length),
|
|
436
|
-
});
|
|
437
|
-
}
|
|
438
|
-
// `attachments` is dropped, not spread through: the stub says the payload was
|
|
439
|
-
// elided, and leaving the images attached would make that a lie AND shed none of
|
|
440
|
-
// the bytes the condensation was performed to shed.
|
|
441
|
-
const { attachments: _elided, ...restOfResult } = msg.toolResult;
|
|
442
|
-
return { ...msg, toolResult: { ...restOfResult, content } };
|
|
443
|
-
}
|
|
444
|
-
}
|
|
445
|
-
|
|
446
|
-
return msg;
|
|
447
|
-
});
|
|
448
|
-
}
|
|
@@ -1,226 +0,0 @@
|
|
|
1
|
-
import { assert, createLogicSuite } from '@genesislcap/foundation-testing';
|
|
2
|
-
import type { CostSessionRecord } from './cost-session-history';
|
|
3
|
-
import {
|
|
4
|
-
clearCostSessionHistory,
|
|
5
|
-
costHistoryStorageKey,
|
|
6
|
-
formatCostSessionDate,
|
|
7
|
-
loadCostSessionHistory,
|
|
8
|
-
resolveBankedUsage,
|
|
9
|
-
saveCostSessionHistory,
|
|
10
|
-
sortRecordsByRecency,
|
|
11
|
-
upsertRecord,
|
|
12
|
-
} from './cost-session-history';
|
|
13
|
-
import { emptyUsage } from './sum-usage';
|
|
14
|
-
|
|
15
|
-
const scope = `test-${Date.now()}`;
|
|
16
|
-
|
|
17
|
-
const usage = (costUsd: number): CostSessionRecord['usage'] => ({
|
|
18
|
-
costUsd,
|
|
19
|
-
uncachedInputTokens: 100_000,
|
|
20
|
-
cacheReadTokens: 1_200_000,
|
|
21
|
-
cacheWriteTokens: 60_000,
|
|
22
|
-
outputTokens: 40_000,
|
|
23
|
-
});
|
|
24
|
-
|
|
25
|
-
const sampleRecord = (overrides: Partial<CostSessionRecord> = {}): CostSessionRecord => ({
|
|
26
|
-
id: 'rec-1',
|
|
27
|
-
projectKey: 'proj-1',
|
|
28
|
-
title: 'Equity Options Pricer',
|
|
29
|
-
updatedAt: '2026-06-14T12:00:00.000Z',
|
|
30
|
-
usage: usage(1.86),
|
|
31
|
-
// Present by default so `delete record.banked` in the legacy-row cases actually removes
|
|
32
|
-
// something, and so the storage round-trip covers it.
|
|
33
|
-
banked: usage(0.5),
|
|
34
|
-
models: [{ model: 'claude-sonnet-4-6', provider: 'anthropic' }],
|
|
35
|
-
...overrides,
|
|
36
|
-
});
|
|
37
|
-
|
|
38
|
-
const suite = createLogicSuite('costSessionHistory');
|
|
39
|
-
|
|
40
|
-
suite('uses a scoped storage key', () => {
|
|
41
|
-
assert.is(costHistoryStorageKey('my-app'), 'genesis-ai-assistant:cost-history:my-app');
|
|
42
|
-
});
|
|
43
|
-
|
|
44
|
-
suite('upsertRecord prepends a project it has not seen', () => {
|
|
45
|
-
const first = upsertRecord([], sampleRecord({ id: 'a', projectKey: 'a', title: 'First' }));
|
|
46
|
-
const both = upsertRecord(first, sampleRecord({ id: 'b', projectKey: 'b', title: 'Second' }));
|
|
47
|
-
assert.is(both.length, 2);
|
|
48
|
-
// Insertion order, not recency — display ordering is sortRecordsByRecency's job.
|
|
49
|
-
assert.is(both[0]?.id, 'b', 'most recently added is prepended');
|
|
50
|
-
assert.is(both[1]?.id, 'a');
|
|
51
|
-
});
|
|
52
|
-
|
|
53
|
-
suite('upsertRecord replaces a project in place rather than stacking a second row', () => {
|
|
54
|
-
const seeded = [
|
|
55
|
-
sampleRecord({ id: 'b', projectKey: 'b', usage: usage(2) }),
|
|
56
|
-
sampleRecord({ id: 'a', projectKey: 'a', usage: usage(1) }),
|
|
57
|
-
];
|
|
58
|
-
// Re-reporting a project must REPLACE its row. Appending here is what made a single
|
|
59
|
-
// project's spend count once per refresh in any total summed over the list.
|
|
60
|
-
const next = upsertRecord(seeded, sampleRecord({ id: 'a', projectKey: 'a', usage: usage(9.5) }));
|
|
61
|
-
assert.is(next.length, 2);
|
|
62
|
-
// Replaced in place rather than moved — the stored array is not a recency ordering.
|
|
63
|
-
assert.is(next[1]?.projectKey, 'a');
|
|
64
|
-
assert.is(next[1]?.usage.costUsd, 9.5);
|
|
65
|
-
assert.is(
|
|
66
|
-
next.reduce((sum, r) => sum + r.usage.costUsd, 0),
|
|
67
|
-
11.5,
|
|
68
|
-
);
|
|
69
|
-
});
|
|
70
|
-
|
|
71
|
-
suite('round-trips records through storage', () => {
|
|
72
|
-
clearCostSessionHistory(scope);
|
|
73
|
-
const records = upsertRecord([], sampleRecord({ id: 'a', projectKey: 'a', usage: usage(3.5) }));
|
|
74
|
-
saveCostSessionHistory(scope, records);
|
|
75
|
-
const loaded = loadCostSessionHistory(scope);
|
|
76
|
-
assert.is(loaded.length, 1);
|
|
77
|
-
assert.is(loaded[0]?.usage.costUsd, 3.5);
|
|
78
|
-
assert.is(loaded[0]?.usage.cacheReadTokens, 1_200_000, 'buckets survive serialization');
|
|
79
|
-
assert.is(loaded[0]?.banked?.costUsd, 0.5, 'banked survives serialization');
|
|
80
|
-
clearCostSessionHistory(scope);
|
|
81
|
-
});
|
|
82
|
-
|
|
83
|
-
suite('discards legacy rows that predate the per-project key', () => {
|
|
84
|
-
clearCostSessionHistory(scope);
|
|
85
|
-
const legacy = sampleRecord({ id: 'old' }) as Partial<CostSessionRecord>;
|
|
86
|
-
delete legacy.projectKey;
|
|
87
|
-
saveCostSessionHistory(scope, [legacy as CostSessionRecord, sampleRecord({ id: 'new' })]);
|
|
88
|
-
const loaded = loadCostSessionHistory(scope);
|
|
89
|
-
// A keyless row can never be matched to a project, so pooling it with the new rows
|
|
90
|
-
// would permanently double-count whichever project it came from.
|
|
91
|
-
assert.is(loaded.length, 1);
|
|
92
|
-
assert.is(loaded[0]?.id, 'new');
|
|
93
|
-
clearCostSessionHistory(scope);
|
|
94
|
-
});
|
|
95
|
-
|
|
96
|
-
suite('discards rows whose usage is not fully numeric', () => {
|
|
97
|
-
clearCostSessionHistory(scope);
|
|
98
|
-
const broken = sampleRecord({ id: 'broken' });
|
|
99
|
-
// A half-shaped `usage` is worse than a missing row: it sums as NaN and poisons every
|
|
100
|
-
// total on the Usage tab, with no clue where the NaN came from.
|
|
101
|
-
delete (broken.usage as Partial<CostSessionRecord['usage']>).outputTokens;
|
|
102
|
-
saveCostSessionHistory(scope, [broken, sampleRecord({ id: 'ok', projectKey: 'ok' })]);
|
|
103
|
-
const loaded = loadCostSessionHistory(scope);
|
|
104
|
-
assert.is(loaded.length, 1);
|
|
105
|
-
assert.is(loaded[0]?.id, 'ok');
|
|
106
|
-
clearCostSessionHistory(scope);
|
|
107
|
-
});
|
|
108
|
-
|
|
109
|
-
suite('discards rows whose banked figure is half-shaped', () => {
|
|
110
|
-
clearCostSessionHistory(scope);
|
|
111
|
-
const broken = sampleRecord({ id: 'broken', banked: { ...usage(2) } });
|
|
112
|
-
// A malformed `banked` poisons the lifetime total exactly as a malformed `usage` does,
|
|
113
|
-
// since the two are added together to produce it.
|
|
114
|
-
delete (broken.banked as Partial<CostSessionRecord['usage']>).cacheWriteTokens;
|
|
115
|
-
saveCostSessionHistory(scope, [broken, sampleRecord({ id: 'ok', projectKey: 'ok' })]);
|
|
116
|
-
const loaded = loadCostSessionHistory(scope);
|
|
117
|
-
assert.is(loaded.length, 1);
|
|
118
|
-
assert.is(loaded[0]?.id, 'ok');
|
|
119
|
-
clearCostSessionHistory(scope);
|
|
120
|
-
});
|
|
121
|
-
|
|
122
|
-
suite('keeps a row whose banked figure is absent', () => {
|
|
123
|
-
clearCostSessionHistory(scope);
|
|
124
|
-
const legacy = sampleRecord({ id: 'legacy' });
|
|
125
|
-
delete legacy.banked;
|
|
126
|
-
saveCostSessionHistory(scope, [legacy]);
|
|
127
|
-
assert.is(loadCostSessionHistory(scope).length, 1, 'absent is valid, unlike half-shaped');
|
|
128
|
-
clearCostSessionHistory(scope);
|
|
129
|
-
});
|
|
130
|
-
|
|
131
|
-
suite('formats session dates for display', () => {
|
|
132
|
-
assert.is(formatCostSessionDate('2026-06-14T12:00:00.000Z'), 'Jun 14, 2026');
|
|
133
|
-
});
|
|
134
|
-
|
|
135
|
-
suite('sortRecordsByRecency puts the most recently worked-on project first', () => {
|
|
136
|
-
const records = [
|
|
137
|
-
sampleRecord({ id: 'old', projectKey: 'old', updatedAt: '2026-01-01T00:00:00.000Z' }),
|
|
138
|
-
sampleRecord({ id: 'newest', projectKey: 'newest', updatedAt: '2026-08-04T09:00:00.000Z' }),
|
|
139
|
-
sampleRecord({ id: 'mid', projectKey: 'mid', updatedAt: '2026-05-01T00:00:00.000Z' }),
|
|
140
|
-
];
|
|
141
|
-
const sorted = sortRecordsByRecency(records);
|
|
142
|
-
assert.equal(
|
|
143
|
-
sorted.map((r) => r.id),
|
|
144
|
-
['newest', 'mid', 'old'],
|
|
145
|
-
);
|
|
146
|
-
// Read-time ordering, so the caller's array must be left alone.
|
|
147
|
-
assert.equal(
|
|
148
|
-
records.map((r) => r.id),
|
|
149
|
-
['old', 'newest', 'mid'],
|
|
150
|
-
'input not mutated',
|
|
151
|
-
);
|
|
152
|
-
});
|
|
153
|
-
|
|
154
|
-
suite('sortRecordsByRecency overrides insertion order, which upsert leaves alone', () => {
|
|
155
|
-
// The behaviour this fixes: revisiting a project replaces its row IN PLACE, so insertion order
|
|
156
|
-
// strands it wherever it was first seen. Display order must come from updatedAt instead.
|
|
157
|
-
let records = upsertRecord(
|
|
158
|
-
[],
|
|
159
|
-
sampleRecord({ id: 'a', projectKey: 'a', updatedAt: '2026-01-01T00:00:00.000Z' }),
|
|
160
|
-
);
|
|
161
|
-
records = upsertRecord(
|
|
162
|
-
records,
|
|
163
|
-
sampleRecord({ id: 'b', projectKey: 'b', updatedAt: '2026-02-01T00:00:00.000Z' }),
|
|
164
|
-
);
|
|
165
|
-
// Work on 'a' again — newer than 'b', but upsert keeps it in slot 1.
|
|
166
|
-
records = upsertRecord(
|
|
167
|
-
records,
|
|
168
|
-
sampleRecord({ id: 'a', projectKey: 'a', updatedAt: '2026-03-01T00:00:00.000Z' }),
|
|
169
|
-
);
|
|
170
|
-
assert.is(records[1]?.id, 'a', 'stored order is unchanged by the revisit');
|
|
171
|
-
assert.is(sortRecordsByRecency(records)[0]?.id, 'a', 'display order tracks the revisit');
|
|
172
|
-
});
|
|
173
|
-
|
|
174
|
-
suite('banks nothing for a project with no prior row', () => {
|
|
175
|
-
assert.equal(resolveBankedUsage(undefined, true), emptyUsage());
|
|
176
|
-
assert.equal(resolveBankedUsage(undefined, false), emptyUsage());
|
|
177
|
-
});
|
|
178
|
-
|
|
179
|
-
suite('carries the prior banked figure forward when the transcript is authoritative', () => {
|
|
180
|
-
// Restored transcript: it already re-proves everything after the row's own banked figure,
|
|
181
|
-
// so only that figure carries over. Taking `usage` here is the compounding double-count.
|
|
182
|
-
const prior = sampleRecord({ usage: usage(10), banked: { ...usage(4) } });
|
|
183
|
-
assert.is(resolveBankedUsage(prior, true).costUsd, 4);
|
|
184
|
-
});
|
|
185
|
-
|
|
186
|
-
suite('banks the whole prior total when the transcript starts empty', () => {
|
|
187
|
-
// Nothing on screen accounts for the previous total, so all of it must carry over or the
|
|
188
|
-
// next turn rewrites the row down to just that turn.
|
|
189
|
-
const prior = sampleRecord({ usage: usage(10), banked: { ...usage(4) } });
|
|
190
|
-
assert.is(resolveBankedUsage(prior, false).costUsd, 10);
|
|
191
|
-
});
|
|
192
|
-
|
|
193
|
-
suite('treats a row predating the banked field as nothing banked', () => {
|
|
194
|
-
const legacy = sampleRecord({ usage: usage(7) });
|
|
195
|
-
delete legacy.banked;
|
|
196
|
-
assert.equal(resolveBankedUsage(legacy, true), emptyUsage());
|
|
197
|
-
// Without persistence the legacy total still carries, so upgrading loses no spend.
|
|
198
|
-
assert.is(resolveBankedUsage(legacy, false).costUsd, 7);
|
|
199
|
-
});
|
|
200
|
-
|
|
201
|
-
suite('is stable across repeated reloads — the regression this guards', () => {
|
|
202
|
-
// Simulate the reload cycle: usage = banked + whatever the transcript proves. With a
|
|
203
|
-
// restored transcript (worth 6 on top of 4 banked) the row must land on 10 every time,
|
|
204
|
-
// not 10 → 14 → 18 as it did when a reload re-banked the restored history.
|
|
205
|
-
const transcript = 6;
|
|
206
|
-
let row = sampleRecord({ usage: usage(10), banked: { ...usage(4) } });
|
|
207
|
-
for (let reload = 0; reload < 3; reload += 1) {
|
|
208
|
-
const banked = resolveBankedUsage(row, true);
|
|
209
|
-
row = { ...row, banked, usage: { ...usage(banked.costUsd + transcript) } };
|
|
210
|
-
assert.is(row.usage.costUsd, 10, `reload ${reload}`);
|
|
211
|
-
}
|
|
212
|
-
});
|
|
213
|
-
|
|
214
|
-
suite('accumulates across reloads when the transcript does not persist', () => {
|
|
215
|
-
// The mirror case: each page-load starts from an empty transcript, so the row must grow by
|
|
216
|
-
// whatever that load spends rather than being overwritten by it.
|
|
217
|
-
let row = sampleRecord({ usage: usage(10), banked: { ...usage(4) } });
|
|
218
|
-
for (const spend of [2, 3]) {
|
|
219
|
-
const banked = resolveBankedUsage(row, false);
|
|
220
|
-
row = { ...row, banked, usage: { ...usage(banked.costUsd + spend) } };
|
|
221
|
-
}
|
|
222
|
-
// 10 banked, +2 → 12; then 12 banked, +3 → 15. Never decreases.
|
|
223
|
-
assert.is(row.usage.costUsd, 15);
|
|
224
|
-
});
|
|
225
|
-
|
|
226
|
-
suite.run();
|