Three primitives, one big idea: agents that hand off work to other agents.
Agents Module 4 OpenAI Agents SDK Handoffs
Agent.run() — one model, many tools.Agent and edges are handoffs.Same underlying loop — new composition primitive on top of it.
It's the openai-agents PyPI package, separate from the base openai client.
pip install openai-agents gives you the SDK; you still install openai underneath for the model HTTP layer.
Imports come from the top-level agents namespace:
from agents import Agent, Runner, handoff, function_tool
AgentA model + instructions + tools + a list of agents it can hand off to.
handoff(target_agent, ...)A typed reference that lets one agent transfer control to another mid-run. The handoff appears to the LLM as if it were a tool call.
@input_guardrail and @output_guardrail decorators that wrap an agent with cheap pre/post checks. They short-circuit a run if their GuardrailFunctionOutput.tripwire_triggered returns true.
Runner.run(agent, input)Async entry point that drives the agent loop. There's also Runner.run_sync(...) for non-async callers.
@function_toolThe SDK's tool decorator. Same idea as Claude SDK's @tool: reads type hints + docstring, builds the JSON Schema for you.
RunResult.final_outputFinal answer object. With output_type= set to a Pydantic model the SDK returns a typed instance, not a string.
from agents import Agent, Runner, function_tool
@function_tool
def get_weather(city: str) -> str:
"""Get current weather for a city."""
return f"{city}: 4°C, light rain"
agent = Agent(
name="WeatherBot",
instructions="You answer weather questions using the get_weather tool.",
model="gpt-4o-mini",
tools=[get_weather],
)
result = Runner.run_sync(agent, "What's the weather in Kyiv?")
print(result.final_output)
Shape is recognisably the same as Claude SDK's hello-world — just with Runner as the driver and function_tool instead of tool.
| Concept | Claude SDK | OpenAI Agents SDK |
|---|---|---|
| Single-agent loop | Agent.run() | Runner.run(agent, ...) |
| Multi-agent compose | (you wire it) | handoffs=[other_agent] |
| Structured final answer | final-answer tool pattern | output_type=Pydantic |
| Pre/post checks | (you wire it) | @input_guardrail / @output_guardrail |
| Model | Claude only | any provider via LiteLLM adapter |
The bold rows justify reaching for this SDK over Claude's: handoffs and guardrails are first-class.
agent_triage = Agent(name="Triage", ..., handoffs=[billing_agent, tech_agent]).transfer_to_billing_agent / transfer_to_tech_agent into the triage agent's tool list.Runner.run(...) call from your code.result.last_agent.name tells you who ended up handling the request.billing = Agent(name="Billing", instructions="Handle billing only.", model="gpt-4o-mini")
triage = Agent(name="Triage", instructions="Route to the right specialist.",
model="gpt-4o-mini", handoffs=[billing])
result = Runner.run_sync(triage, "My invoice is wrong.")
print(result.final_output)
print(result.last_agent.name) # 'Billing'
last_agent tells you who ended up handling the request — the SDK swapped to Billing mid-run without you wiring a second call.
from agents import input_guardrail, GuardrailFunctionOutput
@input_guardrail
async def block_pii(ctx, agent, user_input):
bad = any(tok in user_input for tok in ("SSN", "credit card"))
return GuardrailFunctionOutput(
output_info={"pii_detected": bad},
tripwire_triggered=bad,
)
agent = Agent(name="...", instructions="...", input_guardrails=[block_pii])
The SDK runs block_pii before the model sees the input; tripwire short-circuits the run with an InputGuardrailTripwireTriggered exception.
output_type=).Sweet spot.
max_turns=-style timeout in user code.set_tracing_disabled(False) + an exporter.openai-agents package — not the base openai client.Agent + handoff + guardrails.