Skip to content

feat: add ephemeral /btw command for side questions#3504

Closed
areu01or00 wants to merge 1 commit intoNousResearch:mainfrom
areu01or00:feat/btw-ephemeral-side-questions
Closed

feat: add ephemeral /btw command for side questions#3504
areu01or00 wants to merge 1 commit intoNousResearch:mainfrom
areu01or00:feat/btw-ephemeral-side-questions

Conversation

@areu01or00
Copy link
Copy Markdown
Contributor

What does this PR do?

Adds an ephemeral /btw side-question command to Hermes.

/btw lets users ask a quick follow-up without interrupting the main session. It snapshots the current session context, answers with a separate no-tools helper agent, and does not persist the /btw exchange back into the main session
history.

This is implemented for both the interactive CLI and the messaging gateway.

Related Issue

No tracked issue.

Type of Change

  • 🐛 Bug fix (non-breaking change that fixes an issue)
  • ✨ New feature (non-breaking change that adds functionality)
  • 🔒 Security fix
  • 📝 Documentation update
  • ✅ Tests (adding or improving test coverage)
  • ♻️ Refactor (no behavior change)
  • 🎯 New skill (bundled or hub)

Changes Made

  • Added /btw to the central slash-command registry
  • Implemented CLI /btw as an ephemeral side-question flow that runs in parallel without interrupting the main session
  • Added a persist_session switch to AIAgent so ephemeral helper flows can avoid writing session logs and DB history
  • Implemented gateway /btw support for same-chat/thread replies
  • Added a per-session gateway guard so only one /btw runs at a time per chat/session
  • Updated slash-command reference docs
  • Added CLI and gateway tests for /btw

How to Test

  1. Run the targeted test suites:
    python -m pytest tests/test_cli_btw_command.py
    python -m pytest tests/gateway/test_btw_command.py
    python -m pytest tests/hermes_cli/test_commands.py
  2. In the CLI, start a task and run /btw .
    Verify the /btw reply appears separately and does not interrupt the main task.
  3. In a messaging chat/thread, send /btw .
    Verify the reply appears in the same chat/thread, does not interrupt the main run, and does not persist into main session history.

Checklist

Code

Documentation & Housekeeping

  • I've updated relevant documentation (README, docs/, docstrings) — or N/A
  • I've updated cli-config.yaml.example if I added/changed config keys — or N/A
  • I've updated CONTRIBUTING.md or AGENTS.md if I changed architecture or workflows — or N/A
  • I've considered cross-platform impact (Windows, macOS) per the compatibility guide (https://github.com/NousResearch/hermes-agent/blob/main/CONTRIBUTING.md#cross-platform-compatibility) — or N/A
  • I've updated tool descriptions/schemas if I changed tool behavior — or N/A

Screenshots / Logs

Targeted test results:

  • tests/test_cli_btw_command.py: passed
  • tests/gateway/test_btw_command.py: passed
  • tests/hermes_cli/test_commands.py: passed

@ticketclosed-wontfix
Copy link
Copy Markdown
Contributor

Reviewing this because I genuinely want this feature — ephemeral side questions is something I've wanted for a while. So this is coming from a place of "let's get this mergeable."

The ephemeral isolation design is solid — session_db=None, skip_memory=True, persist_session=False is the right way to keep /btw out of the main session. The per-session guard with add_done_callback cleanup is clean too. Good stuff.

Some issues worth addressing though:

Critical

gateway/run.py ~3884 — self._reasoning_config = reasoning_config inside _run_btw_task writes to the shared GatewayRunner singleton. This runs concurrently with the main session agent, so you can stomp the reasoning config mid-turn. Either pass it as a parameter to _resolve_turn_agent_config or make the btw path fully self-contained.

High

  • gateway/run.py ~3897 — list(getattr(running_agent, "_session_messages", []) or []) reads from a list the main agent is actively mutating from a thread pool executor. list() on a concurrently-modified list can produce a partial copy or raise RuntimeError. Safer to just fall back to session_store.load_transcript when an agent is active.

  • Both CLI and gateway pass the full unbounded conversation history to the btw agent. On long sessions this will blow through token limits for what's supposed to be a quick side question. Worth truncating to the last ~30 messages.

  • gateway/run.py ~3910 — **turn_route["runtime"] unpacking can collide with the explicit kwargs below it (e.g. reasoning_config, session_id). The CLI version correctly uses .get() for each key — mirror that pattern here.

  • tests/gateway/test_btw_command.py:116-130fake_loop.run_in_executor is mocked to return the result directly, so run_sync() never executes and the AIAgent patch is dead code. This test doesn't actually verify the agent gets the right params or that run_conversation is called.

Medium

  • asyncio.get_event_loop() at line ~3930 — deprecated in 3.10+, will raise in some 3.12+ contexts. Should be asyncio.get_running_loop() (there's already a correct usage elsewhere in this file).

  • max_iterations=8 (gateway) / max(1, min(self.max_turns, 8)) (CLI) — a no-tools agent completes in exactly 1 LLM call. Setting this to 1 avoids wasting tokens on retry loops that can't accomplish anything.

  • enabled_toolsets=["__btw_no_tools__"] works by accident — unknown toolset names silently resolve to no tools. If toolset resolution ever starts validating names, this breaks. enabled_toolsets=[] would be a more explicit contract.

  • TOCTOU on self._app in the background thread (cli.py ~4174, ~4198) — app can become None between the check and the call if the user exits. Capture to a local first.

  • When adapter is None in _run_btw_task, it logs a warning and returns silently. The user gets no feedback. Better to check adapter presence in _handle_btw_command and return an error message before spawning the task.

Nit

import time as _tmod appears twice inside run_btw() (success and error branches). Just import it once at the top of the function.


Good enough to ship after addressing the critical and high items. The rest can be follow-ups.

teknium1 added a commit that referenced this pull request Mar 31, 2026
Adds /btw <question> — ask a quick follow-up using the current
session context without interrupting the main conversation.

- Snapshots conversation history, answers with a no-tools agent
- Response is not persisted to session history or DB
- Runs in a background thread (CLI) / async task (gateway)
- Per-session guard prevents concurrent /btw in gateway

Implementation:
- model_tools.py: enabled_toolsets=[] now correctly means "no tools"
  (was falsy, fell through to default "all tools")
- run_agent.py: persist_session=False gates _persist_session()
- cli.py: _handle_btw_command (background thread, Rich panel output)
- gateway/run.py: _handle_btw_command + _run_btw_task (async task)
- hermes_cli/commands.py: CommandDef for "btw"

Inspired by PR #3504 by areu01or00, reimplemented cleanly on current
main with the enabled_toolsets=[] fix and without the __btw_no_tools__
hack.
@teknium1
Copy link
Copy Markdown
Contributor

Merged via #4161 — reimplemented cleanly on current main. Your design was the right approach (snapshot history, no-tools agent, persist_session flag). Key changes: fixed enabled_toolsets=[] to actually mean 'no tools' instead of the btw_no_tools hack, added skip_context_files, and cleaned up the gateway code. Thanks @areu01or00!

@teknium1 teknium1 closed this Mar 31, 2026
teknium1 added a commit that referenced this pull request Mar 31, 2026
Adds /btw <question> — ask a quick follow-up using the current
session context without interrupting the main conversation.

- Snapshots conversation history, answers with a no-tools agent
- Response is not persisted to session history or DB
- Runs in a background thread (CLI) / async task (gateway)
- Per-session guard prevents concurrent /btw in gateway

Implementation:
- model_tools.py: enabled_toolsets=[] now correctly means "no tools"
  (was falsy, fell through to default "all tools")
- run_agent.py: persist_session=False gates _persist_session()
- cli.py: _handle_btw_command (background thread, Rich panel output)
- gateway/run.py: _handle_btw_command + _run_btw_task (async task)
- hermes_cli/commands.py: CommandDef for "btw"

Inspired by PR #3504 by areu01or00, reimplemented cleanly on current
main with the enabled_toolsets=[] fix and without the __btw_no_tools__
hack.
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

3 participants