refactor(gateway): compact docstrings in slash mixins (keep every WHY)
This commit is contained in:
+45
-89
@@ -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
|
||||
|
||||
@@ -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:
|
||||
|
||||
@@ -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 []
|
||||
|
||||
Reference in New Issue
Block a user