mirror of
https://github.com/NousResearch/hermes-agent.git
synced 2026-05-30 06:41:51 +00:00
#33151 flipped THREE Telegram display defaults to false: - tool_progress: new -> off (kept: per-tool stream is too chatty) - interim_assistant_messages: T -> F (REVERTED here) - long_running_notifications: T -> F (REVERTED here) - busy_ack_detail: T -> F (kept: verbose iteration counter) The two reverts were wrong. interim_assistant_messages = the model's REAL words mid-turn ("I'll inspect the repo first.", "Let me check both files in parallel"). That is signal, not noise. Suppressing it left Telegram users staring at "typing..." for the entire turn duration with no feedback. long_running_notifications = the periodic heartbeat. Silent agent for 30 minutes is worse than one bubble updating every 3 minutes. Changes: - gateway/display_config.py: Telegram tier-1 inbox keeps both defaults on (only tool_progress and busy_ack_detail stay off). - gateway/run.py _notify_long_running(): edit a single heartbeat message in place (where the adapter supports it) instead of posting a new "Still working..." bubble each interval. Telegram, Discord, Slack, Matrix all qualify. Falls back to send-new when edit fails. - gateway/run.py: tighten heartbeat text. "⏳ Still working... (12 min elapsed — iteration 21/60, running: terminal)" -> "⏳ Working — 12 min, terminal". Verbose iteration detail moves behind busy_ack_detail (one knob now controls both busy acks AND heartbeat verbosity). - tests/, cli-config.yaml.example, website/docs/user-guide/messaging: updated to reflect the corrected story.
240 lines
8.8 KiB
Python
240 lines
8.8 KiB
Python
"""Per-platform display/verbosity configuration resolver.
|
|
|
|
Provides ``resolve_display_setting()`` — the single entry-point for reading
|
|
display settings with platform-specific overrides and sensible defaults.
|
|
|
|
Resolution order (first non-None wins):
|
|
1. ``display.platforms.<platform>.<key>`` — explicit per-platform user override
|
|
2. ``display.<key>`` — global user setting
|
|
3. ``_PLATFORM_DEFAULTS[<platform>][<key>]`` — built-in sensible default
|
|
4. ``_GLOBAL_DEFAULTS[<key>]`` — built-in global default
|
|
|
|
Exception: ``display.streaming`` is CLI-only. Gateway streaming follows the
|
|
top-level ``streaming`` config unless ``display.platforms.<platform>.streaming``
|
|
sets an explicit per-platform override.
|
|
|
|
Backward compatibility: ``display.tool_progress_overrides`` is still read as a
|
|
fallback for ``tool_progress`` when no ``display.platforms`` entry exists. A
|
|
config migration (version bump) automatically moves the old format into the new
|
|
``display.platforms`` structure.
|
|
"""
|
|
|
|
from __future__ import annotations
|
|
|
|
from typing import Any
|
|
|
|
# ---------------------------------------------------------------------------
|
|
# Overrideable display settings and their global defaults
|
|
# ---------------------------------------------------------------------------
|
|
# These are the settings that can be configured per-platform.
|
|
# Other display settings (compact, personality, skin, etc.) are CLI-only
|
|
# and don't participate in per-platform resolution.
|
|
|
|
_GLOBAL_DEFAULTS: dict[str, Any] = {
|
|
"tool_progress": "all",
|
|
"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 / "⏳ 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
|
|
# live, just cleaned up after success so the chat doesn't fill up with
|
|
# stale breadcrumbs. Failed runs leave bubbles in place as breadcrumbs.
|
|
"cleanup_progress": False,
|
|
}
|
|
|
|
# ---------------------------------------------------------------------------
|
|
# Sensible per-platform defaults — tiered by platform capability
|
|
# ---------------------------------------------------------------------------
|
|
# Tier 1 (high): Supports message editing, typically personal/team use
|
|
# Tier 2 (medium): Supports editing but often workspace/customer-facing
|
|
# Tier 3 (low): No edit support — each progress msg is permanent
|
|
# Tier 4 (minimal): Batch/non-interactive delivery
|
|
|
|
_TIER_HIGH = {
|
|
"tool_progress": "all",
|
|
"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 = {
|
|
"tool_progress": "new",
|
|
"show_reasoning": False,
|
|
"tool_preview_length": 40,
|
|
"streaming": None,
|
|
"interim_assistant_messages": True,
|
|
"long_running_notifications": True,
|
|
"busy_ack_detail": True,
|
|
}
|
|
|
|
_TIER_LOW = {
|
|
"tool_progress": "off",
|
|
"show_reasoning": False,
|
|
"tool_preview_length": 40,
|
|
"streaming": False,
|
|
"interim_assistant_messages": False,
|
|
"long_running_notifications": False,
|
|
"busy_ack_detail": False,
|
|
}
|
|
|
|
_TIER_MINIMAL = {
|
|
"tool_progress": "off",
|
|
"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 is usually a mobile inbox: keep tool_progress quiet and skip
|
|
# the verbose busy-ack iteration counter, but DO surface real mid-turn
|
|
# assistant commentary (interim_assistant_messages) and DO send periodic
|
|
# heartbeats (long_running_notifications) so the user has signal between
|
|
# turn start and final answer. Otherwise it looks like "typing..." for
|
|
# 30 minutes with nothing happening. Opt in to verbose iteration detail
|
|
# via display.platforms.telegram.busy_ack_detail / tool_progress.
|
|
"telegram": {
|
|
**_TIER_HIGH,
|
|
"tool_progress": "off",
|
|
"busy_ack_detail": False,
|
|
},
|
|
"discord": _TIER_HIGH,
|
|
|
|
# Tier 2 — edit support, often customer/workspace channels
|
|
# Slack: tool_progress off by default — Bolt posts cannot be edited like CLI;
|
|
# "new"/"all" spam permanent lines in channels (hermes-agent#14663).
|
|
"slack": {**_TIER_MEDIUM, "tool_progress": "off"},
|
|
"mattermost": _TIER_MEDIUM,
|
|
"matrix": _TIER_MEDIUM,
|
|
"feishu": _TIER_MEDIUM,
|
|
|
|
# Tier 3 — no edit support, progress messages are permanent
|
|
"signal": _TIER_LOW,
|
|
"whatsapp": _TIER_MEDIUM, # Baileys bridge supports /edit
|
|
"bluebubbles": _TIER_LOW,
|
|
"weixin": _TIER_LOW,
|
|
"wecom": _TIER_LOW,
|
|
"wecom_callback": _TIER_LOW,
|
|
"dingtalk": _TIER_LOW,
|
|
|
|
# Tier 4 — batch or non-interactive delivery
|
|
"email": _TIER_MINIMAL,
|
|
"sms": _TIER_MINIMAL,
|
|
"webhook": _TIER_MINIMAL,
|
|
"homeassistant": _TIER_MINIMAL,
|
|
"api_server": {**_TIER_HIGH, "tool_preview_length": 0},
|
|
}
|
|
|
|
# Canonical set of per-platform overrideable keys (for validation).
|
|
OVERRIDEABLE_KEYS = frozenset(_GLOBAL_DEFAULTS.keys())
|
|
|
|
|
|
def resolve_display_setting(
|
|
user_config: dict,
|
|
platform_key: str,
|
|
setting: str,
|
|
fallback: Any = None,
|
|
) -> Any:
|
|
"""Resolve a display setting with per-platform override support.
|
|
|
|
Parameters
|
|
----------
|
|
user_config : dict
|
|
The full parsed config.yaml dict.
|
|
platform_key : str
|
|
Platform config key (e.g. ``"telegram"``, ``"slack"``). Use
|
|
``_platform_config_key(source.platform)`` from gateway/run.py.
|
|
setting : str
|
|
Display setting name (e.g. ``"tool_progress"``, ``"show_reasoning"``).
|
|
fallback : Any
|
|
Fallback value when the setting isn't found anywhere.
|
|
|
|
Returns
|
|
-------
|
|
The resolved value, or *fallback* if nothing is configured.
|
|
"""
|
|
display_cfg = user_config.get("display") or {}
|
|
|
|
# 1. Explicit per-platform override (display.platforms.<platform>.<key>)
|
|
platforms = display_cfg.get("platforms") or {}
|
|
plat_overrides = platforms.get(platform_key)
|
|
if isinstance(plat_overrides, dict):
|
|
val = plat_overrides.get(setting)
|
|
if val is not None:
|
|
return _normalise(setting, val)
|
|
|
|
# 1b. Backward compat: display.tool_progress_overrides.<platform>
|
|
if setting == "tool_progress":
|
|
legacy = display_cfg.get("tool_progress_overrides")
|
|
if isinstance(legacy, dict):
|
|
val = legacy.get(platform_key)
|
|
if val is not None:
|
|
return _normalise(setting, val)
|
|
|
|
# 2. Global user setting (display.<key>). Skip display.streaming because
|
|
# that key controls only CLI terminal streaming; gateway token streaming is
|
|
# governed by the top-level streaming config plus per-platform overrides.
|
|
if setting != "streaming":
|
|
val = display_cfg.get(setting)
|
|
if val is not None:
|
|
return _normalise(setting, val)
|
|
|
|
# 3. Built-in platform default
|
|
plat_defaults = _PLATFORM_DEFAULTS.get(platform_key)
|
|
if plat_defaults:
|
|
val = plat_defaults.get(setting)
|
|
if val is not None:
|
|
return val
|
|
|
|
# 4. Built-in global default
|
|
val = _GLOBAL_DEFAULTS.get(setting)
|
|
if val is not None:
|
|
return val
|
|
|
|
return fallback
|
|
|
|
|
|
# ---------------------------------------------------------------------------
|
|
# Helpers
|
|
# ---------------------------------------------------------------------------
|
|
|
|
def _normalise(setting: str, value: Any) -> Any:
|
|
"""Normalise YAML quirks (bare ``off`` → False in YAML 1.1)."""
|
|
if setting == "tool_progress":
|
|
if value is False:
|
|
return "off"
|
|
if value is True:
|
|
return "all"
|
|
return str(value).lower()
|
|
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)
|
|
if setting == "cleanup_progress":
|
|
if isinstance(value, str):
|
|
return value.lower() in {"true", "1", "yes", "on"}
|
|
return bool(value)
|
|
if setting == "tool_preview_length":
|
|
try:
|
|
return int(value)
|
|
except (TypeError, ValueError):
|
|
return 0
|
|
return value
|