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.
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.
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.
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.
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.
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.
| Asset | Signature / URI | What it returns |
|---|---|---|
| Tool | query_sales(month: str) | Order count, refunds, and completed revenue for one month (YYYY-MM) |
| Tool | top_products(from_date, to_date) | Best-selling products by quantity for a date range (YYYY-MM-DD) |
| Tool | subscription_status() | Active vs cancelled subscription counts, with monthly quantities |
| Resource | daybreak://catalog | Product catalog with prices |
| Resource | daybreak://policy/refunds | Refund and cancellation policy |
| Prompt | monthly_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.
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"
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.
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.
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.
Try it yourself - this week ◐ 30-40 min total
- Run your loop against three questions: "What was March 2026 revenue?", "What are our top products?", and "How many active subscriptions do we have?" - one tool each. Watch the stop_reason on every turn.
- Write one NEW tool schema for Daybreak, description-first: draft the description and schema before any Python, then check it against the three ACI habits from Part 1. (Candidates: order lookup by customer, revenue by product.)
- Break the loop on purpose: remove the max_turns guard, then ask an impossible task ("find the order from 2019" - the data starts in 2026) and watch it never exit. Put the guard back. You now believe in exit conditions.
Official sources covered
Certificates, videos, and graded assessments stay on the official sites (linked each session). This page covers:
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.