Skip to content
Open
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
31 changes: 30 additions & 1 deletion lib/crewai/src/crewai/agents/crew_agent_executor.py
Original file line number Diff line number Diff line change
Expand Up @@ -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:
Expand All @@ -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:
Expand All @@ -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()
Expand Down Expand Up @@ -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 {}))
Expand All @@ -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
Comment thread
cursor[bot] marked this conversation as resolved.
except Exception as e:
result = f"Error executing tool: {e}"
if self.task:
Expand Down Expand Up @@ -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(
Expand All @@ -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",
Expand All @@ -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
Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Error prefix misclassifies valid tool outputs

Medium Severity

The new result_as_answer guard treats any successful result starting with Error or I encountered an error as a failure via _looks_like_tool_error. Legitimate outputs with those prefixes are now prevented from finalizing in crew_agent_executor.py, changing behavior for valid result_as_answer tools.

Additional Locations (1)
Fix in Cursor Fix in Web

and hasattr(original_tool, "result_as_answer")
and original_tool.result_as_answer
):
Expand All @@ -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.

Expand Down