Skip to content

fix(agent): fail open in deterministic_empty for zero-cost endpoints - #89993

Closed
wodesiku wants to merge 1 commit into
NousResearch:mainfrom
wodesiku:fix/89213-local-endpoint-empty-retry-budget
Closed

fix(agent): fail open in deterministic_empty for zero-cost endpoints#89993
wodesiku wants to merge 1 commit into
NousResearch:mainfrom
wodesiku:fix/89213-local-endpoint-empty-retry-budget

Conversation

@wodesiku

Copy link
Copy Markdown

What does this PR do?

Fixes the deterministic-empty guard to fail open on local/self-hosted endpoints, preserving the full retry budget when no cost is known. Previously, the guard would skip retries after 2 consecutive empty completions regardless of cost, causing recoverable transient empties on local endpoints to surface as "No reply".

Related Issue

Fixes #89213

Root Cause

deterministic_empty() in agent/empty_response_guard.py checked only attempt count, usage presence, zero output, and signature equality — cost was never consulted. On a local/self-hosted endpoint (where streak_cost_usd() returns None), two consecutive transient empties would trigger the deterministic-empty guard and skip the remaining retries, even though:

  1. The same request succeeds on retry (measured: same bytes → different outcomes)
  2. There are no charges to avoid (the guard exists to prevent repeat billing)
  3. The cost-aware budget guard (empty_retry_budget) already correctly fails open on zero-cost routes

Fix

Add a cost check to deterministic_empty():

# Fail open when the streak has no known cost (local endpoints).
cost = streak_cost_usd(agent)
if cost is None:
    return False

This mirrors the logic in empty_retry_budget(), which already handles unknown pricing correctly. Both guards now fail open together when cost is absent.

Type of Change

  • 🐛 Bug fix (non-breaking change which fixes an issue)

Changes Made

  • agent/empty_response_guard.py — Added cost check to deterministic_empty() (5 lines of logic + docstring update)
  • tests/agent/test_empty_guard_local_endpoints.py (new) — 8 test cases:
    • Fail-open behavior on zero-cost streak
    • Deterministic detection still works on paid routes
    • Single-attempt boundary case
    • Cost-aware budget parity
    • Guard disabled skip
    • Mixed signature prevention
    • Full coverage of the new logic path

How to Test

Manual verification (reproduction of #89213)

  1. Set up a local LLM endpoint (e.g., MLX, llama.cpp, vLLM):

    # Example: local endpoint at http://localhost:8000/v1
    hermes config set model.provider custom
    hermes config set model.base_url http://localhost:8000/v1
    hermes config set model.model local-model
  2. Send a prompt that occasionally triggers transient empties:

    hermes chat -q "test prompt"
  3. Before this PR: After 2 consecutive empties, remaining retries are skipped → "⚠️ No reply"

  4. After this PR: Full 3 retries are attempted, and the recoverable empty succeeds on attempt 3

Automated tests

# Run the new test suite
pytest tests/agent/test_empty_guard_local_endpoints.py -v

# Expected: 8 passed

Unit test of the fix

python -c "
from agent.empty_response_guard import deterministic_empty, streak_cost_usd
from unittest.mock import Mock

agent = Mock()
agent._empty_guard_enabled = True
agent._empty_attempt_history = [Mock(usage_present=True, zero_output=True, signature=('model', 'custom', 'stop'))] * 2
agent._empty_streak_cost_usd = None  # No cost (local endpoint)

# Should NOT be deterministic
assert not deterministic_empty(agent)
print('✓ deterministic_empty fails open on zero-cost streak')
"

Impact Analysis

Measured empty rate on the reporter's local endpoint: 2 of 6 first-attempts (~33%), so ~11% of turns hit two empties in a row. Every one of those is recoverable — the same request succeeds on retry.

Behavior change: Previously-silent empty-object executions now consume a retry and surface an honest error result. Expect a minor retry-traffic uptick in sessions that were hitting the unrepairable path.

Cost: Zero. The guard exists to prevent repeat charges; with no known charge, there's no downside to the extra retries.

Checklist

Code

  • I've read the Contributing Guide
  • My commit messages follow Conventional Commits (fix(agent):)
  • I searched for existing PRs — none found for this specific issue
  • My PR contains only changes related to this fix (cost-check logic only)
  • I've added tests for my changes (8 test cases covering zero-cost + paid-route scenarios)
  • Tested on my platform: Windows 11 (git bash / MSYS)

Documentation & Housekeeping

  • I've updated relevant documentation — extended docstring explains the fail-open contract
  • N/A — no config keys added/changed
  • N/A — no architecture changes (inline logic addition)
  • Cross-platform impact considered — uses existing streak_cost_usd(), works on all platforms
  • N/A — no tool behavior changes

Design Notes

  • Minimal change: Only added 4 lines of logic (cost check + early return)
  • Symmetry: Both empty guards (deterministic + cost-aware budget) now handle unknown pricing identically
  • Backward compatible: Paid routes keep existing behavior (deterministic detection still fires when cost is known)
  • Fail-open principle: When in doubt (cost unknown), preserve retries

Verification Evidence

Before (from #89213)

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

Result: 3 → 2 attempts, recoverable turn surfaces as "⚠️ No reply"

After (with this fix)

cost estimate for a local attempt : None
accumulated streak cost           : None
retry budget                      : 3
deterministic_empty()             : False    ← now fails open

Result: Full 3 attempts, turn recovers on attempt 3

The deterministic-empty guard skipped retries after 2 consecutive empties
regardless of cost, causing recoverable transient empties on local endpoints
to surface as 'No reply'. The guard exists to prevent repeat charges on paid
routes; with no known charge, skipping retries has no upside.

This commit adds a cost check to deterministic_empty(): when streak_cost_usd()
returns None (local/self-hosted endpoints), the guard fails open and preserves
the full retry budget. This mirrors the existing fail-open logic in the
cost-aware budget guard (empty_retry_budget).

Changes:
- agent/empty_response_guard.py: add cost check to deterministic_empty (4 lines)
- tests/agent/test_empty_guard_local_endpoints.py (new): 8 test cases covering
  zero-cost fail-open, paid-route detection, boundary cases, and symmetry with
  the cost-aware budget guard

Fixes NousResearch#89213
@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/billing Account usage, credit usage, billing (cross-cutting) duplicate This issue or pull request already exists labels Aug 19, 2026
@alt-glitch

Copy link
Copy Markdown
Collaborator

This was generated by AI during triage.

Duplicate of #89215: both patches make the deterministic-empty guard fail open for unknown-cost streaks, preserving retries on local or unpriced endpoints.

@wodesiku

Copy link
Copy Markdown
Author

After reviewing the codebase more thoroughly, I realize this PR makes assumptions about the retry budget behavior that may not align with the project's intended design:

  1. The empty_retry_budget exists specifically for the empty-response pattern
  2. Adding a parallel zero-cost retry budget may introduce complexity without strong evidence of the problem's severity
  3. The Issue [Bug]: deterministic-empty guard skips retries on zero-cost local endpoints, turning recoverable transient empties into "No reply" #89213 data (33% empty responses) needs validation - it may be conflated with legitimate empty completions

Without clearer evidence that zero-cost retries are needed as a distinct pattern from empty-response retries, and without discussion with maintainers about the architectural intent, I'm closing this PR.

I should have opened an RFC/discussion first before implementing a solution. Thank you for your patience!

@wodesiku

Copy link
Copy Markdown
Author

Closing - should have discussed the architectural approach first. Will open an RFC if the problem persists.

@wodesiku wodesiku closed this Aug 20, 2026
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

area/billing Account usage, credit usage, billing (cross-cutting) comp/agent Core agent runtime: loop, agent_init, prompt builder, context-compression, responses endpoint duplicate This issue or pull request already exists 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"

2 participants