/** * Session statistics: turns/steps, LLM and tool time, TTFT, decode rate, and * token usage. * * `tui.ts` owns the rendering; this owns the arithmetic. The rules that used to * live among the event handlers are what make the footer honest, so they live * here with tests instead of inside a 7k-line class: * * - a repeated usage report for the same step replaces its sample (hosts report * usage per step more than once, and naive adding double counts), * - TTFT latches on the *first* token delta of the open step, * - the settled `assistant/message` packed stream wins over that latch (a * retried step would otherwise span both attempts and report a rate ~20x off), * - decode rate needs a finite, non-negative output token count. */ import type { TokenUsage } from '@deepseek-ai/dsh-llm'; import { type ThroughputView } from './throughput.js'; export interface SessionUsage { inputTokens: number; outputTokens: number; /** Sum of the full-call totals the harness itself reported, when it reported one. */ reportedTokens: number; /** Steps whose usage carried a harness total. */ reportedSteps: number; /** Steps whose usage carried none, so the billed parts had to stand in. */ unreportedSteps: number; cacheReadTokens: number; cacheWriteTokens: number; } /** The session-size number the second row shows, and how it was derived. */ export interface SessionTokenTotal { tokens: number; /** * `harness` when every usage sample carried the harness's own total; `sum` * when some step's billed parts had to be added in instead. */ basis: 'harness' | 'sum'; } /** * How many tokens this session has moved, on the accounting the harness * published. * * The harness is the authority: a `totalTokens` it reports is a full-call total * whose parts are defined provider by provider — pi-ai's own adapters build it * as prompt + output + cache read + cache write — and re-deriving one from the * three prompt counters plus output is exactly the arithmetic that * double-counts on a provider that folds cache into its prompt count. So * reported totals are summed, and only a step that reported none contributes * its billed parts. The basis says which happened, and `/status` prints it. */ export declare function sessionTokenTotal(usage: SessionUsage): SessionTokenTotal | undefined; export interface SessionStatsSnapshot { turns: number; steps: number; llmMs: number; toolMs: number; ttftMs: number; ttftSteps: number; decodeMs: number; decodeTokens: number; /** Tokens and milliseconds of the most recent settled step alone. */ lastDecodeMs: number; lastDecodeTokens: number; /** Decode totals excluding the step that is currently open. */ settledDecodeMs: number; settledDecodeTokens: number; usage: SessionUsage; } /** Flat view of the same numbers, for the footer's stats line. */ export interface SessionStatsRow { turns: number; steps: number; llmMs: number; toolMs: number; ttftMs: number; ttftSteps: number; decodeMs: number; decodeTokens: number; inputTokens: number; outputTokens: number; cacheReadTokens: number; cacheWriteTokens: number; totalTokens: number; } export interface OpenStep { turn: number; step: number; } export declare function emptySessionUsage(): SessionUsage; export declare function emptySessionStats(): SessionStatsSnapshot; /** Flatten a snapshot into the legacy stats-row shape. */ export declare function statsRowOf(stats: SessionStatsSnapshot): SessionStatsRow; /** * Accumulates the stats the footer and `/status` show as session events arrive. * Every method is a no-op for events that do not apply, so callers can forward * the raw event stream without checking. */ export declare class SessionStatsTracker { private stats; private openStep; private readonly usageByStep; private readonly pendingToolTimes; private lastTurn; /** Live decode estimate for the step currently streaming. */ private readonly live; /** The step a live chunk belongs to when the host frame carried none. */ currentStep(): OpenStep | undefined; /** * The open step's two clocks, for the status row's elapsed suffix. * * Both are host wall clocks, so a caller may subtract them from `Date.now()` * without a translation. `firstTokenAt` is absent until the step's first * delta, which is exactly the difference between "the request is out" and * "the model is talking". */ stepClocks(): { startedAt?: number; firstTokenAt?: number; }; /** `step/start`: opens the clock LLM time, TTFT and decode are measured from. */ noteStepStart(turn: number, step: number, time: number): void; /** * A live fragment of model output, for the running throughput estimate. * * The text itself travels: the estimate is tokens per character, and how many * tokens a character is worth depends on the script the fragment is written in. * @param text - the fragment. * @param time - wall clock the fragment arrived at. */ noteDelta(text: string, time?: number): void; /** The running throughput estimate, settled value included. */ throughput(now?: number): ThroughputView; /** The first token delta of the open step latches its TTFT clock. */ noteFirstToken(turn: number, step: number, time: number): void; /** Replace one step's usage sample so a repeated report never double counts. */ recordUsage(turn: number, step: number, usage: TokenUsage): void; /** * `assistant/message`: settle the open step's LLM time, TTFT and decode rate. * `firstTokenTime` is the settlement's own packed-stream answer when it has * one; `undefined` falls back to the live latch. */ settleMessage(input: { turn: number; step: number; time: number; firstTokenTime?: number | undefined; outputTokens?: number | undefined; }): void; /** `tool/call`: start the tool clock for this call. */ noteToolStart(callId: string, time: number): void; /** When a still-open tool call started, for the status row's elapsed suffix. */ toolStartedAt(callId: string): number | undefined; /** `tool/result`: add the elapsed tool time, once. */ noteToolEnd(callId: string, time: number): void; /** `step/end`: count the step, and forget the step's usage dedupe key. */ noteStepEnd(turn: number, step: number): void; /** `turn/end`: a tool whose result never arrived contributes nothing. */ noteTurnEnd(): void; /** Current totals. A copy: callers cannot disturb the accumulator. */ snapshot(): SessionStatsSnapshot; }