fix(termux): silence quiet chat tool previews

This commit is contained in:
adybag14-cyber 2026-04-09 11:08:46 +02:00 committed by Teknium
parent 2194425918
commit 4970705ed3
2 changed files with 52 additions and 12 deletions

View file

@ -1486,6 +1486,17 @@ class AIAgent:
except (AttributeError, ValueError, OSError): except (AttributeError, ValueError, OSError):
return False 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: def _emit_status(self, message: str) -> None:
"""Emit a lifecycle status message to both CLI and gateway channels. """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) # Start spinner for CLI mode (skip when TUI handles tool progress)
spinner = None 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) face = random.choice(KawaiiSpinner.KAWAII_WAITING)
spinner = KawaiiSpinner(f"{face} ⚡ running {num_tools} tools concurrently", spinner_type='dots', print_fn=self._print_fn) spinner = KawaiiSpinner(f"{face} ⚡ running {num_tools} tools concurrently", spinner_type='dots', print_fn=self._print_fn)
spinner.start() spinner.start()
@ -6397,7 +6408,7 @@ class AIAgent:
logging.debug(f"Tool result ({len(function_result)} chars): {function_result}") logging.debug(f"Tool result ({len(function_result)} chars): {function_result}")
# Print cute message per tool # 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) cute_msg = _get_cute_tool_message_impl(name, args, tool_duration, result=function_result)
self._safe_print(f" {cute_msg}") self._safe_print(f" {cute_msg}")
elif not self.quiet_mode: elif not self.quiet_mode:
@ -6554,7 +6565,7 @@ class AIAgent:
store=self._todo_store, store=self._todo_store,
) )
tool_duration = time.time() - tool_start_time 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)}") self._vprint(f" {_get_cute_tool_message_impl('todo', function_args, tool_duration, result=function_result)}")
elif function_name == "session_search": elif function_name == "session_search":
if not self._session_db: if not self._session_db:
@ -6569,7 +6580,7 @@ class AIAgent:
current_session_id=self.session_id, current_session_id=self.session_id,
) )
tool_duration = time.time() - tool_start_time 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)}") self._vprint(f" {_get_cute_tool_message_impl('session_search', function_args, tool_duration, result=function_result)}")
elif function_name == "memory": elif function_name == "memory":
target = function_args.get("target", "memory") target = function_args.get("target", "memory")
@ -6582,7 +6593,7 @@ class AIAgent:
store=self._memory_store, store=self._memory_store,
) )
tool_duration = time.time() - tool_start_time 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)}") self._vprint(f" {_get_cute_tool_message_impl('memory', function_args, tool_duration, result=function_result)}")
elif function_name == "clarify": elif function_name == "clarify":
from tools.clarify_tool import clarify_tool as _clarify_tool from tools.clarify_tool import clarify_tool as _clarify_tool
@ -6592,7 +6603,7 @@ class AIAgent:
callback=self.clarify_callback, callback=self.clarify_callback,
) )
tool_duration = time.time() - tool_start_time 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)}") self._vprint(f" {_get_cute_tool_message_impl('clarify', function_args, tool_duration, result=function_result)}")
elif function_name == "delegate_task": elif function_name == "delegate_task":
from tools.delegate_tool import delegate_task as _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] goal_preview = (function_args.get("goal") or "")[:30]
spinner_label = f"🔀 {goal_preview}" if goal_preview else "🔀 delegating" spinner_label = f"🔀 {goal_preview}" if goal_preview else "🔀 delegating"
spinner = None 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) face = random.choice(KawaiiSpinner.KAWAII_WAITING)
spinner = KawaiiSpinner(f"{face} {spinner_label}", spinner_type='dots', print_fn=self._print_fn) spinner = KawaiiSpinner(f"{face} {spinner_label}", spinner_type='dots', print_fn=self._print_fn)
spinner.start() spinner.start()
@ -6625,13 +6636,13 @@ class AIAgent:
cute_msg = _get_cute_tool_message_impl('delegate_task', function_args, tool_duration, result=_delegate_result) cute_msg = _get_cute_tool_message_impl('delegate_task', function_args, tool_duration, result=_delegate_result)
if spinner: if spinner:
spinner.stop(cute_msg) spinner.stop(cute_msg)
elif self.quiet_mode: elif self._should_emit_quiet_tool_messages():
self._vprint(f" {cute_msg}") self._vprint(f" {cute_msg}")
elif self._memory_manager and self._memory_manager.has_tool(function_name): elif self._memory_manager and self._memory_manager.has_tool(function_name):
# Memory provider tools (hindsight_retain, honcho_search, etc.) # Memory provider tools (hindsight_retain, honcho_search, etc.)
# These are not in the tool registry — route through MemoryManager. # These are not in the tool registry — route through MemoryManager.
spinner = None 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) face = random.choice(KawaiiSpinner.KAWAII_WAITING)
emoji = _get_tool_emoji(function_name) emoji = _get_tool_emoji(function_name)
preview = _build_tool_preview(function_name, function_args) or 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) cute_msg = _get_cute_tool_message_impl(function_name, function_args, tool_duration, result=_mem_result)
if spinner: if spinner:
spinner.stop(cute_msg) spinner.stop(cute_msg)
elif self.quiet_mode: elif self._should_emit_quiet_tool_messages():
self._vprint(f" {cute_msg}") self._vprint(f" {cute_msg}")
elif self.quiet_mode: elif self.quiet_mode:
spinner = None 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) face = random.choice(KawaiiSpinner.KAWAII_WAITING)
emoji = _get_tool_emoji(function_name) emoji = _get_tool_emoji(function_name)
preview = _build_tool_preview(function_name, function_args) or 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) cute_msg = _get_cute_tool_message_impl(function_name, function_args, tool_duration, result=_spinner_result)
if spinner: if spinner:
spinner.stop(cute_msg) spinner.stop(cute_msg)
else: elif self._should_emit_quiet_tool_messages():
self._vprint(f" {cute_msg}") self._vprint(f" {cute_msg}")
else: else:
try: try:

View file

@ -1061,6 +1061,35 @@ class TestExecuteToolCalls:
assert len(messages[0]["content"]) < 150_000 assert len(messages[0]["content"]) < 150_000
assert ("Truncated" in messages[0]["content"] or "<persisted-output>" in messages[0]["content"]) assert ("Truncated" in messages[0]["content"] or "<persisted-output>" 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: class TestConcurrentToolExecution:
"""Tests for _execute_tool_calls_concurrent and dispatch logic.""" """Tests for _execute_tool_calls_concurrent and dispatch logic."""