/** A normalized eval record produced from one statelog trace. * * Deliberately project-agnostic: nothing in this shape knows about * specific subagent names (oracle/explorer/code/etc.). Consumers who * want semantic identification ("did the oracle fire?") query the * thread `label` / `session` fields populated by the runtime. * * Statelogs from builds older than PR #726 (before the runtime emitted * thread labels, toolCallStart, interrupt summaries, threadIds on * events) are NOT supported: the tolerance that degraded them to * null-filled records was deleted along with the pre-#733 run-directory * layout fallbacks. Re-extract from a current build, or use a build * from before this change to read museum pieces. */ export type EvalRecord = { /** trace_id from the source statelog. */ traceId: string; /** Format version of the EvalRecord shape itself (NOT the statelog * format_version). Bump when fields change incompatibly. */ recordVersion: 2; /** Statelog wire-format version, copied from the source envelope * (`events[0].format_version`). Lets consumers reason about which * optional fields they can expect (label, threadId, etc.). */ formatVersion: number; /** Total wall-clock duration in milliseconds, derived from the * first event's timestamp to the last event's timestamp. */ durationMs: number; /** Absolute epoch ms of the first event — durationMs is relative to * this. Enables cross-run wall-clock math. */ startedAtMs: number; /** Last `agentName` statelog event in the trace (std::statelog * setAgentName). Absent when the agent never named itself. */ agentName?: string; /** Source file path, for traceability. Always a real path — stdin * input is not supported. */ source: string; /** Values recorded as eval values. Explicit values recorded via * std::statelog.evalValue() are preferred; if absent, the * extractor may synthesize one fallback entry from the last * user-role message of the first promptCompletion on the top-level * thread (warning emitted in `warnings`). Empty only when neither * explicit events nor a heuristic source exists. */ evalValues: EvalValue[]; /** Values used as eval outputs. Explicit values recorded via * std::statelog.evalOutput() are preferred; if absent, the * extractor may synthesize one fallback entry from the last * promptCompletion's completion on the top-level thread (warning * emitted in `warnings`). Empty only when neither explicit events * nor a heuristic source exists. Consumers that want a single * answer usually consume the last element. */ evalOutputs: EvalValue[]; /** Every thread observed in the trace. Each `threadCreated` event * becomes one entry. Resumes (`threadResumed`) do NOT create new * entries — they map back to the existing thread by id. */ threads: ThreadEntry[]; /** Chronological list of normalized events. The single source of * truth — everything else in this record is a derived aggregation * over this list. */ events: NormalizedEvent[]; /** Interrupts that surfaced during the run, with their resolution * outcome. Built from `interruptThrown` / `handlerDecision` / * `interruptResolved` events. Carries the kind/message/data so * consumers can see what was approved/rejected without * correlating manually. */ interrupts: InterruptEntry[]; /** Errors raised during the run. */ errors: ErrorEntry[]; /** Tool invocations that started (`toolCallStart`) but never * emitted a matching `toolCall` end event. Almost always means * the run was killed or aborted mid-tool. */ incomplete: IncompleteInvocation[]; /** Coarse-grained aggregations for quick scanning. NOT meant to be * authoritative — anything load-bearing should be re-derived from * `events` by the consumer. */ metrics: Metrics; /** Warnings the extractor emitted while processing this trace * (e.g. unknown event types, missing fields, suspicious shapes). */ warnings: string[]; }; export type ThreadEntry = { threadId: string; threadType: "thread" | "subthread"; parentThreadId: string | null; /** From `thread(label: "...")`. The most useful semantic tag. */ label: string | null; /** From `thread(session: "...")`. Only populated on first create * of a session; resumes don't re-emit. */ session: string | null; hidden: boolean; /** Milliseconds since trace start (NOT epoch). */ createdAtMs: number; }; /** Fields every normalized event carries, regardless of `kind`. Kept * as a single base type so we don't repeat them across each variant * and so consumers can write helpers that operate on the common * shape (e.g. `function timeOf(e: NormalizedEventBase) {...}`). */ export type NormalizedEventBase = { tMs: number; threadId: string | null; spanId: string | null; parentSpanId: string | null; }; export type NormalizedLlmEvent = NormalizedEventBase & { kind: "llm"; model: string; /** Tool names available to this LLM call. Useful for * fingerprinting an agent when thread labels are absent. */ tools: string[]; durationMs: number | null; costUsd: number | null; tokensIn: number | null; /** Fresh, cached and cache-write input together: what the model was * sent. `tokensIn` is the fresh part only. Absent on records written * before this field existed. */ contextTokens?: number | null; tokensOut: number | null; }; export type NormalizedToolStartEvent = NormalizedEventBase & { kind: "tool_start"; /** Sourced from `toolCallStart.data.toolName`. */ tool: string; argsPreview: string; model: string | null; }; export type NormalizedToolEndEvent = NormalizedEventBase & { kind: "tool_end"; /** Sourced from `toolCall.data.toolName`. */ tool: string; outputPreview: string; durationMs: number | null; }; /** One step in the chronological tool-call / LLM-call sequence. */ export type NormalizedEvent = NormalizedLlmEvent | NormalizedToolStartEvent | NormalizedToolEndEvent; export type InterruptEntry = { interruptId: string; /** From the interrupt summary attached to handlerDecision / * interruptResolved by the runtime. Null on older traces. */ kind: string | null; message: string | null; /** Full data payload as it was on the interrupt object. May be * large — consumers should preview, not log verbatim. */ data: unknown; /** Same vocabulary as interrupt handlers (approve / reject / propagate / * pass) — plus "unresolved", which is not an outcome but the absence of * one: no interruptResolved event recorded a decision (the run ended * mid-interrupt, or a legacy trace never emitted one). "passed" is only * ever an affirmative claim from a resolution event; the extractor never * infers it. * * All four decided values are emitted by the runtime (#736): approved/ * rejected at resolution, and propagated/passed when an interrupt * surfaces with its chain outcome ("passed" = the chain finished without * deciding — every handler passed, or none existed). A surfaced interrupt * the user later decides carries two interruptResolved events; extraction * takes the LAST as authoritative. * * This is a PERSISTED field. Records written by builds between PR #734 * and PR #735 may say "passed" where they mean "unresolved" — the short * window where the extractor mapped no-decision to "passed". */ outcome: "approved" | "rejected" | "propagated" | "passed" | "unresolved"; resolvedBy: "handler" | "user" | "policy" | "ipc" | null; thrownAtMs: number | null; resolvedAtMs: number | null; }; export type ErrorEntry = { tMs: number; errorType: string; message: string; spanId: string | null; }; export type IncompleteInvocation = { tool: string; startedAtMs: number; spanId: string | null; /** The thread that called the tool, if resolvable. */ threadId: string | null; }; export type Metrics = { llmCalls: number; toolStarts: number; toolEnds: number; /** Distinct model strings observed, deduped and sorted ascending * for diff-friendly snapshots. */ models: string[]; /** Sum of fresh input tokens across all `promptCompletion` events, * reading `data.usage.inputTokens` (zero if absent). */ tokensInTotal: number; /** Sum of `contextTokens` across all promptCompletion events. Absent * on records written before this field existed. */ contextTokensTotal?: number; /** Sum of `data.usage.outputTokens` across all promptCompletion * events (zero if absent). */ tokensOutTotal: number; /** Sum of `data.cost.totalCost` across all promptCompletion events. * Treated as USD — `lib/runtime/prompt.ts` records cost in dollars. */ costUsdTotal: number; /** Count of tool END events (`toolCall`) per tool name. Does NOT * count incomplete `toolCallStart`s — those live in `incomplete`. */ toolCounts: Record; }; /** One firing of `evalValue(...)` or `evalOutput(...)` from agent * code, plus enough provenance for consumers to filter by thread * (e.g. drop subagent firings) or correlate with the event timeline. * `tMs` is derived from the envelope timestamp at extract time, not * emitted on the wire. `value` is whatever the agent passed — * already JSON-round-tripped at the stdlib boundary, may be * truncated by the extractor if it exceeds STATELOG_EVAL_MAX_VALUE_BYTES * (in which case `truncated: true` is set; otherwise omitted). */ export type EvalValue = { value: unknown; threadId: string | null; tMs: number; truncated?: true; };