Skip to content

feat(eval): add --runs N flag with median scoring - #111

Closed
yogeshchoudhary147 wants to merge 10 commits into
mainfrom
feat/runs-n-median-scoring
Closed

feat(eval): add --runs N flag with median scoring#111
yogeshchoudhary147 wants to merge 10 commits into
mainfrom
feat/runs-n-median-scoring

Conversation

@yogeshchoudhary147

@yogeshchoudhary147 yogeshchoudhary147 commented Jul 14, 2026

Copy link
Copy Markdown
Contributor

Summary

  • Adds --runs <n> CLI flag (default: 1, fully backward compatible) that runs each eval job N times in parallel and aggregates results using median scoring
  • Scores are medianed per-dimension; costs and tokens are summed across all runs
  • Raw individual run results embedded in runs[] field with run_count for debugging
  • Partial run failures (errors from jobs that also had successful runs) are now surfaced as warnings instead of being silently dropped by aggregation

Test plan

  • npm run build passes
  • npm test passes (734 tests, 0 failures)
  • npm run lint clean on all changed packages
  • New unit tests for aggregateRuns() (15 cases) and --runs validation (6 cases)

Summary by CodeRabbit

  • New Features
    • Added a --runs <n> CLI option to repeat each evaluation job and compute median-based aggregated results.
    • Aggregated outputs now include run count and optional per-run details when multiple runs are used.
  • Documentation
    • Updated AGENTS.md to document the new --runs option.
  • Bug Fixes
    • Improved collection/cleanup of temporary results, ignoring missing/corrupt inputs.
    • Enhanced deduplication by normalizing tool values for more reliable result merging.
  • Tests
    • Expanded CLI, aggregation, persistence, subprocess-runner, and argument-stripping test coverage for --runs.

@coderabbitai

coderabbitai Bot commented Jul 14, 2026

Copy link
Copy Markdown

Review Change Stack

Note

Reviews paused

It looks like this branch is under active development. To avoid overwhelming you with review comments due to an influx of new commits, CodeRabbit has automatically paused this review. You can configure this behavior by changing the reviews.auto_review.auto_pause_after_reviewed_commits setting.

Use the following commands to manage reviews:

  • @coderabbitai resume to resume automatic reviews.
  • @coderabbitai review to trigger a single review.

Use the checkboxes below for quick actions:

  • ▶️ Resume reviews
  • 🔍 Trigger review
📝 Walkthrough

Walkthrough

The CLI adds --runs <n> support, repeats jobs, collects temporary results, aggregates runs using medians, preserves per-run metadata, handles dropped errors, and merges aggregated results into existing output.

Changes

Repeated eval runs

Layer / File(s) Summary
Run configuration and result contracts
AGENTS.md, packages/evals-core/src/types/results.ts, packages/evals/src/cli/config.ts, packages/evals/src/cli/validators.ts, packages/evals/tests/cli-config.test.ts
Adds validated runs configuration, result aggregation metadata, CLI documentation, and validation coverage.
Median result aggregation
packages/evals/src/persistence/results.ts, packages/evals/src/persistence/index.ts, packages/evals/tests/persistence.test.ts
Groups results by job key, computes median representatives, sums cost and token totals, handles errors, and tests the exported helpers.
Repeated subprocess execution and persistence
packages/evals/src/cli/run.ts, packages/evals/src/cli/subprocess-runner.ts, packages/evals/src/cli/index.ts, packages/evals/src/index.ts, packages/evals/tests/run.test.ts, packages/evals/tests/subprocess-runner.test.ts
Expands jobs per run, namespaces temporary files, collects temporary results, aggregates them before merging, and exposes and tests the collection helper.

Estimated code review effort: 3 (Moderate) | ~25 minutes

Sequence Diagram(s)

sequenceDiagram
  participant runCli
  participant spawnEval
  participant collectFromTempFiles
  participant aggregateRuns
  participant mergeIntoOutput
  runCli->>spawnEval: dispatch expanded per-run jobs
  spawnEval->>collectFromTempFiles: collect temporary results
  runCli->>aggregateRuns: aggregate collected results
  aggregateRuns->>mergeIntoOutput: pass aggregated results
  mergeIntoOutput->>runCli: save merged output
Loading

Suggested reviewers: frederikprijck, sanchitmehtagit

🚥 Pre-merge checks | ✅ 5
✅ Passed checks (5 passed)
Check name Status Explanation
Description Check ✅ Passed Check skipped - CodeRabbit’s high-level summary is enabled.
Title check ✅ Passed The title accurately summarizes the main change: adding the --runs flag with median-based scoring.
Docstring Coverage ✅ Passed Docstring coverage is 100.00% which is sufficient. The required threshold is 80.00%.
Linked Issues check ✅ Passed Check skipped because no linked issues were found for this pull request.
Out of Scope Changes check ✅ Passed Check skipped because no linked issues were found for this pull request.
✨ Finishing Touches
🧪 Generate unit tests (beta)
  • Create PR with unit tests
  • Commit unit tests in branch feat/runs-n-median-scoring

Thanks for using CodeRabbit! It's free for OSS, and your support helps us grow. If you like it, consider giving us a shout-out.

❤️ Share

Comment @coderabbitai help to get the list of available commands.

@coderabbitai coderabbitai Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Actionable comments posted: 3

🧹 Nitpick comments (2)
packages/eval/tests/persistence.test.ts (1)

481-481: 📐 Maintainability & Code Quality | 🔵 Trivial | 💤 Low value

Fix typographical error in test description.

The word "medianes" is a typo. It should be "medians".

♻️ Proposed refactor
-  it('medianes per-dimension scores', () => {
+  it('medians per-dimension scores', () => {
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@packages/eval/tests/persistence.test.ts` at line 481, Update the test
description in the “medianes per-dimension scores” test case to use the
correctly spelled “medians”, without changing the test behavior.
packages/eval/src/cli/subprocess-runner.ts (1)

44-53: 📐 Maintainability & Code Quality | 🔵 Trivial | ⚡ Quick win

Unify mergeIntoOutput with the shared results merge. Its local eval_id|model|mode|tools key joins tools without sorting or deduping, so it can drift from resultKey/mergeResults. Reuse the shared persistence helpers instead of maintaining a second dedup path.

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@packages/eval/src/cli/subprocess-runner.ts` around lines 44 - 53, The local
deduplication path in mergeIntoOutput must use the shared resultKey/mergeResults
persistence helpers so tools are normalized consistently. Update
packages/eval/src/cli/subprocess-runner.ts:44-53 to remove the local key logic
and delegate merging to the shared helpers; update
packages/eval/src/cli/index.ts:13-13 to use the same shared merge path where
this flow is wired, preserving temp-file collection, final output writing, and
cleanup.
🤖 Prompt for all review comments with AI agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

Inline comments:
In `@packages/eval/src/cli/run.ts`:
- Around line 459-466: Update the hasErrors calculation in the run flow to
inspect the pre-aggregation allFresh results rather than merged, preserving
failures.length and treating any fresh result with status "error" as a process
failure even when aggregateRuns removes it.

In `@packages/eval/src/cli/subprocess-runner.ts`:
- Around line 25-42: Add Vitest tests for the exported collectFromTempFiles
function covering flattening valid array payloads, skipping nonexistent temp
files, and ignoring corrupt JSON while cleaning up existing files. Use temporary
file fixtures and assert the returned records for each scenario.

In `@packages/eval/src/persistence/results.ts`:
- Around line 160-177: Update medianBaselineResult so graders_passed and
grader_pass_rate are derived from a single consistent aggregated value rather
than independently calculated medians. Preserve the existing median aggregation
semantics while ensuring the resulting fields cannot imply different pass rates;
use the appropriate shared baseline count or rate relationship for the group.

---

Nitpick comments:
In `@packages/eval/src/cli/subprocess-runner.ts`:
- Around line 44-53: The local deduplication path in mergeIntoOutput must use
the shared resultKey/mergeResults persistence helpers so tools are normalized
consistently. Update packages/eval/src/cli/subprocess-runner.ts:44-53 to remove
the local key logic and delegate merging to the shared helpers; update
packages/eval/src/cli/index.ts:13-13 to use the same shared merge path where
this flow is wired, preserving temp-file collection, final output writing, and
cleanup.

In `@packages/eval/tests/persistence.test.ts`:
- Line 481: Update the test description in the “medianes per-dimension scores”
test case to use the correctly spelled “medians”, without changing the test
behavior.
🪄 Autofix (Beta)

Fix all unresolved CodeRabbit comments on this PR:

  • Push a commit to this branch (recommended)
  • Create a new PR with the fixes

ℹ️ Review info
⚙️ Run configuration

Configuration used: defaults

Review profile: CHILL

Plan: Pro Plus

Run ID: 89af2cdd-4410-4ba0-b19c-c9a5555784f6

📥 Commits

Reviewing files that changed from the base of the PR and between 2027210 and 7e98796.

📒 Files selected for processing (11)
  • packages/eval-core/src/types/results.ts
  • packages/eval/src/cli/config.ts
  • packages/eval/src/cli/index.ts
  • packages/eval/src/cli/run.ts
  • packages/eval/src/cli/subprocess-runner.ts
  • packages/eval/src/cli/validators.ts
  • packages/eval/src/index.ts
  • packages/eval/src/persistence/index.ts
  • packages/eval/src/persistence/results.ts
  • packages/eval/tests/cli-config.test.ts
  • packages/eval/tests/persistence.test.ts

Comment thread packages/eval/src/cli/run.ts
Comment thread packages/evals/src/cli/subprocess-runner.ts
Comment thread packages/evals/src/persistence/results.ts
@yogeshchoudhary147

Copy link
Copy Markdown
Contributor Author

Re: CodeRabbit comment on hasErrors check (run.ts:466)

CodeRabbit flagged that hasErrors checks the post-aggregation merged array, which means if 1 of 3 runs errors but 2 succeed, the error gets dropped by aggregateRuns and the process exits 0.

We held this back intentionally — the fix is technically correct but the right behavior here is a product decision:

  • Exit 1 on any partial failure (CodeRabbit's suggestion): conservative, noisy in CI — exits 1 even when you got a valid median result from the other 2 runs, which defeats the purpose of --runs N
  • Exit 1 only when all runs for a job failed: more lenient, but partial failures are silently ignored
  • Log a warning but exit 0: best of both — surfaces the failure without breaking CI when a valid result exists

Happy to go with whichever direction the team prefers before merging.

@yogeshchoudhary147
yogeshchoudhary147 force-pushed the feat/runs-n-median-scoring branch 2 times, most recently from fc4074a to c2d8d70 Compare July 14, 2026 09:31
@yogeshchoudhary147

Copy link
Copy Markdown
Contributor Author

Resolved in 0a99fc7 with Option 3. Before aggregateRuns runs, we scan allFresh for errors whose job key also has at least one success in the same batch — those are the ones that would otherwise be silently dropped — and emit a logger.warn per dropped run. Process still exits 0 when a valid median exists; exit 1 is reserved for subprocess crashes and all-error groups.

@yogeshchoudhary147
yogeshchoudhary147 force-pushed the feat/runs-n-median-scoring branch from 0a99fc7 to f715bfe Compare July 14, 2026 10:08

@coderabbitai coderabbitai Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Actionable comments posted: 1

Caution

Some comments are outside the diff and can’t be posted inline due to platform limitations.

⚠️ Outside diff range comments (1)
AGENTS.md (1)

369-379: 📐 Maintainability & Code Quality | 🟡 Minor | ⚡ Quick win

Keep the documented --model all fallback list in sync.

The text says the fallback is the framework’s KNOWN_WORKING_MODELS, but the bullets omit claude-opus-4-6 and claude-opus-4-7, which are present in that constant. If models.known is empty, --model all will include them despite this documentation. Either list the fallback values accurately or label the bullets as app-specific.

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@AGENTS.md` around lines 369 - 379, The documented --model all list conflicts
with its stated KNOWN_WORKING_MODELS fallback by omitting claude-opus-4-6 and
claude-opus-4-7. Update the documentation around the eval.config.js model list
to include those fallback models, or explicitly label the bullets as
app-specific and separately document the complete fallback values.
🤖 Prompt for all review comments with AI agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

Inline comments:
In `@packages/eval/src/cli/validators.ts`:
- Around line 106-115: Update validateRuns to enforce a documented maximum
positive --runs value before returning, rejecting values above that maximum
through the existing invalid-value path. Define and reuse a named maximum
constant, and add tests covering the maximum accepted value and the first value
above it so runCli cannot expand an unbounded job list.

---

Outside diff comments:
In `@AGENTS.md`:
- Around line 369-379: The documented --model all list conflicts with its stated
KNOWN_WORKING_MODELS fallback by omitting claude-opus-4-6 and claude-opus-4-7.
Update the documentation around the eval.config.js model list to include those
fallback models, or explicitly label the bullets as app-specific and separately
document the complete fallback values.
🪄 Autofix (Beta)

Fix all unresolved CodeRabbit comments on this PR:

  • Push a commit to this branch (recommended)
  • Create a new PR with the fixes

ℹ️ Review info
⚙️ Run configuration

Configuration used: defaults

Review profile: CHILL

Plan: Pro Plus

Run ID: c70910cf-eba7-4c43-a615-d5b2ba7bcc89

📥 Commits

Reviewing files that changed from the base of the PR and between 9148989 and d1a62b6.

📒 Files selected for processing (14)
  • AGENTS.md
  • packages/eval-core/src/types/results.ts
  • packages/eval/src/cli/config.ts
  • packages/eval/src/cli/index.ts
  • packages/eval/src/cli/run.ts
  • packages/eval/src/cli/subprocess-runner.ts
  • packages/eval/src/cli/validators.ts
  • packages/eval/src/index.ts
  • packages/eval/src/persistence/index.ts
  • packages/eval/src/persistence/results.ts
  • packages/eval/tests/cli-config.test.ts
  • packages/eval/tests/persistence.test.ts
  • packages/eval/tests/run.test.ts
  • packages/eval/tests/subprocess-runner.test.ts
🚧 Files skipped from review as they are similar to previous changes (10)
  • packages/eval/src/persistence/index.ts
  • packages/eval/src/index.ts
  • packages/eval/tests/subprocess-runner.test.ts
  • packages/eval/src/cli/index.ts
  • packages/eval/src/cli/config.ts
  • packages/eval/tests/cli-config.test.ts
  • packages/eval/src/persistence/results.ts
  • packages/eval/src/cli/run.ts
  • packages/eval/src/cli/subprocess-runner.ts
  • packages/eval/tests/persistence.test.ts

Comment thread packages/evals/src/cli/validators.ts
Adds a --runs <n> CLI option (default: 1) that runs each eval job N times
in parallel and aggregates the results using median scoring. Scores are
medianed per-dimension; costs and tokens are summed across all runs. Raw
individual run results are embedded in a runs[] field for debugging, and
run_count records how many runs were aggregated.

When runs > 1, a run index suffix is appended to each temp file name to
avoid collisions, and collectFromTempFiles + aggregateRuns replaces the
previous mergeIntoOutput call in the subprocess path.
- Check pre-aggregation allFresh for hasErrors so partial run failures
  are not silently swallowed when aggregateRuns drops error results
- Derive grader_pass_rate from graders_passed/graders_total to keep
  both fields consistent after aggregation
- Add Vitest tests for collectFromTempFiles (7 cases)
- Fix typo: medianes → medians in test description
The hasErrors change (allFresh vs merged) needs further discussion —
reverting until the product decision on partial-run failure semantics
is resolved.
Sort and deduplicate tools in the local key function so it stays
consistent with the shared resultKey helper in persistence.
Addresses CodeRabbit pre-merge check — docstring coverage was at 77.78%
(below the required 80% threshold). Both internal aggregation helpers were
missing JSDoc comments.
…rsion

When --runs N was passed, buildSubprocessArgs forwarded it to every child
process. Each child saw runs > 1, took the subprocess path, and spawned N
more children — each of which did the same, causing exponential process
explosion until the runner was OOM-killed (exit 137).

Fix: add '--runs' to VALUE_FLAGS so it is stripped before forwarding.
Subprocesses run a single job once; run-count expansion is the parent's job.

Adds a regression test that would have caught this.
@yogeshchoudhary147
yogeshchoudhary147 force-pushed the feat/runs-n-median-scoring branch from d1a62b6 to 6914817 Compare July 22, 2026 17:24
…expansion

Without an upper bound, --runs 1000 would expand to 1000 subprocesses.
Adds a named MAX_RUNS constant (10), rejects values above it through the
existing invalid-value path, and adds boundary tests for the max and
first-over-max values.

@coderabbitai coderabbitai Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Actionable comments posted: 3

🧹 Nitpick comments (2)
packages/evals/src/persistence/results.ts (1)

229-234: 🚀 Performance & Scalability | 🔵 Trivial | ⚡ Quick win

findDroppedErrors is O(n²) — fine at current scale, but worth a linear rewrite.

For every error result it scans the full results array again to check for a same-key success, and resultKey re-derives the string each time. With evals × models × modes × runs potentially in the hundreds/thousands, this becomes a quadratic hot path. A single pass to build a Set of keys with successes would make this linear.

♻️ Suggested O(n) rewrite
 export function findDroppedErrors(results: JobResult[]): ErrorJobResult[] {
-  return results.filter(
-    (r): r is ErrorJobResult =>
-      r.status === 'error' && results.some((s) => s.status !== 'error' && resultKey(s) === resultKey(r)),
-  );
+  const successKeys = new Set(results.filter((r) => r.status !== 'error').map(resultKey));
+  return results.filter((r): r is ErrorJobResult => r.status === 'error' && successKeys.has(resultKey(r)));
 }
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@packages/evals/src/persistence/results.ts` around lines 229 - 234, Rewrite
findDroppedErrors to avoid the nested results.some scan: make one pass to
collect resultKey values for non-error results in a Set, then filter error
results by membership in that Set. Reuse each computed key within its pass so
resultKey is not repeatedly derived, while preserving the existing
ErrorJobResult selection behavior.
packages/evals/src/cli/subprocess-runner.ts (1)

44-56: 📐 Maintainability & Code Quality | 🔵 Trivial | ⚡ Quick win

Remove the unused mergeIntoOutput export and keep deduplication in one place.

mergeIntoOutput has no runtime callers; the CLI now uses loadResults/mergeResults/saveResults from persistence. Since it is still re-exported from packages/evals/src/index.ts and cli/index.ts, either remove the public export or move actual call sites to the persistence helpers to avoid duplicated tool-normalization behavior.

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@packages/evals/src/cli/subprocess-runner.ts` around lines 44 - 56, Remove the
unused mergeIntoOutput function and its public re-exports from
packages/evals/src/index.ts and cli/index.ts. Keep the CLI flow using
loadResults, mergeResults, and saveResults from persistence so tool
normalization and deduplication remain centralized.
🤖 Prompt for all review comments with AI agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

Inline comments:
In `@packages/evals-core/src/types/results.ts`:
- Around line 83-86: Define one consistent contract for run_count and runs in
aggregateRuns: specify whether they describe requested runs, successful runs, or
all raw outcomes, and ensure partial failures follow that contract. Update both
metadata declarations in packages/evals-core/src/types/results.ts at lines 83-86
and 148-151; add a regression test covering one successful run plus one error,
including the expected metadata and filtered results.

In `@packages/evals/src/persistence/results.ts`:
- Around line 130-160: Update medianAgentResult’s dimension aggregation to
handle runs with missing dimensions without dereferencing undefined entries. For
each representative dimension, filter the group to runs containing a dimension
at the matching index, compute the median from available scores, and preserve
the existing representative-dimension metadata and score-to-grade calculation;
ensure aggregation continues gracefully when some runs omit that dimension.

In `@packages/evals/tests/persistence.test.ts`:
- Line 17: Update the typed fixtures returned by makeBaseline, makeAgent, and
makeError to include the required judge_cost_usd and total_cost_usd fields,
using values consistent with the existing test fixture conventions while
preserving all other result data.

---

Nitpick comments:
In `@packages/evals/src/cli/subprocess-runner.ts`:
- Around line 44-56: Remove the unused mergeIntoOutput function and its public
re-exports from packages/evals/src/index.ts and cli/index.ts. Keep the CLI flow
using loadResults, mergeResults, and saveResults from persistence so tool
normalization and deduplication remain centralized.

In `@packages/evals/src/persistence/results.ts`:
- Around line 229-234: Rewrite findDroppedErrors to avoid the nested
results.some scan: make one pass to collect resultKey values for non-error
results in a Set, then filter error results by membership in that Set. Reuse
each computed key within its pass so resultKey is not repeatedly derived, while
preserving the existing ErrorJobResult selection behavior.
🪄 Autofix (Beta)

Fix all unresolved CodeRabbit comments on this PR:

  • Push a commit to this branch (recommended)
  • Create a new PR with the fixes

ℹ️ Review info
⚙️ Run configuration

Configuration used: defaults

Review profile: CHILL

Plan: Pro Plus

Run ID: e5954154-a176-4a73-85b6-e031506bc6b0

📥 Commits

Reviewing files that changed from the base of the PR and between d1a62b6 and 6914817.

📒 Files selected for processing (14)
  • AGENTS.md
  • packages/evals-core/src/types/results.ts
  • packages/evals/src/cli/config.ts
  • packages/evals/src/cli/index.ts
  • packages/evals/src/cli/run.ts
  • packages/evals/src/cli/subprocess-runner.ts
  • packages/evals/src/cli/validators.ts
  • packages/evals/src/index.ts
  • packages/evals/src/persistence/index.ts
  • packages/evals/src/persistence/results.ts
  • packages/evals/tests/cli-config.test.ts
  • packages/evals/tests/persistence.test.ts
  • packages/evals/tests/run.test.ts
  • packages/evals/tests/subprocess-runner.test.ts
🚧 Files skipped from review as they are similar to previous changes (1)
  • AGENTS.md

Comment thread packages/evals-core/src/types/results.ts Outdated
Comment thread packages/evals/src/persistence/results.ts
Comment thread packages/evals/tests/persistence.test.ts
@yogeshchoudhary147
yogeshchoudhary147 force-pushed the feat/runs-n-median-scoring branch from 0ab6a3d to 6953935 Compare July 23, 2026 02:18
- Add missing judge_cost_usd and total_cost_usd to makeBaseline,
  makeAgent, and makeError test fixtures so they satisfy the required
  type contract.
- Guard dimensions[i] access in medianAgentResult against runs that
  report fewer dimensions than the representative, falling back to the
  representative's own score instead of throwing a TypeError.
- Clarify run_count/runs JSDoc in BaselineJobResult and AgentJobResult:
  both fields are set whenever --runs N > 1, including when partial
  failures reduce successful runs to 1 (contradicting the old "absent
  when run_count is 1" wording).
- Add regression test: 1 success + 1 error → run_count: 1, runs: [ok].
@yogeshchoudhary147

Copy link
Copy Markdown
Contributor Author

Closing in favor of handling this on the runner side.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant