Files
hermes-agent/tools/tool_output_truncate.py
teknium1 398234748f refactor(agent): one Retry-After parser and one reset-grammar table feed every retry wait
Seven sites hand-rolled `float(headers.get("Retry-After"))` (anon_auth,
shared_metrics_sender, gemini_native_adapter, extract_api_error_context,
nous_rate_guard, skills_hub_github, skills_hub_clawhub x2) and silently
dropped RFC 7231 HTTP-date values that the conversation loop already honours
via agent/retry_utils.py::parse_retry_after_seconds. They now call it; per-site
caps/floors stay at the call site.

The free-text "resets in / quotaResetDelay / retry after N s" regexes lived in
two tables (agent_runtime_helpers vs credential_pool) whose "resets in"
grammars diverged: the pool accepted only integer `Nhr Nmin` while the error
context accepted h/hr/hours + m/min/minutes + s/seconds with decimals. One table
(agent/retry_utils.py::RETRY_DELAY_PATTERNS / reset_delay_from_message) using
the wider grammar, so a pooled credential's cooldown and the UI's reset time
now agree.
2026-09-13 05:09:43 -07:00

33 lines
1.4 KiB
Python

"""Head/tail truncation for oversized tool output (terminal, execute_code, MCP results).
One algorithm and one notice wording: 40% head (errors surface early) / 60% tail (the most
recent lines matter most) around a single ``... [<LABEL> TRUNCATED - N <unit> omitted out of
T total] ...`` marker, so downstream code that recognises the marker sees one shape. Limits
live in ``tools/tool_output_limits.py``; line-snapped, path-bearing footers (web/browser,
read_file pagination) are different products and stay separate.
"""
from __future__ import annotations
HEAD_RATIO = 0.4
def truncation_notice(omitted: int, total: int, *, label: str = "OUTPUT", unit: str = "chars") -> str:
return f"\n\n... [{label} TRUNCATED - {omitted:,} {unit} omitted out of {total:,} total] ...\n\n"
def head_tail_split(budget: int) -> tuple[int, int]:
"""``(head, tail)`` character budgets for ``budget`` total."""
head = int(budget * HEAD_RATIO)
return head, budget - head
def truncate_head_tail(text: str, max_chars: int, *, label: str = "OUTPUT") -> str:
"""``text`` unchanged when it fits ``max_chars``; otherwise head + notice + tail (the kept
text is exactly ``max_chars`` long, the notice rides on top)."""
if len(text) <= max_chars:
return text
head, tail = head_tail_split(max_chars)
omitted = len(text) - head - tail
return text[:head] + truncation_notice(omitted, len(text), label=label) + text[-tail:]