praxis-agent 0.16.0 → 0.17.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (43) hide show
  1. package/README.md +11 -5
  2. package/dist/application/background-agent-manager.d.ts +7 -1
  3. package/dist/application/background-agent-manager.js +148 -5
  4. package/dist/application/metered-model-completion.d.ts +15 -0
  5. package/dist/application/metered-model-completion.js +76 -0
  6. package/dist/application/model-compactor.js +25 -1
  7. package/dist/application/session-cost-tracker.d.ts +50 -0
  8. package/dist/application/session-cost-tracker.js +254 -0
  9. package/dist/application/session-service.d.ts +13 -0
  10. package/dist/application/session-service.js +594 -69
  11. package/dist/application/subagent-service.d.ts +11 -1
  12. package/dist/application/subagent-service.js +41 -1
  13. package/dist/application/tool-use-summary.d.ts +7 -2
  14. package/dist/application/tool-use-summary.js +42 -10
  15. package/dist/application/workflow-manager.d.ts +9 -4
  16. package/dist/application/workflow-manager.js +151 -7
  17. package/dist/cli/interactive.d.ts +5 -1
  18. package/dist/cli/interactive.js +227 -15
  19. package/dist/cli/protocol.d.ts +11 -0
  20. package/dist/cli/protocol.js +16 -1
  21. package/dist/cli/tui/claude-command-inventory.d.ts +27 -27
  22. package/dist/cli/tui/claude-command-inventory.js +26 -26
  23. package/dist/cli/tui/config-dashboard.d.ts +2 -9
  24. package/dist/cli/tui/config-dashboard.js +6 -6
  25. package/dist/cli/tui/cost-summary.d.ts +34 -0
  26. package/dist/cli/tui/cost-summary.js +192 -0
  27. package/dist/cli/tui/doctor-dashboard.d.ts +15 -0
  28. package/dist/cli/tui/doctor-dashboard.js +81 -0
  29. package/dist/cli/tui/slash-commands.js +10 -0
  30. package/dist/cli.d.ts +2 -0
  31. package/dist/cli.js +192 -15
  32. package/dist/core/compaction.d.ts +2 -0
  33. package/dist/core/runtime.d.ts +38 -1
  34. package/dist/core/runtime.js +265 -11
  35. package/dist/persistence/claude-cost-state-store.d.ts +48 -0
  36. package/dist/persistence/claude-cost-state-store.js +335 -0
  37. package/dist/providers/anthropic-compatible.js +34 -9
  38. package/dist/providers/fallback-provider.d.ts +2 -1
  39. package/dist/providers/fallback-provider.js +28 -3
  40. package/dist/tools/line-changes.d.ts +8 -0
  41. package/dist/tools/line-changes.js +35 -0
  42. package/dist/tools/local-tools.js +30 -1
  43. package/package.json +2 -1
package/README.md CHANGED
@@ -15,10 +15,15 @@ Anthropic/OpenAI-compatible model access. Praxis deliberately excludes
15
15
  accounts, organizations, billing, managed enterprise policy, remote control,
16
16
  IDE surfaces, and telemetry control planes.
17
17
 
18
- Claude Code 2.1.208 is the compatibility, architecture, and design baseline.
19
- Every single-user CLI capability remains required unless it falls inside an
20
- explicit exclusion above. A similar Praxis surface is not a substitute for the
21
- corresponding Claude command or runtime contract.
18
+ Claude Code 2.1.208 remains the architecture, design, and observable
19
+ compatibility baseline for included and required single-user developer
20
+ capabilities. Only entries classified `required` block developer-core
21
+ closure; `deferred` entries are optional and demand-driven. Exclusions cover
22
+ the existing enterprise, authentication, hosted, and client surfaces plus the
23
+ explicitly classified subscription-bound integration, campaign, hidden
24
+ maintainer diagnostic, and build-experimental commands. A similar Praxis
25
+ surface is not a substitute for the corresponding Claude command or runtime
26
+ contract.
22
27
 
23
28
  ## Requirements
24
29
 
@@ -98,7 +103,8 @@ troubleshooting. Run `praxis --help` for the authoritative command surface.
98
103
  skills/hooks flow, provider-free per-session `/color` prompt-bar styling,
99
104
  `/mcp`, `/memory` shared instruction and auto-memory
100
105
  access, and live extension-reload controls,
101
- cursor/history composer, per-session model/effort/permission controls,
106
+ cursor/history composer, provider-free `/cost` usage and pricing summaries,
107
+ interactive `/doctor` diagnostics, per-session model/effort/permission controls,
102
108
  context/status/skill/task dashboards, prompt stash and continuation shortcuts,
103
109
  filterable `@` file and agent references, composer undo, `Ctrl+G` external
104
110
  editing, shared `/keybindings` creation/editing and supported-action remapping,
@@ -1,7 +1,10 @@
1
- import type { ModelUsage } from '../core/runtime.js';
1
+ import type { ModelUsage, ModelUsageByModel } from '../core/runtime.js';
2
2
  export interface BackgroundAgentRunResult {
3
3
  text: string;
4
4
  usage: ModelUsage;
5
+ modelUsage?: ModelUsageByModel;
6
+ durationApiMs?: number;
7
+ durationApiWithoutRetriesMs?: number;
5
8
  toolUseCount: number;
6
9
  durationMs: number;
7
10
  isolationPath?: string;
@@ -57,6 +60,9 @@ export declare class BackgroundAgentManager {
57
60
  }): Promise<{
58
61
  messages: string[];
59
62
  usage: ModelUsage;
63
+ modelUsage?: ModelUsageByModel;
64
+ durationApiMs?: number;
65
+ durationApiWithoutRetriesMs?: number;
60
66
  }>;
61
67
  private start;
62
68
  private resolveOptional;
@@ -34,6 +34,118 @@ function waitBounded(operation, milliseconds) {
34
34
  });
35
35
  });
36
36
  }
37
+ const modelUsageCounterFields = [
38
+ 'inputTokens',
39
+ 'outputTokens',
40
+ 'cacheReadInputTokens',
41
+ 'cacheCreationInputTokens',
42
+ 'webSearchRequests',
43
+ ];
44
+ const modelUsageMetadataFields = ['contextWindow', 'maxOutputTokens'];
45
+ function assertValidModelUsageEntry(model, usage) {
46
+ if (model.trim() === '') {
47
+ throw new Error('Model usage breakdown contains a blank model name');
48
+ }
49
+ for (const field of modelUsageCounterFields) {
50
+ const value = usage[field];
51
+ if (value !== undefined && (!Number.isSafeInteger(value) || value < 0)) {
52
+ throw new Error(`Model usage for "${model}" has an invalid ${field} counter`);
53
+ }
54
+ }
55
+ for (const field of modelUsageMetadataFields) {
56
+ const value = usage[field];
57
+ if (value !== undefined && (!Number.isSafeInteger(value) || value < 1)) {
58
+ throw new Error(`Model usage for "${model}" has an invalid ${field} metadata value`);
59
+ }
60
+ }
61
+ }
62
+ function mergeModelUsageMetadata(model, left, right) {
63
+ const contextWindow = mergeModelUsageMetadataField(model, 'contextWindow', left.contextWindow, right.contextWindow);
64
+ const maxOutputTokens = mergeModelUsageMetadataField(model, 'maxOutputTokens', left.maxOutputTokens, right.maxOutputTokens);
65
+ return {
66
+ ...(contextWindow === undefined ? {} : { contextWindow }),
67
+ ...(maxOutputTokens === undefined ? {} : { maxOutputTokens }),
68
+ };
69
+ }
70
+ function mergeModelUsageMetadataField(model, field, left, right) {
71
+ if (left === undefined)
72
+ return right;
73
+ if (right === undefined)
74
+ return left;
75
+ if (left !== right) {
76
+ throw new Error(`Model usage for "${model}" has conflicting ${field} values: ${left} vs ${right}`);
77
+ }
78
+ return left;
79
+ }
80
+ function addUsageChecked(model, left, right) {
81
+ const inputTokens = left.inputTokens + right.inputTokens;
82
+ const outputTokens = left.outputTokens + right.outputTokens;
83
+ const cacheReadInputTokens = (left.cacheReadInputTokens ?? 0) + (right.cacheReadInputTokens ?? 0);
84
+ const cacheCreationInputTokens = (left.cacheCreationInputTokens ?? 0) + (right.cacheCreationInputTokens ?? 0);
85
+ const webSearchRequests = (left.webSearchRequests ?? 0) + (right.webSearchRequests ?? 0);
86
+ if (!Number.isSafeInteger(inputTokens) ||
87
+ !Number.isSafeInteger(outputTokens) ||
88
+ !Number.isSafeInteger(cacheReadInputTokens) ||
89
+ !Number.isSafeInteger(cacheCreationInputTokens) ||
90
+ !Number.isSafeInteger(webSearchRequests)) {
91
+ throw new Error('Model usage total overflow');
92
+ }
93
+ // Aggregates without a model stay counter-only; per-model rows merge their
94
+ // capability metadata with conflict rejection.
95
+ const metadata = model === undefined ? {} : mergeModelUsageMetadata(model, left, right);
96
+ return {
97
+ inputTokens,
98
+ outputTokens,
99
+ ...(cacheReadInputTokens === 0 ? {} : { cacheReadInputTokens }),
100
+ ...(cacheCreationInputTokens === 0 ? {} : { cacheCreationInputTokens }),
101
+ ...(webSearchRequests === 0 ? {} : { webSearchRequests }),
102
+ ...metadata,
103
+ };
104
+ }
105
+ function assertValidResultUsage(usage) {
106
+ for (const field of modelUsageCounterFields) {
107
+ const value = usage[field];
108
+ if (value !== undefined && (!Number.isSafeInteger(value) || value < 0)) {
109
+ throw new Error(`Model usage total has an invalid ${field} counter`);
110
+ }
111
+ }
112
+ }
113
+ function addApiDuration(value, total, field) {
114
+ if (!Number.isFinite(value) || value < 0) {
115
+ throw new TypeError(`${field} must be a finite nonnegative number`);
116
+ }
117
+ const next = total + value;
118
+ if (!Number.isFinite(next) || next < 0) {
119
+ throw new TypeError(`${field} total overflow`);
120
+ }
121
+ return next;
122
+ }
123
+ function mergeModelUsageEntry(map, model, usage) {
124
+ assertValidModelUsageEntry(model, usage);
125
+ const existing = map.get(model);
126
+ if (existing === undefined) {
127
+ map.set(model, { ...usage });
128
+ return;
129
+ }
130
+ map.set(model, addUsageChecked(model, existing, usage));
131
+ }
132
+ function mergeToolModelUsage(map, breakdown) {
133
+ const entries = Object.entries(breakdown);
134
+ if (entries.length === 0)
135
+ return;
136
+ // Validate every key, counter, metadata, and merged sum before adding any
137
+ // entry from this breakdown so malformed or conflicting data never merges
138
+ // partially.
139
+ for (const [model, usage] of entries) {
140
+ assertValidModelUsageEntry(model, usage);
141
+ const existing = map.get(model);
142
+ if (existing !== undefined)
143
+ addUsageChecked(model, existing, usage);
144
+ }
145
+ for (const [model, usage] of entries) {
146
+ mergeModelUsageEntry(map, model, usage);
147
+ }
148
+ }
37
149
  export class BackgroundAgentManager {
38
150
  tasks = new Map();
39
151
  names = new Map();
@@ -203,15 +315,46 @@ export class BackgroundAgentManager {
203
315
  await Promise.race(running);
204
316
  }
205
317
  const notifications = [];
206
- const usage = { inputTokens: 0, outputTokens: 0 };
318
+ let usage = { inputTokens: 0, outputTokens: 0 };
319
+ const modelUsageByModel = new Map();
320
+ let durationApiMs = 0;
321
+ let durationApiWithoutRetriesMs = 0;
322
+ let durationSeen = false;
323
+ const consumedTasks = [];
207
324
  for (const [, task] of eligibleTasks) {
208
- for (const notification of task.notifications.splice(0)) {
325
+ if (task.notifications.length === 0)
326
+ continue;
327
+ for (const notification of task.notifications) {
209
328
  notifications.push(this.formatNotification(task, notification));
210
- usage.inputTokens += notification.result?.usage.inputTokens ?? 0;
211
- usage.outputTokens += notification.result?.usage.outputTokens ?? 0;
329
+ if (notification.result) {
330
+ assertValidResultUsage(notification.result.usage);
331
+ usage = addUsageChecked(undefined, usage, notification.result.usage);
332
+ }
333
+ if (notification.result?.modelUsage) {
334
+ mergeToolModelUsage(modelUsageByModel, notification.result.modelUsage);
335
+ }
336
+ if (notification.result?.durationApiMs !== undefined ||
337
+ notification.result?.durationApiWithoutRetriesMs !== undefined) {
338
+ durationSeen = true;
339
+ const total = notification.result.durationApiMs ?? 0;
340
+ durationApiMs = addApiDuration(total, durationApiMs, 'durationApiMs');
341
+ durationApiWithoutRetriesMs = addApiDuration(notification.result.durationApiWithoutRetriesMs ?? total, durationApiWithoutRetriesMs, 'durationApiWithoutRetriesMs');
342
+ }
212
343
  }
344
+ consumedTasks.push(task);
213
345
  }
214
- return { messages: notifications, usage };
346
+ const modelUsage = modelUsageByModel.size === 0
347
+ ? undefined
348
+ : Object.fromEntries(modelUsageByModel);
349
+ const result = {
350
+ messages: notifications,
351
+ usage,
352
+ ...(modelUsage === undefined ? {} : { modelUsage }),
353
+ ...(durationSeen ? { durationApiMs, durationApiWithoutRetriesMs } : {}),
354
+ };
355
+ for (const task of consumedTasks)
356
+ task.notifications.splice(0);
357
+ return result;
215
358
  }
216
359
  start(task, message, continuation, toolUseId) {
217
360
  task.generation += 1;
@@ -0,0 +1,15 @@
1
+ import { type ModelProvider, type ModelRequest, type ModelToolCall, type ModelUsage } from '../core/runtime.js';
2
+ export interface MeteredModelCompletion {
3
+ text: string;
4
+ toolCalls: readonly ModelToolCall[];
5
+ usage: ModelUsage;
6
+ model?: string;
7
+ durationApiMs: number;
8
+ durationApiWithoutRetriesMs: number;
9
+ }
10
+ export interface MeteredModelRequestOptions {
11
+ onTextDelta?: (delta: string) => void;
12
+ onMetrics?: (metrics: MeteredModelCompletion) => void;
13
+ }
14
+ export declare function completeMeteredModelRequest(provider: ModelProvider, request: ModelRequest, options?: MeteredModelRequestOptions): Promise<MeteredModelCompletion>;
15
+ //# sourceMappingURL=metered-model-completion.d.ts.map
@@ -0,0 +1,76 @@
1
+ import { AgentRunCancelledError, } from '../core/runtime.js';
2
+ function nonblankModel(provider) {
3
+ if (provider.model === undefined)
4
+ return undefined;
5
+ return provider.model.trim() === '' ? undefined : provider.model;
6
+ }
7
+ function enrichCapabilityMetadata(usage, value, field) {
8
+ if (usage[field] !== undefined)
9
+ return usage;
10
+ if (value === undefined || !Number.isSafeInteger(value) || value < 1) {
11
+ return usage;
12
+ }
13
+ return { ...usage, [field]: value };
14
+ }
15
+ function enrichedUsage(provider, usage) {
16
+ return enrichCapabilityMetadata(enrichCapabilityMetadata(usage, provider.capabilities.contextWindowTokens, 'contextWindow'), provider.capabilities.maxOutputTokens, 'maxOutputTokens');
17
+ }
18
+ export async function completeMeteredModelRequest(provider, request, options = {}) {
19
+ const { onTextDelta, onMetrics } = options;
20
+ if (request.signal?.aborted)
21
+ throw new AgentRunCancelledError();
22
+ let text = '';
23
+ const toolCalls = [];
24
+ let usage = { inputTokens: 0, outputTokens: 0 };
25
+ let attemptDurationMs;
26
+ let attemptDurationSeen = false;
27
+ const apiStartedAt = performance.now();
28
+ let metrics;
29
+ try {
30
+ for await (const event of provider.complete(request)) {
31
+ if (request.signal?.aborted)
32
+ throw new AgentRunCancelledError();
33
+ if (event.type === 'api-retry')
34
+ continue;
35
+ if (event.type === 'api-attempt-duration') {
36
+ if (attemptDurationSeen) {
37
+ throw new Error('Provider emitted multiple api-attempt-duration events in one attempt');
38
+ }
39
+ attemptDurationSeen = true;
40
+ const { durationMs } = event;
41
+ if (typeof durationMs !== 'number' ||
42
+ !Number.isFinite(durationMs) ||
43
+ durationMs < 0) {
44
+ throw new TypeError('api-attempt-duration durationMs must be a finite nonnegative number');
45
+ }
46
+ attemptDurationMs = durationMs;
47
+ continue;
48
+ }
49
+ if (event.type === 'text-delta') {
50
+ text += event.delta;
51
+ onTextDelta?.(event.delta);
52
+ }
53
+ else if (event.type === 'usage') {
54
+ usage = event.usage;
55
+ }
56
+ else if (event.type === 'tool-call') {
57
+ toolCalls.push(event.call);
58
+ }
59
+ }
60
+ }
61
+ finally {
62
+ const model = nonblankModel(provider);
63
+ const durationApiMs = Math.max(0, performance.now() - apiStartedAt);
64
+ metrics = {
65
+ text,
66
+ toolCalls,
67
+ usage: enrichedUsage(provider, usage),
68
+ ...(model === undefined ? {} : { model }),
69
+ durationApiMs,
70
+ durationApiWithoutRetriesMs: attemptDurationMs ?? durationApiMs,
71
+ };
72
+ onMetrics?.(metrics);
73
+ }
74
+ return metrics;
75
+ }
76
+ //# sourceMappingURL=metered-model-completion.js.map
@@ -41,6 +41,8 @@ export class ModelCompactor {
41
41
  const startedAt = Date.now();
42
42
  let summary = '';
43
43
  let usage = { inputTokens: 0, outputTokens: 0 };
44
+ let attemptDurationMs;
45
+ let attemptDurationSeen = false;
44
46
  const providerRequest = {
45
47
  messages,
46
48
  ...(request.signal ? { signal: request.signal } : {}),
@@ -51,6 +53,20 @@ export class ModelCompactor {
51
53
  if (event.type === 'tool-call') {
52
54
  throw new Error('Compaction model must not call tools');
53
55
  }
56
+ if (event.type === 'api-attempt-duration') {
57
+ if (attemptDurationSeen) {
58
+ throw new Error('Provider emitted multiple api-attempt-duration events in one attempt');
59
+ }
60
+ attemptDurationSeen = true;
61
+ const { durationMs } = event;
62
+ if (typeof durationMs !== 'number' ||
63
+ !Number.isFinite(durationMs) ||
64
+ durationMs < 0) {
65
+ throw new TypeError('api-attempt-duration durationMs must be a finite nonnegative number');
66
+ }
67
+ attemptDurationMs = durationMs;
68
+ continue;
69
+ }
54
70
  if (event.type === 'text-delta')
55
71
  summary += event.delta;
56
72
  else if (event.type === 'usage')
@@ -64,7 +80,15 @@ export class ModelCompactor {
64
80
  if (summaryTokens > targetTokens) {
65
81
  throw new Error(`Compaction summary exceeded ${targetTokens} tokens (estimated ${summaryTokens})`);
66
82
  }
67
- return { summary, usage, durationMs: Date.now() - startedAt };
83
+ const durationMs = Date.now() - startedAt;
84
+ const model = this.provider.model;
85
+ return {
86
+ summary,
87
+ usage,
88
+ durationMs,
89
+ durationWithoutRetriesMs: attemptDurationMs ?? durationMs,
90
+ ...(model !== undefined && model.trim() !== '' ? { model } : {}),
91
+ };
68
92
  }
69
93
  }
70
94
  //# sourceMappingURL=model-compactor.js.map
@@ -0,0 +1,50 @@
1
+ import type { ModelUsage } from '../core/runtime.js';
2
+ import type { ClaudeSessionCostState } from '../persistence/claude-cost-state-store.js';
3
+ export interface ClaudeSessionCostTrackerOptions {
4
+ readonly sessionId: string;
5
+ readonly restored?: ClaudeSessionCostState | null;
6
+ readonly now?: () => number;
7
+ }
8
+ export interface ClaudeSessionTurnInput {
9
+ readonly model: string;
10
+ readonly usage: ModelUsage;
11
+ readonly costUsd?: number;
12
+ readonly webSearchRequests?: number;
13
+ readonly apiDurationMs?: number;
14
+ readonly apiDurationWithoutRetriesMs?: number;
15
+ readonly toolDurationMs?: number;
16
+ readonly linesAdded?: number;
17
+ readonly linesRemoved?: number;
18
+ }
19
+ export interface ClaudeLineChangesInput {
20
+ readonly linesAdded?: number;
21
+ readonly linesRemoved?: number;
22
+ }
23
+ export interface ClaudeSessionDurationsInput {
24
+ readonly apiDurationMs?: number;
25
+ readonly apiDurationWithoutRetriesMs?: number;
26
+ readonly toolDurationMs?: number;
27
+ }
28
+ export interface ClaudeSessionCostSnapshot extends ClaudeSessionCostState {
29
+ readonly hasUnknownModelCost: boolean;
30
+ }
31
+ export declare class ClaudeSessionCostTracker {
32
+ private readonly sessionId;
33
+ private readonly now;
34
+ private readonly modelUsage;
35
+ private totalCostUsd;
36
+ private apiDurationMs;
37
+ private apiDurationWithoutRetriesMs;
38
+ private toolDurationMs;
39
+ private linesAdded;
40
+ private linesRemoved;
41
+ private hasUnknownModelCost;
42
+ private startTime;
43
+ constructor(options: ClaudeSessionCostTrackerOptions);
44
+ recordTurn(input: ClaudeSessionTurnInput): void;
45
+ recordDurations(input: ClaudeSessionDurationsInput): void;
46
+ private durationTotals;
47
+ recordLineChanges(input: ClaudeLineChangesInput): void;
48
+ snapshot(): ClaudeSessionCostSnapshot;
49
+ }
50
+ //# sourceMappingURL=session-cost-tracker.d.ts.map
@@ -0,0 +1,254 @@
1
+ function requireNonBlank(value, field) {
2
+ if (typeof value !== 'string' || value.trim() === '') {
3
+ throw new TypeError(`${field} must not be blank`);
4
+ }
5
+ return value;
6
+ }
7
+ function requireCounter(value, field) {
8
+ if (!Number.isSafeInteger(value) || value < 0) {
9
+ throw new TypeError(`${field} must be a nonnegative safe integer`);
10
+ }
11
+ return value;
12
+ }
13
+ function requireMetric(value, field) {
14
+ if (typeof value !== 'number' || !Number.isFinite(value) || value < 0) {
15
+ throw new TypeError(`${field} must be a finite nonnegative number`);
16
+ }
17
+ return value;
18
+ }
19
+ function requireCounterTotal(value, field) {
20
+ if (!Number.isSafeInteger(value)) {
21
+ throw new TypeError(`${field} total must be a safe integer`);
22
+ }
23
+ }
24
+ function requireMetricTotal(value, field) {
25
+ if (typeof value !== 'number' || !Number.isFinite(value) || value < 0) {
26
+ throw new TypeError(`${field} total must be a finite nonnegative number`);
27
+ }
28
+ }
29
+ function emptyModelUsage() {
30
+ return {
31
+ inputTokens: 0,
32
+ outputTokens: 0,
33
+ cacheReadInputTokens: 0,
34
+ cacheCreationInputTokens: 0,
35
+ webSearchRequests: 0,
36
+ costUsd: 0,
37
+ };
38
+ }
39
+ export class ClaudeSessionCostTracker {
40
+ sessionId;
41
+ now;
42
+ modelUsage = new Map();
43
+ totalCostUsd;
44
+ apiDurationMs;
45
+ apiDurationWithoutRetriesMs;
46
+ toolDurationMs;
47
+ linesAdded;
48
+ linesRemoved;
49
+ hasUnknownModelCost = false;
50
+ startTime;
51
+ constructor(options) {
52
+ this.sessionId = requireNonBlank(options.sessionId, 'sessionId');
53
+ this.now = options.now ?? Date.now;
54
+ const restored = options.restored ?? null;
55
+ if (restored === null) {
56
+ this.totalCostUsd = 0;
57
+ this.apiDurationMs = 0;
58
+ this.apiDurationWithoutRetriesMs = 0;
59
+ this.toolDurationMs = 0;
60
+ this.linesAdded = 0;
61
+ this.linesRemoved = 0;
62
+ this.startTime = this.now();
63
+ return;
64
+ }
65
+ if (restored.sessionId !== this.sessionId) {
66
+ throw new TypeError('restored sessionId does not match the tracker sessionId');
67
+ }
68
+ this.totalCostUsd = restored.totalCostUsd;
69
+ this.apiDurationMs = restored.apiDurationMs;
70
+ this.apiDurationWithoutRetriesMs = restored.apiDurationWithoutRetriesMs;
71
+ this.toolDurationMs = restored.toolDurationMs;
72
+ this.linesAdded = restored.linesAdded;
73
+ this.linesRemoved = restored.linesRemoved;
74
+ for (const [model, usage] of Object.entries(restored.modelUsage)) {
75
+ this.modelUsage.set(model, {
76
+ inputTokens: usage.inputTokens,
77
+ outputTokens: usage.outputTokens,
78
+ cacheReadInputTokens: usage.cacheReadInputTokens,
79
+ cacheCreationInputTokens: usage.cacheCreationInputTokens,
80
+ webSearchRequests: usage.webSearchRequests,
81
+ costUsd: usage.costUsd,
82
+ });
83
+ }
84
+ const current = this.now();
85
+ this.startTime =
86
+ restored.wallDurationMs !== 0
87
+ ? current - restored.wallDurationMs
88
+ : current;
89
+ }
90
+ recordTurn(input) {
91
+ const model = requireNonBlank(input.model, 'model');
92
+ const inputTokens = requireCounter(input.usage.inputTokens, 'usage.inputTokens');
93
+ const outputTokens = requireCounter(input.usage.outputTokens, 'usage.outputTokens');
94
+ const cacheReadInputTokens = input.usage.cacheReadInputTokens === undefined
95
+ ? 0
96
+ : requireCounter(input.usage.cacheReadInputTokens, 'usage.cacheReadInputTokens');
97
+ const cacheCreationInputTokens = input.usage.cacheCreationInputTokens === undefined
98
+ ? 0
99
+ : requireCounter(input.usage.cacheCreationInputTokens, 'usage.cacheCreationInputTokens');
100
+ const webSearchRequests = input.webSearchRequests === undefined
101
+ ? 0
102
+ : requireCounter(input.webSearchRequests, 'webSearchRequests');
103
+ const costUsd = input.costUsd === undefined
104
+ ? undefined
105
+ : requireMetric(input.costUsd, 'costUsd');
106
+ const apiDurationMs = input.apiDurationMs === undefined
107
+ ? 0
108
+ : requireMetric(input.apiDurationMs, 'apiDurationMs');
109
+ const apiDurationWithoutRetriesMs = input.apiDurationWithoutRetriesMs === undefined
110
+ ? apiDurationMs
111
+ : requireMetric(input.apiDurationWithoutRetriesMs, 'apiDurationWithoutRetriesMs');
112
+ const toolDurationMs = input.toolDurationMs === undefined
113
+ ? 0
114
+ : requireMetric(input.toolDurationMs, 'toolDurationMs');
115
+ const linesAdded = input.linesAdded === undefined
116
+ ? 0
117
+ : requireCounter(input.linesAdded, 'linesAdded');
118
+ const linesRemoved = input.linesRemoved === undefined
119
+ ? 0
120
+ : requireCounter(input.linesRemoved, 'linesRemoved');
121
+ const existing = this.modelUsage.get(model) ?? emptyModelUsage();
122
+ const newInputTokens = existing.inputTokens + inputTokens;
123
+ const newOutputTokens = existing.outputTokens + outputTokens;
124
+ const newCacheReadInputTokens = existing.cacheReadInputTokens + cacheReadInputTokens;
125
+ const newCacheCreationInputTokens = existing.cacheCreationInputTokens + cacheCreationInputTokens;
126
+ const newWebSearchRequests = existing.webSearchRequests + webSearchRequests;
127
+ requireCounterTotal(newInputTokens, 'inputTokens');
128
+ requireCounterTotal(newOutputTokens, 'outputTokens');
129
+ requireCounterTotal(newCacheReadInputTokens, 'cacheReadInputTokens');
130
+ requireCounterTotal(newCacheCreationInputTokens, 'cacheCreationInputTokens');
131
+ requireCounterTotal(newWebSearchRequests, 'webSearchRequests');
132
+ const hasUsage = inputTokens > 0 ||
133
+ outputTokens > 0 ||
134
+ cacheReadInputTokens > 0 ||
135
+ cacheCreationInputTokens > 0 ||
136
+ webSearchRequests > 0;
137
+ const newCostUsd = costUsd === undefined ? existing.costUsd : existing.costUsd + costUsd;
138
+ requireMetricTotal(newCostUsd, 'costUsd');
139
+ const newTotalCostUsd = costUsd === undefined ? this.totalCostUsd : this.totalCostUsd + costUsd;
140
+ requireMetricTotal(newTotalCostUsd, 'totalCostUsd');
141
+ const durationTotals = this.durationTotals({
142
+ apiDurationMs,
143
+ apiDurationWithoutRetriesMs,
144
+ toolDurationMs,
145
+ });
146
+ const newLinesAdded = this.linesAdded + linesAdded;
147
+ const newLinesRemoved = this.linesRemoved + linesRemoved;
148
+ requireCounterTotal(newLinesAdded, 'linesAdded');
149
+ requireCounterTotal(newLinesRemoved, 'linesRemoved');
150
+ // Every metric validated; commit atomically.
151
+ if (costUsd === undefined && hasUsage) {
152
+ this.hasUnknownModelCost = true;
153
+ }
154
+ this.modelUsage.set(model, {
155
+ inputTokens: newInputTokens,
156
+ outputTokens: newOutputTokens,
157
+ cacheReadInputTokens: newCacheReadInputTokens,
158
+ cacheCreationInputTokens: newCacheCreationInputTokens,
159
+ webSearchRequests: newWebSearchRequests,
160
+ costUsd: newCostUsd,
161
+ });
162
+ this.totalCostUsd = newTotalCostUsd;
163
+ this.apiDurationMs = durationTotals.apiDurationMs;
164
+ this.apiDurationWithoutRetriesMs =
165
+ durationTotals.apiDurationWithoutRetriesMs;
166
+ this.toolDurationMs = durationTotals.toolDurationMs;
167
+ this.linesAdded = newLinesAdded;
168
+ this.linesRemoved = newLinesRemoved;
169
+ }
170
+ recordDurations(input) {
171
+ const apiDurationMs = input.apiDurationMs === undefined
172
+ ? undefined
173
+ : requireMetric(input.apiDurationMs, 'apiDurationMs');
174
+ const apiDurationWithoutRetriesMs = input.apiDurationWithoutRetriesMs === undefined
175
+ ? undefined
176
+ : requireMetric(input.apiDurationWithoutRetriesMs, 'apiDurationWithoutRetriesMs');
177
+ const toolDurationMs = input.toolDurationMs === undefined
178
+ ? undefined
179
+ : requireMetric(input.toolDurationMs, 'toolDurationMs');
180
+ // Legacy fallback: a supplied API duration implies the retry-free total
181
+ // when the retry-free value is absent.
182
+ const resolvedApiDurationWithoutRetriesMs = apiDurationMs !== undefined && apiDurationWithoutRetriesMs === undefined
183
+ ? apiDurationMs
184
+ : apiDurationWithoutRetriesMs;
185
+ const durationTotals = this.durationTotals({
186
+ apiDurationMs,
187
+ apiDurationWithoutRetriesMs: resolvedApiDurationWithoutRetriesMs,
188
+ toolDurationMs,
189
+ });
190
+ this.apiDurationMs = durationTotals.apiDurationMs;
191
+ this.apiDurationWithoutRetriesMs =
192
+ durationTotals.apiDurationWithoutRetriesMs;
193
+ this.toolDurationMs = durationTotals.toolDurationMs;
194
+ }
195
+ durationTotals(options) {
196
+ const { apiDurationMs, apiDurationWithoutRetriesMs, toolDurationMs } = options;
197
+ const newApiDurationMs = apiDurationMs === undefined
198
+ ? this.apiDurationMs
199
+ : this.apiDurationMs + apiDurationMs;
200
+ const newApiDurationWithoutRetriesMs = apiDurationWithoutRetriesMs === undefined
201
+ ? this.apiDurationWithoutRetriesMs
202
+ : this.apiDurationWithoutRetriesMs + apiDurationWithoutRetriesMs;
203
+ const newToolDurationMs = toolDurationMs === undefined
204
+ ? this.toolDurationMs
205
+ : this.toolDurationMs + toolDurationMs;
206
+ if (apiDurationMs !== undefined) {
207
+ requireMetricTotal(newApiDurationMs, 'apiDurationMs');
208
+ }
209
+ if (apiDurationWithoutRetriesMs !== undefined) {
210
+ requireMetricTotal(newApiDurationWithoutRetriesMs, 'apiDurationWithoutRetriesMs');
211
+ }
212
+ if (toolDurationMs !== undefined) {
213
+ requireMetricTotal(newToolDurationMs, 'toolDurationMs');
214
+ }
215
+ return {
216
+ apiDurationMs: newApiDurationMs,
217
+ apiDurationWithoutRetriesMs: newApiDurationWithoutRetriesMs,
218
+ toolDurationMs: newToolDurationMs,
219
+ };
220
+ }
221
+ recordLineChanges(input) {
222
+ const linesAdded = input.linesAdded === undefined
223
+ ? 0
224
+ : requireCounter(input.linesAdded, 'linesAdded');
225
+ const linesRemoved = input.linesRemoved === undefined
226
+ ? 0
227
+ : requireCounter(input.linesRemoved, 'linesRemoved');
228
+ const newLinesAdded = this.linesAdded + linesAdded;
229
+ const newLinesRemoved = this.linesRemoved + linesRemoved;
230
+ requireCounterTotal(newLinesAdded, 'linesAdded');
231
+ requireCounterTotal(newLinesRemoved, 'linesRemoved');
232
+ this.linesAdded = newLinesAdded;
233
+ this.linesRemoved = newLinesRemoved;
234
+ }
235
+ snapshot() {
236
+ const modelUsage = {};
237
+ for (const [model, usage] of this.modelUsage) {
238
+ modelUsage[model] = { ...usage };
239
+ }
240
+ return {
241
+ sessionId: this.sessionId,
242
+ totalCostUsd: this.totalCostUsd,
243
+ apiDurationMs: this.apiDurationMs,
244
+ apiDurationWithoutRetriesMs: this.apiDurationWithoutRetriesMs,
245
+ toolDurationMs: this.toolDurationMs,
246
+ wallDurationMs: this.now() - this.startTime,
247
+ linesAdded: this.linesAdded,
248
+ linesRemoved: this.linesRemoved,
249
+ modelUsage,
250
+ hasUnknownModelCost: this.hasUnknownModelCost,
251
+ };
252
+ }
253
+ }
254
+ //# sourceMappingURL=session-cost-tracker.js.map