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
from
July 28, 2026 14:05
1b45a8f to
9b366c8
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 28, 2026 14:08
9b366c8 to
84f4aef
Compare
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, 42k–52k chars) was submitted verbatim as the final answer, which the judge scores 0 every time. It also adds wall-clock deadline management,_complete()shape validation, bounded empty-turn recovery, and richer trace instrumentation.finish_reason == \"length\"turns are now treated as recoverable — the agent sends a nudge requesting only the three labeled answer lines, abandoning afterMAX_EMPTY_RETRIES = 3consecutive failures, withforced_final_reasondistinguishing this from turn/deadline exhaustion.AGENT_BUDGET_SEC(env var, default 900s) with aFINAL_RESERVE_SEC(120s) reserve ensures the forced-final answer is written before harbor'sAgentTimeoutError; tool calls past the reserve are stubbed rather than skipped so the message history stays valid for OpenAI-compatible servers._complete()validates the gateway response shape in one place; the forced-final call is bounded tomax_retries=1andANSWER_MAX_TOKENS=3_000to prevent re-truncation.Confidence Score: 4/5
Safe to merge; closes a well-documented scoring failure with one remaining gap in the forced-final path.
The forced-final answer extraction at lines 488-489 can still submit truncated reasoning if the model exceeds ANSWER_MAX_TOKENS, reproducing the exact bug the PR fixes in the main loop. Everything else in the change is logically sound and well-tested.
Files Needing Attention: harness-engineering-bench/browsecomp-plus/baseline/target/src/browsecomp_plus_agent/agent.py lines 488-489
Important Files Changed
Flowchart
%%{init: {'theme': 'neutral'}}%% flowchart TD A([run starts]) --> B[set deadline = now + AGENT_BUDGET_SEC] B --> C{turn loop 1..MAX_TURNS} C --> D{remaining <= FINAL_RESERVE_SEC?} D -->|yes| E[out_of_time=True] E --> FF D -->|no| F[_complete timeout=remaining-reserve/2] F --> G{has tool_calls?} G -->|yes| H[empty_turns = 0] H --> I{deadline mid-turn?} I -->|yes| J[stub: search budget exhausted] I -->|no| K[execute tool] J --> L[append tool message] K --> L L --> M{submit_response called?} M -->|yes| N[completed=True break] M -->|no| O{out_of_time?} O -->|yes| FF O -->|no| C G -->|no| P{finish_reason == length?} P -->|yes| Q[truncated=True] P -->|no| R{content non-empty?} R -->|yes| S[_submit content completed=True break] R -->|no| Q2[truncated=False] Q --> T[empty_turns++] Q2 --> T T --> U{empty_turns >= MAX_EMPTY_RETRIES?} U -->|yes| V[out_of_answers=True break] U -->|no| W[append nudge continue] W --> C V --> FF FF([forced-final: if not completed]) FF --> FF1[reason = deadline / empty_retries / turns] FF1 --> FF2[_complete tools=False max_tokens=ANSWER_MAX_TOKENS max_retries=1] FF2 --> FF3[submit answer or stub write forced_final_reason]Reviews (2): Last reviewed commit: "fix: stop submitting truncated reasoning..." | Re-trigger Greptile