Skip to content

Conversation

@HezziCode
Copy link

Fix: Preserve Generic Type Information in ToolContext

Problem

There was an inconsistency in how RunContextWrapper[T] generics were handled between dynamic instructions and tool validation functions:

  • Dynamic Instructions: ctx.context.age worked correctly ✅
  • Tool Validation: ctx.context.age threw AttributeError: 'dict' object has no attribute 'age'

This forced developers to manually convert context using UserDataType.model_validate(ctx.context) in tool validation functions.

Root Cause

The issue was in ToolContext.from_agent_context() method which used field-by-field copying:

# Old problematic code
for f in fields(RunContextWrapper):
    setattr(tool_context, f.name, getattr(context, f.name))



# New fixed code
return cls(
    context=context.context,  # Direct assignment preserves type
    usage=context.usage,
    tool_name=tool_name,
    tool_call_id=tool_call_id,
)



# Before (tool validation)
async def tool_validation(ctx: RunContextWrapper[UserDataType], agent):
    user_data = UserDataType.model_validate(ctx.context)  # Manual conversion needed
    if user_data.age >= 18:
        return True

# After (tool validation) 
async def tool_validation(ctx: RunContextWrapper[UserDataType], agent):
    if ctx.context.age >= 18:  # Direct access works now!
        return True

- Fix inconsistency between dynamic instructions and tool validation
- ToolContext.from_agent_context now directly passes context object instead of field copying
- Ensures ctx.context maintains proper type in tool validation functions
- Add comprehensive tests for generic type consistency
- Add edge case tests for None, dict, and usage preservation

Fixes issue where ctx.context.attribute worked in dynamic instructions
but failed with AttributeError in tool validation functions.
@seratch
Copy link
Member

seratch commented Sep 12, 2025

I don't think this change is necessary. The following code works without any issues:

import asyncio
from pydantic import BaseModel
from agents import Agent, RunContextWrapper, Runner, function_tool

class MyContext(BaseModel):
    foo: str

@function_tool
def get_weather(cxt: RunContextWrapper[MyContext], city: str) -> str:
    print(cxt.context.foo)
    return "Sunny with wind."

agent = Agent(
    name="Hello world",
    instructions="You are a helpful agent. You must call the tools.",
    tools=[get_weather],
)

async def main():
    result = await Runner.run(
        agent, input="What's the weather in Tokyo?", context=MyContext(foo="FOO")
    )
    print(result.final_output)

if __name__ == "__main__":
    asyncio.run(main())

@seratch seratch closed this Sep 12, 2025
@HezziCode HezziCode deleted the fix/consistent-generic-type-handling branch September 16, 2025 00:06
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants