fix(gateway): respect tool_preview_length in all/new progress modes (#5937)
Previously, all/new tool progress modes always hard-truncated previews
to 40 chars, ignoring the display.tool_preview_length config. This made
it impossible for gateway users to see meaningful command/path info
without switching to verbose mode (which shows too much detail).
Now all/new modes read tool_preview_length from config:
- tool_preview_length: 0 (default/unset) → 40 chars (no regression)
- tool_preview_length: 120 → 120-char previews in all/new mode
- verbose mode: unchanged (already respected the config)
Users who want longer previews can set:
display:
tool_preview_length: 120
Reported by demontut_ on Discord.
This commit is contained in:
@@ -4910,8 +4910,8 @@ class GatewayRunner:
|
|||||||
cycle = ["off", "new", "all", "verbose"]
|
cycle = ["off", "new", "all", "verbose"]
|
||||||
descriptions = {
|
descriptions = {
|
||||||
"off": "⚙️ Tool progress: **OFF** — no tool activity shown.",
|
"off": "⚙️ Tool progress: **OFF** — no tool activity shown.",
|
||||||
"new": "⚙️ Tool progress: **NEW** — shown when tool changes (short previews).",
|
"new": "⚙️ Tool progress: **NEW** — shown when tool changes (preview length: `display.tool_preview_length`, default 40).",
|
||||||
"all": "⚙️ Tool progress: **ALL** — every tool call shown (short previews).",
|
"all": "⚙️ Tool progress: **ALL** — every tool call shown (preview length: `display.tool_preview_length`, default 40).",
|
||||||
"verbose": "⚙️ Tool progress: **VERBOSE** — every tool call with full arguments.",
|
"verbose": "⚙️ Tool progress: **VERBOSE** — every tool call with full arguments.",
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -6324,10 +6324,15 @@ class GatewayRunner:
|
|||||||
progress_queue.put(msg)
|
progress_queue.put(msg)
|
||||||
return
|
return
|
||||||
|
|
||||||
# "all" / "new" modes: short preview, always truncated (40 chars)
|
# "all" / "new" modes: short preview, respects tool_preview_length
|
||||||
|
# config (defaults to 40 chars when unset to keep gateway messages
|
||||||
|
# compact — unlike CLI spinners, these persist as permanent messages).
|
||||||
if preview:
|
if preview:
|
||||||
if len(preview) > 40:
|
from agent.display import get_tool_preview_max_len
|
||||||
preview = preview[:37] + "..."
|
_pl = get_tool_preview_max_len()
|
||||||
|
_cap = _pl if _pl > 0 else 40
|
||||||
|
if len(preview) > _cap:
|
||||||
|
preview = preview[:_cap - 3] + "..."
|
||||||
msg = f"{emoji} {tool_name}: \"{preview}\""
|
msg = f"{emoji} {tool_name}: \"{preview}\""
|
||||||
else:
|
else:
|
||||||
msg = f"{emoji} {tool_name}..."
|
msg = f"{emoji} {tool_name}..."
|
||||||
|
|||||||
@@ -71,6 +71,24 @@ class FakeAgent:
|
|||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
|
class LongPreviewAgent:
|
||||||
|
"""Agent that emits a tool call with a very long preview string."""
|
||||||
|
LONG_CMD = "cd /home/teknium/.hermes/hermes-agent/.worktrees/hermes-d8860339 && source .venv/bin/activate && python -m pytest tests/gateway/test_run_progress_topics.py -n0 -q"
|
||||||
|
|
||||||
|
def __init__(self, **kwargs):
|
||||||
|
self.tool_progress_callback = kwargs.get("tool_progress_callback")
|
||||||
|
self.tools = []
|
||||||
|
|
||||||
|
def run_conversation(self, message, conversation_history=None, task_id=None):
|
||||||
|
self.tool_progress_callback("tool.started", "terminal", self.LONG_CMD, {})
|
||||||
|
time.sleep(0.35)
|
||||||
|
return {
|
||||||
|
"final_response": "done",
|
||||||
|
"messages": [],
|
||||||
|
"api_calls": 1,
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
def _make_runner(adapter):
|
def _make_runner(adapter):
|
||||||
gateway_run = importlib.import_module("gateway.run")
|
gateway_run = importlib.import_module("gateway.run")
|
||||||
GatewayRunner = gateway_run.GatewayRunner
|
GatewayRunner = gateway_run.GatewayRunner
|
||||||
@@ -217,3 +235,102 @@ async def test_run_agent_progress_uses_event_message_id_for_slack_dm(monkeypatch
|
|||||||
assert adapter.sent
|
assert adapter.sent
|
||||||
assert adapter.sent[0]["metadata"] == {"thread_id": "1234567890.000001"}
|
assert adapter.sent[0]["metadata"] == {"thread_id": "1234567890.000001"}
|
||||||
assert all(call["metadata"] == {"thread_id": "1234567890.000001"} for call in adapter.typing)
|
assert all(call["metadata"] == {"thread_id": "1234567890.000001"} for call in adapter.typing)
|
||||||
|
|
||||||
|
|
||||||
|
# ---------------------------------------------------------------------------
|
||||||
|
# Preview truncation tests (all/new mode respects tool_preview_length)
|
||||||
|
# ---------------------------------------------------------------------------
|
||||||
|
|
||||||
|
|
||||||
|
def _run_long_preview_helper(monkeypatch, tmp_path, preview_length=0):
|
||||||
|
"""Shared setup for long-preview truncation tests.
|
||||||
|
|
||||||
|
Returns (adapter, result) after running the agent with LongPreviewAgent.
|
||||||
|
``preview_length`` controls display.tool_preview_length in the config file
|
||||||
|
that _run_agent reads — so the gateway picks it up the same way production does.
|
||||||
|
"""
|
||||||
|
import asyncio
|
||||||
|
import yaml
|
||||||
|
|
||||||
|
monkeypatch.setenv("HERMES_TOOL_PROGRESS_MODE", "all")
|
||||||
|
|
||||||
|
fake_dotenv = types.ModuleType("dotenv")
|
||||||
|
fake_dotenv.load_dotenv = lambda *args, **kwargs: None
|
||||||
|
monkeypatch.setitem(sys.modules, "dotenv", fake_dotenv)
|
||||||
|
|
||||||
|
fake_run_agent = types.ModuleType("run_agent")
|
||||||
|
fake_run_agent.AIAgent = LongPreviewAgent
|
||||||
|
monkeypatch.setitem(sys.modules, "run_agent", fake_run_agent)
|
||||||
|
|
||||||
|
# Write config.yaml so _run_agent picks up tool_preview_length
|
||||||
|
config = {"display": {"tool_preview_length": preview_length}}
|
||||||
|
(tmp_path / "config.yaml").write_text(yaml.dump(config), encoding="utf-8")
|
||||||
|
|
||||||
|
adapter = ProgressCaptureAdapter()
|
||||||
|
runner = _make_runner(adapter)
|
||||||
|
gateway_run = importlib.import_module("gateway.run")
|
||||||
|
monkeypatch.setattr(gateway_run, "_hermes_home", tmp_path)
|
||||||
|
monkeypatch.setattr(gateway_run, "_resolve_runtime_agent_kwargs", lambda: {"api_key": "***"})
|
||||||
|
|
||||||
|
source = SessionSource(
|
||||||
|
platform=Platform.TELEGRAM,
|
||||||
|
chat_id="12345",
|
||||||
|
chat_type="dm",
|
||||||
|
thread_id=None,
|
||||||
|
)
|
||||||
|
|
||||||
|
result = asyncio.get_event_loop().run_until_complete(
|
||||||
|
runner._run_agent(
|
||||||
|
message="hello",
|
||||||
|
context_prompt="",
|
||||||
|
history=[],
|
||||||
|
source=source,
|
||||||
|
session_id="sess-trunc",
|
||||||
|
session_key="agent:main:telegram:dm:12345",
|
||||||
|
)
|
||||||
|
)
|
||||||
|
return adapter, result
|
||||||
|
|
||||||
|
|
||||||
|
def test_all_mode_default_truncation_40_chars(monkeypatch, tmp_path):
|
||||||
|
"""When tool_preview_length is 0 (default), all/new mode truncates to 40 chars."""
|
||||||
|
adapter, result = _run_long_preview_helper(monkeypatch, tmp_path, preview_length=0)
|
||||||
|
assert result["final_response"] == "done"
|
||||||
|
assert adapter.sent
|
||||||
|
content = adapter.sent[0]["content"]
|
||||||
|
# The long command should be truncated — total preview <= 40 chars
|
||||||
|
assert "..." in content
|
||||||
|
# Extract the preview part between quotes
|
||||||
|
import re
|
||||||
|
match = re.search(r'"(.+)"', content)
|
||||||
|
assert match, f"No quoted preview found in: {content}"
|
||||||
|
preview_text = match.group(1)
|
||||||
|
assert len(preview_text) <= 40, f"Preview too long ({len(preview_text)}): {preview_text}"
|
||||||
|
|
||||||
|
|
||||||
|
def test_all_mode_respects_custom_preview_length(monkeypatch, tmp_path):
|
||||||
|
"""When tool_preview_length is explicitly set (e.g. 120), all/new mode uses that."""
|
||||||
|
adapter, result = _run_long_preview_helper(monkeypatch, tmp_path, preview_length=120)
|
||||||
|
assert result["final_response"] == "done"
|
||||||
|
assert adapter.sent
|
||||||
|
content = adapter.sent[0]["content"]
|
||||||
|
# With 120-char cap, the command (165 chars) should still be truncated but longer
|
||||||
|
import re
|
||||||
|
match = re.search(r'"(.+)"', content)
|
||||||
|
assert match, f"No quoted preview found in: {content}"
|
||||||
|
preview_text = match.group(1)
|
||||||
|
# Should be longer than the 40-char default
|
||||||
|
assert len(preview_text) > 40, f"Preview suspiciously short ({len(preview_text)}): {preview_text}"
|
||||||
|
# But still capped at 120
|
||||||
|
assert len(preview_text) <= 120, f"Preview too long ({len(preview_text)}): {preview_text}"
|
||||||
|
|
||||||
|
|
||||||
|
def test_all_mode_no_truncation_when_preview_fits(monkeypatch, tmp_path):
|
||||||
|
"""Short previews (under the cap) are not truncated."""
|
||||||
|
# Set a generous cap — the LongPreviewAgent's command is ~165 chars
|
||||||
|
adapter, result = _run_long_preview_helper(monkeypatch, tmp_path, preview_length=200)
|
||||||
|
assert result["final_response"] == "done"
|
||||||
|
assert adapter.sent
|
||||||
|
content = adapter.sent[0]["content"]
|
||||||
|
# With a 200-char cap, the 165-char command should NOT be truncated
|
||||||
|
assert "..." not in content, f"Preview was truncated when it shouldn't be: {content}"
|
||||||
|
|||||||
Reference in New Issue
Block a user