From c5d438402f7750127ec5fa2a12da014a380cfde3 Mon Sep 17 00:00:00 2001 From: Devin AI <158243242+devin-ai-integration[bot]@users.noreply.github.com> Date: Wed, 4 Mar 2026 12:37:50 +0000 Subject: [PATCH] fix: use self.tools instead of self.original_tools for no-tools routing MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Address Bugbot concern: self.tools includes internal tools (delegation, human input) while self.original_tools only has user-defined tools. Only route to native_no_tools when there are truly no tools at all, so agents with internal tools still use the ReAct loop. Add test for FC+internal-tools scenario. Co-Authored-By: João --- .../src/crewai/agents/crew_agent_executor.py | 10 +++--- .../agents/test_react_output_pydantic.py | 35 ++++++++++++++++--- 2 files changed, 37 insertions(+), 8 deletions(-) diff --git a/lib/crewai/src/crewai/agents/crew_agent_executor.py b/lib/crewai/src/crewai/agents/crew_agent_executor.py index 03ee6eb2d..62cf026dd 100644 --- a/lib/crewai/src/crewai/agents/crew_agent_executor.py +++ b/lib/crewai/src/crewai/agents/crew_agent_executor.py @@ -320,10 +320,11 @@ class CrewAgentExecutor(CrewAgentExecutorMixin): if supports_fc and self.original_tools: return self._invoke_loop_native_tools() - # FC-capable LLM with no tools but with response_model: use simple + # FC-capable LLM with no user-defined tools but with response_model + # and no internal tools (delegation, human input, etc.): use simple # native call path which correctly passes response_model for structured # output instead of dropping it in the ReAct path. - if supports_fc and not self.original_tools and self.response_model: + if supports_fc and not self.tools and self.response_model: return self._invoke_loop_native_no_tools() # Fall back to ReAct text-based pattern @@ -1146,10 +1147,11 @@ class CrewAgentExecutor(CrewAgentExecutorMixin): if supports_fc and self.original_tools: return await self._ainvoke_loop_native_tools() - # FC-capable LLM with no tools but with response_model: use simple + # FC-capable LLM with no user-defined tools but with response_model + # and no internal tools (delegation, human input, etc.): use simple # native call path which correctly passes response_model for structured # output instead of dropping it in the ReAct path. - if supports_fc and not self.original_tools and self.response_model: + if supports_fc and not self.tools and self.response_model: return await self._ainvoke_loop_native_no_tools() # Fall back to ReAct text-based pattern diff --git a/lib/crewai/tests/agents/test_react_output_pydantic.py b/lib/crewai/tests/agents/test_react_output_pydantic.py index 35d79b562..bb7010266 100644 --- a/lib/crewai/tests/agents/test_react_output_pydantic.py +++ b/lib/crewai/tests/agents/test_react_output_pydantic.py @@ -182,13 +182,14 @@ class TestReActFlowDoesNotPassResponseModel: def test_invoke_loop_routes_to_native_no_tools_when_fc_no_tools_with_response_model( self, ) -> None: - """When LLM supports FC, has no tools, but HAS a response_model, - route to _invoke_loop_native_no_tools (which correctly passes - response_model) instead of falling through to the ReAct path.""" + """When LLM supports FC, has no tools (including internal tools), + but HAS a response_model, route to _invoke_loop_native_no_tools + (which correctly passes response_model for structured output).""" llm = _make_llm(supports_fc=True) executor = _make_executor(llm, response_model=PersonInfo) - # No tools + # No user-defined or internal tools executor.original_tools = [] + executor.tools = [] with patch.object( executor, @@ -203,6 +204,31 @@ class TestReActFlowDoesNotPassResponseModel: mock_react.assert_not_called() mock_native.assert_not_called() + def test_invoke_loop_routes_to_react_when_fc_no_orig_tools_but_internal_tools( + self, + ) -> None: + """When LLM supports FC, has no original_tools but HAS internal tools + (e.g. delegation), fall through to ReAct even with response_model. + Internal tools need the ReAct loop for Action/Observation cycles.""" + llm = _make_llm(supports_fc=True) + executor = _make_executor(llm, response_model=PersonInfo) + executor.original_tools = [] + # Internal tools present (e.g. delegation tool) + executor.tools = [MagicMock()] + + with patch.object( + executor, + "_invoke_loop_react", + return_value=AgentFinish(thought="done", output="test", text="Final Answer: test"), + ) as mock_react: + with patch.object(executor, "_invoke_loop_native_no_tools") as mock_native_no_tools: + with patch.object(executor, "_invoke_loop_native_tools") as mock_native: + executor._invoke_loop() + + mock_react.assert_called_once() + mock_native_no_tools.assert_not_called() + mock_native.assert_not_called() + def test_invoke_loop_routes_to_react_when_fc_no_tools_no_response_model( self, ) -> None: @@ -211,6 +237,7 @@ class TestReActFlowDoesNotPassResponseModel: llm = _make_llm(supports_fc=True) executor = _make_executor(llm, response_model=None) executor.original_tools = [] + executor.tools = [] with patch.object( executor,