diff --git a/lib/crewai/src/crewai/agents/crew_agent_executor.py b/lib/crewai/src/crewai/agents/crew_agent_executor.py index 0707f59d60..b4f608cfb6 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() @@ -987,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 {})) @@ -1011,6 +1016,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 +1078,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 +1089,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 +1106,15 @@ 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. + # 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 ( - original_tool + 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 ): @@ -1110,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.