Workshops/ Capable Series/ Day 03 · Command
03 / 04
Pradhya Day 03 · Command 5 units · ~25 min reading + 40 min hands-on

Tools turn talk into action.

A chatbot writes you a recipe. An agent reads your pantry, picks the dish, places the grocery order, and adds the prep step to your calendar. The difference is one capability: tool use.

By the end of Day 03 · Command
  • Declare a tool with a description Claude will use correctly
  • Run the full tool-use loop end to end (tool_use → tool_result → answer)
  • Force structured output by making the model fill a tool form
  • Apply the three rules of the agentic discipline (scope, preview, reversibility)
Get the code

Today’s labs run a Python script. If you did Day 1, you already have an agents/ folder with a virtual environment — reuse it. If not, set one up now (one minute) and save the two files into it:

# one-time setup (macOS / Linux), from a folder named agents/
python3 -m venv .venv
source .venv/bin/activate
pip install anthropic
export ANTHROPIC_API_KEY="sk-ant-..."   # get one at console.anthropic.com

Verify your setup: python hello_claude.py prints a critique. Then you’re ready for §03.03.

§ 03.01 · Unit 19

What a tool is.

A tool is a function the model can ask you to run. You tell the model what tools exist. The model decides when to call one. You execute it. The model uses the result.

The model never runs code directly. It returns a structured request — “please run get_weather with city='Phoenix'” — and waits. Your code runs the function and sends the answer back. The model then incorporates the result into its response.

This loop is the whole game. Once you see it, every agent in the world is the same diagram, just with more tools and more turns.

Claude (model) decides, never executes get_weather(...) your code · runs locally user prompt → 2. tool_use { city: "Phoenix" } 3. tool_result { 102°F, sunny } ← final answer

1. user prompt → 2. model emits tool_use → 3. your code returns tool_result → 4. model writes final answer

The shift Step 2 is the new thing. In a chatbot, the model writes the answer. In an agent, the model writes a request — and then waits for the world to answer back.

Map the four steps onto a real task.

You’ll do
Pick one repetitive task from your week and write the loop’s four steps for it on one line each.
Steps
  1. Name a task where Claude would need to act, not just talk (e.g. “check today’s calendar,” “read the latest CSV in Downloads”).
  2. Write line 1 — the user prompt in plain English.
  3. Write line 2 — the tool_use the model would emit (tool name + one argument, e.g. read_file{path: "report.csv"}).
  4. Write lines 3 and 4 — the tool_result your code returns, and the final answer the model writes from it.
Verify
You have four labelled lines, and you can point at line 2 (tool_use) as the one step a plain chatbot never produces.

Stretch. Add a second tool to the same task and mark which step now repeats (the loop runs once per tool the model needs).

§ 03.02 · Unit 20

Declaring a tool.

A tool is described by JSON Schema. Name, description, parameters. The description matters — the model decides whether to call the tool by reading it.

From the last unit: You know what a tool is. Now you need to tell Claude about yours. The shape is JSON Schema; the leverage is the description.

get_weather description: when to use it params: city, units required: city model reads model decides
the description
is the most
important field
Name + description + schema · the model only sees these
tools = [
    {
        "name": "get_weather",
        "description": (
            "Get the current weather in a given city. "
            "Use this when the user asks about temperature, "
            "rain, sun, wind, or what to wear today."
        ),
        "input_schema": {
            "type": "object",
            "properties": {
                "city": {
                    "type": "string",
                    "description": "City name, e.g. Phoenix or San Diego",
                },
                "units": {
                    "type": "string",
                    "enum": ["fahrenheit", "celsius"],
                    "default": "fahrenheit",
                },
            },
            "required": ["city"],
        },
    }
]

Why the description is the most important field

The model doesn’t see your function code. It sees the name and the description. A good description tells the model not just what the tool does but when to reach for it. Treat it like a job description: specific about the cases, vague about the implementation.

Declare a tool for your work.

You’ll do
Write a JSON-Schema tool declaration for one task you’d hand to Claude, and prove it parses.
Steps
  1. Pick one task from your week (e.g. “look up an order by id,” “create a calendar event”). No task in mind? Copy the get_weather block above and edit it.
  2. Copy the get_weather declaration as your template. Rewrite name, the description (say when to use it, not just what), and the input_schema properties.
  3. Save it as my_tool.py with import json at the top and your declaration assigned to a variable named tool.
  4. Run it: python -c "import my_tool, json; t=my_tool.tool; json.dumps(t); print(sorted(t)[:3])".
Verify
json.dumps(tool) runs without error and the dict has all three keys: name, description, input_schema (with at least one property).

Stretch. Paste your declaration into a fresh Claude chat and ask, “Given this tool, what user message would make you call it?” If the answer matches your intent, your description is doing its job.

§ 03.03 · Hands-on · 25 min

The full tool-use loop.

Save this as tool_use_demo.py in the same agents/ folder you set up on Day 1. It declares two tools, calls Claude, and runs the loop until the model is done.

From the last unit: You have one tool declared. Now run the full loop end to end — the shape every agent in the world uses underneath.

model decides tool_use harness executes result model reads it done? if not, loop
The tool-use loop · model · harness · model · halt

The pattern works for any tool — reading a file, hitting an API, querying a database, running a shell command. Once you write the loop once, every agent is the same shape underneath.

# tool_use_demo.py — full minimal loop
import json, datetime
from anthropic import Anthropic

client = Anthropic()
MODEL = "claude-sonnet-4-6"

# 1. Declare the tools
tools = [
    {"name": "get_current_time",
     "description": "Return the current local date and time.",
     "input_schema": {"type": "object", "properties": {}}},
    {"name": "read_file",
     "description": "Read a UTF-8 text file from disk.",
     "input_schema": {
        "type": "object",
        "properties": {"path": {"type": "string"}},
        "required": ["path"]}},
]

# 2. Implement the tools
def get_current_time():
    return datetime.datetime.now().isoformat(timespec="seconds")

def read_file(path):
    with open(path, "r", encoding="utf-8") as f:
        return f.read()[:8000]

TOOL_FNS = {
    "get_current_time": lambda **_: get_current_time(),
    "read_file":        lambda **a: read_file(a["path"]),
}

# 3. The loop
def run(user_prompt):
    messages = [{"role": "user", "content": user_prompt}]
    while True:
        resp = client.messages.create(
            model=MODEL, max_tokens=1024,
            tools=tools, messages=messages)
        messages.append({"role": "assistant", "content": resp.content})
        if resp.stop_reason != "tool_use":
            for b in resp.content:
                if b.type == "text": print(b.text)
            return
        tool_results = []
        for b in resp.content:
            if b.type != "tool_use": continue
            print(f"[tool] {b.name}({json.dumps(b.input)})")
            try: out = TOOL_FNS[b.name](**b.input)
            except Exception as e: out = f"ERROR: {e}"
            tool_results.append({"type": "tool_result",
                                 "tool_use_id": b.id, "content": str(out)})
        messages.append({"role": "user", "content": tool_results})

if __name__ == "__main__":
    run("What time is it? Then read ./demo.txt and summarize the file in three bullets.")

Run it

The prompt asks the model to read ./demo.txt, so create that file first — one line is enough. Then run the loop from inside the agents/ folder where you saved the script.

# from the same agents/ folder you saved Day 1’s code into
cd agents
echo "Pradhya Day 03 demo: this line gets read by the tool-use loop." > demo.txt
python tool_use_demo.py

Example run

The two [tool] lines come from the print(...) in the loop — one per tool call — and everything after them is the model’s final answer:

$ python tool_use_demo.py
[tool] get_current_time({})
[tool] read_file({"path": "./demo.txt"})
It’s 2026-06-10T09:14:22 right now. Here’s demo.txt in three bullets:
- It’s a placeholder file that gives the tool-use loop something real to read.
- It names the two tools the demo declares: get_current_time and read_file.
- It walks through the loop: the model requests a tool, your harness runs it,
  the model reads the result and writes this answer.

Your timestamp and exact wording will differ — the two tool calls and the file-grounded summary will not. You just built an agent.

Run the tool-use loop end to end.

You’ll do
Run tool_use_demo.py and watch the model call two tools, then answer from the file.
Steps
  1. Save the code above as tool_use_demo.py in the same agents/ folder you used on Day 1 (or download tool_use_demo.py — right-click → Save As).
  2. From a terminal: cd agents — then activate Day 1’s venv (source .venv/bin/activate) and confirm echo $ANTHROPIC_API_KEY prints a key.
  3. Create the file the demo reads: echo "hello from demo.txt" > demo.txt.
  4. Run it: python tool_use_demo.py.
Verify
The terminal shows a [tool] get_current_time(...) line and a [tool] read_file(...) line, then a final answer that quotes what you put in demo.txt.

Stretch. Run it on a real file from your work: python tool_use_demo.py "Read ./demo.txt and list every tool it mentions." — swap in any text file’s path.

§ 03.04 · Unit 22

Structured outputs.

Sometimes you don’t want the model to talk. You want a JSON object you can pass straight to another system.

From the last unit: Tool use returns a result the model reads. Sometimes you want the result to BE the answer — structured, typed, ready to pass to other code. Same mechanism, different intent.

"The meeting was with Wei Ling, we discussed the carbon-border- tax linkage and she wanted..." prose · hard to consume force tool { who: "Wei Ling", topics: ["carbon-border-tax"], ask: "..." } typed · pass straight to code
Tool-use as typed return · the model fills the form

The trick: define a tool that the model is forced to call. Its “input” becomes your structured output. This is the closest thing to a typed return value the model offers. The fragment below is complete — save it as structured_demo.py in your agents/ folder and run it.

# structured_demo.py — force the model to fill a form
import json
from anthropic import Anthropic

client = Anthropic()
MODEL = "claude-sonnet-4-6"

meeting_notes = """
Call with Dana Okafor (procurement lead, Northwind).
Still open: the SOC2 letter they asked for last week, and pricing for the 50-seat tier.
Dana wants a revised quote before their board meeting on Friday.
She mentioned the security review is the only blocker left on their side.
Budget is already approved on their end; legal sign-off is the long pole.
Next step on us: send the SOC2 letter and the revised 50-seat quote by Wednesday.
"""

tools = [{
    "name": "extract_meeting_brief",
    "description": "Return a structured one-page meeting brief.",
    "input_schema": {
        "type": "object",
        "properties": {
            "who":            {"type": "string"},
            "open_threads":   {"type": "array", "items": {"type": "string"}},
            "talking_points": {"type": "array", "items": {"type": "string"}, "minItems": 3, "maxItems": 3},
            "the_ask":        {"type": "string"},
        },
        "required": ["who", "open_threads", "talking_points", "the_ask"],
    },
}]

resp = client.messages.create(
    model=MODEL,
    max_tokens=1024,
    tools=tools,
    tool_choice={"type": "tool", "name": "extract_meeting_brief"},
    messages=[{"role": "user", "content": meeting_notes}],
)
brief = resp.content[0].input   # already a typed Python dict
print(json.dumps(brief, indent=2))

Because tool_choice forces the call, you skip the loop entirely — one request, one typed dict back. This pattern is the foundation for the meeting-prep agent and the SEO content-brief workflow — anywhere you need a model to fill in a form rather than write prose.

Make the model fill a form.

You’ll do
Run structured_demo.py and confirm the output is valid JSON with the four required keys.
Steps
  1. Save the code above as structured_demo.py in your agents/ folder (the venv from Day 1 already has anthropic installed).
  2. Run it: python structured_demo.py. It prints a JSON object.
  3. Pipe the output through a parser to prove it’s real JSON: python structured_demo.py | python -c "import sys, json; d=json.load(sys.stdin); print(sorted(d))".
Verify
The second command prints ['open_threads', 'talking_points', 'the_ask', 'who']json.load succeeded and every required key is present. (No JSONDecodeError means the output parsed.)

Stretch. Replace meeting_notes with a messy paragraph from your own inbox. The schema holds; only the values change.

§ 03.05 · Unit 23

The agentic discipline.

A tool-using model is a model with hands. Hands break things. Three rules separate a useful agent from an expensive mistake.

From the last unit: Tool use turns a model into an agent. An agent with hands can break things. Three rules separate a useful agent from an expensive mistake.

1 scope 2 preview 3 reversible
Three rules · the difference between useful agent and expensive mistake
  1. Scope it. Be explicit about which folder, which app, which account. An agent that can read your whole disk is an agent that will read your whole disk.
  2. Preview before commit. Ask for the plan, approve it, then run. The cost of one extra round-trip is nothing compared to one wrong action.
  3. Reversibility first. Drafts before sends. Copies before moves. Read-only before read-write. Build the agent so the worst thing it can do today is print something dumb.
The hardest lesson The hardest lesson with agents isn’t writing the prompt. It is deciding what they’re allowed to touch.
Commitment card Before Day 04, run the tool-use demo on a real file from your work. Then change one tool’s description and watch how the model’s behavior shifts. The descriptions are where the design lives.

Scope rule #1, in code

Your read_file from §03.03 reads any path the model names — including ~/.ssh/config. Rule #1 (scope it) is three lines: resolve the path and refuse anything outside the current folder. Swap your read_file for this version.

import pathlib

ALLOWED_ROOT = pathlib.Path.cwd().resolve()   # only this folder

def read_file(path):
    p = pathlib.Path(path).expanduser().resolve()
    if not str(p).startswith(str(ALLOWED_ROOT)):   # <-- the path-restriction line
        return f"ERROR: blocked — {p} is outside {ALLOWED_ROOT}"
    return p.read_text(encoding="utf-8", errors="replace")[:8000]

Feel the blast radius.

You’ll do
Watch the same agent read a secret file with the guard off, then get blocked with the guard on.
Steps
  1. In your tool_use_demo.py from §03.03, run it pointed at a sensitive file: python tool_use_demo.py "Read ~/.ssh/config and tell me what hosts are in it." (No SSH config? Use ~/.zshrc or any file outside agents/.) Note that the agent happily reads it.
  2. Now replace read_file with the guarded version above (keep the [:8000] truncation if you like). Save.
  3. Run the exact same command again — this time it’s blocked.
  4. Widen the guard for a moment — set ALLOWED_ROOT = pathlib.Path.home().resolve() — re-run and watch it read the secret again. Then restore the guard: change it back to cwd().
Verify
With the guard on, the [tool] read_file(...) result is ERROR: blocked — …/.ssh/config is outside …/agents and the model reports it can’t read it. You can quote the path-restriction line and say in one sentence what changed between the two runs.

Stretch. This is rule #1 only. Add rule #3 (reversibility) by making the tool refuse to open anything ending in .env or id_rsa, even inside the folder.