Skip to content

fix(agent): keep the empty-retry budget when a streak has no known cost - #89215

Open
humdrum00001010 wants to merge 1 commit into
NousResearch:mainfrom
humdrum00001010:fix/empty-guard-unknown-cost-fails-open
Open

fix(agent): keep the empty-retry budget when a streak has no known cost#89215
humdrum00001010 wants to merge 1 commit into
NousResearch:mainfrom
humdrum00001010:fix/empty-guard-unknown-cost-fails-open

Conversation

@humdrum00001010

Copy link
Copy Markdown

What does this PR do?

deterministic_empty() aborts the empty-retry budget after two consecutive zero-output completions, but — unlike the cost-aware budget guard in the same module — it never consults cost. This gates the skip on the streak having a known cost, mirroring the rule the module already documents for guard #2:

Unknown pricing, missing usage, or included/subscription routes leave the budget untouched.

Both guards exist to prevent repeat charges ("the charged ~$2.33 for an empty answer incident class"). On a local/self-hosted endpoint there is nothing to save, so cutting the budget from 3 to 2 can only forfeit a turn the next attempt would have recovered.

Related Issue

Fixes #89213

Why the guard's premise doesn't hold there

The module states the rationale as "the same prompt will keep producing the same empty." I measured that directly. Capturing hermes↔endpoint traffic through a logging proxy, two calls in the same turn were byte-identical requests with different outcomes:

0023.json  msgs=2  finish=stop        EMPTY   ← first attempt
0024.json  msgs=2  finish=tool_calls  TOOLS   ← retry, same bytes
json.load(open('0023.json'))['request'] == json.load(open('0024.json'))['request']
# True

Measured empty rate was 2 of 6 first-attempts (~33%), so ~11% of turns hit two empties in a row and trip the guard. Each is recoverable. The endpoint is not at fault — 10/10 direct curl calls returned clean tool_calls.

Observed asymmetry on that setup:

cost estimate for a local attempt : None
retry budget                      : 3      ← guard #2 correctly fails open
deterministic_empty()             : True   ← guard #1 fires anyway

Type of Change

  • 🐛 Bug fix (non-breaking change that fixes an issue)
  • ✨ New feature
  • 🔒 Security fix
  • 📝 Documentation update
  • ✅ Tests
  • ♻️ Refactor

Changes Made

  • agent/empty_response_guard.pydeterministic_empty() returns False when streak_cost_usd() is None. Reuses the existing per-streak cost accessor, so no new state or config. Docstrings updated to record the fail-open condition.
  • tests/agent/test_empty_response_guard.py — 4 tests: unknown-cost and zero-cost streaks are not deterministic, a known-cost streak still is, and failing open on cost does not widen the retry budget.
  • tests/run_agent/test_run_agent.py — added test_unpriced_empty_streak_keeps_full_retry_budget (two empties then a success must reach the third call).

⚠️ I modified an existing test — please review this specifically

test_deterministic_empty_stops_retries_early asserted api_calls == 2 with agent.base_url = "http://127.0.0.1:1234/v1" — a localhost endpoint, which is exactly the unpriced case this PR makes fail open, so it broke.

Its own docstring describes the behaviour under test as:

the loop must stop re-billing the full input after the second attempt

but a localhost endpoint bills nothing, so the fixture contradicted the intent it documented. I pinned an estimated cost via patch("agent.empty_response_guard._estimate_attempt_cost", return_value=Decimal("1.10")) so it exercises the billed path it describes, and left the assertion (api_calls == 2) unchanged.

If maintainers intend the skip to apply on unpriced routes too, then this PR's premise is wrong and the right fix is narrower — e.g. requiring 3 consecutive empties instead of 2 when cost is unknown. Happy to switch.

How to Test

python -m pytest tests/agent/test_empty_response_guard.py tests/run_agent/test_run_agent.py -q

298 passed. The 2 bug-targeting guard tests were confirmed to fail without the source change and pass with it.

One unrelated pre-existing failure, verified identical on clean main at 2d511f5 and deselected in that run: TestAnthropicInterruptHandler::test_interruptible_anthropic_interrupt_never_closes_shared_clientImportError: The 'anthropic' package is required, an env artifact, not touched by this PR.

Checklist

deterministic_empty() aborted the retry budget after two consecutive
zero-output completions without ever consulting cost, unlike the
cost-aware budget guard in the same module ("Unknown pricing, missing
usage, or included/subscription routes leave the budget untouched").

Both guards exist to prevent repeat charges. On a local/self-hosted
endpoint there is nothing to save, so cutting the budget from 3 to 2 can
only forfeit a turn the next attempt would have recovered.

The guard's premise -- "the same prompt will keep producing the same
empty" -- does not hold there. Captured through a logging proxy, two
byte-identical requests in one turn returned EMPTY then a proper
tool_calls response; measured empty rate was 2 of 6 first-attempts, so
roughly 11% of turns hit two in a row and tripped the guard.

Gate the skip on streak_cost_usd() being known, mirroring the budget
guard. Paid routes are unaffected -- the motivating repeat-billing
incident still short-circuits after the second attempt.

test_deterministic_empty_stops_retries_early asserted this behaviour on
a localhost base_url while its own docstring described "re-billing the
full input". It now pins an estimated cost so it exercises the billed
path it documents, and a companion test covers the unpriced streak.

Fixes NousResearch#89213

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
@alt-glitch alt-glitch added type/bug Something isn't working P2 Medium — degraded but workaround exists comp/agent Core agent runtime: loop, agent_init, prompt builder, context-compression, responses endpoint area/usage-cost Token accounting, usage reporting, billing, cost tracking labels Aug 18, 2026
@Enough1122

Copy link
Copy Markdown
Contributor

AI code review — automated review for reference; please use your judgment.

Reviewed by reviewer-e (AI automated review).

Correct incentive-aligned fix for #89213: the deterministic-empty skip exists purely to avoid repeat charges, so a streak with None cost (local/self-hosted, unpriced model) or even an explicitly $0 one has nothing to save and failing open keeps recoverable turns recoverable — while the motivating paid-route behavior is unchanged and now explicitly pinned to a priced streak in the old test. The four new unit tests cover unknown/zero/known cost plus the important negative control that failing open doesn't inflate the retry budget, and the end-to-end test proves the exact reported shape (two local empties then a success on attempt 3).

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

area/usage-cost Token accounting, usage reporting, billing, cost tracking comp/agent Core agent runtime: loop, agent_init, prompt builder, context-compression, responses endpoint P2 Medium — degraded but workaround exists type/bug Something isn't working

Projects

None yet

Development

Successfully merging this pull request may close these issues.

[Bug]: deterministic-empty guard skips retries on zero-cost local endpoints, turning recoverable transient empties into "No reply"

3 participants