refactor(gateway): compact docstrings in slash mixins (keep every WHY)

This commit is contained in:
Teknium
2026-09-02 19:45:24 -07:00
parent 6847c6dc4b
commit 0f4123ae1d
3 changed files with 74 additions and 152 deletions
+45 -89
View File
@@ -209,11 +209,9 @@ class GatewaySlashCommandsMixin(
# ------------------------------------------------------------------ shared helpers
def _cached_agent_for(self, session_key: str):
"""Peek the cached AIAgent for *session_key* without evicting it, or None.
Cache entries are ``(agent, signature, ...)`` tuples; bare agents (test doubles) are
accepted too. Lock/cache may be absent on fixtures that skip ``__init__``.
"""
"""Peek the cached AIAgent for *session_key* without evicting it, or None. Entries are
``(agent, signature, ...)`` tuples (bare agents from test doubles accepted); lock/cache may
be absent on fixtures that skip ``__init__``."""
cache = getattr(self, "_agent_cache", None)
if cache is None:
return None
@@ -227,10 +225,8 @@ class GatewaySlashCommandsMixin(
return entry or None
def _resident_agent_for(self, session_key: str):
"""The live running agent for *session_key*, else the cached one, else None.
The pending sentinel (a run that is starting) never counts as a usable agent.
"""
"""The live running agent for *session_key*, else the cached one, else None. The pending
sentinel (a run that is starting) never counts as a usable agent."""
from gateway.run import _AGENT_PENDING_SENTINEL
agent = self._running_agents.get(session_key)
if agent is not None and agent is not _AGENT_PENDING_SENTINEL:
@@ -271,12 +267,9 @@ class GatewaySlashCommandsMixin(
)
def _write_approval_setter(self, section: str, event: MessageEvent):
"""``set_mode_fn`` for /memory and /skills: persist ``<section>.write_approval``.
Raw read is correct for the write-back round-trip (merged defaults must not be persisted
back to the user's file). The new setting must take effect next message, so the cached
agent is dropped.
"""
"""``set_mode_fn`` for /memory and /skills: persist ``<section>.write_approval``. Raw read is
correct for the write-back round-trip (merged defaults must not be persisted back to the
user's file); the cached agent is dropped so the setting takes effect next message."""
from gateway.run import _gateway_config_home
from hermes_cli.config import read_user_config_raw
config_path = _gateway_config_home() / "config.yaml"
@@ -291,12 +284,9 @@ class GatewaySlashCommandsMixin(
return _set_approval
async def _deliver_approval_confirmation(self, event: MessageEvent, confirmation_text: str, verb: str):
"""Return *confirmation_text* for normal delivery, or push it on native-streaming adapters.
Native-streaming adapters (WeCom msgtype:"stream") need the confirmation sent directly with
control-lane metadata (reliable proactive send, not the finalized reply stream). Everyone
else returns text for normal delivery. (``is not True``: mocks auto-create attrs.)
"""
"""Return *confirmation_text* for normal delivery, or push it on native-streaming adapters
(WeCom msgtype:"stream"), which need it sent directly with control-lane metadata (reliable
proactive send, not the finalized reply stream). ``is not True``: mocks auto-create attrs."""
source = event.source
adapter = self.adapters.get(source.platform)
if adapter:
@@ -360,8 +350,7 @@ class GatewaySlashCommandsMixin(
])
async def _handle_whoami_command(self, event: MessageEvent) -> str:
"""Handle /whoami — the user's slash command access on this scope (always allowed: slash_access
floor). Reports platform, DM-vs-group scope, tier, and the commands the user can run here."""
"""Handle /whoami — platform, DM-vs-group scope, tier and runnable commands (always allowed)."""
from gateway.slash_access import policy_for_source
source = event.source
policy = policy_for_source(self.config, source)
@@ -381,11 +370,8 @@ class GatewaySlashCommandsMixin(
return head + f"Tier: user\nSlash commands you can run: {runnable_str}"
async def _handle_kanban_command(self, event: MessageEvent) -> str:
"""Handle /kanban — delegate to the shared kanban CLI.
DB work runs in a thread pool to keep the event loop responsive. Reads and mutations are
allowed while an agent runs: the board is profile-agnostic and never touches agent state.
"""
"""Handle /kanban — delegate to the shared kanban CLI (DB work in a thread pool). Allowed
while an agent runs: the board is profile-agnostic and never touches agent state."""
from hermes_cli.kanban import run_slash
# Strip the leading "/kanban" (with or without slash), leaving args.
@@ -521,8 +507,7 @@ class GatewaySlashCommandsMixin(
async def _handle_platform_command(self, event: MessageEvent) -> str:
"""Handle ``/platform list|pause|resume [name]`` — inspect and manually control failed/paused
gateway adapters (pause stops the reconnect watcher; resume re-queues for retry).
"""
adapters (pause stops the reconnect watcher; resume re-queues for retry)."""
text = (getattr(event, "content", "") or "").strip()
# Strip the leading "/platform" (or "/PLATFORM") token if present
parts = text.split(maxsplit=2)
@@ -861,8 +846,7 @@ class GatewaySlashCommandsMixin(
async def _handle_background_command(self, event: MessageEvent) -> str:
"""Handle /bg <prompt> — run a prompt in a background thread with its own session; the
result is sent to the same chat without touching the active session's history.
"""
result is sent to the same chat without touching the active session's history."""
prompt = event.get_command_args().strip()
if not prompt:
return t("gateway.background.usage")
@@ -877,10 +861,9 @@ class GatewaySlashCommandsMixin(
return t("gateway.background.started", preview=_preview(prompt), task_id=task_id)
async def _handle_btw_command(self, event: MessageEvent) -> str:
"""Handle /btw <question> — answer a side question via a one-shot auxiliary LLM call on a
transcript snapshot; live history is never touched (alternation + prompt cache intact,
current turn keeps running). Unlike /bg, which spawns a fresh contextless session.
"""
"""Handle /btw <question> — one-shot auxiliary LLM call on a transcript snapshot; live history
is never touched (alternation + prompt cache intact, current turn keeps running). Unlike /bg,
which spawns a fresh contextless session."""
question = event.get_command_args().strip()
if not question:
return t("gateway.btw.usage")
@@ -928,10 +911,8 @@ class GatewaySlashCommandsMixin(
return t("gateway.btw.started", preview=preview)
async def _handle_memory_command(self, event: MessageEvent) -> str:
"""Handle /memory — review pending memory writes + toggle the approval gate.
Entries are small enough to review inline, so the full flow works on every platform.
"""
"""Handle /memory — review pending memory writes + toggle the approval gate. Entries are small
enough to review inline, so the full flow works on every platform."""
from hermes_cli.write_approval_commands import handle_pending_subcommand
from tools import write_approval as wa
from tools.memory_tool import load_on_disk_store
@@ -946,11 +927,9 @@ class GatewaySlashCommandsMixin(
)
async def _handle_skills_command(self, event: MessageEvent) -> str:
"""Handle /skills on the gateway — pending skill-write review only (hub stays CLI-only).
Gated by ``skills.write_approval`` but still answers when staged writes exist after the
gate is off (never stranded). ``diff`` is truncated for chat.
"""
"""Handle /skills on the gateway — pending skill-write review only (hub stays CLI-only). Gated
by ``skills.write_approval`` but still answers when staged writes exist after the gate is off
(never stranded). ``diff`` is truncated for chat."""
from hermes_cli.write_approval_commands import handle_pending_subcommand
from tools import write_approval as wa
args = event.get_command_args().strip().split()
@@ -1004,11 +983,9 @@ class GatewaySlashCommandsMixin(
return EphemeralReply(t("gateway.yolo.enabled"))
async def _handle_verbose_command(self, event: MessageEvent) -> str:
"""Handle /verbose command — cycle tool progress display mode.
Gated by ``display.tool_progress_command`` (default off). Cycles off → new → all → verbose
→ log per *current platform*, saved to ``display.platforms.<platform>.tool_progress``.
"""
"""Handle /verbose — cycle tool progress display mode (off → new → all → verbose → log) per
*current platform*, saved to ``display.platforms.<platform>.tool_progress``. Gated by
``display.tool_progress_command`` (default off)."""
from gateway.run import _load_gateway_config
config_path, platform_key = self._display_config_target(event)
try:
@@ -1129,12 +1106,9 @@ class GatewaySlashCommandsMixin(
return t("gateway.footer.saved", state=_state(new_state), example=example)
async def _handle_reload_mcp_command(self, event: MessageEvent) -> Optional[str]:
"""Handle /reload-mcp — reconnect MCP servers and rebuild the cached agent.
Reloading invalidates the provider prompt cache (tool schemas live in the system prompt),
so it routes through slash-confirm; "Always Approve" persists
``approvals.mcp_reload_confirm: false``.
"""
"""Handle /reload-mcp — reconnect MCP servers and rebuild the cached agent. Reloading
invalidates the provider prompt cache (tool schemas live in the system prompt), so it routes
through slash-confirm; "Always Approve" persists ``approvals.mcp_reload_confirm: false``."""
session_key = self._session_key_for_source(event.source)
# Read the gate fresh from disk so a prior "always" click takes effect on the next
@@ -1170,12 +1144,10 @@ class GatewaySlashCommandsMixin(
)
async def _handle_reload_skills_command(self, event: MessageEvent) -> str:
"""Handle /reload-skills — rescan skills dir, queue a note for next turn.
Skills are invoked at runtime, not baked into the system prompt, so this does NOT clear the
prompt cache. Added/removed skills go into ``_pending_skills_reload_notes[session_key]``,
prepended to the NEXT user message — nothing out-of-band, so alternation is preserved.
"""
"""Handle /reload-skills — rescan skills dir, queue a note for next turn. Skills are invoked at
runtime, not baked into the system prompt, so this does NOT clear the prompt cache. The diff
goes into ``_pending_skills_reload_notes[session_key]``, prepended to the NEXT user message —
nothing out-of-band, so alternation is preserved."""
try:
from agent.skill_commands import reload_skills
@@ -1237,10 +1209,8 @@ class GatewaySlashCommandsMixin(
return t("gateway.reload_skills.failed", error=e)
async def _handle_bundles_command(self, event: MessageEvent) -> str:
"""Handle /bundles — list installed skill bundles (mirrors the CLI handler).
Bundles are loaded by invoking their own ``/<slug>`` command, not by this one.
"""
"""Handle /bundles — list installed skill bundles (mirrors the CLI handler). Bundles are
loaded by invoking their own ``/<slug>`` command, not by this one."""
reply = _execute("bundles")
if "error" in reply.data:
logger.warning("Bundles command unavailable: %s", reply.data["error"])
@@ -1266,9 +1236,7 @@ class GatewaySlashCommandsMixin(
def _blocking_approval_or_stale(self, event: MessageEvent, stale_key: str, none_key: str):
"""``(session_key, None)`` when an agent thread is blocked on approval, else the reply to send.
A pending-approvals entry with no blocked thread is a stale prompt: drop it and say so.
"""
A pending-approvals entry with no blocked thread is a stale prompt: drop it and say so."""
from tools.approval import has_blocking_approval
session_key = self._session_key_for_source(event.source)
if has_blocking_approval(session_key):
@@ -1279,11 +1247,8 @@ class GatewaySlashCommandsMixin(
return session_key, t(none_key)
async def _handle_approve_command(self, event: MessageEvent) -> Optional[str]:
"""Handle /approve command — unblock waiting agent thread(s).
Agent threads block inside tools/approval.py; signalling the event resumes them so the
command executes inline — same flow as the CLI's synchronous approval.
"""
"""Handle /approve — unblock waiting agent thread(s). They block inside tools/approval.py;
signalling the event resumes them so the command executes inline (same flow as the CLI)."""
from tools.approval import resolve_gateway_approval
session_key, stale = self._blocking_approval_or_stale(
event, "gateway.approval_expired", "gateway.approve.no_pending"
@@ -1304,11 +1269,8 @@ class GatewaySlashCommandsMixin(
return await self._deliver_approval_confirmation(event, confirmation_text, "approve")
async def _handle_deny_command(self, event: MessageEvent) -> str:
"""Handle /deny command — reject pending dangerous command(s).
Signals blocked thread(s) with a 'deny' result so they get a definitive BLOCKED message,
as in the CLI. ``/deny`` denies the oldest; ``/deny all`` denies everything.
"""
"""Handle /deny — reject pending dangerous command(s) with a definitive BLOCKED result, as in
the CLI. ``/deny`` denies the oldest; ``/deny all`` denies everything."""
from tools.approval import resolve_gateway_approval
session_key, stale = self._blocking_approval_or_stale(
event, "gateway.deny.stale", "gateway.deny.no_pending"
@@ -1333,11 +1295,8 @@ class GatewaySlashCommandsMixin(
return await self._deliver_approval_confirmation(event, confirmation_text, "deny")
async def _handle_debug_command(self, event: MessageEvent) -> str:
"""Handle /debug — upload debug report (summary only) and return paste URLs.
Uploads ONLY the summary (system info + log tails), never full logs, to protect privacy;
use ``hermes debug share`` from the CLI for full uploads.
"""
"""Handle /debug — upload ONLY the summary (system info + log tails), never full logs, to
protect privacy; ``hermes debug share`` from the CLI does full uploads."""
from hermes_cli.debug import (
_GATEWAY_PRIVACY_NOTICE, _best_effort_sweep_expired_pastes, _capture_dump, _schedule_auto_delete,
collect_debug_report, upload_to_pastebin,
@@ -1365,11 +1324,8 @@ class GatewaySlashCommandsMixin(
return await self._run_in_executor_with_context(_collect_and_upload)
async def _handle_update_command(self, event: MessageEvent) -> str:
"""Handle /update command — update Hermes Agent to the latest version.
Spawns ``hermes update`` detached (``setsid``) so it survives the gateway restart it may
trigger; marker files let this or the next gateway process notify the user on completion.
"""
"""Handle /update — spawn ``hermes update`` detached (``setsid``) so it survives the gateway
restart it may trigger; marker files let this or the next gateway process notify the user."""
from gateway.run import _hermes_home, _resolve_hermes_bin
import json
from hermes_cli.config import is_managed, format_managed_message
+9 -20
View File
@@ -1,8 +1,5 @@
"""Autonomy-loop gateway commands: /goal, /subgoal, /heartbeat, /loop, /refine, /review.
Split out of ``gateway/slash_commands.py``; bound onto ``GatewayRunner`` through
``GatewaySlashCommandsMixin``.
"""
Bound onto ``GatewayRunner`` through ``GatewaySlashCommandsMixin``."""
from __future__ import annotations
@@ -89,8 +86,7 @@ class GatewayGoalCommandsMixin:
"""Enqueue *text* as the next turn through the adapter FIFO (the post-turn judge's path).
A kickoff keeps the triggering message id / channel prompt; a resume continuation carries
none. *route* is a pre-resolved ``(adapter, quick_key)``; otherwise resolved here.
Best-effort: enqueue failures are logged, never surfaced.
none. *route* is a pre-resolved ``(adapter, quick_key)``. Best-effort: failures only logged.
"""
try:
adapter, quick_key = route or self._adapter_and_key_for(event)
@@ -279,10 +275,8 @@ class GatewayGoalCommandsMixin:
)
def _idle_cached_agent_or_error(self, event: MessageEvent, verb: str):
"""``(session_key, cached_agent, None)`` for /refine and /review, or ``(_, _, error_text)``.
Both need a cached agent from a completed turn and refuse while a run is in flight.
"""
"""``(session_key, cached_agent, None)`` for /refine and /review, or ``(_, _, error_text)``:
both need a cached agent from a completed turn and refuse while a run is in flight."""
quick_key = self._session_key_for_source(event.source) if event.source else None
if not quick_key:
return None, None, f"{verb.capitalize()} unavailable (no session)."
@@ -294,11 +288,8 @@ class GatewayGoalCommandsMixin:
return quick_key, agent, None
async def _handle_refine_command(self, event: MessageEvent) -> str:
"""Handle /refine — run the memory/skill review fork on demand.
Runs in a daemon thread against a snapshot of the cached AIAgent's conversation; the live
session and prompt cache are untouched. Requires at least one completed turn.
"""
"""Handle /refine — run the memory/skill review fork on demand, in a daemon thread against a
snapshot of the cached AIAgent's conversation (live session and prompt cache untouched)."""
args = (event.get_command_args() or "").strip()
_quick_key, agent, error = self._idle_cached_agent_or_error(event, "refine")
if error:
@@ -321,11 +312,9 @@ class GatewayGoalCommandsMixin:
)
async def _handle_review_command(self, event: MessageEvent) -> str:
"""Handle /review — spawn an independent reviewer subagent.
The approval session-key contextvar is only bound during agent turns, so bind it explicitly
here or the completion event carries no gateway route and never re-enters this chat.
"""
"""Handle /review — spawn an independent reviewer subagent. The approval session-key
contextvar is only bound during agent turns, so bind it explicitly here or the completion
event carries no gateway route and never re-enters this chat."""
args = (event.get_command_args() or "").strip()
quick_key, agent, error = self._idle_cached_agent_or_error(event, "review")
if error:
+20 -43
View File
@@ -1,8 +1,5 @@
"""Read-only gateway introspection commands: /status, /context, /usage, /agents, /insights, /topup.
Split out of ``gateway/slash_commands.py``; bound onto ``GatewayRunner`` through
``GatewaySlashCommandsMixin``.
"""
Bound onto ``GatewayRunner`` through ``GatewaySlashCommandsMixin``."""
from __future__ import annotations
@@ -222,11 +219,9 @@ class GatewayStatusCommandsMixin:
# model/context display, but it still occupies the session slot.
agent = self._running_agents.get(session_key)
is_running = agent is not None and agent is not _AGENT_PENDING_SENTINEL
# Pending /queue follow-ups (slot + overflow).
adapter = self.adapters.get(source.platform) if source else None
queue_depth = self._queue_depth(session_key, adapter=adapter)
title, session_row, db_total_tokens, persisted_route = await self._status_session_db_facts(
session_entry.session_id
)
@@ -237,14 +232,13 @@ class GatewayStatusCommandsMixin:
status_agent, persisted_route, session_row, session_entry
)
stamp = "%Y-%m-%d %H:%M"
lines = [t("gateway.status.header"), "",
t("gateway.status.session_id", session_id=session_entry.session_id)]
if title:
lines.append(t("gateway.status.title", title=title))
lines += [
t("gateway.status.created", timestamp=session_entry.created_at.strftime('%Y-%m-%d %H:%M')),
t("gateway.status.last_activity", timestamp=session_entry.updated_at.strftime('%Y-%m-%d %H:%M')),
]
lines += [t("gateway.status.created", timestamp=session_entry.created_at.strftime(stamp)),
t("gateway.status.last_activity", timestamp=session_entry.updated_at.strftime(stamp))]
if model_name and provider_name:
lines.append(t("gateway.status.model_provider", model=model_name, provider=provider_name))
elif model_name:
@@ -256,10 +250,8 @@ class GatewayStatusCommandsMixin:
elif context_used:
lines.append(t("gateway.status.context_used", used=_fmt(context_used)))
state = t("gateway.status.state_yes") if is_running else t("gateway.status.state_no")
lines += [
t("gateway.status.tokens", tokens=_fmt(db_total_tokens)),
t("gateway.status.agent_running", state=state),
]
lines += [t("gateway.status.tokens", tokens=_fmt(db_total_tokens)),
t("gateway.status.agent_running", state=state)]
if queue_depth:
lines.append(t("gateway.status.queued", count=queue_depth))
if source.platform == Platform.MATRIX:
@@ -281,9 +273,8 @@ class GatewayStatusCommandsMixin:
async def _status_session_db_facts(self, session_id: str):
"""``(title, session_row, db_total_tokens, persisted_route)`` for /status; each fail-open.
Token totals come from the SQLite session DB rather than the in-memory SessionStore: the
agent's per-turn token deltas are persisted into sessions_db (run_agent.py), not into
SessionEntry, so session_entry.total_tokens is always 0.
Token totals come from the SQLite session DB, not SessionStore: run_agent.py persists per-turn
token deltas into sessions_db, never into SessionEntry (its total_tokens is always 0).
"""
db = self._session_db
if not db:
@@ -307,10 +298,9 @@ class GatewayStatusCommandsMixin:
async def _handle_context_command(self, event: MessageEvent) -> str:
"""Handle /context — the deep context-window view (/status has the one-line summary).
Usage gauge, auto-compression threshold and headroom, compression count and last savings,
and cumulative throughput (clearly labelled as throughput, NOT context size). Resolution
order: running agent, cached agent, SessionStore/SessionDB metadata, transcript estimate as
last resort. ``/context all`` adds per-skill/toolset listings.
Gauge, auto-compression threshold/headroom, compression count + last savings, and cumulative
throughput (labelled as throughput, NOT context size). Resolution: running agent -> cached
agent -> SessionStore/SessionDB metadata -> transcript estimate. ``all`` adds listings.
"""
source = event.source
session_entry = await self.async_session_store.get_or_create_session(source)
@@ -326,9 +316,8 @@ class GatewayStatusCommandsMixin:
# Gauge path: real current-context figure
if used > 0 and context_length > 0:
pct = _pct(used, context_length)
bar_width = 24
filled = int(round(pct / 100 * bar_width))
bar = "█" * max(0, filled) + "░" * max(0, bar_width - filled)
filled = int(round(pct / 100 * 24))
bar = "█" * max(0, filled) + "░" * max(0, 24 - filled)
lines = [
t("gateway.context.header"),
"",
@@ -368,12 +357,8 @@ class GatewayStatusCommandsMixin:
return t("gateway.context.no_data")
async def _resolve_context_figures(self, agent, ctx, session_entry, source):
"""``(used, context_length, model_name)`` for /context with cascading fallbacks.
used : compressor.last_prompt_tokens -> SessionStore.last_prompt_tokens
model : agent.model -> SessionDB row model
window: compressor.context_length -> effective gateway model route -> model metadata
"""
"""``(used, context_length, model_name)`` for /context: used = compressor -> SessionStore;
model = agent -> SessionDB row; window = compressor -> gateway model route -> model metadata."""
used = _n(ctx, "last_prompt_tokens") if ctx is not None else 0
context_length = _n(ctx, "context_length") if ctx is not None else 0
model_name = _clean_str(getattr(agent, "model", "")) if agent is not None else ""
@@ -463,11 +448,8 @@ class GatewayStatusCommandsMixin:
return "\n".join(lines)
async def _handle_topup_command(self, event: MessageEvent) -> str:
"""Handle /topup -- show the Nous balance and hand off to the portal.
Does NOT charge, confirm, or track payment — that happens in the browser; the next /topup
shows the new balance. Fetched off the event loop; fail-open.
"""
"""Handle /topup -- show the Nous balance and hand off to the portal. Does NOT charge, confirm,
or track payment (that happens in the browser; the next /topup shows the new balance)."""
from agent.account_usage import build_credits_view
view = await _quiet(lambda: asyncio.to_thread(build_credits_view, markdown=True))
if view is None or not view.logged_in:
@@ -487,10 +469,8 @@ class GatewayStatusCommandsMixin:
return "\n".join(lines)
def _context_breakdown_block(self, agent, source, expanded: bool) -> list[str]:
"""Render the /context per-category block (plain text, no grid).
Estimated (chars/4), same engine as /usage. Runs in a thread; returns [] and never raises.
"""
"""/context per-category block (plain text, chars/4 estimate, same engine as /usage).
Runs in a thread; returns [] and never raises."""
try:
from agent.context_breakdown import compute_context_details, render_context_breakdown_lines
payload = self._session_context_breakdown(agent, source)
@@ -514,10 +494,7 @@ class GatewayStatusCommandsMixin:
return compute_session_context_breakdown(agent, _quiet_sync(_history, []))
def _context_breakdown_lines(self, agent, source) -> list[str]:
"""Render the per-category context breakdown for /usage.
Estimated (chars/4). Returns [] and never raises so /usage stays robust.
"""
"""/usage per-category context breakdown (chars/4 estimate). Returns [] and never raises."""
try:
payload = self._session_context_breakdown(agent, source)
categories = payload.get("categories") or []