fix: stop submitting truncated reasoning as the browsecomp-plus answer - #61
Open
shehabyasser-scale wants to merge 1 commit into
Open
fix: stop submitting truncated reasoning as the browsecomp-plus answer#61shehabyasser-scale wants to merge 1 commit into
shehabyasser-scale wants to merge 1 commit into
Conversation
varunursekar
force-pushed
the
pr3-harness-bench
branch
2 times, most recently
from
July 28, 2026 02:25
31d8b79 to
ab8717b
Compare
shehabyasser-scale
force-pushed
the
fix/browsecomp-plus-truncated-final-answer
branch
2 times, most recently
from
July 28, 2026 14:08
9b366c8 to
84f4aef
Compare
The headline is a scoring bug. When a completion came back with
`finish_reason == "length"`, the agent submitted `message.content`
verbatim as its final answer. This target writes its chain-of-thought
into ordinary `content` (it has no separate reasoning_content channel)
and routinely runs past the token cap, so what got submitted was 46-52k
characters of truncated mid-sentence reasoning. The judge scores that 0
every time, and the trial looks like a genuine wrong answer rather than
a harness failure. Truncated turns are now treated as recoverable: the
model is asked for just the three required labeled lines instead of
having its thinking submitted for it.
Four supporting changes, all aimed at the same failure mode of losing a
trial that had real retrieved evidence behind it:
- Wall clock, not turn count, is this agent's binding constraint. Most
of a turn is sequential `search.py` calls, each booting a fresh
pyserini/Lucene JVM against the BM25 index (~13s apiece) against ~3-9s
of inference, so trials ran out of clock long before MAX_TURNS. The
turn-budget fallback therefore never fired and harbor killed the trial
mid-search with no answer recorded. There is now a deadline
(BROWSECOMP_AGENT_BUDGET_SEC) with a final-answer reserve
(BROWSECOMP_FINAL_RESERVE_SEC) so the best-effort answer is written
before harbor's AgentTimeoutError. Tool calls past the deadline are
stubbed rather than skipped, because every tool_call still needs a
tool message or the forced-final request is rejected.
- Completion shape is validated in one place. A gateway can answer 200
with a body the SDK does not turn into a completion, observed as
`'str' object has no attribute 'usage'`, which crashed the trial on an
attribute access two lines later. Anything without usable `.choices`
is now rejected as a retryable error.
- An empty or malformed assistant turn nudges instead of raising
`RuntimeError("model returned neither a response nor a tool call")`,
giving up only after MAX_EMPTY_RETRIES consecutive nudges. An
assistant message carrying neither content nor tool calls is dropped
rather than appended, since strict OpenAI-compatible servers reject it.
- The forced-final call is bounded (max_retries=1, ANSWER_MAX_TOKENS) and
degrades to a stub answer on failure rather than propagating, and the
trace/metadata now record finish_reason, per-turn latency, remaining
budget, and a `forced_final_reason` so a post-hoc conservative score
can treat exactly the trials the pre-fix agent would have lost as
zeros.
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
shehabyasser-scale
force-pushed
the
fix/browsecomp-plus-truncated-final-answer
branch
from
July 29, 2026 02:56
84f4aef to
f8b81b5
Compare
shehabyasser-scale
added a commit
that referenced
this pull request
Jul 29, 2026
Nothing in this repository ran tests. The only checks on a pull request were Greptile Review and Socket Security, neither of which invokes pytest, and there was no .github directory on any branch. #61 sat open with a red target suite because of it: the change there reaches the OpenAI client through `with_options`, the test fake did not implement it, and `test_agent_submits_response_and_populates_context` had been failing on AttributeError since the PR was opened with nothing to surface it. Two jobs, seven suites, ~450 vero tests plus 20 across the agents: - `vero`: uv sync --locked, then pytest. Two placeholder credentials are set because the task compiler refuses to emit a task whose declared credentials are absent from the environment. Without them five build tests fail on "declared task credentials are missing", which reads like a code failure and is not one. Setting VERO_SKIP_SECRET_CHECK instead would be wrong: it makes test_compiler_checks_secrets_before_writing_and_rejects_source_overlap vacuous, since that test asserts the check fires. - `targets`: one job per baseline agent, fail-fast off so a single broken agent does not mask the other five. Verified locally at Python 3.12 against ed55bc7 before committing: vero 447 passed / 11 skipped, browsecomp-plus 3, gaia 2, officeqa 2, swe-atlas-qna 2, swe-bench-pro 3, tau3 8. `uv lock --check` is clean in all seven projects, so --locked will not spuriously fail. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
Stacked on
pr3-harness-bench(#46). One file: the BrowseComp+ baseline agent.The scoring bug
When a completion came back with
finish_reason == "length", the agent submittedmessage.contentverbatim as its final answer:This target writes its chain-of-thought into ordinary
content(it has no separatereasoning_contentchannel) and routinely runs past the token cap. So what got submitted was not an answer, it was 42k to 52k characters of truncated, mid-sentence reasoning. The judge scores that 0 every time, and the trial is indistinguishable from a genuine wrong answer.Measured across the smoke runs carrying this change: 6 length-truncated turns over 3 trials, content length min 42,253 / median 46,793 / max 52,104 chars. The pre-fix baseline traces (198 trials over 3 rounds) contain no
finish_reasonfield at all, which is exactly why this went unnoticed. The instrumentation added here is what surfaced it.Truncated turns are now recoverable rather than final: the model is asked for only the three required labeled lines instead of having its unfinished thinking submitted on its behalf.
Supporting changes
All four target the same failure mode of losing a trial that had real retrieved evidence behind it.
Wall-clock deadline with a final-answer reserve. Turn count is not this agent's binding constraint. Most of a turn is sequential
search.pyinvocations, each booting a fresh pyserini/Lucene JVM against the BM25 index (~13s apiece) against ~3-9s of inference. Trials ran out of clock long beforeMAX_TURNS, so the existing turn-budget fallback never fired and harbor killed the trial mid-search with no answer recorded. There is now a deadline (BROWSECOMP_AGENT_BUDGET_SEC, default 900) with a reserve (BROWSECOMP_FINAL_RESERVE_SEC, default 120) so the best-effort answer is written before harbor'sAgentTimeoutError. Harbor does not tell an agent its own timeout, hence the env var; keep it in step with the run's--agent-timeout-multiplier.Tool calls past the deadline are stubbed, never skipped, because every
tool_callstill needs a matching tool message or the forced-final request is rejected for an unanswered call.Completion shape validation. A gateway can answer 200 with a body the SDK does not turn into a completion, observed once as
'str' object has no attribute 'usage', which crashed the trial on an attribute access two lines later._complete()now rejects anything without usable.choicesin one place.Empty turns nudge instead of raising.
raise RuntimeError("model returned neither a response nor a tool call")became a bounded retry, giving up only afterMAX_EMPTY_RETRIESconsecutive empty turns. An assistant message carrying neither content nor tool calls is dropped rather than appended, since strict OpenAI-compatible servers reject it.Bounded forced-final. The last-resort call uses
max_retries=1(the SDK default of 8 could outlast the reserve on its own) andANSWER_MAX_TOKENS(so the model cannot spend the whole reserve on chain-of-thought and get truncated a second time), and degrades to a stub answer on failure instead of propagating.Extra trace fields.
finish_reason, per-turnllm_sec,left_sec, and aforced_final_reasonofdeadlinevsturns, which lets a post-hoc conservative score treat exactly the trials the pre-fix agent would have lost as zeros.Rebased onto the current base
pr3-harness-benchmoved (and its history was rewritten) after this branch was cut, so the original commit no longer applied. Rebuilt as one commit on5727a44.The
_is_reasoning_modeloverlap flagged in the first version of this PR is resolved: #54's version of that gate has landed upstream and is kept verbatim here. All this PR now does to_completion_kwargsis route the already-computed token-limit key through a newmax_tokensparameter, which the bounded forced-final call needs:Also fixed in the rebase
The first version broke the existing test suite.
_completereaches the client throughwith_options(...), and the test fake was a bareSimpleNamespace, sotest_agent_submits_response_and_populates_contextdied onAttributeError: 'types.SimpleNamespace' object has no attribute 'with_options'. The fake is now a smallFakeClientthat implements it. Nothing in CI runs these target test suites (the repo has no workflows; the only checks are Greptile and Socket), which is why a red suite was not visible on the PR.Three tests added, each verified to fail without its fix:
test_truncated_reasoning_is_never_submitted_as_the_answertest_repeated_truncation_is_reported_as_empty_retriesturnsinstead ofempty_retrieswithout the new flag.FakeClient.with_optionsBoth Greptile findings addressed:
_tracesat inside the executed branch, so deadline-stubbed calls were invisible in the JSONL exactly when a post-hoc audit needs them. Moved out so both paths are traced.forced_final_reasonconflated two causes.empty_turns >= MAX_EMPTY_RETRIESalso reported"turns", so a trial that gave up on turn 4 after three truncated turns was indistinguishable from one that used all 32. There is now a third value,empty_retries, and the reason is computed once and used by both the trace andcontext.metadata.Verification
pytest tests/inbrowsecomp-plus/baseline/target: 5 passed.pytest tests/test_v05_benchmark_configs.py: 13 passed, 6 skipped.smoke-browsecomp-plus-qwenfix1/fix2 runs cited above.🤖 Generated with Claude Code
Greptile Summary
This PR fixes a scoring bug in the BrowseComp+ baseline agent where a
finish_reason == "length"response (truncated mid-sentence reasoning) was submitted verbatim as the final answer, scoring 0 every time. It also adds a wall-clock deadline to prevent harbor from killing trials mid-search with no answer recorded.finish_reason == "length"turns are now treated as empty retries (with a nudge to produce only the three labeled lines) rather than submitted as the answer; afterMAX_EMPTY_RETRIESconsecutive truncated/empty turns the agent falls through to the forced-final path.AGENT_BUDGET_SEC/FINAL_RESERVE_SECenv-var constants bound wall-clock time; tool calls past the deadline are stubbed (not skipped) so the forced-final request is never rejected for unanswered tool calls._complete()validates response shape before attribute access, the forced-final call usesmax_retries=1andANSWER_MAX_TOKENSto avoid re-truncation, andforced_final_reasonnow distinguishesdeadline/empty_retries/turnsfor post-hoc scoring.FakeClient.with_optionsrepairs the pre-existing broken test; two new regression tests (TruncatedThenAnswer,AlwaysTruncated) were each verified red before their respective fixes.Confidence Score: 5/5
Safe to merge. The fix is narrowly scoped to the BrowseComp+ baseline agent, all three exit paths are now correctly distinguished in metadata, and every changed behaviour is covered by a regression test verified red before its fix.
The change correctly guards the truncation bug, adds a wall-clock deadline that prevents silent trial loss, and restores the previously-broken test suite. The forced-final path degrades gracefully to a stub on any exception, so no code path can propagate an unhandled error. No logic errors or data-correctness issues were found.
Files Needing Attention: No files require special attention. The only note is three inline numeric literals (15, 20, 30) in agent.py that could join the existing named-constant block at the top of the file.
Important Files Changed
_complete()shape validation, and improved forced-final path. Logic is sound and well-tested.FakeClientwithwith_optionssupport to fix a pre-existing broken test, plus two new regression tests (TruncatedThenAnswer,AlwaysTruncated) that each verified red before their fix.Flowchart
%%{init: {'theme': 'neutral'}}%% flowchart TD A[run starts] --> B{remaining le FINAL_RESERVE_SEC?} B -->|yes| DEADLINE[out_of_time=True, break] B -->|no| C[_complete with timeout] C --> D{finish_reason} D -->|length truncated=True| E{empty_turns ge MAX_EMPTY_RETRIES?} D -->|stop with content| SC[_submit content, completed=True] D -->|tool_calls| T[process tool calls] E -->|yes| OOA[out_of_answers=True, break] E -->|no| NUDGE[append nudge message, continue] NUDGE --> B T --> DC{deadline hit mid-turn?} DC -->|yes stub result| TR[_trace event both paths] DC -->|no execute tool| TR TR --> SR{submit_response called?} SR -->|yes| DONE[completed=True, break] SR -->|no| OT{out_of_time?} OT -->|yes| DEADLINE OT -->|no| B DEADLINE --> FF OOA --> FF FF[forced-final: reason = deadline or empty_retries or turns] FF --> FFC[_complete tools=False, max_retries=1, ANSWER_MAX_TOKENS] FFC -->|success| FS[_submit answer] FFC -->|Exception| STUB[_submit stub answer] FS --> META[context.metadata forced_final_reason] STUB --> METAReviews (3): Last reviewed commit: "fix: stop submitting truncated reasoning..." | Re-trigger Greptile
Context used:
Learned From
scaleapi/scaleapi#126388