/** * Type definitions for the B5 minimal eval harness. * * Design invariants: * - Fixtures are recorded LLM traces (history real runs), not synthesized mocks. * A fixture is the serialized truth of a turn that actually happened against * a real provider; replaying it tells us whether the *harness* still reaches * the same tool-call shape given the same prompt/context. * - Every type here reuses `@mariozechner/pi-ai` primitives verbatim. Shadow * types are forbidden; if pi-ai changes its `AssistantMessage` shape, this * harness breaks loud — that is the intent. * - No runtime field here is optional for "fallback reasons". If a field is * optional, it encodes real optionality of the source data (e.g. a turn * record that does not carry usage because it is a user turn). */ import type { AssistantMessage, ToolResultMessage, UserMessage, } from "@mariozechner/pi-ai"; /** * Top-level fixture file meta header. * * Every JSONL fixture begins with exactly one `meta` record. The runner will * throw if the first line is not a meta record — no fallback default metadata * is synthesized. */ export interface FixtureMeta { type: "meta"; /** Stable fixture identifier; used as the scenario name in eval output. */ fixtureId: string; /** pi-agent-core version used to record this fixture. */ piAgentCoreVersion: string; /** ISO-8601 timestamp when the fixture was recorded against a real provider. */ recordedAt: string; /** Short human description of what the scenario exercises. */ scenario: string; /** sha256 of the system prompt used at recording time. Used to detect drift. */ systemPromptHash: string; } /** * Token ledger captured for assistant turns. * * Shape mirrors `AssistantMessage.usage` but uses snake_case fields that match * the JSONL spec printed in `README.md`. The mapping is 1:1 with pi-ai's * `Usage` primitives (`input`, `output`, `cacheRead`, `cacheWrite`) — renamed * for the on-disk format to stay provider-neutral. */ export interface TokenLedger { input: number; cache_read: number; cache_creation: number; output: number; } export interface UserTurn { type: "turn"; turnIndex: number; role: "user"; /** Verbatim user content blocks from the real recording. */ content: UserMessage["content"]; } export interface AssistantTurn { type: "turn"; turnIndex: number; role: "assistant"; /** Verbatim assistant content blocks (text / thinking / toolCall). */ content: AssistantMessage["content"]; /** Token ledger recorded for this assistant turn. Required on assistant turns. */ tokenLedger: TokenLedger; } export interface ToolResultTurn { type: "turn"; turnIndex: number; role: "toolResult"; toolUseId: string; content: ToolResultMessage["content"]; } export type TraceTurn = UserTurn | AssistantTurn | ToolResultTurn; /** * Structural assertions that can be evaluated against a replayed trace. * * Deliberately minimal: assertions express tool-call shape, order, count, and * textual substrings. Semantic quality judgments (tone, plan soundness) are * out of scope for this tier and will be handled by the LLM-as-judge tier * described in B5.1. */ export type Assertion = | { kind: "tool_called"; name: string; min_count?: number; max_count?: number; /** Ordinal position in the trace (0-based) among all tool calls. */ order?: number; } | { kind: "no_tool_called"; name: string } | { kind: "message_contains_substring"; role: "assistant" | "user"; substring: string; } | { kind: "tool_call_arg_matches"; name: string; /** Dotted JSON path into the tool call's arguments object, e.g. "agent.role". */ arg_path: string; /** ECMAScript regex source evaluated against the argument string. */ matches_regex: string; } | { kind: "trace_max_turns"; max: number }; export interface AssertionsBlock { type: "assertions"; assertions: Assertion[]; } /** * A JSONL fixture file is parsed into exactly one of these records per line. * The loader rejects files that do not begin with `meta` and end with * `assertions`. */ export type FixtureLine = FixtureMeta | TraceTurn | AssertionsBlock; /** * Parsed fixture file ready to be replayed by the runner. */ export interface FixtureFile { meta: FixtureMeta; turns: TraceTurn[]; assertions: Assertion[]; } /** * Per-assertion outcome within a single eval run. */ export interface AssertionResult { assertion: Assertion; passed: boolean; /** Populated when `passed === false`; concise human-readable explanation. */ reason?: string; } /** * Result of evaluating one fixture against the current code path. */ export interface EvalResult { fixtureId: string; passed: boolean; assertions: AssertionResult[]; /** Aggregated token ledger summed across all assistant turns in the fixture. */ tokenLedger: TokenLedger; /** Wall-clock duration of the replay, in milliseconds. */ durationMs: number; }