Agents SDK (OpenAI)
The openai-agents-python (and JS counterpart) SDK is OpenAI’s official agentic framework — a lightweight toolkit built around four primitives: Agents (LLMs configured with instructions + tools + guardrails + handoff targets), Runner (the loop that drives an agent to completion), handoffs (a way for one agent to transfer control to another mid-conversation), and tools (function tools via @function_tool plus hosted tools like web_search, file_search, computer_use, code_interpreter). It’s provider-agnostic despite the name — same code works against the Responses API, Chat Completions, or 100+ other LLM providers via shim adapters. Underneath it sits a tracing layer for observability, a sessions abstraction for conversation history, and guardrails / tripwires for input/output validation that can early-terminate an agent loop. The Sandbox Agent variant wraps an Agent in a container for filesystem + shell operations.
See also
- openai-api-and-sdks — the underlying Responses API surface
- tool-use-and-function-calling-openai — the tools mechanism the SDK wraps
- realtime-api-openai —
RealtimeAgentfor voice - hidden-tricks-and-gotchas-openai — agent loop quirks
- claude-agent-sdk — sister doc for Claude’s agent SDK
- agents-and-orchestration-patterns — vendor-neutral patterns
1. Install
pip install openai-agents
# Optional:
pip install "openai-agents[voice]" # Realtime agents
pip install "openai-agents[redis]" # Redis-backed sessionsPython 3.10+. Node SDK: npm install @openai/agents.
2. Minimal agent
from agents import Agent, Runner
agent = Agent(
name="Tutor",
instructions="You answer history questions clearly and concisely.",
)
result = await Runner.run(agent, "When did the Roman Empire fall?")
print(result.final_output)Runner.run() is async by default; Runner.run_sync() for sync contexts.
result is a RunResult with:
result.final_output— the agent’s final response (str or structured)result.new_items— items added during the run (messages, tool calls, etc.)result.to_input_list()— full conversation history as a serializable listresult.last_agent— which agent ended the run (relevant for handoffs)
3. Tools via @function_tool
from agents import Agent, Runner, function_tool
@function_tool
def get_weather(city: str, unit: str = "celsius") -> dict:
"""Get current weather for a city.
Args:
city: City name.
unit: Temperature unit, celsius or fahrenheit.
"""
return {"temp": 22, "unit": unit, "condition": "sunny"}
agent = Agent(
name="Weather Bot",
instructions="You help with weather queries.",
tools=[get_weather],
)
result = await Runner.run(agent, "Weather in Tokyo?")The SDK introspects:
- Function signature (parameter types via type hints)
- Docstring (becomes the tool description; Google-style
Args:becomes per-param descriptions) - Strict mode is enabled by default (the underlying schema gets
strict: true)
Like Gemini’s automatic function calling: the SDK runs the tool when the model calls it and feeds the result back automatically.
4. Structured output
from pydantic import BaseModel
class WeatherReport(BaseModel):
city: str
temperature: float
condition: str
agent = Agent(
name="Weather Bot",
instructions="...",
tools=[get_weather],
output_type=WeatherReport, # final output must conform to this
)
result = await Runner.run(agent, "Weather in Tokyo?")
report: WeatherReport = result.final_outputUses response_format: json_schema strict mode under the hood.
5. Handoffs
Multi-agent orchestration via explicit handoff targets:
from agents import Agent, Runner, handoff
history_tutor = Agent(
name="History Tutor",
instructions="You answer history questions in detail.",
)
math_tutor = Agent(
name="Math Tutor",
instructions="You solve math problems step by step.",
)
triage = Agent(
name="Triage",
instructions="You route questions to the appropriate tutor.",
handoffs=[history_tutor, math_tutor],
)
result = await Runner.run(triage, "When was the Magna Carta signed?")
print(f"Answered by: {result.last_agent.name}")
print(result.final_output)Under the hood, the SDK injects handoff “tools” into the triage agent’s tool set. The model can call transfer_to_history_tutor like any other tool, and the SDK swaps the active agent in the loop.
on_handoff callback
Pre-process / inject context during handoff:
async def on_handoff_to_history(ctx):
# Fetch related historical sources
sources = await fetch_sources(ctx.last_user_message)
return {"sources": sources}
triage = Agent(
name="Triage",
handoffs=[handoff(history_tutor, on_handoff=on_handoff_to_history)],
)The return value gets added to the conversation as context for the receiving agent.
6. Sessions (conversation history)
from agents import Agent, Runner, Session
agent = Agent(name="Assistant", instructions="...")
session = Session(session_id="user-12345")
# Turn 1
await Runner.run(agent, "Hi, my name is Adam.", session=session)
# Turn 2 — history retained
result = await Runner.run(agent, "What's my name?", session=session)
print(result.final_output) # "Your name is Adam."Session backends:
- In-memory (default; lost on process restart)
- Redis (
pip install openai-agents[redis]) - Custom (implement the
Sessionprotocol)
The session stores conversation items, including handoffs, tool calls, and outputs.
7. Server-side conversation via previous_response_id
Alternative to client-side sessions:
result = await Runner.run(agent, "First message",
run_config=RunConfig(conversation_id="conv_abc"))
result = await Runner.run(agent, "Follow up",
run_config=RunConfig(previous_response_id=result.last_response_id))OpenAI stores the conversation server-side; the SDK passes previous_response_id to Responses API. Lighter weight than client-side session storage for purely server-resident workloads.
8. Guardrails / tripwires
Input + output validation that can short-circuit the agent loop:
from agents import input_guardrail, output_guardrail, GuardrailFunctionOutput
@input_guardrail
async def check_scope(ctx, input):
if "tax" not in input.lower():
return GuardrailFunctionOutput(
output_info="Only tax questions allowed",
tripwire_triggered=True,
)
return GuardrailFunctionOutput(output_info=None, tripwire_triggered=False)
@output_guardrail
async def check_safety(ctx, output):
if contains_pii(output):
return GuardrailFunctionOutput(
output_info="PII detected",
tripwire_triggered=True,
)
return GuardrailFunctionOutput(output_info=None, tripwire_triggered=False)
agent = Agent(
name="Tax Bot",
instructions="...",
input_guardrails=[check_scope],
output_guardrails=[check_safety],
)When a tripwire triggers, Runner.run() raises InputGuardrailTripwireTriggered or OutputGuardrailTripwireTriggered. Catch and handle:
try:
result = await Runner.run(agent, user_input)
except InputGuardrailTripwireTriggered as e:
return f"Refused: {e.guardrail_result.output_info}"Guardrails can also run in parallel with the agent for low-latency validation (the agent starts speaking; if guardrail trips, output is suppressed). Tradeoff: tokens spent before tripwire are wasted.
9. Hosted tools
Wrap OpenAI’s Responses-API hosted tools:
from agents import Agent, Runner
from agents.tools import WebSearchTool, FileSearchTool, ComputerTool, CodeInterpreterTool
agent = Agent(
name="Researcher",
instructions="...",
tools=[
WebSearchTool(),
FileSearchTool(vector_store_ids=["vs_abc"]),
ComputerTool(display_width=1024, display_height=768),
CodeInterpreterTool(),
],
)These map to {type: "web_search_preview"}, etc. on the Responses API.
10. Sandbox Agents
Containerized agents with filesystem + shell access. Useful for code-generation, repo-analysis, file-manipulation workflows.
from agents import SandboxAgent
agent = SandboxAgent(
name="Code Reviewer",
instructions="Review the code in /workspace and suggest improvements.",
workspace="/path/to/repo", # mounted into the sandbox
)
result = await Runner.run(agent, "Summarize this repo.")Under the hood, the sandbox is a container with a shell, filesystem, and standard tools. The agent can run git, ls, grep, cat etc. The Codex CLI (also from OpenAI) is built on top.
11. Realtime agents
For voice:
from agents.realtime import RealtimeAgent, RealtimeRunner
agent = RealtimeAgent(
name="Voice Assistant",
instructions="You are a friendly voice assistant.",
model="gpt-realtime-2",
voice="alloy",
)
async with RealtimeRunner(agent) as runner:
# ... audio I/O ...Handles WebSocket, VAD, interruption, function calling mid-stream. See realtime-api-openai for the underlying API.
12. Tracing
Built-in observability:
from agents import Runner
result = await Runner.run(agent, "...")
print(result.trace_id) # link to trace in OpenAI platform UITraces show:
- Each agent’s run
- LLM calls (tokens, latency)
- Tool calls (args, results)
- Handoffs
- Guardrail evaluations
View in the OpenAI Platform’s tracing UI or export to your own observability stack:
from agents.tracing import set_trace_processors, BatchTraceProcessor
set_trace_processors([
BatchTraceProcessor(my_otel_exporter),
])Integrations with LangSmith, Datadog, Honeycomb via standard OTel adapters.
13. Provider-agnostic mode
from agents import Agent, set_default_openai_client
import litellm # or any OpenAI-compatible client
# Point the SDK at a non-OpenAI provider
custom_client = litellm.Router(...)
set_default_openai_client(custom_client)
agent = Agent(name="...", model="claude-opus-4-7", instructions="...")The SDK speaks OpenAI’s request/response shape; many providers offer OpenAI-compatible APIs (Anthropic via LiteLLM, Together, Groq, Ollama, etc.). Works across 100+ models.
14. Run configuration
from agents import RunConfig
config = RunConfig(
workflow_name="my-workflow",
group_id="session-abc",
trace_metadata={"user_id": "12345"},
model="gpt-5", # override agent's default
model_settings={"temperature": 0.7, "max_output_tokens": 4000},
handoff_input_filter=lambda ctx, input: input, # transform input on handoff
)
result = await Runner.run(agent, "...", run_config=config)15. Streaming
async for event in Runner.run_streamed(agent, "..."):
if event.type == "raw_response_event":
# underlying Responses API event
pass
elif event.type == "agent_updated_stream_event":
# active agent changed (handoff)
pass
elif event.type == "run_item_stream_event":
# new item added (tool call, message, etc.)
if event.item.type == "tool_call_output_item":
print(f"tool result: {event.item.output}")16. Computer use (browser / desktop control)
from agents import Agent, Runner
from agents.tools import ComputerTool
agent = Agent(
name="Operator",
model="computer-use-preview-2025-XX-XX",
instructions="You can use a web browser. Take screenshots and click as needed.",
tools=[ComputerTool(display_width=1024, display_height=768, environment="browser")],
)The SDK handles the screenshot ↔ action loop: model says “click(123,456)”, SDK captures screenshot via the configured environment, sends back result, repeats. Underneath, the same computer_use_preview hosted tool from Responses API.
For self-hosted browsers / desktops, you can implement custom ComputerTool subclasses.
17. Codex CLI integration
The Codex CLI is OpenAI’s terminal coding agent — written in Rust, built atop the Agents SDK pattern. Install:
# Mac / Linux
curl -fsSL https://chatgpt.com/codex/install.sh | sh
# Windows
# PowerShell script available
# npm: npm install -g @openai/codex
# Homebrew: brew install codexcodex "implement feature X in this repo"Authenticates via ChatGPT account (recommended) or API key. Runs as a sandboxed local agent with file edit / shell / git capabilities. The CLI is a real-world example of the Agents SDK pattern productionized.
18. Comparison to other vendors
| Aspect | OpenAI Agents SDK | Anthropic Claude Agent SDK | Google Gemini |
|---|---|---|---|
| Status | GA (openai-agents) | GA (claude-agent-sdk) | Patterns + Vertex AI Agent Builder |
| Provider-agnostic | Yes (100+ via adapters) | Claude-only | Gemini-only |
| Handoffs | Yes (first-class) | Subagents (different model) | Custom orchestration |
| Hosted tools wrappers | Yes | Yes (server tools) | Yes (function declarations) |
| Sandbox agents | Yes (SandboxAgent) | Code execution tool | Code execution tool |
| Realtime / voice | RealtimeAgent | No equivalent | Live API + custom |
| Tracing | Built-in | Built-in (via Claude Code) | Custom (via OTel) |
| Sessions | Built-in | Via ClaudeSDKClient | client.chats |
19. Common pitfalls
- Forgetting to
await Runner.run(...)— returns a coroutine;awaitor userun_sync. - Docstring drives tool description — sparse docstring → unreliable tool calling. Be specific.
- Recursive type hints in tools — strict-mode schema rejects. Flatten.
sessionandprevious_response_idconflict — pick one (client-side history vs server-side state).- Handoff loops — agent A hands off to B, B hands off back to A — infinite loop. Set
max_turnsonRunner.run(..., max_turns=10). output_type+ free text — if the agent decides to refuse, output may not match the schema. Handle refusals.- Tool exceptions abort the loop — wrap risky tools in try/except returning structured errors.
max_turnsdefault is 10 — for longer agentic flows, raise it.- Tracing PII — by default traces include full inputs/outputs. Configure trace processors to redact for sensitive workloads.
- Sandbox limits —
SandboxAgentcontainer has finite disk + memory; long-running workloads may need to checkpoint.
20. Worked example: multi-agent customer support
from agents import Agent, Runner, function_tool
@function_tool
def lookup_customer(email: str) -> dict:
"""Look up a customer by email."""
return {"id": "cust_123", "name": "Adam", "plan": "Pro"}
@function_tool
def search_kb(query: str) -> list[dict]:
"""Search the knowledge base."""
return [{"title": "Refund policy", "url": "..."}]
billing_agent = Agent(
name="Billing",
instructions="You handle billing questions. Use lookup_customer to find the user's plan.",
tools=[lookup_customer],
)
support_agent = Agent(
name="Support",
instructions="You handle product support. Use search_kb to find articles.",
tools=[search_kb],
)
triage = Agent(
name="Triage",
instructions=(
"You route customer messages. Billing questions go to Billing. "
"Product issues go to Support. Greet briefly first."
),
handoffs=[billing_agent, support_agent],
)
result = await Runner.run(triage, "I want a refund.")
# triage greets, then hands off to Billing
# Billing looks up customer, answers
print(result.final_output, "answered by", result.last_agent.name)21. Further reading
- openai-agents-python — repo
- Agents SDK docs — full reference
- Quickstart — getting started
- Practical Guide to Building Agents — OpenAI’s design playbook
- Codex CLI — production agent reference
- AGENTS.md spec — agent-instruction file format