Skip to content
Closed
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
30 changes: 9 additions & 21 deletions run_agent.py
Original file line number Diff line number Diff line change
Expand Up @@ -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,
Expand Down Expand Up @@ -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 (
Expand Down
26 changes: 26 additions & 0 deletions tests/run_agent/test_run_agent.py
Original file line number Diff line number Diff line change
Expand Up @@ -9,6 +9,7 @@
import json
import logging
import re
import ssl
import uuid
from logging.handlers import RotatingFileHandler
from pathlib import Path
Expand Down Expand Up @@ -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."""
Expand Down
Loading