Build the support agent
Lesson 0 promised an agent that reads a message, decides whether it needs a tool, hands the hard cases to another agent, refuses what it should not answer, and asks you before it spends money. Here it is, and every piece of it is something you have already built alone.
Two tools, one of them dangerous
@function_tool
def lookup_order(order_id: str) -> str:
"""Look up the status of an order by its id."""
return f"Order {order_id} shipped on 3 March."@function_tool(needs_approval=True)
def refund(amount: int) -> str:
"""Refund an amount in rupees to the customer."""
return f"Refunded {amount} rupees."Only the second one is marked. Looking something up is safe at any hour; paying money out is not.
The specialists
billing = Agent(name="Billing", instructions="Handle refunds. Be brief.",
model=PretendModel([call("refund", amount=500), "Your refund is on its way."]),
tools=[refund])
technical = Agent(name="Technical", instructions="Handle logins. Be brief.",
model=PretendModel(["Try logging out and back in."]))The refund tool lives on Billing and nowhere else, so Technical cannot issue one however the conversation goes. That is lesson 13's real argument, in two lines.
The front desk
triage = Agent(
name="Triage",
instructions="Send the customer to Billing or Technical.",
tools=[lookup_order],
handoffs=[billing, technical],
input_guardrails=[only_support],
)Everything you have learned, on one object. A tool of its own, two agents it can hand to, and a gate on the way in.
Three turns, one conversation
print("turn 1")
first = await Runner.run(triage, "Where is order A17?", session=session)
print(" ", first.final_output)
print("turn 2")
second = await Runner.run(triage, "I was charged twice, refund me 500", session=session)
print(" waiting on:", second.interruptions[0].raw_item.name)
state = second.to_state()
state.approve(second.interruptions[0])
done = await Runner.run(triage, state, session=session)
print(" ", done.final_output, "| handled by", done.last_agent.name)
print("turn 3")
try:
await Runner.run(triage, "do my homework", session=session)
except InputGuardrailTripwireTriggered:
print(" blocked, off topic")
print()
print("remembered", len(await session.get_items()), "items across the conversation")Read what happened
Turn one was answered by Triage itself. The question named an order, so it used the lookup tool and replied. No handoff, no approval, nothing dramatic.
Turn two went to Billing, which asked for the refund and stopped. The run came back with the request waiting, a person approved it, and only then did the money move. Notice that the answer came from Billing, not Triage.
Turn three never reached a model. The guardrail read it and refused, which cost nothing.
Eleven items across all three turns, in one session. The second turn knew about the first because they share it.
Make it yours
- Make it real. Lesson 23, one line, and the deciding stops being a script.
- Make it survive a restart. Give the session a filename, from lesson 19.
- Make it answerable. Add an output guardrail from lesson 17 so it can never promise a delivery date.
- Make it typed. Give Triage an
output_typefrom lesson 10 and route on the field rather than on the model's wording. - Make it visible. Stream it with lesson 21 so a person watches the handoff happen.
What was left out
Enough exists in the SDK to fill another course, and knowing the names is most of the battle.
| Left out | What it is |
|---|---|
| Realtime and voice agents | The same agents over a live audio connection. Four pages of the docs. |
| Sandboxes | Running an agent's code in an isolated environment, local or hosted. |
| Hosted tools | Web search, file search and computer use, run on OpenAI's side. |
| MCP servers | Tools from an external process rather than functions in your file. |
| Other session backends | SQLAlchemy, encrypted and advanced SQLite sessions. |
| LiteLLM | One more way to reach a hundred other providers. |
| Visualization | draw_graph, which draws your agents and handoffs. |
| Testing | The SDK's own helpers for testing agents without a model. |
run_demo_loop | A one line interactive chat in your terminal, for trying an agent by hand. |
| Lifecycle hooks and tracing processors | Callbacks on every step, and sending traces somewhere other than OpenAI. |
- Add a third specialist for deliveries and route to it.
- Reject the refund instead of approving it and read what the customer is told.
- Give the session a filename and run the file twice.
This is what real progress feels like.