Skip to content

feat(bench): nous-bench framework — Phase 1 + 2#291

Open
Naima2002 wants to merge 25 commits into
mainfrom
bench-framework
Open

feat(bench): nous-bench framework — Phase 1 + 2#291
Naima2002 wants to merge 25 commits into
mainfrom
bench-framework

Conversation

@Naima2002

Copy link
Copy Markdown
Collaborator

Summary

  • Three-layer model: Campaign (question + target) → Variant (agent config) → Experiment (campaign × variants × budget)
  • nous variant wrapping nous run subprocess (Phase 1)
  • claude_plain variant wrapping headless Claude Code (Phase 2)
  • ThreadPoolExecutor parallel runner with --max-parallel-variants flag
  • Claude-as-judge accuracy scoring with pinned bench/judge_prompt.md
  • Markdown report renderer with Correctness / Completeness columns
  • Two live smoke tests on blis_prefix validated end-to-end (~$11 spend); both surfaced real comparison-fairness bugs that unit tests couldn't catch
  • 79 unit tests, 1.3s, no live API calls

Related Issue

Tracks #283. Living draft PR for benchmark tracking — bench-framework keeps accumulating commits as sub-issues land:

  • A: include principles.json in nous final_answer for fair judging
  • B: enable claude_methodology, claude_loop, claude_methodology_loop (+ bootstrapped methodology.md)
  • C: variant workspace isolation hardening

Test Plan

  • python3 -m pytest tests/test_bench_*.py → 79 passed
  • python3 -m bench list shows registered campaigns + variants
  • python3 -m bench run bench/experiments/phase2_compare.yaml smoke run on blis_prefix with claude_plain + nous in parallel + judge
  • /pr-review-toolkit:review-pr round once sub-issues close

🤖 Generated with Claude Code

Naima Abrar and others added 13 commits June 15, 2026 23:14
Phase 0 skeleton + Phase 1.1 contract for the nous-bench framework.
Defines the Variant protocol and the VariantResult, Campaign,
Experiment, and Budget dataclasses in bench/variants/base.py. No
runtime behavior yet.

Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
Phase 1.2: first concrete campaign and experiment yamls plus tests
covering Campaign.from_yaml and Experiment.from_yaml parsers and the
optional max_wall_seconds field. 4/4 tests pass.

Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
Loads experiment configs, validates requested variants, clones the target repo into an isolated workspace per variant, runs variants sequentially, and writes per-variant and combined result JSON files.

Adds runner and isolation tests; 30/30 bench tests passing.
Adds markdown report generation from results.json and the python3 -m bench CLI with run/list commands. Wires report generation into the runner and adds tests for report rendering and CLI parsing.
Runs plain Claude Code headlessly with no methodology, parses JSON output into VariantResult, registers the variant in the runner, and adds parser tests for success and error cases.
Phase 2.2: bench/metrics.py centralizes parsing of `claude --print
--output-format json`. parse_claude_json normalizes to {final_answer,
tokens_in (billable only), tokens_out, dollars, is_error, subtype,
num_turns}. LLMMeter dataclass accumulates across multiple Claude calls
(used by Phase 4 loop variants). claude_plain refactors to use it.
Parser tests migrate to test_bench_metrics.py.
ThreadPoolExecutor swap (per agent plan-review note 1 — variants spawn
their own subprocesses, threads avoid pickle without losing concurrency).
Default --max-parallel-variants is min(N, cpu_count). Output order
matches input order regardless of completion order.

bench/judge.py + bench/judge_prompt.md: a separate Claude session scores
each variant's final_answer on correctness (0-10) and completeness
(0-10). Crashed variants are skipped (scores=None). Judge prompt is
committed and pinned. Token cost in a top-level results.json:judge_usage
field, never per-variant.

Runner wires judge to run after variants complete; --skip-judge for
debugging. New phase2_compare.yaml runs claude_plain + nous on
blis_prefix.
Phase 2.7: smoke test revealed nous's final_answer was just
discrepancy_analysis — a one-paragraph cross-arm summary stripped of all
the per-arm experimental data (predicted/observed/status/diagnostic_note).
The judge correctly scored that 3/2 because what it saw had no numbers,
no mechanism, and no evidence — while claude_plain's full prose got 8/8.
The bug was in extraction, not in the judge.

_read_final_answer now renders the canonical nous structure (arms[] +
discrepancy_analysis + experiment_valid) as a single comparison-friendly
multi-section string when arms are present. The new output for the
smoke run grows from 375 chars (summary only) to 2,393 chars including
per-arm predictions, observations with real TTFT measurements, and
status flags.

3 new tests cover the rich rendering + the fall-through path.
Phase 2 report integration: Correctness / Completeness columns added
when judge scores are present in results.json, judge_usage shown in
header, per-variant rationale section.

Tokens column dropped
_read_final_answer now renders the full structured set nous produces:
- All iter-N findings.json (not just latest), in ascending order
- principles.json (cumulative cross-iteration knowledge)
- ledger.json (per-iter outcome rows with prediction_accuracy)
- report.md (when nous wrote one; gracefully skipped otherwise)

Same shape as Phase 1.7.1 and Phase 2.7 fixes — closing the
extraction-too-narrow loophole for good by widening once. Function
signature changes to take artifacts_dir instead of runs_dir; call
site in NousVariant.run() updated accordingly.

18 new tests covering each source + multi-iteration end-to-end +
missing-source fall-through + double-digit iter ordering + lenient
principle status filtering. 97/97 bench tests pass.

Verified against runs/phase2_compare_with_fix/nous/.../ — all 3 RP-N
principles + iter-1 findings + ledger row appear together (6,869
chars), and report.md absence is handled gracefully.
@Naima2002

Copy link
Copy Markdown
Collaborator Author

Sub-issue A (#292) closed. Commits 220ad73 + 295e12c widen _read_final_answer to render all nous artifacts (findings + principles + ledger + report) plus docs at docs/bench/final-answer-rendering.md. 97/97 tests pass. PR review: ready. Next: sub-issue B.

Naima Abrar added 7 commits June 16, 2026 18:24
claude_plain.py shrinks from ~107 lines to ~25 by delegating subprocess +
parsing + result aggregation to bench/variants/_claude_common.py. New
helpers (ClaudeInvocation/ClaudeRunResult dataclasses, invoke_claude(),
variant_result_from()) are designed to be reused by the three new variants
in the next batches (claude_methodology, claude_loop,
claude_methodology_loop).

variant_result_from supports both single-call (claude_plain,
claude_methodology) and multi-call loop variants. Aggregation rules:
sum tokens/dollars/wall, last non-crashed final_answer, any crashed
trips the variant, summed-total hit_cap, multi-run logs concatenated
into one variant-level log file.

DEFAULT_MODEL re-exported from claude_plain for backward compat.

18 new tests for the shared helpers covering each invoke_claude crash
path and each variant_result_from aggregation rule. 115/115 bench
tests pass (was 97; +18).
Single Claude Code session with methodology.md injected as
--append-system-prompt. The L1 baseline: tests whether nous's methodology
delivered as a prompt produces the same outcomes as nous's methodology
delivered through orchestration.

Implementation: thin wrapper over bench/variants/_claude_common.py's
invoke_claude(), passing system_prompt=methodology_body. Reads
bench/methodology/methodology.md on each call; fails fast with a clear
error if the file is missing. Registered in runner.VARIANT_REGISTRY;
`python3 -m bench list` now shows it alongside nous and claude_plain.

6 new tests: variant identity, methodology threading into system_prompt,
graceful crash on missing methodology.md, metric pass-through, crash
propagation, and a sanity check that METHODOLOGY_PATH points at the
committed file. 121/121 bench tests pass.
ClaudeLoopVariant runs claude_plain N times sequentially (N from
budget.max_iterations). Each call after the first prepends the previous
session's full final_answer to the user message with a clear separator;
no methodology, no principle extraction, no schema enforcement. Tests
'iteration alone' — does running more times help, or does the agent drift
without structure?

Loop short-circuits on first crash. Per-iter logs land at
workspace/.bench-claude-loop-iter-{n}.log; variant_result_from
concatenates them into a single workspace/.bench-claude_loop.log at the
end. Aggregation per the shared rules in _claude_common: sum
tokens/dollars/wall, last non-crashed answer.

Registered in runner.VARIANT_REGISTRY. `python3 -m bench list` now
shows 4 variants.

11 new tests covering: variant identity, max_iterations call count,
short-circuit on crash, first-iter question pass-through, prepend logic
on subsequent iters, no-history-accumulation (only most recent), metric
aggregation, single-iter degenerate case, distinct per-iter log paths,
last-non-crashed final_answer, partial-crash recovery. 132/132 bench
tests pass.
Final batch closing #293:

- bench/experiments/phase4_full_sweep.yaml: runs all 5 variants on
  blis_prefix in parallel (live smoke costs ~$15-25; not run as part
  of this PR)
- docs/bench/walkthrough.md: comprehensive overview — what the bench
  does, three-layer model, what the user sees, what happens in the
  background, all 5 variants explained, parallel execution, output
  layout, token accounting, the 1.7.1 / 2.7 / 2.8 pattern, test
  architecture (161 tests), design choices
- docs/bench/variants.md: variant Protocol + VariantResult contract,
  the 5 currently-registered variants and what each isolates, 5-step
  recipe for adding a new variant, standards every variant must
  follow (token accounting, final_answer fidelity, no live LLM calls
  in tests, behavioral testing only)
- README.md: new 'Benchmark harness' section before 'Project Structure'
  pointing at docs/bench/; Project Structure tree updated to include
  bench/ subdirectories
- docs/architecture.md: brief 'Bench harness' subsection at end
  pointing at docs/bench/walkthrough.md

161/161 bench tests still pass. Sub-issue #293 acceptance fully met:
- All 5 variants registered, `python3 -m bench list` shows them
- phase4_full_sweep.yaml in place
- methodology.md committed and untouched
- User-facing docs added under docs/bench/
… w/ principle carry-forward (#293)

Should have landed before commit 653a15b (the docs commit). The variant
file, its test file, and the runner.py registry registration were left
uncommitted, leaving PR #291 in an inconsistent state where docs and
phase4_full_sweep.yaml reference claude_methodology_loop but the runner
doesn't register it. Fresh clones would crash with 'Unknown variants:
[claude_methodology_loop]'. PR review (workflow Step 7) caught this.

ClaudeMethodologyLoopVariant runs claude_methodology N times sequentially
with deterministic principle extraction (regex-based) and carry-forward
between sessions. Principles accumulate across iterations without
nous's structural enforcement (no schema validation, no dedup, no
conflict detection) — that absence is the L2 experimental signal.

Includes _extract_principles regex with 17 edge-case tests + variant
wiring tests. 161/161 bench tests pass.
@Naima2002

Copy link
Copy Markdown
Collaborator Author

Sub-issue B (#293) closed — bench framework has all 5 variants live-validated on blis_prefix.

iter=1 ladder: L0 8/7 → L0+iter 7/8 → L1 9/9 → L2(degenerate) 9/9 → L3 8/8 ($20)
iter=3 pilot: L2 8/9 vs L3 9/7 (
$19)

Headline: at iter=3, L3's complexity-tier discipline forced hypothesis-family escalation (prefix-cache-ttft → dose-response → kv-pressure), surfacing a 60× TTFT regime change at constrained KV cache. The prompt-based variant did not find this — it parameterized the original hypothesis along input length instead. Judge's current 2-axis rubric (correctness + completeness) understates this asymmetry.

Next: 11-metric selectable judge rubric (separate sub-issue), then 5-system L0/L1/L2/L3 ablation for the paper.

Naima Abrar added 5 commits June 18, 2026 12:02
#295)

  judge_prompt.md is now a frame template; per-metric rubric language lives in
  METRIC_RUBRICS in judge.py. 11 metrics: correctness, completeness, novelty,
  coverage, diagnostic_value, reproducibility, iter_coherence, principle_yield,
  causal_explanation_depth, transferability, structured_artifact_production.

  6 named presets: default, ablation-single-iter, ablation-multi-iter,
  case-study, transferability, minimal. Selectable via --judge-metrics (csv)
  and/or --judge-preset. iter_coherence auto-dropped on single-iter runs.

  New 'bench rejudge' subcommand re-runs the judge over an existing
  results.json without re-running variants — lets us iterate on the rubric
  cheaply.

  New 'bench import-nous' subcommand converts an existing nous campaign
  directory into a bench-compatible results.json. Auto-detects flat vs
  nested artifact layouts. Path-agnostic so anyone helping with experiments
  can run it on their machine. Optional --merge-baselines flag combines
  imported nous result with an existing baselines run for unified rejudging.

  JudgeScore.scores is now dict[str, int|None] for variable metrics; convenience
  accessors .correctness and .completeness preserved for backward compat.

  Validation: rejudged phase4_iter3 under all 11 metrics across Sonnet (3x)
  and Opus (1x). Rubric directionally captures structural-enforcement wins for
  nous (reproducibility +3, structured_artifact_production +3, iter_coherence
  +2, causal_explanation_depth +2) vs methodology's coverage win (-2 completeness,
  -1 coverage). Inter-rater spread: 8/11 metrics within +/-1 across 3 Sonnet
  runs; 3 metrics drift to +/-2 (completeness, coverage, structured_artifact).

  Also fixes a pre-existing bug in _harvest_metrics where 'cost_usd: null'
  rows in some campaigns crashed the parser.

  bench/experiments.md added — the Component 1 ablation plan: 5 systems,
  how to run each, where to look after.

  Tests: 240 passing (was 161). New test files: test_bench_import_nous.py
  (20 tests). Test seam unchanged: no live LLM calls.
Replaces strict-format principle extraction with permissive takeaway
extraction. Rationale: the structured principle store with regime /
mechanism / applicability_bounds / confidence is nous's novelty; giving
the L2 baseline a regex-knockoff version of it understates nous's
structural-enforcement claim in the L0/L1/L2/Nous ablation.

Changes:
- methodology.md: soft suggestion to end with 'Key takeaways from this
  iteration' bullet section. Replaces the strict ## Principles requirement.
- _extract_principles renamed to _extract_takeaways (alias kept for
  backward compat). Heading regex broadened to match: Key Takeaways,
  Takeaways, What I Learned, Lessons Learned, Key Findings, Summary,
  plus Principles for back-compat with the old methodology.
- New fallback: when extraction returns empty, prepend the FULL previous
  iter answer to the next iter's prompt (same mechanism as claude_loop).
  Guarantees claude_methodology_loop >= claude_loop on memory richness.
- Extracted _build_next_question helper for testable prompt construction.

Tests: +15 (45 total in this file). Pin: each new heading variant,
back-compat with Principles heading, unrelated headings (Conclusion) don't
match, [T1] ID prefix stripping works same as old [P1], full-answer
fallback path triggers on empty extraction, takeaway-present path does
NOT dump full answer.
…ut L2 (#295)

Pivot the L0/L1/L2 prompt design to match what an engineer would actually
paste into Claude. Methodology is inlined in the user prompt instead of
passed via --append-system-prompt; L2's carry-forward is the full text of
all prior iterations pasted under a '--- Prior findings ---' marker
instead of regex-extracted takeaways.

L0 (claude_plain) prompt:
  <research_question>

  Investigate this and report your findings.

L1 (claude_methodology) prompt:
  <research_question>

  Approach this systematically:
  - Form hypotheses before running experiments. Predict what
    you expect to observe and why.
  - Run controlled experiments.
  - When your prediction is wrong, think carefully about why.
  - Track what you learn.

  Report your findings with the evidence that supports them.

L2 (claude_methodology_loop) iter-1 prompt:
  <research_question>

  Approach this systematically:
  - Form hypotheses before running experiments. ...
  - Run controlled experiments.
  - When your prediction is wrong, ...
  - Track what you learn. Build on prior findings rather than
    starting from scratch each time.

  This is iteration 1 of N. Report your findings and what you would
  investigate next.

L2 iter-K (K>1) prompt: same as iter-1, then 'This is iteration K of N.
Here is what you found in prior iterations: --- Prior findings --- ...
--- Continue your investigation. Build on what you found before.'

The 'Build on prior findings rather than starting from scratch each time'
clause is L2-only and lives in a separate methodology_loop.md, since L1
is single-session and the cross-iteration framing would be off-message.

Changes:
- methodology.md: stripped to four short scientific bullets (L1 form).
- methodology_loop.md: NEW. Same four bullets but the last carries the
  cross-iteration 'Build on prior findings' clause.
- claude_plain.py: appends 'Investigate this and report your findings.'
  to the research question for L0.
- claude_methodology.py: drops --append-system-prompt; builds L1 user
  prompt with methodology inlined and the 'Report your findings' closing.
- claude_methodology_loop.py: drops _extract_takeaways and the fallback
  branching entirely; per-iter builders inline methodology_loop.md, add
  iteration framing, and paste all prior iters' full output verbatim.
  Reads from METHODOLOGY_LOOP_PATH (the new file), not METHODOLOGY_PATH.
- experiments.md: scrubbed of machine-specific paths. Now uses
  BENCH_NOUS_CAMPAIGNS / BENCH_BLIS_REPO / BENCH_GRAPH_COLORING_REPO /
  BENCH_FLINK_REPO env vars so anyone can follow the doc verbatim.

Tests: 229 passing.
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