From 4970705ed383118bae290fa56745468272735138 Mon Sep 17 00:00:00 2001 From: adybag14-cyber <252811164+adybag14-cyber@users.noreply.github.com> Date: Thu, 9 Apr 2026 11:08:46 +0200 Subject: [PATCH] fix(termux): silence quiet chat tool previews --- run_agent.py | 35 ++++++++++++++++++++----------- tests/run_agent/test_run_agent.py | 29 +++++++++++++++++++++++++ 2 files changed, 52 insertions(+), 12 deletions(-) diff --git a/run_agent.py b/run_agent.py index db3f4b310..fcaa67f6e 100644 --- a/run_agent.py +++ b/run_agent.py @@ -1486,6 +1486,17 @@ class AIAgent: except (AttributeError, ValueError, OSError): return False + def _should_emit_quiet_tool_messages(self) -> bool: + """Return True when quiet-mode tool summaries should print directly. + + When the caller provides ``tool_progress_callback`` (for example the CLI + TUI or a gateway progress renderer), that callback owns progress display. + Emitting quiet-mode summary lines here duplicates progress and leaks tool + previews into flows that are expected to stay silent, such as + ``hermes chat -q``. + """ + return self.quiet_mode and not self.tool_progress_callback + def _emit_status(self, message: str) -> None: """Emit a lifecycle status message to both CLI and gateway channels. @@ -6347,7 +6358,7 @@ class AIAgent: # Start spinner for CLI mode (skip when TUI handles tool progress) spinner = None - if self.quiet_mode and not self.tool_progress_callback and self._should_start_quiet_spinner(): + if self._should_emit_quiet_tool_messages() and self._should_start_quiet_spinner(): face = random.choice(KawaiiSpinner.KAWAII_WAITING) spinner = KawaiiSpinner(f"{face} ⚡ running {num_tools} tools concurrently", spinner_type='dots', print_fn=self._print_fn) spinner.start() @@ -6397,7 +6408,7 @@ class AIAgent: logging.debug(f"Tool result ({len(function_result)} chars): {function_result}") # Print cute message per tool - if self.quiet_mode: + if self._should_emit_quiet_tool_messages(): cute_msg = _get_cute_tool_message_impl(name, args, tool_duration, result=function_result) self._safe_print(f" {cute_msg}") elif not self.quiet_mode: @@ -6554,7 +6565,7 @@ class AIAgent: store=self._todo_store, ) tool_duration = time.time() - tool_start_time - if self.quiet_mode: + if self._should_emit_quiet_tool_messages(): self._vprint(f" {_get_cute_tool_message_impl('todo', function_args, tool_duration, result=function_result)}") elif function_name == "session_search": if not self._session_db: @@ -6569,7 +6580,7 @@ class AIAgent: current_session_id=self.session_id, ) tool_duration = time.time() - tool_start_time - if self.quiet_mode: + if self._should_emit_quiet_tool_messages(): self._vprint(f" {_get_cute_tool_message_impl('session_search', function_args, tool_duration, result=function_result)}") elif function_name == "memory": target = function_args.get("target", "memory") @@ -6582,7 +6593,7 @@ class AIAgent: store=self._memory_store, ) tool_duration = time.time() - tool_start_time - if self.quiet_mode: + if self._should_emit_quiet_tool_messages(): self._vprint(f" {_get_cute_tool_message_impl('memory', function_args, tool_duration, result=function_result)}") elif function_name == "clarify": from tools.clarify_tool import clarify_tool as _clarify_tool @@ -6592,7 +6603,7 @@ class AIAgent: callback=self.clarify_callback, ) tool_duration = time.time() - tool_start_time - if self.quiet_mode: + if self._should_emit_quiet_tool_messages(): self._vprint(f" {_get_cute_tool_message_impl('clarify', function_args, tool_duration, result=function_result)}") elif function_name == "delegate_task": from tools.delegate_tool import delegate_task as _delegate_task @@ -6603,7 +6614,7 @@ class AIAgent: goal_preview = (function_args.get("goal") or "")[:30] spinner_label = f"🔀 {goal_preview}" if goal_preview else "🔀 delegating" spinner = None - if self.quiet_mode and not self.tool_progress_callback and self._should_start_quiet_spinner(): + if self._should_emit_quiet_tool_messages() and self._should_start_quiet_spinner(): face = random.choice(KawaiiSpinner.KAWAII_WAITING) spinner = KawaiiSpinner(f"{face} {spinner_label}", spinner_type='dots', print_fn=self._print_fn) spinner.start() @@ -6625,13 +6636,13 @@ class AIAgent: cute_msg = _get_cute_tool_message_impl('delegate_task', function_args, tool_duration, result=_delegate_result) if spinner: spinner.stop(cute_msg) - elif self.quiet_mode: + elif self._should_emit_quiet_tool_messages(): self._vprint(f" {cute_msg}") elif self._memory_manager and self._memory_manager.has_tool(function_name): # Memory provider tools (hindsight_retain, honcho_search, etc.) # These are not in the tool registry — route through MemoryManager. spinner = None - if self.quiet_mode and not self.tool_progress_callback: + if self._should_emit_quiet_tool_messages() and self._should_start_quiet_spinner(): face = random.choice(KawaiiSpinner.KAWAII_WAITING) emoji = _get_tool_emoji(function_name) preview = _build_tool_preview(function_name, function_args) or function_name @@ -6649,11 +6660,11 @@ class AIAgent: cute_msg = _get_cute_tool_message_impl(function_name, function_args, tool_duration, result=_mem_result) if spinner: spinner.stop(cute_msg) - elif self.quiet_mode: + elif self._should_emit_quiet_tool_messages(): self._vprint(f" {cute_msg}") elif self.quiet_mode: spinner = None - if not self.tool_progress_callback: + if self._should_emit_quiet_tool_messages() and self._should_start_quiet_spinner(): face = random.choice(KawaiiSpinner.KAWAII_WAITING) emoji = _get_tool_emoji(function_name) preview = _build_tool_preview(function_name, function_args) or function_name @@ -6676,7 +6687,7 @@ class AIAgent: cute_msg = _get_cute_tool_message_impl(function_name, function_args, tool_duration, result=_spinner_result) if spinner: spinner.stop(cute_msg) - else: + elif self._should_emit_quiet_tool_messages(): self._vprint(f" {cute_msg}") else: try: diff --git a/tests/run_agent/test_run_agent.py b/tests/run_agent/test_run_agent.py index 98d799ae4..e58170c80 100644 --- a/tests/run_agent/test_run_agent.py +++ b/tests/run_agent/test_run_agent.py @@ -1061,6 +1061,35 @@ class TestExecuteToolCalls: assert len(messages[0]["content"]) < 150_000 assert ("Truncated" in messages[0]["content"] or "" in messages[0]["content"]) + def test_quiet_tool_output_suppressed_when_progress_callback_present(self, agent): + tc = _mock_tool_call(name="web_search", arguments='{"q":"test"}', call_id="c1") + mock_msg = _mock_assistant_msg(content="", tool_calls=[tc]) + messages = [] + agent.tool_progress_callback = lambda *args, **kwargs: None + + with patch("run_agent.handle_function_call", return_value="search result"), \ + patch.object(agent, "_safe_print") as mock_print: + agent._execute_tool_calls(mock_msg, messages, "task-1") + + mock_print.assert_not_called() + assert len(messages) == 1 + assert messages[0]["role"] == "tool" + + def test_quiet_tool_output_prints_without_progress_callback(self, agent): + tc = _mock_tool_call(name="web_search", arguments='{"q":"test"}', call_id="c1") + mock_msg = _mock_assistant_msg(content="", tool_calls=[tc]) + messages = [] + agent.tool_progress_callback = None + + with patch("run_agent.handle_function_call", return_value="search result"), \ + patch.object(agent, "_safe_print") as mock_print: + agent._execute_tool_calls(mock_msg, messages, "task-1") + + mock_print.assert_called_once() + assert "search" in str(mock_print.call_args.args[0]).lower() + assert len(messages) == 1 + assert messages[0]["role"] == "tool" + class TestConcurrentToolExecution: """Tests for _execute_tool_calls_concurrent and dispatch logic."""