From 560311b6691e96108bedeb862a6c9134ad7e82a1 Mon Sep 17 00:00:00 2001 From: Patrick Nilan Date: Fri, 13 Mar 2026 11:34:10 -0700 Subject: [PATCH] Use stderr Console for clean tool-call indicators MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Replace logger.info with a Rich Console(stderr=True) so tool-call indicators print as clean "🛠️ tool call: " lines without logging metadata. The logger is now only used for verbose debug args. --- patchwork/cli.py | 4 ++-- tests/test_tool_logging.py | 23 ++++++++++++----------- 2 files changed, 14 insertions(+), 13 deletions(-) diff --git a/patchwork/cli.py b/patchwork/cli.py index 8847ca2..cd51845 100644 --- a/patchwork/cli.py +++ b/patchwork/cli.py @@ -16,6 +16,7 @@ from patchwork.synth_definitions import load_synth_definitions console = Console() +stderr_console = Console(stderr=True) def _make_event_handler(verbose: bool, logger: logging.Logger): @@ -25,7 +26,7 @@ async def handle_events(ctx: RunContext[PatchworkDeps], events: AsyncIterable) - async for event in events: if isinstance(event, FunctionToolCallEvent): tool_name = event.part.tool_name - logger.info("tool call: %s", tool_name) + stderr_console.print(f"[dim]\U0001f6e0\ufe0f tool call: {tool_name}[/dim]") if verbose: try: @@ -33,7 +34,6 @@ async def handle_events(ctx: RunContext[PatchworkDeps], events: AsyncIterable) - except Exception: args = event.part.args logger.debug("tool args: %s %s", tool_name, json.dumps(args, default=str)) - console.print(f"[dim]⚙ {tool_name}[/dim]") return handle_events diff --git a/tests/test_tool_logging.py b/tests/test_tool_logging.py index 9db451e..3545360 100644 --- a/tests/test_tool_logging.py +++ b/tests/test_tool_logging.py @@ -33,15 +33,15 @@ def logger(): class TestToolCallEventHandler: @pytest.mark.asyncio - async def test_logs_tool_name(self, logger, caplog): + async def test_logs_tool_name_to_stderr(self, logger, capsys): handler = _make_event_handler(verbose=False, logger=logger) event = _make_mock_tool_call_event("send_cc") ctx = MagicMock() - with caplog.at_level(logging.INFO, logger=logger.name): - await handler(ctx, _to_async_iterable([event])) + await handler(ctx, _to_async_iterable([event])) - assert any("send_cc" in record.message for record in caplog.records) + captured = capsys.readouterr() + assert "send_cc" in captured.err @pytest.mark.asyncio async def test_verbose_logs_args(self, logger, caplog): @@ -72,7 +72,7 @@ async def test_ignores_non_tool_events(self, logger, caplog): assert len(tool_records) == 0 @pytest.mark.asyncio - async def test_handles_multiple_events(self, logger, caplog): + async def test_handles_multiple_events(self, logger, capsys): handler = _make_event_handler(verbose=False, logger=logger) events = [ _make_mock_tool_call_event("list_synths"), @@ -80,14 +80,14 @@ async def test_handles_multiple_events(self, logger, caplog): ] ctx = MagicMock() - with caplog.at_level(logging.INFO, logger=logger.name): - await handler(ctx, _to_async_iterable(events)) + await handler(ctx, _to_async_iterable(events)) - tool_records = [r for r in caplog.records if "tool call" in r.message] - assert len(tool_records) == 2 + captured = capsys.readouterr() + assert "list_synths" in captured.err + assert "send_cc" in captured.err @pytest.mark.asyncio - async def test_non_verbose_does_not_print_tool_indicator(self, logger, capsys): + async def test_tool_indicator_prints_to_stderr(self, logger, capsys): handler = _make_event_handler(verbose=False, logger=logger) event = _make_mock_tool_call_event("send_cc") ctx = MagicMock() @@ -95,4 +95,5 @@ async def test_non_verbose_does_not_print_tool_indicator(self, logger, capsys): await handler(ctx, _to_async_iterable([event])) captured = capsys.readouterr() - assert "⚙" not in captured.out + assert "\U0001f6e0\ufe0f" not in captured.out + assert "send_cc" in captured.err