Skip to content

Added runtime validation for Agent constructor arguments #998

New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Open
wants to merge 27 commits into
base: main
Choose a base branch
from
Open
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
27 commits
Select commit Hold shift + click to select a range
e521f89
Duplicate Classifier in pyproject.toml Issue: "Intended Audience :: D…
DanielHashmi Jun 27, 2025
09479af
Import Path Inconsistency - There's an inconsistent import style in t…
DanielHashmi Jun 27, 2025
fbafb99
Merge branch 'openai:main' into main
DanielHashmi Jun 27, 2025
1761401
Fixed the ordering issue of imports
DanielHashmi Jun 27, 2025
c81af1a
Merge branch 'openai:main' into main
DanielHashmi Jun 27, 2025
405cddb
Merge branch 'openai:main' into main
DanielHashmi Jul 1, 2025
9841c5e
Added runtime validation for Agent name field
DanielHashmi Jul 3, 2025
2bc7a9b
Merge branch 'openai:main' into main
DanielHashmi Jul 10, 2025
f479983
Merge branch 'openai:main' into main
DanielHashmi Jul 11, 2025
33c1468
Update tests.yml
DanielHashmi Jul 11, 2025
9d5693a
Update tests.yml
DanielHashmi Jul 11, 2025
e23e7af
Merge branch 'openai:main' into main
DanielHashmi Jul 11, 2025
098bffb
Merge branch 'openai:main' into main
DanielHashmi Jul 14, 2025
ed708d7
Merge branch 'openai:main' into main
DanielHashmi Jul 16, 2025
fe07be5
Update agent.py
DanielHashmi Jul 16, 2025
64f558d
Update tests.yml
DanielHashmi Jul 16, 2025
bb77759
Update agent.py
DanielHashmi Jul 16, 2025
30f5aef
Update agent.py
DanielHashmi Jul 16, 2025
4233028
Update agent.py
DanielHashmi Jul 16, 2025
d14cbe9
Update test_agent_config.py
DanielHashmi Jul 16, 2025
0cb9394
Update test_agent_config.py
DanielHashmi Jul 16, 2025
e3639aa
Merge branch 'openai:main' into main
DanielHashmi Jul 16, 2025
bc1f792
Merge branch 'openai:main' into add_validations
DanielHashmi Jul 16, 2025
e7bf9ac
Merge pull request #3 from DanielHashmi/add_validations
DanielHashmi Jul 16, 2025
52c05bc
Run CI on all commits, not just ones on main (#521)
DanielHashmi Jul 16, 2025
34b8ca7
Merge branch 'openai:main' into main
DanielHashmi Jul 22, 2025
4d56548
Merge branch 'openai:main' into main
DanielHashmi Jul 23, 2025
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
113 changes: 113 additions & 0 deletions src/agents/agent.py
Original file line number Diff line number Diff line change
Expand Up @@ -221,6 +221,119 @@ class Agent(AgentBase, Generic[TContext]):
"""Whether to reset the tool choice to the default value after a tool has been called. Defaults
to True. This ensures that the agent doesn't enter an infinite loop of tool usage."""

def __post_init__(self):
from typing import get_origin

if not isinstance(self.name, str):
raise TypeError(f"Agent name must be a string, got {type(self.name).__name__}")

if self.handoff_description is not None and not isinstance(self.handoff_description, str):
raise TypeError(
f"Agent handoff_description must be a string or None, "
f"got {type(self.handoff_description).__name__}"
)

if not isinstance(self.tools, list):
raise TypeError(f"Agent tools must be a list, got {type(self.tools).__name__}")

if not isinstance(self.mcp_servers, list):
raise TypeError(
f"Agent mcp_servers must be a list, got {type(self.mcp_servers).__name__}"
)

if not isinstance(self.mcp_config, dict):
raise TypeError(
Comment on lines +225 to +245
Copy link
Preview

Copilot AI Jul 16, 2025

Choose a reason for hiding this comment

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

[nitpick] The post_init method contains many repetitive type checks; consider refactoring with a helper function or a mapping of field names to expected types to reduce duplication and improve readability.

Suggested change
from typing import get_origin
if not isinstance(self.name, str):
raise TypeError(f"Agent name must be a string, got {type(self.name).__name__}")
if self.handoff_description is not None and not isinstance(self.handoff_description, str):
raise TypeError(
f"Agent handoff_description must be a string or None, "
f"got {type(self.handoff_description).__name__}"
)
if not isinstance(self.tools, list):
raise TypeError(f"Agent tools must be a list, got {type(self.tools).__name__}")
if not isinstance(self.mcp_servers, list):
raise TypeError(
f"Agent mcp_servers must be a list, got {type(self.mcp_servers).__name__}"
)
if not isinstance(self.mcp_config, dict):
raise TypeError(
def validate_fields(field_mapping: dict[str, tuple[type, bool]]) -> None:
"""Helper function to validate fields based on a mapping of field names to types."""
for field_name, (expected_type, allow_none) in field_mapping.items():
value = getattr(self, field_name)
if value is None and allow_none:
continue
if not isinstance(value, expected_type):
raise TypeError(
f"Agent {field_name} must be a {expected_type.__name__}"
f"{' or None' if allow_none else ''}, got {type(value).__name__}"
)
field_mapping = {
"name": (str, False),
"handoff_description": (str, True),
"tools": (list, False),
"mcp_servers": (list, False),
"mcp_config": (dict, False),
}
validate_fields(field_mapping)
raise TypeError(

Copilot uses AI. Check for mistakes.

f"Agent mcp_config must be a dict, got {type(self.mcp_config).__name__}"
)

if (
self.instructions is not None
and not isinstance(self.instructions, str)
and not callable(self.instructions)
):
raise TypeError(
f"Agent instructions must be a string, callable, or None, "
f"got {type(self.instructions).__name__}"
)

if (
self.prompt is not None
and not callable(self.prompt)
and not hasattr(self.prompt, "get")
):
raise TypeError(
f"Agent prompt must be a Prompt, DynamicPromptFunction, or None, "
f"got {type(self.prompt).__name__}"
)

if not isinstance(self.handoffs, list):
raise TypeError(f"Agent handoffs must be a list, got {type(self.handoffs).__name__}")

if self.model is not None and not isinstance(self.model, str):
from .models.interface import Model

if not isinstance(self.model, Model):
raise TypeError(
f"Agent model must be a string, Model, or None, got {type(self.model).__name__}"
)

if not isinstance(self.model_settings, ModelSettings):
raise TypeError(
f"Agent model_settings must be a ModelSettings instance, "
f"got {type(self.model_settings).__name__}"
)

if not isinstance(self.input_guardrails, list):
raise TypeError(
f"Agent input_guardrails must be a list, got {type(self.input_guardrails).__name__}"
)

if not isinstance(self.output_guardrails, list):
raise TypeError(
f"Agent output_guardrails must be a list, "
f"got {type(self.output_guardrails).__name__}"
)

if self.output_type is not None:
from .agent_output import AgentOutputSchemaBase

if not (
isinstance(self.output_type, (type, AgentOutputSchemaBase))
or get_origin(self.output_type) is not None
):
raise TypeError(
f"Agent output_type must be a type, AgentOutputSchemaBase, or None, "
f"got {type(self.output_type).__name__}"
)

if self.hooks is not None:
from .lifecycle import AgentHooksBase

if not isinstance(self.hooks, AgentHooksBase):
raise TypeError(
f"Agent hooks must be an AgentHooks instance or None, "
f"got {type(self.hooks).__name__}"
)

if (
not (
isinstance(self.tool_use_behavior, str)
and self.tool_use_behavior in ["run_llm_again", "stop_on_first_tool"]
)
and not isinstance(self.tool_use_behavior, dict)
and not callable(self.tool_use_behavior)
):
raise TypeError(
f"Agent tool_use_behavior must be 'run_llm_again', 'stop_on_first_tool', "
f"StopAtTools dict, or callable, got {type(self.tool_use_behavior).__name__}"
)

if not isinstance(self.reset_tool_choice, bool):
raise TypeError(
f"Agent reset_tool_choice must be a boolean, "
f"got {type(self.reset_tool_choice).__name__}"
)

def clone(self, **kwargs: Any) -> Agent[TContext]:
"""Make a copy of the agent, with the given arguments changed. For example, you could do:
```
Expand Down
57 changes: 57 additions & 0 deletions tests/test_agent_config.py
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,8 @@
from pydantic import BaseModel

from agents import Agent, AgentOutputSchema, Handoff, RunContextWrapper, handoff
from agents.lifecycle import AgentHooksBase
from agents.model_settings import ModelSettings
from agents.run import AgentRunner


Expand Down Expand Up @@ -167,3 +169,58 @@ async def test_agent_final_output():
assert schema.is_strict_json_schema() is True
assert schema.json_schema() is not None
assert not schema.is_plain_text()


class TestAgentValidation:
"""Essential validation tests for Agent __post_init__"""

def test_name_validation_critical_cases(self):
"""Test name validation - the original issue that started this PR"""
# This was the original failing case that caused JSON serialization errors
with pytest.raises(TypeError, match="Agent name must be a string, got int"):
Agent(name=1) # type: ignore

with pytest.raises(TypeError, match="Agent name must be a string, got NoneType"):
Agent(name=None) # type: ignore

def test_tool_use_behavior_dict_validation(self):
"""Test tool_use_behavior accepts StopAtTools dict - fixes existing test failures"""
# This test ensures the existing failing tests now pass
Agent(name="test", tool_use_behavior={"stop_at_tool_names": ["tool1"]})

# Invalid cases that should fail
with pytest.raises(TypeError, match="Agent tool_use_behavior must be"):
Agent(name="test", tool_use_behavior=123) # type: ignore

def test_hooks_validation_python39_compatibility(self):
"""Test hooks validation works with Python 3.9 - fixes generic type issues"""

class MockHooks(AgentHooksBase):
pass

# Valid case
Agent(name="test", hooks=MockHooks()) # type: ignore

# Invalid case
with pytest.raises(TypeError, match="Agent hooks must be an AgentHooks instance"):
Agent(name="test", hooks="invalid") # type: ignore

def test_list_field_validation(self):
"""Test critical list fields that commonly get wrong types"""
# These are the most common mistakes users make
with pytest.raises(TypeError, match="Agent tools must be a list"):
Agent(name="test", tools="not_a_list") # type: ignore

with pytest.raises(TypeError, match="Agent handoffs must be a list"):
Agent(name="test", handoffs="not_a_list") # type: ignore

def test_model_settings_validation(self):
"""Test model_settings validation - prevents runtime errors"""
# Valid case
Agent(name="test", model_settings=ModelSettings())

# Invalid case that could cause runtime issues
with pytest.raises(
TypeError, match="Agent model_settings must be a ModelSettings instance"
):
Agent(name="test", model_settings={}) # type: ignore