Objective
The rule: test tools before the model touches them
It's tempting to write a tool and immediately let the agent call it, so you can "see it work end to end." Don't. If something breaks, you won't know whether the bug is in your tool, your schema, or the model's reasoning — three different problems that look identical from the outside. Every tool below gets a 3-line manual test with hardcoded arguments first.
The description is the entire interface
The model never sees your Python. It sees the name, description, and input_schema you wrote, and decides whether and how to call the tool based only on that. A tool with perfect code and a vague description will be ignored or misused. Write descriptions like you're onboarding a very literal new hire who can't ask clarifying questions.
Tool 1 — web_search
Wraps the Serper API (a Google Search API with a generous free tier) so the agent can look things up instead of relying on training-data knowledge that might be stale or wrong.
description = "Search the web for current information. Use this for anything requiring up-to-date facts, current events, or information you're not confident about from memory."
Notice what this description does that a lazier one ("searches the web") doesn't: it tells the model when to prefer this over answering from memory. Without that, a model that's fairly confident about something will just answer directly and never call the tool — which looks like a bug in your tool but is actually a description problem.
Tool 2 — read_file
Reads a local file so the agent can process something the user already has on disk. The failure mode here isn't the model — it's forgetting to handle a missing file gracefully and letting a raw FileNotFoundError traceback become the "tool result" the model sees, which it will happily hallucinate an explanation for.
Tool 3 — run_python
Executes a short Python snippet and returns stdout. This is the tool students get most excited about and most nervous about at the same time.
This tool executes code — treat it like it does
run_python runs arbitrary code the model generates. On your own machine, for your own tasks, with a timeout and no network/filesystem access beyond what you explicitly allow, this is a completely standard agent pattern (it's the same idea behind every "code interpreter" feature). Today's implementation uses a subprocess with a hard timeout specifically so a runaway or malicious snippet can't hang or damage your machine. Never point this tool at untrusted input in anything resembling production without a real sandbox (a container, a VM, or a hosted code-execution service) — a subprocess timeout is a classroom-safe guardrail, not a security boundary.
Tool 4 — fetch_url
Fetches a URL's text content via httpx — the building block your later "research a topic" tasks will lean on hardest.
You test run_python by hand and it works. You wire it to the agent, ask it to "calculate the standard deviation of these 5 numbers," and the agent responds with a made-up answer instead of calling the tool. What's the first thing to check?
If the agent didn't call the tool at all, the tool's own implementation is irrelevant — it never ran. The two most common causes are a description that doesn't signal "use me for math/calculation" clearly enough, or (very common when refactoring) forgetting to actually include the tool in the tools=[...] list passed to that specific call.
What you're building
All four tools as standalone Python functions plus their tool-schema definitions in one module, each tested with 2-3 hardcoded calls before you move on — this file becomes the tool layer every later lesson imports from.