feat: /btw now answers side questions with conversation context; /background renamed to /bg

/bg (formerly /background, which is retired) keeps the existing semantics:
spawn a fresh, independent agent session in the background.

/btw is now its own command matching the convention other harnesses use:
ask a quick side question ABOUT the current conversation without
interrupting it. A one-shot auxiliary LLM call (main model by default,
overridable via auxiliary.side_question.* in config.yaml) answers from a
read-only transcript snapshot — the live session's history, role
alternation, and prompt cache are untouched, and the current turn keeps
running.

Surfaces wired: CLI (inline mid-run dispatch), gateway (all messengers,
busy-dispatch table + idle dispatch, i18n across all 17 locales), TUI
(prompt.btw RPC + btw.complete event), Discord native slash, relay
command manifest, desktop exec routing, docs (EN + zh-Hans).
This commit is contained in:
Teknium
2026-08-29 07:14:17 -07:00
parent b6bd681e89
commit 74a95a3ddf
63 changed files with 966 additions and 169 deletions
+159
View File
@@ -0,0 +1,159 @@
"""Context-aware side questions (``/btw``).
``/btw <question>`` answers a quick question ABOUT the current conversation
without interrupting it: a one-shot auxiliary LLM call receives a read-only
transcript snapshot plus the question, and the answer is delivered alongside
the live session. The live conversation history is never touched — no
synthetic turns, no role-alternation risk, no prompt-cache invalidation.
This is deliberately different from ``/bg`` (``/background``'s successor),
which spawns a fresh, contextless agent session for independent work.
Model selection rides the standard auxiliary plumbing
(:func:`agent.auxiliary_client.call_llm` via :func:`agent.oneshot.run_oneshot`):
pass ``main_runtime`` to inherit the live session's provider/model; users can
override per-task via ``auxiliary.side_question.provider`` / ``.model`` in
config.yaml.
"""
import logging
from typing import Any, Dict, List, Optional
logger = logging.getLogger(__name__)
# Free-form auxiliary task name — resolvable via auxiliary.side_question.* in
# config.yaml, falls back main-model-first like every other aux task.
SIDE_QUESTION_TASK = "side_question"
# Per-message and total character budgets for the transcript snapshot. The
# snapshot is rendered to plain text (never replayed as raw provider messages)
# so assistant tool_calls entries can't trip provider-side validation on a
# tools-less one-shot request.
_PER_MESSAGE_CHAR_CAP = 2000
_TRANSCRIPT_CHAR_BUDGET = 24000
_INSTRUCTIONS = (
"You are the same AI assistant that is currently working inside the "
"conversation transcribed below. The user has asked a quick SIDE question "
"with /btw while the main work continues.\n"
"Rules:\n"
"- Answer ONLY the side question. Do not continue, redo, or critique the "
"main task.\n"
"- Use the transcript as your primary context; it is a snapshot and may "
"not include the very latest activity.\n"
"- If the transcript does not contain enough information to answer, say "
"so plainly instead of guessing.\n"
"- Be concise and direct."
)
def _msg_text(msg: Dict[str, Any]) -> str:
"""Best-effort plain text from a provider-format message content field."""
content = msg.get("content")
if isinstance(content, str):
return content
if isinstance(content, list):
parts = []
for block in content:
if isinstance(block, dict):
text = block.get("text")
if isinstance(text, str):
parts.append(text)
return "\n".join(parts)
return ""
def render_history_for_side_question(
history: Optional[List[Dict[str, Any]]],
char_budget: int = _TRANSCRIPT_CHAR_BUDGET,
) -> str:
"""Render a conversation snapshot as a plain-text transcript.
Keeps the most recent messages that fit ``char_budget``, newest-biased
(older context is what gets dropped). Tool calls are summarized by name;
tool results are included truncated so "what did that command output"
style questions remain answerable.
"""
lines: List[str] = []
for msg in history or []:
if not isinstance(msg, dict):
continue
role = msg.get("role")
text = _msg_text(msg).strip()
if role == "system":
continue # system prompt is not needed and can be huge
if role == "user":
if text:
lines.append(f"USER: {text[:_PER_MESSAGE_CHAR_CAP]}")
elif role == "assistant":
tool_calls = msg.get("tool_calls") or []
if tool_calls:
names = [
(tc.get("function") or {}).get("name", "?")
for tc in tool_calls
if isinstance(tc, dict)
]
lines.append(f"ASSISTANT [called tools: {', '.join(names)}]")
if text:
lines.append(f"ASSISTANT: {text[:_PER_MESSAGE_CHAR_CAP]}")
elif role == "tool":
if text:
lines.append(f"TOOL RESULT: {text[:_PER_MESSAGE_CHAR_CAP]}")
# Newest-biased fit: walk from the end until the budget is spent.
kept: List[str] = []
used = 0
for line in reversed(lines):
cost = len(line) + 1
if used + cost > char_budget and kept:
break
kept.append(line)
used += cost
kept.reverse()
if not kept:
return "(no prior conversation)"
prefix = ""
if len(kept) < len(lines):
prefix = "[...older conversation omitted...]\n"
return prefix + "\n".join(kept)
def answer_side_question(
question: str,
history: Optional[List[Dict[str, Any]]],
*,
main_runtime: Optional[Dict[str, Any]] = None,
max_tokens: int = 2048,
temperature: Optional[float] = 0.3,
timeout: float = 180.0,
) -> str:
"""Answer ``question`` against a snapshot of ``history``.
Returns the model's text answer. Raises whatever the auxiliary client
raises (RuntimeError on no provider, etc.) — callers surface the error
on their own UI.
"""
from agent.oneshot import run_oneshot
question = (question or "").strip()
if not question:
raise ValueError("answer_side_question requires a non-empty question")
transcript = render_history_for_side_question(history)
user_input = (
"Conversation transcript (snapshot):\n"
"-----\n"
f"{transcript}\n"
"-----\n\n"
f"Side question: {question}"
)
return run_oneshot(
instructions=_INSTRUCTIONS,
user_input=user_input,
task=SIDE_QUESTION_TASK,
max_tokens=max_tokens,
temperature=temperature,
timeout=timeout,
main_runtime=main_runtime,
)
@@ -49,7 +49,8 @@ const REGISTRY_CATALOG = registryCatalog(
'/agents': null,
'/steer': 'text',
'/stop': null,
'/background': 'text',
'/bg': 'text',
'/btw': 'text',
'/debug': null,
'/goal': 'mixed',
'/personality': 'options',
@@ -61,7 +62,7 @@ const REGISTRY_CATALOG = registryCatalog(
'/loop': 'mixed',
'/lcm': 'text'
},
{ '/tasks': '/agents', '/bg': '/background', '/q': '/queue', '/proactive': '/loop' }
{ '/tasks': '/agents', '/background': '/bg', '/q': '/queue', '/proactive': '/loop' }
)
describe('desktop slash command curation', () => {
@@ -93,9 +94,11 @@ describe('desktop slash command curation', () => {
it('treats registry and plugin commands as exec when the catalog says so', () => {
expect(resolveDesktopCommand('/refine')?.argumentMode).toBe('text')
expect(isDesktopSlashSuggestion('/refine')).toBe(true)
expect(isDesktopSlashSuggestion('/bg')).toBe(false)
expect(isDesktopSlashCommand('/background')).toBe(true)
expect(desktopSlashCommandArgumentMode('/background')).toBe('text')
expect(isDesktopSlashSuggestion('/background')).toBe(false)
expect(isDesktopSlashCommand('/bg')).toBe(true)
expect(desktopSlashCommandArgumentMode('/bg')).toBe('text')
expect(isDesktopSlashCommand('/btw')).toBe(true)
expect(desktopSlashCommandArgumentMode('/btw')).toBe('text')
expect(resolveDesktopCommand('/lcm')?.surface).toEqual({ kind: 'exec' })
expect(desktopSlashCommandArgumentMode('/lcm')).toBe('text')
})
@@ -230,7 +233,8 @@ describe('desktop slash command curation', () => {
it('still routes commands without dedicated RPCs through exec()', () => {
const execNames = [
'/background',
'/bg',
'/btw',
'/debug',
'/goal',
'/personality',
+21 -19
View File
@@ -6396,7 +6396,7 @@ class HermesCLI(CLIAgentSetupMixin, CLICommandsMixin, CLIBillingMixin):
except Exception:
pass
# Count live /background tasks. The dict entry is removed in the
# Count live /bg tasks. The dict entry is removed in the
# task thread's finally block, so len() reflects truly-running tasks.
# len() on a CPython dict is atomic; safe to read without a lock.
try:
@@ -11947,20 +11947,20 @@ class HermesCLI(CLIAgentSetupMixin, CLICommandsMixin, CLIBillingMixin):
def _should_handle_background_command_inline(
self, text: str, has_images: bool = False
) -> bool:
"""Return True when /background should be dispatched while the agent runs.
"""Return True when /bg or /btw should be dispatched while the agent runs.
Same queue problem /steer had. ``/background`` (``/bg``, ``/btw``)
exists to start independent work *without* waiting for the current
turn, but a slash command typed while the agent is busy goes into
``_pending_input``, and ``process_loop`` is blocked inside
``self.chat()`` for the whole run. The background task therefore only
starts once the foreground turn has finished, which is the one moment
it was not needed.
Same queue problem /steer had. ``/bg`` exists to start independent
work *without* waiting for the current turn, and ``/btw`` exists to
answer a side question about the in-flight conversation, but a slash
command typed while the agent is busy goes into ``_pending_input``,
and ``process_loop`` is blocked inside ``self.chat()`` for the whole
run. The side task would therefore only start once the foreground
turn has finished, which is the one moment it was not needed.
The command's own ``CommandDef`` already declares
Both commands' ``CommandDef`` entries already declare
``busy_policy="dispatch"``; the gateway honours that, the classic CLI
never consulted it. Dispatching inline on the UI thread starts the
background session immediately and leaves the foreground turn running
side session immediately and leaves the foreground turn running
untouched: no interrupt, no steer.
"""
if not text or has_images or not _looks_like_slash_command(text):
@@ -11971,7 +11971,7 @@ class HermesCLI(CLIAgentSetupMixin, CLICommandsMixin, CLIBillingMixin):
from hermes_cli.commands import resolve_command
base = text.split(None, 1)[0].lower().lstrip('/')
cmd = resolve_command(base)
return bool(cmd and cmd.name == "background")
return bool(cmd and cmd.name in ("bg", "btw"))
except Exception:
return False
@@ -12575,8 +12575,10 @@ class HermesCLI(CLIAgentSetupMixin, CLICommandsMixin, CLIBillingMixin):
self._handle_agents_command()
elif canonical == "journey":
self._handle_journey_command(cmd_original)
elif canonical == "background":
elif canonical == "bg":
self._handle_background_command(cmd_original)
elif canonical == "btw":
self._handle_btw_command(cmd_original)
elif canonical == "queue":
# Extract prompt after "/queue " or "/q "
parts = cmd_original.split(None, 1)
@@ -18213,12 +18215,12 @@ class HermesCLI(CLIAgentSetupMixin, CLICommandsMixin, CLIBillingMixin):
event.app.invalidate()
return
# Same treatment for /background (/bg, /btw) while the agent is
# running. Queuing it defeats the entire point of the command:
# process_loop is blocked inside self.chat(), so the background
# task would only start once the foreground turn it was meant to
# run alongside has already finished (#75221). The foreground
# turn is left alone: no interrupt, no steer.
# Same treatment for /bg and /btw while the agent is
# running. Queuing them defeats the entire point of the
# commands: process_loop is blocked inside self.chat(), so the
# side task would only start once the foreground turn it was
# meant to run alongside has already finished (#75221). The
# foreground turn is left alone: no interrupt, no steer.
if self._should_handle_background_command_inline(
text, has_images=has_images
):
+1 -1
View File
@@ -6297,7 +6297,7 @@ class BasePlatformAdapter(ABC):
return
# Other bypass commands (/approve, /deny, /status,
# /background, /restart) just need direct dispatch — they
# /bg, /restart) just need direct dispatch — they
# don't cancel the running task.
logger.debug(
"[%s] Command '/%s' bypassing active-session guard for %s",
+1 -1
View File
@@ -1774,7 +1774,7 @@ class OwnerCommandMiddleware(InboundMiddleware):
# Slash command allowlist that bot owner can execute in group without @Bot
ALLOWLIST: frozenset = frozenset({
"/new", "/reset", "/retry", "/undo", "/stop",
"/approve", "/deny", "/background", "/bg",
"/approve", "/deny", "/bg",
"/btw", "/queue", "/q",
})
+7 -2
View File
@@ -138,8 +138,13 @@ def build_relay_command_manifest() -> List[Dict[str, Any]]:
"options": [_opt("text", "The prompt to queue")],
},
{
"name": "background",
"description": "Run a prompt in the background",
"name": "bg",
"description": "Run a prompt in a separate background session",
"options": [_opt("text", "The prompt to run")],
},
{
"name": "btw",
"description": "Ask a side question about the current conversation",
"options": [_opt("text", "The question to answer")],
},
]
+6 -2
View File
@@ -17201,7 +17201,8 @@ class GatewayRunner(GatewayAuthorizationMixin, GatewayKanbanWatchersMixin, Gatew
"deny": self._handle_deny_command,
"pause": self._handle_pause_command,
"agents": self._handle_agents_command,
"background": self._handle_background_command,
"bg": self._handle_background_command,
"btw": self._handle_btw_command,
"kanban": self._handle_kanban_command,
"subgoal": self._handle_subgoal_command,
"heartbeat": self._handle_heartbeat_command,
@@ -18587,9 +18588,12 @@ class GatewayRunner(GatewayAuthorizationMixin, GatewayKanbanWatchersMixin, Gatew
if canonical == "diff":
return await self._handle_diff_command(event)
if canonical == "background":
if canonical == "bg":
return await self._handle_background_command(event)
if canonical == "btw":
return await self._handle_btw_command(event)
if canonical == "queue":
queue_payload = event.get_command_args().strip()
if not queue_payload:
+76 -1
View File
@@ -3609,7 +3609,7 @@ class GatewaySlashCommandsMixin:
return f"```diff\n{diff}{note}\n```"
async def _handle_background_command(self, event: MessageEvent) -> str:
"""Handle /background <prompt> — run a prompt in a separate background session.
"""Handle /bg <prompt> — run a prompt in a separate background session.
Spawns a new AIAgent in a background thread with its own session.
When it completes, sends the result back to the same chat without
@@ -3645,6 +3645,81 @@ class GatewaySlashCommandsMixin:
preview = prompt[:60] + ("..." if len(prompt) > 60 else "")
return t("gateway.background.started", preview=preview, task_id=task_id)
async def _handle_btw_command(self, event: MessageEvent) -> str:
"""Handle /btw <question> — answer a side question about this conversation.
Snapshots the session transcript and answers the question with a
one-shot auxiliary LLM call (main model by default) — the live
session's history is never touched, so role alternation and the
prompt cache stay intact and the current turn keeps running. The
answer is delivered to the chat when ready.
Deliberately different from /bg, which spawns a fresh contextless
agent session for independent work.
"""
question = event.get_command_args().strip()
if not question:
return t("gateway.btw.usage")
source = event.source
session_entry = await self.async_session_store.get_or_create_session(source)
history = await self.async_session_store.load_transcript(session_entry.session_id)
if not history:
return t("gateway.btw.no_history")
try:
model, runtime_kwargs = self._resolve_session_agent_runtime(
source=source,
)
except Exception:
model, runtime_kwargs = None, {}
if not runtime_kwargs.get("api_key"):
return t("gateway.btw.no_provider")
main_runtime = {
"model": model,
"provider": runtime_kwargs.get("provider"),
"base_url": runtime_kwargs.get("base_url"),
"api_key": runtime_kwargs.get("api_key"),
"api_mode": runtime_kwargs.get("api_mode"),
}
history_snapshot = list(history)
event_message_id = self._reply_anchor_for_event(event)
_thread_metadata = self._thread_metadata_for_source(source, event_message_id)
adapter = self._adapter_for_source(source)
preview = question[:60] + ("..." if len(question) > 60 else "")
async def _run_side_question() -> None:
from agent.side_question import answer_side_question
try:
answer = await asyncio.to_thread(
answer_side_question,
question,
history_snapshot,
main_runtime=main_runtime,
)
except Exception as e:
logger.warning("/btw side question failed: %s", e)
if adapter is not None:
await adapter.send(
source.chat_id,
t("gateway.btw.failed", preview=preview, error=str(e)),
metadata=_thread_metadata,
)
return
if adapter is not None:
await adapter.send(
source.chat_id,
t("gateway.btw.answer", preview=preview, answer=answer or ""),
metadata=_thread_metadata,
)
_task = asyncio.create_task(_run_side_question())
self._background_tasks.add(_task)
_task.add_done_callback(self._background_tasks.discard)
return t("gateway.btw.started", preview=preview)
def _save_gateway_config_key(self, key_path: str, value) -> bool:
"""Save a dot-separated key to config.yaml (shared by /reasoning, /fast
and their interactive pickers)."""
+96 -3
View File
@@ -2203,7 +2203,7 @@ class CLICommandsMixin:
save_config_value(f"{subsystem}.write_approval", bool(enabled))
def _handle_background_command(self, cmd: str):
"""Handle /background <prompt> — run a prompt in a separate background session.
"""Handle /bg <prompt> — run a prompt in a separate background session.
Spawns a new AIAgent in a background thread with its own session.
When it completes, prints the result to the CLI without modifying
@@ -2212,8 +2212,9 @@ class CLICommandsMixin:
from cli import AIAgent, ChatConsole, _accent_hex, _cprint, _maybe_remap_for_light_mode, _render_final_assistant_content, set_approval_callback, set_secret_capture_callback, set_sudo_password_callback
parts = cmd.strip().split(maxsplit=1)
if len(parts) < 2 or not parts[1].strip():
_cprint(" Usage: /background <prompt>")
_cprint(" Example: /background Summarize the top HN stories today")
_cprint(" Usage: /bg <prompt>")
_cprint(" Example: /bg Summarize the top HN stories today")
_cprint(" (For a side question about this conversation, use /btw <question>.)")
_cprint(" The task runs in a separate session and results display here when done.")
return
@@ -2357,6 +2358,98 @@ class CLICommandsMixin:
self._background_tasks[task_id] = thread
thread.start()
def _handle_btw_command(self, cmd: str):
"""Handle /btw <question> — answer a side question about this conversation.
Snapshots the live conversation history and asks a one-shot auxiliary
LLM call (same model as the session by default) to answer the question
against that snapshot. The live session is never touched: no history
mutation, no role-alternation risk, no prompt-cache invalidation. The
current turn keeps running; the answer prints when ready.
"""
from cli import ChatConsole, _accent_hex, _cprint
parts = cmd.strip().split(maxsplit=1)
if len(parts) < 2 or not parts[1].strip():
_cprint(" Usage: /btw <question>")
_cprint(" Example: /btw which file was that error in?")
_cprint(" Answers a quick question about this conversation without interrupting it.")
_cprint(" (For an independent background task, use /bg <prompt>.)")
return
question = parts[1].strip()
if not self._ensure_runtime_credentials():
_cprint(" (>_<) Cannot answer side question: no valid credentials.")
return
# Snapshot NOW, on the UI thread — the foreground turn keeps appending
# to conversation_history while the worker runs.
history_snapshot = list(self.conversation_history or [])
turn_route = self._resolve_turn_agent_config(question)
main_runtime = {
"model": turn_route["model"],
"provider": turn_route["runtime"].get("provider"),
"base_url": turn_route["runtime"].get("base_url"),
"api_key": turn_route["runtime"].get("api_key"),
"api_mode": turn_route["runtime"].get("api_mode"),
}
preview = question[:60] + ("..." if len(question) > 60 else "")
_cprint(f" 💬 Side question: \"{preview}\"")
_cprint(" Answering from a snapshot of this conversation — the current work continues.\n")
def run_side_question():
try:
from agent.side_question import answer_side_question
answer = answer_side_question(
question,
history_snapshot,
main_runtime=main_runtime,
)
if self._app:
self._app.invalidate()
time.sleep(0.05)
print()
ChatConsole().print(f"[{_accent_hex()}]{'─' * 40}[/]")
_cprint(f" 💬 /btw: \"{preview}\"")
ChatConsole().print(f"[{_accent_hex()}]{'─' * 40}[/]")
if answer:
from cli import _maybe_remap_for_light_mode, _render_final_assistant_content
try:
from hermes_cli.skin_engine import get_active_skin
_skin = get_active_skin()
label = _skin.get_branding("response_label", "⚕ Hermes")
_resp_color = _maybe_remap_for_light_mode(_skin.get_color("response_border", "#CD7F32"))
_resp_text = _maybe_remap_for_light_mode(_skin.get_color("banner_text", "#FFF8DC"))
except Exception:
label = "⚕ Hermes"
_resp_color = "#CD7F32"
_resp_text = "#FFF8DC"
ChatConsole().print(Panel(
_render_final_assistant_content(answer, mode=self.final_response_markdown),
title=f"[{_resp_color} bold]{label} (btw)[/]",
title_align="left",
border_style=_resp_color,
style=_resp_text,
box=rich_box.HORIZONTALS,
padding=(1, 4),
width=self._scrollback_box_width(),
))
else:
_cprint(" (No answer generated)")
except Exception as e:
if self._app:
self._app.invalidate()
time.sleep(0.05)
print()
_cprint(f" ❌ /btw failed: {e}")
finally:
if self._app:
self._invalidate(min_interval=0)
threading.Thread(target=run_side_question, daemon=True, name="btw-side-question").start()
def _handle_bundles_command(self, cmd: str) -> None:
"""In-session ``/bundles`` — show installed skill bundles.
+13 -8
View File
@@ -197,8 +197,10 @@ COMMAND_REGISTRY: list[CommandDef] = [
CommandDef("deny", "Deny a pending dangerous command (optionally with a reason)", "Session",
gateway_only=True, args_hint="[all] [reason]", busy_policy="dispatch",
desktop="messaging"),
CommandDef("background", "Run a prompt in the background", "Session",
aliases=("bg", "btw"), args_hint="<prompt>", busy_policy="dispatch"),
CommandDef("bg", "Run a prompt in a separate background session", "Session",
args_hint="<prompt>", busy_policy="dispatch"),
CommandDef("btw", "Ask a side question about the current conversation without interrupting it", "Session",
args_hint="<question>", busy_policy="dispatch"),
CommandDef("agents", "Show active agents and running tasks", "Session",
aliases=("tasks",), busy_policy="dispatch"),
CommandDef("journey", "Open the learning journey timeline",
@@ -507,7 +509,7 @@ HELP_SESSION_SUBGROUPS: dict[str, tuple[str, ...]] = {
"compress", "compact", "context", "ctx", "status",
),
"Background & Automation": (
"background", "bg", "btw", "agents", "tasks", "queue", "q", "steer",
"bg", "btw", "agents", "tasks", "queue", "q", "steer",
"goal", "subgoal", "heartbeat", "hb", "refine", "loop", "proactive",
"moa", "journey", "learning", "memory-graph",
),
@@ -719,7 +721,7 @@ def telegram_bot_commands() -> list[tuple[str, str]]:
underscores. Aliases are skipped -- Telegram shows one menu entry per
canonical command.
Built-in commands that require arguments (e.g. /queue, /steer, /background)
Built-in commands that require arguments (e.g. /queue, /steer, /bg)
are **included** because their handlers return usage text when selected
without a payload, making them discoverable via autocomplete.
@@ -775,7 +777,8 @@ _TELEGRAM_MENU_PRIORITY = (
"deny",
"queue",
"steer",
"background",
"bg",
"btw",
# Lower-priority but still useful operational built-ins.
"reasoning",
"usage",
@@ -1365,7 +1368,9 @@ _SLACK_RESERVED_COMMANDS = frozenset({
# would otherwise get, and the Telegram-parity test fails when a canonical
# gets clamped ("reset" was unpinned for exactly that — /new keeps its
# native slot, the alias spelling stays reachable via /hermes reset).
_SLACK_PRIORITY_ALIASES = ("btw", "bg")
# (Currently empty: /bg and /btw were promoted from aliases of /background
# to canonical commands, so they win first-pass slots on their own.)
_SLACK_PRIORITY_ALIASES: tuple[str, ...] = ()
# Canonical commands intentionally NOT given a native Slack slash slot. Slack
# caps apps at 50 slash commands and the registry is at that ceiling; rather
@@ -1435,7 +1440,7 @@ def slack_native_slashes() -> list[tuple[str, str, str]]:
first-class slash and not a ``/hermes <verb>`` subcommand.
Both canonical names and aliases are included so users can type any
documented form (e.g. ``/background``, ``/bg``, and ``/btw`` all work).
documented form; aliases are surfaced alongside canonical names.
Plugin-registered slash commands are included too.
Commands whose sanitized name collides with a Slack built-in
@@ -1538,7 +1543,7 @@ def slack_subcommand_map() -> dict[str, str]:
"""Return subcommand -> /command mapping for Slack /hermes handler.
Maps both canonical names and aliases so /hermes bg do stuff works
the same as /hermes background do stuff.
the same as /hermes bg do stuff.
Plugin-registered slash commands are included so ``/hermes <plugin-cmd>``
routes through the plugin handler.
+1 -1
View File
@@ -10,7 +10,7 @@ import random
TIPS = [
# --- Slash Commands ---
"/background <prompt> (alias /bg or /btw) runs a task in a separate session while your current one stays free.",
"/bg <prompt> runs a task in a separate session while your current one stays free; /btw <question> answers a side question about this conversation without interrupting it.",
"/branch forks the current session so you can explore a different direction without losing progress.",
"/compress manually compresses conversation context when things get long.",
"/rollback lists filesystem checkpoints — restore files the agent modified to any prior state.",
+9 -1
View File
@@ -68,9 +68,17 @@ gateway:
always_plural: "✅ Opdragte goedgekeur (patroon permanent goedgekeur) ({count} opdragte). Die agent gaan voort..."
background:
usage: "Gebruik: /background <prompt>\nVoorbeeld: /background Som vandag se top HN-stories op\n\nVoer die prompt in 'n aparte sessie uit. Jy kan aanhou gesels — die resultaat verskyn hier wanneer dit klaar is."
usage: "Gebruik: /bg <prompt>\nVoorbeeld: /bg Som vandag se top HN-stories op\n\nVoer die prompt in 'n aparte sessie uit. Jy kan aanhou gesels — die resultaat verskyn hier wanneer dit klaar is."
started: "🔄 Agtergrondtaak begin: \"{preview}\"\nTaak-ID: {task_id}\nJy kan aanhou gesels — resultate verskyn hier wanneer dit klaar is."
btw:
usage: "Gebruik: /btw <vraag>\nVoorbeeld: /btw in watter lêer was daardie fout?\n\nBeantwoord 'n vinnige newevraag oor hierdie gesprek sonder om dit te onderbreek. Vir 'n onafhanklike agtergrondtaak, gebruik /bg <prompt>."
no_history: "Nog geen gesprek nie — stuur jou vraag eerder as 'n gewone boodskap."
no_provider: "❌ Kan nie newevraag beantwoord nie: geen verskaffer-geloofsbriewe opgestel nie."
started: "💬 Newevraag: \"{preview}\"\nBeantwoord vanaf 'n momentopname van hierdie gesprek — die huidige werk gaan voort."
answer: "💬 /btw: \"{preview}\"\n\n{answer}"
failed: "❌ /btw het misluk: \"{preview}\"\n{error}"
branch:
db_unavailable: "Sessie-databasis is nie beskikbaar nie."
no_conversation: "Geen gesprek om te vertak nie — stuur eers 'n boodskap."
+9 -1
View File
@@ -91,9 +91,17 @@ gateway:
always_plural: "✅ تمت الموافقة على الأوامر (النمط مُوافق عليه دائمًا) ({count} أوامر). الوكيل يستأنف..."
background:
usage: "الاستخدام: /background <prompt>\nمثال: /background لخّص أهم قصص HN اليوم\n\nيشغّل الموجِّه في جلسة منفصلة. يمكنك متابعة المحادثة — ستظهر النتيجة هنا عند الانتهاء."
usage: "الاستخدام: /bg <prompt>\nمثال: /bg لخّص أهم قصص HN اليوم\n\nيشغّل الموجِّه في جلسة منفصلة. يمكنك متابعة المحادثة — ستظهر النتيجة هنا عند الانتهاء."
started: "🔄 بدأت مهمة خلفية: \"{preview}\"\nمعرّف المهمة: {task_id}\nيمكنك متابعة المحادثة — ستظهر النتائج عند الانتهاء."
btw:
usage: "الاستخدام: /btw <سؤال>\nمثال: /btw في أي ملف كان ذلك الخطأ؟\n\nيجيب عن سؤال جانبي سريع حول هذه المحادثة دون مقاطعتها. لمهمة خلفية مستقلة استخدم /bg <prompt>."
no_history: "لا توجد محادثة بعد — أرسل سؤالك كرسالة عادية بدلاً من ذلك."
no_provider: "❌ تعذّرت الإجابة عن السؤال الجانبي: لم يتم إعداد بيانات اعتماد مزوّد."
started: "💬 سؤال جانبي: \"{preview}\"\nتتم الإجابة من لقطة لهذه المحادثة — يستمر العمل الحالي."
answer: "💬 /btw: \"{preview}\"\n\n{answer}"
failed: "❌ فشل /btw: \"{preview}\"\n{error}"
branch:
db_unavailable: "قاعدة بيانات الجلسات غير متاحة."
no_conversation: "لا توجد محادثة للتفريع — أرسل رسالة أولًا."
+9 -1
View File
@@ -68,9 +68,17 @@ gateway:
always_plural: "✅ Befehle genehmigt (Muster dauerhaft genehmigt) ({count} Befehle). Der Agent wird fortgesetzt..."
background:
usage: "Verwendung: /background <prompt>\nBeispiel: /background Fasse die Top-HN-Storys von heute zusammen\n\nFührt den Prompt in einer separaten Sitzung aus. Sie können weiter chatten — das Ergebnis erscheint hier, wenn es fertig ist."
usage: "Verwendung: /bg <prompt>\nBeispiel: /bg Fasse die Top-HN-Storys von heute zusammen\n\nFührt den Prompt in einer separaten Sitzung aus. Sie können weiter chatten — das Ergebnis erscheint hier, wenn es fertig ist."
started: "🔄 Hintergrund-Aufgabe gestartet: \"{preview}\"\nAufgaben-ID: {task_id}\nSie können weiter chatten — die Ergebnisse erscheinen hier, wenn sie fertig sind."
btw:
usage: "Verwendung: /btw <Frage>\nBeispiel: /btw in welcher Datei war dieser Fehler?\n\nBeantwortet eine kurze Nebenfrage zu dieser Konversation, ohne sie zu unterbrechen. Für eine unabhängige Hintergrundaufgabe /bg <prompt> verwenden."
no_history: "Noch keine Konversation — senden Sie Ihre Frage stattdessen als normale Nachricht."
no_provider: "❌ Nebenfrage kann nicht beantwortet werden: keine Anbieter-Zugangsdaten konfiguriert."
started: "💬 Nebenfrage: \"{preview}\"\nAntwort aus einem Schnappschuss dieser Konversation — die aktuelle Arbeit läuft weiter."
answer: "💬 /btw: \"{preview}\"\n\n{answer}"
failed: "❌ /btw fehlgeschlagen: \"{preview}\"\n{error}"
branch:
db_unavailable: "Sitzungsdatenbank nicht verfügbar."
no_conversation: "Keine Konversation zum Verzweigen — senden Sie zuerst eine Nachricht."
+9 -1
View File
@@ -83,9 +83,17 @@ gateway:
always_plural: "✅ Commands approved (pattern approved permanently) ({count} commands). The agent is resuming..."
background:
usage: "Usage: /background <prompt>\nExample: /background Summarize the top HN stories today\n\nRuns the prompt in a separate session. You can keep chatting — the result will appear here when done."
usage: "Usage: /bg <prompt>\nExample: /bg Summarize the top HN stories today\n\nRuns the prompt in a separate session. You can keep chatting — the result will appear here when done."
started: "🔄 Background task started: \"{preview}\"\nTask ID: {task_id}\nYou can keep chatting — results will appear when done."
btw:
usage: "Usage: /btw <question>\nExample: /btw which file was that error in?\n\nAnswers a quick side question about this conversation without interrupting it. For an independent background task, use /bg <prompt>."
no_history: "No conversation yet — send your question as a normal message instead."
no_provider: "❌ Cannot answer side question: no provider credentials configured."
started: "💬 Side question: \"{preview}\"\nAnswering from a snapshot of this conversation — the current work continues."
answer: "💬 /btw: \"{preview}\"\n\n{answer}"
failed: "❌ /btw failed: \"{preview}\"\n{error}"
branch:
db_unavailable: "Session database not available."
no_conversation: "No conversation to branch — send a message first."
+9 -1
View File
@@ -68,9 +68,17 @@ gateway:
always_plural: "✅ Comandos aprobados (patrón aprobado permanentemente) ({count} comandos). El agente se está reanudando..."
background:
usage: "Uso: /background <prompt>\nEjemplo: /background Resume las principales historias de HN de hoy\n\nEjecuta el prompt en una sesión separada. Puedes seguir chateando — el resultado aparecerá aquí cuando termine."
usage: "Uso: /bg <prompt>\nEjemplo: /bg Resume las principales historias de HN de hoy\n\nEjecuta el prompt en una sesión separada. Puedes seguir chateando — el resultado aparecerá aquí cuando termine."
started: "🔄 Tarea en segundo plano iniciada: \"{preview}\"\nID de tarea: {task_id}\nPuedes seguir chateando — los resultados aparecerán aquí cuando terminen."
btw:
usage: "Uso: /btw <pregunta>\nEjemplo: /btw ¿en qué archivo estaba ese error?\n\nResponde una pregunta rápida sobre esta conversación sin interrumpirla. Para una tarea independiente en segundo plano, usa /bg <prompt>."
no_history: "Aún no hay conversación — envía tu pregunta como un mensaje normal."
no_provider: "❌ No se puede responder la pregunta: no hay credenciales de proveedor configuradas."
started: "💬 Pregunta aparte: \"{preview}\"\nRespondiendo desde una instantánea de esta conversación — el trabajo actual continúa."
answer: "💬 /btw: \"{preview}\"\n\n{answer}"
failed: "❌ /btw falló: \"{preview}\"\n{error}"
branch:
db_unavailable: "Base de datos de sesiones no disponible."
no_conversation: "No hay conversación para ramificar — envía un mensaje primero."
+9 -1
View File
@@ -68,9 +68,17 @@ gateway:
always_plural: "✅ Commandes approuvées (modèle approuvé de manière permanente) ({count} commandes). L'agent reprend..."
background:
usage: "Usage : /background <prompt>\nExemple : /background Résume les meilleures histoires HN d'aujourd'hui\n\nExécute le prompt dans une session séparée. Vous pouvez continuer à discuter — le résultat apparaîtra ici une fois terminé."
usage: "Usage : /bg <prompt>\nExemple : /bg Résume les meilleures histoires HN d'aujourd'hui\n\nExécute le prompt dans une session séparée. Vous pouvez continuer à discuter — le résultat apparaîtra ici une fois terminé."
started: "🔄 Tâche d'arrière-plan démarrée : « {preview} »\nID de tâche : {task_id}\nVous pouvez continuer à discuter — les résultats apparaîtront ici une fois terminés."
btw:
usage: "Usage : /btw <question>\nExemple : /btw dans quel fichier était cette erreur ?\n\nRépond à une question rapide sur cette conversation sans l'interrompre. Pour une tâche d'arrière-plan indépendante, utilisez /bg <prompt>."
no_history: "Pas encore de conversation — envoyez plutôt votre question comme un message normal."
no_provider: "❌ Impossible de répondre : aucun identifiant de fournisseur configuré."
started: "💬 Question annexe : « {preview} »\nRéponse à partir d'un instantané de cette conversation — le travail en cours continue."
answer: "💬 /btw : « {preview} »\n\n{answer}"
failed: "❌ /btw a échoué : « {preview} »\n{error}"
branch:
db_unavailable: "Base de données des sessions indisponible."
no_conversation: "Aucune conversation à brancher — envoyez d'abord un message."
+9 -1
View File
@@ -72,9 +72,17 @@ gateway:
always_plural: "✅ Orduithe ceadaithe (patrún ceadaithe go buan) ({count} ordú). Tá an gníomhaire ag atosú..."
background:
usage: "Úsáid: /background <leid>\nSampla: /background Déan achoimre ar phríomhscéalta HN inniu\n\nRitheann an leid i seisiún ar leith. Is féidir leat leanúint leis an gcomhrá — taispeánfar an toradh anseo nuair a bheidh sé críochnaithe."
usage: "Úsáid: /bg <leid>\nSampla: /bg Déan achoimre ar phríomhscéalta HN inniu\n\nRitheann an leid i seisiún ar leith. Is féidir leat leanúint leis an gcomhrá — taispeánfar an toradh anseo nuair a bheidh sé críochnaithe."
started: "🔄 Tasc cúlra tosaithe: \"{preview}\"\nAitheantas an tasc: {task_id}\nIs féidir leat leanúint leis an gcomhrá — taispeánfar na torthaí nuair a bheidh sé críochnaithe."
btw:
usage: "Úsáid: /btw <ceist>\nSampla: /btw cén comhad ina raibh an earráid sin?\n\nFreagraíonn ceist thaobhach ghasta faoin gcomhrá seo gan cur isteach air. Le haghaidh tasc cúlra neamhspleách, úsáid /bg <leid>."
no_history: "Níl aon chomhrá ann fós — seol do cheist mar ghnáth-theachtaireacht ina ionad."
no_provider: "❌ Ní féidir an cheist a fhreagairt: níl dintiúir sholáthraí cumraithe."
started: "💬 Ceist thaobhach: \"{preview}\"\nÁ freagairt ó léargas ar an gcomhrá seo — leanann an obair reatha ar aghaidh."
answer: "💬 /btw: \"{preview}\"\n\n{answer}"
failed: "❌ Theip ar /btw: \"{preview}\"\n{error}"
branch:
db_unavailable: "Níl bunachar sonraí na seisiún ar fáil."
no_conversation: "Níl aon chomhrá le brainseáil — seol teachtaireacht ar dtús."
+9 -1
View File
@@ -68,9 +68,17 @@ gateway:
always_plural: "✅ Parancsok jóváhagyva (minta véglegesen jóváhagyva) ({count} parancs). Az ügynök folytatja..."
background:
usage: "Használat: /background <prompt>\nPélda: /background Foglald össze a mai legjobb HN sztorikat\n\nKülön munkamenetben futtatja a promptot. Folytathatod a beszélgetést — az eredmény itt jelenik meg, amint elkészül."
usage: "Használat: /bg <prompt>\nPélda: /bg Foglald össze a mai legjobb HN sztorikat\n\nKülön munkamenetben futtatja a promptot. Folytathatod a beszélgetést — az eredmény itt jelenik meg, amint elkészül."
started: "🔄 Háttérfeladat elindítva: \"{preview}\"\nFeladatazonosító: {task_id}\nFolytathatod a beszélgetést — az eredmények itt jelennek meg, amint elkészülnek."
btw:
usage: "Használat: /btw <kérdés>\nPélda: /btw melyik fájlban volt az a hiba?\n\nGyors mellékkérdésre válaszol erről a beszélgetésről anélkül, hogy megszakítaná. Független háttérfeladathoz használd a /bg <prompt> parancsot."
no_history: "Még nincs beszélgetés — küldd el a kérdésed normál üzenetként."
no_provider: "❌ A mellékkérdés nem válaszolható meg: nincs szolgáltatói hitelesítő adat beállítva."
started: "💬 Mellékkérdés: \"{preview}\"\nA beszélgetés pillanatképéből válaszolunk — a jelenlegi munka folytatódik."
answer: "💬 /btw: \"{preview}\"\n\n{answer}"
failed: "❌ A /btw sikertelen: \"{preview}\"\n{error}"
branch:
db_unavailable: "A munkamenet-adatbázis nem érhető el."
no_conversation: "Nincs elágaztatható beszélgetés — küldj előbb egy üzenetet."
+9 -1
View File
@@ -68,9 +68,17 @@ gateway:
always_plural: "✅ Comandi approvati (modello approvato in modo permanente) ({count} comandi). L'agente sta riprendendo..."
background:
usage: "Uso: /background <prompt>\nEsempio: /background Riassumi le principali notizie di HN di oggi\n\nEsegue il prompt in una sessione separata. Puoi continuare a chattare — il risultato apparirà qui al termine."
usage: "Uso: /bg <prompt>\nEsempio: /bg Riassumi le principali notizie di HN di oggi\n\nEsegue il prompt in una sessione separata. Puoi continuare a chattare — il risultato apparirà qui al termine."
started: "🔄 Attività in background avviata: \"{preview}\"\nID attività: {task_id}\nPuoi continuare a chattare — i risultati appariranno al termine."
btw:
usage: "Uso: /btw <domanda>\nEsempio: /btw in quale file era quell'errore?\n\nRisponde a una rapida domanda su questa conversazione senza interromperla. Per un'attività in background indipendente usa /bg <prompt>."
no_history: "Nessuna conversazione ancora — invia la domanda come messaggio normale."
no_provider: "❌ Impossibile rispondere: nessuna credenziale del provider configurata."
started: "💬 Domanda a margine: \"{preview}\"\nRispondo da uno snapshot di questa conversazione — il lavoro attuale continua."
answer: "💬 /btw: \"{preview}\"\n\n{answer}"
failed: "❌ /btw non riuscito: \"{preview}\"\n{error}"
branch:
db_unavailable: "Database delle sessioni non disponibile."
no_conversation: "Nessuna conversazione da diramare — invia prima un messaggio."
+9 -1
View File
@@ -68,9 +68,17 @@ gateway:
always_plural: "✅ コマンドを承認しました (パターンを永続的に許可) ({count} 件)。エージェントを再開しています..."
background:
usage: "使い方: /background <プロンプト>\n例: /background 今日の HN トップ記事を要約して\n\nプロンプトを別のセッションで実行します。チャットを続けられます — 完了したらここに結果が表示されます。"
usage: "使い方: /bg <プロンプト>\n例: /bg 今日の HN トップ記事を要約して\n\nプロンプトを別のセッションで実行します。チャットを続けられます — 完了したらここに結果が表示されます。"
started: "🔄 バックグラウンドタスクを開始しました: 「{preview}」\nタスク ID: {task_id}\nチャットを続けられます — 完了したらここに結果が表示されます。"
btw:
usage: "使い方: /btw <質問>\n例: /btw あのエラーはどのファイルだった?\n\nこの会話について、作業を中断せずにちょっとした質問に答えます。独立したバックグラウンドタスクには /bg <プロンプト> を使ってください。"
no_history: "まだ会話がありません — 質問は通常のメッセージとして送ってください。"
no_provider: "❌ 質問に回答できません: プロバイダーの認証情報が設定されていません。"
started: "💬 サイド質問: 「{preview}」\nこの会話のスナップショットから回答します — 現在の作業は継続します。"
answer: "💬 /btw: 「{preview}」\n\n{answer}"
failed: "❌ /btw が失敗しました: 「{preview}」\n{error}"
branch:
db_unavailable: "セッションデータベースは利用できません。"
no_conversation: "分岐する会話がありません — まずメッセージを送信してください。"
+9 -1
View File
@@ -68,9 +68,17 @@ gateway:
always_plural: "✅ 명령이 승인되었습니다 (패턴 영구 승인됨) ({count}개). 에이전트가 재개됩니다..."
background:
usage: "사용법: /background <prompt>\n예시: /background 오늘 HN 인기 글을 요약해줘\n\n프롬프트를 별도 세션에서 실행합니다. 계속 대화할 수 있으며, 완료되면 결과가 여기에 표시됩니다."
usage: "사용법: /bg <prompt>\n예시: /bg 오늘 HN 인기 글을 요약해줘\n\n프롬프트를 별도 세션에서 실행합니다. 계속 대화할 수 있으며, 완료되면 결과가 여기에 표시됩니다."
started: "🔄 백그라운드 작업이 시작되었습니다: \"{preview}\"\n작업 ID: {task_id}\n계속 대화하실 수 있습니다 — 완료되면 결과가 여기에 표시됩니다."
btw:
usage: "사용법: /btw <질문>\n예시: /btw 그 오류가 어느 파일에 있었지?\n\n현재 작업을 중단하지 않고 이 대화에 대한 간단한 질문에 답합니다. 독립적인 백그라운드 작업은 /bg <prompt>를 사용하세요."
no_history: "아직 대화가 없습니다 — 질문을 일반 메시지로 보내주세요."
no_provider: "❌ 질문에 답할 수 없습니다: 공급자 자격 증명이 설정되지 않았습니다."
started: "💬 사이드 질문: \"{preview}\"\n이 대화의 스냅샷을 바탕으로 답변합니다 — 현재 작업은 계속됩니다."
answer: "💬 /btw: \"{preview}\"\n\n{answer}"
failed: "❌ /btw 실패: \"{preview}\"\n{error}"
branch:
db_unavailable: "세션 데이터베이스를 사용할 수 없습니다."
no_conversation: "분기할 대화가 없습니다 — 먼저 메시지를 보내주세요."
+9 -1
View File
@@ -68,9 +68,17 @@ gateway:
always_plural: "✅ Comandos aprovados (padrão aprovado permanentemente) ({count} comandos). O agente está a retomar..."
background:
usage: "Uso: /background <prompt>\nExemplo: /background Resume as principais histórias do HN de hoje\n\nExecuta o prompt numa sessão separada. Podes continuar a conversar — o resultado aparecerá aqui quando estiver concluído."
usage: "Uso: /bg <prompt>\nExemplo: /bg Resume as principais histórias do HN de hoje\n\nExecuta o prompt numa sessão separada. Podes continuar a conversar — o resultado aparecerá aqui quando estiver concluído."
started: "🔄 Tarefa em segundo plano iniciada: \"{preview}\"\nID da tarefa: {task_id}\nPodes continuar a conversar — os resultados aparecerão aqui quando estiverem prontos."
btw:
usage: "Uso: /btw <pergunta>\nExemplo: /btw em que ficheiro estava aquele erro?\n\nResponde a uma pergunta rápida sobre esta conversa sem a interromper. Para uma tarefa independente em segundo plano, usa /bg <prompt>."
no_history: "Ainda não há conversa — envia a tua pergunta como mensagem normal."
no_provider: "❌ Não é possível responder: nenhuma credencial de fornecedor configurada."
started: "💬 Pergunta à parte: \"{preview}\"\nA responder a partir de um instantâneo desta conversa — o trabalho atual continua."
answer: "💬 /btw: \"{preview}\"\n\n{answer}"
failed: "❌ /btw falhou: \"{preview}\"\n{error}"
branch:
db_unavailable: "Base de dados de sessões indisponível."
no_conversation: "Não há conversa para ramificar — envia uma mensagem primeiro."
+9 -1
View File
@@ -68,9 +68,17 @@ gateway:
always_plural: "✅ Команды одобрены (шаблон одобрен навсегда) ({count} команд). Агент возобновляет работу..."
background:
usage: "Использование: /background <запрос>\nПример: /background Сделай сводку лучших историй с HN сегодня\n\nЗапускает запрос в отдельном сеансе. Можно продолжить общение — результат появится здесь по завершении."
usage: "Использование: /bg <запрос>\nПример: /bg Сделай сводку лучших историй с HN сегодня\n\nЗапускает запрос в отдельном сеансе. Можно продолжить общение — результат появится здесь по завершении."
started: "🔄 Фоновая задача запущена: «{preview}»\nID задачи: {task_id}\nМожно продолжить общение — результаты появятся здесь по завершении."
btw:
usage: "Использование: /btw <вопрос>\nПример: /btw в каком файле была та ошибка?\n\nОтвечает на быстрый попутный вопрос об этом разговоре, не прерывая его. Для независимой фоновой задачи используйте /bg <запрос>."
no_history: "Разговора ещё нет — отправьте вопрос обычным сообщением."
no_provider: "❌ Не удалось ответить: учётные данные провайдера не настроены."
started: "💬 Попутный вопрос: «{preview}»\nОтвечаю по снимку этого разговора — текущая работа продолжается."
answer: "💬 /btw: «{preview}»\n\n{answer}"
failed: "❌ /btw не удался: «{preview}»\n{error}"
branch:
db_unavailable: "База данных сеансов недоступна."
no_conversation: "Нет беседы для ответвления — сначала отправьте сообщение."
+9 -1
View File
@@ -68,9 +68,17 @@ gateway:
always_plural: "✅ Komutlar onaylandı (desen kalıcı olarak onaylandı) ({count} komut). Ajan devam ediyor..."
background:
usage: "Kullanım: /background <prompt>\nÖrnek: /background Bugünün öne çıkan HN haberlerini özetle\n\nİstemi ayrı bir oturumda çalıştırır. Sohbete devam edebilirsin — sonuç tamamlandığında burada görünecek."
usage: "Kullanım: /bg <prompt>\nÖrnek: /bg Bugünün öne çıkan HN haberlerini özetle\n\nİstemi ayrı bir oturumda çalıştırır. Sohbete devam edebilirsin — sonuç tamamlandığında burada görünecek."
started: "🔄 Arka plan görevi başlatıldı: \"{preview}\"\nGörev kimliği: {task_id}\nSohbete devam edebilirsin — sonuçlar tamamlandığında burada görünecek."
btw:
usage: "Kullanım: /btw <soru>\nÖrnek: /btw o hata hangi dosyadaydı?\n\nBu konuşma hakkında hızlı bir yan soruyu, konuşmayı kesmeden yanıtlar. Bağımsız bir arka plan görevi için /bg <prompt> kullan."
no_history: "Henüz konuşma yok — sorunu normal bir mesaj olarak gönder."
no_provider: "❌ Yan soru yanıtlanamıyor: sağlayıcı kimlik bilgileri yapılandırılmamış."
started: "💬 Yan soru: \"{preview}\"\nBu konuşmanın anlık görüntüsünden yanıtlanıyor — mevcut çalışma devam ediyor."
answer: "💬 /btw: \"{preview}\"\n\n{answer}"
failed: "❌ /btw başarısız oldu: \"{preview}\"\n{error}"
branch:
db_unavailable: "Oturum veritabanı kullanılamıyor."
no_conversation: "Dallandırılacak konuşma yok — önce bir mesaj gönderin."
+9 -1
View File
@@ -68,9 +68,17 @@ gateway:
always_plural: "✅ Команди схвалено (шаблон схвалено назавжди) ({count} команд). Агент відновлює роботу…"
background:
usage: "Використання: /background <запит>\nПриклад: /background Підсумуй найкращі історії з HN сьогодні\n\nЗапускає запит в окремому сеансі. Можна продовжити спілкування — результат з'явиться тут після завершення."
usage: "Використання: /bg <запит>\nПриклад: /bg Підсумуй найкращі історії з HN сьогодні\n\nЗапускає запит в окремому сеансі. Можна продовжити спілкування — результат з'явиться тут після завершення."
started: "🔄 Фонове завдання запущено: «{preview}»\nID завдання: {task_id}\nМожна продовжити спілкування — результати з'являться тут після завершення."
btw:
usage: "Використання: /btw <питання>\nПриклад: /btw у якому файлі була та помилка?\n\nВідповідає на швидке побіжне питання про цю розмову, не перериваючи її. Для незалежного фонового завдання використовуйте /bg <запит>."
no_history: "Розмови ще немає — надішліть питання звичайним повідомленням."
no_provider: "❌ Не вдалося відповісти: облікові дані провайдера не налаштовано."
started: "💬 Побіжне питання: «{preview}»\nВідповідаю за знімком цієї розмови — поточна робота триває."
answer: "💬 /btw: «{preview}»\n\n{answer}"
failed: "❌ /btw не вдалося: «{preview}»\n{error}"
branch:
db_unavailable: "База даних сеансів недоступна."
no_conversation: "Немає розмови для розгалуження — спочатку надішліть повідомлення."
+9 -1
View File
@@ -68,9 +68,17 @@ gateway:
always_plural: "✅ 指令已批准(永久允許該模式)({count} 條指令)。代理正在恢復…"
background:
usage: "用法:/background <提示>\n範例:/background 摘要今天 HN 上的熱門故事\n\n在獨立工作階段中執行該提示。你可以繼續聊天 — 完成後結果將顯示於此。"
usage: "用法:/bg <提示>\n範例:/bg 摘要今天 HN 上的熱門故事\n\n在獨立工作階段中執行該提示。你可以繼續聊天 — 完成後結果將顯示於此。"
started: "🔄 背景任務已啟動:「{preview}」\n任務 ID:{task_id}\n你可以繼續聊天 — 完成後結果將顯示於此。"
btw:
usage: "用法:/btw <問題>\n範例:/btw 那個錯誤在哪個檔案?\n\n在不打斷目前工作的情況下,回答關於此對話的快速問題。若要執行獨立的背景任務,請使用 /bg <提示>。"
no_history: "尚無對話 — 請將問題作為一般訊息傳送。"
no_provider: "❌ 無法回答問題:未設定供應商憑證。"
started: "💬 順帶一問:「{preview}」\n正根據此對話的快照回答 — 目前的工作繼續進行。"
answer: "💬 /btw:「{preview}」\n\n{answer}"
failed: "❌ /btw 失敗:「{preview}」\n{error}"
branch:
db_unavailable: "工作階段資料庫無法使用。"
no_conversation: "沒有可分支的對話 — 請先傳送一則訊息。"
+9 -1
View File
@@ -68,9 +68,17 @@ gateway:
always_plural: "✅ 命令已批准(永久允许该模式)({count} 条命令)。代理正在恢复…"
background:
usage: "用法:/background <提示>\n示例:/background 总结今天 HN 上热门的故事\n\n在独立会话中运行该提示。你可以继续聊天 — 结果完成后将在此显示。"
usage: "用法:/bg <提示>\n示例:/bg 总结今天 HN 上热门的故事\n\n在独立会话中运行该提示。你可以继续聊天 — 结果完成后将在此显示。"
started: "🔄 后台任务已启动:「{preview}」\n任务 ID:{task_id}\n你可以继续聊天 — 完成后结果将在此显示。"
btw:
usage: "用法:/btw <问题>\n示例:/btw 那个错误在哪个文件?\n\n在不打断当前工作的情况下,回答关于此对话的快速问题。要运行独立的后台任务,请使用 /bg <提示>。"
no_history: "还没有对话 — 请将问题作为普通消息发送。"
no_provider: "❌ 无法回答问题:未配置提供商凭据。"
started: "💬 顺便一问:「{preview}」\n正根据此对话的快照回答 — 当前工作继续进行。"
answer: "💬 /btw:「{preview}」\n\n{answer}"
failed: "❌ /btw 失败:「{preview}」\n{error}"
branch:
db_unavailable: "会话数据库不可用。"
no_conversation: "没有可分支的对话 — 请先发送一条消息。"
+8 -3
View File
@@ -5148,7 +5148,7 @@ class DiscordAdapter(BasePlatformAdapter):
# historically ran with NO authorization check — bypassing every gate
# ``on_message`` enforces (DISCORD_ALLOWED_USERS, DISCORD_ALLOWED_ROLES,
# DISCORD_ALLOWED_CHANNELS, DISCORD_IGNORED_CHANNELS). Any guild member
# could invoke ``/background``, ``/restart``, ``/sethome``, etc. as the
# could invoke ``/bg``, ``/restart``, ``/sethome``, etc. as the
# operator. ``_check_slash_authorization`` mirrors the on_message gates
# one-for-one so the slash surface honors the same trust boundary.
#
@@ -6007,10 +6007,15 @@ class DiscordAdapter(BasePlatformAdapter):
async def slash_queue(interaction: discord.Interaction, prompt: str):
await self._run_simple_slash(interaction, f"/queue {prompt}", "Queued for the next turn.")
@tree.command(name="background", description="Run a prompt in the background")
@tree.command(name="bg", description="Run a prompt in a separate background session")
@discord.app_commands.describe(prompt="The prompt to run in the background")
async def slash_background(interaction: discord.Interaction, prompt: str):
await self._run_simple_slash(interaction, f"/background {prompt}", "Background task started~")
await self._run_simple_slash(interaction, f"/bg {prompt}", "Background task started~")
@tree.command(name="btw", description="Ask a side question about the current conversation")
@discord.app_commands.describe(question="The side question to answer without interrupting")
async def slash_btw(interaction: discord.Interaction, question: str):
await self._run_simple_slash(interaction, f"/btw {question}", "Side question dispatched~")
# ── Auto-register any gateway-available commands not yet on the tree ──
# This ensures new commands added to COMMAND_REGISTRY in
+92
View File
@@ -0,0 +1,92 @@
"""Tests for agent/side_question.py — the /btw context-aware side question engine."""
from unittest.mock import patch
from agent.side_question import (
SIDE_QUESTION_TASK,
answer_side_question,
render_history_for_side_question,
)
class TestRenderHistory:
def test_empty_history(self):
assert render_history_for_side_question([]) == "(no prior conversation)"
assert render_history_for_side_question(None) == "(no prior conversation)"
def test_basic_roles(self):
history = [
{"role": "system", "content": "SYSTEM PROMPT — must not appear"},
{"role": "user", "content": "fix the bug in foo.py"},
{
"role": "assistant",
"content": "Looking now.",
"tool_calls": [
{"function": {"name": "read_file"}},
{"function": {"name": "patch"}},
],
},
{"role": "tool", "content": "Traceback: ValueError in foo.py line 3"},
{"role": "assistant", "content": "Fixed it."},
]
out = render_history_for_side_question(history)
assert "SYSTEM PROMPT" not in out
assert "USER: fix the bug in foo.py" in out
assert "ASSISTANT [called tools: read_file, patch]" in out
assert "TOOL RESULT: Traceback: ValueError in foo.py line 3" in out
assert "ASSISTANT: Fixed it." in out
def test_structured_content_blocks(self):
history = [
{"role": "user", "content": [{"type": "text", "text": "hello there"}]},
]
out = render_history_for_side_question(history)
assert "USER: hello there" in out
def test_newest_biased_truncation(self):
history = [
{"role": "user", "content": f"message number {i} " + "x" * 400}
for i in range(200)
]
out = render_history_for_side_question(history, char_budget=3000)
# Newest messages survive; oldest are dropped with a marker.
assert "message number 199" in out
assert "message number 0 " not in out
assert out.startswith("[...older conversation omitted...]")
assert len(out) < 4000
def test_non_dict_entries_ignored(self):
out = render_history_for_side_question(["garbage", None, 42, {"role": "user", "content": "hi"}])
assert "USER: hi" in out
class TestAnswerSideQuestion:
def test_empty_question_raises(self):
try:
answer_side_question(" ", [])
except ValueError:
pass
else:
raise AssertionError("expected ValueError for empty question")
def test_calls_oneshot_with_snapshot_and_task(self):
captured = {}
def fake_run_oneshot(**kwargs):
captured.update(kwargs)
return "the error was in foo.py"
history = [{"role": "user", "content": "run the tests"}]
runtime = {"model": "m", "provider": "p", "base_url": "u", "api_key": "k", "api_mode": "chat_completions"}
with patch("agent.oneshot.run_oneshot", side_effect=fake_run_oneshot):
answer = answer_side_question(
"which file had the error?", history, main_runtime=runtime
)
assert answer == "the error was in foo.py"
assert captured["task"] == SIDE_QUESTION_TASK
assert captured["main_runtime"] is runtime
assert "USER: run the tests" in captured["user_input"]
assert "Side question: which file had the error?" in captured["user_input"]
# The instructions steer the model to answer only the side question.
assert "side" in captured["instructions"].lower()
+1 -1
View File
@@ -249,7 +249,7 @@ class TestCliApprovalUi:
patch.object(cli_module, "_cprint"), \
patch.object(cli_module, "ChatConsole") as chat_console:
chat_console.return_value.print = MagicMock()
cli._handle_background_command("/btw check weather")
cli._handle_background_command("/bg check weather")
# Join the worker thread deterministically rather than polling a
# wall-clock deadline — under load the thread's finally-block pop
+20 -12
View File
@@ -1,8 +1,8 @@
"""Regression tests for classic-CLI mid-run /background dispatch.
"""Regression tests for classic-CLI mid-run /bg and /btw dispatch.
Background
----------
``/background`` (``/bg``, ``/btw``) exists to start independent work while
``/bg`` (formerly ``/background``) exists to start independent work while
the current turn keeps running. Typed while the agent was busy it went into
``self._pending_input`` like ordinary input, and ``process_loop`` is blocked
inside ``self.chat()`` for the whole run, so the background task only started
@@ -72,15 +72,23 @@ class TestBackgroundInlineDetector:
cli = _make_cli()
cli._agent_running = True
assert cli._should_handle_background_command_inline(
"/background inspect the test failures"
"/bg inspect the test failures"
) is True
def test_detects_both_aliases(self):
def test_detects_both_commands(self):
cli = _make_cli()
cli._agent_running = True
assert cli._should_handle_background_command_inline("/bg do work") is True
assert cli._should_handle_background_command_inline("/btw do work") is True
def test_background_alias_still_resolves_to_bg(self):
"""The retired /background spelling no longer resolves to a command."""
cli = _make_cli()
cli._agent_running = True
assert cli._should_handle_background_command_inline(
"/background do work"
) is False
def test_ignores_background_when_agent_idle(self):
"""Idle input falls through to the normal process_loop dispatch."""
cli = _make_cli()
@@ -117,16 +125,16 @@ class TestBackgroundInlineDetector:
class TestBackgroundBusyPolicyContract:
"""The registry already declares the intent this detector implements."""
def test_background_declares_dispatch_while_busy(self):
def test_bg_and_btw_declare_dispatch_while_busy(self):
from hermes_cli.commands import resolve_command
cmd = resolve_command("background")
assert cmd is not None
assert cmd.busy_policy == "dispatch"
for name in ("bg", "btw"):
cmd = resolve_command(name)
assert cmd is not None
assert cmd.name == name
assert cmd.busy_policy == "dispatch"
def test_aliases_resolve_to_background(self):
def test_background_name_is_retired(self):
from hermes_cli.commands import resolve_command
for alias in ("bg", "btw"):
cmd = resolve_command(alias)
assert cmd is not None and cmd.name == "background"
assert resolve_command("background") is None
+109 -25
View File
@@ -1,4 +1,4 @@
"""Tests for /background gateway slash command.
"""Tests for /bg gateway slash command.
Tests the _handle_background_command handler (run a prompt in a separate
background session) across gateway messenger platforms.
@@ -14,7 +14,7 @@ from gateway.platforms.base import MessageEvent
from gateway.session import SessionSource
def _make_event(text="/background", platform=Platform.TELEGRAM,
def _make_event(text="/bg", platform=Platform.TELEGRAM,
user_id="12345", chat_id="67890"):
"""Build a MessageEvent for testing."""
source = SessionSource(
@@ -62,26 +62,18 @@ class TestHandleBackgroundCommand:
@pytest.mark.asyncio
async def test_no_prompt_shows_usage(self):
"""Running /background with no prompt shows usage."""
runner = _make_runner()
event = _make_event(text="/background")
result = await runner._handle_background_command(event)
assert "Usage:" in result
assert "/background" in result
@pytest.mark.asyncio
async def test_bg_alias_no_prompt_shows_usage(self):
"""Running /bg with no prompt shows usage."""
runner = _make_runner()
event = _make_event(text="/bg")
result = await runner._handle_background_command(event)
assert "Usage:" in result
assert "/bg" in result
@pytest.mark.asyncio
async def test_empty_prompt_shows_usage(self):
"""Running /background with only whitespace shows usage."""
"""Running /bg with only whitespace shows usage."""
runner = _make_runner()
event = _make_event(text="/background ")
event = _make_event(text="/bg ")
result = await runner._handle_background_command(event)
assert "Usage:" in result
@@ -172,44 +164,136 @@ class TestRunBackgroundTask:
# ---------------------------------------------------------------------------
# /background in help and known_commands
# /bg in help and known_commands
# ---------------------------------------------------------------------------
class TestBackgroundInHelp:
"""Verify /background appears in help text and known commands."""
"""Verify /bg and /btw appear in help text and known commands."""
@pytest.mark.asyncio
async def test_background_in_help_output(self):
"""The /help output includes /background."""
async def test_bg_and_btw_in_help_output(self):
"""The /help output includes /bg and /btw."""
runner = _make_runner()
event = _make_event(text="/help")
result = await runner._handle_help_command(event)
assert "/background" in result
assert "/bg" in result
assert "/btw" in result
# ---------------------------------------------------------------------------
# CLI /background command definition
# CLI /bg command definition
# ---------------------------------------------------------------------------
class TestBackgroundInCLICommands:
"""Verify /background is registered in the CLI command system."""
"""Verify /bg and /btw are registered in the CLI command system."""
def test_background_autocompletes(self):
"""The /background command appears in autocomplete results."""
def test_bg_autocompletes(self):
"""The /bg and /btw commands appear in autocomplete results."""
pytest.importorskip("prompt_toolkit")
from hermes_cli.commands import SlashCommandCompleter
from prompt_toolkit.document import Document
completer = SlashCommandCompleter()
doc = Document("backgro") # Partial match
doc = Document("bg") # Partial match
completions = list(completer.get_completions(doc, None))
# Text doesn't start with / so no completions
assert len(completions) == 0
doc = Document("/backgro") # With slash prefix
doc = Document("/bg") # With slash prefix
completions = list(completer.get_completions(doc, None))
cmd_displays = [str(c.display) for c in completions]
assert any("/background" in d for d in cmd_displays)
assert any("/bg" in d for d in cmd_displays)
doc = Document("/btw")
completions = list(completer.get_completions(doc, None))
cmd_displays = [str(c.display) for c in completions]
assert any("/btw" in d for d in cmd_displays)
# ---------------------------------------------------------------------------
# _handle_btw_command
# ---------------------------------------------------------------------------
class TestHandleBtwCommand:
"""Tests for GatewayRunner._handle_btw_command (context-aware side question)."""
@pytest.mark.asyncio
async def test_no_question_shows_usage(self):
runner = _make_runner()
event = _make_event(text="/btw")
result = await runner._handle_btw_command(event)
assert "Usage:" in result
assert "/btw" in result
@pytest.mark.asyncio
async def test_no_history_reports_no_conversation(self):
runner = _make_runner()
store = AsyncMock()
store.get_or_create_session.return_value = MagicMock(session_id="s1")
store.load_transcript.return_value = []
store._store = runner.session_store
runner._async_session_store = store
event = _make_event(text="/btw what did we do?")
result = await runner._handle_btw_command(event)
assert "conversation" in result.lower()
@pytest.mark.asyncio
async def test_dispatches_side_question_and_sends_answer(self):
runner = _make_runner()
store = AsyncMock()
store.get_or_create_session.return_value = MagicMock(session_id="s1")
store.load_transcript.return_value = [
{"role": "user", "content": "fix foo.py"},
{"role": "assistant", "content": "done"},
]
store._store = runner.session_store
runner._async_session_store = store
runner._resolve_session_agent_runtime = MagicMock(
return_value=("test-model", {"api_key": "k", "provider": "p",
"base_url": "u", "api_mode": "chat_completions"})
)
runner._reply_anchor_for_event = MagicMock(return_value=None)
runner._thread_metadata_for_source = MagicMock(return_value=None)
mock_adapter = AsyncMock()
runner._adapter_for_source = MagicMock(return_value=mock_adapter)
event = _make_event(text="/btw which file was that?")
with patch("agent.side_question.answer_side_question",
return_value="it was foo.py") as mock_answer:
result = await runner._handle_btw_command(event)
# Ack returned immediately, worker task registered.
assert "which file was that?" in result
# Drain the fire-and-forget task.
for task in list(runner._background_tasks):
await task
# Snapshot + question reached the engine; live history untouched.
args, kwargs = mock_answer.call_args
assert args[0] == "which file was that?"
assert args[1][0]["content"] == "fix foo.py"
assert kwargs["main_runtime"]["model"] == "test-model"
# The answer was delivered to the chat.
mock_adapter.send.assert_called_once()
sent_text = mock_adapter.send.call_args[0][1]
assert "it was foo.py" in sent_text
@pytest.mark.asyncio
async def test_no_credentials_reports_error(self):
runner = _make_runner()
store = AsyncMock()
store.get_or_create_session.return_value = MagicMock(session_id="s1")
store.load_transcript.return_value = [{"role": "user", "content": "hi"}]
store._store = runner.session_store
runner._async_session_store = store
runner._resolve_session_agent_runtime = MagicMock(
return_value=(None, {"api_key": None})
)
event = _make_event(text="/btw what?")
result = await runner._handle_btw_command(event)
assert "❌" in result
@@ -186,18 +186,34 @@ class TestCommandBypassActiveSession:
@pytest.mark.asyncio
async def test_background_bypasses_guard(self):
"""/background must bypass so it spawns a parallel task, not an interrupt."""
"""/bg must bypass so it spawns a parallel task, not an interrupt."""
adapter = _make_adapter()
sk = _session_key()
adapter._active_sessions[sk] = asyncio.Event()
await adapter.handle_message(_make_event("/background summarize HN"))
await adapter.handle_message(_make_event("/bg summarize HN"))
assert sk not in adapter._pending_messages, (
"/background was queued as a pending message instead of being dispatched"
"/bg was queued as a pending message instead of being dispatched"
)
assert any("handled:background" in r for r in adapter.sent_responses), (
"/background response was not sent back to the user"
assert any("handled:bg" in r for r in adapter.sent_responses), (
"/bg response was not sent back to the user"
)
@pytest.mark.asyncio
async def test_btw_bypasses_guard(self):
"""/btw must bypass so the side question dispatches mid-run."""
adapter = _make_adapter()
sk = _session_key()
adapter._active_sessions[sk] = asyncio.Event()
await adapter.handle_message(_make_event("/btw which file was that?"))
assert sk not in adapter._pending_messages, (
"/btw was queued as a pending message instead of being dispatched"
)
assert any("handled:btw" in r for r in adapter.sent_responses), (
"/btw response was not sent back to the user"
)
@pytest.mark.asyncio
+5 -5
View File
@@ -3,7 +3,7 @@
Slash invocations (``_run_simple_slash``, ``_handle_thread_create_slash``)
historically bypassed every gate ``on_message`` enforces — DISCORD_ALLOWED_USERS,
DISCORD_ALLOWED_ROLES, DISCORD_ALLOWED_CHANNELS, DISCORD_IGNORED_CHANNELS.
Any guild member could invoke ``/background``, ``/restart``, etc. as the
Any guild member could invoke ``/bg``, ``/restart``, etc. as the
operator. ``_check_slash_authorization`` mirrors all four gates one-for-one.
These tests pin the security-correct behavior so the bypass cannot regress.
@@ -213,7 +213,7 @@ async def test_no_allowlist_allows_with_gateway_allow_all(adapter, monkeypatch):
async def test_allowed_user_passes(adapter):
adapter._allowed_user_ids = {"100200300"}
interaction = _make_interaction("100200300")
assert await adapter._check_slash_authorization(interaction, "/background hi") is True
assert await adapter._check_slash_authorization(interaction, "/bg hi") is True
interaction.response.send_message.assert_not_awaited()
@@ -256,7 +256,7 @@ async def test_channel_not_in_allowlist_rejected(adapter, monkeypatch, caplog):
monkeypatch.setenv("DISCORD_ALLOWED_CHANNELS", "1111,2222")
interaction = _make_interaction("100200300", channel_id=9999)
with caplog.at_level(logging.WARNING):
assert await adapter._check_slash_authorization(interaction, "/background hi") is False
assert await adapter._check_slash_authorization(interaction, "/bg hi") is False
assert any("DISCORD_ALLOWED_CHANNELS" in r.message for r in caplog.records)
@@ -284,7 +284,7 @@ async def test_unauthorized_attempt_notifies_telegram(adapter):
adapter._allowed_user_ids = {"100200300"}
interaction = _make_interaction("999999999")
await adapter._check_slash_authorization(interaction, "/background hi")
await adapter._check_slash_authorization(interaction, "/bg hi")
# Notify is fire-and-forget — let the scheduled task run.
await asyncio.sleep(0)
@@ -295,7 +295,7 @@ async def test_unauthorized_attempt_notifies_telegram(adapter):
assert chat_id == "987654321"
assert "Unauthorized" in msg
assert "999999999" in msg
assert "/background hi" in msg
assert "/bg hi" in msg
assert "DISCORD_ALLOWED_USERS" in msg
@@ -1,6 +1,6 @@
"""Regression: background tasks respect profile secret scope when multiplexing.
Issue #60726: /background spawns _run_background_task as a fire-and-forget
Issue #60726: /bg spawns _run_background_task as a fire-and-forget
asyncio task with no profile scope, so _resolve_session_agent_runtime()'s
credential reads raise UnscopedSecretError when multiplex_profiles is on.
The fix wraps the task body in _profile_runtime_scope, mirroring _run_agent.
+1 -1
View File
@@ -238,7 +238,7 @@ async def test_admin_runs_quick_command_when_gating_enabled():
# Running-agent fast-path gating — admin/user split must hold even when an
# agent is already running. The fast-path block in _handle_message dispatches
# /stop, /restart, /new, /steer, /model, /approve, /deny, /agents,
# /background, /kanban, /goal, /yolo, /verbose, /footer, /help, /commands,
# /bg, /btw, /kanban, /goal, /yolo, /verbose, /footer, /help, /commands,
# /profile, /update directly without going through the cold dispatch site.
# We must apply the gate there too — otherwise non-admins could bypass
# gating just because an agent happens to be busy.
@@ -23,7 +23,8 @@ _HISTORICAL_BYPASS_NAMES = frozenset(
{
"agents",
"approve",
"background",
"bg",
"btw",
"commands",
"deny",
"help",
+9 -5
View File
@@ -176,11 +176,14 @@ class TestGatewayHelpLines:
assert not re.search(pattern, joined), \
f"cli_only command /{cmd.name} should not be in gateway help"
def test_includes_alias_note_for_bg(self):
def test_bg_and_btw_are_separate_commands(self):
lines = gateway_help_lines()
joined = "\n".join(lines)
assert "`/bg" in joined
assert "`/btw" in joined
# The retired /background canonical name must be gone.
bg_line = [l for l in lines if "/background" in l]
assert len(bg_line) == 1
assert "/bg" in bg_line[0]
assert not bg_line
class TestTelegramBotCommands:
@@ -198,11 +201,12 @@ class TestTelegramBotCommands:
def test_includes_builtin_commands_with_required_args(self):
"""Built-in arg-taking commands (e.g. /queue, /steer, /background)
"""Built-in arg-taking commands (e.g. /queue, /steer, /bg, /btw)
are now included because their handlers return usage text when
invoked without arguments — issue #24312."""
names = {name for name, _ in telegram_bot_commands()}
assert "background" in names
assert "bg" in names
assert "btw" in names
assert "queue" in names
assert "steer" in names
+70
View File
@@ -1380,6 +1380,76 @@ def _(rid, params: dict) -> dict:
return _ok(rid, {"task_id": task_id})
@method("prompt.btw")
def _(rid, params: dict) -> dict:
"""Answer a side question about the session without touching its history.
Snapshots the live conversation (in-flight ``_session_messages`` when a
turn is running, else the persisted ``session["history"]``) and runs a
one-shot auxiliary LLM call against it (``agent/side_question.py``). The
session's history, role alternation, and prompt cache are untouched; the
answer arrives as a ``btw.complete`` event.
"""
session, err = _sess(params, rid)
if err:
return err
text, parent = params.get("text", ""), params.get("session_id", "")
if not text:
return _err(rid, 4012, "text required")
task_id = f"btw_{uuid.uuid4().hex[:6]}"
agent = session.get("agent")
snapshot = list(
getattr(agent, "_session_messages", None)
or session.get("history")
or []
)
main_runtime = {
"model": getattr(agent, "model", None),
"provider": getattr(agent, "provider", None),
"base_url": getattr(agent, "base_url", None),
"api_key": getattr(agent, "api_key", None),
"api_mode": getattr(agent, "api_mode", None),
}
def run():
session_tokens = _set_session_context(task_id, cwd=_session_cwd(session))
try:
from agent.side_question import answer_side_question
_profile_home_str = session.get("profile_home")
home_token = (
set_hermes_home_override(_profile_home_str)
if _profile_home_str
else None
)
try:
answer = answer_side_question(
text,
snapshot,
main_runtime=main_runtime,
)
finally:
if home_token is not None:
reset_hermes_home_override(home_token)
_emit(
"btw.complete",
parent,
{"task_id": task_id, "question": text, "text": answer or ""},
)
except Exception as e:
_emit(
"btw.complete",
parent,
{"task_id": task_id, "question": text, "text": f"error: {e}"},
)
finally:
_clear_session_context(session_tokens)
threading.Thread(target=run, daemon=True).start()
return _ok(rid, {"task_id": task_id})
@method("preview.restart")
def _(rid, params: dict) -> dict:
session, err = _sess(params, rid)
+2 -1
View File
@@ -16,7 +16,8 @@ interface CommandRegistryLoad {
const NATIVE_MUTATING_COMMANDS = new Set(['browser', 'busy', 'fast', 'reload-mcp', 'rollback', 'stop'])
const MUTATING_COMMANDS = [
'background',
'bg',
'btw',
'branch',
'browser',
'busy',
@@ -1295,6 +1295,10 @@ export function createGatewayEventHandler(ctx: GatewayEventHandlerContext): (ev:
dropBgTask(ev.payload.task_id)
sys(`[bg ${ev.payload.task_id}] ${ev.payload.text}`)
return
case 'btw.complete':
sys(`[btw${ev.payload.question ? ` "${ev.payload.question}"` : ''}] ${ev.payload.text}`)
return
case 'review.summary': {
// Self-improvement background review emitted a persistent summary
+23 -3
View File
@@ -78,12 +78,12 @@ const reasoningConfigPayload = (arg: string, sid: string) => {
export const sessionCommands: SlashCommand[] = [
{
aliases: ['bg', 'btw'],
aliases: ['background'],
help: 'launch a background prompt',
name: 'background',
name: 'bg',
run: (arg, ctx) => {
if (!arg) {
return ctx.transcript.sys('/background <prompt>')
return ctx.transcript.sys('/bg <prompt>')
}
ctx.gateway.rpc<BackgroundStartResponse>('prompt.background', { session_id: ctx.sid, text: arg }).then(
@@ -99,6 +99,26 @@ export const sessionCommands: SlashCommand[] = [
}
},
{
help: 'ask a side question about this conversation',
name: 'btw',
run: (arg, ctx) => {
if (!arg) {
return ctx.transcript.sys('/btw <question>')
}
ctx.gateway.rpc<BackgroundStartResponse>('prompt.btw', { session_id: ctx.sid, text: arg }).then(
ctx.guarded<BackgroundStartResponse>(r => {
if (!r.task_id) {
return
}
ctx.transcript.sys(`btw ${r.task_id} — answering from a conversation snapshot`)
})
)
}
},
{
help: 'change or show model',
name: 'model',
+1
View File
@@ -729,6 +729,7 @@ export type GatewayEvent =
| { payload: { env_var: string; prompt: string; request_id: string }; session_id?: string; type: 'secret.request' }
| { payload: { request_id: string }; session_id?: string; type: 'secret.expire' | 'sudo.expire' }
| { payload: { task_id: string; text: string }; session_id?: string; type: 'background.complete' }
| { payload: { question?: string; task_id: string; text: string }; session_id?: string; type: 'btw.complete' }
| { payload?: { text?: string }; session_id?: string; type: 'review.summary' }
| { payload: SubagentEventPayload; session_id?: string; type: 'subagent.spawn_requested' }
| { payload: SubagentEventPayload; session_id?: string; type: 'subagent.start' }
+1 -1
View File
@@ -42,7 +42,7 @@ The single line at the bottom of the TUI. Segments appear only when relevant and
| `⏱` | Per-prompt elapsed time while the turn runs, e.g. `⏱ 12s/3m 45s` (turn time / session time). |
| `⏲` | The same timer, frozen after the turn completes. |
| `cmp N` | The session has been auto-compressed N times. |
| `▶ N` | N `/background` tasks currently running. |
| `▶ N` | N `/bg` tasks currently running. |
| `⚠ YOLO` | YOLO mode is on (auto-approval). Also shown in the startup banner. |
| `⛓ N` | N subagents currently active. |
| `↩ resumes when subagent finishes` | Reassurance shown while you are idle but delegated work is still in flight — the result returns on its own. |
+5 -3
View File
@@ -64,7 +64,8 @@ Type `/` in the CLI to open the autocomplete menu. Built-in commands are case-in
| `/status` | Show session info — model, provider, profile, session ID, working directory, title, created/updated timestamps, token totals, agent-running state — followed by a local **Session recap** block (recent user/assistant turn counts, tool result count, top tools used, last few files touched, the latest user prompt, and the latest assistant reply). The recap is computed locally from the in-memory conversation; no LLM call, no prompt-cache impact. |
| `/context [all]` (alias: `/ctx`) | Visual context-window breakdown. On the CLI/TUI: a 5×20 glyph block grid (each cell ≈ 1% of the model window) plus an estimated per-category table — system prompt, tool definitions, rules, skills index, MCP, subagents, memory, conversation — versus free space. On messaging platforms: a usage gauge with auto-compression threshold/headroom, compression stats, cumulative throughput, and the same category table in plain text. `/context all` appends per-skill and per-toolset cost listings (index cost vs SKILL.md load cost; schema tokens per toolset). Read-only and computed locally — no LLM call, no prompt-cache impact. |
| `/agents` (alias: `/tasks`) | Show active agents and running tasks across the current session. |
| `/background <prompt>` (alias: `/bg`, `/btw`) | Run a prompt in a separate background session. The agent processes your prompt independently — your current session stays free for other work. Results appear as a panel when the task finishes. See [CLI Background Sessions](/user-guide/cli#background-sessions). |
| `/bg <prompt>` | Run a prompt in a separate background session. The agent processes your prompt independently — your current session stays free for other work. Results appear as a panel when the task finishes. See [CLI Background Sessions](/user-guide/cli#background-sessions). |
| `/btw <question>` | Ask a quick side question **about the current conversation** without interrupting it. A one-shot auxiliary LLM call answers from a read-only snapshot of the transcript — the live session's history and prompt cache are untouched, and the current turn keeps running. For independent work with a fresh context, use `/bg`. |
| `/branch [name]` (alias: `/fork`) | Branch the current session (explore a different path) |
| `/worktree [new [name]\|list]` | **CLI only.** Inspect or create isolated git worktrees mid-session (inspired by Copilot CLI's `/worktree new`). Bare `/worktree` shows the active worktree; `/worktree list` lists the repo's worktrees; `/worktree new [name]` creates a worktree under `.worktrees/` (branched from the freshly-fetched remote tip, honoring `worktree_sync`) and retargets the session's terminal and file tools into it. Named trees use your name (`hermes/<name>` branch); unnamed ones get a random `hermes-<id>`. On exit the tree is kept only if it has unpushed commits — same lifecycle as `hermes -w`. See [Git Worktrees](/user-guide/git-worktrees). |
| `/handoff <platform>` | **CLI only.** Hand the current session off to a messaging platform (Telegram, Discord, Slack, WhatsApp, Signal, Matrix). The gateway picks it up immediately, creates a fresh thread on platforms that support threads (Telegram topics, Discord text-channel threads, Slack message-anchored threads), re-binds the destination to your CLI session_id so the full role-aware transcript replays, and forges a synthetic user turn so the agent confirms it's working in the new place. Your CLI exits cleanly on success with a `/resume` hint; resume locally any time with `/resume <title>`. Refused mid-turn. Requires the gateway to be running and a home channel configured for the target platform (`/sethome` from the destination chat). See [Cross-Platform Handoff](/user-guide/sessions#cross-platform-handoff). |
@@ -250,7 +251,8 @@ The messaging gateway supports the following built-in commands inside Telegram,
| `/voice [on\|off\|tts\|join\|channel\|leave\|status]` | Control spoken replies in chat. `join`/`channel`/`leave` manage Discord voice-channel mode. |
| `/rollback [number]` | List or restore filesystem checkpoints. |
| `/diff [staged\|all\|session] [--stat]` | Show git changes in the working directory (fenced and truncated to platform message limits). `session` shows the cumulative diff of everything Hermes changed; `--stat` shows just the summary. |
| `/background <prompt>` | Run a prompt in a separate background session. Results are delivered back to the same chat when the task finishes. See [Messaging Background Sessions](/user-guide/messaging/#background-sessions). |
| `/bg <prompt>` | Run a prompt in a separate background session. Results are delivered back to the same chat when the task finishes. See [Messaging Background Sessions](/user-guide/messaging/#background-sessions). |
| `/btw <question>` | Ask a side question about the current conversation without interrupting it. Answered from a transcript snapshot; the answer is sent to the chat when ready. |
| `/queue <prompt>` (alias: `/q`) | Queue a prompt for the next turn without interrupting the current one. |
| `/steer <prompt>` | Inject a message after the next tool call without interrupting — the model picks it up on its next iteration rather than as a new turn. |
| `/goal <text>` | Set a standing goal Hermes works toward across turns — our take on the Ralph loop. A judge model checks after each turn; if not done, Hermes auto-continues until it is, you pause/clear it, or the turn budget (default 20) is hit. Subcommands: `/goal status`, `/goal pause`, `/goal resume`, `/goal clear`. Safe to run mid-agent for status/pause/clear; setting a new goal requires `/stop` first. See [Persistent Goals](/user-guide/features/goals). |
@@ -295,7 +297,7 @@ The messaging gateway supports the following built-in commands inside Telegram,
- `/verbose` is **CLI-only by default**, but can be enabled for messaging platforms by setting `display.tool_progress_command: true` in `config.yaml`. When enabled, it cycles the `display.tool_progress` mode and saves to config.
- `/focus` and `/verbose` share one suppression path (`display.tool_progress`), so they can never contradict each other: `/focus on` pins tool progress to `off` and stashes your mode under `display.focus_saved_tool_progress`; `/focus off` restores it; cycling `/verbose` while focus is on takes the mode back and clears the focus badge. Focus view is display-only — it never changes conversation history, the system prompt, or anything sent to the model, so it has zero prompt-cache impact.
- `/sethome`, `/restart`, `/approve`, `/deny`, `/topic`, `/platform`, and `/commands` are **messaging-only** commands.
- `/status`, `/egress`, `/version`, `/whoami`, `/background`, `/queue`, `/steer`, `/voice`, `/reload-mcp`, `/reload-skills`, `/rollback`, `/diff`, `/debug`, `/fast`, `/approvals`, `/footer`, `/curator`, `/kanban`, `/topup`, `/suggestions`, `/blueprint`, `/learn`, `/init`, `/sessions`, and `/yolo` work in **both** the CLI and the messaging gateway.
- `/status`, `/egress`, `/version`, `/whoami`, `/bg`, `/btw`, `/queue`, `/steer`, `/voice`, `/reload-mcp`, `/reload-skills`, `/rollback`, `/diff`, `/debug`, `/fast`, `/approvals`, `/footer`, `/curator`, `/kanban`, `/topup`, `/suggestions`, `/blueprint`, `/learn`, `/init`, `/sessions`, and `/yolo` work in **both** the CLI and the messaging gateway.
- `/voice join`, `/voice channel`, and `/voice leave` are only meaningful on Discord.
- In the TUI, `/sessions` shows live sessions in the current TUI process. Use `/resume [name]` or `hermes --tui --resume <id-or-title>` for saved or closed transcripts.
+7 -6
View File
@@ -136,7 +136,7 @@ A persistent status bar sits above the input area, updating in real time:
| Context bar | Visual fill indicator with color-coded thresholds |
| Cost | Estimated session cost (or `n/a` for unknown/zero-priced models) |
| 🗜️ N | **Context compression count** — how many times the running session has been auto-compressed. Appears once the first compression fires. |
| ▶ N | **Active background tasks** — how many `/background` prompts are still running in the current session. Appears whenever at least one task is in flight. |
| ▶ N | **Active background tasks** — how many `/bg` prompts are still running in the current session. Appears whenever at least one task is in flight. |
| Duration | Elapsed session time |
| Session title | Once the session has a title, it appears as a gold badge pinned to the far-right edge. Long titles truncate before displacing the essential model and context fields. |
| ⚠ YOLO | **YOLO mode warning** — shown whenever `HERMES_YOLO_MODE` is on (either `hermes --yolo` at launch or `/yolo` toggled mid-session). Mirrors the banner-line warning so you can't forget you're in auto-approve mode. |
@@ -213,7 +213,8 @@ Common examples:
| `/model` | Show or change the current model |
| `/tools` | List currently available tools |
| `/skills browse` | Browse the skills hub and official optional skills |
| `/background <prompt>` | Run a prompt in a separate background session |
| `/bg <prompt>` | Run a prompt in a separate background session |
| `/btw <question>` | Ask a side question about the current conversation without interrupting it |
| `/skin` | Show or switch the active CLI skin |
| `/voice on` | Enable CLI voice mode (press `Ctrl+B` to record) |
| `/voice tts` | Toggle spoken playback for Hermes replies |
@@ -487,7 +488,7 @@ When compression triggers, middle turns are summarized while the first 3 and las
Run a prompt in a separate background session while continuing to use the CLI for other work:
```
/background Analyze the logs in /var/log and summarize any errors from today
/bg Analyze the logs in /var/log and summarize any errors from today
```
Hermes immediately confirms the task and gives you back the prompt:
@@ -499,7 +500,7 @@ Hermes immediately confirms the task and gives you back the prompt:
### How It Works
Each `/background` prompt spawns a **completely separate agent session** in a daemon thread:
Each `/bg` prompt spawns a **completely separate agent session** in a daemon thread:
- **Isolated conversation** — the background agent has no knowledge of your current session's history. It receives only the prompt you provide.
- **Same configuration** — the background agent inherits your model, provider, toolsets, reasoning settings, and fallback model from the current session.
@@ -523,8 +524,8 @@ If the task fails, you'll see an error notification instead. If `display.bell_on
### Use Cases
- **Long-running research** — "/background research the latest developments in quantum error correction" while you work on code
- **File processing** — "/background analyze all Python files in this repo and list any security issues" while you continue a conversation
- **Long-running research** — "/bg research the latest developments in quantum error correction" while you work on code
- **File processing** — "/bg analyze all Python files in this repo and list any security issues" while you continue a conversation
- **Parallel investigations** — start multiple background tasks to explore different angles simultaneously
:::info
+1 -1
View File
@@ -648,7 +648,7 @@ Hermes automatically registers installed skills as **native Discord Application
- Each skill becomes a Discord slash command (e.g., `/code-review`, `/ascii-art`)
- Skills accept an optional `args` string parameter
- Discord has a limit of 100 application commands per bot — if you have more skills than available slots, extra skills are skipped with a warning in the logs
- Skills are registered during bot startup alongside built-in commands like `/model`, `/reset`, and `/background`
- Skills are registered during bot startup alongside built-in commands like `/model`, `/reset`, and `/bg`
No extra configuration is needed — any skill installed via `hermes skills install` is automatically registered as a Discord slash command on the next gateway restart.
+8 -7
View File
@@ -210,7 +210,8 @@ platform network disconnect as an event-loop failure.
| `/reasoning [level\|show\|hide]` | Change reasoning effort or toggle reasoning display |
| `/voice [on\|off\|tts\|join\|leave\|status]` | Control messaging voice replies and Discord voice-channel behavior |
| `/rollback [number]` | List or restore filesystem checkpoints |
| `/background <prompt>` | Run a prompt in a separate background session |
| `/bg <prompt>` | Run a prompt in a separate background session |
| `/btw <question>` | Ask a side question about the current conversation without interrupting it |
| `/reload-mcp` | Reload MCP servers from config |
| `/update` | Update Hermes Agent to the latest version |
| `/help` | Show available commands |
@@ -495,7 +496,7 @@ When enabled, the bot sends status messages as it works:
Run a prompt in a separate background session so the agent works on it independently while your main chat stays responsive:
```
/background Check all servers in the cluster and report any that are down
/bg Check all servers in the cluster and report any that are down
```
Hermes confirms immediately:
@@ -507,7 +508,7 @@ Hermes confirms immediately:
### How It Works
Each `/background` prompt spawns a **separate agent instance** that runs asynchronously:
Each `/bg` prompt spawns a **separate agent instance** that runs asynchronously:
- **Isolated session** — the background agent has its own session with its own conversation history. It has no knowledge of your current chat context and receives only the prompt you provide.
- **Same configuration** — inherits your model, provider, toolsets, reasoning settings, and provider routing from the current gateway setup.
@@ -539,10 +540,10 @@ HERMES_BACKGROUND_NOTIFICATIONS=result
### Use Cases
- **Server monitoring** — "/background Check the health of all services and alert me if anything is down"
- **Long builds** — "/background Build and deploy the staging environment" while you continue chatting
- **Research tasks** — "/background Research competitor pricing and summarize in a table"
- **File operations** — "/background Organize the photos in ~/Downloads by date into folders"
- **Server monitoring** — "/bg Check the health of all services and alert me if anything is down"
- **Long builds** — "/bg Build and deploy the staging environment" while you continue chatting
- **Research tasks** — "/bg Research competitor pricing and summarize in a table"
- **File operations** — "/bg Organize the photos in ~/Downloads by date into folders"
:::tip
Background tasks on messaging platforms are fire-and-forget — you don't need to wait or check on them. Results arrive in the same chat automatically when the task finishes.
+1 -1
View File
@@ -553,7 +553,7 @@ To find a Room ID: in Element, go to the room → **Settings** → **Advanced**
Hermes supports the same gateway commands in Matrix that it supports on other
messaging platforms, including `/commands`, `/model`, `/stop`, `/queue`,
`/steer`, `/goal`, `/subgoal`, `/background`, `/bg`, `/btw`, `/tasks`, and
`/steer`, `/goal`, `/subgoal`, `/bg`, `/btw`, `/tasks`, and
`/yolo`.
Some Matrix clients reserve leading `/` for local client commands and may not
+1 -1
View File
@@ -301,7 +301,7 @@ Then in Slack:
### Legacy `/hermes <subcommand>` still works
For backward compatibility with older manifests, you can still type
`/hermes btw run the tests` — Hermes routes it the same way as `/btw
`/hermes bg run the tests` — Hermes routes it the same way as `/bg
run the tests`. Free-form questions also work: `/hermes what's the
weather?` is treated as a regular message.
@@ -839,7 +839,7 @@ Shows the current topic's binding: session title, session ID, and hints for `/ne
- The General (pinned top) topic in a forum-enabled DM is treated as the root lobby, regardless of whether Telegram delivers its messages with `message_thread_id=1` or with no thread_id
- Root-lobby reminders are rate-limited to one message per 30 seconds per chat — a user who forgets topic mode is on and types ten prompts in the root won't get ten replies
- BotFather setup screenshots are rate-limited to one send per 5 minutes per chat — repeated `/topic` attempts while Threads Settings are still disabled won't re-upload the same image
- `/background <prompt>` started inside a topic delivers its result back to the same topic; background sessions don't trigger auto-rename of the owning topic
- `/bg <prompt>` started inside a topic delivers its result back to the same topic; background sessions don't trigger auto-rename of the owning topic
- `/topic` itself is gated by the bot's user authorization check — unauthorized DMs get a refusal instead of activation
### Disabling multi-session mode
+1 -1
View File
@@ -323,7 +323,7 @@ Results are delivered to your home channel.
Run long operations without blocking the conversation:
```
/background Analyze all files in the archive
/bg Analyze all files in the archive
```
### Cross-Platform Messages
+1 -1
View File
@@ -216,7 +216,7 @@ The status line also shows:
- **Working directory with git branch** — `~/projects/hermes-agent (docs/two-week-gap-sweep)`. The branch suffix updates when you `git checkout` in a side terminal (mtime-cached) so the TUI reflects your actual active branch, not whatever it was at launch.
- **Per-prompt elapsed time** — `⏱ 12s/3m 45s` while the turn is running (live), frozen to `⏲ 32s / 3m 45s` after the turn completes. First number is time since last user message; second is total session duration. Resets on every new prompt.
- **`🗜️ N`** — number of times the running session has been auto-compressed. Appears once the first compression fires.
- **`▶ N`** — number of `/background` tasks currently running in this session. Appears whenever at least one task is in flight.
- **`▶ N`** — number of `/bg` tasks currently running in this session. Appears whenever at least one task is in flight.
- **`⚠ YOLO`** — visible warning whenever YOLO mode is on (`hermes --yolo`, `/yolo`, or `HERMES_YOLO_MODE=1`). The same badge also appears in the startup banner so you cannot launch an auto-approving session without noticing.
## Configuration
@@ -56,7 +56,8 @@ Hermes 有两个斜杠命令入口,均由 `hermes_cli/commands.py` 中的中
| `/redraw` | 强制完整重绘 UI(在 tmux 调整大小、鼠标选择产生残影等导致终端错位后恢复)。 |
| `/status` | 显示会话信息——模型、提供商、profile、会话 ID、工作目录、标题、创建/更新时间戳、token 总量、agent 运行状态——随后显示本地**会话摘要**块(近期用户/助手轮次数、工具结果数、最常用工具、最近访问的文件、最新用户 prompt 和最新助手回复)。摘要从内存中的对话本地计算,不调用 LLM,不影响 prompt 缓存。 |
| `/agents`(别名:`/tasks`) | 显示当前会话中的活动 agent 和运行中的任务。 |
| `/background <prompt>`(别名:`/bg`、`/btw`) | 在独立的后台会话中运行 prompt。agent 独立处理你的 prompt——当前会话保持空闲可继续其他工作。任务完成后结果以面板形式显示。见 [CLI 后台会话](/user-guide/cli#background-sessions)。 |
| `/bg <prompt>` | 在独立的后台会话中运行 prompt。agent 独立处理你的 prompt——当前会话保持空闲可继续其他工作。任务完成后结果以面板形式显示。见 [CLI 后台会话](/user-guide/cli#background-sessions)。 |
| `/btw <question>` | 在不打断当前工作的情况下,就**当前对话**提出一个快速的顺带问题。由一次一次性辅助 LLM 调用根据对话快照作答——实时会话的历史和提示缓存不受影响,当前回合继续运行。需要全新上下文的独立任务请使用 `/bg`。 |
| `/branch [name]`(别名:`/fork`) | 分支当前会话(探索不同路径) |
| `/handoff <platform>` | **仅限 CLI。** 将当前会话移交给消息平台(Telegram、Discord、Slack、WhatsApp、Signal、Matrix)。gateway 立即接管,在支持线程的平台上创建新线程(Telegram 话题、Discord 文字频道线程、Slack 消息锚定线程),将目标重新绑定到你的 CLI session_id 以重放完整的角色感知转录,并伪造一条合成用户轮次让 agent 确认已在新位置工作。成功后 CLI 干净退出并提示 `/resume`;随时可用 `/resume <title>` 在本地恢复。轮次进行中拒绝执行。需要 gateway 正在运行且目标平台已配置 home 频道(从目标聊天中执行 `/sethome`)。见 [跨平台移交](/user-guide/sessions#cross-platform-handoff)。 |
@@ -220,7 +221,8 @@ hermes config set model.aliases.grok x-ai/grok-4
| `/reasoning [level\|show\|hide]` | 更改推理力度或切换推理显示。 |
| `/voice [on\|off\|tts\|join\|channel\|leave\|status]` | 控制聊天中的语音回复。`join`/`channel`/`leave` 管理 Discord 语音频道模式。 |
| `/rollback [number]` | 列出或恢复文件系统检查点。 |
| `/background <prompt>` | 在独立的后台会话中运行 prompt。任务完成后结果投递回同一聊天。见 [消息平台后台会话](/user-guide/messaging/#background-sessions)。 |
| `/bg <prompt>` | 在独立的后台会话中运行 prompt。任务完成后结果投递回同一聊天。见 [消息平台后台会话](/user-guide/messaging/#background-sessions)。 |
| `/btw <question>` | 在不打断当前对话的情况下,就当前对话提出顺带问题。根据对话快照作答;答案就绪后发送到聊天中。 |
| `/queue <prompt>`(别名:`/q`) | 将 prompt 加入队列等待下一轮处理,不中断当前轮次。 |
| `/steer <prompt>` | 在下一次工具调用后注入一条消息,不中断——模型在下一次迭代时获取,而非作为新轮次。 |
| `/goal <text>` | 设置一个持续目标,Hermes 将跨轮次持续推进——这是我们对 Ralph loop 的实现。裁判模型在每轮后检查;若未完成,Hermes 自动继续,直到完成、你暂停/清除,或达到轮次预算(默认 20)。子命令:`/goal status`、`/goal pause`、`/goal resume`、`/goal clear`。agent 运行中可安全执行 status/pause/clear;设置新目标需先执行 `/stop`。见 [持续目标](/user-guide/features/goals)。 |
@@ -249,7 +251,7 @@ hermes config set model.aliases.grok x-ai/grok-4
- `/skills` **仅在搜索/浏览/安装时属于 CLI-only**;其写入审批子命令(`pending`、`approve`、`reject`、`diff`、`approval`)在 `skills.write_approval` 开启时也可在消息平台使用。`/memory` 可在**两个表面**使用。
- `/verbose` **默认仅限 CLI**,但可通过在 `config.yaml` 中设置 `display.tool_progress_command: true` 为消息平台启用。启用后,它会循环切换 `display.tool_progress` 模式并保存到配置。
- `/sethome`、`/update`、`/restart`、`/approve`、`/deny`、`/topic`、`/platform` 和 `/commands` 是**仅限消息平台**的命令。
- `/status`、`/version`、`/background`、`/queue`、`/steer`、`/voice`、`/reload-mcp`、`/reload-skills`、`/rollback`、`/debug`、`/fast`、`/footer`、`/curator`、`/kanban`、`/credits`、`/suggestions`、`/blueprint`、`/sessions` 和 `/yolo` 在 **CLI 和消息 gateway 中均可使用**。
- `/status`、`/version`、`/bg`、`/btw`、`/queue`、`/steer`、`/voice`、`/reload-mcp`、`/reload-skills`、`/rollback`、`/debug`、`/fast`、`/footer`、`/curator`、`/kanban`、`/credits`、`/suggestions`、`/blueprint`、`/sessions` 和 `/yolo` 在 **CLI 和消息 gateway 中均可使用**。
- `/voice join`、`/voice channel` 和 `/voice leave` 仅在 Discord 上有意义。
## 破坏性命令的确认提示
@@ -69,7 +69,7 @@ hermes -w -z "Fix issue #123" # 在 worktree 中以单次查询模式运行
| 上下文进度条 | 带颜色阈值编码的可视填充指示器 |
| 费用 | 预估会话费用(未知或零价格模型显示 `n/a`) |
| 🗜️ N | **上下文压缩次数**——当前运行会话被自动压缩的次数。首次压缩触发后显示。 |
| ▶ N | **活跃后台任务数**——当前会话中仍在运行的 `/background` prompt(提示词)数量。至少有一个任务进行中时显示。 |
| ▶ N | **活跃后台任务数**——当前会话中仍在运行的 `/bg` prompt(提示词)数量。至少有一个任务进行中时显示。 |
| 时长 | 会话已用时间 |
| ⚠ YOLO | **YOLO 模式警告**——当 `HERMES_YOLO_MODE` 开启时显示(通过启动时的 `hermes --yolo` 或会话中的 `/yolo` 切换)。与横幅行警告保持同步,确保你不会忘记自己处于自动批准模式。 |
@@ -122,7 +122,8 @@ hermes -w -z "Fix issue #123" # 在 worktree 中以单次查询模式运行
| `/model` | 显示或更改当前模型 |
| `/tools` | 列出当前可用工具 |
| `/skills browse` | 浏览 skill 中心和官方可选 skill |
| `/background <prompt>` | 在独立后台会话中运行一个 prompt |
| `/bg <prompt>` | 在独立后台会话中运行一个 prompt |
| `/btw <question>` | 在不打断当前对话的情况下,就当前对话提出顺带问题 |
| `/skin` | 显示或切换当前 CLI 皮肤 |
| `/voice on` | 启用 CLI 语音模式(按 `Ctrl+B` 录音) |
| `/voice tts` | 切换 Hermes 回复的语音播放 |
@@ -383,7 +384,7 @@ auxiliary:
在独立的后台会话中运行 prompt,同时继续使用 CLI 进行其他工作:
```
/background Analyze the logs in /var/log and summarize any errors from today
/bg Analyze the logs in /var/log and summarize any errors from today
```
Hermes 立即确认任务并将提示符还给你:
@@ -395,7 +396,7 @@ Hermes 立即确认任务并将提示符还给你:
### 工作原理
每个 `/background` prompt 会在守护线程中生成一个**完全独立的 agent 会话**:
每个 `/bg` prompt 会在守护线程中生成一个**完全独立的 agent 会话**:
- **隔离对话**——后台 agent 不了解当前会话的历史。它只接收你提供的 prompt。
- **相同配置**——后台 agent 继承当前会话的模型、提供商、工具集、推理设置和回退模型。
@@ -419,8 +420,8 @@ Hermes 立即确认任务并将提示符还给你:
### 使用场景
- **长时间研究**——"/background research the latest developments in quantum error correction",同时继续编写代码
- **文件处理**——"/background analyze all Python files in this repo and list any security issues",同时继续对话
- **长时间研究**——"/bg research the latest developments in quantum error correction",同时继续编写代码
- **文件处理**——"/bg analyze all Python files in this repo and list any security issues",同时继续对话
- **并行调查**——同时启动多个后台任务,从不同角度探索问题
:::info
@@ -624,7 +624,7 @@ Hermes 自动将已安装的技能注册为**原生 Discord 应用命令**。这
- 每个技能成为一个 Discord 斜杠命令(例如 `/code-review`、`/ascii-art`)
- 技能接受一个可选的 `args` 字符串参数
- Discord 每个机器人有 100 个应用命令的限制——如果你的技能数量超过可用槽位,多余的技能会被跳过并在日志中显示警告
- 技能在机器人启动时与内置命令(如 `/model`、`/reset` 和 `/background`)一起注册
- 技能在机器人启动时与内置命令(如 `/model`、`/reset` 和 `/bg`)一起注册
无需额外配置——通过 `hermes skills install` 安装的任何技能都会在下次网关重启时自动注册为 Discord 斜杠命令。
@@ -148,7 +148,8 @@ hermes gateway status --system # 仅 Linux:显式检查系统服务
| `/reasoning [level\|show\|hide]` | 更改推理强度或切换推理显示 |
| `/voice [on\|off\|tts\|join\|leave\|status]` | 控制消息语音回复和 Discord 语音频道行为 |
| `/rollback [number]` | 列出或恢复文件系统检查点 |
| `/background <prompt>` | 在独立后台会话中运行 prompt(提示词) |
| `/bg <prompt>` | 在独立后台会话中运行 prompt(提示词) |
| `/btw <question>` | 在不打断当前对话的情况下,就当前对话提出顺带问题 |
| `/reload-mcp` | 从配置重新加载 MCP 服务器 |
| `/update` | 将 Hermes Agent 更新至最新版本 |
| `/help` | 显示可用命令 |
@@ -317,7 +318,7 @@ display:
在独立的后台会话中运行 prompt,让 agent 独立处理,同时保持主聊天响应:
```
/background Check all servers in the cluster and report any that are down
/bg Check all servers in the cluster and report any that are down
```
Hermes 立即确认:
@@ -329,7 +330,7 @@ Hermes 立即确认:
### 工作原理
每个 `/background` prompt 会生成一个**独立的 agent 实例**异步运行:
每个 `/bg` prompt 会生成一个**独立的 agent 实例**异步运行:
- **隔离会话** — 后台 agent 拥有自己的会话和对话历史。它不了解你当前的聊天上下文,只接收你提供的 prompt。
- **相同配置** — 继承当前网关配置中的模型、提供商、工具集、推理设置和提供商路由。
@@ -361,10 +362,10 @@ HERMES_BACKGROUND_NOTIFICATIONS=result
### 使用场景
- **服务器监控** — "/background Check the health of all services and alert me if anything is down"
- **长时间构建** — "/background Build and deploy the staging environment",同时继续聊天
- **研究任务** — "/background Research competitor pricing and summarize in a table"
- **文件操作** — "/background Organize the photos in ~/Downloads by date into folders"
- **服务器监控** — "/bg Check the health of all services and alert me if anything is down"
- **长时间构建** — "/bg Build and deploy the staging environment",同时继续聊天
- **研究任务** — "/bg Research competitor pricing and summarize in a table"
- **文件操作** — "/bg Organize the photos in ~/Downloads by date into folders"
:::tip
消息平台上的后台任务是即发即忘的——你无需等待或主动查询。任务完成后,结果会自动出现在同一聊天中。
@@ -235,7 +235,7 @@ hermes slack manifest --write
### 旧版 `/hermes <子命令>` 仍然有效
为了向后兼容旧版 manifest,你仍然可以输入 `/hermes btw run the tests`——Hermes 会以与 `/btw run the tests` 相同的方式路由它。自由形式的问题也有效:`/hermes what's the weather?` 会被当作普通消息处理。
为了向后兼容旧版 manifest,你仍然可以输入 `/hermes bg run the tests`——Hermes 会以与 `/bg run the tests` 相同的方式路由它。自由形式的问题也有效:`/hermes what's the weather?` 会被当作普通消息处理。
### 在话题(thread)中使用命令(`!cmd` 前缀)
@@ -757,7 +757,7 @@ Hermes 会确认会话标题,并重放最后一条助手消息以提供上下
- 论坛启用私聊中的 General(置顶顶部)话题被视为根大厅,无论 Telegram 是以 `message_thread_id=1` 还是无 thread_id 投递其消息
- 根大厅提醒每个聊天每 30 秒限速一条——忘记话题模式已开启并在根目录输入十条 prompt 的用户不会收到十条回复
- BotFather 设置截图每个聊天每 5 分钟限速一次发送——在 Threads Settings 仍然禁用时重复尝试 `/topic` 不会重复上传同一张图片
- 在话题内启动的 `/background <prompt>` 会将结果投递回同一话题;后台会话不会触发所属话题的自动重命名
- 在话题内启动的 `/bg <prompt>` 会将结果投递回同一话题;后台会话不会触发所属话题的自动重命名
- `/topic` 本身受机器人用户授权检查限制——未授权的私聊会收到拒绝而非激活
### 禁用多会话模式
@@ -322,7 +322,7 @@ HERMES_LOG_LEVEL=debug hermes gateway
在不阻塞会话的情况下运行长时间操作:
```
/background Analyze all files in the archive
/bg Analyze all files in the archive
```
### 跨平台消息
@@ -260,7 +260,7 @@ hermes uninstall Uninstall Hermes
/stop Kill background processes
/rollback [N] Restore filesystem checkpoint
/snapshot [sub] Create or restore state snapshots of Hermes config/state (CLI)
/background <prompt> Run prompt in background
/bg <prompt> Run prompt in background
/queue <prompt> Queue for next turn
/steer <prompt> Inject a message after the next tool call without interrupting
/agents (/tasks) Show active agents and running tasks
@@ -172,7 +172,7 @@ TUI 的状态栏实时跟踪 agent 状态:
- **工作目录及 git 分支** — `~/projects/hermes-agent (docs/two-week-gap-sweep)`。在旁边的终端执行 `git checkout` 时,分支后缀会更新(mtime 缓存),TUI 反映的是实际活跃分支,而非启动时的分支。
- **每条 prompt 的耗时** — 轮次运行时显示 `⏱ 12s/3m 45s`(实时),轮次完成后冻结为 `⏲ 32s / 3m 45s`。第一个数字是自上次用户消息以来的时间;第二个是会话总时长。每次新 prompt 时重置。
- **`🗜️ N`** — 当前会话被自动压缩的次数。首次压缩触发后显示。
- **`▶ N`** — 当前会话中正在运行的 `/background` 任务数量。至少有一个任务在执行时显示。
- **`▶ N`** — 当前会话中正在运行的 `/bg` 任务数量。至少有一个任务在执行时显示。
- **`⚠ YOLO`** — 每当 YOLO 模式开启时(`hermes --yolo`、`/yolo` 或 `HERMES_YOLO_MODE=1`)显示的可见警告。同一徽章也出现在启动 banner 中,确保你不会在未注意到的情况下启动自动审批会话。
## 配置