mrkeyoor.com_
Wed 05 Aug 10:45 UTC
PyPIAI / MLupdated 05 Aug 2026

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.

Verdict

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.

API stability3/5The Agent, Runner, handoff, and guardrail core has held steady since the 2025 launch, but the project is still 0.x at 0.19.4 with weekly releases, and newer surfaces (sandbox agents, realtime) are explicitly evolving between minors.
Docs4/5openai.github.io/openai-agents-python has a page per concept, generated API reference, and a large runnable examples directory; the fast-moving corners (realtime, sandboxes) lag the code, and some pages assume Responses API familiarity.
Maintenance5/5Pushed within hours of this review, 28k+ stars, weekly releases, and it is OpenAI's flagship open source SDK (the production successor to Swarm), so staffing is not a question.
Ecosystem4/5MCP tool support, a LiteLLM bridge to 100+ models, session backends from SQLite to Dapr, and third-party tracing processors (Logfire, Langfuse, and others); younger and smaller than the LangChain sprawl, but the pieces you need are there.

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
Skip it if

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 turn

Without 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

PackageRegistryPick it when
langgraphPyPIYou want explicit graph state machines with checkpointing and replay for long-running workflows
pydantic-aiPyPIYou want a type-first agent framework built for provider neutrality from day one
claude-agent-sdkPyPIYou want a ready-made coding agent with built-in filesystem and shell tools running on Claude