From cd59af17cc095da08b223a9378c4a1621f7c0393 Mon Sep 17 00:00:00 2001 From: helix4u <4317663+helix4u@users.noreply.github.com> Date: Sat, 18 Apr 2026 14:28:50 -0600 Subject: [PATCH] fix(agent): silence quiet_mode in python library use --- run_agent.py | 17 +++++++------ tests/run_agent/test_run_agent.py | 40 +++++++++++++++++++++++++++++++ 2 files changed, 50 insertions(+), 7 deletions(-) diff --git a/run_agent.py b/run_agent.py index 01064880..050faeea 100644 --- a/run_agent.py +++ b/run_agent.py @@ -1916,13 +1916,16 @@ class AIAgent: 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``. + Quiet mode is used by both the interactive CLI and embedded/library + callers. The CLI may still want compact progress hints when no callback + owns rendering. Embedded/library callers, on the other hand, expect + quiet mode to be truly silent. """ - return self.quiet_mode and not self.tool_progress_callback + return ( + self.quiet_mode + and not self.tool_progress_callback + and getattr(self, "platform", "") == "cli" + ) def _emit_status(self, message: str) -> None: """Emit a lifecycle status message to both CLI and gateway channels. @@ -11184,7 +11187,7 @@ class AIAgent: self._last_content_tools_all_housekeeping = _all_housekeeping if _all_housekeeping and self._has_stream_consumers(): self._mute_post_response = True - elif self.quiet_mode: + elif self.quiet_mode and getattr(self, "platform", "") == "cli": clean = self._strip_think_blocks(turn_content).strip() if clean: relayed = False diff --git a/tests/run_agent/test_run_agent.py b/tests/run_agent/test_run_agent.py index d30445cf..bedb7bbf 100644 --- a/tests/run_agent/test_run_agent.py +++ b/tests/run_agent/test_run_agent.py @@ -1285,6 +1285,7 @@ class TestExecuteToolCalls: tc = _mock_tool_call(name="web_search", arguments='{"q":"test"}', call_id="c1") mock_msg = _mock_assistant_msg(content="", tool_calls=[tc]) messages = [] + agent.platform = "cli" agent.tool_progress_callback = None with patch("run_agent.handle_function_call", return_value="search result"), \ @@ -1296,6 +1297,21 @@ class TestExecuteToolCalls: assert len(messages) == 1 assert messages[0]["role"] == "tool" + def test_quiet_tool_output_suppressed_without_progress_callback_for_non_cli_agent(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.platform = None + 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_not_called() + assert len(messages) == 1 + assert messages[0]["role"] == "tool" + def test_vprint_suppressed_in_parseable_quiet_mode(self, agent): agent.suppress_status_output = True @@ -1876,6 +1892,30 @@ class TestRunConversation: assert all("message_count" in c and "messages" not in c for c in pre_request_calls) assert all("usage" in c and "response" not in c for c in post_request_calls) + def test_content_with_tool_calls_stays_silent_for_non_cli_quiet_mode(self, agent): + self._setup_agent(agent) + agent.platform = None + tc = _mock_tool_call(name="web_search", arguments="{}", call_id="c1") + resp1 = _mock_response( + content="I'll search for that.", + finish_reason="tool_calls", + tool_calls=[tc], + ) + resp2 = _mock_response(content="Done searching", finish_reason="stop") + agent.client.chat.completions.create.side_effect = [resp1, resp2] + + with ( + patch("run_agent.handle_function_call", return_value="search result"), + patch.object(agent, "_safe_print") as mock_print, + patch.object(agent, "_persist_session"), + patch.object(agent, "_save_trajectory"), + patch.object(agent, "_cleanup_task_resources"), + ): + result = agent.run_conversation("search something") + + assert result["final_response"] == "Done searching" + mock_print.assert_not_called() + def test_interrupt_breaks_loop(self, agent): self._setup_agent(agent)