Lab 07 — First agent with the OpenAI Agents SDK
Lesson 07 · Building Your Own Agents · ~30 min hands-on
Goal: at the end of this lab you will have (a) verified that openai-agents is
importable from the agents namespace, (b) written a 10-line hello-world agent
that runs successfully, (c) added a single handoff and observed
result.last_agent change, and (d) added a one-line input guardrail and
triggered the tripwire deliberately. This lab keeps the surface area tiny on purpose —
Lesson 08 scales the same primitives up into a realistic triage-and-handoff agent.
Prereqs
-
The Plan A
agents/requirements.txtalready installed (which pinsopenai-agents>=0.0.10,<0.1). If not, runpip install -r agents/requirements.txtfrom the repo root. OPENAI_API_KEYexported in your shell.- One terminal you can run
pythonscripts in. - Working directory at the repo root (the paths below assume that).
Step 1 — Confirm the SDK is importable
Before writing any code, sanity-check the install. The OpenAI Agents SDK ships as the
openai-agents PyPI package but imports from the top-level agents
namespace — a common stumble:
python -c "from agents import Agent, Runner, function_tool; print('ok')"
Expected: ok. If you get
ModuleNotFoundError: No module named 'agents', you haven’t installed
openai-agents yet — re-run pip install -r agents/requirements.txt
and confirm the installed version matches the row in agents/SDK-VERSIONS.md.
✓ Self-check: the import line prints ok with no traceback.
Step 2 — The 10-line hello world
Create a file called hello_openai_agent.py anywhere convenient (your scratch
dir is fine):
from agents import Agent, Runner, function_tool
@function_tool
def get_weather(city: str) -> str:
"""Get current weather for a city as 'temp°C, conditions'."""
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)
print(f"[last agent: {result.last_agent.name}]")
Run it:
python hello_openai_agent.py
Expected: a one-sentence weather answer (something like “It’s currently 4°C
with light rain in Kyiv.”) followed by [last agent: WeatherBot].
✓ Self-check: you saw a natural-language weather answer and the
last_agent line. No traceback.
Why run_sync?
The SDK’s native entry point is Runner.run(agent, input) — an
async coroutine. Runner.run_sync(...) is a thin wrapper that spins up an
event loop for non-async callers; we use it in this lab to keep the snippets short. The
moment you need streaming (Step 4) we switch back to the async form.
Step 3 — Add a second agent and a handoff
Now feel the SDK’s headline primitive. Define a JokeBot agent and let
WeatherBot hand off to it when the user asks for a joke. Edit
hello_openai_agent.py:
joke = Agent(
name="JokeBot",
instructions="You tell exactly one short, clean joke and stop.",
model="gpt-4o-mini",
)
agent = Agent(
name="WeatherBot",
instructions=(
"You answer weather questions using the get_weather tool. "
"If the user asks for a joke, hand off to JokeBot."
),
model="gpt-4o-mini",
tools=[get_weather],
handoffs=[joke],
)
Re-run with a joke prompt:
result = Runner.run_sync(agent, "Forget the weather. Tell me a joke.")
print(result.final_output)
print(f"[last agent: {result.last_agent.name}]")
Expected: the final output is a joke, and the line prints
[last agent: JokeBot] — the handoff transferred control. Re-run with the
original weather question and confirm last_agent goes back to
WeatherBot.
✓ Self-check: the joke prompt routes to JokeBot; the
weather prompt stays with WeatherBot.
Step 4 — Confirm the handoff appears as a synthetic tool
Internally the SDK implements a handoff as a tool call the LLM emits. To prove it, replace
Runner.run_sync(...) with Runner.run_streamed(...) and iterate
the events:
import asyncio
from agents.stream_events import RunItemStreamEvent
async def main():
stream = Runner.run_streamed(agent, "Tell me a joke.")
async for event in stream.stream_events():
if isinstance(event, RunItemStreamEvent) and event.name in (
"tool_called", "handoff_requested", "handoff_occured"
):
print(event.name, getattr(event.item, "raw_item", event.item))
print("FINAL:", stream.final_output)
asyncio.run(main())
Expected: you see a handoff_requested event for
transfer_to_jokebot (the SDK’s synthetic tool name), then a
handoff_occured event, then the joke text. This confirms what the slides
claimed: a handoff is just a tool call with a fancy name — the LLM picks it the same
way it picks any other tool.
✓ Self-check: at least one handoff_requested /
handoff_occured pair printed before the final joke.
Step 5 — Add a one-line input guardrail
Guardrails are the SDK’s built-in pre/post-condition primitive. Add this near the top of the file:
from agents import input_guardrail, GuardrailFunctionOutput
@input_guardrail
async def block_long_input(ctx, agent, user_input):
too_long = len(user_input) > 200
return GuardrailFunctionOutput(
output_info={"length": len(user_input), "blocked": too_long},
tripwire_triggered=too_long,
)
Then wire it into the WeatherBot constructor by adding
input_guardrails=[block_long_input] to the keyword arguments:
agent = Agent(
name="WeatherBot",
instructions=(
"You answer weather questions using the get_weather tool. "
"If the user asks for a joke, hand off to JokeBot."
),
model="gpt-4o-mini",
tools=[get_weather],
handoffs=[joke],
input_guardrails=[block_long_input],
)
Now run with a deliberately over-long question:
long_q = "What is the weather in Kyiv? " * 10
try:
Runner.run_sync(agent, long_q)
except Exception as e:
print(type(e).__name__, e)
Expected: InputGuardrailTripwireTriggered is raised before any model
call hits the wire. Note the typed exception name vs. the generic
RuntimeError you’d hand-write in a from-scratch loop — the SDK
gives you a dedicated class you can catch precisely. Restore the original behaviour by
deleting the input_guardrails=[...] argument or raising the threshold.
✓ Self-check: the print line shows
InputGuardrailTripwireTriggered (not RuntimeError, not a model
reply).
Step 6 — Inspect the trace
Re-run the original (no guardrail) handoff scenario and print result.new_items
to see what the SDK actually ships to its tracing backend:
result = Runner.run_sync(agent, "Tell me a joke.")
for item in result.new_items:
print(type(item).__name__, getattr(item, "raw_item", item))
Expected: a sequence including HandoffCallItem,
HandoffOutputItem, and MessageOutputItem. These typed items are
the structured record of what happened — you’ll use exactly this shape in
Lesson 08 to verify which specialist the triage agent picked per request.
✓ Self-check: at least one HandoffCallItem and one
HandoffOutputItem appear in the output.
What you should leave with
-
A working
hello_openai_agent.pywith one base agent + one handoff target + one input guardrail (or its removed/loosened form). -
First-hand experience of
result.last_agent, thetransfer_to_<name>synthetic tool, and the typedInputGuardrailTripwireTriggeredexception. -
A read of
result.new_items— the structured trace the SDK exposes per run. -
The instinct that the OpenAI Agents SDK’s “Agent + Runner + handoffs +
guardrails” shape is recognisably the same primitives as Claude SDK’s
Agent.run(), just rearranged for a multi-agent graph — which is what Lesson 08 puts to work.
Troubleshooting
Common stumbles
-
ModuleNotFoundError: No module named 'agents'—openai-agentsisn’t installed. Runpip install -r agents/requirements.txt. Note the package name is hyphenated (openai-agents) but the import is the pluralagents. They’re unrelated to the older single-wordagenton PyPI. -
The joke shows up but
last_agent.nameisWeatherBot— your handoff list is empty. Double-checkhandoffs=[joke]on theWeatherBotconstructor (it’s easy to definejokeand forget to pass it). -
InputGuardrailTripwireTriggeredfires on a short input — confirmlen(user_input) > 200matches the input you’re sending. The SDK passes the raw user string to the guardrail, not a tokenised version — so 200 means 200 characters, not 200 tokens. -
Runner.run_streamedevents look unfamiliar — the event names and item types changed between minoropenai-agentsversions. IfRunItemStreamEventis missing or has different fields, checkagents/SDK-VERSIONS.mdfor the exact version this course was authored against (0.0.10+). -
The model responds with a refusal or an apology instead of using the tool
—
gpt-4o-minioccasionally over-apologises when theinstructionsare too terse. Add a line like “Always callget_weatherfor any weather question” to push it.