diff --git a/src/agents/agent.py b/src/agents/agent.py index a061926b1..e22af84ca 100644 --- a/src/agents/agent.py +++ b/src/agents/agent.py @@ -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 @@ -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 + + valid_tool_types = get_args(Tool) + (Handoff,) + + 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__}" diff --git a/tests/test_agent_config.py b/tests/test_agent_config.py index 5b633b70b..887fe4950 100644 --- a/tests/test_agent_config.py +++ b/tests/test_agent_config.py @@ -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