Chapter 03 · Section III · 10 min read
3.3 Tools and actions: how the loop *does*, not just *says*
The verbs the loop can use to change the world. Without tools the model can only suggest; with tools the model can act. And that changes every safety calculation.
A model without tools can produce text. That text may describe a beautiful action — “I would run pytest, then commit the fix, then open a PR” — but the action doesn’t happen. Give the model a tool for run_shell_command, and now it can actually run pytest. Give it write_file and it can actually commit. Give it open_pr, and the PR actually opens. That is the difference between a chatbot and an agent.
What “a tool” is, mechanically
A tool is a named function the loop offers to the model:
name: run_shell_command
description: Run a shell command in the sandbox and return its stdout, stderr, and exit code.
inputs:
command: string
outputs:
stdout: string
stderr: string
exit_code: number
Every model call includes the list of available tools. When the model decides to use one, it emits a structured request naming the tool and its arguments. Your loop’s code sees that request, executes the real function, and feeds the result back into the next model turn. The model does not run anything directly; your code does.
That last sentence is important. Every “action” the loop takes goes through your code, which means every action is a place you can log, rate-limit, permission-check, or refuse.
Common tool categories
Almost every loop reaches for some subset of:
- Filesystem. Read a file, write a file, list a directory, delete a file. The default for anything code- or document-shaped.
- Shell. Run a command, capture output. The most powerful and most dangerous — a shell tool is a superset of most others.
- Search. Web search, semantic search over your docs, code search. How the loop finds things it doesn’t know.
- HTTP. GET, POST, and friends. How the loop talks to APIs that don’t have a first-class client.
- Specific-service tools. A GitHub tool, a Slack tool, a Google Sheets tool, a database tool. Purpose-built with the right verbs already scoped.
- Human-in-the-loop. “Ask the human to confirm before proceeding.” The most underused tool.
Tool schemas matter more than descriptions
Two ways to define an update_ticket tool. Both work; one is much safer.
Loose:
name: update_ticket
inputs: { ticket_id: string, changes: object }
The model can pass anything in changes, including fields you didn’t intend it to touch — assignee, status, deleted_at, whatever.
Strict:
name: update_ticket_tag
inputs: {
ticket_id: string,
tag: enum["payment_failure", "kyc", "login", "other"]
}
The model can only set the tag, and only to one of four values. The blast radius is bounded by the schema.
The strict version is more work to write and more work to extend. It’s worth it. A strict schema is a permission system encoded in types.
What each tool call should return
Three things:
- The result. What the caller asked for.
- A status. Success? Partial success? Retry-able failure? Permanent failure?
- A trace. A short human-readable record (“ran
pytest, exit 0, 240 tests passed in 12.3s”) that shows up in your log.
The trace is what makes debugging a loop with 200 tool calls per cycle possible. Without it, you have 200 opaque calls; with it, you have a narrative.
Human-in-the-loop as a tool
The most valuable tool in a serious loop is the one that says “wait for a human before doing the next thing.” It looks like:
name: request_human_approval
description: Pause the loop, post the pending action to a Slack channel, and wait for a human to approve or reject before returning.
inputs: {
action_summary: string,
action_details: object,
timeout_minutes: number
}
outputs: {
approved: boolean,
approver: string,
notes: string
}
Chapter 6 covers where to put human gates in detail. For now: giving the model this tool, and telling it “for any action that would spend money, send an external message, or modify production data, request approval first,” is the single biggest jump in trustworthiness a loop can make.
Permissions belong to your code, not to the model
The most common mistake in tool design is thinking of the tool description as “the permission.” It isn’t. The description is a suggestion to the model; the enforcement is in your code.
If your run_shell_command tool’s description says “do not use rm -rf,” and the model has never seen your description, or has decided this specific case is an exception, or has been prompt-injected — the model will use rm -rf. What stops it is not the description. What stops it is your code checking the command against a denylist before executing it.
Description = intent. Code = enforcement. Never confuse them.
Rate limits and cost caps
Every tool with real-world side effects (send email, create ticket, hit an external API) should have:
- A per-cycle cap — this cycle cannot send more than N emails.
- A daily cap — this loop cannot send more than N emails per day.
- A cost meter — this loop’s tool usage this month is $X; alert at $Y; refuse at $Z.
Any of these limits hitting should be a loud event, not a silent no-op. If the loop was about to send 500 duplicate emails and hit the cap at 10, you want to know before the next scheduled run happens.
Check your understanding
Quick check
—