Build minimal LLM eval harness scaffold (B5) — fixtures recorded separately
Land the B5 harness skeleton: fixture loader (strict JSONL contract with meta header + ordered turn records + assertions tail), replay-client that swaps in for pi-ai's streamSimple by replaying recorded assistant turns, structural assertion judge (tool_called / no_tool_called / message_contains_substring / tool_call_arg_matches / trace_max_turns), an eval-runner that sums the token ledger and produces an EvalResult, a vitest driver (tests/eval/harness.test.ts) that discovers *.jsonl fixtures, and a record-fixture CLI that writes fixture meta headers. Hard contracts: - No fallback: replay-client and fixture-loader throw on exhaustion / malformed input rather than synthesizing defaults. - No mock: types reuse @mariozechner/pi-ai AgentMessage / ToolCall verbatim; shadow types are forbidden. - No fake fixture data: tests/eval/fixtures/ ships only README + .gitkeep. Real traces are recorded by the user against live providers. package.json adds test:eval (vitest --passWithNoTests so an empty fixture dir is green) and eval:record (CLI entrypoint). electron-builder already excludes tests/ via its files: glob — fixtures never enter the shipped .asar.
This commit is contained in:
1 parent
76d9031a41
commit
e7f879bed0
12 files changed
+931
No files matched your search
@@ -0,0 +1,55 @@
|
||||
/**
|
||||
* Eval runner for B5 harness.
|
||||
*
|
||||
* Given a parsed `FixtureFile`, produces an `EvalResult` by:
|
||||
* 1. Summing the token ledger across all recorded assistant turns.
|
||||
* 2. Running the assertion judge against the recorded turns.
|
||||
* 3. Measuring wall-clock duration of steps (1)–(2).
|
||||
*
|
||||
* Tonight's scope deliberately evaluates assertions against the RECORDED
|
||||
* trace, not a live replay-driven agent loop. That keeps the harness
|
||||
* useful as a snapshot-regression tool the moment the first fixture lands,
|
||||
* without wiring in the full `pi-agent-core` Agent loop (which requires
|
||||
* tools + hooks and is better done incrementally once we have fixtures).
|
||||
*
|
||||
* The replay-client + fixture-loader are written now so the live-replay
|
||||
* tier can be added in B5.1 without reshaping types.
|
||||
*/
|
||||
|
||||
import { judgeAssertions } from "./assertion-judge.js";
|
||||
import type { EvalResult, FixtureFile, TokenLedger } from "./types.js";
|
||||
|
||||
const ZERO_LEDGER: TokenLedger = {
|
||||
input: 0,
|
||||
cache_read: 0,
|
||||
cache_creation: 0,
|
||||
output: 0,
|
||||
};
|
||||
|
||||
function sumLedger(fixture: FixtureFile): TokenLedger {
|
||||
return fixture.turns.reduce<TokenLedger>((acc, turn) => {
|
||||
if (turn.role !== "assistant") return acc;
|
||||
return {
|
||||
input: acc.input + turn.tokenLedger.input,
|
||||
cache_read: acc.cache_read + turn.tokenLedger.cache_read,
|
||||
cache_creation: acc.cache_creation + turn.tokenLedger.cache_creation,
|
||||
output: acc.output + turn.tokenLedger.output,
|
||||
};
|
||||
}, ZERO_LEDGER);
|
||||
}
|
||||
|
||||
export function runEval(fixture: FixtureFile): EvalResult {
|
||||
const start = Date.now();
|
||||
const results = judgeAssertions(fixture.assertions, fixture.turns);
|
||||
const tokenLedger = sumLedger(fixture);
|
||||
const durationMs = Date.now() - start;
|
||||
const passed = results.every((result) => result.passed);
|
||||
|
||||
return {
|
||||
fixtureId: fixture.meta.fixtureId,
|
||||
passed,
|
||||
assertions: results,
|
||||
tokenLedger,
|
||||
durationMs,
|
||||
};
|
||||
}
|
||||
Reference in new issue
Block a user