From f19876e62a77cb6253b41566fbf97c0dd6a62630 Mon Sep 17 00:00:00 2001 From: HumphreySun98 Date: Tue, 16 Jun 2026 11:10:04 -0500 Subject: [PATCH 1/3] fix(agents): show final result before human feedback prompt when not verbose With human_input=True the feedback panel asks the operator to review "the Final Result above", but the result is only rendered when the agent or crew runs verbose (AgentLogsExecutionEvent and streaming are both verbose-gated). So a non-verbose human-in-the-loop run prompts the operator to approve output that was never displayed. Render the result through the existing console formatter before each feedback prompt when verbose is off, covering both the sync and async paths and every feedback round. No-op when verbose to avoid a duplicate panel. Fixes #6072 Co-Authored-By: Claude Opus 4.8 (1M context) --- .../src/crewai/core/providers/human_input.py | 32 ++++++++ lib/crewai/tests/core/__init__.py | 0 lib/crewai/tests/core/providers/__init__.py | 0 .../tests/core/providers/test_human_input.py | 81 +++++++++++++++++++ 4 files changed, 113 insertions(+) create mode 100644 lib/crewai/tests/core/__init__.py create mode 100644 lib/crewai/tests/core/providers/__init__.py create mode 100644 lib/crewai/tests/core/providers/test_human_input.py diff --git a/lib/crewai/src/crewai/core/providers/human_input.py b/lib/crewai/src/crewai/core/providers/human_input.py index b82e408d9d..d70fb85735 100644 --- a/lib/crewai/src/crewai/core/providers/human_input.py +++ b/lib/crewai/src/crewai/core/providers/human_input.py @@ -179,6 +179,7 @@ def handle_feedback( Returns: The final answer after feedback processing. """ + self._ensure_result_displayed(formatted_answer, context) feedback = self._prompt_input(context.crew) if context._is_training_mode(): @@ -200,6 +201,7 @@ async def handle_feedback_async( Returns: The final answer after feedback processing. """ + self._ensure_result_displayed(formatted_answer, context) feedback = await self._prompt_input_async(context.crew) if context._is_training_mode(): @@ -259,6 +261,7 @@ def _handle_regular_feedback( else: context.messages.append(context._format_feedback_message(feedback)) answer = context._invoke_loop() + self._ensure_result_displayed(answer, context) feedback = self._prompt_input(context.crew) return answer @@ -311,10 +314,39 @@ async def _handle_regular_feedback_async( else: context.messages.append(context._format_feedback_message(feedback)) answer = await context._ainvoke_loop() + self._ensure_result_displayed(answer, context) feedback = await self._prompt_input_async(context.crew) return answer + @staticmethod + def _ensure_result_displayed( + answer: AgentFinish, + context: ExecutorContext, + ) -> None: + """Render the agent's final result before prompting for feedback. + + The feedback panel asks the operator to review "the Final Result + above", but that result is only rendered when the agent or crew runs + verbose. With ``human_input=True`` and verbose off, the operator would + otherwise be asked to approve output that was never shown (#6072). When + verbose, the result was already displayed, so this is a no-op to avoid + a duplicate panel. + + Args: + answer: The agent answer the operator is being asked to review. + context: Executor context exposing the agent and crew. + """ + agent = getattr(context, "agent", None) + crew = getattr(context, "crew", None) + if getattr(agent, "verbose", False) or getattr(crew, "verbose", False): + return + + from crewai.events.event_listener import event_listener + + role = getattr(agent, "role", "") or "" + event_listener.formatter.handle_agent_logs_execution(role, answer, verbose=True) + @staticmethod def _prompt_input(crew: Crew | None) -> str: """Show rich panel and prompt for input. diff --git a/lib/crewai/tests/core/__init__.py b/lib/crewai/tests/core/__init__.py new file mode 100644 index 0000000000..e69de29bb2 diff --git a/lib/crewai/tests/core/providers/__init__.py b/lib/crewai/tests/core/providers/__init__.py new file mode 100644 index 0000000000..e69de29bb2 diff --git a/lib/crewai/tests/core/providers/test_human_input.py b/lib/crewai/tests/core/providers/test_human_input.py new file mode 100644 index 0000000000..72bdefc060 --- /dev/null +++ b/lib/crewai/tests/core/providers/test_human_input.py @@ -0,0 +1,81 @@ +"""Regression tests for SyncHumanInputProvider result display (#6072).""" + +from __future__ import annotations + +from unittest.mock import MagicMock, patch + +from crewai.agents.parser import AgentFinish +from crewai.core.providers.human_input import SyncHumanInputProvider +from crewai.events.event_listener import event_listener + + +class _FakeAgent: + def __init__(self, verbose: bool) -> None: + self.verbose = verbose + self.role = "Researcher" + + +class _FakeCrew: + def __init__(self, verbose: bool) -> None: + self.verbose = verbose + self._train = False + + +class _FakeContext: + """Minimal ExecutorContext stand-in for the accept-on-first-round path.""" + + def __init__(self, agent: _FakeAgent, crew: _FakeCrew) -> None: + self.agent = agent + self.crew = crew + self.ask_for_human_input = True + self.messages: list = [] + + def _is_training_mode(self) -> bool: + return False + + +def _answer() -> AgentFinish: + return AgentFinish(thought="", output="THE FINAL RESULT", text="THE FINAL RESULT") + + +class TestResultDisplayBeforeFeedback: + def test_result_shown_before_prompt_when_not_verbose(self) -> None: + """Non-verbose + human_input must render the result before prompting, + otherwise the operator is asked to approve output they never saw. + """ + provider = SyncHumanInputProvider() + context = _FakeContext(_FakeAgent(verbose=False), _FakeCrew(verbose=False)) + answer = _answer() + + formatter = MagicMock() + with ( + patch.object(event_listener, "formatter", formatter), + patch("builtins.input", return_value=""), + ): + result = provider.handle_feedback(answer, context) # type: ignore[arg-type] + + formatter.handle_agent_logs_execution.assert_called_once_with( + "Researcher", answer, verbose=True + ) + # The result panel must be rendered BEFORE the feedback prompt panel. + method_calls = [c[0] for c in formatter.mock_calls] + render_idx = method_calls.index("handle_agent_logs_execution") + prompt_idx = method_calls.index("console.print") + assert render_idx < prompt_idx + assert result is answer + + def test_result_not_reshown_when_verbose(self) -> None: + """When verbose, the executor already rendered the result, so the + provider must not re-render it (avoid a duplicate panel). + """ + provider = SyncHumanInputProvider() + context = _FakeContext(_FakeAgent(verbose=False), _FakeCrew(verbose=True)) + + formatter = MagicMock() + with ( + patch.object(event_listener, "formatter", formatter), + patch("builtins.input", return_value=""), + ): + provider.handle_feedback(_answer(), context) # type: ignore[arg-type] + + formatter.handle_agent_logs_execution.assert_not_called() From fa70cd91163b5ae98489041cc3f1921aacc7cc95 Mon Sep 17 00:00:00 2001 From: HumphreySun98 Date: Wed, 24 Jun 2026 12:00:06 -0500 Subject: [PATCH 2/3] test(agents): add async parity for human-feedback result display Cover the async feedback entry path (handle_feedback_async) so the result-before-prompt ordering and the verbose no-op are protected there too, matching the sync coverage. Co-Authored-By: Claude Opus 4.8 (1M context) --- .../tests/core/providers/test_human_input.py | 49 ++++++++++++++++++- 1 file changed, 48 insertions(+), 1 deletion(-) diff --git a/lib/crewai/tests/core/providers/test_human_input.py b/lib/crewai/tests/core/providers/test_human_input.py index 72bdefc060..7440df4358 100644 --- a/lib/crewai/tests/core/providers/test_human_input.py +++ b/lib/crewai/tests/core/providers/test_human_input.py @@ -2,8 +2,11 @@ from __future__ import annotations -from unittest.mock import MagicMock, patch +from unittest.mock import AsyncMock, MagicMock, patch +import pytest + +import crewai.core.providers.human_input as human_input_module from crewai.agents.parser import AgentFinish from crewai.core.providers.human_input import SyncHumanInputProvider from crewai.events.event_listener import event_listener @@ -79,3 +82,47 @@ def test_result_not_reshown_when_verbose(self) -> None: provider.handle_feedback(_answer(), context) # type: ignore[arg-type] formatter.handle_agent_logs_execution.assert_not_called() + + +class TestResultDisplayBeforeFeedbackAsync: + """The async feedback path must show the result before prompting too.""" + + @pytest.mark.asyncio + async def test_result_shown_before_prompt_when_not_verbose(self) -> None: + provider = SyncHumanInputProvider() + context = _FakeContext(_FakeAgent(verbose=False), _FakeCrew(verbose=False)) + answer = _answer() + + formatter = MagicMock() + with ( + patch.object(event_listener, "formatter", formatter), + patch.object( + human_input_module, "_async_readline", new=AsyncMock(return_value="") + ), + ): + result = await provider.handle_feedback_async(answer, context) # type: ignore[arg-type] + + formatter.handle_agent_logs_execution.assert_called_once_with( + "Researcher", answer, verbose=True + ) + method_calls = [c[0] for c in formatter.mock_calls] + render_idx = method_calls.index("handle_agent_logs_execution") + prompt_idx = method_calls.index("console.print") + assert render_idx < prompt_idx + assert result is answer + + @pytest.mark.asyncio + async def test_result_not_reshown_when_verbose(self) -> None: + provider = SyncHumanInputProvider() + context = _FakeContext(_FakeAgent(verbose=False), _FakeCrew(verbose=True)) + + formatter = MagicMock() + with ( + patch.object(event_listener, "formatter", formatter), + patch.object( + human_input_module, "_async_readline", new=AsyncMock(return_value="") + ), + ): + await provider.handle_feedback_async(_answer(), context) # type: ignore[arg-type] + + formatter.handle_agent_logs_execution.assert_not_called() From 16d8c40dee3574f2d786a2f6b7fb63ebc24cc8ab Mon Sep 17 00:00:00 2001 From: HumphreySun98 Date: Wed, 24 Jun 2026 12:18:40 -0500 Subject: [PATCH 3/3] test(agents): cover agent-verbose no-op for human-feedback display Add sync and async cases asserting the result is not re-rendered when the agent is verbose but the crew is not, complementing the crew-verbose cases. Co-Authored-By: Claude Opus 4.8 (1M context) --- .../tests/core/providers/test_human_input.py | 32 +++++++++++++++++++ 1 file changed, 32 insertions(+) diff --git a/lib/crewai/tests/core/providers/test_human_input.py b/lib/crewai/tests/core/providers/test_human_input.py index 7440df4358..6f5c55a93c 100644 --- a/lib/crewai/tests/core/providers/test_human_input.py +++ b/lib/crewai/tests/core/providers/test_human_input.py @@ -83,6 +83,22 @@ def test_result_not_reshown_when_verbose(self) -> None: formatter.handle_agent_logs_execution.assert_not_called() + def test_result_not_reshown_when_agent_verbose(self) -> None: + """Agent-verbose (crew non-verbose) is also a no-op: the executor + already rendered the result via the verbose log path. + """ + provider = SyncHumanInputProvider() + context = _FakeContext(_FakeAgent(verbose=True), _FakeCrew(verbose=False)) + + formatter = MagicMock() + with ( + patch.object(event_listener, "formatter", formatter), + patch("builtins.input", return_value=""), + ): + provider.handle_feedback(_answer(), context) # type: ignore[arg-type] + + formatter.handle_agent_logs_execution.assert_not_called() + class TestResultDisplayBeforeFeedbackAsync: """The async feedback path must show the result before prompting too.""" @@ -126,3 +142,19 @@ async def test_result_not_reshown_when_verbose(self) -> None: await provider.handle_feedback_async(_answer(), context) # type: ignore[arg-type] formatter.handle_agent_logs_execution.assert_not_called() + + @pytest.mark.asyncio + async def test_result_not_reshown_when_agent_verbose(self) -> None: + provider = SyncHumanInputProvider() + context = _FakeContext(_FakeAgent(verbose=True), _FakeCrew(verbose=False)) + + formatter = MagicMock() + with ( + patch.object(event_listener, "formatter", formatter), + patch.object( + human_input_module, "_async_readline", new=AsyncMock(return_value="") + ), + ): + await provider.handle_feedback_async(_answer(), context) # type: ignore[arg-type] + + formatter.handle_agent_logs_execution.assert_not_called()