From 282688e37e4792ba559bafa67c3bd317cd483ced Mon Sep 17 00:00:00 2001 From: Teknium <127238744+teknium1@users.noreply.github.com> Date: Wed, 2 Sep 2026 18:34:22 -0700 Subject: [PATCH] refactor(agent/error_surface,i18n): fold layer resolution into helper, collapse guard ladders (strings untouched) --- agent/error_surface.py | 49 ++++++++++------------------- agent/i18n.py | 70 ++++++++++++++---------------------------- 2 files changed, 40 insertions(+), 79 deletions(-) diff --git a/agent/error_surface.py b/agent/error_surface.py index 0e041b4791..146583b8cf 100644 --- a/agent/error_surface.py +++ b/agent/error_surface.py @@ -2,16 +2,11 @@ Maps the internal failure taxonomy (``FailoverReason`` values carried in turn results as ``failure_reason``, or raw exceptions from the turn dispatcher) -onto a small, stable wire descriptor:: - - {"layer": , "code": , "retryable": } - -Layers (wire values): provider (model API rejected/failed the call), endpoint -(user-configured custom/local endpoint transport failure), streaming (SSE -dropped mid-turn), auth, billing (fallback signal; clients usually have a -richer ``billing_block``), gateway (local runtime errored), disk (disk full / -persistence failure). - +onto a small, stable wire descriptor ``{"layer", "code", "retryable"}``. +Layers: provider (model API rejected/failed), endpoint (user-configured +custom/local endpoint transport failure), streaming (SSE dropped mid-turn), +auth, billing (fallback signal; clients usually have a richer +``billing_block``), gateway (local runtime errored), disk (disk full). Dependency-light and NEVER raises: surfacing diagnostics must not break the error path it describes. Descriptors are advisory — clients fall back to string sniffing when absent or partial. @@ -35,10 +30,7 @@ LAYER_DISK = "disk" # failure_reason → UI layer. Unlisted reasons fall back to LAYER_PROVIDER: # every FailoverReason comes from classifying a provider call. _REASON_TO_LAYER = { - "auth": LAYER_AUTH, - "auth_permanent": LAYER_AUTH, - "billing": LAYER_BILLING, - "billing_unverified": LAYER_BILLING, + "auth": LAYER_AUTH, "auth_permanent": LAYER_AUTH, "billing": LAYER_BILLING, "billing_unverified": LAYER_BILLING, } # Failures between us and the base_url (not a provider verdict); on a @@ -102,6 +94,15 @@ def _disk_full(candidate: Any) -> bool: return False +def _result_layer(reason: str, error_text: str, provider: str) -> str: + layer = _REASON_TO_LAYER.get(reason) + if layer is not None: + return layer + if reason in _TRANSPORT_REASONS and _is_custom_endpoint(provider): + return LAYER_ENDPOINT + return LAYER_STREAMING if _looks_like_stream_drop(error_text) else LAYER_PROVIDER + + def build_error_surface_from_result(result: Any, provider: str = "", model: str = "") -> Optional[dict]: """Descriptor for a returned-error turn result (``failed=True`` dicts). @@ -115,35 +116,23 @@ def build_error_surface_from_result(result: Any, provider: str = "", model: str reason = str(result.get("failure_reason") or "").strip() if not error_text and not reason: return None - # Disk-full wins outright: the fix (free space) is unrelated to the # provider stack; hermes_state owns the pattern list. if error_text and _disk_full(error_text): return _surface(LAYER_DISK, "disk_full", False, provider, model) - if result.get("billing_block") or reason in ("billing", "billing_unverified"): return _surface(LAYER_BILLING, reason or "billing", False, provider, model) - if not reason: # Failed result without a classified reason (legacy paths). if _looks_like_stream_drop(error_text): return _surface(LAYER_STREAMING, "stream_drop", True, provider, model) return _surface(LAYER_PROVIDER, "unknown", True, provider, model) - - layer = _REASON_TO_LAYER.get(reason) - if layer is None: - if reason in _TRANSPORT_REASONS and _is_custom_endpoint(provider): - layer = LAYER_ENDPOINT - elif _looks_like_stream_drop(error_text): - layer = LAYER_STREAMING - else: - layer = LAYER_PROVIDER # Prefer the classifier's own verdict (``failure_retryable``); the # reason-set fallback covers older results. retryable = result.get("failure_retryable") if not isinstance(retryable, bool): retryable = reason not in _NON_RETRYABLE_REASONS - return _surface(layer, reason, retryable, provider, model) + return _surface(_result_layer(reason, error_text, provider), reason, retryable, provider, model) except Exception: # pragma: no cover — never break the error path logger.debug("error_surface: result classification failed", exc_info=True) return None @@ -157,13 +146,9 @@ def build_error_surface_from_exception(exc: BaseException, provider: str = "", m """ try: message = str(exc) or type(exc).__name__ - if _disk_full(exc): return _surface(LAYER_DISK, "disk_full", False, provider, model) - - exc_module = type(exc).__module__ or "" - api_like = exc_module.split(".")[0] in _API_EXC_MODULE_PREFIXES or hasattr(exc, "status_code") - + api_like = (type(exc).__module__ or "").split(".")[0] in _API_EXC_MODULE_PREFIXES or hasattr(exc, "status_code") if not api_like or not isinstance(exc, Exception): return _surface(LAYER_GATEWAY, type(exc).__name__, True, provider, model) diff --git a/agent/i18n.py b/agent/i18n.py index 8402df3064..33a63e2785 100644 --- a/agent/i18n.py +++ b/agent/i18n.py @@ -66,27 +66,20 @@ def _locales_dir() -> Path: return candidate logger.warning( "HERMES_BUNDLED_LOCALES points to a non-directory path (%s); " - "falling back to bundled/source locale resolution", - override, + "falling back to bundled/source locale resolution", override, ) return Path(__file__).resolve().parent.parent / "locales" def _normalize_lang(value: Any) -> str: """Map a user-supplied value (code, alias, or regional tag like ``zh-CN``) to a supported code, else default.""" - if not isinstance(value, str): - return DEFAULT_LANGUAGE - key = value.strip().lower() - if not key: - return DEFAULT_LANGUAGE + key = value.strip().lower() if isinstance(value, str) else "" if key in SUPPORTED_LANGUAGES: return key if key in _LANGUAGE_ALIASES: return _LANGUAGE_ALIASES[key] base = key.split("-", 1)[0] # strip region suffix - if base in SUPPORTED_LANGUAGES: - return base - return DEFAULT_LANGUAGE + return base if base in SUPPORTED_LANGUAGES else DEFAULT_LANGUAGE def _cache_catalog(lang: str, flat: dict[str, str]) -> dict[str, str]: @@ -103,20 +96,18 @@ def _load_catalog(lang: str) -> dict[str, str]: return cached path = _locales_dir() / f"{lang}.yaml" + flat: dict[str, str] = {} if not path.is_file(): logger.debug("i18n catalog missing for %s at %s", lang, path) - return _cache_catalog(lang, {}) - - try: - import yaml - with path.open("r", encoding="utf-8") as f: - raw = yaml.safe_load(f) or {} - except Exception as exc: - logger.warning("Failed to load i18n catalog %s: %s", path, exc) - return _cache_catalog(lang, {}) - - flat: dict[str, str] = {} - _flatten_into(raw, "", flat) + else: + try: + import yaml + with path.open("r", encoding="utf-8") as f: + raw = yaml.safe_load(f) or {} + except Exception as exc: + logger.warning("Failed to load i18n catalog %s: %s", path, exc) + else: + _flatten_into(raw, "", flat) return _cache_catalog(lang, flat) @@ -124,8 +115,7 @@ def _flatten_into(node: Any, prefix: str, out: dict[str, str]) -> None: # Non-string, non-dict leaves are ignored -- catalogs are text-only. if isinstance(node, dict): for key, value in node.items(): - child_key = f"{prefix}.{key}" if prefix else str(key) - _flatten_into(value, child_key, out) + _flatten_into(value, f"{prefix}.{key}" if prefix else str(key), out) elif isinstance(node, str): out[prefix] = node @@ -154,9 +144,7 @@ def reset_language_cache() -> None: def get_language() -> str: """Resolve the active language using env > config > default order.""" env_lang = os.environ.get("HERMES_LANGUAGE") - if env_lang: - return _normalize_lang(env_lang) - return _config_language_cached() or DEFAULT_LANGUAGE + return _normalize_lang(env_lang) if env_lang else _config_language_cached() or DEFAULT_LANGUAGE def t(key: str, lang: str | None = None, **format_kwargs: Any) -> str: @@ -167,30 +155,18 @@ def t(key: str, lang: str | None = None, **format_kwargs: Any) -> str: """ target = _normalize_lang(lang) if lang else get_language() value = _load_catalog(target).get(key) - if value is None and target != DEFAULT_LANGUAGE: value = _load_catalog(DEFAULT_LANGUAGE).get(key) - if value is None: logger.debug("i18n miss: key=%r lang=%r", key, target) value = key - - if format_kwargs: - try: - return value.format(**format_kwargs) - except (KeyError, IndexError, ValueError) as exc: - logger.warning( - "i18n format failed for key=%r lang=%r kwargs=%r: %s", - key, target, format_kwargs, exc, - ) - return value - return value + if not format_kwargs: + return value + try: + return value.format(**format_kwargs) + except (KeyError, IndexError, ValueError) as exc: + logger.warning("i18n format failed for key=%r lang=%r kwargs=%r: %s", key, target, format_kwargs, exc) + return value -__all__ = [ - "SUPPORTED_LANGUAGES", - "DEFAULT_LANGUAGE", - "t", - "get_language", - "reset_language_cache", -] +__all__ = ["SUPPORTED_LANGUAGES", "DEFAULT_LANGUAGE", "t", "get_language", "reset_language_cache"]