Back to course overview
Module 1Agent fundamentals 13 min
Function calling
The wire protocol: tool definitions, tool_use and tool_result blocks, the loop's message bookkeeping, and parallel calls — in real code.
Time for the actual mechanics. Every major model API speaks a version of the same protocol; here it is with the Anthropic API, which you'll use all course. Three moves: you declare tools, the model requests calls, you return results — all inside the growing message list.
tools.pypython
TOOLS = [
{
"name": "lookup_order",
"description": ("Look up a Harbor Lane order by ID (format HL-####). "
"Returns items, status, ship dates, and totals. "
"Always use this before any refund or return decision."),
"input_schema": {
"type": "object",
"properties": {
"order_id": {"type": "string", "pattern": "^HL-\\d{4}$"}
},
"required": ["order_id"],
},
},
# search_docs, calculate, update_ticket ... same shape
]loop.pypython
import anthropic, json
client = anthropic.Anthropic()
def run_agent(system: str, task: str, max_turns: int = 10) -> str:
messages = [{"role": "user", "content": task}]
for _ in range(max_turns):
resp = client.messages.create(
# model ids change; if you get model-not-found, use a
# current id from the Anthropic model list.
model="claude-sonnet-5", max_tokens=2048,
system=system, tools=TOOLS, messages=messages,
)
# ALWAYS append the full assistant turn (text + tool_use blocks)
messages.append({"role": "assistant", "content": resp.content})
if resp.stop_reason != "tool_use":
# Teaching loop: everything not tool_use is "done". Production
# loops also branch on max_tokens (truncated) and refusal,
# not just tool_use vs done.
return "".join(b.text for b in resp.content if b.type == "text")
# Execute EVERY tool_use block; one tool_result per id
results = []
for block in resp.content:
if block.type == "tool_use":
out = execute_tool(block.name, block.input) # your code
results.append({"type": "tool_result",
"tool_use_id": block.id,
"content": json.dumps(out)})
messages.append({"role": "user", "content": results})
return "[stopped: turn limit reached]"The bookkeeping rules that cause 90% of beginner bugs
- Echo the full assistant message back — including its
tool_useblocks — before sending results. The model needs to see its own request in history to understand the result. - Every `tool_use` id gets exactly one `tool_result`, matched by
tool_use_id, in the next user message. Skip one and the API rejects the request. - The model may request several tools at once (parallel calls — e.g.
lookup_orderandsearch_docstogether). Execute all, return all results in one user message. Free latency win; your loop above already handles it. - Tool errors go back as results, not exceptions.
{"error": "..."}inside atool_result(optionally withis_error: true) lets the model read the failure and adapt — crashing the loop teaches it nothing. - `stop_reason` is your state machine:
tool_use→ execute and continue;end_turn→ the agent believes it's done; hitting yourmax_turns→ the agent is wandering (a signal to log, not just a brake). - Add a cumulative-cost circuit breaker alongside `max_turns`: track spend across turns (sum each response's input/output tokens × price) and hard-stop the run at a dollar cap.
max_turnsbounds loop count; the cost cap bounds loop expense — the one that protects a small budget when a few turns each burn a large context.
Read one raw transcript this week
Print the full messages list after a real run, once, and read it top to bottom — the model's reasoning text, its tool picks, your results, its adaptation. Every abstraction you'll meet (frameworks, orchestrators) is sugar over exactly this list. Developers who've read the raw transcript debug agents; developers who haven't debug frameworks.