mirror of
https://github.com/NousResearch/hermes-agent.git
synced 2026-04-25 00:51:20 +00:00
feat(steer): /steer <prompt> injects a mid-run note after the next tool call (#12116)
* feat(steer): /steer <prompt> injects a mid-run note after the next tool call Adds a new slash command that sits between /queue (turn boundary) and interrupt. /steer <text> stashes the message on the running agent and the agent loop appends it to the LAST tool result's content once the current tool batch finishes. The model sees it as part of the tool output on its next iteration. No interrupt is fired, no new user turn is inserted, and no prompt cache invalidation happens beyond the normal per-turn tool-result churn. Message-role alternation is preserved — we only modify an existing role:"tool" message's content. Wiring ------ - hermes_cli/commands.py: register /steer + add to ACTIVE_SESSION_BYPASS_COMMANDS. - run_agent.py: add _pending_steer state, AIAgent.steer(), _drain_pending_steer(), _apply_pending_steer_to_tool_results(); drain at end of both parallel and sequential tool executors; clear on interrupt; return leftover as result['pending_steer'] if the agent exits before another tool batch. - cli.py: /steer handler — route to agent.steer() when running, fall back to the regular queue otherwise; deliver result['pending_steer'] as next turn. - gateway/run.py: running-agent intercept calls running_agent.steer(); idle-agent path strips the prefix and forwards as a regular user message. - tui_gateway/server.py: new session.steer JSON-RPC method. - ui-tui: SessionSteerResponse type + local /steer slash command that calls session.steer when ui.busy, otherwise enqueues for the next turn. Fallbacks --------- - Agent exits mid-steer → surfaces in run_conversation result as pending_steer so CLI/gateway deliver it as the next user turn instead of silently dropping it. - All tools skipped after interrupt → re-stashes pending_steer for the caller. - No active agent → /steer reduces to sending the text as a normal message. Tests ----- - tests/run_agent/test_steer.py — accept/reject, concatenation, drain, last-tool-result injection, multimodal list content, thread safety, cleared-on-interrupt, registry membership, bypass-set membership. - tests/gateway/test_steer_command.py — running agent, pending sentinel, missing steer() method, rejected payload, empty payload. - tests/gateway/test_command_bypass_active_session.py — /steer bypasses the Level-1 base adapter guard. - tests/test_tui_gateway_server.py — session.steer RPC paths. 72/72 targeted tests pass under scripts/run_tests.sh. * feat(steer): register /steer in Discord's native slash tree Discord's app_commands tree is a curated subset of slash commands (not derived from COMMAND_REGISTRY like Telegram/Slack). /steer already works there as plain text (routes through handle_message → base adapter bypass → runner), but registering it here adds Discord's native autocomplete + argument hint UI so users can discover and type it like any other first-class command.
This commit is contained in:
parent
f9667331e5
commit
2edebedc9e
12 changed files with 826 additions and 2 deletions
|
|
@ -1340,6 +1340,31 @@ def _(rid, params: dict) -> dict:
|
|||
return _ok(rid, {"status": "interrupted"})
|
||||
|
||||
|
||||
@method("session.steer")
|
||||
def _(rid, params: dict) -> dict:
|
||||
"""Inject a user message into the next tool result without interrupting.
|
||||
|
||||
Mirrors AIAgent.steer(). Safe to call while a turn is running — the text
|
||||
lands on the last tool result of the next tool batch and the model sees
|
||||
it on its next iteration. No interrupt, no new user turn, no role
|
||||
alternation violation.
|
||||
"""
|
||||
text = (params.get("text") or "").strip()
|
||||
if not text:
|
||||
return _err(rid, 4002, "text is required")
|
||||
session, err = _sess_nowait(params, rid)
|
||||
if err:
|
||||
return err
|
||||
agent = session.get("agent")
|
||||
if agent is None or not hasattr(agent, "steer"):
|
||||
return _err(rid, 4010, "agent does not support steer")
|
||||
try:
|
||||
accepted = agent.steer(text)
|
||||
except Exception as exc:
|
||||
return _err(rid, 5000, f"steer failed: {exc}")
|
||||
return _ok(rid, {"status": "queued" if accepted else "rejected", "text": text})
|
||||
|
||||
|
||||
@method("terminal.resize")
|
||||
def _(rid, params: dict) -> dict:
|
||||
session, err = _sess_nowait(params, rid)
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue