Homework Solutions
Lesson 03 · Claude Agent SDK Intro · Model answers + commentary
Solution to Task 1 — The "Tool." docstring
The model behaviour with a "Tool." docstring is non-deterministic but almost
always degrades. Some runs you get an unprompted “What city?” follow-up
question; others you get a confident hallucination (e.g., “75°F and sunny in
Kyiv”) with no tool call at all.
The lesson is structural: the LLM cannot see your Python function’s name in isolation
— it sees the JSON Schema the SDK generates from your function, and the schema’s
description field is the docstring. The docstring is the only
place to tell the model when and why to call the tool. Function names help
(LLMs aren’t blind to them), but they’re nowhere near sufficient.
Common mistake
Trusting that “the tool name is descriptive enough.” This bites you especially
hard with tools whose name overlaps something the model already “knows”
(get_weather looks fine — until you have a more domain-specific tool
like lookup_internal_pricing where the model has no priors). Always write a
1–2-sentence docstring with an example.
Solution to Task 2 — Pydantic model as a tool argument
The full working snippet:
from pydantic import BaseModel
from anthropic.agents import Agent, tool
class TravelQuery(BaseModel):
city: str
days_ahead: int = 0
@tool
def get_forecast(query: TravelQuery) -> str:
"""Get a forecast for `query.city` for `query.days_ahead` days from now."""
return f"{query.city} in {query.days_ahead}d: 7°C, sunny"
# Agent definition unchanged except:
# tools=[get_weather, get_forecast]
Behind the scenes, the SDK has read the Pydantic model and produced this
input_schema automatically:
{
"type": "object",
"properties": {
"query": {
"type": "object",
"properties": {
"city": {"type": "string"},
"days_ahead": {"type": "integer", "default": 0}
},
"required": ["city"]
}
},
"required": ["query"]
}
When the LLM calls the tool, the SDK validates the LLM’s JSON against the model,
instantiates TravelQuery(...), and only then calls your function. If validation
fails, the SDK surfaces it as is_error: true back to the LLM, which usually
retries with a corrected payload — this self-healing loop is one of the quietly-valuable
features you get for free from the SDK.
Common mistake
Using two top-level @tool arguments (city: str, days_ahead: int = 0)
when you wanted nesting. Both work; pick the shape that the LLM finds easier to call —
usually flat for 2–3 args, nested for 4+ or when fields share a domain meaning
(e.g., address.city, address.country).
Solution to Task 3 — Streaming with a usage accumulator
The streaming loop with a small accumulator that gives you the streaming-mode equivalent of
result.usage:
total_in = total_out = 0
for event in agent.stream("What's the weather in Kyiv?"):
if event.type == "text_delta":
print(event.text, end="", flush=True)
elif event.type == "tool_use":
print(f"\n[calling {event.tool.name}({event.tool.input})]")
elif event.type == "message_stop":
total_in += event.usage.input_tokens
total_out += event.usage.output_tokens
elif event.type == "end_turn":
print()
print(f"\n[total tokens: in={total_in}, out={total_out}]")
Why streaming matters
For a tool-heavy agent (next lesson), each turn might take 5–15 seconds. Streaming lets you render progress as it happens — both the LLM’s text and the tool-call announcements — which is the difference between a UX that feels responsive and one that feels broken. It’s also the only sane way to debug a slow agent: you see where in the turn it’s spending the time, not just a wall-clock total.
Common mistake
Trying to break out of the loop on a text_delta to “stop
early.” Don’t — the agent has its own stop conditions
(max_turns, max_cost_usd, end_turn). If you break
the iterator from the outside, the SDK doesn’t get to clean up cost tracking, and
you’ll see incomplete usage data (and possibly an HTTP connection leak in long-running
services).
Closing tip
Now you have the SDK in your fingertips: tools, docstrings as schema descriptions, Pydantic validation, streaming with usage accounting. Lesson 04 puts all of this to work: a tool-heavy research assistant in ~120 lines, which is the canonical Claude Agent SDK use case — and the reference implementation that later modules (LangGraph, OpenAI SDK, CrewAI) rebuild for an apples-to-apples comparison.