Lab 03 — First agent with the Claude Agent SDK
Lesson 03 · Building Your Own Agents · ~25 min hands-on
Goal: at the end of this lab you will have (a) verified that the SDK is importable from
anthropic, (b) written a 12-line hello-world agent that runs successfully, and
(c) ported the L02 weather agent to use @tool decorators and the
Agent class — deleting roughly 70 lines of boilerplate in the process.
Prereqs
-
The Plan A
agents/requirements.txtalready installed (which pinsanthropic>=0.39,<0.40). If not, runpip install -r agents/requirements.txtfrom the repo root. ANTHROPIC_API_KEYexported in your shell.- One terminal you can run
pythonscripts in.
Step 1 — Confirm the SDK is importable
Before writing any code, sanity-check that the right anthropic version is on
your path and that the agent primitives can be imported from the
anthropic.agents namespace.
python -c "from anthropic.agents import Agent, tool; print('ok')"
Expected: ok.
If you get ModuleNotFoundError: No module named 'anthropic.agents', your
installed anthropic is older than 0.39 — re-run
pip install -r agents/requirements.txt and confirm the version matches the
pin in agents/SDK-VERSIONS.md.
Step 2 — The 12-line hello world
Create hello_agent.py in a scratch directory of your choosing:
from anthropic import Anthropic
from anthropic.agents import Agent, tool
@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(
client=Anthropic(),
model="claude-3-5-sonnet-latest",
system="You are a helpful assistant. Use the get_weather tool to answer.",
tools=[get_weather],
max_turns=5,
max_cost_usd=0.05,
)
result = agent.run("What's the weather in Kyiv?")
print(result.final_text)
print(f"[cost: ${result.usage.total_cost_usd:.4f}]")
Run it:
python hello_agent.py
Expected: a one-sentence weather answer (something like “The weather in Kyiv is 4°C
with light rain.”) plus a cost line in the range of ~$0.001–$0.005.
✓ Self-check: the script printed both a natural-language answer and a cost figure, and exited cleanly with no traceback.
Step 3 — Trigger the cost cap
Edit hello_agent.py and change max_cost_usd=0.05 to
max_cost_usd=0.0001. Re-run.
python hello_agent.py
Expected: the SDK raises MaxCostExceeded. Note the difference from L02 —
you get a typed exception, not a generic RuntimeError. That
matters because in real code you can except MaxCostExceeded specifically and
handle it differently from a turn-cap breach or a transport error.
Restore max_cost_usd=0.05 before moving on.
Step 4 — Port the L02 agent to the SDK
Open your L02 agent.py (the hand-rolled weather agent from Lesson 02). With
hello_agent.py open side-by-side as a reference, replace each piece of
boilerplate:
-
The hand-written
TOOLSschema list → decorateget_weatherandget_clothing_advicewith@tooland drop the JSON Schema dicts. -
The
for turn in range(MAX_TURNS):loop body →agent = Agent(...); result = agent.run(question). -
The cost-cap
ifstatement and thetotal_in/total_outaccumulators →max_cost_usd=andresult.usage.total_cost_usd. -
The tool-result dance (the
messages.append(...)calls that built up the conversation by hand) → delete entirely.
Save the result as agent_sdk.py. Run the same question you ran in L02:
python agent_sdk.py "What's the weather in Kyiv? Should I bring a jacket?"
Expected: the same final answer as the L02 hand-rolled version, a similar cost figure,
and ~70 lines deleted from your file. Count them with
wc -l agent.py agent_sdk.py to make the delta concrete.
✓ Self-check: agent_sdk.py answers the same question as agent.py with roughly the same cost, and is dramatically shorter (typically ~12 lines vs ~80).
Step 5 — Add an on_step hook
The SDK exposes a per-turn observability hook. Add this above the
agent = Agent(...) line in agent_sdk.py:
def log_step(step):
print(f"[step {step.turn}] stop_reason={step.stop_reason} "
f"tools={[c.name for c in step.tool_calls]} "
f"cost_so_far=${step.cost_so_far_usd:.4f}")
Then pass on_step=log_step to the Agent(...) constructor and
re-run the script.
Expected: one log line per turn, printed before the final answer. This is the SDK’s built-in observability hook — the same hook you’ll wire to a logging library or an OTEL span later in the course.
Step 6 — Verify MaxTurnsExceeded
To see the turn-cap path, temporarily edit get_weather in
agent_sdk.py to always return a perpetually-incomplete reply:
@tool
def get_weather(city: str) -> str:
"""Get current weather for a city as 'temp°C, conditions'."""
return "need more details, call again"
Re-run the script. Expected: the SDK raises MaxTurnsExceeded after
max_turns iterations — again, a typed exception rather than a
hand-written raise RuntimeError.
Restore the original get_weather body once you’ve seen the exception.
What you should leave with
- A working
hello_agent.py. - A working
agent_sdk.py— the SDK port of L02’s hand-rolled agent. -
First-hand experience of
MaxCostExceeded,MaxTurnsExceeded, and theon_stephook.
The next lesson (L04) uses exactly these primitives to build a multi-tool research assistant — the canonical “tool-heavy” shape that’s the SDK’s sweet spot.
Troubleshooting
Common stumbles
-
ModuleNotFoundError: No module named 'anthropic.agents'— you’re on an oldanthropicversion. Pin to>=0.39and reinstall viapip install -r agents/requirements.txt. -
MaxCostExceededafter one turn at the default cap — your prompt is much longer than expected, or you’re using Opus pricing under a Sonnet model name. Checkresult.usagefor the actual token / cost breakdown. -
Hook fires but
step.tool_callsis empty — that’s normal on the final turn: when the LLM produced a text answer (no tool calls), there’s nothing to list.