learn-ai-agents-with-phoebe / Builder session 2 of 10
Learn AI Agents with Phoebe · Builder track · Session 2 of 10

Tools + your first agent

Last session you learned the loop; tonight you build it. First the quiet craft that decides whether an agent works at all: the tool schema - because the description, not your prompt, is the interface the model actually reads. Then you wire in Daybreak's toolbox (the very MCP server from the MCP course) and hand-roll the agent loop in about thirty lines of raw Python - Claude API first, Ollama as the local path. By the end, the scorecard climbs its first rung: 1/12 becomes 5/12. And one task gets more dangerous, on purpose.

🟢 Builder track Practitioners: DE · DS · AI engineers · builders Python · Claude API + Ollama path 45 min
0-3 · Welcome 3-18 · Tool schemas + ACI 18-42 · Build-along: the minimal loop 42-45 · Q&A
Part 0

Where we are

b1 gave you the loop and the four levers. Tonight turns on the first lever: tools. You will read one tool definition the way the model reads it, meet the Daybreak toolbox you may have built in the MCP course, then write the loop yourself - no framework, no SDK magic - so that every framework you touch afterwards is just this loop wearing a coat.

Live - presented in session Self-study - read after class ▶ Live agent run - step through + toggle levers Official sources covered
★ What you walk out with today The anatomy of a tool schema and the ACI habit (prompt-engineer your tools, not just your prompt), two legitimate ways to wire the Daybreak toolbox in (MCP-native or plain function-calling), a ~30-line agent loop you wrote and understand line by line, and rung 1 of the scorecard: 5/12 with tools on.
Part 1 · covers Anthropic tool-use docs + essay appendix 2 (ACI), HF Unit 1

A tool is an interface for a model 8 min live

A tool definition has three parts: a name, a description, and a JSON Schema for its input. Here is the part almost everyone gets backwards: the model never sees your Python. It sees the description. Anthropic's essay closes on exactly this point - appendix 2 is about crafting the agent-computer interface (ACI): prompt-engineer your tool docs and test them, the same way you would craft a prompt. A vague description gives you a confused agent no system prompt can rescue.

ONE TOOL DEFINITION - query_sales name "query_sales" description "Order count, refunds, and completed revenue for one month (YYYY-MM)." input_schema { month: string (YYYY-MM), required } ROUTING the handle the model emits when it chooses this tool over the others THE MODEL'S MANUAL the ONLY thing the model reads to decide when to call it - prompt-engineer this (ACI) ARGUMENT CONTRACT JSON Schema validates the arguments; strict: true enforces the exact shape ACI (essay appendix 2): prompt-engineer your tools - descriptions are docs the model actually reads. Anthropic tool use supports strict: true - the API then guarantees arguments match the schema exactly.
🔍 Click to zoom - anatomy of one tool definition: name routes, description teaches, schema enforces
LiveWriting descriptions the ACI way4 min▶

Anthropic's ACI guidance boils down to three habits, all visible in the Daybreak toolbox:

  • Say what comes back, not just what goes in. "Order count, refunds, and completed revenue for one month" tells the model exactly when this tool answers its question - and when it does not.
  • Put the format in the description. "(YYYY-MM)" saves the model from guessing "March", "03/2026", or "2026-3" - malformed arguments are the most common tool failure.
  • Test tools like prompts. Run the agent on questions near the tool's edges and watch which tool it picks. If it picks wrong, fix the description before touching the system prompt.
Real world

The one-word fix. An agent kept calling a revenue tool to answer refund-policy questions. The team rewrote the system prompt three times with no luck. The actual fix: the tool's description said "sales data" and nothing else - adding "does NOT cover policies or subscriptions" routed every question correctly from then on. Interface bug, not model bug.

Fix the tool description, not the system prompt ✗ Vague tool description Description says only "sales data" Agent calls query_sales for refunds Three system-prompt rewrites failed ✓ Tight tool description Description adds one exclusion line "does NOT cover policies/subs" Every question routes correctly Interface bug, not model bug. Prompt-engineer the tool description like you would a prompt.
🔍 Click to zoom - the fix was the tool description, not the prompt
Self-studySchemas can be strict - and vendors type their tools3 min read▶

Two refinements worth knowing before b7 makes them matter:

  • strict: true (Anthropic tool use). With strict mode on, the API guarantees the tool-call arguments conform to your JSON Schema exactly - no missing required fields, no surprise extras. Cheap insurance; turn it on for tools whose arguments feed SQL or APIs.
  • OpenAI's tool taxonomy. The OpenAI guide sorts tools into data tools (read: query_sales), action tools (write: send_email, refund_order - arriving later in this track), and orchestration tools (agents used as tools - b6). The taxonomy matters because risk scales with the category: data tools can mislead you; action tools can hurt you. b7 risk-rates every tool on exactly this axis.
Part 2 · covers the running project + Academy tool-use lessons

The Daybreak toolbox 7 min live

This course does not invent a toolbox - it inherits one. The MCP course's Daybreak server (server.py in this repo's project folder) ships three tools, two resources, and one prompt over the same SQLite database the SQL course used. That server is this agent's hands for the rest of the track.

One toolbox, inherited from the MCP course: three tools, two resources, one prompt tool resource prompt TOOL query_sales(month) Order count, refunds, revenue for one month (YYYY-MM) TOOL top_products() Best-selling products by quantity, date range TOOL subscription_status() Active vs cancelled subscription counts RESOURCE daybreak://catalog Product catalog with prices RESOURCE daybreak://policy/refunds Refund and cancellation policy PROMPT monthly_report(month) Standard monthly ops report template One process, three asset types. MCP or plain functions, the b2 loop cannot tell the difference.
🔍 Click to zoom - one process, six assets, every host can share them
AssetSignature / URIWhat it returns
Toolquery_sales(month: str)Order count, refunds, and completed revenue for one month (YYYY-MM)
Tooltop_products(from_date, to_date)Best-selling products by quantity for a date range (YYYY-MM-DD)
Toolsubscription_status()Active vs cancelled subscription counts, with monthly quantities
Resourcedaybreak://catalogProduct catalog with prices
Resourcedaybreak://policy/refundsRefund and cancellation policy
Promptmonthly_report(month)Standard monthly ops report template for the founder
LiveWire it two ways - both are legitimate6 min▶

(a) MCP-native. If you built the server in the MCP course, your agent's client session lists its tools and calls them over the protocol - the toolbox stays one process, every host can share it:

from mcp import ClientSession, StdioServerParameters
from mcp.client.stdio import stdio_client

params = StdioServerParameters(command="uv", args=["run", "server.py"])
async with stdio_client(params) as (read, write):
    async with ClientSession(read, write) as session:
        await session.initialize()
        tools = await session.list_tools()   # query_sales, top_products, subscription_status
        result = await session.call_tool("query_sales", {"month": "2026-03"})

(b) Plain function-calling. No server? Wrap the same SQLite queries as local Python functions and pass their schemas straight to the API:

import sqlite3

def query_sales(month: str) -> str:
    """Order count, refunds, and completed revenue for one month (YYYY-MM)."""
    con = sqlite3.connect("daybreak.db")
    try:
        rows = con.execute(
            "SELECT status, COUNT(DISTINCT o.order_id),"
            "       COALESCE(SUM(oi.quantity * oi.unit_price), 0)"
            "  FROM orders o JOIN order_items oi ON oi.order_id = o.order_id"
            " WHERE substr(o.order_date, 1, 7) = ? GROUP BY status",
            (month,),
        ).fetchall()
    finally:
        con.close()
    ...  # format exactly like the MCP server does

TOOL_FUNCTIONS = {"query_sales": query_sales,
                  "top_products": top_products,
                  "subscription_status": subscription_status}

Same tools, same answers, same agent. The course uses whichever you have - MCP buys you reuse across hosts; plain functions buy you zero setup. The loop in Part 3 cannot tell the difference.

Part 3 · covers Anthropic tool-use docs (loop + Tool Runner), HF Unit 1

Build-along: the minimal loop in raw Python ★ 15 min · type along

Here is the entire agent, no framework. One while-style loop: call the model with tools attached; if it stops because it wants a tool (stop_reason == "tool_use"), run the tool, append the result, go around again; otherwise it answered - break. The b1 exit conditions are all here: a final answer, no more tool calls, an error, or the max_turns guard.

import anthropic

client = anthropic.Anthropic()

TOOLS = [{
    "name": "query_sales",
    "description": ("Order count, refunds, and completed revenue "
                    "for one month (YYYY-MM)."),
    "input_schema": {
        "type": "object",
        "properties": {
            "month": {"type": "string",
                      "description": "Month as YYYY-MM, e.g. 2026-03"},
        },
        "required": ["month"],
    },
}]

def run_agent(goal: str, max_turns: int = 10) -> str:
    messages = [{"role": "user", "content": goal}]
    for _ in range(max_turns):                      # exit 4: max turns
        response = client.messages.create(
            model="claude-opus-5",
            max_tokens=1024,
            tools=TOOLS,
            messages=messages,
        )
        if response.stop_reason == "tool_use":      # model wants a tool run
            messages.append({"role": "assistant", "content": response.content})
            results = []
            for block in response.content:
                if block.type == "tool_use":
                    output = TOOL_FUNCTIONS[block.name](**block.input)
                    results.append({"type": "tool_result",
                                    "tool_use_id": block.id,
                                    "content": output})
            messages.append({"role": "user", "content": results})
            continue                                # observe, then loop
        return response.content[0].text             # exits 1-2: done
    return "Stopped: max_turns reached."

print(run_agent("What was March 2026 revenue?"))
# -> calls query_sales("2026-03") -> "$150.00 completed, 1 refunded"
You just hand-rolled what SDKs automate Anthropic's SDK ships a Tool Runner that runs this exact loop for you - hand it functions, it handles tool_use, execution, and tool_result plumbing. Use it in real projects. We hand-rolled once so that when a framework loop misbehaves, you can see through the coat to the loop underneath.
Self-studyThe Ollama variant - same loop, local model4 min read▶

The loop is vendor-neutral. With Ollama, a tool-capable local model plays the same game - it even accepts plain Python functions as tools and reads their docstrings as descriptions (ACI again):

import ollama

messages = [{"role": "user", "content": "What was March 2026 revenue?"}]
for _ in range(10):
    response = ollama.chat(model="qwen3", messages=messages,
                           tools=[query_sales])   # functions as tools
    messages.append(response.message)
    if not response.message.tool_calls:
        print(response.message.content)           # final answer
        break
    for call in response.message.tool_calls:
        output = TOOL_FUNCTIONS[call.function.name](**call.function.arguments)
        messages.append({"role": "tool", "tool_name": call.function.name,
                         "content": str(output)})

Differences from the Claude path: the done-signal is "no tool_calls" instead of stop_reason, and tool results go back as role: "tool" messages. The shape - decide, act, observe, repeat, exit - is identical. Small local models call tools less reliably; that is a model gap, not a loop gap.

Demo 1 of 2

Your loop, running - predict every step ★ 8 min · everyone runs it

This is the loop you just wrote, running the b1 question over Daybreak's real data. This time you know the machinery - so predict each step before you click Next: what will the model do, which tool, which argument, what comes back?

Before each Next, say the step out loud: reason → query_sales("2026-03") → 📦 result → answer. Score your predictions.

At the 📦 step, check the number: $150.00, 1 refund excluded - computed live from the embedded Daybreak data, exactly what your Python returns.

Toggle Tools off and step through again. Name the failure precisely: no tool_use stop, so the loop never runs a tool - the model improvises from memory.

Demo 2 of 2

Rung 1 unlocked - and a new danger ★ 7 min · explore

The scorecard from b1, with the lever you built tonight switched on. You just unlocked rung 1: 5/12 - up from 1/12 with everything off. Lookups live. But do not skip the fine print on task 12.

Confirm the climb: 1/12 → 5/12 with tools on. Every task that flipped to ✓ is a lookup your three tools can now answer.

Read WHY task 12 got MORE dangerous with tools on: an agent that can act will act on whatever its context says - including instructions hiding inside untrusted data it reads. That is prompt injection, and it is now a live wire. b7 builds the guardrail that catches it.

Note what stays red: multi-step comparisons (memory, b4) and decomposition tasks (planning, b5). Tools alone are hands without a notebook or a plan.

How to read every agent demo in this course 👤 user · 🧠 model reasoning · 🔧 tool call · 📦 tool result · 🛡 guardrail · 💬 final answer. The model's words are a scripted teaching simulation replaying the characteristic behaviors; every number in a 📦 step is genuinely computed from the embedded Daybreak data.
Homework

Try it yourself - this week ◐ 30-40 min total

Source material

Official sources covered

Certificates, videos, and graded assessments stay on the official sites (linked each session). This page covers:

✓Anthropic "Building Effective Agents" - appendix 2 (ACI)Prompt-engineer your tools: descriptions as the agent-computer interface
✓Anthropic tool-use docsTool schemas, the tool_use / tool_result loop, Tool Runner, strict: true
✓Hugging Face Agents course - Unit 1Functions as tools; docstrings become descriptions on the Ollama path
◐OpenAI "A Practical Guide to Building Agents"Data / action / orchestration tool types - risk-rated fully in b7
◐Anthropic Academy "Building with the Claude API" - tool-use lessonsThe same loop, taught with the Academy's exercises; do them for practice
Check yourself

Three questions before you go 🎯 ◐ 90 seconds

1 · When the model picks which tool to call, what does it actually read?

The model sees name + description + JSON Schema, nothing else. That is why ACI (essay appendix 2) says to prompt-engineer descriptions: they ARE the interface. Wrong tool picked? Fix the description first.

2 · The response comes back with stop_reason "tool_use". What does that mean?

The model never runs anything - it emits a request and stops. Your loop runs the function, appends the tool_result, and continues. Any other stop_reason means the loop can exit with an answer.

3 · Turning tools ON made task 12 MORE dangerous on the scorecard. Why?

An LLM with no tools can only say bad things; an agent with tools can do them. Task 12 reads untrusted tickets - a planted instruction becomes a live action. That is prompt injection; b7 builds the guardrail.

Builder session 2 cheat sheet · pin this

Tool anatomyname (routing) + description (the model's manual) + JSON Schema (argument contract). The model never sees your code.
ACI rulePrompt-engineer your tools (essay appendix 2): say what comes back, put formats in the description, test tools like prompts.
Daybreak toolboxquery_sales(month) · top_products(from, to) · subscription_status() + catalog + refund-policy resources + monthly_report prompt.
Two wiringsMCP-native (session lists + calls server tools) or plain function-calling (wrap the same SQLite queries). The loop cannot tell.
The loopmessages.create with tools → stop_reason "tool_use"? run tool, append tool_result, continue → else break with the answer.
Exit conditionsFinal answer · no more tool calls · error · max_turns guard. Remove the guard and an impossible task loops forever.
Tool RunnerSDKs auto-run this loop (Anthropic Tool Runner). Use it in production - hand-roll once so you can debug through it.
Rung 1Tools on: 1/12 → 5/12. And task 12 got MORE dangerous - acting agents obey planted instructions. Guardrails land in b7.