diff --git a/run_agent.py b/run_agent.py index d6af4a1b579e8..e7ae9b77f89fa 100644 --- a/run_agent.py +++ b/run_agent.py @@ -66,6 +66,14 @@ logger.info("No .env file found. Using system environment variables.") +def _is_local_validation_error(error: BaseException) -> bool: + """Return True for local request-shaping bugs, not transport failures.""" + return ( + isinstance(error, (ValueError, TypeError)) + and not isinstance(error, (UnicodeEncodeError, json.JSONDecodeError, ssl.SSLError)) + ) + + # Import our tool system from model_tools import ( get_tool_definitions, @@ -11344,27 +11352,7 @@ def _stop_spinner(): # already accounts for 413, 429, 529 (transient), context # overflow, and generic-400 heuristics. Local validation # errors (ValueError, TypeError) are programming bugs. - # Exclude UnicodeEncodeError — it's a ValueError subclass - # but is handled separately by the surrogate sanitization - # path above. Exclude json.JSONDecodeError — also a - # ValueError subclass, but it indicates a transient - # provider/network failure (malformed response body, - # truncated stream, routing layer corruption), not a - # local programming bug, and should be retried (#14782). - is_local_validation_error = ( - isinstance(api_error, (ValueError, TypeError)) - and not isinstance( - api_error, (UnicodeEncodeError, json.JSONDecodeError) - ) - # ssl.SSLError (and its subclass SSLCertVerificationError) - # inherits from OSError *and* ValueError via Python MRO, - # so the isinstance(ValueError) check above would - # misclassify a TLS transport failure as a local - # programming bug and abort without retrying. Exclude - # ssl.SSLError explicitly so the error classifier's - # retryable=True mapping takes effect instead. - and not isinstance(api_error, ssl.SSLError) - ) + is_local_validation_error = _is_local_validation_error(api_error) is_client_error = ( is_local_validation_error or ( diff --git a/tests/run_agent/test_run_agent.py b/tests/run_agent/test_run_agent.py index f58ebbf14c714..166e3a2563904 100644 --- a/tests/run_agent/test_run_agent.py +++ b/tests/run_agent/test_run_agent.py @@ -9,6 +9,7 @@ import json import logging import re +import ssl import uuid from logging.handlers import RotatingFileHandler from pathlib import Path @@ -52,6 +53,31 @@ def test_is_destructive_command_treats_install_as_mutating(): assert run_agent._is_destructive_command("install template.env .env") is True +def test_ssl_cert_verification_error_is_not_local_validation(): + """TLS certificate failures inherit ValueError but should remain retryable transport errors.""" + error = ssl.SSLCertVerificationError("certificate verify failed") + + assert isinstance(error, (ValueError, OSError)) + assert run_agent._is_local_validation_error(error) is False + + +@pytest.mark.parametrize("error", [ValueError("bad request"), TypeError("bad type")]) +def test_request_shaping_errors_are_local_validation(error): + assert run_agent._is_local_validation_error(error) is True + + +def test_unicode_encode_error_is_not_local_validation(): + error = UnicodeEncodeError("ascii", "é", 0, 1, "ordinal not in range") + + assert run_agent._is_local_validation_error(error) is False + + +def test_json_decode_error_is_not_local_validation(): + error = json.JSONDecodeError("bad response", "", 0) + + assert run_agent._is_local_validation_error(error) is False + + @pytest.fixture() def agent(): """Minimal AIAgent with mocked OpenAI client and tool loading."""