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:
@@ -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',
|
||||
|
||||
@@ -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
|
||||
):
|
||||
|
||||
@@ -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",
|
||||
|
||||
@@ -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",
|
||||
})
|
||||
|
||||
|
||||
@@ -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
@@ -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:
|
||||
|
||||
@@ -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)."""
|
||||
|
||||
@@ -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
@@ -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
@@ -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
@@ -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
@@ -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
@@ -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
@@ -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
@@ -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
@@ -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
@@ -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
@@ -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
@@ -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
@@ -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
@@ -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
@@ -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
@@ -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
@@ -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
@@ -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: "Немає розмови для розгалуження — спочатку надішліть повідомлення."
|
||||
|
||||
@@ -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
@@ -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: "没有可分支的对话 — 请先发送一条消息。"
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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()
|
||||
@@ -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
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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.
|
||||
|
||||
@@ -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",
|
||||
|
||||
@@ -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
|
||||
|
||||
|
||||
@@ -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)
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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',
|
||||
|
||||
@@ -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' }
|
||||
|
||||
@@ -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. |
|
||||
|
||||
@@ -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.
|
||||
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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.
|
||||
|
||||
|
||||
@@ -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.
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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
|
||||
|
||||
+5
-3
@@ -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
|
||||
|
||||
+1
-1
@@ -624,7 +624,7 @@ Hermes 自动将已安装的技能注册为**原生 Discord 应用命令**。这
|
||||
- 每个技能成为一个 Discord 斜杠命令(例如 `/code-review`、`/ascii-art`)
|
||||
- 技能接受一个可选的 `args` 字符串参数
|
||||
- Discord 每个机器人有 100 个应用命令的限制——如果你的技能数量超过可用槽位,多余的技能会被跳过并在日志中显示警告
|
||||
- 技能在机器人启动时与内置命令(如 `/model`、`/reset` 和 `/background`)一起注册
|
||||
- 技能在机器人启动时与内置命令(如 `/model`、`/reset` 和 `/bg`)一起注册
|
||||
|
||||
无需额外配置——通过 `hermes skills install` 安装的任何技能都会在下次网关重启时自动注册为 Discord 斜杠命令。
|
||||
|
||||
|
||||
+8
-7
@@ -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
|
||||
消息平台上的后台任务是即发即忘的——你无需等待或主动查询。任务完成后,结果会自动出现在同一聊天中。
|
||||
|
||||
+1
-1
@@ -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` 前缀)
|
||||
|
||||
|
||||
+1
-1
@@ -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` 本身受机器人用户授权检查限制——未授权的私聊会收到拒绝而非激活
|
||||
|
||||
### 禁用多会话模式
|
||||
|
||||
+1
-1
@@ -322,7 +322,7 @@ HERMES_LOG_LEVEL=debug hermes gateway
|
||||
在不阻塞会话的情况下运行长时间操作:
|
||||
|
||||
```
|
||||
/background Analyze all files in the archive
|
||||
/bg Analyze all files in the archive
|
||||
```
|
||||
|
||||
### 跨平台消息
|
||||
|
||||
+1
-1
@@ -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 中,确保你不会在未注意到的情况下启动自动审批会话。
|
||||
|
||||
## 配置
|
||||
|
||||
Reference in New Issue
Block a user