Files
AgentCompany/tests/eval
ZacharyZhang-NY e7f879bed0 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.
2026-04-20 11:39:53 -04:00
..

B5 eval harness (minimal scaffold)

This directory holds the minimal LLM behavior eval tier for AgentCompany. It replays recorded LLM traces (fixtures) and checks structural assertions against them — tool names, call counts, argument shapes, message substrings, and turn caps. It does not perform semantic quality judgment yet; that tier is proposed in docs/proposals/B5.1-llm-as-judge-followup.md.

File layout

File Role
types.ts Wire types — fixtures, assertions, results. Reuses @mariozechner/pi-ai primitives.
fixture-loader.ts JSONL → FixtureFile. Rejects malformed or missing meta/assertions blocks.
replay-client.ts Swaps in for pi-ai's streamSimple; replays recorded assistant turns. Used by the live-replay tier in B5.1.
assertion-judge.ts Structural assertions over recorded trace. No semantic judgment.
eval-runner.ts Parse fixture → judge assertions → produce EvalResult.
record-fixture.ts CLI for writing fixture meta headers. Full record loop lands in B5.1.
harness.test.ts vitest driver that walks fixtures/ and asserts every fixture passes.
fixtures/ Recorded JSONL traces. Git-tracked. Not bundled into production (electron-builder.yml only ships out/**).

Running

# Assertion-only eval (no fixtures needed; passes on empty fixture dir)
pnpm test:eval

# Record a stub fixture header (full recorder lands in B5.1)
pnpm eval:record -- \
  --fixture-id ceo-team-building-empty \
  --scenario "Empty company, CEO bootstrap" \
  --out tests/eval/fixtures/ceo-team-building-empty.jsonl

Fixture JSONL format

Every fixture begins with a meta header, contains one turn per line in ascending turnIndex order, and ends with an assertions block.

{"type":"meta","fixtureId":"ceo-team-building-empty","piAgentCoreVersion":"0.67.68","recordedAt":"2026-04-20T21:14:00.000Z","scenario":"Empty company, CEO bootstrap","systemPromptHash":"sha256:…"}
{"type":"turn","turnIndex":0,"role":"user","content":[{"type":"text","text":"build me a team"}]}
{"type":"turn","turnIndex":1,"role":"assistant","content":[{"type":"text","text":"Starting team build."},{"type":"toolCall","id":"call_1","name":"hire_agent","arguments":{"role":"engineer"}}],"tokenLedger":{"input":12345,"cache_read":11000,"cache_creation":345,"output":234}}
{"type":"turn","turnIndex":2,"role":"toolResult","toolUseId":"call_1","content":[{"type":"text","text":"hired agent-42"}]}
{"type":"assertions","assertions":[{"kind":"tool_called","name":"hire_agent","min_count":3,"max_count":5},{"kind":"trace_max_turns","max":40}]}

Field contracts:

  • meta.fixtureId — stable kebab-case identifier used as the scenario name.
  • meta.piAgentCoreVersion — the @mariozechner/pi-agent-core version used when the fixture was recorded. Drift against the installed version is allowed but future tooling will surface a warning.
  • meta.systemPromptHash — sha256 of the system prompt text. Changes in this hash are the canonical signal that a fixture is stale.
  • turn.turnIndex — strictly increasing from 0. Gaps or duplicates are hard errors.
  • turn.role = "assistant" requires a tokenLedger in snake_case (input, cache_read, cache_creation, output). This maps 1:1 to pi-ai's Usage.{input, cacheRead, cacheWrite, output} — renamed to provider-neutral snake_case on disk.
  • assertions.assertions — see types.ts for the complete schema.

Assertion kinds

Kind Semantics
tool_called Matches tool calls by name. Supports min_count (default 1), max_count, and order (0-based ordinal across all tool calls in the trace).
no_tool_called Fails if any tool call with the given name appears.
message_contains_substring Any role-matching message whose text content contains substring passes.
tool_call_arg_matches At least one call to name has the dotted arg_path resolving to a string matching matches_regex.
trace_max_turns Trace must have at most max turns.

Anything that requires semantic judgment (tone, strategy, plan quality) belongs in the LLM-as-judge tier. See docs/proposals/B5.1-llm-as-judge-followup.md.

What this scaffold does NOT do tonight

  • No synthesized fixtures. fixtures/ ships empty. The user records real traces by running the recorder against a live provider.
  • No live-replay. replay-client.ts is ready to swap into the agent loop; wiring it into a real pi-agent-core Agent instance is the B5.1 task. Tonight the runner asserts against recorded turns directly.
  • No LLM-as-judge. Assertions are structural only. Semantic judgment tier is proposed separately.
  • No logging / telemetry. Results are returned from runEval as plain values; the vitest reporter is the only output surface.

Each omission is deliberate and documented. Nothing here silently degrades to a placeholder — missing fixture = hard error, malformed fixture = hard error, exhausted fixture = hard error.

Production bundle posture

electron-builder.yml ships only out/** and package.json. tests/ — including tests/eval/fixtures/ — is never packaged into the shipped .asar. Fixtures are dev-time / CI-time artifacts; they do not leak into end-user installs.