From c94ced62254d47c6f2f9fd848a976e98977eed32 Mon Sep 17 00:00:00 2001 From: Teknium <127238744+teknium1@users.noreply.github.com> Date: Wed, 2 Sep 2026 18:01:27 -0700 Subject: [PATCH] refactor(agent/turn): AST-neutral bracket/signature packing across r3-08 slice --- agent/transports/bedrock.py | 21 +- agent/transports/chat_completions.py | 69 ++--- agent/transports/codex.py | 30 +- agent/transports/codex_app_server.py | 15 +- agent/transports/codex_app_server_session.py | 55 +--- agent/transports/codex_event_projector.py | 5 +- agent/tts_provider.py | 20 +- agent/tts_registry.py | 8 +- agent/turn_api_call.py | 76 ++--- agent/turn_api_error.py | 280 +++++------------- agent/turn_api_request.py | 64 ++--- agent/turn_context.py | 184 ++++-------- agent/turn_context_compaction.py | 72 ++--- agent/turn_empty_response.py | 62 ++-- agent/turn_explainers.py | 16 +- agent/turn_facade.py | 42 +-- agent/turn_facade_lease.py | 27 +- agent/turn_final_response.py | 61 +--- agent/turn_finalizer.py | 86 ++---- agent/turn_iteration_prep.py | 64 +---- agent/turn_liveness.py | 19 +- agent/turn_loop_errors.py | 29 +- agent/turn_overflow.py | 95 ++----- agent/turn_preflight.py | 145 +++------- agent/turn_preflight_gate.py | 77 ++--- agent/turn_recovery.py | 282 +++++-------------- agent/turn_request_assembly.py | 55 +--- agent/turn_response_check.py | 178 +++--------- agent/turn_response_intake.py | 34 +-- agent/turn_stop_gates.py | 37 +-- agent/turn_tool_round.py | 83 ++---- agent/turn_tool_validation.py | 19 +- agent/turn_truncation.py | 109 ++----- agent/turn_usage.py | 50 +--- agent/usage_pricing.py | 128 +++------ agent/verification_evidence.py | 26 +- agent/verification_stop.py | 10 +- agent/verify/recipes.py | 15 +- agent/verify/runner.py | 21 +- agent/vertex_adapter.py | 3 +- agent/video_gen_provider.py | 120 ++------ agent/video_gen_registry.py | 3 +- agent/vision_message_prep.py | 11 +- agent/web_search_registry.py | 19 +- 44 files changed, 717 insertions(+), 2108 deletions(-) diff --git a/agent/transports/bedrock.py b/agent/transports/bedrock.py index e7aa316ac9..9a6f35ea3b 100644 --- a/agent/transports/bedrock.py +++ b/agent/transports/bedrock.py @@ -38,22 +38,15 @@ class BedrockTransport(ProviderTransport): return convert_tools_to_converse(tools) def build_kwargs( - self, - model: str, - messages: List[Dict[str, Any]], - tools: Optional[List[Dict[str, Any]]] = None, - **params, + self, model: str, messages: List[Dict[str, Any]], + tools: Optional[List[Dict[str, Any]]] = None, **params, ) -> Dict[str, Any]: """Build converse() kwargs; params: max_tokens (4096), temperature, guardrail_config, region ('us-east-1').""" from agent.bedrock_adapter import build_converse_kwargs kwargs = build_converse_kwargs( - model=model, - messages=messages, - tools=tools, - max_tokens=params.get("max_tokens", 4096), - temperature=params.get("temperature"), - guardrail_config=params.get("guardrail_config"), + model=model, messages=messages, tools=tools, max_tokens=params.get("max_tokens", 4096), + temperature=params.get("temperature"), guardrail_config=params.get("guardrail_config"), ) # Sentinel keys for dispatch — agent pops these before the boto3 call kwargs["__bedrock_converse__"] = True @@ -82,12 +75,10 @@ class BedrockTransport(ProviderTransport): provider_data["bedrock_content_blocks"] = msg.bedrock_content_blocks return NormalizedResponse( - content=msg.content, - tool_calls=tool_calls, + content=msg.content, tool_calls=tool_calls, finish_reason=choice.finish_reason or "stop", reasoning=getattr(msg, "reasoning", None) or getattr(msg, "reasoning_content", None), - usage=usage, - provider_data=provider_data or None, + usage=usage, provider_data=provider_data or None, ) def validate_response(self, response: Any) -> bool: diff --git a/agent/transports/chat_completions.py b/agent/transports/chat_completions.py index 541a3af56f..84c0dd2e5a 100644 --- a/agent/transports/chat_completions.py +++ b/agent/transports/chat_completions.py @@ -9,13 +9,8 @@ from typing import Any, Dict from agent.lmstudio_reasoning import resolve_lmstudio_effort from agent.reasoning_effort import ( - KIMI_K3_EFFORTS, - KIMI_K3_OVERRIDES, - OPENAI_COMPAT_WIRE_EFFORTS, - TOKENHUB_EFFORTS, - clamp_effort, - kimi_supported_efforts, - requested_effort, + KIMI_K3_EFFORTS, KIMI_K3_OVERRIDES, OPENAI_COMPAT_WIRE_EFFORTS, TOKENHUB_EFFORTS, clamp_effort, + kimi_supported_efforts, requested_effort, ) from agent.moonshot_schema import is_moonshot_model, sanitize_moonshot_tools from agent.prompt_builder import DEVELOPER_ROLE_MODELS @@ -49,9 +44,7 @@ def _rename_tool_search_bridge_for_xai( from agent.transports.codex import _alias_reserved_tools return _alias_reserved_tools( - tools, - ("tool_search",), - name_of=lambda t: (t.get("function") or {}).get("name"), + tools, ("tool_search",), name_of=lambda t: (t.get("function") or {}).get("name"), rename=lambda t, alias: {**t, "function": {**t["function"], "name": alias}}, ) @@ -73,13 +66,9 @@ def _static_prompt_instructions(messages: list[dict[str, Any]]) -> str: def _add_prompt_cache_key( - api_kwargs: dict[str, Any], - *, - messages: list[dict[str, Any]], - tools: list[dict[str, Any]] | None, - supports_prompt_cache_key: bool, - session_id: str | None = None, - cache_scope_id: str | None = None, + api_kwargs: dict[str, Any], *, messages: list[dict[str, Any]], + tools: list[dict[str, Any]] | None, supports_prompt_cache_key: bool, + session_id: str | None = None, cache_scope_id: str | None = None, ) -> None: """Add a content-addressed ``prompt_cache_key`` only for a capable endpoint. @@ -90,9 +79,7 @@ def _add_prompt_cache_key( # Share the Responses transport's hash + scope normalization so equivalent # prefixes hit one bucket across modes without merging unrelated sessions (#78941). from agent.transports.codex import ( - _bound_prompt_cache_key_field, - _cache_scope_from_session_id, - _content_cache_key, + _bound_prompt_cache_key_field, _cache_scope_from_session_id, _content_cache_key ) extra_body = api_kwargs.get("extra_body") @@ -104,8 +91,7 @@ def _add_prompt_cache_key( if not supports_prompt_cache_key: return cache_key = _content_cache_key( - _static_prompt_instructions(messages), - tools, + _static_prompt_instructions(messages), tools, _cache_scope_from_session_id(cache_scope_id or session_id), ) if cache_key: @@ -259,11 +245,8 @@ def _finish_kwargs( ) -> dict[str, Any]: """Tail shared by both build paths: content-addressed prompt_cache_key, then return.""" _add_prompt_cache_key( - api_kwargs, - messages=sanitized, - tools=api_kwargs.get("tools"), - supports_prompt_cache_key=supports_prompt_cache_key, - session_id=params.get("session_id"), + api_kwargs, messages=sanitized, tools=api_kwargs.get("tools"), + supports_prompt_cache_key=supports_prompt_cache_key, session_id=params.get("session_id"), cache_scope_id=params.get("cache_scope_id"), ) return api_kwargs @@ -349,10 +332,7 @@ class ChatCompletionsTransport(ProviderTransport): return tools def build_kwargs( - self, - model: str, - messages: list[dict[str, Any]], - tools: list[dict[str, Any]] | None = None, + self, model: str, messages: list[dict[str, Any]], tools: list[dict[str, Any]] | None = None, **params, ) -> dict[str, Any]: """Build chat.completions.create() kwargs. @@ -476,10 +456,8 @@ class ChatCompletionsTransport(ProviderTransport): extra_body_from_profile, top_level_from_profile = profile.build_api_kwargs_extras( reasoning_config=reasoning_config, supports_reasoning=params.get("supports_reasoning", False), - qwen_session_metadata=params.get("qwen_session_metadata"), - model=model, - base_url=params.get("base_url"), - ollama_num_ctx=params.get("ollama_num_ctx"), + qwen_session_metadata=params.get("qwen_session_metadata"), model=model, + base_url=params.get("base_url"), ollama_num_ctx=params.get("ollama_num_ctx"), session_id=params.get("session_id"), ) api_kwargs.update(top_level_from_profile) @@ -487,10 +465,8 @@ class ChatCompletionsTransport(ProviderTransport): extra_body: dict[str, Any] = {} profile_body = profile.build_extra_body( session_id=params.get("session_id"), - provider_preferences=params.get("provider_preferences"), - model=model, - base_url=params.get("base_url"), - reasoning_config=reasoning_config, + provider_preferences=params.get("provider_preferences"), model=model, + base_url=params.get("base_url"), reasoning_config=reasoning_config, openrouter_min_coding_score=params.get("openrouter_min_coding_score"), ) for part in (profile_body, extra_body_from_profile, params.get("extra_body_additions")): @@ -518,9 +494,7 @@ class ChatCompletionsTransport(ProviderTransport): if extra_body: api_kwargs["extra_body"] = extra_body return _finish_kwargs( - api_kwargs, - sanitized, - params, + api_kwargs, sanitized, params, supports_prompt_cache_key=bool(getattr(profile, "supports_prompt_cache_key", False)), ) @@ -572,8 +546,7 @@ class ChatCompletionsTransport(ProviderTransport): tc_provider_data["extra_content"] = extra tool_calls.append( ToolCall( - id=getattr(tc, "id", None), - name=function_name, + id=getattr(tc, "id", None), name=function_name, arguments=function_arguments if function_arguments is not None else "{}", provider_data=tc_provider_data or None, ) @@ -615,12 +588,8 @@ class ChatCompletionsTransport(ProviderTransport): finish_reason = "content_filter" return NormalizedResponse( - content=content, - tool_calls=tool_calls, - finish_reason=finish_reason, - reasoning=reasoning, - usage=usage, - provider_data=provider_data or None, + content=content, tool_calls=tool_calls, finish_reason=finish_reason, + reasoning=reasoning, usage=usage, provider_data=provider_data or None, ) def validate_response(self, response: Any) -> bool: diff --git a/agent/transports/codex.py b/agent/transports/codex.py index 5772643eec..647c6514c4 100644 --- a/agent/transports/codex.py +++ b/agent/transports/codex.py @@ -11,10 +11,7 @@ import re from typing import Any, Callable, Dict, List, Optional, Tuple from agent.reasoning_effort import ( - ACTUAL_RELAY_EFFORTS, - XAI_GROK46_EFFORTS, - XAI_LEGACY_EFFORTS, - clamp_effort, + ACTUAL_RELAY_EFFORTS, XAI_GROK46_EFFORTS, XAI_LEGACY_EFFORTS, clamp_effort, codex_supported_efforts, ) from agent.transports.base import ProviderTransport @@ -99,8 +96,7 @@ def _is_opencode_responses_backend(params: Dict[str, Any]) -> bool: def _alias_reserved_tools( - response_tools: List[Dict[str, Any]], - reserved_names: Tuple[str, ...], + response_tools: List[Dict[str, Any]], reserved_names: Tuple[str, ...], name_of: Callable[[dict], Any] = lambda t: t.get("name"), rename: Callable[[dict, str], dict] = lambda t, alias: {**t, "name": alias}, ) -> Tuple[List[Dict[str, Any]], Dict[str, str]]: @@ -393,8 +389,7 @@ class ResponsesApiTransport(ProviderTransport): issuer = self._resolve_issuer_kind(kwargs) self._last_issuer_kind = issuer return _chat_messages_to_responses_input( - messages, - is_xai_responses=kwargs.get("is_xai_responses") is True, + messages, is_xai_responses=kwargs.get("is_xai_responses") is True, is_github_responses=kwargs.get("is_github_responses") is True, replay_encrypted_reasoning=bool(kwargs.get("replay_encrypted_reasoning", True)), current_issuer_kind=issuer, @@ -407,11 +402,8 @@ class ResponsesApiTransport(ProviderTransport): return _responses_tools(tools) def build_kwargs( - self, - model: str, - messages: List[Dict[str, Any]], - tools: Optional[List[Dict[str, Any]]] = None, - **params, + self, model: str, messages: List[Dict[str, Any]], + tools: Optional[List[Dict[str, Any]]] = None, **params, ) -> Dict[str, Any]: """Build Responses API kwargs (calls convert_messages/convert_tools internally). @@ -458,8 +450,7 @@ class ResponsesApiTransport(ProviderTransport): "model": _strip_ctx_variant(model), "instructions": instructions, "input": _chat_messages_to_responses_input( - payload_messages, - is_xai_responses=is_xai_responses, + payload_messages, is_xai_responses=is_xai_responses, is_github_responses=is_github_responses, replay_encrypted_reasoning=replay_encrypted_reasoning, current_issuer_kind=issuer_kind, @@ -587,8 +578,7 @@ class ResponsesApiTransport(ProviderTransport): elif name in alias_map: name = alias_map[name] tool_calls.append(ToolCall( - id=tc.id if hasattr(tc, "id") else (name or None), - name=name, + id=tc.id if hasattr(tc, "id") else (name or None), name=name, arguments=tc.function.arguments if has_fn else getattr(tc, "arguments", "{}"), provider_data=provider_data or None, )) @@ -627,11 +617,7 @@ class ResponsesApiTransport(ProviderTransport): return status == "incomplete" and str(raw_reason or "").strip().lower() == "content_filter" def preflight_kwargs( - self, - api_kwargs: Any, - *, - allow_stream: bool = False, - is_github_responses: bool = False, + self, api_kwargs: Any, *, allow_stream: bool = False, is_github_responses: bool = False, sanitize_harmony_tokens: bool = False, ) -> dict: """Validate and sanitize Codex API kwargs before the call. diff --git a/agent/transports/codex_app_server.py b/agent/transports/codex_app_server.py index 9686118c59..89e36d106f 100644 --- a/agent/transports/codex_app_server.py +++ b/agent/transports/codex_app_server.py @@ -53,11 +53,8 @@ class CodexAppServerClient: """ def __init__( - self, - codex_bin: str = "codex", - codex_home: Optional[str] = None, - extra_args: Optional[list[str]] = None, - env: Optional[dict[str, str]] = None, + self, codex_bin: str = "codex", codex_home: Optional[str] = None, + extra_args: Optional[list[str]] = None, env: Optional[dict[str, str]] = None, ) -> None: self._codex_bin = codex_bin # codex needs LLM provider creds (inherit_credentials=True) but must not @@ -108,12 +105,8 @@ class CodexAppServerClient: self._stderr_reader.start() def initialize( - self, - client_name: str = "hermes", - client_title: str = "Hermes Agent", - client_version: str = "0.1", - capabilities: Optional[dict] = None, - timeout: float = 10.0, + self, client_name: str = "hermes", client_title: str = "Hermes Agent", + client_version: str = "0.1", capabilities: Optional[dict] = None, timeout: float = 10.0, ) -> dict: """Send ``initialize`` + ``initialized``; return the server's InitializeResponse.""" if self._initialized: diff --git a/agent/transports/codex_app_server_session.py b/agent/transports/codex_app_server_session.py index 17aeeb3a0e..446f52ae1d 100644 --- a/agent/transports/codex_app_server_session.py +++ b/agent/transports/codex_app_server_session.py @@ -20,8 +20,7 @@ from typing import Any, Callable, Optional from agent.codex_responses_adapter import _format_responses_error from agent.redact import redact_sensitive_text from agent.transports.codex_app_server import ( - CodexAppServerClient, - CodexAppServerError, + CodexAppServerClient, CodexAppServerError ) from agent.transports.codex_event_projector import CodexEventProjector, ProjectionResult @@ -175,12 +174,8 @@ class CodexAppServerSession: """ def __init__( - self, - *, - cwd: Optional[str] = None, - codex_bin: str = "codex", - codex_home: Optional[str] = None, - permission_profile: Optional[str] = None, + self, *, cwd: Optional[str] = None, codex_bin: str = "codex", + codex_home: Optional[str] = None, permission_profile: Optional[str] = None, approval_callback: Optional[Callable[..., str]] = None, on_event: Optional[Callable[[dict], None]] = None, request_routing: Optional[_ServerRequestRouting] = None, @@ -385,12 +380,8 @@ class CodexAppServerSession: return projection, aborted def run_turn( - self, - user_input: Any, - *, - turn_timeout: float = 600.0, - notification_poll_timeout: float = 0.25, - post_tool_quiet_timeout: float = 90.0, + self, user_input: Any, *, turn_timeout: float = 600.0, + notification_poll_timeout: float = 0.25, post_tool_quiet_timeout: float = 90.0, ) -> TurnResult: """Send a user message and block until turn/completed, bridging approvals and projecting items into Hermes' messages shape. @@ -491,14 +482,9 @@ class CodexAppServerSession: return True self._drive_turn( - result, - turn_timeout=turn_timeout, - notification_poll_timeout=notification_poll_timeout, - timeout_label="turn", - before_poll=watchdog_tripped, - on_server_request=on_server_request, - on_note=on_note, - accept_final_text_at_deadline=True, + result, turn_timeout=turn_timeout, notification_poll_timeout=notification_poll_timeout, + timeout_label="turn", before_poll=watchdog_tripped, on_server_request=on_server_request, + on_note=on_note, accept_final_text_at_deadline=True, ) with self._active_turn_lock: self._active_turn_id = None @@ -506,15 +492,9 @@ class CodexAppServerSession: return result def _drive_turn( - self, - result: TurnResult, - *, - turn_timeout: float, - notification_poll_timeout: float, - timeout_label: str, - on_server_request: Callable[[dict], bool], - on_note: Callable[[dict, str], bool], - before_poll: Optional[Callable[[], bool]] = None, + self, result: TurnResult, *, turn_timeout: float, notification_poll_timeout: float, + timeout_label: str, on_server_request: Callable[[dict], bool], + on_note: Callable[[dict, str], bool], before_poll: Optional[Callable[[], bool]] = None, pre_scope_filter: Optional[Callable[[dict, str], bool]] = None, accept_final_text_at_deadline: bool = False, ) -> None: @@ -579,10 +559,7 @@ class CodexAppServerSession: result.should_retire = True def compact_thread( - self, - *, - turn_timeout: float = 600.0, - notification_poll_timeout: float = 0.25, + self, *, turn_timeout: float = 600.0, notification_poll_timeout: float = 0.25 ) -> TurnResult: """Trigger Codex-native history compaction for the current thread. @@ -644,12 +621,8 @@ class CodexAppServerSession: return False self._drive_turn( - result, - turn_timeout=turn_timeout, - notification_poll_timeout=notification_poll_timeout, - timeout_label="compact turn", - on_server_request=on_server_request, - on_note=on_note, + result, turn_timeout=turn_timeout, notification_poll_timeout=notification_poll_timeout, + timeout_label="compact turn", on_server_request=on_server_request, on_note=on_note, pre_scope_filter=pre_scope_filter, ) return result diff --git a/agent/transports/codex_event_projector.py b/agent/transports/codex_event_projector.py index 1bf331e77d..649e44507f 100644 --- a/agent/transports/codex_event_projector.py +++ b/agent/transports/codex_event_projector.py @@ -111,10 +111,7 @@ class CodexEventProjector: return ProjectionResult(messages=[{"role": "user", "content": "\n".join(text_parts)}]) def _project_tool_item( - self, - item: dict, - item_id: str, - spec: Callable[[dict], tuple[str, str, dict, str]], + self, item: dict, item_id: str, spec: Callable[[dict], tuple[str, str, dict, str]] ) -> ProjectionResult: """Emit the (assistant tool_call, tool result) pair for a tool-shaped item. diff --git a/agent/tts_provider.py b/agent/tts_provider.py index d7e21ad781..5b6dfd7c0e 100644 --- a/agent/tts_provider.py +++ b/agent/tts_provider.py @@ -56,15 +56,8 @@ class TTSProvider(CatalogProviderBase): @abc.abstractmethod def synthesize( - self, - text: str, - output_path: str, - *, - voice: Optional[str] = None, - model: Optional[str] = None, - speed: Optional[float] = None, - format: str = DEFAULT_OUTPUT_FORMAT, - **extra: Any, + self, text: str, output_path: str, *, voice: Optional[str]=None, model: Optional[str]=None, + speed: Optional[float]=None, format: str=DEFAULT_OUTPUT_FORMAT, ** extra: Any, ) -> str: """Synthesize ``text`` into ``output_path`` and return the written path. @@ -77,13 +70,8 @@ class TTSProvider(CatalogProviderBase): """ def stream( - self, - text: str, - *, - voice: Optional[str] = None, - model: Optional[str] = None, - format: str = "opus", - **extra: Any, + self, text: str, *, voice: Optional[str] = None, model: Optional[str] = None, + format: str = "opus", **extra: Any, ) -> Iterator[bytes]: """Stream synthesized audio bytes (optional). diff --git a/agent/tts_registry.py b/agent/tts_registry.py index a94e1c095e..49c037c12b 100644 --- a/agent/tts_registry.py +++ b/agent/tts_registry.py @@ -58,11 +58,7 @@ def _warn_builtin_collision(key: str) -> None: # Case-insensitive, whitespace-tolerant keys mirror how # ``tools.tts_tool._get_provider`` normalizes the configured ``tts.provider``. _registry: ProviderRegistry[TTSProvider] = ProviderRegistry( - label="TTS", - provider_cls=TTSProvider, - logger=logger, - normalize=lower_key, - builtin_names=_BUILTIN_NAMES, - on_builtin_collision=_warn_builtin_collision, + label="TTS", provider_cls=TTSProvider, logger=logger, normalize=lower_key, + builtin_names=_BUILTIN_NAMES, on_builtin_collision=_warn_builtin_collision, ) _registry.export(globals()) diff --git a/agent/turn_api_call.py b/agent/turn_api_call.py index aabc740115..6c7aa3744d 100644 --- a/agent/turn_api_call.py +++ b/agent/turn_api_call.py @@ -30,19 +30,9 @@ class ApiCallVerdict: def perform_api_call( - agent: Any, - *, - api_kwargs: Any, - _original_api_kwargs: Any, - _llm_middleware_trace: Any, - _moa_prepared_request: Any, - _retry: Any, - thinking_spinner: Any, - retry_count: Any, - api_call_count: Any, - api_request_id: Any, - effective_task_id: Any, - turn_id: Any, + agent: Any, *, api_kwargs: Any, _original_api_kwargs: Any, _llm_middleware_trace: Any, + _moa_prepared_request: Any, _retry: Any, thinking_spinner: Any, retry_count: Any, + api_call_count: Any, api_request_id: Any, effective_task_id: Any, turn_id: Any, interrupted: Any, ) -> ApiCallVerdict: """Issue the request. Streaming is preferred even without consumers (stale-stream / @@ -52,9 +42,7 @@ def perform_api_call( def _verdict(action: str, result: Optional[Dict[str, Any]] = None) -> ApiCallVerdict: return ApiCallVerdict( - action=action, - response=response, - thinking_spinner=thinking_spinner, + action=action, response=response, thinking_spinner=thinking_spinner, interrupted=interrupted, ) @@ -97,9 +85,7 @@ def perform_api_call( def _perform_api_call(next_api_kwargs): if agent.api_mode == "codex_responses": next_api_kwargs = agent._get_transport().preflight_kwargs( - next_api_kwargs, - allow_stream=False, - is_github_responses=agent._is_copilot_url(), + next_api_kwargs, allow_stream=False, is_github_responses=agent._is_copilot_url(), sanitize_harmony_tokens=agent._is_codex_backend(), ) if _use_streaming: @@ -142,20 +128,11 @@ def perform_api_call( _redirect_crossed_response = False try: response = run_llm_execution_middleware( - api_kwargs, - _perform_api_call, - original_request=_original_api_kwargs, - task_id=effective_task_id, - turn_id=turn_id, - api_request_id=api_request_id, - session_id=agent.session_id or "", - platform=agent.platform or "", - model=agent.model, - provider=agent.provider, - base_url=agent.base_url, - api_mode=agent.api_mode, - api_call_count=api_call_count, - middleware_trace=list(_llm_middleware_trace), + api_kwargs, _perform_api_call, original_request=_original_api_kwargs, + task_id=effective_task_id, turn_id=turn_id, api_request_id=api_request_id, + session_id=agent.session_id or "", platform=agent.platform or "", model=agent.model, + provider=agent.provider, base_url=agent.base_url, api_mode=agent.api_mode, + api_call_count=api_call_count, middleware_trace=list(_llm_middleware_trace), ) finally: if _redirect_lock is not None: @@ -197,15 +174,8 @@ class ApiInterruptVerdict: def handle_api_interrupt( - agent: Any, - *, - _retry: Any, - thinking_spinner: Any, - messages: Any, - conversation_history: Any, - api_start_time: Any, - interrupted: Any, - final_response: Any, + agent: Any, *, _retry: Any, thinking_spinner: Any, messages: Any, conversation_history: Any, + api_start_time: Any, interrupted: Any, final_response: Any, ) -> ApiInterruptVerdict: """``InterruptedError`` during the provider call: a pending redirect keeps its correction queued for the outer-loop rebuild; otherwise keep any streamed partial text so the next @@ -266,16 +236,8 @@ class NousRateGuardVerdict: def nous_rate_limit_guard( - agent: Any, - *, - _retry: Any, - api_messages: Any, - messages: Any, - conversation_history: Any, - active_system_prompt: Any, - retry_count: Any, - compression_attempts: Any, - api_call_count: Any, + agent: Any, *, _retry: Any, api_messages: Any, messages: Any, conversation_history: Any, + active_system_prompt: Any, retry_count: Any, compression_attempts: Any, api_call_count: Any, ) -> NousRateGuardVerdict: """Skip the call if another session recorded a Nous Portal rate limit: every attempt (incl. SDK retries) counts against RPH. Never lets the guard itself break the agent loop.""" @@ -285,11 +247,8 @@ def nous_rate_limit_guard( def _verdict(action: str, result: Optional[Dict[str, Any]] = None) -> NousRateGuardVerdict: return NousRateGuardVerdict( - action=action, - active_system_prompt=active_system_prompt, - retry_count=retry_count, - compression_attempts=compression_attempts, - result=result, + action=action, active_system_prompt=active_system_prompt, retry_count=retry_count, + compression_attempts=compression_attempts, result=result, ) # ── Nous Portal rate limit guard ────────────────────── @@ -298,8 +257,7 @@ def nous_rate_limit_guard( if agent.provider == "nous": try: from agent.nous_rate_guard import ( - nous_rate_limit_remaining, - format_remaining as _fmt_nous_remaining, + nous_rate_limit_remaining, format_remaining as _fmt_nous_remaining ) _nous_remaining = nous_rate_limit_remaining() if _nous_remaining is not None and _nous_remaining > 0: diff --git a/agent/turn_api_error.py b/agent/turn_api_error.py index 0c05c78b6f..9c83543137 100644 --- a/agent/turn_api_error.py +++ b/agent/turn_api_error.py @@ -20,14 +20,9 @@ from agent.error_classifier import FailoverReason, classify_api_error from agent.message_sanitization import close_interrupted_tool_sequence from agent.turn_overflow import recover_from_overflow from agent.turn_recovery import ( - compute_error_backoff, - interruptible_backoff_sleep, - log_api_error_attempt, - max_retries_exhausted_result, - nonretryable_client_error_result, - recover_after_classification, - recover_before_classification, - route_classified_error, + compute_error_backoff, interruptible_backoff_sleep, log_api_error_attempt, + max_retries_exhausted_result, nonretryable_client_error_result, recover_after_classification, + recover_before_classification, route_classified_error, ) logger = logging.getLogger("agent.conversation_loop") @@ -55,27 +50,11 @@ class ApiErrorVerdict: def handle_api_error( - agent: Any, - *, - api_error: Any, - _retry: Any, - thinking_spinner: Any, - messages: Any, - api_messages: Any, - api_kwargs: Any, - system_message: Any, - active_system_prompt: Any, - conversation_history: Any, - approx_tokens: Any, - retry_count: Any, - max_retries: Any, - compression_attempts: Any, - max_compression_attempts: Any, - api_call_count: Any, - api_request_id: Any, - api_start_time: Any, - effective_task_id: Any, - turn_id: Any, + agent: Any, *, api_error: Any, _retry: Any, thinking_spinner: Any, messages: Any, + api_messages: Any, api_kwargs: Any, system_message: Any, active_system_prompt: Any, + conversation_history: Any, approx_tokens: Any, retry_count: Any, max_retries: Any, + compression_attempts: Any, max_compression_attempts: Any, api_call_count: Any, + api_request_id: Any, api_start_time: Any, effective_task_id: Any, turn_id: Any, ) -> ApiErrorVerdict: """Recover from ``api_error`` in the original order. Every fallback activation must leave the retry loop with ``restart_with_rebuilt_messages`` armed (``"break"``) so the pre-API @@ -84,17 +63,11 @@ def handle_api_error( def _verdict(action: str, result: Optional[Dict[str, Any]] = None) -> ApiErrorVerdict: return ApiErrorVerdict( - action=action, - thinking_spinner=thinking_spinner, - messages=messages, - active_system_prompt=active_system_prompt, - conversation_history=conversation_history, - approx_tokens=approx_tokens, - retry_count=retry_count, - max_retries=max_retries, + action=action, thinking_spinner=thinking_spinner, messages=messages, + active_system_prompt=active_system_prompt, conversation_history=conversation_history, + approx_tokens=approx_tokens, retry_count=retry_count, max_retries=max_retries, compression_attempts=compression_attempts, - _provider_overflow_recovery_pending=_provider_overflow_recovery_pending, - result=result, + _provider_overflow_recovery_pending=_provider_overflow_recovery_pending, result=result, ) # Stop spinner silently — retry status is buffered and @@ -108,11 +81,7 @@ def handle_api_error( # Pre-classification recovery (encoding sanitization, image rejection, # Bedrock SDK streaming fallback) — see agent/turn_recovery.py. _recovered, active_system_prompt = recover_before_classification( - agent, - api_error, - messages=messages, - api_messages=api_messages, - api_kwargs=api_kwargs, + agent, api_error, messages=messages, api_messages=api_messages, api_kwargs=api_kwargs, active_system_prompt=active_system_prompt, ) if _recovered: @@ -133,8 +102,7 @@ def handle_api_error( agent.log_prefix, api_call_count, api_error, ) _shutdown_summary = ( - "Turn abandoned: the process was shutting down " - "before the model call could complete." + "Turn abandoned: the process was shutting down " "before the model call could complete." ) return _verdict("return", { "final_response": _shutdown_summary, @@ -151,12 +119,9 @@ def handle_api_error( _compressor = getattr(agent, "context_compressor", None) _ctx_len = getattr(_compressor, "context_length", 200000) if _compressor else 200000 classified = classify_api_error( - api_error, - provider=getattr(agent, "provider", "") or "", - model=getattr(agent, "model", "") or "", - approx_tokens=approx_tokens, - context_length=_ctx_len, - num_messages=len(api_messages) if api_messages else 0, + api_error, provider=getattr(agent, "provider", "") or "", + model=getattr(agent, "model", "") or "", approx_tokens=approx_tokens, + context_length=_ctx_len, num_messages=len(api_messages) if api_messages else 0, ) logger.debug( "Error classified: reason=%s status=%s retryable=%s compress=%s rotate=%s fallback=%s", @@ -165,18 +130,10 @@ def handle_api_error( classified.should_rotate_credential, classified.should_fallback, ) agent._invoke_api_request_error_hook( - task_id=effective_task_id, - turn_id=turn_id, - api_request_id=api_request_id, - api_call_count=api_call_count, - api_start_time=api_start_time, - api_kwargs=api_kwargs, - error_type=type(api_error).__name__, - error_message=str(api_error), - status_code=status_code, - retry_count=retry_count, - max_retries=max_retries, - retryable=classified.retryable, + task_id=effective_task_id, turn_id=turn_id, api_request_id=api_request_id, + api_call_count=api_call_count, api_start_time=api_start_time, api_kwargs=api_kwargs, + error_type=type(api_error).__name__, error_message=str(api_error), status_code=status_code, + retry_count=retry_count, max_retries=max_retries, retryable=classified.retryable, reason=classified.reason.value, ) @@ -184,14 +141,8 @@ def handle_api_error( # pool, image/multimodal strips, per-provider 401 refresh, format-recovery # strips) — see agent/turn_recovery.py. _recovered, recovered_with_pool = recover_after_classification( - agent, - api_error, - classified, - _retry, - status_code=status_code, - error_context=error_context, - messages=messages, - api_messages=api_messages, + agent, api_error, classified, _retry, status_code=status_code, error_context=error_context, + messages=messages, api_messages=api_messages, ) if _recovered: return _verdict("continue") @@ -203,14 +154,8 @@ def handle_api_error( ) error_type, error_msg, _provider, _base, _model = log_api_error_attempt( - agent, - api_error, - retry_count=retry_count, - max_retries=max_retries, - status_code=status_code, - elapsed_time=elapsed_time, - api_messages=api_messages, - approx_tokens=approx_tokens, + agent, api_error, retry_count=retry_count, max_retries=max_retries, status_code=status_code, + elapsed_time=elapsed_time, api_messages=api_messages, approx_tokens=approx_tokens, ) # Check for interrupt before deciding to retry @@ -234,25 +179,12 @@ def handle_api_error( }) _ce = route_classified_error( - agent, - api_error, - classified, - _retry, - error_msg=error_msg, - error_context=error_context, - recovered_with_pool=recovered_with_pool, - base_url=_base, - model=_model, - messages=messages, - api_messages=api_messages, - system_message=system_message, - active_system_prompt=active_system_prompt, - conversation_history=conversation_history, - retry_count=retry_count, - max_retries=max_retries, - compression_attempts=compression_attempts, - max_compression_attempts=max_compression_attempts, - api_call_count=api_call_count, + agent, api_error, classified, _retry, error_msg=error_msg, error_context=error_context, + recovered_with_pool=recovered_with_pool, base_url=_base, model=_model, messages=messages, + api_messages=api_messages, system_message=system_message, + active_system_prompt=active_system_prompt, conversation_history=conversation_history, + retry_count=retry_count, max_retries=max_retries, compression_attempts=compression_attempts, + max_compression_attempts=max_compression_attempts, api_call_count=api_call_count, effective_task_id=effective_task_id, ) status_code = _ce.status_code @@ -275,22 +207,12 @@ def handle_api_error( return _verdict("continue") _ov = recover_from_overflow( - agent, - api_error, - classified, - _retry, - status_code=status_code, - error_msg=error_msg, - wrapped_output_cap_budget=_wrapped_output_cap_budget, - messages=messages, - api_messages=api_messages, - system_message=system_message, - active_system_prompt=active_system_prompt, - conversation_history=conversation_history, - approx_tokens=approx_tokens, - compression_attempts=compression_attempts, - max_compression_attempts=max_compression_attempts, - api_call_count=api_call_count, + agent, api_error, classified, _retry, status_code=status_code, error_msg=error_msg, + wrapped_output_cap_budget=_wrapped_output_cap_budget, messages=messages, + api_messages=api_messages, system_message=system_message, + active_system_prompt=active_system_prompt, conversation_history=conversation_history, + approx_tokens=approx_tokens, compression_attempts=compression_attempts, + max_compression_attempts=max_compression_attempts, api_call_count=api_call_count, effective_task_id=effective_task_id, ) messages = _ov.messages @@ -309,27 +231,13 @@ def handle_api_error( return _verdict("continue") _ue = settle_unrecovered_error( - agent, - api_error=api_error, - classified=classified, - _retry=_retry, - status_code=status_code, - error_msg=error_msg, - is_context_length_error=is_context_length_error, - is_rate_limited=is_rate_limited, - _is_zai_coding_overload=_is_zai_coding_overload, - _provider=_provider, - _base=_base, - _model=_model, - messages=messages, - api_messages=api_messages, - api_kwargs=api_kwargs, - active_system_prompt=active_system_prompt, - conversation_history=conversation_history, - approx_tokens=approx_tokens, - retry_count=retry_count, - max_retries=max_retries, - compression_attempts=compression_attempts, + agent, api_error=api_error, classified=classified, _retry=_retry, status_code=status_code, + error_msg=error_msg, is_context_length_error=is_context_length_error, + is_rate_limited=is_rate_limited, _is_zai_coding_overload=_is_zai_coding_overload, + _provider=_provider, _base=_base, _model=_model, messages=messages, + api_messages=api_messages, api_kwargs=api_kwargs, active_system_prompt=active_system_prompt, + conversation_history=conversation_history, approx_tokens=approx_tokens, + retry_count=retry_count, max_retries=max_retries, compression_attempts=compression_attempts, api_call_count=api_call_count, ) active_system_prompt = _ue.active_system_prompt @@ -354,29 +262,11 @@ class UnrecoveredErrorVerdict: def settle_unrecovered_error( - agent: Any, - *, - api_error: Any, - classified: Any, - _retry: Any, - status_code: Any, - error_msg: Any, - is_context_length_error: Any, - is_rate_limited: Any, - _is_zai_coding_overload: Any, - _provider: Any, - _base: Any, - _model: Any, - messages: Any, - api_messages: Any, - api_kwargs: Any, - active_system_prompt: Any, - conversation_history: Any, - approx_tokens: Any, - retry_count: Any, - max_retries: Any, - compression_attempts: Any, - api_call_count: Any, + agent: Any, *, api_error: Any, classified: Any, _retry: Any, status_code: Any, error_msg: Any, + is_context_length_error: Any, is_rate_limited: Any, _is_zai_coding_overload: Any, + _provider: Any, _base: Any, _model: Any, messages: Any, api_messages: Any, api_kwargs: Any, + active_system_prompt: Any, conversation_history: Any, approx_tokens: Any, retry_count: Any, + max_retries: Any, compression_attempts: Any, api_call_count: Any, ) -> UnrecoveredErrorVerdict: """Decide the fate of an API error that every recovery chain declined: local validation / non-retryable client errors (Copilot stale-credential self-heal first, then fallback, then a @@ -384,18 +274,13 @@ def settle_unrecovered_error( result), else the interruptible error backoff. ``FailoverReason.billing`` (402) is deliberately treated as non-retryable (#31273).""" from agent.conversation_loop import ( - _arm_fallback_restart, - _is_copilot_provider, - _is_stale_copilot_credential_error, + _arm_fallback_restart, _is_copilot_provider, _is_stale_copilot_credential_error ) def _verdict(action: str, result: Optional[Dict[str, Any]] = None) -> UnrecoveredErrorVerdict: return UnrecoveredErrorVerdict( - action=action, - active_system_prompt=active_system_prompt, - retry_count=retry_count, - compression_attempts=compression_attempts, - result=result, + action=action, active_system_prompt=active_system_prompt, retry_count=retry_count, + compression_attempts=compression_attempts, result=result, ) # Non-retryable: ValueError/TypeError are local bugs, except @@ -428,12 +313,9 @@ def settle_unrecovered_error( not classified.retryable and not classified.should_compress and classified.reason not in { - FailoverReason.rate_limit, - FailoverReason.overloaded, - FailoverReason.context_overflow, - FailoverReason.payload_too_large, - FailoverReason.long_context_tier, - FailoverReason.thinking_signature, + FailoverReason.rate_limit, FailoverReason.overloaded, + FailoverReason.context_overflow, FailoverReason.payload_too_large, + FailoverReason.long_context_tier, FailoverReason.thinking_signature, } ) ) and not is_context_length_error @@ -474,19 +356,10 @@ def settle_unrecovered_error( compression_attempts = 0 return _verdict("break") return _verdict("return", nonretryable_client_error_result( - agent, - api_error, - classified, - status_code=status_code, - api_kwargs=api_kwargs, - api_messages=api_messages, - messages=messages, - conversation_history=conversation_history, - api_call_count=api_call_count, - approx_tokens=approx_tokens, - provider=_provider, - base_url=_base, - model=_model, + agent, api_error, classified, status_code=status_code, api_kwargs=api_kwargs, + api_messages=api_messages, messages=messages, conversation_history=conversation_history, + api_call_count=api_call_count, approx_tokens=approx_tokens, provider=_provider, + base_url=_base, model=_model, )) if retry_count >= max_retries: @@ -514,39 +387,22 @@ def settle_unrecovered_error( compression_attempts = 0 return _verdict("break") return _verdict("return", max_retries_exhausted_result( - agent, - api_error, - classified, - max_retries=max_retries, - is_rate_limited=is_rate_limited, - error_msg=error_msg, - api_kwargs=api_kwargs, - api_messages=api_messages, - messages=messages, - conversation_history=conversation_history, - api_call_count=api_call_count, - approx_tokens=approx_tokens, - provider=_provider, - base_url=_base, - model=_model, + agent, api_error, classified, max_retries=max_retries, is_rate_limited=is_rate_limited, + error_msg=error_msg, api_kwargs=api_kwargs, api_messages=api_messages, + messages=messages, conversation_history=conversation_history, + api_call_count=api_call_count, approx_tokens=approx_tokens, provider=_provider, + base_url=_base, model=_model, )) wait_time = compute_error_backoff( - agent, - api_error, - retry_count=retry_count, - max_retries=max_retries, - is_rate_limited=is_rate_limited, - is_zai_coding_overload=_is_zai_coding_overload, - base_url=_base, - model=_model, + agent, api_error, retry_count=retry_count, max_retries=max_retries, + is_rate_limited=is_rate_limited, is_zai_coding_overload=_is_zai_coding_overload, + base_url=_base, model=_model, ) # Same preserve-redirect rule as the invalid-response wait: a steering # correction must survive backoff, not die as "Operation interrupted". _interrupted = interruptible_backoff_sleep( - agent, wait_time, _retry, - messages=messages, - conversation_history=conversation_history, + agent, wait_time, _retry, messages=messages, conversation_history=conversation_history, api_call_count=api_call_count, abort_message="Interrupt detected during retry wait, aborting.", interrupt_text=f"Operation interrupted: retrying API call after error (retry {retry_count}/{max_retries}).", diff --git a/agent/turn_api_request.py b/agent/turn_api_request.py index 7cb6a6f20b..6aca189616 100644 --- a/agent/turn_api_request.py +++ b/agent/turn_api_request.py @@ -13,8 +13,7 @@ import logging from typing import Any, Dict, Optional from agent.message_sanitization import ( - _sanitize_structure_non_ascii, - _sanitize_structure_surrogates, + _sanitize_structure_non_ascii, _sanitize_structure_surrogates ) from utils import env_var_enabled @@ -36,28 +35,15 @@ class ApiRequestBuild: def build_api_request( - agent: Any, - *, - api_messages: Any, - _moa_prepared_request: Any, - tools_for_api: Any, - system_message: Any, - messages: Any, - original_user_message: Any, - approx_tokens: Any, - total_chars: Any, - retry_count: Any, - api_call_count: Any, - api_request_id: Any, - api_start_time: Any, - effective_task_id: Any, - turn_id: Any, + agent: Any, *, api_messages: Any, _moa_prepared_request: Any, tools_for_api: Any, + system_message: Any, messages: Any, original_user_message: Any, approx_tokens: Any, + total_chars: Any, retry_count: Any, api_call_count: Any, api_request_id: Any, + api_start_time: Any, effective_task_id: Any, turn_id: Any, ) -> ApiRequestBuild: """Assemble the attempt's request in the original order (every mutation happens BEFORE middleware/hooks/debug dumps observe the payload).""" from agent.conversation_loop import ( - _moa_client_consumes_prepared_request, - _redecorate_prompt_cache_for_provider, + _moa_client_consumes_prepared_request, _redecorate_prompt_cache_for_provider, _system_prompt_for_hooks, ) api_kwargs = None @@ -66,13 +52,9 @@ def build_api_request( def _verdict(action: str, result: Optional[Dict[str, Any]] = None) -> ApiRequestBuild: return ApiRequestBuild( - action=action, - api_messages=api_messages, - _moa_prepared_request=_moa_prepared_request, - tools_for_api=tools_for_api, - api_kwargs=api_kwargs, - _original_api_kwargs=_original_api_kwargs, - _llm_middleware_trace=_llm_middleware_trace, + action=action, api_messages=api_messages, _moa_prepared_request=_moa_prepared_request, + tools_for_api=tools_for_api, api_kwargs=api_kwargs, + _original_api_kwargs=_original_api_kwargs, _llm_middleware_trace=_llm_middleware_trace, ) agent._reset_stream_delivery_tracking() @@ -87,10 +69,7 @@ def build_api_request( # breakpoints and re-render for the current provider. api_messages, _moa_prepared_request, tools_for_api = ( _redecorate_prompt_cache_for_provider( - agent, - api_messages, - system_message=system_message, - moa_prepared=_moa_prepared_request, + agent, api_messages, system_message=system_message, moa_prepared=_moa_prepared_request, tools_for_api=tools_for_api, ) ) @@ -98,8 +77,7 @@ def build_api_request( api_kwargs = agent._build_api_kwargs(api_messages) else: api_kwargs = agent._build_api_kwargs( - api_messages, - tools_for_api=tools_for_api, + api_messages, tools_for_api=tools_for_api ) # Surrogate chokepoint (#50959): tool descriptions, extra_body and # kwargs strings can carry invalid code points (HTTP 400). One walk @@ -109,9 +87,7 @@ def build_api_request( _sanitize_structure_non_ascii(api_kwargs) if agent.api_mode == "codex_responses": api_kwargs = agent._get_transport().preflight_kwargs( - api_kwargs, - allow_stream=False, - is_github_responses=agent._is_copilot_url(), + api_kwargs, allow_stream=False, is_github_responses=agent._is_copilot_url(), sanitize_harmony_tokens=agent._is_codex_backend(), ) # OpenRouter caching replays identical responses, even empty ones; an @@ -131,16 +107,9 @@ def build_api_request( from hermes_cli.middleware import apply_llm_request_middleware _llm_request_mw = apply_llm_request_middleware( - api_kwargs, - task_id=effective_task_id, - turn_id=turn_id, - api_request_id=api_request_id, - session_id=agent.session_id or "", - platform=agent.platform or "", - model=agent.model, - provider=agent.provider, - base_url=agent.base_url, - api_mode=agent.api_mode, + api_kwargs, task_id=effective_task_id, turn_id=turn_id, api_request_id=api_request_id, + session_id=agent.session_id or "", platform=agent.platform or "", model=agent.model, + provider=agent.provider, base_url=agent.base_url, api_mode=agent.api_mode, api_call_count=api_call_count, ) api_kwargs = _llm_request_mw.payload @@ -152,8 +121,7 @@ def build_api_request( try: from hermes_cli.lifecycle import ( - has_hook, - invoke_hook as _invoke_hook, + has_hook, invoke_hook as _invoke_hook ) if has_hook("pre_api_request"): request_messages = api_kwargs.get("messages") diff --git a/agent/turn_context.py b/agent/turn_context.py index edfcb4d317..6c0542daf3 100644 --- a/agent/turn_context.py +++ b/agent/turn_context.py @@ -23,18 +23,14 @@ from agent.memory_manager import build_memory_context_block from agent.memory_provider import is_trivial_prompt from agent.message_metadata import append_message, stamp_message_timestamp from agent.model_metadata import ( - anchored_context_tokens, - estimate_messages_tokens_rough, - estimate_request_tokens_rough, + anchored_context_tokens, estimate_messages_tokens_rough, estimate_request_tokens_rough ) logger = logging.getLogger(__name__) def _preflight_request_tokens( - agent: Any, - messages: List[Dict[str, Any]], - system_prompt: str, + agent: Any, messages: List[Dict[str, Any]], system_prompt: str ) -> int: """Token estimate for automatic preflight compression. @@ -52,10 +48,7 @@ def _preflight_request_tokens( ) native = estimate_native_responses_preflight_tokens( - agent, - messages, - system_prompt=system_prompt or "", - tools=tools, + agent, messages, system_prompt=system_prompt or "", tools=tools ) if isinstance(native, int) and not isinstance(native, bool) and native >= 0: return native @@ -66,9 +59,7 @@ def _preflight_request_tokens( exc_info=True, ) return estimate_request_tokens_rough( - messages, - system_prompt=system_prompt or "", - tools=tools, + messages, system_prompt=system_prompt or "", tools=tools, charge_stale_thinking=_agent_stale_thinking_on_wire(agent), ) @@ -81,19 +72,15 @@ def _agent_stale_thinking_on_wire(agent: Any) -> bool: from agent.message_sanitization import stale_thinking_reaches_wire return stale_thinking_reaches_wire( - getattr(agent, "api_mode", "") or "", - getattr(agent, "provider", "") or "", - getattr(agent, "model", "") or "", - getattr(agent, "base_url", "") or "", + getattr(agent, "api_mode", "") or "", getattr(agent, "provider", "") or "", + getattr(agent, "model", "") or "", getattr(agent, "base_url", "") or "", ) except Exception: return True def compose_user_api_content( - content: Any, - ext_prefetch_cache: str, - plugin_user_context: str, + content: Any, ext_prefetch_cache: str, plugin_user_context: str ) -> Optional[str]: """Compose the API-bound content of the current turn's user message. @@ -121,9 +108,7 @@ def substitute_api_content(api_msg: Dict[str, Any]) -> Optional[str]: Returns the popped sidecar string, or ``None`` when absent.""" sidecar = api_msg.pop("api_content", None) if ( - isinstance(sidecar, str) - and sidecar - and api_msg.get("role") in ("user", "assistant") + isinstance(sidecar, str) and sidecar and api_msg.get("role") in ("user", "assistant") ): api_msg["content"] = sidecar return sidecar @@ -317,17 +302,12 @@ def _compression_warrants_another_preflight_pass( Continue only if still over threshold AND the previous pass cut tokens by >5%.""" return ( - new_tokens >= threshold_tokens - and orig_tokens > 0 - and new_tokens < orig_tokens * 0.95 + new_tokens >= threshold_tokens and orig_tokens > 0 and new_tokens < orig_tokens * 0.95 ) def _should_run_preflight_estimate( - messages: List[Dict[str, Any]], - protect_first_n: int, - protect_last_n: int, - threshold_tokens: int, + messages: List[Dict[str, Any]], protect_first_n: int, protect_last_n: int, threshold_tokens: int ) -> bool: """Cheap gate for the (expensive) full preflight token estimate. @@ -341,13 +321,8 @@ def _should_run_preflight_estimate( def _should_idle_compact( - *, - enabled: bool, - idle_after_seconds: int, - idle_gap_seconds: float, - tokens: int, - floor_tokens: int, - cooldown_active: bool, + *, enabled: bool, idle_after_seconds: int, idle_gap_seconds: float, tokens: int, + floor_tokens: int, cooldown_active: bool, ) -> bool: """Decide whether an idle-triggered compaction should run this turn. @@ -420,15 +395,13 @@ def _publish_runtime_main(agent: Any) -> None: # session uses the physical id until build_api_kwargs re-resolves (#79017). _cache_scope = resolve_prompt_cache_scope_safe(agent) or "" set_runtime_main( - getattr(agent, "provider", "") or "", - getattr(agent, "model", "") or "", + getattr(agent, "provider", "") or "", getattr(agent, "model", "") or "", requested_provider=getattr(agent, "requested_provider", "") or "", base_url=getattr(agent, "base_url", "") or "", api_key=getattr(agent, "api_key", "") or "", api_mode=getattr(agent, "api_mode", "") or "", auth_mode=getattr(agent, "auth_mode", "") or "", - session_id=getattr(agent, "session_id", "") or "", - cache_scope=_cache_scope, + session_id=getattr(agent, "session_id", "") or "", cache_scope=_cache_scope, ) except Exception: pass @@ -454,12 +427,8 @@ def _refresh_mcp_tools_between_turns(agent: Any) -> None: def _bind_turn_identity( - agent: Any, - task_id: Optional[str], - stream_callback, - persist_user_message: Any, - persist_user_timestamp: Optional[float], - persist_user_platform_id: Optional[str], + agent: Any, task_id: Optional[str], stream_callback, persist_user_message: Any, + persist_user_timestamp: Optional[float], persist_user_platform_id: Optional[str], ) -> Tuple[str, str]: """Stage callback/persist overrides on the agent and bind this turn's task and turn ids. Returns ``(effective_task_id, turn_id)``.""" @@ -548,11 +517,8 @@ def _reset_per_turn_agent_state(agent: Any) -> None: def _stage_turn_user_message( - agent: Any, - user_message: Any, - persist_user_message: Any, - persist_user_timestamp: Optional[float], - persist_user_platform_id: Optional[str], + agent: Any, user_message: Any, persist_user_message: Any, + persist_user_timestamp: Optional[float], persist_user_platform_id: Optional[str], persist_user_display_kind: Optional[str], persist_user_display_metadata: Optional[Dict[str, Any]], ) -> Tuple[Dict[str, Any], Any]: @@ -573,8 +539,7 @@ def _stage_turn_user_message( user_msg["content"] = user_message else: user_msg = stamp_message_timestamp( - {"role": "user", "content": user_message}, - timestamp=persist_user_timestamp, + {"role": "user", "content": user_message}, timestamp=persist_user_timestamp ) if isinstance(pending_cli_message, dict): agent._pending_cli_user_message = None @@ -659,13 +624,8 @@ def _ensure_session_row(agent: Any, pending_cli_message: Any) -> None: def _collect_pre_llm_call_context( - agent: Any, - *, - effective_task_id: str, - turn_id: str, - original_user_message: Any, - messages: List[Any], - conversation_history: Optional[List[Any]], + agent: Any, *, effective_task_id: str, turn_id: str, original_user_message: Any, + messages: List[Any], conversation_history: Optional[List[Any]], ) -> str: """Run ``pre_llm_call`` plugins; their context is injected into the user message (never the system prompt). Oversized per-hook context is spilled to disk so a @@ -688,8 +648,7 @@ def _collect_pre_llm_call_context( _ctx_parts: list[str] = [] try: from tools.hook_output_spill import ( - get_spill_config as _spill_cfg, - spill_if_oversized as _spill_if_oversized, + get_spill_config as _spill_cfg, spill_if_oversized as _spill_if_oversized ) _spill_config_cached = _spill_cfg() except Exception: @@ -705,9 +664,7 @@ def _collect_pre_llm_call_context( if _spill_if_oversized is not None: try: _piece = _spill_if_oversized( - _piece, - session_id=agent.session_id, - source="plugin hook", + _piece, session_id=agent.session_id, source="plugin hook", config=_spill_config_cached, ) except Exception as _spill_exc: @@ -739,9 +696,7 @@ def _merge_gateway_notes( append_notes_to_multimodal_content(_gw_turn_content, _gateway_notes) return plugin_user_context return ( - plugin_user_context + "\n\n" + _gateway_notes - if plugin_user_context - else _gateway_notes + plugin_user_context + "\n\n" + _gateway_notes if plugin_user_context else _gateway_notes ) @@ -752,9 +707,7 @@ def _bind_interrupt_scope(agent: Any, ra) -> None: ra()._set_interrupt(False, agent._execution_thread_id) if agent._interrupt_requested: ra()._set_interrupt( - True, - agent._execution_thread_id, - reason=getattr(agent, "_tool_interrupt_reason", None), + True, agent._execution_thread_id, reason=getattr(agent, "_tool_interrupt_reason", None) ) else: agent._interrupt_message = None @@ -792,13 +745,8 @@ def _memory_turn_start_and_prefetch(agent: Any, original_user_message: Any) -> s def _stamp_api_content_sidecar( - agent: Any, - messages: List[Any], - current_turn_user_idx: int, - ext_prefetch_cache: str, - plugin_user_context: str, - *, - preflight_compressed: bool, + agent: Any, messages: List[Any], current_turn_user_idx: int, ext_prefetch_cache: str, + plugin_user_context: str, *, preflight_compressed: bool, ) -> None: """api_content sidecar — persist what you send. Injected context lives only in the API copy; stamp the exact sent bytes on the live dict so replay reproduces the @@ -818,9 +766,7 @@ def _stamp_api_content_sidecar( if _db is not None: try: _db.set_latest_user_api_content( - agent.session_id, - _turn_user_msg.get("content"), - _api_content, + agent.session_id, _turn_user_msg.get("content"), _api_content ) except Exception: logger.warning( @@ -832,9 +778,7 @@ def _stamp_api_content_sidecar( def _persist_turn_start( - agent: Any, - messages: List[Any], - conversation_history: Optional[List[Any]], + agent: Any, messages: List[Any], conversation_history: Optional[List[Any]], pending_cli_message: Any, ) -> None: """Crash-resilience: persist the inbound user turn once, with final api_content, @@ -857,26 +801,13 @@ def _persist_turn_start( def build_turn_context( - agent, - user_message: Any, - system_message: Optional[str], - conversation_history: Optional[List[Dict[str, Any]]], - task_id: Optional[str], - stream_callback, - persist_user_message: Optional[Any], - persist_user_timestamp: Optional[float] = None, - persist_user_platform_id: Optional[str] = None, - *, - persist_user_display_kind: Optional[str] = None, - persist_user_display_metadata: Optional[Dict[str, Any]] = None, - restore_or_build_system_prompt, - install_safe_stdio, - sanitize_surrogates, - summarize_user_message_for_log, - set_session_context, - set_current_write_origin, - ra, - moa_active: bool = False, + agent, user_message: Any, system_message: Optional[str], + conversation_history: Optional[List[Dict[str, Any]]], task_id: Optional[str], stream_callback, + persist_user_message: Optional[Any], persist_user_timestamp: Optional[float]=None, + persist_user_platform_id: Optional[str]=None, *, persist_user_display_kind: Optional[str]=None, + persist_user_display_metadata: Optional[Dict[str, Any]]=None, restore_or_build_system_prompt, + install_safe_stdio, sanitize_surrogates, summarize_user_message_for_log, set_session_context, + set_current_write_origin, ra, moa_active: bool=False, ) -> TurnContext: """Run the once-per-turn setup and return the loop's input context. @@ -973,13 +904,9 @@ def build_turn_context( # ── Idle compaction + preflight compression (or the uncompressed guard) ── compaction = run_turn_start_compaction( - agent, - messages=messages, - system_message=system_message, - active_system_prompt=active_system_prompt, - conversation_history=conversation_history, - current_turn_user_idx=current_turn_user_idx, - user_message=user_message, + agent, messages=messages, system_message=system_message, + active_system_prompt=active_system_prompt, conversation_history=conversation_history, + current_turn_user_idx=current_turn_user_idx, user_message=user_message, effective_task_id=effective_task_id, ) messages = compaction.messages @@ -988,11 +915,8 @@ def build_turn_context( current_turn_user_idx = compaction.current_turn_user_idx plugin_user_context = _collect_pre_llm_call_context( - agent, - effective_task_id=effective_task_id, - turn_id=turn_id, - original_user_message=original_user_message, - messages=messages, + agent, effective_task_id=effective_task_id, turn_id=turn_id, + original_user_message=original_user_message, messages=messages, conversation_history=conversation_history, ) plugin_user_context = _merge_gateway_notes( @@ -1021,31 +945,19 @@ def build_turn_context( _maybe_title_session_at_turn_start(agent, messages) return TurnContext( - user_message=user_message, - original_user_message=original_user_message, - messages=messages, - conversation_history=conversation_history, - active_system_prompt=active_system_prompt, - effective_task_id=effective_task_id, - turn_id=turn_id, - current_turn_user_idx=current_turn_user_idx, - should_review_memory=should_review_memory, - plugin_user_context=plugin_user_context, - ext_prefetch_cache=ext_prefetch_cache, + user_message=user_message, original_user_message=original_user_message, messages=messages, + conversation_history=conversation_history, active_system_prompt=active_system_prompt, + effective_task_id=effective_task_id, turn_id=turn_id, + current_turn_user_idx=current_turn_user_idx, should_review_memory=should_review_memory, + plugin_user_context=plugin_user_context, ext_prefetch_cache=ext_prefetch_cache, preflight_compression_blocked=compaction.blocked, ) def build_api_messages( - agent: Any, - messages: List[Dict[str, Any]], - *, - current_turn_user_idx: Any, - ext_prefetch_cache: Any, - plugin_user_context: Any, - moa_config: Any, - active_system_prompt: Any, + agent: Any, messages: List[Dict[str, Any]], *, current_turn_user_idx: Any, + ext_prefetch_cache: Any, plugin_user_context: Any, moa_config: Any, active_system_prompt: Any, ) -> Tuple[List[Dict[str, Any]], str]: """Build the wire copy of ``messages`` for one API call plus the effective system message. Returns ``(api_messages, effective_system)``. diff --git a/agent/turn_context_compaction.py b/agent/turn_context_compaction.py index 6776824268..ccd3ce050f 100644 --- a/agent/turn_context_compaction.py +++ b/agent/turn_context_compaction.py @@ -17,10 +17,8 @@ from typing import Any, Dict, List, Optional from agent.context_engine import automatic_compaction_status_message from agent.conversation_compression import ( - IDLE_COMPACTION_STATUS_TEMPLATE, - PREFLIGHT_COMPRESSION_STATUS_TEMPLATE, - compression_skipped_due_to_lock, - conversation_history_after_compression, + IDLE_COMPACTION_STATUS_TEMPLATE, PREFLIGHT_COMPRESSION_STATUS_TEMPLATE, + compression_skipped_due_to_lock, conversation_history_after_compression, ) logger = logging.getLogger("agent.turn_context") @@ -67,22 +65,14 @@ def _reanchor(agent: Any, messages: List[Any], user_message: Any) -> int: def run_turn_start_compaction( - agent: Any, - *, - messages: List[Dict[str, Any]], - system_message: Optional[str], - active_system_prompt: Optional[str], - conversation_history: Optional[List[Dict[str, Any]]], - current_turn_user_idx: int, - user_message: Any, - effective_task_id: str, + agent: Any, *, messages: List[Dict[str, Any]], system_message: Optional[str], + active_system_prompt: Optional[str], conversation_history: Optional[List[Dict[str, Any]]], + current_turn_user_idx: int, user_message: Any, effective_task_id: str, ) -> CompactionOutcome: """Idle compaction, then preflight compression (or the uncompressed guard).""" out = CompactionOutcome( - messages=messages, - active_system_prompt=active_system_prompt, - conversation_history=conversation_history, - current_turn_user_idx=current_turn_user_idx, + messages=messages, active_system_prompt=active_system_prompt, + conversation_history=conversation_history, current_turn_user_idx=current_turn_user_idx, ) _idle_compaction(agent, out, system_message, user_message, effective_task_id) _preflight_compression(agent, out, system_message, user_message, effective_task_id) @@ -90,10 +80,7 @@ def run_turn_start_compaction( def _idle_compaction( - agent: Any, - out: CompactionOutcome, - system_message: Optional[str], - user_message: Any, + agent: Any, out: CompactionOutcome, system_message: Optional[str], user_message: Any, effective_task_id: str, ) -> None: """Idle-triggered compaction (opt-in; ``idle_compact_after_seconds``). @@ -122,11 +109,8 @@ def _idle_compaction( _compressor, "get_active_compression_failure_cooldown", lambda: None )() if not _tc._should_idle_compact( - enabled=agent.compression_enabled, - idle_after_seconds=_idle_after, - idle_gap_seconds=_idle_gap, - tokens=_idle_tokens, - floor_tokens=_idle_floor, + enabled=agent.compression_enabled, idle_after_seconds=_idle_after, + idle_gap_seconds=_idle_gap, tokens=_idle_tokens, floor_tokens=_idle_floor, cooldown_active=bool(_idle_cooldown), ): return @@ -148,8 +132,7 @@ def _idle_compaction( if _idle_status: agent._emit_status(_idle_status) out.messages, out.active_system_prompt = agent._compress_context( - messages, system_message, approx_tokens=_idle_tokens, - task_id=effective_task_id, + messages, system_message, approx_tokens=_idle_tokens, task_id=effective_task_id ) # ``_compress_context`` returns the INPUT list object when it skips; only # re-baseline and re-anchor after a real compaction. @@ -173,10 +156,7 @@ def _codex_native_auto_compaction(agent: Any) -> bool: def _preflight_compression( - agent: Any, - out: CompactionOutcome, - system_message: Optional[str], - user_message: Any, + agent: Any, out: CompactionOutcome, system_message: Optional[str], user_message: Any, effective_task_id: str, ) -> None: """Preflight context compression; the cheap pre-check gates the full estimate @@ -189,10 +169,8 @@ def _preflight_compression( _rearm_uncompressed_overflow_warn(agent, out.messages, out.active_system_prompt) return if _tc._review_fork_first_request_pending(agent) or not _tc._should_run_preflight_estimate( - out.messages, - agent.context_compressor.protect_first_n, - agent.context_compressor.protect_last_n, - agent.context_compressor.threshold_tokens, + out.messages, agent.context_compressor.protect_first_n, + agent.context_compressor.protect_last_n, agent.context_compressor.threshold_tokens, ): return @@ -275,8 +253,7 @@ def _preflight_compression( _grown = None if _grown: _compressor.update_model( - agent.model, _grown, - base_url=getattr(agent, "base_url", "") or "", + agent.model, _grown, base_url=getattr(agent, "base_url", "") or "", api_key=getattr(agent, "api_key", "") or "", provider=getattr(agent, "provider", "") or "", api_mode=getattr(agent, "api_mode", "") or "", @@ -313,12 +290,8 @@ def _preflight_compression( def _run_preflight_passes( - agent: Any, - out: CompactionOutcome, - _compressor: Any, - _preflight_tokens: int, - system_message: Optional[str], - effective_task_id: str, + agent: Any, out: CompactionOutcome, _compressor: Any, _preflight_tokens: int, + system_message: Optional[str], effective_task_id: str, ) -> None: """Threshold-triggered preflight passes (honor ``compression.max_attempts`` like the loop's sites, default 3).""" @@ -394,12 +367,8 @@ def _run_preflight_passes( def _engine_preflight_maintenance( - agent: Any, - out: CompactionOutcome, - _compressor: Any, - _preflight_tokens: int, - system_message: Optional[str], - effective_task_id: str, + agent: Any, out: CompactionOutcome, _compressor: Any, _preflight_tokens: int, + system_message: Optional[str], effective_task_id: str, ) -> None: """Engine-driven sub-threshold preflight maintenance (#20316): engines overriding ``should_compress_preflight()`` get exactly ONE ``compress()`` pass; a no-op never @@ -428,8 +397,7 @@ def _engine_preflight_maintenance( ) _engine_input = out.messages out.messages, out.active_system_prompt = agent._compress_context( - _engine_input, system_message, approx_tokens=_preflight_tokens, - task_id=effective_task_id, + _engine_input, system_message, approx_tokens=_preflight_tokens, task_id=effective_task_id ) # ``_compress_context`` returns the INPUT list on every skip path and an engine # may no-op; re-baseline/re-anchor only after a REAL compaction. diff --git a/agent/turn_empty_response.py b/agent/turn_empty_response.py index a83a1b557f..75a8ab063f 100644 --- a/agent/turn_empty_response.py +++ b/agent/turn_empty_response.py @@ -41,18 +41,9 @@ class EmptyResponseVerdict: def recover_empty_response( - agent: Any, - assistant_message: Any, - response: Any, - finish_reason: str, - *, - final_response: Any, - messages: List[Dict[str, Any]], - api_messages: Any, - conversation_history: Any, - active_system_prompt: Any, - api_call_count: int, - turn_exit_reason: Any, + agent: Any, assistant_message: Any, response: Any, finish_reason: str, *, final_response: Any, + messages: List[Dict[str, Any]], api_messages: Any, conversation_history: Any, + active_system_prompt: Any, api_call_count: int, turn_exit_reason: Any, preflight_compression_blocked: bool, ) -> EmptyResponseVerdict: """Recover from a final response with no visible content (see module docstring for @@ -61,9 +52,7 @@ def recover_empty_response( surfaced only at the terminal step, for delivery — the persisted row keeps the ``(empty)`` sentinel.""" from agent.conversation_loop import ( - _EMPTY_TOOL_RESPONSE_NUDGE, - _sync_failover_system_message, - jittered_backoff, + _EMPTY_TOOL_RESPONSE_NUDGE, _sync_failover_system_message, jittered_backoff ) _turn_exit_reason = turn_exit_reason @@ -71,11 +60,8 @@ def recover_empty_response( def _verdict(action: str, result: Optional[Dict[str, Any]] = None) -> EmptyResponseVerdict: return EmptyResponseVerdict( - action=action, - result=result, - final_response=final_response, - turn_exit_reason=_turn_exit_reason, - active_system_prompt=active_system_prompt, + action=action, result=result, final_response=final_response, + turn_exit_reason=_turn_exit_reason, active_system_prompt=active_system_prompt, preflight_compression_blocked=_preflight_compression_blocked, ) @@ -93,8 +79,7 @@ def recover_empty_response( len(_recovered), ) agent._emit_status( - "↻ Stream interrupted — using delivered content " - "as final response" + "↻ Stream interrupted — using delivered content " "as final response" ) final_response = _recovered # A streamed fragment isn't a confirmed preview: keep @@ -130,11 +115,7 @@ def recover_empty_response( # Ollama puts in content, not reasoning_content, so # _has_structured misses it; detect here to route to prefill. _has_inline_thinking = bool( - re.search( - r'||', - final_response or "", - re.IGNORECASE, - ) + re.search( r'||', final_response or "", re.IGNORECASE ) ) if ( _prior_was_tool @@ -147,12 +128,10 @@ def recover_empty_response( agent._last_content_with_tools = None agent._last_content_tools_all_housekeeping = False logger.info( - "Empty response after tool calls — nudging model " - "to continue processing" + "Empty response after tool calls — nudging model " "to continue processing" ) agent._buffer_status( - "⚠️ Model returned empty after tool calls — " - "nudging to continue" + "⚠️ Model returned empty after tool calls — " "nudging to continue" ) # Append the empty assistant first so the sequence stays valid: # tool → assistant("(empty)") → user (APIs reject tool→user). @@ -161,9 +140,7 @@ def recover_empty_response( _nudge_msg["_empty_recovery_synthetic"] = True append_message(messages, _nudge_msg) append_message(messages, { - "role": "user", - "content": _EMPTY_TOOL_RESPONSE_NUDGE, - "_empty_recovery_synthetic": True, + "role": "user", "content": _EMPTY_TOOL_RESPONSE_NUDGE, "_empty_recovery_synthetic": True }) return _verdict("continue") @@ -202,8 +179,7 @@ def recover_empty_response( final_response ).strip() _prefill_exhausted = ( - _has_structured - and agent._thinking_prefill_retries >= 2 + _has_structured and agent._thinking_prefill_retries >= 2 ) _empty_candidate = _truly_empty and ( not _has_structured or _prefill_exhausted @@ -213,9 +189,7 @@ def recover_empty_response( # signature so deterministic empties stop burning paid retries. # Fails open: missing usage or any output keeps the budget. _empty_guard.record_empty_attempt( - agent, - finish_reason=finish_reason, - response=response, + agent, finish_reason=finish_reason, response=response ) _empty_retry_budget = ( _empty_guard.empty_retry_budget(agent, response) @@ -232,9 +206,7 @@ def recover_empty_response( ): agent._empty_content_retries += 1 wait_time = jittered_backoff( - agent._empty_content_retries, - base_delay=5.0, - max_delay=60.0, + agent._empty_content_retries, base_delay=5.0, max_delay=60.0 ) logger.warning( "Empty response (no content or reasoning) — " @@ -292,16 +264,14 @@ def recover_empty_response( agent.provider, ) agent._buffer_status( - "⚠️ Model returning empty responses — " - "switching to fallback provider..." + "⚠️ Model returning empty responses — " "switching to fallback provider..." ) if agent._try_activate_fallback(): active_system_prompt = _sync_failover_system_message( agent, api_messages, active_system_prompt) agent._empty_content_retries = 0 agent._buffer_status( - f"↻ Switched to fallback: {agent.model} " - f"({agent.provider})" + f"↻ Switched to fallback: {agent.model} " f"({agent.provider})" ) logger.info( "Fallback activated after empty responses: " diff --git a/agent/turn_explainers.py b/agent/turn_explainers.py index 192d52b95f..2a91e972a4 100644 --- a/agent/turn_explainers.py +++ b/agent/turn_explainers.py @@ -8,13 +8,10 @@ import re from typing import Any, Dict, Optional from agent.tool_dispatch_helpers import ( - _extract_error_preview, - _extract_file_mutation_targets, - _extract_landed_file_mutation_paths, + _extract_error_preview, _extract_file_mutation_targets, _extract_landed_file_mutation_paths ) from agent.tool_result_classification import ( - FILE_MUTATING_TOOL_NAMES as _FILE_MUTATING_TOOLS, - file_mutation_result_landed, + FILE_MUTATING_TOOL_NAMES as _FILE_MUTATING_TOOLS, file_mutation_result_landed ) @@ -22,11 +19,7 @@ class TurnExplainersMixin: """File-mutation failure footer + turn-completion explainer (see module docstring).""" def _record_file_mutation_result( - self, - tool_name: str, - args: Dict[str, Any], - result: Any, - is_error: bool, + self, tool_name: str, args: Dict[str, Any], result: Any, is_error: bool ) -> None: """Record a ``write_file`` / ``patch`` outcome for the turn-end verifier. @@ -62,8 +55,7 @@ class TurnExplainersMixin: # Keep the FIRST error per path unless a later success replaces it. if path not in state: state[path] = { - "tool": tool_name, - "error_preview": preview, + "tool": tool_name, "error_preview": preview } else: for path in targets: diff --git a/agent/turn_facade.py b/agent/turn_facade.py index b4250fa59d..0cde391def 100644 --- a/agent/turn_facade.py +++ b/agent/turn_facade.py @@ -20,18 +20,12 @@ class TurnFacadeMixin: """run_conversation()/chat() (see module docstring).""" def run_conversation( - self, - user_message: Any, - system_message: str = None, - conversation_history: List[Dict[str, Any]] = None, - task_id: str = None, - stream_callback: Optional[callable] = None, - persist_user_message: Optional[Any] = None, - persist_user_timestamp: Optional[float] = None, - persist_user_display_kind: Optional[str] = None, - persist_user_display_metadata: Optional[Dict[str, Any]] = None, - persist_user_platform_id: Optional[str] = None, - moa_config: Optional[dict[str, Any]] = None, + self, user_message: Any, system_message: str=None, + conversation_history: List[Dict[str, Any]]=None, task_id: str=None, + stream_callback: Optional[callable]=None, persist_user_message: Optional[Any]=None, + persist_user_timestamp: Optional[float]=None, persist_user_display_kind: Optional[str]=None, + persist_user_display_metadata: Optional[Dict[str, Any]]=None, + persist_user_platform_id: Optional[str]=None, moa_config: Optional[dict[str, Any]]=None, ) -> Dict[str, Any]: """Forwarder — see ``agent.conversation_loop.run_conversation``.""" # A review shares this session_id for cache parity: fence review startup or interrupt an admitted @@ -45,9 +39,7 @@ class TurnFacadeMixin: from agent.auxiliary_client import scoped_runtime_main from agent.conversation_loop import run_conversation from agent.portal_tags import ( - reset_affinity_scope, - reset_conversation_context, - set_affinity_scope, + reset_affinity_scope, reset_conversation_context, set_affinity_scope, set_conversation_context, ) from agent.prompt_cache_scope import declared_conversation_scope_safe @@ -82,10 +74,7 @@ class TurnFacadeMixin: # note_turn_finished balances every exit. _review_queue.note_turn_started() admission = admit_durable_turn_lease( - self, - session_id=session_id, - relay_turn_id=relay_turn_id, - task_context=task_context, + self, session_id=session_id, relay_turn_id=relay_turn_id, task_context=task_context, conversation_history=conversation_history, ) if admission.early_result is not None: @@ -98,8 +87,7 @@ class TurnFacadeMixin: relay_lease = relay_runtime.SESSION_COORDINATOR.acquire_conversation( profile_key=relay_runtime.current_profile_key(), - session_id=task_context["session_id"], - platform=task_context["platform"], + session_id=task_context["session_id"], platform=task_context["platform"], parent_session_id=relay_parent_session_id, model=str(getattr(self, "model", None) or ""), ) @@ -129,18 +117,12 @@ class TurnFacadeMixin: if lease is not None: lease.start() result = run_conversation( - self, - user_message, - system_message, - conversation_history, - effective_task_id, - stream_callback, - persist_user_message, + self, user_message, system_message, conversation_history, effective_task_id, + stream_callback, persist_user_message, persist_user_timestamp=persist_user_timestamp, persist_user_display_kind=persist_user_display_kind, persist_user_display_metadata=persist_user_display_metadata, - persist_user_platform_id=persist_user_platform_id, - moa_config=moa_config, + persist_user_platform_id=persist_user_platform_id, moa_config=moa_config, ) finally: # Post-loop relay/task finalization must not receive a late refresh interrupt. The diff --git a/agent/turn_facade_lease.py b/agent/turn_facade_lease.py index 05f15314cd..fb5471c30c 100644 --- a/agent/turn_facade_lease.py +++ b/agent/turn_facade_lease.py @@ -63,14 +63,10 @@ class DurableTurnLease: timeout_s, poll_s = turn_liveness.resolve_turn_liveness_settings(liveness_config) if timeout_s is not None: self.liveness_thread = turn_liveness.TurnLivenessWatchdog( - self.agent, - session_id=self._current_session_id(), - timeout_s=timeout_s, - poll_s=poll_s, - stop_event=self.stop, + self.agent, session_id=self._current_session_id(), timeout_s=timeout_s, + poll_s=poll_s, stop_event=self.stop, activity_lock=self.agent._liveness_activity_lock(), - is_turn_active=self.is_turn_active, - commit_abort=self.commit_liveness_abort, + is_turn_active=self.is_turn_active, commit_abort=self.commit_liveness_abort, deactivate_turn=self.deactivate_after_liveness_abort, ).make_thread() @@ -136,8 +132,7 @@ class DurableTurnLease: with agent._liveness_activity_lock(): current_generation = getattr(agent, "_turn_liveness_activity_generation", 0) if (current_generation, getattr(agent, "_last_activity_ts", None)) != ( - snapshot.generation, - snapshot.activity_ts, + snapshot.generation, snapshot.activity_ts ): return False with self._lock: @@ -256,11 +251,7 @@ def _durable_session_exists(db, session_id: str) -> bool: def admit_durable_turn_lease( - agent, - *, - session_id: str, - relay_turn_id: str, - task_context: Dict[str, Any], + agent, *, session_id: str, relay_turn_id: str, task_context: Dict[str, Any], conversation_history: Optional[List[Dict[str, Any]]], ) -> TurnLeaseAdmission: """Acquire the session turn lease when the session is durable; build (not start) its threads. @@ -305,12 +296,8 @@ def admit_durable_turn_lease( ) if not db.acquire_session_turn_lease( - session_id, - holder, - ttl_seconds=LEASE_TTL_SECONDS, - wait_seconds=LEASE_WAIT_SECONDS, - on_wait=_on_wait, - should_abort=lambda: getattr(agent, "_interrupt_requested", False), + session_id, holder, ttl_seconds=LEASE_TTL_SECONDS, wait_seconds=LEASE_WAIT_SECONDS, + on_wait=_on_wait, should_abort=lambda: getattr(agent, "_interrupt_requested", False), ): admission.early_result = _lease_not_acquired_result(agent, session_id, conversation_history) return admission diff --git a/agent/turn_final_response.py b/agent/turn_final_response.py index bf1baeb13d..64620e34bd 100644 --- a/agent/turn_final_response.py +++ b/agent/turn_final_response.py @@ -38,41 +38,24 @@ class FinalResponseVerdict: def finish_text_response( - agent: Any, - *, - assistant_message: Any, - response: Any, - finish_reason: Any, - messages: Any, - api_messages: Any, - conversation_history: Any, - api_call_count: Any, - user_message: Any, - active_system_prompt: Any, - final_response: Any, - _turn_exit_reason: Any, - _preflight_compression_blocked: Any, - codex_ack_continuations: Any, - truncated_response_parts: Any, - length_continue_retries: Any, - _pending_verification_response: Any, - _pending_verification_response_previewed: Any, + agent: Any, *, assistant_message: Any, response: Any, finish_reason: Any, messages: Any, + api_messages: Any, conversation_history: Any, api_call_count: Any, user_message: Any, + active_system_prompt: Any, final_response: Any, _turn_exit_reason: Any, + _preflight_compression_blocked: Any, codex_ack_continuations: Any, + truncated_response_parts: Any, length_continue_retries: Any, + _pending_verification_response: Any, _pending_verification_response_previewed: Any, ) -> FinalResponseVerdict: """Finish (or defer) a text-only assistant response in the original guard order. Every continuation path sets ``final_response = None`` so an acknowledgment never suppresses iteration-limit summarization; the final message is appended and flushed only after the stop gates accept it.""" from agent.conversation_loop import ( - _CODEX_ACK_CONTINUATION_NUDGE, - _DROPPED_TOOLCALL_NUDGE_CONTENT, - _join_truncated_parts, + _CODEX_ACK_CONTINUATION_NUDGE, _DROPPED_TOOLCALL_NUDGE_CONTENT, _join_truncated_parts ) def _verdict(action: str, result: Optional[Dict[str, Any]] = None) -> FinalResponseVerdict: return FinalResponseVerdict( - action=action, - active_system_prompt=active_system_prompt, - final_response=final_response, + action=action, active_system_prompt=active_system_prompt, final_response=final_response, _turn_exit_reason=_turn_exit_reason, _preflight_compression_blocked=_preflight_compression_blocked, codex_ack_continuations=codex_ack_continuations, @@ -94,16 +77,9 @@ def finish_text_response( # Check if response only has think block with no actual content after it if not agent._has_content_after_think_block(final_response): _ev = recover_empty_response( - agent, - assistant_message, - response, - finish_reason, - final_response=final_response, - messages=messages, - api_messages=api_messages, - conversation_history=conversation_history, - active_system_prompt=active_system_prompt, - api_call_count=api_call_count, + agent, assistant_message, response, finish_reason, final_response=final_response, + messages=messages, api_messages=api_messages, conversation_history=conversation_history, + active_system_prompt=active_system_prompt, api_call_count=api_call_count, turn_exit_reason=_turn_exit_reason, preflight_compression_blocked=_preflight_compression_blocked, ) @@ -126,8 +102,7 @@ def finish_text_response( agent._clear_status_buffer() from agent.agent_runtime_helpers import ( - intent_ack_continuation_mode, - trailing_continue_intent, + intent_ack_continuation_mode, trailing_continue_intent ) _ack_mode = intent_ack_continuation_mode(agent) @@ -147,9 +122,7 @@ def finish_text_response( and agent.valid_tool_names and codex_ack_continuations < 2 and agent._looks_like_codex_intermediate_ack( - user_message=user_message, - assistant_content=final_response, - messages=messages, + user_message=user_message, assistant_content=final_response, messages=messages, require_workspace=(_ack_mode == "codex_only"), ) ): @@ -165,8 +138,7 @@ def finish_text_response( agent._emit_interim_assistant_message(interim_msg) continue_msg = { - "role": "user", - "content": _CODEX_ACK_CONTINUATION_NUDGE, + "role": "user", "content": _CODEX_ACK_CONTINUATION_NUDGE } append_message(messages, continue_msg) agent._session_messages = messages @@ -242,10 +214,7 @@ def finish_text_response( messages.pop() _sg = apply_stop_gates( - agent, - final_msg, - final_response=final_response, - messages=messages, + agent, final_msg, final_response=final_response, messages=messages, conversation_history=conversation_history, pending_verification_response=_pending_verification_response, pending_verification_response_previewed=_pending_verification_response_previewed, diff --git a/agent/turn_finalizer.py b/agent/turn_finalizer.py index 741fe09e5f..dea3dcefb8 100644 --- a/agent/turn_finalizer.py +++ b/agent/turn_finalizer.py @@ -20,8 +20,7 @@ from agent.message_sanitization import _sanitize_surrogates # returned/live history to avoid role-alternation breaks; the assistant response is # real content and is not flagged. (#65919) _VERIFICATION_CONTINUATION_FLAGS = ( - "_verification_stop_synthetic", - "_pre_verify_synthetic", + "_verification_stop_synthetic", "_pre_verify_synthetic" ) _SENTENCE_END = {".", "!", "?", "。", "!", "?", "`", ")"} @@ -50,10 +49,7 @@ def _fill_assistant_tail_content(agent, tail: dict, final_response) -> None: def _record_kanban_budget_exhausted( - kanban_task: str, - api_call_count: int, - max_iterations: int, - logger: logging.Logger, + kanban_task: str, api_call_count: int, max_iterations: int, logger: logging.Logger ) -> None: """Record a terminal ``timed_out`` outcome for a kanban worker out of budget. @@ -78,8 +74,7 @@ def _record_kanban_budget_exhausted( release_claim=True, end_run=True, event_payload_extra={ - "budget_used": api_call_count, - "budget_max": max_iterations, + "budget_used": api_call_count, "budget_max": max_iterations }, ) finally: @@ -89,9 +84,7 @@ def _record_kanban_budget_exhausted( pass except Exception: logger.warning( - "Failed to record budget-exhausted failure for task %s", - kanban_task, - exc_info=True, + "Failed to record budget-exhausted failure for task %s", kanban_task, exc_info=True ) @@ -136,23 +129,13 @@ def _guarded_cleanup(label: str, fn: Callable[[], Any], errors: List[str], logge def _resolve_budget_fallback( - agent, - *, - final_response, - api_call_count, - interrupted, - failed, - messages, - _turn_exit_reason, - _pending_verification_response, - _pending_verification_response_previewed, - logger, + agent, *, final_response, api_call_count, interrupted, failed, messages, _turn_exit_reason, + _pending_verification_response, _pending_verification_response_previewed, logger, ) -> Tuple[Any, Any, bool]: """Iteration-budget exhaustion. Returns ``(final_response, _turn_exit_reason, preserved_verification_fallback)``.""" budget_exhausted = ( - api_call_count >= agent.max_iterations - or agent.iteration_budget.remaining <= 0 + api_call_count >= agent.max_iterations or agent.iteration_budget.remaining <= 0 ) budget_fallback_eligible = ( budget_exhausted @@ -209,9 +192,7 @@ def _rollback_interrupted_preflight_display(agent, interrupted) -> None: and getattr(agent, "context_compressor", None) is not None ): _rollback_fn = getattr( - agent.context_compressor, - "rollback_interrupted_preflight_display_tokens", - None, + agent.context_compressor, "rollback_interrupted_preflight_display_tokens", None ) if callable(_rollback_fn): _rollback_fn(_preflight_snapshot) @@ -392,8 +373,7 @@ def _explain_abnormal_exit(agent, final_response, _turn_exit_reason, preserved_v or str(_turn_exit_reason) == "partial_stream_recovery" ): _explanation = agent._format_turn_completion_explanation( - _turn_exit_reason, - getattr(agent, "_last_persistence_error_cause", None), + _turn_exit_reason, getattr(agent, "_last_persistence_error_cause", None) ) if _explanation: # Replace the bare sentinel; keep a partial fragment and append why. @@ -416,33 +396,17 @@ def _last_turn_reasoning(messages) -> Optional[Any]: def finalize_turn( - agent, - *, - final_response, - api_call_count, - interrupted, - failed, - messages, - conversation_history, - effective_task_id, - turn_id, - user_message, - original_user_message, - _should_review_memory, - _turn_exit_reason, - _pending_verification_response=None, + agent, *, final_response, api_call_count, interrupted, failed, messages, conversation_history, + effective_task_id, turn_id, user_message, original_user_message, _should_review_memory, + _turn_exit_reason, _pending_verification_response=None, _pending_verification_response_previewed=False, ): """Run the post-loop finalization and return the turn ``result`` dict.""" from agent.conversation_loop import logger final_response, _turn_exit_reason, preserved_verification_fallback = _resolve_budget_fallback( - agent, - final_response=final_response, - api_call_count=api_call_count, - interrupted=interrupted, - failed=failed, - messages=messages, + agent, final_response=final_response, api_call_count=api_call_count, + interrupted=interrupted, failed=failed, messages=messages, _turn_exit_reason=_turn_exit_reason, _pending_verification_response=_pending_verification_response, _pending_verification_response_previewed=_pending_verification_response_previewed, @@ -539,16 +503,9 @@ def finalize_turn( # ``_last_turn_usage`` holds the last API response's canonical usage dict, or # ``None`` on turns that never reached a provider response — by contract. _notify_context_engine_turn_complete( - agent, - messages, - usage=getattr(agent, "_last_turn_usage", None), - logger=logger, - turn_id=turn_id, - task_id=effective_task_id, - api_call_count=api_call_count, - interrupted=interrupted, - failed=failed, - turn_exit_reason=_turn_exit_reason, + agent, messages, usage=getattr(agent, "_last_turn_usage", None), logger=logger, + turn_id=turn_id, task_id=effective_task_id, api_call_count=api_call_count, + interrupted=interrupted, failed=failed, turn_exit_reason=_turn_exit_reason, ) except Exception as exc: logger.warning("on_turn_complete notification failed: %s", exc) @@ -635,10 +592,8 @@ def finalize_turn( # External memory provider: sync the completed turn + queue next prefetch. agent._sync_external_memory_for_turn( - original_user_message=original_user_message, - final_response=final_response, - interrupted=interrupted, - messages=messages, + original_user_message=original_user_message, final_response=final_response, + interrupted=interrupted, messages=messages, ) # Background memory/skill review runs AFTER delivery so it never competes with the @@ -654,8 +609,7 @@ def finalize_turn( # _spawn_background_review clones the snapshot structurally so the fork's # in-place sanitizers can't reach the live transcript. agent._spawn_background_review( - messages_snapshot=list(messages), - review_memory=_should_review_memory, + messages_snapshot=list(messages), review_memory=_should_review_memory, review_skills=_should_review_skills, ) except Exception: diff --git a/agent/turn_iteration_prep.py b/agent/turn_iteration_prep.py index b209e27594..210ae3da74 100644 --- a/agent/turn_iteration_prep.py +++ b/agent/turn_iteration_prep.py @@ -32,18 +32,12 @@ class IterationPrep: request_logger: Any -def prepare_iteration( - agent: Any, - *, - messages: Any, - api_call_count: Any, -) -> IterationPrep: +def prepare_iteration(agent: Any,*, messages: Any, api_call_count: Any) -> IterationPrep: """Prepare ``messages`` for this iteration in the original order. Every mutation here is cache-safe by construction: steer text lands in the newest tool result, the ghost-row filter only drops hidden scaffold placeholders, and repair runs BEFORE the request build.""" from agent.conversation_loop import ( - _INTERRUPT_SCAFFOLD_MARKER, - _maybe_inject_run_budget_wrapup, + _INTERRUPT_SCAFFOLD_MARKER, _maybe_inject_run_budget_wrapup ) def _verdict(action: str, result: Optional[Dict[str, Any]] = None) -> IterationPrep: @@ -112,8 +106,7 @@ def prepare_iteration( pass _injected = True logger.debug( - "Pre-API-call steer drain: injected into tool msg at index %d", - _si, + "Pre-API-call steer drain: injected into tool msg at index %d", _si ) break if not _injected: @@ -149,10 +142,7 @@ def prepare_iteration( except Exception: pass repaired_tool_calls = agent._sanitize_tool_call_arguments( - messages, - logger=request_logger, - session_id=agent.session_id, - cursor=_sanitize_cursor, + messages, logger=request_logger, session_id=agent.session_id, cursor=_sanitize_cursor ) if repaired_tool_calls > 0: request_logger.info( @@ -205,12 +195,7 @@ class ApiCallAnnouncement: def announce_api_call( - agent: Any, - *, - messages: Any, - api_messages: Any, - api_call_count: Any, - approx_tokens: Any, + agent: Any, *, messages: Any, api_messages: Any, api_call_count: Any, approx_tokens: Any, total_chars: Any, ) -> ApiCallAnnouncement: """Print the request summary (verbose) or start the quiet-mode thinking indicator.""" @@ -266,21 +251,14 @@ class IterationStart: def begin_iteration( - agent: Any, - *, - messages: Any, - conversation_history: Any, - original_user_message: Any, - api_call_count: Any, - interrupted: Any, - _turn_exit_reason: Any, + agent: Any, *, messages: Any, conversation_history: Any, original_user_message: Any, + api_call_count: Any, interrupted: Any, _turn_exit_reason: Any, ) -> IterationStart: """Iteration entry in the original order: apply a pending redirect, reset the checkpoint dedup, then the interrupt / review-budget / iteration-budget exits. ``api_call_count`` is incremented here (the grace call consumes its flag instead of the budget).""" from agent.conversation_loop import ( - _apply_active_turn_redirect, - _review_input_budget_exhausted, + _apply_active_turn_redirect, _review_input_budget_exhausted ) def _verdict(action: str, result: Optional[Dict[str, Any]] = None) -> IterationStart: @@ -298,8 +276,7 @@ def begin_iteration( _apply_active_turn_redirect(agent, messages, _redirect_text) if isinstance(original_user_message, str): original_user_message = ( - f"{original_user_message}\n\n" - f"User correction during the turn: {_redirect_text}" + f"{original_user_message}\n\n" f"User correction during the turn: {_redirect_text}" ) agent._persist_session(messages, conversation_history) @@ -360,30 +337,17 @@ class RetryRestartVerdict: def apply_retry_restarts( - agent: Any, - *, - _retry: Any, - response: Any, - interrupted: Any, - messages: Any, - conversation_history: Any, - user_message: Any, - api_kwargs: Any, - current_turn_user_idx: Any, - final_response: Any, - retry_count: Any, - api_call_count: Any, - length_continue_retries: Any, - _preflight_compression_blocked: Any, - _turn_exit_reason: Any, + agent: Any, *, _retry: Any, response: Any, interrupted: Any, messages: Any, + conversation_history: Any, user_message: Any, api_kwargs: Any, current_turn_user_idx: Any, + final_response: Any, retry_count: Any, api_call_count: Any, length_continue_retries: Any, + _preflight_compression_blocked: Any, _turn_exit_reason: Any, ) -> RetryRestartVerdict: """Consume the ``TurnRetryState`` restart flags after the retry loop, in the original priority order. Refunds the iteration budget/count for restarts that produced no valid assistant item; ``restart_with_rebuilt_messages`` is the single consumer that clears ``_preflight_compression_blocked`` so the fallback gets a fresh preflight (#84733).""" from agent.conversation_loop import ( - _HANDOFF_SKIP_FINAL_RESPONSE, - _should_skip_model_call_for_reference_handoff, + _HANDOFF_SKIP_FINAL_RESPONSE, _should_skip_model_call_for_reference_handoff ) def _verdict(action: str, result: Optional[Dict[str, Any]] = None) -> RetryRestartVerdict: diff --git a/agent/turn_liveness.py b/agent/turn_liveness.py index 04e381df1f..24f7df2384 100644 --- a/agent/turn_liveness.py +++ b/agent/turn_liveness.py @@ -80,12 +80,10 @@ def resolve_turn_liveness_settings( timeout_s = _resolve_finite_seconds( section.get("timeout_s", DEFAULT_TURN_LIVENESS_TIMEOUT_S), - default=DEFAULT_TURN_LIVENESS_TIMEOUT_S, - key=_CONFIG_TIMEOUT_KEY, + default=DEFAULT_TURN_LIVENESS_TIMEOUT_S, key=_CONFIG_TIMEOUT_KEY, ) poll_s = _resolve_finite_seconds( - section.get("poll_s", DEFAULT_TURN_LIVENESS_POLL_S), - default=DEFAULT_TURN_LIVENESS_POLL_S, + section.get("poll_s", DEFAULT_TURN_LIVENESS_POLL_S), default=DEFAULT_TURN_LIVENESS_POLL_S, key=_CONFIG_POLL_KEY, ) if poll_s <= 0: @@ -104,16 +102,9 @@ class TurnLivenessWatchdog: """ def __init__( - self, - agent: Any, - *, - session_id: str, - timeout_s: float, - poll_s: float, - stop_event: threading.Event, - activity_lock: threading.Lock, - is_turn_active: Callable[[], bool], - commit_abort: Callable[[ActivitySnapshot, str], bool], + self, agent: Any, *, session_id: str, timeout_s: float, poll_s: float, + stop_event: threading.Event, activity_lock: threading.Lock, + is_turn_active: Callable[[], bool], commit_abort: Callable[[ActivitySnapshot, str], bool], deactivate_turn: Callable[[], None], ) -> None: self._agent = agent diff --git a/agent/turn_loop_errors.py b/agent/turn_loop_errors.py index 92da98cb24..c3b97efdb9 100644 --- a/agent/turn_loop_errors.py +++ b/agent/turn_loop_errors.py @@ -34,16 +34,8 @@ class OuterErrorVerdict: def handle_outer_loop_error( - agent: Any, - *, - e: Any, - _outer_error_count: Any, - api_call_count: Any, - messages: Any, - conversation_history: Any, - _turn_exit_reason: Any, - failed: Any, - final_response: Any, + agent: Any, *, e: Any, _outer_error_count: Any, api_call_count: Any, messages: Any, + conversation_history: Any, _turn_exit_reason: Any, failed: Any, final_response: Any, ) -> OuterErrorVerdict: """Handle an exception that escaped the response-processing block. Shutdown and local-processing errors are deterministic and end the turn; API-path errors retry until @@ -51,20 +43,14 @@ def handle_outer_loop_error( assistant message is never appended here: a prefill/interim assistant may already be the tail; ``finalize_turn`` appends only when safe.""" from agent.conversation_loop import ( - _API_CALL_MODULES, - _LOCAL_PROCESSING_MODULES, - _MAX_OUTER_LOOP_ERRORS, - _is_interpreter_shutdown_error, - _ra, + _API_CALL_MODULES, _LOCAL_PROCESSING_MODULES, _MAX_OUTER_LOOP_ERRORS, + _is_interpreter_shutdown_error, _ra, ) def _verdict(action: str, result: Optional[Dict[str, Any]] = None) -> OuterErrorVerdict: return OuterErrorVerdict( - action=action, - _outer_error_count=_outer_error_count, - _turn_exit_reason=_turn_exit_reason, - failed=failed, - final_response=final_response, + action=action, _outer_error_count=_outer_error_count, + _turn_exit_reason=_turn_exit_reason, failed=failed, final_response=final_response, ) # Count every escaped exception before classification so permanent @@ -76,8 +62,7 @@ def handle_outer_loop_error( # Interpreter shutdown makes every executor op raise: break. (#93217) if sys.is_finalizing() or _is_interpreter_shutdown_error(e): error_msg = ( - f"Interpreter is shutting down — cannot continue " - f"(API call #{api_call_count}): {e}" + f"Interpreter is shutting down — cannot continue " f"(API call #{api_call_count}): {e}" ) try: agent._safe_print(f"❌ {error_msg}") diff --git a/agent/turn_overflow.py b/agent/turn_overflow.py index 5eeb9e3836..e74855a084 100644 --- a/agent/turn_overflow.py +++ b/agent/turn_overflow.py @@ -16,18 +16,14 @@ from dataclasses import dataclass from typing import Any, Dict, List, Optional from agent.conversation_compression import ( - COMPRESSION_RETRY_MESSAGES_STATUS_TEMPLATE, - COMPRESSION_RETRY_TOKENS_STATUS_TEMPLATE, - COMPRESSION_RETRY_TOO_LARGE_STATUS_TEMPLATE, - compression_blocked_transiently, - compression_skipped_due_to_lock, - context_compression_timed_out, + COMPRESSION_RETRY_MESSAGES_STATUS_TEMPLATE, COMPRESSION_RETRY_TOKENS_STATUS_TEMPLATE, + COMPRESSION_RETRY_TOO_LARGE_STATUS_TEMPLATE, compression_blocked_transiently, + compression_skipped_due_to_lock, context_compression_timed_out, ) from agent.error_classifier import FailoverReason from agent.message_sanitization import serialized_messages_bytes from agent.model_metadata import ( - get_context_length_from_provider_error, - is_output_cap_error, + get_context_length_from_provider_error, is_output_cap_error, parse_available_output_tokens_from_error, ) from agent.turn_retry_state import TurnRetryState @@ -88,25 +84,17 @@ class _Recovery: def verdict(self, action: str, result: Optional[Dict[str, Any]] = None) -> OverflowVerdict: return OverflowVerdict( - action=action, - result=result, - messages=self.messages, + action=action, result=result, messages=self.messages, active_system_prompt=self.active_system_prompt, - conversation_history=self.conversation_history, - approx_tokens=self.approx_tokens, + conversation_history=self.conversation_history, approx_tokens=self.approx_tokens, compression_attempts=self.compression_attempts, provider_overflow_recovery_pending=self.provider_overflow_recovery_pending, is_context_length_error=self.is_context_length_error, ) def fail_turn( - self, - final_response: str, - *, - notices: tuple = (), - log: Optional[tuple] = None, - compression_exhausted: bool = True, - **extra: Any, + self, final_response: str, *, notices: tuple = (), log: Optional[tuple] = None, + compression_exhausted: bool = True, **extra: Any, ) -> OverflowVerdict: """End the turn as failed/partial. ``notices`` flush the buffered retry trace first so the user sees what compression attempts were made.""" @@ -160,18 +148,15 @@ class _Recovery: spent its wait budget with no committed summary) ends the turn via the typed contract, since re-sending would hit the same overflow (#98722).""" from agent.conversation_loop import ( - _COMPRESSION_TIMEOUT_FINAL_RESPONSE, - _compression_deferred_result, + _COMPRESSION_TIMEOUT_FINAL_RESPONSE, _compression_deferred_result, conversation_history_after_compression, ) agent = self.agent before = self.messages self.messages, self.active_system_prompt = agent._compress_context( - before, self.system_message, - approx_tokens=request_tokens, - task_id=self.effective_task_id, - bypass_cooldown=True, + before, self.system_message, approx_tokens=request_tokens, + task_id=self.effective_task_id, bypass_cooldown=True, ) if self.messages is before: deferred = None @@ -187,8 +172,7 @@ class _Recovery: return self.verdict("return", deferred) if fail_on_timeout and context_compression_timed_out(agent): return self.fail_turn( - _COMPRESSION_TIMEOUT_FINAL_RESPONSE, - turn_exit_reason="context_compression_timeout", + _COMPRESSION_TIMEOUT_FINAL_RESPONSE, turn_exit_reason="context_compression_timeout" ) self.conversation_history = conversation_history_after_compression( agent, self.messages, self.conversation_history @@ -235,8 +219,7 @@ def _recover_payload_too_large(st: _Recovery, _retry: TurnRetryState) -> Overflo agent._buffer_status(COMPRESSION_RETRY_MESSAGES_STATUS_TEMPLATE.format(before=original_len, after=len(messages))) else: agent._buffer_status( - f"🗜️ Compressed {original_bytes:,} → {new_bytes:,} " - f"payload bytes, retrying..." + f"🗜️ Compressed {original_bytes:,} → {new_bytes:,} " f"payload bytes, retrying..." ) time.sleep(2) # Brief pause between compression retries _retry.restart_with_compressed_messages = True @@ -299,8 +282,7 @@ def _clamp_output_cap(st: _Recovery, _retry: TurnRetryState, available_out: int, agent._buffer_status(COMPRESSION_RETRY_TOKENS_STATUS_TEMPLATE.format(before=original_tokens, after=new_tokens)) except Exception: logger.warning( - "%sOutput-cap compression hit an error; retrying on max_tokens only.", - agent.log_prefix, + "%sOutput-cap compression hit an error; retrying on max_tokens only.", agent.log_prefix ) _retry.restart_with_compressed_messages = True return st.verdict("break") @@ -318,12 +300,8 @@ def _adopt_provider_context_limit(st: _Recovery, error_msg: str, old_ctx: int) - if new_ctx is not None: agent._buffer_vprint(f"Context limit detected from API: {new_ctx:,} tokens (was {old_ctx:,})") compressor.update_model( - model=agent.model, - context_length=new_ctx, - base_url=agent.base_url, - api_key=getattr(agent, "api_key", ""), - provider=agent.provider, - api_mode=agent.api_mode, + model=agent.model, context_length=new_ctx, base_url=agent.base_url, + api_key=getattr(agent, "api_key", ""), provider=agent.provider, api_mode=agent.api_mode, ) # Persist the provider-reported limit BEFORE compression/retry: rate limit, # missing usage, or restart must not lose confirmed metadata. Probe flags @@ -342,8 +320,7 @@ def _adopt_provider_context_limit(st: _Recovery, error_msg: str, old_ctx: int) - is_minimax_provider = ( _provider_lower in {"minimax", "minimax-cn"} or _base_lower.startswith(( - "https://api.minimax.io/anthropic", - "https://api.minimaxi.com/anthropic", + "https://api.minimax.io/anthropic", "https://api.minimaxi.com/anthropic" )) ) if is_minimax_provider and "context window exceeds limit (" in error_msg: @@ -438,23 +415,11 @@ def _recover_context_length(st: _Recovery, _retry: TurnRetryState, error_msg: st def recover_from_overflow( - agent: Any, - api_error: Exception, - classified: Any, - _retry: TurnRetryState, - *, - status_code: Optional[int], - error_msg: str, - wrapped_output_cap_budget: Optional[int], - messages: List[Dict[str, Any]], - api_messages: Any, - system_message: Any, - active_system_prompt: Any, - conversation_history: Any, - approx_tokens: int, - compression_attempts: int, - max_compression_attempts: int, - api_call_count: int, + agent: Any, api_error: Exception, classified: Any, _retry: TurnRetryState, *, + status_code: Optional[int], error_msg: str, wrapped_output_cap_budget: Optional[int], + messages: List[Dict[str, Any]], api_messages: Any, system_message: Any, + active_system_prompt: Any, conversation_history: Any, approx_tokens: int, + compression_attempts: int, max_compression_attempts: int, api_call_count: int, effective_task_id: Any, ) -> OverflowVerdict: """413 payload-too-large and context-length recovery (compress + retry, output-cap @@ -464,17 +429,11 @@ def recover_from_overflow( Compression progress is scored in payload BYTES for 413 (never the byte-blind token estimate) and in tokens/message count for context overflow.""" st = _Recovery( - agent=agent, - api_messages=api_messages, - system_message=system_message, - effective_task_id=effective_task_id, - api_call_count=api_call_count, - max_compression_attempts=max_compression_attempts, - messages=messages, - active_system_prompt=active_system_prompt, - conversation_history=conversation_history, - approx_tokens=approx_tokens, - compression_attempts=compression_attempts, + agent=agent, api_messages=api_messages, system_message=system_message, + effective_task_id=effective_task_id, api_call_count=api_call_count, + max_compression_attempts=max_compression_attempts, messages=messages, + active_system_prompt=active_system_prompt, conversation_history=conversation_history, + approx_tokens=approx_tokens, compression_attempts=compression_attempts, ) # GitHub Models free tier caps requests at 8K tokens, under the system prompt + diff --git a/agent/turn_preflight.py b/agent/turn_preflight.py index 173bb0a445..ec2185e9b4 100644 --- a/agent/turn_preflight.py +++ b/agent/turn_preflight.py @@ -16,10 +16,8 @@ from typing import Any, Dict, List, Optional from agent.context_engine import automatic_compaction_status_message from agent.conversation_compression import ( - PRE_API_COMPRESSION_STATUS_TEMPLATE, - compression_blocked_transiently, - compression_skipped_due_to_lock, - context_compression_timed_out, + PRE_API_COMPRESSION_STATUS_TEMPLATE, compression_blocked_transiently, + compression_skipped_due_to_lock, context_compression_timed_out, conversation_history_after_compression, ) from agent.turn_context import _review_fork_first_request_pending @@ -54,28 +52,12 @@ class PreflightVerdict: def run_preflight_compression( - agent: Any, - *, - compressor: Any, - request_pressure_tokens: int, - provider_overflow_preflight: bool, - preflight_compression_blocked: bool, - defer_preflight: Any, - moa_prepared_request: Any, - pending_moa_prepared_request: Any, - messages: List[Dict[str, Any]], - system_message: Any, - user_message: Any, - active_system_prompt: Any, - conversation_history: Any, - api_call_count: int, - compression_attempts: int, - max_compression_attempts: int, - effective_task_id: Any, - final_response: Any, - failed: bool, - compression_timeout_exhausted: bool, - turn_exit_reason: Any, + agent: Any, *, compressor: Any, request_pressure_tokens: int, provider_overflow_preflight: bool, + preflight_compression_blocked: bool, defer_preflight: Any, moa_prepared_request: Any, + pending_moa_prepared_request: Any, messages: List[Dict[str, Any]], system_message: Any, + user_message: Any, active_system_prompt: Any, conversation_history: Any, api_call_count: int, + compression_attempts: int, max_compression_attempts: int, effective_task_id: Any, + final_response: Any, failed: bool, compression_timeout_exhausted: bool, turn_exit_reason: Any, ) -> PreflightVerdict: """Mirror of the turn-prologue guard chain (defer on noisy estimate → skip in failure cooldown → ``should_compress``), #11529. A compression pass that never reaches the @@ -84,11 +66,8 @@ def run_preflight_compression( leaves the progress blocker unarmed (#69870, #97488). A forced provider-overflow preflight that any gate blocks fails closed (llama.cpp may silently truncate).""" from agent.conversation_loop import ( - _COMPRESSION_TIMEOUT_FINAL_RESPONSE, - _HANDOFF_SKIP_FINAL_RESPONSE, - _compression_deferred_result, - _maybe_grow_local_window, - _provider_overflow_exhausted_result, + _COMPRESSION_TIMEOUT_FINAL_RESPONSE, _HANDOFF_SKIP_FINAL_RESPONSE, + _compression_deferred_result, _maybe_grow_local_window, _provider_overflow_exhausted_result, _should_skip_model_call_for_reference_handoff, ) @@ -103,18 +82,12 @@ def run_preflight_compression( def _verdict(action: str, result: Optional[Dict[str, Any]] = None) -> PreflightVerdict: return PreflightVerdict( - action=action, - result=result, - messages=messages, - active_system_prompt=active_system_prompt, - conversation_history=conversation_history, - api_call_count=api_call_count, - compression_attempts=compression_attempts, + action=action, result=result, messages=messages, + active_system_prompt=active_system_prompt, conversation_history=conversation_history, + api_call_count=api_call_count, compression_attempts=compression_attempts, pending_moa_prepared_request=pending_moa_prepared_request, - last_preflight_pressure=_last_preflight_pressure, - final_response=final_response, - failed=failed, - compression_timeout_exhausted=_compression_timeout_exhausted, + last_preflight_pressure=_last_preflight_pressure, final_response=final_response, + failed=failed, compression_timeout_exhausted=_compression_timeout_exhausted, turn_exit_reason=_turn_exit_reason, ) @@ -127,12 +100,10 @@ def run_preflight_compression( and len(messages) > 1 and compression_attempts < max_compression_attempts and ( - not _preflight_compression_blocked - or _provider_overflow_preflight + not _preflight_compression_blocked or _provider_overflow_preflight ) and ( - not _defer_preflight(request_pressure_tokens) - or _provider_overflow_preflight + not _defer_preflight(request_pressure_tokens) or _provider_overflow_preflight ) and not _compression_cooldown and _compressor.should_compress(request_pressure_tokens) @@ -146,9 +117,7 @@ def run_preflight_compression( # Bigger window granted: recalibrate the compressor and skip compression # this pass. _compressor.update_model( - agent.model, - _grown_window, - base_url=getattr(agent, "base_url", "") or "", + agent.model, _grown_window, base_url=getattr(agent, "base_url", "") or "", api_key=getattr(agent, "api_key", "") or "", provider=getattr(agent, "provider", "") or "", api_mode=getattr(agent, "api_mode", "") or "", @@ -203,9 +172,7 @@ def run_preflight_compression( _last_preflight_pressure = request_pressure_tokens _pre_api_input = messages messages, active_system_prompt = agent._compress_context( - messages, - system_message, - approx_tokens=request_pressure_tokens, + messages, system_message, approx_tokens=request_pressure_tokens, task_id=effective_task_id, ) if context_compression_timed_out(agent): @@ -220,8 +187,7 @@ def run_preflight_compression( _turn_exit_reason = "context_compression_timeout" return _verdict("break") if messages is _pre_api_input and ( - compression_skipped_due_to_lock(agent) - or compression_blocked_transiently(agent) + compression_skipped_due_to_lock(agent) or compression_blocked_transiently(agent) ): # Temporary DEFER (lock held / cooldown), not evidence about # compressibility: refund the attempt, leave the progress blocker @@ -268,23 +234,15 @@ def run_preflight_compression( # don't resend; let the next user turn retry after cooldown. agent._persist_session(messages, conversation_history) return _verdict("return", _compression_deferred_result( - agent, - messages, - api_call_count, - reason="transient_block", + agent, messages, api_call_count, reason="transient_block" )) elif ( - _provider_overflow_preflight - and compression_attempts >= max_compression_attempts + _provider_overflow_preflight and compression_attempts >= max_compression_attempts ): # All recovery passes consumed and still over threshold: fail closed — # llama.cpp may silently truncate an oversized retry. return _verdict("return", _provider_overflow_exhausted_result( - agent, - messages, - conversation_history, - api_call_count, - request_pressure_tokens, + agent, messages, conversation_history, api_call_count, request_pressure_tokens, max_compression_attempts, )) elif ( @@ -305,8 +263,7 @@ def run_preflight_compression( _block_reason = None if _block_reason: agent._warn_context_overflow_blocked( - _block_reason, - request_pressure_tokens, + _block_reason, request_pressure_tokens, int(getattr(_compressor, "threshold_tokens", 0) or 0), ) elif not agent.compression_enabled and len(messages) > 1: @@ -317,9 +274,7 @@ def run_preflight_compression( getattr(agent, "context_compressor", None), "context_length", None ) if ( - isinstance(_ctx_len, int) - and _ctx_len > 0 - and request_pressure_tokens > _ctx_len + isinstance(_ctx_len, int) and _ctx_len > 0 and request_pressure_tokens > _ctx_len ): _warn_fn = getattr( agent, "_warn_uncompressed_context_overflow", None @@ -331,11 +286,7 @@ def run_preflight_compression( # Any other gate blocking the forced preflight (e.g. uncompressible one- # message request) must fail closed: the request is proven not to fit. return _verdict("return", _provider_overflow_exhausted_result( - agent, - messages, - conversation_history, - api_call_count, - request_pressure_tokens, + agent, messages, conversation_history, api_call_count, request_pressure_tokens, max_compression_attempts, )) return _verdict("proceed") @@ -357,17 +308,9 @@ class PostToolCompressionVerdict: def compress_after_tool_results( - agent: Any, - *, - messages: List[Dict[str, Any]], - system_message: Any, - user_message: Any, - active_system_prompt: Any, - conversation_history: Any, - compression_attempts: int, - max_compression_attempts: int, - effective_task_id: Any, - final_response: Any, + agent: Any, *, messages: List[Dict[str, Any]], system_message: Any, user_message: Any, + active_system_prompt: Any, conversation_history: Any, compression_attempts: int, + max_compression_attempts: int, effective_task_id: Any, final_response: Any, turn_exit_reason: Any, ) -> PostToolCompressionVerdict: """Post-tool-call compression decision. Pressure comes from API-reported @@ -377,23 +320,17 @@ def compress_after_tool_results( (#62625) plus the deterministic tool-result-only prune, committed only when the engine returns a NEW list (never rebuild ``conversation_history`` for it).""" from agent.conversation_loop import ( - _HANDOFF_SKIP_FINAL_RESPONSE, - _midturn_request_pressure_tokens, - _should_skip_model_call_for_reference_handoff, - estimate_request_tokens_rough, + _HANDOFF_SKIP_FINAL_RESPONSE, _midturn_request_pressure_tokens, + _should_skip_model_call_for_reference_handoff, estimate_request_tokens_rough, ) _turn_exit_reason = turn_exit_reason def _verdict(end_turn: bool) -> PostToolCompressionVerdict: return PostToolCompressionVerdict( - end_turn=end_turn, - messages=messages, - active_system_prompt=active_system_prompt, - conversation_history=conversation_history, - compression_attempts=compression_attempts, - final_response=final_response, - turn_exit_reason=_turn_exit_reason, + end_turn=end_turn, messages=messages, active_system_prompt=active_system_prompt, + conversation_history=conversation_history, compression_attempts=compression_attempts, + final_response=final_response, turn_exit_reason=_turn_exit_reason, ) # Decide compression from API-reported prompt tokens (tight lower bound; @@ -437,13 +374,10 @@ def compress_after_tool_results( # Pass overhead-aware _real_tokens, not last_prompt_tokens (0 in # the no-usage fallback), so the overflow guard sees the true size. messages, active_system_prompt = agent._compress_context( - messages, system_message, - approx_tokens=_real_tokens, - task_id=effective_task_id, + messages, system_message, approx_tokens=_real_tokens, task_id=effective_task_id ) if ( - messages is _post_tool_input - and compression_skipped_due_to_lock(agent) + messages is _post_tool_input and compression_skipped_due_to_lock(agent) ): # Lock-skip no-op is a temporary defer, not evidence about # compressibility: refund so a lock-loser loop doesn't burn the @@ -477,9 +411,7 @@ def compress_after_tool_results( _block_reason = None if _block_reason: agent._warn_context_overflow_blocked( - _block_reason, - _real_tokens, - int(getattr(_compressor, "threshold_tokens", 0) or 0), + _block_reason, _real_tokens, int(getattr(_compressor, "threshold_tokens", 0) or 0) ) # Proactive tool-result prune (deterministic, no LLM, keeps tail): # no-op unless proactive_prune_tokens is exceeded; commits only past @@ -492,8 +424,7 @@ def compress_after_tool_results( ) except Exception: logger.debug( - "proactive tool-result prune failed; skipping", - exc_info=True, + "proactive tool-result prune failed; skipping", exc_info=True ) _pruned_msgs, _pruned_n = messages, 0 # Standard no-op caller contract: only commit when the diff --git a/agent/turn_preflight_gate.py b/agent/turn_preflight_gate.py index 777c303f41..77560cb653 100644 --- a/agent/turn_preflight_gate.py +++ b/agent/turn_preflight_gate.py @@ -42,26 +42,12 @@ class PreflightGateVerdict: def run_preflight_gate( - agent: Any, - *, - request_pressure_tokens: Any, - _moa_prepared_request: Any, - pending_moa_prepared_request: Any, - messages: Any, - system_message: Any, - user_message: Any, - active_system_prompt: Any, - conversation_history: Any, - api_call_count: Any, - compression_attempts: Any, - max_compression_attempts: Any, - effective_task_id: Any, - final_response: Any, - failed: Any, - _turn_exit_reason: Any, - _compression_timeout_exhausted: Any, - _preflight_compression_blocked: Any, - _provider_overflow_recovery_pending: Any, + agent: Any, *, request_pressure_tokens: Any, _moa_prepared_request: Any, + pending_moa_prepared_request: Any, messages: Any, system_message: Any, user_message: Any, + active_system_prompt: Any, conversation_history: Any, api_call_count: Any, + compression_attempts: Any, max_compression_attempts: Any, effective_task_id: Any, + final_response: Any, failed: Any, _turn_exit_reason: Any, _compression_timeout_exhausted: Any, + _preflight_compression_blocked: Any, _provider_overflow_recovery_pending: Any, _last_preflight_pressure: Any, ) -> PreflightGateVerdict: """Run the pre-API guard chain in the original order (#11529). ``_last_preflight_pressure`` @@ -73,21 +59,15 @@ def run_preflight_gate( def _verdict(action: str, result: Optional[Dict[str, Any]] = None) -> PreflightGateVerdict: return PreflightGateVerdict( - action=action, - pending_moa_prepared_request=pending_moa_prepared_request, - messages=messages, - active_system_prompt=active_system_prompt, - conversation_history=conversation_history, - api_call_count=api_call_count, - compression_attempts=compression_attempts, - final_response=final_response, - failed=failed, + action=action, pending_moa_prepared_request=pending_moa_prepared_request, + messages=messages, active_system_prompt=active_system_prompt, + conversation_history=conversation_history, api_call_count=api_call_count, + compression_attempts=compression_attempts, final_response=final_response, failed=failed, _turn_exit_reason=_turn_exit_reason, _compression_timeout_exhausted=_compression_timeout_exhausted, _preflight_compression_blocked=_preflight_compression_blocked, _provider_overflow_recovery_pending=_provider_overflow_recovery_pending, - _last_preflight_pressure=_last_preflight_pressure, - result=result, + _last_preflight_pressure=_last_preflight_pressure, result=result, ) _runtime_context_error = _ollama_context_limit_error( @@ -117,13 +97,11 @@ def run_preflight_gate( _provider_overflow_preflight = ( _provider_overflow_recovery_pending and ( - _preflight_threshold <= 0 - or request_pressure_tokens >= _preflight_threshold + _preflight_threshold <= 0 or request_pressure_tokens >= _preflight_threshold ) ) if ( - _provider_overflow_recovery_pending - and not _provider_overflow_preflight + _provider_overflow_recovery_pending and not _provider_overflow_preflight ): # The outer-loop rebuild includes system prompt, request-only injections and # tool schemas; only that full request with output runway may be sent. @@ -136,9 +114,7 @@ def run_preflight_gate( _previous_preflight_pressure is not None and request_pressure_tokens >= _preflight_threshold and not _compression_warrants_another_preflight_pass( - _previous_preflight_pressure, - request_pressure_tokens, - _preflight_threshold, + _previous_preflight_pressure, request_pressure_tokens, _preflight_threshold ) ): # Stop proactive retries this turn without consuming the shared overflow- @@ -154,25 +130,16 @@ def run_preflight_gate( _compressor, "should_defer_preflight_to_real_usage", lambda _t: False ) _pf = run_preflight_compression( - agent, - compressor=_compressor, - request_pressure_tokens=request_pressure_tokens, + agent, compressor=_compressor, request_pressure_tokens=request_pressure_tokens, provider_overflow_preflight=_provider_overflow_preflight, preflight_compression_blocked=_preflight_compression_blocked, - defer_preflight=_defer_preflight, - moa_prepared_request=_moa_prepared_request, - pending_moa_prepared_request=pending_moa_prepared_request, - messages=messages, - system_message=system_message, - user_message=user_message, - active_system_prompt=active_system_prompt, - conversation_history=conversation_history, - api_call_count=api_call_count, - compression_attempts=compression_attempts, - max_compression_attempts=max_compression_attempts, - effective_task_id=effective_task_id, - final_response=final_response, - failed=failed, + defer_preflight=_defer_preflight, moa_prepared_request=_moa_prepared_request, + pending_moa_prepared_request=pending_moa_prepared_request, messages=messages, + system_message=system_message, user_message=user_message, + active_system_prompt=active_system_prompt, conversation_history=conversation_history, + api_call_count=api_call_count, compression_attempts=compression_attempts, + max_compression_attempts=max_compression_attempts, effective_task_id=effective_task_id, + final_response=final_response, failed=failed, compression_timeout_exhausted=_compression_timeout_exhausted, turn_exit_reason=_turn_exit_reason, ) diff --git a/agent/turn_recovery.py b/agent/turn_recovery.py index 6295b165c7..c99eb5af9e 100644 --- a/agent/turn_recovery.py +++ b/agent/turn_recovery.py @@ -33,14 +33,9 @@ from agent.model_metadata import is_output_cap_error, parse_available_output_tok from agent.retry_utils import is_zai_coding_overload_error, zai_coding_overload_retry_ceiling from agent.error_classifier import FailoverReason from agent.message_sanitization import ( - _looks_like_image_content_rejection, - _sanitize_messages_non_ascii, - _sanitize_messages_surrogates, - _sanitize_structure_non_ascii, - _sanitize_structure_surrogates, - _sanitize_tools_non_ascii, - _strip_images_from_messages, - _strip_non_ascii, + _looks_like_image_content_rejection, _sanitize_messages_non_ascii, + _sanitize_messages_surrogates, _sanitize_structure_non_ascii, _sanitize_structure_surrogates, + _sanitize_tools_non_ascii, _strip_images_from_messages, _strip_non_ascii, close_interrupted_tool_sequence, ) from agent.turn_retry_state import TurnRetryState @@ -53,9 +48,7 @@ def _image_error_max_dimension(error: Exception) -> Optional[int]: """Extract a provider-reported image dimension ceiling, if present.""" parts = [] for value in ( - error, - getattr(error, "message", None), - getattr(error, "body", None), + error, getattr(error, "message", None), getattr(error, "body", None) ): if value: try: @@ -94,13 +87,8 @@ def _try_refresh_nous_paid_entitlement_credentials(agent) -> bool: def recover_before_classification( - agent: Any, - api_error: Exception, - *, - messages: List[Dict[str, Any]], - api_messages: Any, - api_kwargs: Any, - active_system_prompt: Any, + agent: Any, api_error: Exception, *, messages: List[Dict[str, Any]], api_messages: Any, + api_kwargs: Any, active_system_prompt: Any, ) -> Tuple[bool, Any]: """Recovery branches that run BEFORE ``classify_api_error``: UnicodeEncodeError sanitization (surrogates, then ASCII codec), provider image-content rejection @@ -116,8 +104,7 @@ def recover_before_classification( # Surrogate errors: utf-8 refusing U+D800..U+DFFF # ("surrogates not allowed"). _is_surrogate_error = ( - "surrogate" in _err_str - or ("'utf-8'" in _err_str and not _is_ascii_codec) + "surrogate" in _err_str or ("'utf-8'" in _err_str and not _is_ascii_codec) ) # Sanitize `messages` AND `api_messages` (which may carry # `reasoning_content`/`reasoning_details`), plus `api_kwargs` and @@ -252,9 +239,7 @@ def recover_before_classification( # 4xx-only gate: 5xx/timeouts are transient and take the retry path. _status_ok = _err_status is None or (400 <= int(_err_status) < 500) if ( - getattr(agent, "_vision_supported", True) - and _looks_like_image_rejection - and _status_ok + getattr(agent, "_vision_supported", True) and _looks_like_image_rejection and _status_ok ): agent._vision_supported = False _imgs_removed = _strip_images_from_messages(messages) @@ -293,14 +278,8 @@ def recover_before_classification( def recover_after_classification( - agent: Any, - api_error: Exception, - classified: Any, - _retry: TurnRetryState, - *, - status_code: Optional[int], - error_context: Any, - messages: List[Dict[str, Any]], + agent: Any, api_error: Exception, classified: Any, _retry: TurnRetryState, *, + status_code: Optional[int], error_context: Any, messages: List[Dict[str, Any]], api_messages: Any, ) -> Tuple[bool, bool]: """One-shot recovery chain that runs AFTER ``classify_api_error`` and before the @@ -315,16 +294,13 @@ def recover_after_classification( # Shared with the billing/entitlement helpers that stay in the loop module; # lazy so this module never imports agent.conversation_loop at load time. from agent.conversation_loop import ( - _is_copilot_provider, - _is_nous_inference_route, - _print_nous_entitlement_guidance, + _is_copilot_provider, _is_nous_inference_route, _print_nous_entitlement_guidance ) if ( classified.reason == FailoverReason.billing and _is_nous_inference_route( - getattr(agent, "provider", "") or "", - getattr(agent, "base_url", "") or "", + getattr(agent, "provider", "") or "", getattr(agent, "base_url", "") or "" ) and not _retry.nous_paid_entitlement_refresh_attempted ): @@ -338,10 +314,8 @@ def recover_after_classification( return True, False recovered_with_pool, _retry.has_retried_429 = agent._recover_with_credential_pool( - status_code=status_code, - has_retried_429=_retry.has_retried_429, - classified_reason=classified.reason, - error_context=error_context, + status_code=status_code, has_retried_429=_retry.has_retried_429, + classified_reason=classified.reason, error_context=error_context, billing_unverified=classified.billing_unverified, ) if recovered_with_pool: @@ -356,8 +330,7 @@ def recover_after_classification( _retry.image_shrink_retry_attempted = True image_max_dimension = _image_error_max_dimension(api_error) or 8000 if agent._try_shrink_image_parts_in_messages( - api_messages, - max_dimension=image_max_dimension, + api_messages, max_dimension=image_max_dimension ): agent._vprint( f"{agent.log_prefix}📐 Image(s) exceeded provider size limit — " @@ -659,20 +632,9 @@ def recover_after_classification( def nonretryable_client_error_result( - agent: Any, - api_error: Exception, - classified: Any, - *, - status_code: Optional[int], - api_kwargs: Any, - api_messages: Any, - messages: List[Dict[str, Any]], - conversation_history: Any, - api_call_count: int, - approx_tokens: int, - provider: Any, - base_url: Any, - model: Any, + agent: Any, api_error: Exception, classified: Any, *, status_code: Optional[int], + api_kwargs: Any, api_messages: Any, messages: List[Dict[str, Any]], conversation_history: Any, + api_call_count: int, approx_tokens: int, provider: Any, base_url: Any, model: Any, ) -> Dict[str, Any]: """Terminal path for a non-retryable 4xx once fallback is exhausted: dump the request for debugging, flush the buffered retry trace, print actionable auth / @@ -682,11 +644,8 @@ def nonretryable_client_error_result( # Result/guidance helpers stay in the loop module (tests import + patch them # there); lazy import avoids a load-time cycle. from agent.conversation_loop import ( - _CONTENT_POLICY_RECOVERY_HINT, - _billing_failure_result, - _content_policy_blocked_result, - _print_billing_or_entitlement_guidance, - _print_nous_entitlement_guidance, + _CONTENT_POLICY_RECOVERY_HINT, _billing_failure_result, _content_policy_blocked_result, + _print_billing_or_entitlement_guidance, _print_nous_entitlement_guidance, ) if api_kwargs is not None: @@ -702,18 +661,15 @@ def nonretryable_client_error_result( _nonretryable_summary = agent._summarize_api_error(api_error) if classified.reason == FailoverReason.content_policy_blocked: agent._emit_status( - f"❌ Provider safety filter blocked this request: " - f"{_nonretryable_summary}" + f"❌ Provider safety filter blocked this request: " f"{_nonretryable_summary}" ) elif classified.reason == FailoverReason.ssl_cert_verification: agent._emit_status( - f"❌ TLS certificate verification failed: " - f"{_nonretryable_summary}" + f"❌ TLS certificate verification failed: " f"{_nonretryable_summary}" ) else: agent._emit_status( - f"❌ Non-retryable error (HTTP {status_code}): " - f"{_nonretryable_summary}" + f"❌ Non-retryable error (HTTP {status_code}): " f"{_nonretryable_summary}" ) agent._vprint(f"{agent.log_prefix}❌ Non-retryable client error (HTTP {status_code}). Aborting.", force=True) agent._vprint(f"{agent.log_prefix} 🔌 Provider: {provider} Model: {model}", force=True) @@ -721,17 +677,12 @@ def nonretryable_client_error_result( # Actionable guidance for common auth errors if classified.is_auth or classified.reason == FailoverReason.billing: if classified.reason == FailoverReason.billing and _print_billing_or_entitlement_guidance( - agent, - capability="model access", - provider=provider, - base_url=str(base_url), - model=model, - unverified=classified.billing_unverified, + agent, capability="model access", provider=provider, base_url=str(base_url), + model=model, unverified=classified.billing_unverified, ): pass elif provider == "nous" and _print_nous_entitlement_guidance( - agent, - "Nous model access", + agent, "Nous model access" ): pass elif provider in {"openai-codex", "xai-oauth", "nous"} and status_code == 401: @@ -837,22 +788,15 @@ def nonretryable_client_error_result( f"{_CONTENT_POLICY_RECOVERY_HINT}" ) return _content_policy_blocked_result( - messages, - api_call_count, - final_response=_policy_response, + messages, api_call_count, final_response=_policy_response, error_detail=_nonretryable_summary, ) # Billing walls get the same structured recovery descriptor as the # max-retries path so every surface renders one consistent signal. if classified.reason == FailoverReason.billing: return _billing_failure_result( - classified=classified, - summary=_nonretryable_summary, - messages=messages, - api_call_count=api_call_count, - provider=provider, - base_url=base_url, - model=model, + classified=classified, summary=_nonretryable_summary, messages=messages, + api_call_count=api_call_count, provider=provider, base_url=base_url, model=model, ) return { "final_response": _nonretryable_summary, @@ -865,22 +809,10 @@ def nonretryable_client_error_result( def max_retries_exhausted_result( - agent: Any, - api_error: Exception, - classified: Any, - *, - max_retries: int, - is_rate_limited: bool, - error_msg: str, - api_kwargs: Any, - api_messages: Any, - messages: List[Dict[str, Any]], - conversation_history: Any, - api_call_count: int, - approx_tokens: int, - provider: Any, - base_url: Any, - model: Any, + agent: Any, api_error: Exception, classified: Any, *, max_retries: int, is_rate_limited: bool, + error_msg: str, api_kwargs: Any, api_messages: Any, messages: List[Dict[str, Any]], + conversation_history: Any, api_call_count: int, approx_tokens: int, provider: Any, + base_url: Any, model: Any, ) -> Dict[str, Any]: """Terminal path once ``retry_count >= max_retries`` and transport recovery + fallback both failed: flush the buffered trace, emit the billing / rate-limit / @@ -890,9 +822,7 @@ def max_retries_exhausted_result( # Result/guidance helpers stay in the loop module (tests import + patch them # there); lazy import avoids a load-time cycle. from agent.conversation_loop import ( - _billing_block_dict, - _billing_or_entitlement_message, - _billing_terminal_label, + _billing_block_dict, _billing_or_entitlement_message, _billing_terminal_label, _print_billing_or_entitlement_guidance, ) @@ -910,19 +840,12 @@ def max_retries_exhausted_result( else: agent._emit_status(f"❌ Billing or credits exhausted — {_final_summary}") _billing_guidance = _billing_or_entitlement_message( - capability="model access", - provider=provider, - base_url=str(base_url), - model=model, + capability="model access", provider=provider, base_url=str(base_url), model=model, unverified=classified.billing_unverified, ) _print_billing_or_entitlement_guidance( - agent, - capability="model access", - provider=provider, - base_url=str(base_url), - model=model, - unverified=classified.billing_unverified, + agent, capability="model access", provider=provider, base_url=str(base_url), + model=model, unverified=classified.billing_unverified, ) elif is_rate_limited: agent._emit_status(f"❌ Rate limited after {max_retries} retries — {_final_summary}") @@ -964,9 +887,7 @@ def max_retries_exhausted_result( is_thinking_timeout, ) _is_thinking_timeout = is_thinking_timeout( - classified, - model, - error_msg, + classified, model, error_msg ) if _is_thinking_timeout: agent._vprint( @@ -979,8 +900,7 @@ def max_retries_exhausted_result( force=True, ) agent._vprint( - f"{agent.log_prefix} Workarounds in priority order:", - force=True, + f"{agent.log_prefix} Workarounds in priority order:", force=True ) agent._vprint( f"{agent.log_prefix} 1. Set " @@ -1024,8 +944,7 @@ def max_retries_exhausted_result( # Structured recovery descriptor so every surface renders # the same link + label from one signal (see helper). _billing_block = _billing_block_dict( - provider, base_url, model, _billing_guidance, - unverified=_billing_unverified, + provider, base_url, model, _billing_guidance, unverified=_billing_unverified ) else: _final_response = f"API call failed after {max_retries} retries: {_final_summary}" @@ -1036,8 +955,7 @@ def max_retries_exhausted_result( build_thinking_timeout_guidance, ) _final_response += build_thinking_timeout_guidance( - provider=provider, - model=model, + provider=provider, model=model ) elif _is_stream_drop: _final_response += ( @@ -1072,15 +990,8 @@ def max_retries_exhausted_result( def log_api_error_attempt( - agent: Any, - api_error: Exception, - *, - retry_count: int, - max_retries: int, - status_code: Optional[int], - elapsed_time: float, - api_messages: Any, - approx_tokens: int, + agent: Any, api_error: Exception, *, retry_count: int, max_retries: int, + status_code: Optional[int], elapsed_time: float, api_messages: Any, approx_tokens: int, ) -> Tuple[str, str, Any, Any, Any]: """Log one failed API attempt: the ``API call failed`` warning plus the buffered retry trace (provider/endpoint/error/4xx body/elapsed), the OpenRouter @@ -1119,8 +1030,7 @@ def log_api_error_attempt( # OpenRouter "no tool endpoints" hint, buffered with the retry trace # so it only surfaces if every retry+fallback exhausts. if ( - agent._is_openrouter_url() - and "support tool use" in error_msg + agent._is_openrouter_url() and "support tool use" in error_msg ): agent._buffer_vprint( f" 💡 No OpenRouter providers for {_model} support tool calling with your current settings." @@ -1157,16 +1067,9 @@ def log_api_error_attempt( def interruptible_backoff_sleep( - agent: Any, - wait_time: float, - _retry: Optional[TurnRetryState], - *, - messages: List[Dict[str, Any]], - conversation_history: Any, - api_call_count: int, - abort_message: str, - interrupt_text: str, - activity_label: str, + agent: Any, wait_time: float, _retry: Optional[TurnRetryState], *, + messages: List[Dict[str, Any]], conversation_history: Any, api_call_count: int, + abort_message: str, interrupt_text: str, activity_label: str, ) -> Optional[Dict[str, Any]]: """Sleep ``wait_time`` in 200 ms slices so interrupts are honoured promptly, touching activity every ~30 s so the gateway's inactivity monitor knows we are alive. @@ -1205,15 +1108,8 @@ def interruptible_backoff_sleep( def compute_error_backoff( - agent: Any, - api_error: Exception, - *, - retry_count: int, - max_retries: int, - is_rate_limited: bool, - is_zai_coding_overload: bool, - base_url: Any, - model: Any, + agent: Any, api_error: Exception, *, retry_count: int, max_retries: int, is_rate_limited: bool, + is_zai_coding_overload: bool, base_url: Any, model: Any, ) -> float: """Pick the wait before the next API retry and announce it. @@ -1241,10 +1137,7 @@ def compute_error_backoff( _backoff_policy = None if (is_rate_limited or is_zai_coding_overload) and not _retry_after: wait_time, _backoff_policy = adaptive_rate_limit_backoff( - retry_count, - base_url=str(base_url), - model=model, - error=api_error, + retry_count, base_url=str(base_url), model=model, error=api_error, default_wait=wait_time, ) if is_rate_limited or is_zai_coding_overload: @@ -1447,26 +1340,11 @@ class ClassifiedErrorVerdict: def route_classified_error( - agent: Any, - api_error: Exception, - classified: Any, - _retry: TurnRetryState, - *, - error_msg: str, - error_context: Any, - recovered_with_pool: bool, - base_url: Any, - model: Any, - messages: List[Dict[str, Any]], - api_messages: Any, - system_message: Any, - active_system_prompt: Any, - conversation_history: Any, - retry_count: int, - max_retries: int, - compression_attempts: int, - max_compression_attempts: int, - api_call_count: int, + agent: Any, api_error: Exception, classified: Any, _retry: TurnRetryState, *, error_msg: str, + error_context: Any, recovered_with_pool: bool, base_url: Any, model: Any, + messages: List[Dict[str, Any]], api_messages: Any, system_message: Any, + active_system_prompt: Any, conversation_history: Any, retry_count: int, max_retries: int, + compression_attempts: int, max_compression_attempts: int, api_call_count: int, effective_task_id: Any, ) -> ClassifiedErrorVerdict: """Ordered recovery steps between classification logging and overflow handling: @@ -1478,9 +1356,7 @@ def route_classified_error( the cross-session breaker and re-enter the loop exactly once so the top-of-loop guard runs. Order is load-bearing.""" from agent.conversation_loop import ( - _arm_fallback_restart, - _ra, - conversation_history_after_compression, + _arm_fallback_restart, _ra, conversation_history_after_compression, estimate_request_tokens_rough, ) @@ -1493,18 +1369,12 @@ def route_classified_error( def _verdict(action: str, result: Optional[Dict[str, Any]] = None) -> ClassifiedErrorVerdict: return ClassifiedErrorVerdict( - action=action, - result=result, - status_code=status_code, - messages=messages, - active_system_prompt=active_system_prompt, - conversation_history=conversation_history, - retry_count=retry_count, - max_retries=max_retries, + action=action, result=result, status_code=status_code, messages=messages, + active_system_prompt=active_system_prompt, conversation_history=conversation_history, + retry_count=retry_count, max_retries=max_retries, compression_attempts=compression_attempts, provider_overflow_recovery_pending=_provider_overflow_recovery_pending, - is_rate_limited=is_rate_limited, - wrapped_output_cap_budget=_wrapped_output_cap_budget, + is_rate_limited=is_rate_limited, wrapped_output_cap_budget=_wrapped_output_cap_budget, is_zai_coding_overload=_is_zai_coding_overload, ) @@ -1515,8 +1385,7 @@ def route_classified_error( # ``compression.enabled: false`` forbids every automatic trigger, incl. # these overflow recovery paths; error out. Output-cap errors exempt. _overflow_reasons = { - FailoverReason.long_context_tier, - FailoverReason.payload_too_large, + FailoverReason.long_context_tier, FailoverReason.payload_too_large, FailoverReason.context_overflow, } _is_output_cap_error = ( @@ -1569,11 +1438,8 @@ def route_classified_error( old_ctx = compressor.context_length if old_ctx > _reduced_ctx: compressor.update_model( - model=agent.model, - context_length=_reduced_ctx, - base_url=agent.base_url, - api_key=getattr(agent, "api_key", ""), - provider=agent.provider, + model=agent.model, context_length=_reduced_ctx, base_url=agent.base_url, + api_key=getattr(agent, "api_key", ""), provider=agent.provider, api_mode=agent.api_mode, ) # Context probing flags — only set on built-in @@ -1621,9 +1487,7 @@ def route_classified_error( # Eager fallback: rate-limit/billing switch immediately (primary won't # recover in the retry window); transport errors get 1 retry first. is_rate_limited = classified.reason in { - FailoverReason.rate_limit, - FailoverReason.billing, - FailoverReason.upstream_rate_limit, + FailoverReason.rate_limit, FailoverReason.billing, FailoverReason.upstream_rate_limit } # Some relays wrap upstream output-cap 400s as 429 (rate_limit). Only # the max_tokens clamp fixes it (#72281). Parsed once; gates the @@ -1634,8 +1498,7 @@ def route_classified_error( else None ) _is_transport_failure = classified.reason in { - FailoverReason.timeout, - FailoverReason.overloaded, + FailoverReason.timeout, FailoverReason.overloaded } # Z.AI overload 429s classify `overloaded`, which `is_rate_limited` # excludes. Detect directly so the long backoff runs, and raise the @@ -1666,8 +1529,7 @@ def route_classified_error( "upstream_provider", "aggregator" ) agent._buffer_status( - f"⚠️ Upstream {_upstream_name} rate-limited — " - "switching to fallback model..." + f"⚠️ Upstream {_upstream_name} rate-limited — " "switching to fallback model..." ) elif classified.reason == FailoverReason.billing: if classified.billing_unverified: @@ -1726,22 +1588,18 @@ def route_classified_error( _genuine_nous_rate_limit = False try: from agent.nous_rate_guard import ( - is_genuine_nous_rate_limit, - record_nous_rate_limit, + is_genuine_nous_rate_limit, record_nous_rate_limit ) _err_resp = getattr(api_error, "response", None) _err_hdrs = ( - getattr(_err_resp, "headers", None) - if _err_resp else None + getattr(_err_resp, "headers", None) if _err_resp else None ) _genuine_nous_rate_limit = is_genuine_nous_rate_limit( - headers=_err_hdrs, - last_known_state=agent._rate_limit_state, + headers=_err_hdrs, last_known_state=agent._rate_limit_state ) if _genuine_nous_rate_limit: record_nous_rate_limit( - headers=_err_hdrs, - error_context=error_context, + headers=_err_hdrs, error_context=error_context ) else: logger.info( diff --git a/agent/turn_request_assembly.py b/agent/turn_request_assembly.py index ef85a26676..d5c18d06e3 100644 --- a/agent/turn_request_assembly.py +++ b/agent/turn_request_assembly.py @@ -39,27 +39,16 @@ class AssembledRequest: def assemble_api_request( - agent: Any, - *, - messages: Any, - current_turn_user_idx: Any, - _ext_prefetch_cache: Any, - _plugin_user_context: Any, - moa_config: Any, - active_system_prompt: Any, - original_user_message: Any, - pending_moa_prepared_request: Any, - request_logger: Any, + agent: Any, *, messages: Any, current_turn_user_idx: Any, _ext_prefetch_cache: Any, + _plugin_user_context: Any, moa_config: Any, active_system_prompt: Any, + original_user_message: Any, pending_moa_prepared_request: Any, request_logger: Any, ) -> AssembledRequest: """Assemble the request in the original order. ORDER IS LOAD-BEARING: cache breakpoints are injected only after whitespace normalization, the orphan sweep, thinking-only drop / user merge and surrogate stripping, so the same row's bytes never vary across turns.""" from agent.conversation_loop import ( - _apply_context_engine_selection, - _canonicalize_api_tool_calls, - _clone_message_for_send, - _midturn_request_pressure_tokens, - estimate_messages_tokens_rough, + _apply_context_engine_selection, _canonicalize_api_tool_calls, _clone_message_for_send, + _midturn_request_pressure_tokens, estimate_messages_tokens_rough, ) def _verdict(action: str, result: Optional[Dict[str, Any]] = None) -> AssembledRequest: @@ -76,13 +65,9 @@ def assemble_api_request( ) api_messages, effective_system = build_api_messages( - agent, - messages, - current_turn_user_idx=current_turn_user_idx, - ext_prefetch_cache=_ext_prefetch_cache, - plugin_user_context=_plugin_user_context, - moa_config=moa_config, - active_system_prompt=active_system_prompt, + agent, messages, current_turn_user_idx=current_turn_user_idx, + ext_prefetch_cache=_ext_prefetch_cache, plugin_user_context=_plugin_user_context, + moa_config=moa_config, active_system_prompt=active_system_prompt, ) if moa_config: @@ -126,8 +111,7 @@ def assemble_api_request( # Multimodal turn: append MoA context as a trailing text # part instead of silently dropping it. _msg["content"] = [ - *_base, - {"type": "text", "text": "\n\n" + _moa_context}, + *_base, {"type": "text", "text": "\n\n" + _moa_context} ] break except Exception as _moa_exc: @@ -145,16 +129,10 @@ def assemble_api_request( # Per-turn context selection hook: an engine may select/replace context for THIS # call only — request-only, fail-open, and independent of should_compress(). _sel_incoming = ( - messages[current_turn_user_idx] - if 0 <= current_turn_user_idx < len(messages) - else None + messages[current_turn_user_idx] if 0 <= current_turn_user_idx < len(messages) else None ) api_messages = _apply_context_engine_selection( - agent, - api_messages, - messages, - _sel_incoming, - logger=request_logger, + agent, api_messages, messages, _sel_incoming, logger=request_logger ) # Runs unconditionally (not gated on context_compressor) so orphaned tool @@ -178,8 +156,7 @@ def assemble_api_request( # Drop thinking-only assistant turns + merge adjacent users, API copy only: # Anthropic-style backends 400 on a trailing `thinking` block; history keeps it. api_messages = agent._drop_thinking_only_and_merge_users( - api_messages, - drop_codex_reasoning_items=agent.api_mode != "codex_responses", + api_messages, drop_codex_reasoning_items=agent.api_mode != "codex_responses" ) # Normalize whitespace and tool-call JSON for bit-perfect prefixes across turns @@ -213,15 +190,11 @@ def assemble_api_request( # Clamp per-destination: a configured 1h regresses to 5m on # Qwen/Alibaba routes, whose context cache is 5m-only (#84733). cache_ttl=effective_cache_ttl( - agent._cache_ttl, - provider=agent.provider, - model=agent.model, + agent._cache_ttl, provider=agent.provider, model=agent.model ), native_anthropic=agent._use_native_cache_layout, static_system_prefix=( - _static_system_prefix - if isinstance(_static_system_prefix, str) - else None + _static_system_prefix if isinstance(_static_system_prefix, str) else None ), direct_native_tool_cache=agent._direct_native_anthropic_tool_cache_capability(), # LiteLLM-style envelope routes forward part-level markers into diff --git a/agent/turn_response_check.py b/agent/turn_response_check.py index a14e636763..05499b2030 100644 --- a/agent/turn_response_check.py +++ b/agent/turn_response_check.py @@ -43,32 +43,13 @@ class ResponseCheckVerdict: def check_api_response( - agent: Any, - *, - response: Any, - _retry: Any, - thinking_spinner: Any, - messages: Any, - api_messages: Any, - api_kwargs: Any, - active_system_prompt: Any, - conversation_history: Any, - finish_reason: Any, - retry_count: Any, - max_retries: Any, - compression_attempts: Any, - max_compression_attempts: Any, - length_continue_retries: Any, - truncated_response_parts: Any, - truncated_tool_call_retries: Any, - current_turn_user_idx: Any, - api_call_count: Any, - api_request_id: Any, - api_start_time: Any, - effective_task_id: Any, - turn_id: Any, - _preflight_compression_blocked: Any, - _last_preflight_pressure: Any, + agent: Any, *, response: Any, _retry: Any, thinking_spinner: Any, messages: Any, + api_messages: Any, api_kwargs: Any, active_system_prompt: Any, conversation_history: Any, + finish_reason: Any, retry_count: Any, max_retries: Any, compression_attempts: Any, + max_compression_attempts: Any, length_continue_retries: Any, truncated_response_parts: Any, + truncated_tool_call_retries: Any, current_turn_user_idx: Any, api_call_count: Any, + api_request_id: Any, api_start_time: Any, effective_task_id: Any, turn_id: Any, + _preflight_compression_blocked: Any, _last_preflight_pressure: Any, ) -> ResponseCheckVerdict: """Verify ``response`` in the original order. The retry buffer is NOT cleared on success (bytes back != usable content); ``_preflight_compression_blocked``/``_last_preflight_pressure`` @@ -80,19 +61,14 @@ def check_api_response( def _verdict(action: str, result: Optional[Dict[str, Any]] = None) -> ResponseCheckVerdict: return ResponseCheckVerdict( - action=action, - thinking_spinner=thinking_spinner, - messages=messages, - active_system_prompt=active_system_prompt, - finish_reason=finish_reason, - retry_count=retry_count, - compression_attempts=compression_attempts, + action=action, thinking_spinner=thinking_spinner, messages=messages, + active_system_prompt=active_system_prompt, finish_reason=finish_reason, + retry_count=retry_count, compression_attempts=compression_attempts, length_continue_retries=length_continue_retries, truncated_response_parts=truncated_response_parts, truncated_tool_call_retries=truncated_tool_call_retries, _preflight_compression_blocked=_preflight_compression_blocked, - _last_preflight_pressure=_last_preflight_pressure, - api_duration=api_duration, + _last_preflight_pressure=_last_preflight_pressure, api_duration=api_duration, result=result, ) @@ -119,25 +95,14 @@ def check_api_response( if response_invalid: _iv = retry_invalid_response( - agent, - response=response, - error_details=error_details, - _retry=_retry, - thinking_spinner=thinking_spinner, - messages=messages, - api_messages=api_messages, - api_kwargs=api_kwargs, - active_system_prompt=active_system_prompt, - conversation_history=conversation_history, - retry_count=retry_count, - max_retries=max_retries, - compression_attempts=compression_attempts, - api_call_count=api_call_count, - api_request_id=api_request_id, - api_start_time=api_start_time, - api_duration=api_duration, - effective_task_id=effective_task_id, - turn_id=turn_id, + agent, response=response, error_details=error_details, _retry=_retry, + thinking_spinner=thinking_spinner, messages=messages, api_messages=api_messages, + api_kwargs=api_kwargs, active_system_prompt=active_system_prompt, + conversation_history=conversation_history, retry_count=retry_count, + max_retries=max_retries, compression_attempts=compression_attempts, + api_call_count=api_call_count, api_request_id=api_request_id, + api_start_time=api_start_time, api_duration=api_duration, + effective_task_id=effective_task_id, turn_id=turn_id, ) thinking_spinner = _iv.thinking_spinner active_system_prompt = _iv.active_system_prompt @@ -184,9 +149,7 @@ def check_api_response( finish_reason = _finish_result.finish_reason assistant_message = _finish_result if agent._should_treat_stop_as_truncated( - finish_reason, - assistant_message, - messages, + finish_reason, assistant_message, messages ): agent._vprint( f"{agent.log_prefix}⚠️ Treating suspicious Ollama/GLM stop response as truncated", @@ -199,21 +162,11 @@ def check_api_response( # are deterministic: one fallback try, else return the refusal. if finish_reason == "content_filter": _rv = handle_content_policy_refusal( - agent, - response, - _retry, - thinking_spinner=thinking_spinner, - messages=messages, - api_messages=api_messages, - api_kwargs=api_kwargs, - active_system_prompt=active_system_prompt, - conversation_history=conversation_history, - api_call_count=api_call_count, - effective_task_id=effective_task_id, - turn_id=turn_id, - api_request_id=api_request_id, - api_start_time=api_start_time, - retry_count=retry_count, + agent, response, _retry, thinking_spinner=thinking_spinner, messages=messages, + api_messages=api_messages, api_kwargs=api_kwargs, + active_system_prompt=active_system_prompt, conversation_history=conversation_history, + api_call_count=api_call_count, effective_task_id=effective_task_id, turn_id=turn_id, + api_request_id=api_request_id, api_start_time=api_start_time, retry_count=retry_count, max_retries=max_retries, ) thinking_spinner = None @@ -226,20 +179,13 @@ def check_api_response( if finish_reason == "length": _tv = recover_from_truncation( - agent, - response, - finish_reason, - _retry, - messages=messages, - conversation_history=conversation_history, - api_kwargs=api_kwargs, - api_call_count=api_call_count, - effective_task_id=effective_task_id, + agent, response, finish_reason, _retry, messages=messages, + conversation_history=conversation_history, api_kwargs=api_kwargs, + api_call_count=api_call_count, effective_task_id=effective_task_id, current_turn_user_idx=current_turn_user_idx, length_continue_retries=length_continue_retries, truncated_response_parts=truncated_response_parts, - truncated_tool_call_retries=truncated_tool_call_retries, - retry_count=retry_count, + truncated_tool_call_retries=truncated_tool_call_retries, retry_count=retry_count, compression_attempts=compression_attempts, ) messages = _tv.messages @@ -258,12 +204,8 @@ def check_api_response( # Fold provider usage into compressor / anchors / session counters / state.db # (agent/turn_usage.py). A rearmed budget also clears the preflight-block latch. _usage_outcome = record_response_usage( - agent, - response, - messages=messages, - api_call_count=api_call_count, - api_duration=api_duration, - compression_attempts=compression_attempts, + agent, response, messages=messages, api_call_count=api_call_count, + api_duration=api_duration, compression_attempts=compression_attempts, max_compression_attempts=max_compression_attempts, ) compression_attempts = _usage_outcome.compression_attempts @@ -284,8 +226,7 @@ def check_api_response( from agent import relay_llm relay_llm.complete_logical_call( - api_request_id, - outcome="success", + api_request_id, outcome="success" ) agent._touch_activity(f"API call #{api_call_count} completed") return _verdict("break") # Success, exit retry loop @@ -308,61 +249,34 @@ class InvalidResponseVerdict: def retry_invalid_response( - agent: Any, - *, - response: Any, - error_details: Any, - _retry: Any, - thinking_spinner: Any, - messages: Any, - api_messages: Any, - api_kwargs: Any, - active_system_prompt: Any, - conversation_history: Any, - retry_count: Any, - max_retries: Any, - compression_attempts: Any, - api_call_count: Any, - api_request_id: Any, - api_start_time: Any, - api_duration: Any, - effective_task_id: Any, - turn_id: Any, + agent: Any, *, response: Any, error_details: Any, _retry: Any, thinking_spinner: Any, + messages: Any, api_messages: Any, api_kwargs: Any, active_system_prompt: Any, + conversation_history: Any, retry_count: Any, max_retries: Any, compression_attempts: Any, + api_call_count: Any, api_request_id: Any, api_start_time: Any, api_duration: Any, + effective_task_id: Any, turn_id: Any, ) -> InvalidResponseVerdict: """Malformed/empty provider response: fire the error hook, stop the spinner, eager fallback (empty responses often mean rate limiting), terminal result at max retries, else jittered backoff that preserves a pending redirect.""" from agent.conversation_loop import ( - _arm_fallback_restart, - describe_invalid_response, - interruptible_backoff_sleep, + _arm_fallback_restart, describe_invalid_response, interruptible_backoff_sleep, jittered_backoff, ) def _verdict(action: str, result: Optional[Dict[str, Any]] = None) -> InvalidResponseVerdict: return InvalidResponseVerdict( - action=action, - thinking_spinner=thinking_spinner, - active_system_prompt=active_system_prompt, - retry_count=retry_count, - compression_attempts=compression_attempts, - result=result, + action=action, thinking_spinner=thinking_spinner, + active_system_prompt=active_system_prompt, retry_count=retry_count, + compression_attempts=compression_attempts, result=result, ) agent._invoke_api_request_error_hook( - task_id=effective_task_id, - turn_id=turn_id, - api_request_id=api_request_id, - api_call_count=api_call_count, - api_start_time=api_start_time, - api_kwargs=api_kwargs, + task_id=effective_task_id, turn_id=turn_id, api_request_id=api_request_id, + api_call_count=api_call_count, api_start_time=api_start_time, api_kwargs=api_kwargs, error_type="InvalidAPIResponse", error_message=", ".join(error_details) or "Invalid API response", status_code=getattr(getattr(response, "error", None), "code", None), - retry_count=retry_count, - max_retries=max_retries, - retryable=True, - reason="invalid_response", + retry_count=retry_count, max_retries=max_retries, retryable=True, reason="invalid_response", ) # Stop spinner silently — retry status is now buffered # and only surfaced if every retry+fallback exhausts. @@ -431,9 +345,7 @@ def retry_invalid_response( # pending correction (restart_with_redirected_messages) instead of # destroying it with clear_interrupt(). _interrupted = interruptible_backoff_sleep( - agent, wait_time, _retry, - messages=messages, - conversation_history=conversation_history, + agent, wait_time, _retry, messages=messages, conversation_history=conversation_history, api_call_count=api_call_count, abort_message="Interrupt detected during retry wait, aborting.", interrupt_text=f"Operation interrupted during retry ({_failure_hint}, attempt {retry_count}/{max_retries}).", diff --git a/agent/turn_response_intake.py b/agent/turn_response_intake.py index 3bb092e8c1..8ea50d6fce 100644 --- a/agent/turn_response_intake.py +++ b/agent/turn_response_intake.py @@ -33,18 +33,9 @@ class ResponseIntakeVerdict: def normalize_model_response( - agent: Any, - *, - response: Any, - messages: Any, - api_messages: Any, - conversation_history: Any, - api_call_count: Any, - api_duration: Any, - api_start_time: Any, - api_request_id: Any, - effective_task_id: Any, - turn_id: Any, + agent: Any, *, response: Any, messages: Any, api_messages: Any, conversation_history: Any, + api_call_count: Any, api_duration: Any, api_start_time: Any, api_request_id: Any, + effective_task_id: Any, turn_id: Any, ) -> ResponseIntakeVerdict: """Normalize ``response`` into ``assistant_message`` (str content, never dict/list) and run the post-response hooks and continuation guards, in the original order.""" @@ -56,9 +47,7 @@ def normalize_model_response( def _verdict(action: str, result: Optional[Dict[str, Any]] = None) -> ResponseIntakeVerdict: return ResponseIntakeVerdict( - action=action, - assistant_message=assistant_message, - finish_reason=finish_reason, + action=action, assistant_message=assistant_message, finish_reason=finish_reason, result=result, ) @@ -97,8 +86,7 @@ def normalize_model_response( try: from hermes_cli.lifecycle import ( - has_hook, - invoke_hook as _invoke_hook, + has_hook, invoke_hook as _invoke_hook ) if has_hook("post_api_request"): _assistant_tool_calls = ( @@ -131,9 +119,7 @@ def normalize_model_response( message_count=len(api_messages), response_model=getattr(response, "model", None), response=agent._api_response_payload_for_hook( - response, - assistant_message, - finish_reason=finish_reason, + response, assistant_message, finish_reason=finish_reason ), usage=agent._usage_summary_for_api_request_hook(response), assistant_message=assistant_message, @@ -208,12 +194,8 @@ def normalize_model_response( if agent.api_mode == "codex_responses" and finish_reason == "incomplete": _codex_result = continue_codex_incomplete( - agent, - assistant_message, - finish_reason, - messages=messages, - conversation_history=conversation_history, - api_call_count=api_call_count, + agent, assistant_message, finish_reason, messages=messages, + conversation_history=conversation_history, api_call_count=api_call_count, ) if _codex_result is not None: return _verdict("return", _codex_result) diff --git a/agent/turn_stop_gates.py b/agent/turn_stop_gates.py index 7260566d75..5a44030174 100644 --- a/agent/turn_stop_gates.py +++ b/agent/turn_stop_gates.py @@ -33,13 +33,8 @@ class StopGateVerdict: def apply_stop_gates( - agent: Any, - final_msg: Dict[str, Any], - *, - final_response: Any, - messages: List[Dict[str, Any]], - conversation_history: Any, - pending_verification_response: Any, + agent: Any, final_msg: Dict[str, Any], *, final_response: Any, messages: List[Dict[str, Any]], + conversation_history: Any, pending_verification_response: Any, pending_verification_response_previewed: Any, ) -> StopGateVerdict: """Run verify-on-stop → pre_verify hook → kanban stop guard, in that order. Nudges @@ -51,16 +46,14 @@ def apply_stop_gates( def _verdict(continue_turn: bool) -> StopGateVerdict: return StopGateVerdict( - continue_turn=continue_turn, - final_response=None if continue_turn else final_response, + continue_turn=continue_turn, final_response=None if continue_turn else final_response, pending_verification_response=_pending_verification_response, pending_verification_response_previewed=_pending_verification_response_previewed, ) try: from agent.verification_stop import ( - build_verify_on_stop_nudge, - verify_on_stop_enabled, + build_verify_on_stop_nudge, verify_on_stop_enabled ) if verify_on_stop_enabled(): @@ -89,9 +82,7 @@ def apply_stop_gates( except Exception: logger.debug("verify-on-stop interim flush failed", exc_info=True) append_message(messages, { - "role": "user", - "content": _verify_nudge, - "_verification_stop_synthetic": True, + "role": "user", "content": _verify_nudge, "_verification_stop_synthetic": True }) agent._session_messages = messages # Internal nudge: stay silent on the terminal, debug-log only. @@ -126,11 +117,8 @@ def apply_stop_gates( _verify_nudge2 = get_pre_verify_continue_message( session_id=getattr(agent, "session_id", None) or "", platform=getattr(agent, "platform", "") or "", - model=getattr(agent, "model", "") or "", - coding=coding, - attempt=_attempt, - final_response=final_response, - changed_paths=_edited, + model=getattr(agent, "model", "") or "", coding=coding, attempt=_attempt, + final_response=final_response, changed_paths=_edited, ) except Exception: logger.debug("pre_verify hook check failed", exc_info=True) @@ -148,9 +136,7 @@ def apply_stop_gates( except Exception: logger.debug("pre_verify interim flush failed", exc_info=True) append_message(messages, { - "role": "user", - "content": _verify_nudge2, - "_pre_verify_synthetic": True, + "role": "user", "content": _verify_nudge2, "_pre_verify_synthetic": True }) agent._session_messages = messages logger.debug("pre_verify nudge issued (attempt %d)", @@ -168,8 +154,7 @@ def apply_stop_gates( from agent.kanban_stop import build_kanban_stop_nudge _kanban_nudge = build_kanban_stop_nudge( - messages=messages, - attempts=getattr(agent, "_kanban_stop_nudges", 0), + messages=messages, attempts=getattr(agent, "_kanban_stop_nudges", 0) ) except Exception: logger.debug("kanban stop-loop check failed", exc_info=True) @@ -183,9 +168,7 @@ def apply_stop_gates( final_msg["_kanban_stop_synthetic"] = True append_message(messages, final_msg) append_message(messages, { - "role": "user", - "content": _kanban_nudge, - "_kanban_stop_synthetic": True, + "role": "user", "content": _kanban_nudge, "_kanban_stop_synthetic": True }) agent._session_messages = messages logger.info( diff --git a/agent/turn_tool_round.py b/agent/turn_tool_round.py index 3eb4843e12..545dd03089 100644 --- a/agent/turn_tool_round.py +++ b/agent/turn_tool_round.py @@ -39,22 +39,10 @@ class ToolRoundVerdict: def run_tool_round( - agent: Any, - *, - assistant_message: Any, - finish_reason: Any, - messages: Any, - conversation_history: Any, - api_call_count: Any, - effective_task_id: Any, - user_message: Any, - system_message: Any, - active_system_prompt: Any, - compression_attempts: Any, - max_compression_attempts: Any, - final_response: Any, - failed: Any, - _turn_exit_reason: Any, + agent: Any, *, assistant_message: Any, finish_reason: Any, messages: Any, + conversation_history: Any, api_call_count: Any, effective_task_id: Any, user_message: Any, + system_message: Any, active_system_prompt: Any, compression_attempts: Any, + max_compression_attempts: Any, final_response: Any, failed: Any, _turn_exit_reason: Any, truncated_tool_call_retries: Any, ) -> ToolRoundVerdict: """Execute one tool round in the exact original order. Persist-before-execute is a @@ -67,16 +55,10 @@ def run_tool_round( def _verdict(action: str, result: Optional[Dict[str, Any]] = None) -> ToolRoundVerdict: return ToolRoundVerdict( - action=action, - messages=messages, - conversation_history=conversation_history, - active_system_prompt=active_system_prompt, - compression_attempts=compression_attempts, - final_response=final_response, - failed=failed, - _turn_exit_reason=_turn_exit_reason, - truncated_tool_call_retries=truncated_tool_call_retries, - result=result, + action=action, messages=messages, conversation_history=conversation_history, + active_system_prompt=active_system_prompt, compression_attempts=compression_attempts, + final_response=final_response, failed=failed, _turn_exit_reason=_turn_exit_reason, + truncated_tool_call_retries=truncated_tool_call_retries, result=result, ) if not agent.quiet_mode: @@ -89,12 +71,8 @@ def run_tool_round( logging.debug("Tool call: %s with args: %s...", tc.function.name, args_preview) _tvv = validate_tool_calls( - agent, - assistant_message, - finish_reason, - messages=messages, - conversation_history=conversation_history, - api_call_count=api_call_count, + agent, assistant_message, finish_reason, messages=messages, + conversation_history=conversation_history, api_call_count=api_call_count, effective_task_id=effective_task_id, ) _mixed_invalid_batch = _tvv.mixed_invalid_batch @@ -122,10 +100,7 @@ def run_tool_round( ] _st = stage_tool_call_message( - agent, - assistant_message=assistant_message, - finish_reason=finish_reason, - messages=messages, + agent, assistant_message=assistant_message, finish_reason=finish_reason, messages=messages ) assistant_msg = _st.assistant_msg duplicate_previous_interim = _st.duplicate_previous_interim @@ -144,8 +119,7 @@ def run_tool_round( ), }) assistant_message.tool_calls = [ - tc for tc in assistant_message.tool_calls - if tc.function.name in agent.valid_tool_names + tc for tc in assistant_message.tool_calls if tc.function.name in agent.valid_tool_names ] _tool_turn_persisted = None @@ -237,17 +211,11 @@ def run_tool_round( agent.iteration_budget.refund() _ptc = compress_after_tool_results( - agent, - messages=messages, - system_message=system_message, - user_message=user_message, - active_system_prompt=active_system_prompt, - conversation_history=conversation_history, + agent, messages=messages, system_message=system_message, user_message=user_message, + active_system_prompt=active_system_prompt, conversation_history=conversation_history, compression_attempts=compression_attempts, - max_compression_attempts=max_compression_attempts, - effective_task_id=effective_task_id, - final_response=final_response, - turn_exit_reason=_turn_exit_reason, + max_compression_attempts=max_compression_attempts, effective_task_id=effective_task_id, + final_response=final_response, turn_exit_reason=_turn_exit_reason, ) messages = _ptc.messages active_system_prompt = _ptc.active_system_prompt @@ -281,11 +249,7 @@ class StagedToolCallMessage: def stage_tool_call_message( - agent: Any, - *, - assistant_message: Any, - finish_reason: Any, - messages: Any, + agent: Any, *, assistant_message: Any, finish_reason: Any, messages: Any ) -> StagedToolCallMessage: """Build the assistant tool-call row and update the per-turn fallback/mute state: drop a bare bracketed marker beside a call (#78148), classify housekeeping-only rounds, keep visible @@ -311,12 +275,10 @@ def stage_tool_call_message( # protocol scaffolding; persisting it lets the post-tool fallback replay # it forever (#78148). if ( - assistant_message.tool_calls - and _STALE_MARKER_RE.fullmatch(turn_content.strip()) + assistant_message.tool_calls and _STALE_MARKER_RE.fullmatch(turn_content.strip()) ): logger.warning( - "Discarding bare tool-call marker from assistant content: %s", - turn_content, + "Discarding bare tool-call marker from assistant content: %s", turn_content ) turn_content = "" assistant_msg["content"] = "" @@ -327,8 +289,7 @@ def stage_tool_call_message( "memory", "todo_list", "skill_manage", "session_search", }) _all_housekeeping = all( - tc.function.name in _HOUSEKEEPING_TOOLS - for tc in assistant_message.tool_calls + tc.function.name in _HOUSEKEEPING_TOOLS for tc in assistant_message.tool_calls ) # Substantive tools clear any older fallback so a two-turn-old @@ -359,9 +320,7 @@ def stage_tool_call_message( # (tool-call path — same rationale as the final-response path). _had_prefill = False while ( - messages - and isinstance(messages[-1], dict) - and messages[-1].get("_thinking_prefill") + messages and isinstance(messages[-1], dict) and messages[-1].get("_thinking_prefill") ): messages.pop() _had_prefill = True diff --git a/agent/turn_tool_validation.py b/agent/turn_tool_validation.py index 4d4a5aeaac..e71b1302aa 100644 --- a/agent/turn_tool_validation.py +++ b/agent/turn_tool_validation.py @@ -37,14 +37,8 @@ class ToolValidationVerdict: def validate_tool_calls( - agent: Any, - assistant_message: Any, - finish_reason: str, - *, - messages: List[Dict[str, Any]], - conversation_history: Any, - api_call_count: int, - effective_task_id: Any, + agent: Any, assistant_message: Any, finish_reason: str, *, messages: List[Dict[str, Any]], + conversation_history: Any, api_call_count: int, effective_task_id: Any, ) -> ToolValidationVerdict: """Validate ``assistant_message.tool_calls`` in place (ids uniquified, names repaired, dict/empty args normalized to JSON strings). Strikes for invalid names @@ -79,16 +73,14 @@ def validate_tool_calls( # ones; voiding the turn discards real work. Strikes advance only when a # turn has NO valid call, so a degenerate model still halts at 3. _mixed_invalid_batch = bool(invalid_tool_calls) and any( - tc.function.name in agent.valid_tool_names - for tc in assistant_message.tool_calls + tc.function.name in agent.valid_tool_names for tc in assistant_message.tool_calls ) if _mixed_invalid_batch: agent._invalid_tool_retries = 0 invalid_name = invalid_tool_calls[0] invalid_preview = invalid_name[:80] + "..." if len(invalid_name) > 80 else invalid_name _n_valid = sum( - 1 for tc in assistant_message.tool_calls - if tc.function.name in agent.valid_tool_names + 1 for tc in assistant_message.tool_calls if tc.function.name in agent.valid_tool_names ) agent._buffer_vprint( f"⚠️ Unknown tool '{invalid_preview}' in batch — erroring that call, " @@ -163,8 +155,7 @@ def validate_tool_calls( json.loads(args) except json.JSONDecodeError as e: if ( - _mixed_invalid_batch - and tc.function.name not in agent.valid_tool_names + _mixed_invalid_batch and tc.function.name not in agent.valid_tool_names ): # This call never executes (invalid-name error result # below); don't let its broken args trigger the whole-turn diff --git a/agent/turn_truncation.py b/agent/turn_truncation.py index 0f6d58cea0..c7d5e063b0 100644 --- a/agent/turn_truncation.py +++ b/agent/turn_truncation.py @@ -46,21 +46,10 @@ class TruncationVerdict: def recover_from_truncation( - agent: Any, - response: Any, - finish_reason: str, - _retry: TurnRetryState, - *, - messages: List[Dict[str, Any]], - conversation_history: Any, - api_kwargs: Any, - api_call_count: int, - effective_task_id: Any, - current_turn_user_idx: Any, - length_continue_retries: int, - truncated_response_parts: List[str], - truncated_tool_call_retries: int, - retry_count: int, + agent: Any, response: Any, finish_reason: str, _retry: TurnRetryState, *, + messages: List[Dict[str, Any]], conversation_history: Any, api_kwargs: Any, api_call_count: int, + effective_task_id: Any, current_turn_user_idx: Any, length_continue_retries: int, + truncated_response_parts: List[str], truncated_tool_call_retries: int, retry_count: int, compression_attempts: int, ) -> TruncationVerdict: """Recover from a truncated response. Order is load-bearing: thinking exhaustion and @@ -73,13 +62,10 @@ def recover_from_truncation( def _verdict(action: str, result: Optional[Dict[str, Any]] = None) -> TruncationVerdict: return TruncationVerdict( - action=action, - result=result, - messages=messages, + action=action, result=result, messages=messages, length_continue_retries=length_continue_retries, truncated_response_parts=truncated_response_parts, - truncated_tool_call_retries=truncated_tool_call_retries, - retry_count=retry_count, + truncated_tool_call_retries=truncated_tool_call_retries, retry_count=retry_count, compression_attempts=compression_attempts, ) @@ -218,8 +204,7 @@ def recover_from_truncation( response, "_content_filter_terminated", False ) if ( - _cf_terminated - and agent._fallback_index < len(agent._fallback_chain) + _cf_terminated and agent._fallback_index < len(agent._fallback_chain) ): agent._vprint( f"{agent.log_prefix}🛡️ Content filter terminated " @@ -262,8 +247,7 @@ def recover_from_truncation( # history. Append only the nudge. _interim_content = getattr(assistant_message, "content", None) _is_empty_partial_stub = ( - getattr(response, "id", "") == PARTIAL_STREAM_STUB_ID - and not _interim_content + getattr(response, "id", "") == PARTIAL_STREAM_STUB_ID and not _interim_content ) if not _interim_content and not _is_empty_partial_stub: # Thinking-only truncation: continuing with thinking ON @@ -308,9 +292,7 @@ def recover_from_truncation( _is_partial_stream_stub, _dropped_tools ) continue_msg = { - "role": "user", - "content": _continue_content, - "_length_continuation_nudge": True, + "role": "user", "content": _continue_content, "_length_continuation_nudge": True } append_message(messages, continue_msg) agent._session_messages = messages @@ -367,9 +349,7 @@ def recover_from_truncation( ] if partial_response: append_message(messages, { - "role": "assistant", - "content": partial_response, - "finish_reason": "length", + "role": "assistant", "content": partial_response, "finish_reason": "length" }) agent._session_messages = messages agent._cleanup_task_resources(effective_task_id) @@ -480,13 +460,8 @@ def recover_from_truncation( def continue_codex_incomplete( - agent: Any, - assistant_message: Any, - finish_reason: str, - *, - messages: List[Dict[str, Any]], - conversation_history: Any, - api_call_count: int, + agent: Any, assistant_message: Any, finish_reason: str, *, messages: List[Dict[str, Any]], + conversation_history: Any, api_call_count: int, ) -> Optional[Dict[str, Any]]: """Codex Responses ``status=incomplete`` continuation (max 3 per turn). @@ -517,9 +492,7 @@ def continue_codex_incomplete( # provider state drifts per continuation and would defeat dedup # (#52711). last_interim_visible = ( - agent._interim_assistant_visible_text(last_msg) - if isinstance(last_msg, dict) - else "" + agent._interim_assistant_visible_text(last_msg) if isinstance(last_msg, dict) else "" ) current_interim_visible = agent._interim_assistant_visible_text(interim_msg) if last_interim_visible or current_interim_visible: @@ -570,9 +543,7 @@ def continue_codex_incomplete( # bare retry is byte-identical and fails identically; append a # user-role nudge so the retry differs and asks for the answer. interim_replayable = ( - interim_has_content - or interim_has_codex_reasoning - or interim_has_codex_message_items + interim_has_content or interim_has_codex_reasoning or interim_has_codex_message_items ) # Replayable ≠ different: an interim holding only a ``compaction`` # checkpoint in ``codex_reasoning_items`` is replayable yet re-sends @@ -588,13 +559,11 @@ def continue_codex_incomplete( # assistant message; after a too-empty interim it would create # user→user / tool→user. _last_is_assistant = ( - isinstance(_last_msg, dict) - and _last_msg.get("role") == "assistant" + isinstance(_last_msg, dict) and _last_msg.get("role") == "assistant" ) if not _already_nudged and _last_is_assistant: append_message(messages, { - "role": "user", - "content": _CODEX_INCOMPLETE_NUDGE, + "role": "user", "content": _CODEX_INCOMPLETE_NUDGE }) if not agent.quiet_mode: agent._vprint(f"{agent.log_prefix}↻ Codex response incomplete; continuing turn ({agent._codex_incomplete_retries}/3)") @@ -634,32 +603,17 @@ class RefusalVerdict: def handle_content_policy_refusal( - agent: Any, - response: Any, - _retry: TurnRetryState, - *, - thinking_spinner: Any, - messages: List[Dict[str, Any]], - api_messages: Any, - api_kwargs: Any, - active_system_prompt: Any, - conversation_history: Any, - api_call_count: int, - effective_task_id: Any, - turn_id: Any, - api_request_id: Any, - api_start_time: float, - retry_count: int, - max_retries: int, + agent: Any, response: Any, _retry: TurnRetryState, *, thinking_spinner: Any, + messages: List[Dict[str, Any]], api_messages: Any, api_kwargs: Any, active_system_prompt: Any, + conversation_history: Any, api_call_count: int, effective_task_id: Any, turn_id: Any, + api_request_id: Any, api_start_time: float, retry_count: int, max_retries: int, ) -> RefusalVerdict: """HTTP-200 refusal (``finish_reason`` ``content_filter`` / ``guardrail_intervened``). Deterministic for the unchanged prompt — never retried: one configured-fallback try, else surface the refusal (explanation may live only in the reasoning channel). The caller stops its spinner reference; this stops the spinner object.""" from agent.conversation_loop import ( - _CONTENT_POLICY_RECOVERY_HINT, - _arm_fallback_restart, - _content_policy_blocked_result, + _CONTENT_POLICY_RECOVERY_HINT, _arm_fallback_restart, _content_policy_blocked_result ) def _verdict(action: str, result: Optional[Dict[str, Any]] = None) -> RefusalVerdict: @@ -679,18 +633,11 @@ def handle_content_policy_refusal( _refusal_text = (agent._extract_reasoning(_refusal_result) or "").strip() agent._invoke_api_request_error_hook( - task_id=effective_task_id, - turn_id=turn_id, - api_request_id=api_request_id, - api_call_count=api_call_count, - api_start_time=api_start_time, - api_kwargs=api_kwargs, + task_id=effective_task_id, turn_id=turn_id, api_request_id=api_request_id, + api_call_count=api_call_count, api_start_time=api_start_time, api_kwargs=api_kwargs, error_type="ContentPolicyBlocked", error_message=_refusal_text or "model declined to respond (content_filter)", - status_code=None, - retry_count=retry_count, - max_retries=max_retries, - retryable=False, + status_code=None, retry_count=retry_count, max_retries=max_retries, retryable=False, reason=FailoverReason.content_policy_blocked.value, ) @@ -712,9 +659,7 @@ def handle_content_policy_refusal( agent._flush_status_buffer() _refusal_log = ( - _refusal_text[:500] + "..." - if len(_refusal_text) > 500 - else _refusal_text + _refusal_text[:500] + "..." if len(_refusal_text) > 500 else _refusal_text ) logger.warning( "%sModel declined to respond (finish_reason=content_filter). " @@ -741,8 +686,6 @@ def handle_content_policy_refusal( agent._cleanup_task_resources(effective_task_id) agent._persist_session(messages, conversation_history) return _verdict("return", _content_policy_blocked_result( - messages, - api_call_count, - final_response=_refusal_response, + messages, api_call_count, final_response=_refusal_response, error_detail=_refusal_text or "model declined (content_filter)", )) diff --git a/agent/turn_usage.py b/agent/turn_usage.py index f2970e4c41..13944a91be 100644 --- a/agent/turn_usage.py +++ b/agent/turn_usage.py @@ -47,14 +47,8 @@ def _loop_mod(): def record_response_usage( - agent: Any, - response: Any, - *, - messages: List[Dict[str, Any]], - api_call_count: int, - api_duration: float, - compression_attempts: int, - max_compression_attempts: int, + agent: Any, response: Any, *, messages: List[Dict[str, Any]], api_call_count: int, + api_duration: float, compression_attempts: int, max_compression_attempts: int, ) -> ResponseUsageOutcome: """Fold ``response.usage`` into compressor, anchors, session counters, state.db and the API-call log line (see module docstring). No-usage responses only @@ -63,9 +57,7 @@ def record_response_usage( # Track actual token usage from response for context management if hasattr(response, 'usage') and response.usage: canonical_usage = normalize_usage( - response.usage, - provider=agent.provider, - api_mode=agent.api_mode, + response.usage, provider=agent.provider, api_mode=agent.api_mode ) # Aggregator-only usage kept for pricing: advisor tokens are priced # at each advisor's OWN model rate and added as dollars below. @@ -90,8 +82,7 @@ def record_response_usage( getattr(agent, "_current_streamed_assistant_text", "") or "" ) _moa_client.consume_and_save_trace( - agent.session_id, - aggregator_output_fallback=_agg_streamed_text or None, + agent.session_id, aggregator_output_fallback=_agg_streamed_text or None ) except Exception as _moa_trace_exc: # pragma: no cover - defensive logger.debug("MoA trace flush failed: %s", _moa_trace_exc) @@ -114,20 +105,14 @@ def record_response_usage( # it: only the real prompt count right after a compaction rearms the # budget. _completed_compaction_pending = bool( - getattr( - agent.context_compressor, - "_verify_compaction_cleared_threshold", - False, - ) + getattr( agent.context_compressor, "_verify_compaction_cleared_threshold", False ) ) agent.context_compressor.update_from_response(usage_dict) # Usage-anchored accounting: snapshot exact provider usage against # the durable transcript; main-loop ONLY. MoA uses pre-fold # aggregator usage. _new_anchor = capture_usage_anchor( - aggregator_usage.prompt_tokens, - aggregator_usage.output_tokens, - messages, + aggregator_usage.prompt_tokens, aggregator_usage.output_tokens, messages ) if _new_anchor is not None: agent._usage_anchor = _new_anchor @@ -137,14 +122,11 @@ def record_response_usage( if api_call_count == 1: agent._turn_base_usage_anchor = _new_anchor _compression_threshold = int( - getattr(agent.context_compressor, "threshold_tokens", 0) - or 0 + getattr(agent.context_compressor, "threshold_tokens", 0) or 0 ) if _loop_mod()._should_rearm_compression_budget( - compression_attempts, - completed_compaction_pending=_completed_compaction_pending, - prompt_tokens=prompt_tokens, - threshold_tokens=_compression_threshold, + compression_attempts, completed_compaction_pending=_completed_compaction_pending, + prompt_tokens=prompt_tokens, threshold_tokens=_compression_threshold, ): logger.info( "Compression budget rearmed after provider-confirmed " @@ -164,9 +146,7 @@ def record_response_usage( # update_from_response); keep the latest call's — last request. agent._last_turn_usage = dict(usage_dict) elif getattr( - agent.context_compressor, - "awaiting_real_usage_after_compression", - False, + agent.context_compressor, "awaiting_real_usage_after_compression", False ): # No usage -> cannot adjudicate the prior compaction; consume the # pending verdict so later readings aren't charged to it and @@ -226,11 +206,8 @@ def record_response_usage( _agg_cost_provider = _agg_slot.get("provider") or agent.provider _agg_cost_base_url = _agg_slot.get("base_url") or agent.base_url cost_result = estimate_usage_cost( - _agg_cost_model, - aggregator_usage, - provider=_agg_cost_provider, - base_url=_agg_cost_base_url, - api_key=getattr(agent, "api_key", ""), + _agg_cost_model, aggregator_usage, provider=_agg_cost_provider, + base_url=_agg_cost_base_url, api_key=getattr(agent, "api_key", ""), ) if cost_result.amount_usd is not None: agent.session_estimated_cost_usd += float(cost_result.amount_usd) @@ -307,6 +284,5 @@ def record_response_usage( f"({hit_pct:.0f}% hit, {written:,} written)" ) return ResponseUsageOutcome( - compression_attempts=compression_attempts, - rearmed=rearmed, + compression_attempts=compression_attempts, rearmed=rearmed ) diff --git a/agent/usage_pricing.py b/agent/usage_pricing.py index a56b7982a3..7066396ee9 100644 --- a/agent/usage_pricing.py +++ b/agent/usage_pricing.py @@ -83,8 +83,7 @@ class CanonicalUsage: cache_read_tokens=self.cache_read_tokens + other.cache_read_tokens, cache_write_tokens=self.cache_write_tokens + other.cache_write_tokens, reasoning_tokens=self.reasoning_tokens + other.reasoning_tokens, - request_count=self.request_count + other.request_count, - raw_usage=None, + request_count=self.request_count + other.request_count, raw_usage=None, ) @@ -132,25 +131,15 @@ _UTC_NOW = lambda: datetime.now(timezone.utc) def _snap( - inp: str, - out: str, - cache_read: Optional[str] = None, - cache_write: Optional[str] = None, - *, - version: str, - url: Optional[str] = None, - **tiers: Any, + inp: str, out: str, cache_read: Optional[str] = None, cache_write: Optional[str] = None, *, + version: str, url: Optional[str] = None, **tiers: Any, ) -> PricingEntry: """Build an official-docs snapshot entry from per-million USD rate strings.""" return PricingEntry( - input_cost_per_million=Decimal(inp), - output_cost_per_million=Decimal(out), + input_cost_per_million=Decimal(inp), output_cost_per_million=Decimal(out), cache_read_cost_per_million=Decimal(cache_read) if cache_read is not None else None, cache_write_cost_per_million=Decimal(cache_write) if cache_write is not None else None, - source="official_docs_snapshot", - source_url=url, - pricing_version=version, - **tiers, + source="official_docs_snapshot", source_url=url, pricing_version=version, **tiers, ) @@ -355,9 +344,7 @@ def _first_nonzero(obj: Any, *paths: tuple[str, ...]) -> int: def resolve_billing_route( - model_name: str, - provider: Optional[str] = None, - base_url: Optional[str] = None, + model_name: str, provider: Optional[str] = None, base_url: Optional[str] = None ) -> BillingRoute: provider_name = (provider or "").strip().lower() base = (base_url or "").strip().lower() @@ -441,8 +428,7 @@ def _lookup_official_docs_pricing(route: BillingRoute) -> Optional[PricingEntry] # Anthropic dot-notation (opus-4.7) and Bedrock region-prefixed ids need # normalizing before a second lookup. normalize = { - "anthropic": _normalize_anthropic_model_name, - "bedrock": _normalize_bedrock_model_name, + "anthropic": _normalize_anthropic_model_name, "bedrock": _normalize_bedrock_model_name }.get(route.provider) if normalize: normalized = normalize(model) @@ -455,19 +441,14 @@ def _lookup_official_docs_pricing(route: BillingRoute) -> Optional[PricingEntry] def _openrouter_pricing_entry(route: BillingRoute) -> Optional[PricingEntry]: return _pricing_entry_from_metadata( - fetch_model_metadata(), - route.model, + fetch_model_metadata(), route.model, source_url="https://openrouter.ai/docs/api/api-reference/models/get-models", pricing_version="openrouter-models-api", ) def _pricing_entry_from_metadata( - metadata: Dict[str, Dict[str, Any]], - model_id: str, - *, - source_url: str, - pricing_version: str, + metadata: Dict[str, Dict[str, Any]], model_id: str, *, source_url: str, pricing_version: str ) -> Optional[PricingEntry]: if model_id not in metadata: return None @@ -476,9 +457,7 @@ def _pricing_entry_from_metadata( completion = _to_decimal(pricing.get("completion")) request = _to_decimal(pricing.get("request")) cache_read = _to_decimal( - pricing.get("cache_read") - or pricing.get("cached_prompt") - or pricing.get("input_cache_read") + pricing.get("cache_read") or pricing.get("cached_prompt") or pricing.get("input_cache_read") ) cache_write = _to_decimal( pricing.get("cache_write") @@ -495,37 +474,28 @@ def _pricing_entry_from_metadata( input_cost_per_million=_per_million(prompt), output_cost_per_million=_per_million(completion), cache_read_cost_per_million=_per_million(cache_read), - cache_write_cost_per_million=_per_million(cache_write), - request_cost=request, - source="provider_models_api", - source_url=source_url, - pricing_version=pricing_version, + cache_write_cost_per_million=_per_million(cache_write), request_cost=request, + source="provider_models_api", source_url=source_url, pricing_version=pricing_version, fetched_at=_UTC_NOW(), ) def get_pricing_entry( - model_name: str, - provider: Optional[str] = None, - base_url: Optional[str] = None, + model_name: str, provider: Optional[str] = None, base_url: Optional[str] = None, api_key: Optional[str] = None, ) -> Optional[PricingEntry]: route = resolve_billing_route(model_name, provider=provider, base_url=base_url) if route.billing_mode == "subscription_included": return PricingEntry( - input_cost_per_million=_ZERO, - output_cost_per_million=_ZERO, - cache_read_cost_per_million=_ZERO, - cache_write_cost_per_million=_ZERO, - source="none", + input_cost_per_million=_ZERO, output_cost_per_million=_ZERO, + cache_read_cost_per_million=_ZERO, cache_write_cost_per_million=_ZERO, source="none", pricing_version="included-route", ) if route.provider == "openrouter": return _openrouter_pricing_entry(route) if route.base_url: entry = _pricing_entry_from_metadata( - fetch_endpoint_model_metadata(route.base_url, api_key=api_key or ""), - route.model, + fetch_endpoint_model_metadata(route.base_url, api_key=api_key or ""), route.model, source_url=f"{route.base_url.rstrip('/')}/models", pricing_version="openai-compatible-models-api", ) @@ -535,10 +505,7 @@ def get_pricing_entry( def normalize_usage( - response_usage: Any, - *, - provider: Optional[str] = None, - api_mode: Optional[str] = None, + response_usage: Any, *, provider: Optional[str] = None, api_mode: Optional[str] = None ) -> CanonicalUsage: """Normalize raw API response usage into canonical token buckets. @@ -566,8 +533,7 @@ def normalize_usage( # OpenAI's documented GPT-5.6+ field is `cache_write_tokens` (billed at # 1.25x); `cache_creation_tokens` is a fallback for older endpoints. cache_write_tokens = _first_nonzero( - u, - ("input_tokens_details", "cache_write_tokens"), + u, ("input_tokens_details", "cache_write_tokens"), ("input_tokens_details", "cache_creation_tokens"), ) input_tokens = max(0, input_total - cache_read_tokens - cache_write_tokens) @@ -583,18 +549,13 @@ def normalize_usage( # top-level cached_tokens — without these, direct sessions show 0 hits # and bill hits at the full input rate. cache_read_tokens = _first_nonzero( - u, - ("prompt_tokens_details", "cached_tokens"), - ("cache_read_input_tokens",), - ("prompt_cache_hit_tokens",), - ("cached_tokens",), + u, ("prompt_tokens_details", "cached_tokens"), ("cache_read_input_tokens",), + ("prompt_cache_hit_tokens",), ("cached_tokens",), ) cache_write_tokens = _first_nonzero( - u, - ("prompt_tokens_details", "cache_write_tokens"), + u, ("prompt_tokens_details", "cache_write_tokens"), ("prompt_tokens_details", "cache_creation_input_tokens"), - ("cache_creation_input_tokens",), - ("cache_write_tokens",), + ("cache_creation_input_tokens",), ("cache_write_tokens",), ) input_tokens = max(0, prompt_total - cache_read_tokens - cache_write_tokens) @@ -602,8 +563,7 @@ def normalize_usage( # (OpenAI, OpenRouter, DeepSeek, ...): completion_tokens_details.reasoning_tokens. # Hidden thinking dominates output spend on reasoning models, so read both. reasoning_tokens = _first_nonzero( - u, - ("output_tokens_details", "reasoning_tokens"), + u, ("output_tokens_details", "reasoning_tokens"), ("completion_tokens_details", "reasoning_tokens"), ) @@ -623,11 +583,8 @@ def normalize_usage( ) return CanonicalUsage( - input_tokens=input_tokens, - output_tokens=output_tokens, - cache_read_tokens=cache_read_tokens, - cache_write_tokens=cache_write_tokens, - reasoning_tokens=reasoning_tokens, + input_tokens=input_tokens, output_tokens=output_tokens, cache_read_tokens=cache_read_tokens, + cache_write_tokens=cache_write_tokens, reasoning_tokens=reasoning_tokens, ) @@ -636,22 +593,14 @@ def _unknown_cost(source: CostSource, *notes: str) -> CostResult: def estimate_usage_cost( - model_name: str, - usage: CanonicalUsage, - *, - provider: Optional[str] = None, - base_url: Optional[str] = None, - api_key: Optional[str] = None, + model_name: str, usage: CanonicalUsage, *, provider: Optional[str] = None, + base_url: Optional[str] = None, api_key: Optional[str] = None, ) -> CostResult: route = resolve_billing_route(model_name, provider=provider, base_url=base_url) if route.billing_mode == "subscription_included": return CostResult( - amount_usd=_ZERO, - status="included", - source="none", - label="included", - pricing_version="included-route", - notes=(_INCLUDED_NOTE,), + amount_usd=_ZERO, status="included", source="none", label="included", + pricing_version="included-route", notes=(_INCLUDED_NOTE,), ) entry = get_pricing_entry(model_name, provider=provider, base_url=base_url, api_key=api_key) @@ -683,10 +632,8 @@ def estimate_usage_cost( amount = _ZERO for tokens, rate in ( - (usage.input_tokens, input_rate), - (usage.output_tokens, output_rate), - (usage.cache_read_tokens, cache_read_rate), - (usage.cache_write_tokens, cache_write_rate), + (usage.input_tokens, input_rate), (usage.output_tokens, output_rate), + (usage.cache_read_tokens, cache_read_rate), (usage.cache_write_tokens, cache_write_rate), ): if rate is not None: amount += Decimal(tokens) * rate / _ONE_MILLION @@ -705,20 +652,13 @@ def estimate_usage_cost( notes.append("OpenRouter cost is estimated from the models API until reconciled.") return CostResult( - amount_usd=amount, - status=status, - source=entry.source, - label=label, - fetched_at=entry.fetched_at, - pricing_version=entry.pricing_version, - notes=tuple(notes), + amount_usd=amount, status=status, source=entry.source, label=label, + fetched_at=entry.fetched_at, pricing_version=entry.pricing_version, notes=tuple(notes), ) def has_known_pricing( - model_name: str, - provider: Optional[str] = None, - base_url: Optional[str] = None, + model_name: str, provider: Optional[str] = None, base_url: Optional[str] = None, api_key: Optional[str] = None, ) -> bool: """True if pricing data exists for this model+route (direct lookup, no dummy usage).""" diff --git a/agent/verification_evidence.py b/agent/verification_evidence.py index 16a2d34911..c188027186 100644 --- a/agent/verification_evidence.py +++ b/agent/verification_evidence.py @@ -36,8 +36,7 @@ _TARGET_PREFIXES = ("test_", "tests", "spec", "__tests__") # command is not itself a test command; anything else is a test. _KIND_KEYWORDS = ( (("lint", "eslint", "ruff"), "lint"), - (("typecheck", "tsc", "mypy", "pyright", "ty"), "typecheck"), - (("build",), "build"), + (("typecheck", "tsc", "mypy", "pyright", "ty"), "typecheck"), (("build",), "build"), (("fmt", "format"), "format"), ) _PYTEST_SPELLINGS = ( @@ -462,16 +461,12 @@ def classify_verification_command( canonical, trailing_args = match return VerificationEvidence( - command=command, - canonical_command=canonical, + command=command, canonical_command=canonical, kind="ad_hoc" if is_ad_hoc else _kind_for_command(canonical), scope="targeted" if is_ad_hoc or any(map(_looks_like_target, trailing_args)) else "full", - status="passed" if int(exit_code) == 0 else "failed", - exit_code=int(exit_code), - cwd=str(Path(cwd or ".").resolve()), - root=_root_for(facts, cwd), - session_id=str(session_id or "default"), - output_summary=_summarize_output(output), + status="passed" if int(exit_code) == 0 else "failed", exit_code=int(exit_code), + cwd=str(Path(cwd or ".").resolve()), root=_root_for(facts, cwd), + session_id=str(session_id or "default"), output_summary=_summarize_output(output), ) @@ -496,16 +491,11 @@ def record_verify_run( """ resolved = str(Path(root).resolve()) return _insert_evidence(VerificationEvidence( - command=command, - canonical_command="hermes verify", - kind="verify", + command=command, canonical_command="hermes verify", kind="verify", scope=scope if scope in {"full", "targeted"} else "full", - status="passed" if ok else "failed", - exit_code=0 if ok else 1, - cwd=resolved, + status="passed" if ok else "failed", exit_code=0 if ok else 1, cwd=resolved, root=str((_project_facts(root) or {}).get("root") or resolved), - session_id=str(session_id or "default"), - output_summary=_summarize_output(output), + session_id=str(session_id or "default"), output_summary=_summarize_output(output), )) diff --git a/agent/verification_stop.py b/agent/verification_stop.py index b49ea5d3ed..f9bf6d6eee 100644 --- a/agent/verification_stop.py +++ b/agent/verification_stop.py @@ -101,9 +101,7 @@ def _candidate_cwds(paths: Iterable[str]) -> list[Path]: def _verification_snapshot( - *, - session_id: str | None, - changed_paths: list[str], + *, session_id: str | None, changed_paths: list[str] ) -> tuple[dict[str, Any], dict[str, Any]] | None: """Return ``(status, facts)`` for the first edited workspace needing proof. @@ -173,11 +171,7 @@ def _status_detail(status: dict[str, Any]) -> str: def build_verify_on_stop_nudge( - *, - session_id: str | None, - changed_paths: Iterable[str], - attempts: int = 0, - max_attempts: int = 2, + *, session_id: str | None, changed_paths: Iterable[str], attempts: int=0, max_attempts: int=2, ) -> str | None: """Return a synthetic follow-up when edited code lacks fresh verification.""" # Prose-only turns (markdown, skills, README, LICENSE, ...) have nothing to verify. diff --git a/agent/verify/recipes.py b/agent/verify/recipes.py index 1e4d688122..73e2391651 100644 --- a/agent/verify/recipes.py +++ b/agent/verify/recipes.py @@ -111,13 +111,8 @@ def _read_package_json(root: Path) -> dict[str, Any] | None: # Ordered: the first lockfile present wins (grok's detectPackageManager). _LOCKFILE_MANAGERS = ( - ("pnpm-lock.yaml", "pnpm"), - ("bun.lock", "bun"), - ("bun.lockb", "bun"), - ("yarn.lock", "yarn"), - ("package-lock.json", "npm"), - ("uv.lock", "uv"), - ("poetry.lock", "poetry"), + ("pnpm-lock.yaml", "pnpm"), ("bun.lock", "bun"), ("bun.lockb", "bun"), ("yarn.lock", "yarn"), + ("package-lock.json", "npm"), ("uv.lock", "uv"), ("poetry.lock", "poetry"), ("Pipfile.lock", "pipenv"), ) @@ -149,12 +144,10 @@ _SCRIPT_RUNNERS = {"pnpm": "pnpm {}", "bun": "bun run {}", "yarn": "yarn {}"} _NODE_INSTALL = {"pnpm": "pnpm install", "bun": "bun install", "yarn": "yarn install"} # Ordered: the first dependency present decides the framework (kind, label, default port). _NODE_FRAMEWORKS = ( - (("next",), "nextjs", "Next.js", 3000), - (("@sveltejs/kit",), "sveltekit", "SvelteKit", 5173), + (("next",), "nextjs", "Next.js", 3000), (("@sveltejs/kit",), "sveltekit", "SvelteKit", 5173), (("astro",), "astro", "Astro", 4321), (("@remix-run/dev", "@remix-run/react"), "remix", "Remix", 3000), - (("react-scripts",), "cra", "Create React App", 3000), - (("vite",), "vite", "Vite", 5173), + (("react-scripts",), "cra", "Create React App", 3000), (("vite",), "vite", "Vite", 5173), ) diff --git a/agent/verify/runner.py b/agent/verify/runner.py index 5ab15c7fd7..8c215cb7f2 100644 --- a/agent/verify/runner.py +++ b/agent/verify/runner.py @@ -94,10 +94,7 @@ def _tail(text: str, limit: int = _TAIL_CHARS) -> str: def _run_phase_command( - phase: str, - command: str, - root: Path, - timeout: float, + phase: str, command: str, root: Path, timeout: float, on_output: Callable[[str], None] | None = None, ) -> PhaseResult: started = time.monotonic() @@ -175,10 +172,7 @@ def _terminate_process_group(proc: subprocess.Popen) -> None: def _run_start_phase( - recipe: Recipe, - root: Path, - ready_timeout: float, - port_override: int | None = None, + recipe: Recipe, root: Path, ready_timeout: float, port_override: int | None = None ) -> ReadinessResult: assert recipe.start is not None port = port_override or recipe.port or 8000 @@ -200,14 +194,9 @@ def _run_start_phase( def run_verify( - root: Path, - recipe: Recipe, - phases: tuple[str, ...] | list[str] | None = None, - phase_timeout: float = DEFAULT_PHASE_TIMEOUT, - ready_timeout: float = DEFAULT_READY_TIMEOUT, - skip_start: bool = False, - port_override: int | None = None, - stop_on_failure: bool = True, + root: Path, recipe: Recipe, phases: tuple[str, ...] | list[str] | None = None, + phase_timeout: float = DEFAULT_PHASE_TIMEOUT, ready_timeout: float = DEFAULT_READY_TIMEOUT, + skip_start: bool = False, port_override: int | None = None, stop_on_failure: bool = True, on_output: Callable[[str], None] | None = None, ) -> VerifyResult: """Run a verify pass for ``recipe`` at project ``root``. diff --git a/agent/vertex_adapter.py b/agent/vertex_adapter.py index a2de5f90bc..bb6c18238a 100644 --- a/agent/vertex_adapter.py +++ b/agent/vertex_adapter.py @@ -210,8 +210,7 @@ def build_vertex_base_url(project_id: str, region: str = DEFAULT_REGION) -> str: def get_vertex_config( - credentials_path: Optional[str] = None, - region: Optional[str] = None, + credentials_path: Optional[str] = None, region: Optional[str] = None ) -> Tuple[Optional[str], Optional[str]]: """Resolve (access_token, base_url) for Vertex AI, or (None, None) on failure.""" token, project_id = get_vertex_credentials(credentials_path) diff --git a/agent/video_gen_provider.py b/agent/video_gen_provider.py index d8cab4192c..4fab53061b 100644 --- a/agent/video_gen_provider.py +++ b/agent/video_gen_provider.py @@ -84,19 +84,11 @@ class VideoGenProvider(CatalogProviderBase): @abc.abstractmethod def generate( - self, - prompt: str, - *, - model: Optional[str] = None, - image_url: Optional[str] = None, - reference_image_urls: Optional[List[str]] = None, - duration: Optional[int] = None, - aspect_ratio: str = DEFAULT_ASPECT_RATIO, - resolution: str = DEFAULT_RESOLUTION, - negative_prompt: Optional[str] = None, - audio: Optional[bool] = None, - seed: Optional[int] = None, - **kwargs: Any, + self, prompt: str, *, model: Optional[str] = None, image_url: Optional[str] = None, + reference_image_urls: Optional[List[str]] = None, duration: Optional[int] = None, + aspect_ratio: str = DEFAULT_ASPECT_RATIO, resolution: str = DEFAULT_RESOLUTION, + negative_prompt: Optional[str] = None, audio: Optional[bool] = None, + seed: Optional[int] = None, **kwargs: Any, ) -> Dict[str, Any]: """Generate a video from a prompt, or animate ``image_url`` when given. @@ -112,40 +104,23 @@ class VideoGenProvider(CatalogProviderBase): # --------------------------------------------------------------------------- -def save_b64_video( - b64_data: str, - *, - prefix: str = "video", - extension: str = "mp4", -) -> Path: +def save_b64_video(b64_data: str,*, prefix: str="video", extension: str="mp4") -> Path: """Decode base64 video data into ``$HERMES_HOME/cache/videos/``; return the path.""" return provider_media.save_b64("videos", b64_data, prefix=prefix, extension=extension) -def save_bytes_video( - raw: bytes, - *, - prefix: str = "video", - extension: str = "mp4", -) -> Path: +def save_bytes_video(raw: bytes,*, prefix: str="video", extension: str="mp4") -> Path: """Write raw video bytes (e.g. an HTTP download body) to the cache.""" return provider_media.save_bytes("videos", raw, prefix=prefix, extension=extension) _URL_VIDEO_CONTENT_TYPES = { - "video/mp4": "mp4", - "video/webm": "webm", - "video/quicktime": "mov", - "video/x-matroska": "mkv", + "video/mp4": "mp4", "video/webm": "webm", "video/quicktime": "mov", "video/x-matroska": "mkv" } def save_url_video( - url: str, - *, - prefix: str = "video", - timeout: float = 180.0, - max_bytes: int = 200 * 1024 * 1024, + url: str, *, prefix: str = "video", timeout: float = 180.0, max_bytes: int = 200 * 1024 * 1024 ) -> Path: """Download an (often ephemeral) video URL into ``$HERMES_HOME/cache/videos/``. @@ -161,15 +136,8 @@ def save_url_video( def success_response( - *, - video: str, - model: str, - prompt: str, - modality: str = "text", - aspect_ratio: str = "", - duration: int = 0, - provider: str, - extra: Optional[Dict[str, Any]] = None, + *, video: str, model: str, prompt: str, modality: str = "text", aspect_ratio: str = "", + duration: int = 0, provider: str, extra: Optional[Dict[str, Any]] = None, ) -> Dict[str, Any]: """Uniform success dict; ``extra`` keys are added without overriding standard ones.""" payload: Dict[str, Any] = { @@ -189,13 +157,8 @@ def success_response( def error_response( - *, - error: str, - error_type: str = "provider_error", - provider: str = "", - model: str = "", - prompt: str = "", - aspect_ratio: str = "", + *, error: str, error_type: str = "provider_error", provider: str = "", model: str = "", + prompt: str = "", aspect_ratio: str = "", ) -> Dict[str, Any]: """Build a uniform error response dict.""" return { @@ -277,19 +240,11 @@ class OpenAICompatibleVideoGenProvider(VideoGenProvider): return override or self._default_base_url def generate( - self, - prompt: str, - *, - model: Optional[str] = None, - image_url: Optional[str] = None, - reference_image_urls: Optional[List[str]] = None, - duration: Optional[int] = None, - aspect_ratio: str = DEFAULT_ASPECT_RATIO, - resolution: str = DEFAULT_RESOLUTION, - negative_prompt: Optional[str] = None, - audio: Optional[bool] = None, - seed: Optional[int] = None, - **kwargs: Any, + self, prompt: str, *, model: Optional[str] = None, image_url: Optional[str] = None, + reference_image_urls: Optional[List[str]] = None, duration: Optional[int] = None, + aspect_ratio: str = DEFAULT_ASPECT_RATIO, resolution: str = DEFAULT_RESOLUTION, + negative_prompt: Optional[str] = None, audio: Optional[bool] = None, + seed: Optional[int] = None, **kwargs: Any, ) -> Dict[str, Any]: if not prompt or not prompt.strip(): return error_response( @@ -297,8 +252,7 @@ class OpenAICompatibleVideoGenProvider(VideoGenProvider): ) if not self._api_key(): return error_response( - error=f"{self._env_key} is not set", - error_type="missing_credentials", + error=f"{self._env_key} is not set", error_type="missing_credentials", provider=self.name, ) try: @@ -306,16 +260,14 @@ class OpenAICompatibleVideoGenProvider(VideoGenProvider): except ImportError: return error_response( error="openai Python package not installed (pip install openai)", - error_type="missing_dependency", - provider=self.name, + error_type="missing_dependency", provider=self.name, ) model_id = model or self.default_model() if not model_id: return error_response( error=f"no {self.name} video model available (live catalog empty?)", - error_type="no_model", - provider=self.name, + error_type="no_model", provider=self.name, ) # Fields ``videos.create`` doesn't name natively ride in ``extra_body``. @@ -344,12 +296,8 @@ class OpenAICompatibleVideoGenProvider(VideoGenProvider): except Exception as exc: # noqa: BLE001 - surface any SDK/API/timeout failure uniformly logger.debug("%s video generation failed", self.name, exc_info=True) return error_response( - error=f"{self.name} video generation failed: {exc}", - error_type="api_error", - provider=self.name, - model=model_id, - prompt=prompt, - aspect_ratio=aspect_ratio, + error=f"{self.name} video generation failed: {exc}", error_type="api_error", + provider=self.name, model=model_id, prompt=prompt, aspect_ratio=aspect_ratio, ) # DeepInfra reports "succeeded", OpenAI/Sora "completed" — accept both. @@ -359,10 +307,7 @@ class OpenAICompatibleVideoGenProvider(VideoGenProvider): job_error = getattr(video, "error", None) return error_response( error=str(job_error) if job_error else f"video job ended with status={status!r}", - error_type="job_failed", - provider=self.name, - model=model_id, - prompt=prompt, + error_type="job_failed", provider=self.name, model=model_id, prompt=prompt, aspect_ratio=aspect_ratio, ) @@ -389,21 +334,14 @@ class OpenAICompatibleVideoGenProvider(VideoGenProvider): else: return error_response( error=f"{self.name} video job succeeded but no output could be retrieved: {exc}", - error_type="empty_response", - provider=self.name, - model=model_id, - prompt=prompt, - aspect_ratio=aspect_ratio, + error_type="empty_response", provider=self.name, model=model_id, + prompt=prompt, aspect_ratio=aspect_ratio, ) return success_response( - video=video_ref, - model=model_id, - prompt=prompt, - modality="image" if image_url else "text", - aspect_ratio=aspect_ratio, - duration=duration or 0, - provider=self.name, + video=video_ref, model=model_id, prompt=prompt, + modality="image" if image_url else "text", aspect_ratio=aspect_ratio, + duration=duration or 0, provider=self.name, ) finally: close = getattr(client, "close", None) diff --git a/agent/video_gen_registry.py b/agent/video_gen_registry.py index 733f32ca0f..d811ea5196 100644 --- a/agent/video_gen_registry.py +++ b/agent/video_gen_registry.py @@ -49,8 +49,7 @@ def get_active_provider() -> Optional[VideoGenProvider]: if provider is not None: return provider logger.debug( - "video_gen.provider='%s' configured but not registered; failing closed", - configured, + "video_gen.provider='%s' configured but not registered; failing closed", configured ) return None diff --git a/agent/vision_message_prep.py b/agent/vision_message_prep.py index c487e0b18c..3c02daa63d 100644 --- a/agent/vision_message_prep.py +++ b/agent/vision_message_prep.py @@ -80,8 +80,7 @@ class VisionMessagePrepMixin: return cached role_label = { - "assistant": "assistant", - "tool": "tool result", + "assistant": "assistant", "tool": "tool result" }.get(role, "user") analysis_prompt = ( "Describe everything visible in this image in thorough detail. " @@ -235,8 +234,7 @@ class VisionMessagePrepMixin: if not isinstance(msg, dict): continue msg["content"] = self._preprocess_anthropic_content( - msg.get("content"), - str(msg.get("role", "user") or "user"), + msg.get("content"), str(msg.get("role", "user") or "user") ) return transformed @@ -304,10 +302,7 @@ class VisionMessagePrepMixin: _try_shrink_image_parts_in_messages = _forward_static("agent.conversation_compression", "try_shrink_image_parts_in_messages") def _try_strip_image_parts_from_tool_messages( - self, - api_messages: list, - *, - remember_model: bool = True, + self, api_messages: list, *, remember_model: bool = True ) -> bool: """Downgrade list-type tool messages to text summaries in place; returns True if any were downgraded. diff --git a/agent/web_search_registry.py b/agent/web_search_registry.py index 7f18be7e35..6007a19f32 100644 --- a/agent/web_search_registry.py +++ b/agent/web_search_registry.py @@ -82,13 +82,7 @@ def _configured_backend(capability: str) -> Optional[str]: # a free tier on upgrade). Filtered by ``is_available()`` at walk time so # we don't surface a provider the user has no credentials for. _LEGACY_PREFERENCE = ( - "firecrawl", - "parallel", - "tavily", - "exa", - "searxng", - "brave-free", - "ddgs", + "firecrawl", "parallel", "tavily", "exa", "searxng", "brave-free", "ddgs" ) # Keyless free-tier walk — strictly LAST-resort, tried only after the @@ -100,10 +94,7 @@ _LEGACY_PREFERENCE = ( # entirely, and rate-limited requests fail over to the next ring vendor). # Disable the tier with ``web.keyless_fallback: false``. _KEYLESS_PREFERENCE = ( - "exa", - "parallel", - "firecrawl", - "keenable", + "exa", "parallel", "firecrawl", "keenable" ) @@ -121,8 +112,7 @@ def _keyless_preference() -> tuple: start = _ring_cursor % len(_KEYLESS_RING) return tuple( - _KEYLESS_RING[(start + i) % len(_KEYLESS_RING)] - for i in range(len(_KEYLESS_RING)) + _KEYLESS_RING[(start + i) % len(_KEYLESS_RING)] for i in range(len(_KEYLESS_RING)) ) except Exception as exc: # noqa: BLE001 — ring optional in stripped envs logger.debug("keyless ring order unavailable: %s", exc) @@ -156,8 +146,7 @@ def _resolve(configured: Optional[str], *, capability: str) -> Optional[WebSearc return provider if provider is None: logger.debug( - "web backend '%s' configured but not registered; falling back", - configured, + "web backend '%s' configured but not registered; falling back", configured ) else: logger.debug(