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

1. Install

pip install openai-agents
# Optional:
pip install "openai-agents[voice]"          # Realtime agents
pip install "openai-agents[redis]"          # Redis-backed sessions

Python 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 list
  • result.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_output

Uses 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 Session protocol)

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 UI

Traces 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 codex
codex "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

AspectOpenAI Agents SDKAnthropic Claude Agent SDKGoogle Gemini
StatusGA (openai-agents)GA (claude-agent-sdk)Patterns + Vertex AI Agent Builder
Provider-agnosticYes (100+ via adapters)Claude-onlyGemini-only
HandoffsYes (first-class)Subagents (different model)Custom orchestration
Hosted tools wrappersYesYes (server tools)Yes (function declarations)
Sandbox agentsYes (SandboxAgent)Code execution toolCode execution tool
Realtime / voiceRealtimeAgentNo equivalentLive API + custom
TracingBuilt-inBuilt-in (via Claude Code)Custom (via OTel)
SessionsBuilt-inVia ClaudeSDKClientclient.chats

19. Common pitfalls

  1. Forgetting to await Runner.run(...) — returns a coroutine; await or use run_sync.
  2. Docstring drives tool description — sparse docstring → unreliable tool calling. Be specific.
  3. Recursive type hints in tools — strict-mode schema rejects. Flatten.
  4. session and previous_response_id conflict — pick one (client-side history vs server-side state).
  5. Handoff loops — agent A hands off to B, B hands off back to A — infinite loop. Set max_turns on Runner.run(..., max_turns=10).
  6. output_type + free text — if the agent decides to refuse, output may not match the schema. Handle refusals.
  7. Tool exceptions abort the loop — wrap risky tools in try/except returning structured errors.
  8. max_turns default is 10 — for longer agentic flows, raise it.
  9. Tracing PII — by default traces include full inputs/outputs. Configure trace processors to redact for sensitive workloads.
  10. Sandbox limitsSandboxAgent container 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