refactor(agent): consolidate inner-retry-loop recovery flags into TurnRetryState (god-file Phase 1b)

run_conversation's inner retry loop tracked recovery state in ~15 scattered
bare booleans (per-provider OAuth refresh guards, format-recovery guards,
restart signals). They are now fields on a single TurnRetryState dataclass the
loop mutates in place (_retry.<flag>), giving the recovery bookkeeping a named,
testable home.

Loop-control vars (retry_count, max_retries, max_compression_attempts) stay as
plain locals — they're while-mechanics, not recovery bookkeeping.

Behavior-neutral: pure local→attribute rewrite of 42 references; kwarg NAMES
preserved (e.g. has_retried_429=_retry.has_retried_429). Live simple + tool
turns OK.

Validation: tests/run_agent/ 1615 passed / 0 failed under per-file process
isolation; new test_turn_retry_state.py pins the field contract.
This commit is contained in:
teknium1
2026-06-07 22:42:05 -07:00
committed by Teknium
parent 4d926f248d
commit 524453dab5
3 changed files with 175 additions and 56 deletions
+43 -56
View File
@@ -32,6 +32,7 @@ from agent.display import KawaiiSpinner
from agent.error_classifier import FailoverReason, classify_api_error
from agent.iteration_budget import IterationBudget
from agent.turn_context import build_turn_context
from agent.turn_retry_state import TurnRetryState
from agent.memory_manager import build_memory_context_block
from agent.message_sanitization import (
_repair_tool_call_arguments,
@@ -798,22 +799,8 @@ def run_conversation(
api_start_time = time.time()
retry_count = 0
max_retries = agent._api_max_retries
primary_recovery_attempted = False
_retry = TurnRetryState()
max_compression_attempts = 3
codex_auth_retry_attempted=False
anthropic_auth_retry_attempted=False
nous_auth_retry_attempted=False
nous_paid_entitlement_refresh_attempted=False
copilot_auth_retry_attempted=False
thinking_sig_retry_attempted = False
invalid_encrypted_content_retry_attempted = False
image_shrink_retry_attempted = False
multimodal_tool_content_retry_attempted = False
oauth_1m_beta_retry_attempted = False
llama_cpp_grammar_retry_attempted = False
has_retried_429 = False
restart_with_compressed_messages = False
restart_with_length_continuation = False
finish_reason = "stop"
response = None # Guard against UnboundLocalError if all retries fail
@@ -846,7 +833,7 @@ def run_conversation(
if agent._try_activate_fallback():
retry_count = 0
compression_attempts = 0
primary_recovery_attempted = False
_retry.primary_recovery_attempted = False
continue
# No fallback available — surface buffered context
# so user sees the rate-limit message that led here.
@@ -1171,7 +1158,7 @@ def run_conversation(
if agent._try_activate_fallback():
retry_count = 0
compression_attempts = 0
primary_recovery_attempted = False
_retry.primary_recovery_attempted = False
continue
# Check for error field in response (some providers include this)
@@ -1242,7 +1229,7 @@ def run_conversation(
if agent._try_activate_fallback():
retry_count = 0
compression_attempts = 0
primary_recovery_attempted = False
_retry.primary_recovery_attempted = False
continue
# Terminal — flush buffered retry trace so user sees what happened.
agent._flush_status_buffer()
@@ -1466,7 +1453,7 @@ def run_conversation(
}
messages.append(continue_msg)
agent._session_messages = messages
restart_with_length_continuation = True
_retry.restart_with_length_continuation = True
break
partial_response = agent._strip_think_blocks("".join(truncated_response_parts)).strip()
@@ -1715,7 +1702,7 @@ def run_conversation(
f"({hit_pct:.0f}% hit, {written:,} written)"
)
has_retried_429 = False # Reset on success
_retry.has_retried_429 = False # Reset on success
# Note: don't clear the retry buffer here — an "API call
# success" only means we got bytes back, not that we got
# usable content. Empty responses still loop through the
@@ -2045,9 +2032,9 @@ def run_conversation(
getattr(agent, "provider", "") or "",
getattr(agent, "base_url", "") or "",
)
and not nous_paid_entitlement_refresh_attempted
and not _retry.nous_paid_entitlement_refresh_attempted
):
nous_paid_entitlement_refresh_attempted = True
_retry.nous_paid_entitlement_refresh_attempted = True
if _try_refresh_nous_paid_entitlement_credentials(agent):
agent._vprint(
f"{agent.log_prefix}🔐 Nous paid access verified — "
@@ -2056,9 +2043,9 @@ def run_conversation(
)
continue
recovered_with_pool, has_retried_429 = agent._recover_with_credential_pool(
recovered_with_pool, _retry.has_retried_429 = agent._recover_with_credential_pool(
status_code=status_code,
has_retried_429=has_retried_429,
has_retried_429=_retry.has_retried_429,
classified_reason=classified.reason,
error_context=error_context,
)
@@ -2073,9 +2060,9 @@ def run_conversation(
# fails, fall through to normal error handling.
if (
classified.reason == FailoverReason.image_too_large
and not image_shrink_retry_attempted
and not _retry.image_shrink_retry_attempted
):
image_shrink_retry_attempted = True
_retry.image_shrink_retry_attempted = True
if agent._try_shrink_image_parts_in_messages(api_messages):
agent._vprint(
f"{agent.log_prefix}📐 Image(s) exceeded provider size limit — "
@@ -2098,9 +2085,9 @@ def run_conversation(
# downgrade, and retry once. See issue #27344.
if (
classified.reason == FailoverReason.multimodal_tool_content_unsupported
and not multimodal_tool_content_retry_attempted
and not _retry.multimodal_tool_content_retry_attempted
):
multimodal_tool_content_retry_attempted = True
_retry.multimodal_tool_content_retry_attempted = True
if agent._try_strip_image_parts_from_tool_messages(api_messages):
agent._vprint(
f"{agent.log_prefix}📐 Provider rejected list-type tool content — "
@@ -2127,9 +2114,9 @@ def run_conversation(
classified.reason == FailoverReason.oauth_long_context_beta_forbidden
and agent.api_mode == "anthropic_messages"
and agent._is_anthropic_oauth
and not oauth_1m_beta_retry_attempted
and not _retry.oauth_1m_beta_retry_attempted
):
oauth_1m_beta_retry_attempted = True
_retry.oauth_1m_beta_retry_attempted = True
if not getattr(agent, "_oauth_1m_beta_disabled", False):
agent._oauth_1m_beta_disabled = True
try:
@@ -2148,9 +2135,9 @@ def run_conversation(
agent.api_mode == "codex_responses"
and agent.provider in {"openai-codex", "xai-oauth"}
and status_code == 401
and not codex_auth_retry_attempted
and not _retry.codex_auth_retry_attempted
):
codex_auth_retry_attempted = True
_retry.codex_auth_retry_attempted = True
if agent._try_refresh_codex_client_credentials(force=True):
_label = "xAI OAuth" if agent.provider == "xai-oauth" else "Codex"
agent._buffer_vprint(f"🔐 {_label} auth refreshed after 401. Retrying request...")
@@ -2159,9 +2146,9 @@ def run_conversation(
agent.api_mode == "chat_completions"
and agent.provider == "nous"
and status_code == 401
and not nous_auth_retry_attempted
and not _retry.nous_auth_retry_attempted
):
nous_auth_retry_attempted = True
_retry.nous_auth_retry_attempted = True
if agent._try_refresh_nous_client_credentials(force=True):
print(f"{agent.log_prefix}🔐 Nous agent key refreshed after 401. Retrying request...")
continue
@@ -2190,9 +2177,9 @@ def run_conversation(
if (
agent.provider == "copilot"
and status_code == 401
and not copilot_auth_retry_attempted
and not _retry.copilot_auth_retry_attempted
):
copilot_auth_retry_attempted = True
_retry.copilot_auth_retry_attempted = True
if agent._try_refresh_copilot_client_credentials():
agent._buffer_vprint(f"🔐 Copilot credentials refreshed after 401. Retrying request...")
continue
@@ -2200,9 +2187,9 @@ def run_conversation(
agent.api_mode == "anthropic_messages"
and status_code == 401
and hasattr(agent, '_anthropic_api_key')
and not anthropic_auth_retry_attempted
and not _retry.anthropic_auth_retry_attempted
):
anthropic_auth_retry_attempted = True
_retry.anthropic_auth_retry_attempted = True
from agent.anthropic_adapter import _is_oauth_token
from agent.azure_identity_adapter import is_token_provider
if agent._try_refresh_anthropic_client_credentials():
@@ -2243,9 +2230,9 @@ def run_conversation(
# blocks at all. One-shot — don't retry infinitely.
if (
classified.reason == FailoverReason.thinking_signature
and not thinking_sig_retry_attempted
and not _retry.thinking_sig_retry_attempted
):
thinking_sig_retry_attempted = True
_retry.thinking_sig_retry_attempted = True
for _m in messages:
if isinstance(_m, dict):
_m.pop("reasoning_details", None)
@@ -2277,7 +2264,7 @@ def run_conversation(
# handles it (the provider is rejecting something else).
if (
classified.reason == FailoverReason.invalid_encrypted_content
and not invalid_encrypted_content_retry_attempted
and not _retry.invalid_encrypted_content_retry_attempted
and agent.api_mode == "codex_responses"
and bool(getattr(agent, "_codex_reasoning_replay_enabled", True))
and any(
@@ -2288,7 +2275,7 @@ def run_conversation(
for _m in messages
)
):
invalid_encrypted_content_retry_attempted = True
_retry.invalid_encrypted_content_retry_attempted = True
replay_stats = agent._disable_codex_reasoning_replay(messages)
agent._vprint(
f"{agent.log_prefix}⚠️ Encrypted reasoning replay was rejected by the provider — "
@@ -2315,9 +2302,9 @@ def run_conversation(
# fires only for users on llama.cpp's OAI server.
if (
classified.reason == FailoverReason.llama_cpp_grammar_pattern
and not llama_cpp_grammar_retry_attempted
and not _retry.llama_cpp_grammar_retry_attempted
):
llama_cpp_grammar_retry_attempted = True
_retry.llama_cpp_grammar_retry_attempted = True
try:
from tools.schema_sanitizer import strip_pattern_and_format
_, _stripped = strip_pattern_and_format(agent.tools)
@@ -2528,7 +2515,7 @@ def run_conversation(
f"(was {old_ctx:,}), retrying..."
)
time.sleep(2)
restart_with_compressed_messages = True
_retry.restart_with_compressed_messages = True
break
# Fall through to normal error handling if compression
# is exhausted or didn't help.
@@ -2561,7 +2548,7 @@ def run_conversation(
if agent._try_activate_fallback(reason=classified.reason):
retry_count = 0
compression_attempts = 0
primary_recovery_attempted = False
_retry.primary_recovery_attempted = False
continue
# ── Nous Portal: record rate limit & skip retries ─────
@@ -2699,7 +2686,7 @@ def run_conversation(
if len(messages) < original_len:
agent._buffer_status(f"🗜️ Compressed {original_len}{len(messages)} messages, retrying...")
time.sleep(2) # Brief pause between compression retries
restart_with_compressed_messages = True
_retry.restart_with_compressed_messages = True
break
else:
# Terminal — surface buffered context so the user
@@ -2771,7 +2758,7 @@ def run_conversation(
"failed": True,
"compression_exhausted": True,
}
restart_with_compressed_messages = True
_retry.restart_with_compressed_messages = True
break
# Error is about the INPUT being too large. Only reduce
@@ -2856,7 +2843,7 @@ def run_conversation(
if len(messages) < original_len:
agent._buffer_status(f"🗜️ Compressed {original_len}{len(messages)} messages, retrying...")
time.sleep(2) # Brief pause between compression retries
restart_with_compressed_messages = True
_retry.restart_with_compressed_messages = True
break
else:
# Can't compress further and already at minimum tier
@@ -2961,7 +2948,7 @@ def run_conversation(
if agent._try_activate_fallback():
retry_count = 0
compression_attempts = 0
primary_recovery_attempted = False
_retry.primary_recovery_attempted = False
continue
if api_kwargs is not None:
agent._dump_api_request_debug(
@@ -3093,10 +3080,10 @@ def run_conversation(
# client once for transient transport errors (stale
# connection pool, TCP reset). Only attempted once
# per API call block.
if not primary_recovery_attempted and agent._try_recover_primary_transport(
if not _retry.primary_recovery_attempted and agent._try_recover_primary_transport(
api_error, retry_count=retry_count, max_retries=max_retries,
):
primary_recovery_attempted = True
_retry.primary_recovery_attempted = True
retry_count = 0
continue
# Try fallback before giving up entirely
@@ -3105,7 +3092,7 @@ def run_conversation(
if agent._try_activate_fallback():
retry_count = 0
compression_attempts = 0
primary_recovery_attempted = False
_retry.primary_recovery_attempted = False
continue
# Terminal — flush buffered retry/fallback trace.
agent._flush_status_buffer()
@@ -3256,17 +3243,17 @@ def run_conversation(
_turn_exit_reason = "interrupted_during_api_call"
break
if restart_with_compressed_messages:
if _retry.restart_with_compressed_messages:
api_call_count -= 1
agent.iteration_budget.refund()
# Count compression restarts toward the retry limit to prevent
# infinite loops when compression reduces messages but not enough
# to fit the context window.
retry_count += 1
restart_with_compressed_messages = False
_retry.restart_with_compressed_messages = False
continue
if restart_with_length_continuation:
if _retry.restart_with_length_continuation:
# Progressively boost the output token budget on each retry.
# Retry 1 → 2× base, retry 2 → 3× base, capped at 32 768.
# Applies to all providers via _ephemeral_max_output_tokens.