mirror of
https://github.com/NousResearch/hermes-agent.git
synced 2026-05-29 06:31:32 +00:00
gateway: quiet Telegram operational chatter
This commit is contained in:
parent
efa952531b
commit
60f84c6c28
7 changed files with 177 additions and 18 deletions
|
|
@ -916,6 +916,11 @@ display:
|
|||
# Toggle at runtime with /verbose in the CLI
|
||||
tool_progress: all
|
||||
|
||||
# Per-platform defaults can be quieter than the global setting. Telegram
|
||||
# defaults to final-answer-first on mobile: tool progress, interim assistant
|
||||
# updates, long-running "Still working..." heartbeats, and detailed busy acks
|
||||
# are off unless re-enabled under display.platforms.telegram.
|
||||
|
||||
# Auto-cleanup of temporary progress bubbles after the final response lands.
|
||||
# On platforms that support message deletion (currently Telegram), this
|
||||
# removes the tool-progress bubble, "⏳ Still working..." notices, and
|
||||
|
|
@ -939,6 +944,16 @@ display:
|
|||
# false: Only send the final response
|
||||
interim_assistant_messages: true
|
||||
|
||||
# Gateway-only long-running status heartbeats.
|
||||
# When false, the platform does not receive periodic "Still working..."
|
||||
# notifications even if agent.gateway_notify_interval is non-zero.
|
||||
# Telegram default: false. Other high-capability chat platforms default true.
|
||||
long_running_notifications: true
|
||||
|
||||
# Include detailed iteration/tool/status context in busy acknowledgments when
|
||||
# a new user message arrives while a run is active. Telegram default: false.
|
||||
busy_ack_detail: true
|
||||
|
||||
# What Enter does when Hermes is already busy (CLI and gateway platforms).
|
||||
# interrupt: Interrupt the current run and redirect Hermes (default)
|
||||
# queue: Queue your message for the next turn
|
||||
|
|
|
|||
|
|
@ -35,6 +35,11 @@ _GLOBAL_DEFAULTS: dict[str, Any] = {
|
|||
"show_reasoning": False,
|
||||
"tool_preview_length": 0,
|
||||
"streaming": None, # None = follow top-level streaming config
|
||||
# Gateway-only assistant/status chatter controls. These default on for
|
||||
# back-compat, but mobile platforms can opt down to final-answer-first.
|
||||
"interim_assistant_messages": True,
|
||||
"long_running_notifications": True,
|
||||
"busy_ack_detail": True,
|
||||
# When true, delete tool-progress / "Still working..." / status bubbles
|
||||
# after the final response lands on platforms that support message
|
||||
# deletion (e.g. Telegram). Off by default — progress is still shown
|
||||
|
|
@ -56,6 +61,9 @@ _TIER_HIGH = {
|
|||
"show_reasoning": False,
|
||||
"tool_preview_length": 40,
|
||||
"streaming": None, # follow global
|
||||
"interim_assistant_messages": True,
|
||||
"long_running_notifications": True,
|
||||
"busy_ack_detail": True,
|
||||
}
|
||||
|
||||
_TIER_MEDIUM = {
|
||||
|
|
@ -63,6 +71,9 @@ _TIER_MEDIUM = {
|
|||
"show_reasoning": False,
|
||||
"tool_preview_length": 40,
|
||||
"streaming": None,
|
||||
"interim_assistant_messages": True,
|
||||
"long_running_notifications": True,
|
||||
"busy_ack_detail": True,
|
||||
}
|
||||
|
||||
_TIER_LOW = {
|
||||
|
|
@ -70,6 +81,9 @@ _TIER_LOW = {
|
|||
"show_reasoning": False,
|
||||
"tool_preview_length": 40,
|
||||
"streaming": False,
|
||||
"interim_assistant_messages": False,
|
||||
"long_running_notifications": False,
|
||||
"busy_ack_detail": False,
|
||||
}
|
||||
|
||||
_TIER_MINIMAL = {
|
||||
|
|
@ -77,11 +91,23 @@ _TIER_MINIMAL = {
|
|||
"show_reasoning": False,
|
||||
"tool_preview_length": 0,
|
||||
"streaming": False,
|
||||
"interim_assistant_messages": False,
|
||||
"long_running_notifications": False,
|
||||
"busy_ack_detail": False,
|
||||
}
|
||||
|
||||
_PLATFORM_DEFAULTS: dict[str, dict[str, Any]] = {
|
||||
# Tier 1 — full edit support, personal/team use
|
||||
"telegram": {**_TIER_HIGH, "tool_progress": "new"},
|
||||
# Telegram is usually a mobile inbox: default to final-answer-first and
|
||||
# avoid permanent operational breadcrumbs unless users opt back in with
|
||||
# display.platforms.telegram.tool_progress / long_running_notifications.
|
||||
"telegram": {
|
||||
**_TIER_HIGH,
|
||||
"tool_progress": "off",
|
||||
"interim_assistant_messages": False,
|
||||
"long_running_notifications": False,
|
||||
"busy_ack_detail": False,
|
||||
},
|
||||
"discord": _TIER_HIGH,
|
||||
|
||||
# Tier 2 — edit support, often customer/workspace channels
|
||||
|
|
@ -190,7 +216,13 @@ def _normalise(setting: str, value: Any) -> Any:
|
|||
if value is True:
|
||||
return "all"
|
||||
return str(value).lower()
|
||||
if setting in {"show_reasoning", "streaming"}:
|
||||
if setting in {
|
||||
"show_reasoning",
|
||||
"streaming",
|
||||
"interim_assistant_messages",
|
||||
"long_running_notifications",
|
||||
"busy_ack_detail",
|
||||
}:
|
||||
if isinstance(value, str):
|
||||
return value.lower() in {"true", "1", "yes", "on"}
|
||||
return bool(value)
|
||||
|
|
|
|||
|
|
@ -3223,9 +3223,22 @@ class GatewayRunner:
|
|||
|
||||
self._busy_ack_ts[session_key] = now
|
||||
|
||||
# Build a status-rich acknowledgment
|
||||
# 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.
|
||||
status_parts = []
|
||||
if running_agent and running_agent is not _AGENT_PENDING_SENTINEL:
|
||||
busy_ack_detail_enabled = True
|
||||
try:
|
||||
from gateway.display_config import resolve_display_setting as _resolve_display_setting
|
||||
_user_cfg = _load_gateway_config()
|
||||
_platform_key = _platform_config_key(event.source.platform)
|
||||
busy_ack_detail_enabled = bool(
|
||||
_resolve_display_setting(_user_cfg, _platform_key, "busy_ack_detail", True)
|
||||
)
|
||||
except Exception:
|
||||
busy_ack_detail_enabled = True
|
||||
|
||||
if busy_ack_detail_enabled and running_agent and running_agent is not _AGENT_PENDING_SENTINEL:
|
||||
try:
|
||||
summary = running_agent.get_activity_summary()
|
||||
iteration = summary.get("api_call_count", 0)
|
||||
|
|
@ -15874,9 +15887,13 @@ class GatewayRunner:
|
|||
# in chat platforms while opting into concise mid-turn updates.
|
||||
interim_assistant_messages_enabled = (
|
||||
source.platform != Platform.WEBHOOK
|
||||
and is_truthy_value(
|
||||
display_config.get("interim_assistant_messages"),
|
||||
default=True,
|
||||
and bool(
|
||||
resolve_display_setting(
|
||||
user_config,
|
||||
platform_key,
|
||||
"interim_assistant_messages",
|
||||
True,
|
||||
)
|
||||
)
|
||||
)
|
||||
|
||||
|
|
@ -17413,6 +17430,19 @@ class GatewayRunner:
|
|||
# 0 = disable notifications.
|
||||
_NOTIFY_INTERVAL_RAW = _float_env("HERMES_AGENT_NOTIFY_INTERVAL", 180)
|
||||
_NOTIFY_INTERVAL = _NOTIFY_INTERVAL_RAW if _NOTIFY_INTERVAL_RAW > 0 else None
|
||||
try:
|
||||
_notify_enabled = bool(
|
||||
resolve_display_setting(
|
||||
user_config,
|
||||
platform_key,
|
||||
"long_running_notifications",
|
||||
True,
|
||||
)
|
||||
)
|
||||
except Exception:
|
||||
_notify_enabled = True
|
||||
if not _notify_enabled:
|
||||
_NOTIFY_INTERVAL = None
|
||||
_notify_start = time.time()
|
||||
|
||||
async def _notify_long_running():
|
||||
|
|
|
|||
|
|
@ -378,8 +378,15 @@ class TestBusySessionAck:
|
|||
assert adapter._send_with_retry.call_count == 2
|
||||
|
||||
@pytest.mark.asyncio
|
||||
async def test_includes_status_detail(self):
|
||||
async def test_includes_status_detail_when_opted_in(self, monkeypatch):
|
||||
"""Ack message should include iteration and tool info when available."""
|
||||
import gateway.run as _gr
|
||||
|
||||
monkeypatch.setattr(
|
||||
_gr,
|
||||
"_load_gateway_config",
|
||||
lambda: {"display": {"platforms": {"telegram": {"busy_ack_detail": True}}}},
|
||||
)
|
||||
runner, sentinel = _make_runner()
|
||||
runner._busy_input_mode = "interrupt"
|
||||
adapter = _make_adapter()
|
||||
|
|
@ -408,6 +415,37 @@ class TestBusySessionAck:
|
|||
assert "terminal" in content # current tool
|
||||
assert "10 min" in content # elapsed
|
||||
|
||||
@pytest.mark.asyncio
|
||||
async def test_telegram_omits_status_detail_by_default(self):
|
||||
"""Telegram busy acks stay concise unless busy_ack_detail is enabled."""
|
||||
runner, sentinel = _make_runner()
|
||||
runner._busy_input_mode = "interrupt"
|
||||
adapter = _make_adapter()
|
||||
|
||||
event = _make_event(text="yo")
|
||||
sk = build_session_key(event.source)
|
||||
|
||||
agent = MagicMock()
|
||||
agent.get_activity_summary.return_value = {
|
||||
"api_call_count": 21,
|
||||
"max_iterations": 60,
|
||||
"current_tool": "terminal",
|
||||
"last_activity_ts": time.time(),
|
||||
"last_activity_desc": "terminal",
|
||||
"seconds_since_activity": 0.5,
|
||||
}
|
||||
runner._running_agents[sk] = agent
|
||||
runner._running_agents_ts[sk] = time.time() - 600
|
||||
runner.adapters[event.source.platform] = adapter
|
||||
|
||||
await runner._handle_active_session_busy_message(event, sk)
|
||||
|
||||
content = adapter._send_with_retry.call_args.kwargs.get("content", "")
|
||||
assert "Interrupting current task" in content
|
||||
assert "21/60" not in content
|
||||
assert "terminal" not in content
|
||||
assert "10 min" not in content
|
||||
|
||||
@pytest.mark.asyncio
|
||||
async def test_draining_still_works(self):
|
||||
"""Draining case should still produce the drain-specific message."""
|
||||
|
|
|
|||
|
|
@ -41,9 +41,9 @@ class TestResolveDisplaySetting:
|
|||
|
||||
# Empty config — should get built-in defaults
|
||||
config = {}
|
||||
# Telegram tier_high override: "new" (not "all") to reduce edit
|
||||
# pressure during streaming on Telegram's ~1 edit/s flood envelope.
|
||||
assert resolve_display_setting(config, "telegram", "tool_progress") == "new"
|
||||
# Telegram is a mobile inbox by default — final-answer-first unless
|
||||
# explicitly configured otherwise.
|
||||
assert resolve_display_setting(config, "telegram", "tool_progress") == "off"
|
||||
# Email defaults to tier_minimal → "off"
|
||||
assert resolve_display_setting(config, "email", "tool_progress") == "off"
|
||||
|
||||
|
|
@ -180,12 +180,11 @@ class TestPlatformDefaults:
|
|||
"""Built-in defaults reflect platform capability tiers."""
|
||||
|
||||
def test_high_tier_platforms(self):
|
||||
"""Discord defaults to 'all' tool progress; Telegram is in tier_high
|
||||
but overrides tool_progress to 'new' (less edit pressure)."""
|
||||
"""Discord defaults to 'all'; Telegram defaults quiet for mobile."""
|
||||
from gateway.display_config import resolve_display_setting
|
||||
|
||||
# Telegram: tier_high member with tool_progress="new" override.
|
||||
assert resolve_display_setting({}, "telegram", "tool_progress") == "new"
|
||||
# Telegram: tier_high transport, but quiet mobile default.
|
||||
assert resolve_display_setting({}, "telegram", "tool_progress") == "off"
|
||||
# Discord: pure tier_high.
|
||||
assert resolve_display_setting({}, "discord", "tool_progress") == "all"
|
||||
|
||||
|
|
@ -229,6 +228,36 @@ class TestPlatformDefaults:
|
|||
|
||||
assert resolve_display_setting({}, "telegram", "streaming") is None
|
||||
|
||||
def test_telegram_mobile_chatter_defaults_off(self):
|
||||
"""Telegram suppresses operational chat noise unless opted in."""
|
||||
from gateway.display_config import resolve_display_setting
|
||||
|
||||
assert resolve_display_setting({}, "telegram", "interim_assistant_messages") is False
|
||||
assert resolve_display_setting({}, "telegram", "long_running_notifications") is False
|
||||
assert resolve_display_setting({}, "telegram", "busy_ack_detail") is False
|
||||
assert resolve_display_setting({}, "discord", "interim_assistant_messages") is True
|
||||
assert resolve_display_setting({}, "discord", "long_running_notifications") is True
|
||||
assert resolve_display_setting({}, "discord", "busy_ack_detail") is True
|
||||
|
||||
def test_telegram_mobile_chatter_can_opt_in(self):
|
||||
"""Per-platform config can re-enable Telegram status chatter."""
|
||||
from gateway.display_config import resolve_display_setting
|
||||
|
||||
config = {
|
||||
"display": {
|
||||
"platforms": {
|
||||
"telegram": {
|
||||
"interim_assistant_messages": True,
|
||||
"long_running_notifications": "yes",
|
||||
"busy_ack_detail": "on",
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
assert resolve_display_setting(config, "telegram", "interim_assistant_messages") is True
|
||||
assert resolve_display_setting(config, "telegram", "long_running_notifications") is True
|
||||
assert resolve_display_setting(config, "telegram", "busy_ack_detail") is True
|
||||
|
||||
|
||||
# ---------------------------------------------------------------------------
|
||||
# Config migration: tool_progress_overrides → display.platforms
|
||||
|
|
|
|||
|
|
@ -784,12 +784,13 @@ async def test_run_agent_surfaces_real_interim_commentary(monkeypatch, tmp_path)
|
|||
|
||||
|
||||
@pytest.mark.asyncio
|
||||
async def test_run_agent_surfaces_interim_commentary_by_default(monkeypatch, tmp_path):
|
||||
async def test_run_agent_surfaces_interim_commentary_when_globally_enabled(monkeypatch, tmp_path):
|
||||
adapter, result = await _run_with_agent(
|
||||
monkeypatch,
|
||||
tmp_path,
|
||||
CommentaryAgent,
|
||||
session_id="sess-commentary-default-on",
|
||||
session_id="sess-commentary-global-on",
|
||||
config_data={"display": {"interim_assistant_messages": True}},
|
||||
)
|
||||
|
||||
assert any(call["content"] == "I'll inspect the repo first." for call in adapter.sent)
|
||||
|
|
|
|||
|
|
@ -507,9 +507,23 @@ Scheduled auto-resume for N restart-interrupted session(s)
|
|||
|
||||
No configuration is required. If you don't want the heads-up, set `gateway_restart_notification: false` on the platform.
|
||||
|
||||
### Mobile-friendly progress defaults
|
||||
|
||||
Telegram defaults to final-answer-first output: no tool-progress stream, no periodic "still working…" heartbeat, no interim assistant status messages, and concise busy acknowledgments. Opt back into any of those per platform:
|
||||
|
||||
```yaml
|
||||
display:
|
||||
platforms:
|
||||
telegram:
|
||||
tool_progress: new
|
||||
interim_assistant_messages: true
|
||||
long_running_notifications: true
|
||||
busy_ack_detail: true
|
||||
```
|
||||
|
||||
### Progress bubble cleanup (opt-in)
|
||||
|
||||
Tool-progress messages, the "still working…" heartbeat, and status-callback bubbles can be auto-deleted after the final response lands. Enable per-platform via `display.platforms.<platform>.cleanup_progress`:
|
||||
Tool-progress messages, the "still working…" heartbeat, and status-callback bubbles can also be auto-deleted after the final response lands. Enable per-platform via `display.platforms.<platform>.cleanup_progress`:
|
||||
|
||||
```yaml
|
||||
display:
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue