ci: apply all automatic linter fixes available

This commit is contained in:
Maciej Lisiewski committed 2026-10-08 17:22:54 +02:00
1 parent f6258f6cfb
commit d76cce4f11
2366 files changed
+14579 -14723

No files matched your search

+3 -3
View File
@@ -11,7 +11,7 @@ Usage::
# IMPORTANT: hermes_bootstrap must be the very first import — UTF-8 stdio
# on Windows. No-op on POSIX. See hermes_bootstrap.py for full rationale.
try:
import hermes_bootstrap # noqa: F401
import hermes_bootstrap
except ModuleNotFoundError as exc:
# Partial ``hermes update`` (git-reset landed, ``uv pip install -e .`` did not).
if exc.name != "hermes_bootstrap":
@@ -111,8 +111,8 @@ def _print_version() -> None:
def _run_check() -> None:
import acp # noqa: F401
from acp_adapter.server import HermesACPAgent # noqa: F401
import acp
from acp_adapter.server import HermesACPAgent
print("Hermes ACP check OK")
+10 -10
View File
@@ -67,7 +67,7 @@ def _send_update(conn: acp.Client, session_id: str, loop: asyncio.AbstractEventL
logger.debug("Failed to send ACP update", exc_info=True)
def _upgrade_queue(tool_call_ids: Dict[str, Deque[str]], name: str) -> Deque[str] | None:
def _upgrade_queue(tool_call_ids: dict[str, deque[str]], name: str) -> deque[str] | None:
"""Fetch the per-tool FIFO of pending call IDs, upgrading a legacy bare-string entry in place."""
queue = tool_call_ids.get(name)
if isinstance(queue, str):
@@ -76,8 +76,8 @@ def _upgrade_queue(tool_call_ids: Dict[str, Deque[str]], name: str) -> Deque[str
def close_tool_call(
conn: acp.Client, session_id: str, loop: asyncio.AbstractEventLoop, tool_call_ids: Dict[str, Deque[str]],
tool_call_meta: Dict[str, Dict[str, Any]], name: str, result: Any = None, is_error: bool = False,
conn: acp.Client, session_id: str, loop: asyncio.AbstractEventLoop, tool_call_ids: dict[str, deque[str]],
tool_call_meta: dict[str, dict[str, Any]], name: str, result: Any = None, is_error: bool = False,
) -> str | None:
"""Close the oldest open ACP tool call for ``name``; returns its id, or None when none is open."""
queue = _upgrade_queue(tool_call_ids, name)
@@ -95,8 +95,8 @@ def close_tool_call(
def flush_open_tool_calls(
conn: acp.Client, session_id: str, loop: asyncio.AbstractEventLoop, tool_call_ids: Dict[str, Deque[str]],
tool_call_meta: Dict[str, Dict[str, Any]],
conn: acp.Client, session_id: str, loop: asyncio.AbstractEventLoop, tool_call_ids: dict[str, deque[str]],
tool_call_meta: dict[str, dict[str, Any]],
) -> int:
"""Close every tool call still open at the end of a turn, and report how many there were.
@@ -117,10 +117,10 @@ def flush_open_tool_calls(
def make_tool_progress_cb(
conn: acp.Client, session_id: str, loop: asyncio.AbstractEventLoop, tool_call_ids: Dict[str, Deque[str]],
tool_call_meta: Dict[str, Dict[str, Any]],
conn: acp.Client, session_id: str, loop: asyncio.AbstractEventLoop, tool_call_ids: dict[str, deque[str]],
tool_call_meta: dict[str, dict[str, Any]],
edit_approval_policy_getter: Callable[[], tuple[str, str | None]] | None = None,
turn_state: Dict[str, Any] | None = None,
turn_state: dict[str, Any] | None = None,
) -> Callable:
"""Create a ``tool_progress_callback`` for AIAgent.
@@ -253,8 +253,8 @@ def make_message_cb(
def make_step_cb(
conn: acp.Client, session_id: str, loop: asyncio.AbstractEventLoop, tool_call_ids: Dict[str, Deque[str]],
tool_call_meta: Dict[str, Dict[str, Any]], turn_state: Dict[str, Any] | None = None,
conn: acp.Client, session_id: str, loop: asyncio.AbstractEventLoop, tool_call_ids: dict[str, deque[str]],
tool_call_meta: dict[str, dict[str, Any]], turn_state: dict[str, Any] | None = None,
) -> Callable:
"""Create a ``step_callback(api_call_count: int, prev_tools: list)`` for AIAgent."""
+1 -1
View File
@@ -50,7 +50,7 @@ def _named_custom_provider_catalogs() -> list[tuple[str, str, list[tuple[str, st
}
def _entry_catalog(entry: dict) -> tuple[str, str, list[tuple[str, str]]] | None:
field = lambda key: str(entry.get(key) or "").strip() # noqa: E731
field = lambda key: str(entry.get(key) or "").strip()
provider_key, name, base_url = field("provider_key"), field("name"), field("base_url")
if provider_key.lower() in disabled_keys or not name or not base_url:
return None
+4 -4
View File
@@ -18,7 +18,7 @@ from typing import Any, Dict, Optional
_MAX_WALK = 100
def _get_row(db: Any, session_id: str) -> Optional[Dict[str, Any]]:
def _get_row(db: Any, session_id: str) -> Optional[dict[str, Any]]:
try:
return db.get_session(session_id)
except Exception:
@@ -31,7 +31,7 @@ def _is_compression_end(row: Any) -> bool:
def build_session_provenance(
db: Any, acp_session_id: str, current_hermes_session_id: str, *, previous_hermes_session_id: Optional[str] = None,
) -> Optional[Dict[str, Any]]:
) -> Optional[dict[str, Any]]:
"""Build ``_meta.hermes.sessionProvenance`` for an ACP session.
``db`` must expose ``get_session``. ``current_hermes_session_id`` is the live
@@ -62,7 +62,7 @@ def build_session_provenance(
# A continuation is a session whose immediate parent ended with end_reason='compression'.
is_continuation = bool(parent_id) and _is_compression_end(_get_row(db, parent_id))
provenance: Dict[str, Any] = {
provenance: dict[str, Any] = {
"acpSessionId": acp_session_id, "currentHermesSessionId": current_hermes_session_id,
"rootHermesSessionId": root_id, "parentHermesSessionId": parent_id,
"sessionKind": "continuation" if is_continuation else "root", "compressionDepth": compression_depth,
@@ -79,7 +79,7 @@ def build_session_provenance(
def session_provenance_meta(
db: Any, acp_session_id: str, current_hermes_session_id: str, *, previous_hermes_session_id: Optional[str] = None,
) -> Optional[Dict[str, Any]]:
) -> Optional[dict[str, Any]]:
"""Return a ready ``_meta`` payload: ``{"hermes": {"sessionProvenance": ...}}``."""
prov = build_session_provenance(db, acp_session_id, current_hermes_session_id,
previous_hermes_session_id=previous_hermes_session_id)
+3 -3
View File
@@ -899,13 +899,13 @@ class HermesACPAgent(SlashCommandsMixin, acp.Agent):
"""Install the ACP streaming callbacks on the session agent for one turn."""
cbs = _TurnCallbacks()
if conn:
tool_call_ids: dict[str, Deque[str]] = defaultdict(deque)
tool_call_ids: dict[str, deque[str]] = defaultdict(deque)
tool_call_meta: dict[str, dict[str, Any]] = {}
cbs.tool_call_ids, cbs.tool_call_meta = tool_call_ids, tool_call_meta
# Shared with the step callback so a runtime that projects
# ``tool.completed`` closes each call once, not twice.
turn_state: dict[str, Any] = {}
policy_getter = lambda: self._edit_approval_policy_for_state(state) # noqa: E731
policy_getter = lambda: self._edit_approval_policy_for_state(state)
cbs.tool_progress_cb = make_tool_progress_cb(
conn, session_id, loop, tool_call_ids, tool_call_meta, edit_approval_policy_getter=policy_getter,
turn_state=turn_state,
@@ -925,7 +925,7 @@ class HermesACPAgent(SlashCommandsMixin, acp.Agent):
cbs.stream_delta_cb = stream_delta_cb
# Closes the synthetic permission-request bubble once the user has answered.
send_update = lambda update: _send_update(conn, session_id, loop, update) # noqa: E731
send_update = lambda update: _send_update(conn, session_id, loop, update)
cbs.approval_cb = make_approval_callback(conn.request_permission, loop, session_id, send_update=send_update)
try:
from acp_adapter.edit_approval import make_acp_edit_approval_requester
+9 -9
View File
@@ -100,8 +100,8 @@ def _register_task_cwd(task_id: str, cwd: str) -> None:
logger.debug("Failed to register ACP task cwd override", exc_info=True)
def _expand_acp_enabled_toolsets(toolsets: List[str] | None = None,
mcp_server_names: List[str] | None = None) -> List[str]:
def _expand_acp_enabled_toolsets(toolsets: list[str] | None = None,
mcp_server_names: list[str] | None = None) -> list[str]:
"""Return ACP toolsets plus explicit MCP server toolsets for this session."""
names = [n for n in (["hermes-acp"] if toolsets is None else toolsets) if n]
names += [f"mcp-{s}" for s in (mcp_server_names or []) if s]
@@ -118,12 +118,12 @@ def _parse_model_config(mc: Any) -> dict:
def _session_info(sid: str, cwd: str, model: Any, history_len: int, title: Any, preview: Any,
updated_at: Any) -> Dict[str, Any]:
updated_at: Any) -> dict[str, Any]:
return {"session_id": sid, "cwd": cwd, "model": model, "history_len": history_len,
"title": _build_session_title(title, preview, cwd), "updated_at": _format_updated_at(updated_at)}
def _first_user_preview(history: List[Dict[str, Any]], default: str) -> str:
def _first_user_preview(history: list[dict[str, Any]], default: str) -> str:
return next((str(m.get("content") or "").strip() for m in history
if m.get("role") == "user" and str(m.get("content") or "").strip()), default)
@@ -136,7 +136,7 @@ class SessionState:
agent: Any # AIAgent instance
cwd: str = "."
model: str = ""
history: List[Dict[str, Any]] = field(default_factory=list)
history: list[dict[str, Any]] = field(default_factory=list)
cancel_event: Any = None # threading.Event
is_running: bool = False
# A state-mutating slash command (/reset, /compress, /model) is in flight. Turn claims
@@ -144,7 +144,7 @@ class SessionState:
# a bare is_running check in the slash thread would leave a check-then-act window where
# a prompt claims the turn mid-mutation.
command_op: bool = False
queued_prompts: List[str] = field(default_factory=list)
queued_prompts: list[str] = field(default_factory=list)
runtime_lock: Any = field(default_factory=threading.Lock)
current_prompt_text: str = ""
interrupted_prompt_text: str = ""
@@ -162,7 +162,7 @@ class SessionManager:
def __init__(self, agent_factory=None, db=None):
"""``agent_factory``: AIAgent-like factory (tests); default builds a real AIAgent from
the runtime provider config. ``db``: SessionDB; default lazily opens ``~/.hermes/state.db``."""
self._sessions: Dict[str, SessionState] = {}
self._sessions: dict[str, SessionState] = {}
self._lock = threading.Lock()
# Serializes DB restores: session construction runs off the event loop, so two
# overlapping session/load for one id must share a single agent build.
@@ -207,7 +207,7 @@ class SessionManager:
logger.info("Forked ACP session %s -> %s", session_id, new_id)
return state
def list_sessions(self, cwd: str | None = None) -> List[Dict[str, Any]]:
def list_sessions(self, cwd: str | None = None) -> list[dict[str, Any]]:
"""Return lightweight info dicts for all sessions (memory + database)."""
normalized_cwd = _normalize_cwd_for_compare(cwd) if cwd else None
db = self._get_db()
@@ -304,7 +304,7 @@ class SessionManager:
# ---- persistence via SessionDB ------------------------------------------
def _install_state(self, session_id: str, agent: Any, cwd: str, model: str,
history: List[Dict[str, Any]], *, persist: bool = True) -> SessionState:
history: list[dict[str, Any]], *, persist: bool = True) -> SessionState:
"""Build a SessionState, register it in memory, bind its cwd for tools, optionally persist."""
state = SessionState(session_id=session_id, agent=agent, cwd=cwd, model=model,
history=history, cancel_event=threading.Event())
+14 -14
View File
@@ -15,7 +15,7 @@ from agent.display import build_tool_preview
logger = logging.getLogger(__name__)
# Hermes tool name -> ACP ToolKind (anything unlisted is "other").
TOOL_KIND_MAP: Dict[str, ToolKind] = {
TOOL_KIND_MAP: dict[str, ToolKind] = {
name: kind
for kind, names in {
"read": ("read_file", "skill_view", "skills_list", "browser_snapshot", "browser_vision",
@@ -55,7 +55,7 @@ _POLISHED_TOOLS = {
}
_EMPTYISH = (None, "", [], {})
Args = Dict[str, Any]
Args = dict[str, Any]
_Formatter = Callable[[str, Optional[str], Optional[Args]], Optional[str]]
@@ -521,7 +521,7 @@ def _format_media_or_cron_result(tool_name: str, data: Args, args: Args) -> Opti
return "\n".join([f"✅ {tool_name} completed", *(f"- **{k}:** {data.get(k)}" for k in keys if data.get(k))])
def _format_structured_value(key: str, value: Any, *, indent: int = 0, max_depth: int = 3, max_items: int = 8) -> List[str]:
def _format_structured_value(key: str, value: Any, *, indent: int = 0, max_depth: int = 3, max_items: int = 8) -> list[str]:
"""Render nested JSON-ish values as compact Markdown bullets, not inline blobs."""
pad = " " * indent
bullet = f"{pad}- "
@@ -530,7 +530,7 @@ def _format_structured_value(key: str, value: Any, *, indent: int = 0, max_depth
def _line(text: str) -> str:
return f"{bullet}{label} {text}" if label else f"{bullet}{text}"
def _child(child_key: str, child_value: Any, extra_indent: int) -> List[str]:
def _child(child_key: str, child_value: Any, extra_indent: int) -> list[str]:
return _format_structured_value(child_key, child_value, indent=indent + extra_indent, max_depth=max_depth - 1,
max_items=max_items)
@@ -617,7 +617,7 @@ def _format_generic_structured_result(tool_name: str, result: Optional[str], *,
return _truncate_text("\n".join(lines), limit=7000)
_COMPLETION_FORMATTERS: Dict[str, _Formatter] = {
_COMPLETION_FORMATTERS: dict[str, _Formatter] = {
"todo": _format_todo_result,
"read_file": _format_read_file_result,
"write_file": _format_edit_result,
@@ -636,10 +636,10 @@ _COMPLETION_FORMATTERS: Dict[str, _Formatter] = {
}
def _parse_unified_diff_content(diff_text: str) -> List[Any]:
def _parse_unified_diff_content(diff_text: str) -> list[Any]:
"""Convert unified diff text into ACP diff content blocks (one per ``---``/``+++`` pair)."""
content: List[Any] = []
state: Dict[str, Any] = {"old": None, "new": None, "old_lines": [], "new_lines": []}
content: list[Any] = []
state: dict[str, Any] = {"old": None, "new": None, "old_lines": [], "new_lines": []}
def _flush() -> None:
old_path, new_path = state["old"], state["new"]
@@ -674,7 +674,7 @@ def _parse_unified_diff_content(diff_text: str) -> List[Any]:
def _build_tool_complete_content(
tool_name: str, result: Optional[str], *, function_args: Optional[Args] = None, snapshot: Any = None
) -> List[Any]:
) -> list[Any]:
"""Build structured ACP completion content, falling back to plain text."""
if tool_name == "skill_manage":
try:
@@ -697,7 +697,7 @@ def _build_tool_complete_content(
# --- ToolCallStart / ToolCallProgress events ---------------------------------
def _more(items: list, shown: int, unit: str = "") -> List[str]:
def _more(items: list, shown: int, unit: str = "") -> list[str]:
"""``["... N more<unit>"]`` trailer when ``items`` overflowed the ``shown`` cap, else ``[]``."""
return [f"... {len(items) - shown} more{unit}"] if len(items) > shown else []
@@ -724,7 +724,7 @@ def _start_skill_manage(args: Args) -> Any:
target = str(args.get("file_path") or "file")
return acp.tool_diff_content(path=f"skills/{name}/{target}", new_text=str(args.get("file_content") or ""))
if action in {"delete", "remove_file"}:
return f"Removing {str(args.get('file_path') or file_path)} from skill '{name}'"
return f"Removing {args.get('file_path') or file_path!s} from skill '{name}'"
return f"Running skill_manage action '{action}' on skill '{name}' ({file_path})"
@@ -751,7 +751,7 @@ def _preview(label: str, value: str, limit: int) -> str:
# Per-tool start-content builders returning text or one ACP content block. ``None`` means the
# title/location already identify the target (read_file, web_extract): a synthetic content block
# would make Zed render an unhelpful Output section before completion.
_START_CONTENT_BUILDERS: Dict[str, Optional[Callable[[Args], Any]]] = {
_START_CONTENT_BUILDERS: dict[str, Optional[Callable[[Args], Any]]] = {
"patch": lambda a: (
f"Preparing {a.get('mode', 'replace')} edit for {a.get('path') or 'patch input'}. Approval prompt shows the diff."
),
@@ -787,7 +787,7 @@ def build_tool_start(tool_call_id: str, tool_name: str, arguments: Args, *, edit
(mirrors ``get_cute_tool_message`` in ``agent/display.py``)."""
try:
return _build_tool_start(tool_call_id, tool_name, arguments, edit_diff=edit_diff)
except Exception as exc: # noqa: BLE001 — a tool-call render must never abort the turn
except Exception as exc:
logger.debug("ACP tool-start render failed for %r: %s", tool_name, exc)
safe_name = tool_name if isinstance(tool_name, str) and tool_name else "tool"
return acp.start_tool_call(tool_call_id, safe_name, kind=get_tool_kind(safe_name), content=None, locations=[])
@@ -846,7 +846,7 @@ def build_tool_abandoned(tool_call_id: str, tool_name: str) -> ToolCallProgress:
)
def extract_locations(arguments: Args) -> List[ToolCallLocation]:
def extract_locations(arguments: Args) -> list[ToolCallLocation]:
"""Extract file-system locations from tool arguments."""
if not (path := arguments.get("path")):
return []
+1 -1
View File
@@ -1,3 +1,3 @@
"""Agent internals extracted from run_agent.py so it stays focused on AIAgent."""
from . import jiter_preload as _jiter_preload # noqa: F401
from . import jiter_preload as _jiter_preload
+34 -34
View File
@@ -170,7 +170,7 @@ def _custom_provider_runtime_ids(value: Any) -> set[str]:
def _build_codex_gpt5_autoraise_notice(
autoraise: Dict[str, Any], context_length: Optional[int] = None
autoraise: dict[str, Any], context_length: Optional[int] = None
) -> str:
"""One-time notice when Codex gpt-5.x raises compaction (``autoraise``: model/from/to).
@@ -197,7 +197,7 @@ def _build_codex_gpt5_autoraise_notice(
def _resolve_compression_threshold(
global_threshold: float, model_cthresh: Optional[float], *, model: Optional[str] = None,
is_codex_autoraise: bool,
) -> tuple[float, Optional[Dict[str, Any]]]:
) -> tuple[float, Optional[dict[str, Any]]]:
"""Global compaction threshold merged with a per-model override.
Returns ``(threshold, autoraise_notice)``; the notice is set only when a Codex autoraise
@@ -218,7 +218,7 @@ def _codex_gpt55_autoraise_notice_marker():
return get_hermes_home() / ".codex_gpt55_autoraise_notice"
def _codex_gpt55_autoraise_notice_state(autoraise: Dict[str, Any]) -> str:
def _codex_gpt55_autoraise_notice_state(autoraise: dict[str, Any]) -> str:
"""Notice identity keyed on what it displays (model + from→to percentages).
An unchanged threshold stays silent across restarts; a changed global threshold or a
@@ -230,7 +230,7 @@ def _codex_gpt55_autoraise_notice_state(autoraise: Dict[str, Any]) -> str:
return f"{model}:{from_pct}:{to_pct}"
def _codex_gpt55_autoraise_notice_seen(autoraise: Dict[str, Any]) -> bool:
def _codex_gpt55_autoraise_notice_seen(autoraise: dict[str, Any]) -> bool:
"""True if this exact notice was already shown for this profile (unreadable = unseen)."""
try:
current = _codex_gpt55_autoraise_notice_state(autoraise)
@@ -241,7 +241,7 @@ def _codex_gpt55_autoraise_notice_seen(autoraise: Dict[str, Any]) -> bool:
return False
def _record_codex_gpt55_autoraise_notice(autoraise: Dict[str, Any]) -> None:
def _record_codex_gpt55_autoraise_notice(autoraise: dict[str, Any]) -> None:
"""Persist that the notice was shown. Best-effort: a failure only re-shows it later."""
with suppress(OSError, KeyError, TypeError, ValueError):
marker = _codex_gpt55_autoraise_notice_marker()
@@ -255,7 +255,7 @@ def _normalized_custom_base_url(value: Any) -> str:
return value.strip().rstrip("/")
def _custom_provider_model_matches(agent_model: str, entry: Dict[str, Any]) -> bool:
def _custom_provider_model_matches(agent_model: str, entry: dict[str, Any]) -> bool:
agent_model_norm = str(agent_model or "").strip().lower()
# Multi-model entries (`providers.<name>.models` mapping / legacy `models:` list):
# matching ANY catalog entry counts, else a provider whose `model` differs from the
@@ -269,8 +269,8 @@ def _custom_provider_model_matches(agent_model: str, entry: Dict[str, Any]) -> b
def _custom_provider_extra_body_for_agent(
*, provider: str, model: str, base_url: str, custom_providers: List[Dict[str, Any]]
) -> Optional[Dict[str, Any]]:
*, provider: str, model: str, base_url: str, custom_providers: list[dict[str, Any]]
) -> Optional[dict[str, Any]]:
provider_norm = (provider or "").strip().lower()
if provider_norm != "custom" and not provider_norm.startswith("custom:"):
return None
@@ -279,7 +279,7 @@ def _custom_provider_extra_body_for_agent(
if not target_url:
return None
fallback: Optional[Dict[str, Any]] = None
fallback: Optional[dict[str, Any]] = None
for entry in custom_providers or []:
if not isinstance(entry, dict):
continue
@@ -302,7 +302,7 @@ def _custom_provider_extra_body_for_agent(
return fallback
def _merge_custom_provider_extra_body(agent, custom_providers: List[Dict[str, Any]]) -> None:
def _merge_custom_provider_extra_body(agent, custom_providers: list[dict[str, Any]]) -> None:
extra_body = _custom_provider_extra_body_for_agent(
provider=agent.provider, model=agent.model, base_url=agent.base_url,
custom_providers=custom_providers,
@@ -361,12 +361,12 @@ def _parse_config_int(raw: Any, default: int) -> int:
return default
def _cfg_flag(cfg: Dict[str, Any], key: str, default: bool) -> bool:
def _cfg_flag(cfg: dict[str, Any], key: str, default: bool) -> bool:
"""Legacy string-set truthiness used by the ``compression`` section."""
return str(cfg.get(key, default)).lower() in {"true", "1", "yes"}
def _cfg_dict(cfg: Dict[str, Any], key: str) -> Dict[str, Any]:
def _cfg_dict(cfg: dict[str, Any], key: str) -> dict[str, Any]:
"""``cfg[key]`` if it is a mapping, else ``{}`` (malformed sections are ignored)."""
section = cfg.get(key, {})
return section if isinstance(section, dict) else {}
@@ -519,14 +519,14 @@ def _finalize_routing(agent, api_mode, credential_pool):
).start()
def _set_defaults(agent, table: Dict[str, Any]) -> None:
def _set_defaults(agent, table: dict[str, Any]) -> None:
"""Assign each ``name -> value`` on ``agent``; callables are factories (fresh per agent)."""
for name, value in table.items():
setattr(agent, name, value() if callable(value) else value)
# Control-flow state (interrupts / steer / redirect / delegation / background review).
_CONTROL_STATE: Dict[str, Any] = {
_CONTROL_STATE: dict[str, Any] = {
"_executing_tools": False, # lets _vprint print while tools run with stream consumers on
"_trim_after_tool_batch": False, # a >=1 MB tool result was committed; trim once the batch unwinds
"_tool_guardrails": ToolCallGuardrailController,
@@ -565,7 +565,7 @@ _CONTROL_STATE: Dict[str, Any] = {
}
# Per-turn bookkeeping: budgets, activity tracking, rate-limit/credits telemetry.
_TURN_STATE: Dict[str, Any] = {
_TURN_STATE: dict[str, Any] = {
# Intermediate pressure warnings made models give up early; ordinary conversations
# remain opt-in. Dispatcher workers receive a bounded completion checkpoint.
"_iteration_budget_warning_injected": False,
@@ -595,7 +595,7 @@ _TURN_STATE: Dict[str, Any] = {
}
# Session persistence state.
_SESSION_STATE: Dict[str, Any] = {
_SESSION_STATE: dict[str, Any] = {
"_session_messages": list,
# Responses encrypted-reasoning replay. The first ``invalid_encrypted_content`` rejection only
# strips the stale blobs (a rotated sealing key); a second one means the route cannot round-trip
@@ -636,7 +636,7 @@ _SESSION_STATE: Dict[str, Any] = {
}
# Streaming delivery state.
_STREAM_STATE: Dict[str, Any] = {
_STREAM_STATE: dict[str, Any] = {
"_stream_callback": None, # streaming TTS; set early so _vprint can reference it
"_stream_needs_break": False, # one "\n\n" before the next real text delta after tools
# Stateful scrubbers: <memory-context> / thinking spans split across deltas defeat
@@ -765,7 +765,7 @@ def _init_anthropic_client(agent, api_key, base_url, _provider_timeout):
try:
from hermes_cli.auth import build_minimax_oauth_token_provider
effective_key = build_minimax_oauth_token_provider()
except Exception as _mm_exc: # noqa: BLE001 — never block startup on this
except Exception as _mm_exc:
logging.getLogger(__name__).warning(
"MiniMax OAuth: failed to install per-request token provider "
"(%s); falling back to static bearer that will expire ~15min in.",
@@ -813,7 +813,7 @@ def _init_bedrock_client(agent, base_url):
print(f"🤖 AI Agent initialized with model: {agent.model} (AWS Bedrock, {agent._bedrock_region}{_gr_label})")
def _explicit_client_kwargs(agent, api_key, base_url, _provider_timeout) -> Dict[str, Any]:
def _explicit_client_kwargs(agent, api_key, base_url, _provider_timeout) -> dict[str, Any]:
"""OpenAI-client kwargs from explicit CLI/gateway credentials (auth already resolved)."""
_parsed_url = urlparse(base_url)
client_kwargs = {"api_key": api_key, "base_url": base_url}
@@ -844,7 +844,7 @@ def _explicit_client_kwargs(agent, api_key, base_url, _provider_timeout) -> Dict
return client_kwargs
def _routed_client_kwargs(agent, fallback_model, _provider_timeout) -> Optional[Dict[str, Any]]:
def _routed_client_kwargs(agent, fallback_model, _provider_timeout) -> Optional[dict[str, Any]]:
"""OpenAI-client kwargs via the centralized provider router (no explicit creds).
Falls through to the init-time fallback chain, then raises with the missing-key /
@@ -953,7 +953,7 @@ def _routed_client_kwargs(agent, fallback_model, _provider_timeout) -> Optional[
)
def _apply_openai_header_policy(agent, client_kwargs: Dict[str, Any]) -> None:
def _apply_openai_header_policy(agent, client_kwargs: dict[str, Any]) -> None:
"""Mutate ``client_kwargs`` (== ``agent._client_kwargs``) with header/TLS policy, in order:
OpenRouter Claude beta header → model.default_headers → custom-provider TLS/extra_headers."""
# Fine-grained tool streaming for Claude on OpenRouter: without the beta header
@@ -1054,7 +1054,7 @@ def _lazy_headers(module: str, name: str, pass_key: bool = False, pass_base: boo
# Host → default_headers factory for explicit base_url client construction. Ordered: first
# host match wins; no match falls back to the provider profile's declared headers.
_HOST_DEFAULT_HEADERS: List[tuple[str, Callable[[Any, str], Dict[str, str]]]] = [
_HOST_DEFAULT_HEADERS: list[tuple[str, Callable[[Any, str], dict[str, str]]]] = [
("openrouter.ai", _lazy_headers("agent.auxiliary_client", "build_or_headers")),
("integrate.api.nvidia.com",
_lazy_headers("agent.auxiliary_client", "build_nvidia_nim_headers", pass_base=True)),
@@ -1074,7 +1074,7 @@ def _host_default_headers_factory(base_url: str):
return None
def _client_kwargs_from_routed(client, timeout) -> Dict[str, Any]:
def _client_kwargs_from_routed(client, timeout) -> dict[str, Any]:
"""OpenAI-client kwargs mirroring a router-resolved client, keeping its provider headers
(SDK stores them in ``_custom_headers``; older/mocked clients expose ``default_headers``)."""
kwargs = {"api_key": client.api_key, "base_url": str(client.base_url)}
@@ -1255,7 +1255,7 @@ def _apply_display_config(agent, _agent_cfg, platform):
_ra().logger.warning("Tool loop guardrail config ignored: %s", _tlg_err)
def _memory_provider_init_kwargs(agent, platform) -> Dict[str, Any]:
def _memory_provider_init_kwargs(agent, platform) -> dict[str, Any]:
"""Scoping kwargs for ``MemoryManager.initialize_all`` (status_callback is CLI-only:
gateway status travels a different path and the indicator no-ops without it)."""
kwargs = {
@@ -1456,7 +1456,7 @@ def _positive_int(raw: Any, *, reject: tuple = ()) -> Optional[int]:
return parsed if parsed > 0 else None
def _compression_threshold(agent, cfg: Dict[str, Any]) -> tuple[float, bool]:
def _compression_threshold(agent, cfg: dict[str, Any]) -> tuple[float, bool]:
"""Global threshold merged with the per-model override; stashes the autoraise notice.
Codex gpt-5.4/5.5 raise to 85% (272K cap → 50% would compact at ~136K); the opt-out flag
restores the global value, and the notice has its own display gate."""
@@ -1487,7 +1487,7 @@ def _compression_threshold(agent, cfg: Dict[str, Any]) -> tuple[float, bool]:
return threshold, notice_enabled
def _compression_codex_settings(cfg: Dict[str, Any]) -> tuple[str, bool, Optional[int]]:
def _compression_codex_settings(cfg: dict[str, Any]) -> tuple[str, bool, Optional[int]]:
"""``codex_app_server_auto`` / ``codex_responses_native`` / ``codex_responses_compact_threshold``."""
app_server_auto = str(cfg.get("codex_app_server_auto", "native") or "native").lower()
if app_server_auto not in {"native", "hermes", "off"}:
@@ -2300,7 +2300,7 @@ def _init_usage_state(agent):
# Per-session usage accounting.
_USAGE_STATE: Dict[str, Any] = {
_USAGE_STATE: dict[str, Any] = {
"_user_turn_count": 0,
"_is_user_initiated_turn": False, # Copilot x-initiator: first call of a user turn = "user"
# Usage anchors (agent/usage_anchor.py): last response's exact usage + transcript
@@ -2362,11 +2362,11 @@ def init_agent(
agent, base_url: str = None, api_key: str = None, provider: str = None, api_mode: str = None,
acp_command: str = None, acp_args: list[str] | None = None, command: str = None,
args: list[str] | None = None, model: str = "", max_iterations: int = sys.maxsize,
enabled_toolsets: List[str] = None, disabled_toolsets: List[str] = None,
enabled_toolsets: list[str] = None, disabled_toolsets: list[str] = None,
save_trajectories: bool = False, verbose_logging: bool = False, quiet_mode: bool = False,
tool_progress_mode: str = "all", ephemeral_system_prompt: str = None,
log_prefix_chars: int = 100, log_prefix: str = "", providers_allowed: List[str] = None,
providers_ignored: List[str] = None, providers_order: List[str] = None,
log_prefix_chars: int = 100, log_prefix: str = "", providers_allowed: list[str] = None,
providers_ignored: list[str] = None, providers_order: list[str] = None,
provider_sort: str = None, provider_require_parameters: bool = False,
provider_data_collection: str = None, openrouter_min_coding_score: Optional[float] = None,
session_id: str = None, tool_progress_callback: callable = None,
@@ -2381,18 +2381,18 @@ def init_agent(
notice_callback: callable = None, notice_clear_callback: callable = None,
event_callback: Optional[Callable[[str, dict], None]] = None,
reaction_callback: Optional[Callable[[str], None]] = None, max_tokens: int = None,
reasoning_config: Dict[str, Any] = None, service_tier: str = None,
request_overrides: Dict[str, Any] = None, prefill_messages: List[Dict[str, Any]] = None,
reasoning_config: dict[str, Any] = None, service_tier: str = None,
request_overrides: dict[str, Any] = None, prefill_messages: list[dict[str, Any]] = None,
platform: str = None, user_id: str = None, user_id_alt: str = None, user_name: str = None,
chat_id: str = None, chat_name: str = None, chat_type: str = None, thread_id: str = None,
gateway_session_key: str = None, skip_context_files: bool = False,
load_soul_identity: bool = False, skip_memory: bool = False,
skip_background_review: bool = False, session_db=None, parent_session_id: str = None,
iteration_budget: "IterationBudget" = None, run_budget_seconds: Optional[float] = None,
fallback_model: Dict[str, Any] = None, credential_pool=None, checkpoints_enabled: bool = False,
fallback_model: dict[str, Any] = None, credential_pool=None, checkpoints_enabled: bool = False,
checkpoint_max_snapshots: int = 20, checkpoint_max_total_size_mb: int = 500,
checkpoint_max_file_size_mb: int = 10, pass_session_id: bool = False,
requested_provider: str = None, capabilities: Optional[Dict[str, bool]] = None, cwd: Optional[str] = None,
requested_provider: str = None, capabilities: Optional[dict[str, bool]] = None, cwd: Optional[str] = None,
side_agent: bool = False, memory_manager=None,
tool_result_metadata_callback: Optional[Callable[..., dict]] = None,
):
+1 -1
View File
@@ -8,7 +8,7 @@ from typing import Any, Dict, List
logger = logging.getLogger("run_agent")
def _fallback_entries(fallback_model) -> List[Dict[str, Any]]:
def _fallback_entries(fallback_model) -> list[dict[str, Any]]:
"""Normalize legacy single-dict ``fallback_model`` / list ``fallback_providers``."""
if isinstance(fallback_model, dict):
fallback_model = [fallback_model]
+81 -81
View File
@@ -107,7 +107,7 @@ _TRAJECTORY_SYSTEM_PROMPT = (
)
def _trajectory_gpt_prefix(msg: Dict[str, Any]) -> str:
def _trajectory_gpt_prefix(msg: dict[str, Any]) -> str:
"""Leading ``<think>`` block from native reasoning tokens, if any."""
if msg.get("reasoning") and msg["reasoning"].strip():
return f"<think>\n{msg['reasoning']}\n</think>\n"
@@ -119,7 +119,7 @@ def _with_think_block(content: str) -> str:
return content if "<think>" in content else "<think>\n</think>\n" + content
def _trajectory_tool_call_turn(msg: Dict[str, Any]) -> str:
def _trajectory_tool_call_turn(msg: dict[str, Any]) -> str:
content = _trajectory_gpt_prefix(msg)
if msg.get("content") and msg["content"].strip():
# <REASONING_SCRATCHPAD> -> <think> (model reasons via XML when native thinking is off)
@@ -139,7 +139,7 @@ def _trajectory_tool_call_turn(msg: Dict[str, Any]) -> str:
return _with_think_block(content).rstrip()
def _trajectory_tool_responses(msg: Dict[str, Any], messages: List[Dict[str, Any]], start: int) -> Tuple[List[str], int]:
def _trajectory_tool_responses(msg: dict[str, Any], messages: list[dict[str, Any]], start: int) -> tuple[list[str], int]:
"""Collect the ``<tool_response>`` blocks for the tool run starting at ``start``; returns ``(blocks, next_index)``."""
tool_responses = []
j = start
@@ -166,7 +166,7 @@ def _trajectory_tool_responses(msg: Dict[str, Any], messages: List[Dict[str, Any
return tool_responses, j
def convert_to_trajectory_format(agent, messages: List[Dict[str, Any]], user_query: str, completed: bool) -> List[Dict[str, Any]]:
def convert_to_trajectory_format(agent, messages: list[dict[str, Any]], user_query: str, completed: bool) -> list[dict[str, Any]]:
"""Convert internal message history to trajectory format for saving."""
# Trajectories are text-only: swap image-bearing tool messages for their text_summary so ~1MB
# base64 blobs are not embedded.
@@ -313,7 +313,7 @@ def sanitize_tool_call_arguments(
# Session-scoped in-flight registry for note_turn_start: the gateway caches agents per routing key
# while the transcript is keyed by session_id, so two agent objects can run concurrent turns on one
# session unseen by per-agent state.
_INFLIGHT_TURNS_BY_SESSION: Dict[str, Tuple[str, float]] = {}
_INFLIGHT_TURNS_BY_SESSION: dict[str, tuple[str, float]] = {}
_INFLIGHT_TURNS_LOCK = threading.Lock()
@@ -374,7 +374,7 @@ def note_turn_persisted(agent):
agent._inflight_turn_session_id = None
def _is_codex_interim(m: Dict) -> bool:
def _is_codex_interim(m: dict) -> bool:
"""Codex Responses interim turn: carries its own continuation state, replayed verbatim."""
return bool(
m.get("codex_reasoning_items")
@@ -383,7 +383,7 @@ def _is_codex_interim(m: Dict) -> bool:
)
def _merge_assistant_into(prev: Dict, msg: Dict) -> bool:
def _merge_assistant_into(prev: dict, msg: dict) -> bool:
"""Fold consecutive assistant *msg* into *prev* (union tool_calls, concat text). Returns whether *msg*'s
text survives: multimodal (list) content is never joined."""
from agent.context_compressor import _DB_PERSISTED_MARKER
@@ -463,7 +463,7 @@ def _merge_assistant_into(prev: Dict, msg: Dict) -> bool:
return text_kept
def _remember_absorbed_row(survivor: Dict[str, Any], dropped: Dict[str, Any], *, folded: bool) -> None:
def _remember_absorbed_row(survivor: dict[str, Any], dropped: dict[str, Any], *, folded: bool) -> None:
"""Retire *dropped*'s row ids onto *survivor*; record its uid as a merge witness only when *folded* (its
text survives). An empty incoming turn still merges; stamping an empty list would change a message that
absorbed nothing. A dropped id that equals the survivor's own live id (the display-marker merge adopts
@@ -496,7 +496,7 @@ def _remember_absorbed_row(survivor: Dict[str, Any], dropped: Dict[str, Any], *,
record_absorbed_message(survivor, dropped)
def _count_unnamed_row(survivor: Dict[str, Any], retired: Dict[str, Any]) -> None:
def _count_unnamed_row(survivor: dict[str, Any], retired: dict[str, Any]) -> None:
"""On a reload without row ids nothing names *retired*'s durable row once the repair takes the dict
out of the list, so *survivor* counts it. A dict that counts rows was loaded too: a merge may have
popped its marker since. Call before the merge rewrites the survivor: *retired*'s own fields are
@@ -505,7 +505,7 @@ def _count_unnamed_row(survivor: Dict[str, Any], retired: Dict[str, Any]) -> Non
from agent.conversation_compression_archive import (
OWN_ROW, RETIRED_DURABLE_ROWS, UNNAMED_DURABLE_ROWS, retired_row_payload)
def loaded(message: Dict[str, Any]) -> bool:
def loaded(message: dict[str, Any]) -> bool:
return bool(message.get(_DB_PERSISTED_MARKER) or message.get(UNNAMED_DURABLE_ROWS))
if loaded(survivor) and loaded(retired) and not isinstance(retired.get("_row_id"), int):
@@ -516,7 +516,7 @@ def _count_unnamed_row(survivor: Dict[str, Any], retired: Dict[str, Any]) -> Non
survivor.setdefault(RETIRED_DURABLE_ROWS, []).append(retired_row_payload(retired))
def _remember_own_row(survivor: Dict[str, Any]) -> None:
def _remember_own_row(survivor: dict[str, Any]) -> None:
"""An assistant fold rewrites *survivor*'s text, so on a reload without row ids its own row no longer
matches it by content. Record its loaded fields first, once."""
from agent.context_compressor import _DB_PERSISTED_MARKER
@@ -528,7 +528,7 @@ def _remember_own_row(survivor: Dict[str, Any]) -> None:
survivor.setdefault(RETIRED_DURABLE_ROWS, []).append({**retired_row_payload(survivor), OWN_ROW: True})
def _retire_dropped_row(kept: List[Dict], dropped: Dict[str, Any], leading: List[Dict]) -> None:
def _retire_dropped_row(kept: list[dict], dropped: dict[str, Any], leading: list[dict]) -> None:
"""A durable row the repair drops was still handed to the caller, so the survivor before it stands
for it. Left unnamed, an in-place compaction takes it for a row another surface appended and
re-sequences it behind the running turn. A drop with no survivor before it waits in *leading*."""
@@ -539,17 +539,17 @@ def _retire_dropped_row(kept: List[Dict], dropped: Dict[str, Any], leading: List
_remember_absorbed_row(kept[-1], dropped, folded=False)
def _retire_leading_drops(kept: List[Dict], leading: List[Dict]) -> None:
def _retire_leading_drops(kept: list[dict], leading: list[dict]) -> None:
"""Rows dropped ahead of the first survivor sit right before its own row, so it stands for them."""
for dropped in leading if kept and isinstance(kept[0], dict) else ():
_count_unnamed_row(kept[0], dropped)
_remember_absorbed_row(kept[0], dropped, folded=False)
def _merge_consecutive_assistants(messages: List[Dict]) -> Tuple[List[Dict], int]:
def _merge_consecutive_assistants(messages: list[dict]) -> tuple[list[dict], int]:
"""Pass 0: merge consecutive assistant turns (codex interims exempt)."""
repairs = 0
collapsed: List[Dict] = []
collapsed: list[dict] = []
for msg in messages:
prev = collapsed[-1] if collapsed and isinstance(collapsed[-1], dict) else None
if (
@@ -572,12 +572,12 @@ def _merge_consecutive_assistants(messages: List[Dict]) -> Tuple[List[Dict], int
return collapsed, repairs
def _drop_stray_tool_results(messages: List[Dict]) -> Tuple[List[Dict], int]:
def _drop_stray_tool_results(messages: list[dict]) -> tuple[list[dict], int]:
"""Pass 1: drop tool results not following a known assistant tool call. Consumes the whole
alias group (call_id/id/response_item_id/composite) so a duplicate keyed on a sibling
alias is not replayed to strict providers."""
repairs = 0
known_tool_ids: Dict[str, int] = {} # alias -> group id; reset by assistant/user turns
known_tool_ids: dict[str, int] = {} # alias -> group id; reset by assistant/user turns
# Pass 1: drop stray tool messages that don't follow a known assistant tool call. A Responses call can
# have several equivalent spellings (call_id, id, response_item_id, or a composite ``call|item`` id), so
# consume the whole alias group when one spelling is matched. Alias expansion lives in
@@ -585,8 +585,8 @@ def _drop_stray_tool_results(messages: List[Dict]) -> Tuple[List[Dict], int]:
# owner) — which also handles SDK tool_call objects, preserving the #91768 dict-or-object tolerance.
matched_tool_groups: set = set()
next_tool_group = 0
filtered: List[Dict] = []
leading: List[Dict] = []
filtered: list[dict] = []
leading: list[dict] = []
for msg in messages:
role = msg.get("role") if isinstance(msg, dict) else None
if role in ("assistant", "user"):
@@ -617,15 +617,15 @@ def _drop_stray_tool_results(messages: List[Dict]) -> Tuple[List[Dict], int]:
return filtered, repairs
def _prune_unanswered_tool_calls(messages: List[Dict]) -> Tuple[List[Dict], int]:
def _prune_unanswered_tool_calls(messages: list[dict]) -> tuple[list[dict], int]:
"""Pass 2: prune tool_calls not answered in the IMMEDIATELY following tool run (a displaced
result masks the per-call stub pass and strict providers 400). Payload-empty turns are
dropped; codex interims exempt."""
from agent.context_compressor import _DB_PERSISTED_MARKER
repairs = 0
pruned: List[Dict] = []
leading: List[Dict] = []
pruned: list[dict] = []
leading: list[dict] = []
for i, msg in enumerate(messages):
if not (
isinstance(msg, dict) and msg.get("role") == "assistant" and msg.get("tool_calls")
@@ -659,7 +659,7 @@ def _prune_unanswered_tool_calls(messages: List[Dict]) -> Tuple[List[Dict], int]
return pruned, repairs
def _merge_consecutive_users(messages: List[Dict]) -> Tuple[List[Dict], int]:
def _merge_consecutive_users(messages: list[dict]) -> tuple[list[dict], int]:
"""Pass 3: merge consecutive plain-text user messages (no user input lost)."""
from agent.context_compressor import _DB_PERSISTED_MARKER, split_user_originated_turn
from agent.conversation_compression_archive import MERGED_DURABLE_ROWS
@@ -670,7 +670,7 @@ def _merge_consecutive_users(messages: List[Dict]) -> Tuple[List[Dict], int]:
return isinstance(content, str) and not content.startswith(SessionDB._CONTENT_JSON_PREFIX)
repairs = 0
merged: List[Dict] = []
merged: list[dict] = []
for msg in messages:
prev = merged[-1] if merged and isinstance(merged[-1], dict) else None
if (
@@ -744,7 +744,7 @@ _SEQUENCE_REPAIR_PASSES = (
_merge_consecutive_users,
)
def _normalize_sentinel_encoded_content(messages: List[Dict]) -> None:
def _normalize_sentinel_encoded_content(messages: list[dict]) -> None:
"""Decode any sentinel-encoded row content in place before the alternation passes run, so an
image-bearing turn is never merged as text (#125299). A multimodal turn can re-enter the working set
as its ``\\x00json:[…]`` string (e.g. after a proactive prune re-inserts history, #124102). A body
@@ -762,7 +762,7 @@ def _normalize_sentinel_encoded_content(messages: List[Dict]) -> None:
# it makes the append-only flush re-append this user turn as a duplicate (#125331).
def repair_message_sequence(agent, messages: List[Dict]) -> int:
def repair_message_sequence(agent, messages: list[dict]) -> int:
"""Collapse malformed role-alternation left in the live history; returns repair count.
Providers require strict alternation after the system message (violations: silent empty
responses or 400s); this is the pre-call belt for host-fed, resumed or replayed histories.
@@ -785,7 +785,7 @@ def repair_message_sequence(agent, messages: List[Dict]) -> int:
return repairs
def repair_message_sequence_with_cursor(agent, messages: List[Dict]) -> int:
def repair_message_sequence_with_cursor(agent, messages: list[dict]) -> int:
"""Run :func:`repair_message_sequence` and keep ``_last_flushed_db_idx`` consistent. Repair
shrinks the list in place; counting identity-preserved survivors of the flushed prefix gives
the exact new cursor, whereas a ``min()`` clamp would skip unflushed rows (used only without a snapshot)."""
@@ -876,7 +876,7 @@ _USAGE_LIMIT_REASON_TOKENS = ("usage_limit_reached", "gousagelimit")
_USAGE_LIMIT_MESSAGE_TOKENS = ("usage limit reached", "usage limit has been reached")
def _failed_credential_identity(agent, pool) -> Tuple[Optional[str], Optional[str]]:
def _failed_credential_identity(agent, pool) -> tuple[Optional[str], Optional[str]]:
"""``(api_key_hint, credential_id)`` of the key actually dispatched, not ``pool.current()``:
the shared pointer often points at a different healthy entry, and marking it exhausted
can take the whole pool offline from one 429."""
@@ -1006,7 +1006,7 @@ def _recover_rate_limit(pool, *, has_retried_429, error_context, api_key_hint, c
def recover_with_credential_pool(
agent, *, status_code: Optional[int], has_retried_429: bool,
classified_reason: Optional[FailoverReason] = None,
error_context: Optional[Dict[str, Any]] = None, billing_unverified: bool = False,
error_context: Optional[dict[str, Any]] = None, billing_unverified: bool = False,
) -> tuple[bool, bool]:
"""Attempt credential recovery via pool rotation; returns (recovered, has_retried_429).
Rate limits: retry once, then rotate. Billing: rotate immediately. Auth: refresh before
@@ -1133,7 +1133,7 @@ def recover_with_credential_pool(
return False, has_retried_429
def _apply_primary_runtime_fields(agent, rt: Dict[str, Any]) -> None:
def _apply_primary_runtime_fields(agent, rt: dict[str, Any]) -> None:
"""Copy the identity/transport fields of a ``_primary_runtime`` snapshot onto ``agent``
(shared by transport recovery and turn-start restore; the caller rebuilds the client)."""
agent.model = rt["model"]
@@ -1150,7 +1150,7 @@ def _apply_primary_runtime_fields(agent, rt: Dict[str, Any]) -> None:
agent._client_kwargs = dict(rt["client_kwargs"])
def _build_anthropic_client_from_runtime(agent, rt: Dict[str, Any]) -> None:
def _build_anthropic_client_from_runtime(agent, rt: dict[str, Any]) -> None:
"""Rebuild the native Anthropic client from a ``_primary_runtime`` snapshot."""
from agent.anthropic_adapter import build_anthropic_client
agent._anthropic_api_key = rt["anthropic_api_key"]
@@ -1163,7 +1163,7 @@ def _build_anthropic_client_from_runtime(agent, rt: Dict[str, Any]) -> None:
agent.client = None
def _rebuild_primary_client(agent, rt: Dict[str, Any], *, reason: str) -> None:
def _rebuild_primary_client(agent, rt: dict[str, Any], *, reason: str) -> None:
"""Rebuild the primary client from a ``_primary_runtime`` snapshot (MoA facade / native Anthropic / OpenAI wire)."""
if (agent.provider or "").strip().lower() == "moa":
# MoA has empty client_kwargs; rebuild via the shared facade factory so the
@@ -1243,9 +1243,9 @@ _UNMERGEABLE = object()
def drop_thinking_only_and_merge_users(
messages: List[Dict[str, Any]], *, drop_codex_reasoning_items: bool = True,
messages: list[dict[str, Any]], *, drop_codex_reasoning_items: bool = True,
drop_nudge_marker: Optional[str] = None,
) -> List[Dict[str, Any]]:
) -> list[dict[str, Any]]:
"""Drop thinking-only assistant turns and merge adjacent user messages left behind, on the
per-call ``api_messages`` copy only (``agent.messages`` is never mutated). Drop-and-merge
(not stub text) keeps history honest and preserves role alternation.
@@ -1262,7 +1262,7 @@ def drop_thinking_only_and_merge_users(
and not _ra().AIAgent._is_thinking_only_assistant(m, drop_codex_reasoning_items=drop_codex_reasoning_items)
]
dropped = len(messages) - len(kept)
merged: List[Dict[str, Any]] = []
merged: list[dict[str, Any]] = []
merges = 0
for m in kept:
prev = merged[-1] if merged else None
@@ -1312,7 +1312,7 @@ def _primary_reset_gate_blocks(agent, rt, primary_provider, primary_runtime_base
return False, prefetched_pool, prefetched
def _restore_runtime_capabilities(agent, rt: Dict[str, Any]) -> None:
def _restore_runtime_capabilities(agent, rt: dict[str, Any]) -> None:
# ``capabilities`` is the legacy key from the initial capability propagation patch.
raw = rt["runtime_capabilities"] if "runtime_capabilities" in rt else rt.get("capabilities")
if isinstance(raw, dict):
@@ -1508,7 +1508,7 @@ def extract_reasoning(agent, assistant_message) -> Optional[str]:
(OpenRouter unified), else inline thinking blocks in the content; None when absent."""
from agent.message_content import flatten_message_text
parts: List[str] = []
parts: list[str] = []
def _add(text) -> None:
text = flatten_message_text(text, sep="")
@@ -1539,8 +1539,8 @@ def extract_reasoning(agent, assistant_message) -> Optional[str]:
return "\n\n".join(parts) if parts else None
def _api_error_debug_info(error: Exception) -> Dict[str, Any]:
info: Dict[str, Any] = {"type": type(error).__name__, "message": str(error)}
def _api_error_debug_info(error: Exception) -> dict[str, Any]:
info: dict[str, Any] = {"type": type(error).__name__, "message": str(error)}
info.update({
k: v for k in ("status_code", "request_id", "code", "param", "type", "body")
if (v := getattr(error, k, None)) is not None
@@ -1556,7 +1556,7 @@ def _api_error_debug_info(error: Exception) -> Dict[str, Any]:
def dump_api_request_debug(
agent, api_kwargs: Dict[str, Any], *, reason: str, error: Optional[Exception] = None
agent, api_kwargs: dict[str, Any], *, reason: str, error: Optional[Exception] = None
) -> Optional[Path]:
"""Dump the request body from api_kwargs (minus transport keys) for debugging provider 4xx failures."""
try:
@@ -1573,7 +1573,7 @@ def dump_api_request_debug(
endpoint = {"codex_responses": "/responses", "anthropic_messages": "/messages"}.get(
agent.api_mode, "/chat/completions"
)
dump_payload: Dict[str, Any] = {
dump_payload: dict[str, Any] = {
"timestamp": datetime.now().isoformat(), "session_id": agent.session_id, "reason": reason,
"request": {
"method": "POST", "url": f"{agent.base_url.rstrip('/')}{endpoint}",
@@ -1669,7 +1669,7 @@ def plan_cache_sections_for_destination(
messages: list, tools: Optional[list], *, provider: str, base_url: str, api_mode: str,
model: str, cache_disabled: Optional[bool] = None, cache_ttl: Optional[str] = None,
static_system_prefix: Optional[str] = None,
) -> Tuple[list, list]:
) -> tuple[list, list]:
"""Plan request-local cache sections for one resolved destination (MoA / auxiliary senders):
stripped copies (non-caching route) or a ``build_prompt_cache_plan`` layout; never mutates
inputs. ``cache_disabled``/``cache_ttl`` default to live config so the operator's disable and
@@ -2132,7 +2132,7 @@ _SWITCH_SNAPSHOT_FIELDS = (
_MISSING = object()
def _snapshot_switch_state(agent) -> Dict[str, Any]:
def _snapshot_switch_state(agent) -> dict[str, Any]:
"""Snapshot every field the swap+rebuild mutates so a failed rebuild rolls back atomically
(else a new model name + OLD client 400s next turn). The sentinel distinguishes unset from
None: tests build bare agents via ``__new__`` without all fields."""
@@ -2142,7 +2142,7 @@ def _snapshot_switch_state(agent) -> Dict[str, Any]:
return snapshot
def _restore_switch_snapshot(agent, snapshot: Dict[str, Any]) -> None:
def _restore_switch_snapshot(agent, snapshot: dict[str, Any]) -> None:
for name, value in snapshot.items():
if value is _MISSING:
continue # attribute did not exist before the swap; don't fabricate it
@@ -2221,7 +2221,7 @@ def _build_switched_client(agent, new_provider, api_key, base_url, api_mode, new
try:
from hermes_cli.auth import build_minimax_oauth_token_provider
effective_key = build_minimax_oauth_token_provider()
except Exception as _mm_exc: # noqa: BLE001
except Exception as _mm_exc:
logger.warning(
"MiniMax OAuth: failed to install per-request token provider "
"on switch (%s); using static bearer.", _mm_exc,
@@ -2299,7 +2299,7 @@ def _swap_switch_runtime(agent, new_model, new_provider, api_key, base_url, api_
try:
from agent.credential_pool import load_pool
agent._credential_pool = load_pool(new_provider)
except Exception as _pool_exc: # noqa: BLE001
except Exception as _pool_exc:
logger.warning(
"switch_model: credential pool reload failed for %s (%s); "
"continuing without pool rotation this turn", new_provider, _pool_exc,
@@ -2384,7 +2384,7 @@ def _update_switch_compressor(agent, custom_providers, effective_context_length,
revalidate_compression_feasibility(agent)
def _build_primary_runtime_snapshot(agent, api_mode) -> Dict[str, Any]:
def _build_primary_runtime_snapshot(agent, api_mode) -> dict[str, Any]:
"""The ``_primary_runtime`` record that persists a switch across turns."""
cc = getattr(agent, "context_compressor", None) or None
rt = {
@@ -2548,7 +2548,7 @@ def invoke_tool(agent, function_name: str, function_args: dict, effective_task_i
tool_call_id: Optional[str] = None, messages: list = None,
pre_tool_block_checked: bool = False,
skip_tool_request_middleware: bool = False,
tool_request_middleware_trace: Optional[List[Dict[str, Any]]] = None,
tool_request_middleware_trace: Optional[list[dict[str, Any]]] = None,
skip_tool_execution_middleware: bool = False) -> str:
"""Invoke a single tool (agent-level or registry-dispatched) and return the result string;
no display logic. Used by the concurrent path; the sequential path keeps its own inline
@@ -2651,8 +2651,8 @@ def repair_tool_call(agent, tool_name: str) -> str | None:
tool_name = tool_name[:_idx]
if not tool_name:
return None
_norm = lambda s: s.lower().replace("-", "_").replace(" ", "_") # noqa: E731
_camel_snake = lambda s: re.sub(r"(?<!^)(?=[A-Z])", "_", s).lower() # noqa: E731
_norm = lambda s: s.lower().replace("-", "_").replace(" ", "_")
_camel_snake = lambda s: re.sub(r"(?<!^)(?=[A-Z])", "_", s).lower()
def _strip_tool_suffix(s: str) -> str | None:
lc = s.lower()
@@ -2687,7 +2687,7 @@ def repair_tool_call(agent, tool_name: str) -> str | None:
# still fire per window).
_EMPTY_HEAL_ESCALATE_AFTER = 3
_EMPTY_HEAL_WINDOW_S = 600.0
_empty_heal_log_state: Dict[str, Dict[str, Any]] = {}
_empty_heal_log_state: dict[str, dict[str, Any]] = {}
_empty_heal_log_lock = threading.Lock()
# Sessions already told ONCE (out-of-band, never in conversation context); kept apart from the
# windowed log state so a new window never re-arms the notice.
@@ -2697,7 +2697,7 @@ _empty_heal_log_lock = threading.Lock()
_empty_heal_user_notified: set = set()
# One-shot pending notices keyed by session, drained via ``consume_pending_sanitizer_heal_notice``
# and delivered via the status/warning callback.
_empty_heal_pending_notice: Dict[str, str] = {}
_empty_heal_pending_notice: dict[str, str] = {}
def _content_has_payload(content: Any) -> bool:
@@ -2713,7 +2713,7 @@ def _content_has_payload(content: Any) -> bool:
)
def _msg_has_payload(msg: Dict[str, Any]) -> bool:
def _msg_has_payload(msg: dict[str, Any]) -> bool:
"""True if ``msg`` carries anything the API treats as non-empty content (text, multimodal
blocks, tool_calls, reasoning). Role-agnostic counterpart of ``AIAgent._is_thinking_only_assistant``.
Codex Responses item carriers persist with content:"" by design (text lives in codex_*_items
@@ -2728,7 +2728,7 @@ def _msg_has_payload(msg: Dict[str, Any]) -> bool:
)
def fill_empty_non_final_wire_payload(msg: Dict[str, Any], *, is_final: bool) -> bool:
def fill_empty_non_final_wire_payload(msg: dict[str, Any], *, is_final: bool) -> bool:
"""Write the interrupted placeholder onto an empty non-final wire copy; True when filled.
Pass the per-call copy only; durable history must not be mutated."""
if is_final or not isinstance(msg, dict) or msg.get("role") not in ("user", "assistant"):
@@ -2765,7 +2765,7 @@ def consume_pending_sanitizer_heal_notice() -> Optional[str]:
return _empty_heal_pending_notice.pop(key, None)
def get_sanitizer_heal_stats() -> Dict[str, Dict[str, Any]]:
def get_sanitizer_heal_stats() -> dict[str, dict[str, Any]]:
"""Read-only per-session sanitiser heal counters (``heal_events``, ``messages_healed``, ``escalated``) for diagnostics."""
with _empty_heal_log_lock:
return {
@@ -2830,14 +2830,14 @@ def _log_empty_non_final_heal(healed: int) -> None:
)
def repair_empty_non_final_messages(messages: List[Dict[str, Any]]) -> List[Dict[str, Any]]:
def repair_empty_non_final_messages(messages: list[dict[str, Any]]) -> list[dict[str, Any]]:
"""Substitute a placeholder for empty-content non-final messages on the per-call copy.
Anthropic/litellm/Bedrock 400 on any empty non-final message and a persisted stub poisons
every later turn; repairing the wire copy heals the session in memory. Substitution (not
deletion) keeps role alternation and tool-call pairing intact. The final message is untouched."""
if not messages or len(messages) < 2:
return messages
repaired: List[Dict[str, Any]] = []
repaired: list[dict[str, Any]] = []
healed = 0
last_idx = len(messages) - 1
for idx, msg in enumerate(messages):
@@ -2854,7 +2854,7 @@ def repair_empty_non_final_messages(messages: List[Dict[str, Any]]) -> List[Dict
return messages
def _classify_tool_call_orphans(messages: List[Dict[str, Any]]):
def _classify_tool_call_orphans(messages: list[dict[str, Any]]):
"""Classify orphaned tool-call / tool-result pairs; single source of truth for GLOBAL orphan
detection. Returns ``(surviving_call_ids, result_call_ids, orphaned_results, missing_tool_calls)``;
every id variant of a tool_call is registered so a result matching any alias survives, and
@@ -2880,7 +2880,7 @@ def _classify_tool_call_orphans(messages: List[Dict[str, Any]]):
return surviving_call_ids, result_call_ids, orphaned_results, missing_tool_calls
def _drop_invalid_roles(messages: List[Dict[str, Any]]) -> List[Dict[str, Any]]:
def _drop_invalid_roles(messages: list[dict[str, Any]]) -> list[dict[str, Any]]:
"""Drop messages whose role the API won't accept."""
valid = _ra().AIAgent._VALID_API_ROLES
for msg in messages:
@@ -2889,7 +2889,7 @@ def _drop_invalid_roles(messages: List[Dict[str, Any]]) -> List[Dict[str, Any]]:
return [m for m in messages if m.get("role") in valid]
def _drop_empty_tool_calls_arrays(messages: List[Dict[str, Any]]) -> List[Dict[str, Any]]:
def _drop_empty_tool_calls_arrays(messages: list[dict[str, Any]]) -> list[dict[str, Any]]:
"""Strict providers 400 on ``tool_calls: []``; normalize on shallow copies so history stays byte-stable."""
# --- Drop empty / malformed tool_calls arrays on assistant messages --- An assistant message carrying
# ``tool_calls: []`` (an empty array) — or a non-list value under the key — is semantically identical to
@@ -2902,7 +2902,7 @@ def _drop_empty_tool_calls_arrays(messages: List[Dict[str, Any]]) -> List[Dict[s
# per-call copy rather than in ``repair_message_sequence``, which would destructively rewrite the
# persisted trajectory. Shallow-copy the message before dropping the key so stored history (and prompt
# caching) stays byte-stable.
normalized: List[Dict[str, Any]] = []
normalized: list[dict[str, Any]] = []
dropped = 0
for msg in messages:
if (
@@ -2930,7 +2930,7 @@ def _drop_empty_tool_calls_arrays(messages: List[Dict[str, Any]]) -> List[Dict[s
return normalized
def _repair_invalid_tool_call_names(messages: List[Dict[str, Any]]) -> None:
def _repair_invalid_tool_call_names(messages: list[dict[str, Any]]) -> None:
"""Coerce every ``function.name`` to the provider-safe ``^[A-Za-z0-9_-]{1,64}$``. An empty/missing
name becomes the ``invalid_tool_call`` sentinel (dropping would unpair the anti-priming result the
dispatch loop keeps for it); an invalid one (``multi_tool_use.parallel``, a shell command a weak
@@ -2970,7 +2970,7 @@ def _repair_invalid_tool_call_names(messages: List[Dict[str, Any]]) -> None:
}
def _drop_results_without_ids(messages: List[Dict[str, Any]]) -> List[Dict[str, Any]]:
def _drop_results_without_ids(messages: list[dict[str, Any]]) -> list[dict[str, Any]]:
"""Drop tool results with a missing/empty tool_call_id. Kept explicit (not left to the
positional walk) for its own log line and so the final-chokepoint guarantee holds for
callers skipping ``repair_message_sequence``."""
@@ -2986,7 +2986,7 @@ def _drop_results_without_ids(messages: List[Dict[str, Any]]) -> List[Dict[str,
return kept
def _pair_tool_calls_positionally(messages: List[Dict[str, Any]]) -> List[Dict[str, Any]]:
def _pair_tool_calls_positionally(messages: list[dict[str, Any]]) -> list[dict[str, Any]]:
"""Positional tool_call <-> tool_result pairing: strict providers (DeepSeek v4, Kimi) require
results IMMEDIATELY after their call. Drops positional orphans, stubs unanswered declared
ids; matching is alias-aware."""
@@ -3006,8 +3006,8 @@ def _pair_tool_calls_positionally(messages: List[Dict[str, Any]]) -> List[Dict[s
# (``tool_call_id_variants`` / ``tool_result_id_variants``): a result keyed on ANY alias spelling
# (``id`` / ``call_id`` / ``response_item_id`` / composite bridge) answers the call, preserving the
# unified alias policy from #55626/#63000/#93251.
paired: List[Dict[str, Any]] = []
declared_calls: Dict[str, tuple] = {}
paired: list[dict[str, Any]] = []
declared_calls: dict[str, tuple] = {}
dropped = 0
stubs = 0
@@ -3060,12 +3060,12 @@ def _pair_tool_calls_positionally(messages: List[Dict[str, Any]]) -> List[Dict[s
return paired if (dropped or stubs) else messages
def _dedupe_tool_call_ids(messages: List[Dict[str, Any]]) -> List[Dict[str, Any]]:
def _dedupe_tool_call_ids(messages: list[dict[str, Any]]) -> list[dict[str, Any]]:
"""Deduplicate tool_call_ids (strict providers 400 on duplicates): collapse duplicates within
an assistant message, drop results answering no OUTSTANDING call. Tracks outstanding calls
(not ids ever seen) because llama.cpp reuses one constant id, and whole variant groups so
alias-keyed results are not deleted."""
outstanding: Dict[str, int] = {} # every alias of an unanswered call -> its group id
outstanding: dict[str, int] = {} # every alias of an unanswered call -> its group id
# 3. Deduplicate tool_call_ids. Strict providers (DeepSeek) reject a payload where the same tool_call_id
# appears more than once with HTTP 400 "Duplicate value for 'tool_call_id'" (#58327). Duplicates can
# arise from retries, crash/resume glitches, or a compression window that re-emits a tool result. This
@@ -3084,9 +3084,9 @@ def _dedupe_tool_call_ids(messages: List[Dict[str, Any]]) -> List[Dict[str, Any]
# (#55626/#58168/#63000); tracking only the coalesced id here made a result keyed on any OTHER variant
# look like it answered no outstanding call, so this pass deleted the very result step 2's
# variant-aware matching had just preserved (issue #93251 — whole parallel batches vanished).
outstanding_groups: Dict[int, frozenset] = {}
outstanding_groups: dict[int, frozenset] = {}
next_group_id = 0
deduped: List[Dict[str, Any]] = []
deduped: list[dict[str, Any]] = []
removed = 0
for msg in messages:
role = msg.get("role")
@@ -3132,7 +3132,7 @@ def _dedupe_tool_call_ids(messages: List[Dict[str, Any]]) -> List[Dict[str, Any]
return deduped
def _realign_tool_result_names(messages: List[Dict[str, Any]]) -> List[Dict[str, Any]]:
def _realign_tool_result_names(messages: list[dict[str, Any]]) -> list[dict[str, Any]]:
"""Align each tool result's wire ``name`` with its call's function name (per-call copy only):
Google 400s on a mismatch, routine when tool_search bridges via ``tool_call``."""
# 4. Google matches functionResponse.name against functionCall.name and rejects a mismatch with HTTP 400
@@ -3151,7 +3151,7 @@ def _realign_tool_result_names(messages: List[Dict[str, Any]]) -> List[Dict[str,
# changes. A no-op for the native Gemini path, which already resolves the same name. A result whose
# assistant call frame is missing entirely never reaches here — pass 1 above drops it as an orphan —
# so the only results this pass sees are ones whose call name is knowable.
call_names: Dict[str, str] = {}
call_names: dict[str, str] = {}
for msg in messages:
if msg.get("role") == "assistant":
for tc in msg.get("tool_calls") or []:
@@ -3160,8 +3160,8 @@ def _realign_tool_result_names(messages: List[Dict[str, Any]]) -> List[Dict[str,
nm = _ra().AIAgent._get_tool_call_name_static(tc)
if cid and nm:
call_names[cid] = nm
realigned: List[Tuple[str, str]] = []
aligned: List[Dict[str, Any]] = []
realigned: list[tuple[str, str]] = []
aligned: list[dict[str, Any]] = []
for msg in messages:
if msg.get("role") == "tool":
expected = call_names.get((msg.get("tool_call_id") or "").strip())
@@ -3181,7 +3181,7 @@ def _realign_tool_result_names(messages: List[Dict[str, Any]]) -> List[Dict[str,
return aligned
def sanitize_api_messages(messages: List[Dict[str, Any]]) -> List[Dict[str, Any]]:
def sanitize_api_messages(messages: list[dict[str, Any]]) -> list[dict[str, Any]]:
"""Fix orphaned tool_call / tool_result pairs before every LLM call; runs unconditionally (not
gated on the compressor). Order matters: empty non-final messages are healed first so the
substituted turn participates in the pairing and dedup passes."""
@@ -3207,7 +3207,7 @@ _ACK_WORKSPACE_MARKERS = (
def looks_like_codex_intermediate_ack(
agent, user_message: Any, assistant_content: str, messages: List[Dict[str, Any]],
agent, user_message: Any, assistant_content: str, messages: list[dict[str, Any]],
require_workspace: bool = True,
) -> bool:
"""Detect a planning/ack message that should continue instead of ending the turn.
@@ -3398,7 +3398,7 @@ def looks_like_degenerate_final(text: str, user_message: Any = None) -> bool:
return not any(ch.isalpha() and not ch.isascii() for ch in user_text)
def tool_results_this_turn(messages: List[Dict[str, Any]]) -> int:
def tool_results_this_turn(messages: list[dict[str, Any]]) -> int:
"""Tool-result rows after the most recent user row — whether the turn did real tool work.
ANY user row ends the window, the continuation nudges included: that is what bounds the
@@ -3651,7 +3651,7 @@ def _iter_pool_sockets(client: Any):
yield sock
def _set_reset_from_retry_after(context: Dict[str, Any], retry_after: Any) -> None:
def _set_reset_from_retry_after(context: dict[str, Any], retry_after: Any) -> None:
if "reset_at" in context:
return
seconds = parse_retry_after_seconds(retry_after)
@@ -3685,7 +3685,7 @@ def _duration_string_seconds(text: str) -> Optional[float]:
return sum(float(n) * _DURATION_UNIT_SECONDS[u] for n, u in parts)
def _set_reset_from_vendor_headers(context: Dict[str, Any], headers: Any) -> None:
def _set_reset_from_vendor_headers(context: dict[str, Any], headers: Any) -> None:
for name in _VENDOR_RESET_HEADERS:
value = headers.get(name)
if not isinstance(value, str) or not value.strip():
@@ -3699,9 +3699,9 @@ def _set_reset_from_vendor_headers(context: Dict[str, Any], headers: Any) -> Non
return
def extract_api_error_context(error: Exception) -> Dict[str, Any]:
def extract_api_error_context(error: Exception) -> dict[str, Any]:
"""Extract structured rate-limit details from provider errors."""
context: Dict[str, Any] = {}
context: dict[str, Any] = {}
body = getattr(error, "body", None)
payload = (body.get("error") if isinstance(body.get("error"), dict) else body) if isinstance(body, dict) else None
if isinstance(payload, dict):
+14 -14
View File
@@ -248,14 +248,14 @@ _CLAUDE_CODE_PREFIXES = (
_CLAUDE_CODE_NAMES = ("claude", "claude-code")
def _claude_code_candidates() -> List[str]:
def _claude_code_candidates() -> list[str]:
"""Executable paths to try, deduped and filtered to files that exist.
Two passes: every PATH hit first (what the user's shell would run), then the
well-known install prefixes. A single nested loop would probe a stale prefix
``claude`` before a current PATH ``claude-code``.
"""
seen: Dict[str, None] = {}
seen: dict[str, None] = {}
for name in _CLAUDE_CODE_NAMES:
hit = shutil.which(name)
if hit:
@@ -351,12 +351,12 @@ def _common_betas_for_base_url(base_url: str | None, *, drop_context_1m_beta: bo
return betas
def _beta_header(betas: list) -> Dict[str, str]:
def _beta_header(betas: list) -> dict[str, str]:
"""``{"anthropic-beta": ...}`` when there are betas, else ``{}``."""
return {"anthropic-beta": ",".join(betas)} if betas else {}
def _attribution_headers() -> Dict[str, str]:
def _attribution_headers() -> dict[str, str]:
"""Same client-attribution set sent to OpenRouter / Vercel AI Gateway / Fireworks."""
return {
"HTTP-Referer": "https://hermes-agent.nousresearch.com", "X-Title": "Hermes Agent",
@@ -371,13 +371,13 @@ def _client_timeout(timeout):
return Timeout(timeout=float(read), connect=10.0)
def _base_client_kwargs(base_url, timeout) -> tuple[str, Dict[str, Any]]:
def _base_client_kwargs(base_url, timeout) -> tuple[str, dict[str, Any]]:
"""Shared SDK constructor kwargs -> ``(normalized_base_url, kwargs)``. Retry is delegated to
hermes's outer loop (``max_retries=0``): the SDK default of 2 uses its own backoff that ignores
Retry-After and double-retries inside our loop. Any trailing ``/v1`` is stripped because the
SDK appends ``/v1/messages``. Azure's ``api-version`` goes through ``default_query`` so the
base_url is not corrupted into ``/anthropic?api-version=.../v1/messages``."""
kwargs: Dict[str, Any] = {"timeout": _client_timeout(timeout), "max_retries": 0}
kwargs: dict[str, Any] = {"timeout": _client_timeout(timeout), "max_retries": 0}
normalized = re.sub(r"/v1/?$", "", _normalize_base_url_text(base_url).rstrip("/"))
if normalized:
kwargs["base_url"] = normalized
@@ -414,7 +414,7 @@ def _build_anthropic_client_with_bearer_hook(
return _new_sdk_client(sdk, kwargs, headers, route=base_url)
def _new_sdk_client(sdk, kwargs: Dict[str, Any], headers: Dict[str, str], route: str = None):
def _new_sdk_client(sdk, kwargs: dict[str, Any], headers: dict[str, str], route: str = None):
"""``sdk.Anthropic(**kwargs)`` with ``headers`` attached, sending exactly ONE credential.
``route`` is the caller's un-normalized base_url (the ``/v1`` form ``custom_providers`` entries are
keyed by; ``kwargs["base_url"]`` has it stripped) for the per-provider ``extra_headers`` lookup.
@@ -439,7 +439,7 @@ def _new_sdk_client(sdk, kwargs: Dict[str, Any], headers: Dict[str, str], route:
return sdk.Anthropic(**kwargs)
def _custom_provider_extra_headers(base_url) -> Dict[str, str]:
def _custom_provider_extra_headers(base_url) -> dict[str, str]:
"""``extra_headers`` of the ``custom_providers`` entry routed at *base_url*, else ``{}``.
SECURITY: values routinely carry credentials (Cloudflare Access tokens) — never log them."""
if not base_url:
@@ -537,7 +537,7 @@ def _normalize_to_mcp_wire(name: str) -> str:
return _MCP_TOOL_PREFIX + name.removeprefix("mcp_")
def _oauth_wire_namer(anthropic_tools: List[Dict[str, Any]]):
def _oauth_wire_namer(anthropic_tools: list[dict[str, Any]]):
"""Return ``name -> OAuth wire name`` for this request's tool set. An alias must never collide
with a wire name owned by a non-alias tool: two identical tool names in one request is a hard
400, strictly worse than the bug being fixed. Mirrors normalize_response's "registered tool
@@ -596,7 +596,7 @@ def _apply_claude_code_identity(system, anthropic_tools, anthropic_messages, to_
return system
def _thinking_kwargs(reasoning_config: Dict[str, Any], model: str, effective_max_tokens: int) -> Dict[str, Any]:
def _thinking_kwargs(reasoning_config: dict[str, Any], model: str, effective_max_tokens: int) -> dict[str, Any]:
"""Map ``reasoning_config`` to Anthropic thinking kwargs. Adaptive models (Claude 4.6+,
Kimi/Moonshot) get ``thinking.type=adaptive`` + ``output_config.effort``; older models and
manual-only compat endpoints (MiniMax) get budget_tokens. Haiku has no extended thinking. On
@@ -630,11 +630,11 @@ _TOOL_CHOICE_MAP = {None: {"type": "auto"}, "auto": {"type": "auto"}, "required"
def build_anthropic_kwargs(
model: str, messages: List[Dict], tools: Optional[List[Dict]], max_tokens: Optional[int],
reasoning_config: Optional[Dict[str, Any]], tool_choice: Optional[str] = None,
model: str, messages: list[dict], tools: Optional[list[dict]], max_tokens: Optional[int],
reasoning_config: Optional[dict[str, Any]], tool_choice: Optional[str] = None,
is_oauth: bool = False, preserve_dots: bool = False, context_length: Optional[int] = None,
base_url: str | None = None, fast_mode: bool = False, drop_context_1m_beta: bool = False,
) -> Dict[str, Any]:
) -> dict[str, Any]:
"""Build kwargs for anthropic.messages.create(). ``max_tokens`` is the OUTPUT cap for one
response; ``context_length`` is the TOTAL window (input + output). ``max_tokens=None`` uses the
model's native output ceiling; if that exceeds ``context_length`` (small local endpoints) it is
@@ -656,7 +656,7 @@ def build_anthropic_kwargs(
to_wire = _oauth_wire_namer(anthropic_tools) if is_oauth else None
if to_wire:
system = _apply_claude_code_identity(system, anthropic_tools, anthropic_messages, to_wire)
kwargs: Dict[str, Any] = {"model": model, "messages": anthropic_messages, "max_tokens": effective_max_tokens}
kwargs: dict[str, Any] = {"model": model, "messages": anthropic_messages, "max_tokens": effective_max_tokens}
if system:
kwargs["system"] = system
if anthropic_tools:
+23 -23
View File
@@ -85,7 +85,7 @@ def anthropic_route_is_oauth(base_url: Any, credential: Any, *, provider: Option
if callable(credential) and not isinstance(credential, str):
try:
credential = credential()
except Exception: # noqa: BLE001 — classification must never raise
except Exception:
return False
return isinstance(credential, str) and _is_oauth_token(credential)
@@ -223,7 +223,7 @@ _SINGLETON_SOURCE_PATHS = {
}
def _claude_oauth_record(data: Any, source: str) -> Optional[Dict[str, Any]]:
def _claude_oauth_record(data: Any, source: str) -> Optional[dict[str, Any]]:
"""Normalise a ``{"claudeAiOauth": {...}}`` payload into our credential dict."""
oauth_data = data.get("claudeAiOauth")
access_token = oauth_data.get("accessToken", "") if isinstance(oauth_data, dict) else ""
@@ -252,7 +252,7 @@ def _decode_keychain_attr(match: Optional["re.Match[str]"]) -> str:
return match.group("text") or ""
def _find_claude_code_keychain_item() -> Optional[tuple[str, Dict[str, Any]]]:
def _find_claude_code_keychain_item() -> Optional[tuple[str, dict[str, Any]]]:
"""``(account, payload)`` of the ``Claude Code-credentials`` login Keychain item, or None.
One ``find-generic-password -g`` call: attributes on stdout, ``password: …`` on stderr. The
@@ -270,8 +270,8 @@ def _find_claude_code_keychain_item() -> Optional[tuple[str, Dict[str, Any]]]:
return None
if result.returncode != 0:
return None
account = _decode_keychain_attr(re.search(r'^\s*"acct"<blob>=' + _KEYCHAIN_ATTR + r"\s*$", result.stdout, re.M))
raw = _decode_keychain_attr(re.search(r"^password: " + _KEYCHAIN_ATTR + r"\s*$", result.stderr, re.M))
account = _decode_keychain_attr(re.search(r'^\s*"acct"<blob>=' + _KEYCHAIN_ATTR + r"\s*$", result.stdout, re.MULTILINE))
raw = _decode_keychain_attr(re.search(r"^password: " + _KEYCHAIN_ATTR + r"\s*$", result.stderr, re.MULTILINE))
if not account or not raw:
return None
try:
@@ -281,7 +281,7 @@ def _find_claude_code_keychain_item() -> Optional[tuple[str, Dict[str, Any]]]:
return (account, payload) if isinstance(payload, dict) else None
def _read_claude_code_keychain_payload() -> Optional[Dict[str, Any]]:
def _read_claude_code_keychain_payload() -> Optional[dict[str, Any]]:
"""Raw ``{"claudeAiOauth": {...}, ...}`` payload from the macOS Keychain, or None.
Returns the full entry (not the normalised credential record) so a refresh
@@ -312,7 +312,7 @@ def _read_claude_code_keychain_payload() -> Optional[Dict[str, Any]]:
return payload if isinstance(payload, dict) else None
def _keychain_mirror_command(account: str, payload: Dict[str, Any]) -> tuple[list[str], str]:
def _keychain_mirror_command(account: str, payload: dict[str, Any]) -> tuple[list[str], str]:
"""``(argv, stdin)`` that updates the Claude Code Keychain item with ``payload``.
The command line goes to ``security -i`` on stdin, with the secret hex-encoded (``-X``):
@@ -328,7 +328,7 @@ def _keychain_mirror_command(account: str, payload: Dict[str, Any]) -> tuple[lis
return ["security", "-i"], line
def _read_claude_code_credentials_from_keychain() -> Optional[Dict[str, Any]]:
def _read_claude_code_credentials_from_keychain() -> Optional[dict[str, Any]]:
"""Read the "Claude Code-credentials" macOS Keychain entry (Claude Code >=2.1.114)."""
payload = _read_claude_code_keychain_payload()
return _claude_oauth_record(payload, "macos_keychain") if payload else None
@@ -343,12 +343,12 @@ def claude_code_credentials_path() -> Path:
return root / ".credentials.json"
def _read_claude_code_credentials_from_file() -> Optional[Dict[str, Any]]:
def _read_claude_code_credentials_from_file() -> Optional[dict[str, Any]]:
data = _load_json_if_exists(claude_code_credentials_path(), "~/.claude/.credentials.json")
return _claude_oauth_record(data, "claude_code_credentials_file") if data is not None else None
def read_claude_code_credentials() -> Optional[Dict[str, Any]]:
def read_claude_code_credentials() -> Optional[dict[str, Any]]:
"""Read refreshable Claude Code OAuth credentials (Keychain and/or file). When both exist: prefer the only
non-expired one (Claude Code 2.1.x refreshes one source but not the other), else the later ``expiresAt`` so a
refresh uses the freshest refreshToken. ~/.claude.json primaryApiKey is deliberately excluded.
@@ -368,7 +368,7 @@ def read_claude_code_credentials() -> Optional[Dict[str, Any]]:
return kc_creds if (kc_creds.get("expiresAt", 0) or 0) >= (file_creds.get("expiresAt", 0) or 0) else file_creds
def is_claude_code_token_valid(creds: Dict[str, Any]) -> bool:
def is_claude_code_token_valid(creds: dict[str, Any]) -> bool:
"""Non-expired access token (60s buffer); no expiresAt means managed key → valid if present."""
expires_at = creds.get("expiresAt", 0)
return int(time.time() * 1000) < (expires_at - 60_000) if expires_at else bool(creds.get("accessToken"))
@@ -415,7 +415,7 @@ def _oauth_http_error(exc: Any, *, what: str) -> AnthropicOAuthError:
def _post_oauth_token(
data: bytes, *, content_type: str, timeout: int, what: str, user_agent: str = _OAUTH_TOKEN_USER_AGENT
) -> Dict[str, Any]:
) -> dict[str, Any]:
"""POST to the token endpoints in order; raise the last error if all fail."""
import urllib.error
import urllib.request
@@ -438,7 +438,7 @@ def _post_oauth_token(
raise last_error or ValueError(f"Anthropic token {what} failed")
def _oauth_token_state(result: Dict[str, Any], *, fallback_refresh_token: str = "") -> Dict[str, Any]:
def _oauth_token_state(result: dict[str, Any], *, fallback_refresh_token: str = "") -> dict[str, Any]:
"""Token-endpoint JSON -> ``{access_token, refresh_token, expires_at_ms}`` (expires_in defaults to 3600s)."""
return {
"access_token": result.get("access_token", ""),
@@ -447,7 +447,7 @@ def _oauth_token_state(result: Dict[str, Any], *, fallback_refresh_token: str =
}
def refresh_anthropic_oauth_pure(refresh_token: str, *, use_json: bool = False) -> Dict[str, Any]:
def refresh_anthropic_oauth_pure(refresh_token: str, *, use_json: bool = False) -> dict[str, Any]:
"""Refresh an Anthropic OAuth token without mutating local credential files."""
import urllib.parse
if not refresh_token:
@@ -462,7 +462,7 @@ def refresh_anthropic_oauth_pure(refresh_token: str, *, use_json: bool = False)
return _oauth_token_state(result, fallback_refresh_token=refresh_token)
def _refresh_oauth_token(creds: Dict[str, Any]) -> Optional[str]:
def _refresh_oauth_token(creds: dict[str, Any]) -> Optional[str]:
"""Refresh an expired Claude Code OAuth token, returning the new access token. Refresh tokens are single-use and
Claude Code refreshes on its own schedule, so we first re-read the live sources and adopt an already-rotated
token instead of racing it into ``invalid_grant``. Read, decision, POST and write-back share the pool's
@@ -545,7 +545,7 @@ def _write_claude_code_credentials(
except (OSError, ValueError) as e:
logger.error("Failed to write refreshed credentials to %s: %s", cred_path, e)
raise CredentialPersistError(cred_path, e) from e
oauth_data: Dict[str, Any] = {"accessToken": access_token, "refreshToken": refresh_token, "expiresAt": expires_at_ms}
oauth_data: dict[str, Any] = {"accessToken": access_token, "refreshToken": refresh_token, "expiresAt": expires_at_ms}
if scopes is not None:
oauth_data["scopes"] = scopes
elif "claudeAiOauth" in existing and "scopes" in existing["claudeAiOauth"]:
@@ -557,8 +557,8 @@ def _write_claude_code_credentials(
def _merge_keychain_credential_payload(
existing_payload: Dict[str, Any], access_token: str, refresh_token: str, expires_at_ms: int
) -> Dict[str, Any]:
existing_payload: dict[str, Any], access_token: str, refresh_token: str, expires_at_ms: int
) -> dict[str, Any]:
"""Rotate the ``claudeAiOauth`` token triple over the existing Keychain payload,
preserving its metadata (``subscriptionType`` / ``rateLimitTier`` / ``scopes``).
@@ -608,7 +608,7 @@ def _mirror_claude_code_credentials_to_keychain(
# ── Resolution ──
def _resolve_claude_code_token_from_credentials(creds: Optional[Dict[str, Any]] = None) -> Optional[str]:
def _resolve_claude_code_token_from_credentials(creds: Optional[dict[str, Any]] = None) -> Optional[str]:
"""Resolve a token from Claude Code credential files, refreshing if needed."""
creds = creds or read_claude_code_credentials()
if not creds:
@@ -627,7 +627,7 @@ def _resolve_claude_code_token_from_credentials(creds: Optional[Dict[str, Any]]
return refreshed or None
def _prefer_refreshable_claude_code_token(env_token: str, creds: Optional[Dict[str, Any]]) -> Optional[str]:
def _prefer_refreshable_claude_code_token(env_token: str, creds: Optional[dict[str, Any]]) -> Optional[str]:
"""Prefer refreshable Claude Code creds over a static env OAuth token: Hermes historically persisted setup tokens
into ANTHROPIC_TOKEN, and that static token would otherwise win before the refreshable file is inspected."""
if not (env_token and _is_oauth_token(env_token) and isinstance(creds, dict) and creds.get("refreshToken")):
@@ -751,7 +751,7 @@ def _generate_pkce() -> tuple:
return verifier, challenge
def run_hermes_oauth_login_pure() -> Optional[Dict[str, Any]]:
def run_hermes_oauth_login_pure() -> Optional[dict[str, Any]]:
"""Run Hermes-native OAuth PKCE flow and return credential state."""
import webbrowser
from urllib.parse import urlencode
@@ -773,7 +773,7 @@ def run_hermes_oauth_login_pure() -> Optional[Dict[str, Any]]:
try:
from hermes_cli.auth import _can_open_graphical_browser as _can_open_gui
except Exception:
_can_open_gui = lambda: True # noqa: E731 — degrade to prior behavior
_can_open_gui = lambda: True
if _can_open_gui():
with contextlib.suppress(Exception):
webbrowser.open(auth_url)
@@ -806,7 +806,7 @@ def run_hermes_oauth_login_pure() -> Optional[Dict[str, Any]]:
return _oauth_token_state(result)
def read_hermes_oauth_credentials() -> Optional[Dict[str, Any]]:
def read_hermes_oauth_credentials() -> Optional[dict[str, Any]]:
"""Read Hermes-managed OAuth credentials from ~/.hermes/.anthropic_oauth.json."""
data = _load_json_if_exists(_get_hermes_oauth_file(), "Hermes OAuth credentials")
return data if data is not None and data.get("accessToken") else None
+49 -49
View File
@@ -27,7 +27,7 @@ def _block_type(b: Any) -> Any:
return b.get("type") if isinstance(b, dict) else None
def _has_block_type(blocks: List[Any], types) -> bool:
def _has_block_type(blocks: list[Any], types) -> bool:
return any(_block_type(b) in types for b in blocks)
@@ -37,18 +37,18 @@ def _is_blank_text_block(b: Any) -> bool:
return _block_type(b) == "text" and not (isinstance(b.get("text"), str) and b["text"].strip())
def _cache_control_of(b: Any) -> Optional[Dict[str, Any]]:
def _cache_control_of(b: Any) -> Optional[dict[str, Any]]:
cc = b.get("cache_control") if isinstance(b, dict) else None
return cc if isinstance(cc, dict) else None
def _text_block(text: str) -> Dict[str, str]:
def _text_block(text: str) -> dict[str, str]:
return {"type": "text", "text": text}
def _text_block_with_citations(text: Any, cits: Any) -> Dict[str, Any]:
def _text_block_with_citations(text: Any, cits: Any) -> dict[str, Any]:
"""Text block carrying ``citations`` only when it is a non-empty list (the only input-valid shape)."""
block: Dict[str, Any] = _text_block(text)
block: dict[str, Any] = _text_block(text)
if isinstance(cits, list) and cits:
block["citations"] = cits
return block
@@ -62,20 +62,20 @@ def _parse_tool_args(raw: Any) -> Any:
return {}
def _strip_thinking(blocks: List[Any]) -> List[Any]:
def _strip_thinking(blocks: list[Any]) -> list[Any]:
return [b for b in blocks if _block_type(b) not in _THINKING_TYPES]
def _block_ids(blocks: List[Any], btype: str, key: str) -> set:
def _block_ids(blocks: list[Any], btype: str, key: str) -> set:
return {b.get(key) for b in blocks if _block_type(b) == btype}
def _assistant_block_lists(result: List[Dict[str, Any]]):
def _assistant_block_lists(result: list[dict[str, Any]]):
"""``(index, message)`` for every assistant message whose content is a block list."""
return ((i, m) for i, m in enumerate(result) if m.get("role") == "assistant" and isinstance(m.get("content"), list))
def _carry_cache_control(out: Dict[str, Any], b: Any, *, copy: bool = False) -> Dict[str, Any]:
def _carry_cache_control(out: dict[str, Any], b: Any, *, copy: bool = False) -> dict[str, Any]:
"""Carry a dict-valued ``cache_control`` marker from ``b`` onto ``out`` (returned); ``copy``
shallow-copies it so the caller's dict is never shared with the wire payload."""
cc = _cache_control_of(b)
@@ -84,7 +84,7 @@ def _carry_cache_control(out: Dict[str, Any], b: Any, *, copy: bool = False) ->
return out
def _split_blank_text_blocks(blocks: List[Any]) -> Tuple[List[Any], Any, List[int]]:
def _split_blank_text_blocks(blocks: list[Any]) -> tuple[list[Any], Any, list[int]]:
"""``(kept, relocated_cache_control, dropped_indexes)``: drop blank text blocks, remembering
the cache_control of the last one dropped so the caller can relocate the breakpoint."""
dropped = [i for i, blk in enumerate(blocks) if _is_blank_text_block(blk)]
@@ -117,11 +117,11 @@ def _sanitize_tool_id(tool_id: str) -> str:
return (re.sub(r"[^a-zA-Z0-9_-]", "_", tool_id) if tool_id else "") or "tool_0"
def _tool_use_block(tool_id: Any, name: Any, tool_input: Any) -> Dict[str, Any]:
def _tool_use_block(tool_id: Any, name: Any, tool_input: Any) -> dict[str, Any]:
return {"type": "tool_use", "id": _sanitize_tool_id(tool_id), "name": name, "input": tool_input}
def _normalize_tool_input_schema(schema: Any) -> Dict[str, Any]:
def _normalize_tool_input_schema(schema: Any) -> dict[str, Any]:
"""Normalize a tool schema for Anthropic's validator: collapse nullable unions (``anyOf:
[{type: string}, {type: null}]`` from Pydantic/MCP optional fields) to the non-null branch —
optionality is already expressed by ``required``; ``keep_nullable_hint=False`` because the
@@ -141,7 +141,7 @@ def _normalize_tool_input_schema(schema: Any) -> Dict[str, Any]:
return normalized
def convert_tools_to_anthropic(tools: List[Dict]) -> List[Dict]:
def convert_tools_to_anthropic(tools: list[dict]) -> list[dict]:
"""Convert OpenAI tool definitions to Anthropic format. Duplicate names are dropped with a
warning (Anthropic hard-400s on them); ``cache_control`` on the OpenAI tool dict is forwarded."""
result = []
@@ -156,7 +156,7 @@ def convert_tools_to_anthropic(tools: List[Dict]) -> List[Dict]:
continue
if name:
seen_names.add(name)
anthropic_tool: Dict[str, Any] = {
anthropic_tool: dict[str, Any] = {
"name": name, "description": fn.get("description", ""),
"input_schema": _normalize_tool_input_schema(fn.get("parameters") or {}),
}
@@ -164,7 +164,7 @@ def convert_tools_to_anthropic(tools: List[Dict]) -> List[Dict]:
return result
def _image_block_from_openai_url(url: str) -> Dict[str, Any]:
def _image_block_from_openai_url(url: str) -> dict[str, Any]:
"""OpenAI image URL / data URL -> Anthropic ``image`` block. An inline subtype the API rejects
(svg+xml, bmp, tiff) 400s every replay once in history: an SVG is rasterized to PNG when a
rasterizer is installed, anything else unsupported becomes a text placeholder."""
@@ -184,7 +184,7 @@ def _image_block_from_openai_url(url: str) -> Dict[str, Any]:
return {"type": "image", "source": {"type": "base64", "media_type": media_type, "data": data}}
def _convert_content_part_to_anthropic(part: Any) -> Optional[Dict[str, Any]]:
def _convert_content_part_to_anthropic(part: Any) -> Optional[dict[str, Any]]:
"""Convert one OpenAI-style content part to an Anthropic block (None -> dropped)."""
if part is None:
return None
@@ -239,7 +239,7 @@ def _to_plain_data(value: Any, *, _depth: int = 0, _path: Optional[set] = None)
return result
def _extract_preserved_thinking_blocks(message: Dict[str, Any]) -> List[Dict[str, Any]]:
def _extract_preserved_thinking_blocks(message: dict[str, Any]) -> list[dict[str, Any]]:
"""Deep-copied thinking/redacted_thinking blocks from ``reasoning_details``."""
raw_details = message.get("reasoning_details")
if not isinstance(raw_details, list):
@@ -258,10 +258,10 @@ def _convert_content_to_anthropic(content: Any) -> Any:
return [b for b in map(_convert_content_part_to_anthropic, content) if b is not None]
def _content_parts_to_anthropic_blocks(parts: Any) -> List[Dict[str, Any]]:
def _content_parts_to_anthropic_blocks(parts: Any) -> list[dict[str, Any]]:
"""Tool-message content parts -> tool_result inner blocks (text + image only, the types
Anthropic accepts there). Used for multimodal tool results."""
out: List[Dict[str, Any]] = []
out: list[dict[str, Any]] = []
for block in map(_convert_content_part_to_anthropic, parts if isinstance(parts, list) else []):
if not block:
continue
@@ -281,7 +281,7 @@ def _safe_text(text: Any) -> str:
return text if text.strip() else _EMPTY_TEXT_PLACEHOLDER
def _replay_text(b: Dict[str, Any]) -> Optional[Dict[str, Any]]:
def _replay_text(b: dict[str, Any]) -> Optional[dict[str, Any]]:
# Drop blank blocks rather than coerce in place: the caller relocates any cache_control and
# falls back to a placeholder only when nothing survives, so "(empty)" never sits as
# model-visible noise next to real blocks.
@@ -290,20 +290,20 @@ def _replay_text(b: Dict[str, Any]) -> Optional[Dict[str, Any]]:
return _carry_cache_control(_text_block_with_citations(b["text"], b.get("citations")), b)
def _replay_thinking(b: Dict[str, Any]) -> Dict[str, Any]:
def _replay_thinking(b: dict[str, Any]) -> dict[str, Any]:
out = {"type": "thinking", "thinking": b.get("thinking", "")}
return {**out, "signature": b["signature"]} if b.get("signature") else out
def _replay_redacted_thinking(b: Dict[str, Any]) -> Optional[Dict[str, Any]]:
def _replay_redacted_thinking(b: dict[str, Any]) -> Optional[dict[str, Any]]:
return {"type": "redacted_thinking", "data": b["data"]} if b.get("data") else None
def _replay_tool_use(b: Dict[str, Any]) -> Dict[str, Any]:
def _replay_tool_use(b: dict[str, Any]) -> dict[str, Any]:
return _carry_cache_control(_tool_use_block(b.get("id", ""), b.get("name", ""), b.get("input", {})), b)
def _replay_image(b: Dict[str, Any]) -> Optional[Dict[str, Any]]:
def _replay_image(b: dict[str, Any]) -> Optional[dict[str, Any]]:
src = b.get("source")
return {"type": "image", "source": src} if isinstance(src, dict) else None
@@ -314,7 +314,7 @@ _REPLAY_SANITIZERS = {
}
def _sanitize_replay_block(b: Dict[str, Any]) -> Optional[Dict[str, Any]]:
def _sanitize_replay_block(b: dict[str, Any]) -> Optional[dict[str, Any]]:
"""Whitelist a stored Anthropic block so it is valid as REQUEST input. SDK response blocks carry
output-only fields the INPUT schema forbids ("Extra inputs are not permitted": ``parsed_output``,
``caller``, ``citations=None``), and ``_to_plain_data`` captured them verbatim. Whitelist per
@@ -326,7 +326,7 @@ def _sanitize_replay_block(b: Dict[str, Any]) -> Optional[Dict[str, Any]]:
return sanitizer(b) if sanitizer else None
def _apply_assistant_cache_control_to_last_cacheable_block(blocks: List[Dict[str, Any]], cache_control: Any) -> None:
def _apply_assistant_cache_control_to_last_cacheable_block(blocks: list[dict[str, Any]], cache_control: Any) -> None:
if not isinstance(cache_control, dict):
return
for block in reversed(blocks):
@@ -335,7 +335,7 @@ def _apply_assistant_cache_control_to_last_cacheable_block(blocks: List[Dict[str
break
def _replay_ordered_blocks(m: Dict[str, Any], ordered_blocks: List[Any]) -> Optional[List[Dict[str, Any]]]:
def _replay_ordered_blocks(m: dict[str, Any], ordered_blocks: list[Any]) -> Optional[list[dict[str, Any]]]:
"""Interleaved-thinking replay: rebuild the assistant turn from the verbatim block list
normalize_response stored (only for turns interleaving SIGNED thinking with tool_use).
Preserves block ORDER; returns None if nothing survives. tool_use ``input`` is re-sourced from
@@ -346,7 +346,7 @@ def _replay_ordered_blocks(m: Dict[str, Any], ordered_blocks: List[Any]) -> Opti
for tc in m.get("tool_calls", []) or []
if isinstance(tc, dict)
}
replayed: List[Dict[str, Any]] = []
replayed: list[dict[str, Any]] = []
relocated_cc = None
dropped_blank_text = False
for b in ordered_blocks:
@@ -377,7 +377,7 @@ def _replay_ordered_blocks(m: Dict[str, Any], ordered_blocks: List[Any]) -> Opti
return replayed
def assistant_replay_carrier(m: Dict[str, Any]) -> Tuple[bool, List[Dict[str, Any]]]:
def assistant_replay_carrier(m: dict[str, Any]) -> tuple[bool, list[dict[str, Any]]]:
"""``(ordered, blocks)``: the sanitized ordered sidecar when any block survives it (the whole turn
then replays from it), else the ``reasoning_details`` thinking blocks. Single owner of carrier
precedence for conversion, accounting and rejected-signature mirrors."""
@@ -389,7 +389,7 @@ def assistant_replay_carrier(m: Dict[str, Any]) -> Tuple[bool, List[Dict[str, An
return False, _extract_preserved_thinking_blocks(m)
def _convert_assistant_message(m: Dict[str, Any]) -> Dict[str, Any]:
def _convert_assistant_message(m: dict[str, Any]) -> dict[str, Any]:
"""Assistant message -> Anthropic content blocks (thinking, text, tool_use, Kimi/DeepSeek
reasoning_content injection)."""
# apply_anthropic_cache_control marks an assistant turn with non-empty text by writing cache_control
@@ -434,10 +434,10 @@ def _convert_assistant_message(m: Dict[str, Any]) -> Dict[str, Any]:
return {"role": "assistant", "content": effective}
def _tool_result_content(m: Dict[str, Any]) -> Any:
def _tool_result_content(m: dict[str, Any]) -> Any:
"""Resolve a tool message's content into tool_result content (blocks or string)."""
content = m.get("content", "")
multimodal_blocks: Optional[List[Dict[str, Any]]] = None
multimodal_blocks: Optional[list[dict[str, Any]]] = None
if isinstance(content, dict) and content.get("_multimodal"):
multimodal_blocks = _content_parts_to_anthropic_blocks(content.get("content") or [])
if not multimodal_blocks and content.get("text_summary"):
@@ -456,7 +456,7 @@ def _tool_result_content(m: Dict[str, Any]) -> Any:
return (content if isinstance(content, str) else json.dumps(content) if content else "") or "(no output)"
def _convert_tool_message_to_result(result: List[Dict[str, Any]], m: Dict[str, Any]) -> None:
def _convert_tool_message_to_result(result: list[dict[str, Any]], m: dict[str, Any]) -> None:
"""Append a tool_result to ``result``, merging into a trailing tool_result user message when
there is one. Mutates ``result`` in place."""
tool_result = {
@@ -472,7 +472,7 @@ def _convert_tool_message_to_result(result: List[Dict[str, Any]], m: Dict[str, A
result.append({"role": "user", "content": [tool_result]})
def _convert_user_message(content: Any) -> Dict[str, Any]:
def _convert_user_message(content: Any) -> dict[str, Any]:
"""Validate and convert a user message to Anthropic format."""
if isinstance(content, list):
content = _fix_blank_text_blocks_in_list(
@@ -484,7 +484,7 @@ def _convert_user_message(content: Any) -> Dict[str, Any]:
return {"role": "user", "content": content}
def _strip_orphaned_tool_blocks(result: List[Dict[str, Any]]) -> None:
def _strip_orphaned_tool_blocks(result: list[dict[str, Any]]) -> None:
"""Strip tool_use blocks with no matching tool_result, and vice versa. Compression/truncation
can remove either side of a pair or insert messages between them. Anthropic requires the
tool_result in the IMMEDIATELY FOLLOWING user message — a global id match is not enough.
@@ -523,19 +523,19 @@ def _strip_orphaned_tool_blocks(result: List[Dict[str, Any]]) -> None:
m["content"] = new_content if new_content else [_text_block("(tool result removed)")]
def _concat_content(prev: Any, curr: Any) -> List[Any]:
def _concat_content(prev: Any, curr: Any) -> list[Any]:
"""Merge two message contents into one block list, each side's blocks kept intact (a string
becomes its own text block). Strings are never joined: the first turn's bytes must equal what a
later request replays as a standalone turn, or the prompt-cache prefix diverges at that block
(MoA appends per-turn guidance after ``user(task)`` on iteration 1 and replays ``user(task)``
alone on iteration 2 — #112358)."""
as_blocks = lambda c: [_text_block(c)] if isinstance(c, str) else list(c) # noqa: E731
as_blocks = lambda c: [_text_block(c)] if isinstance(c, str) else list(c)
return as_blocks(prev) + as_blocks(curr)
def _merge_consecutive_roles(result: List[Dict[str, Any]]) -> List[Dict[str, Any]]:
def _merge_consecutive_roles(result: list[dict[str, Any]]) -> list[dict[str, Any]]:
"""Merge consecutive same-role messages to enforce alternation. Returns a new list."""
fixed: List[Dict[str, Any]] = []
fixed: list[dict[str, Any]] = []
for m in result:
if not (fixed and fixed[-1]["role"] == m["role"]):
fixed.append(m)
@@ -552,7 +552,7 @@ def _merge_consecutive_roles(result: List[Dict[str, Any]]) -> List[Dict[str, Any
return fixed
def _keep_valid_thinking(content: List[Any], signature_dead: bool) -> List[Any]:
def _keep_valid_thinking(content: list[Any], signature_dead: bool) -> list[Any]:
"""Keep valid signed Anthropic thinking, demoting readable unsigned/invalidated thinking.
Preserved-thinking models apply this to historical turns too; older models apply it only
to the latest assistant turn."""
@@ -570,7 +570,7 @@ def _keep_valid_thinking(content: List[Any], signature_dead: bool) -> List[Any]:
return new_content
def _manage_thinking_signatures(result: List[Dict[str, Any]], base_url: str | None, model: str | None) -> None:
def _manage_thinking_signatures(result: list[dict[str, Any]], base_url: str | None, model: str | None) -> None:
"""Strip or preserve thinking blocks per endpoint. Mutates ``result`` in place.
Anthropic signs thinking blocks against the full turn; any upstream mutation invalidates them
@@ -606,7 +606,7 @@ def _manage_thinking_signatures(result: List[Dict[str, Any]], base_url: str | No
m.pop("_thinking_signature_invalidated", None) # internal flag, never on the wire
def _evict_old_screenshots(result: List[Dict[str, Any]]) -> None:
def _evict_old_screenshots(result: list[dict[str, Any]]) -> None:
"""Retire screenshot payloads once the request would cross the API's per-request image limit.
Mutates ``result`` in place. This wire pass has no byte sizes, so it enforces the block
@@ -639,7 +639,7 @@ def _evict_old_screenshots(result: List[Dict[str, Any]]) -> None:
]
def _ensure_leading_user_turn(result: List[Dict[str, Any]]) -> None:
def _ensure_leading_user_turn(result: list[dict[str, Any]]) -> None:
"""Anthropic requires messages[0].role == user; prepend a placeholder turn otherwise. A second
auto-compaction can leave a role=assistant summary first, which the API rejects (often masked
as a misleading tool_use/tool_result 400). The filler must be non-whitespace text or it trades
@@ -655,8 +655,8 @@ def _ensure_leading_user_turn(result: List[Dict[str, Any]]) -> None:
def _fix_blank_text_blocks_in_list(
blocks: List[Any], *, placeholder_text: str, msg_index: int, role: Any, location: str
) -> List[Any]:
blocks: list[Any], *, placeholder_text: str, msg_index: int, role: Any, location: str
) -> list[Any]:
"""Drop blank text blocks; relocate any cache_control they carried onto the last surviving
cacheable block; if nothing survives, substitute one placeholder block (carrying the relocated
marker). Non-text blocks and order are untouched. Returns a new list; logs structure only."""
@@ -672,7 +672,7 @@ def _fix_blank_text_blocks_in_list(
return kept
def _scrub_blank_text_blocks(result: List[Dict[str, Any]]) -> None:
def _scrub_blank_text_blocks(result: list[dict[str, Any]]) -> None:
"""Final boundary guard against blank text blocks (HTTP 400 "text content blocks must contain
non-whitespace text"), including inside tool_result content. Runs LAST so a blank block from
any current or future producer never reaches the wire. Mutates ``result`` in place."""
@@ -712,15 +712,15 @@ def _convert_system_content(content: Any) -> Any:
def convert_messages_to_anthropic(
messages: List[Dict], base_url: str | None = None, model: str | None = None
) -> Tuple[Optional[Any], List[Dict]]:
messages: list[dict], base_url: str | None = None, model: str | None = None
) -> tuple[Optional[Any], list[dict]]:
"""Convert OpenAI-format messages to Anthropic format -> ``(system, messages)``. System is
extracted into its own param (a string, or a block list when cache_control is present).
``base_url``/``model`` drive thinking-signature policy — third-party endpoints strip signatures
(proprietary, they 400 on them); Kimi-family endpoints/models keep unsigned
reasoning_content-derived blocks, which Kimi requires even when empty."""
system = None
result: List[Dict[str, Any]] = []
result: list[dict[str, Any]] = []
for m in messages:
role = m.get("role", "user")
if role == "system":
+1 -1
View File
@@ -78,7 +78,7 @@ class ApiErrorSummaryMixin:
@staticmethod
def _is_entitlement_failure(
error_context: Optional[Dict[str, Any]], status_code: Optional[int]
error_context: Optional[dict[str, Any]], status_code: Optional[int]
) -> bool:
"""Detect subscription/entitlement 401/403s that masquerade as auth failures.
+5 -5
View File
@@ -32,7 +32,7 @@ def _model_dump(value: Any) -> Any:
class ApiRequestHooksMixin:
"""Hook payload sanitising + ``api_request_error`` dispatch (see module docstring)."""
def _usage_summary_for_api_request_hook(self, response: Any) -> Optional[Dict[str, Any]]:
def _usage_summary_for_api_request_hook(self, response: Any) -> Optional[dict[str, Any]]:
"""Token buckets for ``post_api_request`` plugins (no raw ``response`` object)."""
if response is None:
return None
@@ -86,7 +86,7 @@ class ApiRequestHooksMixin:
)
if isinstance(value, dict):
out: Dict[str, Any] = {}
out: dict[str, Any] = {}
for idx, (key, item) in enumerate(value.items()):
if idx >= max_sequence:
out["_truncated_items"] = len(value) - max_sequence
@@ -131,7 +131,7 @@ class ApiRequestHooksMixin:
"_truncated": True, "original_type": type(value).__name__, "preview": encoded[:limit]
}
def _api_request_payload_for_hook(self, api_kwargs: Optional[Dict[str, Any]]) -> Dict[str, Any]:
def _api_request_payload_for_hook(self, api_kwargs: Optional[dict[str, Any]]) -> dict[str, Any]:
body = {
key: value
for key, value in (api_kwargs or {}).items()
@@ -141,7 +141,7 @@ class ApiRequestHooksMixin:
def _api_response_payload_for_hook(
self, response: Any, assistant_message: Any, *, finish_reason: Optional[str]
) -> Dict[str, Any]:
) -> dict[str, Any]:
# Raw provider SDK tool_call objects are handed to the sanitizer on purpose; `_hook_jsonable` must
# keep normalising them (model_dump / __dict__ / dataclass) or subscribers get str() blobs.
tool_calls = getattr(assistant_message, "tool_calls", None) or []
@@ -162,7 +162,7 @@ class ApiRequestHooksMixin:
def _invoke_api_request_error_hook(
self, *, task_id: str, turn_id: str, api_request_id: str, api_call_count: int,
api_start_time: float, api_kwargs: Optional[Dict[str, Any]], error_type: str,
api_start_time: float, api_kwargs: Optional[dict[str, Any]], error_type: str,
error_message: str, status_code: Optional[int] = None, retry_count: Optional[int] = None,
max_retries: Optional[int] = None, retryable: Optional[bool] = None,
reason: Optional[str] = None,
+1 -1
View File
@@ -33,7 +33,7 @@ def async_api_key(sync_client: Any) -> Any:
return _provide
def configured_default_headers(sync_client: Any) -> Dict[str, str]:
def configured_default_headers(sync_client: Any) -> dict[str, str]:
"""The ``default_headers`` mapping the sync client was constructed with (SDK ``_custom_headers``).
SECURITY: values may carry credentials — never log them.
+211 -194
View File
File diff suppressed because it is too large. Load diff
+1 -1
View File
@@ -124,7 +124,7 @@ def _resolve_plugin_oauth_arm(req: _ResolveRequest) -> _ResolveResult:
# ``api_key`` / ``external_process`` arms stay on the facade (they share its credential
# resolvers); the table maps the auth types whose arms are build-and-route one-liners
# or live here as topical arms.
REGISTRY_AUTHTYPE_ARMS: Dict[str, Callable[[_ResolveRequest], _ResolveResult]] = {
REGISTRY_AUTHTYPE_ARMS: dict[str, Callable[[_ResolveRequest], _ResolveResult]] = {
"vertex": _resolve_vertex_arm,
"aws_sdk": _resolve_bedrock_arm,
"oauth_minimax": _resolve_minimax_oauth_arm,
+1 -1
View File
@@ -11,7 +11,7 @@ from typing import Any, List, Optional, Tuple
def _parse_codex_final_response(
final: Any, *, issuer_kind: Optional[str] = None, issuer_model: Optional[str] = None,
) -> Tuple[List[str], List[Any], Any, str]:
) -> tuple[list[str], list[Any], Any, str]:
"""Normalize Responses output without losing phase or completion state for aux callers."""
from agent.codex_responses_adapter import _lower_or_none, _normalize_codex_response
+3 -3
View File
@@ -9,7 +9,7 @@ This module runs those rungs — and only those — around a candidate's request
from typing import Any, Awaitable, Callable, Dict, Optional
def _parameter_ladder(first_err: Exception, client: Any, kwargs: Dict[str, Any], *,
def _parameter_ladder(first_err: Exception, client: Any, kwargs: dict[str, Any], *,
task: Optional[str], tag: str):
from agent.auxiliary_client import _LadderRoute, _ladder_parameter_rungs
# Keyword construction: the route tuple grows with every new ladder rung (a positional 13-tuple
@@ -25,7 +25,7 @@ def _parameter_ladder(first_err: Exception, client: Any, kwargs: Dict[str, Any],
def send_with_parameter_rungs(
send: Callable[[Any, Dict[str, Any]], Any], client: Any, kwargs: Dict[str, Any], *, task: Optional[str],
send: Callable[[Any, dict[str, Any]], Any], client: Any, kwargs: dict[str, Any], *, task: Optional[str],
) -> Any:
"""``send(client, kwargs)``; on a parameter 400, retry through the parameter rungs. Any other
error (auth, payment, connection) propagates unchanged for the caller's own handling."""
@@ -38,7 +38,7 @@ def send_with_parameter_rungs(
async def send_with_parameter_rungs_async(
send: Callable[[Any, Dict[str, Any]], Awaitable[Any]], client: Any, kwargs: Dict[str, Any], *,
send: Callable[[Any, dict[str, Any]], Awaitable[Any]], client: Any, kwargs: dict[str, Any], *,
task: Optional[str],
) -> Any:
"""Async twin of :func:`send_with_parameter_rungs`."""
+10 -10
View File
@@ -22,7 +22,7 @@ PRE_AUXILIARY_CALL = "pre_auxiliary_call"
POST_AUXILIARY_CALL = "post_auxiliary_call"
def _parent_turn_identity() -> Dict[str, str]:
def _parent_turn_identity() -> dict[str, str]:
"""``session_id`` / ``task_id`` / ``turn_id`` / ``platform`` of the main turn this auxiliary
call runs under, or empty strings for turn-less callers (cron, gateway idle work)."""
ident = {"session_id": "", "task_id": "", "turn_id": "", "platform": ""}
@@ -42,7 +42,7 @@ def _parent_turn_identity() -> Dict[str, str]:
return ident
def _system_prompt(messages: Any, kwargs: Dict[str, Any]) -> str:
def _system_prompt(messages: Any, kwargs: dict[str, Any]) -> str:
if isinstance(kwargs.get("system"), str): # Anthropic
return kwargs["system"]
if isinstance(kwargs.get("instructions"), str): # Responses
@@ -54,7 +54,7 @@ def _system_prompt(messages: Any, kwargs: Dict[str, Any]) -> str:
return ""
def _usage_summary(response: Any, *, provider: str, api_mode: str) -> Optional[Dict[str, Any]]:
def _usage_summary(response: Any, *, provider: str, api_mode: str) -> Optional[dict[str, Any]]:
raw_usage = getattr(response, "usage", None)
if response is None or not raw_usage:
return None
@@ -90,7 +90,7 @@ class _AuxCallHooks:
"""Fires the pre/post pair for one provider attempt; the base payload is built once."""
def __init__(
self, *, aux_task: str, metadata: Dict[str, Any], client: Any, kwargs: Dict[str, Any],
self, *, aux_task: str, metadata: dict[str, Any], client: Any, kwargs: dict[str, Any],
provider: str, model: str, api_mode: str, streaming: bool,
) -> None:
self.provider = provider
@@ -98,7 +98,7 @@ class _AuxCallHooks:
self.streaming = streaming
self.kwargs = kwargs
self.started_at = time.time()
self.base: Dict[str, Any] = dict(_parent_turn_identity())
self.base: dict[str, Any] = dict(_parent_turn_identity())
self.base.update(
aux_task=aux_task,
api_request_id=str(metadata.get("api_request_id") or ""),
@@ -143,7 +143,7 @@ class _AuxCallHooks:
from agent.api_request_hooks import ApiRequestHooksMixin as _Sanitize
ended_at = time.time()
payload: Dict[str, Any] = dict(
payload: dict[str, Any] = dict(
self.base, ended_at=ended_at, api_duration=max(0.0, ended_at - self.started_at),
error=None if error is None else f"{type(error).__name__}: {error}"[:2000],
error_type=None if error is None else type(error).__name__,
@@ -220,8 +220,8 @@ def _post_safely(hooks: Optional[_AuxCallHooks], response: Any = None, error: An
def run_with_aux_hooks(
call: Callable[[], Any], *, aux_task: str, metadata: Dict[str, Any], client: Any,
kwargs: Dict[str, Any], provider: str, model: str, api_mode: str, streaming: bool = False,
call: Callable[[], Any], *, aux_task: str, metadata: dict[str, Any], client: Any,
kwargs: dict[str, Any], provider: str, model: str, api_mode: str, streaming: bool = False,
) -> Any:
"""Run one synchronous provider attempt between ``pre_auxiliary_call`` and
``post_auxiliary_call``; the exception (if any) is reported in ``post`` and re-raised."""
@@ -237,8 +237,8 @@ def run_with_aux_hooks(
async def arun_with_aux_hooks(
call: Callable[[], Awaitable[Any]], *, aux_task: str, metadata: Dict[str, Any], client: Any,
kwargs: Dict[str, Any], provider: str, model: str, api_mode: str,
call: Callable[[], Awaitable[Any]], *, aux_task: str, metadata: dict[str, Any], client: Any,
kwargs: dict[str, Any], provider: str, model: str, api_mode: str,
) -> Any:
"""Async twin of :func:`run_with_aux_hooks`."""
hooks = _hooks_or_none(aux_task=aux_task, metadata=metadata, client=client, kwargs=kwargs,
+1 -1
View File
@@ -15,7 +15,7 @@ from hermes_cli.local_runtime.endpoint import LLAMACPP_ALIASES
def bare_llamacpp_endpoint(provider: Optional[str], base_url: Optional[str],
api_key: Any) -> Optional[Tuple[str, Any]]:
api_key: Any) -> Optional[tuple[str, Any]]:
"""``(base_url, api_key)`` of the profile's local llama.cpp server for a bare alias.
``base_url`` is ``""`` when nothing is serving. ``None`` when the request is not a bare alias:
+3 -3
View File
@@ -40,14 +40,14 @@ def _is_disabled(reasoning_config: Any) -> bool:
return isinstance(reasoning_config, dict) and reasoning_config.get("enabled") is False
def floor_reasoning_config(reasoning_config: Any) -> Dict[str, Any]:
def floor_reasoning_config(reasoning_config: Any) -> dict[str, Any]:
"""The caller's disabled ``reasoning_config`` lifted to the floor; anything else returned as-is."""
if _is_disabled(reasoning_config):
return {"enabled": True, "effort": REASONING_FLOOR_EFFORT}
return reasoning_config
def with_reasoning_floor(kwargs: Dict[str, Any]) -> Optional[Dict[str, Any]]:
def with_reasoning_floor(kwargs: dict[str, Any]) -> Optional[dict[str, Any]]:
"""Copy of *kwargs* with every thinking-OFF encoding lifted to ``REASONING_FLOOR_EFFORT``:
top-level ``reasoning_effort``, ``extra_body.reasoning`` (OpenRouter shape) and the adapter's private
``_reasoning_config``. ``None`` when nothing was disabled, so the ladder never re-sends an unchanged
@@ -72,7 +72,7 @@ def with_reasoning_floor(kwargs: Dict[str, Any]) -> Optional[Dict[str, Any]]:
def remember_reasoning_floor(
provider: Optional[str], base_url: Optional[str], rejected_kwargs: Dict[str, Any], error: BaseException,
provider: Optional[str], base_url: Optional[str], rejected_kwargs: dict[str, Any], error: BaseException,
) -> None:
"""Record that this route's ``rejected_kwargs["model"]`` refuses to disable reasoning (the ladder
calls this after the stepped-up retry succeeded)."""
+4 -4
View File
@@ -45,7 +45,7 @@ def _route_key(provider: Optional[str], base_url: Optional[str]) -> str:
return (urlparse(base_url or "").netloc or "").lower() or str(provider or "").strip().lower()
def _response_format_type(request_kwargs: Dict[str, Any]) -> Optional[str]:
def _response_format_type(request_kwargs: dict[str, Any]) -> Optional[str]:
extra_body = request_kwargs.get("extra_body")
response_format = (extra_body or {}).get("response_format") if isinstance(extra_body, dict) else None
if response_format is None:
@@ -78,7 +78,7 @@ def is_capability_rejection(error: Optional[BaseException]) -> bool:
def remember_structured_output_rejection(
provider: Optional[str], base_url: Optional[str], rejected_kwargs: Dict[str, Any], error: BaseException,
provider: Optional[str], base_url: Optional[str], rejected_kwargs: dict[str, Any], error: BaseException,
) -> None:
"""Record that this route's ``rejected_kwargs["model"]`` rejected the ``response_format`` type carried
by *rejected_kwargs* — only when *error* names the capability, never for a schema-validation 400."""
@@ -88,9 +88,9 @@ def remember_structured_output_rejection(
def without_unsupported_response_format(
extra_body: Dict[str, Any], provider: Optional[str], base_url: Optional[str], model: Optional[str],
extra_body: dict[str, Any], provider: Optional[str], base_url: Optional[str], model: Optional[str],
task: Optional[str] = None,
) -> Dict[str, Any]:
) -> dict[str, Any]:
"""*extra_body* minus a ``response_format`` whose type this route+model is known to reject; unchanged
otherwise."""
response_format = extra_body.get("response_format")
+2 -2
View File
@@ -13,7 +13,7 @@ from typing import Any, Dict
logger = logging.getLogger(__name__)
def _get_auxiliary_task_config(task: str, _seen: frozenset = frozenset()) -> Dict[str, Any]:
def _get_auxiliary_task_config(task: str, _seen: frozenset = frozenset()) -> dict[str, Any]:
"""Config dict for auxiliary.<task>, or {} when unavailable. Plugin-registered tasks get their
declared defaults layered under user config (user wins); built-in defaults live in DEFAULT_CONFIG.
A task registered with ``inherit_from`` is resolved here, at read time, over the base task's
@@ -57,7 +57,7 @@ _AUX_ROUTE_KEYS = frozenset({"provider", "model", "base_url", "api_key", "api_mo
"api_key_env", "reasoning_effort"})
def _layer_over_inherited(inherited: Dict[str, Any], user: Dict[str, Any]) -> Dict[str, Any]:
def _layer_over_inherited(inherited: dict[str, Any], user: dict[str, Any]) -> dict[str, Any]:
"""Merge a task's own ``auxiliary.<task>`` block over its inherited base.
The picker, "reset to auto" and the dashboard persist ``provider: auto`` plus ``""`` for
+10 -10
View File
@@ -41,7 +41,7 @@ _AUTH_HEADERS = ("Authorization", "authorization", "Api-Key", "api-key", "X-Api-
def has_azure_identity_installed() -> bool:
"""Cheap importability check — does not walk the credential chain."""
try:
import azure.identity # noqa: F401
import azure.identity
return True
except Exception:
return False
@@ -85,11 +85,11 @@ class EntraIdentityConfig:
def __post_init__(self) -> None:
object.__setattr__(self, "scope", str(self.scope or "").strip() or SCOPE_AI_AZURE_DEFAULT)
def to_dict(self) -> Dict[str, Any]:
def to_dict(self) -> dict[str, Any]:
return {"scope": self.scope, "exclude_interactive_browser": self.exclude_interactive_browser}
@classmethod
def from_dict(cls, data: Optional[Dict[str, Any]], *, default_scope: Optional[str] = None) -> "EntraIdentityConfig":
def from_dict(cls, data: Optional[dict[str, Any]], *, default_scope: Optional[str] = None) -> "EntraIdentityConfig":
data = data or {}
return cls(
scope=str(data.get("scope") or "").strip() or default_scope or SCOPE_AI_AZURE_DEFAULT,
@@ -114,7 +114,7 @@ def _default_chain_credential(config: EntraIdentityConfig) -> Any:
# the ambient default chain is refused outright when the profile sets no credential of its own: every source it
# could mint from (env SP, CLI/azd/PowerShell caches, host managed identity) is the launch context's identity,
# and _inject_bearer would ship it to whatever base_url the served profile configured.
_credentials_by_home: Dict[tuple, Any] = {}
_credentials_by_home: dict[tuple, Any] = {}
_AMBIENT_CHAIN_REFUSAL = (
"Entra ID auth is refused for this profile: it sets no AZURE_* credential of its own, and under "
@@ -129,7 +129,7 @@ _AMBIENT_CHAIN_REFUSAL = (
def _scoped_credential(ai: Any, config: EntraIdentityConfig) -> Any:
from agent.secret_scope import current_secret_scope, is_multiplex_active
scope = current_secret_scope() or {}
read = lambda name: (scope.get(name) or "").strip() # noqa: E731
read = lambda name: (scope.get(name) or "").strip()
tenant, client = read("AZURE_TENANT_ID"), read("AZURE_CLIENT_ID")
if tenant and client and read("AZURE_CLIENT_SECRET"):
return ai.ClientSecretCredential(tenant, client, read("AZURE_CLIENT_SECRET"))
@@ -164,7 +164,7 @@ def _resolve_config(config: Optional[EntraIdentityConfig], scope: Optional[str],
return EntraIdentityConfig(scope=(scope or "").strip() or SCOPE_AI_AZURE_DEFAULT, **overrides)
def _install_failure(allow_install: bool) -> Optional[Dict[str, Any]]:
def _install_failure(allow_install: bool) -> Optional[dict[str, Any]]:
"""None when ``azure.identity`` is importable (lazy-installing if allowed), else ``{"error", "hint"}``."""
if has_azure_identity_installed():
return None
@@ -187,9 +187,9 @@ def build_token_provider(scope: Optional[str] = None, *, config: Optional[EntraI
return ai.get_bearer_token_provider(build_credential(config), config.scope)
def _probe_token(config: EntraIdentityConfig, timeout_seconds: float) -> Optional[Dict[str, Any]]:
def _probe_token(config: EntraIdentityConfig, timeout_seconds: float) -> Optional[dict[str, Any]]:
"""``get_token`` on a daemon thread under a hard deadline → ``{"token"}`` / ``{"error"}`` / None on timeout."""
result: Dict[str, Any] = {}
result: dict[str, Any] = {}
ctx = contextvars.copy_context()
def _probe() -> None:
@@ -251,11 +251,11 @@ _ENV_SOURCE_CHECKS = (
def describe_active_credential(config: Optional[EntraIdentityConfig] = None, *, scope: Optional[str] = None,
timeout_seconds: float = 10.0, allow_install: bool = True,
**overrides: Any) -> Dict[str, Any]:
**overrides: Any) -> dict[str, Any]:
"""Doctor / preflight diagnostics. Never raises; ``{"ok": False, "error": ...}`` on failure. azure-identity
hides the winning inner credential, so this reports a coarse picture (env sources, token expiry) rather
than a class name; ``AZURE_LOG_LEVEL=DEBUG`` shows the chain."""
info: Dict[str, Any] = {"ok": False}
info: dict[str, Any] = {"ok": False}
failure = _install_failure(allow_install)
if failure is not None:
info["error"], info["hint"] = failure["error"], failure["hint"]
+44 -44
View File
@@ -182,14 +182,14 @@ _REVIEW_INPUT_CONTEXT_FRACTION = 0.75
_REVIEW_MAX_INPUT_TOKENS_FALLBACK = 120_000
def _task_block(cfg: Any) -> Dict[str, Any]:
def _task_block(cfg: Any) -> dict[str, Any]:
"""``cfg["auxiliary"]["background_review"]`` as a dict (``{}`` on any shape mismatch)."""
aux = cfg.get("auxiliary", {}) if isinstance(cfg.get("auxiliary"), dict) else {}
task = aux.get("background_review", {})
return task if isinstance(task, dict) else {}
def _background_review_task_config(task_cfg: Optional[Dict[str, Any]] = None) -> Dict[str, Any]:
def _background_review_task_config(task_cfg: Optional[dict[str, Any]] = None) -> dict[str, Any]:
"""``auxiliary.background_review`` (or ``{}`` on any failure); pass a pre-loaded ``task_cfg``
so the spawn / resolve / prompt paths do not re-read config on every turn."""
if task_cfg is not None:
@@ -213,7 +213,7 @@ def _context_derived_review_input_budget(review_agent: Any = None) -> int:
def _review_input_token_budget(
task_cfg: Optional[Dict[str, Any]] = None, review_agent: Any = None,
task_cfg: Optional[dict[str, Any]] = None, review_agent: Any = None,
) -> Optional[int]:
"""Aggregate input-token budget for one review fork (None = unlimited; <= 0 disables). Unset
or malformed ``max_input_tokens`` → derived from ``review_agent``'s context window."""
@@ -225,7 +225,7 @@ def _review_input_token_budget(
return budget if budget > 0 else None
def load_background_review_settings() -> tuple[bool, Dict[str, Any]]:
def load_background_review_settings() -> tuple[bool, dict[str, Any]]:
"""Single config read -> ``(enabled, task_cfg)``. Fail-open (``enabled=True``) so a broken
config never silently disables reviews — but WARN so the cost is visible."""
try:
@@ -242,7 +242,7 @@ def load_background_review_settings() -> tuple[bool, Dict[str, Any]]:
return True, {}
def _resolve_review_runtime(agent: Any, task_cfg: Optional[Dict[str, Any]] = None) -> Dict[str, Any]:
def _resolve_review_runtime(agent: Any, task_cfg: Optional[dict[str, Any]] = None) -> dict[str, Any]:
"""Resolve provider/model/credentials for the review fork. Default (auto / unset / same as
parent): the parent's live runtime with ``routed=False`` (codex_app_server -> codex_responses
downgrade applied). When ``auxiliary.background_review.{provider,model}`` names a different
@@ -314,14 +314,14 @@ def _parent_can_emit_tool_calls(agent: Any) -> bool:
return True
def _msg_text(m: Dict) -> str:
def _msg_text(m: dict) -> str:
c = m.get("content")
if isinstance(c, list):
c = " ".join(b.get("text", "") for b in c if isinstance(b, dict))
return c.strip() if isinstance(c, str) else ""
def _digest_history(messages_snapshot: List[Dict], tail: int = 24) -> List[Dict]:
def _digest_history(messages_snapshot: list[dict], tail: int = 24) -> list[dict]:
"""Compact replay for the routed (different-model) path only: keep the recent ``tail``
messages verbatim (extended so the kept run never starts on a tool result) and collapse older
turns into one synthetic user-role digest, preserving role alternation."""
@@ -333,7 +333,7 @@ def _digest_history(messages_snapshot: List[Dict], tail: int = 24) -> List[Dict]
tail += 1
else:
return msgs
lines: List[str] = []
lines: list[str] = []
for m in msgs[:-len(keep)]:
if not isinstance(m, dict):
continue
@@ -566,19 +566,19 @@ def _preview(text: str, limit: int) -> str:
# Memory op -> (glyph, which field carries the preview, preview length).
_MEMORY_OP_FORMATS: Dict[str, Tuple[str, str, int]] = {
_MEMORY_OP_FORMATS: dict[str, tuple[str, str, int]] = {
"add": ("➕", "content", 120), "replace": ("✏️", "content", 120), "remove": ("➖", "old_text", 60)
}
def _memory_op_line(label: str, action: str, fields: Dict[str, str]) -> Optional[str]:
def _memory_op_line(label: str, action: str, fields: dict[str, str]) -> Optional[str]:
"""Verbose line for one memory add/replace/remove, or None when no preview text."""
glyph, field_name, limit = _MEMORY_OP_FORMATS.get(action) or (None, "", 0)
text = fields.get(field_name) or "" if glyph else ""
return t("display.review.memory_op_line", label=label, glyph=glyph, preview=_preview(text, limit)) if text else None
def _verbose_skill_line(data: Dict, detail: Dict, message: str) -> str:
def _verbose_skill_line(data: dict, detail: dict, message: str) -> str:
action = detail.get("action", "")
skill_name = detail.get("name", "")
# ``_change`` is free-form (wrapper MCP backends return lists/scalars).
@@ -596,7 +596,7 @@ def _verbose_skill_line(data: Dict, detail: Dict, message: str) -> str:
return t("display.review.skill_message_verbose", message=message) if message else t("display.review.skill_action", action=action)
def _verbose_memory_lines(label: str, detail: Dict) -> List[str]:
def _verbose_memory_lines(label: str, detail: dict) -> list[str]:
# ``operations`` may be any JSON value; only a list of dicts is usable.
ops_raw = detail.get("operations")
if isinstance(ops_raw, list) and ops_raw:
@@ -612,7 +612,7 @@ _CALL_DETAIL_DEFAULTS = (
)
def _collect_review_call_details(review_messages: List[Dict]) -> Tuple[set, dict]:
def _collect_review_call_details(review_messages: list[dict]) -> tuple[set, dict]:
"""Map review-agent tool_call ids -> parsed call arguments for notify tools. Result JSON only
says "Entry added"; the call arguments carry action, target and content previews. Restricting
to notify tools keeps helper tools from surfacing as memory work just because they succeeded."""
@@ -643,11 +643,11 @@ def _collect_review_call_details(review_messages: List[Dict]) -> Tuple[set, dict
return all_tool_call_ids, call_details
def _tool_messages(messages: List[Dict]) -> Iterator[Dict]:
def _tool_messages(messages: list[dict]) -> Iterator[dict]:
return (m for m in messages or [] if isinstance(m, dict) and m.get("role") == "tool")
def _prior_tool_keys(prior_snapshot: List[Dict]) -> Tuple[set, set]:
def _prior_tool_keys(prior_snapshot: list[dict]) -> tuple[set, set]:
"""``(tool_call_ids, contents)`` of tool messages already in the parent snapshot."""
priors = list(_tool_messages(prior_snapshot))
ids = {m["tool_call_id"] for m in priors if m.get("tool_call_id")}
@@ -655,7 +655,7 @@ def _prior_tool_keys(prior_snapshot: List[Dict]) -> Tuple[set, set]:
return ids, contents
def _action_lines(data: Dict, detail: Dict, verbose: bool) -> List[str]:
def _action_lines(data: dict, detail: dict, verbose: bool) -> list[str]:
"""Summary line(s) for one successful notify-tool result (``[]`` when nothing to report)."""
if data.get("staged"):
# The fork's own review summary is never published back, so an unattended-review
@@ -697,8 +697,8 @@ def _action_lines(data: Dict, detail: Dict, verbose: bool) -> List[str]:
def summarize_background_review_actions(
review_messages: List[Dict], prior_snapshot: List[Dict], notification_mode: str = "on"
) -> List[str]:
review_messages: list[dict], prior_snapshot: list[dict], notification_mode: str = "on"
) -> list[str]:
"""Human-facing action summary for a background review pass: successful memory /
skill-management tool results from the review agent's messages, skipping tool messages already
present in ``prior_snapshot`` so inherited results are not re-surfaced as fresh work.
@@ -713,7 +713,7 @@ def summarize_background_review_actions(
verbose = mode == "verbose"
existing_tool_call_ids, existing_tool_contents = _prior_tool_keys(prior_snapshot)
all_tool_call_ids, call_details = _collect_review_call_details(review_messages)
actions: List[str] = []
actions: list[str] = []
for msg in _tool_messages(review_messages):
tcid = msg.get("tool_call_id")
if tcid:
@@ -736,9 +736,9 @@ def summarize_background_review_actions(
def build_memory_write_metadata(
agent: Any, *, write_origin: Optional[str] = None, execution_context: Optional[str] = None,
task_id: Optional[str] = None, tool_call_id: Optional[str] = None,
) -> Dict[str, Any]:
) -> dict[str, Any]:
"""Build provenance metadata for external memory-provider mirrors."""
metadata: Dict[str, Any] = {
metadata: dict[str, Any] = {
"write_origin": write_origin or getattr(agent, "_memory_write_origin", "assistant_tool"),
"execution_context": execution_context or getattr(agent, "_memory_write_context", "foreground"),
"session_id": agent.session_id or "",
@@ -756,7 +756,7 @@ _USAGE_COUNTERS = (
)
def _snapshot_review_usage(review_agent: Any) -> Dict[str, Any]:
def _snapshot_review_usage(review_agent: Any) -> dict[str, Any]:
"""Snapshot in-memory usage counters from a review fork (pre-close)."""
return {
**{key: getattr(review_agent, key, None) for key in ("model", "provider", "base_url")},
@@ -765,7 +765,7 @@ def _snapshot_review_usage(review_agent: Any) -> Dict[str, Any]:
}
def _record_review_usage_to_parent(parent_agent: Any, usage: Dict[str, Any]) -> None:
def _record_review_usage_to_parent(parent_agent: Any, usage: dict[str, Any]) -> None:
"""Record a fork's usage against the parent session (best-effort, never raises). The fork has
``_session_db = None`` so conversation_loop's DB-gated accounting never sees its calls; route
them through the aux-accounting chokepoint, which writes only ``session_model_usage`` — never
@@ -786,7 +786,7 @@ def _record_review_usage_to_parent(parent_agent: Any, usage: Dict[str, Any]) ->
logger.debug("Background review usage recording failed (non-fatal): %s", e)
def _classify_review_result(actions: List[str]) -> str:
def _classify_review_result(actions: list[str]) -> str:
"""Map a review action summary to ``none`` / ``skill`` / ``memory`` / ``skill+memory``.
Prefix-based on the formats :func:`summarize_background_review_actions` emits (``Skill …``,
``📝 Skill …``, ``Memory …``, ``User profile …``), so a free-text line like ``Skipped: no
@@ -801,7 +801,7 @@ def _classify_review_result(actions: List[str]) -> str:
return "+".join(kind for kind, hit in (("skill", has_skill), ("memory", has_memory)) if hit) or "none"
def _log_review_completion(usage: Dict[str, Any], result: str) -> None:
def _log_review_completion(usage: dict[str, Any], result: str) -> None:
"""Emit a per-fork completion line so cost is visible where it is incurred."""
logger.info(
"Background review complete: thread=bg-review calls=%d in=%d out=%d "
@@ -820,12 +820,12 @@ _PROVIDER_PIN_ATTRS = (
)
def _same_model_parity_kwargs(agent: Any) -> Dict[str, Any]:
def _same_model_parity_kwargs(agent: Any) -> dict[str, Any]:
"""AIAgent kwargs that keep a SAME-model fork's request bytes identical to the parent's. Only
for the un-routed path: on a different model the cache is cold anyway, and the parent's
reasoning-effort vocabulary may be invalid for the routed provider (OpenRouter forwards
``reasoning.effort`` unclamped; codex_responses passes ``max``/``ultra`` through unmapped)."""
kwargs: Dict[str, Any] = {
kwargs: dict[str, Any] = {
# Anthropic's cache key is namespaced by ``thinking`` presence; the gateway session context
# is appended to the cached system prompt at API-call time (without it the prompt diverges).
"reasoning_config": getattr(agent, "reasoning_config", None),
@@ -840,7 +840,7 @@ def _same_model_parity_kwargs(agent: Any) -> Dict[str, Any]:
return kwargs
def _warn_ignored_reasoning_effort(agent: Any, task_cfg: Optional[Dict[str, Any]] = None) -> None:
def _warn_ignored_reasoning_effort(agent: Any, task_cfg: Optional[dict[str, Any]] = None) -> None:
"""One-shot user-visible notice: ``auxiliary.background_review.reasoning_effort`` is IGNORED on
the same-model path (#104116). The fork inherits the parent's ``reasoning_config`` verbatim so
its request bytes keep the parent's prompt-cache prefix (#30532: a diverged ``thinking`` field
@@ -890,7 +890,7 @@ def _detach_fork_compression(review_agent: Any) -> None:
review_agent._review_defer_compaction_before_first_response = True
def _routed_reasoning_config(task_cfg: Optional[Dict[str, Any]]) -> Optional[Dict[str, Any]]:
def _routed_reasoning_config(task_cfg: Optional[dict[str, Any]]) -> Optional[dict[str, Any]]:
"""``reasoning_config`` for a ROUTED fork from ``auxiliary.background_review.reasoning_effort``
(#94825). The routed branch never inherits the parent's effort (its vocabulary may be invalid for
the routed provider), but an explicit per-task pin is the user's choice for THAT model and must
@@ -909,14 +909,14 @@ def _routed_reasoning_config(task_cfg: Optional[Dict[str, Any]]) -> Optional[Dic
return parsed
def _fork_init_kwargs(agent: Any, rt: Dict[str, Any], routed: bool, max_iterations: int,
task_cfg: Optional[Dict[str, Any]] = None) -> Dict[str, Any]:
def _fork_init_kwargs(agent: Any, rt: dict[str, Any], routed: bool, max_iterations: int,
task_cfg: Optional[dict[str, Any]] = None) -> dict[str, Any]:
"""AIAgent constructor kwargs for the review fork. skip_memory=True: an external memory plugin
scoped to the parent's session_id would leak the harness prompt into the user's real memory
namespace; built-in MEMORY.md/USER.md state is re-bound by the caller. Toolsets match the
parent so ``tools[]`` is byte-identical (Anthropic's cache key includes it); the runtime
whitelist restricts dispatch."""
kwargs: Dict[str, Any] = {
kwargs: dict[str, Any] = {
"model": rt.get("model") or agent.model, "max_iterations": max_iterations, "quiet_mode": True,
"platform": agent.platform, "provider": rt.get("provider") or agent.provider,
"api_mode": rt.get("api_mode"), "base_url": rt.get("base_url") or None,
@@ -954,9 +954,9 @@ def _inherit_parent_tool_surface(review_agent: Any, agent: Any) -> None:
def build_cache_parity_fork(
agent: Any, task_cfg: Optional[Dict[str, Any]] = None, *, max_iterations: int,
agent: Any, task_cfg: Optional[dict[str, Any]] = None, *, max_iterations: int,
write_origin: str = "background_review",
) -> Tuple[Any, Dict[str, Any], bool]:
) -> tuple[Any, dict[str, Any], bool]:
"""Construct a detached AIAgent fork with warm prompt-cache parity (shared with ``/btw``): same
runtime/credentials as the parent, byte-identical system prompt / tools[] / reasoning config on
the same-model path, shared session_id for prefix warmth, full persistence detachment (no
@@ -1077,8 +1077,8 @@ def _track_review_fork(agent: Any, review_agent: Any, *, register: bool) -> None
def _review_tool_whitelist(
review_agent: Any, task_cfg: Optional[Dict[str, Any]], review_memory: bool = False,
) -> Tuple[set, set]:
review_agent: Any, task_cfg: Optional[dict[str, Any]], review_memory: bool = False,
) -> tuple[set, set]:
"""``(whitelist, configured_extra_tools)`` for the review fork — DISPATCH-side only, so the
advertised ``tools[]`` stays byte-identical to the parent's (prompt-cache parity)."""
from model_tools import get_tool_definitions
@@ -1124,8 +1124,8 @@ class _ReviewForkState:
"""Mutable hand-off between the fork phase and the outer worker's error/cleanup paths."""
review_agent: Any = None
review_messages: List[Dict] = field(default_factory=list)
review_usage: Dict[str, Any] = field(default_factory=dict)
review_messages: list[dict] = field(default_factory=list)
review_usage: dict[str, Any] = field(default_factory=dict)
def _release_fork_clients(review_agent: Any) -> None:
@@ -1136,7 +1136,7 @@ def _release_fork_clients(review_agent: Any) -> None:
def _run_review_fork(
agent: Any, messages_snapshot: List[Dict], prompt: str, task_cfg: Optional[Dict[str, Any]],
agent: Any, messages_snapshot: list[dict], prompt: str, task_cfg: Optional[dict[str, Any]],
review_run: Optional[_BackgroundReviewRun], st: _ReviewForkState, review_memory: bool = False,
explicit: bool = False,
) -> None:
@@ -1199,7 +1199,7 @@ def _run_review_fork(
st.review_agent = None
def _publish_review_summary(agent: Any, actions: List[str]) -> None:
def _publish_review_summary(agent: Any, actions: list[str]) -> None:
summary = " · ".join(dict.fromkeys(actions))
agent._safe_print(t("display.review.summary_cli", summary=summary))
if agent.background_review_callback:
@@ -1208,8 +1208,8 @@ def _publish_review_summary(agent: Any, actions: List[str]) -> None:
def _run_review_in_thread(
agent: Any, messages_snapshot: List[Dict], prompt: str,
task_cfg: Optional[Dict[str, Any]] = None, review_run: Optional[_BackgroundReviewRun] = None,
agent: Any, messages_snapshot: list[dict], prompt: str,
task_cfg: Optional[dict[str, Any]] = None, review_run: Optional[_BackgroundReviewRun] = None,
review_memory: bool = False, explicit: bool = False,
) -> None:
"""Daemon-thread worker: build the fork, run the prompt, surface the action summary via
@@ -1299,9 +1299,9 @@ _PROMPT_NAME_BY_SCOPE = {
def spawn_background_review_thread(
agent: Any, messages_snapshot: List[Dict], review_memory: bool = False,
agent: Any, messages_snapshot: list[dict], review_memory: bool = False,
review_skills: bool = False, focus: Optional[str] = None,
task_cfg: Optional[Dict[str, Any]] = None, review_run: Optional[_BackgroundReviewRun] = None,
task_cfg: Optional[dict[str, Any]] = None, review_run: Optional[_BackgroundReviewRun] = None,
explicit: bool = False,
):
"""Return ``(target, prompt)``; the caller builds the ``threading.Thread`` so test patches of
+1 -1
View File
@@ -46,7 +46,7 @@ def _read_battery_uncached() -> BatteryStatus:
import psutil
# ``sensors_battery`` is missing on some platforms/builds of psutil.
batt = getattr(psutil, "sensors_battery")()
batt = psutil.sensors_battery()
except Exception:
return UNAVAILABLE
if batt is None:
+68 -68
View File
@@ -25,20 +25,20 @@ from agent.errors import EmptyStreamError
logger = logging.getLogger(__name__)
_bedrock_runtime_client_cache: Dict[str, Any] = {}
_bedrock_control_client_cache: Dict[str, Any] = {}
_bedrock_runtime_client_cache: dict[str, Any] = {}
_bedrock_control_client_cache: dict[str, Any] = {}
# Routed multiplex profiles: one client per (profile home, region). boto3 freezes the credential
# chain into the client at construction, so a region-only slot would sign profile B's calls with A's keys.
_bedrock_clients_by_home: Dict[Tuple[str, str, str], Any] = {}
_bedrock_clients_by_home: dict[tuple[str, str, str], Any] = {}
# botocore session kwarg <- profile .env variable (the explicit sources of the default chain).
_AWS_SCOPED_CREDENTIAL_VARS: Tuple[Tuple[str, str], ...] = (
_AWS_SCOPED_CREDENTIAL_VARS: tuple[tuple[str, str], ...] = (
("aws_access_key_id", "AWS_ACCESS_KEY_ID"), ("aws_secret_access_key", "AWS_SECRET_ACCESS_KEY"),
("aws_session_token", "AWS_SESSION_TOKEN"), ("profile_name", "AWS_PROFILE"),
)
def scoped_aws_session_kwargs() -> Dict[str, str]:
def scoped_aws_session_kwargs() -> dict[str, str]:
"""``boto3.session.Session`` kwargs from the routed profile's secret scope, ``{}`` when unscoped.
Under a HERMES_HOME override the process env holds the LAUNCH profile's ``AWS_*`` (or nothing), so
@@ -73,14 +73,14 @@ def scoped_aws_session_kwargs() -> Dict[str, str]:
return kwargs
# Geo/global cross-Region inference-profile prefixes Bedrock prepends to a model ID.
_BEDROCK_PROFILE_PREFIXES: Tuple[str, ...] = ("global", "us", "eu", "apac", "ap", "au", "jp", "ca", "sa", "me", "af")
_BEDROCK_PROFILE_PREFIXES: tuple[str, ...] = ("global", "us", "eu", "apac", "ap", "au", "jp", "ca", "sa", "me", "af")
# The GPT-5.6 Terra/Luna cards also document India geo profiles (``in.openai.gpt-5.6-terra``).
_BEDROCK_OPENAI_PROFILE_PREFIXES: Tuple[str, ...] = _BEDROCK_PROFILE_PREFIXES + ("in",)
_BEDROCK_OPENAI_PROFILE_PREFIXES: tuple[str, ...] = _BEDROCK_PROFILE_PREFIXES + ("in",)
# Bedrock-hosted OpenAI GPT models (bare in-Region IDs). The bare ID is served by the Bedrock Mantle
# OpenAI-compatible endpoint; its geo/global profile form is served by bedrock-runtime, where these
# cards list Converse as supported. Narrow allowlist so GPT-OSS models stay on the native path.
BEDROCK_OPENAI_RESPONSES_MODEL_IDS: Tuple[str, ...] = (
BEDROCK_OPENAI_RESPONSES_MODEL_IDS: tuple[str, ...] = (
"openai.gpt-6-astra", "openai.gpt-6.1-sol", "openai.gpt-6-sol", "openai.gpt-6-luna",
"openai.gpt-5.5", "openai.gpt-5.6-sol", "openai.gpt-5.6-terra", "openai.gpt-5.6-luna",
)
@@ -123,7 +123,7 @@ def _require_boto3():
return boto3
def _cached_client(cache: Dict[str, Any], service: str, region: str):
def _cached_client(cache: dict[str, Any], service: str, region: str):
"""Get or create a per-region boto3 client. Unscoped: the default credential chain, one client per
region. Routed profile: one client per (home, service, region), built from that profile's scoped
``AWS_*`` (falling back to the default chain only for what the profile does not set)."""
@@ -171,7 +171,7 @@ def invalidate_runtime_client(region: str) -> bool:
# --- Bedrock Mantle / OpenAI Responses support ---
def parse_bedrock_openai_model_id(model_id: str) -> Optional[Tuple[str, str]]:
def parse_bedrock_openai_model_id(model_id: str) -> Optional[tuple[str, str]]:
"""``(bare_id, profile_prefix)`` for a Bedrock-hosted OpenAI model, else None. ``profile_prefix``
is ``""`` for the bare in-Region ID and e.g. ``"us"`` / ``"global"`` for a cross-Region profile."""
normalized = str(model_id or "").strip().lower()
@@ -195,7 +195,7 @@ def bedrock_openai_uses_mantle(model_id: str) -> bool:
return parsed is not None and not parsed[1]
def merge_bedrock_openai_model_ids(model_ids: List[str]) -> List[str]:
def merge_bedrock_openai_model_ids(model_ids: list[str]) -> list[str]:
"""Append Mantle-only OpenAI models, which control-plane discovery never lists."""
merged = list(model_ids or [])
seen = {str(m).lower() for m in merged}
@@ -208,7 +208,7 @@ def bedrock_openai_base_url(region: str) -> str:
return f"https://bedrock-mantle.{resolved}.api.aws/openai/v1"
def _mantle_url_parts(base_url: str) -> Tuple[Optional[str], str]:
def _mantle_url_parts(base_url: str) -> tuple[Optional[str], str]:
"""(region or None if not a Mantle host, normalized path) for a base URL."""
parsed = urlparse(str(base_url or ""))
match = _BEDROCK_OPENAI_HOST_RE.match(parsed.hostname or "")
@@ -226,7 +226,7 @@ def is_bedrock_openai_base_url(base_url: str) -> bool:
return region is not None and path in {"", "/openai", "/openai/v1"}
def resolve_bedrock_bearer_token(env: Optional[Dict[str, str]] = None) -> str:
def resolve_bedrock_bearer_token(env: Optional[dict[str, str]] = None) -> str:
"""Return AWS_BEARER_TOKEN_BEDROCK when Bedrock API-key auth is configured.
Under a HERMES_HOME override the read goes through the profile secret scope so a
@@ -270,13 +270,13 @@ class BedrockOpenAISigV4Auth(httpx.Auth):
def build_bedrock_openai_http_client(region: str, *, timeout: Optional[float] = None):
"""Build an httpx client that SigV4-signs Bedrock OpenAI requests."""
kwargs: Dict[str, Any] = {"auth": BedrockOpenAISigV4Auth(region)}
kwargs: dict[str, Any] = {"auth": BedrockOpenAISigV4Auth(region)}
if isinstance(timeout, (int, float)) and not isinstance(timeout, bool) and timeout > 0:
kwargs["timeout"] = timeout
return httpx.Client(**kwargs)
def configure_bedrock_openai_client_kwargs(client_kwargs: Dict[str, Any], *, timeout: Optional[float] = None) -> Dict[str, Any]:
def configure_bedrock_openai_client_kwargs(client_kwargs: dict[str, Any], *, timeout: Optional[float] = None) -> dict[str, Any]:
"""Install SigV4 auth on OpenAI SDK kwargs for Bedrock Mantle; a real API key keeps the SDK's
bearer auth, the ``aws-sdk``/``no-key-required`` placeholders mean IAM chain auth."""
base_url = str(client_kwargs.get("base_url") or "")
@@ -338,7 +338,7 @@ def is_streaming_access_denied_error(exc: BaseException) -> bool:
# --- AWS credential detection ---
# Priority order; the first group whose vars are ALL set names the auth source.
_AWS_AUTH_ENV_CHAIN: Tuple[Tuple[str, ...], ...] = (
_AWS_AUTH_ENV_CHAIN: tuple[tuple[str, ...], ...] = (
("AWS_BEARER_TOKEN_BEDROCK",), # Bedrock bearer token
("AWS_ACCESS_KEY_ID", "AWS_SECRET_ACCESS_KEY"), # explicit IAM key pair
("AWS_PROFILE",), # named profile (SSO, assume-role)
@@ -357,7 +357,7 @@ def _boto3_chain_has_credentials() -> bool:
return False
def resolve_aws_auth_env_var(env: Optional[Dict[str, str]] = None) -> Optional[str]:
def resolve_aws_auth_env_var(env: Optional[dict[str, str]] = None) -> Optional[str]:
"""Name of the active AWS auth source: env vars first (no I/O), then ``"iam-role"`` via boto3's chain, else None."""
env = env if env is not None else os.environ
for group in _AWS_AUTH_ENV_CHAIN:
@@ -366,7 +366,7 @@ def resolve_aws_auth_env_var(env: Optional[Dict[str, str]] = None) -> Optional[s
return "iam-role" if _boto3_chain_has_credentials() else None
def has_aws_credentials(env: Optional[Dict[str, str]] = None) -> bool:
def has_aws_credentials(env: Optional[dict[str, str]] = None) -> bool:
"""True if any AWS credential source (env vars or boto3 chain) is detected.
This two-tier approach mirrors the pattern from OpenClaw PR #62673: cloud environments (EC2, ECS,
@@ -376,7 +376,7 @@ def has_aws_credentials(env: Optional[Dict[str, str]] = None) -> bool:
return resolve_aws_auth_env_var(env) is not None or _boto3_chain_has_credentials()
def resolve_bedrock_region(env: Optional[Dict[str, str]] = None) -> str:
def resolve_bedrock_region(env: Optional[dict[str, str]] = None) -> str:
"""AWS_REGION → AWS_DEFAULT_REGION → botocore configured region (~/.aws/config profiles) → us-east-1."""
env = env if env is not None else os.environ
explicit = env.get("AWS_REGION", "").strip() or env.get("AWS_DEFAULT_REGION", "").strip()
@@ -388,7 +388,7 @@ def resolve_bedrock_region(env: Optional[Dict[str, str]] = None) -> str:
return "us-east-1"
def resolve_bedrock_runtime_region(config: Optional[Dict[str, Any]] = None) -> str:
def resolve_bedrock_runtime_region(config: Optional[dict[str, Any]] = None) -> str:
"""``bedrock.region`` from config.yaml, else :func:`resolve_bedrock_region`. Every non-runtime Bedrock
endpoint must use this so auxiliary calls never leave the primary runtime's region. *config* skips disk."""
if config is None:
@@ -405,7 +405,7 @@ def bedrock_region_from_runtime_url(base_url: str) -> str:
return m.group(1) if m else "us-east-1"
def bedrock_guardrail_config(config: Optional[Dict[str, Any]] = None) -> Optional[Dict[str, Any]]:
def bedrock_guardrail_config(config: Optional[dict[str, Any]] = None) -> Optional[dict[str, Any]]:
"""Converse ``guardrailConfig`` from ``bedrock.guardrail`` in config.yaml (None when unset)."""
if config is None:
config = {}
@@ -422,7 +422,7 @@ def bedrock_guardrail_config(config: Optional[Dict[str, Any]] = None) -> Optiona
return out
def bedrock_guardrail_headers(config: Optional[Dict[str, Any]] = None) -> Dict[str, str]:
def bedrock_guardrail_headers(config: Optional[dict[str, Any]] = None) -> dict[str, str]:
"""InvokeModel/Messages-wire form of the configured guardrail. The AnthropicBedrock SDK speaks
InvokeModel, which has no ``guardrailConfig`` body field; Bedrock reads the guardrail from these
headers instead (same enforcement, keeps prompt caching / thinking / 1M context)."""
@@ -469,7 +469,7 @@ def bind_bedrock_runtime(agent, base_url: str, api_mode: str) -> None:
agent._anthropic_client = None
def bedrock_model_ids_or_none() -> Optional[List[str]]:
def bedrock_model_ids_or_none() -> Optional[list[str]]:
"""Live-discover Bedrock model IDs; None on failure/empty so callers use the static list."""
with suppress(Exception):
discovered = discover_bedrock_models(resolve_bedrock_runtime_region())
@@ -509,7 +509,7 @@ def _model_supports_prompt_cache(model_id: str) -> bool:
# record the verdict, drop the marker there for the rest of the process, and retry once without it.
CACHE_POINT_PLACEMENTS = ("tools", "system", "messages")
_CACHE_POINT_REJECTIONS: Dict[str, set] = {} # model_id (lowercased) → placements Bedrock has rejected this process
_CACHE_POINT_REJECTIONS: dict[str, set] = {} # model_id (lowercased) → placements Bedrock has rejected this process
# e.g. "#/toolConfig/tools/18: extraneous key [cachePoint] is not permitted"
_CACHE_POINT_PATH_PATTERN = re.compile(r"#/(?P<path>[A-Za-z0-9_./\[\]-]*)", re.IGNORECASE)
_CACHE_POINT = {"cachePoint": {"type": "default"}}
@@ -553,7 +553,7 @@ def _without_cache_points(blocks: Any) -> Optional[list]:
return None if len(cleaned) == len(blocks) else cleaned
def strip_cache_points(kwargs: Dict[str, Any], placement: str) -> Dict[str, Any]:
def strip_cache_points(kwargs: dict[str, Any], placement: str) -> dict[str, Any]:
"""Copy of Converse kwargs with ``placement``'s cachePoint removed; the SAME object
back when nothing was stripped (callers use identity to decide a retry cannot help)."""
if placement == "messages":
@@ -576,7 +576,7 @@ def strip_cache_points(kwargs: Dict[str, Any], placement: str) -> Dict[str, Any]
return kwargs
def recover_from_cache_point_rejection(exc: BaseException, kwargs: Dict[str, Any]) -> Optional[Dict[str, Any]]:
def recover_from_cache_point_rejection(exc: BaseException, kwargs: dict[str, Any]) -> Optional[dict[str, Any]]:
"""Record Bedrock's cachePoint verdict and return retry kwargs, or None when the error
was not a cachePoint rejection / the marker was already absent (caller re-raises)."""
placement = cache_point_rejection_placement(exc)
@@ -664,7 +664,7 @@ def is_anthropic_bedrock_model(model_id: str) -> bool:
# --- Message format conversion: OpenAI → Bedrock Converse ---
def convert_tools_to_converse(tools: List[Dict]) -> List[Dict]:
def convert_tools_to_converse(tools: list[dict]) -> list[dict]:
"""OpenAI ``{"function": {...}}`` tool defs → Converse ``{"toolSpec": {...}}``."""
return [{"toolSpec": {
"name": fn.get("name", ""), "description": fn.get("description", ""),
@@ -685,7 +685,7 @@ def _safe_text(text) -> str:
return text if text.strip() else _EMPTY_TEXT_PLACEHOLDER
def _image_block_from_data_url(url: str) -> Dict:
def _image_block_from_data_url(url: str) -> dict:
"""``data:<mime>;base64,...`` → Converse image block with RAW bytes (boto3 base64-encodes on the
wire; passing the string through double-encodes and Bedrock rejects it)."""
header, _, data = url.partition(",")
@@ -698,7 +698,7 @@ def _image_block_from_data_url(url: str) -> Dict:
return {"image": {"format": media_type.split("/")[-1] if "/" in media_type else "jpeg", "source": {"bytes": raw_bytes}}}
def _convert_content_to_converse(content) -> List[Dict]:
def _convert_content_to_converse(content) -> list[dict]:
"""OpenAI content → Converse blocks; blank text → placeholder, remote image URLs → text reference."""
if not isinstance(content, list):
return [{"text": _safe_text(content)}]
@@ -715,14 +715,14 @@ def _convert_content_to_converse(content) -> List[Dict]:
return blocks or [dict(_PLACEHOLDER_BLOCK)]
def _system_blocks(content) -> List[Dict]:
def _system_blocks(content) -> list[dict]:
"""System content → text blocks; blank parts are dropped, not placeholder-filled."""
parts = [content] if isinstance(content, str) else content if isinstance(content, list) else []
texts = [part.get("text", "") if isinstance(part, dict) and part.get("type") == "text" else part for part in parts]
return [{"text": text} for text in texts if isinstance(text, str) and text.strip()]
def _tool_use_block(tool_use_id, name, input_dict) -> Dict:
def _tool_use_block(tool_use_id, name, input_dict) -> dict:
return {"toolUse": {"toolUseId": tool_use_id, "name": name, "input": input_dict}}
@@ -734,10 +734,10 @@ def _decode_redacted(encoded) -> Optional[bytes]:
return None
def _replay_ordered_blocks(ordered_blocks: List) -> List[Dict]:
def _replay_ordered_blocks(ordered_blocks: list) -> list[dict]:
"""Rebuild the exact Bedrock block sequence captured at normalization time; redacted reasoning is
stored base64 (JSON-safe sidecar) and undecodable entries are skipped."""
content_blocks: List[Dict] = []
content_blocks: list[dict] = []
for block in ordered_blocks:
if not isinstance(block, dict):
continue
@@ -750,7 +750,7 @@ def _replay_ordered_blocks(ordered_blocks: List) -> List[Dict]:
# ReasoningContentBlock is a tagged union: reasoningText and redactedContent must go
# out as separate blocks (#115865). Undecodable redacted entries are skipped alone.
if isinstance(reasoning.get("text"), str):
reasoning_text: Dict[str, str] = {"text": reasoning["text"]}
reasoning_text: dict[str, str] = {"text": reasoning["text"]}
if isinstance(reasoning.get("signature"), str) and reasoning["signature"]:
reasoning_text["signature"] = reasoning["signature"] # models that sign thinking reject unsigned replay
content_blocks.append({"reasoningContent": {"reasoningText": reasoning_text}})
@@ -773,7 +773,7 @@ def _parse_tool_args(args) -> Any:
return {}
def _assistant_blocks(msg: Dict, content) -> List[Dict]:
def _assistant_blocks(msg: dict, content) -> list[dict]:
"""Assistant message → Converse blocks. An ordered ``bedrock_content_blocks`` sidecar is authoritative;
otherwise redacted thinking from ``reasoning_details`` (byte-for-byte), then text, then tool calls."""
ordered_blocks = msg.get("bedrock_content_blocks")
@@ -783,7 +783,7 @@ def _assistant_blocks(msg: Dict, content) -> List[Dict]:
_decode_redacted(d.get("data") or d.get("redactedContentBase64"))
for d in (msg.get("reasoning_details") or []) if isinstance(d, dict) and d.get("type") == "redacted_thinking"
]
content_blocks: List[Dict] = [{"reasoningContent": {"redactedContent": r}} for r in redacted if r is not None]
content_blocks: list[dict] = [{"reasoningContent": {"redactedContent": r}} for r in redacted if r is not None]
if isinstance(content, str) and content.strip():
content_blocks.append({"text": content})
elif isinstance(content, list):
@@ -794,14 +794,14 @@ def _assistant_blocks(msg: Dict, content) -> List[Dict]:
return content_blocks
def convert_messages_to_converse(messages: List[Dict]) -> Tuple[Optional[List[Dict]], List[Dict]]:
def convert_messages_to_converse(messages: list[dict]) -> tuple[Optional[list[dict]], list[dict]]:
"""OpenAI messages → ``(system_blocks_or_None, converse_messages)``; tool results become ``toolResult``
user blocks. Converse needs strict user/assistant alternation with a user turn first and last:
same-role neighbours merge, placeholder user turns pad the ends."""
system_blocks: List[Dict] = []
converse_msgs: List[Dict] = []
system_blocks: list[dict] = []
converse_msgs: list[dict] = []
def append_turn(role: str, blocks: List[Dict]) -> None:
def append_turn(role: str, blocks: list[dict]) -> None:
if converse_msgs and converse_msgs[-1]["role"] == role:
converse_msgs[-1]["content"].extend(blocks)
else:
@@ -853,12 +853,12 @@ class _ResponseParts:
"""Accumulator shared by the sync and streaming normalizers."""
def __init__(self) -> None:
self.text_parts: List[str] = []
self.reasoning_parts: List[str] = []
self.reasoning_details: List[Dict[str, Any]] = []
self.tool_calls: List[SimpleNamespace] = []
self.text_parts: list[str] = []
self.reasoning_parts: list[str] = []
self.reasoning_details: list[dict[str, Any]] = []
self.tool_calls: list[SimpleNamespace] = []
def absorb_reasoning(self, reasoning: Any, block: Dict[str, Any], on_text=None) -> None:
def absorb_reasoning(self, reasoning: Any, block: dict[str, Any], on_text=None) -> None:
"""Fold a Converse ``reasoningContent`` payload into the accumulators and ``block``. The sync response
nests ``reasoningText: {text, signature}``; stream deltas carry ``text`` / ``signature`` flat."""
if not isinstance(reasoning, dict):
@@ -879,7 +879,7 @@ class _ResponseParts:
self.reasoning_details.append({"type": "redacted_thinking", "data": encoded})
block["redactedContentBase64"] = encoded
def build(self, ordered_blocks: List[Dict[str, Any]], usage_data: Dict[str, int], stop_reason: str, model: str) -> SimpleNamespace:
def build(self, ordered_blocks: list[dict[str, Any]], usage_data: dict[str, int], stop_reason: str, model: str) -> SimpleNamespace:
"""Assemble the OpenAI-shaped response. Converse's inputTokens EXCLUDES cache read/write tokens
(OpenAI's prompt_tokens includes them), so they are added back."""
msg = SimpleNamespace(
@@ -904,7 +904,7 @@ class _ResponseParts:
)
def normalize_converse_response(response: Dict) -> SimpleNamespace:
def normalize_converse_response(response: dict) -> SimpleNamespace:
"""Bedrock Converse response → OpenAI ``ChatCompletion``-shaped SimpleNamespace (``.choices[0].message.
{content,tool_calls,reasoning_content,reasoning_details,bedrock_content_blocks}``, ``finish_reason``, ``.usage``)."""
parts = _ResponseParts()
@@ -914,7 +914,7 @@ def normalize_converse_response(response: Dict) -> SimpleNamespace:
parts.text_parts.append(block["text"])
ordered_blocks.append({"text": block["text"]})
elif "reasoningContent" in block:
ordered_reasoning: Dict[str, Any] = {}
ordered_reasoning: dict[str, Any] = {}
parts.absorb_reasoning(block["reasoningContent"], ordered_reasoning)
if ordered_reasoning:
ordered_blocks.append({"reasoningContent": ordered_reasoning})
@@ -946,16 +946,16 @@ def stream_converse_with_callbacks(
Blocks are keyed by the ``contentBlockIndex`` Bedrock stamps on every contentBlockStart/Delta/Stop:
text blocks get NO contentBlockStart, so a counter keyed on starts shredded them (#108200)."""
parts = _ResponseParts()
stream_blocks: Dict[int, Dict[str, Any]] = {}
stream_blocks: dict[int, dict[str, Any]] = {}
current_block_index: Optional[int] = None
current_tool: Optional[Dict] = None
current_text_buffer: List[str] = []
current_tool: Optional[dict] = None
current_text_buffer: list[str] = []
has_tool_use = False
stop_reason = None
interrupted = False
usage_data: Dict[str, int] = {}
usage_data: dict[str, int] = {}
def block_index(payload: Dict[str, Any], *, new_block: bool = False) -> int:
def block_index(payload: dict[str, Any], *, new_block: bool = False) -> int:
"""Index of the block a contentBlock* event addresses. Without ``contentBlockIndex`` (test doubles,
proxies) a start opens a fresh slot and a delta/stop continues the current one."""
idx = payload.get("contentBlockIndex")
@@ -1029,18 +1029,18 @@ def stream_converse_with_callbacks(
# --- High-level API: call Bedrock Converse ---
def build_converse_kwargs(
model: str, messages: List[Dict], tools: Optional[List[Dict]] = None, max_tokens: Optional[int] = 4096,
model: str, messages: list[dict], tools: Optional[list[dict]] = None, max_tokens: Optional[int] = 4096,
temperature: Optional[float] = None, top_p: Optional[float] = None,
stop_sequences: Optional[List[str]] = None, guardrail_config: Optional[Dict] = None,
) -> Dict[str, Any]:
stop_sequences: Optional[list[str]] = None, guardrail_config: Optional[dict] = None,
) -> dict[str, Any]:
"""Build kwargs for ``bedrock-runtime.converse()`` / ``converse_stream()``. ``max_tokens=None`` omits
``maxTokens`` (model maximum; default stays 4096). cachePoint markers go on system, tools and the
second-newest message (survives as the tail grows — mirrors Anthropic system_and_3), each only if the
model supports caching and Bedrock has not rejected that placement."""
system_prompt, converse_messages = convert_messages_to_converse(messages)
cache_at = {p for p in CACHE_POINT_PLACEMENTS if cache_point_allowed(model, p)} if _model_supports_prompt_cache(model) else set()
inference_config: Dict[str, Any] = {} if max_tokens is None else {"maxTokens": max_tokens}
kwargs: Dict[str, Any] = {"modelId": model, "messages": converse_messages, "inferenceConfig": inference_config}
inference_config: dict[str, Any] = {} if max_tokens is None else {"maxTokens": max_tokens}
kwargs: dict[str, Any] = {"modelId": model, "messages": converse_messages, "inferenceConfig": inference_config}
if system_prompt:
kwargs["system"] = system_prompt + [dict(_CACHE_POINT)] if "system" in cache_at else system_prompt
from agent.anthropic_adapter import _forbids_sampling_params
@@ -1069,9 +1069,9 @@ def build_converse_kwargs(
def call_converse(
region: str, model: str, messages: List[Dict], tools: Optional[List[Dict]] = None,
region: str, model: str, messages: list[dict], tools: Optional[list[dict]] = None,
max_tokens: Optional[int] = 4096, temperature: Optional[float] = None, top_p: Optional[float] = None,
stop_sequences: Optional[List[str]] = None, guardrail_config: Optional[Dict] = None,
stop_sequences: Optional[list[str]] = None, guardrail_config: Optional[dict] = None,
) -> SimpleNamespace:
"""Non-streaming Converse call → OpenAI-compatible response. Retries once without a rejected cachePoint
placement; evicts the cached client on stale-connection errors."""
@@ -1098,7 +1098,7 @@ def call_converse(
# --- Model discovery ---
_discovery_cache: Dict[str, Any] = {}
_discovery_cache: dict[str, Any] = {}
_DISCOVERY_CACHE_TTL_SECONDS = 3600
@@ -1107,12 +1107,12 @@ def reset_discovery_cache():
_discovery_cache.clear()
def _model_entry(model_id: str, name: Any, provider: str, input_mods: list, output_mods: list) -> Dict[str, Any]:
def _model_entry(model_id: str, name: Any, provider: str, input_mods: list, output_mods: list) -> dict[str, Any]:
return {"id": model_id, "name": (name or model_id).strip(), "provider": provider,
"input_modalities": input_mods, "output_modalities": output_mods, "streaming": True}
def _list_foundation_models(client, filter_set: set, models: List[Dict[str, Any]]) -> None:
def _list_foundation_models(client, filter_set: set, models: list[dict[str, Any]]) -> None:
"""Append active, streaming-capable, text-output foundation models (optionally provider-filtered)."""
for summary in client.list_foundation_models().get("modelSummaries", []):
model_id = (summary.get("modelId") or "").strip()
@@ -1131,7 +1131,7 @@ def _list_foundation_models(client, filter_set: set, models: List[Dict[str, Any]
))
def _list_inference_profiles(client, filter_set: set, models: List[Dict[str, Any]]) -> None:
def _list_inference_profiles(client, filter_set: set, models: list[dict[str, Any]]) -> None:
"""Append active cross-region inference profiles whose IDs are not already present (paginated)."""
profiles, next_token = [], None
while True:
@@ -1152,7 +1152,7 @@ def _list_inference_profiles(client, filter_set: set, models: List[Dict[str, Any
seen_ids.add(profile_id.lower())
def discover_bedrock_models(region: str, provider_filter: Optional[List[str]] = None) -> List[Dict[str, Any]]:
def discover_bedrock_models(region: str, provider_filter: Optional[list[str]] = None) -> list[dict[str, Any]]:
"""Foundation models + inference profiles (cached 1h per region/filter), ``global.`` profiles first then
by name; [] when the client cannot be built."""
# The list is account-scoped (whichever credentials the control client signs with), so a routed
@@ -1169,7 +1169,7 @@ def discover_bedrock_models(region: str, provider_filter: Optional[List[str]] =
except Exception as e:
logger.warning("Failed to create Bedrock client for model discovery: %s", e)
return []
models: List[Dict[str, Any]] = []
models: list[dict[str, Any]] = []
filter_set = {f.lower() for f in (provider_filter or [])}
for step, log, message in (
(_list_foundation_models, logger.warning, "Failed to list Bedrock foundation models: %s"),
@@ -1194,7 +1194,7 @@ def _extract_provider_from_arn(arn: str) -> str:
# Static fallback when the live probe is unavailable (agent/model_metadata.py). Keys match by longest
# substring, so versioned entries win over the generic "anthropic.claude-opus-4".
BEDROCK_CONTEXT_LENGTHS: Dict[str, int] = {
BEDROCK_CONTEXT_LENGTHS: dict[str, int] = {
# https://docs.aws.amazon.com/bedrock/latest/userguide/model-card-xai-grok-4-6.html
"xai.grok-4.6": 500_000,
# Anthropic Claude: 1M GA vs 200K. The 1M entries must match agent/model_metadata.py
@@ -1303,7 +1303,7 @@ def get_bedrock_context_length(model_id: str, region: str = "", probe: bool = Tr
# production caller (agent/model_metadata.py::_resolve_bedrock_context_length) passes none.
_APPLICATION_PROFILE_ARN_RE = re.compile(r":application-inference-profile/")
_ARN_REGION_RE = re.compile(r"^arn:[^:]+:bedrock:([a-z0-9-]+):", re.IGNORECASE)
_inference_profile_model_cache: Dict[str, str] = {}
_inference_profile_model_cache: dict[str, str] = {}
def _resolve_inference_profile_model_id(profile_arn: str, region: str = "") -> str:
+3 -3
View File
@@ -316,15 +316,15 @@ def _dev_fixture_billing_state() -> Optional[BillingState]:
# Prod portal host (matches subscription_view._DEV_FIXTURE_PORTAL) + the /topup deep-link suffix.
common: dict[str, Any] = dict(
logged_in=True, org_id="org_acme", org_slug="acme", org_name="Acme Inc", role="OWNER",
balance_usd=Decimal("3.40"), cli_billing_enabled=True, min_usd=Decimal("5"), max_usd=Decimal("500"),
charge_presets=(Decimal("10"), Decimal("25"), Decimal("50")), portal_url="https://portal.nousresearch.com/billing?topup=open",
balance_usd=Decimal("3.40"), cli_billing_enabled=True, min_usd=Decimal(5), max_usd=Decimal(500),
charge_presets=(Decimal(10), Decimal(25), Decimal(50)), portal_url="https://portal.nousresearch.com/billing?topup=open",
)
card = CardInfo(brand="Visa", last4="4242")
overrides: dict[str, dict[str, Any]] = {
"nocard": dict(card=None),
"card": dict(card=card),
"card-sub": dict(card=CardInfo(brand="Visa", last4="4242", resolved_via="subPin")),
"card-autoreload": dict(card=card, auto_reload=AutoReload(enabled=True, threshold_usd=Decimal("5"), reload_to_usd=Decimal("25"))),
"card-autoreload": dict(card=card, auto_reload=AutoReload(enabled=True, threshold_usd=Decimal(5), reload_to_usd=Decimal(25))),
"notadmin": dict(card=card, role="MEMBER"),
"billing-off": dict(card=None, cli_billing_enabled=False),
}
+3 -3
View File
@@ -37,7 +37,7 @@ def read_streaming_error_body(
stalls and oversize bodies yield best-effort partial text (or ""), so a read error can't mask the
original failure.
"""
chunks: List[bytes] = []
chunks: list[bytes] = []
state = {"truncated": False}
done = threading.Event()
@@ -55,7 +55,7 @@ def read_streaming_error_body(
break
chunks.append(chunk)
total += len(chunk)
except Exception as exc: # noqa: BLE001 - error path must not raise
except Exception as exc:
logger.debug("bounded error-body read failed: %s", exc)
finally:
done.set()
@@ -69,7 +69,7 @@ def read_streaming_error_body(
# Closing cancels any in-flight socket read so the worker unwinds. No join (daemon, may be blocked in C).
try:
response.close()
except Exception: # noqa: BLE001
except Exception:
pass
if state["truncated"]:
+1 -1
View File
@@ -41,7 +41,7 @@ class BrowserProvider(ProviderBase):
``hermes tools`` paint."""
@abc.abstractmethod
def create_session(self, task_id: str) -> Dict[str, object]:
def create_session(self, task_id: str) -> dict[str, object]:
"""Create a cloud browser session and return the metadata dict from the module docstring.
May raise ``ValueError`` (missing credentials) or ``RuntimeError`` (network / API failure);
the dispatcher surfaces these to the user."""
+4 -4
View File
@@ -371,7 +371,7 @@ def _provider_stream_error_from_text(text: str, finish_reason: Optional[str], *,
_IMAGE_PART_TYPES = frozenset({"image_url", "input_image", "image"})
def _image_part_chars(part: Dict[str, Any], image_cost: int) -> int:
def _image_part_chars(part: dict[str, Any], image_cost: int) -> int:
"""Char-equivalent of one image content part: the per-image cost learned from provider usage
(x4 chars/token), never the base64 payload length. A single native screenshot priced as text
read as ~100K+ tokens and selected the giant-conversation watchdog tiers (#63871, #76411)."""
@@ -466,7 +466,7 @@ def _validated_openrouter_provider_sort(raw_sort: Any) -> Optional[str]:
return None
def _provider_preferences_for_agent(agent) -> Dict[str, Any]:
def _provider_preferences_for_agent(agent) -> dict[str, Any]:
"""Build the validated provider-routing object shared by request paths.
``provider_routing.models.<id>`` overlays the flat constructor values for the CURRENT
@@ -510,7 +510,7 @@ def _merge_nous_portal_messages_extra_body(agent, anthropic_kwargs: dict) -> dic
if nous_profile is not None:
anthropic_kwargs.setdefault("extra_body", {}).update(
nous_profile.build_extra_body(session_id=getattr(agent, "session_id", None)))
except Exception as exc: # noqa: BLE001 — never block a turn on tagging
except Exception as exc:
logger.debug("Nous Portal extra_body merge failed: %s", exc)
return anthropic_kwargs
@@ -861,7 +861,7 @@ def _managed_local_load_notice(agent, api_kwargs: dict) -> "Optional[str]":
return f"⚙ processing prompt — {max(0, min(100, round(processed / total * 100)))}%"
# Counter past the estimate (estimator undercounted): no honest denominator, label-only.
return "⚙ processing prompt"
except Exception: # noqa: BLE001 — a status nicety must never break a call
except Exception:
return None
+1 -1
View File
@@ -678,7 +678,7 @@ class ClientLifecycleMixin:
return False
try:
return self._try_refresh_nous_client_credentials(force=False, require_account=str(account))
except Exception: # noqa: BLE001 — a failed adoption leaves the key in hand; the 401 path still heals
except Exception:
logger.debug("Nous key pre-expiry adoption failed", exc_info=True)
return False
+4 -4
View File
@@ -31,7 +31,7 @@ def is_official_codex_base_url(base_url: str) -> bool:
return False
def codex_cloudflare_headers(access_token: str, *, base_url: str = CODEX_AUX_BASE_URL) -> Dict[str, str]:
def codex_cloudflare_headers(access_token: str, *, base_url: str = CODEX_AUX_BASE_URL) -> dict[str, str]:
"""Identity and account headers for chatgpt.com/backend-api/codex.
OpenAI requires third-party harnesses to identify themselves: the official
@@ -51,7 +51,7 @@ def codex_cloudflare_headers(access_token: str, *, base_url: str = CODEX_AUX_BAS
return headers
def codex_account_headers(access_token: str) -> Dict[str, str]:
def codex_account_headers(access_token: str) -> dict[str, str]:
"""Workspace headers the Codex backend derives from the OAuth JWT.
``ChatGPT-Account-ID`` (canonical casing, from codex-rs ``auth.rs``) comes from
@@ -61,7 +61,7 @@ def codex_account_headers(access_token: str) -> Dict[str, str]:
this region". A malformed token drops the headers rather than raising, so it
surfaces as a 401 instead of a crash at client construction.
"""
headers: Dict[str, str] = {}
headers: dict[str, str] = {}
if not isinstance(access_token, str) or not access_token.strip():
return headers
try:
@@ -81,7 +81,7 @@ def codex_account_headers(access_token: str) -> Dict[str, str]:
return headers
def apply_required_codex_headers(client_kwargs: Dict[str, Any], *, access_token: str, base_url: str) -> None:
def apply_required_codex_headers(client_kwargs: dict[str, Any], *, access_token: str, base_url: str) -> None:
"""Keep required Codex identity after user/provider header overrides."""
if not is_official_codex_base_url(base_url):
return
+57 -55
View File
@@ -144,7 +144,7 @@ def _field(obj: Any, name: str, default: Any = None) -> Any:
return obj.get(name) if isinstance(obj, dict) else getattr(obj, name, default)
def _part_type(part: Dict[str, Any]) -> str:
def _part_type(part: dict[str, Any]) -> str:
return str(part.get("type") or "").strip().lower()
@@ -215,7 +215,7 @@ def _iter_content_parts(content: list) -> Iterator[tuple[str, Any]]:
yield "image", part
def _input_image_part(part: Dict[str, Any], role: str = "user", *, keep_empty_url: bool) -> Optional[Dict[str, Any]]:
def _input_image_part(part: dict[str, Any], role: str = "user", *, keep_empty_url: bool) -> Optional[dict[str, Any]]:
"""Responses image part from a chat/Responses image part (``image_url`` may be a str or
``{url, detail}``). Assistant → text placeholder (an assistant ``input_image`` 400s every
replay); user → ``input_image``, None for an empty url unless ``keep_empty_url``; an inline
@@ -240,13 +240,13 @@ def _input_image_part(part: Dict[str, Any], role: str = "user", *, keep_empty_ur
url, mime = png_url, None
if mime is not None:
return {"type": "input_text", "text": f"[image omitted: {mime} is not a supported image format]"}
image_part: Dict[str, Any] = {"type": "input_image", "image_url": url}
image_part: dict[str, Any] = {"type": "input_image", "image_url": url}
if _nonblank(detail):
image_part["detail"] = detail.strip()
return image_part
def _chat_content_to_responses_parts(content: Any, *, role: str = "user") -> List[Dict[str, Any]]:
def _chat_content_to_responses_parts(content: Any, *, role: str = "user") -> list[dict[str, Any]]:
"""Chat-style multimodal content → Responses API input parts ([] if not a list). Text is
``input_text`` (user) / ``output_text`` (assistant) — the API rejects the wrong type per role;
``input_image`` is only legal on user messages (see :func:`_input_image_part`). Unsupported
@@ -257,7 +257,7 @@ def _chat_content_to_responses_parts(content: Any, *, role: str = "user") -> Lis
f"Codex Responses does not support {ptype} input; use a video-capable provider."
)
text_type = _text_type_for(role)
converted: List[Dict[str, Any]] = []
converted: list[dict[str, Any]] = []
for kind, payload in _iter_content_parts(_as_list(content)):
if kind == "text":
converted.append({"type": text_type, "text": payload})
@@ -341,7 +341,7 @@ def _derive_responses_function_call_id(call_id: str, response_item_id: Optional[
# --- Schema conversion --------------------------------------------------------
def _responses_tools(tools: Optional[List[Dict[str, Any]]] = None) -> Optional[List[Dict[str, Any]]]:
def _responses_tools(tools: Optional[list[dict[str, Any]]] = None) -> Optional[list[dict[str, Any]]]:
"""Convert chat-completions tool schemas to Responses function-tool schemas."""
fns = [item.get("function", {}) if isinstance(item, dict) else {} for item in tools or []]
converted = [
@@ -365,10 +365,10 @@ def _normalize_responses_message_status(value: Any, *, default: str = "completed
def _message_item(
content: List[Dict[str, Any]], *, status: str, item_id: Optional[str] = None, phase: Optional[str] = None,
) -> Dict[str, Any]:
content: list[dict[str, Any]], *, status: str, item_id: Optional[str] = None, phase: Optional[str] = None,
) -> dict[str, Any]:
"""Assistant ``message`` item; ``id``/``phase`` are added only when non-empty."""
item: Dict[str, Any] = {"type": "message", "role": "assistant", "status": status, "content": content}
item: dict[str, Any] = {"type": "message", "role": "assistant", "status": status, "content": content}
item.update({k: v for k, v in (("id", item_id), ("phase", phase)) if v})
return item
@@ -376,7 +376,7 @@ def _message_item(
_ROLE_MESSAGE_PHASES = frozenset({"commentary", "final_answer"})
def _role_message_item(role: str, content: Any, phase: Any = None) -> Dict[str, Any]:
def _role_message_item(role: str, content: Any, phase: Any = None) -> dict[str, Any]:
"""Plain ``message`` input item for ``role``. ``type`` is required: llama.cpp's ``/v1/responses``
parser rejects a typeless assistant item ("Cannot determine type of 'item'"). Assistant ``phase``
is forwarded only for values the API accepts on input messages; others would 400."""
@@ -387,9 +387,9 @@ def _role_message_item(role: str, content: Any, phase: Any = None) -> Dict[str,
def _assistant_message_item(
raw: Dict[str, Any], content: List[Dict[str, Any]], *, is_github_responses: bool,
raw: dict[str, Any], content: list[dict[str, Any]], *, is_github_responses: bool,
current_issuer_kind: Optional[str] = None,
) -> Dict[str, Any]:
) -> dict[str, Any]:
"""Replayable assistant ``message`` item from a stored one. ``id`` is kept only when short enough and never for
GitHub Copilot (ids bind to a backend connection; stale → 401); ``phase`` is preserved per OpenAI's cache guidance.
The ChatGPT Codex backend additionally rejects ids that do not begin with ``msg`` (foreign Responses issuers
@@ -405,16 +405,16 @@ def _assistant_message_item(
def _replay_reasoning_items(
msg: Dict[str, Any], *, seen_item_ids: set, current_issuer_kind: Optional[str],
msg: dict[str, Any], *, seen_item_ids: set, current_issuer_kind: Optional[str],
current_issuer_model: Optional[str] = None, native_compaction_eligible: bool,
) -> List[Dict[str, Any]]:
) -> list[dict[str, Any]]:
"""Replay persisted encrypted reasoning/compaction items for one assistant turn. Skips duplicate
ids, ``compaction`` checkpoints unless THIS request carries ``context_management`` (else a persisted
checkpoint erases pre-checkpoint history on a model that cannot decrypt it), and items stamped by
another issuer or model (HTTP 400). Items without a model stamp (legacy or unstamped) replay on a
matching issuer. ``id`` (store=False lookups 404) and the Hermes provenance fields are stripped."""
global _CROSS_ISSUER_WARN_EMITTED
replayed: List[Dict[str, Any]] = []
replayed: list[dict[str, Any]] = []
for ri in _as_list(msg.get("codex_reasoning_items")):
if not (isinstance(ri, dict) and ri.get("encrypted_content")):
continue
@@ -446,8 +446,8 @@ def _replay_reasoning_items(
def _replay_message_items(
msg: Dict[str, Any], *, is_github_responses: bool, current_issuer_kind: Optional[str] = None,
) -> List[Dict[str, Any]]:
msg: dict[str, Any], *, is_github_responses: bool, current_issuer_kind: Optional[str] = None,
) -> list[dict[str, Any]]:
"""Replay exact assistant message items (id/phase) for prefix-cache hits.
A ``msg_*`` id minted in the same response as a ``reasoning`` item is bound to that item's ``rs_*`` id,
@@ -457,7 +457,7 @@ def _replay_message_items(
trimmed by the transport (``codex_reasoning_trimmed``) — and the message goes out as content/status/phase
only. Reasoning-free turns keep their id.
"""
replayed: List[Dict[str, Any]] = []
replayed: list[dict[str, Any]] = []
linked_to_reasoning = bool(msg.get("codex_reasoning_trimmed")) or any(
isinstance(ri, dict) and ri.get("encrypted_content") for ri in _as_list(msg.get("codex_reasoning_items"))
)
@@ -490,8 +490,8 @@ class _WireCallIds:
"""
def __init__(self) -> None:
self._seen: Dict[str, int] = {}
self._queue: Dict[str, List[str]] = {}
self._seen: dict[str, int] = {}
self._queue: dict[str, list[str]] = {}
def for_call(self, call_id: str) -> str:
base = _clamp_responses_call_id(call_id)
@@ -508,10 +508,10 @@ class _WireCallIds:
def _replay_tool_call_items(
msg: Dict[str, Any], *, start_index: int, wire_ids: Optional[_WireCallIds] = None,
) -> List[Dict[str, Any]]:
msg: dict[str, Any], *, start_index: int, wire_ids: Optional[_WireCallIds] = None,
) -> list[dict[str, Any]]:
"""Convert an assistant message's ``tool_calls`` into ``function_call`` items."""
replayed: List[Dict[str, Any]] = []
replayed: list[dict[str, Any]] = []
for tc in _as_list(msg.get("tool_calls")):
if not isinstance(tc, dict):
continue
@@ -529,7 +529,7 @@ def _replay_tool_call_items(
return replayed
def _tool_output_items(msg: Dict[str, Any], *, wire_ids: Optional[_WireCallIds] = None) -> List[Dict[str, Any]]:
def _tool_output_items(msg: dict[str, Any], *, wire_ids: Optional[_WireCallIds] = None) -> list[dict[str, Any]]:
"""Convert a tool-role message to ``[function_call_output]`` (``[]`` if unpairable)."""
raw_tool_call_id = msg.get("tool_call_id")
call_id, tool_response_item_id = _split_responses_tool_id(raw_tool_call_id)
@@ -550,10 +550,10 @@ def _tool_output_items(msg: Dict[str, Any], *, wire_ids: Optional[_WireCallIds]
def _chat_messages_to_responses_input(
messages: List[Dict[str, Any]], *, is_xai_responses: bool = False, is_github_responses: bool = False,
messages: list[dict[str, Any]], *, is_xai_responses: bool = False, is_github_responses: bool = False,
replay_encrypted_reasoning: bool = True, current_issuer_kind: Optional[str] = None,
current_issuer_model: Optional[str] = None, native_compaction_eligible: bool = False,
) -> List[Dict[str, Any]]:
) -> list[dict[str, Any]]:
"""Convert internal chat-style messages to Responses input items.
``is_xai_responses``: signature compatibility only (xAI DOES replay encrypted reasoning).
@@ -591,13 +591,13 @@ def _chat_messages_to_responses_input(
Hermes' local history is never truncated by native compaction, so the full conversation is still on the
wire.
"""
items: List[Dict[str, Any]] = []
items: list[dict[str, Any]] = []
# Parallel to ``items``: source chat message per item. Pruning reads a summary
# carrier's provenance from the source; the converted item may be a lossy shape.
# Pruning needs this to read a canonical summary carrier's up-to-date, provenance-tagged content
# directly — the converted `item` can be a lossy shape (stale exact-replay, or a typed
# `function_call_output` wrapper) that no longer carries it (#90976).
item_sources: List[Optional[Dict[str, Any]]] = []
item_sources: list[Optional[dict[str, Any]]] = []
seen_item_ids: set = set()
wire_ids = _WireCallIds()
# The ChatGPT Codex backend rejects a role message whose ``content`` is a plain string with
@@ -605,7 +605,7 @@ def _chat_messages_to_responses_input(
# (#51512). It accepts only typed parts, so string text goes out as ``input_text``/``output_text``
# there; other Responses routes keep the string shorthand they have always received.
typed_text_only = current_issuer_kind == "codex_backend"
def emit(new_items: List[Dict[str, Any]], msg: Dict[str, Any]) -> None:
def emit(new_items: list[dict[str, Any]], msg: dict[str, Any]) -> None:
items.extend(new_items)
item_sources.extend([msg] * len(new_items))
for msg in messages:
@@ -701,8 +701,8 @@ def classify_responses_route(agent: Any) -> ResponsesRouteFlags:
def _native_responses_replay_items(
agent: Any, messages: List[Dict[str, Any]]
) -> Optional[List[Dict[str, Any]]]:
agent: Any, messages: list[dict[str, Any]]
) -> Optional[list[dict[str, Any]]]:
"""Build the native-compaction-eligible wire items, or ``None`` when ineligible."""
if getattr(agent, "api_mode", None) != "codex_responses" or not isinstance(messages, list):
return None
@@ -731,7 +731,7 @@ def _native_responses_replay_items(
def has_replayable_native_compaction_checkpoint(
agent: Any, messages: List[Dict[str, Any]]
agent: Any, messages: list[dict[str, Any]]
) -> bool:
"""Whether the current route would replay a persisted native checkpoint."""
items = _native_responses_replay_items(agent, messages)
@@ -742,7 +742,7 @@ def has_replayable_native_compaction_checkpoint(
def estimate_native_responses_preflight_tokens(
agent: Any, messages: List[Dict[str, Any]], *, system_prompt: str = "", tools: Optional[List[Dict[str, Any]]] = None,
agent: Any, messages: list[dict[str, Any]], *, system_prompt: str = "", tools: Optional[list[dict[str, Any]]] = None,
) -> Optional[int]:
"""Estimate tokens for the checkpoint-pruned Responses payload (the full transcript overstates a natively compacted
session and fires local compression needlessly). None when native compaction is not proven eligible or conversion fails.
@@ -760,12 +760,14 @@ def estimate_native_responses_preflight_tokens(
# --- Input preflight / validation --------------------------------------------
_PreflightCtx = NamedTuple("_PreflightCtx", [
("sanitize_text", Callable[[str], str]), ("sanitize_harmony_tokens", bool), ("is_github_responses", bool), ("seen_ids", set),
])
class _PreflightCtx(NamedTuple):
sanitize_text: Callable[[str], str]
sanitize_harmony_tokens: bool
is_github_responses: bool
seen_ids: set
def _preflight_function_call(item: Dict[str, Any], idx: int, ctx: _PreflightCtx) -> Dict[str, Any]:
def _preflight_function_call(item: dict[str, Any], idx: int, ctx: _PreflightCtx) -> dict[str, Any]:
call_id, name = item.get("call_id"), item.get("name")
if not _nonblank(call_id):
raise ValueError(f"Codex Responses input[{idx}] function_call is missing call_id.")
@@ -777,14 +779,14 @@ def _preflight_function_call(item: Dict[str, Any], idx: int, ctx: _PreflightCtx)
}
def _preflight_function_call_output(item: Dict[str, Any], idx: int, ctx: _PreflightCtx) -> Dict[str, Any]:
def _preflight_function_call_output(item: dict[str, Any], idx: int, ctx: _PreflightCtx) -> dict[str, Any]:
call_id = item.get("call_id")
if not _nonblank(call_id):
raise ValueError(f"Codex Responses input[{idx}] function_call_output is missing call_id.")
output = item.get("output", "")
if isinstance(output, list):
# Multimodal tool result: keep recognised input_text/input_image parts, drop the rest (4xx otherwise).
cleaned: List[Dict[str, Any]] = []
cleaned: list[dict[str, Any]] = []
for part in output:
ptype = part.get("type") if isinstance(part, dict) else None
if ptype == "input_text" and _nonempty_str(part.get("text")):
@@ -797,7 +799,7 @@ def _preflight_function_call_output(item: Dict[str, Any], idx: int, ctx: _Prefli
return {"type": "function_call_output", "call_id": call_id.strip(), "output": output_value}
def _preflight_encrypted(item: Dict[str, Any], idx: int, ctx: _PreflightCtx) -> Optional[Dict[str, Any]]:
def _preflight_encrypted(item: dict[str, Any], idx: int, ctx: _PreflightCtx) -> Optional[dict[str, Any]]:
"""``reasoning`` / ``compaction`` items: opaque, issuer-sealed; forward only API-defined fields."""
encrypted = item.get("encrypted_content")
if not _nonempty_str(encrypted):
@@ -817,7 +819,7 @@ def _preflight_encrypted(item: Dict[str, Any], idx: int, ctx: _PreflightCtx) ->
}
def _preflight_message(item: Dict[str, Any], idx: int, ctx: _PreflightCtx) -> Dict[str, Any]:
def _preflight_message(item: dict[str, Any], idx: int, ctx: _PreflightCtx) -> dict[str, Any]:
# Only replayed assistant output (a list-content item carrying id/status) takes the strict path
# below. Phase alone is no replay marker: the converter's plain role items carry it too, and
# preflight must not synthesize a status or reject user image parts for them.
@@ -843,7 +845,7 @@ def _preflight_message(item: Dict[str, Any], idx: int, ctx: _PreflightCtx) -> Di
return _assistant_message_item(item, normalized_content, is_github_responses=ctx.is_github_responses)
def _preflight_role_message(item: Dict[str, Any], idx: int, ctx: _PreflightCtx) -> Dict[str, Any]:
def _preflight_role_message(item: dict[str, Any], idx: int, ctx: _PreflightCtx) -> dict[str, Any]:
"""``user``/``assistant`` role message, typed or untyped; string content or Responses parts."""
role = item.get("role")
if role not in {"user", "assistant"}:
@@ -856,7 +858,7 @@ def _preflight_role_message(item: Dict[str, Any], idx: int, ctx: _PreflightCtx)
# Parts are already Responses-shaped; validate and re-type text for the role.
# Unlike history conversion, empty text / empty image urls are kept, not dropped.
text_type = _text_type_for(role)
validated: List[Dict[str, Any]] = []
validated: list[dict[str, Any]] = []
for part_idx, part in enumerate(content):
if isinstance(part, str):
if part:
@@ -876,7 +878,7 @@ def _preflight_role_message(item: Dict[str, Any], idx: int, ctx: _PreflightCtx)
return _role_message_item(role, validated, item.get("phase"))
_PREFLIGHT_ITEM_HANDLERS: Dict[str, Callable[..., Optional[Dict[str, Any]]]] = {
_PREFLIGHT_ITEM_HANDLERS: dict[str, Callable[..., Optional[dict[str, Any]]]] = {
"function_call": _preflight_function_call, "function_call_output": _preflight_function_call_output,
"reasoning": _preflight_encrypted, "compaction": _preflight_encrypted, "message": _preflight_message,
}
@@ -884,12 +886,12 @@ _PREFLIGHT_ITEM_HANDLERS: Dict[str, Callable[..., Optional[Dict[str, Any]]]] = {
def _preflight_codex_input_items(
raw_items: Any, *, is_github_responses: bool = False, sanitize_harmony_tokens: bool = False,
) -> List[Dict[str, Any]]:
) -> list[dict[str, Any]]:
if not isinstance(raw_items, list):
raise ValueError("Codex Responses input must be a list of input items.")
sanitize_text = _neutralize_harmony_tokens if sanitize_harmony_tokens else (lambda text: text)
ctx = _PreflightCtx(sanitize_text, sanitize_harmony_tokens, is_github_responses, set())
normalized: List[Dict[str, Any]] = []
normalized: list[dict[str, Any]] = []
for idx, item in enumerate(raw_items):
if not isinstance(item, dict):
raise ValueError(f"Codex Responses input[{idx}] must be an object.")
@@ -901,7 +903,7 @@ def _preflight_codex_input_items(
return normalized
def _preflight_tool(tool: Any, idx: int) -> Dict[str, Any]:
def _preflight_tool(tool: Any, idx: int) -> dict[str, Any]:
if not isinstance(tool, dict):
raise ValueError(f"Codex Responses tools[{idx}] must be an object.")
tool_type = tool.get("type")
@@ -945,7 +947,7 @@ _PREFLIGHT_ALLOWED_KEYS = {
}
def _optional_dict(api_kwargs: Dict[str, Any], key: str) -> Optional[Dict[str, Any]]:
def _optional_dict(api_kwargs: dict[str, Any], key: str) -> Optional[dict[str, Any]]:
value = api_kwargs.get(key)
if value is not None and not isinstance(value, dict):
raise ValueError(f"Codex Responses request '{key}' must be an object.")
@@ -955,7 +957,7 @@ def _optional_dict(api_kwargs: Dict[str, Any], key: str) -> Optional[Dict[str, A
def _preflight_codex_api_kwargs(
api_kwargs: Any, *, allow_stream: bool = False, is_github_responses: bool = False,
sanitize_harmony_tokens: bool = False,
) -> Dict[str, Any]:
) -> dict[str, Any]:
if not isinstance(api_kwargs, dict):
raise ValueError("Codex Responses request must be a dict.")
if missing := sorted(key for key in ("model", "instructions", "input") if key not in api_kwargs):
@@ -969,7 +971,7 @@ def _preflight_codex_api_kwargs(
input_items = _preflight_codex_input_items(
api_kwargs.get("input"), is_github_responses=is_github_responses, sanitize_harmony_tokens=sanitize_harmony_tokens,
)
normalized: Dict[str, Any] = {
normalized: dict[str, Any] = {
"model": model.strip(), "instructions": instructions, "input": input_items, "store": False,
}
tools = api_kwargs.get("tools")
@@ -1019,7 +1021,7 @@ def _preflight_codex_api_kwargs(
# --- Response extraction helpers ----------------------------------------------
def _text_chunks(parts: Any, types: Optional[set] = None) -> List[str]:
def _text_chunks(parts: Any, types: Optional[set] = None) -> list[str]:
"""Non-empty ``.text`` of each part (optionally filtered by ``.type``); [] if not a list."""
selected = [part for part in _as_list(parts) if types is None or getattr(part, "type", None) in types]
return [text for text in (getattr(part, "text", None) for part in selected) if _nonempty_str(text)]
@@ -1076,13 +1078,13 @@ def _response_tool_call(item: Any, item_type: str, index: int) -> SimpleNamespac
def _capture_encrypted_item(
item: Any, item_type: str, issuer_kind: Optional[str], issuer_model: Optional[str] = None,
) -> Optional[Dict[str, Any]]:
) -> Optional[dict[str, Any]]:
"""``{type, encrypted_content[, _issuer_kind, _issuer_model]}`` for replay, or None without a blob. Reasoning
items also carry ``id`` + ``summary`` (required by the API on replay); transient ``rs_tmp_`` skip."""
encrypted = getattr(item, "encrypted_content", None)
if not _nonempty_str(encrypted):
return None
raw_item: Dict[str, Any] = {"type": item_type, "encrypted_content": encrypted}
raw_item: dict[str, Any] = {"type": item_type, "encrypted_content": encrypted}
if issuer_kind:
raw_item["_issuer_kind"] = issuer_kind
if issuer_model:
@@ -1112,7 +1114,7 @@ class _OutputScan:
self.saw_streaming_or_item_incomplete = response_status in {"queued", "in_progress"}
self.saw_commentary_phase = self.saw_final_answer_phase = self.saw_reasoning_item = False
def scan(self, output: List[Any], issuer_kind: Optional[str], issuer_model: Optional[str] = None) -> None:
def scan(self, output: list[Any], issuer_kind: Optional[str], issuer_model: Optional[str] = None) -> None:
for item in output:
item_type = getattr(item, "type", None)
item_status = _lower_or_none(getattr(item, "status", None))
@@ -1176,7 +1178,7 @@ def _normalize_codex_response(
if out_text:
msg = "Codex response has empty output but output_text is present (%d chars); synthesizing output item."
logger.debug(msg, len(out_text))
content: List[Any] = [SimpleNamespace(type="output_text", text=out_text)]
content: list[Any] = [SimpleNamespace(type="output_text", text=out_text)]
elif response_incomplete_content_filter:
# Provider safety block, not a partial answer: finish content_filter, not incomplete.
content = []
+12 -12
View File
@@ -592,7 +592,7 @@ def _codex_turn_service_tier(agent) -> str | None:
return CODEX_TIER_WORDS.get(effective_request_overrides(agent).get("service_tier"))
def _ensure_codex_session(agent, messages: List[Dict[str, Any]] | None = None) -> None:
def _ensure_codex_session(agent, messages: list[dict[str, Any]] | None = None) -> None:
"""Lazily spawn one CodexAppServerSession per AIAgent (reused across turns, closed by the _cleanup hook).
A live session whose thread was started with a different prompt composition (TUI/Desktop ``/personality``
or a prompt mirror mutate the agent in place) is retired first so the new thread carries the current one.
@@ -659,7 +659,7 @@ def _ensure_codex_session(agent, messages: List[Dict[str, Any]] | None = None) -
)
def _persist_projected_messages(agent, turn, messages: List[Dict[str, Any]]) -> bool:
def _persist_projected_messages(agent, turn, messages: list[dict[str, Any]]) -> bool:
"""Splice the projected messages into ``messages`` and flush them to the session DB; True when the
rows are durable in the session DB (the codex thread binding may then be published).
@@ -693,7 +693,7 @@ def _persist_projected_messages(agent, turn, messages: List[Dict[str, Any]]) ->
return flush_ok is True
def _finish_codex_turn(agent, turn, messages: List[Dict[str, Any]], *, original_user_message: Any,
def _finish_codex_turn(agent, turn, messages: list[dict[str, Any]], *, original_user_message: Any,
should_review_memory: bool) -> dict[str, Any]:
"""Post-turn bookkeeping mirroring the chat_completions loop; returns usage fields."""
# run_conversation() already bumped _turns_since_memory / _user_turn_count; only _iters_since_skill is ours.
@@ -718,8 +718,8 @@ def _finish_codex_turn(agent, turn, messages: List[Dict[str, Any]], *, original_
return usage_result
def run_codex_app_server_turn(agent, *, user_message: str, original_user_message: Any, messages: List[Dict[str, Any]],
effective_task_id: str, should_review_memory: bool = False) -> Dict[str, Any]:
def run_codex_app_server_turn(agent, *, user_message: str, original_user_message: Any, messages: list[dict[str, Any]],
effective_task_id: str, should_review_memory: bool = False) -> dict[str, Any]:
"""Hand the turn to a ``codex app-server`` subprocess and project its events into ``messages``.
Returns the chat_completions result shape. The user message is ALREADY in ``messages`` — never append it again."""
# Defense in depth for compression.checkpoint_required: agent init refuses the combination, but
@@ -764,8 +764,8 @@ def run_codex_app_server_turn(agent, *, user_message: str, original_user_message
)
def _turn_result(interrupt: tuple[bool, Any], messages: List[Dict[str, Any]], *, api_calls: int, completed: bool,
error: Any, final_response: Any, **extra: Any) -> Dict[str, Any]:
def _turn_result(interrupt: tuple[bool, Any], messages: list[dict[str, Any]], *, api_calls: int, completed: bool,
error: Any, final_response: Any, **extra: Any) -> dict[str, Any]:
"""Result shape shared with the chat_completions path (``partial`` == ``not completed``)."""
user_interrupted, interrupt_message = interrupt
return {
@@ -862,15 +862,15 @@ class _CodexResponseAssembler:
def __init__(self, *, model, on_text_delta, on_reasoning_delta, on_commentary_message, on_first_delta):
self.model, self.on_text_delta, self.on_reasoning_delta = model, on_text_delta, on_reasoning_delta
self.on_commentary_message, self.on_first_delta = on_commentary_message, on_first_delta
self.output_items: List[Any] = []
self.output_items: list[Any] = []
# output_index / first-observed sequence per output item, in lockstep, so settled pending calls merge
# back in stream order.
self.output_indexes, self.output_sequences = [], []
self.text_deltas, self.commentary_text_deltas = [], []
# pending_function_calls: announced-but-unconfirmed function calls keyed by item id. announced_output_order:
# first-observed (sequence, output_index) per announced item id so a later .done keeps its announced position.
self.pending_function_calls: Dict[str, Dict[str, Any]] = {}
self.announced_output_order: Dict[str, tuple] = {}
self.pending_function_calls: dict[str, dict[str, Any]] = {}
self.announced_output_order: dict[str, tuple] = {}
def _safe(self, cb: Callable | None, label: str, *args: Any) -> None:
_call_guarded(cb, f"Codex stream {label} raised", args=args)
@@ -1009,7 +1009,7 @@ class _CodexResponseAssembler:
handler = self._EXACT_HANDLERS.get(event_type) or next((h for m, h in self._FUZZY_HANDLERS if m(event_type)), None)
return bool(handler(self, event, event_type)) if handler is not None else False
def _settled_output(self) -> List[Any]:
def _settled_output(self) -> list[Any]:
"""Merge .done items with settled pending calls, keeping stream order."""
indexed = list(zip(self.output_indexes, self.output_sequences, self.output_items))
for pending in self.pending_function_calls.values():
@@ -1031,7 +1031,7 @@ class _CodexResponseAssembler:
def result(self) -> SimpleNamespace:
# With only plain text deltas (no tool calls), synthesize one message item.
output: List[Any] = list(self.output_items)
output: list[Any] = list(self.output_items)
if not output and self.text_deltas and not self.has_tool_calls:
content = [SimpleNamespace(type="output_text", text="".join(self.text_deltas))]
output = [SimpleNamespace(type="message", role="assistant", status="completed", content=content)]
+2 -2
View File
@@ -32,7 +32,7 @@ def _text_of(content: Any) -> str:
return ""
def _render_row(msg: Dict[str, Any]) -> str:
def _render_row(msg: dict[str, Any]) -> str:
role = msg.get("role")
text = _text_of(msg.get("content")).strip()
if role == "user":
@@ -50,7 +50,7 @@ def _render_row(msg: Dict[str, Any]) -> str:
return "" # system rows are the prompt composition, already sent as developerInstructions
def render_history_seed(messages: List[Dict[str, Any]] | None) -> str:
def render_history_seed(messages: list[dict[str, Any]] | None) -> str:
"""Prior turns as one text block, newest last; empty when there is nothing before the current
user message. The trailing user row is the turn being submitted and is never included."""
rows = list(messages or [])
+1 -1
View File
@@ -30,7 +30,7 @@ _COMPACTION_INTERNAL_FIELDS = (
)
def project_compaction_message_for_display(message: Dict[str, Any]) -> Optional[Dict[str, Any]]:
def project_compaction_message_for_display(message: dict[str, Any]) -> Optional[dict[str, Any]]:
"""Return authentic transcript content, or ``None`` for a pure handoff.
Model-facing recovery history retains the complete carrier. Display
+21 -21
View File
@@ -76,10 +76,10 @@ def _skills_block(stable: str) -> str:
return m.group(0) if m else ""
def _split_tools(tools: Sequence[dict]) -> Tuple[List[dict], List[dict], List[dict]]:
builtin: List[dict] = []
mcp: List[dict] = []
subagent: List[dict] = []
def _split_tools(tools: Sequence[dict]) -> tuple[list[dict], list[dict], list[dict]]:
builtin: list[dict] = []
mcp: list[dict] = []
subagent: list[dict] = []
for tool in tools:
fn = tool.get("function") if isinstance(tool, dict) else None
name = str((fn if isinstance(fn, dict) else tool).get("name") or "")
@@ -88,7 +88,7 @@ def _split_tools(tools: Sequence[dict]) -> Tuple[List[dict], List[dict], List[di
return builtin, mcp, subagent
def _memory_blocks(agent: Any) -> Tuple[str, str]:
def _memory_blocks(agent: Any) -> tuple[str, str]:
memory_block = user_block = ""
store = getattr(agent, "_memory_store", None)
try:
@@ -112,7 +112,7 @@ def _join(*parts: str) -> str:
return "\n\n".join(part for part in parts if part).strip()
def _glyph(cat: Dict[str, Any]) -> str:
def _glyph(cat: dict[str, Any]) -> str:
return _CATEGORIES.get(str(cat.get("id") or ""), (None, "▪"))[1]
@@ -127,7 +127,7 @@ def context_display_source(compressor: Any) -> str:
return "local_estimate" if isinstance(real, (int, float)) and shown > 0 and shown != real else "provider_usage"
def context_usage_fields(compressor: Any) -> Dict[str, Any]:
def context_usage_fields(compressor: Any) -> dict[str, Any]:
"""Current occupancy only; lifetime throughput is never a context fallback."""
used = max(0, getattr(compressor, "last_prompt_tokens", 0) or 0)
maximum = getattr(compressor, "context_length", 0) or 0
@@ -140,7 +140,7 @@ def context_usage_fields(compressor: Any) -> Dict[str, Any]:
"context_source": source, "context_estimated": source != "provider_usage"}
def compute_session_context_breakdown(agent: Any, messages: Optional[List[dict]] = None) -> Dict[str, Any]:
def compute_session_context_breakdown(agent: Any, messages: Optional[list[dict]] = None) -> dict[str, Any]:
"""Return a Cursor-style context usage breakdown for one live agent."""
from agent.model_metadata import estimate_messages_tokens_rough
from agent.usage_anchor import anchored_context_tokens
@@ -209,7 +209,7 @@ def compute_session_context_breakdown(agent: Any, messages: Optional[List[dict]]
}
def compute_context_details(agent: Any) -> Dict[str, Any]:
def compute_context_details(agent: Any) -> dict[str, Any]:
"""Expanded per-skill / per-toolset cost listing for ``/context all``.
Reuses the ``hermes prompt-size`` attribution (index-line bytes from the
@@ -245,12 +245,12 @@ def compute_context_details(agent: Any) -> Dict[str, Any]:
# (monospace is not guaranteed on messaging platforms).
def render_context_grid(payload: Dict[str, Any]) -> List[str]:
def render_context_grid(payload: dict[str, Any]) -> list[str]:
"""Glyph grid: 100 cells, one per percent of the context window; categories
fill in declaration order, the remainder is free space."""
context_max = int(payload.get("context_max") or 0)
total_cells = _GRID_COLUMNS * _GRID_ROWS
cells: List[str] = []
cells: list[str] = []
if context_max > 0:
for cat in payload.get("categories") or []:
tokens = int(cat.get("tokens") or 0)
@@ -262,7 +262,7 @@ def render_context_grid(payload: Dict[str, Any]) -> List[str]:
return [" ".join(cells[row * _GRID_COLUMNS:(row + 1) * _GRID_COLUMNS]) for row in range(_GRID_ROWS)]
def render_context_category_lines(payload: Dict[str, Any]) -> List[str]:
def render_context_category_lines(payload: dict[str, Any]) -> list[str]:
"""Render the 'Estimated usage by category' table as plain-text lines."""
categories = payload.get("categories") or []
context_max = int(payload.get("context_max") or 0)
@@ -285,12 +285,12 @@ def render_context_category_lines(payload: Dict[str, Any]) -> List[str]:
return lines
def _toolset_row(group: Dict[str, Any]) -> str:
def _toolset_row(group: dict[str, Any]) -> str:
return t("gateway.context.toolset_row", toolset=f"{group['toolset']:<24}", count=f"{group['tool_count']:>3}",
tokens=f"{group['schema_tokens']:>8,}")
def _skill_row(entry: Dict[str, Any]) -> str:
def _skill_row(entry: dict[str, Any]) -> str:
name = str(entry.get("name") or "")
if len(name) > 28:
name = name[:27] + "…"
@@ -299,7 +299,7 @@ def _skill_row(entry: Dict[str, Any]) -> str:
return t("gateway.context.skill_row", name=f"{name:<28}", index_tokens=f"{entry['index_tokens']:>6,}", md_tokens=md_str)
def _table(lines: List[str], title: str, rows: List[Dict[str, Any]], fmt) -> None:
def _table(lines: list[str], title: str, rows: list[dict[str, Any]], fmt) -> None:
"""Append a titled, display-capped table (blank-separated from a preceding one)."""
if not rows:
return
@@ -311,23 +311,23 @@ def _table(lines: List[str], title: str, rows: List[Dict[str, Any]], fmt) -> Non
lines.append(t("gateway.context.and_more", count=len(rows) - _DETAILS_TABLE_LIMIT))
def render_context_details_lines(details: Dict[str, Any]) -> List[str]:
def render_context_details_lines(details: dict[str, Any]) -> list[str]:
"""Render the expanded ``/context all`` per-skill / per-toolset tables."""
lines: List[str] = []
lines: list[str] = []
_table(lines, t("gateway.context.toolsets_title"), details.get("toolsets") or [], _toolset_row)
_table(lines, t("gateway.context.skills_title"), details.get("skills") or [], _skill_row)
return lines
def render_context_breakdown_lines(
payload: Dict[str, Any],
payload: dict[str, Any],
*,
details: Optional[Dict[str, Any]] = None,
details: Optional[dict[str, Any]] = None,
grid: bool = True,
) -> List[str]:
) -> list[str]:
"""Full /context view. ``grid`` prepends the glyph grid (CLI; the gateway
keeps its own gauge); ``details`` appends the expanded listings."""
lines: List[str] = [*render_context_grid(payload), ""] if grid else []
lines: list[str] = [*render_context_grid(payload), ""] if grid else []
lines.extend(render_context_category_lines(payload))
context_max = int(payload.get("context_max") or 0)
+140 -140
View File
@@ -73,7 +73,7 @@ def _safe_int(value: Any) -> int | None:
# compression run (its only non-recursive call site is the compress path; the two recursive calls are the
# deliberate main-model retry that must NOT re-issue the pin). The summary call is the ONLY auxiliary LLM
# call a lean compaction attempt makes (#96603) — there are no sibling digest calls.
_SUMMARY_ROUTE_PIN: contextvars.ContextVar[Optional[Dict[str, Any]]] = (
_SUMMARY_ROUTE_PIN: contextvars.ContextVar[Optional[dict[str, Any]]] = (
contextvars.ContextVar("hermes_summary_route_pin", default=None)
)
@@ -82,7 +82,7 @@ _PINNED_ROUTE_FIELDS: tuple[str, ...] = ("provider", "model", "base_url", "api_k
@contextlib.contextmanager
def pin_summary_route(route: Optional[Dict[str, Any]]):
def pin_summary_route(route: Optional[dict[str, Any]]):
"""Pin the next summary LLM call to an explicit route; ``None`` is a no-op. Re-entrant: restores the prior pin."""
token = _SUMMARY_ROUTE_PIN.set(route if isinstance(route, dict) else None)
try:
@@ -91,7 +91,7 @@ def pin_summary_route(route: Optional[Dict[str, Any]]):
_SUMMARY_ROUTE_PIN.reset(token)
def take_pinned_summary_route() -> Optional[Dict[str, Any]]:
def take_pinned_summary_route() -> Optional[dict[str, Any]]:
"""Read and consume the pinned summary route (single use: the main-model retry must not re-issue it)."""
route = _SUMMARY_ROUTE_PIN.get()
if route is not None:
@@ -103,7 +103,7 @@ def take_pinned_summary_route() -> Optional[Dict[str, Any]]:
# fallback summary instead (``abort_on_summary_failure`` still aborts). The host pins it when the summary
# route stalls again after a stall-class backoff already burned one idle window (#112420), so a provably
# unhealthy route degrades once instead of re-entering the same silent stream every turn.
DETERMINISTIC_SUMMARY_ROUTE: Dict[str, Any] = {"label": "deterministic fallback summary", "deterministic": True}
DETERMINISTIC_SUMMARY_ROUTE: dict[str, Any] = {"label": "deterministic fallback summary", "deterministic": True}
def take_deterministic_summary_pin() -> bool:
@@ -115,7 +115,7 @@ def take_deterministic_summary_pin() -> bool:
return True
def _pinned_summary_call_kwargs() -> Dict[str, Any]:
def _pinned_summary_call_kwargs() -> dict[str, Any]:
"""Consume the pinned route as explicit ``call_llm`` keyword arguments."""
route = take_pinned_summary_route() or {}
return {field: route[field] for field in _PINNED_ROUTE_FIELDS if route.get(field) not in (None, "")}
@@ -347,7 +347,7 @@ MAX_ITERATIONS_SUMMARY_REQUEST = (
_BACKGROUND_PROCESS_NOTIFICATION_PREFIX = "[IMPORTANT: Background process "
def _fresh_compaction_message_copy(msg: Dict[str, Any]) -> Dict[str, Any]:
def _fresh_compaction_message_copy(msg: dict[str, Any]) -> dict[str, Any]:
"""Copy a message for compaction assembly without persistence markers (``_strip_persistence_markers`` is authoritative)."""
fresh = msg.copy()
fresh.pop(_DB_PERSISTED_MARKER, None)
@@ -364,7 +364,7 @@ def _template_visible_role(message: Any) -> Optional[str]:
return None if role == "tool" or (role == "assistant" and message.get("tool_calls")) else role
def _last_template_visible_role(messages: List[Dict[str, Any]]) -> Optional[str]:
def _last_template_visible_role(messages: list[dict[str, Any]]) -> Optional[str]:
"""Last role a strict alternation template would count in *messages*.
``None`` when every row is template-exempt (tool flow only).
@@ -379,7 +379,7 @@ def _last_template_visible_role(messages: List[Dict[str, Any]]) -> Optional[str]
)
def _strip_persistence_markers(messages: List[Dict[str, Any]]) -> None:
def _strip_persistence_markers(messages: list[dict[str, Any]]) -> None:
"""Enforce the invariant: no assembled message carries a persistence marker.
A leaked ``_db_persisted`` makes the child-session rotation flush skip the row, losing it from state.db.
Per-copy-site strips are positional and re-leak when a copy site is added; this terminal sweep makes the
@@ -399,7 +399,7 @@ class StaleHeldHistory(RuntimeError):
"""
def _archive_watermark_for(session_db: Any, session_id: str, held: List[Dict[str, Any]],
def _archive_watermark_for(session_db: Any, session_id: str, held: list[dict[str, Any]],
start_watermark: Optional[int] = None) -> Optional[int]:
"""The archive watermark for a commit that rewrites the history this process holds.
@@ -426,7 +426,7 @@ def _archive_watermark_for(session_db: Any, session_id: str, held: List[Dict[str
return held_archive_watermark(session_db, session_id, start_watermark, held, stale_raises=True)
def stamp_db_persisted_markers(messages: List[Dict[str, Any]]) -> None:
def stamp_db_persisted_markers(messages: list[dict[str, Any]]) -> None:
"""Fulfil the post-commit contract of ``SessionDB.archive_and_compact()``.
Single stamp site for all callers. Call ONLY after the commit succeeded, on the dict instances the
caller keeps live. Needed because compress() output is marker-swept for the ROTATION flush; an
@@ -441,7 +441,7 @@ def _is_checkpoint_item(item: Any) -> bool:
return isinstance(item, dict) and item.get("type") == "compaction"
def _newest_checkpoint_carrier(messages: List[Dict[str, Any]], key: str) -> int:
def _newest_checkpoint_carrier(messages: list[dict[str, Any]], key: str) -> int:
"""Index of the last assistant message carrying a ``type: "compaction"`` item under *key*, or -1.
Transcript-side mirror of ``native_compaction.prune_pre_checkpoint_items``' newest-run-wins rule:
the wire builder drops every checkpoint before the last one, so this is the only carrier whose
@@ -456,7 +456,7 @@ def _newest_checkpoint_carrier(messages: List[Dict[str, Any]], key: str) -> int:
return -1
def _set_sidecar(msg: Dict[str, Any], key: str, kept: List[Any]) -> None:
def _set_sidecar(msg: dict[str, Any], key: str, kept: list[Any]) -> None:
"""Filter items, never leave an empty sidecar behind."""
if kept:
msg[key] = kept
@@ -465,8 +465,8 @@ def _set_sidecar(msg: Dict[str, Any], key: str, kept: List[Any]) -> None:
def drop_shadowed_checkpoints(
messages: List[Dict[str, Any]], key: str = "codex_reasoning_items", *, before: Optional[int] = None,
) -> List[int]:
messages: list[dict[str, Any]], key: str = "codex_reasoning_items", *, before: Optional[int] = None,
) -> list[int]:
"""Drop ``type: "compaction"`` items from every assistant row older than the newest carrier (rows at
index >= *before* are left alone). A checkpoint a newer carrier shadows has no reader on any wire:
``prune_pre_checkpoint_items`` rebuilds each request around the newest checkpoint run and the replay
@@ -474,7 +474,7 @@ def drop_shadowed_checkpoints(
In place; returns the indices rewritten."""
newest = _newest_checkpoint_carrier(messages, key)
stop = newest if before is None else min(newest, before)
rewritten: List[int] = []
rewritten: list[int] = []
for i in range(max(stop, 0)):
msg = messages[i]
if not isinstance(msg, dict) or msg.get("role") != "assistant":
@@ -487,7 +487,7 @@ def drop_shadowed_checkpoints(
return rewritten
def _prune_stale_reasoning_replay(messages: List[Dict[str, Any]]) -> int:
def _prune_stale_reasoning_replay(messages: list[dict[str, Any]]) -> int:
"""Strip stale ``codex_reasoning_items`` from assistant turns older than the active one.
Boundary is the last USER message (a turn spans several assistant rows): the Responses API replays a
turn's bridging reasoning items together, so cutting at the last ASSISTANT would strip mid-chain.
@@ -546,7 +546,7 @@ _SALVAGE_SUMMARY_MAX_CHARS = 8_000
_SALVAGE_KEEP_RECENT_TOOLS = 2
def _looks_like_compaction_summary(msg: Dict[str, Any], content: str) -> bool:
def _looks_like_compaction_summary(msg: dict[str, Any], content: str) -> bool:
# Only cap standalone handoffs; merged carriers contain live user text. Content heuristics never
# authorize mutating a live turn: require the private compressor marker. Tool messages are
# handled only by the stub/keep-recent pass.
@@ -562,7 +562,7 @@ def _looks_like_compaction_summary(msg: Dict[str, Any], content: str) -> bool:
return bool(msg.get(COMPRESSED_SUMMARY_METADATA_KEY)) or "CONTEXT COMPACTION" in head or "Conversation Summary" in head
def _salvage_reduce_todo_snapshot(out: List[Dict[str, Any]]) -> None:
def _salvage_reduce_todo_snapshot(out: list[dict[str, Any]]) -> None:
"""Last-resort shrink: drop the synthetic todo snapshot, keeping only a pruned-skill reload notice if present."""
from agent.conversation_compression import _PRUNED_SKILL_RELOAD_NOTICE_HEADER
for i in range(len(out) - 1, -1, -1):
@@ -579,8 +579,8 @@ def _salvage_reduce_todo_snapshot(out: List[Dict[str, Any]]) -> None:
def salvage_grown_transcript(
original: List[Dict[str, Any]], candidate: List[Dict[str, Any]], budget: Optional[int] = None,
) -> Optional[List[Dict[str, Any]]]:
original: list[dict[str, Any]], candidate: list[dict[str, Any]], budget: Optional[int] = None,
) -> Optional[list[dict[str, Any]]]:
"""Mechanically shrink a compression candidate (copies, cheapest loss first); ``None`` unless strictly smaller."""
if not candidate or not original:
return None
@@ -699,7 +699,7 @@ _RESTART_HANDOFF_PROBE_EXTRA_MESSAGES = 4
class _HandoffScan:
"""Result of ``ContextCompressor._scan_window_handoffs``."""
turns_to_summarize: List[Dict[str, Any]]
turns_to_summarize: list[dict[str, Any]]
summary_indices: set
tail_start: int
previous_summary_before: Optional[str]
@@ -899,7 +899,7 @@ def _extract_pruned_skill_names(text: str) -> list[str]:
return list(dict.fromkeys(m.group(1) for m in _SKILL_PRUNED_MARKER_RE.finditer(text or "")))
def _collect_ghosted_skill_names(turns: List[Dict[str, Any]]) -> list[str]:
def _collect_ghosted_skill_names(turns: list[dict[str, Any]]) -> list[str]:
"""Skill names about to be lost in compaction: demoted ``skill_view`` rows and raw, never-demoted bodies."""
call_id_to_skill: dict[str, str] = {}
for idx, skill in _skill_view_call_sites(turns):
@@ -980,7 +980,7 @@ def _synthetic_user_row(content: str) -> bool:
return content.lstrip().startswith(_SYNTHETIC_USER_ROW_PREFIXES)
def _build_verbatim_user_section(turns: List[Dict[str, Any]]) -> str:
def _build_verbatim_user_section(turns: list[dict[str, Any]]) -> str:
"""Compacted region's REAL user messages verbatim, newest-first under a char budget (straddler truncated); "" if none."""
collected: list[str] = []
used = 0
@@ -1069,7 +1069,7 @@ _ANCHOR_NOISE = frozenset({
})
def _build_anchor_index(turns: List[Dict[str, Any]]) -> str:
def _build_anchor_index(turns: list[dict[str, Any]]) -> str:
"""Regex-harvest exact identifiers from the compacted region (LLM-free); per-category caps, most-frequent first."""
text = "\n".join(c for c in (msg.get("content") for msg in turns) if isinstance(c, str) and c)
if not text:
@@ -1110,7 +1110,7 @@ def _build_anchor_index(turns: List[Dict[str, Any]]) -> str:
_SKILL_PRUNE_RECENT_WINDOW = 10
def _skill_view_call_sites(messages: List[Dict[str, Any]]) -> list[tuple[int, str]]:
def _skill_view_call_sites(messages: list[dict[str, Any]]) -> list[tuple[int, str]]:
"""Yield ``(message_index, skill_name)`` for every skill_view tool call."""
sites: list[tuple[int, str]] = []
for i, msg in enumerate(messages):
@@ -1127,7 +1127,7 @@ def _skill_view_call_sites(messages: List[Dict[str, Any]]) -> list[tuple[int, st
return sites
def _collect_protected_skill_names(messages: List[Dict[str, Any]], prune_boundary: int) -> set[str]:
def _collect_protected_skill_names(messages: list[dict[str, Any]], prune_boundary: int) -> set[str]:
"""Skill names (lower-cased) whose skill_view bodies must survive Phase-1 demotion.
Recently loaded, loaded inside the protected tail, or named by a tail user message. Applies to
Phase-1/2 only; the Pass-4 pressure demotion ignores it."""
@@ -1216,9 +1216,9 @@ def _extract_tool_call_name_and_args(tool_call: Any) -> tuple[str, str]:
return str(_tc_get(fn, "name") or "unknown"), str(_tc_get(fn, "arguments") or "")
def _tool_calls_by_id(messages: List[Dict[str, Any]]) -> Dict[str, tuple]:
def _tool_calls_by_id(messages: list[dict[str, Any]]) -> dict[str, tuple]:
"""Map ``tool_call_id -> (tool_name, raw_arguments)`` over every assistant tool call."""
out: Dict[str, tuple] = {}
out: dict[str, tuple] = {}
for msg in messages:
if msg.get("role") != "assistant":
continue
@@ -1352,17 +1352,17 @@ def _estimate_msg_budget_tokens(msg: dict, charge_stale_thinking: bool = True) -
return tokens
def _last_index_with_role(messages: "List[Dict[str, Any]]", role: str) -> int:
def _last_index_with_role(messages: "list[dict[str, Any]]", role: str) -> int:
"""Index of the newest dict message with ``role``, or -1."""
return max((i for i, m in enumerate(messages) if isinstance(m, dict) and m.get("role") == role), default=-1)
def _last_assistant_index(messages: "List[Dict[str, Any]]") -> int:
def _last_assistant_index(messages: "list[dict[str, Any]]") -> int:
"""Newest assistant message index, or -1 (the one turn whose thinking may replay; see ``_NEWEST_TURN_ONLY_BUDGET_KEYS``)."""
return _last_index_with_role(messages, "assistant")
def _pending_tool_round(messages: "List[Dict[str, Any]]") -> range:
def _pending_tool_round(messages: "list[dict[str, Any]]") -> range:
"""Indices of the tool results the transcript ends with — a round the model has not answered yet; empty when
the transcript ends in any other row. /steer rows after the round do not answer it: a steer is delivered
after the newest tool result before the next API call, and two can land in one iteration (one when the tool
@@ -1452,7 +1452,7 @@ def _append_text_to_content(content: Any, text: str, *, prepend: bool = False) -
return text + rendered if prepend else rendered + text
def _replace_image_parts(parts: Any, placeholder: str) -> Optional[List[Any]]:
def _replace_image_parts(parts: Any, placeholder: str) -> Optional[list[Any]]:
"""New parts list with every image part replaced by a text placeholder; None if no images."""
if not isinstance(parts, list) or not any(_is_image_part(p) for p in parts):
return None
@@ -1469,7 +1469,7 @@ def _tool_content_has_images(content: Any) -> bool:
return _content_has_images(_tool_result_parts(content))
def _strip_images_from_tool_msg(msg: Dict[str, Any]) -> Optional[Dict[str, Any]]:
def _strip_images_from_tool_msg(msg: dict[str, Any]) -> Optional[dict[str, Any]]:
"""Copy of a tool message with image payloads replaced (stale ``api_content`` dropped); ``None`` if nothing to strip."""
content = msg.get("content")
if isinstance(content, dict) and content.get("_multimodal"):
@@ -1479,7 +1479,7 @@ def _strip_images_from_tool_msg(msg: Dict[str, Any]) -> Optional[Dict[str, Any]]
return None if stripped is None else _rewritten(msg, stripped)
def _rewritten(msg: Dict[str, Any], content: Any) -> Dict[str, Any]:
def _rewritten(msg: dict[str, Any], content: Any) -> dict[str, Any]:
"""Copy of ``msg`` carrying ``content``; drops the stale ``api_content`` sidecar so replay can't resend it."""
new_msg = {**msg, "content": content}
drop_stale_api_content(new_msg)
@@ -1487,7 +1487,7 @@ def _rewritten(msg: Dict[str, Any], content: Any) -> Dict[str, Any]:
def _retire_stale_tool_result_images(
result: List[Dict[str, Any]], keep_newest: int = _MAX_KEEP_TOOL_IMAGES, spared: range = range(0),
result: list[dict[str, Any]], keep_newest: int = _MAX_KEEP_TOOL_IMAGES, spared: range = range(0),
) -> int:
"""Replace image payloads on older tool results with text placeholders.
Keeps the newest ``keep_newest`` image-bearing tool messages and any spared pending round;
@@ -1511,7 +1511,7 @@ def _retire_stale_tool_result_images(
return pruned
def _image_payload(msg: Dict[str, Any]) -> Tuple[int, int]:
def _image_payload(msg: dict[str, Any]) -> tuple[int, int]:
"""``(blocks, bytes)`` of image payload in a message.
The provider counts BLOCKS: one ``tool_result`` carrying three screenshots is three against
@@ -1537,7 +1537,7 @@ def _image_payload(msg: Dict[str, Any]) -> Tuple[int, int]:
return blocks, payload
def evict_stale_outbound_tool_images(api_messages: List[Dict[str, Any]]) -> int:
def evict_stale_outbound_tool_images(api_messages: list[dict[str, Any]]) -> int:
"""Drop stale screenshot/vision payloads from the per-call API copy.
Compression's keep-newest pass only runs when prune/compress fires, and the Anthropic
@@ -1550,7 +1550,7 @@ def evict_stale_outbound_tool_images(api_messages: List[Dict[str, Any]]) -> int:
reserved against the ceiling but never rewritten — policy and rationale in
:mod:`agent.image_eviction_policy`. Returns the number of messages rewritten.
"""
carriers: List[Tuple[int, Tuple[int, int]]] = []
carriers: list[tuple[int, tuple[int, int]]] = []
reserved_blocks = reserved_bytes = 0
for i in range(len(api_messages) - 1, -1, -1):
msg = api_messages[i]
@@ -1598,7 +1598,7 @@ def _strip_images_from_content(content: Any) -> Any:
return content if stripped is None else stripped
def _strip_historical_media(messages: List[Dict[str, Any]], spared: range = range(0)) -> List[Dict[str, Any]]:
def _strip_historical_media(messages: list[dict[str, Any]], spared: range = range(0)) -> list[dict[str, Any]]:
"""Replace image parts in older messages with placeholder text.
Rule 1: strip everything before the newest image-bearing user message. Rule 1b: the opening
attachment ages out once a newer tool image exists. Rule 2: keep only the newest tool-result image,
@@ -1621,7 +1621,7 @@ def _strip_historical_media(messages: List[Dict[str, Any]], spared: range = rang
# Nothing to strip under any rule.
return messages
def _is_stale(index: int, message: Dict[str, Any]) -> bool:
def _is_stale(index: int, message: dict[str, Any]) -> bool:
if index in spared:
return False
# Rule 1: everything before the newest image-bearing user message. Rule 1b: the opening
@@ -1638,7 +1638,7 @@ def _strip_historical_media(messages: List[Dict[str, Any]], spared: range = rang
or (message.get("role") == "tool" and index != tool_anchor)
)
def _stripped(i: int, msg: Any) -> Optional[Dict[str, Any]]:
def _stripped(i: int, msg: Any) -> Optional[dict[str, Any]]:
if not isinstance(msg, dict) or not _is_stale(i, msg):
return None
content = msg.get("content")
@@ -1664,7 +1664,7 @@ def _summary_part_text(part: Any) -> str:
return _image_part_label(part) if ptype in _IMAGE_PART_TYPES else f"[{ptype or 'attachment'}]"
def _image_part_label(part: Dict[str, Any]) -> str:
def _image_part_label(part: dict[str, Any]) -> str:
"""Short summarizer label for an image part: http(s) URLs kept as a handle, ``data:`` URLs collapse to ``[image]``."""
url = part.get("image_url")
if isinstance(url, dict):
@@ -1684,7 +1684,7 @@ def _summarize_tool_result(tool_name: str, tool_args: str, tool_content: str) ->
"""1-line summary of a tool call + result. Never raises: a malformed historical call must not crash-loop compression."""
try:
return _summarize_tool_result_unguarded(tool_name, tool_args, tool_content)
except Exception as exc: # noqa: BLE001 — a summary must never crash compression
except Exception as exc:
logger.debug("Tool-result summary failed for %s: %s", tool_name, exc)
_len = len(tool_content) if isinstance(tool_content, str) else 0
return f"[{tool_name}] ({_len:,} chars result)"
@@ -2110,7 +2110,7 @@ def _today_for_prompt() -> str:
# Per-section summarizer instructions, keyed by "the transcript has a real user turn". Wording
# is deliberately plain: Azure/OpenAI content filters have flagged stronger "injection" /
# "do not respond" framing. Prompt text is byte-pinned — restructure code around it only.
_SECTION_INSTRUCTIONS: Dict[bool, Dict[str, str]] = {
_SECTION_INSTRUCTIONS: dict[bool, dict[str, str]] = {
True: {
"language": (
"Write the summary in the same language the user was using in the "
@@ -2199,14 +2199,14 @@ class ContextCompressor(SummaryDispatchMixin, PreLlmSkipMixin, MicroCompactionMi
def _begin_compression_telemetry(
self, *, current_tokens: int | None, attempt_id: str | None = None, session_id: str | None = None,
trigger_source: str | None = None,
) -> Dict[str, Any]:
) -> dict[str, Any]:
"""Initialize content-free per-attempt compression telemetry."""
seed = getattr(self, "_compression_telemetry_seed", None)
seed = seed if isinstance(seed, dict) else {}
attempt_id = attempt_id or seed.get("attempt_id")
session_id = session_id or seed.get("session_id")
trigger_source = trigger_source or seed.get("trigger_source")
telemetry: Dict[str, Any] = {
telemetry: dict[str, Any] = {
"event": "compression_attempt", "attempt_id": attempt_id or uuid.uuid4().hex,
"session_id": session_id or "", "trigger_source": trigger_source or "unknown",
"main_provider": self.provider or "", "main_model": self.model or "",
@@ -2228,8 +2228,8 @@ class ContextCompressor(SummaryDispatchMixin, PreLlmSkipMixin, MicroCompactionMi
return telemetry
def _record_compression_regions(
self, *, head_messages: List[Dict[str, Any]], middle_messages: List[Dict[str, Any]],
tail_messages: List[Dict[str, Any]],
self, *, head_messages: list[dict[str, Any]], middle_messages: list[dict[str, Any]],
tail_messages: list[dict[str, Any]],
) -> None:
telemetry = getattr(self, "_active_compression_telemetry", None)
if isinstance(telemetry, dict):
@@ -2238,9 +2238,9 @@ class ContextCompressor(SummaryDispatchMixin, PreLlmSkipMixin, MicroCompactionMi
telemetry["protected_tail_tokens"] = estimate_messages_tokens_rough(tail_messages)
def _record_aux_compression_call(
self, *, prompt_messages: List[Dict[str, Any]], max_tokens: int | None, duration_ms: int,
self, *, prompt_messages: list[dict[str, Any]], max_tokens: int | None, duration_ms: int,
aux_provider: str | None = None, aux_model: str | None = None,
effective_aux_context: int | None = None, phase_timings: Dict[str, Any] | None = None,
effective_aux_context: int | None = None, phase_timings: dict[str, Any] | None = None,
) -> None:
telemetry = getattr(self, "_active_compression_telemetry", None)
if not isinstance(telemetry, dict):
@@ -2349,7 +2349,7 @@ class ContextCompressor(SummaryDispatchMixin, PreLlmSkipMixin, MicroCompactionMi
def max_summary_tokens(self, value: int) -> None:
self._max_summary_tokens = value
def on_session_end(self, session_id: str, messages: List[Dict[str, Any]]) -> None:
def on_session_end(self, session_id: str, messages: list[dict[str, Any]]) -> None:
"""Clear all per-session compaction state at a real session boundary.
Session end (CLI exit, gateway expiry, id rotation) — NOT /new or /reset. Every per-session
flag/counter can contaminate the next live session (suppressed compression, stale cooldowns,
@@ -2637,7 +2637,7 @@ class ContextCompressor(SummaryDispatchMixin, PreLlmSkipMixin, MicroCompactionMi
# Any completed boundary (incl. the degraded fallback) settles the overload budget (#123167).
self._reset_consecutive_overload_aborts()
def get_active_compression_failure_cooldown(self, *, refresh: bool = False) -> Optional[Dict[str, Any]]:
def get_active_compression_failure_cooldown(self, *, refresh: bool = False) -> Optional[dict[str, Any]]:
"""Return the live compression-failure cooldown for the bound session."""
if refresh:
# Rollback must distinguish an authoritative empty row from a failed read; the return value can't.
@@ -2990,7 +2990,7 @@ class ContextCompressor(SummaryDispatchMixin, PreLlmSkipMixin, MicroCompactionMi
# must ABORT and preserve the session regardless of abort_on_summary_failure (see _TERMINAL_SUMMARY_FAILURES).
self._clear_terminal_summary_failures()
def update_from_response(self, usage: Dict[str, Any]):
def update_from_response(self, usage: dict[str, Any]):
"""Update tracked token usage from API response."""
self.last_prompt_tokens = usage.get("prompt_tokens", 0)
self.last_completion_tokens = usage.get("completion_tokens", 0)
@@ -3209,7 +3209,7 @@ class ContextCompressor(SummaryDispatchMixin, PreLlmSkipMixin, MicroCompactionMi
return False
def _walk_tail_budget(
self, messages: List[Dict[str, Any]], head_end: int, ceiling: int, min_tail: int, *, cut_at_break: bool,
self, messages: list[dict[str, Any]], head_end: int, ceiling: int, min_tail: int, *, cut_at_break: bool,
) -> tuple[int, int]:
"""Accumulate message tokens newest-first until ``ceiling`` (once ``min_tail`` rows are kept).
Returns ``(cut_idx, accumulated)``; ``cut_idx`` is the first protected index. On the budget
@@ -3228,7 +3228,7 @@ class ContextCompressor(SummaryDispatchMixin, PreLlmSkipMixin, MicroCompactionMi
cut = i
return cut, accumulated
def _tail_row_pricer(self, messages: List[Dict[str, Any]]) -> Callable[[int], int]:
def _tail_row_pricer(self, messages: list[dict[str, Any]]) -> Callable[[int], int]:
"""Per-row tail price ``index -> tokens``: the walk's accounting, shared by every gate that sizes a tail region."""
newest_asst_idx = _last_assistant_index(messages)
charge_all_thinking = self._stale_thinking_on_wire()
@@ -3238,7 +3238,7 @@ class ContextCompressor(SummaryDispatchMixin, PreLlmSkipMixin, MicroCompactionMi
return lambda i: _estimate_msg_budget_tokens(messages[i], charge_all_thinking or i == newest_asst_idx)
def _prune_boundary(
self, result: List[Dict[str, Any]], protect_tail_count: int, protect_tail_tokens: int | None,
self, result: list[dict[str, Any]], protect_tail_count: int, protect_tail_tokens: int | None,
) -> int:
"""First index of the protected tail; token budget (when given) beats the count floor."""
if protect_tail_tokens is None or protect_tail_tokens <= 0:
@@ -3250,7 +3250,7 @@ class ContextCompressor(SummaryDispatchMixin, PreLlmSkipMixin, MicroCompactionMi
return min(boundary, len(result) - min_protect)
@staticmethod
def _dedupe_tool_results(result: List[Dict[str, Any]]) -> int:
def _dedupe_tool_results(result: list[dict[str, Any]]) -> int:
"""Pass 1: keep the newest copy of identical tool results, back-reference older ones."""
pruned = 0
content_hashes: set = set()
@@ -3269,7 +3269,7 @@ class ContextCompressor(SummaryDispatchMixin, PreLlmSkipMixin, MicroCompactionMi
@staticmethod
def _demote_tool_result_at(
result: List[Dict[str, Any]], idx: int, call_id_to_tool: Dict[str, tuple[str, str]],
result: list[dict[str, Any]], idx: int, call_id_to_tool: dict[str, tuple[str, str]],
min_prune_chars: int, protected_skills: Optional[set[str]] = None,
) -> bool:
"""Replace the tool result at ``idx`` with a 1-line summary; True if modified.
@@ -3309,8 +3309,8 @@ class ContextCompressor(SummaryDispatchMixin, PreLlmSkipMixin, MicroCompactionMi
return max(ceiling, token_budget)
def _pressure_demote_tail(
self, result: List[Dict[str, Any]], prune_boundary: int, protect_tail_tokens: int,
call_id_to_tool: Dict[str, tuple[str, str]], min_prune_chars: int, spared: range,
self, result: list[dict[str, Any]], prune_boundary: int, protect_tail_tokens: int,
call_id_to_tool: dict[str, tuple[str, str]], min_prune_chars: int, spared: range,
) -> int:
"""Pass 4: demote tool-result bodies inside the protected tail when it alone exceeds the soft
budget (#61932). Keeps a short recent floor and the ``spared`` pending tool round verbatim; overrides
@@ -3358,7 +3358,7 @@ class ContextCompressor(SummaryDispatchMixin, PreLlmSkipMixin, MicroCompactionMi
)
return demoted
def _spared_pending_tool_round(self, messages: List[Dict[str, Any]]) -> range:
def _spared_pending_tool_round(self, messages: list[dict[str, Any]]) -> range:
"""Return the pending round, owning assistant(tool_calls) row included, only when it fits the input window's
hard share. The call's args are part of the unread round: the model reads its result next to them."""
pending = _pending_tool_round(messages)
@@ -3370,9 +3370,9 @@ class ContextCompressor(SummaryDispatchMixin, PreLlmSkipMixin, MicroCompactionMi
return pending if sum(_estimate_msg_budget_tokens(messages[i]) for i in pending) <= hard_share else range(0)
def _prune_old_tool_results(
self, messages: List[Dict[str, Any]], protect_tail_count: int,
self, messages: list[dict[str, Any]], protect_tail_count: int,
protect_tail_tokens: int | None = None, min_prune_chars: int = _PRUNE_MIN_CHARS,
) -> tuple[List[Dict[str, Any]], int]:
) -> tuple[list[dict[str, Any]], int]:
"""Project old tool-result bodies to bounded summaries without rewriting tool-call arguments.
Returns ``(messages, count)``; token budget (when given) takes priority over the
message-count floor."""
@@ -3480,8 +3480,8 @@ class ContextCompressor(SummaryDispatchMixin, PreLlmSkipMixin, MicroCompactionMi
)
def prune_tool_results_only(
self, messages: List[Dict[str, Any]], current_tokens: int | None = None,
) -> tuple[List[Dict[str, Any]], int]:
self, messages: list[dict[str, Any]], current_tokens: int | None = None,
) -> tuple[list[dict[str, Any]], int]:
"""Deterministic, no-LLM tool-result prune gated on ``proactive_prune_tokens``.
Protects the tail by message COUNT only. A commit breaks the prompt cache, so it requires
``proactive_prune_min_reclaim_tokens`` and a full regrowth runway; otherwise returns the INPUT
@@ -3560,7 +3560,7 @@ class ContextCompressor(SummaryDispatchMixin, PreLlmSkipMixin, MicroCompactionMi
self._last_reclaim_block_warn = None
return pruned_msgs, pruned_count
def _compute_summary_budget(self, turns_to_summarize: List[Dict[str, Any]]) -> int:
def _compute_summary_budget(self, turns_to_summarize: list[dict[str, Any]]) -> int:
"""Scale the summary token budget with content size and context window."""
content_tokens = estimate_messages_tokens_rough(turns_to_summarize)
budget = int(content_tokens * _SUMMARY_RATIO)
@@ -3588,7 +3588,7 @@ class ContextCompressor(SummaryDispatchMixin, PreLlmSkipMixin, MicroCompactionMi
args = args[:self._TOOL_ARGS_HEAD] + "..."
return f" {fn.get('name', '?')}({args})"
def _serialize_records_for_summary(self, turns: List[Dict[str, Any]]) -> List[str]:
def _serialize_records_for_summary(self, turns: list[dict[str, Any]]) -> list[str]:
"""Serialize turns into a list of labeled, redacted records for the summarizer."""
# Lazy import: agent_runtime_helpers pulls heavy transitive imports.
from agent.agent_runtime_helpers import strip_think_blocks
@@ -3613,11 +3613,11 @@ class ContextCompressor(SummaryDispatchMixin, PreLlmSkipMixin, MicroCompactionMi
parts.append(f"[{role.upper()}]: {content}")
return parts
def _serialize_for_summary(self, turns: List[Dict[str, Any]]) -> str:
def _serialize_for_summary(self, turns: list[dict[str, Any]]) -> str:
"""Serialize turns into labeled, redacted text for the summarizer."""
return "\n\n".join(self._serialize_records_for_summary(turns))
def _fallback_anchors(self, turns_to_summarize: List[Dict[str, Any]]) -> Dict[str, list[str]]:
def _fallback_anchors(self, turns_to_summarize: list[dict[str, Any]]) -> dict[str, list[str]]:
"""Locally extractable anchors: user asks, actions, files, blockers, last dropped turns."""
user_asks: list[str] = []
assistant_actions: list[str] = []
@@ -3667,7 +3667,7 @@ class ContextCompressor(SummaryDispatchMixin, PreLlmSkipMixin, MicroCompactionMi
elif role == "tool":
tool_name, tool_args = call_id_to_tool.get(str(msg.get("tool_call_id") or ""), ("unknown", ""))
tool_actions.append(_summarize_tool_result(tool_name, tool_args, text or ""))
if re.search(r"\b(error|failed|exception|traceback|timeout|timed out|fatal)\b", text, re.I):
if re.search(r"\b(error|failed|exception|traceback|timeout|timed out|fatal)\b", text, re.IGNORECASE):
blockers.append(text[:500])
return {
"user_asks": user_asks,
@@ -3678,7 +3678,7 @@ class ContextCompressor(SummaryDispatchMixin, PreLlmSkipMixin, MicroCompactionMi
}
def _build_static_fallback_summary(
self, turns_to_summarize: List[Dict[str, Any]], reason: str | None = None,
self, turns_to_summarize: list[dict[str, Any]], reason: str | None = None,
) -> str:
"""Deterministic handoff when the LLM summarizer is unavailable: locally extractable anchors (user asks,
actions, files, errors) in the normal summary structure so downstream prompts recover gracefully."""
@@ -3745,7 +3745,7 @@ Summary generation was unavailable, so this is a best-effort deterministic fallb
summary = _reinject_pruned_skill_markers(summary, _pruned_names)
return self._augment_summary_lean(summary, turns_to_summarize)
def _demote_stale_tail_tools(self, messages: List[Dict[str, Any]], tail_start: int) -> List[Dict[str, Any]]:
def _demote_stale_tail_tools(self, messages: list[dict[str, Any]], tail_start: int) -> list[dict[str, Any]]:
"""Lean mode: demote tail tool results older than the newest ``_LEAN_TAIL_KEEP_TOOL_ROUNDS`` rounds to
recovery stubs; skill-marker rows untouched. New list (untouched rows shared, demoted copied)."""
session_id = getattr(self, "_session_id", "") or ""
@@ -3773,7 +3773,7 @@ Summary generation was unavailable, so this is a best-effort deterministic fallb
logger.info("Lean tail: demoted %d stale tool result(s)", demoted)
return result
def _augment_summary_lean(self, summary: str, turns_to_summarize: List[Dict[str, Any]]) -> str:
def _augment_summary_lean(self, summary: str, turns_to_summarize: list[dict[str, Any]]) -> str:
"""Append deterministic lean-mode sections to a summary; no-op in legacy mode."""
if getattr(self, "tail_mode", "lean") != "lean":
return summary
@@ -3821,7 +3821,7 @@ Summary generation was unavailable, so this is a best-effort deterministic fallb
head_len = (limit - marker_reserve) // 2
return elide_middle(record, head_len, limit - marker_reserve - head_len)
def _record_summary_input_coverage(self, coverage: Dict[str, int]) -> None:
def _record_summary_input_coverage(self, coverage: dict[str, int]) -> None:
"""Expose lean sampling coverage without including transcript content in telemetry."""
telemetry = getattr(self, "_active_compression_telemetry", None)
if not isinstance(telemetry, dict):
@@ -3836,7 +3836,7 @@ Summary generation was unavailable, so this is a best-effort deterministic fallb
})
@classmethod
def _sample_summary_records(cls, records: Sequence[str]) -> Tuple[str, Dict[str, int]]:
def _sample_summary_records(cls, records: Sequence[str]) -> tuple[str, dict[str, int]]:
"""Sample complete serialized records while retaining the character bound.
Returns the bounded transcript and record-level coverage counters for compression
@@ -3847,7 +3847,7 @@ Summary generation was unavailable, so this is a best-effort deterministic fallb
"""
input_chars = sum(len(r) for r in records)
def _coverage(sampled_chars: int, sampled_record_count: int) -> Dict[str, int]:
def _coverage(sampled_chars: int, sampled_record_count: int) -> dict[str, int]:
return {
"input_chars": input_chars, "sampled_chars": sampled_chars,
"omitted_chars": input_chars - sampled_chars, "record_count": len(records),
@@ -3987,8 +3987,8 @@ Summary generation was unavailable, so this is a best-effort deterministic fallb
Raises RuntimeError for empty content or a length-truncated (PARTIAL) summary so the failure
routes through main-model fallback + cooldown instead of wiping the compacted turns."""
# call_llm writes the route it actually selected; never pre-resolve a second, stale pair.
_aux_route: Dict[str, str] = {}
call_kwargs: Dict[str, Any] = {
_aux_route: dict[str, str] = {}
call_kwargs: dict[str, Any] = {
"task": "compression",
"main_runtime": {
"model": self.model, "provider": self.provider, "base_url": self.base_url, "api_key": self.api_key,
@@ -4005,7 +4005,7 @@ Summary generation was unavailable, so this is a best-effort deterministic fallb
# static marker, losing the real handoff (#23975). Re-entrant: a main-model retry (_generate_summary
# recursion) re-enters harmlessly.
_aux_call_start = time.monotonic()
_latency_info: Dict[str, int] = {"prompt_build_ms": max(0, int((_aux_call_start - prompt_started_at) * 1000))}
_latency_info: dict[str, int] = {"prompt_build_ms": max(0, int((_aux_call_start - prompt_started_at) * 1000))}
call_kwargs["latency_info"] = _latency_info
# Per-attempt observable (#114594): with this line a stalled attempt is distinguishable from a slow
# one — silence before it is prompt build, silence after it is the summary provider.
@@ -4070,7 +4070,7 @@ Summary generation was unavailable, so this is a best-effort deterministic fallb
return content
def _generate_summary(
self, turns_to_summarize: List[Dict[str, Any]], focus_topic: Optional[str] = None,
self, turns_to_summarize: list[dict[str, Any]], focus_topic: Optional[str] = None,
memory_context: str = "", bypass_cooldown: bool = False,
) -> Optional[str]:
"""Structured summary of the turns (iterative update when a previous summary exists); None if all attempts fail."""
@@ -4213,7 +4213,7 @@ This compaction should PRIORITISE preserving all information related to the focu
return ""
@classmethod
def _summary_template_sections(cls, _section: Dict[str, str], summary_budget: int, _session_log_section: str) -> str:
def _summary_template_sections(cls, _section: dict[str, str], summary_budget: int, _session_log_section: str) -> str:
"""The ``## ...`` section template shared by the fresh and iterative-update prompts."""
_temporal_anchoring_rule = cls._temporal_anchoring_rule()
return f"""{HISTORICAL_TASK_HEADING}
@@ -4273,7 +4273,7 @@ Target ~{summary_budget + (_LEAN_SESSION_LOG_BUDGET_TOKENS if _session_log_secti
Write only the summary body. Do not include any preamble or prefix."""
def _on_summary_failure(
self, e: Exception, turns_to_summarize: List[Dict[str, Any]], focus_topic: Optional[str], memory_context: str,
self, e: Exception, turns_to_summarize: list[dict[str, Any]], focus_topic: Optional[str], memory_context: str,
) -> Optional[str]:
"""Classify a summary-call failure; retry once on the main model (returning its result) or arm a cooldown (None)."""
# A detached stale attempt must not arm a failure cooldown or stamp error state the fallback
@@ -4419,7 +4419,7 @@ Write only the summary body. Do not include any preamble or prefix."""
return isinstance(message, dict) and bool(message.get(COMPRESSED_SUMMARY_METADATA_KEY))
@classmethod
def _transcript_has_real_user_turn(cls, messages: List[Dict[str, Any]]) -> bool:
def _transcript_has_real_user_turn(cls, messages: list[dict[str, Any]]) -> bool:
"""Return whether *messages* contain a user-authored (not synthetic summary) turn."""
return any(
isinstance(m, dict) and m.get("role") == "user" and not cls._is_synthetic_compression_user_turn(m)
@@ -4508,7 +4508,7 @@ Write only the summary body. Do not include any preamble or prefix."""
return not cls._is_blank_user_turn(message)
@classmethod
def _blank_echo_indices_after(cls, messages: List[Dict[str, Any]], user_idx: int) -> set[int]:
def _blank_echo_indices_after(cls, messages: list[dict[str, Any]], user_idx: int) -> set[int]:
"""Return contiguous blank echoes after a user event; removable only if an assistant follows."""
if user_idx < 0:
return set()
@@ -4520,7 +4520,7 @@ Write only the summary body. Do not include any preamble or prefix."""
return set(range(user_idx + 1, idx))
@classmethod
def _derive_auto_focus_topic(cls, messages: List[Dict[str, Any]]) -> Optional[str]:
def _derive_auto_focus_topic(cls, messages: list[dict[str, Any]]) -> Optional[str]:
"""Infer a compact focus hint from the most recent real user turns."""
candidates: list[str] = []
for msg in reversed(messages):
@@ -4545,7 +4545,7 @@ Write only the summary body. Do not include any preamble or prefix."""
return focus
@classmethod
def _latest_user_task_snapshot(cls, messages: List[Dict[str, Any]]) -> Optional[str]:
def _latest_user_task_snapshot(cls, messages: list[dict[str, Any]]) -> Optional[str]:
"""Return a deterministic task-snapshot line from the newest real user turn.
The summarizer must not invent the active-task anchor from a prompt example or a stale prior
summary; this grounds it in the exact compacted turns."""
@@ -4573,7 +4573,7 @@ Write only the summary body. Do not include any preamble or prefix."""
return None
@classmethod
def _ground_historical_task_snapshot(cls, summary: str, messages: List[Dict[str, Any]]) -> str:
def _ground_historical_task_snapshot(cls, summary: str, messages: list[dict[str, Any]]) -> str:
"""Force the task snapshot section to match a real user turn when possible."""
snapshot = cls._latest_user_task_snapshot(messages)
if not snapshot:
@@ -4599,7 +4599,7 @@ Write only the summary body. Do not include any preamble or prefix."""
return f"{replacement}{body}".strip()
@classmethod
def _find_context_summaries(cls, messages: List[Dict[str, Any]], start: int, end: int) -> list[tuple[int, str]]:
def _find_context_summaries(cls, messages: list[dict[str, Any]], start: int, end: int) -> list[tuple[int, str]]:
"""Find handoff summaries inside a compression window."""
n = len(messages)
# Clamp: callers may pass end = len(messages)+1.
@@ -4614,14 +4614,14 @@ Write only the summary body. Do not include any preamble or prefix."""
@classmethod
def _find_latest_context_summary(
cls, messages: List[Dict[str, Any]], start: int, end: int,
cls, messages: list[dict[str, Any]], start: int, end: int,
) -> tuple[Optional[int], str]:
"""Find the newest handoff summary inside a compression window."""
summaries = cls._find_context_summaries(messages, start, end)
return summaries[-1] if summaries else (None, "")
@classmethod
def _strip_context_summary_handoff_message(cls, message: Dict[str, Any]) -> Optional[Dict[str, Any]]:
def _strip_context_summary_handoff_message(cls, message: dict[str, Any]) -> Optional[dict[str, Any]]:
"""Drop stale handoff data while preserving merged prior-tail content.
Returns a copy for non-handoff rows, the unwrapped prior-tail content for merged handoffs
(delimiter form, or legacy end-marker form), and ``None`` for standalone ones."""
@@ -4631,7 +4631,7 @@ Write only the summary body. Do not include any preamble or prefix."""
return message.copy()
content = message.get("content")
def _unwrapped(new_content: Any) -> Dict[str, Any]:
def _unwrapped(new_content: Any) -> dict[str, Any]:
unwrapped = {**message, "content": new_content}
unwrapped.pop(COMPRESSED_SUMMARY_METADATA_KEY, None)
return unwrapped
@@ -4698,7 +4698,7 @@ Write only the summary body. Do not include any preamble or prefix."""
from agent.message_sanitization import tool_call_id_variants
return set(tool_call_id_variants(tc))
def _sanitize_tool_pairs(self, messages: List[Dict[str, Any]]) -> List[Dict[str, Any]]:
def _sanitize_tool_pairs(self, messages: list[dict[str, Any]]) -> list[dict[str, Any]]:
"""Remove orphaned tool results and strip orphaned tool_calls after compression.
Stubs would be dropped by repair_message_sequence when call_id != id."""
from agent.agent_runtime_helpers import _classify_tool_call_orphans
@@ -4738,20 +4738,20 @@ Write only the summary body. Do not include any preamble or prefix."""
logger.info("Compression sanitizer: stripped %d orphaned tool_call(s) from assistant messages", stripped_count)
return messages
def _align_boundary_forward(self, messages: List[Dict[str, Any]], idx: int) -> int:
def _align_boundary_forward(self, messages: list[dict[str, Any]], idx: int) -> int:
"""Push a compress-start boundary forward past any orphan tool results."""
while idx < len(messages) and messages[idx].get("role") == "tool":
idx += 1
return idx
def _restart_handoff_probe_bounds(self, messages: List[Dict[str, Any]]) -> tuple[int, int]:
def _restart_handoff_probe_bounds(self, messages: list[dict[str, Any]]) -> tuple[int, int]:
"""Return the bounded transcript region that can indicate restart decay."""
if not messages or self.protect_first_n <= 0:
return 0, 0
first_non_system = 1 if messages[0].get("role") == "system" else 0
return first_non_system, min(len(messages), first_non_system + self.protect_first_n + _RESTART_HANDOFF_PROBE_EXTRA_MESSAGES)
def _effective_protect_first_n(self, messages: Optional[List[Dict[str, Any]]] = None) -> int:
def _effective_protect_first_n(self, messages: Optional[list[dict[str, Any]]] = None) -> int:
"""``protect_first_n``, decayed to 0 once the session has been compressed so early turns don't fossilize.
After a restart the decayed state is inferred from handoff summaries in the resumed head."""
if self.compression_count >= 1 or self._previous_summary:
@@ -4763,7 +4763,7 @@ Write only the summary body. Do not include any preamble or prefix."""
return 0
return self.protect_first_n
def _protect_head_size(self, messages: List[Dict[str, Any]]) -> int:
def _protect_head_size(self, messages: list[dict[str, Any]]) -> int:
"""Head messages to protect: the system prompt (if present) plus the decaying ``protect_first_n`` extra rows.
The ``protect_first_n`` portion DECAYS after the first compression (see _effective_protect_first_n)
@@ -4772,7 +4772,7 @@ Write only the summary body. Do not include any preamble or prefix."""
head = 1 if messages and messages[0].get("role") == "system" else 0
return head + self._effective_protect_first_n(messages)
def _align_boundary_backward(self, messages: List[Dict[str, Any]], idx: int) -> int:
def _align_boundary_backward(self, messages: list[dict[str, Any]], idx: int) -> int:
"""Pull a compress-end boundary back so a tool group is not split (orphaned tail results would be dropped)."""
if idx <= 0 or idx >= len(messages):
return idx
@@ -4783,7 +4783,7 @@ Write only the summary body. Do not include any preamble or prefix."""
return idx
@classmethod
def _is_real_user_turn(cls, message: Dict[str, Any]) -> bool:
def _is_real_user_turn(cls, message: dict[str, Any]) -> bool:
"""Actionable user turn that is not synthetic scaffolding — the row test both index scans share.
Weaker than ``agent.conversation_compression._is_real_user_message``, which also rejects
@@ -4793,14 +4793,14 @@ Write only the summary body. Do not include any preamble or prefix."""
return cls._is_actionable_user_turn(message) and not cls._is_synthetic_compression_user_turn(message)
@classmethod
def _real_user_indices_desc(cls, messages: List[Dict[str, Any]], head_end: int) -> list[int]:
def _real_user_indices_desc(cls, messages: list[dict[str, Any]], head_end: int) -> list[int]:
"""Newest-first indices of actionable, non-synthetic user turns at or after *head_end* (no handoffs/blank echoes)."""
return [
i for i in range(len(messages) - 1, head_end - 1, -1)
if cls._is_real_user_turn(messages[i])
]
def _find_last_user_message_idx(self, messages: List[Dict[str, Any]], head_end: int) -> int:
def _find_last_user_message_idx(self, messages: list[dict[str, Any]], head_end: int) -> int:
"""Return the latest actionable user turn at or after *head_end*, or -1."""
# Early-exit generator: callers want the newest hit only, and collecting every index
# (``_real_user_indices_desc``) costs a full backward scan per call.
@@ -4809,7 +4809,7 @@ Write only the summary body. Do not include any preamble or prefix."""
-1,
)
def _find_last_assistant_message_idx(self, messages: List[Dict[str, Any]], head_end: int) -> int:
def _find_last_assistant_message_idx(self, messages: list[dict[str, Any]], head_end: int) -> int:
"""Last text-bearing non-summary assistant reply at/after *head_end* (else last non-summary assistant), or -1."""
last_any = -1
for i in range(len(messages) - 1, head_end - 1, -1):
@@ -4828,7 +4828,7 @@ Write only the summary body. Do not include any preamble or prefix."""
return last_any
def _ensure_last_assistant_message_in_tail(
self, messages: List[Dict[str, Any]], cut_idx: int, head_end: int,
self, messages: list[dict[str, Any]], cut_idx: int, head_end: int,
) -> int:
"""Keep the most recent assistant reply in the protected tail, re-aligned back so a tool group is not split."""
last_asst_idx = self._find_last_assistant_message_idx(messages, head_end)
@@ -4843,7 +4843,7 @@ Write only the summary body. Do not include any preamble or prefix."""
)
return max(new_cut, head_end + 1)
def _ensure_last_user_message_in_tail(self, messages: List[Dict[str, Any]], cut_idx: int, head_end: int) -> int:
def _ensure_last_user_message_in_tail(self, messages: list[dict[str, Any]], cut_idx: int, head_end: int) -> int:
"""Guarantee the most recent user message is in the protected tail.
Tool-group alignment can pull the cut past the last user message; once summarized, the prefix
tells the model to answer only messages AFTER the summary, so the active ask silently vanishes.
@@ -4895,8 +4895,8 @@ Write only the summary body. Do not include any preamble or prefix."""
@classmethod
def _find_inflight_user_task(
cls, messages: List[Dict[str, Any]]
) -> Optional[Dict[str, Any]]:
cls, messages: list[dict[str, Any]]
) -> Optional[dict[str, Any]]:
"""Return the user turn that is still awaiting completion, or ``None``.
Scans the WHOLE transcript, not just the compressible region: a cron
@@ -4959,9 +4959,9 @@ Write only the summary body. Do not include any preamble or prefix."""
def _reappend_inflight_user_task(
self,
compressed: List[Dict[str, Any]],
inflight: Optional[Dict[str, Any]],
) -> List[Dict[str, Any]]:
compressed: list[dict[str, Any]],
inflight: Optional[dict[str, Any]],
) -> list[dict[str, Any]]:
"""Restate an unfinished user task after the compaction handoff.
``SUMMARY_PREFIX`` instructs the model to act only on a user message
@@ -5102,7 +5102,7 @@ Write only the summary body. Do not include any preamble or prefix."""
return compressed
def _ensure_last_n_user_messages_in_tail(
self, messages: List[Dict[str, Any]], cut_idx: int, head_end: int, n: int,
self, messages: list[dict[str, Any]], cut_idx: int, head_end: int, n: int,
) -> int:
"""Keep the last N actionable user messages in the tail; n <= 1 delegates to the single-message method.
@@ -5124,7 +5124,7 @@ Write only the summary body. Do not include any preamble or prefix."""
return cut_idx
return max(user_indices[min(n, len(user_indices)) - 1], head_end + 1)
def _find_turn_pair_end(self, messages: List[Dict[str, Any]], user_idx: int) -> int:
def _find_turn_pair_end(self, messages: list[dict[str, Any]], user_idx: int) -> int:
"""Index after the turn-pair (user -> assistant -> tools) at *user_idx*; ``user_idx + 1`` when no reply yet."""
idx = user_idx + 1
if idx >= len(messages) or messages[idx].get("role") != "assistant":
@@ -5156,7 +5156,7 @@ Write only the summary body. Do not include any preamble or prefix."""
rejected = session_rejected_thinking(self, getattr(self, "_session_db", None), getattr(self, "_session_id", ""))
def budget(msg: Dict[str, Any]) -> int:
def budget(msg: dict[str, Any]) -> int:
request_copy = dict(msg)
if rejected:
strip_rejected_thinking(request_copy, rejected)
@@ -5166,7 +5166,7 @@ Write only the summary body. Do not include any preamble or prefix."""
return budget
def _find_tail_cut_by_tokens(
self, messages: List[Dict[str, Any]], head_end: int, token_budget: int | None = None,
self, messages: list[dict[str, Any]], head_end: int, token_budget: int | None = None,
*, allow_split_turn: bool = True,
) -> int:
"""Walk backward accumulating tokens until the budget; return the tail start index.
@@ -5289,15 +5289,15 @@ Write only the summary body. Do not include any preamble or prefix."""
# can't split a tool group (backward would give the floor's message back).
return min(n, self._align_boundary_forward(messages, max(cut_idx, head_end + 1)))
def has_content_to_compress(self, messages: List[Dict[str, Any]]) -> bool:
def has_content_to_compress(self, messages: list[dict[str, Any]]) -> bool:
"""True if a non-empty middle region exists (lets the gateway ``/compress`` guard skip the LLM call)."""
compress_start = self._align_boundary_forward(messages, self._protect_head_size(messages))
compress_end = self._find_tail_cut_by_tokens(messages, compress_start)
return compress_start < compress_end
def _scan_window_handoffs(
self, messages: List[Dict[str, Any]], compress_start: int, compress_end: int,
turns_to_summarize: List[Dict[str, Any]],
self, messages: list[dict[str, Any]], compress_start: int, compress_end: int,
turns_to_summarize: list[dict[str, Any]],
) -> "_HandoffScan":
"""Rehydrate ``_previous_summary`` / user-turn provenance from in-transcript handoffs.
Handoff rows are removed from the summarizer window (merged handoffs unwrap to their prior-tail
@@ -5339,7 +5339,7 @@ Write only the summary body. Do not include any preamble or prefix."""
# Summary rows are excluded from summarizer input, but a merged handoff carries genuine
# prior-tail user content — unwrap it into the window (#47274); standalone ones drop (None).
# The newest hit (summary_idx) may itself be a merged handoff — recover its prior tail too.
def _window_row(idx: int, msg: Dict[str, Any]):
def _window_row(idx: int, msg: dict[str, Any]):
if idx not in scan.summary_indices:
return msg
return self._strip_context_summary_handoff_message(_fresh_compaction_message_copy(msg))
@@ -5351,7 +5351,7 @@ Write only the summary body. Do not include any preamble or prefix."""
scan.tail_start = summary_idx + 1
return scan
def _begin_compress_attempt(self, current_tokens: Optional[int], force: bool) -> Dict[str, Any]:
def _begin_compress_attempt(self, current_tokens: Optional[int], force: bool) -> dict[str, Any]:
"""Reset per-call result state (callers read it after compress()) and open telemetry."""
self._last_summary_dropped_count = 0
self._last_summary_fallback_used = False
@@ -5373,18 +5373,18 @@ Write only the summary body. Do not include any preamble or prefix."""
self._structural_no_op_backoff_until = 0.0
return telemetry
def _structural_no_op_result(self, telemetry: Dict[str, Any], failure_class: str, reason: str) -> None:
def _structural_no_op_result(self, telemetry: dict[str, Any], failure_class: str, reason: str) -> None:
"""Nothing eligible to compress: transient backoff (#93022), never an ineffectiveness strike."""
telemetry["failure_class"] = failure_class
self._last_compression_savings_pct = 0.0
self._record_structural_no_op(reason)
def _drop_blank_echoes(self, messages: List[Dict[str, Any]]) -> List[Dict[str, Any]]:
def _drop_blank_echoes(self, messages: list[dict[str, Any]]) -> list[dict[str, Any]]:
"""Remove blank platform echoes trailing the latest actionable user turn."""
blank = self._blank_echo_indices_after(messages, self._find_last_user_message_idx(messages, 0))
return [m for idx, m in enumerate(messages) if idx not in blank] if blank else messages
def _compress_window(self, messages: List[Dict[str, Any]]) -> tuple[int, int]:
def _compress_window(self, messages: list[dict[str, Any]]) -> tuple[int, int]:
"""Return ``(compress_start, compress_end)`` for the summarizable middle."""
compress_start = self._align_boundary_forward(messages, self._protect_head_size(messages))
compress_end = self._find_tail_cut_by_tokens(messages, compress_start)
@@ -5418,7 +5418,7 @@ Write only the summary body. Do not include any preamble or prefix."""
)
def _abort_on_summary_failure(
self, telemetry: Dict[str, Any], n_skipped: int, previous_summary_before_scan: Optional[str],
self, telemetry: dict[str, Any], n_skipped: int, previous_summary_before_scan: Optional[str],
) -> bool:
"""Abort (messages unchanged) on a terminal failure or when configured to; True when aborted.
Access/quota, network, truncated and empty-content failures ALWAYS abort (#29559); an overload
@@ -5452,7 +5452,7 @@ Write only the summary body. Do not include any preamble or prefix."""
_COMPRESSION_NOTE = "[Note: Some earlier conversation turns have been compacted into a handoff summary to preserve context space. The current session state may still reflect earlier work, so build on that summary and state rather than re-doing work. Your persistent memory (MEMORY.md, USER.md) remains fully authoritative regardless of compaction.]"
def _assemble_head(self, messages: List[Dict[str, Any]], compress_start: int) -> List[Dict[str, Any]]:
def _assemble_head(self, messages: list[dict[str, Any]], compress_start: int) -> list[dict[str, Any]]:
"""Protected head with the compaction note on the system prompt and stale handoffs stripped."""
compressed = []
for i in range(compress_start):
@@ -5470,7 +5470,7 @@ Write only the summary body. Do not include any preamble or prefix."""
return compressed
def _fallback_summary_for_window(
self, telemetry: Dict[str, Any], turns_to_summarize: List[Dict[str, Any]],
self, telemetry: dict[str, Any], turns_to_summarize: list[dict[str, Any]],
n_dropped: int, feasibility_skip: bool,
) -> str:
"""Deterministic fallback so the model gets recoverable continuity anchors."""
@@ -5498,10 +5498,10 @@ Write only the summary body. Do not include any preamble or prefix."""
return summary
def _assemble_tail(
self, messages: List[Dict[str, Any]], compress_end: int, tail_start: int, summary_indices: set,
) -> List[Dict[str, Any]]:
self, messages: list[dict[str, Any]], compress_end: int, tail_start: int, summary_indices: set,
) -> list[dict[str, Any]]:
"""Protected tail with already-folded handoff rows dropped and merged handoffs unwrapped."""
tail_messages: List[Dict[str, Any]] = []
tail_messages: list[dict[str, Any]] = []
# Start at tail_start, not compress_end: the rehydration scan may have advanced it (#57835).
for i in range(max(compress_end, tail_start), len(messages)):
if i in summary_indices and i >= tail_start:
@@ -5513,7 +5513,7 @@ Write only the summary body. Do not include any preamble or prefix."""
@staticmethod
def _summary_placement(
compressed: List[Dict[str, Any]], tail_messages: List[Dict[str, Any]], compress_start: int,
compressed: list[dict[str, Any]], tail_messages: list[dict[str, Any]], compress_start: int,
) -> tuple[str, bool, bool, Optional[int]]:
"""Pick the summary row's role so template-visible alternation holds.
Returns ``(summary_role, merge_into_tail, force_user_leading, first_tail_visible_idx)``. Roles
@@ -5548,7 +5548,7 @@ Write only the summary body. Do not include any preamble or prefix."""
return summary_role, merge_into_tail, force_user_leading, first_tail_visible_idx
def _merge_summary_into_tail_row(
self, msg: Dict[str, Any], summary: str, summary_role: str, force_user_leading: bool,
self, msg: dict[str, Any], summary: str, summary_role: str, force_user_leading: bool,
) -> None:
"""Fold the summary into a carried tail row (in place) when no standalone role alternates."""
old_content = msg.get("content", "")
@@ -5569,9 +5569,9 @@ Write only the summary body. Do not include any preamble or prefix."""
drop_stale_api_content(msg)
def _finalize_compressed(
self, compressed: List[Dict[str, Any]], messages: List[Dict[str, Any]], n_messages: int,
self, compressed: list[dict[str, Any]], messages: list[dict[str, Any]], n_messages: int,
spare_pending_images: bool,
) -> List[Dict[str, Any]]:
) -> list[dict[str, Any]]:
"""Post-assembly cleanup: orphan pairs, media, savings, markers, replay prune, mem trim."""
# Single-prompt cron shape: the only live instruction sits in the protected head, BEFORE the
# handoff, and SUMMARY_PREFIX reads that as "nothing to do" — restate it past the boundary
@@ -5628,9 +5628,9 @@ Write only the summary body. Do not include any preamble or prefix."""
return compressed
def compress(
self, messages: List[Dict[str, Any]], current_tokens: Optional[int] = None, focus_topic: Optional[str] = None,
self, messages: list[dict[str, Any]], current_tokens: Optional[int] = None, focus_topic: Optional[str] = None,
force: bool = False, memory_context: str = "", bypass_cooldown: bool = False,
) -> List[Dict[str, Any]]:
) -> list[dict[str, Any]]:
"""Summarize the middle turns: prune tool-result bodies (tool-call args untouched), protect head and a
token-budget tail from the pruned copy, summarize, then clean orphaned tool pairs. ``force`` clears the failure cooldown and bypasses
the feasibility skip; ``bypass_cooldown`` runs the summary LLM without clearing the cooldown.
@@ -5736,8 +5736,8 @@ Write only the summary body. Do not include any preamble or prefix."""
return self._finalize_compressed(compressed, canonical_messages, n_messages, spare_pending_images)
def _assemble_compressed(
self, messages: List[Dict[str, Any]], compress_start: int, compress_end: int, scan: "_HandoffScan", summary: str,
) -> List[Dict[str, Any]]:
self, messages: list[dict[str, Any]], compress_start: int, compress_end: int, scan: "_HandoffScan", summary: str,
) -> list[dict[str, Any]]:
"""Head + summary + tail from the pruned copy: its tool-result demotions are what let an oversized
head/tail compress at all (#61932); tool-call arguments are never rewritten by pruning."""
compressed = self._assemble_head(messages, compress_start)
@@ -5814,13 +5814,13 @@ def _handoff_only_content(content: Any) -> Any:
return projected
def split_user_originated_turn(message: Any) -> tuple[Optional[Dict[str, Any]], Optional[Dict[str, Any]]]:
def split_user_originated_turn(message: Any) -> tuple[Optional[dict[str, Any]], Optional[dict[str, Any]]]:
"""Split a user row into ``(handoff_only, live_view)``; either may be None; fresh dicts."""
if not isinstance(message, dict) or message.get("role") != "user":
return None, None
is_summary = is_compaction_summary_message(message)
handoff: Optional[Dict[str, Any]] = None
handoff: Optional[dict[str, Any]] = None
if is_summary:
handoff = {
"role": "user", "content": _handoff_only_content(message.get("content")),
@@ -5863,14 +5863,14 @@ def split_user_originated_turn(message: Any) -> tuple[Optional[Dict[str, Any]],
return handoff, candidate
def user_originated_turn_view(message: Any) -> Optional[Dict[str, Any]]:
def user_originated_turn_view(message: Any) -> Optional[dict[str, Any]]:
"""Return the live human-authored projection of a user row, if any."""
return split_user_originated_turn(message)[1]
def history_before_user_originated_turn(
messages: List[Dict[str, Any]], index: int,
) -> tuple[List[Dict[str, Any]], Dict[str, Any]]:
messages: list[dict[str, Any]], index: int,
) -> tuple[list[dict[str, Any]], dict[str, Any]]:
"""Rewind prefix and canonical live view for ``index``; a composite carrier keeps its handoff scaffold at the head."""
if index < 0 or index >= len(messages):
raise IndexError("user turn index is outside the transcript")
@@ -5910,7 +5910,7 @@ def _handoff_carries_live_user_content(message: Any) -> bool:
return isinstance(message, dict) and ContextCompressor._strip_context_summary_handoff_message(message) is not None
def reference_handoff_would_drive_next_model_call(messages: Optional[List[Dict[str, Any]]]) -> bool:
def reference_handoff_would_drive_next_model_call(messages: Optional[list[dict[str, Any]]]) -> bool:
"""True when the next model call would be driven only by a handoff; trailing tool rows mean an in-flight exchange."""
if not messages:
return False
+2 -2
View File
@@ -36,7 +36,7 @@ class PreLlmSkipMixin:
float, 0.0, _FALLBACK_PROBE_AT_MODEL_CONFIG_KEY, 0.0,
)
def _fallback_streak_skip(self, telemetry: Dict[str, Any]) -> bool:
def _fallback_streak_skip(self, telemetry: dict[str, Any]) -> bool:
"""Pre-LLM skip while two summaries in a row fell back: compact deterministically instead of
paying for a summary model that keeps failing (#63008). One probe per recovery window; a
healthy summary resets the streak, another fallback benches it again."""
@@ -66,7 +66,7 @@ class PreLlmSkipMixin:
return True
def _feasibility_skip(
self, telemetry: Dict[str, Any], turns_to_summarize: List[Dict[str, Any]],
self, telemetry: dict[str, Any], turns_to_summarize: list[dict[str, Any]],
compress_start: int, compress_end: int,
) -> bool:
"""Pre-LLM skip after a real-usage ineffectiveness strike (reads the counter, never writes)."""
+3 -3
View File
@@ -27,7 +27,7 @@ def _accepts_keyword_argument(callable_obj: Any, name: str) -> bool:
class SummaryDispatchMixin:
def _apply_summary_route(self, call_kwargs: dict, pinned: Optional[Dict[str, Any]] = None) -> None:
def _apply_summary_route(self, call_kwargs: dict, pinned: Optional[dict[str, Any]] = None) -> None:
"""Pin the summary route onto ``call_kwargs`` for both compression entry points.
After a fallback, an omitted route is NOT "use the main model": ``call_llm`` re-resolves
@@ -58,7 +58,7 @@ class SummaryDispatchMixin:
call_kwargs[key] = value
def _summarize_window(
self, messages: List[Dict[str, Any]], turns_to_summarize: List[Dict[str, Any]], scan: "_HandoffScan",
self, messages: list[dict[str, Any]], turns_to_summarize: list[dict[str, Any]], scan: "_HandoffScan",
focus_topic: Optional[str], memory_context: str, bypass_cooldown: bool,
) -> Optional[str]:
"""Run the summary LLM; a cancellation rolls back the handoff scan's self-heal mutation first.
@@ -80,7 +80,7 @@ class SummaryDispatchMixin:
telemetry["failure_class"] = "stall_deterministic_fallback"
return None
# Focus-topic derivation scans user turns; only pay when a summary is generated.
summary_kwargs: Dict[str, Any] = {
summary_kwargs: dict[str, Any] = {
"focus_topic": focus_topic or self._derive_auto_focus_topic(messages),
"memory_context": memory_context,
}
+16 -16
View File
@@ -73,7 +73,7 @@ class ContextEngine(ABC):
emit_automatic_compaction_status: bool = True
@abstractmethod
def update_from_response(self, usage: Dict[str, Any]) -> None:
def update_from_response(self, usage: dict[str, Any]) -> None:
"""Update tracked token usage after every LLM call.
``prompt_tokens``/``completion_tokens``/``total_tokens`` are always present; the
@@ -96,9 +96,9 @@ class ContextEngine(ABC):
@abstractmethod
def compress(
self, messages: List[Dict[str, Any]], current_tokens: Optional[int] = None,
self, messages: list[dict[str, Any]], current_tokens: Optional[int] = None,
focus_topic: Optional[str] = None, force: bool = False, memory_context: str = "",
) -> List[Dict[str, Any]]:
) -> list[dict[str, Any]]:
"""Compact ``messages`` into a valid OpenAI-format list that fits the budget.
``focus_topic`` comes from manual ``/compress <focus>`` (prioritise that topic);
@@ -108,8 +108,8 @@ class ContextEngine(ABC):
"""
def prune_tool_results_only(
self, messages: List[Dict[str, Any]], current_tokens: int | None = None,
) -> tuple[List[Dict[str, Any]], int]:
self, messages: list[dict[str, Any]], current_tokens: int | None = None,
) -> tuple[list[dict[str, Any]], int]:
"""Deterministically trim old tool-result payloads without an LLM call.
Runs on a low, cost-oriented trigger independent of ``should_compress`` so
@@ -119,9 +119,9 @@ class ContextEngine(ABC):
return messages, 0
def select_context(
self, request_messages: List[Dict[str, Any]], *, conversation_messages: List[Dict[str, Any]] = None,
incoming_message: Dict[str, Any] = None, budget_tokens: int = 0,
) -> List[Dict[str, Any]]:
self, request_messages: list[dict[str, Any]], *, conversation_messages: list[dict[str, Any]] = None,
incoming_message: dict[str, Any] = None, budget_tokens: int = 0,
) -> list[dict[str, Any]]:
"""Optionally *select* (replace) the context for THIS request, pre-generation.
Runs on every provider request (also retries), independent of
@@ -140,7 +140,7 @@ class ContextEngine(ABC):
"""
return None
def on_turn_complete(self, messages: List[Dict[str, Any]], usage: Dict[str, Any] = None, **kwargs: Any) -> None:
def on_turn_complete(self, messages: list[dict[str, Any]], usage: dict[str, Any] = None, **kwargs: Any) -> None:
"""Observe a finished turn (complement of ``select_context()``) to index/update
routing state for the next request.
@@ -151,9 +151,9 @@ class ContextEngine(ABC):
``None`` when no provider response was reached (interrupt). ``kwargs`` may include
``turn_id``, ``task_id``, ``api_call_count``, ``interrupted``, ``failed``, ``turn_exit_reason``.
"""
return None
return
def should_compress_preflight(self, messages: List[Dict[str, Any]]) -> bool:
def should_compress_preflight(self, messages: list[dict[str, Any]]) -> bool:
"""Cheap rough check before the API call (no real token count yet); default skips."""
return False
@@ -173,7 +173,7 @@ class ContextEngine(ABC):
"""
return default_message if self.emit_automatic_compaction_status else None
def has_content_to_compress(self, messages: List[Dict[str, Any]]) -> bool:
def has_content_to_compress(self, messages: list[dict[str, Any]]) -> bool:
"""Preflight guard for gateway ``/compress``: False reports "nothing to
compress yet" without an LLM call (e.g. transcript entirely protected)."""
return True
@@ -181,7 +181,7 @@ class ContextEngine(ABC):
def on_session_start(self, session_id: str, **kwargs) -> None:
"""Session begins: load persisted state. kwargs may include hermes_home, platform, model."""
def on_session_end(self, session_id: str, messages: List[Dict[str, Any]]) -> None:
def on_session_end(self, session_id: str, messages: list[dict[str, Any]]) -> None:
"""Real session boundary (CLI exit, /reset, gateway expiry) — never per-turn."""
def on_session_reset(self) -> None:
@@ -201,16 +201,16 @@ class ContextEngine(ABC):
self.last_total_tokens = 0
self.compression_count = 0
def get_tool_schemas(self) -> List[Dict[str, Any]]:
def get_tool_schemas(self) -> list[dict[str, Any]]:
"""Tool schemas this engine exposes to the agent (default: none)."""
return []
def handle_tool_call(self, name: str, args: Dict[str, Any], **kwargs) -> str:
def handle_tool_call(self, name: str, args: dict[str, Any], **kwargs) -> str:
"""Handle a call to one of this engine's tools; must return a JSON string.
kwargs may include ``messages`` (live in-memory list)."""
return json.dumps({"error": f"Unknown context engine tool: {name}"})
def get_status(self) -> Dict[str, Any]:
def get_status(self) -> dict[str, Any]:
"""Status dict with the standard fields run_agent.py expects."""
# Clamp the -1 "compression just ran, awaiting real usage" sentinel to 0 so no
# reader sees a negative usage_percent on the transitional turn.
+6 -6
View File
@@ -34,7 +34,7 @@ _STATUS_DISPLAY = {
}
def _entry(label: str, path: Path, content: str, status: str) -> Dict[str, Any]:
def _entry(label: str, path: Path, content: str, status: str) -> dict[str, Any]:
return {
"label": label, "path": str(path), "chars": len(content), "est_tokens": estimate_tokens_rough(content),
"loaded": status in ("loaded", "truncated", "flagged"), "status": status,
@@ -60,7 +60,7 @@ def _loaded_status(content: str, rendered_len: int, max_chars: int, user_authore
def list_context_file_sources(
cwd: Optional[str] = None, context_length: Optional[int] = None, allow_install_tree_fallback: bool = False,
home_override: "Path | None" = None, skip_soul: bool = False,
) -> List[Dict[str, Any]]:
) -> list[dict[str, Any]]:
"""One dict per context file Hermes considered, in the builder's priority order.
Same signature semantics as ``build_context_files_prompt`` (``cwd=None`` → launch dir, install-tree guard
@@ -70,7 +70,7 @@ def list_context_file_sources(
cwd_path = Path(cwd if cwd is not None else os.getcwd()).resolve()
max_chars = _pb._get_context_file_max_chars(context_length)
suppressed = _pb._project_context_suppressed(cwd, cwd_path, allow_install_tree_fallback)
sources: List[Dict[str, Any]] = []
sources: list[dict[str, Any]] = []
winner: Optional[str] = None
for kind, label, path, content in _pb.discover_context_files(cwd_path):
if not content:
@@ -96,7 +96,7 @@ def list_context_file_sources(
return sources
def context_file_sources_for_agent(agent: Any) -> List[Dict[str, Any]]:
def context_file_sources_for_agent(agent: Any) -> list[dict[str, Any]]:
"""The manifest for a live agent, resolved exactly like ``agent.system_prompt._context_files_part``
(session cwd, install-tree policy per platform, the agent's own profile home)."""
if getattr(agent, "skip_context_files", False):
@@ -112,7 +112,7 @@ def context_file_sources_for_agent(agent: Any) -> List[Dict[str, Any]]:
)
def render_context_file_lines(sources: List[Dict[str, Any]]) -> List[str]:
def render_context_file_lines(sources: list[dict[str, Any]]) -> list[str]:
"""Plain-text ``Context files`` block for ``/context``; [] when nothing was found."""
if not sources:
return []
@@ -121,5 +121,5 @@ def render_context_file_lines(sources: List[Dict[str, Any]]) -> List[str]:
for src in sources:
glyph, note = _STATUS_DISPLAY.get(str(src.get("status") or ""), ("•", ""))
suffix = f" ({note})" if note else ""
lines.append(f"{glyph} {str(src.get('label') or ''):<{width}} ~{int(src.get('est_tokens') or 0):>9,} tokens{suffix}")
lines.append(f"{glyph} {src.get('label') or ''!s:<{width}} ~{int(src.get('est_tokens') or 0):>9,} tokens{suffix}")
return lines
+1 -1
View File
@@ -33,7 +33,7 @@ _context_reference_providers: dict[str, "ContextReferenceProvider"] = {}
class ContextCompletionItem:
"""A single autocomplete result from a context reference provider."""
__slots__ = ("text", "display", "meta")
__slots__ = ("display", "meta", "text")
def __init__(self, text: str, display: str = "", meta: str = "") -> None:
self.text = text
+25 -26
View File
@@ -761,7 +761,7 @@ def _get_compress_timeout_executor():
return _compress_timeout_executor
def resolve_context_compression_timeouts(compression_cfg: Optional[dict] = None) -> Tuple[float, float]:
def resolve_context_compression_timeouts(compression_cfg: Optional[dict] = None) -> tuple[float, float]:
"""Return ``(idle_timeout_seconds, total_ceiling_seconds)``.
``idle_timeout_seconds <= 0`` disables the progress-aware wrapper. The ceiling is clamped to at least one
idle window when the idle budget is positive."""
@@ -792,8 +792,7 @@ def resolve_context_compression_timeouts(compression_cfg: Optional[dict] = None)
from agent.auxiliary_client import _effective_aux_timeout
_aux_budget = float(_effective_aux_timeout("compression", None))
if _aux_budget > 0:
if _aux_budget > ceiling:
ceiling = _aux_budget
ceiling = max(ceiling, _aux_budget)
idle = max(idle, min(_aux_budget, ceiling))
return idle, ceiling
@@ -938,12 +937,12 @@ def request_exceeds_model_window(agent: Any, request_tokens: Any) -> Optional[bo
def _retry_compression_on_fallback_chain(
*, worker: Callable[[CompressionCommitFence], Tuple[list, str]], messages: list,
*, worker: Callable[[CompressionCommitFence], tuple[list, str]], messages: list,
system_prompt_fallback: Any, idle_timeout_seconds: float, total_ceiling_seconds: float,
on_commit_overrun: Optional[Callable[[float, float], None]] = None,
on_timeout_cause: Optional[Callable[[bool, bool], None]] = None, telemetry_agent: Any = None,
new_fence: Optional[Callable[[], CompressionCommitFence]] = None, escalate_deterministic: bool = False,
) -> Optional[Tuple[list, str]]:
) -> Optional[tuple[list, str]]:
"""Re-run an aborted compression with the summary route pinned: once on the configured chain entry,
then — when ``escalate_deterministic`` (a stall backoff already burned one idle window this session,
#112420) — once with the summary LLM skipped so compress() commits its deterministic fallback summary.
@@ -978,11 +977,11 @@ def _retry_compression_on_fallback_chain(
def _run_pinned_compression_retry(
route: dict, *, worker: Callable[[CompressionCommitFence], Tuple[list, str]], messages: list,
route: dict, *, worker: Callable[[CompressionCommitFence], tuple[list, str]], messages: list,
system_prompt_fallback: Any, idle_timeout_seconds: float, total_ceiling_seconds: float,
on_commit_overrun: Optional[Callable[[float, float], None]], on_timeout_cause: Optional[Callable[[bool, bool], None]],
telemetry_agent: Any, new_fence: Optional[Callable[[], CompressionCommitFence]],
) -> Optional[Tuple[list, str]]:
) -> Optional[tuple[list, str]]:
"""One bounded re-run of ``worker`` with ``route`` pinned; ``None`` when it produced no compression."""
# The aborted fence refuses all commits; mint a fresh one via the host factory
# so a /stop during the retry serializes against THIS attempt's commit boundary.
@@ -1053,7 +1052,7 @@ def _run_pinned_compression_retry(
def _await_worker_within_budget(
future: Any, fence: CompressionCommitFence, *, idle: float, ceiling: float, wait_started: float
) -> Tuple[bool, Any]:
) -> tuple[bool, Any]:
"""Poll ``future`` under the idle budget + ceiling; ``(True, result)`` when it settled."""
while True:
waited = time.monotonic() - wait_started
@@ -1162,16 +1161,16 @@ def _release_cancelled_worker(
def run_compress_context_with_progress_timeout(
*, worker: Callable[[CompressionCommitFence], Tuple[list, str]], messages: list,
*, worker: Callable[[CompressionCommitFence], tuple[list, str]], messages: list,
system_prompt_fallback: Any, idle_timeout_seconds: float, total_ceiling_seconds: float,
on_timeout: Optional[Callable[[float, float, float], None]] = None,
on_timeout_cause: Optional[Callable[[bool, bool], None]] = None,
on_commit_overrun: Optional[Callable[[float, float], None]] = None,
fence: Optional[CompressionCommitFence] = None, telemetry_agent: Any = None, stall_fallback: bool = True,
new_fence: Optional[Callable[[], CompressionCommitFence]] = None,
fallback_worker: Optional[Callable[[CompressionCommitFence], Tuple[list, str]]] = None,
fallback_worker: Optional[Callable[[CompressionCommitFence], tuple[list, str]]] = None,
request_exceeds_window: bool = False,
) -> Tuple[list, str]:
) -> tuple[list, str]:
"""Run ``worker(fence)`` under a sync progress-aware (idle + ceiling) timeout.
Budgets bound the PRE-commit phase only: an admitted commit always completes (overrun logged, surfaced
once via ``on_commit_overrun``). A pre-commit cancel returns ``(messages, system_prompt_fallback)`` (lazy
@@ -1498,7 +1497,7 @@ def compression_skipped_due_to_lock(agent: Any) -> bool:
def _get_context_compression_timeout_state(
agent: Any, *, create: bool
) -> Optional[Tuple[Any, Optional[threading.local]]]:
) -> Optional[tuple[Any, Optional[threading.local]]]:
"""Return the stable lock and thread-local timeout state for an agent."""
try:
attributes = vars(agent)
@@ -1621,7 +1620,7 @@ def _rebind_session_context(session_id: str) -> None:
def _adopt_live_compression_child(
agent: Any, session_db: Any, parent_session_id: str
) -> Optional[List[Dict[str, Any]]]:
) -> Optional[list[dict[str, Any]]]:
"""Move a stale compression contender onto the live continuation tip.
Resolve and load first, then mutate the agent, so ambiguous lineage or an unreadable handoff fails closed.
Uses the transitive ``get_compression_tip`` walk; a tip is adopted only while its row is still live.
@@ -1700,7 +1699,7 @@ def _reopen_orphaned_parent(session_db: Any, session_id: str) -> None:
logger.warning("orphaned compression session reopen failed for %s: %s", session_id, exc)
def recover_rotated_compression_session(agent: Any) -> Optional[List[Dict[str, Any]]]:
def recover_rotated_compression_session(agent: Any) -> Optional[list[dict[str, Any]]]:
"""Recover a stale live agent before a new turn writes to its old parent."""
session_db = getattr(agent, "_session_db", None)
session_id = getattr(agent, "session_id", None) or ""
@@ -2242,7 +2241,7 @@ _STEER_FALLBACK_OPEN = "[OUT-OF-BAND USER MESSAGE"
_STEER_FALLBACK_CLOSE = "[/OUT-OF-BAND USER MESSAGE]"
def _steer_markers() -> Tuple[str, str]:
def _steer_markers() -> tuple[str, str]:
"""``(open, close)`` steer markers from prompt_builder, or the stable fallback literals."""
try:
from agent.prompt_builder import STEER_MARKER_CLOSE, STEER_MARKER_OPEN
@@ -2676,7 +2675,7 @@ class _CompressionLease:
self.finish_lock_setup()
def _resolve_lock_api(lock_db: Any) -> Tuple[Any, Optional[Exception]]:
def _resolve_lock_api(lock_db: Any) -> tuple[Any, Optional[Exception]]:
"""Return ``(try_acquire_compression_lock, lookup_error)`` for ``lock_db``.
``(None, None)`` = no db or legacy SessionDB without the lock API (fail open); ``(None, exc)`` = lookup
itself failed (caller fails closed)."""
@@ -2696,7 +2695,7 @@ def _resolve_lock_api(lock_db: Any) -> Tuple[Any, Optional[Exception]]:
def _abort_lease(
agent: Any, lifecycle: _CompactionLifecycle, system_message: str, attempt_started_at: float,
failure_class: str, prompt: Optional[str] = None,
) -> Tuple[None, str]:
) -> tuple[None, str]:
"""Sit-out return for lease acquisition: prompt, aborted telemetry, terminal status edge."""
if prompt is None:
prompt = _existing_system_prompt(agent, system_message)
@@ -2742,7 +2741,7 @@ def _try_acquire_durable_lock(lease: _CompressionLease, try_acquire: Any, commit
def _sit_out_lock_contention(
agent: Any, lease: _CompressionLease, lifecycle: _CompactionLifecycle, system_message: str,
approx_tokens: Optional[int], attempt_started_at: float,
) -> Tuple[None, str]:
) -> tuple[None, str]:
"""Another path holds the lock: publish the lock-skip signal, warn once, sit out."""
existing = None
with contextlib.suppress(Exception):
@@ -2773,7 +2772,7 @@ def _sit_out_lock_contention(
def _acquire_compression_lease(
agent: Any, *, commit_fence: Optional[CompressionCommitFence], lifecycle: _CompactionLifecycle,
system_message: str, approx_tokens: Optional[int], attempt_started_at: float,
) -> Tuple[Optional[_CompressionLease], Optional[str]]:
) -> tuple[Optional[_CompressionLease], Optional[str]]:
"""Take the per-session compression lock; ``(None, prompt)`` means sit out.
Two AIAgents sharing a session_id (e.g. background review fork) would both rotate and orphan a child.
Keyed on the OLD id (what rivals read from SessionEntry). Loser sits out: messages unchanged, caller sees
@@ -2846,7 +2845,7 @@ def _acquire_compression_lease(
def _adopt_if_parent_rotated(
agent: Any, lease: _CompressionLease, messages: list, system_message: str
) -> Optional[Tuple[list, str]]:
) -> Optional[tuple[list, str]]:
"""Sit out (or adopt the live child) when the parent was already rotated.
A late contender can take the parent lock after the winner released it and rotated; holding the lock does
not prove this agent still owns a live parent. Returns the ``compress_context`` result to hand back, or
@@ -2970,7 +2969,7 @@ def _pre_compress_memory_context(agent: Any, messages: list, checkpoint_required
def _resolve_compress_call(
agent: Any, *, approx_tokens: Optional[int], focus_topic: Optional[str], force: bool, memory_context: str,
bypass_cooldown: bool,
) -> Tuple[Callable[..., Any], dict[str, Any]]:
) -> tuple[Callable[..., Any], dict[str, Any]]:
"""Bind ``compress()`` and only the kwargs its signature accepts."""
compress_fn = agent.context_compressor.compress
compress_kwargs = _supported_compression_kwargs(
@@ -3145,7 +3144,7 @@ def _rebuild_system_prompt_at_boundary(agent: Any, system_message: str) -> str:
# object (identity matters to provider-side tool-block caching on some backends).
try:
_refresh_agent_tool_definitions(agent)
except Exception: # noqa: BLE001
except Exception:
logger.warning(
"compaction tool-definition refresh failed; keeping the session's existing tool snapshot", exc_info=True
)
@@ -3182,7 +3181,7 @@ def _rebuild_system_prompt_at_boundary(agent: Any, system_message: str) -> str:
def _salvage_or_refuse_grown_transcript(
agent: Any, messages: list, compressed: list, *, system_message: str, attempt_started_at: float,
attempt_snapshot: dict,
) -> Tuple[Optional[list], Optional[str]]:
) -> tuple[Optional[list], Optional[str]]:
"""Anti-growth guard at the COMMIT SITE (in-place commits before the gateway can inspect).
Compares like-for-like rough estimates; on growth tries one mechanical salvage pass, else treats the
attempt as a refused no-op. Returns ``(compressed, None)`` to proceed or ``(None, prompt)`` when refused
@@ -4017,7 +4016,7 @@ def _begin_compression_attempt(
def _route_codex_compaction(
agent: Any, messages: list, system_message: str, *, commit_fence: Optional[CompressionCommitFence],
attempt: _Attempt, approx_tokens: Optional[int], task_id: str, force: bool,
) -> Tuple[list, str]:
) -> tuple[list, str]:
"""Codex owns the real thread: run its own compact under the commit fence bracket."""
if commit_fence is not None and not commit_fence.begin_commit(getattr(agent, "_hard_interrupt_requested", None)):
attempt.restore_compressor(agent.context_compressor)
@@ -4066,7 +4065,7 @@ def compress_context(
bypass_cooldown: bool = False, defer_context_engine_notification: bool = False,
commit_fence: Optional[CompressionCommitFence] = None, verbatim_tail: Optional[list] = None,
trigger: Optional[str] = None, snapshot_is_current: Optional[Callable[[], bool]] = None,
) -> Tuple[list, str]:
) -> tuple[list, str]:
"""Compress conversation context and split the session in SQLite.
``force`` (manual /compress) clears the summary-failure cooldown; ``bypass_cooldown`` (provider-proven
overflow) skips it once, breakers still apply. ``commit_fence`` stops a timed-out worker mutating session
@@ -4315,7 +4314,7 @@ def _record_codex_compaction_failure(agent: Any, error: str) -> None:
def _compress_context_via_codex_app_server(
agent: Any, messages: list, system_message: Optional[str], *, approx_tokens: Optional[int] = None,
task_id: str = "default", force: bool = False,
) -> Tuple[list, str]:
) -> tuple[list, str]:
"""Route compaction to Codex app-server for Codex-owned threads.
Rewriting the local transcript would not shrink the Codex thread, so Codex compacts its own thread and
Hermes' transcript is left unchanged."""
+8 -8
View File
@@ -28,9 +28,9 @@ RETIRED_ROW = "_retired_row"
OWN_ROW = "_own_row"
def retired_row_payload(message: Dict[str, Any]) -> Dict[str, Any]:
def retired_row_payload(message: dict[str, Any]) -> dict[str, Any]:
"""The fields the commit matches a retired id-less row by."""
payload: Dict[str, Any] = {"role": message.get("role"), "content": message.get("content")}
payload: dict[str, Any] = {"role": message.get("role"), "content": message.get("content")}
if message.get("tool_call_id"):
payload["tool_call_id"] = message["tool_call_id"]
calls = [call.get("id") for call in message.get("tool_calls") or () if isinstance(call, dict)]
@@ -47,7 +47,7 @@ def _positive_id(value: Any) -> Optional[int]:
def held_archive_coverage(
messages: Sequence[Any], verbatim_tail: Optional[Sequence[Any]] = None,
) -> Tuple[List[int], List[Dict[str, Any]]]:
) -> tuple[list[int], list[dict[str, Any]]]:
"""``(covered ids, unresolved dicts)`` from the history the compressor was handed.
A positive ``_row_id`` is covered, including ids a repair merged into that dict.
@@ -55,8 +55,8 @@ def held_archive_coverage(
marker-less miss is an unpersisted turn rather than a reason to archive the
lease watermark.
"""
covered: List[int] = []
unresolved: List[Dict[str, Any]] = []
covered: list[int] = []
unresolved: list[dict[str, Any]] = []
for batch in (messages or (), verbatim_tail or ()):
for message in batch:
if not isinstance(message, dict):
@@ -83,7 +83,7 @@ def newest_exact_held_id(
"""
from agent.context_compressor import _DB_PERSISTED_MARKER
exact: List[int] = []
exact: list[int] = []
for message in messages or ():
if not isinstance(message, dict) or not message.get(_DB_PERSISTED_MARKER):
continue
@@ -101,7 +101,7 @@ def newest_exact_held_id(
def coverage_for_commit(
session_db: Any, session_id: str, messages: Sequence[Any],
verbatim_tail: Optional[Sequence[Any]] = None,
) -> Tuple[Optional[List[int]], Optional[List[Dict[str, Any]]]]:
) -> tuple[Optional[list[int]], Optional[list[dict[str, Any]]]]:
"""Coverage to pass into ``archive_and_compact``, or ``(None, None)`` to keep the watermark.
``None`` when the newest exact held row is already inactive (another compaction
@@ -120,7 +120,7 @@ def coverage_for_commit(
if newest is not None and callable(role_of) and role_of(session_id, newest) is None:
return None, None
covered, unresolved = held_archive_coverage(messages, verbatim_tail)
retired: List[Dict[str, Any]] = []
retired: list[dict[str, Any]] = []
for message in unresolved:
named = [row for row in message.get(RETIRED_DURABLE_ROWS) or () if isinstance(row, dict)]
if int(message.get(UNNAMED_DURABLE_ROWS) or 0) > sum(1 for row in named if not row.get(OWN_ROW)):
+7 -7
View File
@@ -32,14 +32,14 @@ class CompressRequest:
@dataclass
class CompressResult:
status: str # "preview" | "compressed" | "lock_skipped" | "nothing_to_do"
before_messages: List[Dict[str, Any]]
after_messages: List[Dict[str, Any]]
before_messages: list[dict[str, Any]]
after_messages: list[dict[str, Any]]
before_tokens: int
after_tokens: int
request: CompressRequest
lines: List[str] = field(default_factory=list) # preview report lines (status == "preview")
lines: list[str] = field(default_factory=list) # preview report lines (status == "preview")
lock_holder: Any = None
summary: Optional[Dict[str, Any]] = None # ``summarize_manual_compression`` payload when compressed
summary: Optional[dict[str, Any]] = None # ``summarize_manual_compression`` payload when compressed
@property
def removed(self) -> int:
@@ -55,7 +55,7 @@ def parse_compress_args(raw_args: str) -> CompressRequest:
focus_topic=focus_topic or None)
def estimate_request_tokens(agent: Any, messages: Sequence[Dict[str, Any]]) -> int:
def estimate_request_tokens(agent: Any, messages: Sequence[dict[str, Any]]) -> int:
"""Transcript + system prompt + tool schemas: a transcript-only figure understates real request pressure
and can even appear to grow after a dense handoff summary replaces many short turns (#6217)."""
from agent.model_metadata import estimate_request_tokens_rough
@@ -67,7 +67,7 @@ def estimate_request_tokens(agent: Any, messages: Sequence[Dict[str, Any]]) -> i
def compress_now(
agent: Any, history: Sequence[Dict[str, Any]], request: CompressRequest, *,
agent: Any, history: Sequence[dict[str, Any]], request: CompressRequest, *,
system_message: Any = None, task_id: str = "default", skip_without_window: bool = False,
snapshot_is_current: Optional[Callable[[], bool]] = None,
) -> CompressResult:
@@ -150,7 +150,7 @@ def compress_now(
return CompressResult("compressed", before, list(compressed), before_tokens, after_tokens, request, summary=summary)
def render_compress_result(result: CompressResult, *, prefix: str = "") -> List[str]:
def render_compress_result(result: CompressResult, *, prefix: str = "") -> list[str]:
"""Surface-neutral text lines for a result (each surface may add its own icon/prefix)."""
if result.status == "preview":
return [f"{prefix}{line}" for line in result.lines]
+38 -38
View File
@@ -81,7 +81,7 @@ RUN_BUDGET_WRAPUP_NOTICE = (
def _midturn_request_pressure_tokens(
agent: Any, api_messages: List[Dict[str, Any]], effective_system: str, approx_tokens: int
agent: Any, api_messages: list[dict[str, Any]], effective_system: str, approx_tokens: int
) -> int:
"""Token figure the mid-turn pre-API compression guard compares: the pruned
native-Responses estimate when native compaction eligibility is proven (the generic
@@ -121,7 +121,7 @@ def _review_input_budget_exhausted(agent: Any) -> bool:
return isinstance(used, int) and not isinstance(used, bool) and used >= budget
def _maybe_inject_run_budget_wrapup(agent: Any, messages: List[Dict[str, Any]]) -> bool:
def _maybe_inject_run_budget_wrapup(agent: Any, messages: list[dict[str, Any]]) -> bool:
"""Inject the one-time wall-clock wrap-up notice when past 80% of budget.
Appends to the NEWEST ``role:"tool"`` message (cache-safe, like /steer); latches
@@ -157,7 +157,7 @@ def _maybe_inject_run_budget_wrapup(agent: Any, messages: List[Dict[str, Any]])
def _restore_user_after_reference_handoff(
messages: List[Dict[str, Any]], user_message: Any
messages: list[dict[str, Any]], user_message: Any
) -> bool:
"""Re-append this turn's real user ask when compaction left only a handoff (#80622).
Returns True when a restore append happened."""
@@ -175,7 +175,7 @@ def _restore_user_after_reference_handoff(
def _should_skip_model_call_for_reference_handoff(
messages: List[Dict[str, Any]], user_message: Any
messages: list[dict[str, Any]], user_message: Any
) -> bool:
"""Guard post-compaction continues against sole-handoff active turns (#80622)."""
from agent.context_compressor import reference_handoff_would_drive_next_model_call
@@ -291,7 +291,7 @@ def _continuation_overlap_length(previous: str, continuation: str) -> int:
return matched if matched >= _MIN_CONTINUATION_OVERLAP else 0
def _join_truncated_parts(parts: List[tuple[str, bool]]) -> str:
def _join_truncated_parts(parts: list[tuple[str, bool]]) -> str:
"""Join continuation fragments, deduping only interrupted-stream seams."""
joined = ""
previous_was_partial_stub = False
@@ -319,7 +319,7 @@ def _moa_reference_metrics_for_hook(agent: Any) -> Any:
return None
def _apply_active_turn_redirect(agent: Any, messages: List[Dict[str, Any]], text: str) -> None:
def _apply_active_turn_redirect(agent: Any, messages: list[dict[str, Any]], text: str) -> None:
"""Append a provider-safe checkpoint and correction to the live turn so role alternation
holds and cached messages stay byte-identical. INVARIANTS: raw chain-of-thought never enters
replayable content (inlined CoT reads as a prefill jailbreak and bricks the session with
@@ -468,7 +468,7 @@ def _maybe_grow_local_window(agent: Any, compressor: Any,
getattr(agent, "model", "") or "", base_url=base_url,
session_tokens=int(request_tokens), current_window=current_window,
)
except Exception as exc: # noqa: BLE001 — growth must never break a turn
except Exception as exc:
logger.debug("local window growth check failed: %s", exc)
return None
@@ -931,7 +931,7 @@ _LEGACY_LENGTH_CONTINUATION_NETWORK_STUB = (
_LENGTH_CONTINUATION_DROPPED_TOOLS_PREFIX = "[System: Your previous tool call "
def _get_continuation_prompt(is_partial_stub: bool, dropped_tools: Optional[List[str]] = None) -> str:
def _get_continuation_prompt(is_partial_stub: bool, dropped_tools: Optional[list[str]] = None) -> str:
if is_partial_stub and dropped_tools:
tool_list = ", ".join(dropped_tools[:3])
return (
@@ -988,7 +988,7 @@ _EMPTY_TOOL_RESPONSE_NUDGE = (
# each iteration). Sound because canonicalization is pure; malformed strings raise before
# being stored, so the repair fallback is never memoized. The byte budget exists because
# argument strings can run 100KB+, so a count bound alone does not bound memory.
_CANON_ARGS_CACHE: Dict[str, str] = {}
_CANON_ARGS_CACHE: dict[str, str] = {}
_CANON_ARGS_CACHE_MAX = 4096
_CANON_ARGS_CACHE_MAX_BYTES = 32 * 1024 * 1024
_canon_args_cache_bytes = 0
@@ -1064,8 +1064,8 @@ def _invalid_tool_name_error_content(name: str, valid_tool_names) -> str:
def _content_policy_blocked_result(
messages: List[Dict], api_call_count: int, *, final_response: str, error_detail: str
) -> Dict[str, Any]:
messages: list[dict], api_call_count: int, *, final_response: str, error_detail: str
) -> dict[str, Any]:
"""Terminal turn result for a content-policy block (deterministic for the unchanged
prompt, so no retry); shared by the HTTP-200 and exception paths."""
return {
@@ -1076,8 +1076,8 @@ def _content_policy_blocked_result(
def _partial_turn_result(
final_response: str, messages: List[Dict], api_call_count: int, **flags: Any
) -> Dict[str, Any]:
final_response: str, messages: list[dict], api_call_count: int, **flags: Any
) -> dict[str, Any]:
"""Incomplete-turn result whose ``error`` mirrors ``final_response``; ``flags`` add the
recovery-contract keys (``failed``, ``compression_deferred``, ...)."""
return {
@@ -1086,7 +1086,7 @@ def _partial_turn_result(
}
def _compression_deferred_result(agent, messages: List[Dict], api_call_count: int, reason: str = "lock") -> Dict[str, Any]:
def _compression_deferred_result(agent, messages: list[dict], api_call_count: int, reason: str = "lock") -> dict[str, Any]:
"""Soft turn result for a transiently-deferred compression. Both reasons must end as
``compression_deferred``, never ``compression_exhausted`` — the gateway wipes the
session on exhaustion (#9893/#35809). ``failed`` stays False; the turn persists."""
@@ -1122,9 +1122,9 @@ def _compression_deferred_result(agent, messages: List[Dict], api_call_count: in
def _provider_overflow_exhausted_result(
agent, messages: List[Dict], conversation_history, api_call_count: int,
agent, messages: list[dict], conversation_history, api_call_count: int,
request_pressure_tokens: int, max_compression_attempts: int,
) -> Dict[str, Any]:
) -> dict[str, Any]:
"""Fail closed when a rebuilt request is still too large after recovery."""
agent._flush_status_buffer()
logger.error(
@@ -1201,19 +1201,19 @@ def _ensure_cached_system_prompt_static(agent, system_message=None) -> None:
reconstruct_static_prefix(agent, system_message=system_message, log_label="failover redecoration")
def _peel_moa_guidance(messages: List[Dict[str, Any]], guidance: Any) -> List[Dict[str, Any]]:
def _peel_moa_guidance(messages: list[dict[str, Any]], guidance: Any) -> list[dict[str, Any]]:
"""Remove MoA reference guidance attached by ``_attach_reference_guidance``."""
from agent.moa_loop import peel_reference_guidance
return peel_reference_guidance(messages, guidance)
def _redecorate_prompt_cache_for_provider(
agent, api_messages: List[Dict[str, Any]], *, system_message=None,
moa_prepared: Optional[Dict[str, Any]] = None, tools_for_api: Optional[List[Dict[str, Any]]] = None,
) -> tuple[List[Dict[str, Any]], Optional[Dict[str, Any]], List[Dict[str, Any]]]:
agent, api_messages: list[dict[str, Any]], *, system_message=None,
moa_prepared: Optional[dict[str, Any]] = None, tools_for_api: Optional[list[dict[str, Any]]] = None,
) -> tuple[list[dict[str, Any]], Optional[dict[str, Any]], list[dict[str, Any]]]:
"""Strip and re-apply cache_control for the *current* provider policy — failover
``continue`` paths reuse ``api_messages`` (#72626). MoA guidance is peeled and rebased."""
messages: List[Dict[str, Any]] = [dict(m) if isinstance(m, dict) else m for m in (api_messages or [])]
messages: list[dict[str, Any]] = [dict(m) if isinstance(m, dict) else m for m in (api_messages or [])]
prepared = moa_prepared
guidance = prepared.get("guidance") if isinstance(prepared, dict) else None
if guidance:
@@ -1275,9 +1275,9 @@ def _engine_overrides_hook(engine: Any, name: str) -> bool:
def _apply_context_engine_selection(
agent: Any, api_messages: List[Dict[str, Any]], conversation_messages: List[Dict[str, Any]],
incoming_message: Optional[Dict[str, Any]], *, logger: Any,
) -> List[Dict[str, Any]]:
agent: Any, api_messages: list[dict[str, Any]], conversation_messages: list[dict[str, Any]],
incoming_message: Optional[dict[str, Any]], *, logger: Any,
) -> list[dict[str, Any]]:
"""Run the optional per-turn ``ContextEngine.select_context()`` hook, fail-open: any
exception or invalid return yields ``api_messages`` unchanged; history is never mutated."""
engine = getattr(agent, "context_compressor", None)
@@ -1325,7 +1325,7 @@ def _apply_context_engine_selection(
def _notify_context_engine_turn_complete(
agent: Any, messages: List[Dict[str, Any]], *, usage: Optional[Dict[str, Any]] = None, logger: Any, **meta: Any
agent: Any, messages: list[dict[str, Any]], *, usage: Optional[dict[str, Any]] = None, logger: Any, **meta: Any
) -> None:
"""Notify the active context engine that a user turn has finished (fail-open; the engine
gets a copy so it cannot mutate the persisted transcript)."""
@@ -1358,7 +1358,7 @@ def _decode_inline_moa_turn(user_message, persist_user_message):
return user_message, None, persist_user_message
def _preflight_timeout_result(agent, exc, conversation_history) -> Dict[str, Any]:
def _preflight_timeout_result(agent, exc, conversation_history) -> dict[str, Any]:
"""Typed recovery result when turn-start preflight compression timed out (#98424): no
provider call was sent, and surfaces would otherwise hide the actionable guidance."""
logger.warning(
@@ -1420,7 +1420,7 @@ class _LoopState:
restart_count: int = 0
_outer_error_count: int = 0 # outer-loop exceptions this turn (#92450), see _MAX_OUTER_LOOP_ERRORS
truncated_tool_call_retries: int = 0
truncated_response_parts: List[tuple[str, bool]] = field(default_factory=list)
truncated_response_parts: list[tuple[str, bool]] = field(default_factory=list)
compression_attempts: int = 0
_last_preflight_pressure: Optional[int] = None
# A provider overflow outweighs the rough-estimate calibration that defers preflight after
@@ -1467,7 +1467,7 @@ _CTX_FIELDS = frozenset({
"active_system_prompt", "current_turn_user_idx", "_preflight_compression_blocked",
})
# Keyword names each phase helper takes (minus ``agent``), cached per function object.
_PHASE_PARAMS: Dict[Any, tuple] = {}
_PHASE_PARAMS: dict[Any, tuple] = {}
# Verdict fields the loop latches (only ever sets True) instead of copying back:
# ``handle_api_error`` reports overflow recovery per call and must not clear an earlier arm.
_LATCHED_VERDICT_FIELDS = {"handle_api_error": frozenset({"_provider_overflow_recovery_pending"})}
@@ -1494,7 +1494,7 @@ def _run_phase(fn, agent, state: _LoopState, **extra):
return verdict
def _run_api_retry_loop(agent, s: _LoopState) -> Optional[Dict[str, Any]]:
def _run_api_retry_loop(agent, s: _LoopState) -> Optional[dict[str, Any]]:
"""One API call with its retry/recovery loop (guard → build → call → check, error handlers).
Returns a turn result dict when a phase ends the turn, else None once the loop is left
@@ -1526,7 +1526,7 @@ def _run_api_retry_loop(agent, s: _LoopState) -> Optional[Dict[str, Any]]:
return None
def _codex_app_server_turn(agent: Any, s: Any) -> Optional[Dict[str, Any]]:
def _codex_app_server_turn(agent: Any, s: Any) -> Optional[dict[str, Any]]:
"""The codex app-server's result for this turn, or None when its failure activated a fallback and the
generic loop retries the same user turn."""
codex_result = agent._run_codex_app_server_turn(
@@ -1548,19 +1548,19 @@ def _run_conversation_turn(
agent,
user_message: Any,
system_message: str = None,
conversation_history: List[Dict[str, Any]] = 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_display_metadata: Optional[dict[str, Any]] = None,
persist_user_platform_id: Optional[str] = None,
turn_author: Optional[Dict[str, Any]] = None,
turn_author: Optional[dict[str, Any]] = None,
moa_config: Optional[dict[str, Any]] = None,
title_user_message: Optional[str] = None,
prelude: Optional[Prelude] = None,
) -> Dict[str, Any]:
) -> dict[str, Any]:
"""Run a complete conversation with tool calling until completion; returns the result dict.
``stream_callback``: per-text-delta callback (TTS). ``persist_user_message``: clean text to
@@ -1710,19 +1710,19 @@ def run_conversation(
agent,
user_message: Any,
system_message: str = None,
conversation_history: List[Dict[str, Any]] = 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_display_metadata: Optional[dict[str, Any]] = None,
persist_user_platform_id: Optional[str] = None,
moa_config: Optional[dict[str, Any]] = None,
turn_author: Optional[Dict[str, Any]] = None,
turn_author: Optional[dict[str, Any]] = None,
title_user_message: Optional[str] = None,
prelude: Optional[Prelude] = None,
) -> Dict[str, Any]:
) -> dict[str, Any]:
"""Run one turn (see ``_run_conversation_turn``) and export the current-turn boundary.
Every envelope that leaves the loop — success, partial/error, interrupt, retry-exhausted,
+1 -1
View File
@@ -99,7 +99,7 @@ def _credential_secret_fingerprint(payload: Mapping[str, Any]) -> str | None:
def sanitize_borrowed_credential_payload(
payload: Mapping[str, Any],
provider_id: Any = None,
) -> Dict[str, Any]:
) -> dict[str, Any]:
"""Return a disk-safe credential-pool payload.
Owned sources pass through unchanged. Borrowed sources keep labels,
+72 -73
View File
@@ -189,7 +189,7 @@ _NOUS_EXTRA_STATE_KEYS = (
)
# ``replace(entry, **_CLEAR_STATUS)`` returns an entry with no error state.
_CLEAR_STATUS: Dict[str, Any] = {
_CLEAR_STATUS: dict[str, Any] = {
"last_status": None,
"last_status_at": None,
"last_error_code": None,
@@ -197,7 +197,7 @@ _CLEAR_STATUS: Dict[str, Any] = {
"last_error_message": None,
"last_error_reset_at": None,
}
_MARK_OK: Dict[str, Any] = {**_CLEAR_STATUS, "last_status": STATUS_OK}
_MARK_OK: dict[str, Any] = {**_CLEAR_STATUS, "last_status": STATUS_OK}
def _normalize_pool_auth_type(provider: str, token: Any, auth_type: Any) -> str:
@@ -237,8 +237,8 @@ class PooledCredential:
# A provider may rate-limit one model while the same credential remains
# usable for its sibling models. Keep that observation separate from the
# credential-wide status used for auth and billing failures.
model_cooldowns: Optional[Dict[str, float]] = None
extra: Dict[str, Any] = None # type: ignore[assignment]
model_cooldowns: Optional[dict[str, float]] = None
extra: dict[str, Any] = None # type: ignore[assignment]
def __post_init__(self):
if self.extra is None:
@@ -251,7 +251,7 @@ class PooledCredential:
raise AttributeError(f"'{type(self).__name__}' object has no attribute {name!r}")
@classmethod
def from_dict(cls, provider: str, payload: Dict[str, Any]) -> "PooledCredential":
def from_dict(cls, provider: str, payload: dict[str, Any]) -> "PooledCredential":
field_names = {f.name for f in fields(cls) if f.name != "provider"}
data = {k: payload.get(k) for k in field_names if k in payload}
# Rehydrated last_status_at may be an ISO string from to_dict() — normalize to float epoch
@@ -273,8 +273,8 @@ class PooledCredential:
data.setdefault("access_token", "")
return cls(provider=provider, **data)
def to_dict(self) -> Dict[str, Any]:
result: Dict[str, Any] = {}
def to_dict(self) -> dict[str, Any]:
result: dict[str, Any] = {}
for field_def in fields(self):
if field_def.name in {"provider", "extra"}:
continue
@@ -327,7 +327,7 @@ def label_from_token(token: str, fallback: str) -> str:
return fallback
def _codex_principal_identity(access_token: Any) -> Optional[Tuple[str, str]]:
def _codex_principal_identity(access_token: Any) -> Optional[tuple[str, str]]:
"""``(chatgpt_account_id, sub)`` of a Codex access token, or None when either claim is missing.
Decoded without signature verification: this only decides whether two credentials Hermes
@@ -344,7 +344,7 @@ def _codex_principal_identity(access_token: Any) -> Optional[Tuple[str, str]]:
return account_id.strip(), subject.strip()
def _codex_entry_tracks_singleton(entry: PooledCredential, singleton_tokens: Dict[str, Any]) -> bool:
def _codex_entry_tracks_singleton(entry: PooledCredential, singleton_tokens: dict[str, Any]) -> bool:
"""Whether a Codex pool entry may adopt the auth.json singleton's token pair.
``device_code`` IS the singleton. ``manual:device_code`` is ambiguous: a legacy alias of the
@@ -361,7 +361,7 @@ def _codex_entry_tracks_singleton(entry: PooledCredential, singleton_tokens: Dic
return entry_identity is not None and entry_identity == _codex_principal_identity(singleton_tokens.get("access_token"))
def _next_priority(entries: List[PooledCredential]) -> int:
def _next_priority(entries: list[PooledCredential]) -> int:
return max((entry.priority for entry in entries), default=-1) + 1
@@ -442,10 +442,10 @@ def _singleton_predates_entry(state: Any, entry: "PooledCredential") -> bool:
return state_ts < entry_ts
def _normalize_error_context(error_context: Optional[Dict[str, Any]]) -> Dict[str, Any]:
def _normalize_error_context(error_context: Optional[dict[str, Any]]) -> dict[str, Any]:
if not isinstance(error_context, dict):
return {}
normalized: Dict[str, Any] = {}
normalized: dict[str, Any] = {}
for key in ("reason", "message"):
value = error_context.get(key)
if isinstance(value, str) and value.strip():
@@ -510,7 +510,7 @@ def _iter_custom_providers(config: Optional[dict] = None):
yield _normalize_custom_pool_name(entry["name"]), entry
def _custom_entry_name_aliases(norm_name: str, entry: Dict[str, Any]) -> set:
def _custom_entry_name_aliases(norm_name: str, entry: dict[str, Any]) -> set:
aliases = {norm_name}
provider_key = _normalize_custom_pool_name(str(entry.get("provider_key") or ""))
if provider_key:
@@ -528,9 +528,9 @@ def _requested_custom_name_aliases(provider_name: str) -> set:
return aliases
def _pool_keys_for_custom_entry(norm_name: str, entry: Dict[str, Any]) -> List[str]:
def _pool_keys_for_custom_entry(norm_name: str, entry: dict[str, Any]) -> list[str]:
"""Durable ``providers.<key>`` slug first, then legacy ``custom:<name>``."""
keys: List[str] = []
keys: list[str] = []
provider_key = _normalize_custom_pool_name(str(entry.get("provider_key") or ""))
for key in (provider_key, f"{CUSTOM_POOL_PREFIX}{norm_name}" if norm_name else ""):
normalized = key.strip().lower()
@@ -542,7 +542,7 @@ def _pool_keys_for_custom_entry(norm_name: str, entry: Dict[str, Any]) -> List[s
def custom_provider_pool_key_candidates(
base_url: Optional[str],
provider_name: Optional[str] = None,
) -> List[str]:
) -> list[str]:
"""Return pool keys to try for a custom endpoint.
``hermes auth add <key>`` stores ``providers.<key>`` credentials under the
@@ -578,7 +578,7 @@ def get_custom_provider_pool_key(base_url: Optional[str], provider_name: Optiona
return candidates[0] if candidates else None
def list_custom_pool_providers() -> List[str]:
def list_custom_pool_providers() -> list[str]:
"""Return all 'custom:*' pool keys that have entries in auth.json."""
pool_data = read_credential_pool(None)
return sorted(
@@ -589,7 +589,7 @@ def list_custom_pool_providers() -> List[str]:
)
def _get_custom_provider_config(pool_key: str) -> Optional[Dict[str, Any]]:
def _get_custom_provider_config(pool_key: str) -> Optional[dict[str, Any]]:
"""Return the custom_providers config entry matching a pool key like 'custom:together.ai'."""
if not pool_key.startswith(CUSTOM_POOL_PREFIX):
return None
@@ -788,7 +788,7 @@ def _guarded_global_root(global_path: Optional[Path]) -> Optional[Path]:
def _write_through_provider_state_to_global_root(
provider_id: str, state: Dict[str, Any]
provider_id: str, state: dict[str, Any]
) -> None:
"""Persist a rotated OAuth ``state`` into the global-root auth.json.
@@ -830,7 +830,7 @@ def _singleton_target_for_entry(pool: "CredentialPool", entry: "PooledCredential
return None
def _store_owns_pool_provider(auth_store: Dict[str, Any], provider: str) -> bool:
def _store_owns_pool_provider(auth_store: dict[str, Any], provider: str) -> bool:
"""True when an already-loaded *auth_store* has its own rows for *provider*."""
pool = auth_store.get("credential_pool")
entries = pool.get(provider) if isinstance(pool, dict) else None
@@ -867,10 +867,10 @@ def _borrowed_single_use_pool_root() -> Optional[Path]:
def _update_root_pool_rows(
provider: str, payloads: List[Dict[str, Any]], global_path: Path,
provider: str, payloads: list[dict[str, Any]], global_path: Path,
*, status_cleared_ids: Optional[Iterable[str]] = None,
token_bases: Optional[Dict[str, Tuple[Any, Any]]] = None,
) -> List[Dict[str, Any]]:
token_bases: Optional[dict[str, tuple[Any, Any]]] = None,
) -> list[dict[str, Any]]:
"""UPDATE-ONLY merge of *payloads* into the root store's rows for *provider*.
A borrower may refresh the root's rows (rotation, cooldown state) but
@@ -889,7 +889,7 @@ def _update_root_pool_rows(
incoming_by_id = auth_mod._entry_ids(payloads)
cleared = {cid for cid in (status_cleared_ids or ()) if cid}
bases = token_bases or {}
merged: List[Dict[str, Any]] = []
merged: list[dict[str, Any]] = []
changed = False
for disk_entry in existing_list:
did = disk_entry.get("id") if isinstance(disk_entry, dict) else None
@@ -912,12 +912,12 @@ def _update_root_pool_rows(
def persist_pool_entries(
provider: str,
payloads: List[Dict[str, Any]],
payloads: list[dict[str, Any]],
*,
removed_ids: Optional[Iterable[str]] = None,
status_cleared_ids: Optional[Iterable[str]] = None,
token_bases: Optional[Dict[str, Tuple[Any, Any]]] = None,
) -> Optional[List[Dict[str, Any]]]:
token_bases: Optional[dict[str, tuple[Any, Any]]] = None,
) -> Optional[list[dict[str, Any]]]:
"""Persist a provider's pool rows to the store that OWNS them.
A named profile that sees a single-use-refresh provider (see
@@ -958,7 +958,7 @@ def persist_pool_entries(
# (Codex, xAI): log names (sync-message form, "<name> OAuth" form),
# ``hermes_cli.auth`` refresh function and terminal-error predicate (looked
# up at call time so tests can patch them).
_TOKENS_SINGLETON_PROVIDERS: Dict[str, Tuple[str, str, str, str]] = {
_TOKENS_SINGLETON_PROVIDERS: dict[str, tuple[str, str, str, str]] = {
"openai-codex": ("Codex", "Codex", "refresh_codex_oauth_pure", "_is_terminal_codex_oauth_refresh_error"),
"xai-oauth": ("xAI OAuth", "xAI", "refresh_xai_oauth_pure", "_is_terminal_xai_oauth_refresh_error"),
}
@@ -1005,21 +1005,21 @@ class _RefreshDone(Exception):
class CredentialPool(CredentialPoolAdminMixin, CredentialPoolModelCooldownMixin):
def __init__(self, provider: str, entries: List[PooledCredential]):
def __init__(self, provider: str, entries: list[PooledCredential]):
self.provider = provider
self._entries = sorted(entries, key=lambda entry: entry.priority)
self._current_id: Optional[str] = None
# Ids of rows read via the global-root fallback (single-use OAuth
# providers only); set by load_pool(), consumed by add_entry().
self._borrowed_root_ids: Set[str] = set()
self._persisted_token_pairs: Dict[str, Tuple[Any, Any]] = {}
self._borrowed_root_ids: set[str] = set()
self._persisted_token_pairs: dict[str, tuple[Any, Any]] = {}
self._strategy = get_pool_strategy(provider)
# RLock: _replace_entry/_persist self-acquire it so the DEFERRED
# single-use-token refresh path (network I/O outside the lock by
# design) still serializes its pool mutations; in-lock callers
# re-acquire reentrantly.
self._lock = threading.RLock()
self._active_leases: Dict[str, int] = {}
self._active_leases: dict[str, int] = {}
self._max_concurrent = DEFAULT_MAX_CONCURRENT_PER_CREDENTIAL
# Monotonic timestamp of the last "no available entries" log (see
# NO_AVAILABLE_ENTRIES_LOG_THROTTLE_SECONDS). Re-armed to None on every
@@ -1093,7 +1093,7 @@ class CredentialPool(CredentialPoolAdminMixin, CredentialPoolModelCooldownMixin)
)
return min(candidates) if candidates else None
def entries(self) -> List[PooledCredential]:
def entries(self) -> list[PooledCredential]:
with self._lock:
return list(self._entries)
@@ -1146,8 +1146,8 @@ class CredentialPool(CredentialPoolAdminMixin, CredentialPoolModelCooldownMixin)
def _persist(
self,
*,
removed_ids: Optional[List[str]] = None,
status_cleared_ids: Optional[List[str]] = None,
removed_ids: Optional[list[str]] = None,
status_cleared_ids: Optional[list[str]] = None,
) -> None:
# Self-locking: snapshotting self._entries must not race a rotation.
with self._lock:
@@ -1186,7 +1186,7 @@ class CredentialPool(CredentialPoolAdminMixin, CredentialPoolModelCooldownMixin)
return self._find(lambda e: e.id == updated.id) or updated
return updated
def _quarantine_sources(self, entry: PooledCredential, sources: Set[str]) -> None:
def _quarantine_sources(self, entry: PooledCredential, sources: set[str]) -> None:
"""Drop every entry seeded from *sources* and persist the removal.
Atomic read-modify-write of ``self._entries``: this runs on the
@@ -1205,7 +1205,7 @@ class CredentialPool(CredentialPoolAdminMixin, CredentialPoolModelCooldownMixin)
def _is_terminal_auth_failure(
self,
status_code: Optional[int],
normalized_error: Dict[str, Any],
normalized_error: dict[str, Any],
) -> bool:
"""Detect upstream-permanent OAuth failures that won't recover on TTL.
@@ -1225,7 +1225,7 @@ class CredentialPool(CredentialPoolAdminMixin, CredentialPoolModelCooldownMixin)
self,
entry: PooledCredential,
status_code: Optional[int],
error_context: Optional[Dict[str, Any]] = None,
error_context: Optional[dict[str, Any]] = None,
*,
persist: bool = True,
failure_reason: Optional[str] = None,
@@ -1412,7 +1412,7 @@ class CredentialPool(CredentialPoolAdminMixin, CredentialPoolModelCooldownMixin)
"Pool entry %s: syncing %s tokens from auth.json (refreshed by another process)",
entry.id, display,
)
field_updates: Dict[str, Any] = {
field_updates: dict[str, Any] = {
"access_token": store_access or entry.access_token,
"refresh_token": store_refresh or entry.refresh_token,
**_CLEAR_STATUS,
@@ -1448,7 +1448,7 @@ class CredentialPool(CredentialPoolAdminMixin, CredentialPoolModelCooldownMixin)
if not any(v not in (None, "") and getattr(entry, k, None) != v for k, v in comparable.items()):
return entry
logger.debug("Pool entry %s: syncing Nous state from auth.json", entry.id)
field_updates: Dict[str, Any] = dict(_CLEAR_STATUS)
field_updates: dict[str, Any] = dict(_CLEAR_STATUS)
field_updates.update({k: v for k, v in comparable.items() if v})
extra_updates = dict(entry.extra)
extra_updates.update(
@@ -1502,7 +1502,7 @@ class CredentialPool(CredentialPoolAdminMixin, CredentialPoolModelCooldownMixin)
except Exception as exc:
logger.debug("Failed to sync %s pool entry back to auth store: %s", self.provider, exc)
def _apply_entry_to_singleton_state(self, entry: PooledCredential, state: Dict[str, Any]) -> bool:
def _apply_entry_to_singleton_state(self, entry: PooledCredential, state: dict[str, Any]) -> bool:
"""Copy *entry*'s tokens into the provider's auth.json ``state`` in place."""
if self.provider == "nous":
state["access_token"] = entry.access_token
@@ -1635,7 +1635,6 @@ class CredentialPool(CredentialPoolAdminMixin, CredentialPoolModelCooldownMixin)
"message": f"rotated credential was not durably written to {store}: {exc}",
},
)
return None
def _single_use_refresh_lock_timeout(self) -> float:
"""Configured refresh POST timeout plus margin, so a slow token endpoint cannot starve the flock."""
@@ -1644,7 +1643,7 @@ class CredentialPool(CredentialPoolAdminMixin, CredentialPoolModelCooldownMixin)
return max(float(auth_mod.AUTH_LOCK_TIMEOUT_SECONDS), float(refresh_timeout_seconds) + 5.0)
def _commit_anthropic_rotation(
self, entry: PooledCredential, refreshed: Dict[str, Any]
self, entry: PooledCredential, refreshed: dict[str, Any]
) -> None:
"""Write a rotated Anthropic pair to its authoritative singleton, or fail closed.
@@ -2010,11 +2009,11 @@ class CredentialPool(CredentialPoolAdminMixin, CredentialPoolModelCooldownMixin)
self._unmatched_rotation_streak = 0
return entry
def _select_under_lock(self, *, model: Optional[str] = None) -> Tuple[Optional[PooledCredential], List[PooledCredential]]:
def _select_under_lock(self, *, model: Optional[str] = None) -> tuple[Optional[PooledCredential], list[PooledCredential]]:
with self._lock:
return self._select_unlocked(model=model)
def _refresh_pending_entries(self, pending: List[PooledCredential]) -> None:
def _refresh_pending_entries(self, pending: list[PooledCredential]) -> None:
"""Refresh deferred single-use-token entries OUTSIDE the pool lock.
Each refresh takes the cross-process ``_auth_store_lock`` (20+ s
@@ -2071,7 +2070,7 @@ class CredentialPool(CredentialPoolAdminMixin, CredentialPoolModelCooldownMixin)
def _available_entries(
self, *, clear_expired: bool = False, refresh: bool = False, model: Optional[str] = None,
) -> Tuple[List[PooledCredential], List[PooledCredential]]:
) -> tuple[list[PooledCredential], list[PooledCredential]]:
"""Return (available, pending_refresh) for entries not in cooldown.
*clear_expired* resets elapsed cooldowns to STATUS_OK and persists.
@@ -2083,9 +2082,9 @@ class CredentialPool(CredentialPoolAdminMixin, CredentialPoolModelCooldownMixin)
"""
now = time.time()
cleared_any = False
entries_to_prune: List[str] = []
available: List[PooledCredential] = []
pending_refresh: List[PooledCredential] = []
entries_to_prune: list[str] = []
available: list[PooledCredential] = []
pending_refresh: list[PooledCredential] = []
sole_credential = self._is_sole_credential()
for entry in self._entries:
# Borrowed credentials persist as metadata-only references and are
@@ -2166,7 +2165,7 @@ class CredentialPool(CredentialPoolAdminMixin, CredentialPoolModelCooldownMixin)
def _select_unlocked(
self, *, refresh: bool = True, count: bool = True, model: Optional[str] = None,
) -> Tuple[Optional[PooledCredential], List[PooledCredential]]:
) -> tuple[Optional[PooledCredential], list[PooledCredential]]:
"""Select the best available entry; returns ``(entry, pending_refresh)``.
``count=False`` skips the ``request_count`` bump for selections that are
@@ -2298,7 +2297,7 @@ class CredentialPool(CredentialPoolAdminMixin, CredentialPoolModelCooldownMixin)
self,
*,
status_code: Optional[int],
error_context: Optional[Dict[str, Any]] = None,
error_context: Optional[dict[str, Any]] = None,
api_key_hint: Optional[str] = None,
credential_id: Optional[str] = None,
failure_reason: Optional[str] = None,
@@ -2394,7 +2393,7 @@ class CredentialPool(CredentialPoolAdminMixin, CredentialPoolModelCooldownMixin)
def _acquire_lease_under_lock(
self, credential_id: Optional[str],
) -> Tuple[Optional[str], List[PooledCredential]]:
) -> tuple[Optional[str], list[PooledCredential]]:
with self._lock:
if credential_id:
self._active_leases[credential_id] = self._active_leases.get(credential_id, 0) + 1
@@ -2466,7 +2465,7 @@ class CredentialPool(CredentialPoolAdminMixin, CredentialPoolModelCooldownMixin)
# --- Seeding --------------------------------------------------------------
def _upsert_entry(entries: List[PooledCredential], provider: str, source: str, payload: Dict[str, Any]) -> bool:
def _upsert_entry(entries: list[PooledCredential], provider: str, source: str, payload: dict[str, Any]) -> bool:
matching_indices = [idx for idx, entry in enumerate(entries) if entry.source == source]
existing_idx = matching_indices[0] if matching_indices else None
duplicate_indices = set(matching_indices[1:])
@@ -2481,8 +2480,8 @@ def _upsert_entry(entries: List[PooledCredential], provider: str, source: str, p
return True
existing = entries[existing_idx]
field_updates: Dict[str, Any] = {}
extra_updates: Dict[str, Any] = {}
field_updates: dict[str, Any] = {}
extra_updates: dict[str, Any] = {}
_field_names = {f.name for f in fields(existing)}
incoming_token = payload.get("access_token")
token_changed = incoming_token is not None and incoming_token != existing.access_token
@@ -2527,7 +2526,7 @@ _ANTHROPIC_SOURCE_RANK = {
}
def _normalize_pool_priorities(provider: str, entries: List[PooledCredential]) -> bool:
def _normalize_pool_priorities(provider: str, entries: list[PooledCredential]) -> bool:
if provider != "anthropic":
return False
manual_entries = sorted(
@@ -2551,7 +2550,7 @@ def _normalize_pool_priorities(provider: str, entries: List[PooledCredential]) -
return changed
def _retain_sources_not_in(entries: List[PooledCredential], drop: Set[str]) -> bool:
def _retain_sources_not_in(entries: list[PooledCredential], drop: set[str]) -> bool:
"""Remove entries whose source is in *drop*; True if anything was removed."""
retained = [entry for entry in entries if entry.source not in drop]
if len(retained) == len(entries):
@@ -2563,14 +2562,14 @@ def _retain_sources_not_in(entries: List[PooledCredential], drop: Set[str]) -> b
class _Seeder:
"""Accumulates ``_upsert_entry`` results for one ``load_pool`` seeding pass."""
def __init__(self, provider: str, entries: List[PooledCredential]):
def __init__(self, provider: str, entries: list[PooledCredential]):
self.provider = provider
self.entries = entries
self.changed = False
self.active_sources: Set[str] = set()
self.active_sources: set[str] = set()
self.is_suppressed = _is_source_suppressed_fn()
def upsert(self, source: str, payload: Dict[str, Any]) -> bool:
def upsert(self, source: str, payload: dict[str, Any]) -> bool:
"""Upsert unless suppressed (``hermes auth remove`` must stay stable across loads)."""
if self.is_suppressed(self.provider, source):
return False
@@ -2580,7 +2579,7 @@ class _Seeder:
return ingested
@property
def result(self) -> Tuple[bool, Set[str]]:
def result(self) -> tuple[bool, set[str]]:
return self.changed, self.active_sources
@@ -2638,7 +2637,7 @@ def _seed_anthropic_singletons(seed: _Seeder) -> None:
})
def _seed_nous_singleton(seed: _Seeder, auth_store: Dict[str, Any]) -> None:
def _seed_nous_singleton(seed: _Seeder, auth_store: dict[str, Any]) -> None:
state, source_path = _load_provider_state_with_source(auth_store, "nous")
if owned_profile_reads_root_state(auth_store, "nous", source_path):
# e.g. an agent_key-only row surviving a fork strip/heal
@@ -2676,7 +2675,7 @@ def _seed_nous_singleton(seed: _Seeder, auth_store: Dict[str, Any]) -> None:
# Warn once per token per process when Copilot exchange degrades to raw token (#114740).
_COPILOT_RAW_DEGRADATION_WARNED: Set[str] = set()
_COPILOT_RAW_DEGRADATION_WARNED: set[str] = set()
def _warn_copilot_raw_degradation_once(token: str) -> None:
@@ -2795,7 +2794,7 @@ def _seed_minimax_singleton(seed: _Seeder) -> None:
logger.debug("MiniMax OAuth token seed failed: %s", exc)
def _seed_tokens_singleton(seed: _Seeder, auth_store: Dict[str, Any]) -> None:
def _seed_tokens_singleton(seed: _Seeder, auth_store: dict[str, Any]) -> None:
"""Codex / xAI: surface the auth.json ``providers.<id>.tokens`` singleton as ``device_code``.
Hermes owns its own Codex auth state and does NOT auto-import
@@ -2825,7 +2824,7 @@ def _seed_tokens_singleton(seed: _Seeder, auth_store: Dict[str, Any]) -> None:
})
def _seed_from_singletons(provider: str, entries: List[PooledCredential]) -> Tuple[bool, Set[str]]:
def _seed_from_singletons(provider: str, entries: list[PooledCredential]) -> tuple[bool, set[str]]:
seed = _Seeder(provider, entries)
auth_store = _load_auth_store()
if provider == "anthropic":
@@ -2867,7 +2866,7 @@ def get_env_prefer_dotenv(key: str) -> str:
# Providers already warned about env-key -> pool ingestion, once per process
# (#81952 expected-behavior #3).
_ENV_INGESTION_WARNED: Set[str] = set()
_ENV_INGESTION_WARNED: set[str] = set()
def _warn_env_ingestion_once(provider: str, env_var: str) -> None:
@@ -2892,8 +2891,8 @@ def _warn_env_ingestion_once(provider: str, env_var: str) -> None:
)
def _env_payload(*, env_var: str, token: str, base_url: str) -> Dict[str, Any]:
payload: Dict[str, Any] = {
def _env_payload(*, env_var: str, token: str, base_url: str) -> dict[str, Any]:
payload: dict[str, Any] = {
"auth_type": AUTH_TYPE_API_KEY,
"access_token": token,
"base_url": base_url,
@@ -2917,7 +2916,7 @@ _ENV_BASE_URL_RESOLVERS = {
}
def _env_key_var_candidates(env_vars: List[str], entries: List[PooledCredential]) -> List[str]:
def _env_key_var_candidates(env_vars: list[str], entries: list[PooledCredential]) -> list[str]:
"""*env_vars*, their numbered siblings, and the ``env:VAR`` names already persisted.
``VAR_2``, ``VAR_3``, ... are tried for every declared VAR until the first
@@ -2944,7 +2943,7 @@ def _env_key_var_candidates(env_vars: List[str], entries: List[PooledCredential]
return names
def _seed_from_env(provider: str, entries: List[PooledCredential]) -> Tuple[bool, Set[str]]:
def _seed_from_env(provider: str, entries: list[PooledCredential]) -> tuple[bool, set[str]]:
seed = _Seeder(provider, entries)
# Copilot's singleton branch exchanges the raw ghu_ OAuth token for the
# api token via `get_copilot_api_token`; the generic loop would re-read
@@ -2989,8 +2988,8 @@ def _seed_from_env(provider: str, entries: List[PooledCredential]) -> Tuple[bool
def _prune_stale_seeded_entries(
entries: List[PooledCredential],
active_sources: Set[str],
entries: list[PooledCredential],
active_sources: set[str],
*,
prune_env_sources: bool = True,
) -> bool:
@@ -3015,7 +3014,7 @@ def _prune_stale_seeded_entries(
return True
def _seed_custom_pool(pool_key: str, entries: List[PooledCredential]) -> Tuple[bool, Set[str]]:
def _seed_custom_pool(pool_key: str, entries: list[PooledCredential]) -> tuple[bool, set[str]]:
"""Seed a custom endpoint pool from custom_providers config and model config."""
seed = _Seeder(pool_key, entries)
+1 -1
View File
@@ -85,7 +85,7 @@ class CredentialPoolAdminMixin:
self._persist()
return self._find(lambda e: e.id == credential_id)
def resolve_target(self, target: Any) -> Tuple[Optional[int], Optional[PooledCredential], Optional[str]]:
def resolve_target(self, target: Any) -> tuple[Optional[int], Optional[PooledCredential], Optional[str]]:
raw = str(target or "").strip()
if not raw:
return None, None, "No credential target provided."
+5 -5
View File
@@ -32,9 +32,9 @@ def model_cooldown_until(entry: "PooledCredential", model: Optional[str]) -> Opt
return max(active) if active else None
def merge_model_cooldowns(*maps: Any) -> Dict[str, float]:
def merge_model_cooldowns(*maps: Any) -> dict[str, float]:
"""Latest reset per model across snapshots — each writer only observed its own model."""
merged: Dict[str, float] = {}
merged: dict[str, float] = {}
for cooldowns in maps:
if not isinstance(cooldowns, dict):
continue
@@ -75,7 +75,7 @@ class CredentialPoolModelCooldownMixin:
)
def _cool_down_model(
self, entry: "PooledCredential", model: str, error_context: Optional[Dict[str, Any]],
self, entry: "PooledCredential", model: str, error_context: Optional[dict[str, Any]],
failure_reason: Optional[str] = None,
) -> None:
"""Record a cooldown for *model* on *entry* and every sibling sharing its key.
@@ -103,7 +103,7 @@ class CredentialPoolModelCooldownMixin:
self._adopt(scoped, persist=False, model_cooldowns=cooldowns)
self._persist()
def limit_state(self, models: Iterable[str]) -> Optional[Dict[str, Any]]:
def limit_state(self, models: Iterable[str]) -> Optional[dict[str, Any]]:
"""What a picker should say about this pool's rate limits, or ``None`` when nothing is limited.
``{"scope": "account", "resets_at": epoch}`` when every live entry is benched credential-wide
@@ -123,7 +123,7 @@ class CredentialPoolModelCooldownMixin:
if live and len(benched) == len(live):
return {"scope": "account", "resets_at": min(benched.values())}
usable = [entry for entry in live if entry.id not in benched]
cooled: Dict[str, float] = {}
cooled: dict[str, float] = {}
for model in models:
waits = [model_cooldown_until(entry, model) for entry in usable]
if waits and all(waits) and min(waits) - now < MODEL_ENTITLEMENT_BENCH_SECONDS / 2:
+1 -1
View File
@@ -79,7 +79,7 @@ def is_terminal_plugin_refresh_error(exc: BaseException) -> bool:
def recover_failed_plugin_refresh(
pool: "CredentialPool", entry: "PooledCredential", exc: Exception,
) -> Tuple[bool, Optional["PooledCredential"]]:
) -> tuple[bool, Optional["PooledCredential"]]:
"""Recovery for a plugin hook that raised: adopt a peer's rotation, or quarantine a dead grant.
Returns ``(handled, result)``; ``handled=False`` means the caller should bench the row as a
+3 -3
View File
@@ -59,8 +59,8 @@ class RemovalResult:
``load_pool`` skips the source; only ``manual`` entries legitimately use False.
"""
cleaned: List[str] = field(default_factory=list)
hints: List[str] = field(default_factory=list)
cleaned: list[str] = field(default_factory=list)
hints: list[str] = field(default_factory=list)
suppress: bool = True
@@ -224,7 +224,7 @@ def _suppress_only(*hints: str) -> Callable[..., RemovalResult]:
# ORDER MATTERS — ``find_removal_step`` returns the first match. Provider-
# specific steps precede the generic ``env:*`` step so copilot's ``env:GH_TOKEN``
# takes the copilot path (no .env edits) rather than the generic env-var removal.
_REGISTRY: List[RemovalStep] = [
_REGISTRY: list[RemovalStep] = [
RemovalStep(
provider="copilot", source_id="gh_cli",
match_fn=lambda src: src == "gh_cli" or src.startswith("env:"),
+54 -54
View File
@@ -41,8 +41,8 @@ def _state_file() -> Path:
return get_hermes_home() / "skills" / ".curator_state"
def load_state() -> Dict[str, Any]:
base: Dict[str, Any] = {
def load_state() -> dict[str, Any]:
base: dict[str, Any] = {
"last_run_at": None, "last_run_duration_seconds": None, "last_run_summary": None,
"last_run_summary_shown_at": None, "last_report_path": None, "paused": False, "run_count": 0,
}
@@ -57,7 +57,7 @@ def load_state() -> Dict[str, Any]:
return base
def save_state(data: Dict[str, Any]) -> None:
def save_state(data: dict[str, Any]) -> None:
try:
atomic_json_write(_state_file(), data, indent=2, sort_keys=True)
except Exception as e:
@@ -74,14 +74,14 @@ def is_paused() -> bool:
# --- Config access ---
def _subdict(node: Any, *keys: str) -> Dict[str, Any]:
def _subdict(node: Any, *keys: str) -> dict[str, Any]:
"""Walk nested dict keys; {} if any level is missing or not a dict."""
for key in keys:
node = node.get(key) if isinstance(node, dict) else None
return node if isinstance(node, dict) else {}
def _read_config_section(*path: str, label: str, log: logging.Logger = logger) -> Dict[str, Any]:
def _read_config_section(*path: str, label: str, log: logging.Logger = logger) -> dict[str, Any]:
"""Read a nested section of ~/.hermes/config.yaml. Tolerates missing file."""
try:
from hermes_cli.config import load_config_readonly
@@ -92,7 +92,7 @@ def _read_config_section(*path: str, label: str, log: logging.Logger = logger) -
return _subdict(cfg, *path)
def _load_config() -> Dict[str, Any]:
def _load_config() -> dict[str, Any]:
return _read_config_section("curator", label="curator")
@@ -181,7 +181,7 @@ def should_run_now(now: Optional[datetime] = None) -> bool:
# --- Automatic state transitions (pure function, no LLM) ---
def _cron_referenced_skills() -> Set[str]:
def _cron_referenced_skills() -> set[str]:
"""Skill names referenced by any cron job (incl. paused/disabled). Best-effort: a cron import error or corrupt jobs store yields an empty set, never a crash."""
try:
from cron.jobs import referenced_skill_names as _refs
@@ -206,7 +206,7 @@ def _archive_as_curator(_u, name: str) -> bool:
reset_ledger_actor(tok)
def apply_automatic_transitions(now: Optional[datetime] = None) -> Dict[str, int]:
def apply_automatic_transitions(now: Optional[datetime] = None) -> dict[str, int]:
"""Move every curator-managed skill between active/stale/archived based on its latest real activity; pinned skills are
never touched. Built-ins are seeded with a baseline record on first sight so their inactivity clock starts NOW, not at epoch.
Returns a counter dict."""
@@ -487,7 +487,7 @@ def _needle_in_path_component(needle: str, path: str) -> bool:
return any(part and part.rsplit(".", 1)[0].replace("-", "_") == norm_needle for part in path.replace("\\", "/").split("/"))
def _skill_manage_args(tc: Any, *, raw_fallback: bool) -> Optional[Dict[str, Any]]:
def _skill_manage_args(tc: Any, *, raw_fallback: bool) -> Optional[dict[str, Any]]:
"""Parsed arguments of a ``skill_manage`` tool call (JSON string or dict), or None to skip. With *raw_fallback*,
a malformed string yields ``{"_raw": raw}`` so substring matching still catches the common case."""
if not isinstance(tc, dict) or tc.get("name") != "skill_manage":
@@ -502,7 +502,7 @@ def _skill_manage_args(tc: Any, *, raw_fallback: bool) -> Optional[Dict[str, Any
return args if isinstance(args, dict) else None
def _find_reference(args: Dict[str, Any], needles: Set[str]) -> Optional[str]:
def _find_reference(args: dict[str, Any], needles: set[str]) -> Optional[str]:
"""First argument value (file_path, file_content, content, new_string, _raw — in that order) that references one of
*needles*. ``file_path`` must match a whole path component; content fields match on word boundaries so "test" does not match "latest"."""
for key in ("file_path", "file_content", "content", "new_string", "_raw"):
@@ -515,13 +515,13 @@ def _find_reference(args: Dict[str, Any], needles: Set[str]) -> Optional[str]:
def _classify_removed_skills(
removed: List[str], added: List[str], after_names: Set[str], tool_calls: List[Dict[str, Any]],
) -> Dict[str, List[Dict[str, Any]]]:
removed: list[str], added: list[str], after_names: set[str], tool_calls: list[dict[str, Any]],
) -> dict[str, list[dict[str, Any]]]:
"""Split ``removed`` into consolidated vs pruned. Heuristic: a ``skill_manage`` call on a DIFFERENT, surviving-or-new
skill whose file_path/content arguments reference the removed name is the "absorbed" signal; earliest match wins.
Returns ``{"consolidated": [{name, into, evidence}], "pruned": [{name}]}``."""
consolidated: List[Dict[str, Any]] = []
pruned: List[Dict[str, Any]] = []
consolidated: list[dict[str, Any]] = []
pruned: list[dict[str, Any]] = []
parsed_calls = [a for a in (_skill_manage_args(tc, raw_fallback=True) for tc in tool_calls or []) if a is not None]
destinations = set(after_names) | set(added or [])
for name in filter(None, removed):
@@ -541,7 +541,7 @@ def _classify_removed_skills(
return {"consolidated": consolidated, "pruned": pruned}
def _parse_structured_summary(llm_final: str) -> Dict[str, List[Dict[str, str]]]:
def _parse_structured_summary(llm_final: str) -> dict[str, list[dict[str, str]]]:
"""Extract the required fenced ```yaml block (``consolidations:`` / ``prunings:`` lists) from the curator's final
response. Tolerant: missing block or malformed YAML → empty lists (caller falls back to the tool-call heuristic); a partial
block returns what parsed. Returns ``{"consolidations": [{from, into, reason}], "prunings": [{name, reason}]}``."""
@@ -557,7 +557,7 @@ def _parse_structured_summary(llm_final: str) -> Dict[str, List[Dict[str, str]]]
if not isinstance(data, dict):
return {"consolidations": [], "prunings": []}
def _entries(key: str, *fields: str) -> List[Dict[str, str]]:
def _entries(key: str, *fields: str) -> list[dict[str, str]]:
raw = data.get(key) or []
entries = (e for e in raw if isinstance(e, dict)) if isinstance(raw, list) else ()
cleaned = ({f: (v.strip() if isinstance((v := e.get(f)), str) else "") for f in (*fields, "reason")} for e in entries)
@@ -566,11 +566,11 @@ def _parse_structured_summary(llm_final: str) -> Dict[str, List[Dict[str, str]]]
return {"consolidations": _entries("consolidations", "from", "into"), "prunings": _entries("prunings", "name")}
def _extract_absorbed_into_declarations(tool_calls: List[Dict[str, Any]]) -> Dict[str, Dict[str, Any]]:
def _extract_absorbed_into_declarations(tool_calls: list[dict[str, Any]]) -> dict[str, dict[str, Any]]:
"""Model-declared absorption targets from ``skill_manage(action='delete')`` calls — the authoritative classification
signal (beats YAML parsing and substring heuristics). Returns ``{name: {"into": umbrella | "", "declared": True}}``;
``into == ""`` is an explicit prune. Deletes omitting ``absorbed_into`` are absent so the caller falls back to heuristic/YAML (older runs)."""
out: Dict[str, Dict[str, Any]] = {}
out: dict[str, dict[str, Any]] = {}
for args in (_skill_manage_args(tc, raw_fallback=False) for tc in tool_calls or []):
if args is not None and args.get("action") == "delete":
name, target = args.get("name"), args.get("absorbed_into")
@@ -580,9 +580,9 @@ def _extract_absorbed_into_declarations(tool_calls: List[Dict[str, Any]]) -> Dic
def _reconcile_classification(
removed: List[str], heuristic: Dict[str, List[Dict[str, Any]]], model_block: Dict[str, List[Dict[str, str]]],
destinations: Set[str], absorbed_declarations: Optional[Dict[str, Dict[str, Any]]] = None,
) -> Dict[str, List[Dict[str, Any]]]:
removed: list[str], heuristic: dict[str, list[dict[str, Any]]], model_block: dict[str, list[dict[str, str]]],
destinations: set[str], absorbed_declarations: Optional[dict[str, dict[str, Any]]] = None,
) -> dict[str, list[dict[str, Any]]]:
"""Merge heuristic (tool-call evidence) with the model's structured block.
First match wins; every removed skill lands in exactly one bucket:
- ``absorbed_into`` declared at delete is authoritative: existing target → consolidated; ``""`` → pruned; missing target → fall through.
@@ -594,8 +594,8 @@ def _reconcile_classification(
model_cons = {e["from"]: e for e in model_block.get("consolidations", [])}
model_pruned = {e["name"]: e for e in model_block.get("prunings", [])}
declared = absorbed_declarations or {}
consolidated: List[Dict[str, Any]] = []
pruned: List[Dict[str, Any]] = []
consolidated: list[dict[str, Any]] = []
pruned: list[dict[str, Any]] = []
for name in removed:
mc, mp, hc, dec = model_cons.get(name), model_pruned.get(name), heur_cons.get(name), declared.get(name)
mc_reason = (mc.get("reason") or "") if mc else ""
@@ -626,14 +626,14 @@ def _reconcile_classification(
class _RunDiff(NamedTuple):
after_names: Set[str]
removed: List[str]
added: List[str]
consolidated: List[Dict[str, Any]]
pruned: List[Dict[str, Any]]
after_names: set[str]
removed: list[str]
added: list[str]
consolidated: list[dict[str, Any]]
pruned: list[dict[str, Any]]
def _diff_and_classify(before_names: Set[str], after_names: Set[str], tool_calls: List[Dict[str, Any]], model_final: str) -> _RunDiff:
def _diff_and_classify(before_names: set[str], after_names: set[str], tool_calls: list[dict[str, Any]], model_final: str) -> _RunDiff:
"""Diff the before/after skill sets and classify every removal: the model's YAML block carries intent + rationale,
the tool-call heuristic audits for hallucinated umbrellas/omissions, per-delete ``absorbed_into`` beats both."""
removed, added = sorted(before_names - after_names), sorted(after_names - before_names)
@@ -646,11 +646,11 @@ def _diff_and_classify(before_names: Set[str], after_names: Set[str], tool_calls
return _RunDiff(after_names, removed, added, classification["consolidated"], classification["pruned"])
def _by_name(report: List[Dict[str, Any]]) -> Dict[Any, Dict[str, Any]]:
def _by_name(report: list[dict[str, Any]]) -> dict[Any, dict[str, Any]]:
return {r.get("name"): r for r in report if isinstance(r, dict)}
def _build_rename_summary(*, before_names: Set[str], after_report: List[Dict[str, Any]], tool_calls: List[Dict[str, Any]], model_final: str) -> str:
def _build_rename_summary(*, before_names: set[str], after_report: list[dict[str, Any]], tool_calls: list[dict[str, Any]], model_final: str) -> str:
"""The "where did my skills go?" lines appended to the user-visible ``final_summary``; "" when nothing was archived.
Capped at 10 entries so a big consolidation doesn't flood agent.log (full list is in REPORT.md); the pin hint
appears only when a consolidation produced an umbrella."""
@@ -672,7 +672,7 @@ def _build_rename_summary(*, before_names: Set[str], after_report: List[Dict[str
return "\n".join(lines)
def _rewrite_cron_refs(consolidated: List[Dict[str, Any]], pruned: List[Dict[str, Any]]) -> Dict[str, Any]:
def _rewrite_cron_refs(consolidated: list[dict[str, Any]], pruned: list[dict[str, Any]]) -> dict[str, Any]:
"""Point cron jobs at the umbrella when the curator consolidated a skill they list — otherwise the scheduler fails to
load it and the job runs without its instructions. Best-effort: a cron-module issue never breaks the curator."""
try:
@@ -697,8 +697,8 @@ def _write_file(path: Path, label: str, render: Any) -> None:
def _write_run_report(
*, started_at: datetime, elapsed_seconds: float, auto_counts: Dict[str, int], auto_summary: str,
before_report: List[Dict[str, Any]], before_names: Set[str], after_report: List[Dict[str, Any]], llm_meta: Dict[str, Any],
*, started_at: datetime, elapsed_seconds: float, auto_counts: dict[str, int], auto_summary: str,
before_report: list[dict[str, Any]], before_names: set[str], after_report: list[dict[str, Any]], llm_meta: dict[str, Any],
) -> Optional[Path]:
"""Write run.json + REPORT.md under logs/curator/{YYYYMMDD-HHMMSS}[-N]/ (N disambiguates a crash-rerun in the same
second). Returns the report dir, or None if it couldn't be created (reporting is best-effort)."""
@@ -717,7 +717,7 @@ def _write_run_report(
diff = _diff_and_classify(before_names, set(after_by_name), tool_calls, llm_meta.get("final", "") or "")
states = ((n, (before_by_name.get(n) or {}).get("state"), (after_by_name.get(n) or {}).get("state")) for n in sorted(diff.after_names & before_names))
transitions = [{"name": n, "from": b, "to": a} for n, b, a in states if b and a and b != a]
tc_counts: Dict[str, int] = dict(Counter(tc.get("name", "unknown") for tc in tool_calls))
tc_counts: dict[str, int] = dict(Counter(tc.get("name", "unknown") for tc in tool_calls))
cron_rewrites = _rewrite_cron_refs(diff.consolidated, diff.pruned)
jobs_updated = int(cron_rewrites.get("jobs_updated", 0))
payload = {
@@ -741,11 +741,11 @@ def _write_run_report(
return run_dir
def _reason_suffix(entry: Dict[str, Any]) -> str:
def _reason_suffix(entry: dict[str, Any]) -> str:
return f" — {reason}" if (reason := (entry.get("reason") or "").strip()) else ""
def _consolidated_lines(entry: Dict[str, Any]) -> List[str]:
def _consolidated_lines(entry: dict[str, Any]) -> list[str]:
line = f"- `{entry.get('name', '?')}` → merged into `{entry.get('into', '?')}`" + _reason_suffix(entry)
source = entry.get("source", "")
if source and source.startswith("tool-call audit"):
@@ -755,12 +755,12 @@ def _consolidated_lines(entry: Dict[str, Any]) -> List[str]:
if entry.get("model_claimed_into") else [])
def _pruned_lines(entry: Any) -> List[str]:
def _pruned_lines(entry: Any) -> list[str]:
# Reconciler entries are dicts {name, source, reason}; tolerate bare strings (older format).
return [f"- `{entry.get('name', '?')}`" + _reason_suffix(entry) if isinstance(entry, dict) else f"- `{entry}`"]
def _cron_rewrite_lines(entry: Dict[str, Any]) -> List[str]:
def _cron_rewrite_lines(entry: dict[str, Any]) -> list[str]:
job_name = entry.get("job_name") or entry.get("job_id") or "?"
head = f"- `{job_name}`: `{', '.join(entry.get('before') or [])}` → `{', '.join(entry.get('after') or []) or '(none)'}`"
return ([head] + [f" - `{old}` → `{new}` (consolidated)" for old, new in (entry.get("mapped") or {}).items()]
@@ -787,7 +787,7 @@ _REPORT_SECTIONS = (
)
def _render_report_markdown(p: Dict[str, Any]) -> str:
def _render_report_markdown(p: dict[str, Any]) -> str:
"""Render the human-readable REPORT.md."""
mins, secs = divmod(int(p.get("duration_seconds", 0) or 0), 60)
dur_label = f"{mins}m {secs}s" if mins else f"{secs}s"
@@ -861,7 +861,7 @@ def _render_candidate_list() -> str:
])
def _llm_meta(summary: str, error: Optional[str] = None) -> Dict[str, Any]:
def _llm_meta(summary: str, error: Optional[str] = None) -> dict[str, Any]:
"""Structured result of an LLM pass that did not run (skipped or failed)."""
return {"final": "", "summary": summary, "model": "", "provider": "", "tool_calls": [], "error": error}
@@ -872,13 +872,13 @@ def _notify(on_summary: Optional[Callable[[str], None]], message: str) -> None:
on_summary(message)
def _safe_curated_report() -> List[Dict[str, Any]]:
def _safe_curated_report() -> list[dict[str, Any]]:
with contextlib.suppress(Exception):
return skill_usage.curated_report()
return []
def _consolidation_pass(prefix: str, auto_summary: str, dry_run: bool, before_names: Set[str]) -> tuple:
def _consolidation_pass(prefix: str, auto_summary: str, dry_run: bool, before_names: set[str]) -> tuple:
"""The LLM half of a run: fork (unless no candidates), then append the rename map (`old-name → umbrella`) so users
needn't dig into REPORT.md. Returns ``(final_summary, llm_meta)``; never raises."""
try:
@@ -915,7 +915,7 @@ def _consolidation_pass(prefix: str, auto_summary: str, dry_run: bool, before_na
def run_curator_review(
on_summary: Optional[Callable[[str], None]] = None, synchronous: bool = False,
dry_run: bool = False, consolidate: Optional[bool] = None, trigger: str = "manual",
) -> Dict[str, Any]:
) -> dict[str, Any]:
"""Execute a single curator review pass: (1) automatic state transitions (no LLM); (2) if *consolidate* and there are
candidates, fork an AIAgent on the review prompt; (3) update .curator_state; (4) call *on_summary*.
*synchronous* runs the LLM review in the calling thread (default: daemon thread). *consolidate* ``None`` reads
@@ -1005,7 +1005,7 @@ def run_curator_review(
def _record_run_metric(trigger, dry_run, counts, before_report, after_report, llm_meta, hermes_home) -> None:
from hermes_cli.observability.shared_metrics_loop import record_curator_run
def what_changed() -> Dict[str, int]:
def what_changed() -> dict[str, int]:
before, after = _by_name(before_report), _by_name(after_report)
diff = _diff_and_classify(set(before), set(after), llm_meta.get("tool_calls") or [], llm_meta.get("final") or "")
patched = sum(int(after[n].get("patch_count") or 0) > int(before[n].get("patch_count") or 0)
@@ -1023,10 +1023,10 @@ class _ReviewRuntimeBinding(NamedTuple):
model: str
explicit_api_key: Optional[str]
explicit_base_url: Optional[str]
request_overrides: Dict[str, Any]
request_overrides: dict[str, Any]
def _merge_request_overrides(runtime_overrides: Any, slot_extra_body: Any) -> Dict[str, Any]:
def _merge_request_overrides(runtime_overrides: Any, slot_extra_body: Any) -> dict[str, Any]:
"""Merge resolver metadata with task-local request body fields."""
merged = dict(runtime_overrides or {})
if isinstance(slot_extra_body, dict) and slot_extra_body:
@@ -1034,13 +1034,13 @@ def _merge_request_overrides(runtime_overrides: Any, slot_extra_body: Any) -> Di
return merged
def _resolve_review_runtime(cfg: Dict[str, Any]) -> _ReviewRuntimeBinding:
def _resolve_review_runtime(cfg: dict[str, Any]) -> _ReviewRuntimeBinding:
"""Curator is a regular auxiliary task slot (``auxiliary.curator.*``), so it rides the canonical aux-model plumbing. Precedence:
1. ``auxiliary.curator.{provider,model}`` when both are set non-auto
2. Legacy ``curator.auxiliary.{provider,model}`` (deprecated) when both set
3. Main ``model.{provider,default/model}`` pair ("auto" + "" = main chat model)
Non-empty slot ``api_key``/``base_url`` are returned as explicit overrides so ``resolve_runtime_provider`` doesn't reuse the main chat credential chain."""
def _slot(provider: str, model: str, slot: Dict[str, Any]) -> _ReviewRuntimeBinding:
def _slot(provider: str, model: str, slot: dict[str, Any]) -> _ReviewRuntimeBinding:
api_key, base_url = ((str(v).strip() or None) if v is not None else None for v in (slot.get("api_key"), slot.get("base_url")))
return _ReviewRuntimeBinding(provider, model, api_key, base_url, _merge_request_overrides({}, slot.get("extra_body")))
@@ -1061,7 +1061,7 @@ def _resolve_review_provider() -> tuple:
"""``(runtime_provider, model_name, provider_name, request_overrides)`` resolved the way the CLI does: AIAgent() without
explicit provider/model hits an auto-resolution path that fails for OAuth-only providers and pooled credentials
(HTTP 400 "No models provided"). Never raises."""
rp: Dict[str, Any] = {}
rp: dict[str, Any] = {}
overrides, provider, model_name, binding = {}, None, "", None
try:
from hermes_cli.config import load_config_readonly
@@ -1082,10 +1082,10 @@ def _resolve_review_provider() -> tuple:
return rp, model_name, provider, overrides
def _run_llm_review(prompt: str) -> Dict[str, Any]:
def _run_llm_review(prompt: str) -> dict[str, Any]:
"""Spawn an AIAgent fork on the review prompt. Returns ``final`` (untruncated response), ``summary`` (240-char cap),
``model``/``provider`` (what ran), ``tool_calls`` ([{name, arguments}], truncated) and ``error``. Never raises."""
result_meta: Dict[str, Any] = _llm_meta("")
result_meta: dict[str, Any] = _llm_meta("")
try:
from run_agent import AIAgent
except Exception as e:
@@ -1095,7 +1095,7 @@ def _run_llm_review(prompt: str) -> Dict[str, Any]:
result_meta["model"], result_meta["provider"] = model_name, provider or ""
review_agent = None
try:
agent_kwargs: Dict[str, Any] = {}
agent_kwargs: dict[str, Any] = {}
acp_command = rp.get("command")
if isinstance(acp_command, str) and acp_command:
agent_kwargs.update(acp_command=acp_command, acp_args=list(rp.get("args") or []))
@@ -1195,7 +1195,7 @@ def _release_run_claim() -> None:
_run_claim_path().unlink()
def maybe_run_curator(*, idle_for_seconds: Optional[float] = None, on_summary: Optional[Callable[[str], None]] = None) -> Optional[Dict[str, Any]]:
def maybe_run_curator(*, idle_for_seconds: Optional[float] = None, on_summary: Optional[Callable[[str], None]] = None) -> Optional[dict[str, Any]]:
"""Best-effort: run a curator pass if all gates pass. Returns the result dict if a pass was started, else None. Never raises."""
try:
# Idle gating: only enforce when the caller provided a measurement.
+25 -25
View File
@@ -74,11 +74,11 @@ def _jobs_list(parsed: Any) -> Optional[list]:
return parsed if isinstance(parsed, list) else None
def _backup_cron_jobs_into(dest: Path) -> Dict[str, Any]:
def _backup_cron_jobs_into(dest: Path) -> dict[str, Any]:
"""Copy the live ``~/.hermes/cron/jobs.json`` into ``dest`` as ``cron-jobs.json``. Never raises: a missing/unreadable
file yields ``backed_up=False`` plus a reason, and the snapshot proceeds."""
src = get_hermes_home() / "cron" / "jobs.json"
info: Dict[str, Any] = {"backed_up": False, "jobs_count": 0}
info: dict[str, Any] = {"backed_up": False, "jobs_count": 0}
if not src.exists():
return {**info, "reason": "no cron/jobs.json present"}
try:
@@ -106,7 +106,7 @@ def _utc_id(now: Optional[datetime] = None) -> str:
return s.removesuffix("+00:00").replace(":", "-") + "Z"
def _load_config() -> Dict[str, Any]:
def _load_config() -> dict[str, Any]:
return _read_config_section("curator", "backup", label="curator backup", log=logger)
@@ -130,8 +130,8 @@ def _count_skill_files(base: Path) -> int:
return 0
def _write_manifest(dest: Path, reason: str, archive_path: Path, skills_counted: int, cron_info: Dict[str, Any]) -> None:
cron_jobs: Dict[str, Any] = {"backed_up": bool(cron_info.get("backed_up", False)), "jobs_count": int(cron_info.get("jobs_count", 0))}
def _write_manifest(dest: Path, reason: str, archive_path: Path, skills_counted: int, cron_info: dict[str, Any]) -> None:
cron_jobs: dict[str, Any] = {"backed_up": bool(cron_info.get("backed_up", False)), "jobs_count": int(cron_info.get("jobs_count", 0))}
if not cron_info.get("backed_up"):
cron_jobs["reason"] = cron_info.get("reason", "not captured")
if cron_info.get("parse_warning"):
@@ -150,7 +150,7 @@ def _mkdir(path: Path, what: str, *, exist_ok: bool) -> bool:
return False
def snapshot_skills(reason: str = "manual", *, protect_ids: Optional[Set[str]] = None) -> Optional[Path]:
def snapshot_skills(reason: str = "manual", *, protect_ids: Optional[set[str]] = None) -> Optional[Path]:
"""Create a tar.gz snapshot of ``~/.hermes/skills/`` and prune old ones. Returns the snapshot dir, or None when
skipped (disabled, skills dir missing, IO error) — logged at debug so the curator never aborts a pass over a
backup failure. ``protect_ids`` survive the prune step (rollback protects its target)."""
@@ -192,12 +192,12 @@ def snapshot_skills(reason: str = "manual", *, protect_ids: Optional[Set[str]] =
return dest
def prune_old_snapshots() -> List[str]:
def prune_old_snapshots() -> list[str]:
"""Apply ``curator.backup.keep`` without taking a new snapshot (the prune-only curator pass)."""
return _prune_old(keep=get_keep())
def _prune_old(keep: int, protect: Optional[Set[str]] = None) -> List[str]:
def _prune_old(keep: int, protect: Optional[set[str]] = None) -> list[str]:
"""Delete regular snapshots beyond the newest *keep*; returns deleted ids. Ids in *protect* are never deleted —
rollback() uses this so the mandatory pre-rollback safety snapshot cannot evict the snapshot being restored.
Stale ``.rollback-staging-*`` dirs (crashed rollback) are cleaned up on every call; ``.rollback-unrestored-*`` is kept on purpose."""
@@ -210,7 +210,7 @@ def _prune_old(keep: int, protect: Optional[Set[str]] = None) -> List[str]:
entries = sorted((c for c in dirs if _ID_RE.match(c.name)), key=lambda c: c.name, reverse=True)
doomed = [(p, "prune") for p in entries[keep:] if p.name not in protect]
doomed += [(p, "clean stale staging dir") for p in dirs if p.name.startswith(_STAGING_PREFIX)]
deleted: List[str] = []
deleted: list[str] = []
for path, what in doomed:
try:
shutil.rmtree(path)
@@ -222,7 +222,7 @@ def _prune_old(keep: int, protect: Optional[Set[str]] = None) -> List[str]:
# --- List + rollback ---
def _read_manifest(snap_dir: Path) -> Dict[str, Any]:
def _read_manifest(snap_dir: Path) -> dict[str, Any]:
try:
return json.loads((snap_dir / "manifest.json").read_text(encoding="utf-8-sig"))
except (OSError, json.JSONDecodeError):
@@ -234,15 +234,15 @@ def _is_restorable(child: Path) -> bool:
return bool(child.is_dir() and _ID_RE.match(child.name) and (child / _ARCHIVE_NAME).exists())
def _restorable_snapshots() -> List[Path]:
def _restorable_snapshots() -> list[Path]:
"""Restorable snapshot dirs, newest first."""
backups = _backups_dir()
return [c for c in sorted(backups.iterdir(), reverse=True) if _is_restorable(c)] if backups.exists() else []
def list_backups() -> List[Dict[str, Any]]:
def list_backups() -> list[dict[str, Any]]:
"""All restorable snapshots (manifest dicts), newest first."""
out: List[Dict[str, Any]] = []
out: list[dict[str, Any]] = []
for child in _restorable_snapshots():
mf = {"id": child.name, "path": str(child), **_read_manifest(child)}
try:
@@ -261,12 +261,12 @@ def _resolve_backup(backup_id: Optional[str]) -> Optional[Path]:
return next(iter(_restorable_snapshots()), None)
def _restore_cron_skill_links(snapshot_dir: Path) -> Dict[str, Any]:
def _restore_cron_skill_links(snapshot_dir: Path) -> dict[str, Any]:
"""Reconcile backed-up cron skill links into the live ``cron/jobs.json``. Only ``skills``/``skill`` are restored,
and only on jobs that still exist live (by ``id``) — everything else is live state. Backup-only jobs are skipped
and reported; live-only jobs untouched. Never raises; writes through ``cron.jobs`` under the scheduler's lock so
we don't race tick()."""
report: Dict[str, Any] = {"attempted": False, "restored": [], "skipped_missing": [], "unchanged": 0, "error": None}
report: dict[str, Any] = {"attempted": False, "restored": [], "skipped_missing": [], "unchanged": 0, "error": None}
backup_file = snapshot_dir / CRON_JOBS_FILENAME
if not backup_file.exists():
return {**report, "error": f"snapshot has no {CRON_JOBS_FILENAME}"}
@@ -278,7 +278,7 @@ def _restore_cron_skill_links(snapshot_dir: Path) -> Dict[str, Any]:
return {**report, "error": "backed-up cron-jobs.json has no jobs list"}
# Backed-up skill state keyed by job id (legacy single + modern list field).
backup_by_id: Dict[str, Dict[str, Any]] = {
backup_by_id: dict[str, dict[str, Any]] = {
job["id"]: {"skills": job.get("skills"), "skill": job.get("skill"), "name": job.get("name") or job["id"]}
for job in backup_jobs if isinstance(job, dict) and isinstance(job.get("id"), str) and job.get("id")
}
@@ -320,7 +320,7 @@ def _restore_cron_skill_links(snapshot_dir: Path) -> Dict[str, Any]:
for jid, b in backup_by_id.items() if jid not in live_ids]
if changed:
save_jobs(live_jobs)
except Exception as e: # noqa: BLE001 — rollback must not die mid-restore
except Exception as e:
logger.debug("Cron skill-link restore failed: %s", e, exc_info=True)
report["error"] = f"restore failed mid-flight: {e}"
return report
@@ -333,7 +333,7 @@ def _remove_entry(entry: Path) -> None:
entry.unlink()
def _restore_excluded_subtrees(staged: Path, skills: Path) -> List[str]:
def _restore_excluded_subtrees(staged: Path, skills: Path) -> list[str]:
"""Move excluded entries (nested ``.git``/``.hub``/...) from *staged* back under *skills* after a successful extract.
Snapshots never contain these, so the staged copy of the live tree is the only source. ``.git`` may be a dir or a file
(submodule / worktree ``gitdir:`` pointer) — both are moved. Best-effort and conditional: an entry is carried only when
@@ -342,7 +342,7 @@ def _restore_excluded_subtrees(staged: Path, skills: Path) -> List[str]:
Returns entries whose move failed, so their only surviving copies stay staged for recovery."""
from tools.skill_ledger import TRANSIENT_DIRS
failed: List[str] = []
failed: list[str] = []
for dirpath, dirnames, filenames in os.walk(staged):
carried = [Path(dirpath) / n for n in filenames if n in _EXCLUDE_TOP_LEVEL]
carried += [Path(dirpath) / n for n in dirnames if n in _EXCLUDE_TOP_LEVEL or n in TRANSIENT_DIRS]
@@ -358,7 +358,7 @@ def _restore_excluded_subtrees(staged: Path, skills: Path) -> List[str]:
return failed
def _retain_staging(staged: Path, unrestored: List[str], reason: str) -> Tuple[bool, str, None]:
def _retain_staging(staged: Path, unrestored: list[str], reason: str) -> tuple[bool, str, None]:
"""Keep a staging dir that still holds the only copy of *unrestored* entries and build rollback()'s failure result.
The dir is renamed out of the prunable prefix (unique suffix on a same-second collision). If the rename fails, the
original path is reported so recovery never raises out of rollback()."""
@@ -375,11 +375,11 @@ def _retain_staging(staged: Path, unrestored: List[str], reason: str) -> Tuple[b
return (False, f"{reason} - could not restore {', '.join(sorted(unrestored))}; staged copies kept at {kept}{note}", None)
def _unstage(moved: List[Tuple[Path, Path]]) -> List[str]:
def _unstage(moved: list[tuple[Path, Path]]) -> list[str]:
"""Move staged entries back to their original paths; returns names that could not be restored. ``shutil.move``
moves *into* an existing destination dir, so partial-extract debris would bury the real skill
(``skills/foo/foo/``) — clear each original path first. The staged copy is authoritative."""
failed: List[str] = []
failed: list[str] = []
for orig, dest in moved:
try:
_remove_entry(orig)
@@ -389,7 +389,7 @@ def _unstage(moved: List[Tuple[Path, Path]]) -> List[str]:
return failed
def _cron_summary(cron_report: Dict[str, Any]) -> Optional[str]:
def _cron_summary(cron_report: dict[str, Any]) -> Optional[str]:
if not cron_report.get("attempted"):
return None
if cron_report.get("error"):
@@ -403,7 +403,7 @@ def _cron_summary(cron_report: Dict[str, Any]) -> Optional[str]:
return "cron links: " + ", ".join(parts) if parts else None
def rollback(backup_id: Optional[str] = None) -> Tuple[bool, str, Optional[Path]]:
def rollback(backup_id: Optional[str] = None) -> tuple[bool, str, Optional[Path]]:
"""Restore ``~/.hermes/skills/`` from a snapshot (explicit id or newest): safety-snapshot the CURRENT tree; stage
current top-level entries; extract; on failure move staged entries back. Returns ``(ok, message, snapshot_path)``."""
target = _resolve_backup(backup_id)
@@ -433,7 +433,7 @@ def rollback(backup_id: Optional[str] = None) -> Tuple[bool, str, Optional[Path]
except OSError as e:
return (False, f"failed to create staging dir: {e}", None)
moved: List[Tuple[Path, Path]] = []
moved: list[tuple[Path, Path]] = []
try:
for entry in list(skills.iterdir()):
if entry.name not in _EXCLUDE_TOP_LEVEL:
+2 -2
View File
@@ -546,7 +546,7 @@ def tool_labels_for_call(tool_name: str, args: dict | None) -> list:
try:
from tools.tool_labels import labels_for_call
labels = labels_for_call(tool_name, args or {})
except Exception as exc: # noqa: BLE001 — display must never abort a turn
except Exception as exc:
logger.debug("bridge labels failed for %s: %s", tool_name, exc)
return []
skin = _get_skin()
@@ -1247,7 +1247,7 @@ def get_cute_tool_message(tool_name: str, args: dict, duration: float, result: s
"""Render a completion label without letting cosmetic failures escape."""
try:
return _get_cute_tool_message(tool_name, args, duration, result=result)
except Exception as exc: # noqa: BLE001 — display must never abort a turn
except Exception as exc:
logger.debug("Tool completion label failed for %s: %s", tool_name, exc)
safe_name = tool_name[:9] if isinstance(tool_name, str) and tool_name else t("display.cute.fallback_tool_name")
safe_duration = f"{duration:.1f}s" if isinstance(duration, (int, float)) else t("display.cute.fallback_done")
+7 -7
View File
@@ -81,7 +81,7 @@ class EmptyAttempt:
return (self.model, self.provider, self.finish_reason)
def resolve_guard_settings(section: Any) -> Tuple[bool, Decimal]:
def resolve_guard_settings(section: Any) -> tuple[bool, Decimal]:
"""Resolve ``agent.empty_response_guard`` into (enabled, threshold); malformed input → schema defaults."""
if not isinstance(section, dict):
return (DEFAULT_GUARD_ENABLED, DEFAULT_COST_THRESHOLD_USD)
@@ -99,7 +99,7 @@ def resolve_guard_settings(section: Any) -> Tuple[bool, Decimal]:
candidate = Decimal(str(threshold_raw))
if candidate > 0:
threshold = candidate
except Exception: # noqa: BLE001 — malformed config must not break init
except Exception:
logger.debug("empty-guard: invalid cost_threshold_usd %r, using default", threshold_raw)
return (enabled, threshold)
@@ -115,7 +115,7 @@ def _cost_threshold_usd(agent: Any) -> Decimal:
return value if isinstance(value, Decimal) and value > 0 else DEFAULT_COST_THRESHOLD_USD
def _attempts(agent: Any) -> List[EmptyAttempt]:
def _attempts(agent: Any) -> list[EmptyAttempt]:
attempts = getattr(agent, _ATTEMPTS_ATTR, None)
if attempts is None:
attempts = []
@@ -132,7 +132,7 @@ def _normalized_usage(agent: Any, response: Any, what: str) -> Any:
from agent.usage_pricing import normalize_usage
return normalize_usage(raw_usage, provider=getattr(agent, "provider", None),
api_mode=getattr(agent, "api_mode", None))
except Exception: # noqa: BLE001 — pricing must never break the loop
except Exception:
logger.debug("empty-guard: %s failed", what, exc_info=True)
return None
@@ -148,7 +148,7 @@ def _estimate_attempt_cost(agent: Any, response: Any) -> Optional[Decimal]:
getattr(agent, "model", "") or "", canonical, provider=getattr(agent, "provider", None),
base_url=getattr(agent, "base_url", None), api_key=getattr(agent, "api_key", None),
)
except Exception: # noqa: BLE001 — pricing must never break the loop
except Exception:
logger.debug("empty-guard: cost estimation failed", exc_info=True)
return None
return getattr(result, "amount_usd", None)
@@ -184,7 +184,7 @@ def record_empty_attempt(
attempts = _attempts(agent)
if getattr(agent, "_empty_content_retries", 0) == 0:
attempts.clear()
setattr(agent, _STREAK_COST_ATTR, Decimal("0"))
setattr(agent, _STREAK_COST_ATTR, Decimal(0))
usage_present, zero_output = _zero_output(agent, response)
attempts.append(
@@ -200,7 +200,7 @@ def record_empty_attempt(
cost = _estimate_attempt_cost(agent, response)
if cost is not None and cost > 0:
prior = getattr(agent, _STREAK_COST_ATTR, Decimal("0")) or Decimal("0")
prior = getattr(agent, _STREAK_COST_ATTR, Decimal(0)) or Decimal(0)
setattr(agent, _STREAK_COST_ATTR, prior + cost)
+7 -7
View File
@@ -75,7 +75,7 @@ class ClassifiedError:
provider: Optional[str] = None
model: Optional[str] = None
message: str = ""
error_context: Dict[str, Any] = field(default_factory=dict)
error_context: dict[str, Any] = field(default_factory=dict)
# Recovery hints — the retry loop checks these instead of re-classifying.
retryable: bool = True
@@ -326,7 +326,7 @@ _REQUEST_VALIDATION_PATTERNS = (
# A rejection from any other host means the provider's gateway injected the
# field itself: a server-side flake, not our request shape. prompt_cache_retention
# is only sent for api.meta.ai / bedrock-mantle (agent/transports/codex.py).
_SERVER_INJECTED_PARAM_SENDERS: Dict[str, tuple] = {
_SERVER_INJECTED_PARAM_SENDERS: dict[str, tuple] = {
"prompt_cache_retention": ("meta", "muse", "msl", "model-api", "bedrock", "mantle"),
}
_PARAM_REJECTION_WORDS = ("not supported", "unsupported", "unknown", "unrecognized")
@@ -449,7 +449,7 @@ _UPSTREAM_BLOCKED_PATTERNS = (
# ordered ``(patterns, verdict)`` pairs matched first-hit; ``verdict`` may be
# a callable of the error message.
Verdict = Dict[str, Any]
Verdict = dict[str, Any]
def _v(reason: FailoverReason, **hints: Any) -> Verdict:
@@ -571,7 +571,7 @@ def is_reasoning_field_rejection(error_msg: str) -> bool:
def _billing_hints(error_msg: str) -> Verdict:
"""Billing verdict carrying the #82154 ambiguity marker when applicable."""
ctx: Dict[str, Any] = {}
ctx: dict[str, Any] = {}
if any(p in error_msg for p in _UNVERIFIED_BILLING_PATTERNS):
ctx = {"billing_unverified": True, "possible_content_filter": True}
return {**_V_BILLING, "error_context": ctx}
@@ -636,7 +636,7 @@ _MESSAGE_TAIL_RULES = (
# Structured error code → verdict. The error-code rate_limit verdict rotates
# but does not set should_fallback (unlike the message/status paths).
_ERROR_CODE_VERDICTS: Dict[str, Verdict] = {
_ERROR_CODE_VERDICTS: dict[str, Verdict] = {
**dict.fromkeys(("resource_exhausted", "throttled", "rate_limit_exceeded"),
_v(_R.rate_limit, should_rotate_credential=True)),
**dict.fromkeys(_BILLING_ERROR_CODES, _V_BILLING),
@@ -653,7 +653,7 @@ _ERROR_CODE_VERDICTS: Dict[str, Verdict] = {
# to the family key before lookup.
_PROVIDER_CODE_FAMILIES = {"openai-codex": "openai", "google": "gemini", "google-gemini": "gemini",
"google-ai-studio": "gemini", "vertex": "gemini", "google-vertex": "gemini"}
_PROVIDER_CODE_VERDICTS: Dict[str, Dict[str, Verdict]] = {
_PROVIDER_CODE_VERDICTS: dict[str, dict[str, Verdict]] = {
"openai": {"server_error": _V_SERVER_ERROR},
"gemini": {"unavailable": _V_OVERLOADED, "deadline_exceeded": _V_TIMEOUT, "internal": _V_SERVER_ERROR},
"anthropic": {"api_error": _V_SERVER_ERROR, "rate_limit_error": _V_RATE_LIMIT},
@@ -1222,7 +1222,7 @@ def _classify_image_tool_422(c: _Ctx) -> Verdict:
# check, then the client-error abort path (fallback first) is correct. 408 is
# retry-safe (RFC 9110 §15.5.9; proxies emit it when generation outruns the
# read window). Unlisted 4xx → format_error, 5xx → server_error.
_STATUS_HANDLERS: Dict[int, Callable[[_Ctx], Verdict]] = {
_STATUS_HANDLERS: dict[int, Callable[[_Ctx], Verdict]] = {
400: _classify_400, 401: lambda c: _V_AUTH_ROTATE, 402: lambda c: _classify_402(c.msg, dict),
403: _status_403, 404: _status_404, 408: lambda c: _V_TIMEOUT, 413: lambda c: _V_PAYLOAD_TOO_LARGE,
422: lambda c: _classify_image_tool_422(c),
+1 -1
View File
@@ -423,7 +423,7 @@ def raise_if_read_blocked(path: str) -> None:
"""
try:
blocked = get_read_block_error(path)
except Exception: # noqa: BLE001 - guard must never break local-file loading
except Exception:
return
if blocked:
raise ValueError(blocked)
+54 -54
View File
@@ -253,7 +253,7 @@ class GeminiAPIError(Exception):
"""Error shape compatible with Hermes retry/error classification."""
def __init__(self, message: str, *, code: str = "gemini_api_error", status_code: Optional[int] = None,
response: Optional[httpx.Response] = None, retry_after: Optional[float] = None, details: Optional[Dict[str, Any]] = None):
response: Optional[httpx.Response] = None, retry_after: Optional[float] = None, details: Optional[dict[str, Any]] = None):
super().__init__(message)
self.code, self.status_code, self.response = code, status_code, response
self.retry_after, self.details = retry_after, details or {}
@@ -273,7 +273,7 @@ def _coerce_content_to_text(content: Any) -> str:
return "" if content is None else str(content)
def _inline_data_part(item: Dict[str, Any]) -> Optional[Dict[str, Any]]:
def _inline_data_part(item: dict[str, Any]) -> Optional[dict[str, Any]]:
"""``inlineData`` part for an ``image_url`` item carrying a ``data:`` URL; None otherwise."""
url = (item.get("image_url") or {}).get("url") or ""
if item.get("type") != "image_url" or not isinstance(url, str) or not url.startswith("data:"):
@@ -286,21 +286,21 @@ def _inline_data_part(item: Dict[str, Any]) -> Optional[Dict[str, Any]]:
return None
def _multimodal_part(item: Any) -> Optional[Dict[str, Any]]:
def _multimodal_part(item: Any) -> Optional[dict[str, Any]]:
text = _text_of(item)
if text or isinstance(item, str):
return {"text": text}
return _inline_data_part(item) if isinstance(item, dict) else None
def _extract_multimodal_parts(content: Any) -> List[Dict[str, Any]]:
def _extract_multimodal_parts(content: Any) -> list[dict[str, Any]]:
if isinstance(content, list):
return [p for p in map(_multimodal_part, content) if p]
text = _coerce_content_to_text(content)
return [{"text": text}] if text else []
def _tool_call_extra_signature(tool_call: Dict[str, Any]) -> Optional[str]:
def _tool_call_extra_signature(tool_call: dict[str, Any]) -> Optional[str]:
"""Replayed Gemini thoughtSignature from ``extra_content`` (``google.thought_signature`` or flat)."""
extra = tool_call.get("extra_content") or {}
sig = (extra.get("google") or extra.get("thought_signature")) if isinstance(extra, dict) else None
@@ -309,18 +309,18 @@ def _tool_call_extra_signature(tool_call: Dict[str, Any]) -> Optional[str]:
return sig if isinstance(sig, str) and sig else None
def _tool_call_id(tool_call: Dict[str, Any]) -> str:
def _tool_call_id(tool_call: dict[str, Any]) -> str:
return str(tool_call.get("id") or tool_call.get("call_id") or "")
def _translate_tool_call_to_gemini(tool_call: Dict[str, Any], include_ids: bool = False) -> Dict[str, Any]:
def _translate_tool_call_to_gemini(tool_call: dict[str, Any], include_ids: bool = False) -> dict[str, Any]:
fn = tool_call.get("function") or {}
args_raw = fn.get("arguments", "")
try:
args = json.loads(args_raw) if isinstance(args_raw, str) and args_raw else {}
except json.JSONDecodeError:
args = {"_raw": args_raw}
call: Dict[str, Any] = {"name": str(fn.get("name") or ""), "args": args if isinstance(args, dict) else {"_value": args}}
call: dict[str, Any] = {"name": str(fn.get("name") or ""), "args": args if isinstance(args, dict) else {"_value": args}}
if include_ids and (call_id := _tool_call_id(tool_call)):
call["id"] = call_id
return {"functionCall": call, "thoughtSignature": _tool_call_extra_signature(tool_call) or _SKIP_SIGNATURE}
@@ -337,8 +337,8 @@ def _looks_like_json_schema(node: Any) -> bool:
def _translate_tool_result_to_gemini(
message: Dict[str, Any], tool_name_by_call_id: Optional[Dict[str, str]] = None, include_ids: bool = False, *, is_gemini3: bool = False,
) -> Dict[str, Any]:
message: dict[str, Any], tool_name_by_call_id: Optional[dict[str, str]] = None, include_ids: bool = False, *, is_gemini3: bool = False,
) -> dict[str, Any]:
tool_call_id = str(message.get("tool_call_id") or "")
# functionResponse.name must echo the matching functionCall.name, so the call-id
# mapping beats the result's own name (may be an unwrapped MCP name via `tool_call`).
@@ -354,7 +354,7 @@ def _translate_tool_result_to_gemini(
# display_name"; see vercel/ai#14369). A tool result that is itself a JSON Schema (e.g. tool_describe
# output for an MCP tool) must therefore be forwarded as opaque text, not as a structured response.
structured = isinstance(parsed, dict) and not _looks_like_json_schema(parsed)
function_response: Dict[str, Any] = {"name": name, "response": parsed if structured else {"output": content}}
function_response: dict[str, Any] = {"name": name, "response": parsed if structured else {"output": content}}
if include_ids and tool_call_id:
function_response["id"] = tool_call_id
# Gemini 3.x accepts images inside functionResponse.parts; 2.x rejects the field.
@@ -363,18 +363,18 @@ def _translate_tool_result_to_gemini(
return {"functionResponse": function_response}
def _has_function_response(content: Dict[str, Any]) -> bool:
def _has_function_response(content: dict[str, Any]) -> bool:
return any(isinstance(part, dict) and "functionResponse" in part for part in content.get("parts", []))
def _merge_alternating(contents: List[Dict[str, Any]]) -> List[Dict[str, Any]]:
def _merge_alternating(contents: list[dict[str, Any]]) -> list[dict[str, Any]]:
"""Alternation contract for generateContent: 1) adjacent same-role contents merge (else HTTP 400
"multiturn requests [must] alternate"); 2) EXCEPT never fuse a human user text turn into a preceding
user content that only carries functionResponse parts (or vice versa) — Gemini 3 accepts the fold but
reads the text as a continuation of the tool result and returns an empty candidate (parallel
functionResponse + functionResponse still merge); 3) the split pair stays API-valid via an interposed
placeholder model turn."""
merged: List[Dict[str, Any]] = []
merged: list[dict[str, Any]] = []
# Compatibility contract for native Gemini generateContent: 1) Same-role adjacent contents still merge
# in general (strict user/model alternation for ordinary text turns and parallel tool-result grouping;
# consecutive same-role contents are rejected with HTTP 400 "Please ensure that multiturn requests
@@ -400,11 +400,11 @@ def _merge_alternating(contents: List[Dict[str, Any]]) -> List[Dict[str, Any]]:
def _build_gemini_contents(
messages: List[Dict[str, Any]], include_tool_call_ids: bool = False, *, is_gemini3: bool = False
) -> tuple[List[Dict[str, Any]], Optional[Dict[str, Any]]]:
system_text_parts: List[str] = []
contents: List[Dict[str, Any]] = []
tool_name_by_call_id: Dict[str, str] = {}
messages: list[dict[str, Any]], include_tool_call_ids: bool = False, *, is_gemini3: bool = False
) -> tuple[list[dict[str, Any]], Optional[dict[str, Any]]]:
system_text_parts: list[str] = []
contents: list[dict[str, Any]] = []
tool_name_by_call_id: dict[str, str] = {}
for msg in messages:
if not isinstance(msg, dict):
continue
@@ -429,11 +429,11 @@ def _build_gemini_contents(
return _merge_alternating(contents), ({"role": "system", "parts": [{"text": joined_system}]} if joined_system else None)
def _function_declaration(tool: Any, *, json_schema: bool = False) -> Optional[Dict[str, Any]]:
def _function_declaration(tool: Any, *, json_schema: bool = False) -> Optional[dict[str, Any]]:
fn = (tool.get("function") or {}) if isinstance(tool, dict) else None
if not isinstance(fn, dict) or not (isinstance(fn.get("name"), str) and fn["name"]):
return None
decl: Dict[str, Any] = {"name": fn["name"]}
decl: dict[str, Any] = {"name": fn["name"]}
if isinstance(fn.get("description"), str) and fn["description"]:
decl["description"] = fn["description"]
if isinstance(fn.get("parameters"), dict):
@@ -446,13 +446,13 @@ def _function_declaration(tool: Any, *, json_schema: bool = False) -> Optional[D
return decl
def _translate_tools_to_gemini(tools: Any, *, json_schema: bool = False) -> List[Dict[str, Any]]:
def _translate_tools_to_gemini(tools: Any, *, json_schema: bool = False) -> list[dict[str, Any]]:
declarations = [d for d in (_function_declaration(t, json_schema=json_schema)
for t in (tools if isinstance(tools, list) else [])) if d]
return [{"functionDeclarations": declarations}] if declarations else []
def _translate_tool_choice_to_gemini(tool_choice: Any) -> Optional[Dict[str, Any]]:
def _translate_tool_choice_to_gemini(tool_choice: Any) -> Optional[dict[str, Any]]:
if isinstance(tool_choice, str) and tool_choice in _TOOL_CHOICE_MODES:
return {"functionCallingConfig": {"mode": _TOOL_CHOICE_MODES[tool_choice]}}
name = (tool_choice.get("function") or {}).get("name") if isinstance(tool_choice, dict) else None
@@ -467,7 +467,7 @@ _THINKING_KEYS = (
)
def _normalize_thinking_config(config: Any) -> Optional[Dict[str, Any]]:
def _normalize_thinking_config(config: Any) -> Optional[dict[str, Any]]:
if not isinstance(config, dict):
return None
values = {key: config.get(key, config.get(alias)) for key, alias, _, _ in _THINKING_KEYS}
@@ -499,7 +499,7 @@ def _effective_gemini_max_output_tokens(max_tokens: Optional[int], thinking_conf
return requested
def _translate_response_format(response_format: Any, *, json_schema: bool = False) -> Dict[str, Any]:
def _translate_response_format(response_format: Any, *, json_schema: bool = False) -> dict[str, Any]:
"""OpenAI ``response_format`` → Gemini ``generationConfig`` JSON-output keys.
Full-JSON-Schema ``responseJsonSchema`` exists only on the generativelanguage ``v1beta``
@@ -519,17 +519,17 @@ def _translate_response_format(response_format: Any, *, json_schema: bool = Fals
def build_gemini_request(
*, messages: List[Dict[str, Any]], tools: Any = None, tool_choice: Any = None, temperature: Optional[float] = None,
*, messages: list[dict[str, Any]], tools: Any = None, tool_choice: Any = None, temperature: Optional[float] = None,
max_tokens: Optional[int] = None, top_p: Optional[float] = None, stop: Any = None, thinking_config: Any = None,
response_format: Any = None, model: str = "", tools_as_json_schema: bool = False,
) -> Dict[str, Any]:
) -> dict[str, Any]:
# Gemini 3+ both requires tool-call ids and accepts multimodal functionResponse parts.
is_gemini3 = gemini_requires_tool_call_ids(model)
contents, system_instruction = _build_gemini_contents(messages, include_tool_call_ids=is_gemini3, is_gemini3=is_gemini3)
gemini_tools = _translate_tools_to_gemini(tools, json_schema=tools_as_json_schema)
tool_config = _translate_tool_choice_to_gemini(tool_choice)
optional = (("systemInstruction", system_instruction), ("tools", gemini_tools), ("toolConfig", tool_config))
request: Dict[str, Any] = {"contents": contents, **{k: v for k, v in optional if v}}
request: dict[str, Any] = {"contents": contents, **{k: v for k, v in optional if v}}
# Key order is part of the wire format (prompt-cache parity): temperature, maxOutputTokens, topP, stop, thinking.
generation = (
("temperature", temperature), ("maxOutputTokens", _effective_gemini_max_output_tokens(max_tokens, thinking_config)),
@@ -552,29 +552,29 @@ def build_gemini_request(
# ── Gemini → OpenAI response translation ─────────────────────────────────────
def _tool_call_extra_from_part(part: Dict[str, Any]) -> Optional[Dict[str, Any]]:
def _tool_call_extra_from_part(part: dict[str, Any]) -> Optional[dict[str, Any]]:
sig = part.get("thoughtSignature")
return {"google": {"thought_signature": sig}} if isinstance(sig, str) and sig else None
def _provider_call_id(fc: Dict[str, Any]) -> Optional[str]:
def _provider_call_id(fc: dict[str, Any]) -> Optional[str]:
fc_id = fc.get("id")
return fc_id if isinstance(fc_id, str) and fc_id else None
def _new_call_id(fc: Dict[str, Any]) -> str:
def _new_call_id(fc: dict[str, Any]) -> str:
"""Echo the functionCall/delta ``id`` when present, else mint an OpenAI-style one."""
return _provider_call_id(fc) or f"call_{uuid.uuid4().hex[:12]}"
def _dump_call_args(fc: Dict[str, Any], **kwargs: Any) -> str:
def _dump_call_args(fc: dict[str, Any], **kwargs: Any) -> str:
try:
return json.dumps(fc.get("args") or {}, ensure_ascii=False, **kwargs)
except (TypeError, ValueError):
return "{}"
def _usage_from_metadata(usage_meta: Dict[str, Any]) -> SimpleNamespace:
def _usage_from_metadata(usage_meta: dict[str, Any]) -> SimpleNamespace:
"""Gemini ``usageMetadata`` → OpenAI-shaped usage.
Hidden thinking is reported separately in ``thoughtsTokenCount``:
@@ -584,7 +584,7 @@ def _usage_from_metadata(usage_meta: Dict[str, Any]) -> SimpleNamespace:
real output and ``prompt + completion != total``) and also surfaced under
``completion_tokens_details.reasoning_tokens``, where ``normalize_usage`` reads
them. Absent on non-thinking/older responses, which keeps their numbers as-is."""
count = lambda key: int(usage_meta.get(key) or 0) # noqa: E731
count = lambda key: int(usage_meta.get(key) or 0)
reasoning_tokens = count("thoughtsTokenCount")
return SimpleNamespace(
prompt_tokens=count("promptTokenCount"), completion_tokens=count("candidatesTokenCount") + reasoning_tokens,
@@ -605,26 +605,26 @@ def _tool_call_ns(name: str, arguments: str, index: int, call_id: str, extra_con
return SimpleNamespace(id=call_id, type="function", index=index, function=SimpleNamespace(name=name, arguments=arguments), **extra)
def _part_text(part: Dict[str, Any]) -> tuple[Optional[str], bool]:
def _part_text(part: dict[str, Any]) -> tuple[Optional[str], bool]:
"""``(text, is_thought)`` for a candidate part; ``(None, False)`` when it carries no text."""
text = part.get("text")
return (text, part.get("thought") is True) if isinstance(text, str) else (None, False)
def _part_function_call(part: Dict[str, Any]) -> Optional[Dict[str, Any]]:
def _part_function_call(part: dict[str, Any]) -> Optional[dict[str, Any]]:
fc = part.get("functionCall")
return fc if isinstance(fc, dict) and fc.get("name") else None
def translate_gemini_response(resp: Dict[str, Any], model: str) -> SimpleNamespace:
def translate_gemini_response(resp: dict[str, Any], model: str) -> SimpleNamespace:
candidates = resp.get("candidates") or []
cand = parts = None
if isinstance(candidates, list) and candidates:
cand = candidates[0] if isinstance(candidates[0], dict) else {}
content_obj = cand.get("content")
parts = content_obj.get("parts") if isinstance(content_obj, dict) else []
pieces: Dict[bool, List[str]] = {False: [], True: []} # is_thought → text pieces
tool_calls: List[SimpleNamespace] = []
pieces: dict[bool, list[str]] = {False: [], True: []} # is_thought → text pieces
tool_calls: list[SimpleNamespace] = []
for index, part in enumerate(parts or []):
if not isinstance(part, dict):
continue
@@ -645,7 +645,7 @@ class _GeminiStreamChunk(SimpleNamespace): ...
def _make_stream_chunk(
*, model: str, content: str = "", tool_call_delta: Optional[Dict[str, Any]] = None, finish_reason: Optional[str] = None, reasoning: str = "",
*, model: str, content: str = "", tool_call_delta: Optional[dict[str, Any]] = None, finish_reason: Optional[str] = None, reasoning: str = "",
) -> _GeminiStreamChunk:
d = tool_call_delta
tool_calls = None if d is None else [
@@ -675,7 +675,7 @@ def _parse_sse_line(line: str) -> Any:
return payload if isinstance(payload, dict) else None
def _iter_sse_events(response: httpx.Response) -> Iterator[Dict[str, Any]]:
def _iter_sse_events(response: httpx.Response) -> Iterator[dict[str, Any]]:
buffer = ""
for chunk in response.iter_text():
buffer += chunk or ""
@@ -694,8 +694,8 @@ def _iter_sse_events(response: httpx.Response) -> Iterator[Dict[str, Any]]:
yield payload
def _tool_call_slot(fc: Dict[str, Any], part: Dict[str, Any], part_index: int, args_str: str,
tool_call_indices: Dict[str, Dict[str, Any]]) -> tuple[str, Optional[Dict[str, Any]]]:
def _tool_call_slot(fc: dict[str, Any], part: dict[str, Any], part_index: int, args_str: str,
tool_call_indices: dict[str, dict[str, Any]]) -> tuple[str, Optional[dict[str, Any]]]:
"""``(key, existing slot or None)`` for a streamed functionCall.
Gemini 3 ids each tool call, so the id is the slot identity (``part_index`` and the thought
@@ -719,13 +719,13 @@ def _tool_call_slot(fc: Dict[str, Any], part: Dict[str, Any], part_index: int, a
return f"{key}#{len(tool_call_indices)}", None
def translate_stream_event(event: Dict[str, Any], model: str, tool_call_indices: Dict[str, Dict[str, Any]]) -> List[_GeminiStreamChunk]:
def translate_stream_event(event: dict[str, Any], model: str, tool_call_indices: dict[str, dict[str, Any]]) -> list[_GeminiStreamChunk]:
candidates = event.get("candidates") or []
if not candidates:
return []
cand = candidates[0] if isinstance(candidates[0], dict) else {}
parts = (cand.get("content") or {}).get("parts") or []
chunks: List[_GeminiStreamChunk] = []
chunks: list[_GeminiStreamChunk] = []
for part_index, part in enumerate(parts):
if not isinstance(part, dict):
continue
@@ -756,7 +756,7 @@ def translate_stream_event(event: Dict[str, Any], model: str, tool_call_indices:
return chunks
def _error_info(err_obj: Dict[str, Any]) -> tuple[str, Dict[str, Any]]:
def _error_info(err_obj: dict[str, Any]) -> tuple[str, dict[str, Any]]:
"""``(reason, metadata)`` from the first google.rpc.ErrorInfo detail (later ones fill gaps until reason is set)."""
reason, metadata = "", {}
details = err_obj.get("details")
@@ -767,7 +767,7 @@ def _error_info(err_obj: Dict[str, Any]) -> tuple[str, Dict[str, Any]]:
return reason, metadata
def _error_object(body_text: str) -> Dict[str, Any]:
def _error_object(body_text: str) -> dict[str, Any]:
"""The ``error`` object of a Google JSON error body, or ``{}``."""
try:
parsed = json.loads(body_text) if body_text else None
@@ -820,7 +820,7 @@ class GeminiNativeClient:
MISSING_KEY_ERROR = _MISSING_KEY_ERROR
def __init__(
self, *, api_key: str, base_url: Optional[str] = None, default_headers: Optional[Dict[str, str]] = None,
self, *, api_key: str, base_url: Optional[str] = None, default_headers: Optional[dict[str, str]] = None,
timeout: Any = None, http_client: Optional[httpx.Client] = None, **_: Any,
) -> None:
if not (api_key or "").strip():
@@ -843,10 +843,10 @@ class GeminiNativeClient:
def __exit__(self, exc_type, exc_val, exc_tb):
self.close()
def _auth_headers(self) -> Dict[str, str]:
def _auth_headers(self) -> dict[str, str]:
return {"x-goog-api-key": self.api_key}
def _headers(self) -> Dict[str, str]:
def _headers(self) -> dict[str, str]:
return {"Content-Type": "application/json", "Accept": "application/json", **self._auth_headers(),
"User-Agent": f"{_API_CLIENT} (gemini-native)", "X-Goog-Api-Client": _API_CLIENT, **self._default_headers}
@@ -859,9 +859,9 @@ class GeminiNativeClient:
return (True, None) if chunk is _END else (False, chunk)
def _create_chat_completion(
self, *, model: str = "gemini-3.7-flash", messages: Optional[List[Dict[str, Any]]] = None, stream: bool = False,
self, *, model: str = "gemini-3.7-flash", messages: Optional[list[dict[str, Any]]] = None, stream: bool = False,
tools: Any = None, tool_choice: Any = None, temperature: Optional[float] = None, max_tokens: Optional[int] = None,
top_p: Optional[float] = None, stop: Any = None, response_format: Any = None, extra_body: Optional[Dict[str, Any]] = None,
top_p: Optional[float] = None, stop: Any = None, response_format: Any = None, extra_body: Optional[dict[str, Any]] = None,
timeout: Any = None, **_: Any,
) -> Any:
extra = extra_body if isinstance(extra_body, dict) else {}
@@ -886,13 +886,13 @@ class GeminiNativeClient:
) from exc
return translate_gemini_response(payload, model=model)
def _stream_completion(self, model: str, url: str, request: Dict[str, Any], timeout: Any) -> Iterator[_GeminiStreamChunk]:
def _stream_completion(self, model: str, url: str, request: dict[str, Any], timeout: Any) -> Iterator[_GeminiStreamChunk]:
try:
headers = {**self._headers(), "Accept": "text/event-stream"}
with self._http.stream("POST", url, json=request, headers=headers, timeout=timeout) as response:
if response.status_code != 200:
raise self._http_error(response, read_streaming_error_body(response))
tool_call_indices: Dict[str, Dict[str, Any]] = {}
tool_call_indices: dict[str, dict[str, Any]] = {}
for event in _iter_sse_events(response):
yield from translate_stream_event(event, model, tool_call_indices)
except httpx.HTTPError as exc:
+9 -9
View File
@@ -41,9 +41,9 @@ def _stringify_enum_value(item: Any) -> Any:
return item if isinstance(item, str) else None
def _normalize_gemini_type_array(type_array: list, cleaned: Dict[str, Any]) -> None:
def _normalize_gemini_type_array(type_array: list, cleaned: dict[str, Any]) -> None:
"""Keep union alternatives and their branch-local structural constraints."""
derived: Dict[str, Any] = {}
derived: dict[str, Any] = {}
_normalize_type_array(type_array, derived)
if "anyOf" in derived:
constraints = {"anyOf": cleaned["anyOf"]} if "anyOf" in cleaned else {}
@@ -65,13 +65,13 @@ def _normalize_gemini_type_array(type_array: list, cleaned: Dict[str, Any]) -> N
cleaned["nullable"] = True
def sanitize_gemini_schema(schema: Any) -> Dict[str, Any]:
def sanitize_gemini_schema(schema: Any) -> dict[str, Any]:
"""Gemini-compatible copy of a tool parameter schema: keeps only the documented subset
(drops e.g. ``$schema`` / ``additionalProperties``) and recursively sanitizes nested
``properties`` / ``items`` / ``anyOf``."""
if not isinstance(schema, dict):
return {}
cleaned: Dict[str, Any] = {}
cleaned: dict[str, Any] = {}
for key, value in schema.items():
if key not in _GEMINI_SCHEMA_ALLOWED_KEYS:
continue
@@ -119,7 +119,7 @@ def sanitize_gemini_schema(schema: Any) -> Dict[str, Any]:
return cleaned
def sanitize_gemini_tool_parameters(parameters: Any) -> Dict[str, Any]:
def sanitize_gemini_tool_parameters(parameters: Any) -> dict[str, Any]:
"""Normalize tool parameters to a valid Gemini object schema."""
return sanitize_gemini_schema(parameters) or {"type": "object", "properties": {}}
@@ -132,13 +132,13 @@ def sanitize_gemini_tool_parameters(parameters: Any) -> Dict[str, Any]:
# schema goes as-is; only same-document $refs are inlined (MCP pydantic / zod emit
# them and Google rejects reference indirection) and root ``$schema`` is dropped.
_EMPTY_OBJECT_SCHEMA: Dict[str, Any] = {"type": "object", "properties": {}}
_EMPTY_OBJECT_SCHEMA: dict[str, Any] = {"type": "object", "properties": {}}
# Real tool schemas hold a handful of refs; the cap stops circular pydantic models
# from expanding forever.
_MAX_REF_EXPANSIONS = 256
def _resolve_local_ref(root: Dict[str, Any], ref: str) -> Optional[Dict[str, Any]]:
def _resolve_local_ref(root: dict[str, Any], ref: str) -> Optional[dict[str, Any]]:
"""Resolve a same-document JSON pointer (``#/$defs/Foo``) against *root*."""
if not isinstance(ref, str) or not ref.startswith("#/"):
return None
@@ -151,7 +151,7 @@ def _resolve_local_ref(root: Dict[str, Any], ref: str) -> Optional[Dict[str, Any
return node if isinstance(node, dict) else None
def _inline_refs(node: Any, root: Dict[str, Any], budget: List[int], stack: tuple = ()) -> Any:
def _inline_refs(node: Any, root: dict[str, Any], budget: list[int], stack: tuple = ()) -> Any:
"""Recursively inline same-document ``$ref`` nodes; ``ValueError`` on an unresolvable
or circular reference or an exhausted budget (the caller then keeps the original)."""
if isinstance(node, list):
@@ -176,7 +176,7 @@ def _inline_refs(node: Any, root: Dict[str, Any], budget: List[int], stack: tupl
return {**inlined, **_inline_refs(siblings, root, budget, stack)} if siblings else inlined
def prepare_gemini_tool_parameters(parameters: Any) -> Dict[str, Any]:
def prepare_gemini_tool_parameters(parameters: Any) -> dict[str, Any]:
"""Full JSON Schema for ``parametersJsonSchema``: deep-copied, root ``$schema`` dropped,
same-document ``$ref`` inlined, object root guaranteed. A schema whose references
cannot all be resolved is sent untouched so the provider names the real problem."""
+9 -9
View File
@@ -23,7 +23,7 @@ from agent.provider_base import CatalogProviderBase
logger = logging.getLogger(__name__)
VALID_ASPECT_RATIOS: Tuple[str, ...] = ("landscape", "square", "portrait")
VALID_ASPECT_RATIOS: tuple[str, ...] = ("landscape", "square", "portrait")
DEFAULT_ASPECT_RATIO = "landscape"
@@ -32,7 +32,7 @@ class ImageGenProvider(CatalogProviderBase):
:attr:`name` and :meth:`generate`; ``list_models`` entries may add
``speed`` / ``strengths`` / ``price`` for the picker."""
def capabilities(self) -> Dict[str, Any]:
def capabilities(self) -> dict[str, Any]:
"""``modalities`` (``"text"`` and/or ``"image"``) and ``max_reference_images``; optionally
``supports_upscale`` (bool) and ``creative_controls`` (names from the tool's creative-control
vocabulary: ``creativity``, ``intensity``, ``complexity``, ``movement``). Surfaced in the
@@ -48,9 +48,9 @@ class ImageGenProvider(CatalogProviderBase):
aspect_ratio: str = DEFAULT_ASPECT_RATIO,
*,
image_url: Optional[str] = None,
reference_image_urls: Optional[List[str]] = None,
reference_image_urls: Optional[list[str]] = None,
**kwargs: Any,
) -> Dict[str, Any]:
) -> dict[str, Any]:
"""Generate an image, or edit ``image_url`` (``reference_image_urls`` are extra
style/composition refs, clamped to ``max_reference_images``); any source image
routes to the edit endpoint. Return :func:`success_response` / :func:`error_response`.
@@ -65,7 +65,7 @@ def resolve_aspect_ratio(value: Optional[str]) -> str:
return v if v in VALID_ASPECT_RATIOS else DEFAULT_ASPECT_RATIO
def normalize_reference_images(value: Any) -> Optional[List[str]]:
def normalize_reference_images(value: Any) -> Optional[list[str]]:
"""Coerce a str or list into a clean list of non-blank strings; ``None`` when
nothing usable remains so providers treat "no refs" as one sentinel."""
if isinstance(value, str):
@@ -109,10 +109,10 @@ def success_response(
aspect_ratio: str,
provider: str,
modality: str = "text",
extra: Optional[Dict[str, Any]] = None,
) -> Dict[str, Any]:
extra: Optional[dict[str, Any]] = None,
) -> dict[str, Any]:
"""Uniform success dict; ``extra`` keys are added without overriding standard ones."""
payload: Dict[str, Any] = {
payload: dict[str, Any] = {
"success": True, "image": image, "model": model, "prompt": prompt,
"aspect_ratio": aspect_ratio, "modality": modality, "provider": provider,
}
@@ -129,7 +129,7 @@ def error_response(
model: str = "",
prompt: str = "",
aspect_ratio: str = DEFAULT_ASPECT_RATIO,
) -> Dict[str, Any]:
) -> dict[str, Any]:
"""Build a uniform error response dict."""
return {
"success": False, "image": None, "error": error, "error_type": error_type,
+24 -24
View File
@@ -66,7 +66,7 @@ def _existing_file(candidate: str) -> Optional[str]:
return None
def extract_image_refs(text: str) -> Tuple[List[str], List[str]]:
def extract_image_refs(text: str) -> tuple[list[str], list[str]]:
"""Scan free-form text for image references → ``(local_paths, urls)``, each
ordered and deduplicated. Local paths must exist as files; URLs are not
validated (the provider fetches them). Code spans are skipped so pasted
@@ -99,7 +99,7 @@ def _coerce_capability_bool(raw: Any) -> Optional[bool]:
return _BOOL_TOKENS.get(raw.strip().lower()) if isinstance(raw, str) else None
def _dict_or_empty(raw: Any) -> Dict[str, Any]:
def _dict_or_empty(raw: Any) -> dict[str, Any]:
return raw if isinstance(raw, dict) else {}
@@ -124,7 +124,7 @@ def _model_supports_vision_override(models_cfg: Any, model: str) -> Optional[boo
return _coerce_capability_bool(per_model.get("supports_vision", per_model.get("vision")))
def _custom_provider_entries(cfg: Dict[str, Any], names: Iterable[str]) -> Iterable[Dict[str, Any]]:
def _custom_provider_entries(cfg: dict[str, Any], names: Iterable[str]) -> Iterable[dict[str, Any]]:
"""Yield legacy ``custom_providers`` entries matching ``names`` (case-insensitive);
``names`` is the outer loop so list order cannot let a persisted default shadow the live route."""
entries = _custom_provider_list(cfg)
@@ -132,14 +132,14 @@ def _custom_provider_entries(cfg: Dict[str, Any], names: Iterable[str]) -> Itera
yield from (e for e in entries if _clean_str(e.get("name")).lower() == wanted)
def _custom_provider_list(cfg: Dict[str, Any]) -> List[Dict[str, Any]]:
def _custom_provider_list(cfg: dict[str, Any]) -> list[dict[str, Any]]:
"""Dict entries of the legacy ``custom_providers`` list (empty when absent/malformed)."""
raw = cfg.get("custom_providers")
return [e for e in raw if isinstance(e, dict)] if isinstance(raw, list) else []
def _supports_vision_override(
cfg: Optional[Dict[str, Any]],
cfg: Optional[dict[str, Any]],
provider: str,
model: str,
*,
@@ -160,7 +160,7 @@ def _supports_vision_override(
if top is not None:
return top
candidates: List[str] = []
candidates: list[str] = []
for candidate in filter(None, (requested_provider, provider, _clean_str(model_cfg.get("provider")))):
candidates.append(candidate)
if candidate.startswith("custom:") and candidate[len("custom:"):]:
@@ -174,7 +174,7 @@ def _supports_vision_override(
def _resolve_inference_value(
cfg: Optional[Dict[str, Any]],
cfg: Optional[dict[str, Any]],
provider: str,
key: str,
*,
@@ -214,7 +214,7 @@ def _resolve_inference_value(
return next((v for v in (_clean_str(e.get(key)) for e in entries) if v), "")
def _resolve_inference_base_url(cfg: Optional[Dict[str, Any]], provider: str) -> str:
def _resolve_inference_base_url(cfg: Optional[dict[str, Any]], provider: str) -> str:
"""Best-effort base URL for the active inference provider; the runtime value is
only trusted when it belongs to the requested provider (or none was requested)."""
requested = _clean_str(provider).lower()
@@ -224,7 +224,7 @@ def _resolve_inference_base_url(cfg: Optional[Dict[str, Any]], provider: str) ->
)
def _resolve_inference_api_key(cfg: Optional[Dict[str, Any]], provider: str) -> str:
def _resolve_inference_api_key(cfg: Optional[dict[str, Any]], provider: str) -> str:
"""Best-effort API key, resolved like :func:`_resolve_inference_base_url` so it
matches the base URL actually probed; otherwise the local server-type probe hits
a keyed remote endpoint without Authorization and sprays 401s on every image turn.
@@ -259,7 +259,7 @@ def _coerce_mode(raw: Any) -> str:
return mode if mode in _VALID_MODES else "auto"
def _explicit_aux_vision_override(cfg: Optional[Dict[str, Any]]) -> bool:
def _explicit_aux_vision_override(cfg: Optional[dict[str, Any]]) -> bool:
"""True when the user configured a specific ``auxiliary.vision`` backend — the
de-facto image route in ``auto`` mode even when the main model has native vision.
``auto``/empty provider with no model and no base_url is not explicit."""
@@ -271,7 +271,7 @@ def _explicit_aux_vision_override(cfg: Optional[Dict[str, Any]]) -> bool:
)
def _probe_managed_runtime(provider: str, model: str, cfg: Optional[Dict[str, Any]]) -> Optional[bool]:
def _probe_managed_runtime(provider: str, model: str, cfg: Optional[dict[str, Any]]) -> Optional[bool]:
"""Managed local runtime verdict: the server receiving the image is the authority
on whether it can see (its /props reports modalities). Cloud catalogs have never
heard of a local GGUF, so without this every local model reads as text-only and
@@ -282,7 +282,7 @@ def _probe_managed_runtime(provider: str, model: str, cfg: Optional[Dict[str, An
return managed_model_supports_vision(model) if managed else None
def _probe_models_dev(provider: str, model: str, cfg: Optional[Dict[str, Any]]) -> Optional[bool]:
def _probe_models_dev(provider: str, model: str, cfg: Optional[dict[str, Any]]) -> Optional[bool]:
"""models.dev catalog verdict. ``allow_network=True`` on purpose: this runs only
when an image needs routing, and the text-only-main guard depends on catalog
data — a cold cache returning "unknown" would reintroduce attempting the call.
@@ -305,7 +305,7 @@ def _probe_models_dev(provider: str, model: str, cfg: Optional[Dict[str, Any]])
return None if caps is None else caps.supports_vision
def _probe_ollama(provider: str, model: str, cfg: Optional[Dict[str, Any]]) -> Optional[bool]:
def _probe_ollama(provider: str, model: str, cfg: Optional[dict[str, Any]]) -> Optional[bool]:
"""Ollama ``/api/show`` verdict for local endpoints (see :func:`_should_probe_ollama_vision`)."""
base_url = _resolve_inference_base_url(cfg, provider)
if not base_url and _clean_str(provider).lower() == "ollama":
@@ -320,7 +320,7 @@ def _probe_ollama(provider: str, model: str, cfg: Optional[Dict[str, Any]]) -> O
# Capability probes after the config override, in priority order; each returns
# True/False or None (unknown → next probe). Exceptions are logged and treated as None.
_VISION_PROBES: Tuple[Tuple[str, Callable[..., Optional[bool]]], ...] = (
_VISION_PROBES: tuple[tuple[str, Callable[..., Optional[bool]]], ...] = (
("managed-runtime caps lookup", _probe_managed_runtime),
("caps lookup", _probe_models_dev),
("ollama vision probe", _probe_ollama),
@@ -330,7 +330,7 @@ _VISION_PROBES: Tuple[Tuple[str, Callable[..., Optional[bool]]], ...] = (
def _lookup_supports_vision(
provider: str,
model: str,
cfg: Optional[Dict[str, Any]] = None,
cfg: Optional[dict[str, Any]] = None,
*,
requested_provider: str = "",
) -> Optional[bool]:
@@ -375,7 +375,7 @@ def _lookup_supports_vision(
def decide_image_input_mode(
provider: str,
model: str,
cfg: Optional[Dict[str, Any]],
cfg: Optional[dict[str, Any]],
*,
requested_provider: str = "",
) -> str:
@@ -404,7 +404,7 @@ _FTYP_BRANDS = {
**dict.fromkeys((b"avif", b"avis"), "image/avif"),
**dict.fromkeys((b"heic", b"heix", b"hevc", b"hevx", b"mif1", b"msf1", b"heim", b"heis"), "image/heic"),
}
_MAGIC: Tuple[Tuple[Tuple[Tuple[int, bytes], ...], str], ...] = (
_MAGIC: tuple[tuple[tuple[tuple[int, bytes], ...], str], ...] = (
(((0, b"\x89PNG\r\n\x1a\n"),), "image/png"),
(((0, b"\xff\xd8\xff"),), "image/jpeg"),
(((0, b"GIF87a"),), "image/gif"), (((0, b"GIF89a"),), "image/gif"),
@@ -453,7 +453,7 @@ def _transcode_to_png(raw: bytes) -> Optional[bytes]:
pillow_heif.register_heif_opener()
with suppress(Exception):
import pillow_avif # type: ignore # noqa: F401 -- registers AVIF on import
import pillow_avif # type: ignore
try:
with Image.open(BytesIO(raw)) as im:
# Normalise exotic modes to RGBA so PNG can serialise and transparency survives.
@@ -493,7 +493,7 @@ def _accepted_mimes() -> frozenset:
if is_managed_provider(str(_runtime_main_value("provider") or ""), str(_runtime_main_value("base_url") or "")):
return ACCEPTED_IMAGE_MIMES
except Exception: # noqa: BLE001 — best-effort narrowing only
except Exception:
pass
return _UNIVERSALLY_SUPPORTED_MIMES
@@ -532,9 +532,9 @@ def _file_to_data_url(path: Path) -> Optional[str]:
def build_native_content_parts(
user_text: str,
image_paths: List[str],
image_urls: Optional[List[str]] = None,
) -> Tuple[List[Dict[str, Any]], List[str]]:
image_paths: list[str],
image_urls: Optional[list[str]] = None,
) -> tuple[list[dict[str, Any]], list[str]]:
"""Build an OpenAI-style ``content`` list for a user turn.
Local paths become base64 ``data:`` URLs; remote URLs pass through verbatim.
@@ -544,8 +544,8 @@ def build_native_content_parts(
``Runner._enrich_message_with_vision``. Returns ``(content_parts, skipped)``;
``skipped`` holds unreadable local paths (URLs are never skipped).
"""
skipped: List[str] = []
attached: List[Tuple[str, str]] = [] # (url, hint)
skipped: list[str] = []
attached: list[tuple[str, str]] = [] # (url, hint)
for raw_path in image_paths:
p = Path(raw_path)
data_url = _file_to_data_url(p) if p.exists() and p.is_file() else None
+6 -6
View File
@@ -26,11 +26,11 @@ _MIN_PLAUSIBLE, _MAX_PLAUSIBLE = 64, 32_768
_EMA_ALPHA = 0.5
_image_cost_var: ContextVar[Optional[int]] = ContextVar("hermes_image_token_cost", default=None)
_LEARNED: Dict[str, int] = {}
_LEARNED: dict[str, int] = {}
_LOADED = False
# Routed profiles (multiplexed gateway) keep their own table, loaded from THEIR cache file: the
# module slot above is the launch profile's and would otherwise be persisted into every home.
_LEARNED_BY_HOME: Dict[str, Dict[str, int]] = {}
_LEARNED_BY_HOME: dict[str, dict[str, int]] = {}
def _cache_path():
@@ -45,14 +45,14 @@ def _key(model: Any, base_url: Any) -> str:
return f"{model or ''}@{base_url_hostname(base_url or '') or ''}"
def _read_cache() -> Dict[str, int]:
def _read_cache() -> dict[str, int]:
from agent.model_metadata import _load_json_dict
return {k: v for k, v in _load_json_dict(_cache_path()).items()
if isinstance(v, int) and _MIN_PLAUSIBLE <= v <= _MAX_PLAUSIBLE}
def _table() -> Dict[str, int]:
def _table() -> dict[str, int]:
"""The active profile's learned table, loaded lazily from its cache file."""
global _LOADED
from hermes_constants import get_hermes_home_override, hermes_home_key
@@ -94,13 +94,13 @@ def bind_image_token_cost(agent: Any) -> None:
_image_cost_var.set(learned_image_token_cost(getattr(agent, "model", None), getattr(agent, "base_url", None)))
def count_images(messages: List[Dict[str, Any]]) -> int:
def count_images(messages: list[dict[str, Any]]) -> int:
from agent.model_metadata import _count_image_tokens
return sum(_count_image_tokens(m, 1) for m in messages if isinstance(m, dict))
def calibrate_from_usage(agent: Any, messages: List[Dict[str, Any]], prompt_tokens: Any) -> Optional[int]:
def calibrate_from_usage(agent: Any, messages: list[dict[str, Any]], prompt_tokens: Any) -> Optional[int]:
"""Learn the per-image cost from the response that just priced ``messages``.
Requires the PREVIOUS anchor (real count of the prior request) to still match: the residual
+2 -2
View File
@@ -24,7 +24,7 @@ _NON_NAMES = frozenset({
"admin", "administrator", "default", "guest", "me", "owner", "root", "test", "user",
})
_HANDLE_CHARS = re.compile(r"[\d_@/\\]")
_SPARK_MODEL = re.compile(r"\b(dgx|spark|gb10)\b", re.I)
_SPARK_MODEL = re.compile(r"\b(dgx|spark|gb10)\b", re.IGNORECASE)
FORK_QUESTION = "Know what you'd like it to make?"
# Reading the Blender plugin's app declaration takes the network.
@@ -294,7 +294,7 @@ def _handoff(kind: str, description: str) -> dict:
"""The handoff message's parts and its two plans from ``templates/handoff.md``, the machine plan naming this
computer, so the model reads them only when it reaches the handoff."""
text = (skill_dir() / "templates" / "handoff.md").read_text(encoding="utf-8-sig")
sections = dict(re.findall(r"^## (\S+)\n\n(.*?)\n*(?=^## |\Z)", text, re.M | re.S))
sections = dict(re.findall(r"^## (\S+)\n\n(.*?)\n*(?=^## |\Z)", text, re.MULTILINE | re.DOTALL))
machine = sections["machine"].replace("<machine_kind>", kind).replace("<description>", description)
return {"message": sections["message"], "build": sections["build"], "machine": machine}
+5 -5
View File
@@ -17,7 +17,7 @@ from typing import Any, Callable, Dict, Optional, Tuple
from tools.arg_coercion import coerce_tool_args
def tool_hook_ids(agent, effective_task_id: str, tool_call_id: Optional[str]) -> Dict[str, str]:
def tool_hook_ids(agent, effective_task_id: str, tool_call_id: Optional[str]) -> dict[str, str]:
"""Identity kwargs every tool hook/middleware call carries (all coerced to ``""``)."""
return {
"task_id": effective_task_id or "",
@@ -97,10 +97,10 @@ class InlineToolContext:
InlineToolExecutor = Callable[[Any, dict, InlineToolContext], Any]
# ``(kwarg, args_key)`` → ``args.get(key)``; ``(kwarg, args_key, default)`` → ``args.get(key, default)``.
_ArgSpec = Tuple[Any, ...]
_ArgSpec = tuple[Any, ...]
def _call_tool(module: str, func: str, args: dict, arg_specs: Tuple[_ArgSpec, ...], **fixed: Any) -> Any:
def _call_tool(module: str, func: str, args: dict, arg_specs: tuple[_ArgSpec, ...], **fixed: Any) -> Any:
"""Import ``module.func`` lazily and call it with args mapped per ``arg_specs`` plus ``fixed``."""
fn = getattr(import_module(module), func)
return fn(**{spec[0]: args.get(*spec[1:]) for spec in arg_specs}, **fixed)
@@ -234,7 +234,7 @@ def _setup_mcp_shim(agent, args: dict, ctx: InlineToolContext) -> Any:
# Order is the historical if/elif order of ``execute_tool_calls_sequential``.
_RAW_INLINE_TOOL_EXECUTORS: Dict[str, InlineToolExecutor] = {
_RAW_INLINE_TOOL_EXECUTORS: dict[str, InlineToolExecutor] = {
"todo_list": _tool(
"tools.todo_tool", "todo_tool", ("todos", "todos"), ("merge", "merge", False),
store=lambda agent, ctx: agent._todo_store,
@@ -295,7 +295,7 @@ def _coerced(name: str, executor: InlineToolExecutor) -> InlineToolExecutor:
return _exec
INLINE_TOOL_EXECUTORS: Dict[str, InlineToolExecutor] = {
INLINE_TOOL_EXECUTORS: dict[str, InlineToolExecutor] = {
name: _coerced(name, executor) for name, executor in _RAW_INLINE_TOOL_EXECUTORS.items()
}
+24 -24
View File
@@ -26,7 +26,7 @@ def _fmt_est_cost(est_cost: float) -> str:
return format_cost_label(Decimal(str(est_cost)))
def _estimate_cost(session_or_model: Dict[str, Any] | str, input_tokens: int = 0, output_tokens: int = 0, *, cache_read_tokens: int = 0,
def _estimate_cost(session_or_model: dict[str, Any] | str, input_tokens: int = 0, output_tokens: int = 0, *, cache_read_tokens: int = 0,
cache_write_tokens: int = 0, provider: Optional[str] = None, base_url: Optional[str] = None) -> tuple[float, str]:
"""Estimate the USD cost for a session row or a model/token tuple."""
if isinstance(session_or_model, dict):
@@ -41,7 +41,7 @@ def _estimate_cost(session_or_model: Dict[str, Any] | str, input_tokens: int = 0
return float(result.amount_usd or 0.0), result.status
def _bar_chart(values: List[int], max_width: int = 20) -> List[str]:
def _bar_chart(values: list[int], max_width: int = 20) -> list[str]:
peak = max(values) if values else 1
return ["" for _ in values] if peak == 0 else ["█" * max(1, int(v / peak * max_width)) if v > 0 else "" for v in values]
@@ -170,7 +170,7 @@ class InsightsEngine:
sql, params = (getattr(self, base + "_WITH_SOURCE"), (cutoff, source)) if source else (getattr(self, base + "_ALL"), (cutoff,))
return self._conn.execute(sql, params).fetchall()
def generate(self, days: int = 30, source: str = None) -> Dict[str, Any]:
def generate(self, days: int = 30, source: str = None) -> dict[str, Any]:
"""Generate a complete insights report for the last ``days`` days, optionally filtered by source platform."""
cutoff = time.time() - (days * 86400)
# Drain the SessionDB's async accounting queue so counters are exact
@@ -197,7 +197,7 @@ class InsightsEngine:
"top_sessions": self._compute_top_sessions(sessions),
}
def get_usage_breakdown(self, days: int = 30, source: str = None) -> Dict[str, Any]:
def get_usage_breakdown(self, days: int = 30, source: str = None) -> dict[str, Any]:
"""Analytics-usage payload (tools + skills) without a full generate(); the
instr()-prefiltered skill query loads only skill_view/skill_manage messages."""
cutoff = time.time() - (days * 86400)
@@ -206,7 +206,7 @@ class InsightsEngine:
# ------------------------------------------------------------------ SQL
def _get_sessions(self, cutoff: float, source: str = None) -> List[Dict]:
def _get_sessions(self, cutoff: float, source: str = None) -> list[dict]:
# Coerce the two epoch columns once at load: one corrupt/TEXT cell must degrade to "unknown"
# for that session, never abort the whole report (#99959).
rows = [dict(row) for row in self._query("_GET_SESSIONS", cutoff, source)]
@@ -215,7 +215,7 @@ class InsightsEngine:
row[col] = coerce_epoch(row.get(col), session_id=row.get("id"), field=col)
return rows
def _get_tool_usage(self, cutoff: float, source: str = None) -> List[Dict]:
def _get_tool_usage(self, cutoff: float, source: str = None) -> list[dict]:
"""Tool call counts from two sources: ``tool_name`` on 'tool' rows (set
by the gateway) and ``tool_calls`` JSON on assistant rows (covers CLI,
where tool_name is not populated). The two views are reconciled PER
@@ -237,9 +237,9 @@ class InsightsEngine:
tool_counts[key[1]] += max(by_session_tool.get(key, 0), calls_by_session_tool.get(key, 0))
return [{"tool_name": name, "count": count} for name, count in tool_counts.most_common()]
def _get_skill_usage(self, cutoff: float, source: str = None) -> List[Dict]:
def _get_skill_usage(self, cutoff: float, source: str = None) -> list[dict]:
"""Extract per-skill usage from assistant tool calls."""
skill_counts: Dict[str, Dict[str, Any]] = {}
skill_counts: dict[str, dict[str, Any]] = {}
for row in self._query("_GET_SKILL_CALLS", cutoff, source):
timestamp = row["timestamp"]
for func in _iter_functions(row["tool_calls"]):
@@ -255,11 +255,11 @@ class InsightsEngine:
entry["last_used_at"] = timestamp
return list(skill_counts.values())
def _get_message_stats(self, cutoff: float, source: str = None) -> Dict:
def _get_message_stats(self, cutoff: float, source: str = None) -> dict:
rows = self._query("_GET_MESSAGE_STATS", cutoff, source)
return dict(rows[0]) if rows else {"total_messages": 0, "user_messages": 0, "assistant_messages": 0, "tool_messages": 0}
def _get_model_usage(self, cutoff: float, source: str = None) -> List[Dict]:
def _get_model_usage(self, cutoff: float, source: str = None) -> list[dict]:
"""Per-model usage rows; [] when the table is missing (older DB) so the caller falls back to the per-session aggregate."""
try:
return [dict(row) for row in self._query("_GET_MODEL_USAGE", cutoff, source)]
@@ -268,7 +268,7 @@ class InsightsEngine:
# -------------------------------------------------------------- Compute
def _compute_overview(self, sessions: List[Dict], message_stats: Dict, models: Optional[List[Dict]] = None) -> Dict:
def _compute_overview(self, sessions: list[dict], message_stats: dict, models: Optional[list[dict]] = None) -> dict:
# Per-model breakdown includes auxiliary usage rows (vision/compression/
# titles) plus reconciled residuals, while session counters carry
# main-loop usage only — sum the breakdown when available so overview
@@ -315,7 +315,7 @@ class InsightsEngine:
"included_cost_sessions": status_counts["included"],
}
def _compute_model_breakdown(self, sessions: List[Dict], cutoff: float, source: str = None) -> List[Dict]:
def _compute_model_breakdown(self, sessions: list[dict], cutoff: float, source: str = None) -> list[dict]:
"""Tokens/cost per model from session_model_usage, so a session that
switched models via ``/model`` splits across every model it used.
Sessions without per-model rows (pre-table data) fall back to their
@@ -325,10 +325,10 @@ class InsightsEngine:
model_data = defaultdict(lambda: {"sessions": set(), **dict.fromkeys(_TOKEN_KEYS, 0), "reasoning_tokens": 0, "total_tokens": 0,
"api_calls": 0, "tool_calls": 0, "cost": 0.0, "actual_cost": 0.0})
def _accumulate(model, provider, base_url, session_id, counts: Dict[str, int], *,
def _accumulate(model, provider, base_url, session_id, counts: dict[str, int], *,
stored_cost=None, actual_cost=None, cost_status=None):
model = model or "unknown"
d: Dict[str, Any] = model_data[_short_model(model)]
d: dict[str, Any] = model_data[_short_model(model)]
d["sessions"].add(session_id)
for key in _TOKEN_KEYS + ("reasoning_tokens",):
d[key] += counts[key]
@@ -345,7 +345,7 @@ class InsightsEngine:
d["has_pricing"] = has_known_pricing(model, provider or None, base_url) or d.get("has_pricing", False)
usage_totals = defaultdict(lambda: dict.fromkeys(count_keys, 0) | {"estimated_cost_usd": 0.0, "actual_cost_usd": 0.0})
for r in self._get_model_usage(cutoff, source):
totals: Dict[str, Any] = usage_totals[r["session_id"]]
totals: dict[str, Any] = usage_totals[r["session_id"]]
counts = {key: r[key] or 0 for key in count_keys}
for key in count_keys:
totals[key] += counts[key]
@@ -376,7 +376,7 @@ class InsightsEngine:
for model, data in model_data.items()]
return sorted(result, key=lambda x: (x["total_tokens"], x["sessions"]), reverse=True)
def _compute_platform_breakdown(self, sessions: List[Dict]) -> List[Dict]:
def _compute_platform_breakdown(self, sessions: list[dict]) -> list[dict]:
platform_data = defaultdict(lambda: {"sessions": 0, "messages": 0, **dict.fromkeys(_TOKEN_KEYS, 0), "total_tokens": 0, "tool_calls": 0})
for s in sessions:
d = platform_data[s.get("source") or "unknown"]
@@ -388,12 +388,12 @@ class InsightsEngine:
d["tool_calls"] += s.get("tool_call_count") or 0
return sorted(({"platform": platform, **data} for platform, data in platform_data.items()), key=lambda x: x["sessions"], reverse=True)
def _compute_tool_breakdown(self, tool_usage: List[Dict]) -> List[Dict]:
def _compute_tool_breakdown(self, tool_usage: list[dict]) -> list[dict]:
"""Ranked tool list with percentages."""
total_calls = sum(t["count"] for t in tool_usage)
return [{"tool": t["tool_name"], "count": t["count"], "percentage": (t["count"] / total_calls * 100) if total_calls else 0} for t in tool_usage]
def _compute_skill_breakdown(self, skill_usage: List[Dict]) -> Dict[str, Any]:
def _compute_skill_breakdown(self, skill_usage: list[dict]) -> dict[str, Any]:
"""Per-skill usage → summary + ranked list."""
total_skill_loads = sum(s["view_count"] for s in skill_usage)
total_skill_edits = sum(s["manage_count"] for s in skill_usage)
@@ -409,7 +409,7 @@ class InsightsEngine:
"top_skills": top_skills,
}
def _compute_activity_patterns(self, sessions: List[Dict]) -> Dict:
def _compute_activity_patterns(self, sessions: list[dict]) -> dict:
"""Activity by day of week, hour, and active-day streak."""
day_counts, hour_counts, daily_counts = Counter(), Counter(), Counter() # weekday (0=Monday), hour, "YYYY-MM-DD"
for s in sessions:
@@ -439,7 +439,7 @@ class InsightsEngine:
("Most tool calls", lambda s: s.get("tool_call_count") or 0, "{} calls"),
)
def _compute_top_sessions(self, sessions: List[Dict]) -> List[Dict]:
def _compute_top_sessions(self, sessions: list[dict]) -> list[dict]:
"""Notable sessions (longest, most messages, most tokens, most tool calls)."""
top = []
timed = [s for s in sessions if s.get("started_at") and s.get("ended_at")]
@@ -457,11 +457,11 @@ class InsightsEngine:
# ------------------------------------------------------------- Formatting
@staticmethod
def _section(title: str) -> List[str]:
def _section(title: str) -> list[str]:
return [f" {title}", " " + "─" * 56]
@staticmethod
def _cost_lines(o: Dict, templates: tuple) -> List[str]:
def _cost_lines(o: dict, templates: tuple) -> list[str]:
"""One formatted line per non-zero cost bucket (estimated, included, unknown)."""
# Cost breakdown — surface the three buckets so subscription-included and unknown-cost sessions are
# visible instead of silently collapsing to $0. See #77223.
@@ -469,7 +469,7 @@ class InsightsEngine:
values = (_fmt_est_cost(est_cost) if est_cost > 0 else "", o.get("included_cost_sessions", 0), o.get("unknown_cost_sessions", 0))
return [tpl.format(v) for tpl, v in zip(templates, values) if v]
def format_terminal(self, report: Dict) -> str:
def format_terminal(self, report: dict) -> str:
"""Format the insights report for terminal display (CLI)."""
if report.get("empty"):
src = f" (source: {report['source_filter']})" if report.get("source_filter") else ""
@@ -548,7 +548,7 @@ class InsightsEngine:
lines += [f" {ts['label']:<20} {ts['value']:<18} ({ts['date']}, {ts['session_id']})" for ts in report["top_sessions"]] + [""]
return "\n".join(lines)
def format_gateway(self, report: Dict) -> str:
def format_gateway(self, report: dict) -> str:
"""Format the insights report for gateway/messaging (shorter)."""
if report.get("empty"):
return f"No sessions found in the last {report.get('days', 30)} days."
+1 -1
View File
@@ -18,7 +18,7 @@ def preload_jiter_native_extension() -> bool:
return True
try:
importlib.import_module("jiter.jiter")
from jiter import from_json as _from_json # noqa: F401
from jiter import from_json as _from_json
except Exception as exc:
_JITER_PRELOAD_ERROR = exc
return False
+1 -2
View File
@@ -120,8 +120,7 @@ def max_recovery_attempts() -> int:
def recovery_delay_seconds(attempt: int) -> float:
"""Backoff before recovery attempt ``attempt`` (1-based); last entry repeats."""
if attempt < 1:
attempt = 1
attempt = max(attempt, 1)
index = min(attempt - 1, len(RECOVERY_DELAYS_SECONDS) - 1)
return RECOVERY_DELAYS_SECONDS[index]
+2 -2
View File
@@ -27,7 +27,7 @@ Row = list # of runs ``[text, style, alpha, hex?]``; a grid is a list of rows
def _clamp(v: float, lo: float, hi: float) -> float:
return lo if v < lo else hi if v > hi else v
return lo if v < lo else min(v, hi)
def _lerp(a: float, b: float, t: float) -> float:
@@ -191,7 +191,7 @@ def _skill_category_counts(nodes: Iterable[dict[str, Any]]) -> Counter:
# ── Timeline chart frame ─────────────────────────────────────────────────────
class _ChartBucket:
__slots__ = ("label", "ts", "nodes", "rec")
__slots__ = ("label", "nodes", "rec", "ts")
def __init__(self, label: str, ts: float):
self.label, self.ts, self.rec = label, ts, 1.0
+4 -4
View File
@@ -11,7 +11,7 @@ from __future__ import annotations
# hermes_bootstrap first (UTF-8 stdio on Windows; no-op on POSIX), like every other entry point.
try:
import hermes_bootstrap # noqa: F401
import hermes_bootstrap
except ModuleNotFoundError:
pass # partial `hermes update` — only skips the Windows UTF-8 stdio setup
@@ -22,8 +22,8 @@ from hermes_cli import _early_recovery
if _early_recovery.restore_interrupted_pull():
_early_recovery.relaunch_after_restore()
import argparse # noqa: E402
from typing import Callable, List, Optional # noqa: E402
import argparse
from typing import Callable, List, Optional
def _build_parser() -> argparse.ArgumentParser:
@@ -57,7 +57,7 @@ def _build_parser() -> argparse.ArgumentParser:
return parser
def main(argv: Optional[List[str]] = None, *, run: Optional[Callable[..., object]] = None) -> int:
def main(argv: Optional[list[str]] = None, *, run: Optional[Callable[..., object]] = None) -> int:
"""Parse ``argv`` (default ``sys.argv[1:]``) and run one query through ``run_agent.main``.
Metadata flags and a bare invocation never reach the runner; ``run`` lets
+1 -1
View File
@@ -20,7 +20,7 @@ _LM_EFFORT_ALIASES = {"off": "none", "on": "medium"}
_LM_EFFORT_CLAMP = {"max": "xhigh", "ultra": "xhigh"}
def resolve_lmstudio_effort(reasoning_config: Optional[dict], allowed_options: Optional[List[str]]) -> Optional[str]:
def resolve_lmstudio_effort(reasoning_config: Optional[dict], allowed_options: Optional[list[str]]) -> Optional[str]:
"""Return the ``reasoning_effort`` to send to LM Studio, or ``None`` = omit the
field (the user picked a level the model can't honor, so LM Studio falls back
to the model's declared default rather than a silently substituted effort).
+3 -3
View File
@@ -72,7 +72,7 @@ def release_workspace(path: str) -> int:
for svc in services:
try:
released += svc.release_workspace(path)
except Exception as e: # noqa: BLE001
except Exception as e:
logger.debug("LSP workspace release failed for %s: %s", path, e)
return released
@@ -88,7 +88,7 @@ def shutdown_service() -> None:
if svc is not None:
try:
svc.shutdown()
except Exception as e: # noqa: BLE001
except Exception as e:
logger.debug("LSP shutdown error: %s", e)
@@ -96,7 +96,7 @@ def _atexit_shutdown() -> None:
"""atexit wrapper; logs at debug since the user has already seen the final output."""
try:
shutdown_service()
except Exception as e: # noqa: BLE001
except Exception as e:
logger.debug("atexit LSP shutdown failed: %s", e)
+1 -1
View File
@@ -62,7 +62,7 @@ def _all_servers() -> list:
from hermes_cli.config import load_config_readonly
try:
lsp_cfg = load_config_readonly().get("lsp") or {}
except Exception: # noqa: BLE001 — a broken config still lists the built-ins
except Exception:
lsp_cfg = {}
return [*custom_servers(lsp_cfg.get("servers") if isinstance(lsp_cfg, dict) else None), *SERVERS]
+25 -25
View File
@@ -55,7 +55,7 @@ MAX_TRACKED_FILES = 64
_WRITE_ERRORS = (BrokenPipeError, ConnectionResetError, OSError)
_LIVE_STATES = {"starting", "running"}
_CLIENT_CAPABILITIES: Dict[str, Any] = {
_CLIENT_CAPABILITIES: dict[str, Any] = {
"window": {"workDoneProgress": True},
"workspace": {"configuration": True, "workspaceFolders": True,
"didChangeWatchedFiles": {"dynamicRegistration": True}, "diagnostics": {"refreshSupport": False}},
@@ -84,7 +84,7 @@ def file_uri(path: str) -> str:
return "file://" + quote(abs_path, safe="/:")
def _folder(root: str) -> Dict[str, str]:
def _folder(root: str) -> dict[str, str]:
"""Build an LSP ``WorkspaceFolder`` for ``root``."""
return {"name": os.path.basename(root.rstrip(os.sep)) or root, "uri": file_uri(root)}
@@ -99,7 +99,7 @@ def uri_to_path(uri: str) -> str:
return os.path.normpath(unquote(raw))
def _end_position(text: str) -> Dict[str, int]:
def _end_position(text: str) -> dict[str, int]:
"""LSP Position at the end of ``text`` (for a whole-document replace range)."""
if not text:
return {"line": 0, "character": 0}
@@ -119,8 +119,8 @@ class _DocState:
publishDiagnostics get exact tagging; others are credited with the current version at receipt."""
version: int = 0
text: str = ""
push: List[Dict[str, Any]] = field(default_factory=list)
pull: List[Dict[str, Any]] = field(default_factory=list)
push: list[dict[str, Any]] = field(default_factory=list)
pull: list[dict[str, Any]] = field(default_factory=list)
push_version: int = -1
pull_version: int = -1
seed_seen: bool = False
@@ -139,15 +139,15 @@ class LSPClient:
"""One server process + one workspace root. ``start()`` → ``open_file()`` → ``wait_for_diagnostics()`` →
``diagnostics_for()`` → ``shutdown()``."""
def __init__(self, *, server_id: str, workspace_root: str, command: List[str],
env: Optional[Dict[str, str]] = None, cwd: Optional[str] = None,
initialization_options: Optional[Dict[str, Any]] = None,
def __init__(self, *, server_id: str, workspace_root: str, command: list[str],
env: Optional[dict[str, str]] = None, cwd: Optional[str] = None,
initialization_options: Optional[dict[str, Any]] = None,
seed_diagnostics_on_first_push: bool = False) -> None:
self.server_id = server_id
self.workspace_root = workspace_root
# Roots this server serves. Single-root servers only ever hold ``workspace_root``;
# multi-root servers (pyright) grow this via ``add_workspace_folder`` instead of a second process.
self.workspace_folders: List[str] = [workspace_root]
self.workspace_folders: list[str] = [workspace_root]
self._command = list(command)
self._env = env
self._cwd = cwd or workspace_root
@@ -157,16 +157,16 @@ class LSPClient:
self._proc: Optional[asyncio.subprocess.Process] = None
self._stderr_task: Optional[asyncio.Task] = None
# Ring buffer of the most recent server stderr lines, surfaced when spawn/initialize fails.
self._stderr_tail: List[str] = []
self._stderr_tail: list[str] = []
self._exit_code: Optional[int] = None
self._reader_task: Optional[asyncio.Task] = None
self._cleanup_lock = asyncio.Lock()
self._next_id: int = 0
self._pending: Dict[int, asyncio.Future] = {}
self._pending: dict[int, asyncio.Future] = {}
# Server → client requests; anything else gets method-not-found. Capability (un)registration
# and diagnostic refresh are acknowledged but not acted on: we re-pull on every touch anyway.
self._request_handlers: Dict[str, Callable[[Any], Awaitable[Any]]] = {
self._request_handlers: dict[str, Callable[[Any], Awaitable[Any]]] = {
"window/workDoneProgress/create": self._handle_null,
"workspace/configuration": self._handle_workspace_configuration,
"client/registerCapability": self._handle_null,
@@ -175,11 +175,11 @@ class LSPClient:
"workspace/diagnostic/refresh": self._handle_null,
}
# Server → client notifications; others (showMessage, $/progress) are dropped.
self._notification_handlers: Dict[str, Callable[[Any], None]] = {
self._notification_handlers: dict[str, Callable[[Any], None]] = {
"textDocument/publishDiagnostics": self._handle_publish_diagnostics,
}
self._docs: Dict[str, _DocState] = {} # keyed by absolute path (NOT URI)
self._docs: dict[str, _DocState] = {} # keyed by absolute path (NOT URI)
self._state: str = "stopped"
self._sync_kind: int = 1 # 1=Full, 2=Incremental
self._stopping: bool = False
@@ -313,7 +313,7 @@ class LSPClient:
def failure_details(self) -> str:
"""Exit status + last stderr lines for a failed spawn/initialize; empty for a live server."""
parts: List[str] = []
parts: list[str] = []
if exit_desc := self._describe_exit():
parts.append(exit_desc)
if self._stderr_tail:
@@ -353,7 +353,7 @@ class LSPClient:
if unexpected_close:
await self._cleanup_process()
def _workspace_folders(self) -> List[Dict[str, str]]:
def _workspace_folders(self) -> list[dict[str, str]]:
return [_folder(r) for r in self.workspace_folders]
async def add_workspace_folder(self, root: str) -> None:
@@ -405,7 +405,7 @@ class LSPClient:
pass
try:
await self._send_notification("exit", None)
except Exception: # noqa: BLE001
except Exception:
pass
# Signalling right after ``exit`` races the server's own exit: needless SIGTERM
# noise for well-behaved servers and, on Darwin, a reaped-and-reused PID target.
@@ -517,7 +517,7 @@ class LSPClient:
else:
try:
reply = make_response(req_id, await handler(msg.get("params")))
except Exception as e: # noqa: BLE001 — protocol must not blow up
except Exception as e:
logger.warning("[%s] request handler %s failed: %s", self.server_id, method, e)
reply = make_error_response(req_id, -32000, f"handler failed: {e}")
await self._send_reply(reply)
@@ -528,7 +528,7 @@ class LSPClient:
return
try:
handler(msg.get("params"))
except Exception as e: # noqa: BLE001
except Exception as e:
logger.debug("[%s] notification handler %s failed: %s", self.server_id, method, e)
# ---- built-in server-→-client request handlers ----
@@ -611,7 +611,7 @@ class LSPClient:
return 0
# pop + reinsert refreshes LRU recency (dicts are insertion-ordered).
self._docs[abs_path] = self._docs.pop(abs_path)
change: Dict[str, Any] = {"text": text}
change: dict[str, Any] = {"text": text}
if self._sync_kind == 2:
change["range"] = {"start": {"line": 0, "character": 0}, "end": _end_position(doc.text)}
new_version = doc.version + 1
@@ -741,7 +741,7 @@ class LSPClient:
continue
await self._await_push(min(remaining, 0.5))
def diagnostics_for(self, path: str, *, fresh_only: bool = False) -> List[Dict[str, Any]]:
def diagnostics_for(self, path: str, *, fresh_only: bool = False) -> list[dict[str, Any]]:
"""Merged + deduped push/pull diagnostics for one file. With ``fresh_only=True`` a store only
contributes once its version tag has caught up to the document's — report paths must use this
so "stale" and "clean" aren't conflated."""
@@ -753,9 +753,9 @@ class LSPClient:
return _dedupe(push, pull)
def _dedupe(*lists: List[Dict[str, Any]]) -> List[Dict[str, Any]]:
seen: Set[str] = set()
out: List[Dict[str, Any]] = []
def _dedupe(*lists: list[dict[str, Any]]) -> list[dict[str, Any]]:
seen: set[str] = set()
out: list[dict[str, Any]] = []
for d in (d for lst in lists for d in lst if isinstance(d, dict)):
if (key := _diagnostic_key(d)) not in seen:
seen.add(key)
@@ -763,7 +763,7 @@ def _dedupe(*lists: List[Dict[str, Any]]) -> List[Dict[str, Any]]:
return out
def _diagnostic_key(d: Dict[str, Any]) -> str:
def _diagnostic_key(d: dict[str, Any]) -> str:
"""Content-equality key: severity + code + source + message + range. Shared with the manager's
cross-edit delta filter (``_diag_key``) so both layers agree on identity. Range is included so an
identical error at a second site still surfaces as new (the manager line-shifts its baseline first)."""
+3 -3
View File
@@ -50,7 +50,7 @@ def _emit(server_id: str, level: int, message: str) -> None:
event_log.log(level, "lsp[%s] %s", server_id, message)
def _emit_once(bucket: set, key: Tuple, server_id: str, level: int, first: str, repeat: str) -> None:
def _emit_once(bucket: set, key: tuple, server_id: str, level: int, first: str, repeat: str) -> None:
"""Log *first* at *level* the first time *key* is seen, *repeat* at DEBUG thereafter."""
with _announce_lock:
is_first = key not in bucket
@@ -144,7 +144,7 @@ def log_untrusted_skipped(server_id: str, workspace_root: str, file_path: str) -
f"skipped: untrusted workspace {workspace_root}")
def log_reaped(keys: List[Tuple[str, str]], idle_timeout: float) -> None:
def log_reaped(keys: list[tuple[str, str]], idle_timeout: float) -> None:
"""Idle clients were reaped. INFO, one line per sweep.
Also forgets the ``log_active`` announcement for those keys so a respawn
@@ -156,7 +156,7 @@ def log_reaped(keys: List[Tuple[str, str]], idle_timeout: float) -> None:
_emit("reaper", logging.INFO, f"reaped {len(keys)} idle client(s) after {idle_timeout:.0f}s: {summary}")
def log_released(keys: List[Tuple[str, str]], reason: str) -> None:
def log_released(keys: list[tuple[str, str]], reason: str) -> None:
"""Clients were shut down because their workspace went away (worktree released or root deleted).
INFO, one line per event; forgets the ``log_active`` announcement like :func:`log_reaped`."""
with _announce_lock:
+11 -11
View File
@@ -24,15 +24,15 @@ from hermes_constants import find_node_executable, with_hermes_node_path
logger = logging.getLogger("agent.lsp.install")
def _recipe(strategy: str, pkg: str, bin_name: str, **extra: Any) -> Dict[str, Any]:
def _recipe(strategy: str, pkg: str, bin_name: str, **extra: Any) -> dict[str, Any]:
return {"strategy": strategy, "pkg": pkg, "bin": bin_name, **extra}
def _npm(pkg: str, bin_name: str, **extra: Any) -> Dict[str, Any]:
def _npm(pkg: str, bin_name: str, **extra: Any) -> dict[str, Any]:
return _recipe("npm", pkg, bin_name, **extra)
def _manual(bin_name: str) -> Dict[str, Any]:
def _manual(bin_name: str) -> dict[str, Any]:
return _recipe("manual", "", bin_name)
@@ -43,7 +43,7 @@ TYPESCRIPT_SDK_PKG = "typescript@6"
# Recipe key → {strategy, pkg, bin[, extra_pkgs]}. After install we look for
# ``bin`` in ``<HERMES_HOME>/lsp/bin/`` first, then on PATH. ``extra_pkgs``
# are sibling npm packages a server needs in the same node_modules tree.
INSTALL_RECIPES: Dict[str, Dict[str, Any]] = {
INSTALL_RECIPES: dict[str, dict[str, Any]] = {
"pyright": _npm("pyright", "pyright-langserver"),
# tsserver must be importable from the same node_modules tree or
# initialize() fails with "Could not find a valid TypeScript installation".
@@ -72,8 +72,8 @@ INSTALL_RECIPES: Dict[str, Dict[str, Any]] = {
"powershell": _manual("pwsh"),
}
_install_locks: Dict[str, threading.Lock] = {}
_install_results: Dict[str, Optional[str]] = {}
_install_locks: dict[str, threading.Lock] = {}
_install_results: dict[str, Optional[str]] = {}
_install_lock_meta = threading.Lock()
_WINDOWS_WRAPPER_SUFFIXES = (".cmd", ".exe", ".bat")
@@ -101,7 +101,7 @@ def _native_binary_candidates(base: Path, *, is_windows: Optional[bool] = None)
"""
if not (_is_windows() if is_windows is None else is_windows):
return [base]
cands: Dict[str, Path] = {}
cands: dict[str, Path] = {}
for c in (*(Path(str(base) + s) for s in _WINDOWS_WRAPPER_SUFFIXES), base):
cands.setdefault(str(c).lower(), c)
return list(cands.values())
@@ -212,7 +212,7 @@ def _link_into_bin(target: Path) -> str:
# Node package manager → argv that installs into ``<staging>/node_modules`` (``lsp.package_manager``).
# Every manager keeps the staging-dir semantics: nothing touches the user's project or global tree.
_NODE_PM_ARGV: Dict[str, Callable[[str], list]] = {
_NODE_PM_ARGV: dict[str, Callable[[str], list]] = {
"npm": lambda staging: ["install", "--prefix", staging, "--silent", "--no-fund", "--no-audit"],
"pnpm": lambda staging: ["add", "--dir", staging],
# Global ``--cwd`` (before the command) is accepted by both Yarn Classic and Yarn Berry; Berry's
@@ -226,7 +226,7 @@ def _node_package_manager() -> Optional[str]:
try:
from hermes_cli.config import load_config_readonly
lsp_cfg = load_config_readonly().get("lsp") or {}
except Exception: # noqa: BLE001 — installer must not die on a broken config; npm is the historical default
except Exception:
return "npm"
pm = str(lsp_cfg.get("package_manager") or "npm").strip().lower() if isinstance(lsp_cfg, dict) else "npm"
if pm not in _NODE_PM_ARGV:
@@ -250,7 +250,7 @@ def _install_npm(pkg: str, bin_name: str, extra_pkgs: Optional[list] = None) ->
pm_store.ensure("npm")
pm_bin = find_node_executable("npm")
except Exception as exc: # noqa: BLE001 — a refused/failed PM install skips this server, never the session
except Exception as exc:
logger.warning("[install] cannot install %s: managed npm unavailable (%s)", pkg, exc)
return None
if pm_bin is None:
@@ -306,7 +306,7 @@ def _install_pip(pkg: str, bin_name: str) -> Optional[str]:
# strategy → installer(recipe, bin_name). ``manual`` is handled before dispatch.
_INSTALLERS: Dict[str, Callable[[Dict[str, Any], str], Optional[str]]] = {
_INSTALLERS: dict[str, Callable[[dict[str, Any], str], Optional[str]]] = {
"npm": lambda r, b: _install_npm(r["pkg"], b, extra_pkgs=r.get("extra_pkgs") or []),
"go": lambda r, b: _install_go(r["pkg"], b),
"pip": lambda r, b: _install_pip(r["pkg"], b),
+33 -33
View File
@@ -33,8 +33,8 @@ DEFAULT_IDLE_TIMEOUT = 600 # seconds; servers idle for >10min get reaped
_DELTA_BASELINE_CAP = 256 # per-file pre-write snapshots; paths never written again would otherwise live forever (#62950)
MIN_IDLE_TIMEOUT = 30 # floor for config values; must exceed any per-op wait budget
_Key = Tuple[str, str]
_Diags = List[Dict[str, Any]]
_Key = tuple[str, str]
_Diags = list[dict[str, Any]]
def _float_or(value: Any, default: float) -> float:
@@ -44,7 +44,7 @@ def _float_or(value: Any, default: float) -> float:
return default
def _path_list(value: Any) -> Optional[List[str]]:
def _path_list(value: Any) -> Optional[list[str]]:
"""A config list of paths, ``~``-expanded; ``None`` when the value is not a list of strings."""
if value is None:
return []
@@ -53,7 +53,7 @@ def _path_list(value: Any) -> Optional[List[str]]:
return [os.path.expanduser(p) for p in value if p]
def _parse_exclude_roots(value: Any) -> Optional[List[str]]:
def _parse_exclude_roots(value: Any) -> Optional[list[str]]:
"""``lsp.exclude_roots``; ``None`` (fail closed: every root excluded) for a malformed value."""
if (roots := _path_list(value)) is None:
eventlog.event_log.warning(
@@ -62,7 +62,7 @@ def _parse_exclude_roots(value: Any) -> Optional[List[str]]:
return roots
def parse_trusted_workspaces(value: Any) -> List[str]:
def parse_trusted_workspaces(value: Any) -> list[str]:
"""``lsp.trusted_workspaces``; a malformed value trusts nothing extra."""
if (roots := _path_list(value)) is None:
eventlog.event_log.warning(
@@ -103,7 +103,7 @@ class _BackgroundLoop:
finally:
try:
loop.close()
except Exception: # noqa: BLE001
except Exception:
pass
def run(self, coro, *, timeout: Optional[float] = None) -> Any:
@@ -139,12 +139,12 @@ class LSPService:
def __init__(
self, *, enabled: bool, wait_mode: str, wait_timeout: float, install_strategy: str,
binary_overrides: Optional[Dict[str, List[str]]] = None,
env_overrides: Optional[Dict[str, Dict[str, str]]] = None,
init_overrides: Optional[Dict[str, Dict[str, Any]]] = None,
disabled_servers: Optional[List[str]] = None,
binary_overrides: Optional[dict[str, list[str]]] = None,
env_overrides: Optional[dict[str, dict[str, str]]] = None,
init_overrides: Optional[dict[str, dict[str, Any]]] = None,
disabled_servers: Optional[list[str]] = None,
idle_timeout: float = DEFAULT_IDLE_TIMEOUT,
extra_servers: Optional[List[ServerDef]] = None,
extra_servers: Optional[list[ServerDef]] = None,
broken_retry_seconds: float = 0.0,
warmup_timeout: float = 0.0,
exclude_roots: Any = None,
@@ -159,14 +159,14 @@ class LSPService:
self._init_overrides = init_overrides or {}
self._disabled_servers = set(disabled_servers or [])
self._idle_timeout = idle_timeout
self._extra_servers: List[ServerDef] = list(extra_servers or [])
self._extra_servers: list[ServerDef] = list(extra_servers or [])
self._broken_retry = max(0.0, broken_retry_seconds)
self._warmup_timeout = max(0.0, warmup_timeout)
# ``None`` = misconfigured → fail closed (every root excluded) until the user fixes the key: a
# bare string here means "exclude that workspace", and excluding nothing would re-pay the stall it
# was meant to avoid.
self._exclude_roots: Optional[List[str]] = _parse_exclude_roots(exclude_roots)
self._trusted_workspaces: List[str] = parse_trusted_workspaces(trusted_workspaces)
self._exclude_roots: Optional[list[str]] = _parse_exclude_roots(exclude_roots)
self._trusted_workspaces: list[str] = parse_trusted_workspaces(trusted_workspaces)
self._untrusted_skipped: set = set() # (server_id, root) pairs denied by workspace trust
self._operator_roots = frozenset(operator_workspace_roots()) # see _note_operator_roots
@@ -175,15 +175,15 @@ class LSPService:
self._loop.start()
# Per-(server_id, workspace_root) state
self._clients: Dict[_Key, LSPClient] = {}
self._clients: dict[_Key, LSPClient] = {}
# (server_id, root) → monotonic deadline after which the pair may be retried (inf = lifetime).
self._broken: Dict[_Key, float] = {}
self._spawning: Dict[_Key, asyncio.Future] = {}
self._last_used: Dict[_Key, float] = {}
self._broken: dict[_Key, float] = {}
self._spawning: dict[_Key, asyncio.Future] = {}
self._last_used: dict[_Key, float] = {}
self._state_lock = threading.Lock()
self._idle_reaper_task: Optional[asyncio.Task] = None
# abs file path → diagnostics snapshot taken immediately before a write.
self._delta_baseline: Dict[str, _Diags] = {}
self._delta_baseline: dict[str, _Diags] = {}
if self._enabled and self._idle_timeout > 0:
self._loop.run(self._start_idle_reaper(), timeout=2.0)
@@ -194,7 +194,7 @@ class LSPService:
try:
from hermes_cli.config import load_config_readonly
cfg = load_config_readonly()
except Exception as e: # noqa: BLE001
except Exception as e:
logger.debug("LSP config load failed: %s", e)
return None
lsp_cfg = cfg.get("lsp") if isinstance(cfg, dict) else None
@@ -255,7 +255,7 @@ class LSPService:
return None
try:
return (srv.server_id, srv.resolve_root(file_path, ws_root) or ws_root)
except Exception: # noqa: BLE001
except Exception:
return (srv.server_id, ws_root)
def enabled_for(self, file_path: str) -> bool:
@@ -354,7 +354,7 @@ class LSPService:
budget = self._wait_budget(file_path)
t = max(DIAGNOSTICS_DOCUMENT_WAIT + 3.0, budget + 3.0)
diags = self._loop.run(self._snapshot_async(file_path, budget), timeout=t)
except Exception as e: # noqa: BLE001
except Exception as e:
logger.debug("baseline snapshot failed for %s: %s", file_path, e)
self._mark_broken_for_file(file_path, e)
diags = []
@@ -387,7 +387,7 @@ class LSPService:
budget = self._wait_budget(file_path)
t = timeout if timeout is not None else budget + 2.0
diags = self._loop.run(self._open_and_wait_async(file_path, budget=budget), timeout=t)
except Exception as e: # noqa: BLE001
except Exception as e:
if isinstance(e, asyncio.TimeoutError):
eventlog.log_timeout(server_id, file_path)
logger.debug("LSP diagnostics timeout for %s: %s", file_path, e)
@@ -424,7 +424,7 @@ class LSPService:
# Roll the baseline forward so the next call is a delta against this state.
try:
fresh = self._loop.run(self._current_diags_async(file_path), timeout=2.0) or []
except Exception: # noqa: BLE001
except Exception:
fresh = []
if fresh:
self._set_delta_baseline(abs_path, fresh)
@@ -449,7 +449,7 @@ class LSPService:
try:
# Fire-and-forget shutdown — we're already on a slow path.
self._loop.run(client.shutdown(), timeout=1.0)
except Exception: # noqa: BLE001
except Exception:
pass
if not already_broken:
eventlog.log_spawn_failed(key[0], key[1], exc)
@@ -460,12 +460,12 @@ class LSPService:
return
try:
self._loop.run(self._shutdown_async(), timeout=10.0)
except Exception as e: # noqa: BLE001
except Exception as e:
logger.debug("LSP shutdown error: %s", e)
self._loop.stop()
clear_cache()
def get_status(self) -> Dict[str, Any]:
def get_status(self) -> dict[str, Any]:
"""Return a snapshot of the service for ``hermes lsp status``."""
with self._state_lock:
clients = [
@@ -512,7 +512,7 @@ class LSPService:
file_path, version, mode=self._wait_mode,
timeout=budget if budget is not None else self._wait_timeout,
)
except Exception as e: # noqa: BLE001
except Exception as e:
if snapshot:
logger.debug("snapshot open/wait failed: %s", e)
else:
@@ -574,7 +574,7 @@ class LSPService:
if not owner:
try:
client = await spawning
except Exception: # noqa: BLE001
except Exception:
return None
return await self._attach_root(srv, client, root) if client is not None else None
try:
@@ -618,7 +618,7 @@ class LSPService:
)
try:
await client.start()
except Exception as e: # noqa: BLE001
except Exception as e:
eventlog.log_spawn_failed(srv.server_id, root, e)
return None
return client
@@ -641,7 +641,7 @@ class LSPService:
await self._reap_idle_once()
except asyncio.CancelledError:
raise
except Exception as e: # noqa: BLE001
except Exception as e:
# A transient sweep error must not kill the reaper, or the accumulation leak it fixes comes back.
logger.debug("LSP idle reaper sweep error: %s", e)
@@ -671,7 +671,7 @@ class LSPService:
root = os.path.abspath(workspace_root)
try:
return self._loop.run(self._release_async(root), timeout=15.0)
except Exception as e: # noqa: BLE001
except Exception as e:
logger.debug("LSP release of %s failed: %s", root, e)
return 0
@@ -709,7 +709,7 @@ class LSPService:
for folder in folders:
try:
await client.remove_workspace_folder(folder)
except Exception as e: # noqa: BLE001
except Exception as e:
logger.debug("LSP folder removal for %s failed: %s", folder, e)
if clients:
eventlog.log_released([(c.server_id, c.workspace_root) for c in clients], reason)
+1 -1
View File
@@ -119,7 +119,7 @@ def make_error_response(req_id: Any, code: int, message: str, data: Any = None)
return {"jsonrpc": "2.0", "id": req_id, "error": err}
def classify_message(msg: dict) -> Tuple[str, Any]:
def classify_message(msg: dict) -> tuple[str, Any]:
"""Return ``(kind, key)``: kind ∈ request/response/notification/invalid; key is the id (request/response),
the method (notification) or ``None`` (invalid)."""
if not isinstance(msg, dict) or msg.get("jsonrpc") != "2.0":
+2 -2
View File
@@ -42,7 +42,7 @@ def build_line_shift(pre_text: str, post_text: str) -> _Shift:
return shift
def shift_diagnostic_range(diag: Dict[str, Any], shift: _Shift) -> Optional[Dict[str, Any]]:
def shift_diagnostic_range(diag: dict[str, Any], shift: _Shift) -> Optional[dict[str, Any]]:
"""Copy of ``diag`` with its line range remapped; ``None`` if the start line was deleted.
A multi-line diagnostic whose end straddles the deletion collapses to a
@@ -67,7 +67,7 @@ def shift_diagnostic_range(diag: Dict[str, Any], shift: _Shift) -> Optional[Dict
}
def shift_baseline(baseline: List[Dict[str, Any]], shift: _Shift) -> List[Dict[str, Any]]:
def shift_baseline(baseline: list[dict[str, Any]], shift: _Shift) -> list[dict[str, Any]]:
"""Apply ``shift`` to every diagnostic in ``baseline``, dropping deleted entries."""
shifted = (shift_diagnostic_range(d, shift) for d in baseline if isinstance(d, dict))
return [s for s in shifted if s is not None]
+2 -2
View File
@@ -40,7 +40,7 @@ def _sanitize_field(value: Any, *, limit: int) -> str:
return html.escape(raw.strip()[:limit], quote=False)
def format_diagnostic(d: Dict[str, Any]) -> str:
def format_diagnostic(d: dict[str, Any]) -> str:
"""One-line representation of a single diagnostic (fields sanitized)."""
sev = SEVERITY_NAMES.get(d.get("severity") or 1, "ERROR")
start = (d.get("range") or {}).get("start") or {}
@@ -54,7 +54,7 @@ def format_diagnostic(d: Dict[str, Any]) -> str:
def report_for_file(
file_path: str,
diagnostics: List[Dict[str, Any]],
diagnostics: list[dict[str, Any]],
*,
severities: frozenset = DEFAULT_SEVERITIES,
max_per_file: int = MAX_PER_FILE,
+21 -21
View File
@@ -20,7 +20,7 @@ logger = logging.getLogger("agent.lsp.servers")
# LSP languageId for ``textDocument/didOpen``, as language → extensions. A few
# servers (typescript-language-server, vue-language-server) refuse wrong IDs.
_EXTS_BY_LANGUAGE: Dict[str, Sequence[str]] = {
_EXTS_BY_LANGUAGE: dict[str, Sequence[str]] = {
"python": (".py", ".pyi"),
"typescript": (".ts", ".mts", ".cts"),
"typescriptreact": (".tsx",),
@@ -45,7 +45,7 @@ _EXTS_BY_LANGUAGE: Dict[str, Sequence[str]] = {
"dockerfile": (".dockerfile",),
"powershell": (".ps1", ".psm1", ".psd1"),
}
LANGUAGE_BY_EXT: Dict[str, str] = {ext: lang for lang, exts in _EXTS_BY_LANGUAGE.items() for ext in exts}
LANGUAGE_BY_EXT: dict[str, str] = {ext: lang for lang, exts in _EXTS_BY_LANGUAGE.items() for ext in exts}
_SpawnFn = Callable[[str, "ServerContext"], Optional["SpawnSpec"]]
_RootFn = Callable[[str, str], Optional[str]]
@@ -54,11 +54,11 @@ _RootFn = Callable[[str, str], Optional[str]]
@dataclass
class SpawnSpec:
"""Result of resolving a server for a file (``None`` means skip)."""
command: List[str]
command: list[str]
workspace_root: str
cwd: str
env: Dict[str, str] = field(default_factory=dict)
initialization_options: Dict[str, Any] = field(default_factory=dict)
env: dict[str, str] = field(default_factory=dict)
initialization_options: dict[str, Any] = field(default_factory=dict)
seed_diagnostics_on_first_push: bool = False
@@ -67,7 +67,7 @@ class ServerDef:
"""One language server: ``resolve_root(file, ws)`` → per-server root or ``None`` to skip;
``build_spawn(root, ctx)`` → :class:`SpawnSpec` or ``None`` when the binary can't be found."""
server_id: str
extensions: Tuple[str, ...]
extensions: tuple[str, ...]
resolve_root: _RootFn
build_spawn: _SpawnFn
seed_first_push: bool = False
@@ -87,9 +87,9 @@ class ServerContext:
"""User policy passed into :meth:`ServerDef.build_spawn` (install strategy, overrides)."""
workspace_root: str
install_strategy: str = "auto" # "auto" | "manual" | "off"
binary_overrides: Dict[str, List[str]] = field(default_factory=dict)
env_overrides: Dict[str, Dict[str, str]] = field(default_factory=dict)
init_overrides: Dict[str, Dict[str, Any]] = field(default_factory=dict)
binary_overrides: dict[str, list[str]] = field(default_factory=dict)
env_overrides: dict[str, dict[str, str]] = field(default_factory=dict)
init_overrides: dict[str, dict[str, Any]] = field(default_factory=dict)
# Whether the server may load code the project ships (see workspace.is_trusted_workspace).
# Defaults closed: a context built without a trust decision must never run the repo's code.
trusted: bool = False
@@ -142,8 +142,8 @@ def _find_binary(ctx: ServerContext, server_id: str, which: Sequence[str], insta
return bin_path
def _make_spec(root: str, ctx: ServerContext, server_id: str, command: List[str],
base_init: Optional[Dict[str, Any]] = None, seed: bool = False) -> SpawnSpec:
def _make_spec(root: str, ctx: ServerContext, server_id: str, command: list[str],
base_init: Optional[dict[str, Any]] = None, seed: bool = False) -> SpawnSpec:
from pm import env_for
init = ctx.init_overrides.get(server_id, {}) if base_init is None else {**base_init, **ctx.init_overrides.get(server_id, {})}
@@ -154,8 +154,8 @@ def _make_spec(root: str, ctx: ServerContext, server_id: str, command: List[str]
def _simple_spawn(server_id: str, which: Sequence[str], args: Sequence[str] = (),
install_pkg: Optional[str] = None, base_init: Optional[Dict[str, Any]] = None,
seed: bool = False, untrusted_init: Optional[Dict[str, Any]] = None) -> _SpawnFn:
install_pkg: Optional[str] = None, base_init: Optional[dict[str, Any]] = None,
seed: bool = False, untrusted_init: Optional[dict[str, Any]] = None) -> _SpawnFn:
"""Build a spawn function for the common single-binary server shape; ``untrusted_init`` replaces
``base_init`` in an untrusted workspace (the server's own switch for not loading project code)."""
def build(root: str, ctx: ServerContext) -> Optional[SpawnSpec]:
@@ -235,7 +235,7 @@ _VUE_TSDK_MSG = (
)
def _node_modules_trees(bin_path: str, root: Optional[str]) -> List[str]:
def _node_modules_trees(bin_path: str, root: Optional[str]) -> list[str]:
"""``node_modules`` trees that may hold a server and its TypeScript SDK: the launcher's own tree
(symlinks resolved), Hermes staging, then the project's (``root`` None: Hermes's trees only, for
TypeScript's SDK pin in an untrusted workspace, whose own JavaScript must not load)."""
@@ -296,7 +296,7 @@ def _spawn_typescript(root: str, ctx: ServerContext) -> Optional[SpawnSpec]:
bin_path = _find_binary(ctx, "typescript", ("typescript-language-server",), "typescript-language-server")
if bin_path is None:
return None
base: Dict[str, Any] = {}
base: dict[str, Any] = {}
if not ctx.trusted:
sdk = _typescript_sdk_dir(_node_modules_trees(bin_path, None))
if sdk is None:
@@ -396,12 +396,12 @@ _DENO_EXCLUDES = ["deno.json", "deno.jsonc"]
_root_typescript = _markers_root(_JS_MARKERS, _DENO_EXCLUDES)
def _server(server_id: str, extensions: Tuple[str, ...], description: str, *,
def _server(server_id: str, extensions: tuple[str, ...], description: str, *,
markers: Optional[Sequence[str]] = None, excludes: Sequence[str] = (),
resolve_root: Optional[_RootFn] = None, build_spawn: Optional[_SpawnFn] = None,
which: Sequence[str] = (), args: Sequence[str] = (), install_pkg: Optional[str] = None,
base_init: Optional[Dict[str, Any]] = None, seed: bool = False,
untrusted_init: Optional[Dict[str, Any]] = None, multi_root: bool = False) -> ServerDef:
base_init: Optional[dict[str, Any]] = None, seed: bool = False,
untrusted_init: Optional[dict[str, Any]] = None, multi_root: bool = False) -> ServerDef:
"""Registry entry factory: defaults to marker-based root + single-binary spawn."""
return ServerDef(
server_id, extensions,
@@ -411,7 +411,7 @@ def _server(server_id: str, extensions: Tuple[str, ...], description: str, *,
)
SERVERS: List[ServerDef] = [
SERVERS: list[ServerDef] = [
_server("pyright", (".py", ".pyi"), "Python — Microsoft pyright",
markers=["pyproject.toml", "setup.py", "setup.cfg", "requirements.txt", "Pipfile", "pyrightconfig.json"],
build_spawn=_spawn_pyright, multi_root=True),
@@ -498,14 +498,14 @@ def _custom_spawn(server_id: str, command: Sequence[str]) -> _SpawnFn:
return build
def custom_servers(servers_cfg: Any) -> List[ServerDef]:
def custom_servers(servers_cfg: Any) -> list[ServerDef]:
"""``lsp.servers`` entries that declare ``extensions`` and name no built-in server are user-declared
servers (issue #100257). They go AHEAD of the built-ins so a custom entry can claim an extension;
malformed entries are logged and skipped so one typo never disables the rest of the subsystem."""
if not isinstance(servers_cfg, dict):
return []
builtin = {s.server_id for s in SERVERS}
out: List[ServerDef] = []
out: list[ServerDef] = []
for server_id, cfg in servers_cfg.items():
if server_id in builtin or not isinstance(cfg, dict) or "extensions" not in cfg:
continue
+3 -3
View File
@@ -134,7 +134,7 @@ def nearest_root(
return None
def resolve_workspace_for_file(file_path: str, *, cwd: Optional[str] = None) -> Tuple[Optional[str], bool]:
def resolve_workspace_for_file(file_path: str, *, cwd: Optional[str] = None) -> tuple[Optional[str], bool]:
"""Return ``(workspace_root, gated_in)`` for a file. The cwd's worktree wins when the file is
inside it; otherwise the file's own worktree is the fallback anchor (monorepos / unrelated
checkouts). ``(None, False)`` when neither is in a git worktree."""
@@ -154,7 +154,7 @@ def resolve_workspace_for_file(file_path: str, *, cwd: Optional[str] = None) ->
return None, False
def operator_workspace_roots() -> Set[str]:
def operator_workspace_roots() -> set[str]:
"""Git worktrees the operator pointed Hermes at: the launch dir and the surface-set workspace
(``resolve_agent_cwd``: the Desktop/TUI session cwd, ``hermes -w``'s worktree, a gateway's
``terminal.cwd``). The agent's ``cd`` moves neither (it only moves the terminal's cwd). A repo at
@@ -172,7 +172,7 @@ def operator_workspace_roots() -> Set[str]:
if not is_truthy_value(get_session_env("HERMES_CRON_SESSION", "")):
anchors.append(resolve_agent_cwd)
home = normalize_path("~")
roots: Set[str] = set()
roots: set[str] = set()
for anchor in anchors:
try:
root = find_git_worktree(str(anchor()))
+11 -11
View File
@@ -28,7 +28,7 @@ def _disp_width(s: str) -> int:
return max(wcswidth(s), 0)
def split_table_row(row: str) -> List[str]:
def split_table_row(row: str) -> list[str]:
"""Split ``| a | b | c |`` into ``["a", "b", "c"]`` with trims."""
return [c.strip() for c in row.strip().removeprefix("|").removesuffix("|").split("|")]
@@ -51,7 +51,7 @@ def looks_like_table_row(row: str) -> bool:
return bool(stripped) and (stripped.startswith("|") or stripped.count("|") >= 2)
def _render_block(rows: List[List[str]], available_width: int | None = None) -> List[str]:
def _render_block(rows: list[list[str]], available_width: int | None = None) -> list[str]:
"""Render ``rows`` (header + body, divider implied) at uniform widths.
When the horizontal table would exceed ``available_width`` fall back to a
@@ -65,7 +65,7 @@ def _render_block(rows: List[List[str]], available_width: int | None = None) ->
if available_width is not None and sum(widths) + 3 * ncols + 1 > max(available_width, 20):
return _render_vertical(rows, ncols, available_width)
def _row(cells: List[str]) -> str:
def _row(cells: list[str]) -> str:
return "| " + " | ".join(c + " " * max(0, widths[k] - _disp_width(c)) for k, c in enumerate(cells)) + " |"
out = [_row(rows[0]), "|" + "|".join("-" * (w + 2) for w in widths) + "|"]
@@ -73,9 +73,9 @@ def _render_block(rows: List[List[str]], available_width: int | None = None) ->
return out
def _hard_break(word: str, w: int) -> List[str]:
def _hard_break(word: str, w: int) -> list[str]:
"""Split a single over-wide word into display-width-``w`` chunks."""
out: List[str] = []
out: list[str] = []
buf, bw = "", 0
for ch in word:
cw = _disp_width(ch) or 1
@@ -87,7 +87,7 @@ def _hard_break(word: str, w: int) -> List[str]:
return out + [buf] if buf else out
def _wrap_to_width(text: str, width: int) -> List[str]:
def _wrap_to_width(text: str, width: int) -> list[str]:
"""Soft-wrap ``text`` at word boundaries to ``width`` display cells.
Words wider than ``width`` are hard-broken. Empty input yields a single
@@ -98,7 +98,7 @@ def _wrap_to_width(text: str, width: int) -> List[str]:
words = text.split()
if not words:
return [""]
lines: List[str] = []
lines: list[str] = []
current, current_w = "", 0
def _start(word: str, ww: int) -> None:
@@ -126,7 +126,7 @@ def _wrap_to_width(text: str, width: int) -> List[str]:
return lines or [""]
def _render_vertical(rows: List[List[str]], ncols: int, available_width: int) -> List[str]:
def _render_vertical(rows: list[list[str]], ncols: int, available_width: int) -> list[str]:
"""Render a too-wide table as ``Header: value`` blocks (Claude Code's narrow fallback).
Each body row becomes one block with continuation lines indented two spaces,
@@ -137,7 +137,7 @@ def _render_vertical(rows: List[List[str]], ncols: int, available_width: int) ->
labels = [h or f"Column {i + 1}" for i, h in enumerate(rows[0] + [""] * (ncols - len(rows[0])))]
separator = "─" * (max(20, min(40, available_width - 2)) if available_width else 30)
cont_budget = max(10, available_width - 2) # continuation lines are indented two spaces
out: List[str] = []
out: list[str] = []
for ri, row in enumerate(rows[1:]):
if ri > 0:
out.append(separator)
@@ -165,14 +165,14 @@ def realign_markdown_tables(text: str, available_width: int | None = None) -> st
if "|" not in text:
return text
lines = text.split("\n")
out: List[str] = []
out: list[str] = []
i, n = 0, len(lines)
while i < n:
line = lines[i]
# A table starts with a header row whose next line is a divider.
if "|" in line and i + 1 < n and is_table_divider(lines[i + 1]):
header = split_table_row(line)
body: List[List[str]] = []
body: list[list[str]] = []
j = i + 2
while j < n and "|" in lines[j] and lines[j].strip():
if not is_table_divider(lines[j]):
+29 -29
View File
@@ -63,7 +63,7 @@ def _accepts_require_checkpoint(fn: Callable[..., Any]) -> bool:
# -- Tool-schema plumbing -----------------------------------------------------
def normalize_tool_schema(schema: Any) -> Optional[Dict[str, Any]]:
def normalize_tool_schema(schema: Any) -> Optional[dict[str, Any]]:
"""Return a bare function-tool dict with a resolvable top-level ``name``, else None.
Providers should return ``{"name", "description", "parameters"}`` but some return the
@@ -78,7 +78,7 @@ def normalize_tool_schema(schema: Any) -> Optional[Dict[str, Any]]:
return schema if name and isinstance(name, str) else None
def memory_provider_tools_enabled(enabled_toolsets: Optional[List[str]], disabled_toolsets: Optional[List[str]] = None,
def memory_provider_tools_enabled(enabled_toolsets: Optional[list[str]], disabled_toolsets: Optional[list[str]] = None,
*, memory_tool_present: bool = False) -> bool:
"""Return whether external memory-provider tools should be exposed."""
if disabled_toolsets and "memory" in disabled_toolsets:
@@ -226,7 +226,7 @@ def _redact_for_provider(*values: Any) -> Any:
return scrubbed[0] if len(scrubbed) == 1 else tuple(scrubbed)
def _redact_messages_for_egress(messages: Optional[List[Any]]) -> Optional[List[Any]]:
def _redact_messages_for_egress(messages: Optional[list[Any]]) -> Optional[list[Any]]:
return None if messages is None else list(_redact_for_provider(messages))
@@ -396,16 +396,16 @@ class MemoryManager:
"""
def __init__(self, *, external_prefetch_timeout: Optional[float] = None) -> None:
self._providers: List[MemoryProvider] = []
self._tool_to_provider: Dict[str, MemoryProvider] = {}
self._external_prefetch_spill_config: Optional[Dict[str, Any]] = None
self._providers: list[MemoryProvider] = []
self._tool_to_provider: dict[str, MemoryProvider] = {}
self._external_prefetch_spill_config: Optional[dict[str, Any]] = None
self._has_external: bool = False
timeout = external_prefetch_timeout
timeout = _EXTERNAL_PREFETCH_TIMEOUT_S if timeout is None else float(timeout)
if timeout <= 0:
raise ValueError("external_prefetch_timeout must be positive")
self._external_prefetch_timeout = timeout
self._external_prefetch_threads: Dict[str, threading.Thread] = {}
self._external_prefetch_threads: dict[str, threading.Thread] = {}
self._external_prefetch_lock = threading.Lock()
# Single-worker background executor for end-of-turn sync/prefetch, created lazily so
# the builtin-only path spawns no threads; one worker serializes a provider's writes.
@@ -413,17 +413,17 @@ class MemoryManager:
self._sync_executor_lock = threading.Lock()
# Futures by durability class ("write" / "prefetch") so shutdown can drain FIFO
# within a bound, then report exactly what it abandoned.
self._background_futures: Dict[Future, str] = {}
self._background_futures: dict[Future, str] = {}
self._shutting_down = False
self._shutdown_drain_state: Dict[str, Any] = {
self._shutdown_drain_state: dict[str, Any] = {
"status": "not_started", "abandoned_writes": 0, "abandoned_prefetches": 0, "active_tasks": 0,
}
def _each_provider(self, label: str, call: Callable[[MemoryProvider], Any], *, level: int = logging.DEBUG,
providers: Optional[List[MemoryProvider]] = None, exc_info: bool = False) -> List[Any]:
providers: Optional[list[MemoryProvider]] = None, exc_info: bool = False) -> list[Any]:
"""Call ``call(provider)`` per provider, logging+swallowing failures; returns successes in order.
``label`` completes the log line ``Memory provider '<name>' <label>: <exc>``."""
results: List[Any] = []
results: list[Any] = []
for provider in self._providers if providers is None else providers:
try:
results.append(call(provider))
@@ -483,7 +483,7 @@ class MemoryManager:
logger.info("Memory provider '%s' registered (%d tools)", provider.name, len(schemas))
@property
def providers(self) -> List[MemoryProvider]:
def providers(self) -> list[MemoryProvider]:
return list(self._providers)
def get_provider(self, name: str) -> Optional[MemoryProvider]:
@@ -516,7 +516,7 @@ class MemoryManager:
if provider.name == "builtin":
return provider.prefetch(query, session_id=session_id)
result_box: Dict[str, Any] = {}
result_box: dict[str, Any] = {}
def _run() -> None:
try:
@@ -559,7 +559,7 @@ class MemoryManager:
def describe_recall(self) -> str:
"""Deterministic recall indicator line (e.g. ``"🧠 Provider — recalled 3 memories"``); ``""`` if none.
Call right after :meth:`prefetch_all` so the user SEES memory was used even if the model is silent."""
segments: List[str] = []
segments: list[str] = []
for status in self._each_provider("recall_status failed (non-fatal)", lambda p: p.recall_status()):
if status is None:
continue
@@ -588,8 +588,8 @@ class MemoryManager:
return params is None or _has_var_kwargs(params) or keyword in params
def sync_all(self, user_content: str, assistant_content: str, *, session_id: str = "",
messages: Optional[List[Dict[str, Any]]] = None,
turn_author: Optional[Dict[str, Any]] = None) -> None:
messages: Optional[list[dict[str, Any]]] = None,
turn_author: Optional[dict[str, Any]] = None) -> None:
"""Sync a completed turn to all providers on the background worker.
Never inline: a provider's ``sync_turn`` may block for minutes, which kept ``run_conversation``
@@ -609,7 +609,7 @@ class MemoryManager:
optional_kwargs = {"messages": redacted_messages, "turn_author": turn_author}
def _sync(provider: MemoryProvider) -> None:
kwargs: Dict[str, Any] = {"session_id": session_id}
kwargs: dict[str, Any] = {"session_id": session_id}
for keyword, value in optional_kwargs.items():
if value is not None and self._provider_sync_accepts(provider, keyword):
kwargs[keyword] = value
@@ -674,12 +674,12 @@ class MemoryManager:
return isinstance(e, RuntimeError) # executor already shut down — nothing pending
return True
def get_all_tool_schemas(self) -> List[Dict[str, Any]]:
def get_all_tool_schemas(self) -> list[dict[str, Any]]:
"""Collect deduplicated tool schemas from all providers; reserved core tool names are
skipped because :meth:`add_provider` refuses to route them."""
from toolsets import _HERMES_CORE_TOOLS
schemas: List[Dict[str, Any]] = []
schemas: list[dict[str, Any]] = []
seen = set()
def _collect(provider: MemoryProvider) -> None:
@@ -703,7 +703,7 @@ class MemoryManager:
def has_tool(self, tool_name: str) -> bool:
return tool_name in self._tool_to_provider
def handle_tool_call(self, tool_name: str, args: Dict[str, Any], **kwargs) -> str:
def handle_tool_call(self, tool_name: str, args: dict[str, Any], **kwargs) -> str:
"""Route a tool call to its provider; returns a JSON string (tool_error on failure)."""
provider = self._tool_to_provider.get(tool_name)
if provider is None:
@@ -730,12 +730,12 @@ class MemoryManager:
self._each_provider("on_turn_start failed", _tick)
def on_session_end(self, messages: List[Dict[str, Any]]) -> None:
def on_session_end(self, messages: list[dict[str, Any]]) -> None:
messages = _redact_messages_for_egress(messages or [])
self._each_provider("on_session_end failed", lambda p: p.on_session_end(messages), level=logging.WARNING,
exc_info=True)
def commit_session_boundary_async(self, messages: List[Dict[str, Any]], *, new_session_id: str,
def commit_session_boundary_async(self, messages: list[dict[str, Any]], *, new_session_id: str,
parent_session_id: str = "", reason: str = "new_session") -> None:
"""Queue old-session extraction + provider rebinding as ONE serialized task.
@@ -795,8 +795,8 @@ class MemoryManager:
versions = (self._checkpoint_api_version(p) for p in self._providers)
return any(v is not None and v >= api_version for v in versions)
def on_pre_compress(self, messages: List[Dict[str, Any]], *,
evidence_messages: Optional[List[Dict[str, Any]]] = None, require_checkpoint: bool = False,
def on_pre_compress(self, messages: list[dict[str, Any]], *,
evidence_messages: Optional[list[dict[str, Any]]] = None, require_checkpoint: bool = False,
checkpoint_api_version: int = PRE_COMPRESS_CHECKPOINT_API_VERSION) -> str:
"""Notify providers before compression; return their combined summary-prompt text.
@@ -814,7 +814,7 @@ class MemoryManager:
is_checkpoint_provider = version >= checkpoint_api_version
use_evidence = is_checkpoint_provider and evidence_messages is not None
provider_messages = evidence_messages if use_evidence else messages
kwargs: Dict[str, Any] = {}
kwargs: dict[str, Any] = {}
# v1 providers and bare-shape v2 providers never see the signal.
if is_checkpoint_provider and _accepts_require_checkpoint(provider.on_pre_compress):
kwargs["require_checkpoint"] = require_checkpoint
@@ -843,7 +843,7 @@ class MemoryManager:
return "positional" if accepted >= 4 else "legacy"
def on_memory_write(self, action: str, target: str, content: str,
metadata: Optional[Dict[str, Any]] = None) -> None:
metadata: Optional[dict[str, Any]] = None) -> None:
"""Notify external providers when the built-in memory tool writes (skips builtin, the source)."""
content, metadata = _redact_for_provider(content, dict(metadata or {}))
@@ -874,8 +874,8 @@ class MemoryManager:
return False
return isinstance(result, dict) and result.get("success") is True and result.get("staged") is not True
def notify_memory_tool_write(self, tool_result: Any, tool_args: Dict[str, Any], *,
build_metadata: Optional[Callable[[], Dict[str, Any]]] = None) -> None:
def notify_memory_tool_write(self, tool_result: Any, tool_args: dict[str, Any], *,
build_metadata: Optional[Callable[[], dict[str, Any]]] = None) -> None:
"""Mirror a built-in memory tool call to external providers.
Gates on a committed write, expands single-op and batched ``operations`` shapes, keeps only
@@ -926,7 +926,7 @@ class MemoryManager:
providers=self._providers[::-1])
@property
def shutdown_drain_state(self) -> Dict[str, Any]:
def shutdown_drain_state(self) -> dict[str, Any]:
"""Snapshot of the most recent bounded shutdown drain outcome."""
with self._sync_executor_lock:
return dict(self._shutdown_drain_state)
+12 -12
View File
@@ -27,7 +27,7 @@ def ctx_bound(fn: Callable[..., Any]) -> Callable[..., Any]:
def spawn_context_thread(target: Callable[..., Any], *, name: str, daemon: bool = True,
args: tuple = (), kwargs: Optional[Dict[str, Any]] = None) -> threading.Thread:
args: tuple = (), kwargs: Optional[dict[str, Any]] = None) -> threading.Thread:
"""Unstarted thread running *target* under the spawner's contextvars (see :func:`ctx_bound`).
Every memory-provider background job (prefetch, sync, writer loops) must go through this."""
return threading.Thread(target=ctx_bound(target), args=args, kwargs=kwargs, name=name, daemon=daemon)
@@ -132,17 +132,17 @@ class MemoryProvider(ABC):
def sync_turn(
self, user_content: str, assistant_content: str, *,
session_id: str = "", messages: Optional[List[Dict[str, Any]]] = None,
turn_author: Optional[Dict[str, Any]] = None,
session_id: str = "", messages: Optional[list[dict[str, Any]]] = None,
turn_author: Optional[dict[str, Any]] = None,
) -> None:
"""Persist a completed turn (non-blocking). ``messages`` is the OpenAI-style list so far.
``turn_author`` (``{"id", "name", "is_bot"}``) is who wrote the user side; the manager sends it only to signatures that accept it."""
@abstractmethod
def get_tool_schemas(self) -> List[Dict[str, Any]]:
def get_tool_schemas(self) -> list[dict[str, Any]]:
"""OpenAI function-calling schemas ({"name", "description", "parameters"}); [] if none."""
def handle_tool_call(self, tool_name: str, args: Dict[str, Any], **kwargs) -> str:
def handle_tool_call(self, tool_name: str, args: dict[str, Any], **kwargs) -> str:
"""Handle one of this provider's tools; must return a JSON string."""
raise NotImplementedError(f"Provider {self.name} does not handle tool {tool_name}")
@@ -156,13 +156,13 @@ class MemoryProvider(ABC):
author_is_bot. The author trio names who wrote THIS turn (None, None, False without one): a shared session
carries several participants, so a provider keying durable state on identity must read it per turn."""
def identity_signature(self) -> Dict[str, Any]:
def identity_signature(self) -> dict[str, Any]:
"""Identity-mapping values that must bust a cached gateway agent when they change (writer identity, alias
tables, session-name prefixing). Provider-namespaced keys, JSON-serializable values. The gateway calls this
on an uninitialized instance on every inbound message, so keep it cheap and read-only."""
return {}
def on_session_end(self, messages: List[Dict[str, Any]]) -> None:
def on_session_end(self, messages: list[dict[str, Any]]) -> None:
"""End-of-session extraction; fires only at real session boundaries, never per-turn."""
def on_session_switch(
@@ -173,25 +173,25 @@ class MemoryProvider(ABC):
``reset`` is True only for a genuinely new conversation (flush buffers); ``rewound``:
same id but the transcript was truncated."""
def on_pre_compress(self, messages: List[Dict[str, Any]]) -> str:
def on_pre_compress(self, messages: list[dict[str, Any]]) -> str:
"""Extract insights from ``messages`` about to be compressed, fed into the summary prompt."""
return ""
def on_delegation(self, task: str, result: str, *, child_session_id: str = "", **kwargs) -> None:
"""PARENT-side observation of a completed delegation (the subagent has no provider session)."""
def get_config_schema(self) -> List[Dict[str, Any]]:
def get_config_schema(self) -> list[dict[str, Any]]:
"""Setup fields for ``hermes memory setup`` ([] if none): ``key``, ``description``,
optional ``secret`` (goes to .env), ``required``, ``default``, ``choices``, ``type``
(text | integer | number | boolean), ``minimum``/``maximum``/``step``, ``url``,
``env_var`` (explicit secret env var; default auto-generated)."""
return []
def save_config(self, values: Dict[str, Any], hermes_home: str) -> None:
def save_config(self, values: dict[str, Any], hermes_home: str) -> None:
"""Write non-secret setup ``values`` to the provider's native config. Plugins MUST either
override this or use only env vars (every schema field carrying ``env_var``)."""
def on_memory_write(self, action: str, target: str, content: str, metadata: Optional[Dict[str, Any]] = None) -> None:
def on_memory_write(self, action: str, target: str, content: str, metadata: Optional[dict[str, Any]] = None) -> None:
"""Mirror a built-in memory-tool write (``action``: add | replace | remove; ``target``:
memory | user; ``metadata``: provenance such as write_origin, session_id, tool_name).
For replace/remove, ``metadata["previous_content"]`` is the full entry selected
@@ -200,7 +200,7 @@ class MemoryProvider(ABC):
callers may omit this field: ``old_text`` alone is not authoritative identity.
"""
def backup_paths(self) -> List[str]:
def backup_paths(self) -> list[str]:
"""Absolute paths of provider state OUTSIDE HERMES_HOME for ``hermes backup``/``import``
(paths outside the home dir are skipped). MUST work without ``initialize()`` or network."""
return []
+3 -3
View File
@@ -62,7 +62,7 @@ def mint_uid() -> str:
return uuid4().hex
def uid_list(value: Any) -> List[str]:
def uid_list(value: Any) -> list[str]:
"""The unique non-empty string uids of a live list, in order; anything else is ``[]``."""
return list(dict.fromkeys(u for u in (value if isinstance(value, list) else ()) if isinstance(u, str) and u))
@@ -152,7 +152,7 @@ def _uid_occurrences(value: Any) -> list:
return list(value) if isinstance(value, list) else [value]
def per_occurrence_tool_call_uids(uids: Mapping[str, Any], tool_calls: List[Mapping[str, Any]]) -> dict:
def per_occurrence_tool_call_uids(uids: Mapping[str, Any], tool_calls: list[Mapping[str, Any]]) -> dict:
"""*uids* with every provider id this row names more than once spelled out as one uid per occurrence. A
single response that repeats an id shares ONE uid (its results carry it, so every call stays paired);
before a fold appends another turn's occurrences, the shared uid must fill each of this row's slots or
@@ -209,7 +209,7 @@ def resolve_tool_call_uid(index: MutableMapping[str, str], tool_call_id: Any) ->
return None
def tool_call_uid_from_history(messages: List[dict], tool_index: int, owners: dict) -> Optional[str]:
def tool_call_uid_from_history(messages: list[dict], tool_index: int, owners: dict) -> Optional[str]:
"""Resolve a tool-result dict's uid from the nearest preceding assistant dict in ``messages`` that
named its ``tool_call_id`` (the cross-flush case: the assistant row landed in an earlier batch).
``owners`` memoizes each assistant's (named variants, uid index) across one flush's results, so K
+14 -14
View File
@@ -37,7 +37,7 @@ def _is_micro_marker(entry: Any) -> bool:
class MicroCompactionMixin:
"""Rolling micro-compaction; host must be a ``ContextCompressor``."""
def _resolve_compact_cursor(self, messages: List[Dict[str, Any]], head_end: int, tail_start: int) -> int:
def _resolve_compact_cursor(self, messages: list[dict[str, Any]], head_end: int, tail_start: int) -> int:
"""Index of the first message not yet absorbed into the rolling summary: the in-memory
cursor when valid, else just past the last summary marker."""
if head_end < self._micro_compact_cursor < tail_start:
@@ -61,7 +61,7 @@ class MicroCompactionMixin:
return cursor
def _find_one_exchange(
self, messages: List[Dict[str, Any]], start: int, tail_start: int,
self, messages: list[dict[str, Any]], start: int, tail_start: int,
) -> Optional[tuple[int, int]]:
"""Find the next complete exchange (full agent turn) starting at *start*; returns
``(exchange_start, exchange_end)`` or ``None``. Spans assistant+tool rows up to the next
@@ -90,7 +90,7 @@ class MicroCompactionMixin:
return None
return (exchange_start, idx)
def _build_micro_summary_prompt(self, existing_summary: str, exchange_text: str) -> List[Dict[str, str]]:
def _build_micro_summary_prompt(self, existing_summary: str, exchange_text: str) -> list[dict[str, str]]:
"""Build the prompt messages for a single-exchange micro-summary."""
summary_block = existing_summary if existing_summary.strip() else "(No previous summary yet.)"
user_prompt = (
@@ -163,7 +163,7 @@ class MicroCompactionMixin:
"""Return True when the rolling summary is large enough to defrag."""
return estimate_tokens_rough(self._micro_compact_rolling_summary) >= self._micro_compact_defrag_threshold_tokens
def _defrag_rolling_summary(self, messages: List[Dict[str, Any]]) -> bool:
def _defrag_rolling_summary(self, messages: list[dict[str, Any]]) -> bool:
"""Re-summarize the rolling summary text and rewrite the marker in place.
Transcript-shape-neutral (no splice, no cursor move). Returns True when it rewrote."""
old_summary = self._micro_compact_rolling_summary
@@ -200,7 +200,7 @@ class MicroCompactionMixin:
self._micro_compact_consecutive_failures = 0
self._micro_compact_last_failure_cursor = -1
def _micro_compact(self, messages: List[Dict[str, Any]]) -> List[Dict[str, Any]]:
def _micro_compact(self, messages: list[dict[str, Any]]) -> list[dict[str, Any]]:
"""Run one round of micro-compaction (entry point from ``finalize_turn()``). Returns the
(possibly modified) list and syncs the session DB via ``archive_and_compact`` (the
append-only flush alone would double-load on resume)."""
@@ -231,7 +231,7 @@ class MicroCompactionMixin:
_held = list(messages)
_skip, _start_watermark = self._micro_start_watermark(_held)
def _telemetry(outcome: str, result: List[Dict[str, Any]], **extra: Any) -> None:
def _telemetry(outcome: str, result: list[dict[str, Any]], **extra: Any) -> None:
self._emit_micro_compaction_telemetry(
outcome=outcome, messages_before=n_messages, messages_after=len(result),
tokens_before=_tokens_before, duration_ms=int((time.monotonic() - _started_at) * 1000), **extra,
@@ -310,7 +310,7 @@ class MicroCompactionMixin:
self._reset_micro_failure_tracking()
return "exchange_skipped"
def _next_exchange(self, messages: List[Dict[str, Any]]) -> Optional[tuple[int, int]]:
def _next_exchange(self, messages: list[dict[str, Any]]) -> Optional[tuple[int, int]]:
"""The next un-absorbed exchange inside the compressible window, or None."""
compress_start = self._align_boundary_forward(messages, self._protect_head_size(messages))
compress_end = self._find_tail_cut_by_tokens(messages, compress_start, allow_split_turn=False)
@@ -331,7 +331,7 @@ class MicroCompactionMixin:
end = body.find(cc._SUMMARY_END_MARKER)
return (body[:end] if end != -1 else body).strip()
def _cursor_after_splice(self, result: List[Dict[str, Any]], fallback: int) -> int:
def _cursor_after_splice(self, result: list[dict[str, Any]], fallback: int) -> int:
"""Cursor position just past the summary marker in *result*. Must derive from the SPLICED
list: a splice collapses several rows into one marker (and may drop a superseded one), so
pre-splice indices land inside a later exchange and silently skip it."""
@@ -371,7 +371,7 @@ class MicroCompactionMixin:
except Exception as exc:
logger.debug("failed to emit micro-compaction telemetry: %s", exc)
def _micro_start_watermark(self, held: List[Dict[str, Any]]) -> "tuple[Optional[str], Optional[int]]":
def _micro_start_watermark(self, held: list[dict[str, Any]]) -> "tuple[Optional[str], Optional[int]]":
"""``(skip_outcome, watermark)`` taken before a pass's slow step; *skip_outcome* is the telemetry
label of a pass that must not run, or None.
@@ -399,7 +399,7 @@ class MicroCompactionMixin:
return None, watermark
def _sync_micro_compact_to_db(
self, compacted_messages: List[Dict[str, Any]], *, held: Optional[List[Dict[str, Any]]] = None,
self, compacted_messages: list[dict[str, Any]], *, held: Optional[list[dict[str, Any]]] = None,
start_watermark: Optional[int] = None,
) -> bool:
"""Persist the micro-compacted set to the session DB atomically and stamp rows persisted.
@@ -445,8 +445,8 @@ class MicroCompactionMixin:
return True
def _splice_micro_compact_result(
self, messages: List[Dict[str, Any]], splice_start: int, splice_end: int, supersede: bool = True,
) -> List[Dict[str, Any]]:
self, messages: list[dict[str, Any]], splice_start: int, splice_end: int, supersede: bool = True,
) -> list[dict[str, Any]]:
"""Replace *messages[splice_start:splice_end]* with an assistant-role summary marker.
Merges user turns left adjacent by a superseded marker so the result is alternation-valid."""
cc = _cc()
@@ -480,7 +480,7 @@ class MicroCompactionMixin:
cc = _cc()
return f"{cc.SUMMARY_PREFIX}\n\n{cc.HISTORICAL_TASK_HEADING}\n{summary_text.strip()}\n\n{cc._SUMMARY_END_MARKER}"
def _merge_adjacent_user_turns(self, result: List[Dict[str, Any]]) -> List[Dict[str, Any]]:
def _merge_adjacent_user_turns(self, result: list[dict[str, Any]]) -> list[dict[str, Any]]:
"""Merge consecutive plain-text real user turns left by a supersede. Same ``\\n\\n`` join as
``repair_message_sequence`` pass 2, done here so the marker and cursor are never collateral
damage of the downstream repair. Lists untouched."""
@@ -492,7 +492,7 @@ class MicroCompactionMixin:
and isinstance(m.get("content"), str)
)
merged: List[Dict[str, Any]] = []
merged: list[dict[str, Any]] = []
for msg in result:
prev = merged[-1] if merged else None
if _plain_user(msg) and _plain_user(prev):
+69 -69
View File
@@ -60,13 +60,13 @@ def _strip_provider_prefix(model: str) -> str:
return model
_model_metadata_cache: Dict[str, Dict[str, Any]] = {}
_model_metadata_cache: dict[str, dict[str, Any]] = {}
_model_metadata_cache_time: float = 0
_MODEL_CACHE_TTL = 3600
# In-memory memo keyed by (base_url, api-key fingerprint): per-key gateways return a per-key catalog, and
# in a multiplexed process two profiles may share a URL with different keys. The disk memo stays per URL.
_endpoint_model_metadata_cache: Dict[Tuple[str, str], Dict[str, Dict[str, Any]]] = {}
_endpoint_model_metadata_cache_time: Dict[Tuple[str, str], float] = {}
_endpoint_model_metadata_cache: dict[tuple[str, str], dict[str, dict[str, Any]]] = {}
_endpoint_model_metadata_cache_time: dict[tuple[str, str], float] = {}
_ENDPOINT_MODEL_CACHE_TTL = 300
# Server-type verdicts (server_type, monotonic_ts): positive ones live an hour so a
# server swap on the same port is re-detected; None gets the short TTL so a
@@ -78,11 +78,11 @@ _ENDPOINT_PROBE_TTL_SECONDS = 3600.0
# verdict was never cached, so every turn re-probed). Short TTL keeps a transient failure (server starting
# up, key being fixed) recoverable within minutes instead of pinning "undetected" for an hour.
_ENDPOINT_PROBE_FAILURE_TTL_SECONDS = 300.0
_endpoint_probe_path_cache: Dict[str, tuple] = {}
_endpoint_probe_path_cache: dict[str, tuple] = {}
# Routable-but-dead endpoints (corp LAN off-VPN) blackhole TCP: once ANY probe paid
# a full connect timeout, later probes short-circuit for a while.
_ENDPOINT_BLACKHOLE_TTL_SECONDS = 30.0
_endpoint_blackhole_cache: Dict[str, float] = {} # host:port -> monotonic ts
_endpoint_blackhole_cache: dict[str, float] = {} # host:port -> monotonic ts
def _parse_base_url(base_url: str, scheme: str = "http"):
@@ -149,7 +149,7 @@ def _cache_file(name: str) -> Path:
return get_hermes_home() / "cache" / name
def _load_json_dict(path: Path) -> Dict[str, Any]:
def _load_json_dict(path: Path) -> dict[str, Any]:
"""JSON object at ``path``, or {} when missing/invalid."""
try:
with path.open("r", encoding="utf-8-sig") as f:
@@ -211,7 +211,7 @@ def _model_metadata_disk_cache_age_seconds() -> Optional[float]:
return None
def _load_model_metadata_disk_cache() -> Dict[str, Dict[str, Any]]:
def _load_model_metadata_disk_cache() -> dict[str, dict[str, Any]]:
"""Processed OpenRouter metadata cache from disk ({} on any failure)."""
try:
with _get_model_metadata_cache_path().open("r", encoding="utf-8-sig") as f:
@@ -222,7 +222,7 @@ def _load_model_metadata_disk_cache() -> Dict[str, Dict[str, Any]]:
return {}
def _save_model_metadata_disk_cache(data: Dict[str, Dict[str, Any]]) -> None:
def _save_model_metadata_disk_cache(data: dict[str, dict[str, Any]]) -> None:
try:
atomic_json_write(_get_model_metadata_cache_path(), data, indent=0, separators=(",", ":"))
except Exception as e:
@@ -234,7 +234,7 @@ def _get_endpoint_metadata_cache_path() -> Path:
return _cache_file("endpoint_model_metadata.json")
def _endpoint_disk_cache_get(normalized: str) -> Optional[Dict[str, Dict[str, Any]]]:
def _endpoint_disk_cache_get(normalized: str) -> Optional[dict[str, dict[str, Any]]]:
"""Fresh cross-process memo of a remote ``/models`` probe (same TTL as in-memory): one-shot
runs (``hermes -q``, cron) start cold and Nous bypasses the persistent context cache, so
without this every launch paid the live probe. Local endpoints are never memoized."""
@@ -242,7 +242,7 @@ def _endpoint_disk_cache_get(normalized: str) -> Optional[Dict[str, Dict[str, An
return models if isinstance(models, dict) else None
def _endpoint_disk_cache_put(normalized: str, cache: Dict[str, Dict[str, Any]]) -> None:
def _endpoint_disk_cache_put(normalized: str, cache: dict[str, dict[str, Any]]) -> None:
"""Memoize a successful remote ``/models`` probe; expired siblings are dropped."""
_ttl_memo_put(
_get_endpoint_metadata_cache_path(), normalized, cache, _ENDPOINT_MODEL_CACHE_TTL,
@@ -273,7 +273,7 @@ MINIMUM_CONTEXT_LENGTH = 64_000
# In-process (model, base_url) -> (result, monotonic_ts) memo for local probes: one
# startup resolves the same model several times (banner, /model, compressor). Never persisted.
_LOCAL_CTX_PROBE_TTL_SECONDS = 30.0
_LOCAL_CTX_PROBE_CACHE: Dict[tuple, tuple] = {}
_LOCAL_CTX_PROBE_CACHE: dict[tuple, tuple] = {}
# In-process (model, region) -> monotonic_ts memo of a FAILED Bedrock context probe. That probe pads
# prompts of 1.3M/2.2M tokens and attempts up to two converse calls (agent/bedrock_adapter.py
# _BEDROCK_PROBE_TIERS; the second tier is sent only when the first yields no parseable limit, i.e. on
@@ -283,7 +283,7 @@ _LOCAL_CTX_PROBE_CACHE: Dict[tuple, tuple] = {}
# reasoning as _ENDPOINT_PROBE_FAILURE_TTL_SECONDS: a failure is usually transient (expired SSO
# session, offline box), so it must expire rather than stick.
_BEDROCK_PROBE_FAILURE_TTL_SECONDS = 300.0
_BEDROCK_PROBE_FAILURE_CACHE: Dict[tuple, float] = {}
_BEDROCK_PROBE_FAILURE_CACHE: dict[tuple, float] = {}
# Family-pattern fallbacks, used only when provider-aware sources all miss.
# Lookups are longest-key-first substring matches, so dict order is cosmetic
# and a specific key must be STRICTLY longer than its catch-all.
@@ -425,7 +425,7 @@ def _normalize_base_url(base_url: str) -> str:
return (base_url or "").strip().rstrip("/")
def _auth_headers(api_key: object = "") -> Dict[str, str]:
def _auth_headers(api_key: object = "") -> dict[str, str]:
from agent.command_token_source import materialize_probe_api_key
token = materialize_probe_api_key(api_key)
return {"Authorization": f"Bearer {token}"} if token else {}
@@ -439,7 +439,7 @@ def _is_custom_endpoint(base_url: str) -> bool:
# Host substring -> provider. ".githubcopilot.com" covers api.enterprise./api.business.
# hosts; models.inference.ai.azure.com (GitHub Models free tier, ~8K per-request cap)
# is mapped so a targeted hint fires instead of the custom-endpoint path.
_URL_TO_PROVIDER: Dict[str, str] = {
_URL_TO_PROVIDER: dict[str, str] = {
"api.openai.com": "openai", "chatgpt.com": "openai", "api.anthropic.com": "anthropic",
"api.z.ai": "zai", "open.bigmodel.cn": "zai",
"api.moonshot.ai": "kimi-coding", "api.moonshot.cn": "kimi-coding-cn", "api.kimi.com": "kimi-coding",
@@ -556,7 +556,7 @@ def _catalog_key_matches(key: str, model_lower: str) -> bool:
return key in model_lower or _normalize_model_version(key) in _normalize_model_version(model_lower)
def _longest_key_match(table: Dict[str, int], model_lower: str) -> Optional[Tuple[str, int]]:
def _longest_key_match(table: dict[str, int], model_lower: str) -> Optional[tuple[str, int]]:
"""First ``(key, value)`` whose key is a substring of ``model_lower``, longest key first so
specific entries (``gpt-5.4-mini``) beat their family catch-all (``gpt-5``); ties keep table order."""
for key, value in sorted(table.items(), key=lambda x: len(x[0]), reverse=True):
@@ -565,7 +565,7 @@ def _longest_key_match(table: Dict[str, int], model_lower: str) -> Optional[Tupl
return None
def _ollama_show_context(data: Dict[str, Any], *, gguf_first: bool, minimum: Optional[int] = None) -> Optional[int]:
def _ollama_show_context(data: dict[str, Any], *, gguf_first: bool, minimum: Optional[int] = None) -> Optional[int]:
"""Context length from an Ollama ``/api/show`` payload. ``parameters.num_ctx`` is the RUNTIME
window (Modelfile override), ``model_info.*.context_length`` the GGUF training max. Local users
control num_ctx (prefer it); hosted operators may cap it arbitrarily (``gguf_first``)."""
@@ -795,7 +795,7 @@ def _coerce_reasonable_int(value: Any, minimum: int = 1024, maximum: int = 10_00
return result if minimum <= result <= maximum else None
def _extract_first_int(payload: Dict[str, Any], keys: tuple[str, ...]) -> Optional[int]:
def _extract_first_int(payload: dict[str, Any], keys: tuple[str, ...]) -> Optional[int]:
keyset = {key.lower() for key in keys}
for mapping in _iter_nested_dicts(payload):
for key, value in mapping.items():
@@ -806,13 +806,13 @@ def _extract_first_int(payload: Dict[str, Any], keys: tuple[str, ...]) -> Option
return None
def _extract_flat_context_length(payload: Dict[str, Any]) -> Optional[int]:
def _extract_flat_context_length(payload: dict[str, Any]) -> Optional[int]:
"""Top-level-only context WINDOW read (no nested walk, so an unrelated nested section can't leak
a same-named key). ``max_tokens`` is NOT a window key: on OpenAI-compatible passthroughs it is the max OUTPUT."""
return next((c for c in map(_coerce_reasonable_int, (payload.get(k) for k in _CONTEXT_LENGTH_KEYS)) if c is not None), None)
def _context_length_from_model_payload(payload: Dict[str, Any]) -> Optional[int]:
def _context_length_from_model_payload(payload: dict[str, Any]) -> Optional[int]:
"""Context window from a ``/v1/models`` object: window keys first, ``max_tokens`` last (Anthropic
payloads carry ``max_input_tokens`` = 1M window AND ``max_tokens`` = 128k OUTPUT cap)."""
if not isinstance(payload, dict):
@@ -834,10 +834,10 @@ _PER_MILLION_QUOTE_MIN = 0.001
_TOKEN_RATE_FIELDS = ("prompt", "completion", "cache_read", "cache_write")
def _normalize_token_rates(pricing: Dict[str, Any], unit: Any) -> Dict[str, Any]:
def _normalize_token_rates(pricing: dict[str, Any], unit: Any) -> dict[str, Any]:
"""Rescale the generic path's token rates to per-token strings (the contract usage_pricing
multiplies by 1e6), the way the Novita/DeepInfra branches already do for their known units."""
rates: Dict[str, float] = {}
rates: dict[str, float] = {}
for key in _TOKEN_RATE_FIELDS:
try:
rates[key] = float(pricing[key])
@@ -851,8 +851,8 @@ def _normalize_token_rates(pricing: Dict[str, Any], unit: Any) -> Dict[str, Any]
return pricing
def _extract_pricing(payload: Dict[str, Any]) -> Dict[str, Any]:
def _per_token(source: Dict[str, Any], fields: Dict[str, str], scale) -> Dict[str, Any]:
def _extract_pricing(payload: dict[str, Any]) -> dict[str, Any]:
def _per_token(source: dict[str, Any], fields: dict[str, str], scale) -> dict[str, Any]:
# Provider $/MTok (or Novita's 1/10_000-$ per M) -> per-token strings, the same path usage_pricing uses for OpenRouter.
return {target: str(scale(float(source[key]))) for target, key in fields.items() if source.get(key) is not None}
novita_fields = {"prompt": "input_token_price_per_m", "completion": "output_token_price_per_m"}
@@ -873,7 +873,7 @@ def _extract_pricing(payload: Dict[str, Any]) -> Dict[str, Any]:
}
for mapping in _iter_nested_dicts(payload):
normalized = {str(key).lower(): value for key, value in mapping.items()}
pricing: Dict[str, Any] = {}
pricing: dict[str, Any] = {}
for target, aliases in alias_map.items():
for alias in aliases:
if alias in normalized and normalized[alias] not in {None, ""}:
@@ -884,13 +884,13 @@ def _extract_pricing(payload: Dict[str, Any]) -> Dict[str, Any]:
return {}
def _add_model_aliases(cache: Dict[str, Dict[str, Any]], model_id: str, entry: Dict[str, Any]) -> None:
def _add_model_aliases(cache: dict[str, dict[str, Any]], model_id: str, entry: dict[str, Any]) -> None:
cache[model_id] = entry
if "/" in model_id:
cache.setdefault(model_id.split("/", 1)[1], entry)
def fetch_model_metadata(force_refresh: bool = False) -> Dict[str, Dict[str, Any]]:
def fetch_model_metadata(force_refresh: bool = False) -> dict[str, dict[str, Any]]:
"""Fetch model metadata from OpenRouter (cached for 1 hour)."""
global _model_metadata_cache, _model_metadata_cache_time
if not force_refresh:
@@ -938,13 +938,13 @@ def fetch_model_metadata(force_refresh: bool = False) -> Dict[str, Dict[str, Any
return {}
def _endpoint_model_entry(model: Dict[str, Any], model_id: str, context_length: Optional[int]) -> Dict[str, Any]:
def _endpoint_model_entry(model: dict[str, Any], model_id: str, context_length: Optional[int]) -> dict[str, Any]:
"""Cache entry for one ``/models`` item; optional keys are set only when known."""
optional = (("context_length", context_length), ("max_completion_tokens", _extract_first_int(model, _MAX_COMPLETION_KEYS)), ("pricing", _extract_pricing(model) or None))
return {"name": model.get("name", model_id), **{k: v for k, v in optional if v is not None}}
def _lmstudio_loaded_context(model: Dict[str, Any]) -> Optional[int]:
def _lmstudio_loaded_context(model: dict[str, Any]) -> Optional[int]:
"""Context of the first loaded LM Studio instance (the runtime value), else None."""
for inst in model.get("loaded_instances", []) or []:
cfg = inst.get("config", {}) if isinstance(inst, dict) else None
@@ -954,11 +954,11 @@ def _lmstudio_loaded_context(model: Dict[str, Any]) -> Optional[int]:
return None
def _lmstudio_native_models(normalized: str, headers: Dict[str, str]) -> Dict[str, Dict[str, Any]]:
def _lmstudio_native_models(normalized: str, headers: dict[str, str]) -> dict[str, dict[str, Any]]:
"""LM Studio ``/api/v1/models`` → cache; context comes from the first loaded instance."""
response = model_metadata_http.get(_lmstudio_server_root(normalized).rstrip("/") + "/api/v1/models", headers=headers, timeout=(5, 10), verify=model_metadata_http.resolve_verify(normalized))
response.raise_for_status()
cache: Dict[str, Dict[str, Any]] = {}
cache: dict[str, dict[str, Any]] = {}
for model in response.json().get("models", []):
model_id = (model.get("key") or model.get("id")) if isinstance(model, dict) else None
if not model_id:
@@ -971,7 +971,7 @@ def _lmstudio_native_models(normalized: str, headers: Dict[str, str]) -> Dict[st
return cache
def _apply_llamacpp_props(cache: Dict[str, Dict[str, Any]], request_candidate: str, headers: Dict[str, str], verify) -> None:
def _apply_llamacpp_props(cache: dict[str, dict[str, Any]], request_candidate: str, headers: dict[str, str], verify) -> None:
"""Overwrite ``context_length`` with llama.cpp's allocated ``n_ctx`` from /props (``/v1/props``, then
``/props`` for older builds). In router mode the bare endpoint 400s, so each LOADED child is read
via ``/props?model=``; unloaded children are skipped — probing could autoload them."""
@@ -981,7 +981,7 @@ def _apply_llamacpp_props(cache: Dict[str, Dict[str, Any]], request_candidate: s
if resp.is_error:
resp = model_metadata_http.get(base + "/props", params=params, headers=headers, timeout=5, verify=verify)
return resp
def _n_ctx(props: Dict[str, Any]) -> Any:
def _n_ctx(props: dict[str, Any]) -> Any:
return (props.get("default_generation_settings") or {}).get("n_ctx")
props_resp = _props()
if not props_resp.is_error:
@@ -1003,20 +1003,20 @@ def _apply_llamacpp_props(cache: Dict[str, Dict[str, Any]], request_candidate: s
cache[child_id]["context_length"] = child_ctx
def _endpoint_memo_key(normalized: str, api_key: object) -> Tuple[str, str]:
def _endpoint_memo_key(normalized: str, api_key: object) -> tuple[str, str]:
from agent.credential_persistence import fingerprint_secret_value
# Callable (minted) keys are not fingerprinted here: doing so would mint on every cache hit.
return normalized, (fingerprint_secret_value(api_key) or "") if isinstance(api_key, str) else ""
def _remember_endpoint_models(memo_key: Tuple[str, str], cache: Dict[str, Dict[str, Any]]) -> Dict[str, Dict[str, Any]]:
def _remember_endpoint_models(memo_key: tuple[str, str], cache: dict[str, dict[str, Any]]) -> dict[str, dict[str, Any]]:
_endpoint_model_metadata_cache[memo_key] = cache
_endpoint_model_metadata_cache_time[memo_key] = time.time()
return cache
def _parse_models_payload(payload: Dict[str, Any]) -> Dict[str, Dict[str, Any]]:
cache: Dict[str, Dict[str, Any]] = {}
def _parse_models_payload(payload: dict[str, Any]) -> dict[str, dict[str, Any]]:
cache: dict[str, dict[str, Any]] = {}
for model in payload.get("data", []):
model_id = model.get("id") if isinstance(model, dict) else None
if model_id:
@@ -1024,7 +1024,7 @@ def _parse_models_payload(payload: Dict[str, Any]) -> Dict[str, Dict[str, Any]]:
return cache
def fetch_endpoint_model_metadata(base_url: str, api_key: str = "", force_refresh: bool = False) -> Dict[str, Dict[str, Any]]:
def fetch_endpoint_model_metadata(base_url: str, api_key: str = "", force_refresh: bool = False) -> dict[str, dict[str, Any]]:
"""Model metadata from an OpenAI-compatible ``/models`` endpoint (cached per base URL)."""
normalized = _normalize_base_url(base_url)
if not normalized or base_url_host_matches(normalized, "openrouter.ai"):
@@ -1121,12 +1121,12 @@ def _load_context_cache_document() -> dict:
return {}
def _load_context_cache() -> Dict[str, int]:
def _load_context_cache() -> dict[str, int]:
"""Load scalar lengths, preserving the legacy reader contract."""
return _load_context_cache_document().get("context_lengths") or {}
def _write_context_cache(cache: Dict[str, int], bedrock_confirmed: dict | None = None) -> None:
def _write_context_cache(cache: dict[str, int], bedrock_confirmed: dict | None = None) -> None:
"""Atomic write (a truncating write killed mid-dump leaves a partial file that
_load_context_cache() swallows as {}, wiping EVERY cached length). Raises on failure."""
document = {"context_lengths": cache}
@@ -1390,7 +1390,7 @@ _PARSEABLE_OUTPUT_CAP_SIGNALS = (
)
def _sglang_window_and_input(error_lower: str) -> Optional[Tuple[int, int]]:
def _sglang_window_and_input(error_lower: str) -> Optional[tuple[int, int]]:
"""``(window, input_tokens)`` from SGLang's wording, else None; both figures are explicit there."""
_m_ctx = re.search(r'maximum context length of (\d+)\s*token', error_lower)
_m_in = re.search(r'(\d+)\s*tokens from the input messages', error_lower)
@@ -1426,7 +1426,7 @@ def _model_id_matches(candidate_id: str, lookup_model: str) -> bool:
return candidate_id == lookup_model or ("/" in candidate_id and candidate_id.rsplit("/", 1)[1] == lookup_model)
def _ollama_show(server_url: str, api_key: str, bare_model: str, timeout: float = 3.0, *, note_blackhole: bool = False) -> Optional[Dict[str, Any]]:
def _ollama_show(server_url: str, api_key: str, bare_model: str, timeout: float = 3.0, *, note_blackhole: bool = False) -> Optional[dict[str, Any]]:
"""Ollama ``/api/show`` JSON for ``bare_model``, or None on any failure (``note_blackhole``: connect timeouts condemn the host)."""
import httpx
try:
@@ -1692,7 +1692,7 @@ def _query_anthropic_context_length(model: str, base_url: str, api_key: Any) ->
# Codex OAuth `context_window` values (what Codex enforces — lower than the direct API for the same
# slugs). Fallback when the live probe fails; longest-key-first. gpt-5.3-codex-spark is listed so "gpt-5.3-codex" doesn't win.
_CODEX_OAUTH_CONTEXT_FALLBACK: Dict[str, int] = {
_CODEX_OAUTH_CONTEXT_FALLBACK: dict[str, int] = {
"gpt-6-astra": 272_000, "gpt-6.1-sol": 272_000, "gpt-6-sol": 272_000, "gpt-6-luna": 272_000,
"gpt-5.1-codex-max": 272_000, "gpt-5.1-codex-mini": 272_000, "gpt-5.3-codex": 272_000,
"gpt-5.3-codex-spark": 128_000, "gpt-5.2-codex": 272_000, "gpt-5.4-mini": 272_000,
@@ -1707,10 +1707,10 @@ _CODEX_OAUTH_CONTEXT_FALLBACK: Dict[str, int] = {
# aren't routable on Codex); ``gpt-5.4`` is EXACT because gpt-5.4-mini enforces 272K. The gpt-6 tiers
# carry the 5.6 verdict forward: they replace Sol/Terra/Luna on the same Codex route, and the live
# catalog's ``max_context_window`` still caps the bump (#105443) if it publishes a lower ceiling.
_CODEX_OAUTH_VERIFIED_ABOVE_ADVERTISED_PREFIXES: Dict[str, int] = {
_CODEX_OAUTH_VERIFIED_ABOVE_ADVERTISED_PREFIXES: dict[str, int] = {
"gpt-5.6": 900_000, "gpt-6-sol": 900_000, "gpt-6-luna": 900_000,
}
_CODEX_OAUTH_VERIFIED_ABOVE_ADVERTISED_EXACT: Dict[str, int] = {
_CODEX_OAUTH_VERIFIED_ABOVE_ADVERTISED_EXACT: dict[str, int] = {
"gpt-5.4": 900_000, "gpt-daybreak-blue-latest": 900_000,
"gpt-6-astra": 900_000, # advertised 272K; 920,043 input OK, 1,000,043 rejected (live 2026-09-04)
# advertised 272K; 918,137 input OK, ~931K rejected (live 2026-09-29), consistent with the 922K left by
@@ -1782,10 +1782,10 @@ def _verified_codex_ctx_for_slug(model_bare: str) -> Optional[int]:
return next((ctx for key, ctx in _CODEX_OAUTH_VERIFIED_ABOVE_ADVERTISED_PREFIXES.items() if base == key or base.startswith((key + "-", key + "."))), None)
_codex_oauth_context_cache: Dict[str, Tuple[Dict[str, int], float]] = {}
_codex_oauth_context_cache: dict[str, tuple[dict[str, int], float]] = {}
# ``{slug: max_context_window}`` from the same fetch, keyed by the same token fingerprint. Only the
# opted-in ``-900k`` bump reads it (#105443); a catalog without the field leaves the entry empty.
_codex_oauth_max_context_cache: Dict[str, Dict[str, int]] = {}
_codex_oauth_max_context_cache: dict[str, dict[str, int]] = {}
_CODEX_OAUTH_CONTEXT_CACHE_TTL = 3600 # 1 hour
_CODEX_OAUTH_CONTEXT_NEGATIVE_TTL = 300 # a probe that found no catalog is retried after 5 minutes; must stay < TTL
# The Codex models endpoint reads ``client_version`` as a Codex CLI compatibility version and
@@ -1801,7 +1801,7 @@ CODEX_NEWEST_CLIENT_VERSION = "99.0.0"
CODEX_UNGATED_CLIENT_VERSION = "0.0.0"
def _codex_catalog_urls(base_url: str = "") -> Tuple[str, ...]:
def _codex_catalog_urls(base_url: str = "") -> tuple[str, ...]:
"""Catalog URLs against ``base_url`` when it names a Codex-compatible gateway, else the
hard-coded endpoint. A custom base's credential belongs to that service — sending it to
chatgpt.com (or fetching the direct catalog behind a gateway's back) is wrong (#121486)."""
@@ -1833,7 +1833,7 @@ def _codex_catalog_probe_allowed(access_token: str, base_url: str = "") -> bool:
return bool(_decode_jwt_claims(access_token))
def fetch_codex_catalog_entries(get: Callable[[str], Any], base_url: str = "") -> Tuple[List[Any], Optional[int]]:
def fetch_codex_catalog_entries(get: Callable[[str], Any], base_url: str = "") -> tuple[list[Any], Optional[int]]:
"""``(models, last_status)`` from the first catalog URL that answers HTTP 200 with a non-empty
``models`` list; ``get(url)`` is any client returning an object with ``status_code``/``json()``.
An empty or non-200 answer on the newest-client URL falls through to the ``0.0.0`` sentinel."""
@@ -1853,7 +1853,7 @@ def fetch_codex_catalog_entries(get: Callable[[str], Any], base_url: str = "") -
def _codex_oauth_token_fingerprint(access_token: str, base_url: str = "") -> str:
"""Non-secret cache key for a Codex OAuth access token (plus the base it was probed against —
a gateway's catalog can differ from chatgpt.com's for the same forwarded token)."""
return hashlib.sha256(f"{access_token}\n{(base_url or '').strip().rstrip('/')}".encode("utf-8")).hexdigest()[:16]
return hashlib.sha256(f"{access_token}\n{(base_url or '').strip().rstrip('/')}".encode()).hexdigest()[:16]
def _remember_no_codex_catalog(cache_key: str) -> None:
@@ -1869,7 +1869,7 @@ def _remember_no_codex_catalog(cache_key: str) -> None:
_codex_oauth_context_cache[cache_key] = ({}, now - _CODEX_OAUTH_CONTEXT_CACHE_TTL + _CODEX_OAUTH_CONTEXT_NEGATIVE_TTL)
def _fetch_codex_oauth_context_lengths_with_source(access_token: str, base_url: str = "") -> Tuple[Dict[str, int], bool]:
def _fetch_codex_oauth_context_lengths_with_source(access_token: str, base_url: str = "") -> tuple[dict[str, int], bool]:
"""Codex catalogue ``{slug: context_window}`` plus whether it came from HTTP. Cached per token
fingerprint (windows vary by entitlement); ``max_context_window`` lands in
``_codex_oauth_max_context_cache`` under the same key. An in-process hit reports False: not a
@@ -1898,8 +1898,8 @@ def _fetch_codex_oauth_context_lengths_with_source(access_token: str, base_url:
logger.debug("Codex /models probe failed: %s", exc)
_remember_no_codex_catalog(cache_key)
return {}, False
result: Dict[str, int] = {}
max_result: Dict[str, int] = {}
result: dict[str, int] = {}
max_result: dict[str, int] = {}
for item in entries:
slug, ctx, max_ctx = (item.get("slug"), item.get("context_window"), item.get("max_context_window")) if isinstance(item, dict) else (None, None, None)
if isinstance(slug, str) and isinstance(ctx, int) and ctx > 0:
@@ -1926,14 +1926,14 @@ def _cached_codex_catalog_max(access_token: str, base_url: str, slug: str) -> Op
return published.get(_codex_catalog_key(published, slug))
def _resolve_codex_oauth_context_length_with_source(model: str, access_token: str = "", base_url: str = "") -> Tuple[Optional[int], str]:
def _resolve_codex_oauth_context_length_with_source(model: str, access_token: str = "", base_url: str = "") -> tuple[Optional[int], str]:
"""``(context_length, source)`` for a Codex OAuth slug. source: "live" (fresh authenticated probe —
the only one eligible for persistent writes), "memory" (same-token in-process hit), "fallback"
(static table), or "" when unresolved."""
model_bare = _strip_provider_prefix(model).strip()
if not model_bare:
return None, ""
def _apply_verified_bump(ctx: int, source: str, catalog_max: Optional[int] = None) -> Tuple[int, str]:
def _apply_verified_bump(ctx: int, source: str, catalog_max: Optional[int] = None) -> tuple[int, str]:
"""Lift an EXACT stale 272K advertisement to the verified cap for opted-in ``-900k`` variants only,
never above the catalog's own ``max_context_window`` when it publishes one (#105443)."""
bumped = _verified_codex_ctx_for_slug(model_bare)
@@ -1960,7 +1960,7 @@ def _resolve_codex_oauth_context_length_with_source(model: str, access_token: st
return _apply_verified_bump(hit[1], "fallback", catalog_max) if hit else (None, "")
def _resolve_nous_context_length(model: str, base_url: str = "", api_key: str = "") -> Tuple[Optional[int], str]:
def _resolve_nous_context_length(model: str, base_url: str = "", api_key: str = "") -> tuple[Optional[int], str]:
"""``(context_length, source)`` for a Nous Portal model: portal /v1/models is authoritative
("portal"). Fallback matches OR's prefixed ids against the bare Nous id with dot/dash
normalisation ("openrouter" — callers must NOT persist it, or a portal blip freezes the wrong value)."""
@@ -2400,7 +2400,7 @@ def estimate_tokens_rough(text: str) -> int:
return dense + ((len(stripped.encode("utf-8", "replace")) + 3) // CHARS_PER_TOKEN)
def estimate_messages_tokens_rough(messages: List[Dict[str, Any]], *, charge_stale_thinking: bool = True) -> int:
def estimate_messages_tokens_rough(messages: list[dict[str, Any]], *, charge_stale_thinking: bool = True) -> int:
"""Rough token estimate for a message list (pre-flight only). Images cost the per-image price
learned from provider usage (``agent.image_token_cost``; flat default before calibration)
rather than their base64 length. ``charge_stale_thinking=False`` mirrors the tail-budget
@@ -2415,7 +2415,7 @@ def estimate_messages_tokens_rough(messages: List[Dict[str, Any]], *, charge_sta
return sum(_estimate_message_tokens_cached(msg, image_cost) for msg in messages)
def estimate_native_anthropic_messages_tokens_rough(messages: List[Dict[str, Any]]) -> int:
def estimate_native_anthropic_messages_tokens_rough(messages: list[dict[str, Any]]) -> int:
"""Estimate native Anthropic messages with replayed readable thinking charged exactly once."""
from agent.message_sanitization import native_anthropic_accounting_projection
@@ -2426,8 +2426,8 @@ def estimate_native_anthropic_messages_tokens_rough(messages: List[Dict[str, Any
def estimate_native_anthropic_request_tokens_rough(
messages: List[Dict[str, Any]], *, system_prompt: str = "",
tools: Optional[List[Dict[str, Any]]] = None,
messages: list[dict[str, Any]], *, system_prompt: str = "",
tools: Optional[list[dict[str, Any]]] = None,
) -> int:
"""Request estimate for native Anthropic; opaque replay bytes never enter text accounting."""
from agent.message_sanitization import native_anthropic_accounting_projection
@@ -2443,7 +2443,7 @@ def estimate_native_anthropic_request_tokens_rough(
_STALE_THINKING_ESTIMATE_KEYS = ("reasoning", "reasoning_content")
def _strip_stale_thinking_for_estimate(messages: List[Dict[str, Any]]) -> List[Dict[str, Any]]:
def _strip_stale_thinking_for_estimate(messages: list[dict[str, Any]]) -> list[dict[str, Any]]:
"""Copy of ``messages`` with stale thinking keys removed (newest kept). Shallow stripped copies
share the original value objects, so the per-message memo still hits for the stripped shape."""
def _is_assistant(m: Any) -> bool:
@@ -2470,7 +2470,7 @@ def _strip_stale_thinking_for_estimate(messages: List[Dict[str, Any]]) -> List[D
# estimate. Because the api_messages build shallow-copies history dicts each iteration, the copies share the
# same content strings — so unchanged history messages hit the memo even though the outer dicts are fresh
# objects every turn.
_MSG_TOKENS_CACHE: Dict[Any, Tuple[list, int, int]] = {} # pins, text tokens, image count
_MSG_TOKENS_CACHE: dict[Any, tuple[list, int, int]] = {} # pins, text tokens, image count
_MSG_TOKENS_CACHE_MAX = 4096
@@ -2493,7 +2493,7 @@ def _msg_fingerprint(value: Any, pins: list) -> Any:
def _estimate_message_tokens_cached(msg: Any, image_cost: int) -> int:
"""Text tokens + images x ``image_cost``; the memo holds text and image COUNT so a recalibrated
per-image price re-prices cached rows without invalidating them."""
def _compute() -> Tuple[int, int]:
def _compute() -> tuple[int, int]:
return _estimate_message_tokens_without_images(msg), _count_image_tokens(msg, 1)
try:
pins: list = []
@@ -2526,7 +2526,7 @@ def _count_parts(parts: Any, types: set) -> int:
_IMAGE_PART_TYPES = frozenset({"image", "image_url", "input_image"})
def _count_image_tokens(msg: Dict[str, Any], cost_per_image: int) -> int:
def _count_image_tokens(msg: dict[str, Any], cost_per_image: int) -> int:
"""Count image-like content parts in a message; return their token cost."""
if not isinstance(msg, dict):
return 0
@@ -2554,7 +2554,7 @@ def strip_opaque_replay_items(items: Any) -> Any:
]
def _wire_message_shadow(msg: Dict[str, Any]) -> Dict[str, Any]:
def _wire_message_shadow(msg: dict[str, Any]) -> dict[str, Any]:
"""Shadow of a message holding only what the provider actually receives.
* ``api_content`` SUBSTITUTES ``content`` (mirrors ``turn_context.substitute_api_content`` exactly):
only a non-empty STRING sidecar on a user/assistant row displaces content; substituting any
@@ -2570,7 +2570,7 @@ def _wire_message_shadow(msg: Dict[str, Any]) -> Dict[str, Any]:
sidecar_wins = isinstance(sidecar, str) and bool(sidecar) and msg.get("role") in ("user", "assistant")
_rc = msg.get("reasoning_content")
drop_reasoning_dup = isinstance(_rc, str) and bool(_rc.strip())
shadow: Dict[str, Any] = {}
shadow: dict[str, Any] = {}
for k, v in msg.items():
if k in ("_anthropic_content_blocks", "reasoning_details") or k in PERSISTENCE_ONLY_MESSAGE_FIELDS or (k == "reasoning" and drop_reasoning_dup):
continue
@@ -2607,13 +2607,13 @@ def _wire_message_shadow(msg: Dict[str, Any]) -> Dict[str, Any]:
return shadow
def _estimate_message_tokens_without_images(msg: Dict[str, Any]) -> int:
def _estimate_message_tokens_without_images(msg: dict[str, Any]) -> int:
"""Token estimate for a message shadow with image payloads stripped."""
return estimate_tokens_rough(str(_wire_message_shadow(msg) if isinstance(msg, dict) else msg))
def estimate_request_tokens_rough(
messages: List[Dict[str, Any]], *, system_prompt: str = "", tools: Optional[List[Dict[str, Any]]] = None, charge_stale_thinking: bool = True,
messages: list[dict[str, Any]], *, system_prompt: str = "", tools: Optional[list[dict[str, Any]]] = None, charge_stale_thinking: bool = True,
) -> int:
"""Rough token estimate for a full request: system prompt + messages + tool schemas (50+ tools
add 20-30K on their own). ``charge_stale_thinking`` is forwarded — pass False when the route
@@ -2629,7 +2629,7 @@ def estimate_request_tokens_rough(
# Keyed by ``id(tools)``; bounded, oldest-first eviction. Repeated ``str(tools)`` on
# large schemas stalls GUI event loops under GIL pressure.
_TOOLS_TOKENS_CACHE: dict[int, Tuple[int, str, str, int]] = {}
_TOOLS_TOKENS_CACHE: dict[int, tuple[int, str, str, int]] = {}
_TOOLS_TOKENS_CACHE_MAX = 256
@@ -2642,7 +2642,7 @@ def _tool_name_for_cache(tool: Any) -> str:
return name if isinstance(name, str) else ""
def _estimate_tools_tokens_rough(tools: List[Dict[str, Any]]) -> int:
def _estimate_tools_tokens_rough(tools: list[dict[str, Any]]) -> int:
if not tools:
return 0
key = id(tools)
+55 -55
View File
@@ -28,7 +28,7 @@ MODELS_DEV_URL = "https://models.dev/api.json"
_MODELS_DEV_CACHE_TTL = 4 * 3600 # 4 hours — ETag conditional GET makes refresh cheap
_MODELS_DEV_RETRY_DELAY = 300 # 5 minutes after a failed refresh
# In-memory cache
_models_dev_cache: Dict[str, Any] = {}
_models_dev_cache: dict[str, Any] = {}
_models_dev_cache_time: float = 0
_models_dev_retry_after: float = 0
_models_dev_fetch_lock = threading.Lock()
@@ -51,8 +51,8 @@ class ModelInfo:
structured_output: bool = False
open_weights: bool = False
# Modalities
input_modalities: Tuple[str, ...] = () # ("text", "image", "pdf", ...)
output_modalities: Tuple[str, ...] = ()
input_modalities: tuple[str, ...] = () # ("text", "image", "pdf", ...)
output_modalities: tuple[str, ...] = ()
# Limits
context_window: int = 0
max_output: int = 0
@@ -89,7 +89,7 @@ class ProviderInfo:
"""Full metadata for a provider from models.dev."""
id: str # models.dev provider ID
name: str # display name
env: Tuple[str, ...] # env var names for API key
env: tuple[str, ...] # env var names for API key
api: str # base URL
doc: str = "" # documentation URL
model_count: int = 0
@@ -107,7 +107,7 @@ class ModelCapabilities:
# Hermes provider names → models.dev provider IDs
PROVIDER_TO_MODELS_DEV: Dict[str, str] = {
PROVIDER_TO_MODELS_DEV: dict[str, str] = {
"openrouter": "openrouter", "novita": "novita-ai", "anthropic": "anthropic",
"openai": "openai", "openai-api": "openai", "openai-codex": "openai", "zai": "zai",
"kimi": "kimi-for-coding", "kimi-coding": "kimi-for-coding",
@@ -130,10 +130,10 @@ PROVIDER_TO_MODELS_DEV: Dict[str, str] = {
"ollama-cloud": "ollama-cloud",
}
# Reverse mapping: models.dev id → Hermes ids (built lazily; many-to-one).
_MODELS_DEV_TO_PROVIDER: Optional[Dict[str, List[str]]] = None
_MODELS_DEV_TO_PROVIDER: Optional[dict[str, list[str]]] = None
def _models_dev_to_hermes_ids(mdev_id: str) -> List[str]:
def _models_dev_to_hermes_ids(mdev_id: str) -> list[str]:
"""Return the Hermes provider ids that map to *mdev_id* (may be [])."""
global _MODELS_DEV_TO_PROVIDER
if _MODELS_DEV_TO_PROVIDER is None:
@@ -143,12 +143,12 @@ def _models_dev_to_hermes_ids(mdev_id: str) -> List[str]:
return _MODELS_DEV_TO_PROVIDER.get(mdev_id, [])
def _dict_or_empty(value: Any) -> Dict[str, Any]:
def _dict_or_empty(value: Any) -> dict[str, Any]:
return value if isinstance(value, dict) else {}
def _configured_catalog_provider(
provider: str, *, config: Optional[Dict[str, Any]] = None,
provider: str, *, config: Optional[dict[str, Any]] = None,
) -> Optional[str]:
"""``catalog_provider`` declared on a custom provider's ``providers.<name>`` row (or legacy
``custom_providers[]`` entry): the catalogued vendor whose models it resells. None when unset."""
@@ -176,7 +176,7 @@ def _configured_catalog_provider(
def _models_dev_id(
provider: str, *, config: Optional[Dict[str, Any]] = None,
provider: str, *, config: Optional[dict[str, Any]] = None,
) -> Optional[str]:
"""models.dev provider id for a Hermes provider id, or None. A custom provider reaches the
catalog only through its configured ``catalog_provider`` alias (#112649)."""
@@ -199,7 +199,7 @@ def _models_dev_id(
_UNKNOWN_CATALOG_PROVIDER_WARNED: set = set() # (provider, alias) warned once per process
def _cfg_get(*keys: str, default: Any, config: Optional[Dict[str, Any]] = None) -> Any:
def _cfg_get(*keys: str, default: Any, config: Optional[dict[str, Any]] = None) -> Any:
"""``cfg_get`` over the read-only config; *default* on any failure."""
try:
from hermes_cli.config import cfg_get, load_config_readonly
@@ -263,7 +263,7 @@ def _validate_registry(data: Any) -> bool:
return isinstance(data, dict) and len(data) > 0
def _load_disk_cache() -> Dict[str, Any]:
def _load_disk_cache() -> dict[str, Any]:
"""Load the disk cache; a corrupt/empty one is quarantined with a warning so it never
masquerades as ``{}`` and breaks provider/model resolution."""
try:
@@ -303,7 +303,7 @@ def _disk_cache_age_seconds() -> Optional[float]:
return _quietly("stat models.dev disk cache", stat)
def _save_disk_cache(data: Dict[str, Any], etag: str = "") -> None:
def _save_disk_cache(data: dict[str, Any], etag: str = "") -> None:
"""Save the registry atomically, plus the ETag sidecar when non-empty."""
_quietly("save models.dev disk cache", lambda: atomic_json_write(_get_cache_path(), data, indent=None, separators=(",", ":")))
if etag:
@@ -315,12 +315,12 @@ class _NotModified(Exception):
"""Server returned 304 Not Modified — existing cache is still valid."""
def _fetch_models_dev_from_network(*, conditional: bool = False) -> Tuple[Dict[str, Any], str]:
def _fetch_models_dev_from_network(*, conditional: bool = False) -> tuple[dict[str, Any], str]:
"""Fetch the live registry; returns ``(registry, etag)`` (etag "" if none). Raises on network
errors and on an empty/invalid payload. ``conditional`` sends ``If-None-Match`` with the sidecar's
ETag and raises ``_NotModified`` on 304 — pass True ONLY while holding ``_models_dev_fetch_lock``
AND a servable registry, or a 304 leaves the process with no data."""
headers: Dict[str, str] = {}
headers: dict[str, str] = {}
if conditional and (etag := _load_etag()):
headers["If-None-Match"] = etag
# (connect, read): 5 s connect fails fast on blackholed hosts; 10 s read tolerates a slow registry.
@@ -341,7 +341,7 @@ def _mark_stale_cache_grace() -> None:
_models_dev_cache_time = max(_models_dev_cache_time, time.time() - _MODELS_DEV_CACHE_TTL + _MODELS_DEV_RETRY_DELAY)
def _serve_stale(msg: str, *args: Any) -> Dict[str, Any]:
def _serve_stale(msg: str, *args: Any) -> dict[str, Any]:
"""Arm the grace window, kick off a background refresh, return the held cache."""
_mark_stale_cache_grace()
_start_background_refresh_models_dev()
@@ -349,7 +349,7 @@ def _serve_stale(msg: str, *args: Any) -> Dict[str, Any]:
return _models_dev_cache
def _commit_registry(data: Dict[str, Any], *, etag: str = "", where: str) -> None:
def _commit_registry(data: dict[str, Any], *, etag: str = "", where: str) -> None:
"""Persist a fetched registry: disk + in-mem + clear backoff. Callers hold ``_models_dev_fetch_lock``
so a failing refresh on one path can never stomp state a succeeding refresh just committed."""
global _models_dev_cache, _models_dev_cache_time, _models_dev_retry_after
@@ -387,7 +387,7 @@ def _note_refresh_failure(exc: Exception, *, where: str) -> None:
logger.debug("models.dev refresh failed (%s); retry suppressed for %ds: %s", where, _MODELS_DEV_RETRY_DELAY, exc)
def _refresh_locked(where: str) -> Optional[Dict[str, Any]]:
def _refresh_locked(where: str) -> Optional[dict[str, Any]]:
"""One conditional fetch + state update; caller holds ``_models_dev_fetch_lock``. Returns the
registry to serve, or None when the fetch failed (backoff armed)."""
try:
@@ -435,7 +435,7 @@ def _start_background_refresh_models_dev() -> None:
logger.debug("Failed to start models.dev refresh thread: %s", e)
def fetch_models_dev(force_refresh: bool = False, *, allow_network: bool = True) -> Dict[str, Any]:
def fetch_models_dev(force_refresh: bool = False, *, allow_network: bool = True) -> dict[str, Any]:
"""Fetch the models.dev registry (dict keyed by provider ID; {} on failure). Cache hierarchy:
fresh in-memory → stale in-memory (served now, refreshed in one background daemon thread — stale
beats a foreground timeout) → disk of any age (stale triggers the same background refresh) →
@@ -493,7 +493,7 @@ def fetch_models_dev(force_refresh: bool = False, *, allow_network: bool = True)
return _models_dev_cache
def _registry_provider(mdev_id: str, allow_network: bool) -> Optional[Dict[str, Any]]:
def _registry_provider(mdev_id: str, allow_network: bool) -> Optional[dict[str, Any]]:
"""The raw models.dev provider entry, or None."""
# Keep the zero-argument call on the allow_network path: dozens of test sites monkeypatch fetch_models_dev with zero-arg lambdas.
registry = fetch_models_dev() if allow_network else fetch_models_dev(allow_network=False)
@@ -501,7 +501,7 @@ def _registry_provider(mdev_id: str, allow_network: bool) -> Optional[Dict[str,
return provider_data if isinstance(provider_data, dict) else None
def _registry_models(mdev_id: str, *, allow_network: bool) -> Optional[Dict[str, Any]]:
def _registry_models(mdev_id: str, *, allow_network: bool) -> Optional[dict[str, Any]]:
"""The ``models`` dict of a models.dev provider entry, or None."""
provider_data = _registry_provider(mdev_id, allow_network)
models = provider_data.get("models", {}) if provider_data is not None else None
@@ -509,8 +509,8 @@ def _registry_models(mdev_id: str, *, allow_network: bool) -> Optional[Dict[str,
def _get_provider_models(
provider: str, *, allow_network: bool = False, config: Optional[Dict[str, Any]] = None,
) -> Optional[Dict[str, Any]]:
provider: str, *, allow_network: bool = False, config: Optional[dict[str, Any]] = None,
) -> Optional[dict[str, Any]]:
"""Resolve a Hermes provider ID to its models dict, or None if unknown.
``allow_network`` defaults to False — hot-path callers must never block."""
mdev_id = _models_dev_id(provider, config=config)
@@ -545,7 +545,7 @@ def _openrouter_catalog_lookup_base(provider: str, model: str) -> Optional[str]:
def _iter_model_entries(
models: Dict[str, Any], model: str, *, suffix_fallback: bool = True, provider: str = ""
models: dict[str, Any], model: str, *, suffix_fallback: bool = True, provider: str = ""
):
"""Yield ``(model_id, entry)`` candidates: exact, case-insensitive, then (optionally)
``:cloud``/``-cloud`` suffixed forms. Suffix fallback: some providers (ollama-cloud) store
@@ -575,8 +575,8 @@ def _iter_model_entries(
def _find_model_entry(
models: Dict[str, Any], model: str, provider: str = ""
) -> Optional[Dict[str, Any]]:
models: dict[str, Any], model: str, provider: str = ""
) -> Optional[dict[str, Any]]:
"""First catalog entry for *model* (exact, case-insensitive, suffix), or None."""
return next((entry for _mid, entry in _iter_model_entries(models, model, provider=provider)), None)
@@ -587,7 +587,7 @@ def _extract_limit(entry: Any, key: str) -> Optional[int]:
return int(value) if isinstance(value, (int, float)) and value > 0 else None
def _extract_context(entry: Dict[str, Any]) -> Optional[int]:
def _extract_context(entry: dict[str, Any]) -> Optional[int]:
"""Context length from a models.dev model entry, or None if invalid/zero."""
return _extract_limit(entry, "context")
@@ -621,12 +621,12 @@ def lookup_models_dev_context(provider: str, model: str, *, allow_network: bool
_OVERRIDE_WARNED_KEYS: set = set()
# Safe defaults for models absent from the catalog (tools on, 200K context). Capability fields and the
# output limit stay absent so consumers see them as unknown instead of a synthesized value.
_UNKNOWN_MODEL_BASE: Dict[str, Any] = {"limit": {"context": 200000}, "tool_call": True}
_UNKNOWN_MODEL_BASE: dict[str, Any] = {"limit": {"context": 200000}, "tool_call": True}
# Account-gated models may be usable before models.dev has indexed them. Keep
# their capabilities available for an explicitly selected/discovered model
# without adding them to any picker catalog.
_DEEPSEEK_FLASH_VISION: Dict[str, Any] = {
_DEEPSEEK_FLASH_VISION: dict[str, Any] = {
"limit": {"context": 1_000_000, "output": 384_000},
"modalities": {"input": ["text", "image"], "output": ["text"]},
"tool_call": True,
@@ -634,7 +634,7 @@ _DEEPSEEK_FLASH_VISION: Dict[str, Any] = {
"family": "deepseek-flash",
}
_BUILTIN_MODEL_METADATA: Dict[Tuple[str, str], Dict[str, Any]] = {
_BUILTIN_MODEL_METADATA: dict[tuple[str, str], dict[str, Any]] = {
("openai", "gpt-6-astra"): {
"limit": {"context": 1_050_000, "output": 128_000},
"modalities": {"input": ["text", "image"], "output": ["text"]},
@@ -653,7 +653,7 @@ _BUILTIN_MODEL_METADATA: Dict[Tuple[str, str], Dict[str, Any]] = {
}
def _load_model_overrides(*, config: Optional[Dict[str, Any]] = None) -> Dict[str, Any]:
def _load_model_overrides(*, config: Optional[dict[str, Any]] = None) -> dict[str, Any]:
"""The ``model_overrides`` config section ({} on any failure). Deliberately not memoized:
``load_config_readonly()`` is already (mtime, size)-cached upstream, and an ``id(cfg)``-keyed
layer can serve stale overrides after a reload when CPython reuses the dict address."""
@@ -665,7 +665,7 @@ def _load_model_overrides(*, config: Optional[Dict[str, Any]] = None) -> Dict[st
return _dict_or_empty(overrides)
def _provider_override_section(provider: str, *, config: Optional[Dict[str, Any]] = None) -> Optional[Dict[str, Any]]:
def _provider_override_section(provider: str, *, config: Optional[dict[str, Any]] = None) -> Optional[dict[str, Any]]:
"""Override section for *provider* (keyed by Hermes OR models.dev id), or None."""
overrides = (
_load_model_overrides(config=config)
@@ -680,7 +680,7 @@ def _provider_override_section(provider: str, *, config: Optional[Dict[str, Any]
return next((section for section in (overrides.get(key) if key else None for key in candidates) if isinstance(section, dict)), None)
def _explicit_model_override(provider: str, model: str, *, config: Optional[Dict[str, Any]] = None) -> Optional[Dict[str, Any]]:
def _explicit_model_override(provider: str, model: str, *, config: Optional[dict[str, Any]] = None) -> Optional[dict[str, Any]]:
"""Explicit per-provider+model override dict (exact, then case-insensitive skipping the ``_default`` sentinel), or None."""
model_key = (model or "").strip()
section = _provider_override_section(provider, config=config) if model_key else None
@@ -693,7 +693,7 @@ def _explicit_model_override(provider: str, model: str, *, config: Optional[Dict
return next((mdata for mid, mdata in section.items() if mid != "_default" and mid.lower() == model_lower and isinstance(mdata, dict)), None)
def _default_model_override(provider: str, *, config: Optional[Dict[str, Any]] = None) -> Optional[Dict[str, Any]]:
def _default_model_override(provider: str, *, config: Optional[dict[str, Any]] = None) -> Optional[dict[str, Any]]:
"""Fill-gap ``_default`` override: per-provider first, then global; or None."""
section = _provider_override_section(provider, config=config)
if section is not None and isinstance(section.get("_default"), dict):
@@ -708,14 +708,14 @@ def _default_model_override(provider: str, *, config: Optional[Dict[str, Any]] =
def _override_for(
provider: str, model: str, *, catalog_hit: bool, config: Optional[Dict[str, Any]] = None,
) -> Optional[Dict[str, Any]]:
provider: str, model: str, *, catalog_hit: bool, config: Optional[dict[str, Any]] = None,
) -> Optional[dict[str, Any]]:
"""Explicit override if any; else the ``_default`` only on a catalog miss."""
explicit = _explicit_model_override(provider, model, config=config)
return explicit if explicit is not None or catalog_hit else _default_model_override(provider, config=config)
def _override_int(override: Dict[str, Any], key: str) -> Optional[int]:
def _override_int(override: dict[str, Any], key: str) -> Optional[int]:
"""Coerce an override field to a positive int, warning once on garbage."""
raw = override.get(key)
if raw is None:
@@ -746,10 +746,10 @@ def _default_override_context(provider: str) -> Optional[int]:
return _override_int(default, "context_window") if default is not None else None
def _override_to_catalog_shape(override: Dict[str, Any]) -> Tuple[Dict[str, Any], Optional[bool]]:
def _override_to_catalog_shape(override: dict[str, Any]) -> tuple[dict[str, Any], Optional[bool]]:
"""Translate canonical override keys into a models.dev-shaped patch. Returns ``(patch, vision)`` —
vision is out-of-band because it maps onto the ``modalities.input`` list rather than a scalar field."""
patch: Dict[str, Any] = {}
patch: dict[str, Any] = {}
limit = {
catalog_key: value for catalog_key, override_key in (("context", "context_window"),)
if (value := _override_int(override, override_key)) is not None
@@ -767,7 +767,7 @@ def _override_to_catalog_shape(override: Dict[str, Any]) -> Tuple[Dict[str, Any]
return patch, vision
def _merge_catalog_entry_with_override(raw: Dict[str, Any], override: Dict[str, Any]) -> Dict[str, Any]:
def _merge_catalog_entry_with_override(raw: dict[str, Any], override: dict[str, Any]) -> dict[str, Any]:
"""Patch a catalog entry with a canonical-schema override. Sub-dicts (``limit``, ``modalities``) are
merged, not clobbered — setting only ``context_window`` must not wipe the catalog's ``limit.output``."""
shaped, vision_override = _override_to_catalog_shape(override)
@@ -790,14 +790,14 @@ def _merge_catalog_entry_with_override(raw: Dict[str, Any], override: Dict[str,
def _builtin_model_metadata(
provider: str, model: str, *, config: Optional[Dict[str, Any]] = None,
) -> Optional[Dict[str, Any]]:
provider: str, model: str, *, config: Optional[dict[str, Any]] = None,
) -> Optional[dict[str, Any]]:
"""Built-in metadata for a provider/model pair, if Hermes has a vendor-specific entry."""
provider_key = _models_dev_id(provider, config=config) or (provider or "").strip()
return _BUILTIN_MODEL_METADATA.get((provider_key, (model or "").strip().lower()))
def _relay_vision_marker_metadata(provider: str, model: str) -> Optional[Dict[str, Any]]:
def _relay_vision_marker_metadata(provider: str, model: str) -> Optional[dict[str, Any]]:
"""Fill-gap base for an OpenCode Zen/Go ``*-vision*`` model id the catalog does not know. The relays
resell vendor previews (``deepseek-v4-flash-vision-exp``) before models.dev indexes them, and the id's
``-vision`` token is the vendor's own capability marker; without it ``image_input_mode: auto`` treats
@@ -810,7 +810,7 @@ def _relay_vision_marker_metadata(provider: str, model: str) -> Optional[Dict[st
return {**_UNKNOWN_MODEL_BASE, "modalities": {"input": ["text", "image"], "output": ["text"]}}
def _provider_model_capabilities(provider: str, model: str) -> Dict[str, Any]:
def _provider_model_capabilities(provider: str, model: str) -> dict[str, Any]:
"""Exact-model declaration from the registered ``ProviderProfile.model_capabilities`` (canonical
``model_overrides`` schema). The ONE plugin seam: every consumer that reads models.dev through this
module (picker badges, image routing, ``/api/model/info``, context lookup) sees it (#102115)."""
@@ -821,8 +821,8 @@ def _provider_model_capabilities(provider: str, model: str) -> Dict[str, Any]:
def _apply_overrides(
provider: str, model: str, entry: Optional[Dict[str, Any]], *, config: Optional[Dict[str, Any]] = None,
) -> Optional[Dict[str, Any]]:
provider: str, model: str, entry: Optional[dict[str, Any]], *, config: Optional[dict[str, Any]] = None,
) -> Optional[dict[str, Any]]:
"""Catalog/builtin metadata, patched by the plugin's declaration, then by the explicit user override.
``_UNKNOWN_MODEL_BASE`` is the base on a catalog miss; a fill-gap ``_default`` applies only when no
source knows the model. None when nothing knows it."""
@@ -837,7 +837,7 @@ def _apply_overrides(
return base if override is None else _merge_catalog_entry_with_override(base if base is not None else _UNKNOWN_MODEL_BASE, override)
def _entry_supports_vision(entry: Dict[str, Any]) -> bool:
def _entry_supports_vision(entry: dict[str, Any]) -> bool:
"""Prefer explicit ``modalities.input`` (the older ``attachment`` flag can be stale or too broad
for image routing); fall back to it only when the input modalities are absent/invalid."""
input_mods = _dict_or_empty(entry.get("modalities", {})).get("input")
@@ -845,7 +845,7 @@ def _entry_supports_vision(entry: Dict[str, Any]) -> bool:
def get_model_capabilities(
provider: str, model: str, *, allow_network: bool = False, config: Optional[Dict[str, Any]] = None,
provider: str, model: str, *, allow_network: bool = False, config: Optional[dict[str, Any]] = None,
) -> Optional[ModelCapabilities]:
"""Capability metadata from the models.dev cache, or None if unresolvable. EXPLICIT ``model_overrides``
patch catalog fields; ``_default`` fills the gap only for models the catalog does not know. Unspecified
@@ -879,7 +879,7 @@ def get_model_capabilities(
)
def list_provider_models(provider: str, *, allow_network: bool = True) -> List[str]:
def list_provider_models(provider: str, *, allow_network: bool = True) -> list[str]:
"""All model IDs for a provider ([] if unknown). ``allow_network`` defaults to True: the model
picker is interactive and a fresh catalog is worth a short wait."""
from hermes_cli.models import normalize_provider
@@ -908,7 +908,7 @@ def _should_hide_from_provider_catalog(provider: str, model_id: str) -> bool:
return (provider or "").strip().lower() in {"gemini", "google"} and (model_id or "").strip().lower() in _GOOGLE_HIDDEN_MODELS
def list_agentic_models(provider: str, *, allow_network: bool = True) -> List[str]:
def list_agentic_models(provider: str, *, allow_network: bool = True) -> list[str]:
"""Model IDs suitable for agentic use: tool_call=True, minus hidden and noise models. [] on any
failure. ``allow_network`` defaults to True (called from interactive model selection)."""
models = _get_provider_models(provider, allow_network=allow_network)
@@ -918,11 +918,11 @@ def list_agentic_models(provider: str, *, allow_network: bool = True) -> List[st
] if models is not None else []
def _parse_model_info(model_id: str, raw: Dict[str, Any], provider_id: str) -> ModelInfo:
def _parse_model_info(model_id: str, raw: dict[str, Any], provider_id: str) -> ModelInfo:
"""Convert a raw models.dev model entry dict into a ModelInfo dataclass."""
cost = _dict_or_empty(raw.get("cost"))
modalities = _dict_or_empty(raw.get("modalities"))
def _mods(key: str) -> Tuple[str, ...]:
def _mods(key: str) -> tuple[str, ...]:
mods = modalities.get(key) or []
return tuple(mods) if isinstance(mods, list) else ()
def _cost(key: str) -> Optional[float]:
@@ -939,7 +939,7 @@ def _parse_model_info(model_id: str, raw: Dict[str, Any], provider_id: str) -> M
)
def _parse_provider_info(provider_id: str, raw: Dict[str, Any]) -> ProviderInfo:
def _parse_provider_info(provider_id: str, raw: dict[str, Any]) -> ProviderInfo:
"""Convert a raw models.dev provider entry dict into a ProviderInfo."""
env = raw.get("env") or []
models = raw.get("models") or {}
@@ -950,7 +950,7 @@ def _parse_provider_info(provider_id: str, raw: Dict[str, Any]) -> ProviderInfo:
def get_provider_info(
provider_id: str, *, allow_network: bool = True, config: Optional[Dict[str, Any]] = None,
provider_id: str, *, allow_network: bool = True, config: Optional[dict[str, Any]] = None,
) -> Optional[ProviderInfo]:
"""Provider metadata by Hermes or models.dev ID, or None if not cataloged. ``allow_network`` defaults to True (interactive setup)."""
mdev_id = _models_dev_id(provider_id, config=config) or provider_id
@@ -959,7 +959,7 @@ def get_provider_info(
def get_model_info(
provider_id: str, model_id: str, *, allow_network: bool = False, config: Optional[Dict[str, Any]] = None,
provider_id: str, model_id: str, *, allow_network: bool = False, config: Optional[dict[str, Any]] = None,
) -> Optional[ModelInfo]:
"""Full model metadata by Hermes or models.dev provider ID (exact match, then case-insensitive), or
None if not found. EXPLICIT ``model_overrides`` patch known catalog models; ``_default`` fills the gap
+2 -2
View File
@@ -26,7 +26,7 @@ class MonitoringEmitter:
def __init__(self, *, enabled: bool = True) -> None:
self._enabled = enabled
self._q: "queue.Queue[Dict[str, Any]]" = queue.Queue(maxsize=_MAX_QUEUE)
self._q: "queue.Queue[dict[str, Any]]" = queue.Queue(maxsize=_MAX_QUEUE)
self._dropped = 0
self._dispatched = 0
self._stop = threading.Event()
@@ -122,7 +122,7 @@ class MonitoringEmitter:
threading.Thread(target=_wait_for_completion, name="hermes-monitoring-flush", daemon=True).start()
finished.wait(timeout=timeout)
def stats(self) -> Dict[str, int]:
def stats(self) -> dict[str, int]:
return {"queued": self._q.qsize(), "dispatched": self._dispatched, "dropped": self._dropped, "subscribers": len(self._subscribers)}
def close(self) -> None:
+1 -1
View File
@@ -17,7 +17,7 @@ class _MonitoringEvent:
__slots__ = ()
EVENT: ClassVar[str]
def to_dict(self) -> Dict[str, Any]:
def to_dict(self) -> dict[str, Any]:
return {"event": self.EVENT, **asdict(self)}
+5 -5
View File
@@ -24,13 +24,13 @@ logger = logging.getLogger(__name__)
class GatewayMetric:
name: str
value: int | float
attributes: Dict[str, str]
attributes: dict[str, str]
@dataclass(frozen=True, slots=True)
class GatewayHealthSnapshot:
metrics: List[GatewayMetric]
events: List[GatewayHealthEvent | GatewayDiagnosticEvent]
metrics: list[GatewayMetric]
events: list[GatewayHealthEvent | GatewayDiagnosticEvent]
_RUNNING_PLATFORM_STATES = {"running", "connected", "ok", "ready"}
@@ -131,7 +131,7 @@ def _coerce_pid(raw: Any) -> Optional[int]:
def _gateway_status(name: str, fallback: Callable[[], Any], /, **kwargs: Any) -> Any:
"""Prefer ``gateway.status.<name>`` (the runtime-status contract); fall back to the local approximation."""
try:
import gateway.status as status
from gateway import status
return getattr(status, name)(**kwargs)
except Exception:
return fallback()
@@ -179,7 +179,7 @@ def build_gateway_health_snapshot(
"hermes.supervision_mode": mode if mode in _SUPERVISION_MODES else "unknown",
}
def metric(name: str, value: int | float, **extra: str) -> GatewayMetric:
def metric(name: str, value: float, **extra: str) -> GatewayMetric:
attrs = dict(base)
for key, val in extra.items():
if val is not None:
+14 -14
View File
@@ -54,7 +54,7 @@ _OBSERVABLE_METRIC_NAMES = (
)
def _diagnostic_log_attributes(event: Dict[str, Any]) -> Dict[str, Any]:
def _diagnostic_log_attributes(event: dict[str, Any]) -> dict[str, Any]:
# Same allowlist as the span mapping: profile/install_id/ts never egress as attributes.
return _allowlisted_attrs(event, otlp_exporter._KEEP_BY_KIND["gateway_diagnostic"])
@@ -102,15 +102,15 @@ class GatewayHealthExportRuntime:
self.streamer = self.log_streamer = self.metric_provider = self.thread = self.stop_event = None
def _gateway_health_config(config: Dict[str, Any]) -> Dict[str, Any]:
def _gateway_health_config(config: dict[str, Any]) -> dict[str, Any]:
return _monitoring_section(config, "gateway_health_export")
def _enabled(config: Dict[str, Any]) -> bool:
def _enabled(config: dict[str, Any]) -> bool:
return bool(_gateway_health_config(config).get("enabled") and otlp_exporter.is_enabled(config))
def _exporter_kwargs(config: Dict[str, Any], signal: str) -> Dict[str, Any]:
def _exporter_kwargs(config: dict[str, Any], signal: str) -> dict[str, Any]:
otlp = _otlp_config(config)
return {"endpoint": _signal_endpoint(str(otlp.get("endpoint")), signal), "headers": _resolve_headers(otlp.get("headers_env")) or None}
@@ -128,7 +128,7 @@ def _supervision_mode() -> str:
return next((mode for mode, detect in _SUPERVISION_DETECTORS if detect()), "manual")
def _read_gateway_snapshot(config: Dict[str, Any]):
def _read_gateway_snapshot(config: dict[str, Any]):
try:
from gateway.status import read_runtime_status
runtime = read_runtime_status() or {}
@@ -170,7 +170,7 @@ def _read_background_delegations_count() -> int:
return _count("background-delegations count failed", "tools.async_delegation", lambda m: m.active_count())
def _read_runtime_snapshot(config: Dict[str, Any]):
def _read_runtime_snapshot(config: dict[str, Any]):
gateway_snapshot = _read_gateway_snapshot(config)
# Background/subagent work is appended to the gateway snapshot so it rides the same base
# resource attributes (service.instance.id etc.).
@@ -194,7 +194,7 @@ def _read_runtime_snapshot(config: Dict[str, Any]):
return gateway_snapshot
def _emit_snapshot_events(config: Dict[str, Any]) -> None:
def _emit_snapshot_events(config: dict[str, Any]) -> None:
if not _gateway_health_config(config).get("diagnostic_events_enabled", True):
return
try:
@@ -204,7 +204,7 @@ def _emit_snapshot_events(config: Dict[str, Any]) -> None:
logger.debug("gateway health snapshot emit failed", exc_info=True)
def _start_metric_provider(config: Dict[str, Any], sdk: Dict[str, Any]) -> Any:
def _start_metric_provider(config: dict[str, Any], sdk: dict[str, Any]) -> Any:
exporter = sdk["OTLPMetricExporter"](**_exporter_kwargs(config, "metrics"))
interval_ms = max(5, int(_gateway_health_config(config).get("export_interval_seconds", 60))) * 1000
reader = sdk["PeriodicExportingMetricReader"](exporter, export_interval_millis=interval_ms)
@@ -231,14 +231,14 @@ def _start_metric_provider(config: Dict[str, Any], sdk: Dict[str, Any]) -> Any:
_SEVERITY_NAMES = {"critical": "FATAL", "fatal": "FATAL", "error": "ERROR", "info": "INFO", "information": "INFO", "debug": "DEBUG"}
def _severity_number(sdk: Dict[str, Any], severity: Any) -> Any:
def _severity_number(sdk: dict[str, Any], severity: Any) -> Any:
return getattr(sdk["SeverityNumber"], _SEVERITY_NAMES.get(str(severity or "warning").lower(), "WARN"))
class GatewayDiagnosticLogStreamer(EmitterStreamer):
"""Emitter subscriber that sends gateway diagnostic events as OTLP logs."""
def __init__(self, config: Dict[str, Any], sdk: Dict[str, Any]):
def __init__(self, config: dict[str, Any], sdk: dict[str, Any]):
resource = sdk["Resource"].create(_runtime_resource_attributes(config, telemetry_scope="gateway_diagnostics"))
self._provider = sdk["LoggerProvider"](resource=resource)
self._processor = sdk["BatchLogRecordProcessor"](sdk["OTLPLogExporter"](**_exporter_kwargs(config, "logs")))
@@ -247,7 +247,7 @@ class GatewayDiagnosticLogStreamer(EmitterStreamer):
self._sdk = sdk
self.exported = 0
def __call__(self, batch: list[Dict[str, Any]]) -> None:
def __call__(self, batch: list[dict[str, Any]]) -> None:
sdk = self._sdk
for ev in batch:
if ev.get("event") != "gateway_diagnostic":
@@ -266,7 +266,7 @@ class GatewayDiagnosticLogStreamer(EmitterStreamer):
self.exported += 1
def _gateway_health_event(ev: Dict[str, Any]) -> bool:
def _gateway_health_event(ev: dict[str, Any]) -> bool:
return ev.get("event") in {"gateway_health", "cron_execution"}
@@ -276,7 +276,7 @@ def _fail(runtime: GatewayHealthExportRuntime, log: Callable[..., None], msg: st
return GatewayHealthExportRuntime(enabled=False, reason=reason)
def start_gateway_health_export(config: Dict[str, Any]) -> GatewayHealthExportRuntime:
def start_gateway_health_export(config: dict[str, Any]) -> GatewayHealthExportRuntime:
"""Start P0 gateway health export if configured. Never raises."""
if not _enabled(config):
return GatewayHealthExportRuntime(enabled=False, reason="disabled")
@@ -284,7 +284,7 @@ def start_gateway_health_export(config: Dict[str, Any]) -> GatewayHealthExportRu
metrics_on = gh.get("metrics_enabled", True)
diagnostics_on = gh.get("diagnostic_events_enabled", True)
runtime = GatewayHealthExportRuntime(enabled=True, reason="enabled")
sdk: Optional[Dict[str, Any]] = None
sdk: Optional[dict[str, Any]] = None
if metrics_on or diagnostics_on:
try:
sdk = otlp_exporter._require_sdk(_METRICS_SDK, auto_install=True)
+24 -24
View File
@@ -28,7 +28,7 @@ class OTLPUnavailable(RuntimeError):
# ── SDK loading ──────────────────────────────────────────────────────────────
_SDK_MODULES: Dict[str, tuple[str, ...]] = {
_SDK_MODULES: dict[str, tuple[str, ...]] = {
"opentelemetry.sdk.trace": ("TracerProvider",),
"opentelemetry.sdk.trace.export": ("BatchSpanProcessor",),
"opentelemetry.sdk.resources": ("Resource",),
@@ -44,11 +44,11 @@ _SDK_MODULES: Dict[str, tuple[str, ...]] = {
"opentelemetry.sdk.metrics": ("MeterProvider",),
"opentelemetry.sdk.metrics.export": ("PeriodicExportingMetricReader",),
}
_SDK_SYMBOLS: Dict[str, str] = {name: module for module, names in _SDK_MODULES.items() for name in names}
_SDK_SYMBOLS: dict[str, str] = {name: module for module, names in _SDK_MODULES.items() for name in names}
_SPAN_SDK = ("TracerProvider", "BatchSpanProcessor", "Resource", "OTLPSpanExporter", "SpanKind")
def _require_sdk(names: Iterable[str] = _SPAN_SDK, *, auto_install: bool = True) -> Dict[str, Any]:
def _require_sdk(names: Iterable[str] = _SPAN_SDK, *, auto_install: bool = True) -> dict[str, Any]:
"""Import the named OTel SDK symbols, lazily installing the extra on first use.
Routes through pm.ensure_import('otlp') so a missing SDK triggers the
@@ -73,20 +73,20 @@ def _require_sdk(names: Iterable[str] = _SPAN_SDK, *, auto_install: bool = True)
# ── config + connection plumbing ─────────────────────────────────────────────
def _monitoring_section(config: Dict[str, Any], *path: str) -> Dict[str, Any]:
def _monitoring_section(config: dict[str, Any], *path: str) -> dict[str, Any]:
node: Any = (config or {}).get("monitoring") or {}
for key in path:
node = node.get(key) or {}
return node
def _otlp_config(config: Dict[str, Any]) -> Dict[str, Any]:
def _otlp_config(config: dict[str, Any]) -> dict[str, Any]:
return _monitoring_section(config, "export", "otlp")
def _resolve_headers(headers_env: Optional[Dict[str, str]]) -> Dict[str, str]:
def _resolve_headers(headers_env: Optional[dict[str, str]]) -> dict[str, str]:
"""Resolve {header_name: ENV_VAR_NAME} -> {header_name: value}; missing vars skipped."""
resolved: Dict[str, str] = {}
resolved: dict[str, str] = {}
for header_name, env_name in (headers_env or {}).items():
val = os.environ.get(str(env_name))
if val:
@@ -115,7 +115,7 @@ _RESOURCE_ATTRIBUTE_KEYS = frozenset({
_SAFE_RESOURCE_VALUE = re.compile(r"^[A-Za-z0-9._:/-]{1,128}$")
def _install_id(config: Dict[str, Any]) -> str:
def _install_id(config: dict[str, Any]) -> str:
try:
from agent.monitoring.policy import ensure_install_id
return str(ensure_install_id(config))
@@ -123,9 +123,9 @@ def _install_id(config: Dict[str, Any]) -> str:
return "unknown"
def _safe_resource_attributes(raw: Any) -> Dict[str, str]:
def _safe_resource_attributes(raw: Any) -> dict[str, str]:
"""Allowlist bounded resource labels and reject values changed by redaction."""
attrs: Dict[str, str] = {}
attrs: dict[str, str] = {}
for key, value in (raw.items() if isinstance(raw, dict) else ()):
key = str(key)
if key not in _RESOURCE_ATTRIBUTE_KEYS or value is None:
@@ -138,7 +138,7 @@ def _safe_resource_attributes(raw: Any) -> Dict[str, str]:
return attrs
def _runtime_resource_attributes(config: Dict[str, Any], *, telemetry_scope: str) -> Dict[str, str]:
def _runtime_resource_attributes(config: dict[str, Any], *, telemetry_scope: str) -> dict[str, str]:
"""Build the safe OTLP resource shared by spans, metrics and diagnostic logs."""
attrs = _safe_resource_attributes(_monitoring_section(config, "gateway_health_export").get("resource_attributes"))
attrs["service.name"] = "hermes-gateway"
@@ -147,7 +147,7 @@ def _runtime_resource_attributes(config: Dict[str, Any], *, telemetry_scope: str
return attrs
def build_exporter(config: Dict[str, Any]):
def build_exporter(config: dict[str, Any]):
"""Construct an OTLP span exporter from config. Raises OTLPUnavailable if no SDK."""
sdk = _require_sdk()
otlp = _otlp_config(config)
@@ -157,11 +157,11 @@ def build_exporter(config: Dict[str, Any]):
return sdk["OTLPSpanExporter"](endpoint=endpoint, headers=_resolve_headers(otlp.get("headers_env")) or None)
def _resource_attributes(config: Dict[str, Any]) -> Dict[str, str]:
def _resource_attributes(config: dict[str, Any]) -> dict[str, str]:
return _runtime_resource_attributes(config, telemetry_scope="gateway_monitoring")
def _make_provider(config: Dict[str, Any]):
def _make_provider(config: dict[str, Any]):
sdk = _require_sdk()
provider = sdk["TracerProvider"](resource=sdk["Resource"].create(_resource_attributes(config)))
processor = sdk["BatchSpanProcessor"](build_exporter(config))
@@ -171,7 +171,7 @@ def _make_provider(config: Dict[str, Any]):
# ── event -> span attribute mapping ──────────────────────────────────────────
# Per-kind attribute allowlists: everything else (profile, install_id, ...) never egresses.
_KEEP_BY_KIND: Dict[str, tuple[str, ...]] = {
_KEEP_BY_KIND: dict[str, tuple[str, ...]] = {
"gateway_health": (
"name", "gateway_state", "old_state", "new_state", "exit_reason", "restart_requested", "active_agents",
"gateway_busy", "gateway_drainable", "platform_count", "fatal_platform_count", "version", "supervision_mode", "pid",
@@ -181,9 +181,9 @@ _KEEP_BY_KIND: Dict[str, tuple[str, ...]] = {
}
def _allowlisted_attrs(ev: Dict[str, Any], keys: Iterable[str]) -> Dict[str, Any]:
def _allowlisted_attrs(ev: dict[str, Any], keys: Iterable[str]) -> dict[str, Any]:
"""``hermes.<key>`` attributes for present keys; string values are redacted and bounded."""
attrs: Dict[str, Any] = {}
attrs: dict[str, Any] = {}
for col in keys:
v = ev.get(col)
if v is not None:
@@ -191,15 +191,15 @@ def _allowlisted_attrs(ev: Dict[str, Any], keys: Iterable[str]) -> Dict[str, Any
return attrs
def _span_attrs(ev: Dict[str, Any]) -> Dict[str, Any]:
def _span_attrs(ev: dict[str, Any]) -> dict[str, Any]:
"""Span attributes for a monitoring event (content-free by construction)."""
kind = ev.get("event")
attrs: Dict[str, Any] = {"hermes.event": kind or "unknown"}
attrs: dict[str, Any] = {"hermes.event": kind or "unknown"}
attrs.update(_allowlisted_attrs(ev, _KEEP_BY_KIND.get(kind, ()))) # type: ignore[arg-type]
return attrs
def export_batch(provider, batch: List[Dict[str, Any]]) -> int:
def export_batch(provider, batch: list[dict[str, Any]]) -> int:
"""Map a batch of events to OTel spans. Returns spans created."""
tracer = provider.get_tracer("hermes.monitoring")
n = 0
@@ -232,12 +232,12 @@ class EmitterStreamer:
class OTLPStreamer(EmitterStreamer):
"""A live subscriber that pushes each emitter batch to OTLP as spans."""
def __init__(self, config: Dict[str, Any], *, event_filter: Optional[Callable[[Dict[str, Any]], bool]] = None):
def __init__(self, config: dict[str, Any], *, event_filter: Optional[Callable[[dict[str, Any]], bool]] = None):
self._provider, self._processor = _make_provider(config)
self._event_filter = event_filter
self.exported = 0
def __call__(self, batch: List[Dict[str, Any]]) -> None:
def __call__(self, batch: list[dict[str, Any]]) -> None:
if self._event_filter is not None:
batch = [ev for ev in batch if self._event_filter(ev)]
if not batch:
@@ -254,13 +254,13 @@ def is_available() -> bool:
return False
def is_enabled(config: Dict[str, Any]) -> bool:
def is_enabled(config: dict[str, Any]) -> bool:
otlp = _otlp_config(config)
return bool(otlp.get("enabled") and otlp.get("endpoint"))
def start_streaming(
config: Dict[str, Any], *, event_filter: Optional[Callable[[Dict[str, Any]], bool]] = None,
config: dict[str, Any], *, event_filter: Optional[Callable[[dict[str, Any]], bool]] = None,
) -> Optional[OTLPStreamer]:
"""If OTLP is enabled, attach a streamer to the singleton emitter.
+1 -1
View File
@@ -14,7 +14,7 @@ from typing import Any, Dict
logger = logging.getLogger(__name__)
def ensure_install_id(config: Dict[str, Any]) -> str:
def ensure_install_id(config: dict[str, Any]) -> str:
"""Return a stable install id, minting and persisting one when empty.
The id becomes ``service.instance.id`` and must survive restarts, so a fresh
Loaded 100 of 2366 files, more files were not shown because too many files have changed in this diff. Show more