fix(cli): announce "preparing <tool>…" once per tool per batch

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
This commit is contained in:
teknium1
2026-09-12 07:02:45 -07:00
committed by Teknium
parent 67bd2f6571
commit cc269866aa
2 changed files with 36 additions and 2 deletions
+11 -2
View File
@@ -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"):
+25
View File
@@ -0,0 +1,25 @@
"""A batch of parallel tool calls announces "preparing <tool>…" 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