mirror of
https://github.com/NousResearch/hermes-agent.git
synced 2026-07-31 19:16:29 +00:00
feat(surfaces): route busy-input corrections through active-turn redirect
The default `busy_input_mode: interrupt` now redirects the live turn instead of hard-stopping it and re-queuing a fresh turn, wired consistently across every first-party surface via the shared core primitive. - CLI, gateway (busy + PRIORITY paths), TUI (`_handle_busy_submit`), desktop (`session.redirect` RPC), and ACP call `redirect()` when the agent advertises `_supports_active_turn_redirect`, and fall back to the proven interrupt + next-turn queue for older runtimes. - Redirect is gated to plain text with no attachments: captioned or attachment-bearing events (including adapters that classify unknown media as `TEXT`) stay queued so media is never dropped. - ACP `cancel()` records the interrupted prompt, sets its cancel event, and hard-stops the agent while holding `runtime_lock`, closing the cancel-then-correct ordering gap; connection I/O happens after the lock is released. - Desktop appends the correction as a real user transcript message so the live view matches the durable history after reload. - `/busy` help, onboarding hints, and the new `session.redirect` RPC describe the redirect behavior; `/stop` remains the hard stop.
This commit is contained in:
parent
f6d2ee4afc
commit
34d0de80e6
15 changed files with 563 additions and 91 deletions
44
cli.py
44
cli.py
|
|
@ -3816,7 +3816,7 @@ class HermesCLI(CLIAgentSetupMixin, CLICommandsMixin, CLIBillingMixin):
|
|||
enabled=CLI_CONFIG["display"].get("persistent_output", True),
|
||||
max_lines=CLI_CONFIG["display"].get("persistent_output_max_lines", 200),
|
||||
)
|
||||
# busy_input_mode: "interrupt" (Enter interrupts current run),
|
||||
# busy_input_mode: "interrupt" (Enter redirects current run),
|
||||
# "queue" (Enter queues for next turn), or "steer" (Enter injects
|
||||
# mid-run via /steer, arriving after the next tool call).
|
||||
_bim = str(CLI_CONFIG["display"].get("busy_input_mode", "interrupt")).strip().lower()
|
||||
|
|
@ -13479,6 +13479,7 @@ class HermesCLI(CLIAgentSetupMixin, CLICommandsMixin, CLIBillingMixin):
|
|||
payload = (text, images) if images else text
|
||||
if self._agent_running and not (text and _looks_like_slash_command(text)):
|
||||
_effective_mode = self.busy_input_mode
|
||||
redirected = False
|
||||
if _effective_mode == "steer":
|
||||
# Route Enter through /steer — inject mid-run after the
|
||||
# next tool call. Images can't ride along (steer only
|
||||
|
|
@ -13506,15 +13507,35 @@ class HermesCLI(CLIAgentSetupMixin, CLICommandsMixin, CLIBillingMixin):
|
|||
preview = text if text else f"[{len(images)} image{'s' if len(images) != 1 else ''} attached]"
|
||||
_cprint(f" Queued for the next turn: {preview[:80]}{'...' if len(preview) > 80 else ''}")
|
||||
elif _effective_mode == "interrupt":
|
||||
self._interrupt_queue.put(payload)
|
||||
# Debug: log to file when message enters interrupt queue
|
||||
try:
|
||||
_dbg = _hermes_home / "interrupt_debug.log"
|
||||
with open(_dbg, "a", encoding="utf-8") as _f:
|
||||
_f.write(f"{time.strftime('%H:%M:%S')} ENTER: queued interrupt msg={str(payload)[:60]!r}, "
|
||||
f"agent_running={self._agent_running}\n")
|
||||
except Exception:
|
||||
pass
|
||||
if not images and text:
|
||||
try:
|
||||
if (
|
||||
self.agent is not None
|
||||
and getattr(
|
||||
self.agent,
|
||||
"_supports_active_turn_redirect",
|
||||
False,
|
||||
)
|
||||
is True
|
||||
and hasattr(self.agent, "redirect")
|
||||
):
|
||||
redirected = bool(self.agent.redirect(text))
|
||||
except Exception:
|
||||
redirected = False
|
||||
if redirected:
|
||||
preview = text[:80] + ("..." if len(text) > 80 else "")
|
||||
_cprint(f" {_ACCENT}↪ Redirected current turn: '{preview}'{_RST}")
|
||||
else:
|
||||
# Compatibility path for older agents, multimodal
|
||||
# follow-ups, or a turn that finished in the race.
|
||||
self._interrupt_queue.put(payload)
|
||||
try:
|
||||
_dbg = _hermes_home / "interrupt_debug.log"
|
||||
with open(_dbg, "a", encoding="utf-8") as _f:
|
||||
_f.write(f"{time.strftime('%H:%M:%S')} ENTER: queued interrupt msg={str(payload)[:60]!r}, "
|
||||
f"agent_running={self._agent_running}\n")
|
||||
except Exception:
|
||||
pass
|
||||
# First-touch onboarding: on the very first busy-while-running
|
||||
# event for this install, print a one-line tip explaining the
|
||||
# /busy knob. Flag persists to config.yaml and never fires
|
||||
|
|
@ -13528,7 +13549,8 @@ class HermesCLI(CLIAgentSetupMixin, CLICommandsMixin, CLIBillingMixin):
|
|||
mark_seen,
|
||||
)
|
||||
if not is_seen(CLI_CONFIG, BUSY_INPUT_FLAG):
|
||||
_cprint(f" {_DIM}{busy_input_hint_cli(self.busy_input_mode)}{_RST}")
|
||||
_hint_mode = "redirect" if redirected else _effective_mode
|
||||
_cprint(f" {_DIM}{busy_input_hint_cli(_hint_mode)}{_RST}")
|
||||
mark_seen(_hermes_home / "config.yaml", BUSY_INPUT_FLAG)
|
||||
CLI_CONFIG.setdefault("onboarding", {}).setdefault("seen", {})[BUSY_INPUT_FLAG] = True
|
||||
except Exception:
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue