From cc269866aa672e424b541a3115a2f69f08db7f60 Mon Sep 17 00:00:00 2001 From: teknium1 <127238744+teknium1@users.noreply.github.com> Date: Sat, 12 Sep 2026 07:02:45 -0700 Subject: [PATCH] =?UTF-8?q?fix(cli):=20announce=20"preparing=20?= =?UTF-8?q?=E2=80=A6"=20once=20per=20tool=20per=20batch?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit The tool-gen callback fires once per tool CALL, so a model issuing three parallel terminal calls printed three identical status lines before the spinner took over (#10478). Coalesce repeats of the same tool name until a tool actually starts (tool.started), which marks the next generation batch. The line itself stays: it is the only feedback while a large argument payload (e.g. a 45 KB write_file) streams, before the spinner exists. PR #10598 removed it entirely; this keeps the first line and drops only the duplicates. Live A/B (in-process HTTP mock streaming 3 parallel terminal calls into a real HermesCLI with display.streaming on): before 3 lines, after 1. Fixes #10478 --- hermes_cli/cli_stream_mixin.py | 13 +++++++++++-- tests/cli/test_tool_gen_start_dedupe.py | 25 +++++++++++++++++++++++++ 2 files changed, 36 insertions(+), 2 deletions(-) create mode 100644 tests/cli/test_tool_gen_start_dedupe.py diff --git a/hermes_cli/cli_stream_mixin.py b/hermes_cli/cli_stream_mixin.py index 9e847a6375..2a63ddefa3 100644 --- a/hermes_cli/cli_stream_mixin.py +++ b/hermes_cli/cli_stream_mixin.py @@ -621,12 +621,20 @@ class CLIStreamMixin: def _on_tool_gen_start(self, tool_name: str) -> None: """Model began generating tool-call arguments: close open boxes once, then print a status - line so a large payload (e.g. 45 KB write_file) doesn't look like a frozen screen.""" + line so a large payload (e.g. 45 KB write_file) doesn't look like a frozen screen. + + Fires once per tool CALL, so a batch of parallel calls to the same tool printed the same + line N times (#10478); repeats within one generation batch are coalesced. The set is + cleared when a tool actually starts (``tool.started``), i.e. on the next batch.""" from cli import _cprint - if getattr(self, "_stream_box_opened", False): + if getattr(self, '_stream_box_opened', False): self._flush_stream() self._stream_box_opened = False self._close_reasoning_box() + announced = self.__dict__.setdefault("_tool_gen_announced", set()) + if tool_name in announced: + return + announced.add(tool_name) from agent.display import get_tool_emoji _cprint(f" ┊ {get_tool_emoji(tool_name, default='⚡')} preparing {tool_name}…") @@ -666,6 +674,7 @@ class CLIStreamMixin: # Feed the pet: tools mean "running"; a failed tool latches the turn to end on a sulk. if event_type == "tool.started": self._pet_reasoning = False + self.__dict__.pop("_tool_gen_announced", None) elif event_type == "tool.completed" and kwargs.get("is_error"): self._pet_turn_error = True elif event_type and event_type.startswith("reasoning"): diff --git a/tests/cli/test_tool_gen_start_dedupe.py b/tests/cli/test_tool_gen_start_dedupe.py new file mode 100644 index 0000000000..5bea4d8a5e --- /dev/null +++ b/tests/cli/test_tool_gen_start_dedupe.py @@ -0,0 +1,25 @@ +"""A batch of parallel tool calls announces "preparing …" once per tool, not once per call (#10478).""" + +from unittest.mock import patch + +from tests.cli.test_tool_progress_scrollback import _make_cli +import tests.cli.test_tool_progress_scrollback as _scrollback + + +def _announce(cli, names): + printed = [] + with patch.object(_scrollback._cli_mod, "_cprint", lambda line: printed.append(line)): + for n in names: + cli._on_tool_gen_start(n) + return printed + + +def test_repeated_tool_in_one_batch_prints_once(): + cli = _make_cli(tool_progress="off") + printed = _announce(cli, ["terminal", "terminal", "terminal", "read_file"]) + assert sum("preparing terminal" in p for p in printed) == 1 + assert sum("preparing read_file" in p for p in printed) == 1 + # A tool actually starting closes the batch; the next generation announces again. + with patch.object(_scrollback._cli_mod, "_cprint", lambda line: None): + cli._on_tool_progress("tool.started", "terminal", "ls", {"command": "ls"}) + assert sum("preparing terminal" in p for p in _announce(cli, ["terminal"])) == 1