Skip to content
bits
Your first agent loop · Lesson 2 of 9

Tools, seen from the loop

Tutor:

You built the pieces of a tool call in Give your model tools. Here they are in thirty seconds, with the lesson to revisit if one feels shaky.

Describe it. Claude sees a tool's name, description and input_schema, never your code, and the description does most of the work. Revisit Describing a tool and Descriptions Claude follows.

Run it. A reply with stop_reason: "tool_use" holds tool_use blocks. You push Claude's whole reply back unchanged, then one user message with a tool_result per call, matched by tool_use_id. Revisit One round trip.

Fail politely. A failed call still gets a result, marked is_error: true, with a message that says what to try next. Revisit Many calls, some fail.

Tutor:

The weather bot made one round of calls and stopped. An agent doesn't stop: every result becomes the input to Claude's next plan, maybe dozens of times. That changes what your tool code has to get right.

First, your tool layer must never throw. In a loop, an exception doesn't just lose one answer. It ends the run, and Claude never learns what went wrong, so it can't retry or tell the user. Send every call through one function that always returns a tool_result. The loop in the next lesson imports a runTool just like it.

import type Anthropic from "@anthropic-ai/sdk";
type Handler = (input: any) => Promise<unknown>;
const handlers: Record<string, Handler> = {
get_forecast: async ({ latitude, longitude, days }) => {
if (days < 1 || days > 7) throw new Error("days must be 1 to 7. Day 1 is today.");
return fetchForecast(latitude, longitude, days);
},
};
export async function runTool(block: Anthropic.ToolUseBlock): Promise<Anthropic.ToolResultBlockParam> {
try {
const handler = handlers[block.name];
if (!handler) throw new Error(`There is no tool called "${block.name}".`);
const result = await handler(block.input);
return { type: "tool_result", tool_use_id: block.id, content: JSON.stringify(result) };
} catch (err) {
const message = err instanceof Error ? err.message : String(err);
return { type: "tool_result", tool_use_id: block.id, content: message, is_error: true };
}
}
declare function fetchForecast(lat: number, lon: number, days: number): Promise<unknown>;
TypeScriptUTF-8● lines 12–21: never throws
You: why check days in the handler if the schema could say so?
Tutor:

Because strict schemas can't enforce minimum or maximum (as of October 2026). Put the range in the description, so Claude rarely gets it wrong, and check it in code, so a wrong call comes back as an error Claude can fix next turn: “days must be 1 to 7”.

quick check+15 neurons
Tutor:

Your forecast API times out, and the handler's error escapes runTool instead of becoming a tool_result. What happens to the agent?

    Type your answer or tap an option · Enter checks

    Tutor:

    Second, a result is the next turn's input. Write each one to answer the question Claude asks itself next: what can I do now? That means returning the fields the next tool needs, in the names it takes. It means saying what was actually matched, so Claude can catch a wrong match before it builds three more steps on it. And it means staying small, because in a loop every result is re-sent on every later turn.

    Dead-end result vs Result the next turn can useillustrative: made-up numbers that show the idea, not output from a real modelcompare

    promptgeocode("Portland") returns…

    Dead-end result

    { "lat": 45.52, "lon": -122.68 }

    Result the next turn can use

    { "place": "Portland, Oregon, US", "latitude": 45.52, "longitude": -122.68, "also_found": "Portland, Maine, US" }

    promptWhat Claude does next

    Dead-end result

    Renames lat and lon to fit get_forecast, and fetches Oregon's weather for a user who lives in Maine.

    Result the next turn can use

    Passes latitude and longitude straight to get_forecast, and says it picked Oregon in case the user meant Maine.

    Tutor:

    Anthropic calls this designing the agent-computer interface, and suggests you poka-yoke your tools: shape them so mistakes are hard to make. Their coding agent kept fumbling relative file paths, so they made the tool require absolute paths, and the mistakes stopped. Here, get_forecast takes coordinates rather than a place name, so it can never guess which Portland you meant.

    quick check+15 neurons
    Tutor:

    Your agent keeps calling get_forecast with days: 14, but the tool only covers 7. What's the best first fix?

      Type your answer or tap an option · Enter checks

      Tutor:

      Your turn. Give Claude two tool descriptions that are meant to chain, a question, and ask whether it could get from one to the other. Anything it has to guess is a step your agent would guess at too.

      Your turn

      Run your prompt and the replies land here.

        • Describes the first tool, not yet
        • Describes the second tool, not yet
        • Says what a tool returns, not yet
        • Includes a question, not yet
        • Asks what's unclear, not yet
        • Ran at 1.0, not yet
        run at
        ~0 tokens

        Turn on JavaScript to run prompts.