From a322cff21cd0be3acbb88645e8d3290466283e97 Mon Sep 17 00:00:00 2001 From: ThryLox Date: Sat, 27 Jun 2026 18:32:45 -0400 Subject: [PATCH 1/3] fix(agent): implement ExecutorContext protocol on AgentExecutor for human_input support (#6347) In crewAI 1.15.0, AgentExecutor became the default executor class. However, when Task(human_input=True) or ask_for_human_input=True is enabled, human feedback handlers cast AgentExecutor to ExecutorContext and invoke _format_feedback_message, _invoke_loop, and _ainvoke_loop. Because AgentExecutor lacked these methods, execution crashed with AttributeError. This commit implements all required ExecutorContext and AsyncExecutorContext protocol methods on AgentExecutor and adds a regression contract test. --- .../src/crewai/experimental/agent_executor.py | 34 +++++++++++++++++++ .../tests/agents/test_agent_executor.py | 13 +++++++ 2 files changed, 47 insertions(+) diff --git a/lib/crewai/src/crewai/experimental/agent_executor.py b/lib/crewai/src/crewai/experimental/agent_executor.py index 0ecd8e63a4..cda03b9a2b 100644 --- a/lib/crewai/src/crewai/experimental/agent_executor.py +++ b/lib/crewai/src/crewai/experimental/agent_executor.py @@ -3196,6 +3196,40 @@ def _is_training_mode(self) -> bool: """ return bool(self.crew and self.crew._train) + def _format_feedback_message(self, feedback: str) -> LLMMessage: + """Format feedback as a message for the LLM. + + Args: + feedback: User feedback string. + + Returns: + Formatted message dict. + """ + return format_message_for_llm( + I18N_DEFAULT.slice("feedback_instructions").format(feedback=feedback) + ) + + def _invoke_loop(self) -> AgentFinish: + """Invoke the agent loop and return the result for HITL feedback processing.""" + self._finalize_called = False + self.state.is_finished = False + self.kickoff() + result = self.state.current_answer + if not isinstance(result, AgentFinish): + raise RuntimeError("Agent execution ended without reaching a final answer.") + return result + + async def _ainvoke_loop(self) -> AgentFinish: + """Invoke the agent loop asynchronously and return the result for HITL feedback processing.""" + self._finalize_called = False + self.state.is_finished = False + await self.kickoff_async() + result = self.state.current_answer + if not isinstance(result, AgentFinish): + raise RuntimeError("Agent execution ended without reaching a final answer.") + return result + + # Backward compatibility alias (deprecated) CrewAgentExecutorFlow = AgentExecutor diff --git a/lib/crewai/tests/agents/test_agent_executor.py b/lib/crewai/tests/agents/test_agent_executor.py index e4de4a484b..c4e4e7ca43 100644 --- a/lib/crewai/tests/agents/test_agent_executor.py +++ b/lib/crewai/tests/agents/test_agent_executor.py @@ -2391,3 +2391,16 @@ def test_anthropic_provider_has_image_block_converter(self): assert hasattr(AnthropicCompletion, "_convert_image_blocks"), ( "Anthropic provider must have _convert_image_blocks for auto-conversion" ) + + +class TestAgentExecutorHumanInputProtocolContract: + """AgentExecutor must implement full ExecutorContext and AsyncExecutorContext protocol for human_input=True.""" + + def test_agent_executor_implements_human_input_protocol(self): + from crewai.experimental.agent_executor import AgentExecutor + + assert hasattr(AgentExecutor, "_format_feedback_message") + assert hasattr(AgentExecutor, "_invoke_loop") + assert hasattr(AgentExecutor, "_ainvoke_loop") + assert hasattr(AgentExecutor, "_is_training_mode") + From 6d9623efe0704d78312659a7844f365e39859e0e Mon Sep 17 00:00:00 2001 From: ThryLox Date: Sat, 27 Jun 2026 18:44:35 -0400 Subject: [PATCH 2/3] fix(agent): reset state.iterations before re-entering execution loop and strengthen contract tests --- .../src/crewai/experimental/agent_executor.py | 3 +++ lib/crewai/tests/agents/test_agent_executor.py | 17 +++++++++++++++++ 2 files changed, 20 insertions(+) diff --git a/lib/crewai/src/crewai/experimental/agent_executor.py b/lib/crewai/src/crewai/experimental/agent_executor.py index cda03b9a2b..692800287f 100644 --- a/lib/crewai/src/crewai/experimental/agent_executor.py +++ b/lib/crewai/src/crewai/experimental/agent_executor.py @@ -3213,6 +3213,7 @@ def _invoke_loop(self) -> AgentFinish: """Invoke the agent loop and return the result for HITL feedback processing.""" self._finalize_called = False self.state.is_finished = False + self.state.iterations = 0 self.kickoff() result = self.state.current_answer if not isinstance(result, AgentFinish): @@ -3223,6 +3224,7 @@ async def _ainvoke_loop(self) -> AgentFinish: """Invoke the agent loop asynchronously and return the result for HITL feedback processing.""" self._finalize_called = False self.state.is_finished = False + self.state.iterations = 0 await self.kickoff_async() result = self.state.current_answer if not isinstance(result, AgentFinish): @@ -3231,5 +3233,6 @@ async def _ainvoke_loop(self) -> AgentFinish: + # Backward compatibility alias (deprecated) CrewAgentExecutorFlow = AgentExecutor diff --git a/lib/crewai/tests/agents/test_agent_executor.py b/lib/crewai/tests/agents/test_agent_executor.py index c4e4e7ca43..d8b67b1b89 100644 --- a/lib/crewai/tests/agents/test_agent_executor.py +++ b/lib/crewai/tests/agents/test_agent_executor.py @@ -2404,3 +2404,20 @@ def test_agent_executor_implements_human_input_protocol(self): assert hasattr(AgentExecutor, "_ainvoke_loop") assert hasattr(AgentExecutor, "_is_training_mode") + def test_agent_executor_format_feedback_message(self, mock_dependencies): + executor = _build_executor(**mock_dependencies) + msg = executor._format_feedback_message("Please fix the summary") + assert msg["role"] == "user" + assert "Please fix the summary" in msg["content"] + + def test_agent_executor_invoke_loop_resets_iterations(self, mock_dependencies): + executor = _build_executor(**mock_dependencies) + executor.state.iterations = 10 + executor.state.current_answer = AgentFinish(thought="done", output="ok", text="ok") + with patch.object(executor, "kickoff") as mock_kickoff: + res = executor._invoke_loop() + assert executor.state.iterations == 0 + assert res.output == "ok" + mock_kickoff.assert_called_once() + + From ae541248a09e0eb512baa513fa882034bac5e962 Mon Sep 17 00:00:00 2001 From: ThryLox Date: Sat, 27 Jun 2026 18:49:17 -0400 Subject: [PATCH 3/3] test(agent): verify iterations reset timing before kickoff and add async _ainvoke_loop contract test --- .../tests/agents/test_agent_executor.py | 30 ++++++++++++++++--- 1 file changed, 26 insertions(+), 4 deletions(-) diff --git a/lib/crewai/tests/agents/test_agent_executor.py b/lib/crewai/tests/agents/test_agent_executor.py index d8b67b1b89..0177cba036 100644 --- a/lib/crewai/tests/agents/test_agent_executor.py +++ b/lib/crewai/tests/agents/test_agent_executor.py @@ -2413,11 +2413,33 @@ def test_agent_executor_format_feedback_message(self, mock_dependencies): def test_agent_executor_invoke_loop_resets_iterations(self, mock_dependencies): executor = _build_executor(**mock_dependencies) executor.state.iterations = 10 - executor.state.current_answer = AgentFinish(thought="done", output="ok", text="ok") - with patch.object(executor, "kickoff") as mock_kickoff: + iterations_at_kickoff = None + + def mock_kickoff_impl(): + nonlocal iterations_at_kickoff + iterations_at_kickoff = executor.state.iterations + executor.state.current_answer = AgentFinish(thought="done", output="ok", text="ok") + + with patch.object(executor, "kickoff", side_effect=mock_kickoff_impl): res = executor._invoke_loop() - assert executor.state.iterations == 0 + assert iterations_at_kickoff == 0 assert res.output == "ok" - mock_kickoff.assert_called_once() + + @pytest.mark.asyncio + async def test_agent_executor_ainvoke_loop_resets_iterations(self, mock_dependencies): + executor = _build_executor(**mock_dependencies) + executor.state.iterations = 10 + iterations_at_kickoff = None + + async def mock_kickoff_async_impl(): + nonlocal iterations_at_kickoff + iterations_at_kickoff = executor.state.iterations + executor.state.current_answer = AgentFinish(thought="done", output="ok_async", text="ok_async") + + with patch.object(executor, "kickoff_async", side_effect=mock_kickoff_async_impl): + res = await executor._ainvoke_loop() + assert iterations_at_kickoff == 0 + assert res.output == "ok_async" +