From e521f891c63d1940b66108ace56a455eb1e52428 Mon Sep 17 00:00:00 2001 From: Daniel Hashmi <151331476+DanielHashmi@users.noreply.github.com> Date: Fri, 27 Jun 2025 11:50:13 +0500 Subject: [PATCH 01/14] Duplicate Classifier in pyproject.toml Issue: "Intended Audience :: Developers" appears twice in the classifiers list. Line Number 20 and Line Number 26 Fix: Removed the duplicate classifier entry --- pyproject.toml | 1 - 1 file changed, 1 deletion(-) diff --git a/pyproject.toml b/pyproject.toml index 75e714768..6c0bbe9f1 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -23,7 +23,6 @@ classifiers = [ "Programming Language :: Python :: 3.10", "Programming Language :: Python :: 3.11", "Programming Language :: Python :: 3.12", - "Intended Audience :: Developers", "Operating System :: OS Independent", "Topic :: Software Development :: Libraries :: Python Modules", "License :: OSI Approved :: MIT License", From 09479afe3b5cc2c3acfed034bfa2121e9a4660c2 Mon Sep 17 00:00:00 2001 From: Daniel Hashmi <151331476+DanielHashmi@users.noreply.github.com> Date: Fri, 27 Jun 2025 12:24:45 +0500 Subject: [PATCH 02/14] Import Path Inconsistency - There's an inconsistent import style in the tracing module where one import uses an absolute path in line number 3 while all others use relative imports --- src/agents/tracing/__init__.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/agents/tracing/__init__.py b/src/agents/tracing/__init__.py index 64b0bd71f..c33fc5be9 100644 --- a/src/agents/tracing/__init__.py +++ b/src/agents/tracing/__init__.py @@ -1,6 +1,6 @@ import atexit -from agents.tracing.provider import DefaultTraceProvider, TraceProvider +from .provider import DefaultTraceProvider, TraceProvider from .create import ( agent_span, From 17614011a138c9f280d0a4ced16427f65199cc22 Mon Sep 17 00:00:00 2001 From: Daniel Hashmi <151331476+DanielHashmi@users.noreply.github.com> Date: Fri, 27 Jun 2025 19:29:48 +0500 Subject: [PATCH 03/14] Fixed the ordering issue of imports --- src/agents/tracing/__init__.py | 3 +-- 1 file changed, 1 insertion(+), 2 deletions(-) diff --git a/src/agents/tracing/__init__.py b/src/agents/tracing/__init__.py index c33fc5be9..b45c06d75 100644 --- a/src/agents/tracing/__init__.py +++ b/src/agents/tracing/__init__.py @@ -1,7 +1,5 @@ import atexit -from .provider import DefaultTraceProvider, TraceProvider - from .create import ( agent_span, custom_span, @@ -20,6 +18,7 @@ ) from .processor_interface import TracingProcessor from .processors import default_exporter, default_processor +from .provider import DefaultTraceProvider, TraceProvider from .setup import get_trace_provider, set_trace_provider from .span_data import ( AgentSpanData, From 9841c5e4422eef6aef6649e14b016fbd3ed1efd6 Mon Sep 17 00:00:00 2001 From: Daniel Hashmi <151331476+DanielHashmi@users.noreply.github.com> Date: Thu, 3 Jul 2025 10:22:20 +0500 Subject: [PATCH 04/14] Added runtime validation for Agent name field --- src/agents/agent.py | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/src/agents/agent.py b/src/agents/agent.py index 6c87297f1..751f50015 100644 --- a/src/agents/agent.py +++ b/src/agents/agent.py @@ -187,6 +187,10 @@ class Agent(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): + if not isinstance(self.name, str): + raise TypeError(f"Agent name must be a string, got {type(self.name).__name__}") + def clone(self, **kwargs: Any) -> Agent[TContext]: """Make a copy of the agent, with the given arguments changed. For example, you could do: ``` From 33c146812c4c0a86e5bebe449a3d3f1eab5d7498 Mon Sep 17 00:00:00 2001 From: Daniel Hashmi <151331476+DanielHashmi@users.noreply.github.com> Date: Fri, 11 Jul 2025 22:48:16 +0500 Subject: [PATCH 05/14] Update tests.yml --- .github/workflows/tests.yml | 1 + 1 file changed, 1 insertion(+) diff --git a/.github/workflows/tests.yml b/.github/workflows/tests.yml index edd0d898b..fe33622f4 100644 --- a/.github/workflows/tests.yml +++ b/.github/workflows/tests.yml @@ -4,6 +4,7 @@ on: push: branches: - main + - main2 pull_request: # All PRs, including stacked PRs From 9d5693a22f252e60c3606493db2c5c1a55a1f74b Mon Sep 17 00:00:00 2001 From: Daniel Hashmi <151331476+DanielHashmi@users.noreply.github.com> Date: Fri, 11 Jul 2025 22:48:52 +0500 Subject: [PATCH 06/14] Update tests.yml --- .github/workflows/tests.yml | 1 - 1 file changed, 1 deletion(-) diff --git a/.github/workflows/tests.yml b/.github/workflows/tests.yml index fe33622f4..edd0d898b 100644 --- a/.github/workflows/tests.yml +++ b/.github/workflows/tests.yml @@ -4,7 +4,6 @@ on: push: branches: - main - - main2 pull_request: # All PRs, including stacked PRs From fe07be58bd66fe2127e6ad79f2801d97cfcb2b9d Mon Sep 17 00:00:00 2001 From: Daniel Hashmi <151331476+DanielHashmi@users.noreply.github.com> Date: Wed, 16 Jul 2025 19:58:30 +0500 Subject: [PATCH 07/14] Update agent.py --- src/agents/agent.py | 100 ++++++++++++++++++++++++++++++++++++++++++++ 1 file changed, 100 insertions(+) diff --git a/src/agents/agent.py b/src/agents/agent.py index 2655ca022..d7308a9e4 100644 --- a/src/agents/agent.py +++ b/src/agents/agent.py @@ -225,6 +225,106 @@ def __post_init__(self): 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( + 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 isinstance(self.prompt, (Prompt, DynamicPromptFunction)) + and not callable(self.prompt) + ): + raise TypeError( + f"Agent prompt must be a Prompt, DynamicPromptFunction, callable, 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, 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 and not isinstance( + self.output_type, (type, AgentOutputSchemaBase) + ): + 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 AgentHooks + + if not isinstance(self.hooks, AgentHooks): + 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: ``` From 64f558d90207c9be9d3ace9a97b5cb2db3c47e96 Mon Sep 17 00:00:00 2001 From: Daniel Hashmi <151331476+DanielHashmi@users.noreply.github.com> Date: Wed, 16 Jul 2025 19:59:18 +0500 Subject: [PATCH 08/14] Update tests.yml --- .github/workflows/tests.yml | 1 + 1 file changed, 1 insertion(+) diff --git a/.github/workflows/tests.yml b/.github/workflows/tests.yml index edd0d898b..fe98a2394 100644 --- a/.github/workflows/tests.yml +++ b/.github/workflows/tests.yml @@ -4,6 +4,7 @@ on: push: branches: - main + - add_validations pull_request: # All PRs, including stacked PRs From bb777599ebd7aaa0358d3910299aec37e656706f Mon Sep 17 00:00:00 2001 From: Daniel Hashmi <151331476+DanielHashmi@users.noreply.github.com> Date: Wed, 16 Jul 2025 20:32:22 +0500 Subject: [PATCH 09/14] Update agent.py --- src/agents/agent.py | 7 ++++--- 1 file changed, 4 insertions(+), 3 deletions(-) diff --git a/src/agents/agent.py b/src/agents/agent.py index d7308a9e4..f77b4b062 100644 --- a/src/agents/agent.py +++ b/src/agents/agent.py @@ -256,7 +256,7 @@ def __post_init__(self): if ( self.prompt is not None - and not isinstance(self.prompt, (Prompt, DynamicPromptFunction)) + and not isinstance(self.prompt, dict) and not callable(self.prompt) ): raise TypeError( @@ -297,10 +297,11 @@ def __post_init__(self): f"got {type(self.output_type).__name__}" ) + # Fixed hooks validation - use AgentHooksBase instead of generic AgentHooks if self.hooks is not None: - from .lifecycle import AgentHooks + from .lifecycle import AgentHooksBase - if not isinstance(self.hooks, AgentHooks): + if not isinstance(self.hooks, AgentHooksBase): raise TypeError( f"Agent hooks must be an AgentHooks instance or None, " f"got {type(self.hooks).__name__}" From 30f5aef97ac24cf0724fb60f056b8457c54b2a7b Mon Sep 17 00:00:00 2001 From: Daniel Hashmi <151331476+DanielHashmi@users.noreply.github.com> Date: Wed, 16 Jul 2025 20:52:12 +0500 Subject: [PATCH 10/14] Update agent.py --- src/agents/agent.py | 56 ++++++++++++++++++++++++++------------------- 1 file changed, 32 insertions(+), 24 deletions(-) diff --git a/src/agents/agent.py b/src/agents/agent.py index f77b4b062..d3a41253a 100644 --- a/src/agents/agent.py +++ b/src/agents/agent.py @@ -222,6 +222,8 @@ class Agent(AgentBase, Generic[TContext]): 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__}") @@ -256,21 +258,26 @@ def __post_init__(self): if ( self.prompt is not None - and not isinstance(self.prompt, dict) and not callable(self.prompt) + and not hasattr(self.prompt, "get") ): raise TypeError( - f"Agent prompt must be a Prompt, DynamicPromptFunction, callable, or None, " + 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, Model)): - raise TypeError( - f"Agent model must be a string, Model, or None, got {type(self.model).__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__}" + ) + + from .model_settings import ModelSettings if not isinstance(self.model_settings, ModelSettings): raise TypeError( @@ -289,35 +296,36 @@ def __post_init__(self): f"got {type(self.output_guardrails).__name__}" ) - if self.output_type is not None and not isinstance( - self.output_type, (type, AgentOutputSchemaBase) - ): - raise TypeError( - f"Agent output_type must be a type, AgentOutputSchemaBase, or None, " - f"got {type(self.output_type).__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__}" + ) - # Fixed hooks validation - use AgentHooksBase instead of generic AgentHooks if self.hooks is not None: - from .lifecycle import AgentHooksBase + from .lifecycle import AgentHooks - if not isinstance(self.hooks, AgentHooksBase): + if not isinstance(self.hooks, AgentHooks): 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) + if not ( + self.tool_use_behavior == "run_llm_again" + or self.tool_use_behavior == "stop_on_first_tool" + or isinstance(self.tool_use_behavior, list) + or 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__}" + f"a list of tool names, or a callable, got {type(self.tool_use_behavior).__name__}" ) if not isinstance(self.reset_tool_choice, bool): From 4233028a944f0efbf48358ca4208995f013446b2 Mon Sep 17 00:00:00 2001 From: Daniel Hashmi <151331476+DanielHashmi@users.noreply.github.com> Date: Wed, 16 Jul 2025 20:56:51 +0500 Subject: [PATCH 11/14] Update agent.py --- src/agents/agent.py | 20 ++++++++++---------- 1 file changed, 10 insertions(+), 10 deletions(-) diff --git a/src/agents/agent.py b/src/agents/agent.py index d3a41253a..5c6037532 100644 --- a/src/agents/agent.py +++ b/src/agents/agent.py @@ -277,8 +277,6 @@ def __post_init__(self): f"Agent model must be a string, Model, or None, got {type(self.model).__name__}" ) - from .model_settings import ModelSettings - if not isinstance(self.model_settings, ModelSettings): raise TypeError( f"Agent model_settings must be a ModelSettings instance, " @@ -309,23 +307,25 @@ def __post_init__(self): ) if self.hooks is not None: - from .lifecycle import AgentHooks + from .lifecycle import AgentHooksBase - if not isinstance(self.hooks, AgentHooks): + 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 ( - self.tool_use_behavior == "run_llm_again" - or self.tool_use_behavior == "stop_on_first_tool" - or isinstance(self.tool_use_behavior, list) - or callable(self.tool_use_behavior) + 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"a list of tool names, or a callable, got {type(self.tool_use_behavior).__name__}" + f"StopAtTools dict, or callable, got {type(self.tool_use_behavior).__name__}" ) if not isinstance(self.reset_tool_choice, bool): From d14cbe98d9d22363b3e9302b72819f2694cd98e0 Mon Sep 17 00:00:00 2001 From: Daniel Hashmi <151331476+DanielHashmi@users.noreply.github.com> Date: Wed, 16 Jul 2025 21:09:38 +0500 Subject: [PATCH 12/14] Update test_agent_config.py --- tests/test_agent_config.py | 57 ++++++++++++++++++++++++++++++++++++++ 1 file changed, 57 insertions(+) diff --git a/tests/test_agent_config.py b/tests/test_agent_config.py index a985fd60d..963eaca07 100644 --- a/tests/test_agent_config.py +++ b/tests/test_agent_config.py @@ -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 @@ -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) + + with pytest.raises(TypeError, match="Agent name must be a string, got NoneType"): + Agent(name=None) + + 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) + + 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()) + + # Invalid case + with pytest.raises(TypeError, match="Agent hooks must be an AgentHooks instance"): + Agent(name="test", hooks="invalid") + + 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") + + with pytest.raises(TypeError, match="Agent handoffs must be a list"): + Agent(name="test", handoffs="not_a_list") + + 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={}) From 0cb9394bf251cd0731d01dec0667cf8a4884acdd Mon Sep 17 00:00:00 2001 From: Daniel Hashmi <151331476+DanielHashmi@users.noreply.github.com> Date: Wed, 16 Jul 2025 21:12:32 +0500 Subject: [PATCH 13/14] Update test_agent_config.py --- tests/test_agent_config.py | 16 ++++++++-------- 1 file changed, 8 insertions(+), 8 deletions(-) diff --git a/tests/test_agent_config.py b/tests/test_agent_config.py index 963eaca07..5b633b70b 100644 --- a/tests/test_agent_config.py +++ b/tests/test_agent_config.py @@ -178,10 +178,10 @@ 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) + Agent(name=1) # type: ignore with pytest.raises(TypeError, match="Agent name must be a string, got NoneType"): - Agent(name=None) + Agent(name=None) # type: ignore def test_tool_use_behavior_dict_validation(self): """Test tool_use_behavior accepts StopAtTools dict - fixes existing test failures""" @@ -190,7 +190,7 @@ def test_tool_use_behavior_dict_validation(self): # Invalid cases that should fail with pytest.raises(TypeError, match="Agent tool_use_behavior must be"): - Agent(name="test", tool_use_behavior=123) + 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""" @@ -199,20 +199,20 @@ class MockHooks(AgentHooksBase): pass # Valid case - Agent(name="test", hooks=MockHooks()) + 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") + 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") + 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") + Agent(name="test", handoffs="not_a_list") # type: ignore def test_model_settings_validation(self): """Test model_settings validation - prevents runtime errors""" @@ -223,4 +223,4 @@ def test_model_settings_validation(self): with pytest.raises( TypeError, match="Agent model_settings must be a ModelSettings instance" ): - Agent(name="test", model_settings={}) + Agent(name="test", model_settings={}) # type: ignore From 52c05bc3704e9135886f920028cbe459ea9f8e79 Mon Sep 17 00:00:00 2001 From: Daniel Hashmi <151331476+DanielHashmi@users.noreply.github.com> Date: Wed, 16 Jul 2025 21:25:44 +0500 Subject: [PATCH 14/14] Run CI on all commits, not just ones on main (#521) --- .github/workflows/tests.yml | 1 - 1 file changed, 1 deletion(-) diff --git a/.github/workflows/tests.yml b/.github/workflows/tests.yml index fe98a2394..edd0d898b 100644 --- a/.github/workflows/tests.yml +++ b/.github/workflows/tests.yml @@ -4,7 +4,6 @@ on: push: branches: - main - - add_validations pull_request: # All PRs, including stacked PRs