Agent Mentor Learn
Agent Tool Calling: Getting Agents to Actually Do Things · Lesson 6 of 6

Lesson 6: Hands-On: Wiring Three Tools onto an Agent

Learning goals:

  • Write a complete tool execution loop that actually gets an agent running
  • Register a tool's interface definition and implementation in one table, so the two sides never drift apart
  • Fit the loop with safety valves, and read the logs to tell when a tool is misconnected

Prerequisites: Finish Lessons 1-5, and be able to read basic JavaScript / Node.js | Previous: Lesson 5 <<

The Payoff First: One Complete Run

This is what this lesson builds up to. You type one sentence into the terminal, and the agent decides on its own which tools to call and how many times:

$ node agent.js "Are we using lodash in this project? Look into how it's doing on GitHub"
[turn 1] calls search_files { pattern: 'lodash', dir: '.' }[turn 2] calls read_file { path: 'package.json' }[turn 3] calls github_repo_info { owner: 'lodash', repo: 'lodash' }
Final answer:Yes, the project uses lodash. package.json pins it at ^4.17.21, andsrc/utils/format.js requires it directly. On GitHub, lodash/lodashcurrently has over 60k stars, and its last push was a few weeks ago—therepo is still maintained. To confirm whether ^4.17.21 is the latestrelease, you'd need one more query against its release list.

Three turns, three tools, and every turn's arguments build on the previous turn's result: first find which files lodash appears in, then read package.json to confirm the version, then take that name and ask GitHub about it. This isn't a hardcoded script—the model itself decides which tool to call next and what arguments to pass.

This lesson builds it from scratch: three tools, one registry, one execution loop, a few safety valves.

What's Happening Underneath: One API Round-Trip After Another

Every "turn" you saw above is a full HTTP request underneath. Lesson 2, "The Full Round-Trip of a Tool Call," showed what a single tool call's round-trip looks like; here we just wire it into a loop—the model returns stop_reason: "tool_use", your code runs the tool, stitches the result back into the conversation, and sends another request, until the model stops asking for tool calls.1

Three tool-call turns are really four calls to messages.create: on the first three the model keeps asking for tools, and on the fourth it has GitHub's data, decides it has enough, and gives a text answer directly, ending the loop. The judgment of whether to keep asking for tools lives entirely on the model's side; your code only executes and sends results back.

Step 1: Write the Contract for Each Tool

Lesson 4, "Designing Tool Interfaces: Name, Description, Parameters, Return Value," covered the three core fields of a tool interface: name, description, and input_schema.2 Here we turn them straight into code. The three tools map onto three of the five tool types from Lesson 3, "Five Common Tool Types: Read, Write, Execute, Search, Call": search, read, and call—write and execute are left for you to wire up in the exercises.

github_repo_info carries a github_ prefix—the official guidance is to namespace tool names with the service when a tool touches an external service, which sharply lowers the chance the model picks the wrong tool.3 search_files and read_file operate on the local filesystem, where there's no "which service" ambiguity, so they need no prefix.

All three descriptions spell out what text comes back when nothing is found, and that's not filler. Lesson 4 made the point that a good description removes ambiguity in inputs and outputs;4 the ambiguity here isn't in the parameters but in how the tool expresses "I didn't find anything"—a trap that goes off in the "Safety Valves" section.

Step 2: Register the Contract and the Implementation in One Table

A common trap: if the schema list and the handler lookup table used at execution time are written as two separate copies, they'll drift apart sooner or later. You rename search_files to find_in_files but forget to update the key in the handler table; the model issues a call against the new schema, the handler table has nothing under that key, and it throws.

The fix is to maintain a single table where name, description, input_schema, and the function that actually runs all sit in the same object. The schema list the API needs and the handler lookup table execution needs are both derived from this one table:

toolSchemas and toolHandlers stay in sync forever, because they're two views computed from the same data, not two hand-written copies. Renaming a tool or adding a parameter means changing TOOLS in exactly one place.

Step 3: Implement the Three Tools, With Boundaries

searchFiles walks the directory itself rather than shelling out to grep—that avoids splicing user input into a command line and inviting command injection. The hit count is capped, so a single search can't stuff thousands of lines into the context:

readFile does one thing: confirm the target path hasn't escaped the project root. The boundary idea from Lesson 5 shows up here as a single prefix check with a separator. Note it's not a bare startsWith(PROJECT_ROOT): say the project root is /Users/me/proj and the model passes in ../proj-backup/x; after resolve you get /Users/me/proj-backup/x, and a bare prefix match would still pass—append path.sep, and the boundary finally lands on the directory separator:

githubRepoInfo is the only tool that sends data outside the project—local file content, distilled by the model into the two strings owner and repo, then sent to the public internet. This is exactly the scenario where two high-risk conditions meet, "read private data" plus "communicate outward,"5 so it gets an explicit permission rule: the arguments must match GitHub's valid naming format, nothing else:

GITHUB_TOKEN is read from an environment variable, never appearing in the code; it runs without one too, just with lower rate limits on anonymous requests. This is the same idea as Lesson 5's permission rules in a different form: that lesson covered the declarative allow/deny/ask rules in Claude Code's config file,6 and this is the imperative version written into the tool code—both draw a line that a high-risk operation cannot cross.7

Step 4: Write the Execution Loop

With toolSchemas and toolHandlers in hand, the loop itself isn't complicated. The core logic is four steps: send the request, look at stop_reason, return text if it isn't tool_use, and if it is, run every tool-call block and stitch the results back in.1

There's an easy-to-miss detail here: for (const block of response.content) iterates over all the content blocks returned this turn, not just the first. The model often requests two or three tools in parallel in one turn; each one has to be executed and produce its own tool_result, with tool_use_id matched one-to-one, and not one can be missing.8 The Level 2 exercise will walk you through the trap of missing one firsthand.

Safety Valves, and How to Tell When a Tool Is Misconnected

The loop above runs, but it's missing two safeguards. Add them:

Safeguard one: a tool failure has to be fed back, not allowed to crash the loop. Wrap the raw call in a try/catch, and on failure still produce a tool_result, just marked with is_error: true—when the model sees that mark, it usually adjusts the arguments and retries, rather than repeating the same error.9 8

Safeguard two: the same tool with the same arguments, called three times in a row, should stop. This isn't guesswork—it's based on recording the signatures of the last few calls:

Together with MAX_TURNS as the master switch, the three safety valves have distinct jobs: MAX_TURNS guards against "the model keeps asking for tools in new variations and never stops"; the repeat-call detection guards against "the model gets stuck spinning on the same arguments"; and the tools' internal path and format checks (the ones written in Step 3) guard against "the model made up an out-of-bounds argument and the tool dutifully ran it anyway." Drop any one of the three layers and the loop risks running away or overstepping.7

How do you tell from the logs that a tool is misconnected? Two of the most common signals:

  • The model calls the same tool over and over, with arguments varying only within a narrow range (case changes, adding or dropping a word). Nine times out of ten the model isn't dumb—the tool_result content is too vague. "Not found" returns an empty string, the model can't tell "genuinely nothing there" from "the tool is broken," and can only guess and try again.
  • The model fills in arguments by guessing, for instance passing read_file a path that doesn't exist. Tracing back usually turns up one of two causes: the description didn't spell out where the argument should come from (echoing Lesson 4), or the previous tool's output didn't give a precise path, leaving the model to invent one.

Recap

  • Register a tool's schema and handler in the same table (TOOLS), with toolSchemas and toolHandlers both derived from it, so changing one place never leaves the other unchanged
  • The core of the execution loop is: send the request → check whether stop_reason is tool_use → if so, iterate over every tool-call block, execute, and stitch back the tool_result → if not, return text and end the loop
  • One turn may have multiple parallel tool calls; every tool_use needs a uniquely matching tool_result, and missing one errors out the next request
  • The three safety valves each guard a layer: MAX_TURNS stops the model from asking for tools indefinitely, repeat-call detection stops the model from spinning on the same argument set, and the tools' internal path and format checks stop out-of-bounds arguments
  • The tool_result content has to state "not found" versus "an error occurred" clearly; a vague empty return is the number-one cause of the model retrying over and over and the logs looking like a tool is misconnected

You've now finished all six lessons of this course, from "why agents need tools" to writing a working tool execution loop yourself. The most worthwhile thing to do next isn't reading another lesson—it's picking a small, real task from your own project, breaking it into two or three tools, and carrying this loop skeleton over with a few tweaks. Getting it running once beats reading ten more explanations. When debugging and unsure about a specific field, go back to sources.md and check S4 and S5, the two official docs; those are the most primary spec text for this multi-turn loop.

Footnotes

  1. How tool use works — Claude API — https://platform.claude.com/docs/en/agents-and-tools/tool-use/how-tool-use-works 2

  2. Define tools — Claude API — https://platform.claude.com/docs/en/agents-and-tools/tool-use/define-tools

  3. How to implement tool use - Claude Platform Docs — https://platform.claude.com/docs/en/agents-and-tools/tool-use/implement-tool-use

  4. Writing effective tools for AI agents—using AI agents | Anthropic Engineering — https://www.anthropic.com/engineering/writing-tools-for-agents

  5. The lethal trifecta for AI agents - Simon Willison's Weblog — https://simonwillison.net/2025/Jun/16/the-lethal-trifecta/

  6. Configure permissions - Claude Code Docs — https://code.claude.com/docs/en/permissions

  7. LLM06:2025 Excessive Agency - OWASP Gen AI Security Project — https://owasp.org/www-project-top-10-for-large-language-model-applications/2_0_vulns/LLM06_ExcessiveAgency.html 2

  8. Handle tool calls — Claude API — https://platform.claude.com/docs/en/agents-and-tools/tool-use/handle-tool-calls 2

  9. Tools - Model Context Protocol — https://modelcontextprotocol.io/docs/concepts/tools

Exercises

01

Copy this lesson's code into an empty local directory, run npm install @anthropic-ai/sdk, then npm pkg set type=module (all the code in this lesson uses ESM import syntax; on Node versions below 22.7, skipping this step throws "Cannot use import statement outside a module" outright), set up your ANTHROPIC_API_KEY (GITHUB_TOKEN optional), and run node agent.js "Are we using lodash in this project? Look into how it's doing on GitHub" once. Confirm you see at least two different tool-call turns and a final text answer.

Level 1: Get It Running, Then Wire Up a Fourth Tool

Once it runs, wire up a fourth tool, write_report(path, content): write the check results into a Markdown file, allowed only under the project's reports/ directory, and reject writes anywhere else. Change one prompt, for example "write the check results you just gathered into reports/lodash-check.md," and confirm the model calls this new tool on its own.

Done criteria · checked locally
02

The loop code below has a bug. First explain under what condition it causes the next API request to error out, then give the fixed code.

Level 2: Manufacture a Failure, Then Fix It
Done criteria · checked locally