gateway: quiet Telegram operational chatter

This commit is contained in:
houenyang-momo 2026-05-23 16:32:18 +00:00 committed by Teknium
parent efa952531b
commit 60f84c6c28
7 changed files with 177 additions and 18 deletions

View file

@ -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

View file

@ -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)

View file

@ -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():

View file

@ -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."""

View file

@ -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

View file

@ -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)

View file

@ -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: