Build an AI Agent, Stage 5: Give It Tools, and Prove What Each One Does
An agent's tools are where its decisions become changes in someone else's system, so each one needs a contract and a test of what it really does. The name and the documentation describe the intended effect; only a probe of the systems before and after the call shows the actual one.
The example in this series is a composite scenario, built from patterns common to requirements intake in regulated organisations. It is not a single real organisation, and its figures are illustrative. The code is real: every output block below was produced by running it.
The Requirements Agent's tools were wired up in two days. The integration engineer worked from the Stage 3 blueprint: three read tools, three draft tools, six permission scopes on the agent's own identity. Each tool had a schema and a description. The one that mattered most, propose_relation, recorded that a change request looked like a duplicate of an existing backlog item, so that an analyst could confirm or reject it. The backlog tool's API had a call that seemed made for it: link two items, with a link type of duplicate, overlap or relates-to.
The contract review ran each tool once against a sandbox copy of the backlog and compared every system before and after the call. The duplicate link had created the link, as expected. It had also closed the backlog item on the other end.
Nobody had written that behaviour into the agent. It came from a workflow rule configured in the backlog tool years earlier by the delivery office: when an item is linked as a duplicate, close it. For people, the rule was a convenience. For an agent whose blueprint said it may only propose relations, with an analyst confirming each one, it turned a draft into an irreversible change made at machine speed.
This is Stage 5 of Build an AI Agent From Scratch. Stage 4 tagged five prohibitions as "enforced by permission" and one hand-over case, H3, that only works if a failed search looks like a failure. This stage gives the agent its tools and proves both.
Why a tool's name and schema feel like enough
A tool definition looks complete. It has a name that says what it does, a description the model reads, an input schema that rejects malformed arguments and, increasingly, hints such as "read-only" or "destructive". The integration engineer copies the vendor's documentation into the description and moves on.
Every one of those is a claim about the tool, made by whoever wrote it. The Model Context Protocol, which many agents now use to reach their tools, is explicit about the hints: clients "MUST consider tool annotations to be untrusted unless they come from trusted servers" (MCP, 2025). The same caution applies to names and descriptions inside your own estate. The person who named the backlog API's link call didn't know about the workflow rule, and the person who configured the rule never read the API.
The test: for each tool your agent holds, write down where its stated side effect comes from: its name, its description, the vendor's documentation, or an observation of what it changed. Anything without an observation is a claim.
A tool has a declared effect and an actual one
The tool contracts article sets out the contract each tool needs: purpose, a side-effect class, one permission scope, validated inputs, typed errors, limits and an audit record, enforced by a gateway that the model can't bypass. Stage 5 applies it to the Requirements Agent's six tools, all taken from the blueprint.
Tool | Side-effect class | Scope | What it may change |
search_requirements | read | req.search | Nothing |
get_requirement | read | req.read | Nothing |
search_backlog | read | backlog.read | Nothing |
propose_relation | draft | backlog.proposal.create | Its own proposals area only |
save_question_draft | draft | triage.draft.create | Its own drafts area only |
hand_over | draft | triage.handover.create | Its own hand-over queue only |
The last column is the one that turns a contract into a test. A read tool may change nothing. A draft tool may add to its own area and nothing else. Anything else that changes is an undeclared side effect, and the only way to find one is to look.
The probe does exactly that. For each tool it builds a fresh copy of every system the agent can reach, snapshots them, makes one valid call through the gateway, snapshots again and lists every area that changed. It doesn't read the tool's code or its description; it reads the systems.

The test: pick one draft-class tool and call it once in a sandbox. Then look at every record in the systems it can reach, not just the record it was meant to create, and compare with a copy taken beforehand.
Two failures the code review didn't show
The probe runs four checks: each contract matches the blueprint, each tool's observed changes match its class, a forced search failure comes back as an error, and each prohibited action is refused. On the first wiring:
$ python tool_probe.py requirements-agent.blueprint.json v1
Requirements Agent tools (v1)
FAIL propose_relation declared draft, but changed backlog, links: BL-4410 status open -> closed
FAIL search_requirements a timeout returned success with 0 results
checked: 6 contracts, 6 side-effect probes, 1 failure probe, 5 prohibitions at 2 layers
verdict: FAIL (2 fail)
The first line is the duplicate link. The second is the root cause of the incident that opened Stage 4, where a search timed out and the agent's note said "no related requirements found". Stage 4 fixed the instructions: the prompt now tells the agent to hand over to the senior analyst on duty when a search fails. But the search adapter caught the timeout and returned an empty, successful result, its author's idea of a safe default. A prompt rule about failed searches can't fire when the tool reports that the search succeeded. The instruction was right and the agent still couldn't follow it.
Both failures have the same shape: the tool's actual behaviour differed from its declared behaviour, and nothing that reads code or descriptions would show it. The workflow rule lived in the backlog tool's configuration. The swallowed timeout lived in a try block whose comment said "safe default".
The discovery move that finds the first kind is a question to ask the owner of every system a draft tool touches: what else happens in your system when this call succeeds? Workflow rules, triggers, notifications, automations and webhooks are configured by the people who run the system, not the people who build agents, and they rarely appear in an API reference. The backlog's owner knew about the duplicate rule immediately; nobody had asked.
The fix for each was small. propose_relation now creates a proposal object in the backlog's proposals area, shown as "proposed by agent" until an analyst confirms it, and never calls the link endpoint; confirmation, which does link and close, stays a human action. The search adapter now lets the timeout through, and the gateway turns it into an error the agent can act on: code unavailable, retryable, with a next step. The MCP specification draws the same line, reporting API failures as tool execution errors flagged isError, not as content (MCP, 2025).
$ python tool_probe.py requirements-agent.blueprint.json v2
Requirements Agent tools (v2)
checked: 6 contracts, 6 side-effect probes, 1 failure probe, 5 prohibitions at 2 layers
verdict: PASS (0 fail)
The test: for each read tool, find the code path that runs when the underlying call fails or times out. If any path returns an empty list, a null, or "not found", you have a tool that reports failure as an answer.
Every prohibited call, refused twice
Stage 4's boundaries section said, for P1 to P5, "enforced by permission". This is where that claim is proved, and the proof has two layers.
The first is the gateway: there is no tool for merging records, changing priority, editing a requirement, contacting a requester or accepting a request. A model that asks for one gets not_permitted, because the gateway denies by default. The second is the system of record: if anything calls the backlog, the repository or the mail service directly with the agent's identity, the system refuses it, because the identity was never granted backlog.merge, backlog.write, req.write, mail.send or request.accept. OWASP's guidance on excessive agency asks for exactly this, authorisation in the downstream systems rather than relying on the model to decide whether an action is allowed (OWASP, 2025).
Two layers matter because each fails differently. A gateway can gain a tool by accident, when someone registers a convenience wrapper during a busy sprint. A system grant can widen by accident, when an administrator copies a role from a human user. The chance that both drift in the same way at the same time is much smaller than either alone.
The Stage 3 blueprint had already named a test for each prohibition: test_merge_and_edit_refused_by_backlog_api, test_field_change_refused, test_requirement_write_refused, test_no_mail_scope and test_accept_refused. Until now those names pointed at nothing. Stage 5 writes them, each asserting both refusals, alongside a test that a search timeout comes back as a retryable error and a test that a proposal changes nothing outside its own area. With nine new tests, the series' suite has 37, and all pass.
The test: for one action your agent must never take, try it twice in a sandbox: once by asking the agent's gateway for the tool, and once by calling the system directly with the agent's credential. If either succeeds, the prohibition rests on the other layer alone.
The contract review: a method
The review takes a working session per agent with the integration engineer, the owner of each system the tools touch, and whoever will test it.
List every tool against the blueprint. Name, side-effect class, one scope, and the area it may change. Done when every tool in the blueprint has a contract and there are no tools that aren't in the blueprint.
Ask each system owner what else happens on success. Workflow rules, triggers, notifications, webhooks, automations. Done when each system owner has answered for each draft or write tool, in writing.
Probe each tool in a sandbox. Snapshot every reachable system, call once, diff. Done when every read tool changes nothing and every draft tool changes only its own area.
Force each failure. Time out each dependency and confirm the tool returns a typed, retryable error, never an empty success. Done when every read tool has a failure test.
Try every prohibited action at both layers. Done when each prohibition has a test that asserts the gateway refusal and the system refusal, and those tests run in CI.
Re-run steps 2 and 3 whenever a system the agent touches changes its configuration, not only when the agent changes. The duplicate rule was added years before the agent existed; the next one could be added next quarter.
Tradeoffs and exceptions
Approach | Gains | Costs | Best suited for | Warning signs |
Trust names, descriptions and hints | Fastest | Undeclared side effects reach production | Read tools over data you own | A draft tool whose effects nobody has observed |
Contract and gateway only | Typed errors, scopes, audit | Doesn't see what a downstream system does | Tools over systems with no automation | Workflow rules nobody has listed |
Contract, gateway and side-effect probe | Observed behaviour, not claimed | A sandbox per system; probe maintenance | Any draft or write tool | Probes that skip a system the tool reaches |
Proposal objects instead of direct changes | Human confirms every change | Analyst time; a proposals area to build | Actions whose downstream effects are hard to bound | Proposals confirmed in bulk without reading |
A sandbox isn't always available. Some vendors offer none, and some systems can't be copied. In that case, use a dedicated test project or tenant within the production system, with its own data and its own automation rules copied from production, and probe there. If even that is impossible, treat every draft tool against that system as write-class until its owner has listed what else happens on success, and keep a person in front of it.
The probe also has a blind spot: it only sees systems you snapshot. A webhook that sends data to a third party, or a notification email, won't show up as a changed record. Ask the system owner, and include outbound channels such as the mail log and webhook log in the snapshot where you can.
How Stage 5 goes wrong
Convenience wrappers. Someone adds a close_duplicate helper "for analysts" to the same gateway. The probe catches it only if the blueprint check runs too: no tool should exist that the blueprint doesn't list.
Copied roles. An administrator gives the agent's identity the same role as a business analyst so it can "see everything". The system-layer refusal tests fail at once, which is why they run in CI.
Retries that repeat side effects. A draft tool that times out and is retried creates two proposals. The gateway's idempotency key prevents it; a probe that calls each tool twice with the same key proves it.
Configuration drift. A new workflow rule in the backlog tool turns a safe draft into a change. Re-probe on the system's change calendar, not only the agent's.
What to do next
For your agent's draft and write tools, ask each system owner this week what else happens when the call succeeds, and run one sandbox probe per tool. Then force a timeout on each read tool and check what comes back. Stage 6 packages the agent's two capabilities, finding related requirements and drafting clarifying questions, as skills with their own test packs, built on the tools this stage proved.
Final takeaway
A tool's name, description and hints are claims made by whoever wrote them. Put each tool behind a contract, observe what it actually changes, make every failure look like a failure, and prove each prohibited action is refused at the gateway and at the system. Only then do the "enforced by permission" lines in the agent's instructions mean anything.
Sources
Model Context Protocol, Specification 2025-06-18: Tools. Tool annotations untrusted unless from trusted servers; tool execution errors reported with isError.
OWASP GenAI Security Project, LLM06:2025 Excessive Agency, 2025. Authorisation in downstream systems rather than the model.
Series navigation. Previous: Stage 4: Write the Instructions the Blueprint Can Test. Next: Stage 6: Package Its Skills So Each One Is Tested Alone. The full stage list is on the Build an AI Agent From Scratch hub.
Frequently Asked Questions
How do you design tools for an AI agent?
Give each tool a contract: what it is for, its side-effect class (read, draft, write or irreversible), the single permission scope it needs, the inputs it accepts and the errors it can return. Put every tool behind a gateway that enforces the contract, and test what each tool actually changes, not only what it is declared to change.
How do you stop an AI agent calling a tool it shouldn't?
Don't give it the tool, and don't give its identity the permission. A prohibited action should be refused twice: there is no tool for it at the agent's gateway, and the system of record refuses the agent's identity if the call is made directly.
What should an AI agent's tool return when it fails?
An error the agent can act on: a stable code, whether retrying can help, and what to do next. A failure must never come back as an empty success, such as "no results", because the agent will treat it as an answer.
Part of the Agent Engineering curriculum: eight paths from prompts and context to testing, evaluation and governance.


