Skip to content
Closed
Show file tree
Hide file tree
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
18 changes: 18 additions & 0 deletions src/agents/agent.py
Original file line number Diff line number Diff line change
Expand Up @@ -11,6 +11,7 @@
from typing_extensions import NotRequired, TypeAlias, TypedDict

from .agent_output import AgentOutputSchemaBase
from .exceptions import UserError
from .guardrail import InputGuardrail, OutputGuardrail
from .handoffs import Handoff
from .items import ItemHelpers
Expand Down Expand Up @@ -246,6 +247,23 @@ def __post_init__(self):
if not isinstance(self.tools, list):
raise TypeError(f"Agent tools must be a list, got {type(self.tools).__name__}")

# Validate each tool is a valid Tool type
# Tool is a Union type, so we need to get the valid types from it
from typing import get_args
Copy link
Contributor

Choose a reason for hiding this comment

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

I think this import should be placed at the top of the file with the other imports.


valid_tool_types = get_args(Tool) + (Handoff,)
Copy link
Contributor

Choose a reason for hiding this comment

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

I believe Handoff should not be included in valid_tool_types.

See the related discussion here:
#1897 (comment)


for i, tool in enumerate(self.tools):
if not isinstance(tool, valid_tool_types):
# Generate a friendly list of valid types for the error message
type_names = ", ".join(t.__name__ for t in valid_tool_types)
raise UserError(
f"tools[{i}] must be a valid Tool object ({type_names}), "
f"got {type(tool).__name__}. "
f"Did you forget to use @function_tool decorator or pass the function itself "
f"instead of a tool?"
)

if not isinstance(self.mcp_servers, list):
raise TypeError(
f"Agent mcp_servers must be a list, got {type(self.mcp_servers).__name__}"
Expand Down
43 changes: 43 additions & 0 deletions tests/test_agent_config.py
Original file line number Diff line number Diff line change
Expand Up @@ -214,6 +214,49 @@ def test_list_field_validation(self):
with pytest.raises(TypeError, match="Agent handoffs must be a list"):
Agent(name="test", handoffs="not_a_list") # type: ignore

def test_tools_content_validation_issue_1443(self):
"""Test that tools list validates each element is a valid Tool object (Issue #1443)

This test addresses the issue where passing invalid tool types (e.g., raw functions)
in a list would pass __post_init__ validation but fail later at runtime with:
AttributeError: 'function' object has no attribute 'name'

The fix validates each tool in the list during initialization.
"""
from agents.exceptions import UserError

def raw_function():
"""A raw function, not decorated with @function_tool"""
return "test"

# Case 1: Raw function in tools list should raise UserError at init
with pytest.raises(
UserError,
match=r"tools\[0\] must be a valid Tool object.*got function.*@function_tool",
):
Agent(name="test", tools=[raw_function]) # type: ignore

# Case 2: String in tools list should raise UserError at init
with pytest.raises(
UserError,
match=r"tools\[0\] must be a valid Tool object.*got str",
):
Agent(name="test", tools=["invalid_string"]) # type: ignore

# Case 3: Mixed valid and invalid tools - should catch invalid at correct index
from agents import function_tool

@function_tool
def valid_tool() -> str:
"""A valid tool"""
return "ok"

with pytest.raises(
UserError,
match=r"tools\[1\] must be a valid Tool object.*got str",
):
Agent(name="test", tools=[valid_tool, "invalid"]) # type: ignore

def test_model_settings_validation(self):
"""Test model_settings validation - prevents runtime errors"""
# Valid case
Expand Down