OpenAI’s Agent SDK is a lightweight, open-source framework that transforms language models into autonomous agents capable of invoking external tools, collaborating with one another, and executing multi-step workflows. At its core are just three concepts—Agent, Tool, and Runner—which let you define an agent’s instructions, register helper functions or APIs, and drive the reasoning loop without worrying about message history or function-calling boilerplate.
In this tutorial, you’ll first build a single “Math Tutor” agent to answer arithmetic questions, then extend it with a “Currency Checker” tool to fetch live exchange rates. We’ll demonstrate chaining agents in programmatic and agentic hand-offs (speech-to-text → summariser; Tier-1 → Tier-2 support), routing requests to multiple specialist agents, and finally treating one agent as a callable tool in a planner→executor pattern for crafting detailed travel itineraries. By the end, you’ll see how a handful of primitives unlock powerful, collaborative AI applications.
An orchestra of AI agents
Some frameworks stack abstractions so high that you need a guide rope to climb them. OpenAI’s Agent SDK heads in the opposite direction: it keeps the mental model light and transparent while still letting you wire up serious, multi-agent workflows.
What we’ll build
- A math-tutor agent that solves arithmetic problems.
- A currency-checker agent that calls an exchange-rate API.
- A hand-off from a speech-to-text agent to a summariser.
- A help-desk flow escalating from Tier-1 to Tier-2 support.
- Using a translator agent strictly as an in-process tool.
- A hierarchical planner → executor pattern for travel itineraries.
1 · Single-agent “Math Tutor”
When you ask a question, it goes straight into an Agent that adds its own instructions and sends it to the LLM. The LLM replies—using any needed tools along the way—and the Agent wraps up that reply into a final answer. You see just one clean response, even though several steps happened behind the scenes. The data path is simple: user → agent → LLM → agent → user
.
import asyncio
from agents import Agent, Runner
math_agent = Agent(
name="Math Tutor",
instructions=(
"You are a friendly tutor. Provide the answer to the arithmetic"
" question and show the working in a single short paragraph."
),
)
async def run(question: str):
result = await Runner.run(math_agent, question)
return result.final_output
if __name__ == "__main__":
q = input("Ask a maths question: ")
print(asyncio.run(run(q)))
Run the script, type 17*23
, and the agent explains the multiplication before giving 391
.
2 · Adding a tool: “Currency Checker”
Tools let an agent reach beyond the LLM. Here, the tool queries a REST endpoint for today’s EUR → USD rate.
import httpx, asyncio
from agents import Agent, Runner, function_tool
@function_tool
def eur_usd_rate() -> str:
# Return today's EUR→USD rate as plain text.
data = httpx.get(
"https://api.exchangerate.host/convert", params={"from": "EUR", "to": "USD"}
).json()
return f"1 EUR ≈ {data['result']:.2f} USD"
fx_agent = Agent(
name="Currency Checker",
instructions="Use the provided tool to answer any question about EUR/USD value today.",
tools=[eur_usd_rate],
)
print(Runner.run_sync(fx_agent, "What is 50 EUR worth in dollars?").final_output)
3 · Programmatic hand-off
Imagine you have two mini-apps in a row. First, you feed some audio text into the “Speech-to-Text” app and it spits out plain English. Then instead of re-sending all the behind-the-curtains chatter, you just grab that clean transcript and hand it straight to the “Summariser” app. It’s like taking the finished sandwich from one station and giving it to the next station to slice—no need to carry anything extra along. When the second agent doesn’t need the full history, pipe the first’s output into the next call.
from agents import Agent, Runner
import asyncio
stt_agent = Agent(
name="Speech-to-Text",
instructions="Transcribe the phonetic sentence into standard English.",
)
summary_agent = Agent(
name="Summariser",
instructions="Compress the given text into a single sentence.",
)
async def go(audio):
text = await Runner.run(stt_agent, audio)
summary = await Runner.run(summary_agent, text.final_output)
return summary.final_output
print(asyncio.run(go("wun smol step fer man")))
4 · Agentic hand-off (Tier-1 → Tier-2)
Think of it like a help-desk: you first talk to the “Tier-1” agent—if it can handle your question (like a routine password reset), it answers you directly. But if you mention something it can’t fix (say, a wrong billing charge or a VPN outage), it calls over the “Tier-2” expert along with the whole conversation so far. That way, Tier-2 sees exactly what you said and what Tier-1 already tried, and can pick up right where Tier-1 left off—no need to repeat yourself.The first agent decides if it must escalate; if so, the entire chat transcript follows.
from agents import Agent, Runner
senior = Agent(
name="Tier-2 Support",
instructions="Resolve advanced billing or networking issues with full context.",
)
tier1 = Agent(
name="Tier-1 Support",
instructions=(
"Answer routine queries. If the user mentions billing discrepancies "
"or persistent connection drops, hand off to Tier-2 Support."
),
handoffs=[senior],
)
print(Runner.run_sync(tier1, "My VPN keeps timing out and the invoice is wrong!").final_output)
5 · Routing to multiple agents
Add Billing, Connectivity, and General-Info agents to the handoffs
list; Tier-1 will choose the right specialist automatically. The code remains identical.
6 · Agents as tools: planner → executor
Imagine you’re planning a trip and you draft a rough outline—“Day 1: Visit the historic district,” “Day 2: Beach time,” “Day 3: Local markets.” That’s your Planner agent. Now instead of fleshing out those bullets yourself, you hand each one to the Executor agent as if it were a tiny helper function. The Executor takes a bullet like “Visit the historic district” and returns a few lively sentences describing the must-see spots, local tips, and how to get there.
So the Planner drives the overall structure—“create three days of activities”—and whenever it needs more detail for one bullet, it “calls” the Executor agent, just like calling any other tool. When the Executor finishes, control returns to the Planner, which moves on to the next bullet, and so on. The result is a full, polished itinerary without ever leaving your code—or your mental model—of “planner calls executor, executor returns detail.”
from agents import Agent, Runner
executor = Agent(
name="Itinerary Step Writer",
instructions="Expand a bullet into 2–3 lively sentences aimed at budget travellers.",
)
planner = Agent(
name="Trip Planner",
instructions=(
"Draft a 3-day itinerary. For each bullet, call the write_step tool to "
"elaborate it before returning the final markdown."
),
tools=[executor.as_tool(
tool_name="write_step", tool_description="Elaborate an itinerary bullet"
)],
)
print(Runner.run_sync(planner, "Plan three days in Lisbon").final_output)
Structured outputs
If you need the LLM to emit well-formed JSON, pass a Pydantic model via output_type
. The SDK will validate and coerce, saving you from brittle post-hoc parsing.
Wrapping up
With just three primitives—Agent, Tool, and Runner—you can stitch together surprisingly complex workflows:
- Single-agent loops
- External tool invocation
- Programmatic vs. agentic hand-offs
- Multi-agent routing and escalation
- Agents reused as tools
By default, the SDK uses OpenAI’s Responses API, but one line switches it to the Chat Completions API—and thus any compatible LLM:
from agents import set_default_openai_api
set_default_openai_api("chat_completions")
Mistral, Anthropic, or your own model—now you’re ready to build the next great multi-agent application. Or build it with Versatik! ✨