From f9ef7bcd25a98177d847118f21d729876caeb5d4 Mon Sep 17 00:00:00 2001 From: sungl <81428141+Sun-GLiang@users.noreply.github.com> Date: Sat, 22 Aug 2026 00:13:10 +0800 Subject: [PATCH] refactor(runtime-host): remove redundant trace totals Generated-by: Codex --- .../main-process-diagnostics.test.ts | 9 -- .../session-inspector-panel-model.test.ts | 131 ++++++++++++++++-- .../session-inspector-pricing-key.test.ts | 19 --- .../main/__tests__/use-session-trace.test.ts | 3 - apps/desktop/src/preload/preload.ts | 1 - .../renderer/session-inspector-panel-model.ts | 36 +++-- .../src/renderer/session-inspector-panel.tsx | 2 +- .../stories/session-workbar.stories.tsx | 49 ------- .../core/src/__tests__/session-trace.test.ts | 16 +-- packages/core/src/session-trace.ts | 98 +------------ .../execution-inspect-protocol.test.ts | 71 +++++++--- .../src/__tests__/protocol.test.ts | 6 +- .../src/protocol/execution-inspect.ts | 7 +- packages/runtime-host/src/protocol/index.ts | 3 +- .../session-trace-projection.test.ts | 40 ++++-- .../runtime/src/session-trace-projection.ts | 34 ----- 16 files changed, 239 insertions(+), 286 deletions(-) diff --git a/apps/desktop/src/main/__tests__/main-process-diagnostics.test.ts b/apps/desktop/src/main/__tests__/main-process-diagnostics.test.ts index 97b5435c24..e09eef627a 100644 --- a/apps/desktop/src/main/__tests__/main-process-diagnostics.test.ts +++ b/apps/desktop/src/main/__tests__/main-process-diagnostics.test.ts @@ -206,15 +206,6 @@ test('copies bounded evidence for the exact failed Turn', async () => { message: 'No endpoints accepted the request', }, ], - totals: { - durationMs: 3_501, - modelAttempts: 1, - retries: 0, - compactions: 0, - inputTokens: 0, - outputTokens: 0, - unpricedAttempts: 1, - }, failure: { code: 'model_call_failed', message: 'No endpoints accepted the request', diff --git a/apps/desktop/src/main/__tests__/session-inspector-panel-model.test.ts b/apps/desktop/src/main/__tests__/session-inspector-panel-model.test.ts index aec2939ad3..f35d2ebcc6 100644 --- a/apps/desktop/src/main/__tests__/session-inspector-panel-model.test.ts +++ b/apps/desktop/src/main/__tests__/session-inspector-panel-model.test.ts @@ -2,8 +2,10 @@ import assert from 'node:assert/strict'; import { test } from 'node:test'; import { SESSION_TRACE_SCHEMA_VERSION, - emptyTraceTotals, type SessionTrace, + type TraceModelAttempt, + type TraceModelCallStep, + type TraceStep, } from '@maka/core/session-trace'; import { deriveInspectorPanelModel } from '../../renderer/session-inspector-panel-model.js'; import { @@ -118,6 +120,59 @@ test('does not estimate a cache-hit ratio from partial usage', () => { assert.equal(overview.cacheHitRate, undefined); }); +test('derives per-turn cost only from priced model-call step totals', () => { + const cases: readonly { + name: string; + steps: TraceStep[]; + expected: number | undefined; + }[] = [ + { name: 'empty', steps: [], expected: undefined }, + { + name: 'tool-only', + steps: [ + { + kind: 'tool', + id: 'tool-1', + turnId: 'turn-1', + runId: 'run-1', + startedAt: 1, + endedAt: 2, + durationMs: 1, + toolName: 'Read', + status: 'completed', + }, + ], + expected: undefined, + }, + { name: 'unpriced', steps: [modelCallStep('unpriced')], expected: undefined }, + { name: 'priced', steps: [modelCallStep('priced', 0.01)], expected: 0.01 }, + { + name: 'mixed', + steps: [modelCallStep('priced', 0.01), modelCallStep('unpriced')], + expected: 0.01, + }, + { + name: 'multiple calls', + steps: [modelCallStep('first', 0.01), modelCallStep('second', 0.02)], + expected: 0.03, + }, + { name: 'zero-priced', steps: [modelCallStep('free', 0)], expected: 0 }, + { + name: 'retried logical call', + // Deliberately disagree with the nested attempts: the display boundary + // must trust the logical call's already-aggregated price. + steps: [modelCallStep('retry', 0.04, [modelAttempt(0, 0.01), modelAttempt(1, 0.02)])], + expected: 0.04, + }, + ]; + + for (const { name, steps, expected } of cases) { + const turn = deriveInspectorPanelModel(traceWithSteps(steps)).turns[0]; + assert.equal(turn?.costUsd, expected, name); + assert.equal(turn?.durationMs, 9, `${name} duration`); + } +}); + test('shows one compact diagnostic line for a failed history-compaction call', () => { const trace: SessionTrace = { schemaVersion: SESSION_TRACE_SCHEMA_VERSION, @@ -163,20 +218,8 @@ test('shows one compact diagnostic line for a failed history-compaction call', ( ], }, ], - totals: { - ...emptyTraceTotals(), - durationMs: 9, - modelAttempts: 1, - unpricedAttempts: 1, - }, }, ], - totals: { - ...emptyTraceTotals(), - durationMs: 9, - modelAttempts: 1, - unpricedAttempts: 1, - }, coverage: { modelCalls: 'no_known_gap', turnsMissingModelCalls: [], @@ -201,7 +244,6 @@ test('reports runs omitted only by the bounded online view separately', () => { schemaVersion: SESSION_TRACE_SCHEMA_VERSION, sessionId: 'session-1', turns: [], - totals: emptyTraceTotals(), coverage: { modelCalls: 'partial', turnsMissingModelCalls: [], @@ -219,3 +261,64 @@ test('reports runs omitted only by the bounded online view separately', () => { oversizedRuns: 1, }); }); + +function traceWithSteps(steps: TraceStep[]): SessionTrace { + return { + schemaVersion: SESSION_TRACE_SCHEMA_VERSION, + sessionId: 'session-1', + turns: [ + { + turnId: 'turn-1', + runId: 'run-1', + startedAt: 1, + endedAt: 10, + durationMs: 9, + steps, + }, + ], + coverage: { + modelCalls: 'no_known_gap', + turnsMissingModelCalls: [], + turnsWithFewerModelCallsThanSteps: [], + unreadableRecords: 0, + oversizedRuns: 0, + }, + }; +} + +function modelCallStep( + id: string, + costUsd?: number, + attempts: TraceModelAttempt[] = [modelAttempt(0, costUsd)], +): TraceModelCallStep { + return { + kind: 'model_call', + id, + turnId: 'turn-1', + runId: 'run-1', + startedAt: 1, + endedAt: 10, + durationMs: 9, + callKind: 'main', + providerId: 'provider-1', + modelId: 'model-1', + step: 0, + attempts, + status: 'completed', + ...(costUsd !== undefined ? { costUsd } : {}), + }; +} + +function modelAttempt(attempt: number, costUsd?: number): TraceModelAttempt { + return { + attemptId: `attempt-${attempt}`, + attempt, + status: 'completed', + startedAt: 1, + completedAt: 10, + latencyMs: 9, + ...(costUsd !== undefined ? { costUsd } : {}), + costBasis: costUsd === undefined ? 'unpriced' : 'priced', + usageBasis: 'reported', + }; +} diff --git a/apps/desktop/src/main/__tests__/session-inspector-pricing-key.test.ts b/apps/desktop/src/main/__tests__/session-inspector-pricing-key.test.ts index 49fd37356a..9722ad1efc 100644 --- a/apps/desktop/src/main/__tests__/session-inspector-pricing-key.test.ts +++ b/apps/desktop/src/main/__tests__/session-inspector-pricing-key.test.ts @@ -4,20 +4,9 @@ import { SESSION_TRACE_SCHEMA_VERSION, type SessionTrace, type TraceModelAttempt, - type TraceTotals, } from '@maka/core/session-trace'; import { deriveInspectorPanelModel } from '../../renderer/session-inspector-panel-model.js'; -const TOTALS: TraceTotals = { - durationMs: 10, - modelAttempts: 1, - retries: 0, - compactions: 0, - inputTokens: 1, - outputTokens: 1, - unpricedAttempts: 1, -}; - describe('Session Inspector Pricing key', () => { test('uses the canonical provider rather than the connection slug for an unpriced call', () => { const model = deriveInspectorPanelModel(traceWithAttempt(attempt('unpriced'))); @@ -76,16 +65,8 @@ function traceWithAttempt(modelAttempt: TraceModelAttempt): SessionTrace { ...(modelAttempt.costBasis === 'priced' ? { costUsd: 0.01 } : {}), }, ], - totals: { - ...TOTALS, - ...(modelAttempt.costBasis === 'priced' ? { costUsd: 0.01, unpricedAttempts: 0 } : {}), - }, }, ], - totals: { - ...TOTALS, - ...(modelAttempt.costBasis === 'priced' ? { costUsd: 0.01, unpricedAttempts: 0 } : {}), - }, coverage: { modelCalls: 'no_known_gap', turnsMissingModelCalls: [], diff --git a/apps/desktop/src/main/__tests__/use-session-trace.test.ts b/apps/desktop/src/main/__tests__/use-session-trace.test.ts index 2450fe897d..8bc3a902b5 100644 --- a/apps/desktop/src/main/__tests__/use-session-trace.test.ts +++ b/apps/desktop/src/main/__tests__/use-session-trace.test.ts @@ -3,7 +3,6 @@ import { afterEach, describe, it } from 'node:test'; import { act, createElement } from 'react'; import { SESSION_TRACE_SCHEMA_VERSION, - emptyTraceTotals, type SessionTrace, } from '@maka/core/session-trace'; import type { SessionEvent } from '@maka/core/events'; @@ -30,7 +29,6 @@ function trace(sessionId: string): SessionTrace { schemaVersion: SESSION_TRACE_SCHEMA_VERSION, sessionId, turns: [], - totals: emptyTraceTotals(), coverage: { modelCalls: 'none', turnsMissingModelCalls: [], @@ -197,7 +195,6 @@ function tracePage( endedAt: startedAt, durationMs: 0, steps: [], - totals: emptyTraceTotals(), }, ], }, diff --git a/apps/desktop/src/preload/preload.ts b/apps/desktop/src/preload/preload.ts index e6520dd530..18dba010bd 100644 --- a/apps/desktop/src/preload/preload.ts +++ b/apps/desktop/src/preload/preload.ts @@ -826,7 +826,6 @@ async function loadSessionTracePage( schemaVersion: page.schemaVersion, sessionId, turns: [...page.turns], - totals: page.totals, coverage: page.coverage, }; if (!isSessionTrace(trace)) throw new Error('Invalid Session trace projection'); diff --git a/apps/desktop/src/renderer/session-inspector-panel-model.ts b/apps/desktop/src/renderer/session-inspector-panel-model.ts index c67a0c1979..ef9b72b0e6 100644 --- a/apps/desktop/src/renderer/session-inspector-panel-model.ts +++ b/apps/desktop/src/renderer/session-inspector-panel-model.ts @@ -2,7 +2,6 @@ import { type SessionTrace, type TraceModelCallStep, type TraceStep, - type TraceTotals, } from '@maka/core/session-trace'; import { pricingModelKey } from '@maka/core/usage-stats/pricing'; /** @@ -45,7 +44,7 @@ export interface InspectorTurnRow { turnId: string; startedAt: number; durationMs: number; - totals: TraceTotals; + costUsd?: number; failed: boolean; failureCode?: string; steps: InspectorStepRow[]; @@ -73,16 +72,19 @@ export interface InspectorPanelModel { export function deriveInspectorPanelModel(trace: SessionTrace | undefined): InspectorPanelModel { if (!trace) return { turns: [], empty: true }; - const turns = trace.turns.map((turn) => ({ - runId: turn.runId, - turnId: turn.turnId, - startedAt: turn.startedAt, - durationMs: turn.durationMs, - totals: turn.totals, - failed: turn.failure !== undefined, - ...(turn.failure?.code !== undefined ? { failureCode: turn.failure.code } : {}), - steps: turn.steps.map((step) => toStepRow(step, turn.failure?.attributedToStepId)), - })); + const turns = trace.turns.map((turn) => { + const costUsd = deriveTurnCostUsd(turn.steps); + return { + runId: turn.runId, + turnId: turn.turnId, + startedAt: turn.startedAt, + durationMs: turn.durationMs, + ...(costUsd !== undefined ? { costUsd } : {}), + failed: turn.failure !== undefined, + ...(turn.failure?.code !== undefined ? { failureCode: turn.failure.code } : {}), + steps: turn.steps.map((step) => toStepRow(step, turn.failure?.attributedToStepId)), + }; + }); const coverage = coverageNotice(trace); return { @@ -95,6 +97,16 @@ export function deriveInspectorPanelModel(trace: SessionTrace | undefined): Insp }; } +function deriveTurnCostUsd(steps: readonly TraceStep[]): number | undefined { + let total: number | undefined; + for (const step of steps) { + if (step.kind === 'model_call' && step.costUsd !== undefined) { + total = (total ?? 0) + step.costUsd; + } + } + return total; +} + function toStepRow(step: TraceStep, attributedToStepId: string | undefined): InspectorStepRow { const failed = step.id === attributedToStepId || diff --git a/apps/desktop/src/renderer/session-inspector-panel.tsx b/apps/desktop/src/renderer/session-inspector-panel.tsx index cc93642658..b45382d425 100644 --- a/apps/desktop/src/renderer/session-inspector-panel.tsx +++ b/apps/desktop/src/renderer/session-inspector-panel.tsx @@ -631,7 +631,7 @@ function TurnRow(props: { {formatDuration(turn.durationMs)} ยท{' '} - {formatCost(turn.totals.costUsd, copy.costUnavailable)} + {formatCost(turn.costUsd, copy.costUnavailable)} diff --git a/apps/desktop/stories/session-workbar.stories.tsx b/apps/desktop/stories/session-workbar.stories.tsx index 1fe613489e..4c27b2e6a7 100644 --- a/apps/desktop/stories/session-workbar.stories.tsx +++ b/apps/desktop/stories/session-workbar.stories.tsx @@ -320,16 +320,6 @@ const populatedTrace: SessionTrace = { checkpointId: 'checkpoint-9', }, ], - totals: { - durationMs: 8_400, - modelAttempts: 2, - retries: 1, - compactions: 1, - inputTokens: 62_400, - outputTokens: 480, - costUsd: 0.0182, - unpricedAttempts: 1, - }, }, { turnId: 'turn-2', @@ -359,15 +349,6 @@ const populatedTrace: SessionTrace = { message: 'sandbox denied the write', }, ], - totals: { - durationMs: 4_500, - modelAttempts: 0, - retries: 0, - compactions: 0, - inputTokens: 0, - outputTokens: 0, - unpricedAttempts: 0, - }, failure: { code: 'tool_failed', message: 'sandbox denied the write' }, }, { @@ -411,28 +392,8 @@ const populatedTrace: SessionTrace = { ], }, ], - totals: { - durationMs: 3_600, - modelAttempts: 1, - retries: 0, - compactions: 0, - inputTokens: 18_900, - outputTokens: 260, - costUsd: 0.0061, - unpricedAttempts: 0, - }, }, ], - totals: { - durationMs: 16_500, - modelAttempts: 3, - retries: 1, - compactions: 1, - inputTokens: 81_300, - outputTokens: 740, - costUsd: 0.0243, - unpricedAttempts: 1, - }, coverage: { modelCalls: 'partial', turnsMissingModelCalls: [{ runId: 'run-2', turnId: 'turn-2' }], @@ -511,15 +472,6 @@ const emptyTrace: SessionTrace = { schemaVersion: 1, sessionId: SESSION_ID, turns: [], - totals: { - durationMs: 0, - modelAttempts: 0, - retries: 0, - compactions: 0, - inputTokens: 0, - outputTokens: 0, - unpricedAttempts: 0, - }, coverage: { modelCalls: 'none', turnsMissingModelCalls: [], @@ -539,7 +491,6 @@ const olderTrace: SessionTrace = { endedAt: NOW - 60_000, durationMs: 0, steps: [], - totals: { ...emptyTrace.totals }, }, ], }; diff --git a/packages/core/src/__tests__/session-trace.test.ts b/packages/core/src/__tests__/session-trace.test.ts index a1133be49d..429e7ce3b8 100644 --- a/packages/core/src/__tests__/session-trace.test.ts +++ b/packages/core/src/__tests__/session-trace.test.ts @@ -2,7 +2,6 @@ import assert from 'node:assert/strict'; import { test } from 'node:test'; import { SESSION_TRACE_SCHEMA_VERSION, - emptyTraceTotals, mergeDisjointTraceCoverage, mergeSessionTraces, type SessionTrace, @@ -44,8 +43,8 @@ test('coverage merge distinguishes backend absence from a gap in only some pages ); }); -test('page merge orders and deduplicates turns while recomputing totals', () => { - const page = (runId: string, startedAt: number, inputTokens: number): SessionTrace => ({ +test('page merge orders and deduplicates turns', () => { + const page = (runId: string, startedAt: number): SessionTrace => ({ schemaVersion: SESSION_TRACE_SCHEMA_VERSION, sessionId: 'session-1', turns: [ @@ -56,23 +55,16 @@ test('page merge orders and deduplicates turns while recomputing totals', () => endedAt: startedAt, durationMs: 0, steps: [], - totals: { ...emptyTraceTotals(), inputTokens }, }, ], - totals: { ...emptyTraceTotals(), inputTokens }, coverage: coverage('no_known_gap'), }); - const merged = mergeSessionTraces([ - page('run-2', 2, 2), - page('run-1', 1, 1), - page('run-2', 2, 2), - ]); + const merged = mergeSessionTraces([page('run-2', 2), page('run-1', 1), page('run-2', 2)]); assert.deepEqual( merged.turns.map((turn) => turn.runId), ['run-1', 'run-2'], ); - assert.equal(merged.totals.inputTokens, 3); }); test('page merge has a stable order when run timestamps and identities tie', () => { @@ -87,10 +79,8 @@ test('page merge has a stable order when run timestamps and identities tie', () endedAt: 1, durationMs: 0, steps: [], - totals: emptyTraceTotals(), }, ], - totals: emptyTraceTotals(), coverage: coverage('no_known_gap'), }); diff --git a/packages/core/src/session-trace.ts b/packages/core/src/session-trace.ts index 35600e75e6..310b69d6ab 100644 --- a/packages/core/src/session-trace.ts +++ b/packages/core/src/session-trace.ts @@ -202,27 +202,6 @@ export interface TraceFailureAttribution { attributedToStepId?: string; } -export interface TraceTotals { - durationMs: number; - /** Physical provider requests, retries included. */ - modelAttempts: number; - /** Attempts beyond the first of their logical call. */ - retries: number; - compactions: number; - inputTokens: number; - outputTokens: number; - /** - * Summed over priced records only, and absent when none were priced. - * - * Same authority as Settings โ†’ Usage, read at a different scope: both sum - * `ModelCallAttempt`. A total here that disagreed with that surface would be - * a bug in one of them, not two defensible numbers (#1679). - */ - costUsd?: number; - /** Records that carried no price, and are therefore absent from `costUsd`. */ - unpricedAttempts: number; -} - export interface TurnTrace { turnId: string; runId: string; @@ -230,7 +209,6 @@ export interface TurnTrace { endedAt: number; durationMs: number; steps: TraceStep[]; - totals: TraceTotals; failure?: TraceFailureAttribution; } @@ -290,12 +268,11 @@ export interface SessionTrace { schemaVersion: typeof SESSION_TRACE_SCHEMA_VERSION; sessionId: string; turns: TurnTrace[]; - totals: TraceTotals; coverage: SessionTraceCoverage; } const SESSION_TRACE_SHAPE = defineObjectShape()( - ['schemaVersion', 'sessionId', 'turns', 'totals', 'coverage'], + ['schemaVersion', 'sessionId', 'turns', 'coverage'], [], ); const TRACE_COVERAGE_SHAPE = defineObjectShape()( @@ -308,20 +285,8 @@ const TRACE_COVERAGE_SHAPE = defineObjectShape()( ], [], ); -const TRACE_TOTALS_SHAPE = defineObjectShape()( - [ - 'durationMs', - 'modelAttempts', - 'retries', - 'compactions', - 'inputTokens', - 'outputTokens', - 'unpricedAttempts', - ], - ['costUsd'], -); const TURN_TRACE_SHAPE = defineObjectShape()( - ['turnId', 'runId', 'startedAt', 'endedAt', 'durationMs', 'steps', 'totals'], + ['turnId', 'runId', 'startedAt', 'endedAt', 'durationMs', 'steps'], ['failure'], ); const TRACE_FAILURE_SHAPE = defineObjectShape()( @@ -402,7 +367,6 @@ export function isSessionTrace(value: unknown): value is SessionTrace { typeof value.sessionId === 'string' && Array.isArray(value.turns) && value.turns.every(isTurnTrace) && - isTraceTotals(value.totals) && isTraceCoverage(value.coverage) ); } @@ -418,28 +382,10 @@ export function isTurnTrace(value: unknown): value is TurnTrace { isNonnegativeNumber(value.durationMs) && Array.isArray(value.steps) && value.steps.every(isTraceStep) && - isTraceTotals(value.totals) && (value.failure === undefined || isTraceFailure(value.failure)) ); } -function isTraceTotals(value: unknown): value is TraceTotals { - return ( - isRecord(value) && - hasExactShape(value, TRACE_TOTALS_SHAPE) && - [ - value.durationMs, - value.modelAttempts, - value.retries, - value.compactions, - value.inputTokens, - value.outputTokens, - value.unpricedAttempts, - ].every(isNonnegativeNumber) && - isOptionalNonnegativeNumber(value.costUsd) - ); -} - function isTraceCoverage(value: unknown): value is SessionTraceCoverage { return ( isRecord(value) && @@ -595,42 +541,6 @@ function isOptionalNonnegativeNumber(value: unknown): boolean { return value === undefined || isNonnegativeNumber(value); } -/** Empty totals, so callers fold rather than special-case the first element. */ -export function emptyTraceTotals(): TraceTotals { - return { - durationMs: 0, - modelAttempts: 0, - retries: 0, - compactions: 0, - inputTokens: 0, - outputTokens: 0, - unpricedAttempts: 0, - }; -} - -/** - * Folds one set of totals into another. - * - * `costUsd` stays absent until something priced arrives, so a session of - * entirely unpriced calls totals to "no price", not to zero. - */ -export function mergeTraceTotals(base: TraceTotals, next: TraceTotals): TraceTotals { - const costUsd = - base.costUsd === undefined && next.costUsd === undefined - ? undefined - : (base.costUsd ?? 0) + (next.costUsd ?? 0); - return { - durationMs: base.durationMs + next.durationMs, - modelAttempts: base.modelAttempts + next.modelAttempts, - retries: base.retries + next.retries, - compactions: base.compactions + next.compactions, - inputTokens: base.inputTokens + next.inputTokens, - outputTokens: base.outputTokens + next.outputTokens, - unpricedAttempts: base.unpricedAttempts + next.unpricedAttempts, - ...(costUsd !== undefined ? { costUsd } : {}), - }; -} - /** * Combines coverage from disjoint trace partitions. * @@ -692,10 +602,6 @@ export function mergeSessionTraces(traces: readonly SessionTrace[]): SessionTrac schemaVersion: current.schemaVersion, sessionId: current.sessionId, turns: ordered, - totals: ordered.reduce( - (total, turn) => mergeTraceTotals(total, turn.totals), - emptyTraceTotals(), - ), coverage: mergeDisjointTraceCoverage(current.coverage, page.coverage), }; }, first); diff --git a/packages/runtime-host/src/__tests__/execution-inspect-protocol.test.ts b/packages/runtime-host/src/__tests__/execution-inspect-protocol.test.ts index de73bdf1eb..1df6c43e17 100644 --- a/packages/runtime-host/src/__tests__/execution-inspect-protocol.test.ts +++ b/packages/runtime-host/src/__tests__/execution-inspect-protocol.test.ts @@ -76,6 +76,47 @@ describe('execution inspect protocol', () => { result: { kind: 'turn_trace', sessionId: 'session-1', turn: turnTrace() }, }, ); + assert.deepEqual( + decodeHostFrame({ + requestId: 'request-trace-page', + operation: 'execution.inspect.query', + ok: true, + result: tracePage(), + }), + { + requestId: 'request-trace-page', + operation: 'execution.inspect.query', + ok: true, + result: tracePage(), + }, + ); + }); + + test('rejects legacy TraceTotals on Session pages and Turn traces', () => { + assert.throws( + () => + decodeHostFrame({ + requestId: 'request-legacy-page-totals', + operation: 'execution.inspect.query', + ok: true, + result: { ...tracePage(), totals: legacyTraceTotals() }, + }), + isProtocolError, + ); + assert.throws( + () => + decodeHostFrame({ + requestId: 'request-legacy-turn-totals', + operation: 'execution.inspect.query', + ok: true, + result: { + kind: 'turn_trace', + sessionId: 'session-1', + turn: { ...turnTrace(), totals: legacyTraceTotals() }, + }, + }), + isProtocolError, + ); }); test('rejects open shapes, inconsistent resolution, and oversized payloads', () => { @@ -218,15 +259,6 @@ function tracePage() { schemaVersion: 1 as const, sessionId: 'session-1', turns: [], - totals: { - durationMs: 0, - modelAttempts: 0, - retries: 0, - compactions: 0, - inputTokens: 0, - outputTokens: 0, - unpricedAttempts: 0, - }, coverage: { modelCalls: 'none' as const, turnsMissingModelCalls: [], @@ -246,15 +278,18 @@ function turnTrace(turnId = 'turn-1') { endedAt: 2, durationMs: 1, steps: [], - totals: { - durationMs: 1, - modelAttempts: 0, - retries: 0, - compactions: 0, - inputTokens: 0, - outputTokens: 0, - unpricedAttempts: 0, - }, + }; +} + +function legacyTraceTotals() { + return { + durationMs: 0, + modelAttempts: 0, + retries: 0, + compactions: 0, + inputTokens: 0, + outputTokens: 0, + unpricedAttempts: 0, }; } diff --git a/packages/runtime-host/src/__tests__/protocol.test.ts b/packages/runtime-host/src/__tests__/protocol.test.ts index 46349d1a8b..6f23d9ff8e 100644 --- a/packages/runtime-host/src/__tests__/protocol.test.ts +++ b/packages/runtime-host/src/__tests__/protocol.test.ts @@ -107,7 +107,11 @@ describe('Runtime Host bootstrap protocol', () => { }); test('publishes a new compatibility epoch for Session trace pagination', () => { - assert.equal(RUNTIME_HOST_COMPATIBILITY_EPOCH, 35); + assert.ok(RUNTIME_HOST_COMPATIBILITY_EPOCH > 34); + }); + + test('publishes a new compatibility epoch for TraceTotals removal', () => { + assert.equal(RUNTIME_HOST_COMPATIBILITY_EPOCH, 36); }); test('selects the highest mutually supported protocol and rejects a gap', () => { diff --git a/packages/runtime-host/src/protocol/execution-inspect.ts b/packages/runtime-host/src/protocol/execution-inspect.ts index a413003f35..2aefb29ec6 100644 --- a/packages/runtime-host/src/protocol/execution-inspect.ts +++ b/packages/runtime-host/src/protocol/execution-inspect.ts @@ -9,7 +9,6 @@ import { isTurnTrace, SESSION_TRACE_SCHEMA_VERSION, type SessionTraceCoverage, - type TraceTotals, type TurnTrace, } from '@maka/core/session-trace'; import { @@ -85,7 +84,6 @@ export type ExecutionInspectQueryResult = readonly schemaVersion: typeof SESSION_TRACE_SCHEMA_VERSION; readonly sessionId: string; readonly turns: readonly TurnTrace[]; - readonly totals: TraceTotals; readonly coverage: SessionTraceCoverage; readonly nextCursor: string | null; }; @@ -233,7 +231,7 @@ export function decodeExecutionInspectQueryResult(value: unknown): ExecutionInsp value, 'execution.inspect.query result', ['kind'], - ['document', 'schemaVersion', 'sessionId', 'turns', 'totals', 'coverage', 'nextCursor', 'turn'], + ['document', 'schemaVersion', 'sessionId', 'turns', 'coverage', 'nextCursor', 'turn'], ); if (shaped.kind === 'turn_trace') { const record = requireExactRecord(shaped, 'Turn trace result', ['kind', 'sessionId', 'turn']); @@ -249,7 +247,6 @@ export function decodeExecutionInspectQueryResult(value: unknown): ExecutionInsp 'schemaVersion', 'sessionId', 'turns', - 'totals', 'coverage', 'nextCursor', ]); @@ -258,7 +255,6 @@ export function decodeExecutionInspectQueryResult(value: unknown): ExecutionInsp schemaVersion: record.schemaVersion, sessionId, turns: record.turns, - totals: record.totals, coverage: record.coverage, }; if ( @@ -274,7 +270,6 @@ export function decodeExecutionInspectQueryResult(value: unknown): ExecutionInsp schemaVersion: SESSION_TRACE_SCHEMA_VERSION, sessionId, turns: decodedTrace.turns, - totals: decodedTrace.totals, coverage: decodedTrace.coverage, nextCursor, }; diff --git a/packages/runtime-host/src/protocol/index.ts b/packages/runtime-host/src/protocol/index.ts index d8c3d75a78..8391aeb26c 100644 --- a/packages/runtime-host/src/protocol/index.ts +++ b/packages/runtime-host/src/protocol/index.ts @@ -72,7 +72,8 @@ export const RUNTIME_HOST_REGISTRATION_SCHEMA_VERSION = 1 as const; export const RUNTIME_HOST_PROTOCOL_VERSION = 0 as const; // Increment when the same protocol version no longer guarantees safe Client-Host // interoperability. Mismatches are rejected before domain commands are admitted. -export const RUNTIME_HOST_COMPATIBILITY_EPOCH = 35 as const; +export const RUNTIME_HOST_COMPATIBILITY_EPOCH = 36 as const; +// 36: Session trace inspection no longer transports aggregate TraceTotals. // 35: Session trace inspection uses cursor pages and Session usage has its own // invalidation domain. Older peers cannot safely exchange those frames. // 34: ScheduledTask execution templates no longer emit `backend`. Epoch-33 diff --git a/packages/runtime/src/__tests__/session-trace-projection.test.ts b/packages/runtime/src/__tests__/session-trace-projection.test.ts index f534990ce4..5a97bf1d73 100644 --- a/packages/runtime/src/__tests__/session-trace-projection.test.ts +++ b/packages/runtime/src/__tests__/session-trace-projection.test.ts @@ -126,16 +126,41 @@ describe('session trace projection', () => { assert.equal(isSessionTrace(trace), true, 'the Host protocol accepts the projected trace'); }); - test('a session of entirely unpriced calls totals to no price, not to zero', () => { + test('an entirely unpriced logical call keeps its step cost absent', () => { const trace = projectSessionTrace({ sessionId: 'session-1', runtimeEvents: [], modelCallAttempts: [attempt({ costUsd: undefined, costBasis: 'unpriced' })], }); - assert.equal(trace.totals.costUsd, undefined, 'absent price is not a zero price'); - assert.equal(trace.totals.unpricedAttempts, 1); - assert.equal(trace.turns[0]?.steps[0]?.kind, 'model_call'); + const call = trace.turns[0]?.steps[0]; + assert.equal(call?.kind, 'model_call'); + if (call?.kind !== 'model_call') return; + assert.equal(call.costUsd, undefined, 'absent price is not a zero price'); + assert.equal(call.attempts[0]?.costBasis, 'unpriced'); + }); + + test('a logical call sums its priced retry attempts and ignores unpriced ones', () => { + const trace = projectSessionTrace({ + sessionId: 'session-1', + runtimeEvents: [], + modelCallAttempts: [ + attempt({ attemptId: 'attempt-0', attempt: 0, costUsd: 0.001 }), + attempt({ + attemptId: 'attempt-1', + attempt: 1, + costUsd: undefined, + costBasis: 'unpriced', + }), + attempt({ attemptId: 'attempt-2', attempt: 2, costUsd: 0.002 }), + ], + }); + + const call = trace.turns[0]?.steps[0]; + assert.equal(call?.kind, 'model_call'); + if (call?.kind !== 'model_call') return; + assert.equal(call.attempts.length, 3); + assert.equal(call.costUsd, 0.003); }); test('attributes a turn failure to what failed first, not to the terminal error', () => { @@ -202,8 +227,6 @@ describe('session trace projection', () => { assert.equal(trace.coverage.modelCalls, 'absent'); assert.deepEqual(trace.coverage.turnsMissingModelCalls, [{ runId: 'run-1', turnId: 'turn-1' }]); - assert.equal(trace.totals.modelAttempts, 0); - assert.equal(trace.totals.costUsd, undefined); }); test('known unreadable evidence makes an otherwise absent backend partial', () => { @@ -280,9 +303,8 @@ describe('session trace projection', () => { if (call.kind !== 'model_call') return; assert.equal(call.attempts.length, 1, 'one attempt id is one attempt'); assert.equal(call.status, 'completed', 'the later settlement wins'); - assert.equal(trace.totals.retries, 0); - assert.equal(trace.totals.costUsd, 0.002, 'not double-counted against Settings โ†’ Usage'); - assert.equal(trace.totals.unpricedAttempts, 0); + assert.equal(call.costUsd, 0.002, 'not double-counted against Settings โ†’ Usage'); + assert.equal(call.attempts[0]?.costBasis, 'priced'); }); test('reports a shortfall when usage stands for more steps than there are calls', () => { diff --git a/packages/runtime/src/session-trace-projection.ts b/packages/runtime/src/session-trace-projection.ts index b0b030b3ed..4be77cb8f1 100644 --- a/packages/runtime/src/session-trace-projection.ts +++ b/packages/runtime/src/session-trace-projection.ts @@ -5,8 +5,6 @@ import { } from '@maka/core/model-call-attempt'; import { TERMINAL_RUNTIME_EVENT_STATUSES, type RuntimeEvent } from '@maka/core/runtime-event'; import { - emptyTraceTotals, - mergeTraceTotals, SESSION_TRACE_SCHEMA_VERSION, traceTurnIdentityKey, type SessionTrace, @@ -16,7 +14,6 @@ import { type TraceModelAttempt, type TraceModelCallStep, type TraceStep, - type TraceTotals, type TurnTrace, } from '@maka/core/session-trace'; @@ -84,16 +81,10 @@ export function projectSessionTrace(input: SessionTraceInput): SessionTrace { } } - const totals = turns.reduce( - (carry, turn) => mergeTraceTotals(carry, turn.totals), - emptyTraceTotals(), - ); - return { schemaVersion: SESSION_TRACE_SCHEMA_VERSION, sessionId: input.sessionId, turns, - totals, coverage: resolveCoverage( turnsWithModelActivity, turnsMissingModelCalls, @@ -200,7 +191,6 @@ function projectTurn( ]; const startedAt = Math.min(...instants); const endedAt = Math.max(...instants); - const totals = turnTotals(steps, endedAt - startedAt); const failure = attributeTurnFailure(steps, events); return { @@ -209,7 +199,6 @@ function projectTurn( endedAt, durationMs: Math.max(0, endedAt - startedAt), steps, - totals, ...(failure ? { failure } : {}), }; } @@ -450,29 +439,6 @@ export function attributeTurnFailure( }; } -function turnTotals(steps: readonly TraceStep[], durationMs: number): TraceTotals { - const totals = emptyTraceTotals(); - totals.durationMs = Math.max(0, durationMs); - - for (const step of steps) { - if (step.kind === 'model_call') { - totals.modelAttempts += step.attempts.length; - totals.retries += Math.max(0, step.attempts.length - 1); - if (step.callKind === 'history_compact' || step.callKind === 'semantic_compact') { - totals.compactions += 1; - } - for (const attempt of step.attempts) { - totals.inputTokens += attempt.inputTokens ?? 0; - totals.outputTokens += attempt.outputTokens ?? 0; - if (attempt.costUsd === undefined) totals.unpricedAttempts += 1; - } - if (step.costUsd !== undefined) totals.costUsd = (totals.costUsd ?? 0) + step.costUsd; - } - } - - return totals; -} - function stepEndedAt(step: TraceStep): number { if (step.kind === 'model_call') return step.endedAt; if (step.kind === 'tool') return step.endedAt ?? step.startedAt;