Repository navigation
[EPIC-D][LOU-D46] loushy eval records cassettes, replays them in CI and reports trajectory drift - #96
Merged
Merged
Conversation
…nd reports trajectory drift - `loushy eval --record` runs every eval case against its agent's real provider through recordReplay() and writes one cassette per case at `__cassettes__/<eval-name>/<case>.json` next to the eval file. - `--replay` runs every case from its cassette with no network; a missing cassette fails the case with the `--record` command to run. Plain `loushy eval` under CI replays cases that have a cassette; otherwise it is unchanged. - `--drift` re-records into a temp dir and diffs each case's trajectory (tool order, normalized args, steps, finish reason; tokens with `--drift-usage`) against the committed cassette: a `Drift:` table in the summary, a soft `drift` assertion (JUnit <system-out>), a gate failure with `--strict`. The comparison is the pure src/evals/drift.ts. - Hook: while a case runs, AgentExecutor.execute() wraps the provider for that case (AsyncLocalStorage), so existing eval files need no change. - Eval results now carry their `file` (read at run time, not collection). Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_016bY29ibQgv3MDdjAbUT4YR
…eplay-drift # Conflicts: # CHANGELOG.md
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
What
loushy eval --recordruns each eval case against its agent's real provider throughrecordReplay()(record mode). It writes one cassette per case next to the eval file, at__cassettes__/<eval-name>/<case>.json. Names are deterministic slugs, the format is the existing cassette format, and a case with a second provider gets<case>.2.json.loushy eval --replayruns every case from its cassette with no network. When a cassette is missing, the case fails withloushy eval --replay: no cassette for "<eval> [<case>]" at <path>. Record it with: npx loushy eval --record <file>.loushy evalwithCIset replays each case that has a cassette and runs the rest live. WithoutCIit behaves exactly as before.loushy eval --driftre-records into a temp dir (committed cassettes are left alone) and compares each case's trajectory with its committed cassette. It compares ordered tool names, JSON-normalized args, step count and finish reason. Tokens are compared only with--drift-usage.Drift:table with the columns EVAL / CASE / FIELD / COMMITTED / CURRENT.driftassertion, which shows up in JUnit as<system-out>.--drift --strict, drift is a gate failure: a JUnit<failure>and exit code 1.src/evals/drift.ts(trajectoryOf,diffTrajectories). TherenderDriftTablefunction is insrc/cli/evalReport.ts.src/evals/cassettes.ts):defineEvalruns each case insidewithEvalCassettes(). While a case runs, an AsyncLocalStorage-scoped wrapper aroundAgentExecutor.execute()swapsoptions.providerfor that case'srecordReplaywrapper.createAgent()agents,AgentExecutor.stream(), sub-agents and approval resumes all go throughAgentExecutor.execute(). As a result, existing eval files need no change andsrc/createAgent.ts/src/execution/*are not modified. Without the env var thatloushy evalsets, nothing is installed, so a plainvitest runand the smoke evals are unchanged.EvalResultgainscassettes?: string[].EvalResult.filewas read fromexpect.getState().testPathat collection time, so it was always empty. It is now read when the test runs. Cassette paths depend on it.Why
This is differentiator 2 in
.agent-loop/AUDIT.md§7: record/replay as the default test and eval substrate. eve's evals havemockModelbut no record/replay, and open-harness has neither. Evals can now be recorded once against a real model, replayed for free in CI, and checked for trajectory drift before a model or prompt change.Acceptance criteria
--recordwrites one cassette per eval case at__cassettes__/<eval-name>/<case>.jsonnext to the*.eval.ts, with deterministic names and the existing cassette format--replay(explicit, or the default underCIwhen a cassette exists) runs from cassettes with no network; a missing cassette is a clear failure naming the--recordcommand; plainloushy evalkeeps today's behaviour--driftre-records into a temp location and compares tool names/order, normalized args, step count, finish reason and tokens (on request). It reports a per-case diff table in the summary and<system-out>/failure entries in JUnit;--drift --strictmakes drift a gate failuresrc/evals/drift.tsdefineEvalhook added without changing existing eval files;src/evals/__smoke__.eval.tspassesmockModelas the "real" provider. Replay runs with a provider that fails on any call, which shows replay never calls the provider.<system-out>and--strict<failure>.withEvalCassettestests.docs/evals.md"Record, replay and drift";docs/testing.mdcross-link;docs/cli.mdusage; README testing/evals line mentions--drift; llms files regenerated; CHANGELOG (Added)src/cli/eval*.ts,src/evals/*, a new CLI fixture and docs (nosrc/testing/*change needed). About 233 added non-comment, non-test code lines.How verified
npx tsc --noEmit: no errorsnpm run lint: 0 errors, 405 warnings. None of the warnings are in changed files.npm run test:types: Type Errors no errorsnpx vitest run(after the builds): Test Files 146 passed (146); Tests 2106 passed | 3 skipped (2109)npm run build && npm run build --workspace=packages/create-loushy-agent: both exit 0npm run docs:verify-snippets -- --skip-build: all 107 snippet(s) type-check against src/; 6 also run cleanlynpm run docs:llms:check: exit 0npm run test:coverage: exit 0, 146/146 files, All files 90.05% statementsnpm run fallow: the only finding is a pre-existing duplicate clone group insrc/providers/AnthropicProvider.ts:35-49/src/providers/OpenAIProvider.ts:29-43. Those files are identical tomainand this PR does not touch them. No dead code or complexity findings come from this change.🤖 Generated with Claude Code
https://claude.ai/code/session_016bY29ibQgv3MDdjAbUT4YR
Generated by Claude Code