3/3 Add harness-engineering-bench (GAIA + benchmarks)#46
Conversation
|
Review the following changes in direct dependencies. Learn more about Socket for GitHub.
|
| docker pull yimjk/ale-bench:cpp20-202301 | ||
| docker tag yimjk/ale-bench:cpp20-202301 ale-bench:cpp20-202301 |
There was a problem hiding this comment.
The judge image is pulled by mutable tag (
cpp20-202301) without a digest pin. Docker Hub tags can be silently overwritten, so two scoring runs could pull different binaries under the same tag — directly threatening benchmark reproducibility. In the worst case, a compromised yimjk account could push a malicious image that gets pulled at evaluation time. Pinning to the image digest locks the exact bytes regardless of tag mutations.
| docker pull yimjk/ale-bench:cpp20-202301 | |
| docker tag yimjk/ale-bench:cpp20-202301 ale-bench:cpp20-202301 | |
| docker pull yimjk/ale-bench:cpp20-202301@sha256:<DIGEST> | |
| docker tag yimjk/ale-bench:cpp20-202301@sha256:<DIGEST> ale-bench:cpp20-202301 |
Prompt To Fix With AI
This is a comment left during a code review.
Path: harness-engineering-bench/ale-bench/environment/sidecar/dind-entrypoint.sh
Line: 23-24
Comment:
The judge image is pulled by mutable tag (`cpp20-202301`) without a digest pin. Docker Hub tags can be silently overwritten, so two scoring runs could pull different binaries under the same tag — directly threatening benchmark reproducibility. In the worst case, a compromised `yimjk` account could push a malicious image that gets pulled at evaluation time. Pinning to the image digest locks the exact bytes regardless of tag mutations.
```suggestion
docker pull yimjk/ale-bench:cpp20-202301@sha256:<DIGEST>
docker tag yimjk/ale-bench:cpp20-202301@sha256:<DIGEST> ale-bench:cpp20-202301
```
How can I resolve this? If you propose a fix, please make it concise.e0a1efb to
ab4c52f
Compare
573c1ac to
c86351e
Compare
| close() | ||
|
|
||
| judge = str(getattr(result, "overall_judge_result", "")) | ||
| accepted = 1.0 if judge.upper().endswith("ACCEPTED") else 0.0 |
There was a problem hiding this comment.
endswith("ACCEPTED") will incorrectly match any string that ends with "ACCEPTED" — most notably "NOT_ACCEPTED" (or an enum-style repr like "JudgeResult.NOT_ACCEPTED"). If ALE-Bench returns such a value for a failing submission, the accepted metric would be written as 1.0 for a rejected run. Using exact equality is unambiguous regardless of what the library returns.
| accepted = 1.0 if judge.upper().endswith("ACCEPTED") else 0.0 | |
| accepted = 1.0 if judge.upper() == "ACCEPTED" else 0.0 |
Prompt To Fix With AI
This is a comment left during a code review.
Path: harness-engineering-bench/ale-bench/environment/sidecar/harness/score.py
Line: 83
Comment:
`endswith("ACCEPTED")` will incorrectly match any string that ends with "ACCEPTED" — most notably "NOT_ACCEPTED" (or an enum-style repr like `"JudgeResult.NOT_ACCEPTED"`). If ALE-Bench returns such a value for a failing submission, the `accepted` metric would be written as `1.0` for a rejected run. Using exact equality is unambiguous regardless of what the library returns.
```suggestion
accepted = 1.0 if judge.upper() == "ACCEPTED" else 0.0
```
How can I resolve this? If you propose a fix, please make it concise.745ed8e to
6788d76
Compare
214f1aa to
a2fcd19
Compare
|
Approve with nits. The split is the part I care most about for experiment validity, and it checks out. GAIA split is genuinely leak-safe (verified):
One real defect (baseline quality, not integrity): the tau3 baseline drops conversation history after every plain-text turn. One thing to note for readers: every build.yaml ships |
768de08 to
ad2c081
Compare
741bfa6 to
95fed89
Compare
51c152d to
ced19f9
Compare
Across gaia/officeqa/swe-atlas-qna/tau3: 100-run budgets and 4x-pass
case budgets on both agent partitions, submit-mode selection,
baseline_floor off, 16k feedback, parameterized inner environment
(${inner_env:-modal}), and verifier_timeout = 2x timeout_seconds.
swe-atlas-qna and tau3 get explicit case/task-agent timeouts derived
from their datasets' declared agent timeouts (enforced budgets 1800s
and 900s, provisional until empirical wall_seconds data). harness_user
is explicit on gaia/officeqa; tau3 and swe-atlas-qna stay unisolated
pending off-env delivery of task-service credentials. Also fix the
vendor script's stale pre-restructure path in its usage comment.
Root cause of every swe-atlas trial dying at setup: the task images set ENTRYPOINT ["/bin/bash"], Modal prepends it to the sandbox command, so harbor's default keepalive ["sh","-c","sleep infinity"] ran as 'bash sh -c ...' - bash interprets the sh binary as a script and exits 126, killing the sandbox before the first exec. Pass bash's own args via the JSON-decoded keepalive env kwarg instead. Validated end-to-end on Modal (3 dev trials, 379-602s wall). CONFIGURATION.md records the empirical probe timings behind the provisional case timeouts.
scripts/per_trial_tokens.py reads a session's gateway request log and attributes token/latency to Harbor trials: by the gateway's stamped thread_id when present (request_log.attribution builds → full coverage incl. stateful chains), else a legacy fallback that recovers each conversation's root turn (labelable via --tasks-dir) and residualizes unrecoverable follow-ups. Reports gateway-total vs attributed vs agent-reported per evaluation. Stdlib-only; tests cover both paths.
…r at turn cap Chat Completions is the only universally-supported inference surface — the OpenAI Responses API doesn't translate cleanly to non-OpenAI models via litellm (parallel_tool_calls rejected, output_text crashes on a null message part). Rewrite AtlasAgent's loop to stateless chat messages + tool_calls, validated cross-provider (gpt-5.4-mini and fireworks_ai/gpt-oss-120b both run the multi-turn tool loop). Also: at the turn cap, force one final tool-free answer instead of raising, so a budget-exhausted trial scores best-effort rather than losing the case.
Same port as swe-atlas: stateless chat messages + tool_calls, works across providers (validated cross-provider on gpt-5.4-mini and fireworks_ai/gpt-oss-120b). officeqa drops the hosted web_search tool (closed-corpus benchmark, and chat/completions has no hosted-tool equivalent) and moves read_image feedback to chat image_url content. tau3 keeps its host-side MCP loop; the stateless history also fixes the previous_response_id reset that dropped context after plain-text turns, and the MCP session id is now charset-validated before shell interpolation (greptile injection finding).
Mirrors the swe-atlas fix: at MAX_TURNS force one tool-free completion for the best answer and submit it, so a budget-exhausted case scores best-effort rather than being lost with no answer recorded.
A fifth harness-engineering benchmark: optimize a retrieval + reasoning agent over BrowseComp-Plus's fixed corpus and canonical BM25 index, graded by a semantic answer judge (gpt-4.1). Task packages are generated from the encrypted upstream dataset by scripts/build_tasks.py and are gitignored (they contain decrypted questions/answers); the upstream repo is pinned as a submodule and the 2.2 GB index is pulled at image-build time. Wired consistently with the normalized suite: 33/66/66 stratified split, 4-pass budgets, submit-mode selection, harness_user null + task_services_use_upstream for the in-container judge. Baseline agent uses Chat Completions (works across providers) and forces a best-effort answer at the turn cap. The task image installs a full JDK with JAVA_HOME resolved from it, so pyserini's Lucene BM25 searcher works (a headless JRE with JAVA_HOME unset left jnius unable to locate the JVM).
… succeeds pyserini 1.2.0 constructs an openai.OpenAI() client at import time for its unused OpenAI encoders; BM25/Lucene search never calls OpenAI, so set a placeholder key in search.py before the import.
The task.toml had no verifier.env, so the gpt-4.1 answer judge ran without OPENAI_API_KEY and scored every case 0 (Missing credentials). Template the upstream creds and judge model into the verifier phase.
…las) From a 10-trial per-benchmark probe of gpt-5.4-mini vs gpt-oss-120b vs deepseek-v4-flash: deepseek matches/beats both on tau3 (0.875) and is ~2-3x gpt-oss on the grounded-reasoning benchmarks (officeqa/browsecomp 0.60) at roughly gpt-oss cost and far below mini. It is weaker only on swe-atlas (0.30 vs gpt-oss 0.59 mean rubric), which is pinned to gpt-oss-120b — cheaper and stronger there. Updates each benchmark's target `model` and the evaluation-scope `allowed_models` (kept with the fireworks_ai/ prefix, since agents strip only the openai/ prefix and the gateway matches the sent model exactly). Producer scopes and tau3's user/assertion models are unchanged. Also hardens the swe-atlas atlas agent: a shell command emitting non-UTF-8 (binary) output raised UnicodeDecodeError and killed the whole trial; it now returns a tool error so the model can retry. Co-Authored-By: Claude Opus 4.8 <[email protected]>
… relock - .gitignore: remove the paper-planning benchmark-scoping.md ignore - README.md: correct candidate paths (candidates/ -> top level), add browsecomp-plus, drop the stale nested table - vero/uv.lock: relock Co-Authored-By: Claude Opus 4.8 <[email protected]>
Fireworks' shared-org rate limit surfaces as a RateLimitError inside the agent's OpenAI client; harbor only retries infra/env failures, not LLM 429s, so a burst lost trials. The SDK's default 2 retries were exhausted under sustained load. Bump to 8 so transient 429s back off in-place instead of failing the trial (durable fix is gateway-level retry, tracked separately). Co-Authored-By: Claude Opus 4.8 <[email protected]>
…line Full held-out (test) pass of the seed harness, 3 independent rounds, under the new targets (deepseek-v4-flash; gpt-oss-120b on swe-atlas): swe-atlas 0.097 ±0.011 (agg_score 0.632) [gpt-oss-120b] tau3 0.611 ±0.021 [deepseek-v4-flash] officeqa 0.360 ±0.042 [deepseek-v4-flash] browsecomp 0.449 ±0.007 [deepseek-v4-flash] Each pinned into its target's baseline_reward with score_baseline: false, so finalization uses the number instead of re-scoring the seed every run. The three deepseek benchmarks logged zero exceptions over 945 trials (max_retries absorbed the shared-quota 429s); swe-atlas lost 5/150 to gpt-oss 128k overflow. CONFIGURATION.md documents the numbers, method, and the swe-atlas binary-vs- agg_score note. gaia deferred. Co-Authored-By: Claude Opus 4.8 <[email protected]>
It was the suite's only Family B task and the only one that bypassed the compiler: a hand-written task directory with no build.yaml, so it got none of the build-time validation the other five benchmarks get. Its optimizer was also unmetered -- no gateway container, raw upstream credentials in main, no model allow-list -- which is a fair-play hole in a benchmark whose whole point is controlled measurement. Its sidecar factory had additionally gone stale against SidecarEvaluationPolicy and would have died at startup. The nested-backend refactor now supports `evaluation_backend: command`, so a replacement Family B task can be built through the compiler and get the gateway for free. examples/harbor-circle-packing carries a worked version of exactly that shape, verified end to end. Co-Authored-By: Claude Opus 5 (1M context) <[email protected]>
These tests moved here from pr2-add-vero, where they could only ever skip. They had been failing for a while against a pinned literal -- evaluation allowed_models == ["gpt-5.4-mini-2026-03-17"] -- that stopped matching when the benchmarks switched to fireworks-served targets. Rewrite the assertions so they cannot drift that way again: the evaluation scope must allow exactly [config.model], derived rather than pinned. That is the real invariant, and it now matches the build-time validator that rejects a model outside its scope's allow-list. Also drop the skip guard, so a missing harness-engineering-bench fails rather than passing vacuously, and extend coverage from three benchmarks to all five. 415 passed, 5 skipped -- the three long-standing failures are gone. Co-Authored-By: Claude Opus 5 (1M context) <[email protected]>
Six findings from the reviews on #45/#46 that survived checking against the current tree. Each was verified against the live litellm endpoint the gateway proxies to, not inferred from a commit message. gaia moves to gpt-5.4-mini. It is the one multimodal benchmark and deepseek-v4-flash is text-only: 5 of the 66 held-out tasks send images and get 400 "This model does not support image inputs", which caps achievable reward near 0.92 and reads as ordinary agent failure. gpt-5.4-mini returns 200 for every request shape the gaia agent sends -- image input, hosted web_search, reasoning.effort, parallel_tool_calls -- confirmed through the SDK path the agent actually uses. The name is unprefixed on purpose: every agent sends model_name.removeprefix("openai/"), so an openai/-prefixed name would be allow-listed in one form and requested in another and the gateway would deny it. The gaia held-out baseline now needs re-measuring. tau3 executes every tool call the model returns instead of only the first. deepseek-v4-flash does return two in a turn, and the usual guard is not available: litellm rejects parallel_tool_calls for fireworks_ai with UnsupportedParamsError, so the reviewer's suggested one-liner would have 400'd every tau3 call. Dropping calls[1:] silently skipped actions -- for a customer-service agent, a verified identity with no message sent. officeqa and swe-atlas-qna run their tool dispatch inside the try rather than an else, and catch KeyError/TypeError/ValueError alongside JSONDecodeError, matching browsecomp-plus. Neither tool schema is strict, so the model can return valid JSON missing a required key; that ended the trial instead of feeding the model an error. Widening the except alone would have caught nothing, since the lookups were in the else. gaia's client gets max_retries=8, the only one of the five without it. A within-trial transient failure scores at the failure value for competitive evaluations, so an unretried 429 cost a candidate a 0.0. tau3 quotes the MCP session id before interpolating it into a shell command, for the same reason the payload beside it is base64-encoded. per_trial_tokens buckets unattributed records under a name rather than None, which crashed sorted() as soon as one appeared beside an attributed record. Not actioned: the browsecomp-plus grader was reported as vulnerable to format-string injection from curly braces in the agent's response. str.format does not rescan substituted values, so there is nothing to fix. Co-Authored-By: Claude Opus 5 (1M context) <[email protected]>
These tests fail 6 of 16 on a clean checkout. officeqa and browsecomp-plus point task_source at a gitignored tasks/ tree built by their own scripts/, and the loader only absolutizes a relative task_source when it exists, so on a fresh clone the path stays literal and is rejected as an unpinned registry reference. They pass here only because this machine has already run the vendor scripts. Dropping the blanket skip guard in d131412 was the right call -- that guard is what let the earlier version rot unnoticed -- but it was aimed at the wrong condition. A missing harness-engineering-bench still errors loudly; what is skipped now is one benchmark whose tasks are not fetched yet, and the skip names the directory and points at the script. That is a precondition of the machine, not a property of the config. Verified by moving both tasks/ directories aside: 10 passed, 6 skipped with the reason attached, where before it was 6 failed. Reported by shehabyasser-scale on #46. Co-Authored-By: Claude Opus 5 (1M context) <[email protected]>
The gateway matches a requested model against the scope allow-list as an
exact string, so the producer default and the -m the outer trial is
launched with have to agree character for character. Both gpt-5.4 and
openai/gpt-5.4 resolve on the router, so this is a convention choice, not a
correctness fix: prefixed matches how every evaluation scope names its
model and how harbor's own uploader keeps the two spellings apart.
Note the resulting asymmetry in gaia, which is deliberate. Its producer is
openai/gpt-5.4 while its target is bare gpt-5.4-mini, because the baseline
agents send model_name.removeprefix("openai/") and the optimizer agent does
not. The allow-list has to match what each actually sends.
Co-Authored-By: Claude Opus 5 (1M context) <[email protected]>
5397fe4 to
ec0b444
Compare
gaia still had the try/except/else shape the last round removed from officeqa and swe-atlas-qna: only json.loads sat inside the try, so every argument lookup and every tool call ran unguarded. It matters more here than it did there, because gaia is the one agent with read_image, which downloads a model-supplied path and then stats and reads it -- a hallucinated path fails in the filesystem, not in json.loads, and took the whole trial with it. gaia is also the benchmark about to be re-baselined on a multimodal model, so that path is now exercised rather than dormant. OSError joins the caught set in all four agents, not just gaia. Every one of them writes a file in _submit, and three download or upload through the environment, so the previous tuple of JSONDecodeError/KeyError/TypeError/ ValueError left the I/O half of the dispatch uncovered everywhere. A failed tool call should reach the model as a tool error it can react to, not end the trial. Reported by greptile on #46 (gaia); extended to the other three for the same reason rather than waiting to be told about each in turn. Co-Authored-By: Claude Opus 5 (1M context) <[email protected]>
gaia was never ported off the Responses API and lacked two robustness
guards the other four baseline agents have, causing ~27% of held-out trials
to error out (and silently inflate any reward computed over survivors):
- A turn where the model only reasoned, ran a hosted web_search, or was
truncated at max_output_tokens returns no function_call and no message.
The loop treated that as fatal ("neither answer nor tool call"); it now
carries the response chain forward with a nudge. MAX_TURNS bounds it.
- Exceeding MAX_TURNS raised RuntimeError; it now forces one final tool-free
answer so the case scores best-effort.
Held-out crash rate drops 27% -> 2% (189 OK / 5 forced / 4 infra of 198);
clean baseline_reward 0.5736 (sd 0.010 across 3 rounds).
Co-Authored-By: Claude Opus 4.8 <[email protected]>
Clean K=3 held-out pass with gpt-5.4-mini after the gaia agent crash fix: baseline_reward 0.5736 (sd 0.010 across rounds 0.576/0.561/0.585), 197/198 trials scored (189 OK, 5 forced-final, 4 infra). Pins the number and disables per-run baseline re-scoring, matching the other four benchmarks. CONFIGURATION.md baseline row + footnote updated (gaia now measured). Co-Authored-By: Claude Opus 4.8 <[email protected]>
| assert first == second | ||
| assert {name: len(tasks) for name, tasks in first.items()} == PARTITION_COUNTS | ||
| flattened = [task for tasks in first.values() for task in tasks] | ||
| assert len(flattened) == len(set(flattened)) == EXPECTED_TASKS |
There was a problem hiding this comment.
The assertion
== EXPECTED_TASKS will always fail because _partition intentionally returns only sum(PARTITION_COUNTS.values()) = 165 partitioned tasks out of EXPECTED_TASKS = 830 — the remaining rows are rendered as task dirs but left unassigned (per the _partition docstring). Running this test today would produce AssertionError: assert 165 == 165 == 830. The intent appears to be verifying no duplicates and that the total equals the sum of partition counts.
| assert len(flattened) == len(set(flattened)) == EXPECTED_TASKS | |
| assert len(flattened) == len(set(flattened)) == sum(PARTITION_COUNTS.values()) |
Prompt To Fix With AI
This is a comment left during a code review.
Path: harness-engineering-bench/browsecomp-plus/scripts/test_build_tasks.py
Line: 35
Comment:
The assertion `== EXPECTED_TASKS` will always fail because `_partition` intentionally returns only `sum(PARTITION_COUNTS.values()) = 165` partitioned tasks out of `EXPECTED_TASKS = 830` — the remaining rows are rendered as task dirs but left unassigned (per the `_partition` docstring). Running this test today would produce `AssertionError: assert 165 == 165 == 830`. The intent appears to be verifying no duplicates and that the total equals the sum of partition counts.
```suggestion
assert len(flattened) == len(set(flattened)) == sum(PARTITION_COUNTS.values())
```
How can I resolve this? If you propose a fix, please make it concise.
Third of three stacked PRs splitting #41. Base:
pr2-add-vero.Adds
harness-engineering-bench/with each benchmark as a top-level sibling (gaia,ale-bench,officeqa,swe-atlas-qna,tau3) + scripts. Pure adds (71 files).Union of 1/3 + 2/3 + 3/3 ==
v0.5plus alegacy/archive (verified).🤖 Generated with Claude Code
Greptile Summary
This PR adds
harness-engineering-bench/with five benchmark integrations —browsecomp-plus,gaia,officeqa,swe-atlas-qna, andtau3— each as a self-contained directory with a baseline agent, partitions, and build scripts, alongside a newvero/src/vero/sandbox.pyabstraction layer and structural tests invero/tests/.per_trial_tokens.pypreviously-flaggedNonekey bug is resolved in this version (or "unattributed"at line 128).vero/src/vero/sandbox.pyaddsLocalSandboxandDockerSandboxwith proper POSIX process-group cleanup and PID-file-based container termination.test_build_tasks.pycontains a broken assertion that compares the number of partitioned tasks (165) againstEXPECTED_TASKS(830), causing the test to always fail when run.Confidence Score: 4/5
Safe to merge with the test assertion fix — the broken assertion in test_build_tasks.py will cause that test to fail whenever run, but does not affect production evaluation.
The broken test in test_build_tasks.py compares 165 partitioned tasks against EXPECTED_TASKS=830 and will always fail. Everything else in this pure-additions PR — the browsecomp-plus agent, sandbox abstraction, build scripts, and structural config tests — looks correct. The previously flagged issues in gaia/officeqa/tau3/ale-bench (from earlier stacked PRs) are unresolved but pre-existing relative to this diff.
Files Needing Attention: harness-engineering-bench/browsecomp-plus/scripts/test_build_tasks.py (broken assertion) and harness-engineering-bench/browsecomp-plus/task-template/tests/evaluate.py (format-string injection flagged in prior review, still unresolved).
Important Files Changed
== EXPECTED_TASKSwill always fail —_partitionreturns 165 tasks, not 830parallel_tool_calls=Falsebut lower impact here sincesubmit_responseterminates the loopor 'unattributed'fallback at line 128Sequence Diagram
sequenceDiagram participant H as Harbor Harness participant A as BrowseCompPlusAgent participant GW as Inference Gateway participant M as OpenAI Model participant E as Environment (corpus) participant V as Verifier (evaluate.py) H->>A: run(instruction, environment, context) loop Turn 1..MAX_TURNS A->>GW: chat.completions.create(messages, tools) GW->>M: proxied request (budget metered) M-->>GW: tool_calls or content GW-->>A: response alt tool_calls present loop each tool call A->>E: exec search.py / get-document E-->>A: JSON result A->>A: append tool result to messages note over A: break if submit_response called end else no tool calls (content only) A->>E: upload answer.txt A->>A: set context.metadata, break end end note over A: else branch (budget exhausted): force final answer call A->>E: upload answer.txt H->>V: python evaluate.py V->>M: responses.create (judge prompt) M-->>V: correct yes/no V->>V: write reward.txtPrompt To Fix All With AI
Reviews (42): Last reviewed commit: "Pin gaia held-out baseline (0.5736) with..." | Re-trigger Greptile