Skip to content

Bench: harbor + swe-bench generation mode with runtime hardening (+ a templated dataset card)#17

Draft
Quantumlyy wants to merge 8 commits into
TeichAI:mainfrom
AletheiaResearch:bench-and-verifiable-tasks
Draft

Bench: harbor + swe-bench generation mode with runtime hardening (+ a templated dataset card)#17
Quantumlyy wants to merge 8 commits into
TeichAI:mainfrom
AletheiaResearch:bench-and-verifiable-tasks

Conversation

@Quantumlyy

Copy link
Copy Markdown
Contributor

Summary

Adds a benchmark generation mode — teich generate --mode bench — that runs reward-labeled benchmark tasks through a pluggable backend (harbor or swe-bench), harvests each native trace routed by its verifier score into output/{passed,failed,borderline}/, and hardens the run for real Docker use (per-task image purge, working Ctrl-C). Everything is opt-in: --mode prompts (the default), github_repo, and all existing behavior are unchanged, bench lives behind the teich[harbor] / teich[swe] extras, and the only new core runtime dependency is jinja2>=3.1 (previously dev-only).

What's included

1. teich generate --mode bench

teich generate grows a --mode {prompts,bench} flag (default prompts) and routes bench through a shared driver + harvest so backends stay thin.

  • src/teich/bench/runner.py run_bench(cfg, console, resume, refresh) is imported lazily from cli.py so harbor / swebench stay optional extras; tasks run through a bounded pool sized by max_concurrency (default 1).
  • src/teich/bench/backends/base.py defines the BenchBackend protocol (require / tasks / run, where run returns a native trace plus a rewards dict) plus reward routing (primary_score, route_split: score 1 -> passed, 0/unscored -> failed, anything else -> borderline) and harvest().
  • Native traces land in output/{passed,failed,borderline}/ with scores + provenance written to output/metadata/<stem>.json; filenames are namespaced bench-<source>-<task>.
  • Bench stays its own dataset, never co-mingled with prompts: rows are written as bench-<task>.jsonl, bench/** is added to UPLOAD_IGNORE_PATTERNS, and a mode guard warns against mixing bench and prompt rows.
  • src/teich/converter.py excludes bench / verification / sandbox intermediates from the dataset view, while verifier sidecars under metadata/ are still uploaded as dataset provenance — this is what makes the dataset "verifiable": each row carries its task's reward outcome.
  • src/teich/config.py adds bench.sources (per-source type / source / split / instances / repo / version / backend) and output.bench_dir (defaults to a sibling bench dir beside traces_dir, outside the dataset); config.example.yaml documents both.
  • --resume skips already-harvested bench tasks; --refresh re-downloads remote harbor sources only (swe-bench uses Hugging Face's own dataset cache).

2. Harbor backend

  • src/teich/bench/backends/harbor.py runs Harbor-format tasks by driving the harbor package as a library (registry spec, local task dir, or git/HF registry) and reads the verifier's reward dict off the trial env.
  • Installed via teich[harbor] (harbor>=0.15.0, Python >= 3.12).

3. SWE-bench backend

SWE-bench ships an evaluation harness, not runnable tasks, so teich runs the agent itself and grades what it produces.

  • src/teich/bench/backends/swebench.py checks out the clean repo @ base_commit inside swebench's prebuilt instance image, renders a thin agent layer on top (src/teich/bench/templates/agent.dockerfile.j2), takes the agent's git diff as the candidate patch, and grades it with swebench.run_instance; the resolved verdict becomes the reward (resolved -> passed, unresolved -> failed).
  • Installed via teich[swe] (swebench>=4.1.0); teich[bench] = teich[harbor,swe].

4. Ctrl-C actually stops a run

The driver ran tasks in a ThreadPoolExecutor, but SIGINT only reaches the main thread and with ThreadPoolExecutor does shutdown(wait=True), so Ctrl-C looked stopped while Docker ground on in the workers.

  • max_concurrency == 1 now runs each task inline on the main thread, so Ctrl-C propagates straight into the Docker call and stops cleanly (the smoke-test / --resume path).
  • max_concurrency > 1 cancels queued tasks (cancel_futures) and lets the few already inside a Docker call finish before exit (threads can't be signalled).
  • Both paths print a "re-run with --resume" hint.

5. Per-task Docker image purge

harbor's teardown runs compose down --rmi local, which does not remove its custom-tagged hb__<task> image, so per-task images accumulated and filled the disk.

  • On every outcome (passed / failed / error) teich removes the per-task image in a finally, gated by output.keep_bench_images (default false = purge).
  • harbor: removes hb__<task> read off the trial env, with a deterministic sanitize("hb__" + task) fallback covering the error path where the trial never returned.
  • swe-bench: removes the per-task agent-layer image but leaves the shared instance image in place (reused, expensive to re-pull).

6. Templated dataset card

src/teich/trace_readme.py is reworked to render the dataset card from src/teich/templates/dataset_card.md.j2 off a presentation-only context dict — byte-identical to the previous hand-built card, locked by a golden test.

  • output.license adds an SPDX id to the frontmatter.
  • output.card_extra merges arbitrary extra frontmatter keys (reserved keys dropped, dumped via yaml.safe_dump).
  • output.readme_template points at a user .j2 to fully override the card (rendered with the same context); a field_validator errors at config load if the file is missing.
  • size_categories is auto-populated from the dataset row count using HF's standard buckets (n<1K ... n>1T).
  • jinja2>=3.1 is promoted from a dev-only to a core dependency; both templates ship in the wheel.

Validation

  • uv run --extra dev python -m pytest -q -> 612 passed, 27 skipped
  • uv run --extra dev python -m pytest -q tests/test_bench.py tests/test_swebench.py tests/test_trace_readme.py tests/test_cli.py tests/test_converter.py -> 168 passed, 2 skipped (bench 31, swe-bench 21, trace_readme 30, cli 29, converter 59)
  • Reward routing is covered directly: test_rewards_resolved, test_rewards_unresolved_routes_to_failed, test_rewards_resolved_routes_to_passed
  • The card is byte-locked: test_rendered_card_matches_golden_byte_for_byte, plus test_size_category_bucket_boundaries, license, card_extra, and custom-template-override tests
  • uv run --extra dev ruff check src/teich/bench src/teich/trace_readme.py src/teich/config.py src/teich/cli.py -> All checks passed
  • uv build --wheel -> built teich-0.2.8-py3-none-any.whl; confirmed teich/templates/dataset_card.md.j2 and teich/bench/templates/agent.dockerfile.j2 are packaged

Compatibility

--mode prompts (the default), github_repo, and every existing config key behave exactly as before; bench is opt-in behind teich[harbor] / teich[swe] / teich[bench], the new output knobs all default to today's behavior (keep_bench_images defaults to purge, the card renders byte-identically), and the only new core runtime dependency is jinja2>=3.1 (previously dev-only).

Note: the offline surface (dataset loading, Dockerfile render, reward mapping, backend selection/registration) is unit-tested above; the swe-bench Docker build -> run agent -> grade path is integration-validated only, since CI has no Docker.

Squashed fork delta off upstream/main, bundling the work that landed on the
AletheiaResearch fork via PR #5 and PR #7:

- bench mode: drive Harbor as a library and add a swe-bench backend under a
  single `generate --mode bench` flow (its own dataset, no mode co-mingling).
- seed-workspace verifiable tasks: exit-code-reward verifier that runs over a
  writable snapshot (binary reward via VerificationResult).
- bench runtime hardening: purge per-task Docker images after each run
  (gated by output.keep_bench_images), make Ctrl-C actually stop the run, and
  render the dataset card from a configurable Jinja template
  (output.license / card_extra / readme_template).
@armand0e armand0e self-assigned this Jul 1, 2026
@armand0e

armand0e commented Jul 1, 2026

Copy link
Copy Markdown
Collaborator

@codex review

@chatgpt-codex-connector chatgpt-codex-connector 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.

💡 Codex Review

Here are some automated review suggestions for this pull request.

Reviewed commit: 43c8ce30a6

ℹ️ About Codex in GitHub

Your team has set up Codex to review pull requests in this repo. Reviews are triggered when you

  • Open a pull request for review
  • Mark a draft as ready
  • Comment "@codex review".

If Codex has suggestions, it will comment; otherwise it will react with 👍.

Codex can also answer questions or update the PR. Try commenting "@codex address that feedback".

Comment thread src/teich/trace_readme.py Outdated
Comment thread src/teich/bench/backends/harbor.py Outdated
Comment thread src/teich/bench/backends/base.py Outdated
Comment thread src/teich/config.py
Comment thread src/teich/cli.py
Comment thread src/teich/bench/backends/base.py
- trace_readme: fix data_files regression (P1) — the templated-card train
  fallback advertised only top-level *.jsonl, so nested extractions (Cursor's
  project-relative transcripts) were counted in the card but excluded from HF
  data_files, publishing an empty/incomplete dataset. Now advertise a recursive
  <dir>/**/*.jsonl for each data-bearing, non-excluded subdir.
- trace_readme: card reward summary now also reads bench's metadata/<stem>.json
  sidecars (split + numeric reward), not just verification/<stem>.json (P3), so
  bench datasets show their pass/fail counts instead of just the tag.
- bench/backends/base: bound the instances discriminator in source_id (P2) — a
  large SWE-bench subset no longer leaks its full comma-joined list into
  filenames / Docker tags / container names past OS/Docker length limits (hash
  when long; sorted, so listing order is irrelevant).
- bench/backends/harbor: key the source download cache on repo too (P2), matching
  source_id, so two sources with the same spec/version but different repos don't
  share a cache dir and reuse the wrong tasks.
- config: resolve a relative output.readme_template against the config file like
  prompts_file (P2), not the process CWD.
- bench/runner: fail fast when agent.provider=codex + use_host_auth and no API
  key (P2) — bench backends don't seed the host-auth snapshot into the task
  container, so this would otherwise launch a silently-unauthenticated run.

Full suite 618 passed / 27 skipped; ruff clean.
@Quantumlyy

Copy link
Copy Markdown
Contributor Author

Thanks for the review pass. All six findings verified against the code and addressed in d943875:

  • P1 data_files regression — the templated-card train fallback dropped nested extractions (Cursor project-relative transcripts) from HF data_files while still counting them; now emits a recursive <dir>/**/*.jsonl per data-bearing subdir.
  • P2 Harbor source cache now keyed on repo (matches source_id).
  • P2 instances discriminator bounded (hashed when long, sorted) so big SWE-bench subsets can't overflow filename / Docker-tag / container-name limits.
  • P2 relative output.readme_template now resolves against the config file, like prompts_file.
  • P2 bench now fails fast on agent.provider: codex + use_host_auth with no API key (full host-auth wiring into bench containers tracked as a follow-up).
  • P3 card reward summary now reads bench's metadata/ sidecars, so bench datasets show pass/fail counts.

Each fix has a regression test (test_card_data_files_reach_nested_extractions, test_harbor_source_slug_keys_on_repo, test_source_id_bounds_large_instance_list, test_config_readme_template_resolves_relative_to_config_dir, test_run_bench_rejects_codex_host_auth, test_reward_stats_from_bench_metadata_sidecars). Full suite: 618 passed / 27 skipped; ruff clean.

@Quantumlyy

Copy link
Copy Markdown
Contributor Author

@codex review

@chatgpt-codex-connector chatgpt-codex-connector 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.

💡 Codex Review

Here are some automated review suggestions for this pull request.

Reviewed commit: d943875c5f

ℹ️ About Codex in GitHub

Your team has set up Codex to review pull requests in this repo. Reviews are triggered when you

  • Open a pull request for review
  • Mark a draft as ready
  • Comment "@codex review".

If Codex has suggestions, it will comment; otherwise it will react with 👍.

Codex can also answer questions or update the PR. Try commenting "@codex address that feedback".

Comment thread src/teich/bench/backends/base.py
Comment thread src/teich/bench/backends/swebench.py Outdated
Comment thread src/teich/trace_readme.py Outdated
Comment thread src/teich/converter.py
Comment on lines +27 to +28
NON_DATA_TRACE_DIR_NAMES = frozenset(
{"partials", "failures", "verification", "sandbox", "__pycache__", "bench"}

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

P2 Badge Limit non-data directory skips to output sidecars

Because _jsonl_files checks these names against every path component, any real trace data organized under a directory named bench, verification, or sandbox is now silently dropped by teich convert, README generation, and related dataset scans. This is easy to hit with extracted or manually staged datasets, such as a Cursor workspace/repo named bench; the skip should be scoped to known top-level Teich sidecar directories instead of arbitrary nested folders.

Useful? React with 👍 / 👎.

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

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

Deferring this one — reasoning: the per-component non-data-dir match is pre-existing upstream behavior (it applied to partials/failures before this PR); this PR only expanded the name set. The nested-name exclusion is intentional and documented (# the name is listed here only to defend a nested override), and the cited Cursor scenario doesn't actually trip it — cursor dirs are slugified full paths like c-Users-...-bench, which aren't an exact bench component match. Scoping the skip to top-level would flip that documented behavior and needs to touch ~6 call sites (converter/trace_readme/cli/runner) to be consistent, so it's better as its own PR than folded into this review round. Happy to open that follow-up if you'd like it.

Comment thread src/teich/bench/backends/harbor.py
Comment thread src/teich/bench/backends/base.py Outdated
…erred)

- bench/backends/base: harvest now deletes any sibling-split copy before writing
  (P2) — a re-run (no --resume) whose score crosses a routing boundary otherwise
  left a stale, contradictorily-labeled row that the dataset scanners still read.
- bench/backends/base: guard the *default* bench_root against the output dir too
  (P2), not just an explicit output.bench_dir — e.g. --output bench made the
  sibling default collide with the dataset and upload raw trials as rows.
- trace_readme: count borderline bench tasks in the card reward summary (P3);
  they were dropped, so mixed runs undercounted and an all-borderline run showed
  no reward section. Card shows '.. / N borderline' only when present.
- config: resolve a relative *local* bench source (./tasks, ../x, ~/x) against
  the config file like prompts_file (P2), not the process CWD.
- bench/backends/swebench: fail fast when the claude-code agent is paired with a
  non-anthropic api.provider (P2) — the container's  CLI has no usable
  ANTHROPIC_* creds otherwise and would emit empty traces.

Deferred: the converter's per-component non-data-dir skip (Codex P2) — pre-existing
behavior, the nested-name exclusion is intentional/documented, and top-level-only
scoping is a behavior change across ~6 call sites; better as its own PR.

Full suite 623 passed / 27 skipped; ruff clean.
@Quantumlyy

Copy link
Copy Markdown
Contributor Author

Round 2 addressed in 9098444 — 5 fixed, 1 deferred (each verified against the code):

  • P2 harvest deletes the sibling-split copy before writing, so a re-run that reroutes a task can't leave a duplicate contradictorily-labeled row.
  • P2 bench_root guards the computed default (not just explicit output.bench_dir) — --output bench no longer collides bench intermediates with the dataset.
  • P2 relative local bench source (./tasks) resolves against the config file, like prompts_file.
  • P2 claude-code + non-anthropic api.provider now fails fast (the container's claude CLI has no usable ANTHROPIC_* creds otherwise).
  • P3 borderline bench tasks are now counted in the card reward summary.
  • Deferred — the converter's per-component non-data-dir skip: pre-existing behavior, the nested-name exclusion is intentional/documented, and top-level scoping is a cross-cutting change (~6 sites) better as its own PR. Detail in the thread; happy to open the follow-up.

Each fix has a regression test. Full suite: 623 passed / 27 skipped; ruff clean.

@Quantumlyy

Copy link
Copy Markdown
Contributor Author

@codex review

@chatgpt-codex-connector chatgpt-codex-connector 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.

💡 Codex Review

Here are some automated review suggestions for this pull request.

Reviewed commit: 9098444011

ℹ️ About Codex in GitHub

Your team has set up Codex to review pull requests in this repo. Reviews are triggered when you

  • Open a pull request for review
  • Mark a draft as ready
  • Comment "@codex review".

If Codex has suggestions, it will comment; otherwise it will react with 👍.

Codex can also answer questions or update the PR. Try commenting "@codex address that feedback".

Comment thread src/teich/bench/backends/swebench.py Outdated
Comment thread src/teich/bench/templates/agent.dockerfile.j2
Comment thread src/teich/cli.py
Comment thread src/teich/trace_readme.py Outdated
Comment thread src/teich/config.py Outdated
Comment thread src/teich/cli.py
Terminal-bench 2.0 tasks declare [environment].docker_image, so harbor runs
them in prebuilt mode: compose pulls that image directly and never builds an
hb__<task> image, and harbor's teardown (down --rmi local) does not remove
pulled tagged images. The per-task purge only ever removed hb__* names (even
harbor's _main_image_name returns the hb__ name in prebuilt mode), so every
task leaked its pulled image (up to ~2GB each) on all outcomes, including
Ctrl-C.

Parse task.toml for the prebuilt image name(s) — [environment].docker_image
and a separate [verifier.environment].docker_image — and include them in the
purge list. Images declared by more than one task of the source are kept
(deduped per task first: one task using the same image for agent + verifier
isn't sharing): rmi'ing a shared image can race a concurrent sibling's
compose up (No such image) and forces a re-pull per task. Tests that need
tomllib (py3.11+) skip on 3.10, where the harbor backend can't run anyway.

Squashed cherry-pick of #9 (32d2c67 + review-fix
bc16887).
@Quantumlyy

Copy link
Copy Markdown
Contributor Author

Added 76928d7 — a bug fix for per-task Docker image cleanup, squash-picked from AletheiaResearch#9 (reviewed + merged there).

Bug: terminal-bench 2.0 tasks declare [environment].docker_image, which puts harbor in prebuilt mode: compose pulls that image directly and never builds an hb__<task> image, and harbor's down --rmi local teardown doesn't remove pulled tagged images. The per-task purge only removed hb__* names, so every prebuilt task leaked its pulled image (up to ~2GB each) on all outcomes, including Ctrl-C.

Fix: parse the task's task.toml for [environment].docker_image + [verifier.environment].docker_image and include them in the purge list (deterministic, so it also covers the interrupt path where no trial object exists). Images shared by more than one task of the source are kept — deduped per task first — since force-removing a shared tag can race a sibling's compose up and forces re-pulls. keep_bench_images: true still opts out. Four new tests cover the prebuilt purge, the Ctrl-C path, shared-image skip, and within-task duplicates; tomllib-dependent tests skip on py3.10.

🤖 Generated with Claude Code

@Quantumlyy

Copy link
Copy Markdown
Contributor Author

@codex review

@chatgpt-codex-connector chatgpt-codex-connector 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.

💡 Codex Review

Here are some automated review suggestions for this pull request.

Reviewed commit: 76928d7c96

ℹ️ About Codex in GitHub

Your team has set up Codex to review pull requests in this repo. Reviews are triggered when you

  • Open a pull request for review
  • Mark a draft as ready
  • Comment "@codex review".

If Codex has suggestions, it will comment; otherwise it will react with 👍.

Codex can also answer questions or update the PR. Try commenting "@codex address that feedback".

if other != split:
(cfg.output.traces_dir / other / f"{stem}.jsonl").unlink(missing_ok=True)
trace_path.parent.mkdir(parents=True, exist_ok=True)
trace_path.write_text("\n".join(run.native_lines) + "\n", encoding="utf-8")

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

P1 Badge Attach rewards to the harvested trace rows

When users run generate --mode bench and then load or convert the dataset through Teich, this writes the JSONL as the raw native trace while keeping split/reward only in output/metadata/<stem>.json. convert_traces_to_training_data() consumes the JSONL trace files, and remote load_traces() does not download those metadata JSON sidecars, so the published bench dataset has no passed/reward fields despite being marked reward-labeled; reward training or filtering will silently lose the verifier labels.

Useful? React with 👍 / 👎.

Comment thread src/teich/trace_readme.py
return "\n".join(lines)
if splits:
return splits
patterns = ["*.jsonl"]

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

P2 Badge Only advertise JSONL globs that actually exist

For nested-only extractions such as Cursor project-relative transcripts, this always adds the top-level *.jsonl pattern even when there are no top-level JSONL files. Hugging Face resolves each data_files glob and raises when a listed pattern matches nothing, so the generated dataset card can make the Hub viewer or load_dataset fail for the nested-only case this helper is trying to support; include the top-level glob only when it has at least one match.

Useful? React with 👍 / 👎.

Comment thread src/teich/converter.py
# uploads verifier sidecars as dataset provenance.) The bench intermediates normally live
# in a sibling ``bench`` dir; the name is listed here only to defend a nested override.
NON_DATA_TRACE_DIR_NAMES = frozenset(
{"partials", "failures", "verification", "sandbox", "__pycache__", "bench"}

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

P2 Badge Stop dropping data under ordinary artifact-named folders

When extracting nested sessions, for example Cursor project transcripts, Teich preserves the project directory under the output tree; if that project or any parent directory is named bench, sandbox, or verification, this global component-name filter makes _jsonl_files() skip every valid JSONL row below it. That silently removes legitimate sessions from conversion/README/mode detection even though the directory is user data, not a configured artifact directory; exclude the actual artifact locations instead of any matching path component.

Useful? React with 👍 / 👎.

from harbor.models.environment_type import EnvironmentType
from harbor.models.trial.config import TaskConfig, TrialConfig

config = TrialConfig(task=TaskConfig(path=task_dir), trials_dir=trials_dir)

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

P2 Badge Honor the configured timeout for Harbor trials

When a Harbor bench source is run with timeout_seconds set, this builds a default TrialConfig and never copies Teich's timeout into Harbor's agent/verifier timeout overrides. Harbor therefore uses the task/default timeouts instead of the user's cap, so a long-running or hung Harbor agent can keep running well past timeout_seconds; propagate the configured timeout into the trial config before launching it.

Useful? React with 👍 / 👎.

"rewards": run.rewards or {},
"agent": cfg.get_agent_provider(),
"trace_file": f"{split}/{stem}.jsonl",
**run.metadata,

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

P2 Badge Preserve bench provenance when merging trace metadata

For Codex/Hermes native traces, trace_metadata() can return a source key from the recovered session metadata, often null or a provider-local value. Expanding run.metadata after the bench fields overwrites the benchmark source_id(source) written above, so the uploaded sidecar no longer reliably identifies which benchmark source produced the row; merge recovered trace metadata first or protect the reserved provenance keys.

Useful? React with 👍 / 👎.

- bench/runner: exit non-zero when tasks were attempted but nothing harvested —
  a run where every task fails no longer looks like a successful empty benchmark
  in automation. Resume 'nothing to do' and legitimately-empty runs stay exit 0.
- cli: write the dataset card for a partial bench dataset before exiting on a
  later-source failure (parity with prompt mode); no auto-publish.
- trace_readme: thread excluded_dirs into _split_data_files, so a custom
  output.failures_dir under traces_dir isn't advertised as a train split
  (follow-up to the round-1 data_files fix, which only skipped reserved names).
- config: also resolve a bare relative local bench source (data/tasks,
  instances.jsonl) that exists beside the config, not just ./ ../ ~ prefixes
  (follow-up to the round-2 local-source fix); registry/HF specs untouched.
- bench/backends/swebench: chmod the bind-mounted capture tree back to a
  host-writable mode before the container exits, so the next no-resume run's
  rmtree() can't fail on root-owned files (native-Linux Docker).

Deferred: local SWE-bench custom-instance images (Codex P2) — the fix needs a new
bench.sources[].namespace config field (new API surface) and only affects novel
unpublished instances; better as its own PR.

Full suite 632 passed / 27 skipped; ruff clean.
@Quantumlyy

Copy link
Copy Markdown
Contributor Author

Round 3 addressed in 1b3f284 — 5 fixed, 1 deferred (each verified against the code, with a regression test where unit-testable):

  • P2 run_bench now exits non-zero when tasks were attempted but nothing harvested (all-fail no longer looks like a successful empty run).
  • P2 partial bench dataset gets a README before exiting on a later-source failure (parity with prompt mode; no auto-publish).
  • P2 _split_data_files takes excluded_dirs, so a custom output.failures_dir under traces_dir isn't advertised as a train split (follow-up to the round-1 data_files fix).
  • P2 bare relative local bench sources (data/tasks, instances.jsonl) that exist beside the config now resolve, not just .//..//~ (follow-up to the round-2 fix).
  • P2 swebench container chmods the capture tree back to host-writable before exit, so the next no-resume run's cleanup can't fail on root-owned files (native-Linux Docker; no CI coverage, so no unit test).
  • Deferred — local SWE-bench custom-instance images: needs a new bench.sources[].namespace config field (additive API) and only affects novel unpublished instances. Better as its own PR — offered in-thread.

Full suite: 632 passed / 27 skipped; ruff clean. (Also resolved the now-fixed threads from all three rounds so they collapse; the two deferred threads stay open.)

@Quantumlyy

Copy link
Copy Markdown
Contributor Author

@codex review

- swe-bench: add a per-source `namespace` (BenchSource.namespace, default
  "swebench") wired through make_test_spec + _ensure_instance_image; set it to
  null to build instance images locally, which is required for custom/unpublished
  instances (previously the build branch was unreachable). Documented in
  config.example.yaml / CONFIG_TEMPLATE. (Lands the round-3 deferred item.)
- runner: add a rich per-source progress bar (_BenchProgress) showing M/N with a
  live passed/failed/borderline/error tally, mirroring prompt mode's live display.
  No-ops when not a terminal (per-task lines still print); never installs a signal
  handler, so the Ctrl-C teardown and the all-failed/partial-README behavior are
  unchanged. Counts are tracked even when disabled, so the logic is unit-tested.

Adversarially reviewed (interrupt/teardown, concurrency/counting, fallbacks) —
no findings. Full suite 635 passed / 27 skipped; ruff clean.

@chatgpt-codex-connector chatgpt-codex-connector 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.

💡 Codex Review

Here are some automated review suggestions for this pull request.

Reviewed commit: 1b3f284a69

ℹ️ About Codex in GitHub

Your team has set up Codex to review pull requests in this repo. Reviews are triggered when you

  • Open a pull request for review
  • Mark a draft as ready
  • Comment "@codex review".

If Codex has suggestions, it will comment; otherwise it will react with 👍.

Codex can also answer questions or update the PR. Try commenting "@codex address that feedback".

Comment thread src/teich/bench/runner.py Outdated
existing = base.existing_output(cfg, base.bench_stem(source, task.id))
if existing is not None:
out(f"[yellow]bench: skipping {task.id} (already harvested)[/yellow]")
written.append(existing)

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

P2 Badge Don't count resume-skipped rows as new harvests

When --resume skips an already-harvested task, this appends that old path to written; if every remaining pending task then fails, the final if attempted and not written check never fires, so the command exits 0 and may publish a stale partial dataset even though no attempted task produced a row. Track newly harvested rows separately from resume-skipped rows, or base the all-attempted-failed check only on rows written during this invocation.

Useful? React with 👍 / 👎.

Comment thread src/teich/cli.py

try:
written = run_bench(cfg, console=console, resume=resume, refresh=refresh)
except RuntimeError as exc:

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

P2 Badge Handle bench interrupts in the bench branch

In bench mode this branch sits before the prompt-mode KeyboardInterrupt handler below, but it only catches RuntimeError; if the user presses Ctrl-C after some bench tasks were harvested, run_bench re-raises KeyboardInterrupt and the CLI bypasses _try_write_completed_output_metadata, so the completed rows are left without the README/upload hint that prompt mode writes on interruption. Add a KeyboardInterrupt handler here mirroring the prompt-mode cleanup path.

Useful? React with 👍 / 👎.

"rewards": run.rewards or {},
"agent": cfg.get_agent_provider(),
"trace_file": f"{split}/{stem}.jsonl",
**run.metadata,

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

P2 Badge Preserve bench provenance keys when merging metadata

For real native traces, trace_metadata() returns converter metadata that already contains keys such as source (often None for Codex session metadata). Because run.metadata is expanded after the bench sidecar fields, those trace keys can overwrite "source": source_id(source) and other reserved provenance fields, leaving output/metadata/<stem>.json without the benchmark source that produced the reward. Merge trace metadata first or nest/filter it so bench fields remain authoritative.

Useful? React with 👍 / 👎.

Comment thread src/teich/cli.py
# (no effect on swe-bench, which uses Hugging Face's own dataset cache).
# Keep bench its own project: dedicated output.traces_dir + publish.repo_id.
bench:
sources: []

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

P2 Badge Use a block list for bench source examples

The generated config template has sources: [] followed by indented example list items; if a user follows the comments and uncomments one of those - { ... } lines, the YAML is invalid because the sequence is nested under an already-complete flow-style empty list. Use sources: with the commented items beneath it (and make the same change in config.example.yaml) so the bench examples can be uncommented directly.

Useful? React with 👍 / 👎.

Comment thread src/teich/converter.py
# uploads verifier sidecars as dataset provenance.) The bench intermediates normally live
# in a sibling ``bench`` dir; the name is listed here only to defend a nested override.
NON_DATA_TRACE_DIR_NAMES = frozenset(
{"partials", "failures", "verification", "sandbox", "__pycache__", "bench"}

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

P2 Badge Don't globally drop directories named bench

This skip list is used by conversion, README generation, and mode detection, but bench and sandbox are ordinary project names too; Cursor native extraction preserves the project directory under the output tree, so a workspace named bench writes real traces under output/bench/... that teich convert and the README will now silently ignore even outside bench mode. Prefer excluding the configured bench working directory explicitly (or a less ambiguous marker) instead of filtering every path component with this name.

Useful? React with 👍 / 👎.

Comment thread src/teich/bench/backends/swebench.py Outdated
env["OPENROUTER_API_KEY"] = api_key
base_url = cfg.get_base_url()
if base_url:
env["OPENAI_BASE_URL"] = base_url

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

P2 Badge Export Anthropic base URLs for Claude bench runs

When swe-bench is run with agent.provider: claude-code and an Anthropic-compatible custom api.base_url, this only passes OPENAI_BASE_URL into the container. The Claude runner used elsewhere in this repo sets ANTHROPIC_BASE_URL, which is the variable the Claude CLI path relies on, so this bench container ignores the configured endpoint and uses the default Anthropic API. Set ANTHROPIC_BASE_URL when cfg.api.provider is anthropic (the mirrored harbor helper has the same mapping).

Useful? React with 👍 / 👎.

"trace_file": f"{split}/{stem}.jsonl",
**run.metadata,
}
meta_path = cfg.output.traces_dir / "metadata" / f"{stem}.json"

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

P2 Badge Make bench rewards loadable with the traces

This writes the only copy of split/reward into metadata/*.json while the harvested JSONL remains a native trace; however load_traces() only downloads JSONL/README/tools files from Hub snapshots and convert_traces_to_training_data() never joins these sidecars, so published bench datasets lose their reward labels when users load them for reward-based filtering or training. Either embed the verifier outcome into the converted row metadata or teach the loader/converter to fetch and merge these sidecars.

Useful? React with 👍 / 👎.

local = Path(spec).expanduser()
if local.exists():
return local
cache_dir = base.bench_root(cfg) / "sources" / _source_slug(spec, source.version, source.repo)

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

P3 Badge Reject missing local Harbor paths before remote fetch

When a config uses an explicit local Harbor source such as ./tasks or /tmp/tasks and the directory is missing, this falls through into the remote-download path and asks Harbor to download that filesystem path as a dataset ref, producing a misleading network/auth/spec error instead of the actionable local bench task directory not found error. Treat absolute or explicitly path-like specs as local and fail before _fetch_remote().

Useful? React with 👍 / 👎.

… endpoints)

Prompt mode threads api.wire_api into the agent (codex config; pi provider_settings
via _pi_provider_api -> openai-completions/openai-responses). Bench does neither — it
only sets the model prefix + OPENAI_BASE_URL — so a chat-completions-only endpoint
(e.g. provider: openai + base_url: api.z.ai + wire_api: chat_completions) is silently
hit on /responses by pi/codex and fails, ignoring the config.

Rather than silently misbehave, run_bench now raises when the agent is pi or codex,
the provider isn't openrouter (whose prefix routing already selects completions), and
api.wire_api is a chat-completions variant. Points the user at provider: openrouter or
a Responses-API endpoint. Actually threading wire_api into the bench agents is a larger
follow-up. Full suite 636 passed / 27 skipped; ruff clean.
…e_url

Step 1 of making bench reuse prompt mode's agent config instead of a thinner,
diverging copy. New src/teich/agent_cfg.py holds the pure cfg->env/model/url logic:
- provider_env_key: the FULL provider->key map (zai->GLM_API_KEY, deepseek, xai,
  google, ...). Bench previously only knew openai/openrouter/anthropic, so a
  provider: zai run got no usable key var.
- container_base_url: rewrites localhost/127.0.0.1 -> host.docker.internal so a
  host-local endpoint is reachable from the container. Bench used raw get_base_url().
- bench_auth_env / pi_prefixed_model: the shared credential+base-url env and pi
  model-prefix rule.

swebench (_auth_env/_model_name) and harbor (_agent_auth_env/_bench_model_name) now
delegate to it (deleting their duplicated maps), and runner.py's
ExternalCliRunner._provider_env_key + _container_base_url delegate too, so it's one
source of truth, not a fork.

Next: seed pi models.json/settings.json + codex config.toml into the swe-bench
container so wire_api/reasoning/etc. are honored too (replacing the fail-fast guard).

Full suite 640 passed / 27 skipped; ruff clean.
@Quantumlyy Quantumlyy marked this pull request as draft July 2, 2026 17:17
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.

2 participants