feat: safely parallelize evals + add statistical rigor for measuring skill changes#35
Merged
Conversation
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.
Safely parallelize evals + add statistical rigor for measuring skill changes
Summary
This PR delivers two major capabilities for the eval runner:
--concurrency <n>for within-lane and cross-lane parallel execution--trials N, trial aggregation with CIs, and--compare-baselinefor paired A/B comparisonWhy
A single eval run has a ~16% baseline pass/fail flip rate (measured empirically — two identical serial runs disagree on ~16% of cases). This means you cannot tell whether a skill change helped by comparing two single runs. The eval system needed:
What changed
Parallelization (Phases 0–5)
lane:caseId:condition:trialkeysfinallycleanup blocksrunScheduled()with all-settled semanticsenumerate*WorkItems()+execute*WorkItem()--concurrency <n>CLI flagMeasured speedups:
Parallelism does NOT cause regressions — empirically confirmed:
Statistical rigor
--trials Nevals/lib/statistics.ts--compare-baseline <path>.mux/skills/eval-guide/SKILL.mdwith learnings and recommended workflowsVerdict system:
improved= CI excludes 0 AND effect ≥ practical threshold (5pp pass rate / 0.05 score)regressed= CI upper bound < 0inconclusive= CI crosses 0Dogfood validation: Two identical 3-trial runs correctly produce
inconclusiveverdict (23/24 cases inconclusive, 14W/15L/43T — pure noise correctly identified).New CLI flags
How to use
Files changed (20 files, +4,080/−527)
New files:
evals/lib/scheduler.ts— bounded work-item schedulerevals/lib/statistics.ts— pure statistics helpers (zero external deps).mux/skills/eval-guide/SKILL.md— learnings + recommended workflowstest/unit/evals/scheduler.test.ts— 8 teststest/unit/evals/statistics.test.ts— 20 teststest/unit/evals/reporting.test.ts— 5 teststest/unit/evals/workItem.test.ts— 4 teststest/unit/evals/promptRunner.test.ts— 5 teststest/unit/evals/executionRunner.test.ts— 3 teststest/unit/evals/dogfoodRunner.test.ts— 3 teststest/integration/evals-parallelization.test.ts— parity testtest/integration/evals-trials.test.ts— trials wiring testtest/integration/evals-compare-baseline.test.ts— A/B comparison testModified files:
evals/run.ts— CLI flags, lane dispatch, cross-lane concurrencyevals/prompt/runner.ts— enumerate + execute + scheduleevals/execution/runner.ts— enumerate + execute + schedule + cleanupevals/dogfood/runner.ts— enumerate + execute + schedule + cleanupevals/lib/types.ts— work-item identity, aggregation types, comparison typesevals/lib/schemas.ts— Zod schemas for new typesevals/lib/reporting.ts— aggregation, comparison, markdown generationevals/lib/matrix.ts— reuse shared computeMeanValidation
npm run typechecknpm run testnpm run lintnpm run format:check📋 Implementation Plan
Plan: safely parallelize evals without breaking determinism or isolation
Recommendation
Do not parallelize evals by sprinkling
Promise.all()acrossevals/run.tsor by running the three lanes independently with ad hoc lane-level concurrency.Instead, ship this as a scheduler + invariants rollout:
concurrency = 1.That path keeps the current deterministic report model, minimizes artifact collisions, and gives us a single place to enforce provider throttling, logging, cleanup, and error semantics.
Why this plan is the safe one
Evidence from the current repo
evals/run.tscurrently runs lanes sequentially in afor...ofloop and aggregates results after eachawait runLane(...).evals/prompt/runner.ts,evals/execution/runner.ts, andevals/dogfood/runner.tsall execute case/condition work serially with nested loops.evals/execution/runner.tsalready creates isolated temp homes viacreateIsolatedEvalHome()and isolated output directories;evals/dogfood/runner.tsuses run-id/provider/case/condition-scoped paths.evals/lib/artifacts.tsalready gives us stable run IDs and validated paths, and reporting sorts results later, so final report generation does not depend on push order.evals/lib/cliHarness.tshas cleanup helpers, but current runners do not consistently guarantee cleanup infinally.Goals
AGENT_TTY_HOME, temp output, bundles, and artifacts.Non-goals for the first rollout
Proposed architecture
1) Introduce an explicit work-item model
Add a small scheduling abstraction under
evals/lib/and stop treating each lane as an opaque sequential loop.Likely new concepts:
Guidance:
prompt: one work item per case/condition/trial.execution: one work item per case/condition.dogfood: one work item per case/condition.enumerate*WorkItems(...)execute*WorkItem(...)2) Use one shared bounded scheduler
Add a scheduler helper, e.g.
evals/lib/scheduler.ts, that accepts all work items and enforces:planvsagent).Recommended policy:
concurrency = 1, so current semantics stay unchanged;A rough target shape:
3) Preserve isolation boundaries
Parallelize only at the current safe boundaries:
Do not split a single execution/dogfood case into smaller concurrent subtasks in the first rollout. The per-case isolated home/output directory is the right cleanup and failure boundary.
4) Make concurrency observable and controllable
In
evals/run.tsand the CLI option parsing/types:--concurrency <n>option with default1;n > 1experimental for the first merge if we want an easier rollback story;--lane-concurrencyunless a real need appears later.Phased implementation plan
Phase 0 — harden invariants with no behavior change
Files/symbols to touch
evals/run.tsevals/lib/cliHarness.tsevals/lib/artifacts.tsevals/prompt/runner.ts,evals/execution/runner.ts,evals/dogfood/runner.tstest/unit/evals/andtest/integration/Work
lane + caseId + condition + trialIndex.finally, especially aroundcreateIsolatedEvalHome()call sites.caseId, lane-qualify it or add an invariant/assert.Quality gate before Phase 1
concurrency = 1.Phase 1 — refactor to work-plan execution, still serial
Files/symbols to touch
evals/run.tsevals/lib/scheduler.tsorevals/lib/workPlan.tsevals/prompt/runner.tsevals/execution/runner.tsevals/dogfood/runner.tsWork
concurrency = 1) even if the scheduler abstraction exists.Quality gate before Phase 2
Phase 2 — enable prompt-lane parallelism first
Why first
AGENT_TTY_HOME, no session cleanup, and fewer subprocess/resource concerns.Files/symbols to touch
evals/prompt/runner.tsevals/run.tsWork
stub/fixture providers.EvalResultwithout canceling other prompt work.Quality gate before Phase 3
stubprovider: serial vs parallel prompt runs have parity for sorted results and scores.Phase 3 — parallelize execution lane at whole-case granularity
Why second
Files/symbols to touch
evals/execution/runner.tsevals/lib/cliHarness.tsevals/lib/artifacts.tsWork
cleanupEvalHome()(or equivalent) infinally.Quality gate before Phase 4
Phase 4 — parallelize dogfood lane last
Why last
Files/symbols to touch
evals/dogfood/runner.tsdogfood/README.mdand/ordogfood/CATALOG.mdif proof-bundle workflows changeWork
runId/provider/case/conditiondirectory structure.Quality gate before Phase 5
Phase 5 — optional cross-lane concurrency via the shared scheduler
Recommendation
Promise.all(lanes)path.Work
buildEvalWorkPlan(...)to emit work items across all selected lanes.Quality gate
Testing strategy
Unit tests
Add focused coverage under
test/unit/evals/for:Likely new/expanded suites:
test/unit/evals/runners.test.tstest/unit/evals/scheduler.test.tstest/unit/evals/isolation.test.tstest/unit/evals/provider-concurrency.test.tsIntegration tests
Add integration coverage for:
--concurrency > 1parity onstub,AGENT_TTY_HOMEusage,Likely suites:
test/integration/evals-lanes.test.tstest/integration/evals-parallelization.test.tsCI and task-runner updates
If the implementation lands, update:
.github/workflows/ci.ymlmise.tomlevals/README.mdRecommended CI rollout:
Dogfooding plan
Because this is a CLI project, dogfooding should be done against isolated homes and captured as reviewable terminal artifacts.
Setup
AGENT_TTY_HOMEfor each dogfood run.npx tsx evals/run.ts ...for the eval runner,npx tsx src/cli/main.ts ...if using agent-tty itself to capture proof artifacts.stubprovider; use real providers only after the stub parity gates pass.Dogfood scenarios
--concurrency 1;Proof artifacts reviewers should receive
For every dogfood phase above, produce:
Recommended capture workflow:
agent-ttysession.dogfood/with a clear run label for the phase and concurrency setting.Acceptance criteria
The implementation should not be considered complete until all of the following are true:
--concurrencydefaults to1, preserving current behavior.Risks and mitigations
finallyand add integration tests.Suggested delivery order
--concurrency.Generated with
mux• Model:anthropic:claude-opus-4-6• Thinking:xhigh