praxis-agent 0.55.1 → 0.55.2

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.
@@ -0,0 +1,74 @@
1
+ import type { ModelUsage } from '../core/runtime.js';
2
+ import type { ModelPricingRegistry } from '../core/usage.js';
3
+ import { ClaudeSessionCostTracker } from './session-cost-tracker.js';
4
+ export interface TurnToolMetric {
5
+ readonly isError: boolean;
6
+ readonly usage?: ModelUsage;
7
+ readonly modelUsage?: Readonly<Record<string, ModelUsage>>;
8
+ readonly durationApiMs?: number;
9
+ readonly durationApiWithoutRetriesMs?: number;
10
+ readonly durationToolMs?: number;
11
+ readonly linesAdded?: number;
12
+ readonly linesRemoved?: number;
13
+ }
14
+ export interface TurnRuntimeMetric {
15
+ readonly usage: ModelUsage;
16
+ readonly modelUsage?: Readonly<Record<string, ModelUsage>>;
17
+ readonly durationApiMs?: number;
18
+ readonly durationApiWithoutRetriesMs?: number;
19
+ readonly durationToolMs?: number;
20
+ readonly linesAdded?: number;
21
+ readonly linesRemoved?: number;
22
+ readonly unrecordedModelUsage?: Readonly<Record<string, ModelUsage>>;
23
+ readonly unrecordedDurationApiMs?: number;
24
+ readonly unrecordedDurationApiWithoutRetriesMs?: number;
25
+ }
26
+ export interface TurnCompactionMetric {
27
+ readonly usage: ModelUsage;
28
+ readonly model?: string;
29
+ readonly durationApiMs: number;
30
+ readonly durationApiWithoutRetriesMs: number;
31
+ }
32
+ export interface TurnRuntimeCompletion {
33
+ readonly kind: 'runtime';
34
+ readonly recovery: readonly TurnToolMetric[];
35
+ readonly result: TurnRuntimeMetric;
36
+ }
37
+ export interface TurnShellCompletion {
38
+ readonly kind: 'shell';
39
+ readonly recovery: readonly TurnToolMetric[];
40
+ readonly result: TurnToolMetric;
41
+ }
42
+ export type TurnAccountingCompletion = TurnRuntimeCompletion | TurnShellCompletion;
43
+ export interface TurnAccountingOutcome {
44
+ readonly usage: ModelUsage;
45
+ readonly modelUsage?: Readonly<Record<string, ModelUsage>>;
46
+ readonly costUsd?: number;
47
+ readonly durationApiMs?: number;
48
+ }
49
+ export interface PreparedCompactionAccounting {
50
+ readonly commit: () => void;
51
+ }
52
+ export declare class TurnAccounting {
53
+ private readonly tracker;
54
+ private readonly pricing;
55
+ private compactionUsage;
56
+ private compactionModelUsage;
57
+ private compactionDurationMs;
58
+ private compactionDurationWithoutRetriesMs;
59
+ private hasCompaction;
60
+ private outstandingCompaction;
61
+ private completed;
62
+ constructor(options: {
63
+ readonly tracker: ClaudeSessionCostTracker;
64
+ readonly pricing?: ModelPricingRegistry;
65
+ });
66
+ prepareCompaction(input: TurnCompactionMetric): PreparedCompactionAccounting;
67
+ complete(input: TurnAccountingCompletion): TurnAccountingOutcome;
68
+ private compactionStateAfter;
69
+ private compactionPlan;
70
+ private aggregate;
71
+ private trackerPlan;
72
+ private appendModelInputs;
73
+ }
74
+ //# sourceMappingURL=turn-accounting.d.ts.map
@@ -0,0 +1,444 @@
1
+ import { usageCostUsd } from '../core/usage.js';
2
+ import { ClaudeSessionCostTracker, } from './session-cost-tracker.js';
3
+ const counterFields = [
4
+ 'inputTokens',
5
+ 'outputTokens',
6
+ 'cacheReadInputTokens',
7
+ 'cacheCreationInputTokens',
8
+ 'webSearchRequests',
9
+ ];
10
+ const metadataFields = ['contextWindow', 'maxOutputTokens'];
11
+ function clone(value) {
12
+ return structuredClone(value);
13
+ }
14
+ function mergeUsage(left, right) {
15
+ for (const [usage, prefix] of [
16
+ [left, 'usage.left'],
17
+ [right, 'usage.right'],
18
+ ]) {
19
+ for (const field of counterFields) {
20
+ const value = usage[field];
21
+ if (value !== undefined && (!Number.isSafeInteger(value) || value < 0)) {
22
+ throw new TypeError(`${prefix}.${field} must be a nonnegative safe integer`);
23
+ }
24
+ }
25
+ }
26
+ const cacheReadInputTokens = (left.cacheReadInputTokens ?? 0) + (right.cacheReadInputTokens ?? 0);
27
+ const cacheCreationInputTokens = (left.cacheCreationInputTokens ?? 0) + (right.cacheCreationInputTokens ?? 0);
28
+ const webSearchRequests = (left.webSearchRequests ?? 0) + (right.webSearchRequests ?? 0);
29
+ if (!Number.isSafeInteger(left.inputTokens + right.inputTokens) ||
30
+ !Number.isSafeInteger(left.outputTokens + right.outputTokens) ||
31
+ !Number.isSafeInteger(cacheReadInputTokens) ||
32
+ !Number.isSafeInteger(cacheCreationInputTokens) ||
33
+ !Number.isSafeInteger(webSearchRequests)) {
34
+ throw new Error('Model usage total overflow');
35
+ }
36
+ return {
37
+ inputTokens: left.inputTokens + right.inputTokens,
38
+ outputTokens: left.outputTokens + right.outputTokens,
39
+ ...(cacheReadInputTokens === 0 ? {} : { cacheReadInputTokens }),
40
+ ...(cacheCreationInputTokens === 0 ? {} : { cacheCreationInputTokens }),
41
+ ...(webSearchRequests === 0 ? {} : { webSearchRequests }),
42
+ };
43
+ }
44
+ function assertValidSessionUsageEntry(model, usage) {
45
+ if (model.trim() === '') {
46
+ throw new Error('Model usage breakdown contains a blank model name');
47
+ }
48
+ for (const field of counterFields) {
49
+ const value = usage[field];
50
+ if (value !== undefined && (!Number.isSafeInteger(value) || value < 0)) {
51
+ throw new Error(`Model usage for "${model}" has an invalid ${field} counter`);
52
+ }
53
+ }
54
+ for (const field of metadataFields) {
55
+ const value = usage[field];
56
+ if (value !== undefined && (!Number.isSafeInteger(value) || value < 1)) {
57
+ throw new Error(`Model usage for "${model}" has an invalid ${field} metadata value`);
58
+ }
59
+ }
60
+ }
61
+ function mergeSessionUsageMetadataField(model, field, left, right) {
62
+ if (left === undefined)
63
+ return right;
64
+ if (right === undefined)
65
+ return left;
66
+ if (left !== right) {
67
+ throw new Error(`Model usage for "${model}" has conflicting ${field} values: ${left} vs ${right}`);
68
+ }
69
+ return left;
70
+ }
71
+ function mergeSessionUsageMetadata(model, left, right) {
72
+ const contextWindow = mergeSessionUsageMetadataField(model, 'contextWindow', left.contextWindow, right.contextWindow);
73
+ const maxOutputTokens = mergeSessionUsageMetadataField(model, 'maxOutputTokens', left.maxOutputTokens, right.maxOutputTokens);
74
+ return {
75
+ ...(contextWindow === undefined ? {} : { contextWindow }),
76
+ ...(maxOutputTokens === undefined ? {} : { maxOutputTokens }),
77
+ };
78
+ }
79
+ function addSessionUsageChecked(model, left, right) {
80
+ const inputTokens = left.inputTokens + right.inputTokens;
81
+ const outputTokens = left.outputTokens + right.outputTokens;
82
+ const cacheReadInputTokens = (left.cacheReadInputTokens ?? 0) + (right.cacheReadInputTokens ?? 0);
83
+ const cacheCreationInputTokens = (left.cacheCreationInputTokens ?? 0) + (right.cacheCreationInputTokens ?? 0);
84
+ const webSearchRequests = (left.webSearchRequests ?? 0) + (right.webSearchRequests ?? 0);
85
+ if (!Number.isSafeInteger(inputTokens) ||
86
+ !Number.isSafeInteger(outputTokens) ||
87
+ !Number.isSafeInteger(cacheReadInputTokens) ||
88
+ !Number.isSafeInteger(cacheCreationInputTokens) ||
89
+ !Number.isSafeInteger(webSearchRequests)) {
90
+ throw new Error('Model usage total overflow');
91
+ }
92
+ const metadata = mergeSessionUsageMetadata(model, left, right);
93
+ return {
94
+ inputTokens,
95
+ outputTokens,
96
+ ...(cacheReadInputTokens === 0 ? {} : { cacheReadInputTokens }),
97
+ ...(cacheCreationInputTokens === 0 ? {} : { cacheCreationInputTokens }),
98
+ ...(webSearchRequests === 0 ? {} : { webSearchRequests }),
99
+ ...metadata,
100
+ };
101
+ }
102
+ function mergeModelUsage(...parts) {
103
+ const merged = new Map();
104
+ for (const part of parts) {
105
+ if (part === undefined)
106
+ continue;
107
+ for (const [model, usage] of Object.entries(part)) {
108
+ assertValidSessionUsageEntry(model, usage);
109
+ const existing = merged.get(model);
110
+ merged.set(model, existing === undefined
111
+ ? { ...usage }
112
+ : addSessionUsageChecked(model, existing, usage));
113
+ }
114
+ }
115
+ return merged.size === 0 ? undefined : Object.fromEntries(merged);
116
+ }
117
+ function hasNonZeroUsage(usage) {
118
+ return (usage.inputTokens > 0 ||
119
+ usage.outputTokens > 0 ||
120
+ (usage.cacheReadInputTokens ?? 0) > 0 ||
121
+ (usage.cacheCreationInputTokens ?? 0) > 0 ||
122
+ (usage.webSearchRequests ?? 0) > 0);
123
+ }
124
+ function validateMetricUsage(usage) {
125
+ for (const field of ['inputTokens', 'outputTokens']) {
126
+ const value = usage[field];
127
+ if (!Number.isSafeInteger(value) || value < 0) {
128
+ throw new TypeError(`usage.${field} must be a nonnegative safe integer`);
129
+ }
130
+ }
131
+ for (const field of counterFields.slice(2)) {
132
+ const value = usage[field];
133
+ if (value !== undefined && (!Number.isSafeInteger(value) || value < 0)) {
134
+ throw new TypeError(`usage.${field} must be a nonnegative safe integer`);
135
+ }
136
+ }
137
+ }
138
+ function requireMetric(value, field) {
139
+ if (typeof value !== 'number' || !Number.isFinite(value) || value < 0) {
140
+ throw new TypeError(`${field} must be a finite nonnegative number`);
141
+ }
142
+ return value;
143
+ }
144
+ function addMetric(value, total, field) {
145
+ if (value === undefined)
146
+ return total;
147
+ const next = total + requireMetric(value, field);
148
+ if (!Number.isFinite(next) || next < 0) {
149
+ throw new TypeError(`${field} total overflow`);
150
+ }
151
+ return next;
152
+ }
153
+ function addLines(value, total, field) {
154
+ if (value === undefined)
155
+ return total;
156
+ if (!Number.isSafeInteger(value) || value < 0) {
157
+ throw new TypeError(`${field} must be a nonnegative safe integer`);
158
+ }
159
+ const next = total + value;
160
+ if (!Number.isSafeInteger(next) || next < 0) {
161
+ throw new TypeError(`${field} total must be a nonnegative safe integer`);
162
+ }
163
+ return next;
164
+ }
165
+ function costForModelUsage(modelUsage, pricing) {
166
+ if (modelUsage === undefined)
167
+ return undefined;
168
+ let costUsd;
169
+ for (const [model, usage] of Object.entries(modelUsage)) {
170
+ const resolved = pricing?.resolve(model);
171
+ if (resolved !== undefined) {
172
+ costUsd = (costUsd ?? 0) + usageCostUsd(usage, resolved);
173
+ }
174
+ }
175
+ return costUsd;
176
+ }
177
+ function applyPlan(tracker, plan) {
178
+ for (const input of plan.turnInputs)
179
+ tracker.recordTurn(input);
180
+ if (plan.durations !== undefined)
181
+ tracker.recordDurations(plan.durations);
182
+ if (plan.linesAdded !== 0 || plan.linesRemoved !== 0) {
183
+ tracker.recordLineChanges({
184
+ ...(plan.linesAdded === 0 ? {} : { linesAdded: plan.linesAdded }),
185
+ ...(plan.linesRemoved === 0 ? {} : { linesRemoved: plan.linesRemoved }),
186
+ });
187
+ }
188
+ }
189
+ function cloneTracker(tracker) {
190
+ const snapshot = tracker.snapshot();
191
+ return new ClaudeSessionCostTracker({
192
+ sessionId: snapshot.sessionId,
193
+ restored: snapshot,
194
+ });
195
+ }
196
+ export class TurnAccounting {
197
+ tracker;
198
+ pricing;
199
+ compactionUsage = {
200
+ inputTokens: 0,
201
+ outputTokens: 0,
202
+ };
203
+ compactionModelUsage;
204
+ compactionDurationMs = 0;
205
+ compactionDurationWithoutRetriesMs = 0;
206
+ hasCompaction = false;
207
+ outstandingCompaction = false;
208
+ completed = false;
209
+ constructor(options) {
210
+ this.tracker = options.tracker;
211
+ this.pricing = options.pricing;
212
+ }
213
+ prepareCompaction(input) {
214
+ if (this.completed) {
215
+ throw new Error('Cannot prepare compaction after turn completion');
216
+ }
217
+ if (this.outstandingCompaction) {
218
+ throw new Error('A compaction accounting plan is already outstanding');
219
+ }
220
+ const snapshot = clone(input);
221
+ this.compactionStateAfter(snapshot);
222
+ const plan = this.compactionPlan(snapshot);
223
+ const preflight = cloneTracker(this.tracker);
224
+ applyPlan(preflight, plan);
225
+ this.outstandingCompaction = true;
226
+ let consumed = false;
227
+ return {
228
+ commit: () => {
229
+ if (consumed)
230
+ throw new Error('Prepared compaction accounting was already committed');
231
+ consumed = true;
232
+ try {
233
+ const latestState = this.compactionStateAfter(snapshot);
234
+ const latestPlan = this.compactionPlan(snapshot);
235
+ const latestPreflight = cloneTracker(this.tracker);
236
+ applyPlan(latestPreflight, latestPlan);
237
+ applyPlan(this.tracker, latestPlan);
238
+ this.compactionUsage = clone(latestState.usage);
239
+ this.compactionModelUsage = clone(latestState.modelUsage);
240
+ this.compactionDurationMs = latestState.durationApiMs;
241
+ this.compactionDurationWithoutRetriesMs =
242
+ latestState.durationApiWithoutRetriesMs;
243
+ this.hasCompaction = true;
244
+ }
245
+ finally {
246
+ this.outstandingCompaction = false;
247
+ }
248
+ },
249
+ };
250
+ }
251
+ complete(input) {
252
+ if (this.completed) {
253
+ throw new Error('Turn accounting is already complete');
254
+ }
255
+ if (this.outstandingCompaction) {
256
+ throw new Error('Cannot complete a turn while compaction accounting is outstanding');
257
+ }
258
+ const snapshot = clone(input);
259
+ const aggregate = this.aggregate(snapshot);
260
+ const plan = this.trackerPlan(snapshot);
261
+ const preflight = cloneTracker(this.tracker);
262
+ applyPlan(preflight, plan);
263
+ applyPlan(this.tracker, plan);
264
+ this.completed = true;
265
+ return clone({
266
+ usage: aggregate.usage,
267
+ ...(aggregate.modelUsage === undefined
268
+ ? {}
269
+ : { modelUsage: aggregate.modelUsage }),
270
+ ...(aggregate.costUsd === undefined
271
+ ? {}
272
+ : { costUsd: aggregate.costUsd }),
273
+ ...(aggregate.durationApiMs === undefined
274
+ ? {}
275
+ : { durationApiMs: aggregate.durationApiMs }),
276
+ });
277
+ }
278
+ compactionStateAfter(input) {
279
+ validateMetricUsage(input.usage);
280
+ requireMetric(input.durationApiMs, 'compaction durationMs');
281
+ requireMetric(input.durationApiWithoutRetriesMs, 'compaction durationWithoutRetriesMs');
282
+ const meaningfulUsage = hasNonZeroUsage(input.usage);
283
+ if (meaningfulUsage &&
284
+ (input.model === undefined || input.model.trim() === '')) {
285
+ throw new Error('Auto compact usage requires a nonblank model identity');
286
+ }
287
+ const modelUsage = input.model !== undefined && input.model.trim() !== ''
288
+ ? { [input.model]: input.usage }
289
+ : undefined;
290
+ return {
291
+ usage: mergeUsage(this.compactionUsage, input.usage),
292
+ modelUsage: mergeModelUsage(this.compactionModelUsage, modelUsage),
293
+ durationApiMs: addMetric(input.durationApiMs, this.compactionDurationMs, 'compaction durationMs'),
294
+ durationApiWithoutRetriesMs: addMetric(input.durationApiWithoutRetriesMs, this.compactionDurationWithoutRetriesMs, 'compaction durationWithoutRetriesMs'),
295
+ };
296
+ }
297
+ compactionPlan(input) {
298
+ const turnInputs = [];
299
+ if (hasNonZeroUsage(input.usage)) {
300
+ const model = input.model;
301
+ if (model === undefined || model.trim() === '') {
302
+ throw new Error('Auto compact usage requires a nonblank model identity');
303
+ }
304
+ const costUsd = this.pricing
305
+ ? costForModelUsage({ [model]: input.usage }, this.pricing)
306
+ : undefined;
307
+ turnInputs.push({
308
+ model,
309
+ usage: input.usage,
310
+ ...(costUsd === undefined ? {} : { costUsd }),
311
+ ...(input.usage.webSearchRequests === undefined
312
+ ? {}
313
+ : { webSearchRequests: input.usage.webSearchRequests }),
314
+ });
315
+ }
316
+ const durations = {
317
+ ...(input.durationApiMs === 0
318
+ ? {}
319
+ : { apiDurationMs: input.durationApiMs }),
320
+ apiDurationWithoutRetriesMs: input.durationApiWithoutRetriesMs,
321
+ };
322
+ return {
323
+ turnInputs,
324
+ durations,
325
+ linesAdded: 0,
326
+ linesRemoved: 0,
327
+ };
328
+ }
329
+ aggregate(input) {
330
+ if (input.kind === 'runtime') {
331
+ requireMetric(input.result.durationApiMs ?? 0, 'durationApiMs');
332
+ requireMetric(input.result.durationApiWithoutRetriesMs ?? 0, 'durationApiWithoutRetriesMs');
333
+ }
334
+ if (input.kind === 'shell') {
335
+ if (input.result.durationApiMs !== undefined)
336
+ requireMetric(input.result.durationApiMs, 'durationApiMs');
337
+ if (input.result.durationApiWithoutRetriesMs !== undefined)
338
+ requireMetric(input.result.durationApiWithoutRetriesMs, 'durationApiWithoutRetriesMs');
339
+ }
340
+ const recoveryUsage = input.recovery.reduce((total, result) => result.usage ? mergeUsage(total, result.usage) : total, { inputTokens: 0, outputTokens: 0 });
341
+ const recoveryModelUsage = mergeModelUsage(...input.recovery.map((result) => result.isError ? undefined : result.modelUsage));
342
+ const resultUsage = input.result.usage ?? {
343
+ inputTokens: 0,
344
+ outputTokens: 0,
345
+ };
346
+ const usage = input.kind === 'runtime'
347
+ ? mergeUsage(mergeUsage(recoveryUsage, this.compactionUsage), resultUsage)
348
+ : mergeUsage(recoveryUsage, resultUsage);
349
+ const modelUsage = mergeModelUsage(recoveryModelUsage, ...(input.kind === 'runtime' ? [this.compactionModelUsage] : []), input.kind === 'runtime'
350
+ ? input.result.modelUsage
351
+ : input.result.isError
352
+ ? undefined
353
+ : input.result.modelUsage);
354
+ const hasDuration = input.kind === 'runtime' &&
355
+ (this.hasCompaction || input.result.durationApiMs !== undefined);
356
+ const durationApiMs = input.kind === 'runtime' && hasDuration
357
+ ? addMetric(input.result.durationApiMs, this.compactionDurationMs, 'durationApiMs')
358
+ : input.result.durationApiMs;
359
+ const costUsd = costForModelUsage(modelUsage, this.pricing);
360
+ return {
361
+ usage,
362
+ ...(modelUsage === undefined ? {} : { modelUsage }),
363
+ ...(costUsd === undefined ? {} : { costUsd }),
364
+ ...(durationApiMs === undefined ? {} : { durationApiMs }),
365
+ };
366
+ }
367
+ trackerPlan(input) {
368
+ const turnInputs = [];
369
+ const lines = { linesAdded: 0, linesRemoved: 0 };
370
+ let toolDurationMs = 0;
371
+ for (const result of input.recovery) {
372
+ toolDurationMs = addMetric(result.durationToolMs, toolDurationMs, 'durationToolMs');
373
+ if (!result.isError) {
374
+ lines.linesAdded = addLines(result.linesAdded, lines.linesAdded, 'linesAdded');
375
+ lines.linesRemoved = addLines(result.linesRemoved, lines.linesRemoved, 'linesRemoved');
376
+ }
377
+ }
378
+ const trackedModelUsage = input.kind === 'shell'
379
+ ? mergeModelUsage(...input.recovery.map((result) => result.isError ? undefined : result.modelUsage), input.result.isError ? undefined : input.result.modelUsage)
380
+ : mergeModelUsage(...input.recovery.map((result) => result.isError ? undefined : result.modelUsage), input.result.unrecordedModelUsage ?? input.result.modelUsage);
381
+ if (trackedModelUsage !== undefined) {
382
+ this.appendModelInputs(turnInputs, trackedModelUsage);
383
+ }
384
+ if (input.kind === 'shell') {
385
+ toolDurationMs = addMetric(input.result.durationToolMs, toolDurationMs, 'durationToolMs');
386
+ if (!input.result.isError) {
387
+ lines.linesAdded = addLines(input.result.linesAdded, lines.linesAdded, 'linesAdded');
388
+ lines.linesRemoved = addLines(input.result.linesRemoved, lines.linesRemoved, 'linesRemoved');
389
+ }
390
+ return {
391
+ turnInputs,
392
+ durations: {
393
+ ...(input.result.durationApiMs === undefined
394
+ ? {}
395
+ : { apiDurationMs: input.result.durationApiMs }),
396
+ ...(input.result.durationApiWithoutRetriesMs === undefined
397
+ ? {}
398
+ : {
399
+ apiDurationWithoutRetriesMs: input.result.durationApiWithoutRetriesMs,
400
+ }),
401
+ ...(toolDurationMs === 0 ? {} : { toolDurationMs }),
402
+ },
403
+ ...lines,
404
+ };
405
+ }
406
+ toolDurationMs = addMetric(input.result.durationToolMs, toolDurationMs, 'durationToolMs');
407
+ lines.linesAdded = addLines(input.result.linesAdded, lines.linesAdded, 'linesAdded');
408
+ lines.linesRemoved = addLines(input.result.linesRemoved, lines.linesRemoved, 'linesRemoved');
409
+ const trackedDurationApiMs = input.result.unrecordedDurationApiMs ?? input.result.durationApiMs;
410
+ const trackedDurationApiWithoutRetriesMs = input.result.unrecordedDurationApiWithoutRetriesMs ??
411
+ input.result.durationApiWithoutRetriesMs;
412
+ return {
413
+ turnInputs,
414
+ durations: {
415
+ ...(trackedDurationApiMs === undefined
416
+ ? {}
417
+ : { apiDurationMs: trackedDurationApiMs }),
418
+ ...(trackedDurationApiWithoutRetriesMs === undefined
419
+ ? {}
420
+ : {
421
+ apiDurationWithoutRetriesMs: trackedDurationApiWithoutRetriesMs,
422
+ }),
423
+ ...(toolDurationMs === 0 ? {} : { toolDurationMs }),
424
+ },
425
+ ...lines,
426
+ };
427
+ }
428
+ appendModelInputs(target, modelUsage) {
429
+ for (const [model, usage] of Object.entries(modelUsage)) {
430
+ const costUsd = this.pricing
431
+ ? costForModelUsage({ [model]: usage }, this.pricing)
432
+ : undefined;
433
+ target.push({
434
+ model,
435
+ usage,
436
+ ...(costUsd === undefined ? {} : { costUsd }),
437
+ ...(usage.webSearchRequests === undefined
438
+ ? {}
439
+ : { webSearchRequests: usage.webSearchRequests }),
440
+ });
441
+ }
442
+ }
443
+ }
444
+ //# sourceMappingURL=turn-accounting.js.map
@@ -0,0 +1,62 @@
1
+ import type { ModelMessage } from '../core/runtime.js';
2
+ import type { TranscriptEvent } from '../core/transcript-event.js';
3
+ import type { NativeTranscriptEntry } from '../native/schema.js';
4
+ import type { NativeCompactionAppend, NativeInterruption, NativeMessageAppend, NativeSessionTranscriptLease } from './native-session-transcript.js';
5
+ import type { NativeTranscriptTail } from '../persistence/native-transcript-store.js';
6
+ export interface TurnPersistenceView {
7
+ readonly projectionEntries: readonly NativeTranscriptEntry[];
8
+ readonly projectionTail: NativeTranscriptTail;
9
+ readonly activeEvents: readonly TranscriptEvent[];
10
+ readonly activeMessages: readonly ModelMessage[];
11
+ readonly interruption: NativeInterruption;
12
+ }
13
+ export type TurnPersistenceCommand = {
14
+ readonly kind: 'projection';
15
+ readonly entries: readonly NativeTranscriptEntry[];
16
+ } | {
17
+ readonly kind: 'messages';
18
+ readonly input: NativeMessageAppend;
19
+ readonly projectionEntries?: readonly NativeTranscriptEntry[];
20
+ } | {
21
+ readonly kind: 'tool-execution-started';
22
+ readonly callId: string;
23
+ } | {
24
+ readonly kind: 'tool-completion';
25
+ readonly input: Parameters<NativeSessionTranscriptLease['appendToolCompletion']>[0];
26
+ } | {
27
+ readonly kind: 'compaction';
28
+ readonly input: NativeCompactionAppend;
29
+ };
30
+ export type TurnPersistenceReceipt = {
31
+ readonly kind: 'projection';
32
+ readonly lastProjectionId: string | null;
33
+ } | {
34
+ readonly kind: 'messages';
35
+ readonly eventId: string;
36
+ } | {
37
+ readonly kind: 'tool-execution-started';
38
+ readonly callId: string;
39
+ } | {
40
+ readonly kind: 'tool-completion';
41
+ readonly callId: string;
42
+ } | {
43
+ readonly kind: 'compaction';
44
+ readonly boundaryId: string;
45
+ readonly summaryId: string;
46
+ };
47
+ export declare class TurnPersistence {
48
+ private readonly input;
49
+ private projectionEntries;
50
+ private projectionTail;
51
+ private commitQueue;
52
+ constructor(input: {
53
+ readonly native: NativeSessionTranscriptLease;
54
+ readonly initialProjectionEntries?: readonly NativeTranscriptEntry[];
55
+ });
56
+ view(): TurnPersistenceView;
57
+ refresh(): TurnPersistenceView;
58
+ commit(command: TurnPersistenceCommand): Promise<TurnPersistenceReceipt>;
59
+ private commitCommand;
60
+ private stageProjection;
61
+ }
62
+ //# sourceMappingURL=turn-persistence.d.ts.map
@@ -0,0 +1,114 @@
1
+ import { projectNativeSessionEntries } from './native-session-projection.js';
2
+ const emptyProjectionTail = () => ({
3
+ byteLength: 0,
4
+ lastLineHash: null,
5
+ lastEventId: null,
6
+ newlineTerminated: true,
7
+ });
8
+ export class TurnPersistence {
9
+ input;
10
+ projectionEntries;
11
+ projectionTail = emptyProjectionTail();
12
+ commitQueue = Promise.resolve();
13
+ constructor(input) {
14
+ this.input = input;
15
+ this.projectionEntries = structuredClone(input.initialProjectionEntries
16
+ ? [...input.initialProjectionEntries]
17
+ : projectNativeSessionEntries(input.native.activeEvents()));
18
+ }
19
+ view() {
20
+ return {
21
+ projectionEntries: structuredClone(this.projectionEntries),
22
+ projectionTail: structuredClone(this.projectionTail),
23
+ activeEvents: structuredClone(this.input.native.activeEvents()),
24
+ activeMessages: structuredClone(this.input.native.activeMessages()),
25
+ interruption: structuredClone(this.input.native.interruption()),
26
+ };
27
+ }
28
+ refresh() {
29
+ this.projectionEntries = structuredClone(projectNativeSessionEntries(this.input.native.activeEvents()));
30
+ return this.view();
31
+ }
32
+ commit(command) {
33
+ let ownership;
34
+ try {
35
+ ownership = { ok: true, command: structuredClone(command) };
36
+ }
37
+ catch (error) {
38
+ ownership = { ok: false, error };
39
+ }
40
+ const operation = this.commitQueue.then(() => {
41
+ if (!ownership.ok)
42
+ throw ownership.error;
43
+ return this.commitCommand(ownership.command);
44
+ });
45
+ this.commitQueue = operation.then(() => undefined, () => undefined);
46
+ return operation;
47
+ }
48
+ async commitCommand(command) {
49
+ switch (command.kind) {
50
+ case 'projection': {
51
+ const staged = this.stageProjection(command.entries);
52
+ this.projectionEntries = staged.entries;
53
+ this.projectionTail = staged.tail;
54
+ const lastProjectionId = [...command.entries]
55
+ .reverse()
56
+ .find((entry) => typeof entry.uuid === 'string')?.uuid;
57
+ return {
58
+ kind: 'projection',
59
+ lastProjectionId: typeof lastProjectionId === 'string' ? lastProjectionId : null,
60
+ };
61
+ }
62
+ case 'messages': {
63
+ if (command.input.messages.length === 0)
64
+ throw new Error('native transcript cannot append empty messages');
65
+ const staged = command.projectionEntries === undefined
66
+ ? undefined
67
+ : this.stageProjection(command.projectionEntries);
68
+ const eventId = await this.input.native.appendMessages(command.input);
69
+ if (staged) {
70
+ this.projectionEntries = staged.entries;
71
+ this.projectionTail = staged.tail;
72
+ }
73
+ return { kind: 'messages', eventId };
74
+ }
75
+ case 'tool-execution-started':
76
+ await this.input.native.beginToolExecution(command.callId);
77
+ return { kind: 'tool-execution-started', callId: command.callId };
78
+ case 'tool-completion':
79
+ await this.input.native.appendToolCompletion(command.input);
80
+ return { kind: 'tool-completion', callId: command.input.callId };
81
+ case 'compaction': {
82
+ const result = await this.input.native.appendCompaction(command.input);
83
+ return { kind: 'compaction', ...result };
84
+ }
85
+ default:
86
+ throw new Error('Unknown turn persistence command');
87
+ }
88
+ }
89
+ stageProjection(entries) {
90
+ if (entries.length === 0)
91
+ throw new Error('Cannot append an empty projection');
92
+ const stagedEntries = [...this.projectionEntries, ...entries];
93
+ const lastUuidValue = [...entries]
94
+ .reverse()
95
+ .find((entry) => typeof entry.uuid === 'string')?.uuid;
96
+ const lastUuid = typeof lastUuidValue === 'string' ? lastUuidValue : undefined;
97
+ const byteLength = this.projectionTail.byteLength + entries.length;
98
+ return {
99
+ entries: stagedEntries,
100
+ tail: {
101
+ ...this.projectionTail,
102
+ byteLength,
103
+ lastLineHash: `projection:${byteLength}`,
104
+ lastEventId: typeof lastUuid === 'string'
105
+ ? lastUuid
106
+ : this.projectionTail.lastEventId,
107
+ ...(this.projectionTail.branchParentId === undefined
108
+ ? {}
109
+ : { branchParentId: null }),
110
+ },
111
+ };
112
+ }
113
+ }
114
+ //# sourceMappingURL=turn-persistence.js.map