diff --git a/gateway/slash_commands.py b/gateway/slash_commands.py index b8e392117b..98dcc90e3f 100644 --- a/gateway/slash_commands.py +++ b/gateway/slash_commands.py @@ -1,9 +1,13 @@ """Gateway slash-command handlers for GatewayRunner. -The in-session slash commands (/model, /reset, /usage, ...) dispatched from ``_handle_message``, -lifted out of ``gateway/run.py`` into a mixin so every ``self._handle_*_command`` reference keeps -working via the MRO. run.py helpers (``_hermes_home``, ``_load_gateway_config``, ...) are imported -lazily inside handler bodies — a deferred ``from gateway.run import ...`` avoids the import cycle. +The in-session slash commands dispatched from ``_handle_message``, lifted out of ``gateway/run.py`` +into a mixin so every ``self._handle_*_command`` reference keeps working via the MRO. Cohesive +clusters live in sibling mixins this class inherits: ``slash_commands_model`` (/model, /reasoning, +/fast, ...), ``slash_commands_session`` (/new, /resume, /branch, /compress, ...), +``slash_commands_status`` (/status, /context, /usage, ...) and ``slash_commands_goals`` (/goal, +/loop, /heartbeat, ...). This module keeps the shared helpers plus the remaining one-off commands. +run.py helpers (``_hermes_home``, ``_load_gateway_config``, ...) are imported lazily inside handler +bodies — a deferred ``from gateway.run import ...`` avoids the import cycle. """ from __future__ import annotations @@ -24,25 +28,16 @@ from typing import Optional, Union from agent.i18n import t from gateway.config import HomeChannel, Platform, PlatformConfig, persist_home_channel from gateway.platforms.base import EphemeralReply, MessageEvent -from gateway.session import ( - AsyncSessionStore, -) -from hermes_cli.config import atomic_config_write, cfg_get -from utils import ( - atomic_json_write, - is_truthy_value, -) - -from gateway.slash_commands_model import ( # noqa: F401 — _model_switch_skew_guard re-exported for callers/tests +from gateway.session import AsyncSessionStore +from gateway.slash_commands_goals import GatewayGoalCommandsMixin +from gateway.slash_commands_model import ( # noqa: F401 — _model_switch_skew_guard re-exported for tests GatewayModelCommandsMixin, _model_switch_skew_guard, ) - from gateway.slash_commands_session import GatewaySessionCommandsMixin - from gateway.slash_commands_status import GatewayStatusCommandsMixin - -from gateway.slash_commands_goals import GatewayGoalCommandsMixin +from hermes_cli.config import atomic_config_write, cfg_get +from utils import atomic_json_write, is_truthy_value logger = logging.getLogger("gateway.run") @@ -190,8 +185,13 @@ def _home_thread_from_source(source) -> Optional[str]: return str(thread_id) -class GatewaySlashCommandsMixin(GatewayModelCommandsMixin, GatewaySessionCommandsMixin, GatewayStatusCommandsMixin, GatewayGoalCommandsMixin): - """In-session slash-command handlers for GatewayRunner.""" +class GatewaySlashCommandsMixin( + GatewayModelCommandsMixin, + GatewaySessionCommandsMixin, + GatewayStatusCommandsMixin, + GatewayGoalCommandsMixin, +): + """In-session slash-command handlers for GatewayRunner (plus the helpers the sibling mixins share).""" async_session_store: AsyncSessionStore @@ -320,7 +320,6 @@ class GatewaySlashCommandsMixin(GatewayModelCommandsMixin, GatewaySessionCommand adapter = self.adapters.get(platform) if getattr(self, "adapters", None) else None return getattr(adapter, "typed_command_prefix", "/") if adapter is not None else "/" - async def _handle_profile_command(self, event: MessageEvent) -> str: """Handle /profile — show the profile serving this source and its home. @@ -523,7 +522,6 @@ class GatewaySlashCommandsMixin(GatewayModelCommandsMixin, GatewaySessionCommand await asyncio.to_thread(_sub) return True - async def _handle_stop_command(self, event: MessageEvent) -> Union[str, EphemeralReply]: """Handle /stop command - interrupt a running agent. @@ -791,7 +789,6 @@ class GatewaySlashCommandsMixin(GatewayModelCommandsMixin, GatewaySessionCommand ) return self._telegramized_command_reply(event, reply.text) - async def _handle_set_home_command(self, event: MessageEvent) -> str: """Handle /sethome command -- set the current chat as the platform's home channel.""" from gateway.run import _home_target_env_var, _home_thread_env_var @@ -1183,7 +1180,6 @@ class GatewaySlashCommandsMixin(GatewayModelCommandsMixin, GatewaySessionCommand 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. @@ -1247,7 +1243,6 @@ class GatewaySlashCommandsMixin(GatewayModelCommandsMixin, GatewaySessionCommand f"~/.hermes/pending/skills/{pending_id}.json)") return out - async def _handle_approvals_command(self, event: MessageEvent) -> str: """Show or persist the profile-wide dangerous-command approval mode.""" from gateway.slash_access import policy_for_source @@ -1441,7 +1436,6 @@ class GatewaySlashCommandsMixin(GatewayModelCommandsMixin, GatewaySessionCommand example = t("gateway.footer.example_line", preview=preview) return t("gateway.footer.saved", state=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. diff --git a/gateway/slash_commands_goals.py b/gateway/slash_commands_goals.py index 75e21a638a..9e6e2fa110 100644 --- a/gateway/slash_commands_goals.py +++ b/gateway/slash_commands_goals.py @@ -8,6 +8,7 @@ import ...``) inside the bodies to avoid the import cycle. from __future__ import annotations import logging + from agent.i18n import t from gateway.platforms.base import MessageEvent, MessageType diff --git a/gateway/slash_commands_model.py b/gateway/slash_commands_model.py index 9627674ecf..3d0344b31d 100644 --- a/gateway/slash_commands_model.py +++ b/gateway/slash_commands_model.py @@ -9,10 +9,11 @@ from __future__ import annotations import logging import asyncio +from typing import Optional + from agent.i18n import t from gateway.platforms.base import MessageEvent from hermes_cli.config import atomic_config_write, clear_model_endpoint_credentials -from typing import Optional from utils import base_url_host_matches # Log-record parity with gateway/run.py and the origin module. diff --git a/gateway/slash_commands_session.py b/gateway/slash_commands_session.py index 9f6f3ca3a4..e7ed1b12ba 100644 --- a/gateway/slash_commands_session.py +++ b/gateway/slash_commands_session.py @@ -13,12 +13,13 @@ import contextlib import dataclasses import os import shlex +from typing import Optional, Union + from agent.i18n import t from agent.turn_context import extract_api_content_sidecar from gateway.config import Platform from gateway.platforms.base import EphemeralReply, MessageEvent, MessageType from gateway.session import SessionSource, build_session_key, is_shared_multi_user_session -from typing import Optional, Union # Log-record parity with gateway/run.py and the origin module. logger = logging.getLogger("gateway.run") diff --git a/gateway/slash_commands_status.py b/gateway/slash_commands_status.py index b1275dc076..33c4285367 100644 --- a/gateway/slash_commands_status.py +++ b/gateway/slash_commands_status.py @@ -13,11 +13,12 @@ import hashlib import os import re import time +from typing import Any + from agent.account_usage import fetch_account_usage, render_account_usage_lines from agent.i18n import t from gateway.config import Platform from gateway.platforms.base import MessageEvent -from typing import Any # Log-record parity with gateway/run.py and the origin module. logger = logging.getLogger("gateway.run") @@ -610,20 +611,9 @@ class GatewayStatusCommandsMixin: Estimated (chars/4), same engine as /usage. Runs in a thread; returns [] and never raises. """ try: - from agent.context_breakdown import ( - compute_context_details, - compute_session_context_breakdown, - render_context_breakdown_lines, - ) + from agent.context_breakdown import compute_context_details, render_context_breakdown_lines - history: list[dict] = [] - try: - entry = self.session_store.get_or_create_session(source) - history = self.session_store.load_transcript(entry.session_id) or [] - except Exception: - history = [] - - payload = compute_session_context_breakdown(agent, history) + payload = self._session_context_breakdown(agent, source) if not (payload.get("categories") or []): return [] @@ -638,22 +628,25 @@ class GatewayStatusCommandsMixin: except Exception: return [] + def _session_context_breakdown(self, agent, source) -> dict: + """Per-category context estimate (chars/4) for *agent* over the session transcript (sync).""" + from agent.context_breakdown import compute_session_context_breakdown + + history: list[dict] = [] + try: + entry = self.session_store.get_or_create_session(source) + history = self.session_store.load_transcript(entry.session_id) or [] + except Exception: + history = [] + return compute_session_context_breakdown(agent, 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. """ try: - from agent.context_breakdown import compute_session_context_breakdown - - history: list[dict] = [] - try: - entry = self.session_store.get_or_create_session(source) - history = self.session_store.load_transcript(entry.session_id) or [] - except Exception: - history = [] - - payload = compute_session_context_breakdown(agent, history) + payload = self._session_context_breakdown(agent, source) categories = payload.get("categories") or [] if not categories: return []