refactor(agent/turn): AST-neutral bracket/signature packing across r3-08 slice
This commit is contained in:
@@ -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:
|
||||
|
||||
@@ -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:
|
||||
|
||||
@@ -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.
|
||||
|
||||
@@ -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:
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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
@@ -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).
|
||||
|
||||
|
||||
@@ -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
@@ -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
@@ -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
@@ -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
@@ -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)``.
|
||||
|
||||
@@ -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.
|
||||
|
||||
@@ -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: "
|
||||
|
||||
@@ -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
@@ -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
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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
@@ -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:
|
||||
|
||||
@@ -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
@@ -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
|
||||
|
||||
@@ -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
@@ -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
@@ -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
|
||||
|
||||
@@ -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
@@ -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(
|
||||
|
||||
@@ -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
@@ -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}).",
|
||||
|
||||
@@ -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
@@ -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
@@ -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
|
||||
|
||||
@@ -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
@@ -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
@@ -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
@@ -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)."""
|
||||
|
||||
@@ -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),
|
||||
))
|
||||
|
||||
|
||||
|
||||
@@ -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
@@ -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
@@ -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``.
|
||||
|
||||
@@ -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
@@ -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)
|
||||
|
||||
@@ -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
|
||||
|
||||
|
||||
@@ -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.
|
||||
|
||||
|
||||
@@ -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(
|
||||
|
||||
Reference in New Issue
Block a user