@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,137 +0,0 @@
|
|
|
1
|
-
import { __awaiter } from "tslib";
|
|
2
|
-
import { assert, createLogicSuite } from '@genesislcap/foundation-testing';
|
|
3
|
-
// Side-effect import — MUST come before `./chat-driver` so the driver subclasses
|
|
4
|
-
// jsdom's EventTarget rather than Node's native one. Mirrors chat-driver.test.ts.
|
|
5
|
-
import './align-event-globals';
|
|
6
|
-
import { ChatDriver } from './chat-driver';
|
|
7
|
-
// ---------------------------------------------------------------------------
|
|
8
|
-
// Per-turn `thinkingPolicy` resolution.
|
|
9
|
-
//
|
|
10
|
-
// The cost case for this option is the SHAPE of a tool loop, not a single call: the
|
|
11
|
-
// opening turn is a real decision and worth reasoning over, while the iterations that
|
|
12
|
-
// follow mostly pick the next tool from a narrow set — and on a thinking model each of
|
|
13
|
-
// those bills reasoning at the full output rate. That saving only exists if the
|
|
14
|
-
// resolver runs per iteration, so these tests assert the sequence across a loop, not
|
|
15
|
-
// just that one value arrives.
|
|
16
|
-
//
|
|
17
|
-
// The other half is the undefined case. Every agent written before this option leaves
|
|
18
|
-
// it unset, and a resolver may answer `undefined` on any given turn; both must reach
|
|
19
|
-
// the transport as `undefined` so the model keeps its own default. `undefined` reaching
|
|
20
|
-
// the wire as `'off'` would be a silent capability regression, and as `'auto'` a silent
|
|
21
|
-
// bill increase — so it is asserted explicitly rather than assumed.
|
|
22
|
-
// ---------------------------------------------------------------------------
|
|
23
|
-
/** Captures the policy seen on each model call; calls one tool, then finishes. */
|
|
24
|
-
const capturingProvider = () => {
|
|
25
|
-
const seen = [];
|
|
26
|
-
let turns = 0;
|
|
27
|
-
return {
|
|
28
|
-
seen,
|
|
29
|
-
chat: (_history, _userMessage, options) => __awaiter(void 0, void 0, void 0, function* () {
|
|
30
|
-
seen.push(options === null || options === void 0 ? void 0 : options.thinkingPolicy);
|
|
31
|
-
turns += 1;
|
|
32
|
-
// Two tool calls, so the loop runs three model calls in total — enough for a
|
|
33
|
-
// per-turn resolver to say something different on the later ones.
|
|
34
|
-
if (turns <= 2) {
|
|
35
|
-
return {
|
|
36
|
-
role: 'assistant',
|
|
37
|
-
content: '',
|
|
38
|
-
toolCalls: [{ id: `t${turns}`, name: 'step', args: {} }],
|
|
39
|
-
};
|
|
40
|
-
}
|
|
41
|
-
return { role: 'assistant', content: 'done' };
|
|
42
|
-
}),
|
|
43
|
-
};
|
|
44
|
-
};
|
|
45
|
-
const makeRegistry = (provider) => ({
|
|
46
|
-
get: () => provider,
|
|
47
|
-
default: () => provider,
|
|
48
|
-
defaultName: () => 'test',
|
|
49
|
-
names: () => ['test'],
|
|
50
|
-
getStatus: () => __awaiter(void 0, void 0, void 0, function* () { return null; }),
|
|
51
|
-
listStatuses: () => __awaiter(void 0, void 0, void 0, function* () { return []; }),
|
|
52
|
-
});
|
|
53
|
-
const agent = (overrides) => (Object.assign({ name: 'worker', description: 'test agent', toolDefinitions: [
|
|
54
|
-
{ name: 'step', description: 'step', parameters: { type: 'object', properties: {} } },
|
|
55
|
-
], toolHandlers: { step: () => __awaiter(void 0, void 0, void 0, function* () { return 'stepped'; }) } }, overrides));
|
|
56
|
-
/** Run one user turn through a driver carrying `config`, and return the policies seen. */
|
|
57
|
-
const policiesFor = (config) => __awaiter(void 0, void 0, void 0, function* () {
|
|
58
|
-
const provider = capturingProvider();
|
|
59
|
-
const driver = new ChatDriver(makeRegistry(provider), {
|
|
60
|
-
maxToolIterations: 10,
|
|
61
|
-
maxFoldOperations: 5,
|
|
62
|
-
sessionKey: '',
|
|
63
|
-
});
|
|
64
|
-
driver.applyAgent(agent(config));
|
|
65
|
-
yield driver.sendMessage('go');
|
|
66
|
-
return provider.seen;
|
|
67
|
-
});
|
|
68
|
-
const suite = createLogicSuite('ChatDriver thinkingPolicy');
|
|
69
|
-
suite('leaves the policy undefined when the agent does not set one', () => __awaiter(void 0, void 0, void 0, function* () {
|
|
70
|
-
// The compatibility case: every existing agent. Undefined must reach the transport
|
|
71
|
-
// as undefined so each model keeps its own default posture — not silently coerced
|
|
72
|
-
// to 'off' (a capability regression) or 'auto' (a bill increase).
|
|
73
|
-
const seen = yield policiesFor({});
|
|
74
|
-
assert.ok(seen.length >= 3, `expected a multi-call loop, got ${seen.length}`);
|
|
75
|
-
assert.equal(seen.filter((p) => p !== undefined), [], 'no turn invents a policy');
|
|
76
|
-
}));
|
|
77
|
-
suite('applies a static policy to every turn of the loop', () => __awaiter(void 0, void 0, void 0, function* () {
|
|
78
|
-
const seen = yield policiesFor({ thinkingPolicy: 'off' });
|
|
79
|
-
assert.ok(seen.length >= 3);
|
|
80
|
-
assert.equal([...new Set(seen)], ['off'], 'a static value is not just a first-turn setting');
|
|
81
|
-
}));
|
|
82
|
-
suite('pins the resolved policy for the whole tool loop', () => __awaiter(void 0, void 0, void 0, function* () {
|
|
83
|
-
// A tool-use loop is ONE assistant turn and Anthropic requires a single thinking mode
|
|
84
|
-
// for its duration. Toggling part-way does not error — the API silently disables
|
|
85
|
-
// thinking for that request and strips blocks that would leave the turn structure
|
|
86
|
-
// invalid, so an 'auto' -> 'off' switch loses the continuity the opening call
|
|
87
|
-
// established and 'off' -> 'auto' never delivers the reasoning asked for. It also
|
|
88
|
-
// invalidates the prompt cache, costing more than the reasoning it meant to save.
|
|
89
|
-
//
|
|
90
|
-
// So a resolver that changes its mind mid-loop must NOT be honoured mid-loop. This
|
|
91
|
-
// asserts the opposite of what it looks like it should: the later values are ignored.
|
|
92
|
-
let call = 0;
|
|
93
|
-
const seen = yield policiesFor({
|
|
94
|
-
thinkingPolicy: () => {
|
|
95
|
-
call += 1;
|
|
96
|
-
return call === 1 ? 'auto' : 'off';
|
|
97
|
-
},
|
|
98
|
-
});
|
|
99
|
-
assert.ok(seen.length >= 3, `expected a multi-call loop, got ${seen.length}`);
|
|
100
|
-
assert.equal([...new Set(seen)], ['auto'], 'the first call decides; later resolutions do not take effect until the next user turn');
|
|
101
|
-
}));
|
|
102
|
-
suite('re-resolves on the next user turn', () => __awaiter(void 0, void 0, void 0, function* () {
|
|
103
|
-
// The flip side: pinning is per turn, not for the driver's lifetime, so a state change
|
|
104
|
-
// between turns still lands.
|
|
105
|
-
const provider = capturingProvider();
|
|
106
|
-
const driver = new ChatDriver(makeRegistry(provider), {
|
|
107
|
-
maxToolIterations: 10,
|
|
108
|
-
maxFoldOperations: 5,
|
|
109
|
-
sessionKey: '',
|
|
110
|
-
});
|
|
111
|
-
let turn = 0;
|
|
112
|
-
driver.applyAgent(agent({
|
|
113
|
-
thinkingPolicy: () => (turn === 0 ? 'auto' : 'off'),
|
|
114
|
-
}));
|
|
115
|
-
yield driver.sendMessage('go');
|
|
116
|
-
const firstTurn = [...provider.seen];
|
|
117
|
-
turn = 1;
|
|
118
|
-
yield driver.sendMessage('again');
|
|
119
|
-
const secondTurn = provider.seen.slice(firstTurn.length);
|
|
120
|
-
assert.equal([...new Set(firstTurn)], ['auto'], 'turn one holds its posture');
|
|
121
|
-
assert.equal([...new Set(secondTurn)], ['off'], 'turn two picks up the new one');
|
|
122
|
-
}));
|
|
123
|
-
suite('passes undefined through when the resolver declines to choose', () => __awaiter(void 0, void 0, void 0, function* () {
|
|
124
|
-
// A resolver may answer on some turns and not others. `undefined` is its third answer —
|
|
125
|
-
// "leave this model alone" — and must not be normalised into a value. Asserted across
|
|
126
|
-
// the whole loop because the first call's answer is the one that gets pinned.
|
|
127
|
-
const seen = yield policiesFor({ thinkingPolicy: () => undefined });
|
|
128
|
-
assert.ok(seen.length >= 3);
|
|
129
|
-
assert.equal(seen.filter((p) => p !== undefined), [], 'declining is not the same as choosing');
|
|
130
|
-
}));
|
|
131
|
-
suite('awaits an async resolver', () => __awaiter(void 0, void 0, void 0, function* () {
|
|
132
|
-
const seen = yield policiesFor({
|
|
133
|
-
thinkingPolicy: () => __awaiter(void 0, void 0, void 0, function* () { return 'off'; }),
|
|
134
|
-
});
|
|
135
|
-
assert.equal([...new Set(seen)], ['off'], 'a promise is resolved, not passed through');
|
|
136
|
-
}));
|
|
137
|
-
suite.run();
|
|
@@ -1,200 +0,0 @@
|
|
|
1
|
-
import { __awaiter } from "tslib";
|
|
2
|
-
import { assert, createLogicSuite } from '@genesislcap/foundation-testing';
|
|
3
|
-
import { sumUsage } from '../../utils/sum-usage';
|
|
4
|
-
import { usageRows } from '../../utils/usage-rows';
|
|
5
|
-
// Side-effect import — MUST come before `./chat-driver` so the driver subclasses
|
|
6
|
-
// jsdom's EventTarget rather than Node's native one. Mirrors chat-driver.test.ts.
|
|
7
|
-
import './align-event-globals';
|
|
8
|
-
import { ChatDriver } from './chat-driver';
|
|
9
|
-
// ---------------------------------------------------------------------------
|
|
10
|
-
// Sub-agent trace capture when ONE tool call invokes SEVERAL sub-agents.
|
|
11
|
-
//
|
|
12
|
-
// The capture slot used to be a single `{ trace?: ChatMessage[] }` per tool call,
|
|
13
|
-
// assigned by each `requestSubAgent`. That covers N tool calls × 1 sub-agent — the
|
|
14
|
-
// case its docblock named — but not 1 tool call × N sub-agents, which is what a
|
|
15
|
-
// code-driven scheduler or a retry produces. All but the last trace was dropped, so
|
|
16
|
-
// those children ran, were billed, and then had no record in history.
|
|
17
|
-
//
|
|
18
|
-
// The failure was invisible to every existing check: `sumUsage` and `usageRows` both
|
|
19
|
-
// recurse into `subAgentTrace`, so they summed a truncated input and still reconciled
|
|
20
|
-
// with each other perfectly. Measured at 1 of 7 traces kept on a seven-way fan-out,
|
|
21
|
-
// ~2.3x under the true cost.
|
|
22
|
-
//
|
|
23
|
-
// These tests assert the traces SURVIVE, which is upstream of any cost assertion.
|
|
24
|
-
// ---------------------------------------------------------------------------
|
|
25
|
-
/** Answers by rule: concurrent children under one turn make a FIFO queue nondeterministic. */
|
|
26
|
-
const ruleProvider = () => {
|
|
27
|
-
let parentTurns = 0;
|
|
28
|
-
return {
|
|
29
|
-
chat: (_history, _userMessage, options) => __awaiter(void 0, void 0, void 0, function* () {
|
|
30
|
-
var _a;
|
|
31
|
-
const tools = ((_a = options === null || options === void 0 ? void 0 : options.tools) !== null && _a !== void 0 ? _a : []).map((t) => t.name);
|
|
32
|
-
// Child turn: finishes via its completion tool, and reports usage so the
|
|
33
|
-
// reconciliation assertions have something to add up.
|
|
34
|
-
if (tools.includes('work')) {
|
|
35
|
-
return {
|
|
36
|
-
role: 'assistant',
|
|
37
|
-
content: '',
|
|
38
|
-
model: 'claude-sonnet-5',
|
|
39
|
-
cost: 0.01,
|
|
40
|
-
inputTokens: 100,
|
|
41
|
-
outputTokens: 20,
|
|
42
|
-
toolCalls: [{ id: 'w1', name: 'work', args: {} }],
|
|
43
|
-
};
|
|
44
|
-
}
|
|
45
|
-
if (tools.includes('delegate') && parentTurns === 0) {
|
|
46
|
-
parentTurns += 1;
|
|
47
|
-
return {
|
|
48
|
-
role: 'assistant',
|
|
49
|
-
content: '',
|
|
50
|
-
model: 'claude-sonnet-5',
|
|
51
|
-
cost: 0.02,
|
|
52
|
-
inputTokens: 200,
|
|
53
|
-
outputTokens: 30,
|
|
54
|
-
toolCalls: [{ id: 'd0', name: 'delegate', args: {} }],
|
|
55
|
-
};
|
|
56
|
-
}
|
|
57
|
-
return { role: 'assistant', content: 'done', model: 'claude-sonnet-5' };
|
|
58
|
-
}),
|
|
59
|
-
};
|
|
60
|
-
};
|
|
61
|
-
const makeRegistry = (provider) => ({
|
|
62
|
-
get: () => provider,
|
|
63
|
-
default: () => provider,
|
|
64
|
-
defaultName: () => 'test',
|
|
65
|
-
names: () => ['test'],
|
|
66
|
-
getStatus: () => __awaiter(void 0, void 0, void 0, function* () { return null; }),
|
|
67
|
-
listStatuses: () => __awaiter(void 0, void 0, void 0, function* () { return []; }),
|
|
68
|
-
});
|
|
69
|
-
const agent = (overrides) => (Object.assign({ description: 'test agent' }, overrides));
|
|
70
|
-
/** A child that reports one unit of usage and completes. */
|
|
71
|
-
const worker = (name) => agent({
|
|
72
|
-
name,
|
|
73
|
-
toolDefinitions: [
|
|
74
|
-
{ name: 'work', description: 'work', parameters: { type: 'object', properties: {} } },
|
|
75
|
-
],
|
|
76
|
-
toolHandlers: {
|
|
77
|
-
work: (_args, ctx) => __awaiter(void 0, void 0, void 0, function* () {
|
|
78
|
-
var _a;
|
|
79
|
-
(_a = ctx.completeSubAgent) === null || _a === void 0 ? void 0 : _a.call(ctx, { ok: true });
|
|
80
|
-
return 'worked';
|
|
81
|
-
}),
|
|
82
|
-
},
|
|
83
|
-
});
|
|
84
|
-
/**
|
|
85
|
-
* A parent whose single `delegate` tool call invokes `names` in turn — the shape a
|
|
86
|
-
* code-driven scheduler produces, and the one the old single slot truncated.
|
|
87
|
-
*/
|
|
88
|
-
const boss = (children, invoke) => agent({
|
|
89
|
-
name: 'boss',
|
|
90
|
-
subAgents: children,
|
|
91
|
-
toolDefinitions: [
|
|
92
|
-
{ name: 'delegate', description: 'delegate', parameters: { type: 'object', properties: {} } },
|
|
93
|
-
],
|
|
94
|
-
toolHandlers: {
|
|
95
|
-
delegate: (_args, ctx) => __awaiter(void 0, void 0, void 0, function* () {
|
|
96
|
-
yield Promise.all(invoke.map((n) => ctx.requestSubAgent(n, { task: 'go' })));
|
|
97
|
-
return 'delegated';
|
|
98
|
-
}),
|
|
99
|
-
},
|
|
100
|
-
});
|
|
101
|
-
const run = (config) => __awaiter(void 0, void 0, void 0, function* () {
|
|
102
|
-
const driver = new ChatDriver(makeRegistry(ruleProvider()), {
|
|
103
|
-
maxToolIterations: 20,
|
|
104
|
-
maxFoldOperations: 5,
|
|
105
|
-
sessionKey: '',
|
|
106
|
-
});
|
|
107
|
-
driver.applyAgent(config);
|
|
108
|
-
yield driver.sendMessage('go');
|
|
109
|
-
return driver.getHistory();
|
|
110
|
-
});
|
|
111
|
-
const tracesIn = (history) => history.flatMap((m) => { var _a; return ((_a = m.toolCalls) !== null && _a !== void 0 ? _a : []).flatMap((tc) => (tc.subAgentTrace ? [tc.subAgentTrace] : [])); });
|
|
112
|
-
const suite = createLogicSuite('ChatDriver sub-agent trace capture');
|
|
113
|
-
// ---------------------------------------------------------------------------
|
|
114
|
-
suite('keeps every trace when one tool call invokes three sub-agents', () => __awaiter(void 0, void 0, void 0, function* () {
|
|
115
|
-
const names = ['gen_a', 'gen_b', 'gen_c'];
|
|
116
|
-
const history = yield run(boss(names.map(worker), names));
|
|
117
|
-
const traces = tracesIn(history);
|
|
118
|
-
assert.is(traces.length, 1, 'one tool call, so one concatenated trace');
|
|
119
|
-
// Each child contributes at least its own assistant turn. Before the fix this was
|
|
120
|
-
// one child's worth regardless of how many ran.
|
|
121
|
-
const seen = new Set(traces[0].map((m) => m.agentName).filter(Boolean));
|
|
122
|
-
assert.equal([...seen].sort(), names, `every invoked child must appear — got ${JSON.stringify([...seen])}`);
|
|
123
|
-
}));
|
|
124
|
-
suite('prices every child — usageRows emits a row per child and reconciles', () => __awaiter(void 0, void 0, void 0, function* () {
|
|
125
|
-
// The reconciliation test extended to a fan-out. The single-child version passed
|
|
126
|
-
// before the fix AND after it, which is exactly why it did not catch this.
|
|
127
|
-
const names = ['gen_a', 'gen_b', 'gen_c'];
|
|
128
|
-
const history = yield run(boss(names.map(worker), names));
|
|
129
|
-
const rows = usageRows(history);
|
|
130
|
-
const childRows = rows.filter((r) => r.subAgentDepth === 1);
|
|
131
|
-
assert.is(childRows.length, names.length, 'one row per child that ran');
|
|
132
|
-
const rowTotal = rows.reduce((n, r) => { var _a, _b; return n + ((_a = r.costUsd) !== null && _a !== void 0 ? _a : 0) + ((_b = r.externalCostUsd) !== null && _b !== void 0 ? _b : 0); }, 0);
|
|
133
|
-
assert.ok(Math.abs(rowTotal - sumUsage(history).costUsd) < 1e-12, `rows ${rowTotal} vs sumUsage ${sumUsage(history).costUsd}`);
|
|
134
|
-
// Parent turn ($0.02) + three children ($0.01 each). Asserted as a number so a
|
|
135
|
-
// regression that silently drops a child fails here rather than only in the
|
|
136
|
-
// reconciliation above, which would still agree with a truncated input.
|
|
137
|
-
assert.ok(Math.abs(rowTotal - 0.05) < 1e-12, `expected 0.05, got ${rowTotal}`);
|
|
138
|
-
}));
|
|
139
|
-
suite('keeps both traces when the same sub-agent is invoked twice (a retry)', () => __awaiter(void 0, void 0, void 0, function* () {
|
|
140
|
-
// The retry shape: one handler, one child name, two invocations. Under the old slot
|
|
141
|
-
// the first attempt's trace was overwritten by the second.
|
|
142
|
-
const history = yield run(boss([worker('gen_a')], ['gen_a', 'gen_a']));
|
|
143
|
-
const traces = tracesIn(history);
|
|
144
|
-
assert.is(traces.length, 1);
|
|
145
|
-
const childTurns = traces[0].filter((m) => m.agentName === 'gen_a' && m.cost != null);
|
|
146
|
-
assert.is(childTurns.length, 2, 'both attempts survive, not just the last');
|
|
147
|
-
const rows = usageRows(history).filter((r) => r.subAgentDepth === 1);
|
|
148
|
-
assert.is(rows.length, 2, 'and both are priced');
|
|
149
|
-
}));
|
|
150
|
-
suite('keeps child traces when the parent handler throws after they ran', () => __awaiter(void 0, void 0, void 0, function* () {
|
|
151
|
-
// Children that completed were billed by the provider. If the error path drops
|
|
152
|
-
// their traces, the run under-reports exactly as the single-slot bug did — and
|
|
153
|
-
// just as silently, since `sumUsage` and `usageRows` would still agree with each
|
|
154
|
-
// other over the truncated input. PR review.
|
|
155
|
-
const names = ['gen_a', 'gen_b'];
|
|
156
|
-
const parent = agent({
|
|
157
|
-
name: 'boss',
|
|
158
|
-
subAgents: names.map(worker),
|
|
159
|
-
toolDefinitions: [
|
|
160
|
-
{ name: 'delegate', description: 'delegate', parameters: { type: 'object', properties: {} } },
|
|
161
|
-
],
|
|
162
|
-
toolHandlers: {
|
|
163
|
-
delegate: (_args, ctx) => __awaiter(void 0, void 0, void 0, function* () {
|
|
164
|
-
yield Promise.all(names.map((n) => ctx.requestSubAgent(n, { task: 'go' })));
|
|
165
|
-
throw new Error('post-processing the children failed');
|
|
166
|
-
}),
|
|
167
|
-
},
|
|
168
|
-
});
|
|
169
|
-
const history = yield run(parent);
|
|
170
|
-
const traces = tracesIn(history);
|
|
171
|
-
assert.is(traces.length, 1, 'the failed tool call still carries its trace');
|
|
172
|
-
const seen = [...new Set(traces[0].map((m) => m.agentName).filter(Boolean))].sort();
|
|
173
|
-
assert.equal(seen, names, 'both children survive the throw');
|
|
174
|
-
const childRows = usageRows(history).filter((r) => r.subAgentDepth === 1);
|
|
175
|
-
assert.is(childRows.length, 2, 'and both are still priced');
|
|
176
|
-
}));
|
|
177
|
-
suite('leaves subAgentTrace undefined when no sub-agent ran', () => __awaiter(void 0, void 0, void 0, function* () {
|
|
178
|
-
var _a;
|
|
179
|
-
// Presence is the signal readers key off (`usage-rows.ts`, the UI's `when(...)`),
|
|
180
|
-
// so an empty array would be a different and wrong claim.
|
|
181
|
-
const history = yield run(agent({
|
|
182
|
-
name: 'boss',
|
|
183
|
-
subAgents: [worker('gen_a')],
|
|
184
|
-
toolDefinitions: [
|
|
185
|
-
{
|
|
186
|
-
name: 'delegate',
|
|
187
|
-
description: 'delegate',
|
|
188
|
-
parameters: { type: 'object', properties: {} },
|
|
189
|
-
},
|
|
190
|
-
],
|
|
191
|
-
toolHandlers: { delegate: () => __awaiter(void 0, void 0, void 0, function* () { return 'did it myself'; }) },
|
|
192
|
-
}));
|
|
193
|
-
for (const m of history) {
|
|
194
|
-
for (const tc of (_a = m.toolCalls) !== null && _a !== void 0 ? _a : []) {
|
|
195
|
-
assert.is(tc.subAgentTrace, undefined, 'not an empty array');
|
|
196
|
-
}
|
|
197
|
-
}
|
|
198
|
-
assert.equal(usageRows(history).filter((r) => r.subAgentDepth === 1), [], 'no child rows');
|
|
199
|
-
}));
|
|
200
|
-
suite.run();
|
|
@@ -1,268 +0,0 @@
|
|
|
1
|
-
import { __awaiter } from "tslib";
|
|
2
|
-
import { assert, createLogicSuite } from '@genesislcap/foundation-testing';
|
|
3
|
-
import { clearMetaEventRegistry, getMetaEvents } from '../../state/debug-event-log';
|
|
4
|
-
import { messageUsage, sumUsage } from '../../utils/sum-usage';
|
|
5
|
-
// Side-effect import — MUST come before `./chat-driver` so the driver subclasses
|
|
6
|
-
// jsdom's EventTarget rather than Node's native one. Mirrors chat-driver.test.ts.
|
|
7
|
-
import './align-event-globals';
|
|
8
|
-
import { ChatDriver } from './chat-driver';
|
|
9
|
-
// ---------------------------------------------------------------------------
|
|
10
|
-
// Per-call usage on the turn snapshots — the four token buckets plus USD priced
|
|
11
|
-
// onto each `kind: 'turn'` entry of the exported debug log.
|
|
12
|
-
//
|
|
13
|
-
// The snapshot is captured BEFORE the model call (that is where the prompt, tool
|
|
14
|
-
// surface and agent state line up with what the model saw), so usage has to be
|
|
15
|
-
// back-filled when the response lands. Two things follow, and both are asserted
|
|
16
|
-
// here: a completed call's turn agrees with the message it produced, and a call
|
|
17
|
-
// that produced NO message (a blank response, retried) is still priced — that
|
|
18
|
-
// spend appears nowhere in the transcript.
|
|
19
|
-
// ---------------------------------------------------------------------------
|
|
20
|
-
const makeRegistry = (provider) => ({
|
|
21
|
-
get: () => provider,
|
|
22
|
-
default: () => provider,
|
|
23
|
-
defaultName: () => 'test',
|
|
24
|
-
names: () => ['test'],
|
|
25
|
-
getStatus: () => __awaiter(void 0, void 0, void 0, function* () { return null; }),
|
|
26
|
-
listStatuses: () => __awaiter(void 0, void 0, void 0, function* () { return []; }),
|
|
27
|
-
});
|
|
28
|
-
/** Answers from a queue, then ends the turn with a plain unpriced reply. */
|
|
29
|
-
const scriptedProvider = (responses) => {
|
|
30
|
-
const queue = [...responses];
|
|
31
|
-
return {
|
|
32
|
-
chat: () => __awaiter(void 0, void 0, void 0, function* () { var _a; return (_a = queue.shift()) !== null && _a !== void 0 ? _a : { role: 'assistant', content: 'done' }; }),
|
|
33
|
-
};
|
|
34
|
-
};
|
|
35
|
-
const agent = (overrides) => (Object.assign({ description: 'test agent' }, overrides));
|
|
36
|
-
const makeDriver = (config, provider) => {
|
|
37
|
-
const driver = new ChatDriver(makeRegistry(provider), {
|
|
38
|
-
maxToolIterations: 50,
|
|
39
|
-
maxFoldOperations: 5,
|
|
40
|
-
sessionKey: '',
|
|
41
|
-
});
|
|
42
|
-
driver.applyAgent(config);
|
|
43
|
-
return driver;
|
|
44
|
-
};
|
|
45
|
-
const def = (name) => ({
|
|
46
|
-
name,
|
|
47
|
-
description: `${name} tool`,
|
|
48
|
-
parameters: { type: 'object', properties: {} },
|
|
49
|
-
});
|
|
50
|
-
/** One priced response: a 1000-token prompt that was mostly a cache hit. */
|
|
51
|
-
const priced = (over = {}) => (Object.assign({ role: 'assistant', content: 'answer', cost: 0.1, inputTokens: 1000, cacheReadTokens: 900, cacheWriteTokens: 50, outputTokens: 20 }, over));
|
|
52
|
-
const suite = createLogicSuite('ChatDriver per-turn usage');
|
|
53
|
-
suite('prices each turn snapshot with the four buckets and USD of its own call', () => __awaiter(void 0, void 0, void 0, function* () {
|
|
54
|
-
const driver = makeDriver(agent({ name: 'a' }), scriptedProvider([priced()]));
|
|
55
|
-
yield driver.sendMessage('go');
|
|
56
|
-
const [turn] = driver.getTurnSnapshots();
|
|
57
|
-
assert.equal(turn.usage, {
|
|
58
|
-
costUsd: 0.1,
|
|
59
|
-
// The prompt split into disjoint buckets: 1000 total, 900 read + 50 written,
|
|
60
|
-
// so 50 uncached. A reader of the log gets this without re-deriving it.
|
|
61
|
-
uncachedInputTokens: 50,
|
|
62
|
-
cacheReadTokens: 900,
|
|
63
|
-
cacheWriteTokens: 50,
|
|
64
|
-
outputTokens: 20,
|
|
65
|
-
}, 'the turn carries its own call’s usage');
|
|
66
|
-
// The turn and the message it produced are the SAME charge, so they must agree
|
|
67
|
-
// exactly — a log whose two views of one call disagree is worse than one view.
|
|
68
|
-
assert.equal(turn.usage, sumUsage(driver.getHistory()), 'turn agrees with the transcript total');
|
|
69
|
-
}));
|
|
70
|
-
suite('prices every model call in a tool loop separately', () => __awaiter(void 0, void 0, void 0, function* () {
|
|
71
|
-
const driver = makeDriver(agent({
|
|
72
|
-
name: 'a',
|
|
73
|
-
toolDefinitions: [def('work')],
|
|
74
|
-
toolHandlers: { work: () => __awaiter(void 0, void 0, void 0, function* () { return 'worked'; }) },
|
|
75
|
-
}), scriptedProvider([
|
|
76
|
-
priced({ content: '', cost: 0.02, toolCalls: [{ id: 'w1', name: 'work', args: {} }] }),
|
|
77
|
-
priced({ cost: 0.03 }),
|
|
78
|
-
]));
|
|
79
|
-
yield driver.sendMessage('go');
|
|
80
|
-
const snapshots = driver.getTurnSnapshots();
|
|
81
|
-
assert.is(snapshots.length, 2, 'one snapshot per model call, not per user turn');
|
|
82
|
-
assert.equal(snapshots.map((s) => { var _a; return (_a = s.usage) === null || _a === void 0 ? void 0 : _a.costUsd; }), [0.02, 0.03]);
|
|
83
|
-
}));
|
|
84
|
-
suite('prices a blank response that produced no message', () => __awaiter(void 0, void 0, void 0, function* () {
|
|
85
|
-
var _a;
|
|
86
|
-
// A blank turn is billed and then discarded before the retry, so the transcript can
|
|
87
|
-
// never account for it. The snapshot is the only record — which is the whole reason
|
|
88
|
-
// usage is stamped before the empty-response branch runs.
|
|
89
|
-
const driver = makeDriver(agent({ name: 'a' }), scriptedProvider([
|
|
90
|
-
priced({ content: '', cost: 0.07, outputTokens: 0 }),
|
|
91
|
-
priced({ cost: 0.03 }),
|
|
92
|
-
]));
|
|
93
|
-
yield driver.sendMessage('go');
|
|
94
|
-
const snapshots = driver.getTurnSnapshots();
|
|
95
|
-
assert.is(snapshots.length, 2, 'the retried attempt has its own snapshot');
|
|
96
|
-
assert.is((_a = snapshots[0].usage) === null || _a === void 0 ? void 0 : _a.costUsd, 0.07, 'the discarded attempt is still priced');
|
|
97
|
-
assert.is(sumUsage(driver.getHistory()).costUsd, 0.03, 'and is genuinely absent from the transcript — that is the gap the snapshot fills');
|
|
98
|
-
}));
|
|
99
|
-
suite('leaves usage undefined when the provider reports none', () => __awaiter(void 0, void 0, void 0, function* () {
|
|
100
|
-
// Absent, not zeroed: "this provider reports no usage" (e.g. Chrome's built-in model)
|
|
101
|
-
// must not read as a free call.
|
|
102
|
-
const driver = makeDriver(agent({ name: 'a' }), scriptedProvider([]));
|
|
103
|
-
yield driver.sendMessage('go');
|
|
104
|
-
assert.is(driver.getTurnSnapshots()[0].usage, undefined);
|
|
105
|
-
}));
|
|
106
|
-
suite.run();
|
|
107
|
-
// ---------------------------------------------------------------------------
|
|
108
|
-
// The reasoning/narration/answer split must not multiply the cache buckets.
|
|
109
|
-
//
|
|
110
|
-
// One model response becomes up to three messages; only the last carries usage.
|
|
111
|
-
// The clearing list was written when usage meant cost + input + output, so when the
|
|
112
|
-
// cache buckets arrived they kept riding along on the display-only copies — and every
|
|
113
|
-
// bucket aggregate (session totals, usage rows, the exported log) counted a split
|
|
114
|
-
// turn's cache volume two or three times. The cost total hid it, since that comes
|
|
115
|
-
// from `cost`, which WAS cleared.
|
|
116
|
-
// ---------------------------------------------------------------------------
|
|
117
|
-
const split = createLogicSuite('ChatDriver response split usage');
|
|
118
|
-
split('counts a reasoning + answer split once, not twice', () => __awaiter(void 0, void 0, void 0, function* () {
|
|
119
|
-
const response = priced({ reasoning: 'thinking…' });
|
|
120
|
-
const driver = makeDriver(agent({ name: 'a' }), scriptedProvider([response]));
|
|
121
|
-
yield driver.sendMessage('go');
|
|
122
|
-
const history = driver.getHistory();
|
|
123
|
-
assert.is(history.filter((m) => m.role === 'assistant').length, 2, 'the response did split into a reasoning message and an answer');
|
|
124
|
-
assert.equal(sumUsage(history), messageUsage(response), 'the split totals exactly one call’s usage');
|
|
125
|
-
const reasoning = history.find((m) => m.category === 'reasoning');
|
|
126
|
-
assert.is(reasoning.cacheReadTokens, undefined, 'the display-only copy carries no cache volume');
|
|
127
|
-
assert.is(reasoning.cacheWriteTokens, undefined);
|
|
128
|
-
}));
|
|
129
|
-
split('counts a reasoning + narration + answer split once', () => __awaiter(void 0, void 0, void 0, function* () {
|
|
130
|
-
// The three-way case: content alongside a tool call is interstitial narration, so this
|
|
131
|
-
// response produces reasoning + narration + the tool-call message. Worst case for the
|
|
132
|
-
// old behaviour — cache volume counted three times.
|
|
133
|
-
const first = priced({
|
|
134
|
-
content: 'let me look that up',
|
|
135
|
-
reasoning: 'thinking…',
|
|
136
|
-
cost: 0.02,
|
|
137
|
-
toolCalls: [{ id: 'w1', name: 'work', args: {} }],
|
|
138
|
-
});
|
|
139
|
-
const driver = makeDriver(agent({
|
|
140
|
-
name: 'a',
|
|
141
|
-
toolDefinitions: [def('work')],
|
|
142
|
-
toolHandlers: { work: () => __awaiter(void 0, void 0, void 0, function* () { return 'worked'; }) },
|
|
143
|
-
}), scriptedProvider([first, priced({ cost: 0.03 })]));
|
|
144
|
-
yield driver.sendMessage('go');
|
|
145
|
-
const total = sumUsage(driver.getHistory());
|
|
146
|
-
assert.is(total.costUsd, 0.05, 'two calls, each counted once');
|
|
147
|
-
assert.is(total.cacheReadTokens, 1800, '900 per call — not 2700 with the narration copies');
|
|
148
|
-
assert.is(total.cacheWriteTokens, 100);
|
|
149
|
-
assert.is(total.uncachedInputTokens, 100);
|
|
150
|
-
}));
|
|
151
|
-
split.run();
|
|
152
|
-
// ---------------------------------------------------------------------------
|
|
153
|
-
// Per-turn model attribution across a tier switch.
|
|
154
|
-
//
|
|
155
|
-
// The shape this exists for is an agent whose `provider` selector varies by state —
|
|
156
|
-
// e.g. the showcase trade-operations agent: `({state}) => isPlanning(state) ? High : Low`,
|
|
157
|
-
// planning on sonnet and executing on haiku. Each call has to name the model that ran
|
|
158
|
-
// it, on the turn itself: joining a turn to the message after it works for a normal
|
|
159
|
-
// step but not for a call that produced no message, and `provider.selected` fires only
|
|
160
|
-
// when the slot CHANGES, so the nearest event can be many turns back.
|
|
161
|
-
//
|
|
162
|
-
// The ordering trap: the snapshot is recorded BEFORE the provider is resolved for that
|
|
163
|
-
// call, so stamping `lastResolvedModel` at creation time yields the previous call's
|
|
164
|
-
// model — wrong on exactly the turn the tier changes.
|
|
165
|
-
// ---------------------------------------------------------------------------
|
|
166
|
-
const tiers = createLogicSuite('ChatDriver per-turn model attribution');
|
|
167
|
-
/** A tiered registry: named slots, each reporting its own model via `getStatus`. */
|
|
168
|
-
const tieredRegistry = (slots, defaultName) => ({
|
|
169
|
-
get: (name) => slots[name],
|
|
170
|
-
default: () => slots[defaultName],
|
|
171
|
-
defaultName: () => defaultName,
|
|
172
|
-
names: () => Object.keys(slots),
|
|
173
|
-
getStatus: () => __awaiter(void 0, void 0, void 0, function* () { return null; }),
|
|
174
|
-
listStatuses: () => __awaiter(void 0, void 0, void 0, function* () { return []; }),
|
|
175
|
-
});
|
|
176
|
-
const tierProvider = (model, responses) => {
|
|
177
|
-
const queue = [...responses];
|
|
178
|
-
return {
|
|
179
|
-
getStatus: () => __awaiter(void 0, void 0, void 0, function* () { return ({ model, provider: 'anthropic' }); }),
|
|
180
|
-
chat: () => __awaiter(void 0, void 0, void 0, function* () { var _a; return (_a = queue.shift()) !== null && _a !== void 0 ? _a : { role: 'assistant', content: 'done' }; }),
|
|
181
|
-
};
|
|
182
|
-
};
|
|
183
|
-
tiers('attributes each step to the tier that ran it when the agent switches mid-flow', () => __awaiter(void 0, void 0, void 0, function* () {
|
|
184
|
-
// Responses deliberately carry NO `model` of their own — the common case for a
|
|
185
|
-
// transport that leaves attribution to the driver, and the one where a stale
|
|
186
|
-
// `lastResolved*` read would go unnoticed.
|
|
187
|
-
const slots = {
|
|
188
|
-
high: tierProvider('claude-sonnet-4-6', [
|
|
189
|
-
priced({
|
|
190
|
-
content: '',
|
|
191
|
-
cost: 0.05,
|
|
192
|
-
toolCalls: [{ id: 'p1', name: 'finish_planning', args: {} }],
|
|
193
|
-
}),
|
|
194
|
-
]),
|
|
195
|
-
low: tierProvider('claude-haiku-4-5-20251001', [priced({ content: 'Booked.', cost: 0.001 })]),
|
|
196
|
-
};
|
|
197
|
-
// The "state" the selector reads, advanced by the tool — as a flow agent's machine does.
|
|
198
|
-
let planning = true;
|
|
199
|
-
const driver = new ChatDriver(tieredRegistry(slots, 'low'), {
|
|
200
|
-
maxToolIterations: 20,
|
|
201
|
-
maxFoldOperations: 5,
|
|
202
|
-
sessionKey: 'tiers',
|
|
203
|
-
});
|
|
204
|
-
driver.applyAgent(agent({
|
|
205
|
-
name: 'Trade Operations',
|
|
206
|
-
provider: () => (planning ? 'high' : 'low'),
|
|
207
|
-
toolDefinitions: [def('finish_planning')],
|
|
208
|
-
toolHandlers: {
|
|
209
|
-
finish_planning: () => __awaiter(void 0, void 0, void 0, function* () {
|
|
210
|
-
planning = false;
|
|
211
|
-
return 'planned';
|
|
212
|
-
}),
|
|
213
|
-
},
|
|
214
|
-
}));
|
|
215
|
-
yield driver.sendMessage('book me a trade');
|
|
216
|
-
const snapshots = driver.getTurnSnapshots();
|
|
217
|
-
assert.equal(snapshots.map((s) => { var _a; return [s.providerName, s.model, (_a = s.usage) === null || _a === void 0 ? void 0 : _a.costUsd]; }), [
|
|
218
|
-
['high', 'claude-sonnet-4-6', 0.05],
|
|
219
|
-
['low', 'claude-haiku-4-5-20251001', 0.001],
|
|
220
|
-
], 'the planning call is attributed to the high tier and the execution call to the low one');
|
|
221
|
-
assert.equal(snapshots.map((s) => s.provider), ['anthropic', 'anthropic'], 'the vendor behind each slot is recorded too');
|
|
222
|
-
// A turn and the message it produced must never disagree about which model ran.
|
|
223
|
-
const assistantModels = driver
|
|
224
|
-
.getHistory()
|
|
225
|
-
.filter((m) => m.role === 'assistant' && m.cost != null)
|
|
226
|
-
.map((m) => m.model);
|
|
227
|
-
assert.equal(assistantModels, snapshots.map((s) => s.model));
|
|
228
|
-
}));
|
|
229
|
-
tiers('records the SERVING model when a fallback answers on another model', () => __awaiter(void 0, void 0, void 0, function* () {
|
|
230
|
-
// A server-side fallback chain answers on a different model than the one requested.
|
|
231
|
-
// The transport stamps the real one; the turn must follow it rather than relabel the
|
|
232
|
-
// call as the tier's configured model — that would misattribute the spend.
|
|
233
|
-
const slots = {
|
|
234
|
-
high: tierProvider('claude-fable-5', [
|
|
235
|
-
priced({ content: 'answered by the fallback', model: 'claude-opus-4-8' }),
|
|
236
|
-
]),
|
|
237
|
-
};
|
|
238
|
-
const driver = new ChatDriver(tieredRegistry(slots, 'high'), {
|
|
239
|
-
maxToolIterations: 20,
|
|
240
|
-
maxFoldOperations: 5,
|
|
241
|
-
sessionKey: 'fallback',
|
|
242
|
-
});
|
|
243
|
-
driver.applyAgent(agent({ name: 'a', provider: 'high' }));
|
|
244
|
-
yield driver.sendMessage('go');
|
|
245
|
-
const [turn] = driver.getTurnSnapshots();
|
|
246
|
-
assert.is(turn.model, 'claude-opus-4-8', 'the model that answered, not the one asked for');
|
|
247
|
-
assert.is(turn.providerName, 'high', 'the slot asked for is still recorded');
|
|
248
|
-
}));
|
|
249
|
-
tiers('names the model and vendor on the provider.selected event, not just the slot', () => __awaiter(void 0, void 0, void 0, function* () {
|
|
250
|
-
var _a, _b, _c;
|
|
251
|
-
clearMetaEventRegistry();
|
|
252
|
-
const slots = {
|
|
253
|
-
high: tierProvider('claude-sonnet-4-6', [priced({ content: 'hi' })]),
|
|
254
|
-
};
|
|
255
|
-
const driver = new ChatDriver(tieredRegistry(slots, 'high'), {
|
|
256
|
-
maxToolIterations: 20,
|
|
257
|
-
maxFoldOperations: 5,
|
|
258
|
-
sessionKey: 'selected',
|
|
259
|
-
});
|
|
260
|
-
driver.applyAgent(agent({ name: 'a', provider: 'high' }));
|
|
261
|
-
yield driver.sendMessage('go');
|
|
262
|
-
const selected = getMetaEvents('selected').find((e) => e.type === 'provider.selected');
|
|
263
|
-
assert.ok(selected, 'a provider.selected event is recorded');
|
|
264
|
-
assert.is((_a = selected.detail) === null || _a === void 0 ? void 0 : _a.provider, 'high', 'the registry slot');
|
|
265
|
-
assert.is((_b = selected.detail) === null || _b === void 0 ? void 0 : _b.model, 'claude-sonnet-4-6', 'and the model behind it');
|
|
266
|
-
assert.is((_c = selected.detail) === null || _c === void 0 ? void 0 : _c.vendor, 'anthropic');
|
|
267
|
-
}));
|
|
268
|
-
tiers.run();
|