Most agent frameworks sell you an abstraction and hide the loop. This one does the opposite: the abstraction is a plain Python dataclass, and the loop is laid out in a dedicated internals package where every hard decision gets its own file. openai/openai-agents-python is the SDK behind the “agents as software” pattern that the rest of the ecosystem now copies — agents configured with instructions, tools, guardrails, and handoffs; runs that alternate model calls and tool turns until final output. But what earns it a source tour is what the README does not lead with. A five-thousand-line state machine that serializes a live run so it can pause for a human approval and resume tomorrow. A translation layer that speaks the Responses API natively and reassembles Chat Completions deltas into the same event vocabulary. A sandbox subsystem with two thousand lines devoted solely to deciding what may be mounted into a container and what must never leak out. MIT-licensed, Python 3.10+, installed with pip install openai-agents, and large enough that reading it feels like reading three frameworks that share a namespace.
The architecture splits along its package seams. The core is the Agent — one dataclass holding instructions, tools, output types, guardrails, and handoffs — and the Runner, whose public surface is four start methods and whose private surface is a loop split into turn preparation, tool planning, tool execution, and turn resolution. Around the core sits the capability layer: function tools with schemas generated from type hints, agents wrapped as tools for other agents, MCP server clients, input and output guardrails, and the handoff machinery that transfers a conversation between agents while rewriting its history. Below that is the model layer — an interface of exactly two methods, implemented against the Responses API and adapted to Chat Completions by a converter, routable to a hundred external LLMs through a LiteLLM bridge. And around everything run the services: session protocols for memory, tracing providers for observability, voice and realtime front doors, and the sandbox execution environment. As always in this series, what follows is an educational tour of published source code.
The SDK at a glance: the Runner drives the Agent through the run loop and its serializable state, the capability layer arms it with tools, MCP, guardrails and handoffs, the model layer routes to Responses or beyond, and the sandbox and voice stacks run it in containers and over audio.
Reading the overview from left to right:
- The agent abstraction is a dataclass of instructions, tools, output types, guardrails and handoffs.
- The Runner exposes sync, async, streamed and resumed entry points around one loop.
- The run loop internals alternate model calls with tool turns until final output.
- The resumable run state serializes a live run so it can pause for approval and resume.
- The tool system builds function tools, agent-as-tool wrappers and hosted tools.
- The handoff machinery transfers conversations between agents with history filtering.
- The model contract is two methods every provider must implement in Responses format.
- The sandbox agent binds an agent to a guarded container workspace for long tasks.
Why You Need This
The first reason is the run loop, because it is the most thoroughly decomposed agent loop in public Python. The loop itself is only the skeleton; the decisions live in siblings like turn_resolution.py — three and a half thousand lines deciding whether a model response means final output, more tool calls, a handoff, or a continuation — and tool_execution.py, which runs tool calls in parallel, enforces approvals, and records outputs as items. The payoff is run_state.py: five thousand lines that serialize an in-flight run to JSON and resurrect it, approvals pending and all, which is how the SDK supports human-in-the-loop workflows that span process restarts. The run context threads typed local state, token usage, and permission decisions through every step, and the result classes give streaming consumers the same item vocabulary the loop uses internally. Read these five files and you will never again wonder what “the agent loop” actually means.
The second reason is the model layer, because provider-agnosticism is claimed by every framework and structurally enforced by this one. The Model interface is two abstract methods — a non-streaming get_response and a streaming stream_response — and the docstring alone is a lesson: tool invocations must carry a call ID stable across the run’s serialized resume lineage, so a paused-and-resumed agent can match outputs to calls. The Responses implementation is the native path, two thousand lines including a websocket session; the Chat Completions converter and its stream handler reassemble completion deltas into Responses-style events so the loop cannot tell the difference; and the multi-provider router dispatches on model-name prefixes, handing off to the LiteLLM bridge for the hundred-model long tail. The capability side keeps pace: the MCP client — two thousand four hundred lines over stdio, streaming, and in-process servers — and its lifecycle manager fold external tool ecosystems into the same toolset as local functions.
The third reason is the sandbox subsystem, because long-horizon agents need a filesystem and a shell, and this codebase treats that requirement with paranoia worth studying. The SandboxAgent binds an agent to a workspace declared by a manifest, where entries can be cloned git repositories or mounts pulled from S3, GCS, Azure Blob, Box, or R2 through the mount pattern layer. The mount security module — two thousand lines of path checks, redaction, and policy enforcement — guards the boundary before anything is mounted, and the provider matrix underneath runs from a local Docker implementation to eight cloud sandboxes including the Modal client. Memory follows the same protocol-driven style: the Session protocol is under two hundred lines, the SQLite default ships in the core, and Redis, MongoDB, SQLAlchemy, and encrypted variants live in extensions — with a compaction session for histories that outgrow the context window.
The detail view: run internals on the left with turn resolution, tool execution and serialized state; the tool and delegation layer with MCP and handoff history; model providers from Responses through the converter to LiteLLM; session backends and trace exporters; and the sandbox subsystem with its security screening and provider matrix.
The detail view shows how the seams meet. The loop edges all point inward — turn resolution dispatches to tool execution, tool execution invokes the tool objects whose schemas come from the schema generator, and everything is observed by trace creation and exported through batch processors. The model layer is a fan: one interface, two native dialects, one router, one bridge — which is why a new provider is a file, not a fork. On the right, the sandbox stack is the widest: guardrails on the conversation side, mount security on the filesystem side, and handoff history rewriting on the delegation side, each a boundary where the SDK chose enforcement over convention. The voice and realtime sessions ride the same run loop rather than replacing it, which is the quiet architectural decision that keeps audio agents from becoming a second framework.
From Install to a First Handoff
Installation is pip install openai-agents (or uv add openai-agents), with pip install 'openai-agents[voice]' adding the audio stack and [redis] the distributed session store. Set OPENAI_API_KEY, and the smallest useful program is two objects: an Agent with a name and instructions, and Runner.run_sync(agent, "task") whose result carries final_output. From there the SDK’s design invites incrementally adding one capability at a time — pass handoffs=[other_agent] and the conversation can transfer when the routing prompt fires; pass session=SQLiteSession("id") and history persists between runs with no code changes; wrap a plain function with the @function_tool decorator and it joins the toolset with a schema generated from its signature. For long-horizon work, instantiate a SandboxAgent with a manifest and a Docker or cloud sandbox client; for approval-gated runs, capture the run state when the SDK pauses and call the resume entry point after human sign-off. The examples directory covers each pattern from a two-file script upward, and the tracing dashboard makes the first debugging session visual rather than print-driven.
Honest limits: pull requests are limited to repository collaborators — issues are welcome, code contributions are not, which shapes how the project evolves. The docs lean OpenAI-first, and the realtime and voice stacks are built against OpenAI model endpoints, so truly non-OpenAI deployments should verify their path through the LiteLLM bridge rather than assume parity. The sandbox subsystem is powerful but platform-specific — the Unix local sandbox is macOS and Linux only, and Windows users are pointed at Docker. And the internals package names are not a stability promise: the public API is the Enjoyed this post? Never miss out on future posts by following us agents namespace, while run_internal can and does reshuffle between releases, so pin your versions and read the changelog before upgrading.