Skip to content

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

Open
yogeshchoudhary147 wants to merge 9 commits into
mainfrom
feat/runs-n-median-scoring
Open

feat(eval): add --runs N flag with median scoring#111
yogeshchoudhary147 wants to merge 9 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> option to repeat evaluation jobs and report median-based results.
    • Aggregated results now include run counts and optional per-run details.
    • Added handling for run-level errors and consolidated evaluation output.
  • Bug Fixes

    • Improved result merging and tool matching.
    • Temporary result files are reliably collected and cleaned up, including invalid files.

@coderabbitai

coderabbitai Bot commented Jul 14, 2026

Copy link
Copy Markdown

Review Change Stack

Warning

Review limit reached

@yogeshchoudhary147, you've reached your PR review limit, so we couldn't start this review.

Next review available in: 50 minutes

Enable usage-based reviews in Billing to review now. Otherwise, wait until the next included review is available.
You're only billed for reviews past your plan's rate limits ($0.25/file).

How can I continue?

After more reviews become available, a review can be triggered using the @coderabbitai review command as a PR comment. Alternatively, push new commits to this PR.

To avoid repeated limits, reduce automatic review volume by pausing incremental auto-reviews earlier, using label-based review opt-in, excluding WIP or generated PR titles, or requesting reviews manually when the PR is ready. If your team needs uninterrupted high-volume reviews, an organization admin can enable usage-based reviews.

How do review limits work?

CodeRabbit enforces per-developer PR review limits for each organization. Most developers receive the normal plan review availability.

For paid Pro and Pro+ PR reviews, CodeRabbit uses adaptive limits for sustained high-volume activity. When a developer's recent PR review activity reaches the 95th percentile or higher among CodeRabbit users, additional reviews become available more gradually as earlier reviews age out of the rolling window.

Please refer docs for additional details.

Review details
⚙️ Run configuration

Configuration used: defaults

Review profile: CHILL

Plan: Pro Plus

Run ID: 1fb458f8-befd-4a54-9aa2-42c06482de00

📥 Commits

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

📒 Files selected for processing (2)
  • packages/evals/src/cli/validators.ts
  • packages/evals/tests/cli-config.test.ts
📝 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: 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 clearly summarizes the main change: adding a --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
yogeshchoudhary147 and others added 8 commits July 22, 2026 22:50
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.

Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
@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.

Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>

@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 on lines +83 to +86
/** Number of runs aggregated into this result (absent when run_count is 1). */
run_count?: number;
/** Raw individual run results when run_count > 1. */
runs?: BaselineJobResult[];

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

🗄️ Data Integrity & Integration | 🟡 Minor | ⚡ Quick win

Define consistent metadata semantics for partial failures. aggregateRuns() filters errors before aggregation, so a requested two-run job with one success returns run_count: 1 and runs: [success]. That contradicts “absent when run_count is 1” and makes runs ambiguous.

  • packages/evals-core/src/types/results.ts#L83-L86: document or model whether these fields represent requested runs, successful runs, or all raw outcomes.
  • packages/evals-core/src/types/results.ts#L148-L151: apply the same contract and add a regression test for one success plus one error.

As per coding guidelines, “Add a regression test reproducing each bug before its fix and passing after the fix.”

📍 Affects 1 file
  • packages/evals-core/src/types/results.ts#L83-L86 (this comment)
  • packages/evals-core/src/types/results.ts#L148-L151
🤖 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-core/src/types/results.ts` around lines 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.

Source: Coding guidelines

Comment on lines +130 to +160
/** Aggregates a group of agent results into a single representative result using median scores and summed costs. */
function medianAgentResult(group: AgentJobResult[]): AgentJobResult {
const sorted = [...group].sort((a, b) => a.overall_score - b.overall_score);
const mid = Math.floor(sorted.length / 2);
const rep = sorted[mid]!;

const medianOverallScore = median(group.map((r) => r.overall_score));

const dimensions: DimensionSummary[] = rep.dimensions.map((dim, i) => {
const score = median(group.map((r) => r.dimensions[i]!.score));
return { ...dim, score, weighted: score * dim.weight, grade: scoreToGrade(score) };
});

return {
...rep,
overall_score: medianOverallScore,
overall_grade: scoreToGrade(medianOverallScore),
grader_pass_rate: median(group.map((r) => r.grader_pass_rate)),
wall_time: median(group.map((r) => r.wall_time)),
active_time: median(group.map((r) => r.active_time)),
tool_calls: Math.round(median(group.map((r) => r.tool_calls))),
interruptions: Math.round(median(group.map((r) => r.interruptions))),
tokens: group.reduce((sum, r) => sum + r.tokens, 0),
cost_usd: group.reduce((sum, r) => sum + r.cost_usd, 0),
judge_cost_usd: group.reduce((sum, r) => sum + r.judge_cost_usd, 0),
total_cost_usd: group.reduce((sum, r) => sum + r.total_cost_usd, 0),
dimensions,
run_count: group.length,
runs: group,
};
}

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

🩺 Stability & Availability | 🟡 Minor | ⚡ Quick win

Unsafe dimensions[i] indexing can crash if runs report different dimension counts.

rep.dimensions.map((dim, i) => median(group.map((r) => r.dimensions[i]!.score))) assumes every run in group has the same dimensions array length/order as rep. If any run's dimensions array is shorter (e.g. a grader failed to produce a dimension for one run), r.dimensions[i] is undefined and .score throws a TypeError, crashing the whole aggregation for that job/eval instead of degrading gracefully.

🐛 Proposed defensive fix
   const dimensions: DimensionSummary[] = rep.dimensions.map((dim, i) => {
-    const score = median(group.map((r) => r.dimensions[i]!.score));
+    const scores = group
+      .map((r) => r.dimensions[i]?.score)
+      .filter((s): s is number => typeof s === 'number');
+    const score = scores.length > 0 ? median(scores) : dim.score;
     return { ...dim, score, weighted: score * dim.weight, grade: scoreToGrade(score) };
   });
📝 Committable suggestion

‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.

Suggested change
/** Aggregates a group of agent results into a single representative result using median scores and summed costs. */
function medianAgentResult(group: AgentJobResult[]): AgentJobResult {
const sorted = [...group].sort((a, b) => a.overall_score - b.overall_score);
const mid = Math.floor(sorted.length / 2);
const rep = sorted[mid]!;
const medianOverallScore = median(group.map((r) => r.overall_score));
const dimensions: DimensionSummary[] = rep.dimensions.map((dim, i) => {
const score = median(group.map((r) => r.dimensions[i]!.score));
return { ...dim, score, weighted: score * dim.weight, grade: scoreToGrade(score) };
});
return {
...rep,
overall_score: medianOverallScore,
overall_grade: scoreToGrade(medianOverallScore),
grader_pass_rate: median(group.map((r) => r.grader_pass_rate)),
wall_time: median(group.map((r) => r.wall_time)),
active_time: median(group.map((r) => r.active_time)),
tool_calls: Math.round(median(group.map((r) => r.tool_calls))),
interruptions: Math.round(median(group.map((r) => r.interruptions))),
tokens: group.reduce((sum, r) => sum + r.tokens, 0),
cost_usd: group.reduce((sum, r) => sum + r.cost_usd, 0),
judge_cost_usd: group.reduce((sum, r) => sum + r.judge_cost_usd, 0),
total_cost_usd: group.reduce((sum, r) => sum + r.total_cost_usd, 0),
dimensions,
run_count: group.length,
runs: group,
};
}
/** Aggregates a group of agent results into a single representative result using median scores and summed costs. */
function medianAgentResult(group: AgentJobResult[]): AgentJobResult {
const sorted = [...group].sort((a, b) => a.overall_score - b.overall_score);
const mid = Math.floor(sorted.length / 2);
const rep = sorted[mid]!;
const medianOverallScore = median(group.map((r) => r.overall_score));
const dimensions: DimensionSummary[] = rep.dimensions.map((dim, i) => {
const scores = group
.map((r) => r.dimensions[i]?.score)
.filter((s): s is number => typeof s === 'number');
const score = scores.length > 0 ? median(scores) : dim.score;
return { ...dim, score, weighted: score * dim.weight, grade: scoreToGrade(score) };
});
return {
...rep,
overall_score: medianOverallScore,
overall_grade: scoreToGrade(medianOverallScore),
grader_pass_rate: median(group.map((r) => r.grader_pass_rate)),
wall_time: median(group.map((r) => r.wall_time)),
active_time: median(group.map((r) => r.active_time)),
tool_calls: Math.round(median(group.map((r) => r.tool_calls))),
interruptions: Math.round(median(group.map((r) => r.interruptions))),
tokens: group.reduce((sum, r) => sum + r.tokens, 0),
cost_usd: group.reduce((sum, r) => sum + r.cost_usd, 0),
judge_cost_usd: group.reduce((sum, r) => sum + r.judge_cost_usd, 0),
total_cost_usd: group.reduce((sum, r) => sum + r.total_cost_usd, 0),
dimensions,
run_count: group.length,
runs: group,
};
}
🤖 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 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.

aggregateRuns,
findDroppedErrors,
} from '../src/persistence/index.js';
import type { AgentJobResult, BaselineJobResult, DimensionSummary, ErrorJobResult } from '@a0/evals-core';

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

🎯 Functional Correctness | 🔴 Critical | ⚡ Quick win

🧩 Analysis chain

🏁 Script executed:

#!/bin/bash
rg -n -A48 '^function make(Baseline|Agent)' packages/evals/tests/persistence.test.ts
rg -n -A45 '^export interface (BaselineJobResult|AgentJobResult)' packages/evals-core/src/types/results.ts

Repository: auth0/auth0-evals

Length of output: 5808


🏁 Script executed:

#!/bin/bash
set -e
echo "== persistence.test.ts lines 1-85 =="
sed -n '1,85p' packages/evals/tests/persistence.test.ts
echo
echo "== results.ts type declarations relevant to error/job results =="
sed -n '1,220p' packages/evals-core/src/types/results.ts
echo
echo "== package scripts =="
python3 - <<'PY'
import json
from pathlib import Path
for p in ["package.json", "packages/evals/package.json", "packages/evals-core/package.json"]:
    path=Path(p)
    if path.exists():
        data=json.loads(path.read_text())
        print(f"{p} scripts:")
        print(json.dumps(data.get("scripts", {}), indent=2))
PY
echo
echo "== lockfile ts versions =="
if [ -f package-lock.json ]; then jq '.packages[] | select(.name=="typescript") | {path, version}' package-lock.json | head -80; fi

Repository: auth0/auth0-evals

Length of output: 9932


Add the required cost fields to the typed result fixtures.

BaselineJobResult, AgentJobResult, and ErrorJobResult require judge_cost_usd and total_cost_usd, but makeBaseline, makeAgent, and makeError omit them, so this package’s tests won’t type-check.

Proposed fix
 function makeBaseline(overrides: Partial<BaselineJobResult> = {}): BaselineJobResult {
   return {
     // ...
     cost_usd: 0.01,
+    judge_cost_usd: 0,
+    total_cost_usd: 0.01,
     // ...
   };
 }

 function makeAgent(overrides: Partial<AgentJobResult> = {}): AgentJobResult {
   return {
     // ...
     cost_usd: 0.05,
+    judge_cost_usd: 0,
+    total_cost_usd: 0.05,
     // ...
   };
 }

 function makeError(overrides: Partial<ErrorJobResult> = {}): ErrorJobResult {
   return {
     // ...
     cost_usd: 0,
+    judge_cost_usd: 0,
+    total_cost_usd: 0,
     // ...
   };
 }
🤖 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/tests/persistence.test.ts` at 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.

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