From 20cdc92142aa97a6eaed450d75fc4cb4d2597c25 Mon Sep 17 00:00:00 2001 From: Devin AI <158243242+devin-ai-integration[bot]@users.noreply.github.com> Date: Fri, 11 Jul 2025 08:04:32 +0000 Subject: [PATCH] fix: properly cleanup Rich Live sessions after flow/crew completion - Add stop_live() method to ConsoleFormatter to clean up Live sessions - Call cleanup in FlowFinishedEvent and CrewKickoffCompletedEvent handlers - Add comprehensive tests for Live session cleanup functionality - Fixes issue #3136 where logging output was suppressed after CrewAI operations The issue was that Rich Live sessions were not being explicitly stopped when CrewAI flows or crews completed, leaving the terminal in a state where subsequent logging output would be suppressed until process exit. This fix ensures that Live sessions are properly cleaned up by: 1. Adding a stop_live() method that safely stops and clears Live sessions 2. Calling this cleanup method in the appropriate event handlers 3. Adding tests to prevent regression Resolves #3136 Co-Authored-By: Jo\u00E3o --- src/crewai/utilities/events/event_listener.py | 2 + .../events/utils/console_formatter.py | 6 ++ tests/test_rich_live_cleanup.py | 57 +++++++++++++++++++ .../test_console_formatter_pause_resume.py | 35 ++++++++++++ 4 files changed, 100 insertions(+) create mode 100644 tests/test_rich_live_cleanup.py diff --git a/src/crewai/utilities/events/event_listener.py b/src/crewai/utilities/events/event_listener.py index 1917eca1f..bb57bb03c 100644 --- a/src/crewai/utilities/events/event_listener.py +++ b/src/crewai/utilities/events/event_listener.py @@ -120,6 +120,7 @@ class EventListener(BaseEventListener): "completed", final_string_output, ) + self.formatter.stop_live() @crewai_event_bus.on(CrewKickoffFailedEvent) def on_crew_failed(source, event: CrewKickoffFailedEvent): @@ -262,6 +263,7 @@ class EventListener(BaseEventListener): self.formatter.update_flow_status( self.formatter.current_flow_tree, event.flow_name, source.flow_id ) + self.formatter.stop_live() @crewai_event_bus.on(MethodExecutionStartedEvent) def on_method_execution_started(source, event: MethodExecutionStartedEvent): diff --git a/src/crewai/utilities/events/utils/console_formatter.py b/src/crewai/utilities/events/utils/console_formatter.py index 24f92e386..9a7e8e356 100644 --- a/src/crewai/utilities/events/utils/console_formatter.py +++ b/src/crewai/utilities/events/utils/console_formatter.py @@ -1753,3 +1753,9 @@ class ConsoleFormatter: Attempts=f"{retry_count + 1}", ) self.print_panel(content, "🛡️ Guardrail Failed", "red") + + def stop_live(self) -> None: + """Stop and clear any active Live session to restore normal terminal output.""" + if self._live: + self._live.stop() + self._live = None diff --git a/tests/test_rich_live_cleanup.py b/tests/test_rich_live_cleanup.py new file mode 100644 index 000000000..04d094870 --- /dev/null +++ b/tests/test_rich_live_cleanup.py @@ -0,0 +1,57 @@ +import logging +from io import StringIO +from unittest.mock import MagicMock, patch +from rich.logging import RichHandler +from rich.tree import Tree +from crewai.utilities.events.utils.console_formatter import ConsoleFormatter +from crewai.utilities.events.event_listener import EventListener + + +class TestRichLiveCleanup: + """Test that Rich Live sessions are properly cleaned up after CrewAI operations.""" + + def test_logging_works_after_tree_rendering(self): + """Test that logging output appears after tree rendering with proper cleanup.""" + formatter = ConsoleFormatter() + + tree = Tree("Test Flow") + formatter.print(tree) + + assert formatter._live is not None + + formatter.stop_live() + + assert formatter._live is None + + with patch.object(formatter.console, 'print') as mock_print: + formatter.print("This should appear immediately") + mock_print.assert_called_once_with("This should appear immediately") + + def test_event_listener_cleanup_integration(self): + """Test that EventListener properly cleans up Live sessions.""" + event_listener = EventListener() + formatter = event_listener.formatter + + tree = Tree("Test Crew") + formatter.print(tree) + assert formatter._live is not None + + formatter.stop_live() + assert formatter._live is None + + def test_stop_live_restores_normal_output(self): + """Test that stop_live properly restores normal console output behavior.""" + formatter = ConsoleFormatter() + + tree = Tree("Test Tree") + formatter.print(tree) + + assert formatter._live is not None + + formatter.stop_live() + + assert formatter._live is None + + with patch.object(formatter.console, 'print') as mock_print: + formatter.print("Normal output") + mock_print.assert_called_once_with("Normal output") diff --git a/tests/utilities/test_console_formatter_pause_resume.py b/tests/utilities/test_console_formatter_pause_resume.py index e150671ac..1a1208310 100644 --- a/tests/utilities/test_console_formatter_pause_resume.py +++ b/tests/utilities/test_console_formatter_pause_resume.py @@ -114,3 +114,38 @@ class TestConsoleFormatterPauseResume: assert hasattr(formatter, '_live_paused') assert not formatter._live_paused + + def test_stop_live_with_active_session(self): + """Test stopping Live session when one is active.""" + formatter = ConsoleFormatter() + + mock_live = MagicMock(spec=Live) + formatter._live = mock_live + + formatter.stop_live() + + mock_live.stop.assert_called_once() + assert formatter._live is None + + def test_stop_live_with_no_session(self): + """Test stopping Live session when none exists.""" + formatter = ConsoleFormatter() + + formatter._live = None + + formatter.stop_live() + + assert formatter._live is None + + def test_stop_live_multiple_calls(self): + """Test multiple calls to stop_live are safe.""" + formatter = ConsoleFormatter() + + mock_live = MagicMock(spec=Live) + formatter._live = mock_live + + formatter.stop_live() + formatter.stop_live() + + mock_live.stop.assert_called_once() + assert formatter._live is None