The OpenAI Agents SDK (formerly Swarm, released as stable in early 2026) is a Python library for building multi-agent AI systems. Unlike LangChain’s abstraction-heavy approach or CrewAI’s role-playing model, the Agents SDK exposes five clean primitives and gets out of the way.
This guide covers everything from setup to production deployment, including handoffs, guardrails, sessions, and tracing.
Installing and Configuring
pip install openai-agents
Enter fullscreen mode Exit fullscreen mode
Python 3.10+ required. Set your API key:
export OPENAI_API_KEY=sk-...
Enter fullscreen mode Exit fullscreen mode
The SDK also works with non-OpenAI models via LiteLLM — more on that later.
The Five Core Primitives
Agent → LLM + system instructions + tools + handoffs
Runner → executes the agent loop (sync or async)
Tools → Python functions the agent can call
Handoffs → transfer control to another agent
Guardrails → validate input/output before processing
Sessions → persistent conversation state
Enter fullscreen mode Exit fullscreen mode
Your First Agent
from agents import Agent, Runner
agent = Agent(
name="Code Reviewer",
instructions="""You are a senior Python developer reviewing code for correctness,
security issues, and adherence to PEP 8. Be specific and actionable."""
)
result = Runner.run_sync(agent, "Review this function: def add(a,b): return a+b")
print(result.final_output)
Enter fullscreen mode Exit fullscreen mode
Runner.run_sync() is the blocking version. Use await Runner.run() in async contexts.
Tools: Extending What Agents Can Do
The @function_tool decorator converts any Python function into a tool the agent can call. The docstring becomes the tool’s description — write it clearly:
from agents import Agent, Runner, function_tool
import subprocess
import os
@function_tool
def run_tests(test_path: str) -> str:
"""Run pytest on the specified test file or directory.
Args:
test_path: Relative path to test file or directory (must be within ./tests/)
"""
# Security: restrict to tests/ directory only
safe_path = os.path.join("./tests", os.path.basename(test_path))
if not os.path.exists(safe_path):
return f"Error: {safe_path} does not exist"
result = subprocess.run(
["pytest", safe_path, "-v", "--tb=short"],
capture_output=True,
text=True,
timeout=60,
cwd=os.getcwd()
)
return result.stdout + result.stderr
@function_tool
def read_file(filepath: str) -> str:
"""Read the contents of a source file.
Args:
filepath: Path relative to the project root (no ../ allowed)
"""
if ".." in filepath:
return "Error: path traversal not allowed"
try:
with open(filepath, "r") as f:
return f.read()
except FileNotFoundError:
return f"Error: {filepath} not found"
agent = Agent(
name="Test Runner",
instructions="Run tests and analyze failures. Suggest fixes based on test output.",
tools=[run_tests, read_file]
)
Enter fullscreen mode Exit fullscreen mode
The agent decides when to call tools — you don’t orchestrate the sequence. The SDK handles the tool call loop until the agent returns a final response.
Built-in Tools
from agents.tools import WebSearchTool, CodeInterpreterTool
research_agent = Agent(
name="Research Assistant",
instructions="Search for current information and summarize findings accurately.",
tools=[WebSearchTool()]
)
data_agent = Agent(
name="Data Analyst",
instructions="Analyze data and generate visualizations.",
tools=[CodeInterpreterTool()]
)
Enter fullscreen mode Exit fullscreen mode
WebSearchTool uses OpenAI’s built-in search. CodeInterpreterTool runs Python in a sandbox — useful for data analysis without security concerns.
Handoffs: Multi-Agent Systems
The most powerful feature. When an agent calls handoff(), it transfers the conversation to a specialist agent with full context:
from agents import Agent, Runner, handoff
typescript_expert = Agent(
name="TypeScript Expert",
instructions="""You are a TypeScript specialist. Review code for:
- Type safety and proper generic usage
- Potential runtime errors hidden by 'any' types
- Missing null checks
Provide specific fixes, not general advice."""
)
security_reviewer = Agent(
name="Security Reviewer",
instructions="""You are a security engineer. Review code for:
- Injection vulnerabilities (SQL, command, path traversal)
- Insecure direct object references
- Missing input validation
- Exposed secrets or credentials
Flag severity as CRITICAL, HIGH, MEDIUM, or LOW."""
)
performance_analyst = Agent(
name="Performance Analyst",
instructions="""Analyze code for performance issues:
- N+1 query patterns
- Unnecessary re-renders (React)
- Memory leaks
- Blocking I/O in hot paths
Estimate the impact before suggesting optimizations."""
)
coordinator = Agent(
name="Code Review Coordinator",
instructions="""You coordinate code reviews. Analyze the code and route to specialists:
- TypeScript issues → typescript_expert
- Security concerns → security_reviewer
- Performance problems → performance_analyst
You can route to multiple specialists for the same code.""",
handoffs=[
handoff(typescript_expert),
handoff(security_reviewer),
handoff(performance_analyst)
]
)
result = Runner.run_sync(coordinator, """
Review this Next.js API route:
export async function GET(req: Request) {
const id = req.nextUrl.searchParams.get('id')
const user = await db.query(`SELECT * FROM users WHERE id = ${id}`)
return Response.json(user)
}
""")
print(result.final_output)
Enter fullscreen mode Exit fullscreen mode
The coordinator routes to security_reviewer (SQL injection), typescript_expert (untyped id), and potentially performance_analyst (SELECT * inefficiency). Each specialist reviews with full conversation history.
Typed Handoffs
Pass structured data between agents:
from pydantic import BaseModel
class BugReport(BaseModel):
severity: str # critical, high, medium, low
component: str
description: str
reproduction_steps: list[str]
bug_filer = Agent(
name="Bug Filer",
instructions="You file bugs in our tracking system from user reports.",
handoffs=[handoff(
triage_agent,
input_type=BugReport,
description="File a structured bug report for triage"
)]
)
Enter fullscreen mode Exit fullscreen mode
Guardrails: Safety and Validation
Guardrails run before the agent processes input or after it generates output. They can block, modify, or allow the request:
from agents import Agent, Runner, input_guardrail, output_guardrail, GuardrailTripwireTriggered, RunContextWrapper
from agents.guardrails import GuardrailFunctionOutput
@input_guardrail
async def block_pii(ctx: RunContextWrapper, agent: Agent, input: str) -> GuardrailFunctionOutput:
"""Block requests containing obvious PII."""
pii_patterns = ["ssn", "social security", "credit card", "cvv", "password"]
lower_input = input.lower()
for pattern in pii_patterns:
if pattern in lower_input:
return GuardrailFunctionOutput(
output_info=f"PII pattern detected: '{pattern}'",
tripwire_triggered=True # blocks the request
)
return GuardrailFunctionOutput(output_info="clean", tripwire_triggered=False)
@output_guardrail
async def check_output_safety(ctx: RunContextWrapper, agent: Agent, output: str) -> GuardrailFunctionOutput:
"""Ensure output doesn't contain code that could be harmful."""
dangerous_patterns = ["rm -rf", "DROP TABLE", "os.system", "__import__"]
for pattern in dangerous_patterns:
if pattern in output:
return GuardrailFunctionOutput(
output_info=f"Dangerous pattern in output: '{pattern}'",
tripwire_triggered=True
)
return GuardrailFunctionOutput(output_info="safe", tripwire_triggered=False)
safe_agent = Agent(
name="Safe Assistant",
instructions="Help users with coding questions.",
input_guardrails=[block_pii],
output_guardrails=[check_output_safety]
)
try:
result = Runner.run_sync(safe_agent, "My SSN is 123-45-6789, help me with Python")
except GuardrailTripwireTriggered as e:
print(f"Request blocked: {e}") # Request blocked: PII pattern detected: 'ssn'
Enter fullscreen mode Exit fullscreen mode
Guardrails run in parallel with the agent — they don’t add latency. The tripwire_triggered=True response immediately stops processing.
Structured Output
Force the agent to return a specific Pydantic model:
from pydantic import BaseModel
from typing import Literal
class CodeReview(BaseModel):
summary: str
issues: list[dict] # [{severity, description, line, fix}]
verdict: Literal["approve", "request_changes", "needs_discussion"]
estimated_fix_time: str
reviewer = Agent(
name="Structured Reviewer",
instructions="Review code and return structured feedback.",
output_type=CodeReview
)
result = Runner.run_sync(reviewer, code_snippet)
review: CodeReview = result.final_output # fully typed
print(f"Verdict: {review.verdict}")
print(f"Issues found: {len(review.issues)}")
Enter fullscreen mode Exit fullscreen mode
The SDK uses JSON schema under the hood to constrain model output. No parsing, no json.loads(), no error handling for malformed JSON.
Sessions: Persistent State
Without sessions, every Runner.run() starts fresh. Sessions maintain conversation history:
from agents import Agent, Runner
from agents.sessions import InMemorySession, SqliteSession
# In-memory: good for testing, lost on restart
session = InMemorySession()
agent = Agent(
name="Support Agent",
instructions="Help users troubleshoot issues. Remember context from earlier in the conversation."
)
# First message
result1 = Runner.run_sync(agent, "I'm having trouble with my login", session_id="user-123", session=session)
print(result1.final_output)
# Second message — agent remembers the login issue
result2 = Runner.run_sync(agent, "The error code is ERR_AUTH_TOKEN_EXPIRED", session_id="user-123", session=session)
print(result2.final_output)
Enter fullscreen mode Exit fullscreen mode
For production, use SqliteSession (included) or implement the Session interface for Redis/Postgres:
from agents.sessions import SqliteSession
session = SqliteSession("./data/sessions.db")
# Sessions persist across process restarts
result = Runner.run_sync(
agent,
"Continue from where we left off",
session_id=f"user-{user_id}",
session=session
)
Enter fullscreen mode Exit fullscreen mode
Tracing and Observability
import agents
# Simple stdout logging
agents.enable_verbose_stdout_logging()
# Or structured tracing for production
from opentelemetry.exporter.otlp.proto.grpc.trace_exporter import OTELSpanExporter
from agents.tracing import set_trace_exporter
exporter = OTELSpanExporter(endpoint="http://jaeger:4317")
set_trace_exporter(exporter)
Enter fullscreen mode Exit fullscreen mode
Every tool call, handoff, and guardrail check generates a span. You get full visibility into what the agent did and why — critical for debugging multi-agent systems.
Using Non-OpenAI Models
The SDK supports any model via LiteLLM:
from agents import Agent
from agents.models import LitellmModel
claude_agent = Agent(
name="Claude Reviewer",
instructions="You are a helpful code reviewer.",
model=LitellmModel(model="anthropic/claude-sonnet-4-6")
)
gemini_agent = Agent(
name="Gemini Analyst",
instructions="Analyze data and generate insights.",
model=LitellmModel(model="gemini/gemini-2.0-flash")
)
Enter fullscreen mode Exit fullscreen mode
You can mix models within a handoff chain — use Claude for reasoning-heavy tasks, GPT-4o-mini for simple routing decisions.
Production Pattern: Support Ticket Classifier
A complete example that runs in production:
from agents import Agent, Runner, handoff, function_tool
from agents.sessions import SqliteSession
from pydantic import BaseModel
from typing import Literal
import logging
logger = logging.getLogger(__name__)
# Structured ticket format
class TicketClassification(BaseModel):
category: Literal["billing", "technical", "account", "other"]
priority: Literal["urgent", "high", "normal", "low"]
summary: str
requires_human: bool
# Tool to log ticket actions
@function_tool
def create_ticket(user_id: str, classification: dict) -> str:
"""Create a support ticket in the system.
Args:
user_id: The user's ID
classification: Ticket category, priority, and summary
"""
# In production: insert into your tickets table
ticket_id = f"TICK-{hash(user_id) % 10000:04d}"
logger.info(f"Created ticket {ticket_id} for user {user_id}: {classification}")
return f"Ticket {ticket_id} created successfully"
# Specialist agents
billing_agent = Agent(
name="Billing Specialist",
instructions="""You handle billing and payment issues. You can:
- Explain charges and invoice line items
- Process refund requests (flag for human approval if >$100)
- Update payment methods
Never access actual payment details directly — use the provided tools."""
)
technical_agent = Agent(
name="Technical Support",
instructions="""You handle technical issues with our product. You:
- Diagnose integration errors from error messages and logs
- Provide step-by-step debugging guidance
- Escalate to engineering (requires_human=True) if you find a likely bug"""
)
# Triage agent with output structure
triage_agent = Agent(
name="Support Triage",
instructions="""Classify incoming support requests and route to the right team.
Always create a ticket first, then route to the specialist.""",
output_type=TicketClassification,
tools=[create_ticket],
handoffs=[
handoff(billing_agent, description="Route billing and payment questions"),
handoff(technical_agent, description="Route technical/integration issues")
]
)
session = SqliteSession("./data/support_sessions.db")
async def handle_support_request(user_id: str, message: str) -> str:
try:
result = await Runner.run(
triage_agent,
message,
session_id=f"support-{user_id}",
session=session
)
return result.final_output
except Exception as e:
logger.error(f"Agent error for user {user_id}: {e}")
return "I'm having trouble right now. A human agent will follow up shortly."
Enter fullscreen mode Exit fullscreen mode
Agents SDK vs LangGraph vs CrewAI
Feature OpenAI Agents SDK LangGraph CrewAI Learning curve Low High Medium Orchestration Agent-driven Explicit graph Role-based Typed I/O Pydantic native Manual Limited Sessions Built-in Manual Manual Guardrails Built-in Custom nodes Limited Non-OpenAI models LiteLLM Any Any Production tracing OTEL native LangSmith Manual Best for Multi-agent pipelines Complex state machines Simulated teamsLangGraph wins when you need deterministic control flow — when the order of operations must be explicit and predictable. The Agents SDK wins when you want agents to make routing decisions themselves.
What Works in Production
After building several Agents SDK systems, a few patterns hold up:
Keep instructions focused. Agents with 10 clear rules perform better than agents with 50 vague guidelines. Write instructions like you’re onboarding a new hire — specific, complete, and unambiguous.
Guardrails are not optional. Even internal tools need input validation. An agent processing untrusted user input without guardrails will eventually do something unexpected.
Sessions need a TTL. SqliteSession doesn’t expire conversations by default. Add a cleanup job to remove sessions older than your retention policy.
Test handoff chains explicitly. The hardest bugs in multi-agent systems are routing failures — when the coordinator sends work to the wrong specialist, or loops between agents. Write integration tests that verify routing, not just output quality.
Full article at stacknotice.com/blog/openai-agents-sdk-complete-guide-2026
답글 남기기