Files
hermes-agent/gateway/response_filters.py
T
Teknium 3c7069bdcb refactor(gateway): dead-code removal, helper unification, defensive-layer collapse and rationale-preserving comment compaction across 40 modules
authz_mixin, browser_control_broker, delivery, delivery_ledger, display_config, drain_control,
hosted_room_links/peer/policy_checkpoint, hosted_rooms, platform_registry, relay/__init__,
relay/ws_transport, run.py and slash_commands.py (comments), session_context, session_state,
streaming_tts_consumer, turn_lease and small modules.

- HostedRoomPolicyCheckpoint._apply_event -> per-kind handler table
- WebSocketRelayTransport._handle_frame -> frame-handler table
- GatewayAuthorizationMixin: unified adapter setting/flag/extra readers
- dead symbols removed (verified zero references): RoomLinkProbe/select_room_link,
  relay_bot_username, is_restart_loop_tripped, debug_rows, DeadTargetRegistry.all_dead,
  BrowserControlBroker.detach_owner/_prune_tickets, StreamingTTSConsumer.started/_enqueue_done/
  _iter_stream_chunks/_next_stream_chunk, RecoverableHandleCache.status_for, _auth_env,
  _copy_default_catalog, _parse_timestamp_prefix, _present_* helpers, _send_result_error_kind,
  _truthy_env, SessionFieldView/TurnLeaseTokenView dunder shims, and their orphaned tests.
- lost WHY/invariant text from the earlier compaction restored compactly (541 hunks audited)
2026-09-02 13:30:50 -07:00

125 lines
4.8 KiB
Python

"""Gateway response filtering helpers.
These decide whether a completed agent turn should be delivered to the chat,
not what should be persisted in conversation history.
"""
from __future__ import annotations
import unicodedata
from typing import Any
# Exact whole-response markers meaning "the agent intentionally chose not to
# reply". Keep small and explicit; arbitrary empty output remains an
# error/empty-response path, not silence.
LIVE_GATEWAY_SILENT_MARKERS = frozenset({
"[SILENT]",
"SILENT",
"NO_REPLY",
"NO REPLY",
})
# Longer than any marker could plausibly be, even with stray punctuation.
_MARKER_LENGTH_CAP = 64
def _canonical_silence_candidate(text: str) -> str:
return " ".join(text.strip().upper().split())
def _is_edge_punctuation(ch: str) -> bool:
# Square brackets stay structural so malformed ``[SILENT`` cannot become ``SILENT``.
return ch not in "[]" and unicodedata.category(ch).startswith("P")
def _strip_edge_silence_punctuation(text: str) -> str:
"""Strip stray edge punctuation (``.NO_REPLY``, ``*NO_REPLY*``) without erasing marker structure."""
start, end = 0, len(text)
while start < end and _is_edge_punctuation(text[start]):
start += 1
while end > start and _is_edge_punctuation(text[end - 1]):
end -= 1
return text[start:end].strip()
def _canonical_silence_candidates(text: str) -> tuple[str, ...]:
exact = _canonical_silence_candidate(text)
stripped = _strip_edge_silence_punctuation(text.strip())
if stripped == text.strip():
return (exact,)
return (exact, _canonical_silence_candidate(stripped))
def _short_stripped(text: Any) -> str:
"""Stripped text if it is a non-empty string within the marker cap, else ''."""
if not isinstance(text, str):
return ""
stripped = text.strip()
return stripped if 0 < len(stripped) <= _MARKER_LENGTH_CAP else ""
def is_intentional_silence_response(response: Any) -> bool:
"""True only when ``response`` is exactly a silence marker.
Prose that merely mentions ``NO_REPLY`` must be delivered normally. A blank
response is not silence either — that is the empty-response failure path.
"""
stripped = _short_stripped(response)
return bool(stripped) and any(
c in LIVE_GATEWAY_SILENT_MARKERS for c in _canonical_silence_candidates(stripped)
)
def is_autonomous_silence_response(response: Any) -> bool:
"""Loose silence matcher for autonomous lanes (cron, webhook).
Autonomous lanes ask for ``[SILENT]`` when a tick produced nothing worth
attention, and models reliably bracket the marker with a short note. Unlike
:func:`is_intentional_silence_response` (interactive rule: EXACTLY a marker),
this suppresses when a marker is the whole response, sits on its own first or
last line, or the bracketed sentinel opens the response (``[SILENT] No
changes detected``). A token buried mid-sentence is still delivered.
Shares :data:`LIVE_GATEWAY_SILENT_MARKERS` so the two sets cannot drift.
"""
if not isinstance(response, str):
return False
stripped = response.strip()
if not stripped:
return False
def _is_token(line: str) -> bool:
return _canonical_silence_candidate(line) in LIVE_GATEWAY_SILENT_MARKERS
if _is_token(stripped):
return True
lines = [ln for ln in stripped.splitlines() if ln.strip()]
if lines and (_is_token(lines[0]) or _is_token(lines[-1])):
return True
# Bracketed form only, so a bare "Silent retry succeeded" is NOT swallowed.
return stripped.upper().startswith("[SILENT]")
def is_intentional_silence_agent_result(agent_result: dict | None, response: Any) -> bool:
"""Silence markers suppress delivery only for successful agent turns."""
if not isinstance(agent_result, dict) or agent_result.get("failed"):
return False
return is_intentional_silence_response(response)
def is_partial_silence_marker(text: Any) -> bool:
"""True while streamed ``text`` could still resolve to a silence marker.
The streaming path must decide, before the whole response is known, whether
to show its buffer. A buffer whose canonical form is a non-empty *prefix* of
a marker (``"NO"`` on the way to ``"NO_REPLY"``, or an exact marker not yet
terminated by stream-end) is held back so a raw marker is never shown and
then retracted. Anything that has diverged from every marker, or exceeds the
marker cap, returns False so normal streaming resumes. Shares the marker set
and canonicalization with :func:`is_intentional_silence_response`.
"""
stripped = _short_stripped(text)
return bool(stripped) and any(
c and any(marker.startswith(c) for marker in LIVE_GATEWAY_SILENT_MARKERS)
for c in _canonical_silence_candidates(stripped)
)