diff --git a/docs/agents.md b/docs/agents.md index 15ced6255..ec70fceed 100644 --- a/docs/agents.md +++ b/docs/agents.md @@ -280,6 +280,19 @@ agent = Agent( ) ``` +## Advanced Reasoning + +Agents can be configured to perform advanced reasoning using the `reasoning` parameter in `ModelSettings`. + +Key configurable options: + +- `strategy`: The reasoning strategy, e.g., `"tree_of_thoughts"` or `"step_by_step"`. +- `depth`: The number of reasoning steps the model should perform. + +!!! note + The maximum value for `depth` is currently **not documented**. Contributors should refer to model-specific limits, and start with low values (like 2–3) to avoid excessively long computations or errors. + + !!! note To prevent infinite loops, the framework automatically resets `tool_choice` to "auto" after a tool call. This behavior is configurable via [`agent.reset_tool_choice`][agents.agent.Agent.reset_tool_choice]. The infinite loop is because tool results are sent to the LLM, which then generates another tool call because of `tool_choice`, ad infinitum. diff --git a/src/agents/tool_context.py b/src/agents/tool_context.py index 16845badd..a16f78a33 100644 --- a/src/agents/tool_context.py +++ b/src/agents/tool_context.py @@ -33,10 +33,15 @@ def from_agent_context( ) -> "ToolContext": """ Create a ToolContext from a RunContextWrapper. + + This method preserves the generic type information of the context object, + ensuring that ctx.context maintains its proper type in tool validation functions. """ - # Grab the names of the RunContextWrapper's init=True fields - base_values: dict[str, Any] = { - f.name: getattr(context, f.name) for f in fields(RunContextWrapper) if f.init - } + # Directly pass the context and usage to preserve generic type information tool_name = tool_call.name if tool_call is not None else _assert_must_pass_tool_name() - return cls(tool_name=tool_name, tool_call_id=tool_call_id, **base_values) + return cls( + context=context.context, + usage=context.usage, + tool_name=tool_name, + tool_call_id=tool_call_id, + ) diff --git a/tests/test_generic_type_consistency.py b/tests/test_generic_type_consistency.py new file mode 100644 index 000000000..a32c3be95 --- /dev/null +++ b/tests/test_generic_type_consistency.py @@ -0,0 +1,190 @@ +import asyncio +import pytest +from pydantic import BaseModel + +from agents import Agent, RunContextWrapper, Runner, function_tool + + +class UserDataType(BaseModel): + """Test context type with an age attribute.""" + name: str + age: int + + +def dynamic_instructions(ctx: RunContextWrapper[UserDataType], agent: Agent[UserDataType]) -> str: + """Dynamic instructions that access ctx.context.age directly.""" + # This should work - direct access to the generic type + return f"You are helping {ctx.context.name} who is {ctx.context.age} years old." + + +@function_tool +def tool_with_context_access(ctx: RunContextWrapper[UserDataType]) -> str: + """Tool that tries to access ctx.context.age directly.""" + # This currently fails with AttributeError: 'dict' object has no attribute 'age' + # and requires manual conversion: UserDataType.model_validate(ctx.context) + return f"Tool called for {ctx.context.name} who is {ctx.context.age} years old." + + +@function_tool +def tool_with_manual_conversion(ctx: RunContextWrapper[UserDataType]) -> str: + """Tool that manually converts ctx.context to the proper type.""" + # This is the current workaround + user_data = UserDataType.model_validate(ctx.context) + return f"Tool called for {user_data.name} who is {user_data.age} years old." + + +@pytest.mark.asyncio +async def test_dynamic_instructions_work(): + """Test that dynamic instructions can access ctx.context.age directly.""" + user_context = UserDataType(name="Alice", age=30) + + agent = Agent( + name="Test Agent", + instructions=dynamic_instructions, + ) + + # This should work without issues + result = await agent.get_system_prompt(RunContextWrapper(context=user_context)) + assert result == "You are helping Alice who is 30 years old." + + +@pytest.mark.asyncio +async def test_tool_validation_fails_with_direct_access(): + """Test that tool validation currently fails when accessing ctx.context.age directly.""" + user_context = UserDataType(name="Alice", age=30) + + agent = Agent( + name="Test Agent", + instructions="You are a helpful assistant.", + tools=[tool_with_context_access], + ) + + # This should fail with AttributeError: 'dict' object has no attribute 'age' + with pytest.raises(AttributeError, match="'dict' object has no attribute 'age'"): + await Runner.run( + agent, + "Call the tool", + context=user_context, + ) + + +@pytest.mark.asyncio +async def test_tool_validation_works_with_manual_conversion(): + """Test that tool validation works when manually converting ctx.context.""" + user_context = UserDataType(name="Alice", age=30) + + agent = Agent( + name="Test Agent", + instructions="You are a helpful assistant. Call the tool.", + tools=[tool_with_manual_conversion], + ) + + # This should work with manual conversion + result = await Runner.run( + agent, + "Call the tool", + context=user_context, + ) + + # The tool should have been called successfully + assert "Tool called for Alice who is 30 years old" in str( + result.final_output) + + +@pytest.mark.asyncio +async def test_generic_type_consistency_after_fix(): + """Test that both dynamic instructions and tool validation work consistently after the fix.""" + user_context = UserDataType(name="Bob", age=25) + + agent = Agent( + name="Test Agent", + instructions=dynamic_instructions, + tools=[tool_with_context_access], + ) + + # After the fix, this should work without manual conversion + result = await Runner.run( + agent, + "Call the tool", + context=user_context, + ) + + # Both dynamic instructions and tool should work + assert "Tool called for Bob who is 25 years old" in str( + result.final_output) + + +def test_tool_context_preserves_generic_type(): + """Test that ToolContext.from_agent_context preserves generic type information.""" + # Create a RunContextWrapper with typed context + user_data = UserDataType(name="Alice", age=30) + run_context = RunContextWrapper(context=user_data) + + # Create ToolContext using from_agent_context (this is what happens in tool validation) + tool_context = ToolContext.from_agent_context( + context=run_context, + tool_call_id="test_call_123" + ) + + # After the fix, the context should maintain its type + assert isinstance(tool_context.context, UserDataType) + assert tool_context.context.age == 30 + assert tool_context.context.name == "Alice" + + # Direct access should work without AttributeError + assert tool_context.context.age == 30 + assert tool_context.context.name == "Alice" + + +def test_tool_context_with_none_context(): + """Test that ToolContext.from_agent_context works with None context.""" + # Create a RunContextWrapper with None context + run_context = RunContextWrapper(context=None) + + # Create ToolContext using from_agent_context + tool_context = ToolContext.from_agent_context( + context=run_context, + tool_call_id="test_call_123" + ) + + # The context should be None + assert tool_context.context is None + + +def test_tool_context_with_dict_context(): + """Test that ToolContext.from_agent_context works with dict context.""" + # Create a RunContextWrapper with dict context + dict_context = {"name": "Charlie", "age": 35} + run_context = RunContextWrapper(context=dict_context) + + # Create ToolContext using from_agent_context + tool_context = ToolContext.from_agent_context( + context=run_context, + tool_call_id="test_call_123" + ) + + # The context should maintain its dict type + assert isinstance(tool_context.context, dict) + assert tool_context.context["name"] == "Charlie" + assert tool_context.context["age"] == 35 + + +def test_tool_context_usage_preservation(): + """Test that ToolContext.from_agent_context preserves usage information.""" + from agents.usage import Usage + + # Create a RunContextWrapper with custom usage + user_data = UserDataType(name="Dave", age=40) + custom_usage = Usage() + custom_usage.total_tokens = 100 + run_context = RunContextWrapper(context=user_data, usage=custom_usage) + + # Create ToolContext using from_agent_context + tool_context = ToolContext.from_agent_context( + context=run_context, + tool_call_id="test_call_123" + ) + + # The usage should be preserved + assert tool_context.usage is custom_usage + assert tool_context.usage.total_tokens == 100