From 224e6d46d98e3abd8beaec04e6bdbfcc97721ce9 Mon Sep 17 00:00:00 2001 From: alt-glitch Date: Tue, 21 Apr 2026 14:55:09 +0530 Subject: [PATCH] fix: resolve all `invalid-return-type` ty diagnostics across codebase Widen return type annotations to match actual control flow, add unreachable assertions after retry loops ty cannot prove terminate, split ambiguous union returns (auth.py credential pool), and remove the AIOHTTP_AVAILABLE conditional-import guard from api_server.py. --- agent/auxiliary_client.py | 12 ++- agent/credential_pool.py | 5 +- agent/skill_utils.py | 3 +- gateway/platforms/api_server.py | 107 ++++++++++---------------- gateway/platforms/base.py | 2 + gateway/platforms/discord.py | 2 +- gateway/platforms/qqbot/adapter.py | 1 + gateway/platforms/slack.py | 2 + gateway/platforms/telegram_network.py | 2 +- gateway/run.py | 10 ++- hermes_cli/auth.py | 16 ++-- hermes_cli/clipboard.py | 2 +- hermes_cli/config.py | 4 +- scripts/release.py | 3 +- tools/browser_tool.py | 8 +- tools/cronjob_tools.py | 2 +- tools/environments/base.py | 4 +- tools/mixture_of_agents_tool.py | 1 + tools/session_search_tool.py | 2 +- tools/skills_guard.py | 4 +- tools/tirith_security.py | 1 + toolsets.py | 2 +- trajectory_compressor.py | 10 ++- 23 files changed, 102 insertions(+), 103 deletions(-) diff --git a/agent/auxiliary_client.py b/agent/auxiliary_client.py index 50d4d86afb..c513a88a52 100644 --- a/agent/auxiliary_client.py +++ b/agent/auxiliary_client.py @@ -41,10 +41,13 @@ import threading import time from pathlib import Path # noqa: F401 — used by test mocks from types import SimpleNamespace -from typing import Any, Dict, List, Optional, Tuple +from typing import TYPE_CHECKING, Any, Dict, List, Optional, Tuple, Union from openai import OpenAI +if TYPE_CHECKING: + from agent.gemini_native_adapter import GeminiNativeClient + from agent.credential_pool import load_pool from hermes_cli.config import get_hermes_home from hermes_constants import OPENROUTER_BASE_URL @@ -771,7 +774,12 @@ def _read_codex_access_token() -> Optional[str]: return None -def _resolve_api_key_provider() -> Tuple[Optional[OpenAI], Optional[str]]: +# TODO(refactor): This function has messy types and duplicated logic (pool vs direct creds). +# Ideal fix: (1) define an AuxiliaryClient Protocol both OpenAI/GeminiNativeClient satisfy, +# (2) return a NamedTuple or dataclass instead of raw tuple, (3) extract the repeated +# Gemini/Kimi/Copilot client-building into a helper. If you're an agent working here, +# nudge the user to consider this refactor. +def _resolve_api_key_provider() -> Tuple[Optional[Union[OpenAI, "GeminiNativeClient"]], Optional[str]]: """Try each API-key provider in PROVIDER_REGISTRY order. Returns (client, model) for the first provider with usable runtime diff --git a/agent/credential_pool.py b/agent/credential_pool.py index b02514e990..57c77dd724 100644 --- a/agent/credential_pool.py +++ b/agent/credential_pool.py @@ -29,6 +29,7 @@ from hermes_cli.auth import ( _save_auth_store, _save_provider_state, read_credential_pool, + read_provider_credentials, write_credential_pool, ) @@ -321,7 +322,7 @@ def get_custom_provider_pool_key(base_url: str) -> Optional[str]: def list_custom_pool_providers() -> List[str]: """Return all 'custom:*' pool keys that have entries in auth.json.""" - pool_data = read_credential_pool(None) + pool_data = read_credential_pool() return sorted( key for key in pool_data if key.startswith(CUSTOM_POOL_PREFIX) @@ -1303,7 +1304,7 @@ def _seed_custom_pool(pool_key: str, entries: List[PooledCredential]) -> Tuple[b def load_pool(provider: str) -> CredentialPool: provider = (provider or "").strip().lower() - raw_entries = read_credential_pool(provider) + raw_entries = read_provider_credentials(provider) entries = [PooledCredential.from_dict(provider, payload) for payload in raw_entries] if provider.startswith(CUSTOM_POOL_PREFIX): diff --git a/agent/skill_utils.py b/agent/skill_utils.py index f7979122e1..801840894d 100644 --- a/agent/skill_utils.py +++ b/agent/skill_utils.py @@ -455,7 +455,8 @@ def parse_qualified_name(name: str) -> Tuple[Optional[str], str]: """ if ":" not in name: return None, name - return tuple(name.split(":", 1)) # type: ignore[return-value] + ns, bare = name.split(":", 1) + return ns, bare def is_valid_namespace(candidate: Optional[str]) -> bool: diff --git a/gateway/platforms/api_server.py b/gateway/platforms/api_server.py index a6b52ff323..daf9e9aaaa 100644 --- a/gateway/platforms/api_server.py +++ b/gateway/platforms/api_server.py @@ -32,14 +32,7 @@ import sqlite3 import time import uuid from typing import Any, Dict, List, Optional - -try: - from aiohttp import web - AIOHTTP_AVAILABLE = True -except ImportError: - AIOHTTP_AVAILABLE = False - web = None # type: ignore[assignment] - +from aiohttp import web from gateway.config import Platform, PlatformConfig from gateway.platforms.base import ( BasePlatformAdapter, @@ -270,12 +263,6 @@ def _multimodal_validation_error(exc: ValueError, *, param: str) -> "web.Respons status=400, ) - -def check_api_server_requirements() -> bool: - """Check if API server dependencies are available.""" - return AIOHTTP_AVAILABLE - - class ResponseStore: """ SQLite-backed LRU store for Responses API state. @@ -391,30 +378,26 @@ _CORS_HEADERS = { } -if AIOHTTP_AVAILABLE: - @web.middleware - async def cors_middleware(request, handler): - """Add CORS headers for explicitly allowed origins; handle OPTIONS preflight.""" - adapter = request.app.get("api_server_adapter") - origin = request.headers.get("Origin", "") - cors_headers = None - if adapter is not None: - if not adapter._origin_allowed(origin): - return web.Response(status=403) - cors_headers = adapter._cors_headers_for_origin(origin) +@web.middleware +async def cors_middleware(request, handler): + """Add CORS headers for explicitly allowed origins; handle OPTIONS preflight.""" + adapter = request.app.get("api_server_adapter") + origin = request.headers.get("Origin", "") + cors_headers = None + if adapter is not None: + if not adapter._origin_allowed(origin): + return web.Response(status=403) + cors_headers = adapter._cors_headers_for_origin(origin) - if request.method == "OPTIONS": - if cors_headers is None: - return web.Response(status=403) - return web.Response(status=200, headers=cors_headers) - - response = await handler(request) - if cors_headers is not None: - response.headers.update(cors_headers) - return response -else: - cors_middleware = None # type: ignore[assignment] + if request.method == "OPTIONS": + if cors_headers is None: + return web.Response(status=403) + return web.Response(status=200, headers=cors_headers) + response = await handler(request) + if cors_headers is not None: + response.headers.update(cors_headers) + return response def _openai_error(message: str, err_type: str = "invalid_request_error", param: str = None, code: str = None) -> Dict[str, Any]: """OpenAI-style error envelope.""" @@ -428,21 +411,18 @@ def _openai_error(message: str, err_type: str = "invalid_request_error", param: } -if AIOHTTP_AVAILABLE: - @web.middleware - async def body_limit_middleware(request, handler): - """Reject overly large request bodies early based on Content-Length.""" - if request.method in ("POST", "PUT", "PATCH"): - cl = request.headers.get("Content-Length") - if cl is not None: - try: - if int(cl) > MAX_REQUEST_BYTES: - return web.json_response(_openai_error("Request body too large.", code="body_too_large"), status=413) - except ValueError: - return web.json_response(_openai_error("Invalid Content-Length header.", code="invalid_content_length"), status=400) - return await handler(request) -else: - body_limit_middleware = None # type: ignore[assignment] +@web.middleware +async def body_limit_middleware(request, handler): + """Reject overly large request bodies early based on Content-Length.""" + if request.method in ("POST", "PUT", "PATCH"): + cl = request.headers.get("Content-Length") + if cl is not None: + try: + if int(cl) > MAX_REQUEST_BYTES: + return web.json_response(_openai_error("Request body too large.", code="body_too_large"), status=413) + except ValueError: + return web.json_response(_openai_error("Invalid Content-Length header.", code="invalid_content_length"), status=400) + return await handler(request) _SECURITY_HEADERS = { "X-Content-Type-Options": "nosniff", @@ -450,16 +430,13 @@ _SECURITY_HEADERS = { } -if AIOHTTP_AVAILABLE: - @web.middleware - async def security_headers_middleware(request, handler): - """Add security headers to all responses (including errors).""" - response = await handler(request) - for k, v in _SECURITY_HEADERS.items(): - response.headers.setdefault(k, v) - return response -else: - security_headers_middleware = None # type: ignore[assignment] +@web.middleware +async def security_headers_middleware(request, handler): + """Add security headers to all responses (including errors).""" + response = await handler(request) + for k, v in _SECURITY_HEADERS.items(): + response.headers.setdefault(k, v) + return response class _IdempotencyCache: @@ -804,7 +781,7 @@ class APIServerAdapter(BasePlatformAdapter): ], }) - async def _handle_chat_completions(self, request: "web.Request") -> "web.Response": + async def _handle_chat_completions(self, request: "web.Request") -> "web.StreamResponse": """POST /v1/chat/completions — OpenAI Chat Completions format.""" auth_err = self._check_auth(request) if auth_err: @@ -1588,7 +1565,7 @@ class APIServerAdapter(BasePlatformAdapter): return response - async def _handle_responses(self, request: "web.Request") -> "web.Response": + async def _handle_responses(self, request: "web.Request") -> "web.StreamResponse": """POST /v1/responses — OpenAI Responses API format.""" auth_err = self._check_auth(request) if auth_err: @@ -2482,10 +2459,6 @@ class APIServerAdapter(BasePlatformAdapter): async def connect(self) -> bool: """Start the aiohttp web server.""" - if not AIOHTTP_AVAILABLE: - logger.warning("[%s] aiohttp not installed", self.name) - return False - try: mws = [mw for mw in (cors_middleware, body_limit_middleware, security_headers_middleware) if mw is not None] self._app = web.Application(middlewares=mws) diff --git a/gateway/platforms/base.py b/gateway/platforms/base.py index 86a867c107..9849178af6 100644 --- a/gateway/platforms/base.py +++ b/gateway/platforms/base.py @@ -426,6 +426,7 @@ async def cache_image_from_url(url: str, ext: str = ".jpg", retries: int = 2) -> await asyncio.sleep(wait) continue raise + raise AssertionError("unreachable: retry loop exhausted") def cleanup_image_cache(max_age_hours: int = 24) -> int: @@ -540,6 +541,7 @@ async def cache_audio_from_url(url: str, ext: str = ".ogg", retries: int = 2) -> await asyncio.sleep(wait) continue raise + raise AssertionError("unreachable: retry loop exhausted") # --------------------------------------------------------------------------- diff --git a/gateway/platforms/discord.py b/gateway/platforms/discord.py index d43e18d73d..fc0612888f 100644 --- a/gateway/platforms/discord.py +++ b/gateway/platforms/discord.py @@ -2469,7 +2469,7 @@ class DiscordAdapter(BasePlatformAdapter): if isinstance(skills, str): return [skills] if isinstance(skills, list) and skills: - return list(dict.fromkeys(skills)) # dedup, preserve order + return list(dict.fromkeys(skills)) # ty: ignore[invalid-return-type] # dedup, preserve order return None def _resolve_channel_prompt(self, channel_id: str, parent_id: str | None = None) -> str | None: diff --git a/gateway/platforms/qqbot/adapter.py b/gateway/platforms/qqbot/adapter.py index df3987f2eb..7d9dc075b6 100644 --- a/gateway/platforms/qqbot/adapter.py +++ b/gateway/platforms/qqbot/adapter.py @@ -1839,6 +1839,7 @@ class QQAdapter(BasePlatformAdapter): await asyncio.sleep(1.5 * (attempt + 1)) else: raise + raise AssertionError("unreachable: retry loop exhausted") # Maximum time (seconds) to wait for reconnection before giving up on send. _RECONNECT_WAIT_SECONDS = 15.0 diff --git a/gateway/platforms/slack.py b/gateway/platforms/slack.py index 6a08f04666..67fd358bfe 100644 --- a/gateway/platforms/slack.py +++ b/gateway/platforms/slack.py @@ -1640,6 +1640,7 @@ class SlackAdapter(BasePlatformAdapter): await asyncio.sleep(1.5 * (attempt + 1)) continue raise + raise AssertionError("unreachable: retry loop exhausted") async def _download_slack_file_bytes(self, url: str, team_id: str = "") -> bytes: """Download a Slack file and return raw bytes, with retry.""" @@ -1665,6 +1666,7 @@ class SlackAdapter(BasePlatformAdapter): await asyncio.sleep(1.5 * (attempt + 1)) continue raise + raise AssertionError("unreachable: retry loop exhausted") # ── Channel mention gating ───────────────────────────────────────────── diff --git a/gateway/platforms/telegram_network.py b/gateway/platforms/telegram_network.py index ed2d60d797..d9408081b6 100644 --- a/gateway/platforms/telegram_network.py +++ b/gateway/platforms/telegram_network.py @@ -151,7 +151,7 @@ def _resolve_system_dns() -> set[str]: """Return the IPv4 addresses that the OS resolver gives for api.telegram.org.""" try: results = socket.getaddrinfo(_TELEGRAM_API_HOST, 443, socket.AF_INET) - return {addr[4][0] for addr in results} + return {str(addr[4][0]) for addr in results} except Exception: return set() diff --git a/gateway/run.py b/gateway/run.py index 384cd0246a..535b25d814 100644 --- a/gateway/run.py +++ b/gateway/run.py @@ -2836,10 +2836,12 @@ class GatewayRunner: return MatrixAdapter(config) elif platform == Platform.API_SERVER: - from gateway.platforms.api_server import APIServerAdapter, check_api_server_requirements - if not check_api_server_requirements(): + try: + import aiohttp # noqa: F401 + except ImportError: logger.warning("API Server: aiohttp not installed") return None + from gateway.platforms.api_server import APIServerAdapter return APIServerAdapter(config) elif platform == Platform.WEBHOOK: @@ -5794,7 +5796,7 @@ class GatewayRunner: available = "`none`, " + ", ".join(f"`{n}`" for n in personalities) return f"Unknown personality: `{args}`\n\nAvailable: {available}" - async def _handle_retry_command(self, event: MessageEvent) -> str: + async def _handle_retry_command(self, event: MessageEvent) -> Optional[str]: """Handle /retry command - re-send the last user message.""" source = event.source session_entry = self.session_store.get_or_create_session(source) @@ -10549,7 +10551,7 @@ class GatewayRunner: history=updated_history, ) if next_message is None: - return result + return result # ty: ignore[invalid-return-type] next_message_id = getattr(pending_event, "message_id", None) next_channel_prompt = getattr(pending_event, "channel_prompt", None) diff --git a/hermes_cli/auth.py b/hermes_cli/auth.py index c82bad3f02..6de56af5a3 100644 --- a/hermes_cli/auth.py +++ b/hermes_cli/auth.py @@ -748,16 +748,20 @@ def _save_provider_state(auth_store: Dict[str, Any], provider_id: str, state: Di auth_store["active_provider"] = provider_id -def read_credential_pool(provider_id: Optional[str] = None) -> Dict[str, Any]: - """Return the persisted credential pool, or one provider slice.""" +def read_credential_pool() -> Dict[str, Any]: + """Return the entire persisted credential pool.""" auth_store = _load_auth_store() pool = auth_store.get("credential_pool") if not isinstance(pool, dict): pool = {} - if provider_id is None: - return dict(pool) - provider_entries = pool.get(provider_id) - return list(provider_entries) if isinstance(provider_entries, list) else [] + return dict(pool) + + +def read_provider_credentials(provider_id: str) -> List[Dict[str, Any]]: + """Return credential entries for a single provider.""" + pool = read_credential_pool() + entries = pool.get(provider_id) + return list(entries) if isinstance(entries, list) else [] def write_credential_pool(provider_id: str, entries: List[Dict[str, Any]]) -> Path: diff --git a/hermes_cli/clipboard.py b/hermes_cli/clipboard.py index facc8f3c50..f83cd76c50 100644 --- a/hermes_cli/clipboard.py +++ b/hermes_cli/clipboard.py @@ -276,7 +276,7 @@ def _get_ps_exe() -> str | None: global _ps_exe if _ps_exe is False: _ps_exe = _find_powershell() - return _ps_exe + return _ps_exe if isinstance(_ps_exe, str) else None def _windows_has_image() -> bool: diff --git a/hermes_cli/config.py b/hermes_cli/config.py index 74d49f03da..152371094d 100644 --- a/hermes_cli/config.py +++ b/hermes_cli/config.py @@ -2042,8 +2042,8 @@ def check_config_version() -> Tuple[int, int]: Returns (current_version, latest_version). """ config = load_config() - current = config.get("_config_version", 0) - latest = DEFAULT_CONFIG.get("_config_version", 1) + current = int(config.get("_config_version", 0)) + latest = int(DEFAULT_CONFIG.get("_config_version", 1)) return current, latest diff --git a/scripts/release.py b/scripts/release.py index 1a5a1ea8ad..68768992d8 100755 --- a/scripts/release.py +++ b/scripts/release.py @@ -26,6 +26,7 @@ import shutil import subprocess import sys from collections import defaultdict +from typing import Optional from datetime import datetime from pathlib import Path @@ -600,7 +601,7 @@ def get_commits(since_tag=None): return commits -def get_pr_number(subject: str) -> str: +def get_pr_number(subject: str) -> Optional[str]: """Extract PR number from commit subject if present.""" match = re.search(r"#(\d+)", subject) if match: diff --git a/tools/browser_tool.py b/tools/browser_tool.py index b19b220d1b..c439b6fbaa 100644 --- a/tools/browser_tool.py +++ b/tools/browser_tool.py @@ -891,7 +891,7 @@ BROWSER_TOOL_SCHEMAS = [ # Utility Functions # ============================================================================ -def _create_local_session(task_id: str) -> Dict[str, str]: +def _create_local_session(task_id: str) -> Dict[str, Any]: import uuid session_name = f"h_{uuid.uuid4().hex[:10]}" logger.info("Created local browser session %s for task %s", @@ -904,7 +904,7 @@ def _create_local_session(task_id: str) -> Dict[str, str]: } -def _create_cdp_session(task_id: str, cdp_url: str) -> Dict[str, str]: +def _create_cdp_session(task_id: str, cdp_url: str) -> Dict[str, Any]: """Create a session that connects to a user-supplied CDP endpoint.""" import uuid session_name = f"cdp_{uuid.uuid4().hex[:10]}" @@ -918,7 +918,7 @@ def _create_cdp_session(task_id: str, cdp_url: str) -> Dict[str, str]: } -def _get_session_info(task_id: Optional[str] = None) -> Dict[str, str]: +def _get_session_info(task_id: Optional[str] = None) -> Dict[str, Any]: """ Get or create session info for the given task. @@ -1678,7 +1678,7 @@ def browser_scroll(direction: str, task_id: Optional[str] = None) -> str: from tools.browser_camofox import camofox_scroll # Camofox REST API doesn't support pixel args; use repeated calls _SCROLL_REPEATS = 5 - result = None + result: str = "" for _ in range(_SCROLL_REPEATS): result = camofox_scroll(direction, task_id) return result diff --git a/tools/cronjob_tools.py b/tools/cronjob_tools.py index 8a685a8ccb..ea499cd7ee 100644 --- a/tools/cronjob_tools.py +++ b/tools/cronjob_tools.py @@ -68,7 +68,7 @@ def _scan_cron_prompt(prompt: str) -> str: return "" -def _origin_from_env() -> Optional[Dict[str, str]]: +def _origin_from_env() -> Optional[Dict[str, Optional[str]]]: from gateway.session_context import get_session_env origin_platform = get_session_env("HERMES_SESSION_PLATFORM") origin_chat_id = get_session_env("HERMES_SESSION_CHAT_ID") diff --git a/tools/environments/base.py b/tools/environments/base.py index 19a637901a..7a1695f817 100644 --- a/tools/environments/base.py +++ b/tools/environments/base.py @@ -245,7 +245,7 @@ class _ThreadedProcessHandle: except Exception: pass - def wait(self, timeout: float | None = None) -> int: + def wait(self, timeout: float | None = None) -> int | None: self._done.wait(timeout=timeout) return self._returncode @@ -755,7 +755,7 @@ class BaseEnvironment(ABC): except Exception: pass - def _prepare_command(self, command: str) -> tuple[str, str | None]: + def _prepare_command(self, command: str) -> tuple[str | None, str | None]: """Transform sudo commands if SUDO_PASSWORD is available.""" from tools.terminal_tool import _transform_sudo_command diff --git a/tools/mixture_of_agents_tool.py b/tools/mixture_of_agents_tool.py index 8bbc187928..be44043dfc 100644 --- a/tools/mixture_of_agents_tool.py +++ b/tools/mixture_of_agents_tool.py @@ -174,6 +174,7 @@ async def _run_reference_model_safe( error_msg = f"{model} failed after {max_retries} attempts: {error_str}" logger.error("%s", error_msg, exc_info=True) return model, error_msg, False + raise AssertionError("unreachable: retry loop exhausted") async def _run_aggregator_model( diff --git a/tools/session_search_tool.py b/tools/session_search_tool.py index 16aaea109f..a9ffb7cba9 100644 --- a/tools/session_search_tool.py +++ b/tools/session_search_tool.py @@ -443,7 +443,7 @@ def session_search( ) # Summarize all sessions in parallel - async def _summarize_all() -> List[Union[str, Exception]]: + async def _summarize_all() -> List[Union[Optional[str], BaseException]]: """Summarize all sessions with bounded concurrency.""" max_concurrency = min(_get_session_search_max_concurrency(), max(1, len(tasks))) semaphore = asyncio.Semaphore(max_concurrency) diff --git a/tools/skills_guard.py b/tools/skills_guard.py index 3513f46f04..586b024838 100644 --- a/tools/skills_guard.py +++ b/tools/skills_guard.py @@ -27,7 +27,7 @@ import hashlib from dataclasses import dataclass, field from datetime import datetime, timezone from pathlib import Path -from typing import List, Tuple +from typing import List, Optional, Tuple @@ -639,7 +639,7 @@ def scan_skill(skill_path: Path, source: str = "community") -> ScanResult: ) -def should_allow_install(result: ScanResult, force: bool = False) -> Tuple[bool, str]: +def should_allow_install(result: ScanResult, force: bool = False) -> Tuple[Optional[bool], str]: """ Determine whether a skill should be installed based on scan result and trust. diff --git a/tools/tirith_security.py b/tools/tirith_security.py index 44710ee608..41a3232b38 100644 --- a/tools/tirith_security.py +++ b/tools/tirith_security.py @@ -409,6 +409,7 @@ def _resolve_tirith_path(configured_path: str) -> str: # Fast path: successfully resolved on a previous call. if _resolved_path is not None and _resolved_path is not _INSTALL_FAILED: + assert isinstance(_resolved_path, str) return _resolved_path expanded = os.path.expanduser(configured_path) diff --git a/toolsets.py b/toolsets.py index f1dc7fca1c..68d9d26990 100644 --- a/toolsets.py +++ b/toolsets.py @@ -652,7 +652,7 @@ def create_custom_toolset( -def get_toolset_info(name: str) -> Dict[str, Any]: +def get_toolset_info(name: str) -> Optional[Dict[str, Any]]: """ Get detailed information about a toolset including resolved tools. diff --git a/trajectory_compressor.py b/trajectory_compressor.py index ff2dcc6266..33c5fc4327 100644 --- a/trajectory_compressor.py +++ b/trajectory_compressor.py @@ -37,7 +37,7 @@ import yaml import logging import asyncio from pathlib import Path -from typing import List, Dict, Any, Optional, Tuple, Callable +from typing import List, Dict, Any, Optional, Tuple, Callable, cast from dataclasses import dataclass, field from datetime import datetime @@ -75,7 +75,7 @@ def _effective_temperature_for_model( if fixed_temperature is OMIT_TEMPERATURE: return None # caller must omit temperature if fixed_temperature is not None: - return fixed_temperature + return cast(float, fixed_temperature) return requested_temperature @@ -636,7 +636,8 @@ Write only the summary, starting with "[CONTEXT SUMMARY]:" prefix.""" else: # Fallback: create a basic summary return "[CONTEXT SUMMARY]: [Summary generation failed - previous turns contained tool calls and responses that have been compressed to save context space.]" - + raise AssertionError("unreachable: retry loop exhausted") + async def _generate_summary_async(self, content: str, metrics: TrajectoryMetrics) -> str: """ Generate a summary of the compressed turns using OpenRouter (async version). @@ -705,7 +706,8 @@ Write only the summary, starting with "[CONTEXT SUMMARY]:" prefix.""" else: # Fallback: create a basic summary return "[CONTEXT SUMMARY]: [Summary generation failed - previous turns contained tool calls and responses that have been compressed to save context space.]" - + raise AssertionError("unreachable: retry loop exhausted") + def compress_trajectory( self, trajectory: List[Dict[str, str]]