mirror of
https://github.com/deepseek-ai/deepseek-harness.git
synced 2026-09-14 04:01:35 +00:00
Session format v2 embeds each attempt's compact stream in assistant/message and assistant/attempt, but Host and client consumers still expanded it into per-member TimedStreamChunk arrays and did per-member work; expandAssistantStream materializes the full array before find/toReversed/break can answer. Session Stats (the projection phase of every Session open), the token meter's usage and provider-assembly folds, the subagent output fold, and the Session Controller image lookup still paid O(members) allocation and time per settlement. The Chat and Trajectory definitions were already settled from message.content on master; the remaining per-member folds stay. dsh-llm now exports record-level readers (first token, visible content, visible text, last raw chunk of a type, raw chunks of a type, joined text, run-aware assembly, per-run first-token/first-visible times) that scan the compact records once with early exit. Session Stats reads assistantStreamFirstTokenTime, the token meter reads lastAssistantStreamChunk(stream, 'usage') and assembles through assembleAssistantStream, the subagent output fold appends joinAssistantStreamText, and the Session Controller scans assistantStreamChunks(stream, 'block-end'). expandAssistantStream is deliberately not memoized: retaining expansions costs roughly ten times the compact stream for the Session's lifetime. It remains the validating path at durable boundaries. Synthetic 200-turn v0 migration benchmark, median of five: first-open projection 28.0 ms -> 5.4 ms, first-open total 76.9 -> 50.0 ms, peak RSS 137.2 -> 94.9 MB; reopen projection 17.8 -> 5.6 ms; all phase budgets and the 128 MB heap constraint keep passing.
213 lines
8.6 KiB
TypeScript
213 lines
8.6 KiB
TypeScript
/**
|
|
* The `sessionStats` projection unit: a pure fold of step boundaries, stream
|
|
* embedded streams, tool pairs, and assembled assistant messages into whole-log counts
|
|
* and wall times.
|
|
*
|
|
* `step/end` — not `assistant/message` — is the counted step event because it
|
|
* is the step lifecycle authority: the loop appends exactly one per entered
|
|
* step, in a `finally`, so completed, failed, cancelled, and max-tokens steps
|
|
* all land one. Counting assembled assistant messages instead would overcount
|
|
* max-tokens usage-host messages (empty content, excluded from the surface)
|
|
* and undercount cancelled steps (aborted before the message assembles).
|
|
*
|
|
* The wall-time folds mirror the client window fold field by field
|
|
* (`deriveStats` in dsh-client-ui-conversation, that fold's whole-window
|
|
* fallback role): model time is `step/start` → `assistant/message`, first
|
|
* token is the first non-empty delta chunk and survives an in-step
|
|
* `llm/retry`, decode spans first token → assembled message on steps that
|
|
* also report output tokens, and tool time pairs `tool/call` → `tool/result`
|
|
* by callId. A cancelled step assembles no message, so its partial stream
|
|
* time stays uncounted in every time figure — matching the window, which
|
|
* renders it as an untimed interrupted node.
|
|
*
|
|
* @module @deepseek-ai/dsh-session-stats/projection
|
|
*/
|
|
|
|
import { z } from 'zod'
|
|
import { assistantStreamFirstTokenTime } from '@deepseek-ai/dsh-llm'
|
|
import type { ProjectionDefinition } from '@deepseek-ai/dsh-session-projection'
|
|
|
|
|
|
/** Accumulated whole-log figures (the view is exactly these totals). */
|
|
interface SessionStatsTotals {
|
|
/** Distinct turns with at least one closed step so far. */
|
|
turns: number
|
|
/** Closed steps so far. */
|
|
steps: number
|
|
/** Summed model wall time over message-assembling steps, ms. */
|
|
llmMs: number
|
|
/** Summed matched tool call→result wall time, ms. */
|
|
toolMs: number
|
|
/** Summed first-token latency over `ttftSteps`, ms. */
|
|
ttftMs: number
|
|
/** Steps carrying a recorded first token. */
|
|
ttftSteps: number
|
|
/** Summed decode wall time over usage-reporting steps, ms. */
|
|
decodeMs: number
|
|
/** Summed provider output tokens over the same steps. */
|
|
decodeTokens: number
|
|
}
|
|
|
|
/**
|
|
* Fold state: the totals plus the in-flight boundaries they accrue from.
|
|
* Turn numbers are host-assigned and monotonic per session, so a single
|
|
* `lastTurn` slot decides "first closed step of a new turn"; the state is
|
|
* plain JSON per the unit contract (persisted-cache precondition).
|
|
*/
|
|
interface SessionStatsState extends SessionStatsTotals {
|
|
/** Turn of the last counted `step/end`; null before the first. */
|
|
lastTurn: number | null
|
|
/** The open step's boundary facts; null outside a step or after its message assembled. */
|
|
openStep: { turn: number; step: number; startTime: number; firstTokenTime: number | null } | null
|
|
/** Dispatch times of tool calls whose result has not landed, by callId. */
|
|
pendingCalls: Record<string, number>
|
|
}
|
|
|
|
declare module '@deepseek-ai/dsh-session-projection/types' {
|
|
interface SessionProjectionStateMap {
|
|
sessionStats: SessionStatsState
|
|
}
|
|
}
|
|
|
|
const sessionStatsSchema = z.object({
|
|
turns: z.number().int().nonnegative(),
|
|
steps: z.number().int().nonnegative(),
|
|
llmMs: z.number().nonnegative(),
|
|
toolMs: z.number().nonnegative(),
|
|
ttftMs: z.number().nonnegative(),
|
|
ttftSteps: z.number().int().nonnegative(),
|
|
decodeMs: z.number().nonnegative(),
|
|
decodeTokens: z.number().nonnegative(),
|
|
}).strict()
|
|
|
|
/**
|
|
* The fold state's shape (totals plus in-flight boundaries), validated on
|
|
* persisted-cache rows after their `ver` gate — the unit's input boundary.
|
|
* The view is a strict subset of the state, so this schema extends
|
|
* `sessionStatsSchema` (the wire output boundary) with the boundary fields.
|
|
*/
|
|
const sessionStatsStateSchema = sessionStatsSchema.extend({
|
|
lastTurn: z.number().int().nonnegative().nullable(),
|
|
openStep: z.object({
|
|
turn: z.number().int().nonnegative(),
|
|
step: z.number().int().nonnegative(),
|
|
startTime: z.number().nonnegative(),
|
|
firstTokenTime: z.number().nonnegative().nullable(),
|
|
}).nullable(),
|
|
pendingCalls: z.record(z.string(), z.number().nonnegative()),
|
|
})
|
|
|
|
/**
|
|
* Provider-reported completion tokens, guarded the way the window fold guards
|
|
* node usage.
|
|
* @param usage - the assistant/message event's optional usage record.
|
|
* @returns the output-token count, or null when unreported or invalid.
|
|
*/
|
|
function usageOutputTokens(usage: unknown): number | null {
|
|
if (typeof usage !== 'object' || usage === null) return null
|
|
const value = (usage as { outputTokens?: unknown }).outputTokens
|
|
return typeof value === 'number' && Number.isFinite(value) && value >= 0 ? value : null
|
|
}
|
|
|
|
/** The `sessionStats` unit registered on `ctx.sessionProjections` (exported for the unit spec). */
|
|
export const sessionStatsProjectionDefinition = {
|
|
key: 'sessionStats',
|
|
stateVersion: 1,
|
|
stateSchema: sessionStatsStateSchema,
|
|
init: () => ({
|
|
turns: 0,
|
|
steps: 0,
|
|
llmMs: 0,
|
|
toolMs: 0,
|
|
ttftMs: 0,
|
|
ttftSteps: 0,
|
|
decodeMs: 0,
|
|
decodeTokens: 0,
|
|
lastTurn: null,
|
|
openStep: null,
|
|
pendingCalls: {},
|
|
}),
|
|
apply: (state, event) => {
|
|
// Every uninteresting event returns the same reference (Object.is gates the change feed).
|
|
switch (event.type) {
|
|
case 'step/start':
|
|
return {
|
|
...state,
|
|
openStep: { turn: event.data.turn, step: event.data.step, startTime: event.time, firstTokenTime: null },
|
|
}
|
|
case 'assistant/attempt': {
|
|
const open = state.openStep
|
|
if (open === null || open.turn !== event.data.turn || open.step !== event.data.step) return state
|
|
const first = assistantStreamFirstTokenTime(event.data.stream) ?? null
|
|
if (open.firstTokenTime !== null || first === null) return state
|
|
return { ...state, openStep: { ...open, firstTokenTime: first } }
|
|
}
|
|
case 'assistant/message': {
|
|
const open = state.openStep
|
|
if (open === null || open.turn !== event.data.turn || open.step !== event.data.step) return state
|
|
const firstToken = open.firstTokenTime ?? assistantStreamFirstTokenTime(event.data.stream) ?? null
|
|
// One assembled message per step: closing the boundary means a
|
|
// defensive duplicate cannot accrue twice.
|
|
const next: SessionStatsState = {
|
|
...state,
|
|
llmMs: state.llmMs + Math.max(0, event.time - open.startTime),
|
|
openStep: null,
|
|
}
|
|
if (firstToken !== null) {
|
|
next.ttftMs += Math.max(0, firstToken - open.startTime)
|
|
next.ttftSteps += 1
|
|
const outputTokens = usageOutputTokens(event.data.usage)
|
|
if (outputTokens !== null) {
|
|
next.decodeMs += Math.max(0, event.time - firstToken)
|
|
next.decodeTokens += outputTokens
|
|
}
|
|
}
|
|
return next
|
|
}
|
|
case 'tool/call':
|
|
return { ...state, pendingCalls: { ...state.pendingCalls, [event.data.callId]: event.time } }
|
|
case 'tool/result': {
|
|
// Own-key check: callId is provider-minted (model/tool JSON boundary),
|
|
// so a prototype property name ('constructor', 'toString') on a result
|
|
// with no recorded call must read as unmatched, not as an inherited
|
|
// function that would poison toolMs with NaN.
|
|
const callId = event.data.message.source.callId
|
|
const dispatched = Object.hasOwn(state.pendingCalls, callId) ? state.pendingCalls[callId] : undefined
|
|
if (dispatched === undefined) return state
|
|
const pendingCalls = Object.fromEntries(
|
|
Object.entries(state.pendingCalls).filter(([id]) => id !== callId),
|
|
)
|
|
return { ...state, toolMs: state.toolMs + Math.max(0, event.time - dispatched), pendingCalls }
|
|
}
|
|
case 'step/end':
|
|
return {
|
|
...state,
|
|
turns: state.lastTurn === event.data.turn ? state.turns : state.turns + 1,
|
|
steps: state.steps + 1,
|
|
lastTurn: event.data.turn,
|
|
openStep: null,
|
|
}
|
|
case 'turn/end':
|
|
// A call whose result never landed belongs to a cancelled or failed
|
|
// turn; results always land within their turn, so drop the leftovers
|
|
// instead of growing persisted state forever.
|
|
return Object.keys(state.pendingCalls).length === 0 ? state : { ...state, pendingCalls: {} }
|
|
default:
|
|
return state
|
|
}
|
|
},
|
|
wire: {
|
|
viewSchema: sessionStatsSchema,
|
|
view: state => ({
|
|
turns: state.turns,
|
|
steps: state.steps,
|
|
llmMs: state.llmMs,
|
|
toolMs: state.toolMs,
|
|
ttftMs: state.ttftMs,
|
|
ttftSteps: state.ttftSteps,
|
|
decodeMs: state.decodeMs,
|
|
decodeTokens: state.decodeTokens,
|
|
}),
|
|
},
|
|
} satisfies ProjectionDefinition<'sessionStats', SessionStatsState>
|