mirror of
https://github.com/NousResearch/hermes-agent.git
synced 2026-07-08 13:12:08 +00:00
chore: limit generic status phrases to long-running notifications
This commit is contained in:
parent
4bf5b563bd
commit
fddc95f4c2
12 changed files with 121 additions and 317 deletions
5
cli.py
5
cli.py
|
|
@ -3698,7 +3698,7 @@ class HermesCLI(CLIAgentSetupMixin, CLICommandsMixin):
|
|||
self.console = Console()
|
||||
self.config = CLI_CONFIG
|
||||
self.compact = compact if compact is not None else CLI_CONFIG["display"].get("compact", False)
|
||||
# tool_progress: "off", "generic", "new", "all", "verbose" (from config.yaml display section)
|
||||
# tool_progress: "off", "new", "all", "verbose" (from config.yaml display section)
|
||||
# YAML 1.1 parses bare `off` as boolean False — normalise to string.
|
||||
_raw_tp = CLI_CONFIG["display"].get("tool_progress", "all")
|
||||
self.tool_progress_mode = "off" if _raw_tp is False else str(_raw_tp)
|
||||
|
|
@ -9191,7 +9191,7 @@ class HermesCLI(CLIAgentSetupMixin, CLICommandsMixin):
|
|||
explicit ``-v``/``--verbose`` flag and the ``/verbose-logging``
|
||||
toggle. See PR #6a1aa420e for the history that decoupled them.
|
||||
"""
|
||||
cycle = ["off", "generic", "new", "all", "verbose"]
|
||||
cycle = ["off", "new", "all", "verbose"]
|
||||
try:
|
||||
idx = cycle.index(self.tool_progress_mode)
|
||||
except ValueError:
|
||||
|
|
@ -9212,7 +9212,6 @@ class HermesCLI(CLIAgentSetupMixin, CLICommandsMixin):
|
|||
from hermes_cli.colors import Colors as _Colors
|
||||
labels = {
|
||||
"off": f"{_Colors.DIM}Tool progress: OFF{_Colors.RESET} — silent mode, just the final response.",
|
||||
"generic": f"{_Colors.YELLOW}Tool progress: GENERIC{_Colors.RESET} — show friendly placeholders instead of tool details.",
|
||||
"new": f"{_Colors.YELLOW}Tool progress: NEW{_Colors.RESET} — show each new tool (skip repeats).",
|
||||
"all": f"{_Colors.GREEN}Tool progress: ALL{_Colors.RESET} — show every tool call.",
|
||||
"verbose": f"{_Colors.BOLD}{_Colors.GREEN}Tool progress: VERBOSE{_Colors.RESET} — full args, results, and think blocks.",
|
||||
|
|
|
|||
|
|
@ -1,198 +1,52 @@
|
|||
# Default phrases for generic gateway visibility surfaces.
|
||||
#
|
||||
# These are Hermes UI/status surfaces, not app/vendor/domain buckets. Keep them
|
||||
# generic: do not mention Jira, Confluence, Gmail, local paths, model names,
|
||||
# secrets, or tool arguments. User config can append/replace these via:
|
||||
#
|
||||
# display:
|
||||
# status_phrases:
|
||||
# mode: append # append | replace
|
||||
# thinking:
|
||||
# - one sec, thinking this through
|
||||
# platforms:
|
||||
# whatsapp:
|
||||
# status_phrases:
|
||||
# status:
|
||||
# - still on it
|
||||
|
||||
thinking:
|
||||
- one sec, thinking it through
|
||||
- thinking this through
|
||||
- hmm, working through it
|
||||
- one sec, lining it up
|
||||
- checking the shape of it
|
||||
- following the thread
|
||||
- sorting the pieces
|
||||
- thinking through the tradeoffs
|
||||
- one sec, making sure this fits
|
||||
- working out the clean version
|
||||
- holding the thought for a second
|
||||
- checking the logic
|
||||
- one sec, there’s a detail here
|
||||
- turning it over quickly
|
||||
- making sure I’m not missing a bit
|
||||
- mapping the moving pieces
|
||||
- letting the idea settle for a sec
|
||||
- checking the edge cases
|
||||
- narrowing the answer down
|
||||
- one sec, this has a wrinkle
|
||||
- tracing the dependency chain
|
||||
- sorting signal from noise
|
||||
- making the answer less wrong
|
||||
- checking the assumption
|
||||
- building the mental model
|
||||
- sanity-checking it
|
||||
- working through the awkward part
|
||||
- finding the clean path
|
||||
- one sec, connecting the dots
|
||||
- checking whether that actually follows
|
||||
|
||||
tool:
|
||||
- checking that now
|
||||
- looking into it
|
||||
- one sec, checking the details
|
||||
- working through the lookup
|
||||
- checking the actual thing
|
||||
- pulling the thread
|
||||
- one sec, doing the lookup
|
||||
- checking the source
|
||||
- looking at the live data
|
||||
- grabbing the relevant bit
|
||||
- checking what’s there
|
||||
- one sec, verifying it
|
||||
- reading through it
|
||||
- checking the current state
|
||||
- looking for the signal
|
||||
- checking the record
|
||||
- digging through the source
|
||||
- fetching the useful bit
|
||||
- seeing what it says
|
||||
- looking under the hood
|
||||
- checking the real state
|
||||
- pulling current context
|
||||
- scanning the relevant output
|
||||
- checking the available info
|
||||
- one sec, comparing the pieces
|
||||
- checking the latest version
|
||||
- confirming against the source
|
||||
- looking for the exact bit
|
||||
- checking where this lands
|
||||
- one sec, validating it
|
||||
|
||||
command:
|
||||
- running it now
|
||||
- checking what the machine says
|
||||
- one sec, running the command
|
||||
- running the check
|
||||
- asking the machine directly
|
||||
- letting it run for a sec
|
||||
- checking the output
|
||||
- running the useful bit
|
||||
- one sec, getting a real result
|
||||
- testing it locally
|
||||
- checking the logs
|
||||
- running the verification
|
||||
- waiting on the command
|
||||
- checking the system state
|
||||
- letting the process finish
|
||||
- running the test
|
||||
- asking the shell
|
||||
- waiting for the output
|
||||
- checking the result
|
||||
- letting the command complete
|
||||
- running the boring bit
|
||||
- checking the terminal output
|
||||
- one sec, the machine is thinking
|
||||
- running the local check
|
||||
- waiting on the process
|
||||
- checking whether it passes
|
||||
- letting the job finish
|
||||
- watching the output
|
||||
- verifying it directly
|
||||
- checking the exit status
|
||||
|
||||
interim:
|
||||
- one sec, there’s a bit more here
|
||||
- still shaping the answer
|
||||
- stitching this together
|
||||
- almost got the useful version
|
||||
- one sec, connecting the pieces
|
||||
- working this into a clean answer
|
||||
- still narrowing it down
|
||||
- pulling this into shape
|
||||
- one sec, checking the last part
|
||||
- getting the answer into a useful form
|
||||
- almost there
|
||||
- one more bit to check
|
||||
- turning this into something readable
|
||||
- still pulling it together
|
||||
- one sec, making this less messy
|
||||
- checking the last thread
|
||||
- shaping the final answer
|
||||
- joining the dots
|
||||
- just tightening the answer
|
||||
- one sec, cleaning this up
|
||||
- still organizing it
|
||||
- almost got the shape
|
||||
- one more pass
|
||||
- making the useful part clearer
|
||||
- still stitching
|
||||
- checking the last detail
|
||||
- making sure this lands cleanly
|
||||
- one sec, summarizing the useful bit
|
||||
- pulling the answer together
|
||||
- final bit of cleanup
|
||||
|
||||
status:
|
||||
- still on it
|
||||
- still working through it
|
||||
- this is taking a bit, still running
|
||||
- still checking
|
||||
- not stuck, just still running
|
||||
- waiting on the slow part
|
||||
- still alive, still working
|
||||
- letting it finish
|
||||
- still processing this
|
||||
- one sec, this is still going
|
||||
- still making progress
|
||||
- waiting for the result
|
||||
- still here, checking
|
||||
- the slow bit is still running
|
||||
- still working on the useful answer
|
||||
- no result yet, still waiting
|
||||
- still running the check
|
||||
- waiting on the backend bit
|
||||
- this one needs a minute
|
||||
- still moving
|
||||
- still going, not frozen
|
||||
- waiting for the useful output
|
||||
- still chewing through it
|
||||
- one sec, long bit is still running
|
||||
- still waiting on the result
|
||||
- working through the slow step
|
||||
- this is taking longer than usual
|
||||
- still checking, no panic
|
||||
- still in progress
|
||||
- letting the slow thing finish
|
||||
|
||||
- still on it
|
||||
- still working through it
|
||||
- this is taking a bit, still running
|
||||
- still checking
|
||||
- not stuck, just still running
|
||||
- waiting on the slow part
|
||||
- still alive, still working
|
||||
- letting it finish
|
||||
- still processing this
|
||||
- one sec, this is still going
|
||||
- still making progress
|
||||
- waiting for the result
|
||||
- still here, checking
|
||||
- the slow bit is still running
|
||||
- still working on the useful answer
|
||||
- no result yet, still waiting
|
||||
- still running the check
|
||||
- waiting on the backend bit
|
||||
- this one needs a minute
|
||||
- still moving
|
||||
- still going, not frozen
|
||||
- waiting for the useful output
|
||||
- still chewing through it
|
||||
- one sec, long bit is still running
|
||||
- still waiting on the result
|
||||
- working through the slow step
|
||||
- this is taking longer than usual
|
||||
- still checking, no panic
|
||||
- still in progress
|
||||
- letting the slow thing finish
|
||||
generic:
|
||||
- on it
|
||||
- one sec
|
||||
- checking that now
|
||||
- give me a sec
|
||||
- looking into it
|
||||
- working through it
|
||||
- checking
|
||||
- still working
|
||||
- one moment
|
||||
- taking a look
|
||||
- got it, checking
|
||||
- give me a moment
|
||||
- looking now
|
||||
- sorting it
|
||||
- checking the useful bit
|
||||
- one sec, working on it
|
||||
- taking a proper look
|
||||
- checking the details
|
||||
- working it out
|
||||
- still looking
|
||||
- on it
|
||||
- one sec
|
||||
- checking that now
|
||||
- give me a sec
|
||||
- looking into it
|
||||
- working through it
|
||||
- checking
|
||||
- still working
|
||||
- one moment
|
||||
- taking a look
|
||||
- got it, checking
|
||||
- give me a moment
|
||||
- looking now
|
||||
- sorting it
|
||||
- checking the useful bit
|
||||
- one sec, working on it
|
||||
- taking a proper look
|
||||
- checking the details
|
||||
- working it out
|
||||
- still looking
|
||||
|
|
|
|||
|
|
@ -234,7 +234,7 @@ def _normalise(setting: str, value: Any) -> Any:
|
|||
if value is True:
|
||||
return "all"
|
||||
val = str(value).lower()
|
||||
return val if val in {"off", "generic", "new", "all", "verbose"} else "all"
|
||||
return val if val in {"off", "new", "all", "verbose"} else "all"
|
||||
if setting in {
|
||||
"show_reasoning",
|
||||
"streaming",
|
||||
|
|
@ -245,11 +245,7 @@ def _normalise(setting: str, value: Any) -> Any:
|
|||
}:
|
||||
if isinstance(value, str):
|
||||
val = value.strip().lower()
|
||||
if val == "generic" and setting in {
|
||||
"thinking_progress",
|
||||
"interim_assistant_messages",
|
||||
"long_running_notifications",
|
||||
}:
|
||||
if val == "generic" and setting == "long_running_notifications":
|
||||
return "generic"
|
||||
return val in {"true", "1", "yes", "on", "raw", "verbose"}
|
||||
return bool(value)
|
||||
|
|
|
|||
|
|
@ -16499,6 +16499,7 @@ class GatewayRunner(GatewayAuthorizationMixin, GatewayKanbanWatchersMixin, Gatew
|
|||
*,
|
||||
default: bool = False,
|
||||
require_platform_override_for: set[Any] | None = None,
|
||||
allow_generic: bool = False,
|
||||
) -> str:
|
||||
"""Return off|raw|generic for a gateway visibility surface."""
|
||||
if require_platform_override_for:
|
||||
|
|
@ -16514,7 +16515,7 @@ class GatewayRunner(GatewayAuthorizationMixin, GatewayKanbanWatchersMixin, Gatew
|
|||
return "off"
|
||||
value = resolve_display_setting(user_config, platform_key, setting, default)
|
||||
if isinstance(value, str) and value.strip().lower() == "generic":
|
||||
return "generic"
|
||||
return "generic" if allow_generic else "off"
|
||||
return "raw" if bool(value) else "off"
|
||||
|
||||
def _generic_status_phrase(kind: str, *, tool_name: str | None = None, preview: str | None = None, args: Any = None) -> str:
|
||||
|
|
@ -16689,10 +16690,7 @@ class GatewayRunner(GatewayAuthorizationMixin, GatewayKanbanWatchersMixin, Gatew
|
|||
if not _thinking_enabled:
|
||||
return
|
||||
thinking_text = preview if tool_name == "_thinking" else tool_name
|
||||
if _thinking_mode == "generic":
|
||||
msg = _generic_status_phrase("thinking", preview=str(thinking_text or ""))
|
||||
else:
|
||||
msg = f"💬 {thinking_text}" if thinking_text else None
|
||||
msg = f"💬 {thinking_text}" if thinking_text else None
|
||||
if msg:
|
||||
progress_queue.put(msg)
|
||||
return
|
||||
|
|
@ -16727,17 +16725,6 @@ class GatewayRunner(GatewayAuthorizationMixin, GatewayKanbanWatchersMixin, Gatew
|
|||
return
|
||||
last_tool[0] = tool_name
|
||||
|
||||
if tool_progress_generic:
|
||||
progress_queue.put(
|
||||
_generic_status_phrase(
|
||||
"tool",
|
||||
tool_name=str(tool_name or ""),
|
||||
preview=str(preview or ""),
|
||||
args=args,
|
||||
)
|
||||
)
|
||||
return
|
||||
|
||||
# Build progress message with primary argument preview
|
||||
from agent.display import get_tool_emoji
|
||||
emoji = get_tool_emoji(tool_name, default="⚙️")
|
||||
|
|
@ -17537,11 +17524,7 @@ class GatewayRunner(GatewayAuthorizationMixin, GatewayKanbanWatchersMixin, Gatew
|
|||
def _interim_assistant_cb(text: str, *, already_streamed: bool = False) -> None:
|
||||
if not _run_still_current():
|
||||
return
|
||||
display_text = (
|
||||
_generic_status_phrase("interim", preview=str(text or ""))
|
||||
if interim_assistant_messages_mode == "generic"
|
||||
else text
|
||||
)
|
||||
display_text = text
|
||||
if _stream_consumer is not None:
|
||||
if already_streamed:
|
||||
_stream_consumer.on_segment_break()
|
||||
|
|
@ -18727,6 +18710,7 @@ class GatewayRunner(GatewayAuthorizationMixin, GatewayKanbanWatchersMixin, Gatew
|
|||
_long_running_mode = _display_surface_mode(
|
||||
"long_running_notifications",
|
||||
default=True,
|
||||
allow_generic=True,
|
||||
)
|
||||
if _long_running_mode == "off":
|
||||
_NOTIFY_INTERVAL = None
|
||||
|
|
|
|||
|
|
@ -2936,7 +2936,6 @@ class GatewaySlashCommandsMixin:
|
|||
cycle = ["off", "new", "all", "verbose", "log"]
|
||||
descriptions = {
|
||||
"off": t("gateway.verbose.mode_off"),
|
||||
"generic": "GENERIC — show human-friendly placeholders instead of tool details.",
|
||||
"new": t("gateway.verbose.mode_new"),
|
||||
"all": t("gateway.verbose.mode_all"),
|
||||
"verbose": t("gateway.verbose.mode_verbose"),
|
||||
|
|
|
|||
|
|
@ -1,8 +1,8 @@
|
|||
"""Human-friendly generic gateway status phrases.
|
||||
|
||||
These helpers deliberately avoid relaying raw model scratch text. They turn
|
||||
Hermes' own gateway visibility surfaces (thinking/tool/command/interim/status)
|
||||
into short status lines suitable for chat surfaces.
|
||||
Hermes' long-running gateway status surface into short status lines suitable
|
||||
for chat surfaces.
|
||||
|
||||
Built-in defaults live in ``gateway/assets/status_phrases.yaml``. Users can add
|
||||
portable, profile-relative phrase catalogs under ``HERMES_HOME`` either by using
|
||||
|
|
@ -36,20 +36,15 @@ import yaml
|
|||
|
||||
from hermes_constants import get_hermes_home
|
||||
|
||||
# These are Hermes UI surfaces, not app/vendor/domain buckets. Do not add
|
||||
# Jira/Confluence/Gmail/etc. categories here; tool names are only used to choose
|
||||
# between the built-in "tool" and "command" surfaces.
|
||||
_STATUS_SURFACES = ("thinking", "tool", "command", "interim", "status", "generic")
|
||||
_COMMAND_TOOL_NAMES = {"terminal", "execute_code", "process"}
|
||||
# These are Hermes UI surfaces, not app/vendor/domain buckets. Keep this
|
||||
# long-running-only: regular tool/thinking/interim chatter is intentionally not
|
||||
# rewritten into generic placeholders because that gets noisy fast in chat.
|
||||
_STATUS_SURFACES = ("status", "generic")
|
||||
_MAX_CUSTOM_PHRASES_PER_SURFACE = 80
|
||||
_MAX_PHRASE_CHARS = 160
|
||||
_CONVENTIONAL_RELATIVE_PATHS = ("status_phrases.yaml", "status_phrases")
|
||||
|
||||
_FALLBACK_PHRASES: dict[str, list[str]] = {
|
||||
"thinking": ["one sec, thinking it through", "thinking this through", "checking the logic"],
|
||||
"tool": ["checking that now", "looking into it", "checking the source"],
|
||||
"command": ["running it now", "checking what the machine says", "checking the output"],
|
||||
"interim": ["still shaping the answer", "stitching this together", "almost there"],
|
||||
"status": ["still on it", "still working through it", "waiting for the result"],
|
||||
"generic": ["on it", "one sec", "checking that now"],
|
||||
}
|
||||
|
|
@ -198,15 +193,6 @@ def classify_status_context(
|
|||
normalized = str(kind or "").strip().lower()
|
||||
if normalized in {"heartbeat", "waiting", "long_running", "status"}:
|
||||
return "status"
|
||||
if normalized in {"thinking", "_thinking", "reasoning"}:
|
||||
return "thinking"
|
||||
if normalized in {"interim", "interim_assistant"}:
|
||||
return "interim"
|
||||
if normalized in {"command", "terminal"}:
|
||||
return "command"
|
||||
if normalized == "tool":
|
||||
name = (tool_name or "").strip().lower()
|
||||
return "command" if name in _COMMAND_TOOL_NAMES else "tool"
|
||||
return "generic"
|
||||
|
||||
|
||||
|
|
|
|||
|
|
@ -1773,11 +1773,11 @@ DEFAULT_CONFIG = {
|
|||
# applies where tool_progress is already enabled. Per-platform override
|
||||
# via display.platforms.<platform>.tool_progress_grouping.
|
||||
"tool_progress_grouping": "accumulate",
|
||||
# Optional custom phrases for display.* generic visibility modes.
|
||||
# Optional custom phrases for generic long-running status messages.
|
||||
# Built-in defaults live in gateway/assets/status_phrases.yaml. Users
|
||||
# can set `path`/`paths` to HERMES_HOME-relative YAML files/directories
|
||||
# (or rely on conventional status_phrases.yaml / status_phrases/*.yaml).
|
||||
# Keys: thinking, tool, command, interim, status, generic. Use
|
||||
# Keys: status, generic. Use
|
||||
# mode: "append" (default) to add phrases, or "replace" to fully
|
||||
# replace configured surfaces. Per-platform overrides live under
|
||||
# display.platforms.<platform>.status_phrases.
|
||||
|
|
@ -5346,7 +5346,7 @@ def migrate_config(interactive: bool = True, quiet: bool = False) -> Dict[str, A
|
|||
if old_enabled and old_enabled.lower() in {"false", "0", "no"}:
|
||||
display["tool_progress"] = "off"
|
||||
results["config_added"].append("display.tool_progress=off (from HERMES_TOOL_PROGRESS=false)")
|
||||
elif old_mode and old_mode.lower() in {"generic", "new", "all", "verbose"}:
|
||||
elif old_mode and old_mode.lower() in {"new", "all", "verbose"}:
|
||||
display["tool_progress"] = old_mode.lower()
|
||||
results["config_added"].append(f"display.tool_progress={old_mode.lower()} (from HERMES_TOOL_PROGRESS_MODE)")
|
||||
else:
|
||||
|
|
|
|||
|
|
@ -99,7 +99,7 @@ class TestVerboseAndToolProgress:
|
|||
def test_tool_progress_mode_is_string(self):
|
||||
cli = _make_cli()
|
||||
assert isinstance(cli.tool_progress_mode, str)
|
||||
assert cli.tool_progress_mode in {"off", "generic", "new", "all", "verbose"}
|
||||
assert cli.tool_progress_mode in {"off", "new", "all", "verbose"}
|
||||
|
||||
|
||||
class TestFallbackChainInit:
|
||||
|
|
|
|||
|
|
@ -149,13 +149,13 @@ class TestYAMLNormalisation:
|
|||
config = {"display": {"tool_progress": True}}
|
||||
assert resolve_display_setting(config, "telegram", "tool_progress") == "all"
|
||||
|
||||
def test_tool_progress_generic_is_first_class_mode(self):
|
||||
def test_tool_progress_generic_is_not_a_mode(self):
|
||||
from gateway.display_config import resolve_display_setting
|
||||
|
||||
config = {"display": {"platforms": {"whatsapp": {"tool_progress": "generic"}}}}
|
||||
assert resolve_display_setting(config, "whatsapp", "tool_progress") == "generic"
|
||||
assert resolve_display_setting(config, "whatsapp", "tool_progress") == "all"
|
||||
|
||||
def test_chatter_visibility_surfaces_accept_generic_mode(self):
|
||||
def test_only_long_running_visibility_accepts_generic_mode(self):
|
||||
from gateway.display_config import resolve_display_setting
|
||||
|
||||
config = {
|
||||
|
|
@ -169,8 +169,8 @@ class TestYAMLNormalisation:
|
|||
}
|
||||
}
|
||||
}
|
||||
assert resolve_display_setting(config, "whatsapp", "thinking_progress") == "generic"
|
||||
assert resolve_display_setting(config, "whatsapp", "interim_assistant_messages") == "generic"
|
||||
assert resolve_display_setting(config, "whatsapp", "thinking_progress") is False
|
||||
assert resolve_display_setting(config, "whatsapp", "interim_assistant_messages") is False
|
||||
assert resolve_display_setting(config, "whatsapp", "long_running_notifications") == "generic"
|
||||
|
||||
def test_thinking_progress_string_false_normalised_to_false(self):
|
||||
|
|
|
|||
|
|
@ -7,72 +7,59 @@ from gateway.status_phrases import (
|
|||
)
|
||||
|
||||
|
||||
def test_terminal_tool_uses_command_surface_bucket():
|
||||
assert classify_status_context("tool", tool_name="terminal") == "command"
|
||||
assert classify_status_context("command") == "command"
|
||||
def test_long_running_context_uses_status_bucket():
|
||||
assert classify_status_context("status") == "status"
|
||||
assert classify_status_context("heartbeat") == "status"
|
||||
assert classify_status_context("long_running") == "status"
|
||||
|
||||
|
||||
def test_regular_tool_uses_tool_surface_bucket_not_domain_bucket():
|
||||
assert classify_status_context("tool", tool_name="web_search", preview="weather") == "tool"
|
||||
assert classify_status_context("tool", tool_name="mcp_atlassian_infohealth_getJiraIssue") == "tool"
|
||||
def test_non_status_context_falls_back_to_generic_bucket():
|
||||
assert classify_status_context("tool", tool_name="terminal") == "generic"
|
||||
assert classify_status_context("thinking") == "generic"
|
||||
assert classify_status_context("interim_assistant") == "generic"
|
||||
|
||||
|
||||
def test_interim_uses_interim_surface_bucket():
|
||||
assert classify_status_context("interim_assistant") == "interim"
|
||||
|
||||
|
||||
def test_generic_phrase_does_not_leak_raw_thinking_text():
|
||||
def test_status_phrase_does_not_leak_raw_preview_or_args():
|
||||
msg = choose_status_phrase(
|
||||
"thinking",
|
||||
"status",
|
||||
preview="actual private scratch text should not be sent",
|
||||
args={"secret": "SECRET-123"},
|
||||
rng=random.Random(4),
|
||||
)
|
||||
|
||||
assert "actual private scratch" not in msg
|
||||
assert "SECRET-123" not in msg
|
||||
assert msg
|
||||
|
||||
|
||||
def test_generic_tool_phrase_does_not_leak_vendor_or_args():
|
||||
msg = choose_status_phrase(
|
||||
"tool",
|
||||
tool_name="mcp_atlassian_infohealth_getJiraIssue",
|
||||
args={"issueIdOrKey": "SECRET-123"},
|
||||
rng=random.Random(1),
|
||||
)
|
||||
|
||||
assert "jira" not in msg.lower()
|
||||
assert "confluence" not in msg.lower()
|
||||
assert "atlassian" not in msg.lower()
|
||||
assert "SECRET-123" not in msg
|
||||
|
||||
|
||||
def test_generic_phrase_avoids_recent_repetition():
|
||||
def test_status_phrase_avoids_recent_repetition():
|
||||
recent: list[str] = []
|
||||
first = choose_status_phrase("tool", tool_name="web_search", rng=random.Random(2), recent=recent)
|
||||
second = choose_status_phrase("tool", tool_name="web_search", rng=random.Random(2), recent=recent)
|
||||
first = choose_status_phrase("status", rng=random.Random(2), recent=recent)
|
||||
second = choose_status_phrase("status", rng=random.Random(2), recent=recent)
|
||||
|
||||
assert first != second
|
||||
assert recent[-2:] == [first, second]
|
||||
|
||||
|
||||
def test_builtin_catalog_is_loaded_from_external_asset_and_is_not_tiny():
|
||||
def test_builtin_catalog_is_loaded_from_external_asset_and_is_status_only():
|
||||
catalog = resolve_status_phrase_catalog({}, "whatsapp")
|
||||
|
||||
for surface in ("thinking", "tool", "command", "interim", "status"):
|
||||
assert len(catalog[surface]) >= 25, surface
|
||||
assert set(catalog) == {"status", "generic"}
|
||||
assert len(catalog["status"]) >= 25
|
||||
assert len(catalog["generic"]) >= 10
|
||||
|
||||
|
||||
def test_relative_status_phrase_path_loads_from_hermes_home(tmp_path, monkeypatch):
|
||||
monkeypatch.setenv("HERMES_HOME", str(tmp_path))
|
||||
phrase_file = tmp_path / "phrases.yaml"
|
||||
phrase_file.write_text("mode: replace\ntool:\n - relative safe tool text\n", encoding="utf-8")
|
||||
phrase_file.write_text("mode: replace\nstatus:\n - relative safe status text\n", encoding="utf-8")
|
||||
|
||||
catalog = resolve_status_phrase_catalog(
|
||||
{"display": {"status_phrases": {"path": "phrases.yaml"}}},
|
||||
"whatsapp",
|
||||
)
|
||||
|
||||
assert catalog["tool"] == ["relative safe tool text"]
|
||||
assert catalog["status"] == ["relative safe status text"]
|
||||
|
||||
|
||||
def test_status_phrase_path_can_load_relative_directory(tmp_path, monkeypatch):
|
||||
|
|
@ -92,7 +79,7 @@ def test_status_phrase_path_can_load_relative_directory(tmp_path, monkeypatch):
|
|||
def test_absolute_or_parent_phrase_paths_are_ignored(tmp_path, monkeypatch):
|
||||
monkeypatch.setenv("HERMES_HOME", str(tmp_path))
|
||||
outside = tmp_path.parent / "outside-phrases.yaml"
|
||||
outside.write_text("mode: replace\ntool:\n - should not load\n", encoding="utf-8")
|
||||
outside.write_text("mode: replace\nstatus:\n - should not load\n", encoding="utf-8")
|
||||
|
||||
catalog = resolve_status_phrase_catalog(
|
||||
{"display": {"status_phrases": {"path": str(outside)}}},
|
||||
|
|
@ -103,8 +90,8 @@ def test_absolute_or_parent_phrase_paths_are_ignored(tmp_path, monkeypatch):
|
|||
"whatsapp",
|
||||
)
|
||||
|
||||
assert catalog["tool"] != ["should not load"]
|
||||
assert escaped["tool"] != ["should not load"]
|
||||
assert catalog["status"] != ["should not load"]
|
||||
assert escaped["status"] != ["should not load"]
|
||||
|
||||
|
||||
def test_conventional_relative_status_phrase_file_is_loaded(tmp_path, monkeypatch):
|
||||
|
|
@ -119,23 +106,23 @@ def test_conventional_relative_status_phrase_file_is_loaded(tmp_path, monkeypatc
|
|||
assert catalog["status"] == ["conventional status text"]
|
||||
|
||||
|
||||
def test_global_custom_phrase_catalog_appends_to_builtin():
|
||||
def test_global_custom_status_phrase_catalog_appends_to_builtin():
|
||||
catalog = resolve_status_phrase_catalog(
|
||||
{
|
||||
"display": {
|
||||
"status_phrases": {
|
||||
"thinking": ["custom thinking placeholder"],
|
||||
"status": ["custom long-running placeholder"],
|
||||
}
|
||||
}
|
||||
},
|
||||
"whatsapp",
|
||||
)
|
||||
|
||||
assert "custom thinking placeholder" in catalog["thinking"]
|
||||
assert len(catalog["thinking"]) > 1
|
||||
assert "custom long-running placeholder" in catalog["status"]
|
||||
assert len(catalog["status"]) > 1
|
||||
|
||||
|
||||
def test_platform_custom_phrase_catalog_can_replace_surface():
|
||||
def test_platform_custom_status_phrase_catalog_can_replace_surface():
|
||||
catalog = resolve_status_phrase_catalog(
|
||||
{
|
||||
"display": {
|
||||
|
|
@ -143,7 +130,7 @@ def test_platform_custom_phrase_catalog_can_replace_surface():
|
|||
"whatsapp": {
|
||||
"status_phrases": {
|
||||
"mode": "replace",
|
||||
"tool": ["custom tool placeholder"],
|
||||
"status": ["custom status placeholder"],
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
@ -152,22 +139,21 @@ def test_platform_custom_phrase_catalog_can_replace_surface():
|
|||
"whatsapp",
|
||||
)
|
||||
|
||||
assert catalog["tool"] == ["custom tool placeholder"]
|
||||
assert len(catalog["thinking"]) > 1
|
||||
assert catalog["status"] == ["custom status placeholder"]
|
||||
assert len(catalog["generic"]) > 1
|
||||
|
||||
|
||||
def test_choose_status_phrase_uses_custom_catalog_without_leaking_args():
|
||||
catalog = resolve_status_phrase_catalog(
|
||||
{"display": {"status_phrases": {"mode": "replace", "tool": ["custom safe tool text"]}}},
|
||||
{"display": {"status_phrases": {"mode": "replace", "status": ["custom safe status text"]}}},
|
||||
"whatsapp",
|
||||
)
|
||||
|
||||
msg = choose_status_phrase(
|
||||
"tool",
|
||||
tool_name="web_search",
|
||||
"status",
|
||||
args={"query": "SECRET SEARCH"},
|
||||
catalog=catalog,
|
||||
)
|
||||
|
||||
assert msg == "custom safe tool text"
|
||||
assert msg == "custom safe status text"
|
||||
assert "SECRET" not in msg
|
||||
|
|
|
|||
|
|
@ -132,7 +132,7 @@ class TestVerboseCommand:
|
|||
|
||||
Telegram's tier-1 preset overrides ``tool_progress`` to ``"off"`` so the
|
||||
platform stays final-answer-first by default on mobile inboxes. The
|
||||
first ``/verbose`` invocation therefore cycles ``off → generic``.
|
||||
first ``/verbose`` invocation therefore cycles ``off → new``.
|
||||
"""
|
||||
hermes_home = tmp_path / "hermes"
|
||||
hermes_home.mkdir()
|
||||
|
|
@ -147,10 +147,10 @@ class TestVerboseCommand:
|
|||
runner = _make_runner()
|
||||
result = await runner._handle_verbose_command(_make_event())
|
||||
|
||||
# Telegram platform default is "off" → cycles to "generic"
|
||||
assert "GENERIC" in result
|
||||
# Telegram platform default is "off" → cycles to "new"
|
||||
assert "NEW" in result
|
||||
saved = yaml.safe_load(config_path.read_text(encoding="utf-8"))
|
||||
assert saved["display"]["platforms"]["telegram"]["tool_progress"] == "generic"
|
||||
assert saved["display"]["platforms"]["telegram"]["tool_progress"] == "new"
|
||||
|
||||
@pytest.mark.asyncio
|
||||
async def test_per_platform_isolation(self, tmp_path, monkeypatch):
|
||||
|
|
@ -158,7 +158,7 @@ class TestVerboseCommand:
|
|||
|
||||
Without a global tool_progress, each platform uses its built-in
|
||||
default — Telegram = 'off' (tier-1 inbox override), Slack = 'off'
|
||||
(quiet Slack default). Both cycle to 'generic' on first /verbose.
|
||||
(quiet Slack default). Both cycle to 'new' on first /verbose.
|
||||
"""
|
||||
hermes_home = tmp_path / "hermes"
|
||||
hermes_home.mkdir()
|
||||
|
|
@ -183,10 +183,10 @@ class TestVerboseCommand:
|
|||
|
||||
saved = yaml.safe_load(config_path.read_text(encoding="utf-8"))
|
||||
platforms = saved["display"]["platforms"]
|
||||
# Telegram: off -> generic (platform default = off, tier-1 inbox override)
|
||||
assert platforms["telegram"]["tool_progress"] == "generic"
|
||||
# Slack: off -> generic (first /verbose cycle from quiet default)
|
||||
assert platforms["slack"]["tool_progress"] == "generic"
|
||||
# Telegram: off -> new (platform default = off, tier-1 inbox override)
|
||||
assert platforms["telegram"]["tool_progress"] == "new"
|
||||
# Slack: off -> new (first /verbose cycle from quiet default)
|
||||
assert platforms["slack"]["tool_progress"] == "new"
|
||||
|
||||
@pytest.mark.asyncio
|
||||
async def test_no_config_file_returns_disabled(self, tmp_path, monkeypatch):
|
||||
|
|
|
|||
|
|
@ -2385,7 +2385,7 @@ def _load_memory_notifications() -> str:
|
|||
|
||||
def _load_tool_progress_mode() -> str:
|
||||
env = os.environ.get("HERMES_TUI_TOOL_PROGRESS", "").strip().lower()
|
||||
if env in {"off", "generic", "new", "all", "verbose"}:
|
||||
if env in {"off", "new", "all", "verbose"}:
|
||||
return env
|
||||
raw = (_load_cfg().get("display") or {}).get("tool_progress", "all")
|
||||
if raw is False:
|
||||
|
|
@ -2393,7 +2393,7 @@ def _load_tool_progress_mode() -> str:
|
|||
if raw is True:
|
||||
return "all"
|
||||
mode = str(raw or "all").strip().lower()
|
||||
return mode if mode in {"off", "generic", "new", "all", "verbose"} else "all"
|
||||
return mode if mode in {"off", "new", "all", "verbose"} else "all"
|
||||
|
||||
|
||||
def _load_enabled_toolsets() -> list[str] | None:
|
||||
|
|
@ -9930,7 +9930,7 @@ def _(rid, params: dict) -> dict:
|
|||
return _ok(rid, {"key": key, "value": raw})
|
||||
|
||||
if key == "verbose":
|
||||
cycle = ["off", "generic", "new", "all", "verbose"]
|
||||
cycle = ["off", "new", "all", "verbose"]
|
||||
cur = (
|
||||
session.get("tool_progress_mode", _load_tool_progress_mode())
|
||||
if session
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue