Lesson 03 Claude Agent SDK Intro
Theme:

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.