TAI BUI
Back to curriculum
LearnPython

The Tool Interface — Why Agents Need Structured I/O

A language model produces tokens. A program takes actions. The tool interface sits between them: a contract where the model requests an action and the host carries it out. Function calling on OpenAI, Anthropic, and Gemini, MCP's `tools/call`, and A2A's task parts are different encodings of the same four-step loop. This lesson names the loop and builds the minimum machinery to run it.

TIME: ~45 minutesPREREQUISITES: Phase 11 (LLM completion APIs)

Learning Objectives

  • Explain why an LLM that can only generate text cannot, on its own, take actions against the real world.
  • Draw the four-step tool-call loop (describe → decide → execute → observe) and name who owns each step.
  • Write a tool description as three parts: name, JSON Schema input, and a deterministic executor function.
  • Distinguish pure and side-effecting tools and state why the split matters for safety.

The Problem

An LLM emits a probability distribution over the next token. That is the entire output surface. Ask a chat model "what is the weather in Bengaluru right now" and it can write a plausible sentence, but it cannot dial into a weather API. The sentence might be right by coincidence or three days stale.

The tool interface closes that gap. The host program (your agent runtime, Claude Desktop, ChatGPT, Cursor, or a custom script) advertises a list of callable tools to the model. When the model decides an action is needed, it emits a structured payload naming a tool and its arguments. The host parses that payload, runs the tool for real, and feeds the result back. The loop continues until the model decides no more calls are needed.

The first version of this contract shipped in June 2023 as OpenAI's "functions" parameter. Anthropic followed with tool_use blocks in Claude 2.1, and Gemini added functionDeclarations a few months later. All three now expose the same shape: a JSON-Schema-typed tool list in, a JSON-payload tool call out. The Model Context Protocol (November 2024) generalized the contract so one tool registry can serve many models. A2A (April 2026, v1.0) applies the same primitive to agent-to-agent delegation.

The four-step loop is the invariant underneath all of these. The rest of Phase 13 elaborates on it.

Warm-up0/3
1. You send a completion API the prompt "what is the weather in Bengaluru right now?" with no tools attached. What is the most likely result?Warm-up
A prompt box reading what is the weather in Bengaluru right now, an arrow to a box for a completion API with tools set to none, and a dashed box for the reply.
Typed schematic, not captured.
2. A retry wrapper automatically re-runs a failed request. Which request is safest to re-run blindly?Warm-up
A request fails, passes through a retry wrapper, and is run again, ending in a dashed box asking about the effect. A line below says the policy is to run the request again on failure with no check on what it does.
Typed schematic, not captured.
3. A function reads `data["age"]` from a dict parsed out of untrusted JSON and treats it as a number. What should it do first?Warm-up
Untrusted JSON passes through json.loads into a dict named data, then a small function reads data of age and treats it as a number. A dashed box asks what the function should do first.
Typed schematic, not captured.
Check: The Problem0/1
1. A chat completion API returns generated text. At the lowest level, what is a model's entire output before any tools are added?Define
A prompt reading The capital of France is, an arrow into a chat completion model box marked no tools added, and a dashed box asking for its entire output.
Typed schematic, not captured.

The Concept

Step one: describe

The host declares each tool with three fields.

  • Name. A stable, machine-readable identifier. get_weather, not "weather thing".
  • Description. A one-paragraph natural-language brief. "Use when the user asks about current conditions for a specific city. Do not use for historical data."
  • Input schema. A JSON Schema object (draft 2020-12) describing the tool's arguments.

The model receives the list. Modern providers serialize these declarations into the system prompt using a provider-specific template, so as the caller you only deal with the structured form.

Step two: decide

Given the user's message and the available tools, the model picks one of three behaviors.

  1. Answer directly in text. No tool call.
  2. Call one or more tools. Emit structured call objects. Under parallel_tool_calls: true (default on OpenAI and Gemini, opt-in on Anthropic) the model can emit multiple calls in one turn.
  3. Refuse. Strict-mode structured outputs can produce a typed refusal block instead of a call.

A tool call payload has three stable fields: a call id, a tool name, and a JSON arguments object. The id lets the host match a later result to the specific call, which matters when parallel calls come back out of order.

Step three: execute

The host receives the call, validates arguments against the declared schema, and runs the executor. Invalid arguments mean the model hallucinated a field or used the wrong type, a common failure on weaker models. Production hosts do one of three things: fail fast and surface the error to the model, repair the JSON with a constrained parser, or retry the model with the validation error included in the prompt.

The executor itself is ordinary code. Python, TypeScript, a shell command, a database query. It produces a result, which is usually a string but can be any JSON value or a structured content block (text, image, or resource reference in MCP). The result has to be serializable.

Step four: observe

The host appends the tool result to the conversation (as a tool role message with the matching id) and re-invokes the model. With the tool output in context, the model can produce a final answer or request more calls. This continues until the model stops emitting calls or the host hits a safety limit on iteration count.

The trust split

Tools come in two flavors that matter for safety.

  • Pure. Read-only, deterministic, no side effects. get_weather, search_docs, get_current_time. Safe to call speculatively.
  • Consequential. Mutates state, spends money, touches user data. send_email, delete_file, execute_trade. Must be gated.

Meta's 2026 "Rule of Two" for agent security says a single turn may combine at most two of: untrusted input, sensitive data, consequential action. The tool interface is where you enforce that rule, by rejecting calls, requiring user confirmation, or escalating scopes. See Phase 13 · 15 for the full security chapter and Phase 14 · 09 for agent-level permission policies.

Where the loop lives

ContextWho describesWho decidesWho executes
Single-turn function calling (OpenAI/Anthropic/Gemini)App developerLLMApp developer
MCPMCP serverLLM via MCP clientMCP server
A2AAgent Card publisherCalling agentCalled agent
Web browser (function-calling agent)Browser extension / WebMCPLLMBrowser runtime

The column names change from row to row. The four steps stay put.

Why not just prompt the model to emit JSON?

"Ask the model to reply in JSON" was the pre-function-calling pattern. It fails ~5 to 15 percent of the time on frontier models and far more on smaller models. Failure modes include missing braces, trailing commas, hallucinated fields, and wrong types. You then need a JSON repair pass, a retry, or a constrained decoder.

Native function calling is better for three reasons. First, the provider trains the model end-to-end on the exact call shape, so valid-JSON rate climbs to 98 to 99 percent on strict mode. Second, the call payload sits in its own protocol slot instead of inside free text, so a tool call does not leak into the user-visible reply. Third, providers enforce schema compliance with constrained decoding (OpenAI's strict mode, Anthropic's tool_use, Gemini's responseSchema), so the output is guaranteed to validate.

Phase 13 · 02 walks the three provider APIs side by side. Phase 13 · 04 goes deep on structured outputs.

Circuit breakers

The loop terminates when the model stops emitting calls or the host hits a maximum turn count. Production hosts set this between 5 and 20 turns. Past that, you are probably in a loop the model cannot exit. Claude Code defaults to 20, OpenAI Assistants to 10, and Cursor's agent mode to 25.

Unbounded loops are how you get "agent spent $400 in API calls overnight" post-mortems. Put a bound on yours before shipping.

Phase 14 · 12 covers error recovery and self-healing in depth. Phase 17 covers production rate limits.

Where Phase 13 goes from here

  • Lessons 02 through 05 polish the provider-level tool-call surface.
  • Lessons 06 through 14 generalize the loop into MCP.
  • Lessons 15 through 18 defend the loop against hostile servers, adversarial users, and unauthenticated remote auth surfaces.
  • Lessons 19 through 22 extend the pattern to agent-to-agent collaboration, observability, routing, and packaging.
  • Lesson 23 ships a complete ecosystem using each primitive.

Hold the four-step loop in mind. Each remaining lesson elaborates on it.

Check: The Concept0/27
1. In the four-step loop, what happens during the "decide" step?Define
Four tiles named decide, describe, execute and observe, listed alphabetically with decide highlighted, and a dashed box asking what happens during decide.
Typed schematic. The order shown is alphabetical, not the loop order.
2. What is a tool in the tool interface?Define
Three boxes drawn from the add tool in main.py: the identifier add, a compact schema with number fields a and b, and the executor tool_add taking args, above a dashed box asking what a tool is.
Typed excerpt of main.py's add tool.
3. What does "function calling" mean as a provider feature?Define
A table of three providers and how each spells the feature in its API (OpenAI functions parameter, Anthropic tool_use blocks, Gemini functionDeclarations), a line showing a chat completions create call with a tools list, and a dashed box asking for the general name.
Names from the lesson's Problem section; the call is the comment in fake_decide.
4. What is a tool call?Define
A user message reading please add 4 and 9, a model box for turn 1, and the tool_add executor box, with a dashed box asking what the model produces in turn 1.
Typed schematic, not captured.
5. What is a tool result?Define
The tool_add executor box beside its input a equals 4.0 and b equals 9.0, an arrow to a dashed box asking what it hands back, and a two-line code excerpt about appending to history.
Typed excerpt of the pattern in run_loop.
6. What are parallel tool calls?Define
A user message asking for the weather in Tokyo and the time in UTC, an arrow to a model box for one turn, and a dashed box asking what the turn emits. The host advertises get_weather and get_time.
Typed schematic. The message is illustrative.
7. What does strict mode do?Define
The get_weather input schema from main.py (city string, units enum of celsius and fahrenheit, city required), beside a strict mode on tag, and below it a model box that writes a call with an arrow to a dashed box asking about its output.
Schema as declared in main.py (formatting condensed).
8. What makes a tool pure?Define
A terminal listing the registry printed by main.py: add, get_time and get_weather each tagged pure, with descriptions cut off, and a dashed box asking what the pure label tells the host.
Captured from describe_registry; the descriptions are cut to fit.
9. What marks a tool as consequential?Define
Three tool names from the lesson, send_email, delete_file and execute_trade, in blue boxes, and a dashed box asking what marks a tool as consequential.
Tool names from the lesson's trust split section.
10. What are the four steps of the tool-call loop, in order?Define
A row of four dashed slots numbered step 1 to step 4, labelled as the steps of the tool-call loop in time order.
Typed schematic, not captured.
11. What is the host in a tool-calling system?Define
Three boxes naming Claude Desktop, Cursor and a custom script, and a dashed box asking what role all three play in a tool-calling system.
Examples named in the lesson's Problem section. Typed schematic, not captured.
12. In the "Where the loop lives" table in The Concept, who executes the tool call under MCP?Define
A four-row table with columns context, who describes, who decides and who executes. The rows are single-turn function calling, MCP, A2A and web browser, with the MCP row highlighted and every cell replaced by a dashed question mark.
Row and column names from the lesson; every cell is hidden. Row names shortened.
13. Why does a tool call payload include an `id` field?Explain
A terminal showing a call payload cut down to its id field, a value beginning call_, with other fields elided, and a dashed box asking why the payload carries an id.
The id is real fake_decide output; the other fields are left out.
14. What does Meta's 2026 "Rule of Two" for agent security forbid?Explain
A box named Rule of Two with the note Meta, 2026, agent security, an arrow to a dashed box asking what it forbids.
Typed schematic, not captured.
15. Why is native function calling more reliable than prompting a model to reply in JSON?Explain
Two boxes side by side, one for asking in the prompt to reply only in JSON and one for native function calling with tools declared in the API, and a dashed box asking why the second is more reliable.
Typed schematic, not captured.
16. How does the trust split separate a pure tool from a consequential one?Explain
Two columns of tool names from the lesson: get_weather, search_docs and get_current_time under pure, send_email, delete_file and execute_trade under consequential, and a dashed box asking what separates them.
Names from the lesson's trust split section. Typed schematic, not captured.
17. Which problem makes prompting a model to reply in JSON less reliable than native function calling?Explain
A row of three boxes: a prompt saying reply only in JSON, a model that writes text, and a host that parses the text, above a dashed box asking what makes this less reliable.
Typed schematic, not captured.
18. The model sends invalid tool arguments. Which set lists what a production host can do?Explain
A get_weather call with the argument city set to the number 7 goes from the model to the host, followed by a dashed box asking what a production host can do. The note says city is a string in the declared schema.
Typed schematic. The example call is illustrative.
19. Why does a tool declaration carry a natural-language description in addition to its name and schema?Explain
A tool declaration box for get_weather showing its name, a hidden description marked with a dashed question mark, and an input schema with city and units, above a dashed box asking why a description is declared.
Name and schema as in main.py; the description is hidden here. Typed schematic, not captured.
20. Why does the host append the tool result to the conversation and call the model again?Explain
Three boxes: the model finishing turn 1 with a call, the executor having run the code, and the host about to append and call the model again, above a dashed box asking why the host calls the model again.
Typed schematic, not captured.
21. A weather tool declares `units` as an enum of `celsius` and `fahrenheit`. The model emits `"units": "kelvin"`. Where should the host catch this, and what should it do?Apply
The get_weather units enum of celsius and fahrenheit beside the emitted call with city Tokyo and units kelvin, above a dashed box asking where the host should catch it and what to do.
Schema as declared in main.py. Typed schematic, not captured.
22. An overnight run made hundreds of model calls and a large API bill because the agent never stopped re-calling the same tool. Which safeguard was missing?Analyze
A row of boxes reading model call, same tool, model call, same tool and an ellipsis, a box saying next morning there were hundreds of model calls and a large API bill, and a dashed box asking which safeguard was missing.
Typed schematic, not captured.
23. A host runs two parallel calls and appends both results without call ids. The model then attaches the weather answer to the time question. Which ingredient was missing?Analyze
A model turn emitting a get_weather call and a get_time call, the history after execution showing two tool messages with weather and time results and no ids, and a dashed box asking for the missing ingredient.
Result values are real executor output; the history shape is typed.
24. After a team swaps native function calling for "reply in JSON" prompting, users sometimes see raw call JSON inside the chat reply. What explains it?Analyze
A chat window with a user message asking about the weather in Tokyo and an assistant bubble containing the sentence Let me check that followed by a raw JSON call to get_weather, above a dashed box asking what explains it.
Illustrative chat. Typed schematic, not captured.
25. You are adding a `send_invoice` tool that emails customers a payment request. How should it be registered?Design
A box for a send_invoice tool that emails customers a payment request, a one-line REGISTRY.append pattern for it, and a dashed box asking how it should be registered.
Typed schematic, not captured.
26. An agent reads untrusted web pages, can see a customer's private records, and also has a `post_to_forum` tool. Which design change fits the Rule of Two?Design
Three stacked boxes: an agent that reads untrusted web pages, can see a customer's private records, and has a post_to_forum tool, above a dashed box asking which design change fits the Rule of Two.
Typed schematic, not captured.
27. Your host runs a smaller model that sometimes sends bad argument types. You want it to recover without crashing or running a bad call. Which design fits?Design
A smaller model emits an add call with a as the word seven and b as 35, which goes to the host and then to the tool_add executor, with a dashed box asking what design recovers without crashing.
Typed schematic. The example call is illustrative.

Use It

code/main.py runs the four-step loop without an LLM. A fake "decider" function stands in for the model by pattern-matching on the user message, while the executor, schema validator, and observe-step harness are real. Run it to see the full request/response choreography with printable intermediate state, then swap the fake decider for any real provider in a later lesson.

What to look at:

  • The tool registry holds five fields per tool: name, description, schema, an executor reference, and a consequential flag.
  • The validator is a minimal JSON Schema subset (types, required, enum) written in stdlib only. Phase 13 · 04 ships a fuller one.
  • The loop bounds iteration count at five. Production agents need this kind of circuit breaker.
Check: Use It0/9
1. What does the tool registry in `main.py` store per tool, and how many turns does the loop allow? (Use It)Apply
The three registered tools add, get_time and get_weather stacked beside a dashed box asking what each Tool entry stores, and below it the loop line for turn in range from 1 to MAX_TURNS plus 1 beside a dashed box asking for the value of MAX_TURNS.
Names and the loop line are from main.py.
2. In `main.py`, what happens on the first turn of `run_loop("please add 7 and 35")`?Apply
An excerpt of the add branch of fake_decide with its regular expressions, then the call run_loop with please add 7 and 35, and a dashed box asking what turn 1 does.
Excerpt copied from main.py; add takes numbers a and b, both required.
3. In `main.py`, what does `run_loop("write me a haiku about tea")` print?Apply
An excerpt listing the patterns fake_decide checks for add, time and weather, then the call run_loop with write me a haiku about tea, and a dashed box asking what is printed.
Excerpt copied from main.py; the rest of the function is elided.
4. In `main.py`, how many turns does `run_loop("tell me the weather in Bengaluru")` take?Apply
An excerpt of run_loop with the history list and the for loop over turns, the weather pattern from fake_decide, the call run_loop with tell me the weather in Bengaluru, and a dashed box asking how many turns the loop takes.
Excerpts copied from main.py.
5. A `get_weather` call arrives with arguments {"city": "Tokyo", "colour": "blue"}. What does `validate` in `main.py` do?Apply
The get_weather schema from main.py, with city required, beside the arguments city Tokyo and colour blue, and a dashed box asking what validate gives.
Schema as declared in main.py. Typed schematic, not captured.
6. In `main.py`, what does `tool_get_weather` return for the arguments {"city": "Paris"}?Apply
The lookup table in tool_get_weather with Bengaluru, Tokyo, Zurich and Lagos temperatures, then the call with city Paris, and a dashed box asking what it returns.
Excerpt copied from main.py; the rest of the function is elided.
7. A call to `add` arrives with arguments {"a": 7} only. What does `run_loop` in `main.py` do?Apply
The add schema with a and b required beside the arguments a equals 7 only, and a dashed box asking what run_loop does.
Schema as declared in main.py. Typed schematic, not captured.
8. A `delete_file` tool is registered with the default flags. The agent deletes a user's file and no confirmation line ever prints. What is the bug?Analyze
A terminal with the real output of run_loop for a delete_file call: a user line, a DECIDE line calling delete_file, its arguments, and an EXECUTE line, with no confirmation line between them, and a dashed box asking for the bug.
Real run_loop output with a delete_file tool added by this script. Its stub deletes nothing; path and id are made up.
9. The harness prints `ERROR : unknown tool get_forecast` and stops. What happened?Analyze
A terminal with an ellipsis and the line ERROR : unknown tool get_forecast, and a dashed box asking what happened.
Line copied from the stem and from run_loop's format; earlier lines are elided.

Ship It

This lesson produces outputs/skill-tool-interface-reviewer.md. Given a draft tool definition (name, description, schema, and executor outline), the skill audits it for loop fitness. Is the name machine-stable? Is the description a complete usage brief? Does the schema use JSON Schema 2020-12 correctly? Is the pure-vs-consequential classification explicit?

Check: Ship It0/1
1. What does the tool-interface-reviewer skill audit a draft tool definition for? (Ship It)Define
A box for a draft tool definition, an arrow to a box for the reviewer skill, and an arrow to a dashed box asking what it audits for.
Typed schematic, not captured.
Author's Note

I'd run main.py before rereading the prose. It sends four queries through the loop and prints each step as DECIDE, EXECUTE, and OBSERVE lines, with the call id and arguments, so you can watch the choreography instead of imagining it. The last query, a haiku about tea, matches no tool and gets a plain text answer, which is the "answer directly" branch of the decide step.

My bet is that validate and run_loop are the parts worth reading slowly. Notice how little the validator does. It checks types, required fields, and enums, and it does not check for unknown fields, so an extra argument passes straight through. On a validation error the harness just prints and returns, where a real host would hand the error back to the model. Exercise 2 asks you to decide what the right behavior is.

I'd also keep the toy in proportion. fake_decide is a keyword router and get_weather returns canned data, so nothing here tells you how a real model behaves. The consequential flag exists on the Tool dataclass and prints a gate line, but all three tools are pure, so that branch never runs until you add a tool that needs it.

Exercises

  1. Add a fourth tool to code/main.py called get_stock_price(ticker). Write its description as "Use when the user asks for a current stock price by ticker. Do not use for historical prices or market summaries." Run the harness and confirm the fake decider routes queries mentioning tickers to the new tool.

  2. Break the schema validator. Pass a call whose arguments object is missing a required field, and confirm the host rejects it before execution. Then pass a call with an extra unknown field. Decide: should the host reject or ignore? Justify your choice with a safety argument.

  3. Classify each tool in the harness as pure or consequential. The Tool dataclass already has a consequential flag and the loop already prints a gate line when it is set. Add a fourth tool, send_email, with consequential=True, and confirm the gate line appears before it executes. This is the shape of the confirmation gate every production host needs.

  4. Draw the four-step loop on paper with the provider-column table above filled in for your favorite client (Claude Desktop, Cursor, ChatGPT, or a custom stack). Cross-reference with the MCP-specific variant in Phase 13 · 06.

  5. Read OpenAI's function-calling guide top to bottom. Identify the one field that sits in the request but not in the four-step loop as presented here. Explain what it adds and why it is convenient rather than essential.

Key Terms

TermWhat people sayWhat it actually means
Tool"A thing the model can call"A triple of name + JSON-Schema-typed input + executor function
Function calling"Native tool use"Provider-level API support for emitting structured tool calls instead of prose
Tool call"The model's request to act"A JSON payload with id, name, arguments emitted by the model
Tool result"What the tool returned"The executor's output, wrapped in a tool role message with matching id
Parallel tool calls"Many calls at once"Multiple call objects in one model turn, independent and orderable by id
Strict mode"Guaranteed JSON"Constrained decoding that forces the model's output to validate against the declared schema
Pure tool"Read-only tool"No side effects; safe to re-run
Consequential tool"Action tool"Mutates external state; requires gate, audit, or user confirmation
Four-step loop"The tool-call cycle"describe → decide → execute → observe
Host"Agent runtime"The program that holds the tool registry, calls the model, and runs the executor

Further Reading

Code

Ships With