The Agent system is the heart of the OpenAI Agents SDK. An Agent represents an AI assistant that can be configured with instructions, tools, guardrails, handoffs, and more. Think of an Agent as a "persona" or "role" that the LLM adopts, equipped with specific capabilities and constraints.
Core Classes
AgentBase
AgentBase is the base class for all agents. It provides the foundational attributes that are shared across different agent types (including Agent and RealtimeAgent).
name: str - The name of the agent. This is used for identification, tracing, and when the agent is exposed as a tool or handoff.
handoff_description: str | None - A human-readable description of what this agent does. This is crucial when the agent is used in handoffs, as it helps other agents understand when and why they should delegate to this agent.
tools: list[Tool] - A list of tools that this agent can use. Tools are functions or capabilities the agent can call to perform actions (e.g., web search, file operations, API calls).
mcp_servers: list[MCPServer] - A list of Model Context Protocol (MCP) servers that provide additional tools. MCP is a standard protocol for exposing tools to AI models.
mcp_config: MCPConfig - Configuration for MCP servers, including schema conversion settings and error handling.
Key Methods:
async def get_mcp_tools(run_context) - Fetches available tools from MCP servers. This is called each time the agent runs to get the current set of available tools.
async def get_all_tools(run_context) - Returns all tools available to the agent, combining both direct tools and MCP tools. It also filters out disabled tools and checks for tool name collisions.
Agent
Agent is the main agent class you'll use most often. It extends AgentBase and adds agent-specific configuration.
Location:src/agents/agent.py
Key Attributes:
instructions: str | Callable | None - The system prompt for the agent. This is the most important attribute as it defines the agent's behavior, personality, and capabilities. It can be:
A static string
A function that dynamically generates instructions based on context
None (no specific instructions)
prompt: Prompt | DynamicPromptFunction | None - A more advanced way to configure prompts using OpenAI's Prompt API. This allows dynamic configuration of instructions, tools, and other settings outside of your code. Only usable with OpenAI models using the Responses API.
handoffs: list[Agent | Handoff] - A list of sub-agents or handoff configurations that this agent can delegate to. This enables multi-agent workflows where specialized agents handle specific tasks.
model: str | Model | None - The model to use for this agent. If not specified, it uses the default model (currently "gpt-4.1"). You can specify:
A string model name (e.g., "gpt-4o")
A custom Model instance
model_settings: ModelSettings - Model-specific tuning parameters like temperature, top_p, max tokens, etc. These control the randomness and creativity of the model's responses.
input_guardrails: list[InputGuardrail] - Guardrails that run before the agent processes input. These are safety checks that can validate, filter, or reject input before it reaches the LLM.
output_guardrails: list[OutputGuardrail] - Guardrails that run after the agent produces output. These validate the final output to ensure it meets safety or quality standards.
output_type: type | AgentOutputSchemaBase | None - The expected type of the output. If not specified, output is a string. You can specify:
A Python type (dataclass, Pydantic model, TypedDict, etc.)
A custom AgentOutputSchemaBase for custom JSON schemas
AgentOutputSchema with strict_json_schema=False for non-strict schemas
hooks: AgentHooks | None - A class that receives callbacks on various lifecycle events for this specific agent. This allows you to hook into the agent's execution to add custom logic.
tool_use_behavior: Literal | StopAtTools | Callable - Controls how tool use is handled:
"run_llm_again" (default) - Tools are executed, then the LLM receives the results and can respond again
"stop_on_first_tool" - The first tool's output is treated as the final result
StopAtTools dict - Stop if specific tools are called
Custom function - Fine-grained control over tool-to-output logic
reset_tool_choice: bool - Whether to reset tool choice after a tool call. Defaults to True to prevent infinite loops of tool usage.
Agent Lifecycle
1. Creation
Python
from agents import Agent
agent = Agent(
name="research_agent",
instructions="You are a research assistant that helps find and summarize information.",
tools=[web_search_tool],
)
When you create an agent, the __post_init__ method runs extensive validation:
Checks that name is a string
Validates that tools is a list
Ensures instructions is either a string, callable, or None
Validates model settings compatibility
Checks that guardrails are lists
Validates output_type is a type or schema
Ensures hooks is an AgentHooks instance if provided
Validates tool_use_behavior is a valid option
This validation happens at creation time, so you get immediate feedback if configuration is invalid.
2. Execution
When you run an agent via Runner.run(), the following happens:
Context Setup - A RunContextWrapper is created with your user context
Tool Resolution - All available tools (direct + MCP) are gathered
Input Preparation - Input is converted to the format expected by the model
Guardrail Check - Input guardrails run (if configured)
Model Call - The LLM is called with instructions, input, and tools
Tool Execution - If the model calls tools, they're executed
Response Processing - Model output is processed
Output Guardrail Check - Output guardrails run (if configured)
Result Return - Final output is returned
3. Cloning
Agents support shallow cloning via the clone() method:
When called, it runs the nested agent with the provided input
The nested agent's output is returned to the calling agent
The nested agent runs in isolation - it doesn't automatically see the parent conversation history
You can configure input parameters, streaming, approval requirements, and more
Key Parameters of as_tool():
tool_name - Name of the tool (defaults to agent name transformed to function style)
tool_description - Description for the LLM to understand when to use it
custom_output_extractor - Function to extract output from the nested agent
is_enabled - Whether the tool is enabled (can be dynamic)
on_stream - Callback to receive streaming events from nested runs
run_config - Run configuration for the nested agent
max_turns - Maximum turns for the nested agent
needs_approval - Whether the tool requires approval
parameters - Structured input type for the tool
input_builder - Function to build nested agent input from structured data
Difference from Handoffs:
Handoffs - The new agent receives the full conversation history and takes over the conversation
Agent as Tool - The new agent receives generated input and returns as a tool result, conversation continues with original agent
Context and Generics
Agents are generic over a context type:
Python
classAgent(Generic[TContext]):
...
What is Context?
Context is a mutable object you create that is passed to:
Tool functions
Handoff functions
Guardrail functions
Lifecycle hooks
Why use Context?
Context allows you to:
Share state across tool calls
Track information during agent execution
Pass configuration to tools
Maintain application-specific state
Example:
Python
from dataclasses import dataclass
from agents import Agent, function_tool, RunContextWrapper
@dataclassclassMyContext:
user_id: str
session_data: dict@function_tooldefget_user_preferences(context: RunContextWrapper[MyContext]) -> str:
user_id = context.context.user_id
# Fetch preferences for this userreturnf"Preferences for {user_id}"
agent = Agent(
name="personalized_agent",
instructions="Help the user with personalized recommendations",
tools=[get_user_preferences],
)
# When running
context = MyContext(user_id="123", session_data={})
result = await Runner.run(agent, "What do you recommend?", context=context)
Dynamic Instructions
Instructions can be dynamic functions that generate the system prompt based on context:
Python
defdynamic_instructions(
context: RunContextWrapper[MyContext],
agent: Agent[MyContext],
) -> str:
user_id = context.context.user_id
returnf"You are helping user {user_id}. Be friendly and helpful."
agent = Agent(
name="dynamic_agent",
instructions=dynamic_instructions,
)
This is useful when:
You need to personalize instructions based on user data
Instructions depend on runtime state
You want to A/B test different instructions
Instructions need to be generated from external sources
Tool Management
Adding Tools
Python
from agents import Agent, function_tool
@function_tooldefcalculate_sum(a: int, b: int) -> int:
return a + b
agent = Agent(
name="math_agent",
instructions="Help with math calculations",
tools=[calculate_sum],
)
Tool Namespaces
Tools can be organized into namespaces to avoid name collisions:
Python
from agents import Agent, function_tool, tool_namespace
@tool_namespace("math")@function_tooldefadd(a: int, b: int) -> int:
return a + b
@tool_namespace("math")@function_tooldefmultiply(a: int, b: int) -> int:
return a * b
agent = Agent(
name="calculator",
instructions="Use math tools for calculations",
tools=[add, multiply],
)
Dynamic Tool Enablement
Tools can be dynamically enabled or disabled:
Python
@function_tooldefadmin_function(context: RunContextWrapper[MyContext]) -> str:
return"Admin-only function"
agent = Agent(
name="admin_agent",
instructions="Admin functions",
tools=[admin_function],
)
# The tool can have an is_enabled function
admin_function.is_enabled = lambda ctx, agent: ctx.context.user_id == "admin"
Handoff Management
Adding Handoffs
Python
from agents import Agent, handoff
specialist_agent = Agent(
name="specialist",
instructions="Handle specialized tasks",
handoff_description="Specialist for complex technical issues",
)
general_agent = Agent(
name="general",
instructions="Handle general queries",
handoffs=[handoff(specialist_agent)],
)
Handoff Input Filters
You can filter what information is passed to the next agent:
Python
deffilter_handoff_input(data: HandoffInputData) -> HandoffInputData:
# Only pass the last 5 itemsreturn data.clone(new_items=data.new_items[-5:])
agent = Agent(
name="general",
instructions="General agent",
handoffs=[handoff(specialist_agent, input_filter=filter_handoff_input)],
)
from agents import Agent
agent = Agent(
name="fast_agent",
instructions="Quick responses",
model="gpt-4o-mini", # Faster, cheaper model
)
Model Settings
Python
from agents import Agent, ModelSettings
agent = Agent(
name="creative_agent",
instructions="Be creative",
model_settings=ModelSettings(
temperature=0.9, # More creative
top_p=0.9,
),
)
GPT-5 Special Handling
The SDK has special handling for GPT-5 models, which require specific reasoning settings. If you specify a non-GPT-5 model but the default is GPT-5, the SDK automatically adjusts the model settings to be compatible.
Output Types
String Output (Default)
Python
agent = Agent(
name="simple_agent",
instructions="Simple responses",
)
# Output is a string
Structured Output
Python
from dataclasses import dataclass
from agents import Agent
@dataclassclassSummary:
title: str
points: list[str]
agent = Agent(
name="summarizer",
instructions="Summarize the input",
output_type=Summary,
)
# Output is a Summary dataclass instance
agent = Agent(
name="default_agent",
instructions="Use tools as needed",
tool_use_behavior="run_llm_again", # Default
)
# Tools run, LLM sees results, can respond again
Stop on First Tool
Python
agent = Agent(
name="direct_agent",
instructions="Call one tool and return",
tool_use_behavior="stop_on_first_tool",
)
# First tool output is the final result
Stop at Specific Tools
Python
from agents import Agent, StopAtTools
agent = Agent(
name="controlled_agent",
instructions="Use tools carefully",
tool_use_behavior=StopAtTools(stop_at_tool_names=["sensitive_tool"]),
)
# Stops if sensitive_tool is called
Custom Behavior
Python
from agents import Agent, ToolsToFinalOutputResult, ToolsToFinalOutputFunction
asyncdefcustom_tool_handler(
context: RunContextWrapper,
tool_results: list[FunctionToolResult],
) -> ToolsToFinalOutputResult:
# Custom logic to decide if tool results are finalifany("error"in r.output for r in tool_results):
return ToolsToFinalOutputResult(is_final_output=True, final_output="Error occurred")
return ToolsToFinalOutputResult(is_final_output=False)
agent = Agent(
name="custom_agent",
instructions="Custom tool handling",
tool_use_behavior=custom_tool_handler,
)
Lifecycle Hooks
Agent-level hooks allow you to react to events specific to this agent:
# Good
agent = Agent(
name="summarizer",
instructions="You are a summarization expert. Given a text, provide a concise summary in 3 bullet points. Each bullet should be under 20 words.",
)
# Avoid
agent = Agent(
name="vague_agent",
instructions="Summarize things", # Too vague
)
specialist = Agent(
name="technical_support",
instructions="Handle technical issues",
handoff_description="Technical support specialist for debugging and troubleshooting",
)
4. Context Design
Keep context focused:
Python
# Good - focused context@dataclassclassUserContext:
user_id: str
preferences: dict# Avoid - bloated context@dataclassclassEverythingContext:
user_id: str
preferences: dict
session_history: list
cache: dict
database_connection: Any# Don't put heavy resources here
5. Guardrail Granularity
Use guardrails at appropriate levels:
Python
# Input guardrail - for filtering what comes in@input_guardraildefcheck_safety(input: str) -> GuardrailFunctionOutput:
...
# Output guardrail - for validating what goes out@output_guardraildefcheck_quality(output: str) -> GuardrailFunctionOutput:
...
# Tool guardrail - for specific tool validation@tool_input_guardraildefcheck_tool_args(args: dict) -> ToolGuardrailFunctionOutput:
...
from agents import Agent, ModelBehaviorError
try:
result = await Runner.run(agent, input)
except ModelBehaviorError as e:
print(f"Model behaved unexpectedly: {e}")
Tool Errors
Tools can raise errors:
Python
@function_tooldefrisky_operation() -> str:
raise ValueError("Something went wrong")
# The SDK will catch this and format an error message for the model
Guardrail Tripwires
When guardrails trigger:
Python
from agents import InputGuardrailTripwireTriggered, OutputGuardrailTripwireTriggered
try:
result = await Runner.run(agent, input)
except InputGuardrailTripwireTriggered as e:
print(f"Input guardrail triggered: {e}")
except OutputGuardrailTripwireTriggered as e:
print(f"Output guardrail triggered: {e}")