OpenAI Agents SDK: Handoffs, Guardrails, Tracing
The OpenAI Agents SDK is a lightweight multi-agent framework built on the Responses API, and it comes down to five primitives: Agent, Handoff, Guardrail, Session, Tracing. Handoffs are just tools named `transfer_to_<agent>`. Guardrails trip on input or output. Tracing is on by default.
Learning Objectives
- Name the five primitives of the OpenAI Agents SDK.
- Explain handoffs: why they are modeled as tools, what name shape the model sees, and how context transfers.
- Distinguish input guardrails, output guardrails, and tool guardrails; explain `run_in_parallel` vs blocking mode.
- Implement a stdlib runtime with handoffs + guardrails + span-style tracing.
The Problem
An agent that can't delegate cleanly ends up with everything stuffed into one prompt. An agent without guardrails ships PII, policy-violating output, or loops forever. The SDK codifies five primitives that make multi-agent work tractable, and this lesson walks through each one and then rebuilds the shape in stdlib Python.
The Concept
Five primitives
- Agent. An LLM bundled with instructions, tools, and handoffs.
- Handoff. Delegation from one agent to another. The model sees it as a tool named
transfer_to_<agent_name>. - Guardrail. A validation check that can halt a run. It runs on input (first agent only), on output (last agent only), or on a tool invocation (per function tool).
- Session. Conversation history that persists across turns without you managing it by hand.
- Tracing. Built-in spans for LLM generations, tool calls, handoffs, and guardrails.
Handoffs as tools
The model sees transfer_to_billing_agent in its tool list like any other tool. When it calls that tool, the runtime does three things:
- Copies the conversation context, or collapses it with the
nest_handoff_historybeta. - Initializes the target agent with its own instructions.
- Continues the run with the target agent.
This is the supervisor pattern (Lesson 28) shipped as a built-in feature.
Guardrails
Three flavors:
- Input guardrails. Run on the first agent's input and reject unsafe or out-of-scope requests. In blocking mode they run before the main LLM call; in the default parallel mode they run alongside it (see the modes below).
- Output guardrails. Run on the last agent's output and catch PII leaks, policy violations, and malformed responses.
- Tool guardrails. Run per function tool. They validate arguments, check permissions, and audit execution.
Each guardrail also has a mode:
- Parallel (default). The guardrail LLM runs alongside the main LLM, which keeps tail latency low. If it trips, the main LLM's work is discarded and those tokens are wasted.
- Blocking (
run_in_parallel=False). The guardrail LLM runs first. If it trips, the main call never happens, so no tokens are wasted.
A tripped guardrail raises InputGuardrailTripwireTriggered or OutputGuardrailTripwireTriggered.
Tracing
Tracing is on by default. Every LLM generation, tool call, handoff, and guardrail emits a span. Set OPENAI_AGENTS_DISABLE_TRACING=1 to opt out, or call add_trace_processor(processor) to fan spans out to your own backend alongside OpenAI's.
Sessions
A Session stores conversation history in a backend (SQLite, Redis, or your own). Pass it in with Runner.run(agent, input, session=session) and the runner loads history before the run and appends to it after.
Where this pattern goes wrong
- Handoff drift. Agent A hands off to Agent B, which hands back to Agent A. Add a hop counter.
- Guardrail bypass. Tool guardrails only fire on function tools. Built-in tools (file reader, web fetch) need their own policy.
- Over-tracing. Sensitive content ends up in spans. Pair tracing with the OTel GenAI content-capture rules (Lesson 23): store the content externally and reference it by ID.
Build It
code/main.py implements the SDK's shape in stdlib Python:
Agent,FunctionTool, andHandoff(a function tool with transfer semantics).Runnerwith input, output, and tool guardrails, handoff dispatch, and a hop counter.- A simple span emitter to show what a trace looks like.
- A triage agent that hands off to billing or support based on the user's query, with a guardrail that trips on one of the inputs. The toy runner checks input before dispatching the agent, so it behaves like blocking mode.
Run it:
python3 code/main.py
The trace shows two successful handoffs, one input guardrail trip, and a span tree mirroring what the real SDK emits.
Use It
Pick the harness by what you're building:
- OpenAI Agents SDK for OpenAI-first products.
- Claude Agent SDK (Lesson 17) for Claude-first products.
- LangGraph (Lesson 13) when you want explicit state and durable resume.
- Custom when you need exact control: voice, multi-provider, or federated deployments.
Ship It
outputs/skill-agents-sdk-scaffold.md scaffolds an Agents SDK app with a triage agent, handoffs, input/output/tool guardrails, a session store, and a trace processor.
To the model, a handoff is one more tool in the list. Hold onto that, because it means everything you already know about tool design applies: clear names, narrow scope, and a runtime that won't follow a bad call forever. Put the hop counter in before you need it.
Choose the guardrail mode by what a trip costs you. If a discarded main call is cheap, parallel buys you latency. If it's expensive, block. And don't read a tidy set of tool guardrails as full coverage while built-in tools sit outside it.
Keep tracing on, but decide what is allowed into a span before real user data starts flowing through it.
Exercises
- Add a handoff hop counter that refuses after N transfers. Trace the behavior.
- Implement
nest_handoff_historyas an option: collapse prior messages into one summary before transferring. - Write a blocking output guardrail. Compare latency on prompts that would trip it against prompts that pass.
- Wire
add_trace_processorto a JSON logger. What shape does it emit per span? - Read the SDK docs and port your stdlib toy to
openai-agents-python. What did you model wrong?
Key Terms
| Term | What people say | What it actually means |
|---|---|---|
| Agent | "LLM + instructions" | Agent type in the SDK; owns tools and handoffs |
| Handoff | "Transfer" | Tool the model calls to delegate to another agent |
| Guardrail | "Policy check" | Validation on input / output / tool invocation |
| Tripwire | "Guardrail trip" | Exception raised when guardrail rejects |
| Session | "History store" | Conversation memory persisted between runs |
| Tracing | "Spans" | Built-in observability over LLM + tool + handoff + guardrail |
| Blocking guardrail | "Sequential check" | Guardrail runs first; no token waste on trip |
| Parallel guardrail | "Concurrent check" | Guardrail runs alongside; lower latency, wastes tokens on trip |
Further Reading
- OpenAI Agents SDK docs: primitives, handoffs, guardrails, tracing
- Claude Agent SDK overview: the Claude-flavored counterpart
- Anthropic, Building Effective Agents: when to reach for handoffs at all
- OpenTelemetry GenAI semantic conventions: the standard Agents SDK spans map to
Code
- main.py (python, 227 lines)