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:
ZacharyZhang-NY committed 2026-04-20 11:39:53 -04:00
1 parent 76d9031a41
commit e7f879bed0
12 files changed
+931

No files matched your search

+55
View File
@@ -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,
};
}