Skip to content

3/3 Add harness-engineering-bench (GAIA + benchmarks)#46

Open
varunursekar wants to merge 39 commits into
pr2-add-verofrom
pr3-harness-bench
Open

3/3 Add harness-engineering-bench (GAIA + benchmarks)#46
varunursekar wants to merge 39 commits into
pr2-add-verofrom
pr3-harness-bench

Conversation

@varunursekar

@varunursekar varunursekar commented Jul 23, 2026

Copy link
Copy Markdown
Collaborator

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.5 plus a legacy/ 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, and tau3 — each as a self-contained directory with a baseline agent, partitions, and build scripts, alongside a new vero/src/vero/sandbox.py abstraction layer and structural tests in vero/tests/.

  • browsecomp-plus is a new deep-research benchmark backed by a pinned BM25 corpus; its agent, task builder, verifier, and search tool are all new. The per_trial_tokens.py previously-flagged None key bug is resolved in this version (or "unattributed" at line 128).
  • vero/src/vero/sandbox.py adds LocalSandbox and DockerSandbox with proper POSIX process-group cleanup and PID-file-based container termination.
  • test_build_tasks.py contains a broken assertion that compares the number of partitioned tasks (165) against EXPECTED_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

Filename Overview
harness-engineering-bench/browsecomp-plus/scripts/test_build_tasks.py Test assertion == EXPECTED_TASKS will always fail — _partition returns 165 tasks, not 830
harness-engineering-bench/browsecomp-plus/baseline/target/src/browsecomp_plus_agent/agent.py New BrowseComp-Plus Chat Completions agent; handles max-turns exhaustion correctly with a best-effort fallback; no parallel_tool_calls=False but lower impact here since submit_response terminates the loop
harness-engineering-bench/browsecomp-plus/task-template/tests/evaluate.py BrowseComp-Plus verifier; GRADER_TEMPLATE.format(...) will raise KeyError if question/response/answer contain curly braces (previously flagged)
harness-engineering-bench/scripts/per_trial_tokens.py Post-hoc token accounting script; previously-flagged None evaluation key issue is fixed via or 'unattributed' fallback at line 128
harness-engineering-bench/browsecomp-plus/scripts/build_tasks.py Reproducible task builder with pinned dataset/index revisions, deterministic partitioning, atomic output rename, and safe shell quoting; looks correct
vero/src/vero/sandbox.py New sandbox abstraction (Local + Docker) with proper process-group cleanup, PID-file-based container termination, and atomic file transfer; no issues found
vero/tests/test_v05_benchmark_configs.py Structural tests verifying all five benchmarks route inference through the gateway, enforce model allow-lists, and scope optimizer/evaluation budgets independently

Sequence 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.txt
Loading

Fix All in Cursor Fix All in Claude Code Fix All in Codex

Prompt To Fix All With AI
Fix the following 1 code review issue. Work through them one at a time, proposing concise fixes.

---

### Issue 1 of 1
harness-engineering-bench/browsecomp-plus/scripts/test_build_tasks.py:35
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())
```

Reviews (42): Last reviewed commit: "Pin gaia held-out baseline (0.5736) with..." | Re-trigger Greptile

Greptile also left 1 inline comment on this PR.

@socket-security

socket-security Bot commented Jul 23, 2026

Copy link
Copy Markdown

Review the following changes in direct dependencies. Learn more about Socket for GitHub.

Diff Package Supply Chain
Security
Vulnerability Quality Maintenance License
Addedpypi/​tqdm@​4.68.49810010010070
Addedpypi/​tqdm@​4.69.09810010010070
Addedpypi/​click@​8.4.296100100100100
Addedpypi/​uvicorn@​0.51.098100100100100
Addedpypi/​tiktoken@​0.13.099100100100100
Addedpypi/​requests@​2.34.299100100100100
Addedpypi/​fastapi@​0.139.2100100100100100
Addedpypi/​fastapi@​0.140.0100100100100100

View full report

Comment on lines +23 to +24
docker pull yimjk/ale-bench:cpp20-202301
docker tag yimjk/ale-bench:cpp20-202301 ale-bench:cpp20-202301

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 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.

Suggested change
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.

Fix in Cursor Fix in Claude Code Fix in Codex

@varunursekar
varunursekar force-pushed the pr3-harness-bench branch 3 times, most recently from e0a1efb to ab4c52f Compare July 23, 2026 20:00
@varunursekar
varunursekar force-pushed the pr3-harness-bench branch 2 times, most recently from 573c1ac to c86351e Compare July 23, 2026 22:58
close()

judge = str(getattr(result, "overall_judge_result", ""))
accepted = 1.0 if judge.upper().endswith("ACCEPTED") else 0.0

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 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.

Suggested change
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.

Fix in Cursor Fix in Claude Code Fix in Codex

@varunursekar
varunursekar force-pushed the pr3-harness-bench branch 3 times, most recently from 745ed8e to 6788d76 Compare July 24, 2026 03:49
Comment thread harness-engineering-bench/officeqa/baseline/build.yaml Outdated
@varunursekar
varunursekar force-pushed the pr3-harness-bench branch 4 times, most recently from 214f1aa to a2fcd19 Compare July 24, 2026 06:41
@shehabyasser-scale

Copy link
Copy Markdown
Collaborator

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):

  • Deterministic stratified partition keyed by sha256("vero-gaia-v1:{task}") (partition_gaia.py:116), no RNG, with a --check mode that fails CI if the committed manifest drifts.
  • Verified disjoint: development 33 / validation 66 / test 66, zero pairwise overlap, union 165.
  • Wiring is correct: development at disclosure: full, validation at aggregate (min_aggregate_cases: 5, expose_case_resources: false), selection on validation, and reward/target on the test partition which is never agent-visible. A proper held-out test set.
  • k-anonymity floors are safe in every build.yaml (tau3 / officeqa / swe-atlas-qna / gaia all set 5). The ale-bench endswith("ACCEPTED") judge is fine too: upstream JudgeResult has no NOT_ACCEPTED member, so no failing verdict ends in that string.

One real defect (baseline quality, not integrity): the tau3 baseline drops conversation history after every plain-text turn. tau3_agent/agent.py:370 sets previous_response_id = None in the non-tool branch, while the tool branch threads it correctly (:347). After any non-terminal customer reply the model loses the domain policy (only present in the turn-0 input), prior tool results, and reasoning state, which depresses tau3 from the first follow-up onward. The Responses API supports threading after a message output, so None isn't required. Suggest previous_response_id = response.id on the text branch as well. Confirmed tau3-specific; the GAIA baseline has no such reset.

One thing to note for readers: every build.yaml ships infrastructure_max_attempts: 3 + aggregate_attempts: best, so the competitive-selection integrity of these benchmarks depends on the aggregate/retry fix over on #45, not on any change here.

@varunursekar
varunursekar force-pushed the pr3-harness-bench branch 4 times, most recently from 768de08 to ad2c081 Compare July 24, 2026 22:28
Comment thread harness-engineering-bench/scripts/per_trial_tokens.py
varunursekar and others added 24 commits July 26, 2026 09:51
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]>
Comment thread harness-engineering-bench/gaia/baseline/target/src/gaia_agent/agent.py Outdated
varunursekar and others added 3 commits July 26, 2026 10:06
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

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 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.

Suggested change
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.

Fix in Cursor Fix in Claude Code Fix in Codex

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