Gateway tool progress display and chat formatting fixes
NousResearch/hermes-agent · cluster 13584 · acp
Data as of
2 open items. Fix merged in #42420. Check it covers your case.
The short answer
- Known?
- Yes. 15 related items grouped here, 3 issues already closed.
- Fix event
- A fix was merged to main in PR #42420. Check it covers your case. No release data is tracked.
- Tracker
- The maintainers' tracker marks this
partially resolved, deciding PR #42420. That is separate from the fix event. - Still open
- 2 issues and 0 PRs.
Next step: read #42420, then check whether your report is one of the 2 still open.
Read #42420Pick up a Lab taskThese items address how terminal tool progress and command output are formatted and truncated when sent to messaging platforms like Discord and Telegram, alongside related ACP tool completion update issues. Most members share the theme of controlling what shell command content appears in chat progress messages.
Open issues2
Open PRs0
Fixes on record
-
#42420 fix(gateway): stop terminal progress from posting full commands to messaging chats
files:
gateway/platforms/base.py,gateway/run.py,tests/gateway/test_run_progress_topics.pyShow the diff
-
gateway/platforms/base.py@@ -1795,9 +1795,11 @@ class BasePlatformAdapter(ABC): # Whether this platform renders triple-backtick fenced code blocks (i.e. # ``format_message`` translates/preserves markdown fences into a real code - # block). Drives presentation choices like rendering a ``terminal`` tool - # call's command as a ```bash block instead of a flat preview line. + # block). Capability flag for markdown-aware presentation choices. # Default False (plain-text platforms); markdown-rendering adapters set True. + # Note: tool-progress deliberately does NOT use this to render a terminal + # command as a ```bash block — that exposed full commands in chat. Progress + # shows a short truncated preview only (see gateway/run.py progress_callback). supports_code_blocks: bool = False def __init__(self, config: PlatformConfig, platform: Platform): -
gateway/run.py@@ -12971,32 +12971,10 @@ def progress_callback(event_type: str, tool_name: str = None, preview: str = Non # Build progress message with primary argument preview from agent.display import get_tool_emoji emoji = get_tool_emoji(tool_name, default="⚙️") - - # Markdown-capable platforms render a terminal command as a native - # ```bash fenced block (full command, no quotes, no label, no - # truncation) instead of the noisy `terminal: "cmd…"` line. Gated - # on the adapter's ``supports_code_blocks`` capability so every - # markdown-rendering platform (and plugin adapters that opt in) gets - # it, while plain-text platforms keep the compact line. - _bash_block = None - try: - _progress_adapter = self.adapters.get(source.platform) - except Exception: - _progress_adapter = None - if ( - getattr(_progress_adapter, "supports_code_blocks", False) - and tool_name == "terminal" - and isinstance(args, dict) - and isinstance(args.get("command"), str) - and args["command"].strip() - ): - _bash_block = f"```bash\n{args['command'].rstrip()}\n```" # Verbose mode: show detailed arguments, respects tool_preview_length if progress_mode == "verbose": - if _bash_block is not None: - msg = _bash_block - elif args: + if args: from agent.display import get_tool_preview_max_len _pl = get_tool_preview_max_len() args_str = json.dumps(args, ensure_ascii=False, default=str) @@ -13016,9 +12994,7 @@ def progress_callback(event_type: str, tool_name: str = None, preview: str = Non # "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 _bash_block is not None: - msg = _bash_block - elif preview: + if preview: from agent.display import get_tool_preview_max_len _pl = get_tool_preview_max_len() _cap = _pl if _pl > 0 else 40 -
tests/gateway/test_run_progress_topics.py@@ -1264,3 +1264,123 @@ async def test_verbose_mode_respects_explicit_tool_preview_length(monkeypatch, t assert VerboseAgent.LONG_CODE not in all_content # But should still contain the truncated portion with "..." assert "..." in all_content + + +class CodeBlockProgressAdapter(ProgressCaptureAdapter): + """A markdown-capable progress adapter (declares supports_code_blocks).""" + + supports_code_blocks = True + + +class TerminalCommandAgent: + """Emits a terminal tool.started with a real, multi-line command arg.""" + + CMD = ( + "set -euo pipefail\n" + "printf 'node: '; node --version\n" + "npm install -g hyperframes@latest" + ) + + 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.CMD, {"command": self.CMD} + ) + # Let the async progress task drain the queue and send before returning. + time.sleep(0.35) + return {"final_response": "done", "messages": [], "api_calls": 1} + + [redacted:email] +async def test_terminal_progress_is_truncated_preview_not_bash_block(monkeypatch, tmp_path): + """Regression for #41215: terminal progress must render as a short truncated + preview, never the full command in a fenced ```bash block, even on a + markdown-capable (supports_code_blocks) gateway.""" + 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 = TerminalCommandAgent + monkeypatch.setitem(sys.modules, "run_agent", fake_run_agent) + import tools.terminal_tool # noqa: F401 - register terminal emoji + + adapter = CodeBlockProgressAdapter(platform=Platform.TELEGRAM) + 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 = await runner._run_agent( + message="hello", + context_prompt="", + history=[], + source=source, + session_id="sess-terminal-no-bash-block", + session_key="agent:main:telegram:dm:12345", + ) + + assert result["final_response"] == "done" + all_content = " ".join(call["content"] for call in adapter.sent) + all_content += " ".join(call["content"] for call in adapter.edits) + # Compact truncated preview, not a fenced bash block. + assert "```bash" not in all_content + assert 'terminal: "' in all_content + # The full multi-line command body must not reach the chat. + assert "npm install -g hyperframes@latest" not in all_content + + [redacted:email] +async def test_terminal_progress_no_bash_block_in_verbose_mode(monkeypatch, tmp_path): + """#41215 also rendered the bash block in verbose mode. The revert removed it + from both branches, so verbose progress must not emit a fenced ```bash block + either (verbose still shows args by opt-in, just not as a code block).""" + monkeypatch.setenv("HERMES_TOOL_PROGRESS_MODE", "verbose") + + 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 = TerminalCommandAgent + monkeypatch.setitem(sys.modules, "run_agent", fake_run_agent) + import tools.terminal_tool # noqa: F401 - register terminal emoji + + adapter = CodeBlockProgressAdapter(platform=Platform.TELEGRAM) + 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 = await runner._run_agent( + message="hello", + context_prompt="", + history=[], + source=source, + session_id="sess-terminal-verbose-no-bash", + session_key="agent:main:telegram:dm:12345", + ) + + assert result["final_response"] == "done" + all_content = " ".join(call["content"] for call in adapter.sent) + all_content += " ".join(call["content"] for call in adapter.edits) + assert "```bash" not in all_content
-
-
#42729 fix(gateway): cap terminal code-block preview in non-verbose mode
files:
gateway/run.py,tests/gateway/test_run_progress_topics.pyShow the diff
-
gateway/run.py@@ -13002,13 +13002,20 @@ def progress_callback(event_type: str, tool_name: str = None, preview: str = Non emoji = get_tool_emoji(tool_name, default="⚙️") # Markdown-capable platforms render a terminal command as a fenced - # code block (full command, no truncation) instead of the compact - # `terminal: "cmd…"` preview. Gated on the adapter's - # ``supports_code_blocks`` capability so plain-text platforms keep - # the short line. No language tag is emitted — Slack mrkdwn renders - # the tag as a literal first code line ("bash"), and a bare fence - # renders correctly everywhere that supports blocks. - _code_block = None + # code block instead of the compact `terminal: "cmd…"` preview. + # Gated on the adapter's ``supports_code_blocks`` capability so + # plain-text platforms keep the short line. No language tag is + # emitted — Slack mrkdwn renders the tag as a literal first code + # line ("bash"), and a bare fence renders correctly everywhere + # that supports blocks. + # + # Verbose mode shows the FULL command. Non-verbose ("all"/"new") + # modes still wrap in a fence but truncate to a single line capped + # at ``tool_preview_length`` (default 40) so a long or multi-line + # command doesn't render as a huge block — matching the budget the + # non-terminal preview path already applies (#42634). + _code_block_full = None + _code_block_short = None try: _progress_adapter = self.adapters.get(source.platform) except Exception: @@ -13020,12 +13027,25 @@ def progress_callback(event_type: str, tool_name: str = None, preview: str = Non and isinstance(args.get("command"), str) and args["command"].strip() ): - _code_block = f"{emoji} {tool_name}\n```\n{args['command'].rstrip()}\n```" + from agent.display import get_tool_preview_max_len + _cmd_full = args["command"].rstrip() + _code_block_full = f"{emoji} {tool_name}\n```\n{_cmd_full}\n```" + # Single-line, capped preview for non-verbose modes. + _pl = get_tool_preview_max_len() + _cap = _pl if _pl > 0 else 40 + _lines = _cmd_full.splitlines() + _cmd_short = _lines[0] if _lines else _cmd_full + _multiline = len(_lines) > 1 + if len(_cmd_short) > _cap: + _cmd_short = _cmd_short[:_cap - 3] + "..." + elif _multiline: + _cmd_short = _cmd_short + " ..." + _code_block_short = f"{emoji} {tool_name}\n```\n{_cmd_short}\n```" # Verbose mode: show detailed arguments, respects tool_preview_length if progress_mode == "verbose": - if _code_block is not None: - progress_queue.put(_code_block) + if _code_block_full is not None: + progress_queue.put(_code_block_full) return if args: from agent.display import get_tool_preview_max_len @@ -13047,10 +13067,10 @@ def progress_callback(event_type: str, tool_name: str = None, preview: str = Non # "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). - # Terminal commands on markdown platforms get the full fenced block - # built above instead of the truncated preview. - if _code_block is not None: - msg = _code_block + # Terminal commands on markdown platforms get a single-line capped + # fenced block (built above) instead of the truncated preview. + if _code_block_short is not None: + msg = _code_block_short elif preview: from agent.display import get_tool_preview_max_len _pl = get_tool_preview_max_len() -
tests/gateway/test_run_progress_topics.py@@ -1297,8 +1297,10 @@ def run_conversation(self, message, conversation_history=None, task_id=None): @pytest.mark.asyncio async def test_terminal_progress_renders_fenced_code_block(monkeypatch, tmp_path): """Terminal progress on a markdown-capable (supports_code_blocks) gateway - renders the full command in a bare fenced code block — no language tag - (Slack mrkdwn would print 'bash' as a literal first code line).""" + renders a bare fenced code block — no language tag (Slack mrkdwn would print + 'bash' as a literal first code line). In non-verbose ("all"/"new") mode the + command is collapsed to a single line capped at tool_preview_length so a long + or multi-line command doesn't render as a huge block (#42634).""" monkeypatch.setenv("HERMES_TOOL_PROGRESS_MODE", "all") fake_dotenv = types.ModuleType("dotenv") @@ -1338,12 +1340,63 @@ async def test_terminal_progress_renders_fenced_code_block(monkeypatch, tmp_path # Bare fenced block, no language tag (no '```bash'). assert "```" in all_content assert "```bash" not in all_content - # The full multi-line command body IS present in the block. - assert "npm install -g hyperframes@latest" in all_content + # Non-verbose collapses to the first line + truncation marker — the later + # command lines must NOT appear (this was the "huge block" regression). + assert "set -euo pipefail" in all_content + assert "npm install -g hyperframes@latest" not in all_content + assert "node --version" not in all_content # No truncated quoted preview for the terminal command. assert 'terminal: "' not in all_content [redacted:email] +async def test_terminal_progress_verbose_shows_full_command(monkeypatch, tmp_path): + """Verbose mode on a markdown-capable gateway renders the FULL multi-line + command in a bare fenced block (no truncation, no 'bash' tag). This is the + parity guarantee for #42634: verbose keeps full detail, non-verbose caps.""" + monkeypatch.setenv("HERMES_TOOL_PROGRESS_MODE", "verbose") + + 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 = TerminalCommandAgent + monkeypatch.setitem(sys.modules, "run_agent", fake_run_agent) + import tools.terminal_tool # noqa: F401 - register terminal emoji + + adapter = CodeBlockProgressAdapter(platform=Platform.TELEGRAM) + 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 = await runner._run_agent( + message="hello", + context_prompt="", + history=[], + source=source, + session_id="sess-terminal-code-block-verbose", + session_key="agent:main:telegram:dm:12345", + ) + + assert result["final_response"] == "done" + all_content = " ".join(call["content"] for call in adapter.sent) + all_content += " ".join(call["content"] for call in adapter.edits) + assert "```" in all_content + assert "```bash" not in all_content + # Full command body present — verbose is uncapped. + assert "npm install -g hyperframes@latest" in all_content + assert "node --version" in all_content + + @pytest.mark.asyncio async def test_terminal_progress_no_bash_block_in_verbose_mode(monkeypatch, tmp_path): """#41215 also rendered the bash block in verbose mode. The revert removed it
-
-
#114914 fix(acp): every tool call reaches a terminal status before the turn ends (#114395, salvage #114442)
files:
acp_adapter/edit_approval.py,acp_adapter/events.py,acp_adapter/permissions.py,acp_adapter/server.py,acp_adapter/tools.py,contributors/emails/[redacted:email],tests/acp_adapter/test_events.py,tests/acp_adapter/test_permissions.py,tests/acp_adapter/test_server.py,website/docs/developer-guide/acp-internals.md
7 PRs closed without merging
From the close comments: superseded by another PR 2 · done elsewhere 1 · duplicate 1 · merged via another change 1 · other 2
- #13599 fix(gateway): update Telegram progress lines on tool completion · other
-
#41733 fix(gateway): keep Discord terminal progress compact · superseded by another PR →
#42634,#54432 - #41794 fix(progress): skip fenced bash block in compact tool progress mode (#41732) · other
-
#41950 fix(gateway): stop terminal progress from posting the full command to messaging chats · merged via another change →
421226e,#42420,#41215,#41955,#41732 -
#42638 feat(gateway): add per-platform terminal_progress knob to keep terminal commands compact in chat · done elsewhere →
#42729
Closed issues · 3
- Closed#41732Discord compact tool progress leaks terminal commands as code blocks
- Closed#41955Gateway tool progress leaks raw shell commands and code bodies to messaging chats
- Closed#42637[Bug]: Terminal tool progress posts the full command as a fenced code block to messaging chats (regression after #42576)
How this cluster was grouped
-
Link evidence (
partly structural links): some members are connected by structural evidence (shared changed files, or a closing/superseded reference); the rest are joined by an automated relevance judgement over their text. - Members outside that structural core may belong to a different underlying problem.
-
Pairwise link density
0.12: the share of item pairs that carry a link, counting a structural link in full and a relevance-only link at 0.36.1.00means every pair is linked.
Pick up a Lab task
Check whether a fix covers the open issues here, with your own agent. Bring back evidence for maintainers.
Open the Lab