mirror of
https://github.com/NousResearch/hermes-agent.git
synced 2026-07-08 13:12:08 +00:00
feat: make busy steer ack configurable
This commit is contained in:
parent
46fbd73f66
commit
12f03b11ff
4 changed files with 96 additions and 1 deletions
|
|
@ -47,6 +47,11 @@ _GLOBAL_DEFAULTS: dict[str, Any] = {
|
|||
"interim_assistant_messages": True,
|
||||
"long_running_notifications": True,
|
||||
"busy_ack_detail": True,
|
||||
# Whether busy_input_mode=steer sends a visible "Steered into current run"
|
||||
# acknowledgment after successfully injecting the user's mid-turn message.
|
||||
# Disable when the platform should steer silently (the text still lands in
|
||||
# the active run; only the confirmation echo is suppressed).
|
||||
"busy_steer_ack_enabled": True,
|
||||
# When true, delete tool-progress / "⏳ Working — N min" / status bubbles
|
||||
# after the final response lands on platforms that support message
|
||||
# deletion (e.g. Telegram). Off by default — progress is still shown
|
||||
|
|
|
|||
|
|
@ -1563,6 +1563,8 @@ if _config_path.exists():
|
|||
os.environ["HERMES_GATEWAY_BUSY_TEXT_MODE"] = str(_display_cfg["busy_text_mode"])
|
||||
if "busy_ack_enabled" in _display_cfg:
|
||||
os.environ["HERMES_GATEWAY_BUSY_ACK_ENABLED"] = str(_display_cfg["busy_ack_enabled"])
|
||||
if "busy_steer_ack_enabled" in _display_cfg:
|
||||
os.environ["HERMES_GATEWAY_BUSY_STEER_ACK_ENABLED"] = str(_display_cfg["busy_steer_ack_enabled"])
|
||||
# Timezone: bridge config.yaml → HERMES_TIMEZONE env var.
|
||||
_tz_cfg = _cfg.get("timezone", "")
|
||||
if _tz_cfg and isinstance(_tz_cfg, str):
|
||||
|
|
@ -5330,6 +5332,30 @@ class GatewayRunner(GatewayAuthorizationMixin, GatewayKanbanWatchersMixin, Gatew
|
|||
logger.debug("Busy ack suppressed for session %s", session_key)
|
||||
return True # input still processed, just no ack sent
|
||||
|
||||
from gateway.display_config import resolve_display_setting
|
||||
platform_key = _platform_config_key(event.source.platform)
|
||||
|
||||
# In steer mode the user's text has already been injected into the
|
||||
# active run. Some mobile chat setups want that steering to be silent,
|
||||
# like STT transcript echo suppression: keep the behavior, drop only
|
||||
# the confirmation bubble.
|
||||
if is_steer_mode:
|
||||
steer_ack_env = os.environ.get("HERMES_GATEWAY_BUSY_STEER_ACK_ENABLED")
|
||||
if steer_ack_env is not None:
|
||||
steer_ack_enabled = steer_ack_env.strip().lower() in {"1", "true", "yes", "on"}
|
||||
else:
|
||||
steer_ack_enabled = bool(
|
||||
resolve_display_setting(
|
||||
_load_gateway_config(),
|
||||
platform_key,
|
||||
"busy_steer_ack_enabled",
|
||||
True,
|
||||
)
|
||||
)
|
||||
if not steer_ack_enabled:
|
||||
logger.debug("Busy steer ack suppressed for session %s", session_key)
|
||||
return True
|
||||
|
||||
# Debounce: only send an acknowledgment once every 30 seconds per session
|
||||
# to avoid spamming the user when they send multiple messages quickly
|
||||
_BUSY_ACK_COOLDOWN = 30
|
||||
|
|
@ -5343,7 +5369,6 @@ class GatewayRunner(GatewayAuthorizationMixin, GatewayKanbanWatchersMixin, Gatew
|
|||
# Build a status-rich acknowledgment. Mobile chat defaults keep this
|
||||
# terse; detailed iteration/tool state is still available in logs and
|
||||
# can be opted in per platform via display.platforms.<platform>.busy_ack_detail.
|
||||
from gateway.display_config import resolve_display_setting
|
||||
status_parts = []
|
||||
busy_ack_detail_enabled = bool(
|
||||
resolve_display_setting(
|
||||
|
|
|
|||
|
|
@ -1675,6 +1675,10 @@ DEFAULT_CONFIG = {
|
|||
# behavior of showing tool-call summaries inline.
|
||||
"resume_skip_tool_only": True,
|
||||
"busy_input_mode": "interrupt", # interrupt | queue | steer
|
||||
# When busy_input_mode="steer", suppress only the visible
|
||||
# "Steered into current run" confirmation bubble by setting this false.
|
||||
# The mid-turn steering itself still happens.
|
||||
"busy_steer_ack_enabled": True,
|
||||
# Which interface bare `hermes` (and `hermes chat`) launches by default:
|
||||
# "cli" — the classic prompt_toolkit REPL (default, preserves prior behavior)
|
||||
# "tui" — the modern Ink TUI (same as passing `--tui`)
|
||||
|
|
|
|||
|
|
@ -297,6 +297,67 @@ class TestBusySessionAck:
|
|||
assert "Steered" in content or "steer" in content.lower()
|
||||
assert "Interrupting" not in content
|
||||
|
||||
@pytest.mark.asyncio
|
||||
async def test_steer_mode_can_suppress_visible_ack_without_disabling_steer(self, monkeypatch):
|
||||
"""busy_steer_ack_enabled=false keeps steering but drops the echo bubble."""
|
||||
import gateway.run as _gr
|
||||
|
||||
monkeypatch.delenv("HERMES_GATEWAY_BUSY_STEER_ACK_ENABLED", raising=False)
|
||||
monkeypatch.setattr(
|
||||
_gr,
|
||||
"_load_gateway_config",
|
||||
lambda: {"display": {"platforms": {"telegram": {"busy_steer_ack_enabled": False}}}},
|
||||
)
|
||||
|
||||
runner, sentinel = _make_runner()
|
||||
runner._busy_input_mode = "steer"
|
||||
adapter = _make_adapter()
|
||||
|
||||
event = _make_event(text="also check the tests")
|
||||
sk = build_session_key(event.source)
|
||||
runner.adapters[event.source.platform] = adapter
|
||||
|
||||
agent = MagicMock()
|
||||
agent.steer = MagicMock(return_value=True)
|
||||
runner._running_agents[sk] = agent
|
||||
|
||||
await runner._handle_active_session_busy_message(event, sk)
|
||||
|
||||
agent.steer.assert_called_once_with("also check the tests")
|
||||
agent.interrupt.assert_not_called()
|
||||
adapter._send_with_retry.assert_not_called()
|
||||
assert sk not in adapter._pending_messages
|
||||
|
||||
@pytest.mark.asyncio
|
||||
async def test_steer_ack_env_override_can_suppress_visible_ack(self, monkeypatch):
|
||||
"""Env override supports process-level suppression for gateway services."""
|
||||
import gateway.run as _gr
|
||||
|
||||
monkeypatch.setenv("HERMES_GATEWAY_BUSY_STEER_ACK_ENABLED", "false")
|
||||
monkeypatch.setattr(
|
||||
_gr,
|
||||
"_load_gateway_config",
|
||||
lambda: {"display": {"platforms": {"telegram": {"busy_steer_ack_enabled": True}}}},
|
||||
)
|
||||
|
||||
runner, sentinel = _make_runner()
|
||||
runner._busy_input_mode = "steer"
|
||||
adapter = _make_adapter()
|
||||
|
||||
event = _make_event(text="steer silently")
|
||||
sk = build_session_key(event.source)
|
||||
runner.adapters[event.source.platform] = adapter
|
||||
|
||||
agent = MagicMock()
|
||||
agent.steer = MagicMock(return_value=True)
|
||||
runner._running_agents[sk] = agent
|
||||
|
||||
await runner._handle_active_session_busy_message(event, sk)
|
||||
|
||||
agent.steer.assert_called_once_with("steer silently")
|
||||
adapter._send_with_retry.assert_not_called()
|
||||
assert sk not in adapter._pending_messages
|
||||
|
||||
@pytest.mark.asyncio
|
||||
async def test_steer_mode_falls_back_to_queue_when_agent_rejects(self):
|
||||
"""If agent.steer() returns False, fall back to queue behavior."""
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue