From c2e9a26f64b7a4ac836e7ea4b26f3b097617adc0 Mon Sep 17 00:00:00 2001 From: wzr <2668940489@qq.com> Date: Tue, 31 Mar 2026 04:37:20 +0800 Subject: [PATCH 1/3] fix: result_as_answer should not apply to tool errors When a tool with result_as_answer=True encounters an error (exception, hook block, max usage limit, or tool-not-found), the error message was being returned as the final agent answer. This prevents the agent from reflecting on the error and retrying. Fix: track is_error flag through _execute_single_native_tool_call and check it in _append_tool_result_and_check_finality before returning AgentFinish. Only successful tool outputs should become the final answer when result_as_answer=True. Closes #5156 --- lib/crewai/src/crewai/agents/crew_agent_executor.py | 11 ++++++++++- 1 file changed, 10 insertions(+), 1 deletion(-) diff --git a/lib/crewai/src/crewai/agents/crew_agent_executor.py b/lib/crewai/src/crewai/agents/crew_agent_executor.py index 0707f59d60..9e1fe4d0d9 100644 --- a/lib/crewai/src/crewai/agents/crew_agent_executor.py +++ b/lib/crewai/src/crewai/agents/crew_agent_executor.py @@ -899,6 +899,7 @@ def _execute_single_native_tool_call( func_args, func_name, call_id, original_tool ) if parse_error is not None: + parse_error["is_error"] = True return parse_error if original_tool is None: @@ -920,6 +921,7 @@ def _execute_single_native_tool_call( max_usage_reached = True from_cache = False + is_error = True # "Tool not found" is the default error state result: str = "Tool not found" input_str = json.dumps(args_dict) if args_dict else "" if self.tools_handler and self.tools_handler.cache: @@ -933,6 +935,7 @@ def _execute_single_native_tool_call( else cached_result ) from_cache = True + is_error = False agent_key = getattr(self.agent, "key", "unknown") if self.agent else "unknown" started_at = datetime.now() @@ -1011,6 +1014,7 @@ def _execute_single_native_tool_call( result = ( str(raw_result) if not isinstance(raw_result, str) else raw_result ) + is_error = False except Exception as e: result = f"Error executing tool: {e}" if self.task: @@ -1072,6 +1076,7 @@ def _execute_single_native_tool_call( "result": result, "from_cache": from_cache, "original_tool": original_tool, + "is_error": is_error, } def _append_tool_result_and_check_finality( @@ -1082,6 +1087,7 @@ def _append_tool_result_and_check_finality( result = cast(str, execution_result["result"]) from_cache = cast(bool, execution_result["from_cache"]) original_tool = execution_result["original_tool"] + is_error = cast(bool, execution_result.get("is_error", False)) tool_message: LLMMessage = { "role": "tool", @@ -1098,8 +1104,11 @@ def _append_tool_result_and_check_finality( color="green", ) + # result_as_answer only applies to successful tool outputs. + # If the tool errored, let the agent reflect on the error instead. if ( - original_tool + not is_error + and original_tool and hasattr(original_tool, "result_as_answer") and original_tool.result_as_answer ): From f54eda7347bbe3b5760b060be9e677858d618a68 Mon Sep 17 00:00:00 2001 From: wzr <2668940489@qq.com> Date: Tue, 31 Mar 2026 09:04:36 +0800 Subject: [PATCH 2/3] fix: set is_error=True when hook_blocked or max_usage_reached overrides cached result Addresses Bugbot review: when a cache hit sets is_error=False but the tool is subsequently blocked by a hook or usage limit, the result is overwritten with an error message but is_error was not updated. This caused result_as_answer to incorrectly return error strings as final answers. --- lib/crewai/src/crewai/agents/crew_agent_executor.py | 2 ++ 1 file changed, 2 insertions(+) diff --git a/lib/crewai/src/crewai/agents/crew_agent_executor.py b/lib/crewai/src/crewai/agents/crew_agent_executor.py index 9e1fe4d0d9..94f43b7489 100644 --- a/lib/crewai/src/crewai/agents/crew_agent_executor.py +++ b/lib/crewai/src/crewai/agents/crew_agent_executor.py @@ -990,8 +990,10 @@ def _execute_single_native_tool_call( if hook_blocked: result = f"Tool execution blocked by hook. Tool: {func_name}" + is_error = True elif max_usage_reached and original_tool: result = f"Tool '{func_name}' has reached its usage limit of {original_tool.max_usage_count} times and cannot be used anymore." + is_error = True elif not from_cache and func_name in available_functions: try: raw_result = available_functions[func_name](**(args_dict or {})) From c273dffa4adfb491ee2343f8fc143ee50b3083d2 Mon Sep 17 00:00:00 2001 From: wzr <2668940489@qq.com> Date: Wed, 1 Apr 2026 04:35:41 +0800 Subject: [PATCH 3/3] fix: add string-based error detection for tools returning error strings Address review feedback: many built-in crewAI tools return error strings like "Error performing search: ..." instead of raising exceptions. Add _looks_like_tool_error() heuristic to detect these, complementing the is_error flag which handles exception/parse failures. Patterns detected: - "Error ..." (most built-in tools: brave_search, rag, aws/s3, ocr) - "I encountered an error ..." (tool_usage.py i18n messages) --- .../src/crewai/agents/crew_agent_executor.py | 18 ++++++++++++++++++ 1 file changed, 18 insertions(+) diff --git a/lib/crewai/src/crewai/agents/crew_agent_executor.py b/lib/crewai/src/crewai/agents/crew_agent_executor.py index 94f43b7489..b4f608cfb6 100644 --- a/lib/crewai/src/crewai/agents/crew_agent_executor.py +++ b/lib/crewai/src/crewai/agents/crew_agent_executor.py @@ -1108,8 +1108,12 @@ def _append_tool_result_and_check_finality( # result_as_answer only applies to successful tool outputs. # If the tool errored, let the agent reflect on the error instead. + # Two checks: (1) is_error flag from exception/parse failures, + # (2) string-based detection for tools that return error strings + # without raising exceptions (e.g., "Error performing search: ..."). if ( not is_error + and not self._looks_like_tool_error(result) and original_tool and hasattr(original_tool, "result_as_answer") and original_tool.result_as_answer @@ -1121,6 +1125,20 @@ def _append_tool_result_and_check_finality( ) return None + @staticmethod + def _looks_like_tool_error(result: str) -> bool: + """Check if a tool result string looks like an error. + + Many built-in crewAI tools return error strings (e.g., 'Error + performing search: ...') instead of raising exceptions. This + heuristic catches those cases so result_as_answer doesn't treat + them as final agent output. + """ + if not result: + return False + stripped = result.strip() + return stripped.startswith("Error ") or stripped.startswith("I encountered an error") + async def ainvoke(self, inputs: dict[str, Any]) -> dict[str, Any]: """Execute the agent asynchronously with given inputs.