From 506c586c13c18485bb3c80c68b57af5f5e79bd41 Mon Sep 17 00:00:00 2001
From: Teknium <127238744+teknium1@users.noreply.github.com>
Date: Wed, 2 Sep 2026 18:18:55 -0700
Subject: [PATCH 01/20] refactor(agent/agent_runtime_helpers): split
repair_message_sequence into 4 pass helpers + ordered pass table
---
agent/agent_runtime_helpers.py | 250 ++++++++++++++++-----------------
1 file changed, 120 insertions(+), 130 deletions(-)
diff --git a/agent/agent_runtime_helpers.py b/agent/agent_runtime_helpers.py
index db1b6b66ca..50c8b27b5d 100644
--- a/agent/agent_runtime_helpers.py
+++ b/agent/agent_runtime_helpers.py
@@ -454,43 +454,52 @@ def note_turn_persisted(agent):
agent._inflight_turn_session_id = None
-def repair_message_sequence(agent, messages: List[Dict]) -> int:
- """Collapse malformed role-alternation left in the live history.
+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")
+ or m.get("codex_message_items")
+ or m.get("finish_reason") == "incomplete"
+ )
- Providers require strict alternation after the system message; violations
- cause silent empty responses or HTTP 400s. Runs right before the API call as
- a defensive belt for host-fed, resumed, or replayed histories.
- Passes: 0. merge consecutive assistant turns (union tool_calls, concat
- content; codex interim turns exempt; #29148, #49147); 1. drop stray ``tool``
- results with no preceding matching tool_call; 2. prune tool_calls not
- answered in the immediately following tool run, dropping the turn if left
- empty (codex interims exempt); 3. merge consecutive user messages.
- A user turn directly after an assistant turn is valid and left alone.
+def _merge_assistant_into(prev: Dict, msg: Dict) -> None:
+ """Fold a consecutive assistant ``msg`` into ``prev`` (union tool_calls, concat text)."""
+ prev_calls = list(prev.get("tool_calls") or [])
+ new_calls = list(msg.get("tool_calls") or [])
+ if new_calls:
+ prev["tool_calls"] = prev_calls + new_calls
+ elif prev_calls:
+ prev["tool_calls"] = prev_calls
+ else:
+ # Drop a stale ``tool_calls: []`` at the source: strict providers (DeepSeek v4,
+ # Kimi) 400 on it and it persists into replayed history.
+ prev.pop("tool_calls", None)
+ # Concatenate plain-text content only; leave multimodal (list) content alone.
+ prev_content = prev.get("content")
+ new_content = msg.get("content")
+ content_rewritten = False
+ if isinstance(prev_content, str) and isinstance(new_content, str):
+ joined = "\n".join(p for p in (prev_content.strip(), new_content.strip()) if p)
+ prev["content"] = joined
+ # A falsy new_content leaves ``joined`` == prev_content; that is not a rewrite.
+ content_rewritten = joined != prev_content
+ elif not prev_content and new_content is not None:
+ prev["content"] = new_content
+ content_rewritten = new_content != prev_content
+ # Carry reasoning_content from the later turn only if the earlier lacks it
+ # (strict thinking providers need one on the merged tool-call turn).
+ if not prev.get("reasoning_content") and msg.get("reasoning_content"):
+ prev["reasoning_content"] = msg["reasoning_content"]
+ # A stale ``api_content`` sidecar overrides ``content`` at API-build time and would
+ # replay pre-merge bytes; drop it only when content actually changed.
+ if content_rewritten:
+ drop_stale_api_content(prev)
- Returns the number of repairs made.
- """
- if not messages:
- return 0
+def _merge_consecutive_assistants(messages: List[Dict]) -> Tuple[List[Dict], int]:
+ """Pass 0: merge consecutive assistant turns (codex interims exempt)."""
repairs = 0
-
- # Pass 0: merge consecutive assistant messages, BEFORE Pass 1 so the merged
- # tool_call-id union is known. Codex Responses interim turns are exempt:
- # each carries its own continuation state that must be replayed verbatim.
- def _is_codex_interim(m: Dict) -> bool:
- return bool(
- m.get("codex_reasoning_items")
- or m.get("codex_message_items")
- or m.get("finish_reason") == "incomplete"
- )
-
- def _is_verification_candidate(m: Dict) -> bool:
- return m.get("finish_reason") in {
- "verification_required",
- "verify_hook_continue",
- }
-
collapsed: List[Dict] = []
for msg in messages:
if (
@@ -503,61 +512,30 @@ def repair_message_sequence(agent, messages: List[Dict]) -> int:
and not _is_codex_interim(collapsed[-1])
):
prev = collapsed[-1]
- # A provisional verification candidate (finish_reason =
- # verification_required / verify_hook_continue) is superseded, not unioned (#65919 §7).
- if _is_verification_candidate(prev):
+ # A provisional verification candidate is superseded, not unioned.
+ if prev.get("finish_reason") in {"verification_required", "verify_hook_continue"}:
collapsed[-1] = msg
- repairs += 1
- continue
- # Union tool_calls (preserve order, both may carry them).
- prev_calls = list(prev.get("tool_calls") or [])
- new_calls = list(msg.get("tool_calls") or [])
- if new_calls:
- prev["tool_calls"] = prev_calls + new_calls
- elif prev_calls:
- prev["tool_calls"] = prev_calls
else:
- # Drop a stale ``tool_calls: []`` at the source: strict providers
- # (DeepSeek v4, Kimi) 400 on it and it persists into replayed history (#58755, #77921).
- prev.pop("tool_calls", None)
- # Concatenate plain-text content only; leave multimodal (list) content alone.
- prev_content = prev.get("content")
- new_content = msg.get("content")
- content_rewritten = False
- if isinstance(prev_content, str) and isinstance(new_content, str):
- joined = "\n".join(
- p for p in (prev_content.strip(), new_content.strip()) if p
- )
- prev["content"] = joined
- # A falsy new_content leaves ``joined`` == prev_content; that is not a rewrite (#78063).
- content_rewritten = joined != prev_content
- elif not prev_content and new_content is not None:
- prev["content"] = new_content
- content_rewritten = new_content != prev_content
- # Carry reasoning_content from the later turn only if the earlier lacks it
- # (strict thinking providers need one on the merged tool-call turn).
- if not prev.get("reasoning_content") and msg.get("reasoning_content"):
- prev["reasoning_content"] = msg["reasoning_content"]
- # A stale ``api_content`` sidecar overrides ``content`` at API-build time and
- # would replay pre-merge bytes; drop it only when content actually changed (#78063).
- if content_rewritten:
- drop_stale_api_content(prev)
+ _merge_assistant_into(prev, msg)
repairs += 1
continue
collapsed.append(msg)
+ return collapsed, repairs
- # Pass 1: drop stray tool messages not following a known assistant tool call.
- # Consume 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 (#66974, #91768).
+
+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] = {}
matched_tool_groups: set = set()
next_tool_group = 0
filtered: List[Dict] = []
- for msg in collapsed:
- if not isinstance(msg, dict):
- filtered.append(msg)
- continue
- role = msg.get("role")
+ for msg in messages:
+ role = msg.get("role") if isinstance(msg, dict) else None
if role == "assistant":
known_tool_ids = {}
matched_tool_groups = set()
@@ -569,40 +547,39 @@ def repair_message_sequence(agent, messages: List[Dict]) -> int:
next_tool_group += 1
for tc_id in variants:
known_tool_ids.setdefault(tc_id, group_id)
- filtered.append(msg)
elif role == "tool":
result_variants = tool_result_id_variants(msg.get("tool_call_id"))
candidate_groups = {
known_tool_ids[tc_id]
for tc_id in result_variants
- if tc_id in known_tool_ids
- and known_tool_ids[tc_id] not in matched_tool_groups
+ if tc_id in known_tool_ids and known_tool_ids[tc_id] not in matched_tool_groups
}
- if not result_variants:
- filtered.append(msg)
- elif candidate_groups:
- # Consume the whole alias group so a second result on any sibling
- # spelling is dropped; strict providers 400 on duplicates (#58327, #66974, #55436).
- group_id = min(candidate_groups)
- filtered.append(msg)
- matched_tool_groups.add(group_id)
- else:
+ if result_variants and not candidate_groups:
repairs += 1
- else:
- if role == "user":
- # A user turn closes the tool-result run; later tool messages are orphans.
- known_tool_ids = {}
- matched_tool_groups = set()
- filtered.append(msg)
+ continue
+ if candidate_groups:
+ matched_tool_groups.add(min(candidate_groups))
+ elif role == "user":
+ # A user turn closes the tool-result run; later tool messages are orphans.
+ known_tool_ids = {}
+ matched_tool_groups = set()
+ filtered.append(msg)
+ return filtered, repairs
- # Pass 2: prune tool_calls never answered in the IMMEDIATELY following tool run
- # (any id variant, same alias policy as Pass 1); a displaced result masks the per-call
- # stub pass and strict providers (DeepSeek v4) 400. Drop payload-empty turns; codex interims exempt.
+
+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.
+ """
+ repairs = 0
pruned: List[Dict] = []
+ n = len(messages)
i = 0
- n = len(filtered)
while i < n:
- msg = filtered[i]
+ msg = messages[i]
+ i += 1
if not (
isinstance(msg, dict)
and msg.get("role") == "assistant"
@@ -610,45 +587,35 @@ def repair_message_sequence(agent, messages: List[Dict]) -> int:
and not _is_codex_interim(msg)
):
pruned.append(msg)
- i += 1
continue
answered: set = set()
- j = i + 1
- while (
- j < n
- and isinstance(filtered[j], dict)
- and filtered[j].get("role") == "tool"
- ):
- tid = (filtered[j].get("tool_call_id") or "").strip()
+ j = i
+ while j < n and isinstance(messages[j], dict) and messages[j].get("role") == "tool":
+ tid = (messages[j].get("tool_call_id") or "").strip()
if tid:
answered.update(tool_result_id_variants(tid))
j += 1
- kept_calls: List[Dict] = []
- dropped_calls = 0
- for tc in msg.get("tool_calls") or []:
- variants = tool_call_id_variants(tc)
- if variants and (variants & answered):
- kept_calls.append(tc)
- else:
- dropped_calls += 1
- if dropped_calls:
+ kept_calls = [tc for tc in msg["tool_calls"] if tool_call_id_variants(tc) & answered]
+ if len(kept_calls) != len(msg["tool_calls"]):
repairs += 1
if not kept_calls and not _msg_has_payload(
{k: v for k, v in msg.items() if k != "tool_calls"}
):
# Pruned calls were the only payload; drop the turn (empty assistant messages 400).
- i += 1
continue
if kept_calls:
msg["tool_calls"] = kept_calls
else:
msg.pop("tool_calls", None)
pruned.append(msg)
- i += 1
+ return pruned, repairs
- # Pass 3: merge consecutive user messages (no user input lost).
+
+def _merge_consecutive_users(messages: List[Dict]) -> Tuple[List[Dict], int]:
+ """Pass 3: merge consecutive plain-text user messages (no user input lost)."""
+ repairs = 0
merged: List[Dict] = []
- for msg in pruned:
+ for msg in messages:
if (
merged
and isinstance(msg, dict)
@@ -657,19 +624,15 @@ def repair_message_sequence(agent, messages: List[Dict]) -> int:
and merged[-1].get("role") == "user"
):
prev = merged[-1]
- # A summary carrier followed by a new user row is a deliberate durable shape
- # after retry/rewind; never mutate the persisted carrier (sanitizers merge copies later).
+ # A summary carrier followed by a new user row is a deliberate durable shape after
+ # retry/rewind; never mutate the persisted carrier (sanitizers merge copies later).
from agent.context_compressor import split_user_originated_turn
handoff, _ = split_user_originated_turn(prev)
- if handoff is not None:
- merged.append(msg)
- continue
-
prev_content = prev.get("content", "")
new_content = msg.get("content", "")
# Only merge plain-text content; leave multimodal (list) content alone.
- if isinstance(prev_content, str) and isinstance(new_content, str):
+ if handoff is None and isinstance(prev_content, str) and isinstance(new_content, str):
prev["content"] = (
(prev_content + "\n\n" + new_content)
if prev_content and new_content
@@ -680,11 +643,38 @@ def repair_message_sequence(agent, messages: List[Dict]) -> int:
repairs += 1
continue
merged.append(msg)
+ return merged, repairs
+
+_SEQUENCE_REPAIR_PASSES = (
+ _merge_consecutive_assistants,
+ _drop_stray_tool_results,
+ _prune_unanswered_tool_calls,
+ _merge_consecutive_users,
+)
+
+
+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 cause
+ silent empty responses or HTTP 400s. Runs right before the API call as a defensive
+ belt for host-fed, resumed, or replayed histories. Passes, in order: merge
+ consecutive assistant turns (BEFORE orphan detection so the merged tool_call-id
+ union is known); drop stray tool results; prune tool_calls unanswered in the
+ immediately following tool run; merge consecutive user messages. A user turn
+ directly after an assistant turn is valid and left alone.
+ """
+ if not messages:
+ return 0
+ repairs = 0
+ current = messages
+ for repair_pass in _SEQUENCE_REPAIR_PASSES:
+ current, made = repair_pass(current)
+ repairs += made
if repairs > 0:
# Rewrite in place so persistence/return value/DB flush see the repaired sequence.
- messages[:] = merged
-
+ messages[:] = current
return repairs
From af6f9eb3f457a4a6b7248b5ac977778b7524698d Mon Sep 17 00:00:00 2001
From: Teknium <127238744+teknium1@users.noreply.github.com>
Date: Wed, 2 Sep 2026 18:21:37 -0700
Subject: [PATCH 02/20] refactor(agent/agent_runtime_helpers): split
sanitize_api_messages into 7 phase helpers
---
agent/agent_runtime_helpers.py | 232 +++++++++++++++++----------------
1 file changed, 120 insertions(+), 112 deletions(-)
diff --git a/agent/agent_runtime_helpers.py b/agent/agent_runtime_helpers.py
index 50c8b27b5d..39f3fa2e20 100644
--- a/agent/agent_runtime_helpers.py
+++ b/agent/agent_runtime_helpers.py
@@ -3111,32 +3111,22 @@ def _classify_tool_call_orphans(messages: List[Dict[str, Any]]):
return surviving_call_ids, result_call_ids, orphaned_results, missing_tool_calls
-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)."""
- # --- Role allowlist: drop messages with roles the API won't accept ---
+def _drop_invalid_roles(messages: List[Dict[str, Any]]) -> List[Dict[str, Any]]:
+ """Drop messages whose role the API won't accept."""
filtered = []
for msg in messages:
role = msg.get("role")
if role not in _ra().AIAgent._VALID_API_ROLES:
- _ra().logger.debug(
- "Pre-call sanitizer: dropping message with invalid role %r",
- role,
- )
+ _ra().logger.debug("Pre-call sanitizer: dropping message with invalid role %r", role)
continue
filtered.append(msg)
- messages = filtered
+ return filtered
- # --- Heal empty-content non-final messages (self-recovery) ---
- # A dead stream can leave an empty stub mid-transcript that 400s every later request;
- # repair the per-call copy so the session heals in memory. Done first so the substituted
- # turn participates in the tool-pair and dedup passes below.
- messages = repair_empty_non_final_messages(messages)
- # --- Drop empty / malformed tool_calls arrays on assistant messages ---
- # Strict providers 400 on ``tool_calls: []`` (#58755, #56980). Normalize on the
- # per-call copy (shallow-copy) so persisted history stays byte-stable.
+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."""
normalized: List[Dict[str, Any]] = []
- dropped_empty_tool_calls = 0
+ dropped = 0
for msg in messages:
if (
isinstance(msg, dict)
@@ -3145,27 +3135,27 @@ def sanitize_api_messages(messages: List[Dict[str, Any]]) -> List[Dict[str, Any]
and not (isinstance(msg["tool_calls"], list) and msg["tool_calls"])
):
msg = {k: v for k, v in msg.items() if k != "tool_calls"}
- dropped_empty_tool_calls += 1
+ dropped += 1
normalized.append(msg)
- if dropped_empty_tool_calls:
- messages = normalized
- _ra().logger.debug(
- "Pre-call sanitizer: dropped empty/invalid tool_calls on %d "
- "assistant message(s)",
- dropped_empty_tool_calls,
- )
+ if not dropped:
+ return messages
+ _ra().logger.debug(
+ "Pre-call sanitizer: dropped empty/invalid tool_calls on %d assistant message(s)", dropped
+ )
+ return normalized
- # --- Repair tool_calls whose function.name is empty/missing ---
- # Rename to a sentinel instead of dropping: the dispatch loop keeps empty-name calls
- # paired with an anti-priming result (#47967), and Responses adapters drop nameless calls (400).
- _EMPTY_NAME_SENTINEL = "invalid_tool_call"
+
+def _repair_nameless_tool_calls(messages: List[Dict[str, Any]]) -> None:
+ """Rename empty/missing ``function.name`` to a sentinel (in place).
+
+ Dropping would unpair the anti-priming result the dispatch loop keeps for
+ empty-name calls, and Responses adapters 400 on nameless calls.
+ """
+ sentinel = "invalid_tool_call"
for msg in messages:
if msg.get("role") != "assistant":
continue
- tcs = msg.get("tool_calls") or []
- if not tcs:
- continue
- for tc in tcs:
+ for tc in msg.get("tool_calls") or []:
if isinstance(tc, dict):
fn = tc.get("function")
name = fn.get("name") if isinstance(fn, dict) else getattr(fn, "name", None)
@@ -3175,87 +3165,89 @@ def sanitize_api_messages(messages: List[Dict[str, Any]]) -> List[Dict[str, Any]
if isinstance(name, str) and name.strip():
continue
_ra().logger.warning(
- "Pre-call sanitizer: repairing tool_call with empty "
- "function.name -> %r (id=%s)",
- _EMPTY_NAME_SENTINEL,
+ "Pre-call sanitizer: repairing tool_call with empty function.name -> %r (id=%s)",
+ sentinel,
_ra().AIAgent._get_tool_call_id_static(tc),
)
if isinstance(fn, dict):
- fn["name"] = _EMPTY_NAME_SENTINEL
+ fn["name"] = sentinel
elif fn is not None and hasattr(fn, "name"):
try:
- fn.name = _EMPTY_NAME_SENTINEL
+ fn.name = sentinel
except Exception:
pass
elif isinstance(tc, dict):
- tc["function"] = {"name": _EMPTY_NAME_SENTINEL, "arguments": "{}"}
+ tc["function"] = {"name": sentinel, "arguments": "{}"}
- # --- 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`` (#78071).
- _pre_id_filter_count = len(messages)
- messages = [
+
+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``.
+ """
+ kept = [
m for m in messages
if not (m.get("role") == "tool" and not (m.get("tool_call_id") or "").strip())
]
- if len(messages) != _pre_id_filter_count:
+ if len(kept) != len(messages):
_ra().logger.debug(
"Pre-call sanitizer: dropped %d tool result(s) with missing/empty tool_call_id",
- _pre_id_filter_count - len(messages),
+ len(messages) - len(kept),
)
+ return kept
- # --- Positional tool_call <-> tool_result pairing (#94704) ---
- # Strict providers (DeepSeek v4, Kimi) require results IMMEDIATELY after their call:
- # drop positional orphans, stub unanswered declared ids; matching is alias-aware (#55626/#63000/#93251).
+
+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:
+ drop positional orphans, stub unanswered declared ids; matching is alias-aware.
+ """
paired: List[Dict[str, Any]] = []
declared_calls: Dict[str, tuple] = {}
- dropped_positional_orphans = 0
- added_stubs = 0
+ dropped = 0
+ stubs = 0
def _flush_unanswered_stubs() -> None:
- nonlocal added_stubs
+ nonlocal stubs
for key in sorted(declared_calls):
tc, _variants = declared_calls[key]
- cid = coalesce_tool_call_id(tc) or key
paired.append({
"role": "tool",
"name": _ra().AIAgent._get_tool_call_name_static(tc),
"content": "[Result unavailable — see context summary above]",
- "tool_call_id": cid,
+ "tool_call_id": coalesce_tool_call_id(tc) or key,
})
- added_stubs += 1
+ stubs += 1
declared_calls.clear()
for msg in messages:
role = msg.get("role")
if role == "assistant":
- # A new assistant turn closes the previous tool-result run:
- # anything still pending was never answered positionally.
+ # A new assistant turn closes the previous tool-result run: anything still
+ # pending was never answered positionally.
_flush_unanswered_stubs()
declared_calls = {}
for tc in msg.get("tool_calls") or []:
variants = tool_call_id_variants(tc)
if variants:
- # Key on a stable representative of the alias group so
- # a result matching ANY spelling can consume the call.
+ # Key on a stable representative of the alias group so a result
+ # matching ANY spelling can consume the call.
declared_calls[sorted(variants)[0]] = (tc, variants)
paired.append(msg)
elif role == "tool":
result_variants = tool_result_id_variants(msg.get("tool_call_id"))
matched = next(
- (
- key
- for key, (_tc, variants) in declared_calls.items()
- if variants & result_variants
- ),
+ (key for key, (_tc, variants) in declared_calls.items() if variants & result_variants),
None,
)
- if matched is not None:
- paired.append(msg)
- # Consume so a duplicate result reusing the id is dropped (strict providers reject duplicates).
- declared_calls.pop(matched, None)
- else:
- dropped_positional_orphans += 1
+ if matched is None:
+ dropped += 1
+ continue
+ paired.append(msg)
+ # Consume so a duplicate result reusing the id is dropped (strict providers reject duplicates).
+ declared_calls.pop(matched, None)
else:
if role == "user":
# A user turn closes the tool-result run; later tool messages are orphans.
@@ -3263,31 +3255,34 @@ def sanitize_api_messages(messages: List[Dict[str, Any]]) -> List[Dict[str, Any]
paired.append(msg)
# The transcript may end right after an unanswered assistant turn.
_flush_unanswered_stubs()
- if dropped_positional_orphans or added_stubs:
- messages = paired
- if dropped_positional_orphans:
+ if dropped:
_ra().logger.debug(
- "Pre-call sanitizer: removed %d positionally orphaned tool result(s)",
- dropped_positional_orphans,
+ "Pre-call sanitizer: removed %d positionally orphaned tool result(s)", dropped
)
- if added_stubs:
+ if stubs:
_ra().logger.debug(
"Pre-call sanitizer: added %d stub tool result(s) for "
"positionally unanswered tool call(s)",
- added_stubs,
+ stubs,
)
+ return paired if (dropped or stubs) else messages
- # 3. Deduplicate tool_call_ids (strict providers 400 on duplicates, #58327): collapse
- # duplicates within an assistant message; drop results answering no OUTSTANDING call.
- # Track outstanding calls (not ids ever seen) because llama.cpp reuses one constant id,
- # and track the whole variant group so alias-keyed results are not deleted (#93251).
+
+def _dedupe_tool_call_ids(messages: List[Dict[str, Any]]) -> List[Dict[str, Any]]:
+ """Deduplicate tool_call_ids (strict providers 400 on duplicates).
+
+ Collapses duplicates within an assistant message and drops results answering
+ no OUTSTANDING call. Tracks outstanding calls (not ids ever seen) because
+ llama.cpp reuses one constant id, and the whole variant group so
+ alias-keyed results are not deleted.
+ """
seen_assistant_call_ids: set = set()
outstanding_call_ids: set = set()
outstanding_groups: Dict[int, frozenset] = {}
variant_to_group: Dict[str, int] = {}
next_group_id = 0
deduped: List[Dict[str, Any]] = []
- removed_dupes = 0
+ removed = 0
for msg in messages:
role = msg.get("role")
if role == "assistant" and msg.get("tool_calls"):
@@ -3295,7 +3290,7 @@ def sanitize_api_messages(messages: List[Dict[str, Any]]) -> List[Dict[str, Any]
for tc in msg.get("tool_calls") or []:
variants = tool_call_id_variants(tc)
if variants and variants & seen_assistant_call_ids:
- removed_dupes += 1
+ removed += 1
continue
if variants:
group_id = next_group_id
@@ -3310,40 +3305,38 @@ def sanitize_api_messages(messages: List[Dict[str, Any]]) -> List[Dict[str, Any]
msg = {**msg, "tool_calls": kept_tcs}
elif len(kept_tcs) != len(msg.get("tool_calls") or []):
msg = {k: v for k, v in msg.items() if k != "tool_calls"}
- deduped.append(msg)
elif role == "tool":
result_variants = tool_result_id_variants(msg.get("tool_call_id"))
candidate_groups = {
variant_to_group[variant]
for variant in result_variants
- if variant in variant_to_group
- and variant in outstanding_call_ids
+ if variant in variant_to_group and variant in outstanding_call_ids
}
if result_variants and not candidate_groups:
- removed_dupes += 1
+ removed += 1
continue
if candidate_groups:
# Consume EVERY variant of the matched call; ids are re-armed by the next call reusing them.
group_id = min(candidate_groups)
- group_variants = outstanding_groups.pop(group_id, frozenset())
- for variant in group_variants:
+ for variant in outstanding_groups.pop(group_id, frozenset()):
outstanding_call_ids.discard(variant)
seen_assistant_call_ids.discard(variant)
if variant_to_group.get(variant) == group_id:
variant_to_group.pop(variant, None)
- deduped.append(msg)
- else:
- deduped.append(msg)
- if removed_dupes:
- messages = deduped
- _ra().logger.debug(
- "Pre-call sanitizer: removed %d duplicate tool_call_id reference(s)",
- removed_dupes,
- )
+ deduped.append(msg)
+ if not removed:
+ return messages
+ _ra().logger.debug(
+ "Pre-call sanitizer: removed %d duplicate tool_call_id reference(s)", removed
+ )
+ return deduped
- # 4. Align each tool result's wire ``name`` with its call's function name: Google 400s
- # on a mismatch, which is routine when tool_search bridges via ``tool_call`` (#72089).
- # Done here, provider-agnostically, on the per-call copy only.
+
+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, which is routine when tool_search bridges via ``tool_call``.
+ """
call_names: Dict[str, str] = {}
for msg in messages:
if msg.get("role") == "assistant":
@@ -3357,23 +3350,38 @@ def sanitize_api_messages(messages: List[Dict[str, Any]]) -> List[Dict[str, Any]
aligned: List[Dict[str, Any]] = []
for msg in messages:
if msg.get("role") == "tool":
- cid = (msg.get("tool_call_id") or "").strip()
- expected = call_names.get(cid)
+ expected = call_names.get((msg.get("tool_call_id") or "").strip())
current = msg.get("name")
# Only rewrite a present, disagreeing name; clean transcripts must stay byte-identical for prompt caching.
if expected and current and current != expected:
msg = {**msg, "name": expected}
realigned.append((current, expected))
aligned.append(msg)
- if realigned:
- messages = aligned
- _ra().logger.debug(
- "Pre-call sanitizer: realigned %d tool result name(s) with their "
- "tool_call function name (%s)",
- len(realigned),
- ", ".join(f"{was} -> {now}" for was, now in realigned),
- )
- return messages
+ if not realigned:
+ return messages
+ _ra().logger.debug(
+ "Pre-call sanitizer: realigned %d tool result name(s) with their "
+ "tool_call function name (%s)",
+ len(realigned),
+ ", ".join(f"{was} -> {now}" for was, now in realigned),
+ )
+ return aligned
+
+
+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.
+ """
+ messages = _drop_invalid_roles(messages)
+ messages = repair_empty_non_final_messages(messages)
+ messages = _drop_empty_tool_calls_arrays(messages)
+ _repair_nameless_tool_calls(messages)
+ messages = _drop_results_without_ids(messages)
+ messages = _pair_tool_calls_positionally(messages)
+ messages = _dedupe_tool_call_ids(messages)
+ return _realign_tool_result_names(messages)
From b0ee6e6cf37e1faaf600c66255b9cef3109c5a64 Mon Sep 17 00:00:00 2001
From: Teknium <127238744+teknium1@users.noreply.github.com>
Date: Wed, 2 Sep 2026 18:26:03 -0700
Subject: [PATCH 03/20] refactor(agent/agent_runtime_helpers): split
switch_model into 9 phase helpers + snapshot field table
---
agent/agent_runtime_helpers.py | 619 ++++++++++++++++-----------------
1 file changed, 305 insertions(+), 314 deletions(-)
diff --git a/agent/agent_runtime_helpers.py b/agent/agent_runtime_helpers.py
index 39f3fa2e20..cf2362c09f 100644
--- a/agent/agent_runtime_helpers.py
+++ b/agent/agent_runtime_helpers.py
@@ -2189,42 +2189,67 @@ def _apply_switched_provider_request_overrides(agent, new_provider):
agent.request_overrides = overrides
-def switch_model(
- agent,
- new_model,
- new_provider,
- api_key='',
- base_url='',
- api_mode='',
- capabilities=None,
-):
- """Switch the model/provider in-place for a live agent (rebuild clients, caching flags, compressor).
+_SWITCH_SNAPSHOT_FIELDS = (
+ "model",
+ "provider",
+ "requested_provider",
+ "base_url",
+ "api_mode",
+ "api_key",
+ "client",
+ "_anthropic_client",
+ "_anthropic_api_key",
+ "_anthropic_base_url",
+ "_is_anthropic_oauth",
+ "_config_context_length",
+ "_reasoning_echo_flag",
+ "runtime_capabilities",
+ # Pool reload is part of the switch and must be reversible on rollback.
+ "_credential_pool",
+ "_credential_pool_entry_id",
+)
+_MISSING = object()
- Mirrors ``_try_activate_fallback()`` but also updates ``_primary_runtime`` so
- the change persists across turns.
+
+def _snapshot_switch_state(agent) -> Dict[str, Any]:
+ """Snapshot every field the swap+rebuild mutates so a failed rebuild rolls back atomically.
+
+ Otherwise a new model name + OLD client 400s next turn. The sentinel distinguishes
+ unset from None: tests build bare agents via ``__new__`` without all fields.
"""
+ snapshot = {name: getattr(agent, name, _MISSING) for name in _SWITCH_SNAPSHOT_FIELDS}
+ # Shallow-copy the dict so mutating the live one doesn't poison the rollback target.
+ snapshot["_client_kwargs"] = dict(getattr(agent, "_client_kwargs", {}) or {})
+ return snapshot
+
+
+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
+ try:
+ setattr(agent, name, value)
+ except Exception: # noqa: BLE001
+ pass
+
+
+def _resolve_switch_destination(agent, new_model, new_provider, base_url, api_mode, capabilities, old_norm, new_norm):
+ """Resolve ``(api_mode, base_url, destination_capabilities)`` for the switch target."""
from hermes_cli.providers import determine_api_mode
from agent.native_compaction import resolve_native_compaction_capabilities
-
- old_model = agent.model
- old_provider = agent.provider
- old_norm = (old_provider or "").strip().lower()
- new_norm = (new_provider or "").strip().lower()
+ from hermes_cli.models import opencode_provider_family
# Pass model so dual-wire providers (Nous Portal anthropic/* -> Messages) resolve correctly.
if not api_mode:
api_mode = determine_api_mode(new_provider, base_url, model=new_model)
-
if not base_url and new_norm == "openai":
# An omitted URL means the provider's canonical direct endpoint.
base_url = "https://api.openai.com/v1"
-
# Same-provider switches may omit base_url (e.g. credential refresh); resolve
# capabilities from the endpoint the normalization below retains.
effective_base_url = base_url
if not effective_base_url and old_norm == new_norm:
effective_base_url = getattr(agent, "base_url", "")
-
destination_capabilities = (
dict(capabilities)
if isinstance(capabilities, dict)
@@ -2232,14 +2257,11 @@ def switch_model(
model=new_model,
base_url=effective_base_url,
provider=new_provider,
- is_codex_backend=new_norm == 'openai-codex',
+ is_codex_backend=new_norm == "openai-codex",
)
)
-
# Guard against a trailing /v1 on OpenCode base_url reaching the anthropic_messages
# client (double-/v1 404); model_switch already strips it, direct callers may not.
- from hermes_cli.models import opencode_provider_family
-
if (
api_mode == "anthropic_messages"
and opencode_provider_family(new_provider) is not None
@@ -2247,181 +2269,130 @@ def switch_model(
and base_url
):
base_url = re.sub(r"/v1/?$", "", base_url)
+ return api_mode, base_url, destination_capabilities
- # Snapshot every field the swap+rebuild mutates so a failed rebuild rolls back atomically
- # (else new model name + OLD client -> 400s next turn). Sentinel distinguishes unset from
- # None: tests build bare agents via __new__ without all fields.
- _MISSING = object()
- _snapshot = {
- name: getattr(agent, name, _MISSING)
- for name in (
- "model",
- "provider",
- "requested_provider",
- "base_url",
- "api_mode",
- "api_key",
- "client",
- "_anthropic_client",
- "_anthropic_api_key",
- "_anthropic_base_url",
- "_is_anthropic_oauth",
- "_config_context_length",
- "_reasoning_echo_flag",
- "runtime_capabilities",
- )
- }
- # Shallow-copy the dict so mutating the live one doesn't poison the rollback target.
- _snapshot["_client_kwargs"] = dict(getattr(agent, "_client_kwargs", {}) or {})
- # Pool reload is part of this switch and must be reversible on rollback (#52727).
- _snapshot["_credential_pool"] = getattr(agent, "_credential_pool", _MISSING)
- _snapshot["_credential_pool_entry_id"] = getattr(
- agent, "_credential_pool_entry_id", _MISSING
- )
- def _restore_snapshot() -> None:
- for _name, _value in _snapshot.items():
- if _value is _MISSING:
- # Attribute did not exist before the swap; don't fabricate it.
- continue
+def _build_switched_client(agent, new_provider, api_key, base_url, api_mode, new_norm) -> None:
+ """Build the client for the switched-to destination (MoA facade / native Anthropic / OpenAI wire)."""
+ if new_norm == "moa":
+ from agent.moa_loop import build_moa_facade
+
+ # MoA speaks only chat.completions via the MoAClient facade; the aggregator's real
+ # transport is applied inside the fan-out. Pin api_mode so the loop never dispatches
+ # client.responses.create against the facade (matches agent_init.py).
+ agent.api_mode = "chat_completions"
+ agent.api_key = api_key or "moa-virtual-provider"
+ agent.base_url = "moa://local"
+ agent._client_kwargs = {}
+ agent.client = build_moa_facade(agent, agent.model)
+ return
+ if api_mode == "anthropic_messages":
+ from agent.anthropic_adapter import build_anthropic_client, resolve_anthropic_token, _is_oauth_token
+
+ # Only fall back to ANTHROPIC_TOKEN for native Anthropic; other anthropic_messages
+ # providers must never receive Anthropic credentials.
+ is_native_anthropic = new_provider == "anthropic"
+ effective_key = api_key or agent.api_key or (resolve_anthropic_token() if is_native_anthropic else "") or ""
+ # MiniMax OAuth: per-request callable token provider survives 15-min expiry
+ # (rationale in agent_init.py).
+ if new_provider == "minimax-oauth" and isinstance(effective_key, str) and effective_key:
try:
- setattr(agent, _name, _value)
- except Exception: # noqa: BLE001
- pass
-
- try:
- # Clear the per-config override so the new model's context window is re-resolved.
- agent._config_context_length = None
-
- # ── Swap core runtime fields ──
- agent.model = new_model
- agent.provider = new_provider
- agent.requested_provider = new_provider
- # Re-read reasoning_echo so the flag reflects the new primary model (see _reasoning_echo_opt_in).
- agent._reasoning_echo_flag = agent._read_reasoning_echo_from_config()
- # Empty base_url while the provider changes means upstream resolution failed; falling
- # back to the old provider's URL pairs the wrong host and persists via _primary_runtime
- # (#47828). Fail loud. Same-provider re-select (credential refresh) may keep the URL.
- if base_url:
- agent.base_url = base_url
- elif old_norm != new_norm:
- raise ValueError(
- f"switch_model: no base_url resolved for provider "
- f"'{new_provider}' (switching from '{old_provider}'); "
- "refusing to keep the previous provider's endpoint"
- )
- agent.api_mode = api_mode
- # New api_mode may need a different transport.
- if hasattr(agent, "_transport_cache"):
- agent._transport_cache.clear()
- if api_key:
- agent.api_key = api_key
-
- # Reload the credential pool on provider change (#52727): a pool with a mismatched
- # provider makes recover_with_credential_pool short-circuit. Reload failure is non-fatal.
- if old_norm != new_norm or getattr(agent, "_credential_pool", None) is None:
- # A pool bound to the old provider is worse than none: the recovery guard rejects it.
- agent._credential_pool = None
- agent._credential_pool_entry_id = None
- try:
- from agent.credential_pool import load_pool
- agent._credential_pool = load_pool(new_provider)
- except Exception as _pool_exc: # noqa: BLE001
+ from hermes_cli.auth import build_minimax_oauth_token_provider
+ effective_key = build_minimax_oauth_token_provider()
+ except Exception as _mm_exc: # noqa: BLE001
logger.warning(
- "switch_model: credential pool reload failed for %s (%s); "
- "continuing without pool rotation this turn",
- new_provider, _pool_exc,
+ "MiniMax OAuth: failed to install per-request token provider "
+ "on switch (%s); using static bearer.",
+ _mm_exc,
)
- # ── Build new client ──
- if new_norm == "moa":
- from agent.moa_loop import build_moa_facade
+ agent.api_key = effective_key
+ agent._anthropic_api_key = effective_key
+ agent._anthropic_base_url = base_url or getattr(agent, "_anthropic_base_url", None)
+ agent._anthropic_client = build_anthropic_client(
+ effective_key, agent._anthropic_base_url,
+ timeout=get_provider_request_timeout(agent.provider, agent.model),
+ )
+ agent._is_anthropic_oauth = (
+ _is_oauth_token(effective_key) if (is_native_anthropic and isinstance(effective_key, str)) else False
+ )
+ agent.client = None
+ agent._client_kwargs = {}
+ return
+ effective_base = base_url or agent.base_url
+ agent._client_kwargs = {"api_key": api_key or agent.api_key, "base_url": effective_base}
+ try:
+ from hermes_cli.config import (
+ apply_custom_provider_tls_to_client_kwargs,
+ get_compatible_custom_providers,
+ load_config_readonly,
+ )
- # MoA speaks only chat.completions via the MoAClient facade; the aggregator's real
- # transport is applied inside the fan-out. Pin api_mode so the loop never dispatches
- # client.responses.create against the facade (matches agent_init.py).
- agent.api_mode = "chat_completions"
- agent.api_key = api_key or "moa-virtual-provider"
- agent.base_url = "moa://local"
- agent._client_kwargs = {}
- agent.client = build_moa_facade(agent, agent.model)
- elif api_mode == "anthropic_messages":
- from agent.anthropic_adapter import (
- build_anthropic_client,
- resolve_anthropic_token,
- _is_oauth_token,
- )
- # Only fall back to ANTHROPIC_TOKEN for native Anthropic; other anthropic_messages
- # providers must never receive Anthropic credentials.
- _is_native_anthropic = new_provider == "anthropic"
- effective_key = (api_key or agent.api_key or resolve_anthropic_token() or "") if _is_native_anthropic else (api_key or agent.api_key or "")
-
- # MiniMax OAuth: per-request callable token provider survives 15-min expiry
- # (rationale in agent_init.py).
- if new_provider == "minimax-oauth" and isinstance(effective_key, str) and effective_key:
- 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
- logger.warning(
- "MiniMax OAuth: failed to install per-request token provider "
- "on switch (%s); using static bearer.",
- _mm_exc,
- )
-
- agent.api_key = effective_key
- agent._anthropic_api_key = effective_key
- agent._anthropic_base_url = base_url or getattr(agent, "_anthropic_base_url", None)
- agent._anthropic_client = build_anthropic_client(
- effective_key, agent._anthropic_base_url,
- timeout=get_provider_request_timeout(agent.provider, agent.model),
- )
- agent._is_anthropic_oauth = _is_oauth_token(effective_key) if (_is_native_anthropic and isinstance(effective_key, str)) else False
- agent.client = None
- agent._client_kwargs = {}
- else:
- effective_key = api_key or agent.api_key
- effective_base = base_url or agent.base_url
- agent._client_kwargs = {
- "api_key": effective_key,
- "base_url": effective_base,
- }
- try:
- from hermes_cli.config import (
- apply_custom_provider_tls_to_client_kwargs,
- get_compatible_custom_providers,
- load_config_readonly,
- )
-
- # Read live config, not agent._custom_providers, so mid-session ssl_ca_cert /
- # ssl_verify edits are honored (#15779).
- apply_custom_provider_tls_to_client_kwargs(
- agent._client_kwargs,
- str(effective_base or ""),
- get_compatible_custom_providers(load_config_readonly()),
- )
- except Exception:
- logger.debug("custom-provider TLS resolution skipped on switch_model", exc_info=True)
- _sm_timeout = get_provider_request_timeout(agent.provider, agent.model)
- if _sm_timeout is not None:
- agent._client_kwargs["timeout"] = _sm_timeout
- # Reapply provider headers (OpenRouter HTTP-Referer/X-Title) lost when
- # _client_kwargs was rebuilt; otherwise attribution shows "Unknown".
- agent._apply_client_headers_for_base_url(effective_base)
- agent.client = agent._create_openai_client(
- dict(agent._client_kwargs),
- reason="switch_model",
- shared=True,
- )
-
- sync_credential_pool_entry_id(agent)
+ # Read live config, not agent._custom_providers, so mid-session ssl_ca_cert /
+ # ssl_verify edits are honored.
+ apply_custom_provider_tls_to_client_kwargs(
+ agent._client_kwargs,
+ str(effective_base or ""),
+ get_compatible_custom_providers(load_config_readonly()),
+ )
except Exception:
- # Roll back to the pre-swap snapshot so the agent stays consistent; callers
- # (cli.py / gateway/run.py / tui_gateway) catch the re-raised exception.
- _restore_snapshot()
- raise
+ logger.debug("custom-provider TLS resolution skipped on switch_model", exc_info=True)
+ timeout = get_provider_request_timeout(agent.provider, agent.model)
+ if timeout is not None:
+ agent._client_kwargs["timeout"] = timeout
+ # Reapply provider headers (OpenRouter HTTP-Referer/X-Title) lost when
+ # _client_kwargs was rebuilt; otherwise attribution shows "Unknown".
+ agent._apply_client_headers_for_base_url(effective_base)
+ agent.client = agent._create_openai_client(dict(agent._client_kwargs), reason="switch_model", shared=True)
- # LM Studio: preload before probing context length.
- _sm_custom_providers = None
+
+def _swap_switch_runtime(agent, new_model, new_provider, api_key, base_url, api_mode, old_provider, old_norm, new_norm) -> None:
+ """Swap identity/transport fields, reload the pool, rebuild the client (rolled back by the caller on error)."""
+ # Clear the per-config override so the new model's context window is re-resolved.
+ agent._config_context_length = None
+ agent.model = new_model
+ agent.provider = new_provider
+ agent.requested_provider = new_provider
+ # Re-read reasoning_echo so the flag reflects the new primary model (see _reasoning_echo_opt_in).
+ agent._reasoning_echo_flag = agent._read_reasoning_echo_from_config()
+ # Empty base_url while the provider changes means upstream resolution failed; falling
+ # back to the old provider's URL pairs the wrong host and persists via _primary_runtime.
+ # Fail loud. Same-provider re-select (credential refresh) may keep the URL.
+ if base_url:
+ agent.base_url = base_url
+ elif old_norm != new_norm:
+ raise ValueError(
+ f"switch_model: no base_url resolved for provider "
+ f"'{new_provider}' (switching from '{old_provider}'); "
+ "refusing to keep the previous provider's endpoint"
+ )
+ agent.api_mode = api_mode
+ # New api_mode may need a different transport.
+ if hasattr(agent, "_transport_cache"):
+ agent._transport_cache.clear()
+ if api_key:
+ agent.api_key = api_key
+ # Reload the credential pool on provider change: a pool with a mismatched provider
+ # makes recover_with_credential_pool short-circuit. Reload failure is non-fatal.
+ if old_norm != new_norm or getattr(agent, "_credential_pool", None) is None:
+ # A pool bound to the old provider is worse than none: the recovery guard rejects it.
+ agent._credential_pool = None
+ agent._credential_pool_entry_id = None
+ try:
+ from agent.credential_pool import load_pool
+ agent._credential_pool = load_pool(new_provider)
+ except Exception as _pool_exc: # noqa: BLE001
+ logger.warning(
+ "switch_model: credential pool reload failed for %s (%s); "
+ "continuing without pool rotation this turn",
+ new_provider, _pool_exc,
+ )
+ _build_switched_client(agent, new_provider, api_key, base_url, api_mode, new_norm)
+ sync_credential_pool_entry_id(agent)
+
+
+def _resolve_switch_context_length(agent, snapshot):
+ """Resolve the destination context length (LM Studio preload first); returns ``(custom_providers, effective_len)``."""
+ custom_providers = None
try:
from hermes_cli.config import (
get_compatible_custom_providers,
@@ -2429,119 +2400,70 @@ def switch_model(
load_config,
)
- _sm_cfg = load_config()
- _sm_custom_providers = get_compatible_custom_providers(_sm_cfg)
- _destination_context_intent = get_custom_provider_context_length(
- model=agent.model,
- base_url=agent.base_url,
- custom_providers=_sm_custom_providers,
+ custom_providers = get_compatible_custom_providers(load_config())
+ intent = get_custom_provider_context_length(
+ model=agent.model, base_url=agent.base_url, custom_providers=custom_providers
)
except Exception:
- _destination_context_intent = None
- agent._config_context_length = _destination_context_intent
+ intent = None
+ agent._config_context_length = intent
+ runtime_len = None
if hasattr(agent, "_ensure_lmstudio_runtime_loaded"):
try:
- _runtime_context_length = agent._ensure_lmstudio_runtime_loaded(
- _destination_context_intent
- )
+ runtime_len = agent._ensure_lmstudio_runtime_loaded(intent)
except Exception:
- _restore_snapshot()
+ _restore_switch_snapshot(agent, snapshot)
raise
- else:
- _runtime_context_length = None
- if (
- hasattr(agent, "_lmstudio_load_was_unverified")
- and agent._lmstudio_load_was_unverified(_runtime_context_length)
- ):
+ if hasattr(agent, "_lmstudio_load_was_unverified") and agent._lmstudio_load_was_unverified(runtime_len):
logger.warning(
"LM Studio model activation was rejected or completed without a "
"verifiable active context length during model switch; continuing "
"with configured context"
)
+ effective = intent
if hasattr(agent, "_effective_lmstudio_context_length"):
- _effective_context_length = agent._effective_lmstudio_context_length(
- _destination_context_intent,
- _runtime_context_length,
- )
- else:
- _effective_context_length = _destination_context_intent
+ effective = agent._effective_lmstudio_context_length(intent, runtime_len)
+ return custom_providers, effective
- # Refresh the custom-provider snapshot from the config just loaded so the prompt_caching
- # lookup sees flags added to config.yaml after session start.
- if _sm_custom_providers is not None:
- agent._custom_providers = _sm_custom_providers
- agent._use_prompt_caching, agent._use_native_cache_layout = (
- agent._anthropic_prompt_cache_policy(
- provider=new_provider,
- base_url=agent.base_url,
- api_mode=api_mode,
- model=new_model,
- )
- )
- # ── Update context compressor ──
- if hasattr(agent, "context_compressor") and agent.context_compressor:
- from agent.model_metadata import get_model_context_length
- if _sm_custom_providers is None:
- try:
- from hermes_cli.config import get_compatible_custom_providers, load_config
- _sm_custom_providers = get_compatible_custom_providers(load_config())
- except Exception:
- _sm_custom_providers = None
- # agent.api_key may be a callable (Azure Foundry Entra ID); get_model_context_length
- # expects a string for live probes, so coerce defensively.
- _ctx_api_key = agent.api_key if isinstance(agent.api_key, str) else ""
+def _update_switch_compressor(agent, custom_providers, effective_context_length, snapshot) -> None:
+ """Point the context compressor at the new model (rolls back the switch on failure)."""
+ from agent.model_metadata import get_model_context_length
+ if custom_providers is None:
try:
- new_context_length = get_model_context_length(
- agent.model,
- base_url=agent.base_url,
- api_key=_ctx_api_key,
- provider=agent.provider,
- config_context_length=_effective_context_length,
- custom_providers=_sm_custom_providers,
- )
- agent.context_compressor.update_model(
- model=agent.model,
- context_length=new_context_length,
- base_url=agent.base_url,
- api_key=agent.api_key, # context_compressor forwards to call_llm; callable preserved
- provider=agent.provider,
- api_mode=agent.api_mode,
- )
+ from hermes_cli.config import get_compatible_custom_providers, load_config
+ custom_providers = get_compatible_custom_providers(load_config())
except Exception:
- _restore_snapshot()
- raise
-
- # Re-read the per-model reasoning_effort override so it applies immediately
- # (per-model > global; YAML False = disabled).
+ custom_providers = None
+ # agent.api_key may be a callable (Azure Foundry Entra ID); get_model_context_length
+ # expects a string for live probes, so coerce defensively.
+ ctx_api_key = agent.api_key if isinstance(agent.api_key, str) else ""
try:
- from hermes_constants import resolve_reasoning_config
- from hermes_cli.config import load_config as _sm_load_config
-
- _reasoning_cfg = _sm_load_config() or {}
- agent.reasoning_config = resolve_reasoning_config(_reasoning_cfg, agent.model)
- logger.info(
- "switch_model: reasoning_config resolved for %s: %s",
- agent.model, agent.reasoning_config,
+ new_context_length = get_model_context_length(
+ agent.model,
+ base_url=agent.base_url,
+ api_key=ctx_api_key,
+ provider=agent.provider,
+ config_context_length=effective_context_length,
+ custom_providers=custom_providers,
)
- except Exception as _reasoning_err:
- logger.debug("switch_model: could not re-resolve reasoning_config: %s", _reasoning_err)
+ agent.context_compressor.update_model(
+ model=agent.model,
+ context_length=new_context_length,
+ base_url=agent.base_url,
+ api_key=agent.api_key, # context_compressor forwards to call_llm; callable preserved
+ provider=agent.provider,
+ api_mode=agent.api_mode,
+ )
+ except Exception:
+ _restore_switch_snapshot(agent, snapshot)
+ raise
- # Invalidate the cached system prompt so it rebuilds next turn.
- agent._cached_system_prompt = None
- # Publish the destination capability map only after every runtime setup
- # above has succeeded. Failed switches must leave the old map intact.
- agent.runtime_capabilities = destination_capabilities
-
- # Reset the cross-turn stale-call circuit breaker (#58962); otherwise the latched
- # streak keeps short-circuiting the freshly selected healthy provider.
- from agent.chat_completion_helpers import _reset_stale_streak
- _reset_stale_streak(agent)
-
- # Update _primary_runtime so the change persists across turns.
- _cc = agent.context_compressor if hasattr(agent, "context_compressor") and agent.context_compressor else None
- agent._primary_runtime = {
+def _build_primary_runtime_snapshot(agent, api_mode) -> Dict[str, Any]:
+ """The ``_primary_runtime`` record that persists a switch across turns."""
+ cc = agent.context_compressor if hasattr(agent, "context_compressor") and agent.context_compressor else None
+ rt = {
"model": agent.model,
"provider": agent.provider,
"requested_provider": agent.requested_provider,
@@ -2554,30 +2476,32 @@ def switch_model(
"reasoning_config": dict(agent.reasoning_config) if getattr(agent, "reasoning_config", None) else None,
"reasoning_echo_flag": getattr(agent, "_reasoning_echo_flag", False),
# Overrides must travel with the switched-to identity or a later recovery/restore
- # resurrects PRE-switch overrides from the stale init snapshot (#75091).
+ # resurrects PRE-switch overrides from the stale init snapshot.
"request_overrides": dict(getattr(agent, "request_overrides", {}) or {}),
"runtime_capabilities": dict(getattr(agent, "runtime_capabilities", {}) or {}),
- "compressor_model": getattr(_cc, "model", agent.model) if _cc else agent.model,
- "compressor_base_url": getattr(_cc, "base_url", agent.base_url) if _cc else agent.base_url,
- "compressor_api_key": getattr(_cc, "api_key", "") if _cc else "",
- "compressor_provider": getattr(_cc, "provider", agent.provider) if _cc else agent.provider,
- "compressor_context_length": _cc.context_length if _cc else 0,
- "compressor_api_mode": getattr(_cc, "api_mode", agent.api_mode) if _cc else agent.api_mode,
- "compressor_threshold_tokens": _cc.threshold_tokens if _cc else 0,
+ "compressor_model": getattr(cc, "model", agent.model) if cc else agent.model,
+ "compressor_base_url": getattr(cc, "base_url", agent.base_url) if cc else agent.base_url,
+ "compressor_api_key": getattr(cc, "api_key", "") if cc else "",
+ "compressor_provider": getattr(cc, "provider", agent.provider) if cc else agent.provider,
+ "compressor_context_length": cc.context_length if cc else 0,
+ "compressor_api_mode": getattr(cc, "api_mode", agent.api_mode) if cc else agent.api_mode,
+ "compressor_threshold_tokens": cc.threshold_tokens if cc else 0,
}
if api_mode == "anthropic_messages":
- agent._primary_runtime.update({
+ rt.update({
"anthropic_api_key": agent._anthropic_api_key,
"anthropic_base_url": agent._anthropic_base_url,
"is_anthropic_oauth": agent._is_anthropic_oauth,
})
+ return rt
- # ── Reset fallback state ──
+
+def _finish_switch(agent, new_provider, old_norm, new_norm) -> None:
+ """Post-switch bookkeeping: fallback reset/prune, request_overrides, billing route."""
agent._fallback_activated = False
agent._provider_fallback_active = False
agent._provider_fallback_route = None
agent._fallback_index = 0
-
# On a deliberate provider swap, prune fallback entries targeting the OLD or NEW primary;
# otherwise a failed turn silently re-activates the provider the user just rejected.
fallback_chain = list(getattr(agent, "_fallback_chain", []) or [])
@@ -2588,35 +2512,102 @@ def switch_model(
]
agent._fallback_chain = fallback_chain
agent._fallback_model = fallback_chain[0] if fallback_chain else None
-
# Apply the switched-to provider's request_overrides (custom_providers extra_body).
try:
_apply_switched_provider_request_overrides(agent, new_provider)
except Exception:
logger.debug("switch_model: request_overrides re-derivation failed", exc_info=True)
+
+def _persist_switch_billing_route(agent) -> None:
+ """Persist the billing route so dashboard Model cards show the post-switch provider."""
+ # _session_db / session_id may be unset (tests, bare agents).
+ session_db = getattr(agent, "_session_db", None)
+ session_id = getattr(agent, "session_id", None)
+ if session_db is None or not session_id:
+ return
+ try:
+ session_db.update_session_billing_route(
+ session_id,
+ provider=agent.provider,
+ base_url=agent.base_url,
+ billing_mode=getattr(agent, "api_mode", None),
+ )
+ except Exception:
+ logger.warning("Failed to persist billing route after model switch", exc_info=True)
+
+
+def switch_model(
+ agent,
+ new_model,
+ new_provider,
+ api_key='',
+ base_url='',
+ api_mode='',
+ capabilities=None,
+):
+ """Switch the model/provider in-place for a live agent (rebuild clients, caching flags, compressor).
+
+ Mirrors ``_try_activate_fallback()`` but also updates ``_primary_runtime`` so the
+ change persists across turns. Any failure during the swap/rebuild rolls the agent
+ back to its pre-switch snapshot and re-raises (callers catch).
+ """
+ old_model = agent.model
+ old_provider = agent.provider
+ old_norm = (old_provider or "").strip().lower()
+ new_norm = (new_provider or "").strip().lower()
+ api_mode, base_url, destination_capabilities = _resolve_switch_destination(
+ agent, new_model, new_provider, base_url, api_mode, capabilities, old_norm, new_norm
+ )
+ snapshot = _snapshot_switch_state(agent)
+ try:
+ _swap_switch_runtime(
+ agent, new_model, new_provider, api_key, base_url, api_mode, old_provider, old_norm, new_norm
+ )
+ except Exception:
+ _restore_switch_snapshot(agent, snapshot)
+ raise
+
+ custom_providers, effective_context_length = _resolve_switch_context_length(agent, snapshot)
+ # Refresh the custom-provider snapshot from the config just loaded so the prompt_caching
+ # lookup sees flags added to config.yaml after session start.
+ if custom_providers is not None:
+ agent._custom_providers = custom_providers
+ agent._use_prompt_caching, agent._use_native_cache_layout = agent._anthropic_prompt_cache_policy(
+ provider=new_provider, base_url=agent.base_url, api_mode=api_mode, model=new_model
+ )
+ if hasattr(agent, "context_compressor") and agent.context_compressor:
+ _update_switch_compressor(agent, custom_providers, effective_context_length, snapshot)
+
+ # Re-read the per-model reasoning_effort override so it applies immediately
+ # (per-model > global; YAML False = disabled).
+ try:
+ from hermes_constants import resolve_reasoning_config
+ from hermes_cli.config import load_config as _sm_load_config
+
+ agent.reasoning_config = resolve_reasoning_config(_sm_load_config() or {}, agent.model)
+ logger.info(
+ "switch_model: reasoning_config resolved for %s: %s", agent.model, agent.reasoning_config
+ )
+ except Exception as _reasoning_err:
+ logger.debug("switch_model: could not re-resolve reasoning_config: %s", _reasoning_err)
+
+ # Invalidate the cached system prompt so it rebuilds next turn.
+ agent._cached_system_prompt = None
+ # Publish the destination capability map only after every runtime setup above has
+ # succeeded. Failed switches must leave the old map intact.
+ agent.runtime_capabilities = destination_capabilities
+ # Reset the cross-turn stale-call circuit breaker; otherwise the latched streak keeps
+ # short-circuiting the freshly selected healthy provider.
+ from agent.chat_completion_helpers import _reset_stale_streak
+ _reset_stale_streak(agent)
+ agent._primary_runtime = _build_primary_runtime_snapshot(agent, api_mode)
+ _finish_switch(agent, new_provider, old_norm, new_norm)
logger.info(
"Model switched in-place: %s (%s) -> %s (%s)",
old_model, old_provider, new_model, new_provider,
)
-
- # Persist billing route so dashboard Model cards show the post-switch provider (#48248).
- # _session_db / session_id may be unset (tests, bare agents).
- _session_db = getattr(agent, "_session_db", None)
- _session_id = getattr(agent, "session_id", None)
- if _session_db is not None and _session_id:
- try:
- _session_db.update_session_billing_route(
- _session_id,
- provider=agent.provider,
- base_url=agent.base_url,
- billing_mode=getattr(agent, "api_mode", None),
- )
- except Exception:
- logger.warning(
- "Failed to persist billing route after model switch",
- exc_info=True,
- )
+ _persist_switch_billing_route(agent)
def invoke_tool(agent, function_name: str, function_args: dict, effective_task_id: str,
From 7c62fc26dba248872654c131fcfaa7a08f474c0b Mon Sep 17 00:00:00 2001
From: Teknium <127238744+teknium1@users.noreply.github.com>
Date: Wed, 2 Sep 2026 18:32:30 -0700
Subject: [PATCH 04/20] refactor(agent/agent_runtime_helpers): split
recover_with_credential_pool by failover reason + status->reason table
---
agent/agent_runtime_helpers.py | 371 ++++++++++++++++-----------------
1 file changed, 182 insertions(+), 189 deletions(-)
diff --git a/agent/agent_runtime_helpers.py b/agent/agent_runtime_helpers.py
index cf2362c09f..d26f334d7c 100644
--- a/agent/agent_runtime_helpers.py
+++ b/agent/agent_runtime_helpers.py
@@ -781,6 +781,141 @@ def sync_credential_pool_entry_id(agent) -> None:
agent._credential_pool_entry_id = None
+_STATUS_TO_FAILOVER_REASON = {
+ 402: FailoverReason.billing,
+ 429: FailoverReason.rate_limit,
+ 401: FailoverReason.auth,
+ 403: FailoverReason.auth,
+}
+
+_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]]:
+ """``(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.
+ """
+ api_key_hint = getattr(agent, "api_key", None) or None
+ raw_id = getattr(agent, "_credential_pool_entry_id", None)
+ credential_id = raw_id if isinstance(raw_id, str) and raw_id else None
+ if not api_key_hint:
+ cur = pool.current()
+ if cur:
+ api_key_hint = getattr(cur, "runtime_api_key", None)
+ if not credential_id:
+ current_id = getattr(cur, "id", None)
+ if isinstance(current_id, str) and current_id:
+ credential_id = current_id
+ return api_key_hint, credential_id
+
+
+def _is_entitlement_403(agent, status_code, error_context) -> bool:
+ """Entitlement 403s look like auth failures but refresh cannot fix them.
+
+ Any xai-oauth 403 is treated as entitlement EXCEPT xAI's stale-token signals
+ (``[WKE=unauthenticated:...]``, "could not be validated"), which must stay refreshable.
+ """
+ if agent._is_entitlement_failure(error_context, status_code):
+ return True
+ if status_code != 403:
+ return False
+ haystack = " ".join(
+ str(error_context.get(k) or "").lower()
+ for k in ("message", "reason", "code", "error")
+ if isinstance(error_context, dict)
+ )
+ if "oauth authentication is currently not allowed for this organization" in haystack:
+ return True
+ provider = agent.provider or ""
+ if provider == "anthropic" and getattr(agent, "api_mode", "") == "anthropic_messages":
+ return True
+ if provider == "xai-oauth":
+ return not (
+ "[wke=unauthenticated:" in haystack
+ or "oauth2 access token could not be validated" in haystack
+ )
+ return False
+
+
+def _recover_auth_failure(agent, pool, *, status_code, has_retried_429, error_context, api_key_hint, credential_id, rotate_and_swap):
+ if _is_entitlement_403(agent, status_code, error_context):
+ _ra().logger.info(
+ "Credential %s — entitlement-shaped 403 from %s; "
+ "skipping pool refresh (account lacks subscription, "
+ "not a transient auth failure).",
+ status_code if status_code is not None else "auth",
+ agent.provider or "provider",
+ )
+ return False, has_retried_429
+ # Refresh the entry that supplied the failing key, not current(): refreshing a
+ # healthy entry burns its single-use refresh token for a failure it never had.
+ refresh_kwargs = {"api_key_hint": api_key_hint}
+ if credential_id:
+ refresh_kwargs["credential_id"] = credential_id
+ refreshed = pool.try_refresh_matching(**refresh_kwargs)
+ if refreshed is not None:
+ # try_refresh_matching() reports success even when upstream keeps rejecting;
+ # cap same-entry refreshes so a single-entry pool falls through to fallback.
+ refreshed_id = getattr(refreshed, "id", None)
+ if refreshed_id is not None:
+ refresh_counts = getattr(agent, "_auth_pool_refresh_counts", None)
+ if refresh_counts is None:
+ refresh_counts = {}
+ agent._auth_pool_refresh_counts = refresh_counts
+ refresh_key = (agent.provider, refreshed_id)
+ refresh_counts[refresh_key] = refresh_counts.get(refresh_key, 0) + 1
+ if refresh_counts[refresh_key] > _MAX_AUTH_REFRESH_ATTEMPTS:
+ _ra().logger.warning(
+ "Credential auth failure persists after %s refreshes for "
+ "pool entry %s — treating as unrecoverable and allowing "
+ "fallback to activate.",
+ refresh_counts[refresh_key] - 1,
+ refreshed_id,
+ )
+ return False, has_retried_429
+ _ra().logger.info("Credential auth failure — refreshed pool entry %s", getattr(refreshed, 'id', '?'))
+ agent._swap_credential(refreshed)
+ return True, has_retried_429
+ # Refresh failed; rotate (the failed entry is already marked exhausted).
+ if rotate_and_swap(401, "auth refresh failed"):
+ return True, False
+ return False, has_retried_429
+
+
+def _recover_rate_limit(pool, *, has_retried_429, error_context, api_key_hint, credential_id, rotate_and_swap):
+ # Already-exhausted credential: rotate immediately. Avoids the "cancel-between-429s"
+ # trap where the local has_retried_429 resets per prompt and retries forever.
+ current_entry = None
+ if credential_id:
+ current_entry = next((e for e in pool.entries() if e.id == credential_id), None)
+ if api_key_hint:
+ current_entry = current_entry or next(
+ (e for e in pool.entries() if e.runtime_api_key == api_key_hint), None
+ )
+ if current_entry is None:
+ current_entry = pool.current()
+ current_last_status = getattr(current_entry, "last_status", None) if current_entry else None
+ if current_last_status == STATUS_EXHAUSTED:
+ _ra().logger.info(
+ "Credential already exhausted (last_status=%s) — rotating immediately instead of retrying",
+ current_last_status,
+ )
+ return (True, False) if rotate_and_swap(429, "rate limit, pre-exhausted") else (False, True)
+ usage_limit_reached = False
+ if error_context:
+ context_reason = str(error_context.get("reason") or "").lower()
+ context_message = str(error_context.get("message") or "").lower()
+ usage_limit_reached = any(t in context_reason for t in _USAGE_LIMIT_REASON_TOKENS) or any(
+ t in context_message for t in _USAGE_LIMIT_MESSAGE_TOKENS
+ )
+ if not has_retried_429 and not usage_limit_reached:
+ return False, True
+ return (True, False) if rotate_and_swap(429, "rate limit") else (False, True)
+
+
def recover_with_credential_pool(
agent,
*,
@@ -795,97 +930,63 @@ def recover_with_credential_pool(
Returns (recovered, has_retried_429). Rate limits: retry once, then rotate.
Billing exhaustion: rotate immediately. Auth failures: refresh before rotating.
``classified_reason`` honors the structured classifier over raw HTTP codes
- (e.g. Anthropic 400 for "out of extra usage"). ``billing_unverified`` (#82154)
- persists an ambiguous billing verdict so the entry gets a short cooldown, not
- the one-hour bench.
+ (e.g. Anthropic 400 for "out of extra usage"). ``billing_unverified`` persists an
+ ambiguous billing verdict so the entry gets a short cooldown, not the one-hour bench.
"""
pool = agent._credential_pool
if pool is None:
return False, has_retried_429
- # The pool belongs to the PRIMARY provider: acting on fallback errors would
- # corrupt its state (#33088) and reset base_url to the primary endpoint (#33163).
+ # The pool belongs to the PRIMARY provider: acting on fallback errors would corrupt
+ # its state and reset base_url to the primary endpoint. Empty pool provider means
+ # unscoped; empty agent provider is a mismatch (swap would leave provider="" model="").
current_provider = (getattr(agent, "provider", "") or "").strip().lower()
pool_provider = (getattr(pool, "provider", "") or "").strip().lower()
- # Skip recovery when the pool is scoped to another provider. Empty pool provider
- # means unscoped; empty agent provider is a mismatch (swap would leave provider="" model="").
- if pool_provider:
- # Same fail-closed boundary predicate as runtime binding (named-custom
- # aliases, endpoint validation, fallback isolation).
- if not credential_pool_matches_provider(
- pool,
- current_provider,
- base_url=getattr(agent, "base_url", None),
- ):
- _ra().logger.warning(
- "Credential pool provider mismatch: pool=%s, agent=%s — "
- "skipping pool mutation to avoid cross-provider contamination",
- pool_provider, current_provider,
- )
- return False, has_retried_429
+ if pool_provider and not credential_pool_matches_provider(
+ pool, current_provider, base_url=getattr(agent, "base_url", None)
+ ):
+ # Same fail-closed boundary predicate as runtime binding.
+ _ra().logger.warning(
+ "Credential pool provider mismatch: pool=%s, agent=%s — "
+ "skipping pool mutation to avoid cross-provider contamination",
+ pool_provider, current_provider,
+ )
+ return False, has_retried_429
- # Attribute the failure to 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 (#43747).
- _api_key_hint = getattr(agent, "api_key", None) or None
- _raw_credential_id = getattr(agent, "_credential_pool_entry_id", None)
- _credential_id = (
- _raw_credential_id
- if isinstance(_raw_credential_id, str) and _raw_credential_id
- else None
- )
- if not _api_key_hint:
- _cur = pool.current()
- if _cur:
- _api_key_hint = getattr(_cur, "runtime_api_key", None)
- if not _credential_id:
- _current_id = getattr(_cur, "id", None)
- if isinstance(_current_id, str) and _current_id:
- _credential_id = _current_id
-
- def _rotate_failed_credential(rotate_status: int):
- kwargs = {
- "status_code": rotate_status,
- "error_context": error_context,
- "api_key_hint": _api_key_hint,
- }
- if _credential_id:
- kwargs["credential_id"] = _credential_id
- # Pass classified semantics, not just the status: a billing 403 and an
- # edge-throttle 403 need opposite cooldowns. ``effective_reason`` is resolved below.
- if effective_reason is not None:
- _failure_reason = effective_reason.value
- if effective_reason == FailoverReason.billing and billing_unverified:
- # Ambiguous billing body (#82154): size the cooldown as transient, not a 1-hour bench.
- from agent.credential_pool import FAILURE_REASON_BILLING_UNVERIFIED
- _failure_reason = FAILURE_REASON_BILLING_UNVERIFIED
- kwargs["failure_reason"] = _failure_reason
- return pool.mark_exhausted_and_rotate(**kwargs)
+ api_key_hint, credential_id = _failed_credential_identity(agent, pool)
+ effective_reason = classified_reason
+ if effective_reason is None:
+ effective_reason = _STATUS_TO_FAILOVER_REASON.get(status_code)
def _rotate_and_swap(default_status: int, label: str) -> bool:
"""Rotate away from the failed credential; True when a new entry was swapped in."""
rotate_status = status_code if status_code is not None else default_status
- next_entry = _rotate_failed_credential(rotate_status)
+ kwargs = {
+ "status_code": rotate_status,
+ "error_context": error_context,
+ "api_key_hint": api_key_hint,
+ }
+ if credential_id:
+ kwargs["credential_id"] = credential_id
+ # Pass classified semantics, not just the status: a billing 403 and an
+ # edge-throttle 403 need opposite cooldowns.
+ if effective_reason is not None:
+ failure_reason = effective_reason.value
+ if effective_reason == FailoverReason.billing and billing_unverified:
+ # Ambiguous billing body: size the cooldown as transient, not a 1-hour bench.
+ from agent.credential_pool import FAILURE_REASON_BILLING_UNVERIFIED
+ failure_reason = FAILURE_REASON_BILLING_UNVERIFIED
+ kwargs["failure_reason"] = failure_reason
+ next_entry = pool.mark_exhausted_and_rotate(**kwargs)
if next_entry is None:
return False
_ra().logger.info(
"Credential %s (%s) — rotated to pool entry %s",
- rotate_status,
- label,
- getattr(next_entry, "id", "?"),
+ rotate_status, label, getattr(next_entry, "id", "?"),
)
agent._swap_credential(next_entry)
return True
- effective_reason = classified_reason
- if effective_reason is None:
- if status_code == 402:
- effective_reason = FailoverReason.billing
- elif status_code == 429:
- effective_reason = FailoverReason.rate_limit
- elif status_code in {401, 403}:
- effective_reason = FailoverReason.auth
-
if effective_reason == FailoverReason.upstream_rate_limit:
# Upstream (e.g. DeepSeek behind OpenRouter) is throttling the aggregator; the
# credential is healthy. Do not rotate/exhaust; let fallback switch models.
@@ -902,129 +1003,21 @@ def recover_with_credential_pool(
"credential rotation, deferring to fallback chain"
)
return False, has_retried_429
-
if effective_reason == FailoverReason.billing:
# A separate pool instance may have resolved runtime credentials, leaving
# no ``current_id``; match the key that failed, not a different account.
- if _rotate_and_swap(402, "billing"):
- return True, False
- return False, has_retried_429
-
+ return (True, False) if _rotate_and_swap(402, "billing") else (False, has_retried_429)
if effective_reason == FailoverReason.rate_limit:
- # Already-exhausted credential: rotate immediately. Avoids the "cancel-between-429s"
- # trap where the local has_retried_429 resets per prompt and retries forever.
- current_entry = None
- if _credential_id:
- current_entry = next(
- (e for e in pool.entries() if e.id == _credential_id),
- None,
- )
- if _api_key_hint:
- current_entry = current_entry or next(
- (e for e in pool.entries() if e.runtime_api_key == _api_key_hint),
- None,
- )
- if current_entry is None:
- current_entry = pool.current()
- current_last_status = getattr(current_entry, "last_status", None) if current_entry else None
- if current_last_status == STATUS_EXHAUSTED:
- _ra().logger.info(
- "Credential already exhausted (last_status=%s) — rotating immediately instead of retrying",
- current_last_status,
- )
- if _rotate_and_swap(429, "rate limit, pre-exhausted"):
- return True, False
- return False, True
-
- usage_limit_reached = False
- if error_context:
- context_reason = str(error_context.get("reason") or "").lower()
- context_message = str(error_context.get("message") or "").lower()
- usage_limit_reached = (
- "usage_limit_reached" in context_reason
- or "gousagelimit" in context_reason
- or "usage limit reached" in context_message
- or "usage limit has been reached" in context_message
- )
- if not has_retried_429 and not usage_limit_reached:
- return False, True
- if _rotate_and_swap(429, "rate limit"):
- return True, False
- return False, True
-
- if effective_reason == FailoverReason.auth:
- # Entitlement 403s look like auth failures but refresh cannot fix them; any
- # xai-oauth 403 is treated as entitlement (#26847) EXCEPT xAI's stale-token
- # signals (``[WKE=unauthenticated:...]``, "could not be validated"), which must
- # stay refreshable (#29344).
- is_entitlement = agent._is_entitlement_failure(error_context, status_code)
- _auth_haystack = " ".join(
- str(error_context.get(k) or "").lower()
- for k in ("message", "reason", "code", "error")
- if isinstance(error_context, dict)
+ return _recover_rate_limit(
+ pool, has_retried_429=has_retried_429, error_context=error_context,
+ api_key_hint=api_key_hint, credential_id=credential_id, rotate_and_swap=_rotate_and_swap,
+ )
+ if effective_reason == FailoverReason.auth:
+ return _recover_auth_failure(
+ agent, pool, status_code=status_code, has_retried_429=has_retried_429,
+ error_context=error_context, api_key_hint=api_key_hint, credential_id=credential_id,
+ rotate_and_swap=_rotate_and_swap,
)
- if (
- not is_entitlement
- and status_code == 403
- and "oauth authentication is currently not allowed for this organization" in _auth_haystack
- ):
- is_entitlement = True
- if (
- not is_entitlement
- and status_code == 403
- and (agent.provider or "") == "anthropic"
- and getattr(agent, "api_mode", "") == "anthropic_messages"
- ):
- is_entitlement = True
- if not is_entitlement and status_code == 403 and (agent.provider or "") == "xai-oauth":
- _is_xai_auth_failure = (
- "[wke=unauthenticated:" in _auth_haystack
- or "oauth2 access token could not be validated" in _auth_haystack
- )
- if not _is_xai_auth_failure:
- is_entitlement = True
- if is_entitlement:
- _ra().logger.info(
- "Credential %s — entitlement-shaped 403 from %s; "
- "skipping pool refresh (account lacks subscription, "
- "not a transient auth failure).",
- status_code if status_code is not None else "auth",
- agent.provider or "provider",
- )
- return False, has_retried_429
- # Refresh the entry that supplied the failing key, not current(): refreshing a
- # healthy entry burns its single-use refresh token for a failure it never had.
- refresh_kwargs = {"api_key_hint": _api_key_hint}
- if _credential_id:
- refresh_kwargs["credential_id"] = _credential_id
- refreshed = pool.try_refresh_matching(**refresh_kwargs)
- if refreshed is not None:
- # try_refresh_matching() reports success even when upstream keeps rejecting;
- # cap same-entry refreshes so a single-entry pool falls through to fallback (#26080).
- refreshed_id = getattr(refreshed, "id", None)
- if refreshed_id is not None:
- refresh_counts = getattr(agent, "_auth_pool_refresh_counts", None)
- if refresh_counts is None:
- refresh_counts = {}
- agent._auth_pool_refresh_counts = refresh_counts
- refresh_key = (agent.provider, refreshed_id)
- refresh_counts[refresh_key] = refresh_counts.get(refresh_key, 0) + 1
- if refresh_counts[refresh_key] > _MAX_AUTH_REFRESH_ATTEMPTS:
- _ra().logger.warning(
- "Credential auth failure persists after %s refreshes for "
- "pool entry %s — treating as unrecoverable and allowing "
- "fallback to activate.",
- refresh_counts[refresh_key] - 1,
- refreshed_id,
- )
- return False, has_retried_429
- _ra().logger.info("Credential auth failure — refreshed pool entry %s", getattr(refreshed, 'id', '?'))
- agent._swap_credential(refreshed)
- return True, has_retried_429
- # Refresh failed; rotate (the failed entry is already marked exhausted).
- if _rotate_and_swap(401, "auth refresh failed"):
- return True, False
-
return False, has_retried_429
From 666b525e4d4ec5fa656d2edfc2b4e4a8b43e3173 Mon Sep 17 00:00:00 2001
From: Teknium <127238744+teknium1@users.noreply.github.com>
Date: Wed, 2 Sep 2026 18:38:17 -0700
Subject: [PATCH 05/20] refactor(agent/agent_runtime_helpers): split
restore_primary_runtime into gate/capabilities/pool-rebind helpers
---
agent/agent_runtime_helpers.py | 311 +++++++++++++++------------------
1 file changed, 143 insertions(+), 168 deletions(-)
diff --git a/agent/agent_runtime_helpers.py b/agent/agent_runtime_helpers.py
index d26f334d7c..a3d31f8253 100644
--- a/agent/agent_runtime_helpers.py
+++ b/agent/agent_runtime_helpers.py
@@ -1053,6 +1053,21 @@ 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:
+ """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
+ # reference_callback relay survives recovery.
+ from agent.moa_loop import build_moa_facade
+
+ agent.client = build_moa_facade(agent, agent.model)
+ agent._anthropic_client = None
+ elif agent.api_mode == "anthropic_messages":
+ _build_anthropic_client_from_runtime(agent, rt)
+ else:
+ agent.client = agent._create_openai_client(dict(rt["client_kwargs"]), reason=reason, shared=True)
+
+
def try_recover_primary_transport(
agent, api_error: Exception, *, retry_count: int, max_retries: int,
) -> bool:
@@ -1062,12 +1077,9 @@ def try_recover_primary_transport(
"""
if agent._fallback_activated:
return False
-
error_type = type(api_error).__name__
if error_type not in _TRANSIENT_TRANSPORT_ERRORS:
return False
-
- # Skip for aggregator providers — they manage their own retry infra
if agent._is_openrouter_url():
return False
provider_lower = (agent.provider or "").strip().lower()
@@ -1078,36 +1090,26 @@ def try_recover_primary_transport(
and getattr(agent, "api_mode", None) != "anthropic_messages"
):
return False
-
try:
- # Never hard-close the shared client here (#70773): stale streaming workers may
- # still be unwinding on the old pool; _retire_shared_openai_client defers FD release to GC.
+ # Never hard-close the shared client here: stale streaming workers may still be
+ # unwinding on the old pool; _retire_shared_openai_client defers FD release to GC.
if getattr(agent, "client", None) is not None:
try:
- agent._retire_shared_openai_client(
- agent.client, reason="primary_recovery",
- )
+ agent._retire_shared_openai_client(agent.client, reason="primary_recovery")
except Exception:
pass
-
rt = agent._primary_runtime
_apply_primary_runtime_fields(agent, rt)
-
if agent.api_mode == "anthropic_messages":
_build_anthropic_client_from_runtime(agent, rt)
elif (agent.provider or "").strip().lower() == "moa":
- # MoA has empty client_kwargs; rebuild via the shared facade factory so the
- # reference_callback relay survives recovery (#53802).
from agent.moa_loop import build_moa_facade
agent.client = build_moa_facade(agent, agent.model)
else:
agent.client = agent._create_openai_client(
- dict(rt["client_kwargs"]),
- reason="primary_recovery",
- shared=True,
+ dict(rt["client_kwargs"]), reason="primary_recovery", shared=True
)
-
wait_time = min(3 + retry_count, 8)
agent._vprint(
f"{agent.log_prefix}🔁 Transient {error_type} on {agent.provider} — "
@@ -1204,48 +1206,19 @@ def drop_thinking_only_and_merge_users(
-def restore_primary_runtime(agent) -> bool:
- """Restore the primary runtime at the start of a new turn so fallback stays turn-scoped.
+def _primary_reset_gate_blocks(agent, rt, primary_provider, primary_runtime_base_url, matches_primary, load_primary_pool):
+ """Reset-aware gate: skip a guaranteed-to-fail restore while the primary pool reports a future reset.
- Needed for long-lived CLI agents and the gateway's cached agents (``_agent_cache``).
+ Fails open on any error/None. Returns ``(blocked, prefetched_pool, prefetched)`` so the
+ rebind step can reuse the loaded pool (one auth.json read at most).
"""
- if not agent._fallback_activated:
- # Reset the index even without activation: a failed _try_activate_fallback() can strand
- # _fallback_index past the chain end and silently block future fallbacks (#20465).
- agent._fallback_index = 0
- return False
-
- if getattr(agent, "_rate_limited_until", 0) > time.monotonic():
- return False # primary still in rate-limit cooldown, stay on fallback
-
- # Reset-aware gate: when the credential pool reports a reset time still in the future
- # (subscription windows), skip the guaranteed-to-fail restore (saves two cache invalidations
- # per turn). Fails open on any error/None. The loaded primary pool is handed to the
- # rebind block below via ``prefetched_primary_pool`` so it loads at most once.
- rt = agent._primary_runtime
- primary_provider = str((rt or {}).get("provider") or "").strip().lower()
- primary_runtime_base_url = str((rt or {}).get("base_url") or "")
-
- def _matches_primary(candidate) -> bool:
- return credential_pool_matches_provider(
- candidate, primary_provider, base_url=primary_runtime_base_url
- )
-
- def _load_primary_pool():
- """Load the primary provider's pool; None when absent or provider-mismatched."""
- from agent.credential_pool import load_pool
-
- key = resolve_runtime_pool_key(primary_provider, primary_runtime_base_url)
- loaded = load_pool(key) if key else None
- return loaded if loaded is not None and _matches_primary(loaded) else None
-
- prefetched_primary_pool = None
- primary_pool_prefetched = False
+ prefetched_pool = None
+ prefetched = False
try:
pool = getattr(agent, "_credential_pool", None)
- if not _matches_primary(pool):
- prefetched_primary_pool = pool = _load_primary_pool()
- primary_pool_prefetched = True
+ if not matches_primary(pool):
+ prefetched_pool = pool = load_primary_pool()
+ prefetched = True
next_at = getattr(pool, "next_available_at", lambda: None)()
if next_at is not None and next_at > time.time():
if not getattr(agent, "_restore_wait_logged", False):
@@ -1258,72 +1231,131 @@ def restore_primary_runtime(agent) -> bool:
agent.provider,
agent.model,
)
- return False
+ return True, prefetched_pool, prefetched
except Exception:
- logger.debug(
- "Reset-aware restore gate failed; falling back to per-turn retry",
- exc_info=True,
+ logger.debug("Reset-aware restore gate failed; falling back to per-turn retry", exc_info=True)
+ return False, prefetched_pool, prefetched
+
+
+def _restore_runtime_capabilities(agent, rt: Dict[str, Any]) -> None:
+ if "runtime_capabilities" in rt:
+ raw = rt["runtime_capabilities"]
+ if not isinstance(raw, dict):
+ logger.warning("Ignoring malformed runtime capabilities snapshot")
+ else:
+ agent.runtime_capabilities = dict(raw)
+ elif "capabilities" in rt:
+ # Snapshots written by the initial capability propagation patch.
+ raw = rt["capabilities"]
+ if isinstance(raw, dict):
+ agent.runtime_capabilities = dict(raw)
+
+
+def _rebind_primary_credential_pool(agent, primary_provider, matches_primary, load_primary_pool, prefetched_pool, prefetched) -> None:
+ """Rebind and re-select the primary credential pool after a fallback turn.
+
+ A cross-provider fallback attaches the fallback's pool; leaving it would trip the
+ provider-mismatch guard on the next 401/429. Reload the primary pool, else clear it.
+ The snapshot api_key may be stale after pool rotation; re-select the pool's current
+ best entry, keeping the snapshot key when no usable entry exists.
+ """
+ pool = getattr(agent, "_credential_pool", None)
+ pool_provider = str(getattr(pool, "provider", "") or "").strip().lower()
+ if pool is not None and pool_provider and not matches_primary(pool):
+ agent._credential_pool = None
+ agent._credential_pool_entry_id = None
+ try:
+ # Reuse the pool the reset-aware gate already loaded (avoids a second auth.json read).
+ agent._credential_pool = prefetched_pool if prefetched else load_primary_pool()
+ except Exception as exc:
+ logger.warning(
+ "Restore could not reload primary credential pool for %s: %s", primary_provider, exc
+ )
+ agent._credential_pool_entry_id = None
+ pool = getattr(agent, "_credential_pool", None)
+ if pool is None or not pool.has_available():
+ return
+ entry = pool.select()
+ if entry is None:
+ return
+ entry_key = getattr(entry, "runtime_api_key", None) or getattr(entry, "access_token", "")
+ if not entry_key:
+ return
+ if matches_primary(entry):
+ # _swap_credential rebuilds the client and reapplies base-url-scoped headers.
+ agent._swap_credential(entry)
+ logger.info(
+ "Restore re-selected pool entry %s (%s)",
+ getattr(entry, "id", "?"), getattr(entry, "label", "?"),
)
+ else:
+ logger.info(
+ "Restore skipped pool entry %s (%s): provider %s does not match primary provider %s",
+ getattr(entry, "id", "?"),
+ getattr(entry, "label", "?"),
+ str(getattr(entry, "provider", "") or "").strip().lower() or "?",
+ primary_provider or "?",
+ )
+
+
+def restore_primary_runtime(agent) -> bool:
+ """Restore the primary runtime at the start of a new turn so fallback stays turn-scoped.
+
+ Needed for long-lived CLI agents and the gateway's cached agents (``_agent_cache``).
+ """
+ if not agent._fallback_activated:
+ # Reset the index even without activation: a failed _try_activate_fallback() can strand
+ # _fallback_index past the chain end and silently block future fallbacks.
+ agent._fallback_index = 0
+ return False
+ if getattr(agent, "_rate_limited_until", 0) > time.monotonic():
+ return False # primary still in rate-limit cooldown, stay on fallback
+
+ rt = agent._primary_runtime
+ primary_provider = str((rt or {}).get("provider") or "").strip().lower()
+ primary_runtime_base_url = str((rt or {}).get("base_url") or "")
+
+ def _matches_primary(candidate) -> bool:
+ return credential_pool_matches_provider(candidate, primary_provider, base_url=primary_runtime_base_url)
+
+ def _load_primary_pool():
+ """Load the primary provider's pool; None when absent or provider-mismatched."""
+ from agent.credential_pool import load_pool
+
+ key = resolve_runtime_pool_key(primary_provider, primary_runtime_base_url)
+ loaded = load_pool(key) if key else None
+ return loaded if loaded is not None and _matches_primary(loaded) else None
+
+ blocked, prefetched_pool, prefetched = _primary_reset_gate_blocks(
+ agent, rt, primary_provider, primary_runtime_base_url, _matches_primary, _load_primary_pool
+ )
+ if blocked:
+ return False
agent._restore_wait_logged = False
fallback_route = getattr(agent, "_provider_fallback_route", None)
- if (
- isinstance(fallback_route, (list, tuple))
- and len(fallback_route) == 2
- ):
+ if isinstance(fallback_route, (list, tuple)) and len(fallback_route) == 2:
previous_model = str(fallback_route[0] or "unknown")
previous_provider = str(fallback_route[1] or "unknown")
else:
previous_model = str(getattr(agent, "model", "") or "unknown")
previous_provider = str(getattr(agent, "provider", "") or "unknown")
- provider_fallback_active = bool(
- getattr(agent, "_provider_fallback_active", False)
- )
+ provider_fallback_active = bool(getattr(agent, "_provider_fallback_active", False))
try:
- # ── Core runtime state ──
_apply_primary_runtime_fields(agent, rt)
- if "runtime_capabilities" in rt:
- raw_capabilities = rt["runtime_capabilities"]
- if not isinstance(raw_capabilities, dict):
- logger.warning("Ignoring malformed runtime capabilities snapshot")
- else:
- agent.runtime_capabilities = dict(raw_capabilities)
- elif "capabilities" in rt:
- # Read snapshots written by the initial capability propagation patch.
- raw_capabilities = rt["capabilities"]
- if isinstance(raw_capabilities, dict):
- agent.runtime_capabilities = dict(raw_capabilities)
+ _restore_runtime_capabilities(agent, rt)
agent._use_prompt_caching = rt["use_prompt_caching"]
# Default to native layout for snapshots predating the native-vs-proxy split.
agent._use_native_cache_layout = rt.get(
"use_native_cache_layout",
agent.api_mode == "anthropic_messages" and agent.provider == "anthropic",
)
- # An operator cache disable (_cache_disabled) must survive snapshot restoration (#33555).
+ # An operator cache disable (_cache_disabled) must survive snapshot restoration.
if getattr(agent, "_cache_disabled", False):
agent._use_prompt_caching = False
agent._use_native_cache_layout = False
-
- # ── Rebuild client for the primary provider ──
- if agent.provider == "moa":
- # MoA has no real OpenAI client kwargs; rebuild via the shared facade factory so the
- # reference_callback relay stays wired (#53802).
- from agent.moa_loop import build_moa_facade
-
- agent.client = build_moa_facade(agent, agent.model)
- agent._anthropic_client = None
- elif agent.api_mode == "anthropic_messages":
- _build_anthropic_client_from_runtime(agent, rt)
- else:
- agent.client = agent._create_openai_client(
- dict(rt["client_kwargs"]),
- reason="restore_primary",
- shared=True,
- )
-
- # ── Restore context engine state ──
- cc = agent.context_compressor
- cc.update_model(
+ _rebuild_primary_client(agent, rt, reason="restore_primary")
+ agent.context_compressor.update_model(
model=rt["compressor_model"],
context_length=rt["compressor_context_length"],
base_url=rt["compressor_base_url"],
@@ -1331,79 +1363,23 @@ def restore_primary_runtime(agent) -> bool:
provider=rt["compressor_provider"],
api_mode=rt.get("compressor_api_mode", ""),
)
-
- # ── Rebind and re-select the primary credential pool ──
- # A cross-provider fallback attaches the fallback's pool; leaving it would trip the
- # provider-mismatch guard on the next 401/429. Reload the primary pool, else clear it.
- pool = getattr(agent, "_credential_pool", None)
- pool_provider = str(getattr(pool, "provider", "") or "").strip().lower()
- if pool is not None and pool_provider and not _matches_primary(pool):
- agent._credential_pool = None
- agent._credential_pool_entry_id = None
- try:
- # Reuse the pool the reset-aware gate already loaded (avoids a second auth.json read).
- agent._credential_pool = (
- prefetched_primary_pool if primary_pool_prefetched else _load_primary_pool()
- )
- except Exception as exc:
- logger.warning(
- "Restore could not reload primary credential pool for %s: %s",
- primary_provider,
- exc,
- )
-
- # The snapshot api_key may be stale after pool rotation; re-select the pool's current
- # best entry, keeping the snapshot key when no usable entry exists (#25205).
- agent._credential_pool_entry_id = None
- pool = getattr(agent, "_credential_pool", None)
- if pool is not None and pool.has_available():
- entry = pool.select()
- if entry is not None:
- entry_provider = str(getattr(entry, "provider", "") or "").strip().lower()
- entry_key = (
- getattr(entry, "runtime_api_key", None)
- or getattr(entry, "access_token", "")
- )
- if entry_key and _matches_primary(entry):
- # _swap_credential rebuilds the client and reapplies base-url-scoped headers (#33163).
- agent._swap_credential(entry)
- logger.info(
- "Restore re-selected pool entry %s (%s)",
- getattr(entry, "id", "?"),
- getattr(entry, "label", "?"),
- )
- elif entry_key:
- logger.info(
- "Restore skipped pool entry %s (%s): provider %s does not match primary provider %s",
- getattr(entry, "id", "?"),
- getattr(entry, "label", "?"),
- entry_provider or "?",
- primary_provider or "?",
- )
-
- # ── Restore reasoning_config if saved (older snapshots keep the current value) ──
+ _rebind_primary_credential_pool(
+ agent, primary_provider, _matches_primary, _load_primary_pool, prefetched_pool, prefetched
+ )
+ # Older snapshots have no reasoning_config; keep the current value.
saved_reasoning = rt.get("reasoning_config")
if saved_reasoning is not None:
agent.reasoning_config = dict(saved_reasoning)
-
- # ── Reset fallback chain for the new turn ──
agent._fallback_activated = False
agent._fallback_index = 0
- agent._rate_limit_backoff_count = 0 # reset exponential backoff counter
-
- # Reset the stale-call circuit breaker (#58962): its streak measured the fallback provider.
- from agent.chat_completion_helpers import _reset_stale_streak
+ agent._rate_limit_backoff_count = 0
+ # Reset the stale-call circuit breaker: its streak measured the fallback provider.
+ from agent.chat_completion_helpers import _reset_stale_streak, rewrite_prompt_model_identity
_reset_stale_streak(agent)
-
- # Undo the fallback's identity rewrite so the prompt is
- # byte-identical to the stored copy again (prefix cache match).
- from agent.chat_completion_helpers import rewrite_prompt_model_identity
+ # Undo the fallback's identity rewrite so the prompt is byte-identical to the
+ # stored copy again (prefix cache match).
rewrite_prompt_model_identity(agent, rt["model"], rt["provider"])
-
- logger.info(
- "Primary runtime restored for new turn: %s (%s)",
- agent.model, agent.provider,
- )
+ logger.info("Primary runtime restored for new turn: %s (%s)", agent.model, agent.provider)
agent._provider_fallback_active = False
agent._provider_fallback_route = None
if provider_fallback_active:
@@ -1413,8 +1389,7 @@ def restore_primary_runtime(agent) -> bool:
f"fallback {previous_model} via {previous_provider} is no longer active."
)
except Exception:
- # Notification surfaces are best-effort and must never undo a
- # successful runtime restoration.
+ # Notification surfaces are best-effort and must never undo a successful restore.
pass
return True
except Exception as e:
From 6bcad19d1d4df1922c2478dbb437005a2d7521e3 Mon Sep 17 00:00:00 2001
From: Teknium <127238744+teknium1@users.noreply.github.com>
Date: Wed, 2 Sep 2026 18:43:18 -0700
Subject: [PATCH 06/20] refactor(agent/agent_runtime_helpers): split
anthropic_prompt_cache_policy (MoA + custom-route gate helpers)
---
agent/agent_runtime_helpers.py | 258 ++++++++++++++-------------------
1 file changed, 112 insertions(+), 146 deletions(-)
diff --git a/agent/agent_runtime_helpers.py b/agent/agent_runtime_helpers.py
index a3d31f8253..eb04989d34 100644
--- a/agent/agent_runtime_helpers.py
+++ b/agent/agent_runtime_helpers.py
@@ -1721,6 +1721,81 @@ def _has_litellm_token(value: str, delimiters: str) -> bool:
return "litellm" in value.split()
+def _moa_aggregator_cache_policy(agent, eff_model: str) -> tuple[bool, bool]:
+ """MoA virtual provider: resolve the policy from the preset's real aggregator slot.
+
+ The virtual provider matches no caching branch and would silently lose caching
+ for the acting aggregator.
+ """
+ try:
+ from hermes_cli.config import load_config as _load_moa_cfg
+ from hermes_cli.moa_config import resolve_moa_preset
+ from hermes_cli.runtime_provider import resolve_runtime_provider
+
+ preset = resolve_moa_preset(_load_moa_cfg().get("moa") or {}, eff_model or None)
+ agg = preset.get("aggregator") or {}
+ agg_provider = str(agg.get("provider") or "").strip()
+ agg_model = str(agg.get("model") or "").strip()
+ if agg_provider and agg_model:
+ agg_base_url = ""
+ agg_api_mode = ""
+ try:
+ rt = resolve_runtime_provider(requested=agg_provider, target_model=agg_model)
+ agg_base_url = rt.get("base_url") or ""
+ agg_api_mode = rt.get("api_mode") or ""
+ except Exception:
+ pass
+ return anthropic_prompt_cache_policy(
+ agent, provider=agg_provider, base_url=agg_base_url, api_mode=agg_api_mode, model=agg_model
+ )
+ except Exception as _moa_exc: # pragma: no cover - defensive
+ logger.debug("MoA aggregator cache-policy resolution failed: %s", _moa_exc)
+ return False, False
+
+
+def _route_may_be_custom(agent, eff_provider: str, provider_lower: str, eff_base_url: str) -> bool:
+ """Cheap identity gate deciding whether a custom-provider capability lookup is worth running."""
+ custom_providers = getattr(agent, "_custom_providers", None)
+ if custom_providers:
+ # Same semantics as the capability helper (normalize_route_base_url +
+ # custom_provider_aliases) so spelling differences don't drop declarations.
+ from hermes_cli.providers import custom_provider_aliases
+ from hermes_cli.route_identity import normalize_route_base_url
+
+ provider_ids = {provider_lower}
+ if provider_lower.startswith("custom:"):
+ provider_ids.add(provider_lower.removeprefix("custom:"))
+ eff_url_normalized = normalize_route_base_url(eff_base_url)
+ for entry in custom_providers:
+ if not isinstance(entry, dict):
+ continue
+ entry_ids = custom_provider_aliases(
+ str(entry.get("name") or ""), str(entry.get("provider_key") or "")
+ )
+ if provider_ids & entry_ids or (
+ eff_url_normalized and normalize_route_base_url(entry.get("base_url")) == eff_url_normalized
+ ):
+ return True
+ return False
+ if custom_providers is not None:
+ return False # attached empty list never matches
+ # None = list not attached yet (early init or blank stub). Avoid rebuilding the
+ # list for ordinary built-in routes.
+ try:
+ from hermes_cli.providers import get_provider
+
+ # allow_network=False: never trigger a registry fetch from the send path;
+ # a catalog miss degrades to the conservative capability lookup.
+ provider_def = get_provider(eff_provider, allow_network=False)
+ return provider_def is None or (
+ bool(provider_def.base_url)
+ and base_url_hostname(provider_def.base_url) != base_url_hostname(eff_base_url)
+ )
+ except Exception as _pd_exc:
+ logger.debug("provider lookup failed during cache-policy pre-gate: %s", _pd_exc)
+ return provider_lower.startswith("custom:")
+
+
def anthropic_prompt_cache_policy(
agent,
*,
@@ -1733,9 +1808,9 @@ def anthropic_prompt_cache_policy(
``use_native_layout`` puts markers on inner content blocks (native Anthropic wire);
otherwise on the message envelope (OpenRouter / OpenAI-wire proxies). Qwen/Alibaba routes
- also honour envelope markers (pi-mono #3392). An operator disable is read from
- ``_cache_disabled`` (not ``_cache_ttl``, unset during init) so it survives switches and
- restores (#33555).
+ also honour envelope markers. An operator disable is read from ``_cache_disabled``
+ (not ``_cache_ttl``, unset during init) so it survives switches and restores.
+ Branch ORDER is load-bearing (see inline notes).
"""
if getattr(agent, "_cache_disabled", False):
return (False, False)
@@ -1744,42 +1819,8 @@ def anthropic_prompt_cache_policy(
eff_base_url = base_url if base_url is not None else (agent.base_url or "")
eff_api_mode = api_mode if api_mode is not None else (agent.api_mode or "")
eff_model = (model if model is not None else agent.model) or ""
-
- # MoA virtual provider matches no caching branch, silently losing caching for the acting
- # aggregator; resolve the policy from the preset's real aggregator slot instead.
if eff_provider.strip().lower() == "moa":
- try:
- from hermes_cli.config import load_config as _load_moa_cfg
- from hermes_cli.moa_config import resolve_moa_preset
- from hermes_cli.runtime_provider import resolve_runtime_provider
-
- _preset = resolve_moa_preset(
- _load_moa_cfg().get("moa") or {}, eff_model or None
- )
- _agg = _preset.get("aggregator") or {}
- _agg_provider = str(_agg.get("provider") or "").strip()
- _agg_model = str(_agg.get("model") or "").strip()
- if _agg_provider and _agg_model:
- _agg_base_url = ""
- _agg_api_mode = ""
- try:
- _rt = resolve_runtime_provider(
- requested=_agg_provider, target_model=_agg_model
- )
- _agg_base_url = _rt.get("base_url") or ""
- _agg_api_mode = _rt.get("api_mode") or ""
- except Exception:
- pass
- return anthropic_prompt_cache_policy(
- agent,
- provider=_agg_provider,
- base_url=_agg_base_url,
- api_mode=_agg_api_mode,
- model=_agg_model,
- )
- except Exception as _moa_exc: # pragma: no cover - defensive
- logger.debug("MoA aggregator cache-policy resolution failed: %s", _moa_exc)
- return False, False
+ return _moa_aggregator_cache_policy(agent, eff_model)
if isinstance(eff_model, dict):
eff_model = eff_model.get('model') or eff_model.get('default') or ''
@@ -1788,85 +1829,27 @@ def anthropic_prompt_cache_policy(
provider_lower = eff_provider.lower()
is_claude = "claude" in model_lower
# Kimi/Moonshot via OpenRouter uses the same envelope cache_control as Claude; without
- # this branch it serves ~1% cache hits (#25970). Family matcher covers bare k1./k2. slugs.
+ # this it serves ~1% cache hits. Family matcher covers bare k1./k2. slugs.
from agent.anthropic_adapter import _model_name_is_kimi_family
- is_kimi = (
- _model_name_is_kimi_family(eff_model) or "moonshot" in model_lower
- )
+ is_kimi = _model_name_is_kimi_family(eff_model) or "moonshot" in model_lower
is_openrouter = base_url_host_matches(eff_base_url, "openrouter.ai")
# Nous Portal proxies to OpenRouter; treat as OpenRouter-equivalent for cache layout.
is_nous_portal = base_url_host_matches(eff_base_url, "nousresearch.com")
is_anthropic_wire = eff_api_mode == "anthropic_messages"
- is_native_anthropic = (
- is_anthropic_wire
- and (eff_provider == "anthropic" or base_url_hostname(eff_base_url) == "api.anthropic.com")
+ is_native_anthropic = is_anthropic_wire and (
+ eff_provider == "anthropic" or base_url_hostname(eff_base_url) == "api.anthropic.com"
)
# Honor a configured route's per-model ``prompt_caching`` capability (explicit false too);
# only for the two transports this planner handles, not Responses/Bedrock.
- custom_prompt_caching = None
- _supports_anthropic_cache_markers = eff_api_mode in {
- "anthropic_messages",
- "chat_completions",
- }
- _litellm_openai_wire = (
- eff_api_mode == "chat_completions"
- and is_claude
- and _is_litellm_route(provider_lower, eff_base_url)
+ supports_cache_markers = eff_api_mode in {"anthropic_messages", "chat_completions"}
+ litellm_openai_wire = (
+ eff_api_mode == "chat_completions" and is_claude and _is_litellm_route(provider_lower, eff_base_url)
)
- _custom_providers = getattr(agent, "_custom_providers", None)
- _route_may_be_custom = False
- if not _supports_anthropic_cache_markers:
- # Responses/Bedrock never consume the declaration — skip the
- # identity probe entirely for those transports.
- pass
- elif _custom_providers:
- # Cheap identity gate before the capability helper, matching its semantics
- # (normalize_route_base_url + custom_provider_aliases) so spelling differences don't drop declarations.
- from hermes_cli.providers import custom_provider_aliases
- from hermes_cli.route_identity import normalize_route_base_url
-
- _provider_ids = {provider_lower}
- if provider_lower.startswith("custom:"):
- _provider_ids.add(provider_lower.removeprefix("custom:"))
- _eff_url_normalized = normalize_route_base_url(eff_base_url)
- for _entry in _custom_providers:
- if not isinstance(_entry, dict):
- continue
- _entry_ids = custom_provider_aliases(
- str(_entry.get("name") or ""),
- str(_entry.get("provider_key") or ""),
- )
- if _provider_ids & _entry_ids or (
- _eff_url_normalized
- and normalize_route_base_url(_entry.get("base_url"))
- == _eff_url_normalized
- ):
- _route_may_be_custom = True
- break
- elif _custom_providers is None:
- # None = list not attached yet (early init or blank stub); an attached empty list never
- # matches. Avoid rebuilding the list for ordinary built-in routes.
- try:
- from hermes_cli.providers import get_provider
-
- # allow_network=False: never trigger a registry fetch from the send path;
- # a catalog miss degrades to the conservative capability lookup.
- _provider_def = get_provider(eff_provider, allow_network=False)
- _route_may_be_custom = _provider_def is None or (
- bool(_provider_def.base_url)
- and base_url_hostname(_provider_def.base_url)
- != base_url_hostname(eff_base_url)
- )
- except Exception as _pd_exc:
- logger.debug(
- "provider lookup failed during cache-policy pre-gate: %s",
- _pd_exc,
- )
- _route_may_be_custom = provider_lower.startswith("custom:")
-
- if _supports_anthropic_cache_markers and (
- is_anthropic_wire or _litellm_openai_wire or _route_may_be_custom
+ if supports_cache_markers and (
+ is_anthropic_wire
+ or litellm_openai_wire
+ or _route_may_be_custom(agent, eff_provider, provider_lower, eff_base_url)
):
try:
from hermes_cli.config import get_custom_provider_model_capability
@@ -1875,73 +1858,56 @@ def anthropic_prompt_cache_policy(
model=eff_model,
base_url=eff_base_url,
capability="prompt_caching",
- custom_providers=_custom_providers,
+ custom_providers=getattr(agent, "_custom_providers", None),
)
+ if custom_prompt_caching is not None:
+ # Layout follows the transport: native Messages → inner blocks; OpenAI wire → envelope.
+ return custom_prompt_caching, custom_prompt_caching and is_anthropic_wire
except Exception as _cap_exc:
- logger.debug(
- "custom-provider prompt_caching capability lookup failed: %s",
- _cap_exc,
- )
- if custom_prompt_caching is not None:
- # Layout follows the transport: native Messages → inner blocks; OpenAI wire → envelope.
- return custom_prompt_caching, custom_prompt_caching and is_anthropic_wire
+ logger.debug("custom-provider prompt_caching capability lookup failed: %s", _cap_exc)
# MiniMax-M3 uses server-side automatic prefix caching; explicit markers are dead weight.
# Checked BEFORE the native-Anthropic return since provider="anthropic" may point at a MiniMax proxy.
- is_minimax_provider = provider_lower in {"minimax", "minimax-cn"}
- is_minimax_host = (
- base_url_host_matches(eff_base_url, "api.minimax.io")
+ is_minimax_route = (
+ provider_lower in {"minimax", "minimax-cn"}
+ or base_url_host_matches(eff_base_url, "api.minimax.io")
or base_url_host_matches(eff_base_url, "api.minimaxi.com")
)
- is_minimax_route = is_minimax_provider or is_minimax_host
if is_anthropic_wire and is_minimax_route:
from agent.model_metadata import _model_name_suggests_minimax_m3
if _model_name_suggests_minimax_m3(eff_model):
return False, False
-
if is_native_anthropic:
return True, True
# Envelope layout is OpenAI-wire only; Portal Claude on native Messages must fall through
# to the anthropic_messages branch (inner-block markers) or it serves 0% cache hits.
- if (
- (is_openrouter or is_nous_portal)
- and (is_claude or is_kimi)
- and not is_anthropic_wire
- ):
+ if (is_openrouter or is_nous_portal) and (is_claude or is_kimi) and not is_anthropic_wire:
return True, False
- # Nous Portal Qwen takes the envelope path too; the alibaba-family check below only matches
- # provider=opencode/alibaba and would leave Portal traffic uncached.
+ # Nous Portal Qwen takes the envelope path too; the alibaba-family check below only
+ # matches provider=opencode/alibaba and would leave Portal traffic uncached.
if is_nous_portal and "qwen" in model_lower:
return True, False
if is_anthropic_wire and is_claude:
- # Third-party Anthropic-compatible gateway.
- return True, True
-
- # LiteLLM fronting Claude on the OpenAI-compatible wire supports cache_control but matched
- # no grant branch above (#84506). Claude-only: strict relays reject the block format for
- # other models (#77217). Envelope layout: the native layout's top-level markers are only
- # relocated by the anthropic_messages adapter and cause HTTP 400 via LiteLLM (#69512).
- # Gated on chat_completions explicitly; codex_responses/bedrock_converse have their own handling.
- if _litellm_openai_wire:
+ return True, True # third-party Anthropic-compatible gateway
+ # LiteLLM fronting Claude on the OpenAI wire supports cache_control but matched no grant
+ # above. Claude-only: strict relays reject the block format for other models. Envelope
+ # layout: native top-level markers are only relocated by the anthropic_messages adapter
+ # and 400 via LiteLLM. Gated on chat_completions; codex_responses/bedrock_converse have
+ # their own handling.
+ if litellm_openai_wire:
return True, False
-
# MiniMax's own models (M2.x) on its Anthropic-compatible endpoint support cache_control;
# opt them in past the is_claude gate. M3 is excluded above.
if is_anthropic_wire and is_minimax_route:
return True, True
-
- # Qwen/Alibaba on OpenCode and DashScope accept envelope cache_control on the OpenAI wire.
- # DeepSeek on OpenCode is excluded: its relay 400s on block-array content (#77217).
- # Family set/predicate shared with the effective_cache_ttl clamp (#84733).
+ # Qwen/Alibaba on OpenCode and DashScope accept envelope cache_control on the OpenAI wire
+ # (pi-mono's "alibaba" cacheControlFormat). DeepSeek on OpenCode is excluded: its relay
+ # 400s on block-array content. Family set/predicate shared with the effective_cache_ttl clamp.
from agent.prompt_caching import ALIBABA_FAMILY_PROVIDERS, is_qwen_model
- model_is_qwen = is_qwen_model(model_lower)
- provider_is_alibaba_family = provider_lower in ALIBABA_FAMILY_PROVIDERS
- if provider_is_alibaba_family and model_is_qwen:
- # Envelope layout (native_anthropic=False), matching pi-mono's "alibaba" cacheControlFormat.
+ if provider_lower in ALIBABA_FAMILY_PROVIDERS and is_qwen_model(model_lower):
return True, False
-
return False, False
From 92c5101c78be0ddd0af90ce9e37d60841df39e59 Mon Sep 17 00:00:00 2001
From: Teknium <127238744+teknium1@users.noreply.github.com>
Date: Wed, 2 Sep 2026 18:51:04 -0700
Subject: [PATCH 07/20] refactor(agent/agent_runtime_helpers):
trajectory/strip_think/extract_reasoning helpers + pattern tables; drop dead
_tool_call_id_variants
---
agent/agent_runtime_helpers.py | 395 ++++++++++++++-------------------
1 file changed, 162 insertions(+), 233 deletions(-)
diff --git a/agent/agent_runtime_helpers.py b/agent/agent_runtime_helpers.py
index eb04989d34..d227503ea9 100644
--- a/agent/agent_runtime_helpers.py
+++ b/agent/agent_runtime_helpers.py
@@ -92,145 +92,106 @@ AGENT_RUNTIME_POST_HOOK_TOOL_NAMES = frozenset(
)
+_TRAJECTORY_SYSTEM_PROMPT = (
+ "You are a function calling AI model. You are provided with function signatures within XML tags. "
+ "You may call one or more functions to assist with the user query. If available tools are not relevant in assisting "
+ "with user query, just respond in natural conversational language. Don't make assumptions about what values to plug "
+ "into functions. After calling & executing the functions, you will be provided with function results within "
+ " XML tags. Here are the available tools:\n"
+ "\n{tools}\n\n"
+ "For each function call return a JSON object, with the following pydantic model json schema for each:\n"
+ "{{'title': 'FunctionCall', 'type': 'object', 'properties': {{'name': {{'title': 'Name', 'type': 'string'}}, "
+ "'arguments': {{'title': 'Arguments', 'type': 'object'}}}}, 'required': ['name', 'arguments']}}\n"
+ "Each function call should be enclosed within XML tags.\n"
+ "Example:\n\n{{'name': ,'arguments': }}\n"
+)
+
+
+def _trajectory_gpt_prefix(msg: Dict[str, Any]) -> str:
+ """Leading ```` block from native reasoning tokens, if any."""
+ if msg.get("reasoning") and msg["reasoning"].strip():
+ return f"\n{msg['reasoning']}\n\n"
+ return ""
+
+
+def _with_think_block(content: str) -> str:
+ """Every gpt turn gets a block (empty if none) for a consistent training format."""
+ return content if "" in content else "\n\n" + content
+
+
+def _trajectory_tool_call_turn(msg: Dict[str, Any]) -> str:
+ content = _trajectory_gpt_prefix(msg)
+ if msg.get("content") and msg["content"].strip():
+ # -> (model reasons via XML when native thinking is off)
+ content += convert_scratchpad_to_think(msg["content"]) + "\n"
+ for tool_call in msg["tool_calls"]:
+ if not tool_call or not isinstance(tool_call, dict):
+ continue
+ raw_args = tool_call["function"]["arguments"]
+ # Arguments were validated during conversation; degrade to {} rather than abort.
+ try:
+ arguments = json.loads(raw_args) if isinstance(raw_args, str) else raw_args
+ except json.JSONDecodeError:
+ logger.warning("Unexpected invalid JSON in trajectory conversion: %s", raw_args[:100])
+ arguments = {}
+ tool_call_json = {"name": tool_call["function"]["name"], "arguments": arguments}
+ content += f"\n{json.dumps(tool_call_json, ensure_ascii=False)}\n\n"
+ 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]:
+ """Collect the ```` blocks for the tool run starting at ``start``; returns ``(blocks, next_index)``."""
+ tool_responses = []
+ j = start
+ while j < len(messages) and messages[j]["role"] == "tool":
+ tool_msg = messages[j]
+ tool_content = tool_msg["content"]
+ try: # pretty-print tool content if it looks like JSON
+ if tool_content.strip().startswith(("{", "[")):
+ tool_content = json.loads(tool_content)
+ except (json.JSONDecodeError, AttributeError):
+ pass
+ tool_index = len(tool_responses)
+ tool_name = (
+ msg["tool_calls"][tool_index]["function"]["name"]
+ if tool_index < len(msg["tool_calls"])
+ else "unknown"
+ )
+ payload = json.dumps(
+ {"tool_call_id": tool_msg.get("tool_call_id", ""), "name": tool_name, "content": tool_content},
+ ensure_ascii=False,
+ )
+ tool_responses.append(f"\n{payload}\n")
+ j += 1
+ return tool_responses, j
+
+
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.
messages = [_trajectory_normalize_msg(m) for m in messages]
- trajectory = []
-
- system_msg = (
- "You are a function calling AI model. You are provided with function signatures within XML tags. "
- "You may call one or more functions to assist with the user query. If available tools are not relevant in assisting "
- "with user query, just respond in natural conversational language. Don't make assumptions about what values to plug "
- "into functions. After calling & executing the functions, you will be provided with function results within "
- " XML tags. Here are the available tools:\n"
- f"\n{agent._format_tools_for_system_message()}\n\n"
- "For each function call return a JSON object, with the following pydantic model json schema for each:\n"
- "{'title': 'FunctionCall', 'type': 'object', 'properties': {'name': {'title': 'Name', 'type': 'string'}, "
- "'arguments': {'title': 'Arguments', 'type': 'object'}}, 'required': ['name', 'arguments']}\n"
- "Each function call should be enclosed within XML tags.\n"
- "Example:\n\n{'name': ,'arguments': }\n"
- )
-
- trajectory.append({
- "from": "system",
- "value": system_msg
- })
-
- trajectory.append({
- "from": "human",
- "value": user_query
- })
-
- # Skip messages[0] (already added). Prefill is injected at API-call time
- # only, so no offset adjustment is needed.
+ trajectory = [
+ {"from": "system", "value": _TRAJECTORY_SYSTEM_PROMPT.format(tools=agent._format_tools_for_system_message())},
+ {"from": "human", "value": user_query},
+ ]
+ # Skip messages[0] (already added). Prefill is injected at API-call time only, so no offset adjustment is needed.
i = 1
-
while i < len(messages):
msg = messages[i]
-
if msg["role"] == "assistant":
- if "tool_calls" in msg and msg["tool_calls"]:
- content = ""
-
- # Prepend reasoning in tags if available (native thinking tokens)
- if msg.get("reasoning") and msg["reasoning"].strip():
- content = f"\n{msg['reasoning']}\n\n"
-
- if msg.get("content") and msg["content"].strip():
- # -> (model reasons via XML when native thinking is off)
- content += convert_scratchpad_to_think(msg["content"]) + "\n"
-
- for tool_call in msg["tool_calls"]:
- if not tool_call or not isinstance(tool_call, dict): continue
- # Arguments were validated during conversation; try/except is a safety net
- try:
- arguments = json.loads(tool_call["function"]["arguments"]) if isinstance(tool_call["function"]["arguments"], str) else tool_call["function"]["arguments"]
- except json.JSONDecodeError:
- # Should not happen (validated during the conversation); degrade to {} rather than abort.
- logger.warning("Unexpected invalid JSON in trajectory conversion: %s", tool_call['function']['arguments'][:100])
- arguments = {}
-
- tool_call_json = {
- "name": tool_call["function"]["name"],
- "arguments": arguments
- }
- content += f"\n{json.dumps(tool_call_json, ensure_ascii=False)}\n\n"
-
- # Every gpt turn gets a block (empty if none) for a consistent training format
- if "" not in content:
- content = "\n\n" + content
-
- trajectory.append({
- "from": "gpt",
- "value": content.rstrip()
- })
-
- tool_responses = []
- j = i + 1
- while j < len(messages) and messages[j]["role"] == "tool":
- tool_msg = messages[j]
- tool_response = "\n"
-
- # Pretty-print tool content if it looks like JSON
- tool_content = tool_msg["content"]
- try:
- if tool_content.strip().startswith(("{", "[")):
- tool_content = json.loads(tool_content)
- except (json.JSONDecodeError, AttributeError):
- pass # Keep as string if not valid JSON
-
- tool_index = len(tool_responses)
- tool_name = (
- msg["tool_calls"][tool_index]["function"]["name"]
- if tool_index < len(msg["tool_calls"])
- else "unknown"
- )
- tool_response += json.dumps({
- "tool_call_id": tool_msg.get("tool_call_id", ""),
- "name": tool_name,
- "content": tool_content
- }, ensure_ascii=False)
- tool_response += "\n"
- tool_responses.append(tool_response)
- j += 1
-
+ if msg.get("tool_calls"):
+ trajectory.append({"from": "gpt", "value": _trajectory_tool_call_turn(msg)})
+ tool_responses, j = _trajectory_tool_responses(msg, messages, i + 1)
if tool_responses:
- trajectory.append({
- "from": "tool",
- "value": "\n".join(tool_responses)
- })
- i = j - 1 # Skip the tool messages we just processed
-
+ trajectory.append({"from": "tool", "value": "\n".join(tool_responses)})
+ i = j - 1 # skip the tool messages just processed
else:
- content = ""
-
- # Prepend reasoning in tags if available (native thinking tokens)
- if msg.get("reasoning") and msg["reasoning"].strip():
- content = f"\n{msg['reasoning']}\n\n"
-
- # -> (model reasons via XML when native thinking is off)
- raw_content = msg["content"] or ""
- content += convert_scratchpad_to_think(raw_content)
-
- # Every gpt turn gets a block (empty if none) for a consistent training format
- if "" not in content:
- content = "\n\n" + content
-
- trajectory.append({
- "from": "gpt",
- "value": content.strip()
- })
-
+ content = _trajectory_gpt_prefix(msg) + convert_scratchpad_to_think(msg["content"] or "")
+ trajectory.append({"from": "gpt", "value": _with_think_block(content).strip()})
elif msg["role"] == "user":
- trajectory.append({
- "from": "human",
- "value": msg["content"]
- })
-
+ trajectory.append({"from": "human", "value": msg["content"]})
i += 1
-
return trajectory
@@ -706,60 +667,62 @@ def repair_message_sequence_with_cursor(agent, messages: List[Dict]) -> int:
+def _flatten_content_text(content: Any) -> str:
+ """Flatten list/dict content (e.g. Anthropic-via-OpenRouter block lists) to text.
+
+ A raw list hitting ``re.sub`` raises TypeError and the conversation loop retries forever.
+ Thinking/reasoning blocks are dropped outright; their text key varies per provider.
+ """
+ if isinstance(content, str):
+ return content
+ if isinstance(content, list):
+ parts: list[str] = []
+ for part in content:
+ if isinstance(part, str):
+ parts.append(part)
+ elif isinstance(part, dict):
+ if str(part.get("type") or "").strip().lower() in {"thinking", "reasoning", "redacted_thinking"}:
+ continue
+ text = part.get("text")
+ if isinstance(text, str) and text:
+ parts.append(text)
+ return "".join(parts)
+ if isinstance(content, dict):
+ return str(content.get("text") or content.get("content") or "")
+ return str(content)
+
+
+# Order matters: closed pairs first (case-insensitive so mixed-case tags don't fall through
+# to the unterminated pass and eat trailing content), then tool-call XML blocks, the
+# boundary+name-gated block, the unterminated reasoning block, stray orphan
+# reasoning tags, and finally stray tool-call CLOSERS only (bare/unterminated
+# is kept: a truncated streaming tail may still be valuable, matching OpenClaw's asymmetry).
+_THINK_STRIP_PATTERNS = (
+ *_REASONING_BLOCK_PATTERNS,
+ *_TOOL_CALL_BLOCK_PATTERNS,
+ _NAMED_FUNCTION_BLOCK_PATTERN,
+ _UNTERMINATED_REASONING_BLOCK_PATTERN,
+ _ORPHAN_REASONING_TAG_PATTERN,
+ _STRAY_TOOL_CALL_CLOSER_PATTERN,
+)
+
+
def strip_think_blocks(agent, content: str) -> str:
"""Remove reasoning/thinking blocks from content, returning only visible text.
Strips closed tag pairs, unterminated open tags at a block boundary (mirrors
- ``gateway/stream_consumer.py``), stray orphan tags, and all case-insensitive
- variants (think/thinking/reasoning/REASONING_SCRATCHPAD/thought). Also strips
- standalone tool-call XML blocks some open models emit in content (ported from
- openclaw/openclaw#67318); the ```` variant is boundary- and
+ ``gateway/stream_consumer.py``), stray orphan tags, all case-insensitive variants
+ (think/thinking/reasoning/REASONING_SCRATCHPAD/thought), and standalone tool-call XML
+ blocks some open models emit; the ```` variant is boundary- and
``name=``-gated so prose mentions survive.
"""
if not content:
return ""
- # Flatten list/dict content (e.g. Anthropic-via-OpenRouter block lists from
- # stored history) before regex: a raw list hits re.sub, raises TypeError,
- # and the conversation loop retries forever.
- if not isinstance(content, str):
- if isinstance(content, list):
- _parts: list[str] = []
- for _part in content:
- if isinstance(_part, str):
- _parts.append(_part)
- elif isinstance(_part, dict):
- _ptype = str(_part.get("type") or "").strip().lower()
- # Drop thinking/reasoning blocks outright; their text key varies per provider.
- if _ptype in {"thinking", "reasoning", "redacted_thinking"}:
- continue
- _text = _part.get("text")
- if isinstance(_text, str) and _text:
- _parts.append(_text)
- content = "".join(_parts)
- elif isinstance(content, dict):
- content = str(content.get("text") or content.get("content") or "")
- else:
- content = str(content)
- if not content:
- return ""
- # 1. Closed tag pairs, case-insensitive so mixed-case tags do not fall
- # through to the unterminated pass and eat trailing content.
- for _pattern in _REASONING_BLOCK_PATTERNS:
- content = _pattern.sub('', content)
- # 1b. Tool-call XML blocks (openclaw/openclaw#67318); generic tags need no attribute gating.
- for _pattern in _TOOL_CALL_BLOCK_PATTERNS:
- content = _pattern.sub('', content)
- # 1c. Gemma-style block: strip only at a block boundary
- # AND with a name attribute so prose mentions of survive.
- content = _NAMED_FUNCTION_BLOCK_PATTERN.sub('', content)
- # 2. Unterminated reasoning block at a block boundary: strip to end of
- # string (#8878, #9568: MiniMax M2.7 leaking raw reasoning).
- content = _UNTERMINATED_REASONING_BLOCK_PATTERN.sub('', content)
- # 3. Stray orphan open/close tags that slipped through.
- content = _ORPHAN_REASONING_TAG_PATTERN.sub('', content)
- # 3b. Stray tool-call closers only; bare/unterminated is kept since a
- # truncated streaming tail may still be valuable (matches OpenClaw asymmetry).
- content = _STRAY_TOOL_CALL_CLOSER_PATTERN.sub('', content)
+ content = _flatten_content_text(content)
+ if not content:
+ return ""
+ for pattern in _THINK_STRIP_PATTERNS:
+ content = pattern.sub('', content)
return content
@@ -1406,64 +1369,43 @@ _TRANSIENT_TRANSPORT_ERRORS = frozenset({
+_INLINE_REASONING_PATTERNS = tuple(
+ re.compile(rf"<{tag}>(.*?){tag}>", re.DOTALL | re.IGNORECASE)
+ for tag in ("think", "thinking", "thought", "reasoning", "REASONING_SCRATCHPAD")
+)
+
+
def extract_reasoning(agent, assistant_message) -> Optional[str]:
"""Extract reasoning text from an assistant message, or None.
Checks ``reasoning``, ``reasoning_content``, ``reasoning_details`` (OpenRouter unified),
then inline thinking blocks in list content.
"""
- reasoning_parts = []
-
- if hasattr(assistant_message, 'reasoning') and assistant_message.reasoning:
- reasoning_parts.append(assistant_message.reasoning)
-
- if hasattr(assistant_message, 'reasoning_content') and assistant_message.reasoning_content:
- if assistant_message.reasoning_content not in reasoning_parts:
- reasoning_parts.append(assistant_message.reasoning_content)
-
- # reasoning_details: [{"type": "reasoning.summary", "summary": "...", ...}, ...]
- if hasattr(assistant_message, 'reasoning_details') and assistant_message.reasoning_details:
- for detail in assistant_message.reasoning_details:
- if isinstance(detail, dict):
- summary = (
- detail.get('summary')
- or detail.get('thinking')
- or detail.get('content')
- or detail.get('text')
- )
- if summary and summary not in reasoning_parts:
- reasoning_parts.append(summary)
+ parts: List[str] = []
+ def _add(text) -> None:
+ if text and text not in parts:
+ parts.append(text)
+
+ _add(getattr(assistant_message, "reasoning", None))
+ _add(getattr(assistant_message, "reasoning_content", None))
+ # reasoning_details: [{"type": "reasoning.summary", "summary": "...", ...}, ...]
+ for detail in getattr(assistant_message, "reasoning_details", None) or []:
+ if isinstance(detail, dict):
+ _add(detail.get('summary') or detail.get('thinking') or detail.get('content') or detail.get('text'))
# Fall back to reasoning embedded in content only when no structured field was found.
content = getattr(assistant_message, "content", None)
- if not reasoning_parts and isinstance(content, list):
+ if not parts and isinstance(content, list):
# DeepSeek V4 Pro returns typed content blocks ({"type": "thinking", ...}); dropping
- # them makes the next turn fail with HTTP 400 "thinking must be passed back" (#21944).
+ # them makes the next turn fail with HTTP 400 "thinking must be passed back".
for block in content:
if isinstance(block, dict) and block.get("type") == "thinking":
- thinking_text = block.get("thinking") or block.get("text") or ""
- thinking_text = thinking_text.strip()
- if thinking_text and thinking_text not in reasoning_parts:
- reasoning_parts.append(thinking_text)
- if not reasoning_parts and isinstance(content, str) and content:
- inline_patterns = (
- r"(.*?)",
- r"(.*?)",
- r"(.*?)",
- r"(.*?)",
- r"(.*?)",
- )
- for pattern in inline_patterns:
- flags = re.DOTALL | re.IGNORECASE
- for block in re.findall(pattern, content, flags=flags):
- cleaned = block.strip()
- if cleaned and cleaned not in reasoning_parts:
- reasoning_parts.append(cleaned)
-
- if reasoning_parts:
- return "\n\n".join(reasoning_parts)
-
- return None
+ _add((block.get("thinking") or block.get("text") or "").strip())
+ if not parts and isinstance(content, str) and content:
+ for pattern in _INLINE_REASONING_PATTERNS:
+ for block in pattern.findall(content):
+ _add(block.strip())
+ return "\n\n".join(parts) if parts else None
@@ -1707,18 +1649,14 @@ def _is_litellm_route(provider_lower: str, base_url: str) -> bool:
``litellm`` must match as a whole delimited token (not substring) in provider or host;
a path segment never qualifies.
"""
- if _has_litellm_token(provider_lower, ":-_/"):
- return True
- return _has_litellm_token(base_url_hostname(base_url), ".-")
+ return _has_litellm_token(provider_lower, ":-_/") or _has_litellm_token(base_url_hostname(base_url), ".-")
def _has_litellm_token(value: str, delimiters: str) -> bool:
"""True when ``value`` contains ``litellm`` as a whole delimited token."""
if not value:
return False
- for delimiter in delimiters:
- value = value.replace(delimiter, " ")
- return "litellm" in value.split()
+ return "litellm" in value.translate(str.maketrans(delimiters, " " * len(delimiters))).split()
def _moa_aggregator_cache_policy(agent, eff_model: str) -> tuple[bool, bool]:
@@ -2743,15 +2681,6 @@ def repair_tool_call(agent, tool_name: str) -> str | None:
return None
-def _tool_call_id_variants(tc: Any) -> set:
- """Return every id a tool result might match this tool_call on.
-
- Thin backward-compatible forwarder; policy owner is
- ``agent.message_sanitization.tool_call_id_variants``.
- """
- return set(tool_call_id_variants(tc))
-
-
# Placeholder for an empty non-final message the provider would reject. Kept identical to
# the stub placeholder in chat_completion_helpers so healed transcripts read consistently.
_INTERRUPTED_PLACEHOLDER = "[response interrupted]"
From 512ea3b0c60c719fb8dc1d6100d110ea7f498e6b Mon Sep 17 00:00:00 2001
From: Teknium <127238744+teknium1@users.noreply.github.com>
Date: Wed, 2 Sep 2026 18:58:17 -0700
Subject: [PATCH 08/20] refactor(agent/agent_runtime_helpers): helper
extraction for arg sanitizer, user-merge, heal log, socket walk,
error-context regexes
---
agent/agent_runtime_helpers.py | 485 ++++++++++++++-------------------
1 file changed, 211 insertions(+), 274 deletions(-)
diff --git a/agent/agent_runtime_helpers.py b/agent/agent_runtime_helpers.py
index d227503ea9..e8cdc09f00 100644
--- a/agent/agent_runtime_helpers.py
+++ b/agent/agent_runtime_helpers.py
@@ -196,6 +196,46 @@ def convert_to_trajectory_format(agent, messages: List[Dict[str, Any]], user_que
+def _prepend_corruption_marker(tool_msg: dict, marker: str) -> None:
+ existing = tool_msg.get("content")
+ if isinstance(existing, str):
+ if not existing:
+ tool_msg["content"] = marker
+ elif not existing.startswith(marker):
+ tool_msg["content"] = f"{marker}\n{existing}"
+ return
+ if existing is None:
+ tool_msg["content"] = marker
+ return
+ try:
+ existing_text = json.dumps(existing)
+ except TypeError:
+ existing_text = str(existing)
+ tool_msg["content"] = f"{marker}\n{existing_text}"
+
+
+def _find_tool_result(messages: list, start: int, tool_call: dict) -> Optional[dict]:
+ """The tool result answering ``tool_call`` in the run starting at ``start``, if any."""
+ for candidate in messages[start:]:
+ if not isinstance(candidate, dict) or candidate.get("role") != "tool":
+ return None
+ if tool_result_id_variants(candidate.get("tool_call_id")) & tool_call_id_variants(tool_call):
+ return candidate
+ return None
+
+
+def _cursor_skip_prefix(messages: list, cursor: Optional[dict]) -> int:
+ """Length of the ``is``-identical prefix already validated on the previous call."""
+ start = 0
+ if cursor is not None:
+ prev_prefix = cursor.get("prefix")
+ if isinstance(prev_prefix, list):
+ limit = min(len(prev_prefix), len(messages))
+ while start < limit and messages[start] is prev_prefix[start]:
+ start += 1
+ return start
+
+
def sanitize_tool_call_arguments(
messages: list,
*,
@@ -205,134 +245,73 @@ def sanitize_tool_call_arguments(
) -> int:
"""Repair corrupted assistant tool-call argument JSON in-place.
- ``cursor`` (optional caller-owned dict) stores under ``"prefix"`` strong
- references to the message objects validated last call; the longest
- ``is``-identical prefix is skipped on the next call. Skipping is safe
- because only the surrogate/non-ASCII sanitizers mutate arguments on live
- dicts (inside JSON string values), and every other path replaces or
- reorders dicts, breaking identity. Strong refs (not ``id()``) rule out
- address-reuse aliasing (#50372).
+ ``cursor`` (optional caller-owned dict) stores under ``"prefix"`` strong references to
+ the message objects validated last call; the longest ``is``-identical prefix is skipped
+ on the next call. Skipping is safe because only the surrogate/non-ASCII sanitizers
+ mutate arguments on live dicts (inside JSON string values), and every other path
+ replaces or reorders dicts, breaking identity. Strong refs (not ``id()``) rule out
+ address-reuse aliasing.
"""
log = logger or logging.getLogger(__name__)
if not isinstance(messages, list):
return 0
-
- start_index = 0
- if cursor is not None:
- prev_prefix = cursor.get("prefix")
- if isinstance(prev_prefix, list):
- limit = min(len(prev_prefix), len(messages))
- while start_index < limit and messages[start_index] is prev_prefix[start_index]:
- start_index += 1
-
repaired = 0
marker = _ra().AIAgent._TOOL_CALL_ARGUMENTS_CORRUPTION_MARKER
-
- def _prepend_marker(tool_msg: dict) -> None:
- existing = tool_msg.get("content")
- if isinstance(existing, str):
- if not existing:
- tool_msg["content"] = marker
- elif not existing.startswith(marker):
- tool_msg["content"] = f"{marker}\n{existing}"
- return
- if existing is None:
- tool_msg["content"] = marker
- return
- try:
- existing_text = json.dumps(existing)
- except TypeError:
- existing_text = str(existing)
- tool_msg["content"] = f"{marker}\n{existing_text}"
-
- message_index = start_index
+ message_index = _cursor_skip_prefix(messages, cursor)
while message_index < len(messages):
msg = messages[message_index]
- if not isinstance(msg, dict) or msg.get("role") != "assistant":
- message_index += 1
- continue
-
- tool_calls = msg.get("tool_calls")
+ tool_calls = msg.get("tool_calls") if isinstance(msg, dict) and msg.get("role") == "assistant" else None
if not isinstance(tool_calls, list) or not tool_calls:
message_index += 1
continue
-
insert_at = message_index + 1
for tool_call in tool_calls:
- if not isinstance(tool_call, dict):
- continue
- function = tool_call.get("function")
+ function = tool_call.get("function") if isinstance(tool_call, dict) else None
if not isinstance(function, dict):
continue
-
arguments = function.get("arguments")
- if arguments is None or arguments == "":
- function["arguments"] = "{}"
- continue
- if isinstance(arguments, str) and not arguments.strip():
+ if arguments is None or (isinstance(arguments, str) and not arguments.strip()):
function["arguments"] = "{}"
continue
if not isinstance(arguments, str):
continue
-
try:
json.loads(arguments)
+ continue
except json.JSONDecodeError:
- # Use canonical ``call_id || id`` precedence so scan and stub share the id
- # the pipeline uses; bare ``id`` misses Codex call_id results and orphans a stub (#58168).
- tool_call_id = _ra().AIAgent._get_tool_call_id_static(tool_call) or None
- function_name = function.get("name", "?")
- # Log the FULL (bounded) argument string: we are about to overwrite the only
- # copy, which may hold real user content from a truncated write_file/patch (#80498).
- preview = arguments[:_FULL_ARGS_LOG_BOUND]
- log.warning(
- "Corrupted tool_call arguments repaired before request "
- "(session=%s, message_index=%s, tool_call_id=%s, function=%s, "
- "original_arguments=%r)",
- session_id or "-",
- message_index,
- tool_call_id or "-",
- function_name,
- preview,
+ pass
+ # Canonical ``call_id || id`` precedence so scan and stub share the id the
+ # pipeline uses; bare ``id`` misses Codex call_id results and orphans a stub.
+ tool_call_id = _ra().AIAgent._get_tool_call_id_static(tool_call) or None
+ function_name = function.get("name", "?")
+ # Log the FULL (bounded) argument string: we are about to overwrite the only
+ # copy, which may hold real user content from a truncated write_file/patch.
+ log.warning(
+ "Corrupted tool_call arguments repaired before request "
+ "(session=%s, message_index=%s, tool_call_id=%s, function=%s, "
+ "original_arguments=%r)",
+ session_id or "-",
+ message_index,
+ tool_call_id or "-",
+ function_name,
+ arguments[:_FULL_ARGS_LOG_BOUND],
+ )
+ function["arguments"] = "{}"
+ existing_tool_msg = _find_tool_result(messages, message_index + 1, tool_call)
+ if existing_tool_msg is None:
+ messages.insert(
+ insert_at,
+ make_tool_result_message(function_name if function_name != "?" else "", marker, tool_call_id),
)
- function["arguments"] = "{}"
-
- existing_tool_msg = None
- scan_index = message_index + 1
- while scan_index < len(messages):
- candidate = messages[scan_index]
- if not isinstance(candidate, dict) or candidate.get("role") != "tool":
- break
- if (
- tool_result_id_variants(candidate.get("tool_call_id"))
- & tool_call_id_variants(tool_call)
- ):
- existing_tool_msg = candidate
- break
- scan_index += 1
-
- if existing_tool_msg is None:
- messages.insert(
- insert_at,
- make_tool_result_message(
- function_name if function_name != "?" else "",
- marker,
- tool_call_id,
- ),
- )
- insert_at += 1
- else:
- _prepend_marker(existing_tool_msg)
-
- repaired += 1
-
+ insert_at += 1
+ else:
+ _prepend_corruption_marker(existing_tool_msg, marker)
+ repaired += 1
message_index += 1
-
if cursor is not None:
- # Strong refs to the objects validated this call; any divergence
- # (compression, undo, repair, steer) forces a re-scan from that index.
+ # Strong refs to the objects validated this call; any divergence (compression,
+ # undo, repair, steer) forces a re-scan from that index.
cursor["prefix"] = messages[:]
-
return repaired
@@ -367,19 +346,18 @@ def note_turn_start(agent, turn_id: str):
getattr(agent, "session_id", None) or "-",
)
overlap = prev
-
- # Cross-agent leg: same session_id in flight under another agent object
- # (busy guard is keyed by routing key and cannot see it). Persist-disabled
- # forks share the parent's session_id but never write, so they must not
- # register or pop here (note_turn_persisted skips them symmetrically).
+ # Cross-agent leg: same session_id in flight under another agent object (busy guard is
+ # keyed by routing key and cannot see it). Persist-disabled forks share the parent's
+ # session_id but never write, so they must not register or pop here
+ # (note_turn_persisted skips them symmetrically).
session_id = getattr(agent, "session_id", None)
if session_id and not getattr(agent, "_persist_disabled", False):
now = time.time()
with _INFLIGHT_TURNS_LOCK:
entry = _INFLIGHT_TURNS_BY_SESSION.get(session_id)
_INFLIGHT_TURNS_BY_SESSION[session_id] = (turn_id, now)
- # Record the session id registered under: compression can rotate
- # agent.session_id mid-turn and persist must pop the slot actually held.
+ # Record the session id registered under: compression can rotate agent.session_id
+ # mid-turn and persist must pop the slot actually held.
agent._inflight_turn_session_id = session_id
if entry and entry[0] not in (turn_id, prev):
logger.warning(
@@ -1089,6 +1067,25 @@ def try_recover_primary_transport(
+def _merge_user_content(prev_content: Any, cur_content: Any) -> Any:
+ """Merged content for two adjacent user messages; ``_UNMERGEABLE`` for unknown shapes.
+
+ String+string joins with a blank line; list sides append as separate blocks.
+ """
+ if isinstance(prev_content, str) and isinstance(cur_content, str):
+ return prev_content + ("\n\n" if prev_content and cur_content else "") + cur_content
+ if isinstance(prev_content, list) and isinstance(cur_content, list):
+ return list(prev_content) + list(cur_content)
+ if isinstance(prev_content, list) and isinstance(cur_content, str):
+ return list(prev_content) + ([{"type": "text", "text": cur_content}] if cur_content else [])
+ if isinstance(prev_content, str) and isinstance(cur_content, list):
+ return ([{"type": "text", "text": prev_content}] if prev_content else []) + list(cur_content)
+ return _UNMERGEABLE
+
+
+_UNMERGEABLE = object()
+
+
def drop_thinking_only_and_merge_users(
messages: List[Dict[str, Any]],
*,
@@ -1102,63 +1099,27 @@ def drop_thinking_only_and_merge_users(
"""
if not messages:
return messages
-
- # Pass 1: drop thinking-only assistant turns.
kept = [
m for m in messages
- if not _ra().AIAgent._is_thinking_only_assistant(
- m,
- drop_codex_reasoning_items=drop_codex_reasoning_items,
- )
+ if not _ra().AIAgent._is_thinking_only_assistant(m, drop_codex_reasoning_items=drop_codex_reasoning_items)
]
dropped = len(messages) - len(kept)
-
- # Pass 2: merge any newly-adjacent user messages.
merged: List[Dict[str, Any]] = []
merges = 0
for m in kept:
prev = merged[-1] if merged else None
- if (
- prev is not None
- and prev.get("role") == "user"
- and m.get("role") == "user"
- ):
- prev_content = prev.get("content", "")
- cur_content = m.get("content", "")
- # Copy ``prev`` so caller dicts are never mutated (safe from tests/other loops).
- prev_copy = dict(prev)
- # Only string+string content merges; list (multimodal) sides append as separate blocks.
- if isinstance(prev_content, str) and isinstance(cur_content, str):
- sep = "\n\n" if prev_content and cur_content else ""
- prev_copy["content"] = prev_content + sep + cur_content
- elif isinstance(prev_content, list) and isinstance(cur_content, list):
- prev_copy["content"] = list(prev_content) + list(cur_content)
- elif isinstance(prev_content, list) and isinstance(cur_content, str):
- if cur_content:
- prev_copy["content"] = list(prev_content) + [
- {"type": "text", "text": cur_content}
- ]
- else:
- prev_copy["content"] = list(prev_content)
- elif isinstance(prev_content, str) and isinstance(cur_content, list):
- new_blocks: List[Dict[str, Any]] = []
- if prev_content:
- new_blocks.append({"type": "text", "text": prev_content})
- new_blocks.extend(cur_content)
- prev_copy["content"] = new_blocks
- else:
- # Unknown content shape — fall back to appending separately
- # (violates alternation, but safer than raising in a hot path).
- merged.append(m)
+ if prev is not None and prev.get("role") == "user" and m.get("role") == "user":
+ content = _merge_user_content(prev.get("content", ""), m.get("content", ""))
+ if content is not _UNMERGEABLE:
+ # Copy ``prev`` so caller dicts are never mutated.
+ merged[-1] = {**prev, "content": content}
+ merges += 1
continue
- merged[-1] = prev_copy
- merges += 1
- else:
- merged.append(m)
-
+ # Unknown content shape: append separately (violates alternation, but safer
+ # than raising in a hot path).
+ merged.append(m)
if dropped == 0 and merges == 0:
return messages
-
_ra().logger.debug(
"Pre-call sanitizer: dropped %d thinking-only assistant turn(s), "
"merged %d adjacent user message(s)",
@@ -2712,26 +2673,25 @@ def _msg_has_payload(msg: Dict[str, Any]) -> bool:
for block in content:
if isinstance(block, dict):
# any typed block counts, as long as a text block is not itself blank
- if block.get("type") == "text":
- if isinstance(block.get("text"), str) and block["text"].strip():
- return True
- continue
- return True
+ if block.get("type") != "text":
+ return True
+ if isinstance(block.get("text"), str) and block["text"].strip():
+ return True
elif block:
return True
elif content not in (None, ""):
return True
- # Structural payloads that make an "empty-content" message still valid.
- if msg.get("tool_calls"):
- return True
- if isinstance(msg.get("reasoning_content"), str) and msg["reasoning_content"].strip():
- return True
- if msg.get("reasoning") or msg.get("reasoning_details"):
- return True
- # Codex Responses item carriers persist with content:"" by design (text lives in
- # codex_message_items / codex_reasoning_items and is replayed); treat as payload so
- # the repair never rewrites a designed-empty codex turn.
- return bool(msg.get("codex_message_items") or msg.get("codex_reasoning_items"))
+ # Structural payloads that make an "empty-content" message still valid. Codex Responses
+ # item carriers persist with content:"" by design (text lives in codex_*_items and is
+ # replayed); treating them as payload keeps the repair from rewriting a designed-empty turn.
+ return bool(
+ msg.get("tool_calls")
+ or (isinstance(msg.get("reasoning_content"), str) and msg["reasoning_content"].strip())
+ or msg.get("reasoning")
+ or msg.get("reasoning_details")
+ or msg.get("codex_message_items")
+ or msg.get("codex_reasoning_items")
+ )
def fill_empty_non_final_wire_payload(
@@ -2805,7 +2765,7 @@ def get_sanitizer_heal_stats() -> Dict[str, Dict[str, Any]]:
def _log_empty_non_final_heal(healed: int) -> None:
- """WARNING on the first heals in a window, one ERROR at the threshold, then silent (#96870).
+ """WARNING on the first heals in a window, one ERROR at the threshold, then silent.
The threshold also queues a ONE-TIME out-of-band user notice (drained by
``consume_pending_sanitizer_heal_notice``); never re-armed by a new window.
@@ -2816,14 +2776,12 @@ def _log_empty_non_final_heal(healed: int) -> None:
with _empty_heal_log_lock:
state = _empty_heal_log_state.get(key)
if state is None or (now - state["window_start"]) > _EMPTY_HEAL_WINDOW_S:
- prior_events = state.get("total_events", 0) if state else 0
- prior_healed = state.get("total_healed", 0) if state else 0
state = {
"count": 0,
"window_start": now,
"escalated": False,
- "total_events": prior_events,
- "total_healed": prior_healed,
+ "total_events": state.get("total_events", 0) if state else 0,
+ "total_healed": state.get("total_healed", 0) if state else 0,
}
_empty_heal_log_state[key] = state
state["count"] += 1
@@ -2832,9 +2790,11 @@ def _log_empty_non_final_heal(healed: int) -> None:
count = state["count"]
total_events = state["total_events"]
total_healed = state["total_healed"]
- if threshold > 0 and count >= threshold and not state["escalated"]:
+ if state["escalated"]:
+ return
+ escalate = threshold > 0 and count >= threshold
+ if escalate:
state["escalated"] = True
- level = "error"
if key not in _empty_heal_user_notified:
_empty_heal_user_notified.add(key)
_empty_heal_pending_notice[key] = (
@@ -2845,14 +2805,7 @@ def _log_empty_non_final_heal(healed: int) -> None:
"doctor` to capture diagnostics, or /new to start a "
"clean session."
)
- elif state["escalated"]:
- level = "silent"
- else:
- level = "warning"
-
- if level == "silent":
- return
- if level == "error":
+ if escalate:
_ra().logger.error(
"Pre-call sanitizer: repeated-heal escalation for session %s — "
"healed %d empty non-final message(s) this send; heal pattern: "
@@ -3449,26 +3402,48 @@ def _connection_candidates(conn: Any):
stack.append(inner)
+def _socket_from_stream(stream: Any):
+ """Raw socket behind an httpcore network stream (several backends), or None."""
+ sock = getattr(stream, "_sock", None)
+ if sock is None:
+ get_extra_info = getattr(stream, "get_extra_info", None)
+ if callable(get_extra_info):
+ try:
+ sock = get_extra_info("socket")
+ except Exception:
+ sock = None
+ if sock is None:
+ wrapped = getattr(stream, "stream", None)
+ if wrapped is not None:
+ sock = getattr(wrapped, "_sock", None)
+ if sock is None:
+ # anyio-backed streams expose the raw socket through SocketAttribute.raw_socket.
+ extra = getattr(getattr(stream, "_stream", None), "extra", None)
+ if callable(extra):
+ try:
+ from anyio.abc import SocketAttribute
+ sock = extra(SocketAttribute.raw_socket)
+ except Exception:
+ sock = None
+ return sock
+
+
def _iter_pool_sockets(client: Any):
"""Yield raw sockets reachable from an OpenAI/httpx client pool.
Traversal is defensive over private httpcore internals (``conn._connection``,
proxy tunnel wrappers) that vary by release. Also walks mount transports and
- in-flight ``PoolRequest.connection`` objects, reachable when
- ``_connections`` is empty during checkout (#85252).
+ in-flight ``PoolRequest.connection`` objects, reachable when ``_connections``
+ is empty during checkout.
"""
try:
+ # Some SDK wrappers *are* the httpx client; fall through so mount-aware discovery runs.
http_client = getattr(client, "_client", None)
if http_client is None:
- # Some SDK wrappers *are* the httpx client; fall through so mount-aware discovery runs.
http_client = client
pools = list(_iter_httpx_pool_objects(http_client))
except Exception:
return
-
- if not pools:
- return
-
seen: set[int] = set()
for pool in pools:
# ``is None``, not falsiness: an empty ``_connections`` must still let us walk in-flight ``_requests``.
@@ -3482,41 +3457,11 @@ def _iter_pool_sockets(client: Any):
connections.append(conn)
for conn in connections:
for candidate in _connection_candidates(conn):
- stream = (
- getattr(candidate, "_network_stream", None)
- or getattr(candidate, "_stream", None)
- )
- if stream is None:
+ stream = getattr(candidate, "_network_stream", None) or getattr(candidate, "_stream", None)
+ sock = _socket_from_stream(stream) if stream is not None else None
+ if sock is None or id(sock) in seen:
continue
- sock = getattr(stream, "_sock", None)
- if sock is None:
- get_extra_info = getattr(stream, "get_extra_info", None)
- if callable(get_extra_info):
- try:
- sock = get_extra_info("socket")
- except Exception:
- sock = None
- if sock is None:
- wrapped = getattr(stream, "stream", None)
- if wrapped is not None:
- sock = getattr(wrapped, "_sock", None)
- if sock is None:
- # anyio-backed streams expose the raw socket through
- # SocketAttribute.raw_socket when available.
- wrapped = getattr(stream, "_stream", None)
- extra = getattr(wrapped, "extra", None)
- if callable(extra):
- try:
- from anyio.abc import SocketAttribute
- sock = extra(SocketAttribute.raw_socket)
- except Exception:
- sock = None
- if sock is None:
- continue
- marker = id(sock)
- if marker in seen:
- continue
- seen.add(marker)
+ seen.add(id(sock))
yield sock
@@ -3557,10 +3502,42 @@ def cleanup_dead_connections(agent) -> bool:
+_QUOTA_RESET_DELAY_RE = re.compile(r"quotaResetDelay[:\s\"]+(\d+(?:\.\d+)?)(ms|s)", re.IGNORECASE)
+_RESETS_IN_RE = re.compile(
+ r"resets?\s+in\s+"
+ r"(?:(\d+(?:\.\d+)?)\s*(?:h|hr|hrs|hour|hours)\b\s*)?"
+ r"(?:(\d+(?:\.\d+)?)\s*(?:m|min|mins|minute|minutes)\b\s*)?"
+ r"(?:(\d+(?:\.\d+)?)\s*(?:s|sec|secs|second|seconds)\b)?",
+ re.IGNORECASE,
+)
+_RETRY_AFTER_SECONDS_RE = re.compile(r"retry\s+(?:after\s+)?(\d+(?:\.\d+)?)\s*(?:sec|secs|seconds|s\b)", re.IGNORECASE)
+
+
+def _reset_delay_from_message(message: str) -> Optional[float]:
+ """Seconds-until-reset parsed from free-text provider messages, or None."""
+ m = _QUOTA_RESET_DELAY_RE.search(message)
+ if m:
+ value = float(m.group(1))
+ return value / 1000.0 if m.group(2).lower() == "ms" else value
+ m = _RESETS_IN_RE.search(message)
+ if m and any(m.groups()):
+ return float(m.group(1) or 0) * 3600 + float(m.group(2) or 0) * 60 + float(m.group(3) or 0)
+ m = _RETRY_AFTER_SECONDS_RE.search(message)
+ return float(m.group(1)) if m else None
+
+
+def _set_reset_from_retry_after(context: Dict[str, Any], retry_after: Any) -> None:
+ if retry_after in {None, ""} or "reset_at" in context:
+ return
+ try:
+ context["reset_at"] = time.time() + float(retry_after)
+ except (TypeError, ValueError):
+ pass
+
+
def extract_api_error_context(error: Exception) -> Dict[str, Any]:
"""Extract structured rate-limit details from provider errors."""
context: Dict[str, Any] = {}
-
body = getattr(error, "body", None)
payload = None
if isinstance(body, dict):
@@ -3571,8 +3548,7 @@ def extract_api_error_context(error: Exception) -> Dict[str, Any]:
context["reason"] = reason.strip()
message = payload.get("message") or payload.get("error_description")
if not message and isinstance(payload.get("error"), str):
- # xAI uses a top-level string ``error`` beside a structured
- # ``code`` (for example personal-team-blocked:spending-limit).
+ # xAI uses a top-level string ``error`` beside a structured ``code``.
message = payload.get("error")
if isinstance(message, str) and message.strip():
context["message"] = message.strip()
@@ -3581,62 +3557,23 @@ def extract_api_error_context(error: Exception) -> Dict[str, Any]:
if value not in {None, ""}:
context["reset_at"] = value
break
- retry_after = payload.get("retry_after")
- if retry_after not in {None, ""} and "reset_at" not in context:
- try:
- context["reset_at"] = time.time() + float(retry_after)
- except (TypeError, ValueError):
- pass
-
- response = getattr(error, "response", None)
- headers = getattr(response, "headers", None)
+ _set_reset_from_retry_after(context, payload.get("retry_after"))
+ headers = getattr(getattr(error, "response", None), "headers", None)
if headers:
- retry_after = headers.get("retry-after") or headers.get("Retry-After")
- if retry_after and "reset_at" not in context:
- try:
- context["reset_at"] = time.time() + float(retry_after)
- except (TypeError, ValueError):
- pass
+ _set_reset_from_retry_after(context, headers.get("retry-after") or headers.get("Retry-After") or None)
ratelimit_reset = headers.get("x-ratelimit-reset")
if ratelimit_reset and "reset_at" not in context:
context["reset_at"] = ratelimit_reset
-
if "message" not in context:
raw_message = str(error).strip()
if raw_message:
context["message"] = raw_message[:500]
-
if "reset_at" not in context:
message = context.get("message") or ""
if isinstance(message, str):
- delay_match = re.search(r"quotaResetDelay[:\s\"]+(\d+(?:\.\d+)?)(ms|s)", message, re.IGNORECASE)
- if delay_match:
- value = float(delay_match.group(1))
- seconds = value / 1000.0 if delay_match.group(2).lower() == "ms" else value
- context["reset_at"] = time.time() + seconds
- else:
- resets_in_match = re.search(
- r"resets?\s+in\s+"
- r"(?:(\d+(?:\.\d+)?)\s*(?:h|hr|hrs|hour|hours)\b\s*)?"
- r"(?:(\d+(?:\.\d+)?)\s*(?:m|min|mins|minute|minutes)\b\s*)?"
- r"(?:(\d+(?:\.\d+)?)\s*(?:s|sec|secs|second|seconds)\b)?",
- message,
- re.IGNORECASE,
- )
- if resets_in_match and any(resets_in_match.groups()):
- hours = float(resets_in_match.group(1) or 0)
- minutes = float(resets_in_match.group(2) or 0)
- seconds = float(resets_in_match.group(3) or 0)
- context["reset_at"] = time.time() + (hours * 3600) + (minutes * 60) + seconds
- else:
- sec_match = re.search(
- r"retry\s+(?:after\s+)?(\d+(?:\.\d+)?)\s*(?:sec|secs|seconds|s\b)",
- message,
- re.IGNORECASE,
- )
- if sec_match:
- context["reset_at"] = time.time() + float(sec_match.group(1))
-
+ delay = _reset_delay_from_message(message)
+ if delay is not None:
+ context["reset_at"] = time.time() + delay
return context
From 25207b1d8b64cfe430f941fc8482f26066646194 Mon Sep 17 00:00:00 2001
From: Teknium <127238744+teknium1@users.noreply.github.com>
Date: Wed, 2 Sep 2026 19:11:28 -0700
Subject: [PATCH 09/20] refactor(agent/agent_runtime_helpers):
client/invoke_tool/debug-dump/ack/steer helpers; compact async_utils +
aux_accounting
---
agent/agent_runtime_helpers.py | 552 ++++++++++++++-------------------
agent/async_utils.py | 57 +---
agent/aux_accounting.py | 64 +---
3 files changed, 255 insertions(+), 418 deletions(-)
diff --git a/agent/agent_runtime_helpers.py b/agent/agent_runtime_helpers.py
index e8cdc09f00..bfbdbc0136 100644
--- a/agent/agent_runtime_helpers.py
+++ b/agent/agent_runtime_helpers.py
@@ -1370,6 +1370,22 @@ def extract_reasoning(agent, assistant_message) -> Optional[str]:
+def _api_error_debug_info(error: Exception) -> Dict[str, Any]:
+ info: Dict[str, Any] = {"type": type(error).__name__, "message": str(error)}
+ for attr_name in ("status_code", "request_id", "code", "param", "type", "body"):
+ attr_value = getattr(error, attr_name, None)
+ if attr_value is not None:
+ info[attr_name] = attr_value
+ response_obj = getattr(error, "response", None)
+ if response_obj is not None:
+ try:
+ info["response_status"] = getattr(response_obj, "status_code", None)
+ info["response_text"] = response_obj.text
+ except Exception as e:
+ _ra().logger.debug("Could not extract error response details: %s", e)
+ return info
+
+
def dump_api_request_debug(
agent,
api_kwargs: Dict[str, Any],
@@ -1382,20 +1398,19 @@ def dump_api_request_debug(
body = copy.deepcopy(api_kwargs)
body.pop("timeout", None)
body = {k: v for k, v in body.items() if v is not None}
-
api_key = None
try:
api_key = getattr(agent.client, "api_key", None)
except Exception as e:
_ra().logger.debug("Could not extract API key for debug dump: %s", e)
-
+ endpoint = "/responses" if agent.api_mode == "codex_responses" else "/chat/completions"
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('/')}{'/responses' if agent.api_mode == 'codex_responses' else '/chat/completions'}",
+ "url": f"{agent.base_url.rstrip('/')}{endpoint}",
"headers": {
"Authorization": f"Bearer {agent._mask_api_key_for_logs(api_key)}",
"Content-Type": "application/json",
@@ -1403,49 +1418,22 @@ def dump_api_request_debug(
"body": body,
},
}
-
if error is not None:
- error_info: Dict[str, Any] = {
- "type": type(error).__name__,
- "message": str(error),
- }
- for attr_name in ("status_code", "request_id", "code", "param", "type"):
- attr_value = getattr(error, attr_name, None)
- if attr_value is not None:
- error_info[attr_name] = attr_value
-
- body_attr = getattr(error, "body", None)
- if body_attr is not None:
- error_info["body"] = body_attr
-
- response_obj = getattr(error, "response", None)
- if response_obj is not None:
- try:
- error_info["response_status"] = getattr(response_obj, "status_code", None)
- error_info["response_text"] = response_obj.text
- except Exception as e:
- _ra().logger.debug("Could not extract error response details: %s", e)
-
- dump_payload["error"] = error_info
-
+ dump_payload["error"] = _api_error_debug_info(error)
timestamp = datetime.now().strftime("%Y%m%d_%H%M%S_%f")
# Sanitize the session ID (may come from an untrusted X-Hermes-Session-Id header)
# so a "../"-shaped ID cannot write outside logs_dir.
safe_sid = _ra()._safe_session_filename_component(agent.session_id)
dump_file = agent.logs_dir / f"request_dump_{safe_sid}_{timestamp}.json"
-
# Redact secrets first: this fires unconditionally on API errors and captures the full
# request body, so context-embedded secrets would otherwise land in cleartext on disk.
from agent.redact import redact_sensitive_text
_serialized = json.dumps(dump_payload, ensure_ascii=False, indent=2, default=str)
_redacted_payload = json.loads(redact_sensitive_text(_serialized, force=True))
atomic_json_write(dump_file, _redacted_payload, default=str)
-
agent._vprint(f"{agent.log_prefix}🧾 Request debug dump written to: {dump_file}")
-
if env_var_enabled("HERMES_DUMP_REQUEST_STDOUT"):
print(json.dumps(_redacted_payload, ensure_ascii=False, indent=2, default=str))
-
return dump_file
except Exception as dump_error:
if agent.verbose_logging:
@@ -1814,16 +1802,15 @@ def anthropic_prompt_cache_policy(
def _provider_supplied_client(agent, client_kwargs: dict) -> Any | None:
"""Ask the registered ProviderProfile for a custom client, if any.
- Resolves by provider name first, then by the ``base_url`` scheme prefix so a
- runtime configured only by URL (``acp://…``) still reaches its profile.
- A profile that raises is logged and skipped: a third-party plugin must not
- be able to take the turn down, it can only fail to provide a client.
+ Resolves by provider name first, then by the ``base_url`` scheme prefix so a runtime
+ configured only by URL (``acp://…``) still reaches its profile. A profile that raises
+ is logged and skipped: a third-party plugin must not be able to take the turn down,
+ it can only fail to provide a client.
"""
try:
from providers import get_provider_profile
except Exception:
return None
-
profile = None
provider_name = (getattr(agent, "provider", "") or "").strip()
if provider_name:
@@ -1837,13 +1824,11 @@ def _provider_supplied_client(agent, client_kwargs: dict) -> Any | None:
profile = _profile_for_base_url(base_url)
if profile is None:
return None
-
try:
return profile.create_client(**client_kwargs)
except Exception:
_ra().logger.warning(
- "Provider profile %r failed to create a client; falling back to the "
- "standard client path",
+ "Provider profile %r failed to create a client; falling back to the standard client path",
getattr(profile, "name", provider_name) or "?",
exc_info=True,
)
@@ -1851,104 +1836,29 @@ def _provider_supplied_client(agent, client_kwargs: dict) -> Any | None:
def _profile_for_base_url(base_url: str) -> Any | None:
- """Find a registered profile whose own base_url matches ``base_url``.
+ """Registered profile whose own base_url is a prefix of ``base_url`` (used when the provider name did not resolve).
- Only used when the provider name did not resolve. Matches on exact base_url
- so a non-HTTP scheme (``acp://copilot``) routes to its profile even when the
- caller passed no provider name.
+ Prefix match, not equality: the replaced copilot-acp branch keyed on
+ ``startswith("acp://copilot")``, so a base_url carrying a path or a user override
+ under the same root must still resolve.
"""
try:
from providers import list_providers
- except Exception:
- return None
- target = base_url.rstrip("/").lower()
- try:
candidates = list_providers()
except Exception:
return None
+ target = base_url.rstrip("/").lower()
for candidate in candidates or []:
own = str(getattr(candidate, "base_url", "") or "").rstrip("/").lower()
- # Prefix match, not equality: the replaced copilot-acp branch keyed on
- # ``startswith("acp://copilot")``, so a base_url carrying a path or a
- # user override under the same root must still resolve.
if own and (target == own or target.startswith(own + "/")):
return candidate
return None
-def create_openai_client(agent, client_kwargs: dict, *, reason: str, shared: bool) -> Any:
- from agent.auxiliary_client import _validate_base_url, _validate_proxy_env_urls
- from agent.ssl_verify import resolve_httpx_verify
- # Treat client_kwargs as read-only: callers pass agent._client_kwargs, and in-place
- # mutation leaks into later requests (#10933: a torn-down httpx transport got reused).
- client_kwargs = dict(client_kwargs)
- # The MoA virtual provider has no OpenAI wire endpoint; the facade *is* the client.
- # Rebuild the facade, never a native client (#78382 TypeError, #53802 relay re-wire).
- if (getattr(agent, "provider", "") or "").strip().lower() == "moa":
- from agent.moa_loop import build_moa_facade
- return build_moa_facade(agent, getattr(agent, "model", None) or "default")
- ssl_ca_cert = client_kwargs.pop("ssl_ca_cert", None)
- ssl_verify_cfg = client_kwargs.pop("ssl_verify", None)
- httpx_verify = resolve_httpx_verify(ca_bundle=ssl_ca_cert, ssl_verify=ssl_verify_cfg)
- _validate_proxy_env_urls()
- _validate_base_url(client_kwargs.get("base_url"))
- # ── Provider-supplied client (registration seam) ──────────────────────
- # A provider whose wire protocol is not OpenAI-over-HTTP supplies its own
- # client from its ProviderProfile.create_client(). Consulted before the
- # built-in ladder so a profile registered from ~/.hermes/plugins/ or a pip
- # entry point can ship a transport without editing this function — that is
- # what makes an out-of-tree ACP provider possible at all. Returning None
- # (the default) falls through to the paths below, so every existing
- # provider is unaffected.
- provider_client = _provider_supplied_client(agent, client_kwargs)
- if provider_client is not None:
- _ra().logger.info(
- "%s client created from provider profile (%s, shared=%s) %s",
- agent.provider,
- reason,
- shared,
- agent._client_log_context(),
- )
- return provider_client
- if agent.provider == "gemini":
- from agent.gemini_native_adapter import GeminiNativeClient, is_native_gemini_base_url
-
- base_url = str(client_kwargs.get("base_url", "") or "")
- if is_native_gemini_base_url(base_url):
- safe_kwargs = {
- k: v for k, v in client_kwargs.items()
- if k in {"api_key", "base_url", "default_headers", "timeout", "http_client"}
- }
- if "http_client" not in safe_kwargs:
- keepalive_http = agent._build_keepalive_http_client(
- base_url, verify=httpx_verify,
- )
- if keepalive_http is not None:
- safe_kwargs["http_client"] = keepalive_http
- client = GeminiNativeClient(**safe_kwargs)
- _ra().logger.info(
- "Gemini native client created (%s, shared=%s) %s",
- reason,
- shared,
- agent._client_log_context(),
- )
- return client
- # TCP keepalives so dead provider connections are detected (~60s) instead of hanging in
- # CLOSE-WAIT (#10324). Injected into the local copy only (#10933), so each client gets its
- # own httpx.Client; pinned by tests/run_agent/test_create_openai_client_reuse.py and
- # tests/run_agent/test_sequential_chats_live.py.
- if "http_client" not in client_kwargs:
- keepalive_http = agent._build_keepalive_http_client(
- client_kwargs.get("base_url", ""), verify=httpx_verify,
- )
- if keepalive_http is not None:
- client_kwargs["http_client"] = keepalive_http
- # Retries belong to the outer conversation loop (honors Retry-After); SDK retries would
- # double-retry inside it (#26293). auxiliary_client keeps SDK retries as it isn't wrapped.
- client_kwargs.setdefault("max_retries", 0)
- # Defense-in-depth: primary_recovery/restore_primary rebuild from a _primary_runtime
- # snapshot without re-running header wiring; missing Copilot-Integration-Id causes
- # model_not_available_for_integrator 400s. Only ADD missing keys, never override.
+def _ensure_copilot_headers(client_kwargs: dict) -> None:
+ """Defense-in-depth: recovery/restore rebuild from a snapshot without re-running header
+ wiring; a missing Copilot-Integration-Id causes model_not_available_for_integrator 400s.
+ Only ADD missing keys, never override."""
try:
if base_url_host_matches(str(client_kwargs.get("base_url", "")), "githubcopilot.com"):
from hermes_cli.models import copilot_default_headers
@@ -1960,17 +1870,81 @@ def create_openai_client(agent, client_kwargs: dict, *, reason: str, shared: boo
client_kwargs["default_headers"] = existing
except Exception:
_ra().logger.debug("Copilot default-header guard skipped", exc_info=True)
+
+
+def _gemini_native_client(agent, client_kwargs: dict, httpx_verify, *, reason: str, shared: bool):
+ """Native Gemini client when the base_url is the Gemini API, else None."""
+ from agent.gemini_native_adapter import GeminiNativeClient, is_native_gemini_base_url
+
+ base_url = str(client_kwargs.get("base_url", "") or "")
+ if not is_native_gemini_base_url(base_url):
+ return None
+ safe_kwargs = {
+ k: v for k, v in client_kwargs.items()
+ if k in {"api_key", "base_url", "default_headers", "timeout", "http_client"}
+ }
+ if "http_client" not in safe_kwargs:
+ keepalive_http = agent._build_keepalive_http_client(base_url, verify=httpx_verify)
+ if keepalive_http is not None:
+ safe_kwargs["http_client"] = keepalive_http
+ client = GeminiNativeClient(**safe_kwargs)
+ _ra().logger.info(
+ "Gemini native client created (%s, shared=%s) %s", reason, shared, agent._client_log_context()
+ )
+ return client
+
+
+def create_openai_client(agent, client_kwargs: dict, *, reason: str, shared: bool) -> Any:
+ from agent.auxiliary_client import _validate_base_url, _validate_proxy_env_urls
+ from agent.ssl_verify import resolve_httpx_verify
+ # Treat client_kwargs as read-only: callers pass agent._client_kwargs, and in-place
+ # mutation leaks into later requests (a torn-down httpx transport got reused).
+ client_kwargs = dict(client_kwargs)
+ # The MoA virtual provider has no OpenAI wire endpoint; the facade *is* the client.
+ # Rebuild the facade, never a native client (TypeError; relay re-wire).
+ if (getattr(agent, "provider", "") or "").strip().lower() == "moa":
+ from agent.moa_loop import build_moa_facade
+ return build_moa_facade(agent, getattr(agent, "model", None) or "default")
+ ssl_ca_cert = client_kwargs.pop("ssl_ca_cert", None)
+ ssl_verify_cfg = client_kwargs.pop("ssl_verify", None)
+ httpx_verify = resolve_httpx_verify(ca_bundle=ssl_ca_cert, ssl_verify=ssl_verify_cfg)
+ _validate_proxy_env_urls()
+ _validate_base_url(client_kwargs.get("base_url"))
+ # Provider-supplied client (registration seam): a provider whose wire protocol is not
+ # OpenAI-over-HTTP supplies its own client from ProviderProfile.create_client(). Consulted
+ # before the built-in ladder so a profile registered from ~/.hermes/plugins/ or a pip entry
+ # point can ship a transport without editing this function (what makes an out-of-tree ACP
+ # provider possible). None (the default) falls through, so existing providers are unaffected.
+ provider_client = _provider_supplied_client(agent, client_kwargs)
+ if provider_client is not None:
+ _ra().logger.info(
+ "%s client created from provider profile (%s, shared=%s) %s",
+ agent.provider, reason, shared, agent._client_log_context(),
+ )
+ return provider_client
+ if agent.provider == "gemini":
+ client = _gemini_native_client(agent, client_kwargs, httpx_verify, reason=reason, shared=shared)
+ if client is not None:
+ return client
+ # TCP keepalives so dead provider connections are detected (~60s) instead of hanging in
+ # CLOSE-WAIT. Injected into the local copy only, so each client gets its own httpx.Client;
+ # pinned by tests/run_agent/test_create_openai_client_reuse.py and test_sequential_chats_live.py.
+ if "http_client" not in client_kwargs:
+ keepalive_http = agent._build_keepalive_http_client(client_kwargs.get("base_url", ""), verify=httpx_verify)
+ if keepalive_http is not None:
+ client_kwargs["http_client"] = keepalive_http
+ # Retries belong to the outer conversation loop (honors Retry-After); SDK retries would
+ # double-retry inside it. auxiliary_client keeps SDK retries as it isn't wrapped.
+ client_kwargs.setdefault("max_retries", 0)
+ _ensure_copilot_headers(client_kwargs)
# OpenCode Free is served anonymously: any unrecognized bearer is a 401, so an empty
# Authorization default_header overrides the SDK's "Bearer ".
if agent.provider == "opencode-free":
from hermes_cli.models import opencode_zen_free_headers
- _existing = dict(client_kwargs.get("default_headers") or {})
- _existing.update(opencode_zen_free_headers())
- client_kwargs["default_headers"] = _existing
-
- # All primary construction and recovery paths must identify Hermes to the
- # official Codex endpoint, including snapshots with custom header overrides.
+ client_kwargs["default_headers"] = {**(client_kwargs.get("default_headers") or {}), **opencode_zen_free_headers()}
+ # All primary construction and recovery paths must identify Hermes to the official Codex
+ # endpoint, including snapshots with custom header overrides.
from agent.codex_headers import apply_required_codex_headers
apply_required_codex_headers(
@@ -1980,12 +1954,7 @@ def create_openai_client(agent, client_kwargs: dict, *, reason: str, shared: boo
)
# Module-level `OpenAI` is resolved lazily via __getattr__; tests patch `run_agent.OpenAI`.
client = _ra().OpenAI(**client_kwargs)
- _ra().logger.info(
- "OpenAI client created (%s, shared=%s) %s",
- reason,
- shared,
- agent._client_log_context(),
- )
+ _ra().logger.info("OpenAI client created (%s, shared=%s) %s", reason, shared, agent._client_log_context())
return client
@@ -2443,6 +2412,23 @@ def switch_model(
_persist_switch_billing_route(agent)
+def _pre_tool_block_message(agent, function_name, function_args, effective_task_id, tool_call_id, middleware_trace):
+ """Plugin pre-tool-call hook verdict: ``(block_message, function_args)``; failures never block."""
+ try:
+ from hermes_cli.plugins import _dispatch_pre_tool_call_hooks
+ block_message, modified_args = _dispatch_pre_tool_call_hooks(
+ function_name, function_args, task_id=effective_task_id or "",
+ session_id=getattr(agent, "session_id", "") or "",
+ tool_call_id=tool_call_id or "",
+ turn_id=getattr(agent, "_current_turn_id", "") or "",
+ api_request_id=getattr(agent, "_current_api_request_id", "") or "",
+ middleware_trace=list(middleware_trace),
+ )
+ return block_message, (modified_args if modified_args is not None else function_args)
+ except Exception:
+ return None, function_args
+
+
def invoke_tool(agent, function_name: str, function_args: dict, effective_task_id: str,
tool_call_id: Optional[str] = None, messages: list = None,
pre_tool_block_checked: bool = False,
@@ -2463,39 +2449,23 @@ def invoke_tool(agent, function_name: str, function_args: dict, effective_task_i
if not isinstance(function_args, dict):
function_args = {}
-
+ hook_ids = tool_hook_ids(agent, effective_task_id, tool_call_id)
_tool_middleware_trace = list(tool_request_middleware_trace or [])
try:
from hermes_cli.middleware import apply_tool_request_middleware
if not skip_tool_request_middleware:
- _tool_request_mw = apply_tool_request_middleware(
- function_name,
- function_args,
- **tool_hook_ids(agent, effective_task_id, tool_call_id),
- )
+ _tool_request_mw = apply_tool_request_middleware(function_name, function_args, **hook_ids)
function_args = _tool_request_mw.payload
_tool_middleware_trace = _tool_request_mw.trace
except Exception as _mw_err:
logger.debug("tool_request middleware error: %s", _mw_err)
- # Check plugin hooks for a block or approval directive before executing.
block_message: Optional[str] = None
if not pre_tool_block_checked:
- try:
- from hermes_cli.plugins import _dispatch_pre_tool_call_hooks
- block_message, modified_args = _dispatch_pre_tool_call_hooks(
- function_name, function_args, task_id=effective_task_id or "",
- session_id=getattr(agent, "session_id", "") or "",
- tool_call_id=tool_call_id or "",
- turn_id=getattr(agent, "_current_turn_id", "") or "",
- api_request_id=getattr(agent, "_current_api_request_id", "") or "",
- middleware_trace=list(_tool_middleware_trace),
- )
- if modified_args is not None:
- function_args = modified_args
- except Exception:
- block_message = None
+ block_message, function_args = _pre_tool_block_message(
+ agent, function_name, function_args, effective_task_id, tool_call_id, _tool_middleware_trace
+ )
if block_message is not None:
result = json.dumps({"error": block_message}, ensure_ascii=False)
emit_terminal_post_tool_call(
@@ -2513,30 +2483,25 @@ def invoke_tool(agent, function_name: str, function_args: dict, effective_task_i
return result
tool_start_time = time.monotonic()
-
- def _finish_agent_tool(result: Any, observed_args: Optional[dict] = None) -> Any:
- emit_terminal_post_tool_call(
- agent,
- function_name=function_name,
- function_args=observed_args if isinstance(observed_args, dict) else function_args,
- result=result,
- effective_task_id=effective_task_id,
- tool_call_id=tool_call_id,
- duration_ms=int((time.monotonic() - tool_start_time) * 1000),
- middleware_trace=_tool_middleware_trace,
- )
- return result
-
inline_executor = resolve_invoke_tool_executor(agent, function_name)
if inline_executor is not None:
inline_ctx = InlineToolContext(
- effective_task_id=effective_task_id,
- tool_call_id=tool_call_id,
- messages=messages,
+ effective_task_id=effective_task_id, tool_call_id=tool_call_id, messages=messages
)
def _execute(next_args: dict) -> Any:
- return _finish_agent_tool(inline_executor(agent, next_args, inline_ctx), next_args)
+ result = inline_executor(agent, next_args, inline_ctx)
+ emit_terminal_post_tool_call(
+ agent,
+ function_name=function_name,
+ function_args=next_args if isinstance(next_args, dict) else function_args,
+ result=result,
+ effective_task_id=effective_task_id,
+ tool_call_id=tool_call_id,
+ duration_ms=int((time.monotonic() - tool_start_time) * 1000),
+ middleware_trace=_tool_middleware_trace,
+ )
+ return result
else:
def _execute(next_args: dict) -> Any:
dispatch_kwargs = dict(
@@ -2553,16 +2518,10 @@ def invoke_tool(agent, function_name: str, function_args: dict, effective_task_i
)
if skip_tool_execution_middleware:
dispatch_kwargs["skip_tool_execution_middleware"] = True
- return _ra().handle_function_call(
- function_name,
- next_args,
- effective_task_id,
- **dispatch_kwargs,
- )
+ return _ra().handle_function_call(function_name, next_args, effective_task_id, **dispatch_kwargs)
if skip_tool_execution_middleware:
return _execute(function_args)
-
from hermes_cli.middleware import run_tool_execution_middleware
return run_tool_execution_middleware(
@@ -2570,7 +2529,7 @@ def invoke_tool(agent, function_name: str, function_args: dict, effective_task_i
function_args,
lambda next_args: _execute(next_args if isinstance(next_args, dict) else function_args),
original_args=function_args,
- **tool_hook_ids(agent, effective_task_id, tool_call_id),
+ **hook_ids,
)
@@ -2578,19 +2537,16 @@ def invoke_tool(agent, function_name: str, function_args: dict, effective_task_i
def repair_tool_call(agent, tool_name: str) -> str | None:
"""Repair a mismatched tool name (case, separators, CamelCase, ``_tool`` suffixes, then fuzzy match) before aborting.
- Suffix stripping is applied twice so ``TodoTool_tool`` reduces fully (#14784).
+ Suffix stripping is applied twice so ``TodoTool_tool`` reduces fully.
Returns the repaired name if in valid_tool_names, else None.
"""
- import re
from difflib import get_close_matches
if not tool_name:
return None
-
- # VolcEngine api/plan (#33007) leaks XML attribute fragments into tool_use.name
+ # VolcEngine api/plan leaks XML attribute fragments into tool_use.name
# (`terminal" parameter="command" ...`); trim at the first quote/angle bracket.
- # Do NOT split on whitespace: "write file" must reach ``_norm`` -> ``write_file``
- # (test_space_to_underscore in tests/run_agent/test_repair_tool_call_name.py).
+ # Do NOT split on whitespace: "write file" must reach ``_norm`` -> ``write_file``.
for _xml_sep in ('"', "'", "<", ">"):
_idx = tool_name.find(_xml_sep)
if _idx > 0:
@@ -2618,28 +2574,19 @@ def repair_tool_call(agent, tool_name: str) -> str | None:
normalized = _norm(tool_name)
if normalized in agent.valid_tool_names:
return normalized
-
cands: set[str] = {tool_name, lowered, normalized, _camel_snake(tool_name)}
- # Strip trailing tool-suffix up to twice (TodoTool_tool needs it).
- for _ in range(2):
+ for _ in range(2): # strip trailing tool-suffix up to twice (TodoTool_tool needs it)
extra: set[str] = set()
for c in cands:
stripped = _strip_tool_suffix(c)
if stripped:
- extra.add(stripped)
- extra.add(_norm(stripped))
- extra.add(_camel_snake(stripped))
+ extra.update((stripped, _norm(stripped), _camel_snake(stripped)))
cands |= extra
-
for c in cands:
if c and c in agent.valid_tool_names:
return c
-
matches = get_close_matches(lowered, agent.valid_tool_names, n=1, cutoff=0.7)
- if matches:
- return matches[0]
-
- return None
+ return matches[0] if matches else None
# Placeholder for an empty non-final message the provider would reject. Kept identical to
@@ -3192,6 +3139,17 @@ def sanitize_api_messages(messages: List[Dict[str, Any]]) -> List[Dict[str, Any]
+_ACK_FUTURE_RE = re.compile(r"\b(i['’]ll|i will|let me|i can do that|i can help with that)\b")
+_ACK_ACTION_MARKERS = (
+ "look into", "look at", "inspect", "scan", "check", "analyz", "review", "explore", "read", "open",
+ "run", "test", "fix", "debug", "search", "find", "walkthrough", "report back", "summarize",
+)
+_ACK_WORKSPACE_MARKERS = (
+ "directory", "current directory", "current dir", "cwd", "repo", "repository", "codebase",
+ "project", "folder", "filesystem", "file tree", "files", "path",
+)
+
+
def looks_like_codex_intermediate_ack(
agent,
user_message: Any,
@@ -3201,84 +3159,33 @@ def looks_like_codex_intermediate_ack(
) -> bool:
"""Detect a planning/ack message that should continue instead of ending the turn.
- ``require_workspace=False`` (user opted into ``agent.intent_ack_continuation``
- for all api_modes) drops the filesystem/repo reference requirement; the
- future-ack + short-content + no-prior-tools + action-verb checks always apply.
+ ``require_workspace=False`` (user opted into ``agent.intent_ack_continuation`` for all
+ api_modes) drops the filesystem/repo reference requirement; the future-ack +
+ short-content + no-prior-tools + action-verb checks always apply.
"""
if any(isinstance(msg, dict) and msg.get("role") == "tool" for msg in messages):
return False
-
assistant_text = agent._strip_think_blocks(assistant_content or "").strip().lower()
- if not assistant_text:
+ if not assistant_text or len(assistant_text) > 1200:
return False
- if len(assistant_text) > 1200:
+ if not _ACK_FUTURE_RE.search(assistant_text):
return False
-
- has_future_ack = bool(
- re.search(r"\b(i['’]ll|i will|let me|i can do that|i can help with that)\b", assistant_text)
- )
- if not has_future_ack:
+ if not any(marker in assistant_text for marker in _ACK_ACTION_MARKERS):
return False
-
- action_markers = (
- "look into",
- "look at",
- "inspect",
- "scan",
- "check",
- "analyz",
- "review",
- "explore",
- "read",
- "open",
- "run",
- "test",
- "fix",
- "debug",
- "search",
- "find",
- "walkthrough",
- "report back",
- "summarize",
- )
- workspace_markers = (
- "directory",
- "current directory",
- "current dir",
- "cwd",
- "repo",
- "repository",
- "codebase",
- "project",
- "folder",
- "filesystem",
- "file tree",
- "files",
- "path",
- )
-
- assistant_mentions_action = any(marker in assistant_text for marker in action_markers)
- if not assistant_mentions_action:
- return False
-
# Opted-in (all-api_mode) path: future-ack + action verb + no prior tool call suffices.
if not require_workspace:
return True
-
# ``user_message`` may be a multi-part content list (vision via the OpenAI-compat
# server); a list survives ``or ""`` and ``.strip()`` raises, so flatten first.
from agent.codex_responses_adapter import _summarize_user_message_for_log
user_text = _summarize_user_message_for_log(user_message).strip().lower()
- user_targets_workspace = (
- any(marker in user_text for marker in workspace_markers)
+ return (
+ any(marker in user_text for marker in _ACK_WORKSPACE_MARKERS)
or "~/" in user_text
or "/" in user_text
+ or any(marker in assistant_text for marker in _ACK_WORKSPACE_MARKERS)
)
- assistant_targets_workspace = any(
- marker in assistant_text for marker in workspace_markers
- )
- return user_targets_workspace or assistant_targets_workspace
# Narrow "trailing continue-intent" detector for the stall guard (agent.stall_guards):
@@ -3302,23 +3209,25 @@ def trailing_continue_intent(text: str) -> bool:
return bool(_TRAILING_CONTINUE_INTENT_RE.search(t[-160:]))
+_INTENT_ACK_ON = {"true", "always", "yes", "on"}
+_INTENT_ACK_OFF = {"false", "never", "no", "off"}
+
+
def intent_ack_continuation_mode(agent) -> str:
"""Resolve the intent-ack continuation mode: ``"off"``, ``"codex_only"`` (workspace acks on codex_responses), or ``"all"``.
- Mirrors ``agent.tool_use_enforcement``: ``"auto"`` -> codex_only; true-ish
- values -> all; false-ish -> off; ``list`` -> all when a substring matches
- the active model name, else off.
+ Mirrors ``agent.tool_use_enforcement``: ``"auto"`` -> codex_only; true-ish values -> all;
+ false-ish -> off; ``list`` -> all when a substring matches the active model name, else off.
"""
mode = getattr(agent, "_intent_ack_continuation", "auto")
-
- if mode is True or (isinstance(mode, str) and mode.lower() in {"true", "always", "yes", "on"}):
+ if mode is True or (isinstance(mode, str) and mode.lower() in _INTENT_ACK_ON):
return "all"
- if mode is False or (isinstance(mode, str) and mode.lower() in {"false", "never", "no", "off"}):
+ if mode is False or (isinstance(mode, str) and mode.lower() in _INTENT_ACK_OFF):
return "off"
if isinstance(mode, list):
model_lower = (agent.model or "").lower()
return "all" if any(p.lower() in model_lower for p in mode if isinstance(p, str)) else "off"
- # "auto" or any unrecognised value — historical codex-only behavior.
+ # "auto" or any unrecognised value: historical codex-only behavior.
return "codex_only" if agent.api_mode == "codex_responses" else "off"
@@ -3349,31 +3258,22 @@ def reapply_reasoning_echo_for_provider(agent, api_messages: list) -> int:
def _iter_httpx_pool_objects(http_client: Any):
"""Yield httpcore pool objects reachable from an httpx client, including mounted transports.
- Keepalive (#10324) and proxy configs put live connections on ``client._mounts``;
- walking only ``_transport`` made ``force_close_tcp_sockets`` miss them (#72975).
+ Keepalive and proxy configs put live connections on ``client._mounts``; walking
+ only ``_transport`` made ``force_close_tcp_sockets`` miss them.
"""
seen_pools: set[int] = set()
- def _emit(pool: Any):
- if pool is None:
- return
- marker = id(pool)
- if marker in seen_pools:
- return
- seen_pools.add(marker)
- yield pool
-
def _pools_for_transport(transport: Any):
if transport is None:
return
# Connections live under ``_pool``; a directly mounted HTTPProxy *is* a
# ConnectionPool, so ``_connections`` may sit on the transport itself.
pool = getattr(transport, "_pool", None)
- if pool is not None:
- yield from _emit(pool)
- return
- if getattr(transport, "_connections", None) is not None:
- yield from _emit(transport)
+ if pool is None and getattr(transport, "_connections", None) is not None:
+ pool = transport
+ if pool is not None and id(pool) not in seen_pools:
+ seen_pools.add(id(pool))
+ yield pool
try:
yield from _pools_for_transport(getattr(http_client, "_transport", None))
@@ -3465,35 +3365,32 @@ def _iter_pool_sockets(client: Any):
yield sock
+def _socket_is_dead(sock) -> bool:
+ """Probe socket health with a non-blocking recv peek."""
+ import socket as _socket
+ try:
+ sock.setblocking(False)
+ return sock.recv(1, _socket.MSG_PEEK | _socket.MSG_DONTWAIT) == b""
+ except BlockingIOError:
+ return False # no data available: socket is healthy
+ except OSError:
+ return True
+ finally:
+ try:
+ sock.setblocking(True)
+ except OSError:
+ pass
+
+
def cleanup_dead_connections(agent) -> bool:
"""Force-close and rebuild the primary client if its pool has dead sockets (CLOSE-WAIT, errors); returns True if cleaned."""
client = getattr(agent, "client", None)
if client is None:
return False
try:
- dead_count = 0
- for sock in _iter_pool_sockets(client):
- # Probe socket health with a non-blocking recv peek
- import socket as _socket
- try:
- sock.setblocking(False)
- data = sock.recv(1, _socket.MSG_PEEK | _socket.MSG_DONTWAIT)
- if data == b"":
- dead_count += 1
- except BlockingIOError:
- pass # No data available — socket is healthy
- except OSError:
- dead_count += 1
- finally:
- try:
- sock.setblocking(True)
- except OSError:
- pass
+ dead_count = sum(1 for sock in _iter_pool_sockets(client) if _socket_is_dead(sock))
if dead_count > 0:
- _ra().logger.warning(
- "Found %d dead connection(s) in client pool — rebuilding client",
- dead_count,
- )
+ _ra().logger.warning("Found %d dead connection(s) in client pool — rebuilding client", dead_count)
agent._replace_primary_openai_client(reason="dead_connection_cleanup")
return True
except Exception as exc:
@@ -3578,6 +3475,18 @@ def extract_api_error_context(error: Exception) -> Dict[str, Any]:
+def _requeue_pending_steer(agent, steer_text: str) -> None:
+ """Put drained steer text back so the caller's fallback delivers it as a next-turn user message."""
+ lock = getattr(agent, "_pending_steer_lock", None)
+ if lock is not None:
+ with lock:
+ existing = agent._pending_steer
+ agent._pending_steer = (existing + "\n" + steer_text) if existing else steer_text
+ return
+ existing = getattr(agent, "_pending_steer", None)
+ agent._pending_steer = (existing + "\n" + steer_text) if existing else steer_text
+
+
def apply_pending_steer_to_tool_results(agent, messages: list, num_tool_msgs: int) -> None:
"""Append pending /steer text to the last ``role:"tool"`` message of this batch, marked as user-origin.
@@ -3590,29 +3499,22 @@ def apply_pending_steer_to_tool_results(agent, messages: list, num_tool_msgs: in
if not steer_text:
return
# Skip non-tool messages in the tail in case something else is appended at the boundary.
- target_idx = None
- for j in range(len(messages) - 1, max(len(messages) - num_tool_msgs - 1, -1), -1):
- msg = messages[j]
- if isinstance(msg, dict) and msg.get("role") == "tool":
- target_idx = j
- break
+ target_idx = next(
+ (
+ j for j in range(len(messages) - 1, max(len(messages) - num_tool_msgs - 1, -1), -1)
+ if isinstance(messages[j], dict) and messages[j].get("role") == "tool"
+ ),
+ None,
+ )
if target_idx is None:
- # No tool result in this batch (e.g. all skipped by interrupt): put the steer
- # back so the caller's fallback delivers it as a next-turn user message.
- _lock = getattr(agent, "_pending_steer_lock", None)
- if _lock is not None:
- with _lock:
- if agent._pending_steer:
- agent._pending_steer = agent._pending_steer + "\n" + steer_text
- else:
- agent._pending_steer = steer_text
- else:
- existing = getattr(agent, "_pending_steer", None)
- agent._pending_steer = (existing + "\n" + steer_text) if existing else steer_text
+ # No tool result in this batch (e.g. all skipped by interrupt).
+ _requeue_pending_steer(agent, steer_text)
return
marker = format_steer_marker(steer_text)
existing_content = messages[target_idx].get("content", "")
- if not isinstance(existing_content, str):
+ if isinstance(existing_content, str):
+ messages[target_idx]["content"] = existing_content + marker
+ else:
# Anthropic multimodal content blocks: preserve them and append a text block.
try:
blocks = list(existing_content) if existing_content else []
@@ -3621,8 +3523,6 @@ def apply_pending_steer_to_tool_results(agent, messages: list, num_tool_msgs: in
except Exception:
# Fall back to string replacement if content shape is unexpected.
messages[target_idx]["content"] = f"{existing_content}{marker}"
- else:
- messages[target_idx]["content"] = existing_content + marker
_ra().logger.info(
"Delivered /steer to agent after tool batch (%d chars): %s",
len(steer_text),
diff --git a/agent/async_utils.py b/agent/async_utils.py
index 07442b63c5..15c288f34d 100644
--- a/agent/async_utils.py
+++ b/agent/async_utils.py
@@ -1,24 +1,10 @@
"""Async/sync bridging helpers.
-The codebase has ~30 sites that schedule a coroutine onto an event loop from a
-worker thread via :func:`asyncio.run_coroutine_threadsafe`. That function can
-raise :class:`RuntimeError` (e.g. the loop was closed during a shutdown race),
-and when it does the coroutine object is never awaited and never closed —
-which triggers a ``"coroutine '' was never awaited"`` RuntimeWarning and
-leaks the coroutine's frame until GC.
-
-:func:`safe_schedule_threadsafe` wraps the call, closes the coroutine on
-scheduling failure, and returns ``None`` (instead of a half-formed future) so
-callers can branch cleanly:
-
- fut = safe_schedule_threadsafe(coro, loop)
- if fut is None:
- return # or fallback behavior
- fut.result(timeout=5)
-
-The helper deliberately does NOT also handle ``future.result()`` failures —
-that is a separate concern. Once the loop has accepted the coroutine, its
-lifecycle belongs to the loop, not the scheduling thread.
+``asyncio.run_coroutine_threadsafe`` can raise ``RuntimeError`` (loop closed during a
+shutdown race); the coroutine is then never awaited or closed, which triggers a
+"coroutine was never awaited" RuntimeWarning and leaks its frame. The helpers here
+close the coroutine on scheduling failure. ``future.result()`` failures are deliberately
+NOT handled: once the loop accepts the coroutine its lifecycle belongs to the loop.
"""
from __future__ import annotations
@@ -41,25 +27,14 @@ def safe_schedule_threadsafe(
) -> Optional[Future]:
"""Schedule ``coro`` on ``loop`` from a sync context, leak-safe.
- Returns the :class:`concurrent.futures.Future` on success, or ``None`` if
- the loop is missing or :func:`asyncio.run_coroutine_threadsafe` raised
- (e.g. the loop was closed during a shutdown race). In all failure paths
- the coroutine is :meth:`close`-d so it does not trigger
- ``"coroutine was never awaited"`` warnings or leak its frame.
-
- Callers retain full control over what to do with the returned future
- (call ``.result(timeout=...)``, attach ``add_done_callback``, ignore it
- fire-and-forget, etc.).
+ Returns the Future on success, or ``None`` if the loop is missing or scheduling
+ raised; in every failure path the coroutine is closed. Callers keep full control
+ over the returned future (``.result(timeout=...)``, callbacks, fire-and-forget).
"""
log = logger if logger is not None else _DEFAULT_LOGGER
-
- if loop is None:
- if asyncio.iscoroutine(coro):
- coro.close()
- log.log(log_level, "%s: loop is None", log_message)
- return None
-
try:
+ if loop is None:
+ raise RuntimeError("loop is None")
return asyncio.run_coroutine_threadsafe(coro, loop)
except Exception as exc:
if asyncio.iscoroutine(coro):
@@ -69,15 +44,9 @@ def safe_schedule_threadsafe(
def consume_detached_task_result(task: "asyncio.Future[Any]") -> None:
- """Retrieve a detached task's result without surfacing cancellation.
-
- Used as an ``add_done_callback`` on tasks that were cancelled and
- detached (e.g. an adapter close path that swallows ``CancelledError``
- past its teardown deadline). Observing ``task.exception()`` prevents
- "exception was never retrieved" noise on the event loop; cancellation
- and any terminal error are deliberately swallowed — the task's owner
- already gave up on it.
- """
+ """``add_done_callback`` for cancelled-and-detached tasks: observe the exception so the
+ loop does not log "exception was never retrieved"; cancellation and terminal errors
+ are swallowed because the task's owner already gave up on it."""
try:
task.exception()
except (asyncio.CancelledError, Exception):
diff --git a/agent/aux_accounting.py b/agent/aux_accounting.py
index a6994d901c..ee9d15bff2 100644
--- a/agent/aux_accounting.py
+++ b/agent/aux_accounting.py
@@ -1,27 +1,11 @@
"""Ambient session-accounting context for auxiliary LLM calls.
-Auxiliary calls (vision, compression, title generation, web_extract,
-session_search, ...) funnel through ``agent.auxiliary_client`` which has no
-session handle — so their token usage was historically discarded, leaving
-dashboard analytics blind to aux model spend (issue #23270).
-
-Instead of threading ``session_db``/``session_id`` parameters through every
-aux call site, the agent loop publishes them here (mirroring the Nous Portal
-conversation context in ``agent.portal_tags``) and the auxiliary client
-records usage at its single response-validation chokepoint.
-
-ContextVar semantics give us the right isolation for free:
-
-* concurrent agents in one process (gateway sessions, delegate subagents)
- never see each other's accounting context;
-* worker threads spawned via ``tools.thread_context.propagate_context_to_thread``
- (MoA fan-out, background review) inherit the parent turn's context;
-* asyncio tasks inherit the context of the code that created them.
-
-MoA reference/aggregator slots are explicitly EXCLUDED from recording:
-``agent/conversation_loop.py`` already folds MoA advisor usage and cost into
-the main loop's ``update_token_counts`` delta, so recording them here would
-double-count (see ``_EXCLUDED_TASKS``).
+Aux calls (vision, compression, title generation, web_extract, session_search, ...) go
+through ``agent.auxiliary_client`` which has no session handle, so their usage was
+historically discarded. The agent loop publishes ``(session_db, session_id)`` here
+(mirroring ``agent.portal_tags``) and the aux client records usage at its single
+response-validation chokepoint. ContextVar semantics isolate concurrent agents, propagate
+to worker threads via ``tools.thread_context`` and to asyncio tasks automatically.
"""
from __future__ import annotations
@@ -33,22 +17,17 @@ from typing import Any, Optional
logger = logging.getLogger(__name__)
# (session_db, session_id) for the active agent turn, or None outside one.
-_accounting: ContextVar[Optional[tuple]] = ContextVar(
- "aux_accounting_context", default=None
-)
+_accounting: ContextVar[Optional[tuple]] = ContextVar("aux_accounting_context", default=None)
-# Aux tasks whose usage is already accounted by the main loop — recording
-# them here would double-count. MoA advisor/aggregator usage is folded into
-# conversation_loop's update_token_counts delta (tokens AND cost).
+# MoA advisor/aggregator usage is already folded into conversation_loop's
+# update_token_counts delta (tokens AND cost); recording it here would double-count.
_EXCLUDED_TASKS = frozenset({"moa_reference", "moa_aggregator"})
def set_accounting_context(session_db: Any, session_id: Optional[str]):
- """Publish the active session's accounting handles for aux usage recording.
+ """Publish the active session's accounting handles; returns the token for ``reset_accounting_context``.
- Called by the agent loop at turn entry. Returns the ContextVar token so
- callers can ``reset_accounting_context(token)`` on turn exit. Publishing
- ``None`` handles (no DB / no session id) clears the context.
+ ``None`` handles (no DB / no session id) clear the context.
"""
if session_db is None or not session_id:
return _accounting.set(None)
@@ -72,17 +51,10 @@ def record_aux_usage(
) -> None:
"""Record an auxiliary response's token usage against the ambient session.
- Called from the auxiliary client's response-validation chokepoint. Strictly
- best-effort: any failure is swallowed (accounting must never break an aux
- call). No-ops when:
-
- * no accounting context is published (call is outside any agent turn),
- * the task is main-loop-accounted (MoA slots — see ``_EXCLUDED_TASKS``),
- * the response carries no usage object.
-
- The model is read from ``response.model`` (accurate even after the aux
- client's provider-fallback chains); *provider*/*base_url* reflect the
- originally-resolved route and are best-effort.
+ Strictly best-effort (accounting must never break an aux call). No-ops outside an
+ agent turn, for main-loop-accounted tasks (``_EXCLUDED_TASKS``), or without usage.
+ The model is read from ``response.model`` (accurate after aux provider fallback);
+ *provider*/*base_url* reflect the originally-resolved route.
"""
try:
if not task or task in _EXCLUDED_TASKS:
@@ -104,18 +76,14 @@ def record_aux_usage(
or usage.reasoning_tokens
):
return
-
model = str(getattr(response, "model", "") or "") or "unknown"
estimated_cost = None
try:
- cost = estimate_usage_cost(
- model, usage, provider=provider, base_url=base_url
- )
+ cost = estimate_usage_cost(model, usage, provider=provider, base_url=base_url)
if cost.amount_usd is not None:
estimated_cost = float(cost.amount_usd)
except Exception:
logger.debug("Aux usage cost estimation failed", exc_info=True)
-
session_db.record_auxiliary_usage(
session_id,
task,
From bea9bc1ab9a7b35ad33cbf3fddfde0143d39daa6 Mon Sep 17 00:00:00 2001
From: Teknium <127238744+teknium1@users.noreply.github.com>
Date: Wed, 2 Sep 2026 19:12:43 -0700
Subject: [PATCH 10/20] refactor(agent/agent_runtime_helpers): AST-neutral
layout pass (hug/pack bracket spans, blank runs)
---
agent/agent_runtime_helpers.py | 240 ++++++++-------------------------
1 file changed, 56 insertions(+), 184 deletions(-)
diff --git a/agent/agent_runtime_helpers.py b/agent/agent_runtime_helpers.py
index bfbdbc0136..20ce8367de 100644
--- a/agent/agent_runtime_helpers.py
+++ b/agent/agent_runtime_helpers.py
@@ -18,18 +18,13 @@ from typing import Any, Dict, List, Optional, Tuple
from hermes_cli.timeouts import get_provider_request_timeout
from agent.message_sanitization import (
- _FULL_ARGS_LOG_BOUND,
- coalesce_tool_call_id,
- tool_call_id_variants,
- tool_result_id_variants,
+ _FULL_ARGS_LOG_BOUND, coalesce_tool_call_id, tool_call_id_variants, tool_result_id_variants
)
from agent.prompt_builder import format_steer_marker
from agent.tool_dispatch_helpers import _trajectory_normalize_msg, make_tool_result_message
from agent.trajectory import convert_scratchpad_to_think
from agent.credential_pool import (
- STATUS_EXHAUSTED,
- credential_pool_matches_provider,
- resolve_runtime_pool_key,
+ STATUS_EXHAUSTED, credential_pool_matches_provider, resolve_runtime_pool_key
)
from agent.error_classifier import FailoverReason
from agent.turn_context import drop_stale_api_content
@@ -47,8 +42,7 @@ _REASONING_TAG_NAMES = ("think", "thinking", "reasoning", "REASONING_SCRATCHPAD"
_TOOL_CALL_TAG_NAMES = ("tool_call", "tool_calls", "tool_result", "function_call", "function_calls")
_REASONING_BLOCK_PATTERNS = tuple(
- re.compile(rf"<{name}>.*?{name}>", re.DOTALL | re.IGNORECASE)
- for name in _REASONING_TAG_NAMES
+ re.compile(rf"<{name}>.*?{name}>", re.DOTALL | re.IGNORECASE) for name in _REASONING_TAG_NAMES
)
_TOOL_CALL_BLOCK_PATTERNS = tuple(
@@ -66,18 +60,15 @@ _NAMED_FUNCTION_BLOCK_PATTERN = re.compile(
)
_UNTERMINATED_REASONING_BLOCK_PATTERN = re.compile(
- rf'(?:^|\n)[ \t]*<(?:{"|".join(_REASONING_TAG_NAMES)})\b[^>]*>.*$',
- re.DOTALL | re.IGNORECASE,
+ rf'(?:^|\n)[ \t]*<(?:{"|".join(_REASONING_TAG_NAMES)})\b[^>]*>.*$', re.DOTALL | re.IGNORECASE
)
_ORPHAN_REASONING_TAG_PATTERN = re.compile(
- rf'?(?:{"|".join(_REASONING_TAG_NAMES)})>\s*',
- re.IGNORECASE,
+ rf'?(?:{"|".join(_REASONING_TAG_NAMES)})>\s*', re.IGNORECASE
)
_STRAY_TOOL_CALL_CLOSER_PATTERN = re.compile(
- rf'(?:{"|".join(_TOOL_CALL_TAG_NAMES)}|function)>\s*',
- re.IGNORECASE,
+ rf'(?:{"|".join(_TOOL_CALL_TAG_NAMES)}|function)>\s*', re.IGNORECASE
)
@@ -195,7 +186,6 @@ def convert_to_trajectory_format(agent, messages: List[Dict[str, Any]], user_que
return trajectory
-
def _prepend_corruption_marker(tool_msg: dict, marker: str) -> None:
existing = tool_msg.get("content")
if isinstance(existing, str):
@@ -237,11 +227,7 @@ def _cursor_skip_prefix(messages: list, cursor: Optional[dict]) -> int:
def sanitize_tool_call_arguments(
- messages: list,
- *,
- logger=None,
- session_id: str = None,
- cursor: Optional[dict] = None,
+ messages: list, *, logger=None, session_id: str = None, cursor: Optional[dict] = None
) -> int:
"""Repair corrupted assistant tool-call argument JSON in-place.
@@ -586,9 +572,7 @@ def _merge_consecutive_users(messages: List[Dict]) -> Tuple[List[Dict], int]:
_SEQUENCE_REPAIR_PASSES = (
- _merge_consecutive_assistants,
- _drop_stray_tool_results,
- _prune_unanswered_tool_calls,
+ _merge_consecutive_assistants, _drop_stray_tool_results, _prune_unanswered_tool_calls,
_merge_consecutive_users,
)
@@ -644,7 +628,6 @@ def repair_message_sequence_with_cursor(agent, messages: List[Dict]) -> int:
return repairs
-
def _flatten_content_text(content: Any) -> str:
"""Flatten list/dict content (e.g. Anthropic-via-OpenRouter block lists) to text.
@@ -676,11 +659,8 @@ def _flatten_content_text(content: Any) -> str:
# reasoning tags, and finally stray tool-call CLOSERS only (bare/unterminated
# is kept: a truncated streaming tail may still be valuable, matching OpenClaw's asymmetry).
_THINK_STRIP_PATTERNS = (
- *_REASONING_BLOCK_PATTERNS,
- *_TOOL_CALL_BLOCK_PATTERNS,
- _NAMED_FUNCTION_BLOCK_PATTERN,
- _UNTERMINATED_REASONING_BLOCK_PATTERN,
- _ORPHAN_REASONING_TAG_PATTERN,
+ *_REASONING_BLOCK_PATTERNS, *_TOOL_CALL_BLOCK_PATTERNS, _NAMED_FUNCTION_BLOCK_PATTERN,
+ _UNTERMINATED_REASONING_BLOCK_PATTERN, _ORPHAN_REASONING_TAG_PATTERN,
_STRAY_TOOL_CALL_CLOSER_PATTERN,
)
@@ -704,7 +684,6 @@ def strip_think_blocks(agent, content: str) -> str:
return content
-
def sync_credential_pool_entry_id(agent) -> None:
"""Rebind ``agent._credential_pool_entry_id`` from the current pool + key.
@@ -714,18 +693,14 @@ def sync_credential_pool_entry_id(agent) -> None:
pool = getattr(agent, "_credential_pool", None)
try:
agent._credential_pool_entry_id = (
- pool.entry_id_for_api_key(getattr(agent, "api_key", None))
- if pool is not None
- else None
+ pool.entry_id_for_api_key(getattr(agent, "api_key", None)) if pool is not None else None
)
except Exception:
agent._credential_pool_entry_id = None
_STATUS_TO_FAILOVER_REASON = {
- 402: FailoverReason.billing,
- 429: FailoverReason.rate_limit,
- 401: FailoverReason.auth,
+ 402: FailoverReason.billing, 429: FailoverReason.rate_limit, 401: FailoverReason.auth,
403: FailoverReason.auth,
}
@@ -858,13 +833,9 @@ 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,
+ 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.
@@ -962,7 +933,6 @@ def recover_with_credential_pool(
return False, has_retried_429
-
def _apply_primary_runtime_fields(agent, rt: Dict[str, Any]) -> None:
"""Copy the identity/transport fields of a ``_primary_runtime`` snapshot onto ``agent``.
@@ -1066,7 +1036,6 @@ def try_recover_primary_transport(
# ── End provider fallback ──────────────────────────────────────────────
-
def _merge_user_content(prev_content: Any, cur_content: Any) -> Any:
"""Merged content for two adjacent user messages; ``_UNMERGEABLE`` for unknown shapes.
@@ -1087,9 +1056,7 @@ _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
) -> List[Dict[str, Any]]:
"""Drop thinking-only assistant turns and merge adjacent user messages left behind.
@@ -1129,7 +1096,6 @@ def drop_thinking_only_and_merge_users(
return merged
-
def _primary_reset_gate_blocks(agent, rt, primary_provider, primary_runtime_base_url, matches_primary, load_primary_pool):
"""Reset-aware gate: skip a guaranteed-to-fail restore while the primary pool reports a future reset.
@@ -1280,12 +1246,9 @@ def restore_primary_runtime(agent) -> bool:
agent._use_native_cache_layout = False
_rebuild_primary_client(agent, rt, reason="restore_primary")
agent.context_compressor.update_model(
- model=rt["compressor_model"],
- context_length=rt["compressor_context_length"],
- base_url=rt["compressor_base_url"],
- api_key=rt["compressor_api_key"],
- provider=rt["compressor_provider"],
- api_mode=rt.get("compressor_api_mode", ""),
+ model=rt["compressor_model"], context_length=rt["compressor_context_length"],
+ base_url=rt["compressor_base_url"], api_key=rt["compressor_api_key"],
+ provider=rt["compressor_provider"], api_mode=rt.get("compressor_api_mode", ""),
)
_rebind_primary_credential_pool(
agent, primary_provider, _matches_primary, _load_primary_pool, prefetched_pool, prefetched
@@ -1329,7 +1292,6 @@ _TRANSIENT_TRANSPORT_ERRORS = frozenset({
})
-
_INLINE_REASONING_PATTERNS = tuple(
re.compile(rf"<{tag}>(.*?){tag}>", re.DOTALL | re.IGNORECASE)
for tag in ("think", "thinking", "thought", "reasoning", "REASONING_SCRATCHPAD")
@@ -1369,7 +1331,6 @@ 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)}
for attr_name in ("status_code", "request_id", "code", "param", "type", "body"):
@@ -1387,11 +1348,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:
@@ -1441,14 +1398,9 @@ def dump_api_request_debug(
return None
-
def _direct_native_anthropic_tool_cache_capability(
- agent,
- *,
- provider: Optional[str] = None,
- base_url: Optional[str] = None,
- api_mode: Optional[str] = None,
- model: Optional[str] = None,
+ agent, *, provider: Optional[str] = None, base_url: Optional[str] = None,
+ api_mode: Optional[str] = None, model: Optional[str] = None,
) -> bool:
"""Return whether this resolved destination accepts native tool markers."""
eff_base_url = base_url if base_url is not None else (agent.base_url or "")
@@ -1516,24 +1468,13 @@ def blank_cache_policy_stub(cache_disabled: Optional[bool] = None):
if cache_disabled is None:
cache_disabled = prompt_caching_disabled_from_config()
return SimpleNamespace(
- provider="",
- base_url="",
- api_mode="",
- model="",
- _cache_disabled=bool(cache_disabled),
+ provider="", base_url="", api_mode="", model="", _cache_disabled=bool(cache_disabled)
)
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,
+ 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]:
"""Plan request-local cache sections for one resolved destination (MoA / auxiliary senders).
@@ -1544,20 +1485,13 @@ def plan_cache_sections_for_destination(
``static_system_prefix`` gives the system prompt the same early breakpoint as the main loop.
"""
from agent.prompt_caching import (
- build_prompt_cache_plan,
- effective_cache_ttl,
- envelope_tool_part_cache_markers_supported,
- strip_anthropic_cache_control,
- strip_anthropic_tool_cache_control,
+ build_prompt_cache_plan, effective_cache_ttl, envelope_tool_part_cache_markers_supported,
+ strip_anthropic_cache_control, strip_anthropic_tool_cache_control,
)
stub = blank_cache_policy_stub(cache_disabled)
should_cache, native_layout = anthropic_prompt_cache_policy(
- stub,
- provider=provider,
- base_url=base_url,
- api_mode=api_mode,
- model=model,
+ stub, provider=provider, base_url=base_url, api_mode=api_mode, model=model
)
if not should_cache:
canonical_messages = copy.deepcopy(messages or [])
@@ -1577,11 +1511,7 @@ def plan_cache_sections_for_destination(
static_system_prefix if isinstance(static_system_prefix, str) else None
),
direct_native_tool_cache=_direct_native_anthropic_tool_cache_capability(
- stub,
- provider=provider,
- base_url=base_url,
- api_mode=api_mode,
- model=model,
+ stub, provider=provider, base_url=base_url, api_mode=api_mode, model=model
),
# LiteLLM-style envelope routes forward part-level markers into
# tool_result.content[] → non-retryable 400 (#89886).
@@ -1684,12 +1614,8 @@ def _route_may_be_custom(agent, eff_provider: str, provider_lower: str, eff_base
def anthropic_prompt_cache_policy(
- agent,
- *,
- provider: Optional[str] = None,
- base_url: Optional[str] = None,
- api_mode: Optional[str] = None,
- model: Optional[str] = None,
+ agent, *, provider: Optional[str] = None, base_url: Optional[str] = None,
+ api_mode: Optional[str] = None, model: Optional[str] = None,
) -> tuple[bool, bool]:
"""Decide whether to apply Anthropic prompt caching; returns ``(should_cache, use_native_layout)``.
@@ -1742,9 +1668,7 @@ def anthropic_prompt_cache_policy(
from hermes_cli.config import get_custom_provider_model_capability
custom_prompt_caching = get_custom_provider_model_capability(
- model=eff_model,
- base_url=eff_base_url,
- capability="prompt_caching",
+ model=eff_model, base_url=eff_base_url, capability="prompt_caching",
custom_providers=getattr(agent, "_custom_providers", None),
)
if custom_prompt_caching is not None:
@@ -1798,7 +1722,6 @@ def anthropic_prompt_cache_policy(
return False, False
-
def _provider_supplied_client(agent, client_kwargs: dict) -> Any | None:
"""Ask the registered ProviderProfile for a custom client, if any.
@@ -1948,8 +1871,7 @@ def create_openai_client(agent, client_kwargs: dict, *, reason: str, shared: boo
from agent.codex_headers import apply_required_codex_headers
apply_required_codex_headers(
- client_kwargs,
- access_token=client_kwargs.get("api_key", ""),
+ client_kwargs, access_token=client_kwargs.get("api_key", ""),
base_url=str(client_kwargs.get("base_url", "")),
)
# Module-level `OpenAI` is resolved lazily via __getattr__; tests patch `run_agent.OpenAI`.
@@ -1978,10 +1900,8 @@ def _apply_switched_provider_request_overrides(agent, new_provider):
custom_providers = []
new_extra_body = _custom_provider_extra_body_for_agent(
- provider=new_provider,
- model=getattr(agent, "model", "") or "",
- base_url=getattr(agent, "base_url", "") or "",
- custom_providers=custom_providers or [],
+ provider=new_provider, model=getattr(agent, "model", "") or "",
+ base_url=getattr(agent, "base_url", "") or "", custom_providers=custom_providers or [],
)
overrides = dict(getattr(agent, "request_overrides", {}) or {})
@@ -2056,9 +1976,7 @@ def _resolve_switch_destination(agent, new_model, new_provider, base_url, api_mo
dict(capabilities)
if isinstance(capabilities, dict)
else resolve_native_compaction_capabilities(
- model=new_model,
- base_url=effective_base_url,
- provider=new_provider,
+ model=new_model, base_url=effective_base_url, provider=new_provider,
is_codex_backend=new_norm == "openai-codex",
)
)
@@ -2124,16 +2042,14 @@ def _build_switched_client(agent, new_provider, api_key, base_url, api_mode, new
agent._client_kwargs = {"api_key": api_key or agent.api_key, "base_url": effective_base}
try:
from hermes_cli.config import (
- apply_custom_provider_tls_to_client_kwargs,
- get_compatible_custom_providers,
+ apply_custom_provider_tls_to_client_kwargs, get_compatible_custom_providers,
load_config_readonly,
)
# Read live config, not agent._custom_providers, so mid-session ssl_ca_cert /
# ssl_verify edits are honored.
apply_custom_provider_tls_to_client_kwargs(
- agent._client_kwargs,
- str(effective_base or ""),
+ agent._client_kwargs, str(effective_base or ""),
get_compatible_custom_providers(load_config_readonly()),
)
except Exception:
@@ -2197,9 +2113,7 @@ def _resolve_switch_context_length(agent, snapshot):
custom_providers = None
try:
from hermes_cli.config import (
- get_compatible_custom_providers,
- get_custom_provider_context_length,
- load_config,
+ get_compatible_custom_providers, get_custom_provider_context_length, load_config
)
custom_providers = get_compatible_custom_providers(load_config())
@@ -2242,12 +2156,8 @@ def _update_switch_compressor(agent, custom_providers, effective_context_length,
ctx_api_key = agent.api_key if isinstance(agent.api_key, str) else ""
try:
new_context_length = get_model_context_length(
- agent.model,
- base_url=agent.base_url,
- api_key=ctx_api_key,
- provider=agent.provider,
- config_context_length=effective_context_length,
- custom_providers=custom_providers,
+ agent.model, base_url=agent.base_url, api_key=ctx_api_key, provider=agent.provider,
+ config_context_length=effective_context_length, custom_providers=custom_providers,
)
agent.context_compressor.update_model(
model=agent.model,
@@ -2330,9 +2240,7 @@ def _persist_switch_billing_route(agent) -> None:
return
try:
session_db.update_session_billing_route(
- session_id,
- provider=agent.provider,
- base_url=agent.base_url,
+ session_id, provider=agent.provider, base_url=agent.base_url,
billing_mode=getattr(agent, "api_mode", None),
)
except Exception:
@@ -2340,13 +2248,7 @@ def _persist_switch_billing_route(agent) -> None:
def switch_model(
- agent,
- new_model,
- new_provider,
- api_key='',
- base_url='',
- api_mode='',
- capabilities=None,
+ agent, new_model, new_provider, api_key='', base_url='', api_mode='', capabilities=None
):
"""Switch the model/provider in-place for a live agent (rebuild clients, caching flags, compressor).
@@ -2418,8 +2320,7 @@ def _pre_tool_block_message(agent, function_name, function_args, effective_task_
from hermes_cli.plugins import _dispatch_pre_tool_call_hooks
block_message, modified_args = _dispatch_pre_tool_call_hooks(
function_name, function_args, task_id=effective_task_id or "",
- session_id=getattr(agent, "session_id", "") or "",
- tool_call_id=tool_call_id or "",
+ session_id=getattr(agent, "session_id", "") or "", tool_call_id=tool_call_id or "",
turn_id=getattr(agent, "_current_turn_id", "") or "",
api_request_id=getattr(agent, "_current_api_request_id", "") or "",
middleware_trace=list(middleware_trace),
@@ -2441,10 +2342,7 @@ def invoke_tool(agent, function_name: str, function_args: dict, effective_task_i
path; the sequential path keeps its own inline invocation for display.
"""
from agent.inline_tool_executors import (
- InlineToolContext,
- emit_terminal_post_tool_call,
- resolve_invoke_tool_executor,
- tool_hook_ids,
+ InlineToolContext, emit_terminal_post_tool_call, resolve_invoke_tool_executor, tool_hook_ids
)
if not isinstance(function_args, dict):
@@ -2469,15 +2367,9 @@ def invoke_tool(agent, function_name: str, function_args: dict, effective_task_i
if block_message is not None:
result = json.dumps({"error": block_message}, ensure_ascii=False)
emit_terminal_post_tool_call(
- agent,
- function_name=function_name,
- function_args=function_args,
- result=result,
- effective_task_id=effective_task_id,
- tool_call_id=tool_call_id,
- status="blocked",
- error_type="plugin_block",
- error_message=block_message,
+ agent, function_name=function_name, function_args=function_args, result=result,
+ effective_task_id=effective_task_id, tool_call_id=tool_call_id, status="blocked",
+ error_type="plugin_block", error_message=block_message,
middleware_trace=_tool_middleware_trace,
)
return result
@@ -2492,12 +2384,9 @@ def invoke_tool(agent, function_name: str, function_args: dict, effective_task_i
def _execute(next_args: dict) -> Any:
result = inline_executor(agent, next_args, inline_ctx)
emit_terminal_post_tool_call(
- agent,
- function_name=function_name,
+ agent, function_name=function_name,
function_args=next_args if isinstance(next_args, dict) else function_args,
- result=result,
- effective_task_id=effective_task_id,
- tool_call_id=tool_call_id,
+ result=result, effective_task_id=effective_task_id, tool_call_id=tool_call_id,
duration_ms=int((time.monotonic() - tool_start_time) * 1000),
middleware_trace=_tool_middleware_trace,
)
@@ -2505,13 +2394,11 @@ def invoke_tool(agent, function_name: str, function_args: dict, effective_task_i
else:
def _execute(next_args: dict) -> Any:
dispatch_kwargs = dict(
- tool_call_id=tool_call_id,
- session_id=agent.session_id or "",
+ tool_call_id=tool_call_id, session_id=agent.session_id or "",
turn_id=getattr(agent, "_current_turn_id", "") or "",
api_request_id=getattr(agent, "_current_api_request_id", "") or "",
enabled_tools=list(agent.valid_tool_names) if agent.valid_tool_names else None,
- skip_pre_tool_call_hook=True,
- skip_tool_request_middleware=True,
+ skip_pre_tool_call_hook=True, skip_tool_request_middleware=True,
enabled_toolsets=getattr(agent, "enabled_toolsets", None),
disabled_toolsets=getattr(agent, "disabled_toolsets", None),
tool_request_middleware_trace=list(_tool_middleware_trace),
@@ -2525,15 +2412,12 @@ def invoke_tool(agent, function_name: str, function_args: dict, effective_task_i
from hermes_cli.middleware import run_tool_execution_middleware
return run_tool_execution_middleware(
- function_name,
- function_args,
+ function_name, function_args,
lambda next_args: _execute(next_args if isinstance(next_args, dict) else function_args),
- original_args=function_args,
- **hook_ids,
+ original_args=function_args, **hook_ids,
)
-
def repair_tool_call(agent, tool_name: str) -> str | None:
"""Repair a mismatched tool name (case, separators, CamelCase, ``_tool`` suffixes, then fuzzy match) before aborting.
@@ -2847,15 +2731,11 @@ def _classify_tool_call_orphans(messages: List[Dict[str, Any]]):
result_call_ids.update(variants)
orphaned_results = [
- msg
- for msg, variants in result_entries
- if variants and not (variants & surviving_call_ids)
+ msg for msg, variants in result_entries if variants and not (variants & surviving_call_ids)
]
orphaned_ids = {id(msg) for msg in orphaned_results}
surviving_result_variants = [
- variants
- for msg, variants in result_entries
- if variants and id(msg) not in orphaned_ids
+ variants for msg, variants in result_entries if variants and id(msg) not in orphaned_ids
]
missing_tool_calls = [
tc
@@ -3138,7 +3018,6 @@ def sanitize_api_messages(messages: List[Dict[str, Any]]) -> List[Dict[str, Any]
return _realign_tool_result_names(messages)
-
_ACK_FUTURE_RE = re.compile(r"\b(i['’]ll|i will|let me|i can do that|i can help with that)\b")
_ACK_ACTION_MARKERS = (
"look into", "look at", "inspect", "scan", "check", "analyz", "review", "explore", "read", "open",
@@ -3151,10 +3030,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 +3274,6 @@ def cleanup_dead_connections(agent) -> bool:
return False
-
_QUOTA_RESET_DELAY_RE = re.compile(r"quotaResetDelay[:\s\"]+(\d+(?:\.\d+)?)(ms|s)", re.IGNORECASE)
_RESETS_IN_RE = re.compile(
r"resets?\s+in\s+"
@@ -3474,7 +3349,6 @@ def extract_api_error_context(error: Exception) -> Dict[str, Any]:
return context
-
def _requeue_pending_steer(agent, steer_text: str) -> None:
"""Put drained steer text back so the caller's fallback delivers it as a next-turn user message."""
lock = getattr(agent, "_pending_steer_lock", None)
@@ -3530,7 +3404,6 @@ def apply_pending_steer_to_tool_results(agent, messages: list, num_tool_msgs: in
)
-
def force_close_tcp_sockets(client: Any) -> int:
"""Abort in-flight TCP I/O via ``shutdown(SHUT_RDWR)`` WITHOUT closing FDs.
@@ -3567,7 +3440,6 @@ def force_close_tcp_sockets(client: Any) -> int:
return shutdown_count
-
__all__ = [
"convert_to_trajectory_format",
"sanitize_tool_call_arguments",
From ee1ef6acb6b9e28228d825fa2b4d86654c41ea91 Mon Sep 17 00:00:00 2001
From: Teknium <127238744+teknium1@users.noreply.github.com>
Date: Wed, 2 Sep 2026 19:22:29 -0700
Subject: [PATCH 11/20] refactor(agent/agent_runtime_helpers): collapse small
helpers, compact docstrings/comments (WHYs kept, issue refs dropped)
---
agent/agent_runtime_helpers.py | 616 +++++++++++----------------------
1 file changed, 211 insertions(+), 405 deletions(-)
diff --git a/agent/agent_runtime_helpers.py b/agent/agent_runtime_helpers.py
index 20ce8367de..695e437457 100644
--- a/agent/agent_runtime_helpers.py
+++ b/agent/agent_runtime_helpers.py
@@ -1,7 +1,7 @@
-"""Assorted AIAgent runtime helpers, moved out of run_agent.py.
-
-Each function takes the parent ``AIAgent`` as ``agent`` except the stateless
-helpers (``sanitize_tool_call_arguments``, ``drop_thinking_only_and_merge_users``).
+"""Assorted AIAgent runtime helpers (message repair/sanitization, credential recovery, primary
+runtime restore, prompt-cache policy, client construction, model switching, tool invocation).
+Each function takes the parent ``AIAgent`` as ``agent`` except the stateless message helpers.
+``_ra()`` resolves ``run_agent`` lazily so tests patching ``run_agent.X`` keep intercepting.
"""
from __future__ import annotations
@@ -33,8 +33,7 @@ from utils import base_url_host_matches, base_url_hostname, env_var_enabled, ato
logger = logging.getLogger(__name__)
-# Cap consecutive same-entry OAuth token refreshes on a persistent auth failure;
-# without it a single-entry pool re-mints forever and never reaches fallback (#26080).
+# Cap same-entry OAuth refreshes on a persistent auth failure, else a single-entry pool re-mints forever.
_MAX_AUTH_REFRESH_ATTEMPTS = 2
@@ -50,8 +49,7 @@ _TOOL_CALL_BLOCK_PATTERNS = tuple(
for name in _TOOL_CALL_TAG_NAMES
)
-# Named blocks; see strip_think_blocks step 1c for the
-# boundary/tempered-dot rationale.
+# Named blocks; boundary- and name-gated (see _THINK_STRIP_PATTERNS note).
_NAMED_FUNCTION_BLOCK_PATTERN = re.compile(
r'(?:(?<=^)|(?<=[\n\r.!?:]))[ \t]*'
r']*\bname\s*=[^>]*>'
@@ -231,12 +229,9 @@ def sanitize_tool_call_arguments(
) -> int:
"""Repair corrupted assistant tool-call argument JSON in-place.
- ``cursor`` (optional caller-owned dict) stores under ``"prefix"`` strong references to
- the message objects validated last call; the longest ``is``-identical prefix is skipped
- on the next call. Skipping is safe because only the surrogate/non-ASCII sanitizers
- mutate arguments on live dicts (inside JSON string values), and every other path
- replaces or reorders dicts, breaking identity. Strong refs (not ``id()``) rule out
- address-reuse aliasing.
+ ``cursor["prefix"]`` holds strong refs (not ``id()``: address reuse aliases) to the
+ messages validated last call; the ``is``-identical prefix is skipped. Safe because only
+ the surrogate sanitizers mutate live dicts; every other path replaces dicts, breaking identity.
"""
log = logger or logging.getLogger(__name__)
if not isinstance(messages, list):
@@ -301,21 +296,18 @@ def sanitize_tool_call_arguments(
return repaired
-# Session-scoped in-flight registry for note_turn_start. The gateway caches agents
-# per routing key while the transcript is keyed by session_id (many-to-one), so two
-# agent objects can run concurrent turns on one session unseen by per-agent state (#64934).
+# 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_LOCK = threading.Lock()
def note_turn_start(agent, turn_id: str):
- """Tripwire: warn when a turn starts while a previous turn of the same agent
- or the same session (on another agent object) has not finished its persist.
-
- Does not prevent the overlap; it names it with both turn ids so the dispatch
- route that bypassed the busy guard can be found in logs. Returns the previous
- in-flight turn_id on overlap, else None; takes ownership of the slot either way.
- """
+ """Tripwire: warn when a turn starts while a previous turn of the same agent or session
+ (on another agent object) has not finished its persist. Does not prevent the overlap; it
+ names both turn ids so the dispatch route that bypassed the busy guard is findable in logs.
+ Returns the previous in-flight turn_id on overlap, else None; takes the slot either way."""
prev = getattr(agent, "_inflight_turn_id", None)
prev_started = getattr(agent, "_inflight_turn_started", 0.0)
agent._inflight_turn_id = turn_id
@@ -361,18 +353,14 @@ def note_turn_start(agent, turn_id: str):
def note_turn_persisted(agent):
- """Clear the in-flight marker at turn-end persist (see note_turn_start).
-
- Unconditional by design: on a real overlap the first persist clears the
- second slot and the tripwire under-reports rather than double-reports.
- """
+ """Clear the in-flight marker at turn-end persist (see note_turn_start). Unconditional by
+ design: on a real overlap the first persist clears the second slot, so the tripwire
+ under-reports rather than double-reports."""
agent._inflight_turn_id = None
# Persist-disabled forks never registered a slot; popping here would
# steal the live parent turn's slot (symmetric with note_turn_start).
if not getattr(agent, "_persist_disabled", False):
- session_id = getattr(agent, "_inflight_turn_session_id", None) or getattr(
- agent, "session_id", None
- )
+ session_id = getattr(agent, "_inflight_turn_session_id", None) or getattr(agent, "session_id", None)
if session_id:
with _INFLIGHT_TURNS_LOCK:
_INFLIGHT_TURNS_BY_SESSION.pop(session_id, None)
@@ -449,11 +437,9 @@ def _merge_consecutive_assistants(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.
- """
+ """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] = {}
matched_tool_groups: set = set()
@@ -493,11 +479,9 @@ def _drop_stray_tool_results(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.
- """
+ """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."""
repairs = 0
pruned: List[Dict] = []
n = len(messages)
@@ -580,13 +564,11 @@ _SEQUENCE_REPAIR_PASSES = (
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 cause
- silent empty responses or HTTP 400s. Runs right before the API call as a defensive
- belt for host-fed, resumed, or replayed histories. Passes, in order: merge
- consecutive assistant turns (BEFORE orphan detection so the merged tool_call-id
- union is known); drop stray tool results; prune tool_calls unanswered in the
- immediately following tool run; merge consecutive user messages. A user turn
- directly after an assistant turn is valid and left alone.
+ 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.
+ Passes in order: merge consecutive assistant turns (BEFORE orphan detection so the merged
+ tool_call-id union is known); drop stray tool results; prune unanswered tool_calls; merge
+ consecutive user turns. A user turn directly after an assistant turn is valid and left alone.
"""
if not messages:
return 0
@@ -602,38 +584,24 @@ def repair_message_sequence(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 (#44837).
-
- Repair shrinks the list in place; counting survivors from the flushed prefix
- (identity-preserved) gives the exact new cursor, whereas a ``min()`` clamp
- would skip unflushed rows. Falls back to the clamp without a snapshot.
- """
- pre_repair_flushed_ids = None
+ """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)."""
flush_cursor = getattr(agent, "_last_flushed_db_idx", None)
- if isinstance(flush_cursor, int) and flush_cursor > 0:
- pre_repair_flushed_ids = {id(m) for m in messages[:flush_cursor]}
-
+ flushed_ids = {id(m) for m in messages[:flush_cursor]} if isinstance(flush_cursor, int) and flush_cursor > 0 else None
repairs = repair_message_sequence(agent, messages)
-
if repairs > 0 and hasattr(agent, "_last_flushed_db_idx"):
- if pre_repair_flushed_ids is not None:
- agent._last_flushed_db_idx = sum(
- 1 for m in messages if id(m) in pre_repair_flushed_ids
- )
+ if flushed_ids is not None:
+ agent._last_flushed_db_idx = sum(1 for m in messages if id(m) in flushed_ids)
else:
- agent._last_flushed_db_idx = min(
- agent._last_flushed_db_idx, len(messages)
- )
-
+ agent._last_flushed_db_idx = min(agent._last_flushed_db_idx, len(messages))
return repairs
def _flatten_content_text(content: Any) -> str:
- """Flatten list/dict content (e.g. Anthropic-via-OpenRouter block lists) to text.
-
- A raw list hitting ``re.sub`` raises TypeError and the conversation loop retries forever.
- Thinking/reasoning blocks are dropped outright; their text key varies per provider.
- """
+ """Flatten list/dict content (e.g. Anthropic-via-OpenRouter block lists) to text: a raw list
+ hitting ``re.sub`` raises TypeError and the loop retries forever. Thinking/reasoning blocks
+ are dropped outright; their text key varies per provider."""
if isinstance(content, str):
return content
if isinstance(content, list):
@@ -666,14 +634,10 @@ _THINK_STRIP_PATTERNS = (
def strip_think_blocks(agent, content: str) -> str:
- """Remove reasoning/thinking blocks from content, returning only visible text.
-
- Strips closed tag pairs, unterminated open tags at a block boundary (mirrors
- ``gateway/stream_consumer.py``), stray orphan tags, all case-insensitive variants
- (think/thinking/reasoning/REASONING_SCRATCHPAD/thought), and standalone tool-call XML
- blocks some open models emit; the ```` variant is boundary- and
- ``name=``-gated so prose mentions survive.
- """
+ """Remove reasoning/thinking blocks from content, returning only visible text: closed tag
+ pairs, unterminated open tags at a block boundary (mirrors ``gateway/stream_consumer.py``),
+ stray orphan tags (all case-insensitive variants), and standalone tool-call XML blocks some
+ open models emit; ```` is boundary- and ``name=``-gated so prose mentions survive."""
if not content:
return ""
content = _flatten_content_text(content)
@@ -685,11 +649,9 @@ def strip_think_blocks(agent, content: str) -> str:
def sync_credential_pool_entry_id(agent) -> None:
- """Rebind ``agent._credential_pool_entry_id`` from the current pool + key.
-
- OAuth refreshes can replace the token before recovery runs, so the key
- alone cannot attribute a failure; the stable entry ID can. Cleared when no pool is bound.
- """
+ """Rebind ``agent._credential_pool_entry_id`` from the current pool + key. OAuth refreshes
+ can replace the token before recovery runs, so the key alone cannot attribute a failure;
+ the stable entry ID can. Cleared when no pool is bound."""
pool = getattr(agent, "_credential_pool", None)
try:
agent._credential_pool_entry_id = (
@@ -709,11 +671,9 @@ _USAGE_LIMIT_MESSAGE_TOKENS = ("usage limit reached", "usage limit has been reac
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.
- """
+ """``(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."""
api_key_hint = getattr(agent, "api_key", None) or None
raw_id = getattr(agent, "_credential_pool_entry_id", None)
credential_id = raw_id if isinstance(raw_id, str) and raw_id else None
@@ -729,11 +689,9 @@ def _failed_credential_identity(agent, pool) -> Tuple[Optional[str], Optional[st
def _is_entitlement_403(agent, status_code, error_context) -> bool:
- """Entitlement 403s look like auth failures but refresh cannot fix them.
-
- Any xai-oauth 403 is treated as entitlement EXCEPT xAI's stale-token signals
- (``[WKE=unauthenticated:...]``, "could not be validated"), which must stay refreshable.
- """
+ """Entitlement 403s look like auth failures but refresh cannot fix them. Any xai-oauth 403
+ is entitlement EXCEPT xAI's stale-token signals (``[WKE=unauthenticated:...]``,
+ "could not be validated"), which must stay refreshable."""
if agent._is_entitlement_failure(error_context, status_code):
return True
if status_code != 403:
@@ -837,14 +795,10 @@ def recover_with_credential_pool(
classified_reason: Optional[FailoverReason] = None,
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 exhaustion: rotate immediately. Auth failures: refresh before rotating.
- ``classified_reason`` honors the structured classifier over raw HTTP codes
- (e.g. Anthropic 400 for "out of extra usage"). ``billing_unverified`` persists an
- ambiguous billing verdict so the entry gets a short cooldown, not the one-hour bench.
- """
+ """Attempt credential recovery via pool rotation; returns (recovered, has_retried_429).
+ Rate limits: retry once, then rotate. Billing: rotate immediately. Auth: refresh before
+ rotating. ``classified_reason`` beats raw HTTP codes (e.g. Anthropic 400 "out of extra
+ usage"); ``billing_unverified`` gives the entry a short cooldown, not the one-hour bench."""
pool = agent._credential_pool
if pool is None:
return False, has_retried_429
@@ -934,10 +888,8 @@ def recover_with_credential_pool(
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.
- """
+ """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"]
agent.provider = rt["provider"]
agent.requested_provider = rt.get("requested_provider", agent.provider)
@@ -982,10 +934,8 @@ def _rebuild_primary_client(agent, rt: Dict[str, Any], *, reason: str) -> None:
def try_recover_primary_transport(
agent, api_error: Exception, *, retry_count: int, max_retries: int,
) -> bool:
- """Rebuild the primary client once and retry after ``max_retries`` exhaust on a transient transport error.
-
- Skipped for aggregator providers (OpenRouter, Nous) that already manage pools/retries server-side.
- """
+ """Rebuild the primary client once and retry after ``max_retries`` exhaust on a transient
+ transport error. Skipped for aggregators (OpenRouter, Nous) that manage retries server-side."""
if agent._fallback_activated:
return False
error_type = type(api_error).__name__
@@ -1037,10 +987,8 @@ def try_recover_primary_transport(
def _merge_user_content(prev_content: Any, cur_content: Any) -> Any:
- """Merged content for two adjacent user messages; ``_UNMERGEABLE`` for unknown shapes.
-
- String+string joins with a blank line; list sides append as separate blocks.
- """
+ """Merged content for two adjacent user messages (``_UNMERGEABLE`` for unknown shapes):
+ string+string joins with a blank line; list sides append as separate blocks."""
if isinstance(prev_content, str) and isinstance(cur_content, str):
return prev_content + ("\n\n" if prev_content and cur_content else "") + cur_content
if isinstance(prev_content, list) and isinstance(cur_content, list):
@@ -1058,12 +1006,9 @@ _UNMERGEABLE = object()
def drop_thinking_only_and_merge_users(
messages: List[Dict[str, Any]], *, drop_codex_reasoning_items: bool = True
) -> List[Dict[str, Any]]:
- """Drop thinking-only assistant turns and merge adjacent user messages left behind.
-
- Operates 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
- (mirrors Claude Code's ``normalizeMessagesForAPI``).
- """
+ """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."""
if not messages:
return messages
kept = [
@@ -1097,11 +1042,9 @@ def drop_thinking_only_and_merge_users(
def _primary_reset_gate_blocks(agent, rt, primary_provider, primary_runtime_base_url, matches_primary, load_primary_pool):
- """Reset-aware gate: skip a guaranteed-to-fail restore while the primary pool reports a future reset.
-
- Fails open on any error/None. Returns ``(blocked, prefetched_pool, prefetched)`` so the
- rebind step can reuse the loaded pool (one auth.json read at most).
- """
+ """Reset-aware gate: skip a guaranteed-to-fail restore while the primary pool reports a
+ future reset; fails open on any error/None. Returns ``(blocked, prefetched_pool, prefetched)``
+ so the rebind step reuses the loaded pool (one auth.json read at most)."""
prefetched_pool = None
prefetched = False
try:
@@ -1142,13 +1085,10 @@ def _restore_runtime_capabilities(agent, rt: Dict[str, Any]) -> None:
def _rebind_primary_credential_pool(agent, primary_provider, matches_primary, load_primary_pool, prefetched_pool, prefetched) -> None:
- """Rebind and re-select the primary credential pool after a fallback turn.
-
- A cross-provider fallback attaches the fallback's pool; leaving it would trip the
- provider-mismatch guard on the next 401/429. Reload the primary pool, else clear it.
- The snapshot api_key may be stale after pool rotation; re-select the pool's current
- best entry, keeping the snapshot key when no usable entry exists.
- """
+ """Rebind and re-select the primary credential pool after a fallback turn. A cross-provider
+ fallback attaches its own pool, which would trip the provider-mismatch guard on the next
+ 401/429: reload the primary pool, else clear it. The snapshot api_key may be stale after
+ rotation; re-select the pool's best entry, keeping the snapshot key when none is usable."""
pool = getattr(agent, "_credential_pool", None)
pool_provider = str(getattr(pool, "provider", "") or "").strip().lower()
if pool is not None and pool_provider and not matches_primary(pool):
@@ -1189,10 +1129,8 @@ def _rebind_primary_credential_pool(agent, primary_provider, matches_primary, lo
def restore_primary_runtime(agent) -> bool:
- """Restore the primary runtime at the start of a new turn so fallback stays turn-scoped.
-
- Needed for long-lived CLI agents and the gateway's cached agents (``_agent_cache``).
- """
+ """Restore the primary runtime at the start of a new turn so fallback stays turn-scoped
+ (long-lived CLI agents and the gateway's cached agents)."""
if not agent._fallback_activated:
# Reset the index even without activation: a failed _try_activate_fallback() can strand
# _fallback_index past the chain end and silently block future fallbacks.
@@ -1299,11 +1237,8 @@ _INLINE_REASONING_PATTERNS = tuple(
def extract_reasoning(agent, assistant_message) -> Optional[str]:
- """Extract reasoning text from an assistant message, or None.
-
- Checks ``reasoning``, ``reasoning_content``, ``reasoning_details`` (OpenRouter unified),
- then inline thinking blocks in list content.
- """
+ """Reasoning text from ``reasoning`` / ``reasoning_content`` / ``reasoning_details``
+ (OpenRouter unified), else inline thinking blocks in the content; None when absent."""
parts: List[str] = []
def _add(text) -> None:
@@ -1412,11 +1347,8 @@ def _direct_native_anthropic_tool_cache_capability(
def cache_ttl_means_disabled(ttl: Any) -> bool:
- """Return True when a ``prompt_caching.cache_ttl`` value means caching off.
-
- Single predicate shared by ``agent_init`` and the stub policy paths (#76085).
- Unknown values (``"2h"``, integers) are NOT a disable.
- """
+ """True when a ``prompt_caching.cache_ttl`` value means caching off (single predicate shared
+ by ``agent_init`` and the stub policy paths). Unknown values (``"2h"``, ints) are NOT a disable."""
if ttl in ("5m", "1h"):
return False
if ttl is False or ttl is None:
@@ -1428,41 +1360,31 @@ def cache_ttl_means_disabled(ttl: Any) -> bool:
VALID_CACHE_TTLS = ("5m", "1h")
-def _raw_cache_ttl_from_config() -> Any:
- """Read the raw ``prompt_caching.cache_ttl`` config value (may raise)."""
- from hermes_cli.config import load_config_readonly
+def _raw_cache_ttl_from_config(default: Any) -> Any:
+ """Raw ``prompt_caching.cache_ttl`` config value, or ``default`` when config cannot be read."""
+ try:
+ from hermes_cli.config import load_config_readonly
- pc_cfg = load_config_readonly().get("prompt_caching", {}) or {}
- return pc_cfg.get("cache_ttl", "5m")
+ return (load_config_readonly().get("prompt_caching", {}) or {}).get("cache_ttl", "5m")
+ except Exception:
+ return default
def prompt_caching_disabled_from_config() -> bool:
- """Return True when ``prompt_caching.cache_ttl`` is configured as off (same detection as ``agent_init``; #76085 / #33555)."""
- try:
- ttl = _raw_cache_ttl_from_config()
- except Exception:
- return False
- return cache_ttl_means_disabled(ttl)
+ """True when ``prompt_caching.cache_ttl`` is configured as off (same detection as ``agent_init``)."""
+ return cache_ttl_means_disabled(_raw_cache_ttl_from_config("5m"))
def configured_cache_ttl() -> Optional[str]:
- """Return the configured ``prompt_caching.cache_ttl`` tier (``5m``/``1h``), else None.
-
- Mirrors ``agent_init`` so stub paths don't regress a configured ``1h`` to 5m (#84733).
- """
- try:
- ttl = _raw_cache_ttl_from_config()
- except Exception:
- return None
+ """Configured ``prompt_caching.cache_ttl`` tier (``5m``/``1h``), else None; mirrors
+ ``agent_init`` so stub paths don't regress a configured ``1h`` to 5m."""
+ ttl = _raw_cache_ttl_from_config(None)
return ttl if ttl in VALID_CACHE_TTLS else None
def blank_cache_policy_stub(cache_disabled: Optional[bool] = None):
- """Build the destination-identity-blank stub for ``anthropic_prompt_cache_policy``.
-
- Sole sanctioned constructor so ``_cache_disabled`` is never omitted (#76085); when
- ``cache_disabled`` is None the global config is consulted.
- """
+ """Destination-identity-blank stub for ``anthropic_prompt_cache_policy``; the sole sanctioned
+ constructor so ``_cache_disabled`` is never omitted (None consults the global config)."""
from types import SimpleNamespace
if cache_disabled is None:
@@ -1477,13 +1399,10 @@ def plan_cache_sections_for_destination(
model: str, cache_disabled: Optional[bool] = None, cache_ttl: Optional[str] = None,
static_system_prefix: Optional[str] = None,
) -> Tuple[list, list]:
- """Plan request-local cache sections for one resolved destination (MoA / auxiliary senders).
-
- Returns stripped copies (non-caching route) or a ``build_prompt_cache_plan`` layout; never
- mutates ``messages``/``tools``. ``cache_disabled`` and ``cache_ttl`` default to live config
- so these paths honor the operator's disable (#76085) and tier (#84733);
- ``static_system_prefix`` gives the system prompt the same early breakpoint as the main loop.
- """
+ """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
+ tier are honored; ``static_system_prefix`` gives the system prompt the main loop's early breakpoint."""
from agent.prompt_caching import (
build_prompt_cache_plan, effective_cache_ttl, envelope_tool_part_cache_markers_supported,
strip_anthropic_cache_control, strip_anthropic_tool_cache_control,
@@ -1514,7 +1433,7 @@ def plan_cache_sections_for_destination(
stub, provider=provider, base_url=base_url, api_mode=api_mode, model=model
),
# LiteLLM-style envelope routes forward part-level markers into
- # tool_result.content[] → non-retryable 400 (#89886).
+ # tool_result.content[] → non-retryable 400.
tool_part_markers=envelope_tool_part_cache_markers_supported(
provider, base_url
),
@@ -1523,11 +1442,8 @@ def plan_cache_sections_for_destination(
def _is_litellm_route(provider_lower: str, base_url: str) -> bool:
- """True when a route is a LiteLLM proxy, by provider id or host token.
-
- ``litellm`` must match as a whole delimited token (not substring) in provider or host;
- a path segment never qualifies.
- """
+ """True when a route is a LiteLLM proxy: ``litellm`` as a whole delimited token (not
+ substring) in the provider id or host; a path segment never qualifies."""
return _has_litellm_token(provider_lower, ":-_/") or _has_litellm_token(base_url_hostname(base_url), ".-")
@@ -1539,11 +1455,8 @@ def _has_litellm_token(value: str, delimiters: str) -> bool:
def _moa_aggregator_cache_policy(agent, eff_model: str) -> tuple[bool, bool]:
- """MoA virtual provider: resolve the policy from the preset's real aggregator slot.
-
- The virtual provider matches no caching branch and would silently lose caching
- for the acting aggregator.
- """
+ """MoA virtual provider: resolve the policy from the preset's real aggregator slot (the
+ virtual provider matches no caching branch and would silently lose caching)."""
try:
from hermes_cli.config import load_config as _load_moa_cfg
from hermes_cli.moa_config import resolve_moa_preset
@@ -1619,11 +1532,10 @@ def anthropic_prompt_cache_policy(
) -> tuple[bool, bool]:
"""Decide whether to apply Anthropic prompt caching; returns ``(should_cache, use_native_layout)``.
- ``use_native_layout`` puts markers on inner content blocks (native Anthropic wire);
- otherwise on the message envelope (OpenRouter / OpenAI-wire proxies). Qwen/Alibaba routes
- also honour envelope markers. An operator disable is read from ``_cache_disabled``
- (not ``_cache_ttl``, unset during init) so it survives switches and restores.
- Branch ORDER is load-bearing (see inline notes).
+ Native layout puts markers on inner content blocks (Anthropic wire), else on the message
+ envelope (OpenRouter / OpenAI-wire proxies; Qwen/Alibaba too). The operator disable is read
+ from ``_cache_disabled`` (not ``_cache_ttl``, unset during init) so it survives switches
+ and restores. Branch ORDER is load-bearing (see inline notes).
"""
if getattr(agent, "_cache_disabled", False):
return (False, False)
@@ -1723,13 +1635,10 @@ def anthropic_prompt_cache_policy(
def _provider_supplied_client(agent, client_kwargs: dict) -> Any | None:
- """Ask the registered ProviderProfile for a custom client, if any.
-
- Resolves by provider name first, then by the ``base_url`` scheme prefix so a runtime
- configured only by URL (``acp://…``) still reaches its profile. A profile that raises
- is logged and skipped: a third-party plugin must not be able to take the turn down,
- it can only fail to provide a client.
- """
+ """Ask the registered ProviderProfile for a custom client, if any. Resolves by provider name,
+ then by ``base_url`` prefix so a URL-only runtime (``acp://…``) still reaches its profile.
+ A profile that raises is logged and skipped: a third-party plugin must not be able to take
+ the turn down, it can only fail to provide a client."""
try:
from providers import get_provider_profile
except Exception:
@@ -1759,12 +1668,9 @@ def _provider_supplied_client(agent, client_kwargs: dict) -> Any | None:
def _profile_for_base_url(base_url: str) -> Any | None:
- """Registered profile whose own base_url is a prefix of ``base_url`` (used when the provider name did not resolve).
-
- Prefix match, not equality: the replaced copilot-acp branch keyed on
- ``startswith("acp://copilot")``, so a base_url carrying a path or a user override
- under the same root must still resolve.
- """
+ """Registered profile whose own base_url is a prefix of ``base_url`` (provider name did not
+ resolve). Prefix, not equality: the replaced copilot-acp branch keyed on
+ ``startswith("acp://copilot")``, so a path or user override under the same root must resolve."""
try:
from providers import list_providers
candidates = list_providers()
@@ -1881,13 +1787,10 @@ def create_openai_client(agent, client_kwargs: dict, *, reason: str, shared: boo
def _apply_switched_provider_request_overrides(agent, new_provider):
- """Re-derive the switched-to provider's ``request_overrides`` (custom_providers ``extra_body``) onto a live agent.
-
+ """Re-derive the switched-to provider's ``request_overrides`` (custom_providers ``extra_body``).
Matches by provider key, base_url AND model (same rule as
- ``agent_init._merge_custom_provider_extra_body``) so a different model at the
- same endpoint never inherits another model's ``extra_body``. Stale
- ``extra_body`` is cleared; ``service_tier`` / ``speed`` overrides are preserved.
- """
+ ``agent_init._merge_custom_provider_extra_body``) so a different model at the same endpoint
+ never inherits another's ``extra_body``. Stale ``extra_body`` cleared; ``service_tier``/``speed`` kept."""
from agent.agent_init import _custom_provider_extra_body_for_agent
# Prefer the init-time cache (agent._custom_providers); reload only if absent.
@@ -1898,12 +1801,12 @@ def _apply_switched_provider_request_overrides(agent, new_provider):
custom_providers = get_compatible_custom_providers(load_config())
except Exception:
custom_providers = []
-
new_extra_body = _custom_provider_extra_body_for_agent(
- provider=new_provider, model=getattr(agent, "model", "") or "",
- base_url=getattr(agent, "base_url", "") or "", custom_providers=custom_providers or [],
+ provider=new_provider,
+ model=getattr(agent, "model", "") or "",
+ base_url=getattr(agent, "base_url", "") or "",
+ custom_providers=custom_providers or [],
)
-
overrides = dict(getattr(agent, "request_overrides", {}) or {})
overrides.pop("extra_body", None) # always drop the previous provider's extra_body
if new_extra_body:
@@ -1934,11 +1837,9 @@ _MISSING = object()
def _snapshot_switch_state(agent) -> Dict[str, Any]:
- """Snapshot every field the swap+rebuild mutates so a failed rebuild rolls back atomically.
-
- Otherwise a new model name + OLD client 400s next turn. The sentinel distinguishes
- unset from None: tests build bare agents via ``__new__`` without all fields.
- """
+ """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."""
snapshot = {name: getattr(agent, name, _MISSING) for name in _SWITCH_SNAPSHOT_FIELDS}
# Shallow-copy the dict so mutating the live one doesn't poison the rollback target.
snapshot["_client_kwargs"] = dict(getattr(agent, "_client_kwargs", {}) or {})
@@ -2250,12 +2151,10 @@ def _persist_switch_billing_route(agent) -> None:
def switch_model(
agent, new_model, new_provider, api_key='', base_url='', api_mode='', capabilities=None
):
- """Switch the model/provider in-place for a live agent (rebuild clients, caching flags, compressor).
-
- Mirrors ``_try_activate_fallback()`` but also updates ``_primary_runtime`` so the
- change persists across turns. Any failure during the swap/rebuild rolls the agent
- back to its pre-switch snapshot and re-raises (callers catch).
- """
+ """Switch the model/provider in-place for a live agent (rebuild clients, caching flags,
+ compressor). Mirrors ``_try_activate_fallback()`` but also updates ``_primary_runtime`` so
+ the change persists across turns. A failed swap/rebuild rolls back to the pre-switch
+ snapshot and re-raises (callers catch)."""
old_model = agent.model
old_provider = agent.provider
old_norm = (old_provider or "").strip().lower()
@@ -2336,11 +2235,9 @@ def invoke_tool(agent, function_name: str, function_args: dict, effective_task_i
skip_tool_request_middleware: bool = False,
tool_request_middleware_trace: Optional[List[Dict[str, Any]]] = None,
skip_tool_execution_middleware: bool = False) -> str:
- """Invoke a single tool and return the result string; no display logic.
-
- Handles agent-level and registry-dispatched tools. Used by the concurrent
- path; the sequential path keeps its own inline invocation for display.
- """
+ """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
+ invocation for display."""
from agent.inline_tool_executors import (
InlineToolContext, emit_terminal_post_tool_call, resolve_invoke_tool_executor, tool_hook_ids
)
@@ -2419,11 +2316,9 @@ def invoke_tool(agent, function_name: str, function_args: dict, effective_task_i
def repair_tool_call(agent, tool_name: str) -> str | None:
- """Repair a mismatched tool name (case, separators, CamelCase, ``_tool`` suffixes, then fuzzy match) before aborting.
-
- Suffix stripping is applied twice so ``TodoTool_tool`` reduces fully.
- Returns the repaired name if in valid_tool_names, else None.
- """
+ """Repair a mismatched tool name (case, separators, CamelCase, ``_tool`` suffixes twice so
+ ``TodoTool_tool`` reduces fully, then fuzzy match) before aborting. Returns the repaired
+ name if in valid_tool_names, else None."""
from difflib import get_close_matches
if not tool_name:
@@ -2477,14 +2372,14 @@ def repair_tool_call(agent, tool_name: str) -> str | None:
# the stub placeholder in chat_completion_helpers so healed transcripts read consistently.
_INTERRUPTED_PLACEHOLDER = "[response interrupted]"
-# Escalate repeated heals once per session window, then stay quiet (#96870). Default
+# Escalate repeated heals once per session window, then stay quiet. Default
# threshold; tunable via ``agent.sanitizer_heal_escalation_threshold`` (<= 0 disables).
_EMPTY_HEAL_ESCALATE_AFTER = 3
_EMPTY_HEAL_WINDOW_S = 600.0
_empty_heal_log_state: Dict[str, Dict[str, Any]] = {}
_empty_heal_log_lock = threading.Lock()
# Sessions already given the one-time user notice; separate from the windowed log state
-# so the user is told ONCE per session (#96870, out-of-band, never in conversation context).
+# so the user is told ONCE per session (out-of-band, never in conversation context).
_empty_heal_user_notified: set = set()
# One-shot pending notices keyed by session, drained by the conversation loop via
# ``consume_pending_sanitizer_heal_notice`` and delivered via the status/warning callback.
@@ -2492,10 +2387,8 @@ _empty_heal_pending_notice: Dict[str, str] = {}
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, tool_call_id, reasoning).
-
- Role-agnostic counterpart of ``AIAgent._is_thinking_only_assistant``.
- """
+ """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``."""
content = msg.get("content")
if isinstance(content, str):
if content.strip():
@@ -2525,17 +2418,10 @@ def _msg_has_payload(msg: Dict[str, Any]) -> 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; returns True when filled.
-
- Pass the per-call copy only; durable history must not be mutated
- (#88955, #96870).
- """
- if is_final or not isinstance(msg, dict):
- return False
- if msg.get("role") not in ("user", "assistant"):
+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"):
return False
if _msg_has_payload(msg):
return False
@@ -2568,22 +2454,15 @@ def _heal_escalation_threshold() -> int:
def consume_pending_sanitizer_heal_notice() -> Optional[str]:
- """Drain the one-time user notice for the current session, if any (at most one per session lifetime).
-
- Delivered through the status/warning callback, NEVER appended to the
- conversation context.
- """
+ """Drain the one-time user notice for the current session (at most one per session lifetime).
+ Delivered through the status/warning callback, NEVER appended to the conversation context."""
key = _session_id_for_heal_log() or "-"
with _empty_heal_log_lock:
return _empty_heal_pending_notice.pop(key, None)
def get_sanitizer_heal_stats() -> Dict[str, Dict[str, Any]]:
- """Read-only snapshot of per-session sanitiser heal counters for diagnostics.
-
- Keyed by session id; values carry ``heal_events``, ``messages_healed`` and
- ``escalated``.
- """
+ """Read-only per-session sanitiser heal counters (``heal_events``, ``messages_healed``, ``escalated``) for diagnostics."""
with _empty_heal_log_lock:
return {
k: {
@@ -2596,11 +2475,9 @@ def get_sanitizer_heal_stats() -> Dict[str, Dict[str, Any]]:
def _log_empty_non_final_heal(healed: int) -> None:
- """WARNING on the first heals in a window, one ERROR at the threshold, then silent.
-
- The threshold also queues a ONE-TIME out-of-band user notice (drained by
- ``consume_pending_sanitizer_heal_notice``); never re-armed by a new window.
- """
+ """WARNING on the first heals in a window, one ERROR at the threshold, then silent. The
+ threshold also queues a ONE-TIME out-of-band user notice (drained by
+ ``consume_pending_sanitizer_heal_notice``); never re-armed by a new window."""
key = _session_id_for_heal_log() or "-"
threshold = _heal_escalation_threshold()
now = time.monotonic()
@@ -2665,12 +2542,9 @@ 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 left untouched.
- """
+ 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
@@ -2700,16 +2574,11 @@ def repair_empty_non_final_messages(
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 (``id``,
- ``call_id``, ``response_item_id``, composite bridge) is registered so a
- result matching any alias survives (#55626, #63000, #58357).
- ``orphaned_results`` are the actual dicts (filter by ``id(msg)``).
- ``sanitize_api_messages`` pairs positionally instead (#94704) but shares
- the ``tool_call_id_variants`` / ``tool_result_id_variants`` alias policy.
- """
+ """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
+ ``orphaned_results`` are the actual dicts (filter by ``id(msg)``). ``sanitize_api_messages``
+ pairs positionally instead but shares the ``*_id_variants`` alias policy."""
assistant_call_variants: List[tuple[Any, frozenset[str]]] = []
surviving_call_ids: set[str] = set()
for msg in messages:
@@ -2780,11 +2649,9 @@ def _drop_empty_tool_calls_arrays(messages: List[Dict[str, Any]]) -> List[Dict[s
def _repair_nameless_tool_calls(messages: List[Dict[str, Any]]) -> None:
- """Rename empty/missing ``function.name`` to a sentinel (in place).
-
- Dropping would unpair the anti-priming result the dispatch loop keeps for
- empty-name calls, and Responses adapters 400 on nameless calls.
- """
+ """Rename empty/missing ``function.name`` to a sentinel (in place): dropping would unpair the
+ anti-priming result the dispatch loop keeps for empty-name calls, and Responses adapters
+ 400 on nameless calls."""
sentinel = "invalid_tool_call"
for msg in messages:
if msg.get("role") != "assistant":
@@ -2815,11 +2682,9 @@ def _repair_nameless_tool_calls(messages: List[Dict[str, Any]]) -> None:
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``.
- """
+ """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``."""
kept = [
m for m in messages
if not (m.get("role") == "tool" and not (m.get("tool_call_id") or "").strip())
@@ -2833,11 +2698,9 @@ def _drop_results_without_ids(messages: List[Dict[str, Any]]) -> List[Dict[str,
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:
- drop positional orphans, stub unanswered declared ids; matching is alias-aware.
- """
+ """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."""
paired: List[Dict[str, Any]] = []
declared_calls: Dict[str, tuple] = {}
dropped = 0
@@ -2903,13 +2766,10 @@ def _pair_tool_calls_positionally(messages: List[Dict[str, Any]]) -> List[Dict[s
def _dedupe_tool_call_ids(messages: List[Dict[str, Any]]) -> List[Dict[str, Any]]:
- """Deduplicate tool_call_ids (strict providers 400 on duplicates).
-
- Collapses duplicates within an assistant message and drops results answering
- no OUTSTANDING call. Tracks outstanding calls (not ids ever seen) because
- llama.cpp reuses one constant id, and the whole variant group so
- alias-keyed results are not deleted.
- """
+ """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."""
seen_assistant_call_ids: set = set()
outstanding_call_ids: set = set()
outstanding_groups: Dict[int, frozenset] = {}
@@ -2967,10 +2827,8 @@ def _dedupe_tool_call_ids(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, which is routine when tool_search bridges via ``tool_call``.
- """
+ """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``."""
call_names: Dict[str, str] = {}
for msg in messages:
if msg.get("role") == "assistant":
@@ -3003,11 +2861,9 @@ def _realign_tool_result_names(messages: List[Dict[str, Any]]) -> List[Dict[str,
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.
- """
+ """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."""
messages = _drop_invalid_roles(messages)
messages = repair_empty_non_final_messages(messages)
messages = _drop_empty_tool_calls_arrays(messages)
@@ -3034,11 +2890,8 @@ def looks_like_codex_intermediate_ack(
require_workspace: bool = True,
) -> bool:
"""Detect a planning/ack message that should continue instead of ending the turn.
-
- ``require_workspace=False`` (user opted into ``agent.intent_ack_continuation`` for all
- api_modes) drops the filesystem/repo reference requirement; the future-ack +
- short-content + no-prior-tools + action-verb checks always apply.
- """
+ ``require_workspace=False`` (opt-in for all api_modes) drops the filesystem/repo reference
+ requirement; future-ack + short-content + no-prior-tools + action-verb checks always apply."""
if any(isinstance(msg, dict) and msg.get("role") == "tool" for msg in messages):
return False
assistant_text = agent._strip_think_blocks(assistant_content or "").strip().lower()
@@ -3090,11 +2943,9 @@ _INTENT_ACK_OFF = {"false", "never", "no", "off"}
def intent_ack_continuation_mode(agent) -> str:
- """Resolve the intent-ack continuation mode: ``"off"``, ``"codex_only"`` (workspace acks on codex_responses), or ``"all"``.
-
- Mirrors ``agent.tool_use_enforcement``: ``"auto"`` -> codex_only; true-ish values -> all;
- false-ish -> off; ``list`` -> all when a substring matches the active model name, else off.
- """
+ """Intent-ack continuation mode: ``"off"``, ``"codex_only"`` (workspace acks on codex_responses)
+ or ``"all"``. Mirrors ``agent.tool_use_enforcement``: ``"auto"`` -> codex_only; true-ish -> all;
+ false-ish -> off; ``list`` -> all when a substring matches the active model name, else off."""
mode = getattr(agent, "_intent_ack_continuation", "auto")
if mode is True or (isinstance(mode, str) and mode.lower() in _INTENT_ACK_ON):
return "all"
@@ -3117,13 +2968,10 @@ def copy_reasoning_content_for_api(agent, source_msg: dict, api_msg: dict) -> No
def reapply_reasoning_echo_for_provider(agent, api_messages: list) -> int:
- """Re-pad or strip assistant turns' reasoning_content for the CURRENT provider after a fallback switch.
-
- ``api_messages`` is shaped for the primary provider; require-side providers
- (DeepSeek/Kimi/MiMo) 400 without the pad, strict ones (Mistral, Cerebras,
- Groq, ...) 400/422 with it (#45655). Idempotent. Returns the number of
- assistant turns changed.
- """
+ """Re-pad or strip assistant turns' reasoning_content for the CURRENT provider after a
+ fallback switch: ``api_messages`` is shaped for the primary; require-side providers
+ (DeepSeek/Kimi/MiMo) 400 without the pad, strict ones (Mistral, Cerebras, Groq) 400/422
+ with it. Idempotent; returns the number of assistant turns changed."""
from agent.message_sanitization import reapply_reasoning_echo
return reapply_reasoning_echo(
@@ -3132,11 +2980,9 @@ def reapply_reasoning_echo_for_provider(agent, api_messages: list) -> int:
def _iter_httpx_pool_objects(http_client: Any):
- """Yield httpcore pool objects reachable from an httpx client, including mounted transports.
-
- Keepalive and proxy configs put live connections on ``client._mounts``; walking
- only ``_transport`` made ``force_close_tcp_sockets`` miss them.
- """
+ """Yield httpcore pool objects reachable from an httpx client, including mounted transports:
+ keepalive and proxy configs put live connections on ``client._mounts``, which a
+ ``_transport``-only walk misses."""
seen_pools: set[int] = set()
def _pools_for_transport(transport: Any):
@@ -3163,19 +3009,10 @@ def _iter_httpx_pool_objects(http_client: Any):
def _connection_candidates(conn: Any):
"""Walk nested ``_connection`` wrappers (proxy tunnel → HTTP11/2)."""
seen: set[int] = set()
- stack = [conn]
- while stack:
- candidate = stack.pop()
- if candidate is None:
- continue
- marker = id(candidate)
- if marker in seen:
- continue
- seen.add(marker)
- yield candidate
- inner = getattr(candidate, "_connection", None)
- if inner is not None and id(inner) not in seen:
- stack.append(inner)
+ while conn is not None and id(conn) not in seen:
+ seen.add(id(conn))
+ yield conn
+ conn = getattr(conn, "_connection", None)
def _socket_from_stream(stream: Any):
@@ -3205,13 +3042,10 @@ def _socket_from_stream(stream: Any):
def _iter_pool_sockets(client: Any):
- """Yield raw sockets reachable from an OpenAI/httpx client pool.
-
- Traversal is defensive over private httpcore internals (``conn._connection``,
- proxy tunnel wrappers) that vary by release. Also walks mount transports and
- in-flight ``PoolRequest.connection`` objects, reachable when ``_connections``
- is empty during checkout.
- """
+ """Yield raw sockets reachable from an OpenAI/httpx client pool. Defensive over private
+ httpcore internals (``conn._connection``, proxy tunnel wrappers) that vary by release; also
+ walks mount transports and in-flight ``PoolRequest.connection`` objects (``_connections``
+ is empty during checkout)."""
try:
# Some SDK wrappers *are* the httpx client; fall through so mount-aware discovery runs.
http_client = getattr(client, "_client", None)
@@ -3362,11 +3196,9 @@ def _requeue_pending_steer(agent, steer_text: str) -> None:
def apply_pending_steer_to_tool_results(agent, messages: list, num_tool_msgs: int) -> None:
- """Append pending /steer text to the last ``role:"tool"`` message of this batch, marked as user-origin.
-
- Modifies existing content only, so role alternation is preserved.
- ``num_tool_msgs`` bounds the tail slice searched.
- """
+ """Append pending /steer text to the last ``role:"tool"`` message of this batch (bounded by
+ ``num_tool_msgs``), marked as user-origin. Modifies existing content only, so role
+ alternation is preserved."""
if num_tool_msgs <= 0 or not messages:
return
steer_text = agent._drain_pending_steer()
@@ -3405,24 +3237,17 @@ def apply_pending_steer_to_tool_results(agent, messages: list, num_tool_msgs: in
def force_close_tcp_sockets(client: Any) -> int:
- """Abort in-flight TCP I/O via ``shutdown(SHUT_RDWR)`` WITHOUT closing FDs.
-
- ``close()`` from a non-owner thread is unsafe: the SSL BIO caches the raw
- FD, the kernel recycles it, and a flushed TLS record lands in the wrong
- file (#29507 clobbered a SQLite header). ``shutdown()`` is FD-safe from
- any thread; the owning httpx thread releases the FD on unwind.
-
- Returns the number of sockets shut down (logged as ``tcp_force_closed=N``
- for backwards-compatible parsing).
- """
+ """Abort in-flight TCP I/O via ``shutdown(SHUT_RDWR)`` WITHOUT closing FDs. ``close()`` from
+ a non-owner thread is unsafe: the SSL BIO caches the raw FD, the kernel recycles it, and a
+ flushed TLS record lands in the wrong file (once clobbered a SQLite header). ``shutdown()``
+ is FD-safe from any thread. Returns the count (logged as ``tcp_force_closed=N``)."""
import socket as _socket
shutdown_count = 0
try:
for sock in _iter_pool_sockets(client):
try:
- # Clear a blocking timeout so a hung SSL_read notices the shutdown (#85252).
- # Still no close() — that is the #29507 race.
+ # Clear a blocking timeout so a hung SSL_read notices the shutdown. Still no close().
settimeout = getattr(sock, "settimeout", None)
if callable(settimeout):
try:
@@ -3431,9 +3256,7 @@ def force_close_tcp_sockets(client: Any) -> int:
pass
sock.shutdown(_socket.SHUT_RDWR)
except OSError:
- # Already shut down / not connected / FD invalid — all benign.
- pass
- # IMPORTANT (#29507): do NOT call sock.close() here. See docstring.
+ pass # already shut down / not connected / FD invalid: all benign
shutdown_count += 1
except Exception as exc:
_ra().logger.debug("Force-close TCP sockets sweep error: %s", exc)
@@ -3441,30 +3264,13 @@ def force_close_tcp_sockets(client: Any) -> int:
__all__ = [
- "convert_to_trajectory_format",
- "sanitize_tool_call_arguments",
- "repair_message_sequence",
- "strip_think_blocks",
- "recover_with_credential_pool",
- "try_recover_primary_transport",
- "drop_thinking_only_and_merge_users",
- "restore_primary_runtime",
- "extract_reasoning",
- "dump_api_request_debug",
- "prompt_caching_disabled_from_config",
- "blank_cache_policy_stub",
- "plan_cache_sections_for_destination",
- "anthropic_prompt_cache_policy",
- "create_openai_client",
- "switch_model",
- "invoke_tool",
- "repair_tool_call",
- "sanitize_api_messages",
- "looks_like_codex_intermediate_ack",
- "copy_reasoning_content_for_api",
- "cleanup_dead_connections",
- "extract_api_error_context",
- "apply_pending_steer_to_tool_results",
- "_iter_pool_sockets",
+ "convert_to_trajectory_format", "sanitize_tool_call_arguments", "repair_message_sequence",
+ "strip_think_blocks", "recover_with_credential_pool", "try_recover_primary_transport",
+ "drop_thinking_only_and_merge_users", "restore_primary_runtime", "extract_reasoning",
+ "dump_api_request_debug", "prompt_caching_disabled_from_config", "blank_cache_policy_stub",
+ "plan_cache_sections_for_destination", "anthropic_prompt_cache_policy", "create_openai_client",
+ "switch_model", "invoke_tool", "repair_tool_call", "sanitize_api_messages",
+ "looks_like_codex_intermediate_ack", "copy_reasoning_content_for_api", "cleanup_dead_connections",
+ "extract_api_error_context", "apply_pending_steer_to_tool_results", "_iter_pool_sockets",
"force_close_tcp_sockets",
]
From e758eb2e0f801144bcf71433b0a1959b0432e3c0 Mon Sep 17 00:00:00 2001
From: Teknium <127238744+teknium1@users.noreply.github.com>
Date: Wed, 2 Sep 2026 19:37:48 -0700
Subject: [PATCH 12/20] refactor(agent/agent_runtime_helpers):
contextlib.suppress, dedupe-pass single alias map, pack log calls
---
agent/agent_runtime_helpers.py | 229 +++++++++------------------------
1 file changed, 59 insertions(+), 170 deletions(-)
diff --git a/agent/agent_runtime_helpers.py b/agent/agent_runtime_helpers.py
index 695e437457..20395aa34b 100644
--- a/agent/agent_runtime_helpers.py
+++ b/agent/agent_runtime_helpers.py
@@ -6,6 +6,7 @@ Each function takes the parent ``AIAgent`` as ``agent`` except the stateless mes
from __future__ import annotations
+import contextlib
import copy
import json
import logging
@@ -53,8 +54,7 @@ _TOOL_CALL_BLOCK_PATTERNS = tuple(
_NAMED_FUNCTION_BLOCK_PATTERN = re.compile(
r'(?:(?<=^)|(?<=[\n\r.!?:]))[ \t]*'
r']*\bname\s*=[^>]*>'
- r'(?:(?:(?!).)*)',
- re.DOTALL | re.IGNORECASE,
+ r'(?:(?:(?!).)*)', re.DOTALL | re.IGNORECASE,
)
_UNTERMINATED_REASONING_BLOCK_PATTERN = re.compile(
@@ -270,12 +270,8 @@ def sanitize_tool_call_arguments(
log.warning(
"Corrupted tool_call arguments repaired before request "
"(session=%s, message_index=%s, tool_call_id=%s, function=%s, "
- "original_arguments=%r)",
- session_id or "-",
- message_index,
- tool_call_id or "-",
- function_name,
- arguments[:_FULL_ARGS_LOG_BOUND],
+ "original_arguments=%r)", session_id or "-", message_index, tool_call_id or "-",
+ function_name, arguments[:_FULL_ARGS_LOG_BOUND],
)
function["arguments"] = "{}"
existing_tool_msg = _find_tool_result(messages, message_index + 1, tool_call)
@@ -317,9 +313,7 @@ def note_turn_start(agent, turn_id: str):
logger.warning(
"turn %s starting while turn %s (started %.0fs ago) has not "
"completed its turn-end persist (session=%s) — concurrent turns "
- "on one session; transcript writes may interleave",
- turn_id,
- prev,
+ "on one session; transcript writes may interleave", turn_id, prev,
time.time() - prev_started if prev_started else -1.0,
getattr(agent, "session_id", None) or "-",
)
@@ -342,11 +336,8 @@ def note_turn_start(agent, turn_id: str):
"turn %s starting while turn %s (started %.0fs ago) is still "
"in flight on session %s under a different agent object — "
"two routing keys are mapped to one session_id; concurrent "
- "turns on one session; transcript writes may interleave",
- turn_id,
- entry[0],
- now - entry[1] if entry[1] else -1.0,
- session_id,
+ "turns on one session; transcript writes may interleave", turn_id, entry[0],
+ now - entry[1] if entry[1] else -1.0, session_id,
)
overlap = overlap or entry[0]
return overlap
@@ -536,7 +527,6 @@ def _merge_consecutive_users(messages: List[Dict]) -> Tuple[List[Dict], int]:
# A summary carrier followed by a new user row is a deliberate durable shape after
# retry/rewind; never mutate the persisted carrier (sanitizers merge copies later).
from agent.context_compressor import split_user_originated_turn
-
handoff, _ = split_user_originated_turn(prev)
prev_content = prev.get("content", "")
new_content = msg.get("content", "")
@@ -719,8 +709,7 @@ def _recover_auth_failure(agent, pool, *, status_code, has_retried_429, error_co
_ra().logger.info(
"Credential %s — entitlement-shaped 403 from %s; "
"skipping pool refresh (account lacks subscription, "
- "not a transient auth failure).",
- status_code if status_code is not None else "auth",
+ "not a transient auth failure).", status_code if status_code is not None else "auth",
agent.provider or "provider",
)
return False, has_retried_429
@@ -745,9 +734,7 @@ def _recover_auth_failure(agent, pool, *, status_code, has_retried_429, error_co
_ra().logger.warning(
"Credential auth failure persists after %s refreshes for "
"pool entry %s — treating as unrecoverable and allowing "
- "fallback to activate.",
- refresh_counts[refresh_key] - 1,
- refreshed_id,
+ "fallback to activate.", refresh_counts[refresh_key] - 1, refreshed_id,
)
return False, has_retried_429
_ra().logger.info("Credential auth failure — refreshed pool entry %s", getattr(refreshed, 'id', '?'))
@@ -860,8 +847,7 @@ def recover_with_credential_pool(
if upstream:
_ra().logger.info(
"Upstream provider %s rate-limited via aggregator — skipping "
- "credential rotation, deferring to fallback chain",
- upstream,
+ "credential rotation, deferring to fallback chain", upstream,
)
else:
_ra().logger.info(
@@ -922,7 +908,6 @@ def _rebuild_primary_client(agent, rt: Dict[str, Any], *, reason: str) -> None:
# MoA has empty client_kwargs; rebuild via the shared facade factory so the
# reference_callback relay survives recovery.
from agent.moa_loop import build_moa_facade
-
agent.client = build_moa_facade(agent, agent.model)
agent._anthropic_client = None
elif agent.api_mode == "anthropic_messages":
@@ -955,17 +940,14 @@ def try_recover_primary_transport(
# Never hard-close the shared client here: stale streaming workers may still be
# unwinding on the old pool; _retire_shared_openai_client defers FD release to GC.
if getattr(agent, "client", None) is not None:
- try:
+ with contextlib.suppress(Exception):
agent._retire_shared_openai_client(agent.client, reason="primary_recovery")
- except Exception:
- pass
rt = agent._primary_runtime
_apply_primary_runtime_fields(agent, rt)
if agent.api_mode == "anthropic_messages":
_build_anthropic_client_from_runtime(agent, rt)
elif (agent.provider or "").strip().lower() == "moa":
from agent.moa_loop import build_moa_facade
-
agent.client = build_moa_facade(agent, agent.model)
else:
agent.client = agent._create_openai_client(
@@ -974,8 +956,7 @@ def try_recover_primary_transport(
wait_time = min(3 + retry_count, 8)
agent._vprint(
f"{agent.log_prefix}🔁 Transient {error_type} on {agent.provider} — "
- f"rebuilt client, waiting {wait_time}s before one last primary attempt.",
- force=True,
+ f"rebuilt client, waiting {wait_time}s before one last primary attempt.", force=True,
)
time.sleep(wait_time)
return True
@@ -1034,9 +1015,7 @@ def drop_thinking_only_and_merge_users(
return messages
_ra().logger.debug(
"Pre-call sanitizer: dropped %d thinking-only assistant turn(s), "
- "merged %d adjacent user message(s)",
- dropped,
- merges,
+ "merged %d adjacent user message(s)", dropped, merges,
)
return merged
@@ -1058,10 +1037,8 @@ def _primary_reset_gate_blocks(agent, rt, primary_provider, primary_runtime_base
agent._restore_wait_logged = True
logger.info(
"Primary %s rate-limited until %s; staying on fallback "
- "%s/%s until the reset elapses",
- primary_provider or "?",
- datetime.fromtimestamp(next_at).isoformat(timespec="seconds"),
- agent.provider,
+ "%s/%s until the reset elapses", primary_provider or "?",
+ datetime.fromtimestamp(next_at).isoformat(timespec="seconds"), agent.provider,
agent.model,
)
return True, prefetched_pool, prefetched
@@ -1077,11 +1054,9 @@ def _restore_runtime_capabilities(agent, rt: Dict[str, Any]) -> None:
logger.warning("Ignoring malformed runtime capabilities snapshot")
else:
agent.runtime_capabilities = dict(raw)
- elif "capabilities" in rt:
+ elif isinstance(rt.get("capabilities"), dict):
# Snapshots written by the initial capability propagation patch.
- raw = rt["capabilities"]
- if isinstance(raw, dict):
- agent.runtime_capabilities = dict(raw)
+ agent.runtime_capabilities = dict(rt["capabilities"])
def _rebind_primary_credential_pool(agent, primary_provider, matches_primary, load_primary_pool, prefetched_pool, prefetched) -> None:
@@ -1121,8 +1096,7 @@ def _rebind_primary_credential_pool(agent, primary_provider, matches_primary, lo
else:
logger.info(
"Restore skipped pool entry %s (%s): provider %s does not match primary provider %s",
- getattr(entry, "id", "?"),
- getattr(entry, "label", "?"),
+ getattr(entry, "id", "?"), getattr(entry, "label", "?"),
str(getattr(entry, "provider", "") or "").strip().lower() or "?",
primary_provider or "?",
)
@@ -1149,7 +1123,6 @@ def restore_primary_runtime(agent) -> bool:
def _load_primary_pool():
"""Load the primary provider's pool; None when absent or provider-mismatched."""
from agent.credential_pool import load_pool
-
key = resolve_runtime_pool_key(primary_provider, primary_runtime_base_url)
loaded = load_pool(key) if key else None
return loaded if loaded is not None and _matches_primary(loaded) else None
@@ -1208,24 +1181,20 @@ def restore_primary_runtime(agent) -> bool:
agent._provider_fallback_active = False
agent._provider_fallback_route = None
if provider_fallback_active:
- try:
+ # Notification surfaces are best-effort and must never undo a successful restore.
+ with contextlib.suppress(Exception):
agent._emit_status(
f"✅ Primary model restored: {agent.model} via {agent.provider}; "
f"fallback {previous_model} via {previous_provider} is no longer active."
)
- except Exception:
- # Notification surfaces are best-effort and must never undo a successful restore.
- pass
return True
except Exception as e:
logger.warning("Failed to restore primary runtime: %s", e)
return False
-# Which error types indicate a transient transport failure worth
-# one more attempt with a rebuilt client / connection pool.
+# Transient transport failures worth one more attempt with a rebuilt client / connection pool.
_TRANSIENT_TRANSPORT_ERRORS = frozenset({
- "ReadTimeout", "ConnectTimeout", "PoolTimeout",
- "ConnectError", "RemoteProtocolError",
+ "ReadTimeout", "ConnectTimeout", "PoolTimeout", "ConnectError", "RemoteProtocolError",
"APIConnectionError", "APITimeoutError",
})
@@ -1364,7 +1333,6 @@ def _raw_cache_ttl_from_config(default: Any) -> Any:
"""Raw ``prompt_caching.cache_ttl`` config value, or ``default`` when config cannot be read."""
try:
from hermes_cli.config import load_config_readonly
-
return (load_config_readonly().get("prompt_caching", {}) or {}).get("cache_ttl", "5m")
except Exception:
return default
@@ -1386,7 +1354,6 @@ def blank_cache_policy_stub(cache_disabled: Optional[bool] = None):
"""Destination-identity-blank stub for ``anthropic_prompt_cache_policy``; the sole sanctioned
constructor so ``_cache_disabled`` is never omitted (None consults the global config)."""
from types import SimpleNamespace
-
if cache_disabled is None:
cache_disabled = prompt_caching_disabled_from_config()
return SimpleNamespace(
@@ -1407,7 +1374,6 @@ def plan_cache_sections_for_destination(
build_prompt_cache_plan, effective_cache_ttl, envelope_tool_part_cache_markers_supported,
strip_anthropic_cache_control, strip_anthropic_tool_cache_control,
)
-
stub = blank_cache_policy_stub(cache_disabled)
should_cache, native_layout = anthropic_prompt_cache_policy(
stub, provider=provider, base_url=base_url, api_mode=api_mode, model=model
@@ -1461,7 +1427,6 @@ def _moa_aggregator_cache_policy(agent, eff_model: str) -> tuple[bool, bool]:
from hermes_cli.config import load_config as _load_moa_cfg
from hermes_cli.moa_config import resolve_moa_preset
from hermes_cli.runtime_provider import resolve_runtime_provider
-
preset = resolve_moa_preset(_load_moa_cfg().get("moa") or {}, eff_model or None)
agg = preset.get("aggregator") or {}
agg_provider = str(agg.get("provider") or "").strip()
@@ -1491,7 +1456,6 @@ def _route_may_be_custom(agent, eff_provider: str, provider_lower: str, eff_base
# custom_provider_aliases) so spelling differences don't drop declarations.
from hermes_cli.providers import custom_provider_aliases
from hermes_cli.route_identity import normalize_route_base_url
-
provider_ids = {provider_lower}
if provider_lower.startswith("custom:"):
provider_ids.add(provider_lower.removeprefix("custom:"))
@@ -1513,7 +1477,6 @@ def _route_may_be_custom(agent, eff_provider: str, provider_lower: str, eff_base
# list for ordinary built-in routes.
try:
from hermes_cli.providers import get_provider
-
# allow_network=False: never trigger a registry fetch from the send path;
# a catalog miss degrades to the conservative capability lookup.
provider_def = get_provider(eff_provider, allow_network=False)
@@ -1578,7 +1541,6 @@ def anthropic_prompt_cache_policy(
):
try:
from hermes_cli.config import get_custom_provider_model_capability
-
custom_prompt_caching = get_custom_provider_model_capability(
model=eff_model, base_url=eff_base_url, capability="prompt_caching",
custom_providers=getattr(agent, "_custom_providers", None),
@@ -1598,7 +1560,6 @@ def anthropic_prompt_cache_policy(
)
if is_anthropic_wire and is_minimax_route:
from agent.model_metadata import _model_name_suggests_minimax_m3
-
if _model_name_suggests_minimax_m3(eff_model):
return False, False
if is_native_anthropic:
@@ -1628,7 +1589,6 @@ def anthropic_prompt_cache_policy(
# (pi-mono's "alibaba" cacheControlFormat). DeepSeek on OpenCode is excluded: its relay
# 400s on block-array content. Family set/predicate shared with the effective_cache_ttl clamp.
from agent.prompt_caching import ALIBABA_FAMILY_PROVIDERS, is_qwen_model
-
if provider_lower in ALIBABA_FAMILY_PROVIDERS and is_qwen_model(model_lower):
return True, False
return False, False
@@ -1661,8 +1621,7 @@ def _provider_supplied_client(agent, client_kwargs: dict) -> Any | None:
except Exception:
_ra().logger.warning(
"Provider profile %r failed to create a client; falling back to the standard client path",
- getattr(profile, "name", provider_name) or "?",
- exc_info=True,
+ getattr(profile, "name", provider_name) or "?", exc_info=True,
)
return None
@@ -1704,7 +1663,6 @@ def _ensure_copilot_headers(client_kwargs: dict) -> None:
def _gemini_native_client(agent, client_kwargs: dict, httpx_verify, *, reason: str, shared: bool):
"""Native Gemini client when the base_url is the Gemini API, else None."""
from agent.gemini_native_adapter import GeminiNativeClient, is_native_gemini_base_url
-
base_url = str(client_kwargs.get("base_url", "") or "")
if not is_native_gemini_base_url(base_url):
return None
@@ -1770,12 +1728,10 @@ def create_openai_client(agent, client_kwargs: dict, *, reason: str, shared: boo
# Authorization default_header overrides the SDK's "Bearer ".
if agent.provider == "opencode-free":
from hermes_cli.models import opencode_zen_free_headers
-
client_kwargs["default_headers"] = {**(client_kwargs.get("default_headers") or {}), **opencode_zen_free_headers()}
# All primary construction and recovery paths must identify Hermes to the official Codex
# endpoint, including snapshots with custom header overrides.
from agent.codex_headers import apply_required_codex_headers
-
apply_required_codex_headers(
client_kwargs, access_token=client_kwargs.get("api_key", ""),
base_url=str(client_kwargs.get("base_url", "")),
@@ -1792,7 +1748,6 @@ def _apply_switched_provider_request_overrides(agent, new_provider):
``agent_init._merge_custom_provider_extra_body``) so a different model at the same endpoint
never inherits another's ``extra_body``. Stale ``extra_body`` cleared; ``service_tier``/``speed`` kept."""
from agent.agent_init import _custom_provider_extra_body_for_agent
-
# Prefer the init-time cache (agent._custom_providers); reload only if absent.
custom_providers = getattr(agent, "_custom_providers", None)
if custom_providers is None:
@@ -1802,10 +1757,8 @@ def _apply_switched_provider_request_overrides(agent, new_provider):
except Exception:
custom_providers = []
new_extra_body = _custom_provider_extra_body_for_agent(
- provider=new_provider,
- model=getattr(agent, "model", "") or "",
- base_url=getattr(agent, "base_url", "") or "",
- custom_providers=custom_providers or [],
+ provider=new_provider, model=getattr(agent, "model", "") or "",
+ base_url=getattr(agent, "base_url", "") or "", custom_providers=custom_providers or [],
)
overrides = dict(getattr(agent, "request_overrides", {}) or {})
overrides.pop("extra_body", None) # always drop the previous provider's extra_body
@@ -1850,10 +1803,8 @@ 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
- try:
+ with contextlib.suppress(Exception):
setattr(agent, name, value)
- except Exception: # noqa: BLE001
- pass
def _resolve_switch_destination(agent, new_model, new_provider, base_url, api_mode, capabilities, old_norm, new_norm):
@@ -1861,7 +1812,6 @@ def _resolve_switch_destination(agent, new_model, new_provider, base_url, api_mo
from hermes_cli.providers import determine_api_mode
from agent.native_compaction import resolve_native_compaction_capabilities
from hermes_cli.models import opencode_provider_family
-
# Pass model so dual-wire providers (Nous Portal anthropic/* -> Messages) resolve correctly.
if not api_mode:
api_mode = determine_api_mode(new_provider, base_url, model=new_model)
@@ -1897,7 +1847,6 @@ def _build_switched_client(agent, new_provider, api_key, base_url, api_mode, new
"""Build the client for the switched-to destination (MoA facade / native Anthropic / OpenAI wire)."""
if new_norm == "moa":
from agent.moa_loop import build_moa_facade
-
# MoA speaks only chat.completions via the MoAClient facade; the aggregator's real
# transport is applied inside the fan-out. Pin api_mode so the loop never dispatches
# client.responses.create against the facade (matches agent_init.py).
@@ -1909,7 +1858,6 @@ def _build_switched_client(agent, new_provider, api_key, base_url, api_mode, new
return
if api_mode == "anthropic_messages":
from agent.anthropic_adapter import build_anthropic_client, resolve_anthropic_token, _is_oauth_token
-
# Only fall back to ANTHROPIC_TOKEN for native Anthropic; other anthropic_messages
# providers must never receive Anthropic credentials.
is_native_anthropic = new_provider == "anthropic"
@@ -1923,8 +1871,7 @@ def _build_switched_client(agent, new_provider, api_key, base_url, api_mode, new
except Exception as _mm_exc: # noqa: BLE001
logger.warning(
"MiniMax OAuth: failed to install per-request token provider "
- "on switch (%s); using static bearer.",
- _mm_exc,
+ "on switch (%s); using static bearer.", _mm_exc,
)
agent.api_key = effective_key
agent._anthropic_api_key = effective_key
@@ -1946,7 +1893,6 @@ def _build_switched_client(agent, new_provider, api_key, base_url, api_mode, new
apply_custom_provider_tls_to_client_kwargs, get_compatible_custom_providers,
load_config_readonly,
)
-
# Read live config, not agent._custom_providers, so mid-session ssl_ca_cert /
# ssl_verify edits are honored.
apply_custom_provider_tls_to_client_kwargs(
@@ -2002,8 +1948,7 @@ def _swap_switch_runtime(agent, new_model, new_provider, api_key, base_url, api_
except Exception as _pool_exc: # noqa: BLE001
logger.warning(
"switch_model: credential pool reload failed for %s (%s); "
- "continuing without pool rotation this turn",
- new_provider, _pool_exc,
+ "continuing without pool rotation this turn", new_provider, _pool_exc,
)
_build_switched_client(agent, new_provider, api_key, base_url, api_mode, new_norm)
sync_credential_pool_entry_id(agent)
@@ -2016,7 +1961,6 @@ def _resolve_switch_context_length(agent, snapshot):
from hermes_cli.config import (
get_compatible_custom_providers, get_custom_provider_context_length, load_config
)
-
custom_providers = get_compatible_custom_providers(load_config())
intent = get_custom_provider_context_length(
model=agent.model, base_url=agent.base_url, custom_providers=custom_providers
@@ -2075,7 +2019,7 @@ def _update_switch_compressor(agent, custom_providers, effective_context_length,
def _build_primary_runtime_snapshot(agent, api_mode) -> Dict[str, Any]:
"""The ``_primary_runtime`` record that persists a switch across turns."""
- cc = agent.context_compressor if hasattr(agent, "context_compressor") and agent.context_compressor else None
+ cc = getattr(agent, "context_compressor", None) or None
rt = {
"model": agent.model,
"provider": agent.provider,
@@ -2092,12 +2036,12 @@ def _build_primary_runtime_snapshot(agent, api_mode) -> Dict[str, Any]:
# resurrects PRE-switch overrides from the stale init snapshot.
"request_overrides": dict(getattr(agent, "request_overrides", {}) or {}),
"runtime_capabilities": dict(getattr(agent, "runtime_capabilities", {}) or {}),
- "compressor_model": getattr(cc, "model", agent.model) if cc else agent.model,
- "compressor_base_url": getattr(cc, "base_url", agent.base_url) if cc else agent.base_url,
- "compressor_api_key": getattr(cc, "api_key", "") if cc else "",
- "compressor_provider": getattr(cc, "provider", agent.provider) if cc else agent.provider,
+ "compressor_model": getattr(cc, "model", agent.model),
+ "compressor_base_url": getattr(cc, "base_url", agent.base_url),
+ "compressor_api_key": getattr(cc, "api_key", ""),
+ "compressor_provider": getattr(cc, "provider", agent.provider),
"compressor_context_length": cc.context_length if cc else 0,
- "compressor_api_mode": getattr(cc, "api_mode", agent.api_mode) if cc else agent.api_mode,
+ "compressor_api_mode": getattr(cc, "api_mode", agent.api_mode),
"compressor_threshold_tokens": cc.threshold_tokens if cc else 0,
}
if api_mode == "anthropic_messages":
@@ -2187,7 +2131,6 @@ def switch_model(
try:
from hermes_constants import resolve_reasoning_config
from hermes_cli.config import load_config as _sm_load_config
-
agent.reasoning_config = resolve_reasoning_config(_sm_load_config() or {}, agent.model)
logger.info(
"switch_model: reasoning_config resolved for %s: %s", agent.model, agent.reasoning_config
@@ -2241,14 +2184,12 @@ def invoke_tool(agent, function_name: str, function_args: dict, effective_task_i
from agent.inline_tool_executors import (
InlineToolContext, emit_terminal_post_tool_call, resolve_invoke_tool_executor, tool_hook_ids
)
-
if not isinstance(function_args, dict):
function_args = {}
hook_ids = tool_hook_ids(agent, effective_task_id, tool_call_id)
_tool_middleware_trace = list(tool_request_middleware_trace or [])
try:
from hermes_cli.middleware import apply_tool_request_middleware
-
if not skip_tool_request_middleware:
_tool_request_mw = apply_tool_request_middleware(function_name, function_args, **hook_ids)
function_args = _tool_request_mw.payload
@@ -2307,7 +2248,6 @@ def invoke_tool(agent, function_name: str, function_args: dict, effective_task_i
if skip_tool_execution_middleware:
return _execute(function_args)
from hermes_cli.middleware import run_tool_execution_middleware
-
return run_tool_execution_middleware(
function_name, function_args,
lambda next_args: _execute(next_args if isinstance(next_args, dict) else function_args),
@@ -2320,7 +2260,6 @@ def repair_tool_call(agent, tool_name: str) -> str | None:
``TodoTool_tool`` reduces fully, then fuzzy match) before aborting. Returns the repaired
name if in valid_tool_names, else None."""
from difflib import get_close_matches
-
if not tool_name:
return None
# VolcEngine api/plan leaks XML attribute fragments into tool_use.name
@@ -2432,7 +2371,6 @@ def fill_empty_non_final_wire_payload(msg: Dict[str, Any], *, is_final: bool) ->
def _session_id_for_heal_log() -> str:
try:
from hermes_logging import _session_context
-
return str(getattr(_session_context, "session_id", None) or "")
except Exception:
return ""
@@ -2440,16 +2378,11 @@ def _session_id_for_heal_log() -> str:
def _heal_escalation_threshold() -> int:
"""Escalation threshold from ``agent.sanitizer_heal_escalation_threshold``, else the module default (fail-safe on any read error)."""
- try:
+ with contextlib.suppress(Exception):
from hermes_cli.config import load_config_readonly
-
- raw = (load_config_readonly().get("agent", {}) or {}).get(
- "sanitizer_heal_escalation_threshold"
- )
+ raw = (load_config_readonly().get("agent", {}) or {}).get("sanitizer_heal_escalation_threshold")
if raw is not None:
return int(raw)
- except Exception:
- pass
return _EMPTY_HEAL_ESCALATE_AFTER
@@ -2519,13 +2452,8 @@ def _log_empty_non_final_heal(healed: int) -> None:
"healed %d empty non-final message(s) this send; heal pattern: "
"%d heal events / %d messages healed this session "
"(%d in the current session window, threshold %d). The transcript "
- "is being repaired on every send; /new drops the poisoned turns.",
- key,
- healed,
- total_events,
- total_healed,
- count,
- threshold,
+ "is being repaired on every send; /new drops the poisoned turns.", key, healed,
+ total_events, total_healed, count, threshold,
)
return
_ra().logger.warning(
@@ -2533,36 +2461,25 @@ def _log_empty_non_final_heal(healed: int) -> None:
"substituting placeholder content — an empty-content turn was in "
"the transcript and would 400 the request ('messages must have "
"non-empty content' / INVALID_REQUEST_BODY). Self-recovering the "
- "poisoned transcript in memory; no restart needed.",
- healed,
+ "poisoned transcript in memory; no restart needed.", healed,
)
-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]] = []
healed = 0
last_idx = len(messages) - 1
for idx, msg in enumerate(messages):
- if (
- idx != last_idx
- and isinstance(msg, dict)
- # Tool results are checked by their own pairing pass; empty ones are a separate concern.
- and msg.get("role") in ("assistant", "user")
- and not _msg_has_payload(msg)
- ):
+ # Tool results are checked by their own pairing pass; empty ones are a separate concern.
+ if idx != last_idx and isinstance(msg, dict) and msg.get("role") in ("assistant", "user") and not _msg_has_payload(msg):
# Shallow-copy so stored history / prompt caching stays byte-stable.
- fixed = dict(msg)
- fixed["content"] = _INTERRUPTED_PLACEHOLDER
- repaired.append(fixed)
+ repaired.append({**msg, "content": _INTERRUPTED_PLACEHOLDER})
healed += 1
else:
repaired.append(msg)
@@ -2667,16 +2584,13 @@ def _repair_nameless_tool_calls(messages: List[Dict[str, Any]]) -> None:
continue
_ra().logger.warning(
"Pre-call sanitizer: repairing tool_call with empty function.name -> %r (id=%s)",
- sentinel,
- _ra().AIAgent._get_tool_call_id_static(tc),
+ sentinel, _ra().AIAgent._get_tool_call_id_static(tc),
)
if isinstance(fn, dict):
fn["name"] = sentinel
elif fn is not None and hasattr(fn, "name"):
- try:
+ with contextlib.suppress(Exception):
fn.name = sentinel
- except Exception:
- pass
elif isinstance(tc, dict):
tc["function"] = {"name": sentinel, "arguments": "{}"}
@@ -2759,8 +2673,7 @@ def _pair_tool_calls_positionally(messages: List[Dict[str, Any]]) -> List[Dict[s
if stubs:
_ra().logger.debug(
"Pre-call sanitizer: added %d stub tool result(s) for "
- "positionally unanswered tool call(s)",
- stubs,
+ "positionally unanswered tool call(s)", stubs,
)
return paired if (dropped or stubs) else messages
@@ -2770,10 +2683,8 @@ def _dedupe_tool_call_ids(messages: List[Dict[str, Any]]) -> List[Dict[str, Any]
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."""
- seen_assistant_call_ids: set = set()
- outstanding_call_ids: set = set()
+ outstanding: Dict[str, int] = {} # every alias of an unanswered call -> its group id
outstanding_groups: Dict[int, frozenset] = {}
- variant_to_group: Dict[str, int] = {}
next_group_id = 0
deduped: List[Dict[str, Any]] = []
removed = 0
@@ -2783,7 +2694,7 @@ def _dedupe_tool_call_ids(messages: List[Dict[str, Any]]) -> List[Dict[str, Any]
kept_tcs = []
for tc in msg.get("tool_calls") or []:
variants = tool_call_id_variants(tc)
- if variants and variants & seen_assistant_call_ids:
+ if variants and variants & outstanding.keys():
removed += 1
continue
if variants:
@@ -2791,9 +2702,7 @@ def _dedupe_tool_call_ids(messages: List[Dict[str, Any]]) -> List[Dict[str, Any]
next_group_id += 1
outstanding_groups[group_id] = variants
for variant in variants:
- seen_assistant_call_ids.add(variant)
- outstanding_call_ids.add(variant)
- variant_to_group.setdefault(variant, group_id)
+ outstanding.setdefault(variant, group_id)
kept_tcs.append(tc)
if kept_tcs:
msg = {**msg, "tool_calls": kept_tcs}
@@ -2801,11 +2710,7 @@ def _dedupe_tool_call_ids(messages: List[Dict[str, Any]]) -> List[Dict[str, Any]
msg = {k: v for k, v in msg.items() if k != "tool_calls"}
elif role == "tool":
result_variants = tool_result_id_variants(msg.get("tool_call_id"))
- candidate_groups = {
- variant_to_group[variant]
- for variant in result_variants
- if variant in variant_to_group and variant in outstanding_call_ids
- }
+ candidate_groups = {outstanding[v] for v in result_variants if v in outstanding}
if result_variants and not candidate_groups:
removed += 1
continue
@@ -2813,10 +2718,8 @@ def _dedupe_tool_call_ids(messages: List[Dict[str, Any]]) -> List[Dict[str, Any]
# Consume EVERY variant of the matched call; ids are re-armed by the next call reusing them.
group_id = min(candidate_groups)
for variant in outstanding_groups.pop(group_id, frozenset()):
- outstanding_call_ids.discard(variant)
- seen_assistant_call_ids.discard(variant)
- if variant_to_group.get(variant) == group_id:
- variant_to_group.pop(variant, None)
+ if outstanding.get(variant) == group_id:
+ del outstanding[variant]
deduped.append(msg)
if not removed:
return messages
@@ -2853,8 +2756,7 @@ def _realign_tool_result_names(messages: List[Dict[str, Any]]) -> List[Dict[str,
return messages
_ra().logger.debug(
"Pre-call sanitizer: realigned %d tool result name(s) with their "
- "tool_call function name (%s)",
- len(realigned),
+ "tool_call function name (%s)", len(realigned),
", ".join(f"{was} -> {now}" for was, now in realigned),
)
return aligned
@@ -2907,7 +2809,6 @@ def looks_like_codex_intermediate_ack(
# ``user_message`` may be a multi-part content list (vision via the OpenAI-compat
# server); a list survives ``or ""`` and ``.strip()`` raises, so flatten first.
from agent.codex_responses_adapter import _summarize_user_message_for_log
-
user_text = _summarize_user_message_for_log(user_message).strip().lower()
return (
any(marker in user_text for marker in _ACK_WORKSPACE_MARKERS)
@@ -2922,8 +2823,7 @@ def looks_like_codex_intermediate_ack(
_TRAILING_CONTINUE_INTENT_RE = re.compile(
r"(?:\blet me now\b|\bi(?:['\u2019])?ll now\b|\bi will now\b"
r"|\bnow i(?:['\u2019]ll| will)\b|\bnext[,:] i\b)"
- r"[^.!?\n]{0,100}[.:\u2026]?\s*$",
- re.IGNORECASE,
+ r"[^.!?\n]{0,100}[.:\u2026]?\s*$", re.IGNORECASE,
)
# Content longer than this is a substantive reply, not a dangling ack.
@@ -2961,7 +2861,6 @@ def intent_ack_continuation_mode(agent) -> str:
def copy_reasoning_content_for_api(agent, source_msg: dict, api_msg: dict) -> None:
"""Forward reasoning fields onto an API replay message; policy lives in ``agent.message_sanitization.apply_reasoning_content_policy``."""
from agent.message_sanitization import apply_reasoning_content_policy
-
apply_reasoning_content_policy(
source_msg, api_msg, agent._needs_thinking_reasoning_pad()
)
@@ -2973,7 +2872,6 @@ def reapply_reasoning_echo_for_provider(agent, api_messages: list) -> int:
(DeepSeek/Kimi/MiMo) 400 without the pad, strict ones (Mistral, Cerebras, Groq) 400/422
with it. Idempotent; returns the number of assistant turns changed."""
from agent.message_sanitization import reapply_reasoning_echo
-
return reapply_reasoning_echo(
api_messages, agent._needs_thinking_reasoning_pad()
)
@@ -3086,10 +2984,8 @@ def _socket_is_dead(sock) -> bool:
except OSError:
return True
finally:
- try:
+ with contextlib.suppress(OSError):
sock.setblocking(True)
- except OSError:
- pass
def cleanup_dead_connections(agent) -> bool:
@@ -3113,8 +3009,7 @@ _RESETS_IN_RE = re.compile(
r"resets?\s+in\s+"
r"(?:(\d+(?:\.\d+)?)\s*(?:h|hr|hrs|hour|hours)\b\s*)?"
r"(?:(\d+(?:\.\d+)?)\s*(?:m|min|mins|minute|minutes)\b\s*)?"
- r"(?:(\d+(?:\.\d+)?)\s*(?:s|sec|secs|second|seconds)\b)?",
- re.IGNORECASE,
+ r"(?:(\d+(?:\.\d+)?)\s*(?:s|sec|secs|second|seconds)\b)?", re.IGNORECASE,
)
_RETRY_AFTER_SECONDS_RE = re.compile(r"retry\s+(?:after\s+)?(\d+(?:\.\d+)?)\s*(?:sec|secs|seconds|s\b)", re.IGNORECASE)
@@ -3135,10 +3030,8 @@ def _reset_delay_from_message(message: str) -> Optional[float]:
def _set_reset_from_retry_after(context: Dict[str, Any], retry_after: Any) -> None:
if retry_after in {None, ""} or "reset_at" in context:
return
- try:
+ with contextlib.suppress(TypeError, ValueError):
context["reset_at"] = time.time() + float(retry_after)
- except (TypeError, ValueError):
- pass
def extract_api_error_context(error: Exception) -> Dict[str, Any]:
@@ -3230,8 +3123,7 @@ def apply_pending_steer_to_tool_results(agent, messages: list, num_tool_msgs: in
# Fall back to string replacement if content shape is unexpected.
messages[target_idx]["content"] = f"{existing_content}{marker}"
_ra().logger.info(
- "Delivered /steer to agent after tool batch (%d chars): %s",
- len(steer_text),
+ "Delivered /steer to agent after tool batch (%d chars): %s", len(steer_text),
steer_text[:120] + ("..." if len(steer_text) > 120 else ""),
)
@@ -3242,7 +3134,6 @@ def force_close_tcp_sockets(client: Any) -> int:
flushed TLS record lands in the wrong file (once clobbered a SQLite header). ``shutdown()``
is FD-safe from any thread. Returns the count (logged as ``tcp_force_closed=N``)."""
import socket as _socket
-
shutdown_count = 0
try:
for sock in _iter_pool_sockets(client):
@@ -3250,10 +3141,8 @@ def force_close_tcp_sockets(client: Any) -> int:
# Clear a blocking timeout so a hung SSL_read notices the shutdown. Still no close().
settimeout = getattr(sock, "settimeout", None)
if callable(settimeout):
- try:
+ with contextlib.suppress(OSError):
settimeout(0)
- except OSError:
- pass
sock.shutdown(_socket.SHUT_RDWR)
except OSError:
pass # already shut down / not connected / FD invalid: all benign
From d1b0553203e0aca55495b1109bd10aa52a09d6a4 Mon Sep 17 00:00:00 2001
From: Teknium <127238744+teknium1@users.noreply.github.com>
Date: Wed, 2 Sep 2026 19:41:28 -0700
Subject: [PATCH 13/20] refactor(agent/agent_runtime_helpers): compact comments
and intra-function blank lines
---
agent/agent_runtime_helpers.py | 347 ++++++++++++++-------------------
1 file changed, 149 insertions(+), 198 deletions(-)
diff --git a/agent/agent_runtime_helpers.py b/agent/agent_runtime_helpers.py
index 20395aa34b..f2b1ddccab 100644
--- a/agent/agent_runtime_helpers.py
+++ b/agent/agent_runtime_helpers.py
@@ -5,7 +5,6 @@ Each function takes the parent ``AIAgent`` as ``agent`` except the stateless mes
"""
from __future__ import annotations
-
import contextlib
import copy
import json
@@ -16,7 +15,6 @@ import time
from datetime import datetime
from pathlib import Path
from typing import Any, Dict, List, Optional, Tuple
-
from hermes_cli.timeouts import get_provider_request_timeout
from agent.message_sanitization import (
_FULL_ARGS_LOG_BOUND, coalesce_tool_call_id, tool_call_id_variants, tool_result_id_variants
@@ -30,21 +28,15 @@ from agent.credential_pool import (
from agent.error_classifier import FailoverReason
from agent.turn_context import drop_stale_api_content
from utils import base_url_host_matches, base_url_hostname, env_var_enabled, atomic_json_write
-
logger = logging.getLogger(__name__)
-
# Cap same-entry OAuth refreshes on a persistent auth failure, else a single-entry pool re-mints forever.
_MAX_AUTH_REFRESH_ATTEMPTS = 2
-
-
_REASONING_TAG_NAMES = ("think", "thinking", "reasoning", "REASONING_SCRATCHPAD", "thought")
_TOOL_CALL_TAG_NAMES = ("tool_call", "tool_calls", "tool_result", "function_call", "function_calls")
-
_REASONING_BLOCK_PATTERNS = tuple(
re.compile(rf"<{name}>.*?{name}>", re.DOTALL | re.IGNORECASE) for name in _REASONING_TAG_NAMES
)
-
_TOOL_CALL_BLOCK_PATTERNS = tuple(
re.compile(rf"<{name}\b[^>]*>.*?{name}>", re.DOTALL | re.IGNORECASE)
for name in _TOOL_CALL_TAG_NAMES
@@ -56,15 +48,12 @@ _NAMED_FUNCTION_BLOCK_PATTERN = re.compile(
r']*\bname\s*=[^>]*>'
r'(?:(?:(?!).)*)', re.DOTALL | re.IGNORECASE,
)
-
_UNTERMINATED_REASONING_BLOCK_PATTERN = re.compile(
rf'(?:^|\n)[ \t]*<(?:{"|".join(_REASONING_TAG_NAMES)})\b[^>]*>.*$', re.DOTALL | re.IGNORECASE
)
-
_ORPHAN_REASONING_TAG_PATTERN = re.compile(
rf'?(?:{"|".join(_REASONING_TAG_NAMES)})>\s*', re.IGNORECASE
)
-
_STRAY_TOOL_CALL_CLOSER_PATTERN = re.compile(
rf'(?:{"|".join(_TOOL_CALL_TAG_NAMES)}|function)>\s*', re.IGNORECASE
)
@@ -76,10 +65,10 @@ def _ra():
return run_agent
-AGENT_RUNTIME_POST_HOOK_TOOL_NAMES = frozenset(
- {"todo_list", "session_search", "memory", "clarify", "read_terminal", "desktop_preview", "drive_preview", "annotate_preview", "read_window_below", "setup_mcp", "gui_tour", "delegate_task"}
-)
-
+AGENT_RUNTIME_POST_HOOK_TOOL_NAMES = frozenset({
+ "todo_list", "session_search", "memory", "clarify", "read_terminal", "desktop_preview",
+ "drive_preview", "annotate_preview", "read_window_below", "setup_mcp", "gui_tour", "delegate_task",
+})
_TRAJECTORY_SYSTEM_PROMPT = (
"You are a function calling AI model. You are provided with function signatures within XML tags. "
@@ -157,8 +146,8 @@ def _trajectory_tool_responses(msg: Dict[str, Any], messages: 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.
+ # Trajectories are text-only: swap image-bearing tool messages for their text_summary so ~1MB
+ # base64 blobs are not embedded.
messages = [_trajectory_normalize_msg(m) for m in messages]
trajectory = [
{"from": "system", "value": _TRAJECTORY_SYSTEM_PROMPT.format(tools=agent._format_tools_for_system_message())},
@@ -228,7 +217,6 @@ def sanitize_tool_call_arguments(
messages: list, *, logger=None, session_id: str = None, cursor: Optional[dict] = None
) -> int:
"""Repair corrupted assistant tool-call argument JSON in-place.
-
``cursor["prefix"]`` holds strong refs (not ``id()``: address reuse aliases) to the
messages validated last call; the ``is``-identical prefix is skipped. Safe because only
the surrogate sanitizers mutate live dicts; every other path replaces dicts, breaking identity.
@@ -261,12 +249,12 @@ def sanitize_tool_call_arguments(
continue
except json.JSONDecodeError:
pass
- # Canonical ``call_id || id`` precedence so scan and stub share the id the
- # pipeline uses; bare ``id`` misses Codex call_id results and orphans a stub.
+ # Canonical ``call_id || id`` precedence so scan and stub share the id the pipeline
+ # uses; bare ``id`` misses Codex call_id results and orphans a stub.
tool_call_id = _ra().AIAgent._get_tool_call_id_static(tool_call) or None
function_name = function.get("name", "?")
- # Log the FULL (bounded) argument string: we are about to overwrite the only
- # copy, which may hold real user content from a truncated write_file/patch.
+ # Log the FULL (bounded) argument string: we are about to overwrite the only copy, which
+ # may hold real user content from a truncated write_file/patch.
log.warning(
"Corrupted tool_call arguments repaired before request "
"(session=%s, message_index=%s, tool_call_id=%s, function=%s, "
@@ -286,15 +274,15 @@ def sanitize_tool_call_arguments(
repaired += 1
message_index += 1
if cursor is not None:
- # Strong refs to the objects validated this call; any divergence (compression,
- # undo, repair, steer) forces a re-scan from that index.
+ # Strong refs to the objects validated this call; any divergence (compression, undo, repair,
+ # steer) forces a re-scan from that index.
cursor["prefix"] = messages[:]
return repaired
-# 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.
+# 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_LOCK = threading.Lock()
@@ -318,18 +306,17 @@ def note_turn_start(agent, turn_id: str):
getattr(agent, "session_id", None) or "-",
)
overlap = prev
- # Cross-agent leg: same session_id in flight under another agent object (busy guard is
- # keyed by routing key and cannot see it). Persist-disabled forks share the parent's
- # session_id but never write, so they must not register or pop here
- # (note_turn_persisted skips them symmetrically).
+ # Cross-agent leg: same session_id in flight under another agent object (busy guard is keyed by
+ # routing key and cannot see it). Persist-disabled forks share the parent's session_id but never
+ # write, so they must not register or pop here (note_turn_persisted skips them symmetrically).
session_id = getattr(agent, "session_id", None)
if session_id and not getattr(agent, "_persist_disabled", False):
now = time.time()
with _INFLIGHT_TURNS_LOCK:
entry = _INFLIGHT_TURNS_BY_SESSION.get(session_id)
_INFLIGHT_TURNS_BY_SESSION[session_id] = (turn_id, now)
- # Record the session id registered under: compression can rotate agent.session_id
- # mid-turn and persist must pop the slot actually held.
+ # Record the session id registered under: compression can rotate agent.session_id mid-turn
+ # and persist must pop the slot actually held.
agent._inflight_turn_session_id = session_id
if entry and entry[0] not in (turn_id, prev):
logger.warning(
@@ -348,8 +335,8 @@ def note_turn_persisted(agent):
design: on a real overlap the first persist clears the second slot, so the tripwire
under-reports rather than double-reports."""
agent._inflight_turn_id = None
- # Persist-disabled forks never registered a slot; popping here would
- # steal the live parent turn's slot (symmetric with note_turn_start).
+ # Persist-disabled forks never registered a slot; popping here would steal the live parent
+ # turn's slot (symmetric with note_turn_start).
if not getattr(agent, "_persist_disabled", False):
session_id = getattr(agent, "_inflight_turn_session_id", None) or getattr(agent, "session_id", None)
if session_id:
@@ -376,8 +363,8 @@ def _merge_assistant_into(prev: Dict, msg: Dict) -> None:
elif prev_calls:
prev["tool_calls"] = prev_calls
else:
- # Drop a stale ``tool_calls: []`` at the source: strict providers (DeepSeek v4,
- # Kimi) 400 on it and it persists into replayed history.
+ # Drop a stale ``tool_calls: []`` at the source: strict providers (DeepSeek v4, Kimi) 400 on
+ # it and it persists into replayed history.
prev.pop("tool_calls", None)
# Concatenate plain-text content only; leave multimodal (list) content alone.
prev_content = prev.get("content")
@@ -391,12 +378,12 @@ def _merge_assistant_into(prev: Dict, msg: Dict) -> None:
elif not prev_content and new_content is not None:
prev["content"] = new_content
content_rewritten = new_content != prev_content
- # Carry reasoning_content from the later turn only if the earlier lacks it
- # (strict thinking providers need one on the merged tool-call turn).
+ # Carry reasoning_content from the later turn only if the earlier lacks it (strict thinking
+ # providers need one on the merged tool-call turn).
if not prev.get("reasoning_content") and msg.get("reasoning_content"):
prev["reasoning_content"] = msg["reasoning_content"]
- # A stale ``api_content`` sidecar overrides ``content`` at API-build time and would
- # replay pre-merge bytes; drop it only when content actually changed.
+ # A stale ``api_content`` sidecar overrides ``content`` at API-build time and would replay
+ # pre-merge bytes; drop it only when content actually changed.
if content_rewritten:
drop_stale_api_content(prev)
@@ -553,7 +540,6 @@ _SEQUENCE_REPAIR_PASSES = (
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.
Passes in order: merge consecutive assistant turns (BEFORE orphan detection so the merged
@@ -611,11 +597,11 @@ def _flatten_content_text(content: Any) -> str:
return str(content)
-# Order matters: closed pairs first (case-insensitive so mixed-case tags don't fall through
-# to the unterminated pass and eat trailing content), then tool-call XML blocks, the
-# boundary+name-gated block, the unterminated reasoning block, stray orphan
-# reasoning tags, and finally stray tool-call CLOSERS only (bare/unterminated
-# is kept: a truncated streaming tail may still be valuable, matching OpenClaw's asymmetry).
+# Order matters: closed pairs first (case-insensitive so mixed-case tags don't fall through to the
+# unterminated pass and eat trailing content), then tool-call XML blocks, the boundary+name-gated
+# block, the unterminated reasoning block, stray orphan reasoning tags, and finally stray
+# tool-call CLOSERS only (bare/unterminated is kept: a truncated streaming tail may still
+# be valuable, matching OpenClaw's asymmetry).
_THINK_STRIP_PATTERNS = (
*_REASONING_BLOCK_PATTERNS, *_TOOL_CALL_BLOCK_PATTERNS, _NAMED_FUNCTION_BLOCK_PATTERN,
_UNTERMINATED_REASONING_BLOCK_PATTERN, _ORPHAN_REASONING_TAG_PATTERN,
@@ -655,7 +641,6 @@ _STATUS_TO_FAILOVER_REASON = {
402: FailoverReason.billing, 429: FailoverReason.rate_limit, 401: FailoverReason.auth,
403: FailoverReason.auth,
}
-
_USAGE_LIMIT_REASON_TOKENS = ("usage_limit_reached", "gousagelimit")
_USAGE_LIMIT_MESSAGE_TOKENS = ("usage limit reached", "usage limit has been reached")
@@ -713,15 +698,15 @@ def _recover_auth_failure(agent, pool, *, status_code, has_retried_429, error_co
agent.provider or "provider",
)
return False, has_retried_429
- # Refresh the entry that supplied the failing key, not current(): refreshing a
- # healthy entry burns its single-use refresh token for a failure it never had.
+ # Refresh the entry that supplied the failing key, not current(): refreshing a healthy entry
+ # burns its single-use refresh token for a failure it never had.
refresh_kwargs = {"api_key_hint": api_key_hint}
if credential_id:
refresh_kwargs["credential_id"] = credential_id
refreshed = pool.try_refresh_matching(**refresh_kwargs)
if refreshed is not None:
- # try_refresh_matching() reports success even when upstream keeps rejecting;
- # cap same-entry refreshes so a single-entry pool falls through to fallback.
+ # try_refresh_matching() reports success even when upstream keeps rejecting; cap same-entry
+ # refreshes so a single-entry pool falls through to fallback.
refreshed_id = getattr(refreshed, "id", None)
if refreshed_id is not None:
refresh_counts = getattr(agent, "_auth_pool_refresh_counts", None)
@@ -747,8 +732,8 @@ def _recover_auth_failure(agent, pool, *, status_code, has_retried_429, error_co
def _recover_rate_limit(pool, *, has_retried_429, error_context, api_key_hint, credential_id, rotate_and_swap):
- # Already-exhausted credential: rotate immediately. Avoids the "cancel-between-429s"
- # trap where the local has_retried_429 resets per prompt and retries forever.
+ # Already-exhausted credential: rotate immediately. Avoids the "cancel-between-429s" trap where
+ # the local has_retried_429 resets per prompt and retries forever.
current_entry = None
if credential_id:
current_entry = next((e for e in pool.entries() if e.id == credential_id), None)
@@ -789,10 +774,9 @@ def recover_with_credential_pool(
pool = agent._credential_pool
if pool is None:
return False, has_retried_429
-
- # The pool belongs to the PRIMARY provider: acting on fallback errors would corrupt
- # its state and reset base_url to the primary endpoint. Empty pool provider means
- # unscoped; empty agent provider is a mismatch (swap would leave provider="" model="").
+ # The pool belongs to the PRIMARY provider: acting on fallback errors would corrupt its state
+ # and reset base_url to the primary endpoint. Empty pool provider means unscoped; empty agent
+ # provider is a mismatch (swap would leave provider="" model="").
current_provider = (getattr(agent, "provider", "") or "").strip().lower()
pool_provider = (getattr(pool, "provider", "") or "").strip().lower()
if pool_provider and not credential_pool_matches_provider(
@@ -805,7 +789,6 @@ def recover_with_credential_pool(
pool_provider, current_provider,
)
return False, has_retried_429
-
api_key_hint, credential_id = _failed_credential_identity(agent, pool)
effective_reason = classified_reason
if effective_reason is None:
@@ -821,8 +804,8 @@ def recover_with_credential_pool(
}
if credential_id:
kwargs["credential_id"] = credential_id
- # Pass classified semantics, not just the status: a billing 403 and an
- # edge-throttle 403 need opposite cooldowns.
+ # Pass classified semantics, not just the status: a billing 403 and an edge-throttle 403
+ # need opposite cooldowns.
if effective_reason is not None:
failure_reason = effective_reason.value
if effective_reason == FailoverReason.billing and billing_unverified:
@@ -839,10 +822,9 @@ def recover_with_credential_pool(
)
agent._swap_credential(next_entry)
return True
-
if effective_reason == FailoverReason.upstream_rate_limit:
- # Upstream (e.g. DeepSeek behind OpenRouter) is throttling the aggregator; the
- # credential is healthy. Do not rotate/exhaust; let fallback switch models.
+ # Upstream (e.g. DeepSeek behind OpenRouter) is throttling the aggregator; the credential is
+ # healthy. Do not rotate/exhaust; let fallback switch models.
upstream = (error_context or {}).get("upstream_provider") if error_context else None
if upstream:
_ra().logger.info(
@@ -856,8 +838,8 @@ def recover_with_credential_pool(
)
return False, has_retried_429
if effective_reason == FailoverReason.billing:
- # A separate pool instance may have resolved runtime credentials, leaving
- # no ``current_id``; match the key that failed, not a different account.
+ # A separate pool instance may have resolved runtime credentials, leaving no ``current_id``;
+ # match the key that failed, not a different account.
return (True, False) if _rotate_and_swap(402, "billing") else (False, has_retried_429)
if effective_reason == FailoverReason.rate_limit:
return _recover_rate_limit(
@@ -929,16 +911,16 @@ def try_recover_primary_transport(
if agent._is_openrouter_url():
return False
provider_lower = (agent.provider or "").strip().lower()
- # Portal OpenAI-wire traffic rides aggregator retry infra (skip), but Portal
- # Claude on native Messages holds a local Anthropic client that needs the rebuild.
+ # Portal OpenAI-wire traffic rides aggregator retry infra (skip), but Portal Claude on native
+ # Messages holds a local Anthropic client that needs the rebuild.
if (
provider_lower in {"nous", "nous-portal", "nousresearch"}
and getattr(agent, "api_mode", None) != "anthropic_messages"
):
return False
try:
- # Never hard-close the shared client here: stale streaming workers may still be
- # unwinding on the old pool; _retire_shared_openai_client defers FD release to GC.
+ # Never hard-close the shared client here: stale streaming workers may still be unwinding on
+ # the old pool; _retire_shared_openai_client defers FD release to GC.
if getattr(agent, "client", None) is not None:
with contextlib.suppress(Exception):
agent._retire_shared_openai_client(agent.client, reason="primary_recovery")
@@ -1008,8 +990,8 @@ def drop_thinking_only_and_merge_users(
merged[-1] = {**prev, "content": content}
merges += 1
continue
- # Unknown content shape: append separately (violates alternation, but safer
- # than raising in a hot path).
+ # Unknown content shape: append separately (violates alternation, but safer than raising
+ # in a hot path).
merged.append(m)
if dropped == 0 and merges == 0:
return messages
@@ -1112,7 +1094,6 @@ def restore_primary_runtime(agent) -> bool:
return False
if getattr(agent, "_rate_limited_until", 0) > time.monotonic():
return False # primary still in rate-limit cooldown, stay on fallback
-
rt = agent._primary_runtime
primary_provider = str((rt or {}).get("provider") or "").strip().lower()
primary_runtime_base_url = str((rt or {}).get("base_url") or "")
@@ -1126,14 +1107,12 @@ def restore_primary_runtime(agent) -> bool:
key = resolve_runtime_pool_key(primary_provider, primary_runtime_base_url)
loaded = load_pool(key) if key else None
return loaded if loaded is not None and _matches_primary(loaded) else None
-
blocked, prefetched_pool, prefetched = _primary_reset_gate_blocks(
agent, rt, primary_provider, primary_runtime_base_url, _matches_primary, _load_primary_pool
)
if blocked:
return False
agent._restore_wait_logged = False
-
fallback_route = getattr(agent, "_provider_fallback_route", None)
if isinstance(fallback_route, (list, tuple)) and len(fallback_route) == 2:
previous_model = str(fallback_route[0] or "unknown")
@@ -1174,8 +1153,8 @@ def restore_primary_runtime(agent) -> bool:
# Reset the stale-call circuit breaker: its streak measured the fallback provider.
from agent.chat_completion_helpers import _reset_stale_streak, rewrite_prompt_model_identity
_reset_stale_streak(agent)
- # Undo the fallback's identity rewrite so the prompt is byte-identical to the
- # stored copy again (prefix cache match).
+ # Undo the fallback's identity rewrite so the prompt is byte-identical to the stored copy
+ # again (prefix cache match).
rewrite_prompt_model_identity(agent, rt["model"], rt["provider"])
logger.info("Primary runtime restored for new turn: %s (%s)", agent.model, agent.provider)
agent._provider_fallback_active = False
@@ -1192,13 +1171,12 @@ def restore_primary_runtime(agent) -> bool:
logger.warning("Failed to restore primary runtime: %s", e)
return False
+
# Transient transport failures worth one more attempt with a rebuilt client / connection pool.
_TRANSIENT_TRANSPORT_ERRORS = frozenset({
"ReadTimeout", "ConnectTimeout", "PoolTimeout", "ConnectError", "RemoteProtocolError",
"APIConnectionError", "APITimeoutError",
})
-
-
_INLINE_REASONING_PATTERNS = tuple(
re.compile(rf"<{tag}>(.*?){tag}>", re.DOTALL | re.IGNORECASE)
for tag in ("think", "thinking", "thought", "reasoning", "REASONING_SCRATCHPAD")
@@ -1213,7 +1191,6 @@ def extract_reasoning(agent, assistant_message) -> Optional[str]:
def _add(text) -> None:
if text and text not in parts:
parts.append(text)
-
_add(getattr(assistant_message, "reasoning", None))
_add(getattr(assistant_message, "reasoning_content", None))
# reasoning_details: [{"type": "reasoning.summary", "summary": "...", ...}, ...]
@@ -1223,8 +1200,8 @@ def extract_reasoning(agent, assistant_message) -> Optional[str]:
# Fall back to reasoning embedded in content only when no structured field was found.
content = getattr(assistant_message, "content", None)
if not parts and isinstance(content, list):
- # DeepSeek V4 Pro returns typed content blocks ({"type": "thinking", ...}); dropping
- # them makes the next turn fail with HTTP 400 "thinking must be passed back".
+ # DeepSeek V4 Pro returns typed content blocks ({"type": "thinking", ...}); dropping them
+ # makes the next turn fail with HTTP 400 "thinking must be passed back".
for block in content:
if isinstance(block, dict) and block.get("type") == "thinking":
_add((block.get("thinking") or block.get("text") or "").strip())
@@ -1282,8 +1259,8 @@ def dump_api_request_debug(
if error is not None:
dump_payload["error"] = _api_error_debug_info(error)
timestamp = datetime.now().strftime("%Y%m%d_%H%M%S_%f")
- # Sanitize the session ID (may come from an untrusted X-Hermes-Session-Id header)
- # so a "../"-shaped ID cannot write outside logs_dir.
+ # Sanitize the session ID (may come from an untrusted X-Hermes-Session-Id header) so a
+ # "../"-shaped ID cannot write outside logs_dir.
safe_sid = _ra()._safe_session_filename_component(agent.session_id)
dump_file = agent.logs_dir / f"request_dump_{safe_sid}_{timestamp}.json"
# Redact secrets first: this fires unconditionally on API errors and captures the full
@@ -1398,8 +1375,8 @@ def plan_cache_sections_for_destination(
direct_native_tool_cache=_direct_native_anthropic_tool_cache_capability(
stub, provider=provider, base_url=base_url, api_mode=api_mode, model=model
),
- # LiteLLM-style envelope routes forward part-level markers into
- # tool_result.content[] → non-retryable 400.
+ # LiteLLM-style envelope routes forward part-level markers into tool_result.content[] →
+ # non-retryable 400.
tool_part_markers=envelope_tool_part_cache_markers_supported(
provider, base_url
),
@@ -1473,12 +1450,12 @@ def _route_may_be_custom(agent, eff_provider: str, provider_lower: str, eff_base
return False
if custom_providers is not None:
return False # attached empty list never matches
- # None = list not attached yet (early init or blank stub). Avoid rebuilding the
- # list for ordinary built-in routes.
+ # None = list not attached yet (early init or blank stub). Avoid rebuilding the list for
+ # ordinary built-in routes.
try:
from hermes_cli.providers import get_provider
- # allow_network=False: never trigger a registry fetch from the send path;
- # a catalog miss degrades to the conservative capability lookup.
+ # allow_network=False: never trigger a registry fetch from the send path; a catalog miss
+ # degrades to the conservative capability lookup.
provider_def = get_provider(eff_provider, allow_network=False)
return provider_def is None or (
bool(provider_def.base_url)
@@ -1494,7 +1471,6 @@ def anthropic_prompt_cache_policy(
api_mode: Optional[str] = None, model: Optional[str] = None,
) -> tuple[bool, bool]:
"""Decide whether to apply Anthropic prompt caching; returns ``(should_cache, use_native_layout)``.
-
Native layout puts markers on inner content blocks (Anthropic wire), else on the message
envelope (OpenRouter / OpenAI-wire proxies; Qwen/Alibaba too). The operator disable is read
from ``_cache_disabled`` (not ``_cache_ttl``, unset during init) so it survives switches
@@ -1502,22 +1478,20 @@ def anthropic_prompt_cache_policy(
"""
if getattr(agent, "_cache_disabled", False):
return (False, False)
-
eff_provider = (provider if provider is not None else agent.provider) or ""
eff_base_url = base_url if base_url is not None else (agent.base_url or "")
eff_api_mode = api_mode if api_mode is not None else (agent.api_mode or "")
eff_model = (model if model is not None else agent.model) or ""
if eff_provider.strip().lower() == "moa":
return _moa_aggregator_cache_policy(agent, eff_model)
-
if isinstance(eff_model, dict):
eff_model = eff_model.get('model') or eff_model.get('default') or ''
eff_model = eff_model if isinstance(eff_model, str) else str(eff_model or '')
model_lower = eff_model.lower()
provider_lower = eff_provider.lower()
is_claude = "claude" in model_lower
- # Kimi/Moonshot via OpenRouter uses the same envelope cache_control as Claude; without
- # this it serves ~1% cache hits. Family matcher covers bare k1./k2. slugs.
+ # Kimi/Moonshot via OpenRouter uses the same envelope cache_control as Claude; without this it
+ # serves ~1% cache hits. Family matcher covers bare k1./k2. slugs.
from agent.anthropic_adapter import _model_name_is_kimi_family
is_kimi = _model_name_is_kimi_family(eff_model) or "moonshot" in model_lower
is_openrouter = base_url_host_matches(eff_base_url, "openrouter.ai")
@@ -1527,9 +1501,8 @@ def anthropic_prompt_cache_policy(
is_native_anthropic = is_anthropic_wire and (
eff_provider == "anthropic" or base_url_hostname(eff_base_url) == "api.anthropic.com"
)
-
- # Honor a configured route's per-model ``prompt_caching`` capability (explicit false too);
- # only for the two transports this planner handles, not Responses/Bedrock.
+ # Honor a configured route's per-model ``prompt_caching`` capability (explicit false too); only
+ # for the two transports this planner handles, not Responses/Bedrock.
supports_cache_markers = eff_api_mode in {"anthropic_messages", "chat_completions"}
litellm_openai_wire = (
eff_api_mode == "chat_completions" and is_claude and _is_litellm_route(provider_lower, eff_base_url)
@@ -1550,9 +1523,9 @@ def anthropic_prompt_cache_policy(
return custom_prompt_caching, custom_prompt_caching and is_anthropic_wire
except Exception as _cap_exc:
logger.debug("custom-provider prompt_caching capability lookup failed: %s", _cap_exc)
-
# MiniMax-M3 uses server-side automatic prefix caching; explicit markers are dead weight.
- # Checked BEFORE the native-Anthropic return since provider="anthropic" may point at a MiniMax proxy.
+ # Checked BEFORE the native-Anthropic return since provider="anthropic" may point at a MiniMax
+ # proxy.
is_minimax_route = (
provider_lower in {"minimax", "minimax-cn"}
or base_url_host_matches(eff_base_url, "api.minimax.io")
@@ -1564,30 +1537,29 @@ def anthropic_prompt_cache_policy(
return False, False
if is_native_anthropic:
return True, True
- # Envelope layout is OpenAI-wire only; Portal Claude on native Messages must fall through
- # to the anthropic_messages branch (inner-block markers) or it serves 0% cache hits.
+ # Envelope layout is OpenAI-wire only; Portal Claude on native Messages must fall through to the
+ # anthropic_messages branch (inner-block markers) or it serves 0% cache hits.
if (is_openrouter or is_nous_portal) and (is_claude or is_kimi) and not is_anthropic_wire:
return True, False
- # Nous Portal Qwen takes the envelope path too; the alibaba-family check below only
- # matches provider=opencode/alibaba and would leave Portal traffic uncached.
+ # Nous Portal Qwen takes the envelope path too; the alibaba-family check below only matches
+ # provider=opencode/alibaba and would leave Portal traffic uncached.
if is_nous_portal and "qwen" in model_lower:
return True, False
if is_anthropic_wire and is_claude:
return True, True # third-party Anthropic-compatible gateway
- # LiteLLM fronting Claude on the OpenAI wire supports cache_control but matched no grant
- # above. Claude-only: strict relays reject the block format for other models. Envelope
- # layout: native top-level markers are only relocated by the anthropic_messages adapter
- # and 400 via LiteLLM. Gated on chat_completions; codex_responses/bedrock_converse have
- # their own handling.
+ # LiteLLM fronting Claude on the OpenAI wire supports cache_control but matched no grant above.
+ # Claude-only: strict relays reject the block format for other models. Envelope layout: native
+ # top-level markers are only relocated by the anthropic_messages adapter and 400 via LiteLLM.
+ # Gated on chat_completions; codex_responses/bedrock_converse have their own handling.
if litellm_openai_wire:
return True, False
- # MiniMax's own models (M2.x) on its Anthropic-compatible endpoint support cache_control;
- # opt them in past the is_claude gate. M3 is excluded above.
+ # MiniMax's own models (M2.x) on its Anthropic-compatible endpoint support cache_control; opt
+ # them in past the is_claude gate. M3 is excluded above.
if is_anthropic_wire and is_minimax_route:
return True, True
# Qwen/Alibaba on OpenCode and DashScope accept envelope cache_control on the OpenAI wire
- # (pi-mono's "alibaba" cacheControlFormat). DeepSeek on OpenCode is excluded: its relay
- # 400s on block-array content. Family set/predicate shared with the effective_cache_ttl clamp.
+ # (pi-mono's "alibaba" cacheControlFormat). DeepSeek on OpenCode is excluded: its relay 400s on
+ # block-array content. Family set/predicate shared with the effective_cache_ttl clamp.
from agent.prompt_caching import ALIBABA_FAMILY_PROVIDERS, is_qwen_model
if provider_lower in ALIBABA_FAMILY_PROVIDERS and is_qwen_model(model_lower):
return True, False
@@ -1684,11 +1656,11 @@ def _gemini_native_client(agent, client_kwargs: dict, httpx_verify, *, reason: s
def create_openai_client(agent, client_kwargs: dict, *, reason: str, shared: bool) -> Any:
from agent.auxiliary_client import _validate_base_url, _validate_proxy_env_urls
from agent.ssl_verify import resolve_httpx_verify
- # Treat client_kwargs as read-only: callers pass agent._client_kwargs, and in-place
- # mutation leaks into later requests (a torn-down httpx transport got reused).
+ # Treat client_kwargs as read-only: callers pass agent._client_kwargs, and in-place mutation
+ # leaks into later requests (a torn-down httpx transport got reused).
client_kwargs = dict(client_kwargs)
- # The MoA virtual provider has no OpenAI wire endpoint; the facade *is* the client.
- # Rebuild the facade, never a native client (TypeError; relay re-wire).
+ # The MoA virtual provider has no OpenAI wire endpoint; the facade *is* the client. Rebuild the
+ # facade, never a native client (TypeError; relay re-wire).
if (getattr(agent, "provider", "") or "").strip().lower() == "moa":
from agent.moa_loop import build_moa_facade
return build_moa_facade(agent, getattr(agent, "model", None) or "default")
@@ -1715,7 +1687,8 @@ def create_openai_client(agent, client_kwargs: dict, *, reason: str, shared: boo
return client
# TCP keepalives so dead provider connections are detected (~60s) instead of hanging in
# CLOSE-WAIT. Injected into the local copy only, so each client gets its own httpx.Client;
- # pinned by tests/run_agent/test_create_openai_client_reuse.py and test_sequential_chats_live.py.
+ # pinned by tests/run_agent/test_create_openai_client_reuse.py and
+ # test_sequential_chats_live.py.
if "http_client" not in client_kwargs:
keepalive_http = agent._build_keepalive_http_client(client_kwargs.get("base_url", ""), verify=httpx_verify)
if keepalive_http is not None:
@@ -1767,24 +1740,12 @@ def _apply_switched_provider_request_overrides(agent, new_provider):
agent.request_overrides = overrides
+# Pool reload is part of the switch and must be reversible on rollback, hence the pool fields.
_SWITCH_SNAPSHOT_FIELDS = (
- "model",
- "provider",
- "requested_provider",
- "base_url",
- "api_mode",
- "api_key",
- "client",
- "_anthropic_client",
- "_anthropic_api_key",
- "_anthropic_base_url",
- "_is_anthropic_oauth",
- "_config_context_length",
- "_reasoning_echo_flag",
- "runtime_capabilities",
- # Pool reload is part of the switch and must be reversible on rollback.
- "_credential_pool",
- "_credential_pool_entry_id",
+ "model", "provider", "requested_provider", "base_url", "api_mode", "api_key", "client",
+ "_anthropic_client", "_anthropic_api_key", "_anthropic_base_url", "_is_anthropic_oauth",
+ "_config_context_length", "_reasoning_echo_flag", "runtime_capabilities",
+ "_credential_pool", "_credential_pool_entry_id",
)
_MISSING = object()
@@ -1818,8 +1779,8 @@ def _resolve_switch_destination(agent, new_model, new_provider, base_url, api_mo
if not base_url and new_norm == "openai":
# An omitted URL means the provider's canonical direct endpoint.
base_url = "https://api.openai.com/v1"
- # Same-provider switches may omit base_url (e.g. credential refresh); resolve
- # capabilities from the endpoint the normalization below retains.
+ # Same-provider switches may omit base_url (e.g. credential refresh); resolve capabilities from
+ # the endpoint the normalization below retains.
effective_base_url = base_url
if not effective_base_url and old_norm == new_norm:
effective_base_url = getattr(agent, "base_url", "")
@@ -1831,8 +1792,8 @@ def _resolve_switch_destination(agent, new_model, new_provider, base_url, api_mo
is_codex_backend=new_norm == "openai-codex",
)
)
- # Guard against a trailing /v1 on OpenCode base_url reaching the anthropic_messages
- # client (double-/v1 404); model_switch already strips it, direct callers may not.
+ # Guard against a trailing /v1 on OpenCode base_url reaching the anthropic_messages client
+ # (double-/v1 404); model_switch already strips it, direct callers may not.
if (
api_mode == "anthropic_messages"
and opencode_provider_family(new_provider) is not None
@@ -1847,8 +1808,8 @@ def _build_switched_client(agent, new_provider, api_key, base_url, api_mode, new
"""Build the client for the switched-to destination (MoA facade / native Anthropic / OpenAI wire)."""
if new_norm == "moa":
from agent.moa_loop import build_moa_facade
- # MoA speaks only chat.completions via the MoAClient facade; the aggregator's real
- # transport is applied inside the fan-out. Pin api_mode so the loop never dispatches
+ # MoA speaks only chat.completions via the MoAClient facade; the aggregator's real transport
+ # is applied inside the fan-out. Pin api_mode so the loop never dispatches
# client.responses.create against the facade (matches agent_init.py).
agent.api_mode = "chat_completions"
agent.api_key = api_key or "moa-virtual-provider"
@@ -1858,12 +1819,12 @@ def _build_switched_client(agent, new_provider, api_key, base_url, api_mode, new
return
if api_mode == "anthropic_messages":
from agent.anthropic_adapter import build_anthropic_client, resolve_anthropic_token, _is_oauth_token
- # Only fall back to ANTHROPIC_TOKEN for native Anthropic; other anthropic_messages
- # providers must never receive Anthropic credentials.
+ # Only fall back to ANTHROPIC_TOKEN for native Anthropic; other anthropic_messages providers
+ # must never receive Anthropic credentials.
is_native_anthropic = new_provider == "anthropic"
effective_key = api_key or agent.api_key or (resolve_anthropic_token() if is_native_anthropic else "") or ""
- # MiniMax OAuth: per-request callable token provider survives 15-min expiry
- # (rationale in agent_init.py).
+ # MiniMax OAuth: per-request callable token provider survives 15-min expiry (rationale in
+ # agent_init.py).
if new_provider == "minimax-oauth" and isinstance(effective_key, str) and effective_key:
try:
from hermes_cli.auth import build_minimax_oauth_token_provider
@@ -1893,8 +1854,8 @@ def _build_switched_client(agent, new_provider, api_key, base_url, api_mode, new
apply_custom_provider_tls_to_client_kwargs, get_compatible_custom_providers,
load_config_readonly,
)
- # Read live config, not agent._custom_providers, so mid-session ssl_ca_cert /
- # ssl_verify edits are honored.
+ # Read live config, not agent._custom_providers, so mid-session ssl_ca_cert / ssl_verify
+ # edits are honored.
apply_custom_provider_tls_to_client_kwargs(
agent._client_kwargs, str(effective_base or ""),
get_compatible_custom_providers(load_config_readonly()),
@@ -1904,8 +1865,8 @@ def _build_switched_client(agent, new_provider, api_key, base_url, api_mode, new
timeout = get_provider_request_timeout(agent.provider, agent.model)
if timeout is not None:
agent._client_kwargs["timeout"] = timeout
- # Reapply provider headers (OpenRouter HTTP-Referer/X-Title) lost when
- # _client_kwargs was rebuilt; otherwise attribution shows "Unknown".
+ # Reapply provider headers (OpenRouter HTTP-Referer/X-Title) lost when _client_kwargs was
+ # rebuilt; otherwise attribution shows "Unknown".
agent._apply_client_headers_for_base_url(effective_base)
agent.client = agent._create_openai_client(dict(agent._client_kwargs), reason="switch_model", shared=True)
@@ -1919,9 +1880,9 @@ def _swap_switch_runtime(agent, new_model, new_provider, api_key, base_url, api_
agent.requested_provider = new_provider
# Re-read reasoning_echo so the flag reflects the new primary model (see _reasoning_echo_opt_in).
agent._reasoning_echo_flag = agent._read_reasoning_echo_from_config()
- # Empty base_url while the provider changes means upstream resolution failed; falling
- # back to the old provider's URL pairs the wrong host and persists via _primary_runtime.
- # Fail loud. Same-provider re-select (credential refresh) may keep the URL.
+ # Empty base_url while the provider changes means upstream resolution failed; falling back to
+ # the old provider's URL pairs the wrong host and persists via _primary_runtime. Fail loud.
+ # Same-provider re-select (credential refresh) may keep the URL.
if base_url:
agent.base_url = base_url
elif old_norm != new_norm:
@@ -1936,8 +1897,8 @@ def _swap_switch_runtime(agent, new_model, new_provider, api_key, base_url, api_
agent._transport_cache.clear()
if api_key:
agent.api_key = api_key
- # Reload the credential pool on provider change: a pool with a mismatched provider
- # makes recover_with_credential_pool short-circuit. Reload failure is non-fatal.
+ # Reload the credential pool on provider change: a pool with a mismatched provider makes
+ # recover_with_credential_pool short-circuit. Reload failure is non-fatal.
if old_norm != new_norm or getattr(agent, "_credential_pool", None) is None:
# A pool bound to the old provider is worse than none: the recovery guard rejects it.
agent._credential_pool = None
@@ -1996,8 +1957,8 @@ def _update_switch_compressor(agent, custom_providers, effective_context_length,
custom_providers = get_compatible_custom_providers(load_config())
except Exception:
custom_providers = None
- # agent.api_key may be a callable (Azure Foundry Entra ID); get_model_context_length
- # expects a string for live probes, so coerce defensively.
+ # agent.api_key may be a callable (Azure Foundry Entra ID); get_model_context_length expects a
+ # string for live probes, so coerce defensively.
ctx_api_key = agent.api_key if isinstance(agent.api_key, str) else ""
try:
new_context_length = get_model_context_length(
@@ -2032,8 +1993,8 @@ def _build_primary_runtime_snapshot(agent, api_mode) -> Dict[str, Any]:
"use_native_cache_layout": agent._use_native_cache_layout,
"reasoning_config": dict(agent.reasoning_config) if getattr(agent, "reasoning_config", None) else None,
"reasoning_echo_flag": getattr(agent, "_reasoning_echo_flag", False),
- # Overrides must travel with the switched-to identity or a later recovery/restore
- # resurrects PRE-switch overrides from the stale init snapshot.
+ # Overrides must travel with the switched-to identity or a later recovery/restore resurrects
+ # PRE-switch overrides from the stale init snapshot.
"request_overrides": dict(getattr(agent, "request_overrides", {}) or {}),
"runtime_capabilities": dict(getattr(agent, "runtime_capabilities", {}) or {}),
"compressor_model": getattr(cc, "model", agent.model),
@@ -2114,10 +2075,9 @@ def switch_model(
except Exception:
_restore_switch_snapshot(agent, snapshot)
raise
-
custom_providers, effective_context_length = _resolve_switch_context_length(agent, snapshot)
- # Refresh the custom-provider snapshot from the config just loaded so the prompt_caching
- # lookup sees flags added to config.yaml after session start.
+ # Refresh the custom-provider snapshot from the config just loaded so the prompt_caching lookup
+ # sees flags added to config.yaml after session start.
if custom_providers is not None:
agent._custom_providers = custom_providers
agent._use_prompt_caching, agent._use_native_cache_layout = agent._anthropic_prompt_cache_policy(
@@ -2125,9 +2085,8 @@ def switch_model(
)
if hasattr(agent, "context_compressor") and agent.context_compressor:
_update_switch_compressor(agent, custom_providers, effective_context_length, snapshot)
-
- # Re-read the per-model reasoning_effort override so it applies immediately
- # (per-model > global; YAML False = disabled).
+ # Re-read the per-model reasoning_effort override so it applies immediately (per-model > global;
+ # YAML False = disabled).
try:
from hermes_constants import resolve_reasoning_config
from hermes_cli.config import load_config as _sm_load_config
@@ -2137,11 +2096,10 @@ def switch_model(
)
except Exception as _reasoning_err:
logger.debug("switch_model: could not re-resolve reasoning_config: %s", _reasoning_err)
-
# Invalidate the cached system prompt so it rebuilds next turn.
agent._cached_system_prompt = None
- # Publish the destination capability map only after every runtime setup above has
- # succeeded. Failed switches must leave the old map intact.
+ # Publish the destination capability map only after every runtime setup above has succeeded.
+ # Failed switches must leave the old map intact.
agent.runtime_capabilities = destination_capabilities
# Reset the cross-turn stale-call circuit breaker; otherwise the latched streak keeps
# short-circuiting the freshly selected healthy provider.
@@ -2196,7 +2154,6 @@ def invoke_tool(agent, function_name: str, function_args: dict, effective_task_i
_tool_middleware_trace = _tool_request_mw.trace
except Exception as _mw_err:
logger.debug("tool_request middleware error: %s", _mw_err)
-
block_message: Optional[str] = None
if not pre_tool_block_checked:
block_message, function_args = _pre_tool_block_message(
@@ -2211,7 +2168,6 @@ def invoke_tool(agent, function_name: str, function_args: dict, effective_task_i
middleware_trace=_tool_middleware_trace,
)
return result
-
tool_start_time = time.monotonic()
inline_executor = resolve_invoke_tool_executor(agent, function_name)
if inline_executor is not None:
@@ -2244,7 +2200,6 @@ def invoke_tool(agent, function_name: str, function_args: dict, effective_task_i
if skip_tool_execution_middleware:
dispatch_kwargs["skip_tool_execution_middleware"] = True
return _ra().handle_function_call(function_name, next_args, effective_task_id, **dispatch_kwargs)
-
if skip_tool_execution_middleware:
return _execute(function_args)
from hermes_cli.middleware import run_tool_execution_middleware
@@ -2262,9 +2217,9 @@ def repair_tool_call(agent, tool_name: str) -> str | None:
from difflib import get_close_matches
if not tool_name:
return None
- # VolcEngine api/plan leaks XML attribute fragments into tool_use.name
- # (`terminal" parameter="command" ...`); trim at the first quote/angle bracket.
- # Do NOT split on whitespace: "write file" must reach ``_norm`` -> ``write_file``.
+ # VolcEngine api/plan leaks XML attribute fragments into tool_use.name (`terminal"
+ # parameter="command" ...`); trim at the first quote/angle bracket. Do NOT split on whitespace:
+ # "write file" must reach ``_norm`` -> ``write_file``.
for _xml_sep in ('"', "'", "<", ">"):
_idx = tool_name.find(_xml_sep)
if _idx > 0:
@@ -2284,7 +2239,6 @@ def repair_tool_call(agent, tool_name: str) -> str | None:
if lc.endswith(suffix):
return s[: -len(suffix)].rstrip("_-")
return None
-
# Cheap fast-paths first.
lowered = tool_name.lower()
if lowered in agent.valid_tool_names:
@@ -2307,19 +2261,21 @@ def repair_tool_call(agent, tool_name: str) -> str | None:
return matches[0] if matches else None
-# Placeholder for an empty non-final message the provider would reject. Kept identical to
-# the stub placeholder in chat_completion_helpers so healed transcripts read consistently.
+# Placeholder for an empty non-final message the provider would reject. Kept identical to the stub
+# placeholder in chat_completion_helpers so healed transcripts read consistently.
_INTERRUPTED_PLACEHOLDER = "[response interrupted]"
-# Escalate repeated heals once per session window, then stay quiet. Default
-# threshold; tunable via ``agent.sanitizer_heal_escalation_threshold`` (<= 0 disables).
+# Escalate repeated heals once per session window, then stay quiet. Default threshold; tunable via
+# ``agent.sanitizer_heal_escalation_threshold`` (<= 0 disables).
_EMPTY_HEAL_ESCALATE_AFTER = 3
_EMPTY_HEAL_WINDOW_S = 600.0
_empty_heal_log_state: Dict[str, Dict[str, Any]] = {}
_empty_heal_log_lock = threading.Lock()
-# Sessions already given the one-time user notice; separate from the windowed log state
-# so the user is told ONCE per session (out-of-band, never in conversation context).
+
+# Sessions already given the one-time user notice; separate from the windowed log state so the user
+# is told ONCE per session (out-of-band, never in conversation context).
_empty_heal_user_notified: set = set()
+
# One-shot pending notices keyed by session, drained by the conversation loop via
# ``consume_pending_sanitizer_heal_notice`` and delivered via the status/warning callback.
_empty_heal_pending_notice: Dict[str, str] = {}
@@ -2344,9 +2300,9 @@ def _msg_has_payload(msg: Dict[str, Any]) -> bool:
return True
elif content not in (None, ""):
return True
- # Structural payloads that make an "empty-content" message still valid. Codex Responses
- # item carriers persist with content:"" by design (text lives in codex_*_items and is
- # replayed); treating them as payload keeps the repair from rewriting a designed-empty turn.
+ # Structural payloads that make an "empty-content" message still valid. Codex Responses item
+ # carriers persist with content:"" by design (text lives in codex_*_items and is replayed);
+ # treating them as payload keeps the repair from rewriting a designed-empty turn.
return bool(
msg.get("tool_calls")
or (isinstance(msg.get("reasoning_content"), str) and msg["reasoning_content"].strip())
@@ -2483,7 +2439,6 @@ def repair_empty_non_final_messages(messages: List[Dict[str, Any]]) -> List[Dict
healed += 1
else:
repaired.append(msg)
-
if healed:
_log_empty_non_final_heal(healed)
return repaired
@@ -2506,7 +2461,6 @@ def _classify_tool_call_orphans(messages: List[Dict[str, Any]]):
if variants:
assistant_call_variants.append((tc, variants))
surviving_call_ids.update(variants)
-
result_entries = [
(msg, tool_result_id_variants(msg.get("tool_call_id")))
for msg in messages
@@ -2515,7 +2469,6 @@ def _classify_tool_call_orphans(messages: List[Dict[str, Any]]):
result_call_ids: set[str] = set()
for _, variants in result_entries:
result_call_ids.update(variants)
-
orphaned_results = [
msg for msg, variants in result_entries if variants and not (variants & surviving_call_ids)
]
@@ -2632,19 +2585,18 @@ def _pair_tool_calls_positionally(messages: List[Dict[str, Any]]) -> List[Dict[s
})
stubs += 1
declared_calls.clear()
-
for msg in messages:
role = msg.get("role")
if role == "assistant":
- # A new assistant turn closes the previous tool-result run: anything still
- # pending was never answered positionally.
+ # A new assistant turn closes the previous tool-result run: anything still pending was
+ # never answered positionally.
_flush_unanswered_stubs()
declared_calls = {}
for tc in msg.get("tool_calls") or []:
variants = tool_call_id_variants(tc)
if variants:
- # Key on a stable representative of the alias group so a result
- # matching ANY spelling can consume the call.
+ # Key on a stable representative of the alias group so a result matching ANY
+ # spelling can consume the call.
declared_calls[sorted(variants)[0]] = (tc, variants)
paired.append(msg)
elif role == "tool":
@@ -2806,8 +2758,8 @@ def looks_like_codex_intermediate_ack(
# Opted-in (all-api_mode) path: future-ack + action verb + no prior tool call suffices.
if not require_workspace:
return True
- # ``user_message`` may be a multi-part content list (vision via the OpenAI-compat
- # server); a list survives ``or ""`` and ``.strip()`` raises, so flatten first.
+ # ``user_message`` may be a multi-part content list (vision via the OpenAI-compat server); a
+ # list survives ``or ""`` and ``.strip()`` raises, so flatten first.
from agent.codex_responses_adapter import _summarize_user_message_for_log
user_text = _summarize_user_message_for_log(user_message).strip().lower()
return (
@@ -2818,8 +2770,8 @@ def looks_like_codex_intermediate_ack(
)
-# Narrow "trailing continue-intent" detector for the stall guard (agent.stall_guards):
-# only the message TAIL announcing a next action, so mid-sentence "I will" never trips it.
+# Narrow "trailing continue-intent" detector for the stall guard (agent.stall_guards): only the
+# message TAIL announcing a next action, so mid-sentence "I will" never trips it.
_TRAILING_CONTINUE_INTENT_RE = re.compile(
r"(?:\blet me now\b|\bi(?:['\u2019])?ll now\b|\bi will now\b"
r"|\bnow i(?:['\u2019]ll| will)\b|\bnext[,:] i\b)"
@@ -2886,15 +2838,14 @@ def _iter_httpx_pool_objects(http_client: Any):
def _pools_for_transport(transport: Any):
if transport is None:
return
- # Connections live under ``_pool``; a directly mounted HTTPProxy *is* a
- # ConnectionPool, so ``_connections`` may sit on the transport itself.
+ # Connections live under ``_pool``; a directly mounted HTTPProxy *is* a ConnectionPool, so
+ # ``_connections`` may sit on the transport itself.
pool = getattr(transport, "_pool", None)
if pool is None and getattr(transport, "_connections", None) is not None:
pool = transport
if pool is not None and id(pool) not in seen_pools:
seen_pools.add(id(pool))
yield pool
-
try:
yield from _pools_for_transport(getattr(http_client, "_transport", None))
mounts = getattr(http_client, "_mounts", None) or {}
From f9d27dfcd03d052e21d77bc5e59e560a1ce5f624 Mon Sep 17 00:00:00 2001
From: Teknium <127238744+teknium1@users.noreply.github.com>
Date: Wed, 2 Sep 2026 19:47:44 -0700
Subject: [PATCH 14/20] refactor(agent/agent_runtime_helpers): flatten auth
recovery, socket walk, payload predicate, heal-log state
---
agent/agent_runtime_helpers.py | 192 +++++++++++++--------------------
1 file changed, 76 insertions(+), 116 deletions(-)
diff --git a/agent/agent_runtime_helpers.py b/agent/agent_runtime_helpers.py
index f2b1ddccab..77b0e915c9 100644
--- a/agent/agent_runtime_helpers.py
+++ b/agent/agent_runtime_helpers.py
@@ -704,31 +704,28 @@ def _recover_auth_failure(agent, pool, *, status_code, has_retried_429, error_co
if credential_id:
refresh_kwargs["credential_id"] = credential_id
refreshed = pool.try_refresh_matching(**refresh_kwargs)
- if refreshed is not None:
- # try_refresh_matching() reports success even when upstream keeps rejecting; cap same-entry
- # refreshes so a single-entry pool falls through to fallback.
- refreshed_id = getattr(refreshed, "id", None)
- if refreshed_id is not None:
- refresh_counts = getattr(agent, "_auth_pool_refresh_counts", None)
- if refresh_counts is None:
- refresh_counts = {}
- agent._auth_pool_refresh_counts = refresh_counts
- refresh_key = (agent.provider, refreshed_id)
- refresh_counts[refresh_key] = refresh_counts.get(refresh_key, 0) + 1
- if refresh_counts[refresh_key] > _MAX_AUTH_REFRESH_ATTEMPTS:
- _ra().logger.warning(
- "Credential auth failure persists after %s refreshes for "
- "pool entry %s — treating as unrecoverable and allowing "
- "fallback to activate.", refresh_counts[refresh_key] - 1, refreshed_id,
- )
- return False, has_retried_429
- _ra().logger.info("Credential auth failure — refreshed pool entry %s", getattr(refreshed, 'id', '?'))
- agent._swap_credential(refreshed)
- return True, has_retried_429
- # Refresh failed; rotate (the failed entry is already marked exhausted).
- if rotate_and_swap(401, "auth refresh failed"):
- return True, False
- return False, has_retried_429
+ if refreshed is None:
+ # Refresh failed; rotate (the failed entry is already marked exhausted).
+ return (True, False) if rotate_and_swap(401, "auth refresh failed") else (False, has_retried_429)
+ # try_refresh_matching() reports success even when upstream keeps rejecting; cap same-entry
+ # refreshes so a single-entry pool falls through to fallback.
+ refreshed_id = getattr(refreshed, "id", None)
+ if refreshed_id is not None:
+ if getattr(agent, "_auth_pool_refresh_counts", None) is None:
+ agent._auth_pool_refresh_counts = {}
+ refresh_counts = agent._auth_pool_refresh_counts
+ refresh_key = (agent.provider, refreshed_id)
+ refresh_counts[refresh_key] = refresh_counts.get(refresh_key, 0) + 1
+ if refresh_counts[refresh_key] > _MAX_AUTH_REFRESH_ATTEMPTS:
+ _ra().logger.warning(
+ "Credential auth failure persists after %s refreshes for "
+ "pool entry %s — treating as unrecoverable and allowing "
+ "fallback to activate.", refresh_counts[refresh_key] - 1, refreshed_id,
+ )
+ return False, has_retried_429
+ _ra().logger.info("Credential auth failure — refreshed pool entry %s", getattr(refreshed, 'id', '?'))
+ agent._swap_credential(refreshed)
+ return True, has_retried_429
def _recover_rate_limit(pool, *, has_retried_429, error_context, api_key_hint, credential_id, rotate_and_swap):
@@ -1214,10 +1211,10 @@ def extract_reasoning(agent, assistant_message) -> Optional[str]:
def _api_error_debug_info(error: Exception) -> Dict[str, Any]:
info: Dict[str, Any] = {"type": type(error).__name__, "message": str(error)}
- for attr_name in ("status_code", "request_id", "code", "param", "type", "body"):
- attr_value = getattr(error, attr_name, None)
- if attr_value is not None:
- info[attr_name] = attr_value
+ info.update({
+ k: v for k in ("status_code", "request_id", "code", "param", "type", "body")
+ if (v := getattr(error, k, None)) is not None
+ })
response_obj = getattr(error, "response", None)
if response_obj is not None:
try:
@@ -1377,9 +1374,7 @@ def plan_cache_sections_for_destination(
),
# LiteLLM-style envelope routes forward part-level markers into tool_result.content[] →
# non-retryable 400.
- tool_part_markers=envelope_tool_part_cache_markers_supported(
- provider, base_url
- ),
+ tool_part_markers=envelope_tool_part_cache_markers_supported(provider, base_url),
)
return plan.messages, plan.tools
@@ -2281,29 +2276,25 @@ _empty_heal_user_notified: set = set()
_empty_heal_pending_notice: Dict[str, str] = {}
+def _content_has_payload(content: Any) -> bool:
+ if isinstance(content, str):
+ return bool(content.strip())
+ if isinstance(content, list):
+ # Any typed block counts, as long as a text block is not itself blank.
+ return any(
+ (block.get("type") != "text" or (isinstance(block.get("text"), str) and block["text"].strip()))
+ if isinstance(block, dict) else bool(block)
+ for block in content
+ )
+ return content not in (None, "")
+
+
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``."""
- content = msg.get("content")
- if isinstance(content, str):
- if content.strip():
- return True
- elif isinstance(content, list):
- for block in content:
- if isinstance(block, dict):
- # any typed block counts, as long as a text block is not itself blank
- if block.get("type") != "text":
- return True
- if isinstance(block.get("text"), str) and block["text"].strip():
- return True
- elif block:
- return True
- elif content not in (None, ""):
- return True
- # Structural payloads that make an "empty-content" message still valid. Codex Responses item
- # carriers persist with content:"" by design (text lives in codex_*_items and is replayed);
- # treating them as payload keeps the repair from rewriting a designed-empty turn.
- return bool(
+ 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
+ and is replayed); treating them as payload keeps the repair from rewriting a designed-empty turn."""
+ return _content_has_payload(msg.get("content")) or bool(
msg.get("tool_calls")
or (isinstance(msg.get("reasoning_content"), str) and msg["reasoning_content"].strip())
or msg.get("reasoning")
@@ -2373,20 +2364,15 @@ def _log_empty_non_final_heal(healed: int) -> None:
with _empty_heal_log_lock:
state = _empty_heal_log_state.get(key)
if state is None or (now - state["window_start"]) > _EMPTY_HEAL_WINDOW_S:
- state = {
- "count": 0,
- "window_start": now,
- "escalated": False,
- "total_events": state.get("total_events", 0) if state else 0,
- "total_healed": state.get("total_healed", 0) if state else 0,
+ prior = state or {}
+ state = _empty_heal_log_state[key] = {
+ "count": 0, "window_start": now, "escalated": False,
+ "total_events": prior.get("total_events", 0), "total_healed": prior.get("total_healed", 0),
}
- _empty_heal_log_state[key] = state
state["count"] += 1
state["total_events"] = state.get("total_events", 0) + 1
state["total_healed"] = state.get("total_healed", 0) + healed
- count = state["count"]
- total_events = state["total_events"]
- total_healed = state["total_healed"]
+ count, total_events, total_healed = state["count"], state["total_events"], state["total_healed"]
if state["escalated"]:
return
escalate = threshold > 0 and count >= threshold
@@ -2399,8 +2385,7 @@ def _log_empty_non_final_heal(healed: int) -> None:
f"({total_events} heal passes so far). Replies keep "
"working, but a corrupted turn is stuck in this "
"session's history — run /debug share or `hermes "
- "doctor` to capture diagnostics, or /new to start a "
- "clean session."
+ "doctor` to capture diagnostics, or /new to start a clean session."
)
if escalate:
_ra().logger.error(
@@ -2578,13 +2563,13 @@ def _pair_tool_calls_positionally(messages: List[Dict[str, Any]]) -> List[Dict[s
for key in sorted(declared_calls):
tc, _variants = declared_calls[key]
paired.append({
- "role": "tool",
- "name": _ra().AIAgent._get_tool_call_name_static(tc),
+ "role": "tool", "name": _ra().AIAgent._get_tool_call_name_static(tc),
"content": "[Result unavailable — see context summary above]",
"tool_call_id": coalesce_tool_call_id(tc) or key,
})
stubs += 1
declared_calls.clear()
+
for msg in messages:
role = msg.get("role")
if role == "assistant":
@@ -2598,30 +2583,22 @@ def _pair_tool_calls_positionally(messages: List[Dict[str, Any]]) -> List[Dict[s
# Key on a stable representative of the alias group so a result matching ANY
# spelling can consume the call.
declared_calls[sorted(variants)[0]] = (tc, variants)
- paired.append(msg)
elif role == "tool":
result_variants = tool_result_id_variants(msg.get("tool_call_id"))
- matched = next(
- (key for key, (_tc, variants) in declared_calls.items() if variants & result_variants),
- None,
- )
+ matched = next((k for k, (_tc, v) in declared_calls.items() if v & result_variants), None)
if matched is None:
dropped += 1
continue
- paired.append(msg)
# Consume so a duplicate result reusing the id is dropped (strict providers reject duplicates).
declared_calls.pop(matched, None)
- else:
- if role == "user":
- # A user turn closes the tool-result run; later tool messages are orphans.
- _flush_unanswered_stubs()
- paired.append(msg)
+ elif role == "user":
+ # A user turn closes the tool-result run; later tool messages are orphans.
+ _flush_unanswered_stubs()
+ paired.append(msg)
# The transcript may end right after an unanswered assistant turn.
_flush_unanswered_stubs()
if dropped:
- _ra().logger.debug(
- "Pre-call sanitizer: removed %d positionally orphaned tool result(s)", dropped
- )
+ _ra().logger.debug("Pre-call sanitizer: removed %d positionally orphaned tool result(s)", dropped)
if stubs:
_ra().logger.debug(
"Pre-call sanitizer: added %d stub tool result(s) for "
@@ -2813,9 +2790,7 @@ def intent_ack_continuation_mode(agent) -> str:
def copy_reasoning_content_for_api(agent, source_msg: dict, api_msg: dict) -> None:
"""Forward reasoning fields onto an API replay message; policy lives in ``agent.message_sanitization.apply_reasoning_content_policy``."""
from agent.message_sanitization import apply_reasoning_content_policy
- apply_reasoning_content_policy(
- source_msg, api_msg, agent._needs_thinking_reasoning_pad()
- )
+ apply_reasoning_content_policy(source_msg, api_msg, agent._needs_thinking_reasoning_pad())
def reapply_reasoning_echo_for_provider(agent, api_messages: list) -> int:
@@ -2824,9 +2799,7 @@ def reapply_reasoning_echo_for_provider(agent, api_messages: list) -> int:
(DeepSeek/Kimi/MiMo) 400 without the pad, strict ones (Mistral, Cerebras, Groq) 400/422
with it. Idempotent; returns the number of assistant turns changed."""
from agent.message_sanitization import reapply_reasoning_echo
- return reapply_reasoning_echo(
- api_messages, agent._needs_thinking_reasoning_pad()
- )
+ return reapply_reasoning_echo(api_messages, agent._needs_thinking_reasoning_pad())
def _iter_httpx_pool_objects(http_client: Any):
@@ -2867,26 +2840,16 @@ def _connection_candidates(conn: Any):
def _socket_from_stream(stream: Any):
"""Raw socket behind an httpcore network stream (several backends), or None."""
sock = getattr(stream, "_sock", None)
+ if sock is None and callable(getattr(stream, "get_extra_info", None)):
+ with contextlib.suppress(Exception):
+ sock = stream.get_extra_info("socket")
if sock is None:
- get_extra_info = getattr(stream, "get_extra_info", None)
- if callable(get_extra_info):
- try:
- sock = get_extra_info("socket")
- except Exception:
- sock = None
- if sock is None:
- wrapped = getattr(stream, "stream", None)
- if wrapped is not None:
- sock = getattr(wrapped, "_sock", None)
- if sock is None:
+ sock = getattr(getattr(stream, "stream", None), "_sock", None)
+ if sock is None and callable(getattr(getattr(stream, "_stream", None), "extra", None)):
# anyio-backed streams expose the raw socket through SocketAttribute.raw_socket.
- extra = getattr(getattr(stream, "_stream", None), "extra", None)
- if callable(extra):
- try:
- from anyio.abc import SocketAttribute
- sock = extra(SocketAttribute.raw_socket)
- except Exception:
- sock = None
+ with contextlib.suppress(Exception):
+ from anyio.abc import SocketAttribute
+ sock = stream._stream.extra(SocketAttribute.raw_socket)
return sock
@@ -2898,9 +2861,7 @@ def _iter_pool_sockets(client: Any):
try:
# Some SDK wrappers *are* the httpx client; fall through so mount-aware discovery runs.
http_client = getattr(client, "_client", None)
- if http_client is None:
- http_client = client
- pools = list(_iter_httpx_pool_objects(http_client))
+ pools = list(_iter_httpx_pool_objects(client if http_client is None else http_client))
except Exception:
return
seen: set[int] = set()
@@ -2910,18 +2871,17 @@ def _iter_pool_sockets(client: Any):
if raw_conns is None:
raw_conns = getattr(pool, "_pool", None)
connections = list(raw_conns or [])
- for pool_req in list(getattr(pool, "_requests", None) or []):
- conn = getattr(pool_req, "connection", None)
- if conn is not None:
- connections.append(conn)
+ connections += [
+ c for c in (getattr(r, "connection", None) for r in list(getattr(pool, "_requests", None) or []))
+ if c is not None
+ ]
for conn in connections:
for candidate in _connection_candidates(conn):
stream = getattr(candidate, "_network_stream", None) or getattr(candidate, "_stream", None)
sock = _socket_from_stream(stream) if stream is not None else None
- if sock is None or id(sock) in seen:
- continue
- seen.add(id(sock))
- yield sock
+ if sock is not None and id(sock) not in seen:
+ seen.add(id(sock))
+ yield sock
def _socket_is_dead(sock) -> bool:
From a2fb880d790db201b47823a45023e8d4a1357fd6 Mon Sep 17 00:00:00 2001
From: Teknium <127238744+teknium1@users.noreply.github.com>
Date: Wed, 2 Sep 2026 19:51:54 -0700
Subject: [PATCH 15/20] refactor(agent/agent_runtime_helpers): flatten pool
walk, steer requeue, error-context, orphan classifier
---
agent/agent_runtime_helpers.py | 137 ++++++++++++---------------------
1 file changed, 49 insertions(+), 88 deletions(-)
diff --git a/agent/agent_runtime_helpers.py b/agent/agent_runtime_helpers.py
index 77b0e915c9..f5ab5e7a8f 100644
--- a/agent/agent_runtime_helpers.py
+++ b/agent/agent_runtime_helpers.py
@@ -2436,49 +2436,33 @@ def _classify_tool_call_orphans(messages: List[Dict[str, Any]]):
every id variant of a tool_call is registered so a result matching any alias survives, and
``orphaned_results`` are the actual dicts (filter by ``id(msg)``). ``sanitize_api_messages``
pairs positionally instead but shares the ``*_id_variants`` alias policy."""
- assistant_call_variants: List[tuple[Any, frozenset[str]]] = []
- surviving_call_ids: set[str] = set()
- for msg in messages:
- if msg.get("role") != "assistant":
- continue
- for tc in msg.get("tool_calls") or []:
- variants = tool_call_id_variants(tc)
- if variants:
- assistant_call_variants.append((tc, variants))
- surviving_call_ids.update(variants)
+ assistant_call_variants = [
+ (tc, variants)
+ for msg in messages if msg.get("role") == "assistant"
+ for tc in msg.get("tool_calls") or []
+ if (variants := tool_call_id_variants(tc))
+ ]
+ surviving_call_ids: set[str] = set().union(*(v for _, v in assistant_call_variants))
result_entries = [
- (msg, tool_result_id_variants(msg.get("tool_call_id")))
- for msg in messages
- if msg.get("role") == "tool"
- ]
- result_call_ids: set[str] = set()
- for _, variants in result_entries:
- result_call_ids.update(variants)
- orphaned_results = [
- msg for msg, variants in result_entries if variants and not (variants & surviving_call_ids)
+ (msg, tool_result_id_variants(msg.get("tool_call_id"))) for msg in messages if msg.get("role") == "tool"
]
+ result_call_ids: set[str] = set().union(*(v for _, v in result_entries))
+ orphaned_results = [msg for msg, v in result_entries if v and not (v & surviving_call_ids)]
orphaned_ids = {id(msg) for msg in orphaned_results}
- surviving_result_variants = [
- variants for msg, variants in result_entries if variants and id(msg) not in orphaned_ids
- ]
+ surviving_result_variants = [v for msg, v in result_entries if v and id(msg) not in orphaned_ids]
missing_tool_calls = [
- tc
- for tc, variants in assistant_call_variants
- if not any(variants & rv for rv in surviving_result_variants)
+ tc for tc, v in assistant_call_variants if not any(v & rv for rv in surviving_result_variants)
]
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]]:
"""Drop messages whose role the API won't accept."""
- filtered = []
+ valid = _ra().AIAgent._VALID_API_ROLES
for msg in messages:
- role = msg.get("role")
- if role not in _ra().AIAgent._VALID_API_ROLES:
- _ra().logger.debug("Pre-call sanitizer: dropping message with invalid role %r", role)
- continue
- filtered.append(msg)
- return filtered
+ if msg.get("role") not in valid:
+ _ra().logger.debug("Pre-call sanitizer: dropping message with invalid role %r", msg.get("role"))
+ 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]]:
@@ -2807,23 +2791,20 @@ def _iter_httpx_pool_objects(http_client: Any):
keepalive and proxy configs put live connections on ``client._mounts``, which a
``_transport``-only walk misses."""
seen_pools: set[int] = set()
-
- def _pools_for_transport(transport: Any):
- if transport is None:
- return
- # Connections live under ``_pool``; a directly mounted HTTPProxy *is* a ConnectionPool, so
- # ``_connections`` may sit on the transport itself.
- pool = getattr(transport, "_pool", None)
- if pool is None and getattr(transport, "_connections", None) is not None:
- pool = transport
- if pool is not None and id(pool) not in seen_pools:
- seen_pools.add(id(pool))
- yield pool
try:
- yield from _pools_for_transport(getattr(http_client, "_transport", None))
- mounts = getattr(http_client, "_mounts", None) or {}
- for _pattern, mounted in list(mounts.items()):
- yield from _pools_for_transport(mounted)
+ transports = [getattr(http_client, "_transport", None)]
+ transports += list((getattr(http_client, "_mounts", None) or {}).values())
+ for transport in transports:
+ if transport is None:
+ continue
+ # Connections live under ``_pool``; a directly mounted HTTPProxy *is* a ConnectionPool,
+ # so ``_connections`` may sit on the transport itself.
+ pool = getattr(transport, "_pool", None)
+ if pool is None and getattr(transport, "_connections", None) is not None:
+ pool = transport
+ if pool is not None and id(pool) not in seen_pools:
+ seen_pools.add(id(pool))
+ yield pool
except Exception:
return
@@ -2949,9 +2930,7 @@ def extract_api_error_context(error: Exception) -> Dict[str, Any]:
"""Extract structured rate-limit details from provider errors."""
context: Dict[str, Any] = {}
body = getattr(error, "body", None)
- payload = None
- if isinstance(body, dict):
- payload = body.get("error") if isinstance(body.get("error"), dict) else body
+ payload = (body.get("error") if isinstance(body.get("error"), dict) else body) if isinstance(body, dict) else None
if isinstance(payload, dict):
reason = payload.get("code") or payload.get("type") or payload.get("error")
if isinstance(reason, str) and reason.strip():
@@ -2962,11 +2941,9 @@ def extract_api_error_context(error: Exception) -> Dict[str, Any]:
message = payload.get("error")
if isinstance(message, str) and message.strip():
context["message"] = message.strip()
- for key in ("resets_at", "reset_at"):
- value = payload.get(key)
- if value not in {None, ""}:
- context["reset_at"] = value
- break
+ reset = next((payload.get(k) for k in ("resets_at", "reset_at") if payload.get(k) not in {None, ""}), None)
+ if reset is not None:
+ context["reset_at"] = reset
_set_reset_from_retry_after(context, payload.get("retry_after"))
headers = getattr(getattr(error, "response", None), "headers", None)
if headers:
@@ -2974,29 +2951,20 @@ def extract_api_error_context(error: Exception) -> Dict[str, Any]:
ratelimit_reset = headers.get("x-ratelimit-reset")
if ratelimit_reset and "reset_at" not in context:
context["reset_at"] = ratelimit_reset
- if "message" not in context:
- raw_message = str(error).strip()
- if raw_message:
- context["message"] = raw_message[:500]
- if "reset_at" not in context:
- message = context.get("message") or ""
- if isinstance(message, str):
- delay = _reset_delay_from_message(message)
- if delay is not None:
- context["reset_at"] = time.time() + delay
+ if "message" not in context and str(error).strip():
+ context["message"] = str(error).strip()[:500]
+ if "reset_at" not in context and isinstance(context.get("message") or "", str):
+ delay = _reset_delay_from_message(context.get("message") or "")
+ if delay is not None:
+ context["reset_at"] = time.time() + delay
return context
def _requeue_pending_steer(agent, steer_text: str) -> None:
"""Put drained steer text back so the caller's fallback delivers it as a next-turn user message."""
- lock = getattr(agent, "_pending_steer_lock", None)
- if lock is not None:
- with lock:
- existing = agent._pending_steer
- agent._pending_steer = (existing + "\n" + steer_text) if existing else steer_text
- return
- existing = getattr(agent, "_pending_steer", None)
- agent._pending_steer = (existing + "\n" + steer_text) if existing else steer_text
+ with getattr(agent, "_pending_steer_lock", None) or contextlib.nullcontext():
+ existing = getattr(agent, "_pending_steer", None)
+ agent._pending_steer = (existing + "\n" + steer_text) if existing else steer_text
def apply_pending_steer_to_tool_results(agent, messages: list, num_tool_msgs: int) -> None:
@@ -3009,30 +2977,23 @@ def apply_pending_steer_to_tool_results(agent, messages: list, num_tool_msgs: in
if not steer_text:
return
# Skip non-tool messages in the tail in case something else is appended at the boundary.
- target_idx = next(
- (
- j for j in range(len(messages) - 1, max(len(messages) - num_tool_msgs - 1, -1), -1)
- if isinstance(messages[j], dict) and messages[j].get("role") == "tool"
- ),
- None,
- )
- if target_idx is None:
+ tail = range(len(messages) - 1, max(len(messages) - num_tool_msgs - 1, -1), -1)
+ target = next((messages[j] for j in tail if isinstance(messages[j], dict) and messages[j].get("role") == "tool"), None)
+ if target is None:
# No tool result in this batch (e.g. all skipped by interrupt).
_requeue_pending_steer(agent, steer_text)
return
marker = format_steer_marker(steer_text)
- existing_content = messages[target_idx].get("content", "")
+ existing_content = target.get("content", "")
if isinstance(existing_content, str):
- messages[target_idx]["content"] = existing_content + marker
+ target["content"] = existing_content + marker
else:
# Anthropic multimodal content blocks: preserve them and append a text block.
try:
- blocks = list(existing_content) if existing_content else []
- blocks.append({"type": "text", "text": marker.lstrip()})
- messages[target_idx]["content"] = blocks
+ target["content"] = [*(existing_content or []), {"type": "text", "text": marker.lstrip()}]
except Exception:
# Fall back to string replacement if content shape is unexpected.
- messages[target_idx]["content"] = f"{existing_content}{marker}"
+ target["content"] = f"{existing_content}{marker}"
_ra().logger.info(
"Delivered /steer to agent after tool batch (%d chars): %s", len(steer_text),
steer_text[:120] + ("..." if len(steer_text) > 120 else ""),
From 78a9babc481356d766b31c960d8caf93369d890f Mon Sep 17 00:00:00 2001
From: Teknium <127238744+teknium1@users.noreply.github.com>
Date: Wed, 2 Sep 2026 19:56:34 -0700
Subject: [PATCH 16/20] refactor(agent/agent_runtime_helpers): tighten repair
passes and content flattening
---
agent/agent_runtime_helpers.py | 124 +++++++++++++--------------------
1 file changed, 48 insertions(+), 76 deletions(-)
diff --git a/agent/agent_runtime_helpers.py b/agent/agent_runtime_helpers.py
index f5ab5e7a8f..15651617d3 100644
--- a/agent/agent_runtime_helpers.py
+++ b/agent/agent_runtime_helpers.py
@@ -393,16 +393,12 @@ def _merge_consecutive_assistants(messages: List[Dict]) -> Tuple[List[Dict], int
repairs = 0
collapsed: List[Dict] = []
for msg in messages:
+ prev = collapsed[-1] if collapsed and isinstance(collapsed[-1], dict) else None
if (
- collapsed
- and isinstance(msg, dict)
- and msg.get("role") == "assistant"
- and isinstance(collapsed[-1], dict)
- and collapsed[-1].get("role") == "assistant"
- and not _is_codex_interim(msg)
- and not _is_codex_interim(collapsed[-1])
+ prev is not None and prev.get("role") == "assistant"
+ and isinstance(msg, dict) and msg.get("role") == "assistant"
+ and not _is_codex_interim(msg) and not _is_codex_interim(prev)
):
- prev = collapsed[-1]
# A provisional verification candidate is superseded, not unioned.
if prev.get("finish_reason") in {"verification_required", "verify_hook_continue"}:
collapsed[-1] = msg
@@ -419,28 +415,27 @@ def _drop_stray_tool_results(messages: List[Dict]) -> Tuple[List[Dict], int]:
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] = {}
+ known_tool_ids: Dict[str, int] = {} # alias -> group id; reset by assistant/user turns
matched_tool_groups: set = set()
next_tool_group = 0
filtered: List[Dict] = []
for msg in messages:
role = msg.get("role") if isinstance(msg, dict) else None
- if role == "assistant":
+ if role in ("assistant", "user"):
+ # An assistant turn starts a new tool-result run; a user turn closes it (later tool
+ # messages are orphans).
known_tool_ids = {}
matched_tool_groups = set()
- for tc in (msg.get("tool_calls") or []):
+ for tc in (msg.get("tool_calls") or []) if role == "assistant" else ():
variants = tool_call_id_variants(tc)
- if not variants:
- continue
- group_id = next_tool_group
- next_tool_group += 1
- for tc_id in variants:
- known_tool_ids.setdefault(tc_id, group_id)
+ if variants:
+ for tc_id in variants:
+ known_tool_ids.setdefault(tc_id, next_tool_group)
+ next_tool_group += 1
elif role == "tool":
result_variants = tool_result_id_variants(msg.get("tool_call_id"))
candidate_groups = {
- known_tool_ids[tc_id]
- for tc_id in result_variants
+ known_tool_ids[tc_id] for tc_id in result_variants
if tc_id in known_tool_ids and known_tool_ids[tc_id] not in matched_tool_groups
}
if result_variants and not candidate_groups:
@@ -448,10 +443,6 @@ def _drop_stray_tool_results(messages: List[Dict]) -> Tuple[List[Dict], int]:
continue
if candidate_groups:
matched_tool_groups.add(min(candidate_groups))
- elif role == "user":
- # A user turn closes the tool-result run; later tool messages are orphans.
- known_tool_ids = {}
- matched_tool_groups = set()
filtered.append(msg)
return filtered, repairs
@@ -462,32 +453,24 @@ def _prune_unanswered_tool_calls(messages: List[Dict]) -> Tuple[List[Dict], int]
dropped; codex interims exempt."""
repairs = 0
pruned: List[Dict] = []
- n = len(messages)
- i = 0
- while i < n:
- msg = messages[i]
- i += 1
+ for i, msg in enumerate(messages):
if not (
- isinstance(msg, dict)
- and msg.get("role") == "assistant"
- and msg.get("tool_calls")
+ isinstance(msg, dict) and msg.get("role") == "assistant" and msg.get("tool_calls")
and not _is_codex_interim(msg)
):
pruned.append(msg)
continue
answered: set = set()
- j = i
- while j < n and isinstance(messages[j], dict) and messages[j].get("role") == "tool":
- tid = (messages[j].get("tool_call_id") or "").strip()
+ for follower in messages[i + 1:]:
+ if not (isinstance(follower, dict) and follower.get("role") == "tool"):
+ break
+ tid = (follower.get("tool_call_id") or "").strip()
if tid:
answered.update(tool_result_id_variants(tid))
- j += 1
kept_calls = [tc for tc in msg["tool_calls"] if tool_call_id_variants(tc) & answered]
if len(kept_calls) != len(msg["tool_calls"]):
repairs += 1
- if not kept_calls and not _msg_has_payload(
- {k: v for k, v in msg.items() if k != "tool_calls"}
- ):
+ if not kept_calls and not _msg_has_payload({k: v for k, v in msg.items() if k != "tool_calls"}):
# Pruned calls were the only payload; drop the turn (empty assistant messages 400).
continue
if kept_calls:
@@ -500,34 +483,29 @@ def _prune_unanswered_tool_calls(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 split_user_originated_turn
+
repairs = 0
merged: List[Dict] = []
for msg in messages:
+ prev = merged[-1] if merged and isinstance(merged[-1], dict) else None
if (
- merged
- and isinstance(msg, dict)
- and msg.get("role") == "user"
- and isinstance(merged[-1], dict)
- and merged[-1].get("role") == "user"
- ):
- prev = merged[-1]
+ prev is not None and prev.get("role") == "user"
+ and isinstance(msg, dict) and msg.get("role") == "user"
# A summary carrier followed by a new user row is a deliberate durable shape after
# retry/rewind; never mutate the persisted carrier (sanitizers merge copies later).
- from agent.context_compressor import split_user_originated_turn
- handoff, _ = split_user_originated_turn(prev)
- prev_content = prev.get("content", "")
- new_content = msg.get("content", "")
+ and split_user_originated_turn(prev)[0] is None
# Only merge plain-text content; leave multimodal (list) content alone.
- if handoff is None and isinstance(prev_content, str) and isinstance(new_content, str):
- prev["content"] = (
- (prev_content + "\n\n" + new_content)
- if prev_content and new_content
- else (prev_content or new_content)
- )
- # Merged content invalidates the api_content sidecar; drop it so replay cannot use stale bytes.
- drop_stale_api_content(prev)
- repairs += 1
- continue
+ and isinstance(prev.get("content", ""), str) and isinstance(msg.get("content", ""), str)
+ ):
+ prev_content, new_content = prev.get("content", ""), msg.get("content", "")
+ prev["content"] = (
+ (prev_content + "\n\n" + new_content) if prev_content and new_content else (prev_content or new_content)
+ )
+ # Merged content invalidates the api_content sidecar; drop it so replay cannot use stale bytes.
+ drop_stale_api_content(prev)
+ repairs += 1
+ continue
merged.append(msg)
return merged, repairs
@@ -581,17 +559,15 @@ def _flatten_content_text(content: Any) -> str:
if isinstance(content, str):
return content
if isinstance(content, list):
- parts: list[str] = []
- for part in content:
- if isinstance(part, str):
- parts.append(part)
- elif isinstance(part, dict):
- if str(part.get("type") or "").strip().lower() in {"thinking", "reasoning", "redacted_thinking"}:
- continue
- text = part.get("text")
- if isinstance(text, str) and text:
- parts.append(text)
- return "".join(parts)
+ return "".join(
+ part if isinstance(part, str) else part.get("text")
+ for part in content
+ if isinstance(part, str) or (
+ isinstance(part, dict)
+ and str(part.get("type") or "").strip().lower() not in {"thinking", "reasoning", "redacted_thinking"}
+ and isinstance(part.get("text"), str) and part.get("text")
+ )
+ )
if isinstance(content, dict):
return str(content.get("text") or content.get("content") or "")
return str(content)
@@ -614,12 +590,8 @@ def strip_think_blocks(agent, content: str) -> str:
pairs, unterminated open tags at a block boundary (mirrors ``gateway/stream_consumer.py``),
stray orphan tags (all case-insensitive variants), and standalone tool-call XML blocks some
open models emit; ```` is boundary- and ``name=``-gated so prose mentions survive."""
- if not content:
- return ""
- content = _flatten_content_text(content)
- if not content:
- return ""
- for pattern in _THINK_STRIP_PATTERNS:
+ content = _flatten_content_text(content) if content else ""
+ for pattern in _THINK_STRIP_PATTERNS if content else ():
content = pattern.sub('', content)
return content
From d85caa9360bad56e524f63ec945626096fad7e53 Mon Sep 17 00:00:00 2001
From: Teknium <127238744+teknium1@users.noreply.github.com>
Date: Wed, 2 Sep 2026 20:01:00 -0700
Subject: [PATCH 17/20] refactor(agent/agent_runtime_helpers): compact
cache-plan/MoA/custom-route helpers
---
agent/agent_runtime_helpers.py | 63 ++++++++++------------------------
1 file changed, 18 insertions(+), 45 deletions(-)
diff --git a/agent/agent_runtime_helpers.py b/agent/agent_runtime_helpers.py
index 15651617d3..c4c348eac8 100644
--- a/agent/agent_runtime_helpers.py
+++ b/agent/agent_runtime_helpers.py
@@ -1255,10 +1255,7 @@ def _direct_native_anthropic_tool_cache_capability(
"""Return whether this resolved destination accepts native tool markers."""
eff_base_url = base_url if base_url is not None else (agent.base_url or "")
eff_api_mode = api_mode if api_mode is not None else (agent.api_mode or "")
- return (
- eff_api_mode == "anthropic_messages"
- and base_url_hostname(eff_base_url) == "api.anthropic.com"
- )
+ return eff_api_mode == "anthropic_messages" and base_url_hostname(eff_base_url) == "api.anthropic.com"
def cache_ttl_means_disabled(ttl: Any) -> bool:
@@ -1302,9 +1299,7 @@ def blank_cache_policy_stub(cache_disabled: Optional[bool] = None):
from types import SimpleNamespace
if cache_disabled is None:
cache_disabled = prompt_caching_disabled_from_config()
- return SimpleNamespace(
- provider="", base_url="", api_mode="", model="", _cache_disabled=bool(cache_disabled)
- )
+ return SimpleNamespace(provider="", base_url="", api_mode="", model="", _cache_disabled=bool(cache_disabled))
def plan_cache_sections_for_destination(
@@ -1321,29 +1316,19 @@ def plan_cache_sections_for_destination(
strip_anthropic_cache_control, strip_anthropic_tool_cache_control,
)
stub = blank_cache_policy_stub(cache_disabled)
- should_cache, native_layout = anthropic_prompt_cache_policy(
- stub, provider=provider, base_url=base_url, api_mode=api_mode, model=model
- )
+ dest = dict(provider=provider, base_url=base_url, api_mode=api_mode, model=model)
+ should_cache, native_layout = anthropic_prompt_cache_policy(stub, **dest)
if not should_cache:
canonical_messages = copy.deepcopy(messages or [])
strip_anthropic_cache_control(canonical_messages)
return canonical_messages, strip_anthropic_tool_cache_control(tools)
plan = build_prompt_cache_plan(
- messages,
- tools,
- cache_ttl=effective_cache_ttl(
- # effective_cache_ttl resolves None → "5m"; cache-disabled agents never reach here.
- cache_ttl,
- provider=provider,
- model=model,
- ),
+ messages, tools,
+ # effective_cache_ttl resolves None → "5m"; cache-disabled agents never reach here.
+ cache_ttl=effective_cache_ttl(cache_ttl, provider=provider, model=model),
native_anthropic=native_layout,
- static_system_prefix=(
- static_system_prefix if isinstance(static_system_prefix, str) else None
- ),
- direct_native_tool_cache=_direct_native_anthropic_tool_cache_capability(
- stub, provider=provider, base_url=base_url, api_mode=api_mode, model=model
- ),
+ static_system_prefix=static_system_prefix if isinstance(static_system_prefix, str) else None,
+ direct_native_tool_cache=_direct_native_anthropic_tool_cache_capability(stub, **dest),
# LiteLLM-style envelope routes forward part-level markers into tool_result.content[] →
# non-retryable 400.
tool_part_markers=envelope_tool_part_cache_markers_supported(provider, base_url),
@@ -1371,19 +1356,15 @@ def _moa_aggregator_cache_policy(agent, eff_model: str) -> tuple[bool, bool]:
from hermes_cli.config import load_config as _load_moa_cfg
from hermes_cli.moa_config import resolve_moa_preset
from hermes_cli.runtime_provider import resolve_runtime_provider
- preset = resolve_moa_preset(_load_moa_cfg().get("moa") or {}, eff_model or None)
- agg = preset.get("aggregator") or {}
+ agg = resolve_moa_preset(_load_moa_cfg().get("moa") or {}, eff_model or None).get("aggregator") or {}
agg_provider = str(agg.get("provider") or "").strip()
agg_model = str(agg.get("model") or "").strip()
if agg_provider and agg_model:
- agg_base_url = ""
- agg_api_mode = ""
- try:
+ agg_base_url = agg_api_mode = ""
+ with contextlib.suppress(Exception):
rt = resolve_runtime_provider(requested=agg_provider, target_model=agg_model)
agg_base_url = rt.get("base_url") or ""
agg_api_mode = rt.get("api_mode") or ""
- except Exception:
- pass
return anthropic_prompt_cache_policy(
agent, provider=agg_provider, base_url=agg_base_url, api_mode=agg_api_mode, model=agg_model
)
@@ -1400,21 +1381,13 @@ def _route_may_be_custom(agent, eff_provider: str, provider_lower: str, eff_base
# custom_provider_aliases) so spelling differences don't drop declarations.
from hermes_cli.providers import custom_provider_aliases
from hermes_cli.route_identity import normalize_route_base_url
- provider_ids = {provider_lower}
- if provider_lower.startswith("custom:"):
- provider_ids.add(provider_lower.removeprefix("custom:"))
+ provider_ids = {provider_lower, provider_lower.removeprefix("custom:")}
eff_url_normalized = normalize_route_base_url(eff_base_url)
- for entry in custom_providers:
- if not isinstance(entry, dict):
- continue
- entry_ids = custom_provider_aliases(
- str(entry.get("name") or ""), str(entry.get("provider_key") or "")
- )
- if provider_ids & entry_ids or (
- eff_url_normalized and normalize_route_base_url(entry.get("base_url")) == eff_url_normalized
- ):
- return True
- return False
+ return any(
+ provider_ids & custom_provider_aliases(str(entry.get("name") or ""), str(entry.get("provider_key") or ""))
+ or (eff_url_normalized and normalize_route_base_url(entry.get("base_url")) == eff_url_normalized)
+ for entry in custom_providers if isinstance(entry, dict)
+ )
if custom_providers is not None:
return False # attached empty list never matches
# None = list not attached yet (early init or blank stub). Avoid rebuilding the list for
From 58ae7dcff61e5349fb0e62a02f3f3853933658d3 Mon Sep 17 00:00:00 2001
From: Teknium <127238744+teknium1@users.noreply.github.com>
Date: Wed, 2 Sep 2026 20:05:25 -0700
Subject: [PATCH 18/20] refactor(agent/agent_runtime_helpers): compact
transport recovery, user-merge, reset gate, turn tripwire
---
agent/agent_runtime_helpers.py | 37 ++++++++++++----------------------
1 file changed, 13 insertions(+), 24 deletions(-)
diff --git a/agent/agent_runtime_helpers.py b/agent/agent_runtime_helpers.py
index c4c348eac8..cffe6eb66c 100644
--- a/agent/agent_runtime_helpers.py
+++ b/agent/agent_runtime_helpers.py
@@ -302,8 +302,7 @@ def note_turn_start(agent, turn_id: str):
"turn %s starting while turn %s (started %.0fs ago) has not "
"completed its turn-end persist (session=%s) — concurrent turns "
"on one session; transcript writes may interleave", turn_id, prev,
- time.time() - prev_started if prev_started else -1.0,
- getattr(agent, "session_id", None) or "-",
+ time.time() - prev_started if prev_started else -1.0, getattr(agent, "session_id", None) or "-",
)
overlap = prev
# Cross-agent leg: same session_id in flight under another agent object (busy guard is keyed by
@@ -872,18 +871,13 @@ def try_recover_primary_transport(
) -> bool:
"""Rebuild the primary client once and retry after ``max_retries`` exhaust on a transient
transport error. Skipped for aggregators (OpenRouter, Nous) that manage retries server-side."""
- if agent._fallback_activated:
- return False
error_type = type(api_error).__name__
- if error_type not in _TRANSIENT_TRANSPORT_ERRORS:
+ if agent._fallback_activated or error_type not in _TRANSIENT_TRANSPORT_ERRORS or agent._is_openrouter_url():
return False
- if agent._is_openrouter_url():
- return False
- provider_lower = (agent.provider or "").strip().lower()
# Portal OpenAI-wire traffic rides aggregator retry infra (skip), but Portal Claude on native
# Messages holds a local Anthropic client that needs the rebuild.
if (
- provider_lower in {"nous", "nous-portal", "nousresearch"}
+ (agent.provider or "").strip().lower() in {"nous", "nous-portal", "nousresearch"}
and getattr(agent, "api_mode", None) != "anthropic_messages"
):
return False
@@ -901,9 +895,7 @@ def try_recover_primary_transport(
from agent.moa_loop import build_moa_facade
agent.client = build_moa_facade(agent, agent.model)
else:
- agent.client = agent._create_openai_client(
- dict(rt["client_kwargs"]), reason="primary_recovery", shared=True
- )
+ agent.client = agent._create_openai_client(dict(rt["client_kwargs"]), reason="primary_recovery", shared=True)
wait_time = min(3 + retry_count, 8)
agent._vprint(
f"{agent.log_prefix}🔁 Transient {error_type} on {agent.provider} — "
@@ -915,8 +907,6 @@ def try_recover_primary_transport(
logger.warning("Primary transport recovery failed: %s", e)
return False
-# ── End provider fallback ──────────────────────────────────────────────
-
def _merge_user_content(prev_content: Any, cur_content: Any) -> Any:
"""Merged content for two adjacent user messages (``_UNMERGEABLE`` for unknown shapes):
@@ -952,16 +942,16 @@ def drop_thinking_only_and_merge_users(
merges = 0
for m in kept:
prev = merged[-1] if merged else None
+ content = _UNMERGEABLE
if prev is not None and prev.get("role") == "user" and m.get("role") == "user":
content = _merge_user_content(prev.get("content", ""), m.get("content", ""))
- if content is not _UNMERGEABLE:
- # Copy ``prev`` so caller dicts are never mutated.
- merged[-1] = {**prev, "content": content}
- merges += 1
- continue
- # Unknown content shape: append separately (violates alternation, but safer than raising
- # in a hot path).
- merged.append(m)
+ if content is _UNMERGEABLE:
+ # Not a user pair, or an unknown content shape: append separately (the latter violates
+ # alternation, but is safer than raising in a hot path).
+ merged.append(m)
+ else:
+ merged[-1] = {**prev, "content": content} # copy so caller dicts are never mutated
+ merges += 1
if dropped == 0 and merges == 0:
return messages
_ra().logger.debug(
@@ -975,8 +965,7 @@ def _primary_reset_gate_blocks(agent, rt, primary_provider, primary_runtime_base
"""Reset-aware gate: skip a guaranteed-to-fail restore while the primary pool reports a
future reset; fails open on any error/None. Returns ``(blocked, prefetched_pool, prefetched)``
so the rebind step reuses the loaded pool (one auth.json read at most)."""
- prefetched_pool = None
- prefetched = False
+ prefetched_pool, prefetched = None, False
try:
pool = getattr(agent, "_credential_pool", None)
if not matches_primary(pool):
From db579c75a9b8e06b363401929cfc2513911a51b6 Mon Sep 17 00:00:00 2001
From: Teknium <127238744+teknium1@users.noreply.github.com>
Date: Wed, 2 Sep 2026 20:10:03 -0700
Subject: [PATCH 19/20] refactor(agent/agent_runtime_helpers): collapse marker
prepend, cursor prefix, ttl predicate
---
agent/agent_runtime_helpers.py | 47 +++++++++++++---------------------
1 file changed, 18 insertions(+), 29 deletions(-)
diff --git a/agent/agent_runtime_helpers.py b/agent/agent_runtime_helpers.py
index cffe6eb66c..bdf9f8dbef 100644
--- a/agent/agent_runtime_helpers.py
+++ b/agent/agent_runtime_helpers.py
@@ -175,20 +175,14 @@ def convert_to_trajectory_format(agent, messages: List[Dict[str, Any]], user_que
def _prepend_corruption_marker(tool_msg: dict, marker: str) -> None:
existing = tool_msg.get("content")
- if isinstance(existing, str):
- if not existing:
- tool_msg["content"] = marker
- elif not existing.startswith(marker):
- tool_msg["content"] = f"{marker}\n{existing}"
+ if isinstance(existing, str) and existing.startswith(marker):
return
- if existing is None:
- tool_msg["content"] = marker
- return
- try:
- existing_text = json.dumps(existing)
- except TypeError:
- existing_text = str(existing)
- tool_msg["content"] = f"{marker}\n{existing_text}"
+ if not isinstance(existing, (str, type(None))):
+ try:
+ existing = json.dumps(existing)
+ except TypeError:
+ existing = str(existing)
+ tool_msg["content"] = f"{marker}\n{existing}" if existing else marker
def _find_tool_result(messages: list, start: int, tool_call: dict) -> Optional[dict]:
@@ -203,13 +197,11 @@ def _find_tool_result(messages: list, start: int, tool_call: dict) -> Optional[d
def _cursor_skip_prefix(messages: list, cursor: Optional[dict]) -> int:
"""Length of the ``is``-identical prefix already validated on the previous call."""
+ prev_prefix = cursor.get("prefix") if cursor is not None else None
start = 0
- if cursor is not None:
- prev_prefix = cursor.get("prefix")
- if isinstance(prev_prefix, list):
- limit = min(len(prev_prefix), len(messages))
- while start < limit and messages[start] is prev_prefix[start]:
- start += 1
+ if isinstance(prev_prefix, list):
+ while start < min(len(prev_prefix), len(messages)) and messages[start] is prev_prefix[start]:
+ start += 1
return start
@@ -1216,11 +1208,10 @@ def dump_api_request_debug(
}
if error is not None:
dump_payload["error"] = _api_error_debug_info(error)
- timestamp = datetime.now().strftime("%Y%m%d_%H%M%S_%f")
# Sanitize the session ID (may come from an untrusted X-Hermes-Session-Id header) so a
# "../"-shaped ID cannot write outside logs_dir.
safe_sid = _ra()._safe_session_filename_component(agent.session_id)
- dump_file = agent.logs_dir / f"request_dump_{safe_sid}_{timestamp}.json"
+ dump_file = agent.logs_dir / f"request_dump_{safe_sid}_{datetime.now().strftime('%Y%m%d_%H%M%S_%f')}.json"
# Redact secrets first: this fires unconditionally on API errors and captures the full
# request body, so context-embedded secrets would otherwise land in cleartext on disk.
from agent.redact import redact_sensitive_text
@@ -1247,18 +1238,16 @@ def _direct_native_anthropic_tool_cache_capability(
return eff_api_mode == "anthropic_messages" and base_url_hostname(eff_base_url) == "api.anthropic.com"
+# The cache_ttl tiers accepted by config; mirrored by agent_init's live-agent snapshot.
+VALID_CACHE_TTLS = ("5m", "1h")
+
+
def cache_ttl_means_disabled(ttl: Any) -> bool:
"""True when a ``prompt_caching.cache_ttl`` value means caching off (single predicate shared
by ``agent_init`` and the stub policy paths). Unknown values (``"2h"``, ints) are NOT a disable."""
- if ttl in ("5m", "1h"):
+ if ttl in VALID_CACHE_TTLS:
return False
- if ttl is False or ttl is None:
- return True
- return str(ttl).lower() in ("off", "false", "disabled", "no", "none")
-
-
-# The cache_ttl tiers accepted by config; mirrored by agent_init's live-agent snapshot.
-VALID_CACHE_TTLS = ("5m", "1h")
+ return ttl is False or ttl is None or str(ttl).lower() in ("off", "false", "disabled", "no", "none")
def _raw_cache_ttl_from_config(default: Any) -> Any:
From 1bb0ebf47cbc92ab74a598997e38cfeec37ea83c Mon Sep 17 00:00:00 2001
From: Teknium <127238744+teknium1@users.noreply.github.com>
Date: Wed, 2 Sep 2026 20:17:58 -0700
Subject: [PATCH 20/20] refactor(agent/agent_runtime_helpers): final defensive
collapses (debug dump, pool rebind, tool-name repair)
---
agent/agent_runtime_helpers.py | 93 ++++++++++++----------------------
1 file changed, 32 insertions(+), 61 deletions(-)
diff --git a/agent/agent_runtime_helpers.py b/agent/agent_runtime_helpers.py
index bdf9f8dbef..839181801e 100644
--- a/agent/agent_runtime_helpers.py
+++ b/agent/agent_runtime_helpers.py
@@ -980,15 +980,12 @@ def _primary_reset_gate_blocks(agent, rt, primary_provider, primary_runtime_base
def _restore_runtime_capabilities(agent, rt: Dict[str, Any]) -> None:
- if "runtime_capabilities" in rt:
- raw = rt["runtime_capabilities"]
- if not isinstance(raw, dict):
- logger.warning("Ignoring malformed runtime capabilities snapshot")
- else:
- agent.runtime_capabilities = dict(raw)
- elif isinstance(rt.get("capabilities"), dict):
- # Snapshots written by the initial capability propagation patch.
- agent.runtime_capabilities = dict(rt["capabilities"])
+ # ``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):
+ agent.runtime_capabilities = dict(raw)
+ elif "runtime_capabilities" in rt:
+ logger.warning("Ignoring malformed runtime capabilities snapshot")
def _rebind_primary_credential_pool(agent, primary_provider, matches_primary, load_primary_pool, prefetched_pool, prefetched) -> None:
@@ -1010,13 +1007,8 @@ def _rebind_primary_credential_pool(agent, primary_provider, matches_primary, lo
)
agent._credential_pool_entry_id = None
pool = getattr(agent, "_credential_pool", None)
- if pool is None or not pool.has_available():
- return
- entry = pool.select()
- if entry is None:
- return
- entry_key = getattr(entry, "runtime_api_key", None) or getattr(entry, "access_token", "")
- if not entry_key:
+ entry = pool.select() if pool is not None and pool.has_available() else None
+ if entry is None or not (getattr(entry, "runtime_api_key", None) or getattr(entry, "access_token", "")):
return
if matches_primary(entry):
# _swap_credential rebuilds the client and reapplies base-url-scoped headers.
@@ -1064,12 +1056,9 @@ def restore_primary_runtime(agent) -> bool:
return False
agent._restore_wait_logged = False
fallback_route = getattr(agent, "_provider_fallback_route", None)
- if isinstance(fallback_route, (list, tuple)) and len(fallback_route) == 2:
- previous_model = str(fallback_route[0] or "unknown")
- previous_provider = str(fallback_route[1] or "unknown")
- else:
- previous_model = str(getattr(agent, "model", "") or "unknown")
- previous_provider = str(getattr(agent, "provider", "") or "unknown")
+ if not (isinstance(fallback_route, (list, tuple)) and len(fallback_route) == 2):
+ fallback_route = (getattr(agent, "model", ""), getattr(agent, "provider", ""))
+ previous_model, previous_provider = (str(v or "unknown") for v in fallback_route)
provider_fallback_active = bool(getattr(agent, "_provider_fallback_active", False))
try:
_apply_primary_runtime_fields(agent, rt)
@@ -1183,9 +1172,7 @@ def dump_api_request_debug(
) -> Optional[Path]:
"""Dump the request body from api_kwargs (minus transport keys) for debugging provider 4xx failures."""
try:
- body = copy.deepcopy(api_kwargs)
- body.pop("timeout", None)
- body = {k: v for k, v in body.items() if v is not None}
+ body = {k: v for k, v in copy.deepcopy(api_kwargs).items() if v is not None and k != "timeout"}
api_key = None
try:
api_key = getattr(agent.client, "api_key", None)
@@ -1193,12 +1180,9 @@ def dump_api_request_debug(
_ra().logger.debug("Could not extract API key for debug dump: %s", e)
endpoint = "/responses" if agent.api_mode == "codex_responses" else "/chat/completions"
dump_payload: Dict[str, Any] = {
- "timestamp": datetime.now().isoformat(),
- "session_id": agent.session_id,
- "reason": reason,
+ "timestamp": datetime.now().isoformat(), "session_id": agent.session_id, "reason": reason,
"request": {
- "method": "POST",
- "url": f"{agent.base_url.rstrip('/')}{endpoint}",
+ "method": "POST", "url": f"{agent.base_url.rstrip('/')}{endpoint}",
"headers": {
"Authorization": f"Bearer {agent._mask_api_key_for_logs(api_key)}",
"Content-Type": "application/json",
@@ -1752,16 +1736,13 @@ def _build_switched_client(agent, new_provider, api_key, base_url, api_mode, new
"MiniMax OAuth: failed to install per-request token provider "
"on switch (%s); using static bearer.", _mm_exc,
)
- agent.api_key = effective_key
- agent._anthropic_api_key = effective_key
+ agent.api_key = agent._anthropic_api_key = effective_key
agent._anthropic_base_url = base_url or getattr(agent, "_anthropic_base_url", None)
agent._anthropic_client = build_anthropic_client(
effective_key, agent._anthropic_base_url,
timeout=get_provider_request_timeout(agent.provider, agent.model),
)
- agent._is_anthropic_oauth = (
- _is_oauth_token(effective_key) if (is_native_anthropic and isinstance(effective_key, str)) else False
- )
+ agent._is_anthropic_oauth = bool(is_native_anthropic and isinstance(effective_key, str) and _is_oauth_token(effective_key))
agent.client = None
agent._client_kwargs = {}
return
@@ -1794,8 +1775,7 @@ def _swap_switch_runtime(agent, new_model, new_provider, api_key, base_url, api_
# Clear the per-config override so the new model's context window is re-resolved.
agent._config_context_length = None
agent.model = new_model
- agent.provider = new_provider
- agent.requested_provider = new_provider
+ agent.provider = agent.requested_provider = new_provider
# Re-read reasoning_echo so the flag reflects the new primary model (see _reasoning_echo_opt_in).
agent._reasoning_echo_flag = agent._read_reasoning_echo_from_config()
# Empty base_url while the provider changes means upstream resolution failed; falling back to
@@ -2144,19 +2124,12 @@ def repair_tool_call(agent, tool_name: str) -> str | None:
tool_name = tool_name[:_idx]
if not tool_name:
return None
-
- def _norm(s: str) -> str:
- return s.lower().replace("-", "_").replace(" ", "_")
-
- def _camel_snake(s: str) -> str:
- return re.sub(r"(? str | None:
lc = s.lower()
- for suffix in ("_tool", "-tool", "tool"):
- if lc.endswith(suffix):
- return s[: -len(suffix)].rstrip("_-")
- return None
+ return next((s[: -len(sfx)].rstrip("_-") for sfx in ("_tool", "-tool", "tool") if lc.endswith(sfx)), None)
# Cheap fast-paths first.
lowered = tool_name.lower()
if lowered in agent.valid_tool_names:
@@ -2189,27 +2162,25 @@ _EMPTY_HEAL_ESCALATE_AFTER = 3
_EMPTY_HEAL_WINDOW_S = 600.0
_empty_heal_log_state: Dict[str, Dict[str, Any]] = {}
_empty_heal_log_lock = threading.Lock()
-
-# Sessions already given the one-time user notice; separate from the windowed log state so the user
-# is told ONCE per session (out-of-band, never in conversation context).
+# 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.
_empty_heal_user_notified: set = set()
-
-# One-shot pending notices keyed by session, drained by the conversation loop via
-# ``consume_pending_sanitizer_heal_notice`` and delivered via the status/warning callback.
+# 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] = {}
def _content_has_payload(content: Any) -> bool:
if isinstance(content, str):
return bool(content.strip())
- if isinstance(content, list):
- # Any typed block counts, as long as a text block is not itself blank.
- return any(
- (block.get("type") != "text" or (isinstance(block.get("text"), str) and block["text"].strip()))
- if isinstance(block, dict) else bool(block)
- for block in content
- )
- return content not in (None, "")
+ if not isinstance(content, list):
+ return content not in (None, "")
+ # Any typed block counts, as long as a text block is not itself blank.
+ return any(
+ (block.get("type") != "text" or (isinstance(block.get("text"), str) and block["text"].strip()))
+ if isinstance(block, dict) else bool(block)
+ for block in content
+ )
def _msg_has_payload(msg: Dict[str, Any]) -> bool: