openai-agents
OpenAI's first-party framework for multi-agent workflows. An Agent is a model plus instructions, tools, guardrails, and handoffs; Runner executes the loop until a final output. It supports the Responses and Chat Completions APIs plus 100+ other models via LiteLLM, ships sessions for automatic conversation history, has tracing built in, and keeps adding surfaces: sandbox agents for long-horizon container work, realtime voice agents over WebSocket, and voice pipelines.
The sane default for agent orchestration on OpenAI models: a small concept count, strong docs, and first-party backing. Treat the provider-agnostic pitch with mild skepticism, and pin your versions while it is 0.x.
Use it if
- You are orchestrating several specialized agents and want handoffs and agents-as-tools as first-class primitives instead of hand-rolled routing code
- You want typed results: set output_type to a Pydantic model and final_output comes back validated against it
- You need conversation memory without bookkeeping: pass a session (SQLite, Redis, SQLAlchemy, MongoDB, or OpenAI-hosted) and history threads itself across runs
- You live in the OpenAI stack and want run traces in the OpenAI dashboard with zero setup
- You mostly call one model with one prompt; Agent plus Runner is indirection you do not need over the plain openai package
- It is 0.x and moves fast: releases land weekly, newer surfaces like sandbox and realtime agents are still shifting, and minor bumps have changed APIs, so unpinned installs are a hazard
- You want explicit graph state machines with checkpointing, replay, and human-in-the-loop persistence as the core abstraction; the SDK's loop is deliberately simple and LangGraph-style control is not the design
- Non-OpenAI providers are second-tier in practice: tracing uploads to OpenAI by default, hosted tools like web search, computer use, and sandboxes assume OpenAI models, and the LiteLLM bridge adds its own dependency and quirks
Setup reality
pip install openai-agents and hello world runs with only OPENAI_API_KEY set. Then the extras stack up: voice needs [voice], Redis sessions [redis], Docker sandboxes [docker], LiteLLM providers [litellm]. The API is async-first; Runner.run_sync exists for scripts but fails inside notebooks or servers that already run an event loop, a recurring first-day trap. Tracing uploads run data to OpenAI by default, which surprises teams with data policies, so disable or redirect it deliberately. Weekly 0.x releases mean you will re-pin often.
Patterns
One agent, one runhello-world-agent
from agents import Agent, Runner
agent = Agent(name="Assistant", instructions="You are a helpful assistant")
result = Runner.run_sync(agent, "Write a haiku about recursion in programming.")
print(result.final_output)run_sync wraps an event loop, so it raises inside Jupyter or an async web server; use await Runner.run(...) there. OPENAI_API_KEY must be set before the first run.
Turn a Python function into a toolfunction-tool
from agents import Agent, Runner, function_tool
@function_tool
def get_weather(city: str) -> str:
"""Fetch current weather for a city."""
return f"Sunny in {city}, 22C"
agent = Agent(
name="Weather bot",
instructions="Answer weather questions using the tool.",
tools=[get_weather],
)
result = Runner.run_sync(agent, "How is the weather in Pune?")
print(result.final_output)The schema comes from type hints and the docstring, so vague hints produce vague schemas the model misuses. Tool exceptions are surfaced to the model as tool output by default, not raised to you; pass failure_error_function=None if you want them raised.
Route between specialized agentshandoffs
from agents import Agent, Runner
billing = Agent(name="Billing", instructions="Handle billing questions.")
refunds = Agent(name="Refunds", instructions="Handle refund requests.")
triage = Agent(
name="Triage",
instructions="Decide whether this is billing or refunds and hand off.",
handoffs=[billing, refunds],
)
result = Runner.run_sync(triage, "I was charged twice last month")
print(result.last_agent.name, "->", result.final_output)A handoff transfers the whole conversation to the target agent, unlike agents-as-tools where the parent keeps control. Wrap with handoff(agent, ...) when you need input filters or callbacks, and check result.last_agent to know who actually answered.
Validated Pydantic outputstructured-output
from pydantic import BaseModel
from agents import Agent, Runner
class CalendarEvent(BaseModel):
name: str
date: str
participants: list[str]
agent = Agent(
name="Extractor",
instructions="Extract calendar events from the text.",
output_type=CalendarEvent,
)
result = Runner.run_sync(agent, "Alice and Bob meet for standup on Friday")
event = result.final_output # a CalendarEvent instance
print(event.participants)output_type switches the model into structured output mode and final_output is the parsed instance, not a string. Models that fail to produce valid JSON raise, so keep the schema modest and field names obvious.
Persistent conversation historysession-memory
from agents import Agent, Runner, SQLiteSession
agent = Agent(name="Assistant", instructions="Reply concisely.")
session = SQLiteSession("user_123", "conversations.db")
result = Runner.run_sync(agent, "Hi, my name is Keyoor", session=session)
result = Runner.run_sync(agent, "What is my name?", session=session)
print(result.final_output) # remembers the earlier turnWithout the db path argument SQLiteSession is in-memory and vanishes with the process. Sessions store history only; for shared infra use RedisSession.from_url or SQLAlchemySession, and OpenAIConversationsSession stores history on OpenAI's side.
Stream output token by tokenstreaming-tokens
import asyncio
from openai.types.responses import ResponseTextDeltaEvent
from agents import Agent, Runner
async def main():
agent = Agent(name="Joker", instructions="Tell short jokes.")
result = Runner.run_streamed(agent, "Tell me a joke")
async for event in result.stream_events():
if event.type == "raw_response_event" and isinstance(event.data, ResponseTextDeltaEvent):
print(event.data.delta, end="", flush=True)
asyncio.run(main())run_streamed returns immediately with a RunResultStreaming; the run is not finished until stream_events() is fully consumed, and session persistence can complete after the last visible token. Stopping early leaves the run incomplete.
Reject bad input with a tripwireinput-guardrail
from agents import (
Agent,
Runner,
GuardrailFunctionOutput,
InputGuardrailTripwireTriggered,
input_guardrail,
)
@input_guardrail
async def no_homework(ctx, agent, user_input):
flagged = "do my homework" in str(user_input).lower()
return GuardrailFunctionOutput(output_info=None, tripwire_triggered=flagged)
agent = Agent(
name="Tutor",
instructions="Explain concepts, do not just give answers.",
input_guardrails=[no_homework],
)
try:
result = Runner.run_sync(agent, "Please do my homework for me")
except InputGuardrailTripwireTriggered:
print("Blocked by guardrail")A triggered tripwire raises an exception rather than returning a result, so wrap runs in try/except. Input guardrails only execute on the first agent in a run; output guardrails only on the last.
Keep control while calling sub-agentsagents-as-tools
from agents import Agent, Runner
translator = Agent(name="Translator", instructions="Translate to French.")
orchestrator = Agent(
name="Orchestrator",
instructions="Use tools to translate when asked.",
tools=[
translator.as_tool(
tool_name="translate_to_french",
tool_description="Translate the given text to French",
)
],
)
result = Runner.run_sync(orchestrator, "Say 'good morning' in French")
print(result.final_output)Unlike a handoff, the parent agent stays in charge and the sub-agent's answer comes back as tool output. By default the tool takes a single string input; pass parameters= with a Pydantic model for a structured schema.
Run an agent on a non-OpenAI model via LiteLLMnon-openai-model
from agents import Agent, Runner, set_tracing_disabled
from agents.extensions.models.litellm_model import LitellmModel
set_tracing_disabled(True) # traces would otherwise upload to OpenAI
agent = Agent(
name="Assistant",
instructions="Reply concisely.",
model=LitellmModel(model="anthropic/claude-opus-5", api_key="sk-ant-..."),
)
result = Runner.run_sync(agent, "Hello")
print(result.final_output)Requires pip install 'openai-agents[litellm]'. Hosted OpenAI tools (web search, computer use, sandboxes) do not work on third-party models, and tracing still points at OpenAI unless you disable it or set a different processor.
Cap the agent loopmax-turns-cap
from agents import Agent, Runner
from agents.exceptions import MaxTurnsExceeded
agent = Agent(name="Researcher", instructions="Answer using tools if needed.")
try:
result = Runner.run_sync(agent, "Summarize the topic", max_turns=5)
except MaxTurnsExceeded:
print("Agent looped too long, aborting")The default cap is generous, and a misbehaving tool loop burns tokens fast; set max_turns deliberately on anything unattended. The exception carries no partial result, so log intermediate items if you need forensics.
Alternatives
| Package | Registry | Pick it when |
|---|---|---|
| langgraph | PyPI | You want explicit graph state machines with checkpointing and replay for long-running workflows |
| pydantic-ai | PyPI | You want a type-first agent framework built for provider neutrality from day one |
| claude-agent-sdk | PyPI | You want a ready-made coding agent with built-in filesystem and shell tools running on Claude |