refactor(agent/turn): AST-neutral bracket/signature packing across r3-08 slice

This commit is contained in:
Teknium
2026-09-02 18:01:27 -07:00
parent 113f04616b
commit c94ced6225
44 changed files with 717 additions and 2108 deletions
+6 -15
View File
@@ -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:
+19 -50
View File
@@ -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:
+8 -22
View File
@@ -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.
+4 -11
View File
@@ -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:
+14 -41
View File
@@ -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
+1 -4
View File
@@ -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.
+4 -16
View File
@@ -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).
+2 -6
View File
@@ -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())
+17 -59
View File
@@ -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:
+68 -212
View File
@@ -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}).",
+16 -48
View File
@@ -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")
+48 -136
View File
@@ -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)``.
+20 -52
View File
@@ -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.
+16 -46
View File
@@ -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 <think> in content, not reasoning_content, so
# _has_structured misses it; detect here to route to prefill.
_has_inline_thinking = bool(
re.search(
r'<think>|<thinking>|<reasoning>',
final_response or "",
re.IGNORECASE,
)
re.search( r'<think>|<thinking>|<reasoning>', 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: "
+4 -12
View File
@@ -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:
+12 -30
View File
@@ -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
+7 -20
View File
@@ -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
+15 -46
View File
@@ -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,
+20 -66
View File
@@ -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:
+14 -50
View File
@@ -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:
+5 -14
View File
@@ -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
+7 -22
View File
@@ -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}")
+27 -68
View File
@@ -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 +
+38 -107
View File
@@ -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
+22 -55
View File
@@ -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,
)
+70 -212
View File
@@ -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(
+14 -41
View File
@@ -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
+45 -133
View File
@@ -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}).",
+8 -26
View File
@@ -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)
+10 -27
View File
@@ -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(
+21 -62
View File
@@ -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
+5 -14
View File
@@ -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
+26 -83
View File
@@ -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)",
))
+13 -37
View File
@@ -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
)
+34 -94
View File
@@ -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)."""
+8 -18
View File
@@ -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),
))
+2 -8
View File
@@ -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.
+4 -11
View File
@@ -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),
)
+5 -16
View File
@@ -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``.
+1 -2
View File
@@ -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)
+29 -91
View File
@@ -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)
+1 -2
View File
@@ -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
+3 -8
View File
@@ -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.
+4 -15
View File
@@ -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(