OpenAI Agents SDK: Building Production AI Agents (2026)

OpenAI Agents SDK: Building Production AI Agents (2026)

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 teams LangGraph 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

Original Source

Read the full article at Dev →

KhanList aggregates and links to publicly available news content. We do not host full articles from third-party sources. Always verify important information with original sources.