AI Engineering No. 266

Tool-Use Agents skill for Claude Code

Tools the model can pick right, loops that stop. Claude designs agent tools the way the model needs them: few high-level tools with descriptions that say when to use them, strict schemas, size-capped results, and errors phrased as next steps. The agent loop returns all parallel results in one message, enforces turn, token and time budgets, detects repeated calls, and gates refunds, emails and deletes behind explicit approval.

  • nametool-use-agents
  • size209 lines · ≈2,300 tokens
  • files1
  • licenseMIT
  • updated

What Claude sees before loading it

This description is the only part of the skill that sits in context all the time. Claude reads it to decide when to load the full playbook below.

description: Designs tools and agent loops for LLMs - tool names, JSON schemas, descriptions the model can act on, actionable error results, parallel tool calls, the request-execute-respond loop, budgets, stopping conditions, and human approval for risky actions. Use when giving an LLM function calling or tools, building an agent loop with the Messages API, when an agent picks the wrong tool, loops forever, ignores errors, or burns tokens, or when reviewing tool definitions.

Try asking

  • Give our support assistant tools to look up orders and issue refunds safely
  • The agent keeps calling search over and over and never finishes. Fix the loop
  • Review these tool definitions and tell me why the model picks the wrong one

Use it when

  • You are adding function calling or tools to an LLM feature
  • An agent chooses wrong tools, loops, or ignores tool errors
  • Agent runs are too slow or cost too much per task
  • Side-effecting tools need approval and permission checks

Skip it when

How to check the result

Scripted scenarios with mocked tools, including tool errors and an injected instruction, reach the right end state within the turn budget, and transcripts show no repeated or invented-ID calls.

The common mistake it prevents

Returning tool failures as raised exceptions or empty strings, so the model never learns what went wrong and retries the same call until the budget runs out.

~/.claude/skills/tool-use-agents/SKILL.md

--- name: tool-use-agents description: Designs tools and agent loops for LLMs - tool names, JSON schemas, descriptions the model can act on, actionable error results, parallel… license: MIT ---

An agent is well built when each tool does one clear job with a description that says when to use it, errors come back as instructions the model can act on, the loop has hard budgets and a defined finish condition, and destructive actions need approval. Most “the model is dumb” agent bugs are tool-design bugs.

Ground rules

  • Start with a workflow (code-controlled steps). Use an open agent loop only when the steps genuinely cannot be known in advance.
  • Fewer, higher-level tools beat many thin ones. search_orders(customer_email, status) beats list_orders + get_order + filter_orders.
  • Tool results are untrusted input to the model. Treat fetched web pages, emails, and documents as data (see prompt-injection-defense).
  • Every loop has a max iteration count, a token or cost budget, and a wall-clock timeout.
  • Tools that write, spend money, send messages, or delete require explicit user confirmation unless the user pre-approved that exact scope.

Workflow

- [ ] 1. List the jobs the agent must do and the tools each needs
- [ ] 2. Write tool definitions (name, description, strict schema)
- [ ] 3. Implement handlers with actionable errors and size-capped results
- [ ] 4. Write the loop with budgets and stop conditions
- [ ] 5. Add approval gates for side-effecting tools
- [ ] 6. Test with scripted scenarios and read the transcripts

2. Tool definitions

tools = [{
    "name": "search_orders",
    "description": (
        "Search a customer's orders. Use this before any action on an order, to find the "
        "order_id. Returns at most 20 orders, newest first, with id, date, status, total. "
        "Does not return line items; call get_order for those."
    ),
    "strict": True,
    "input_schema": {
        "type": "object",
        "properties": {
            "customer_email": {"type": "string", "description": "Exact email on the account"},
            "status": {"type": "string", "enum": ["open", "shipped", "delivered", "cancelled", "all"],
                       "description": "Filter by status; 'all' when the user did not specify"},
        },
        "required": ["customer_email", "status"],
        "additionalProperties": False,
    },
}]

Description checklist (3-6 sentences is normal; one line is too little):

  • What it does and what it returns (fields, limits, ordering).
  • When to use it, and when to use a different tool instead.
  • Input formats with examples (IDs, dates, units).
  • Side effects, cost, or latency if notable.

Naming: verb_noun, distinct prefixes per domain (crm_search_contacts, billing_create_refund) so similar tools are not confused. Avoid two tools whose descriptions overlap; merge them or state the boundary in both.

3. Handlers and results

DoDo not
Return concise, relevant fields (IDs, names, statuses)Dump raw API JSON with 200 fields
Cap size (e.g. 20 items, 2-5K tokens) and say “showing 20 of 340; refine with status”Return unbounded lists that flood context
Return errors as results with is_error: true and a fix: “customer_email not found; ask the user to confirm the address”Raise an exception that kills the loop, or return an empty string
Use human-readable IDs or names alongside opaque IDsReturn only UUIDs the model must juggle
Make write tools idempotent (accept an idempotency key)Let a retried call double-charge

4. The loop

Python, manual loop on the Anthropic Messages API:

import anthropic

client = anthropic.Anthropic()
MAX_TURNS, MAX_OUTPUT_TOKENS = 20, 200_000

def run_agent(task: str) -> str:
    messages = [{"role": "user", "content": task}]
    spent = 0
    for turn in range(MAX_TURNS):
        resp = client.messages.create(
            model="claude-opus-5-5", max_tokens=8000,
            system=SYSTEM_PROMPT, tools=tools, messages=messages,
        )
        spent += resp.usage.output_tokens
        messages.append({"role": "assistant", "content": resp.content})  # keep full content

        if resp.stop_reason == "end_turn":
            return "".join(b.text for b in resp.content if b.type == "text")
        if resp.stop_reason == "refusal":
            return REFUSAL_FALLBACK
        if resp.stop_reason == "max_tokens":
            messages.append({"role": "user", "content": "Your last reply was cut off. Continue concisely."})
            continue
        if resp.stop_reason == "pause_turn":   # server tools paused a long turn; resend to resume
            continue

        results = []
        for block in resp.content:
            if block.type != "tool_use":
                continue
            try:
                out = dispatch(block.name, block.input)     # validates, checks approval
                results.append({"type": "tool_result", "tool_use_id": block.id, "content": out})
            except ToolError as e:
                results.append({"type": "tool_result", "tool_use_id": block.id,
                                "content": str(e), "is_error": True})
        messages.append({"role": "user", "content": results})  # ALL results in ONE message

        if spent > MAX_OUTPUT_TOKENS:
            messages.append({"role": "user", "content": "Budget reached. Summarize progress and stop."})
    return "Stopped: turn limit reached. " + summarize(messages)

Loop rules:

  • Append the assistant’s full content (including tool_use and thinking blocks), not just its text.
  • Return every tool_result for a turn in a single user message, matched by tool_use_id. Splitting them across messages teaches the model to stop calling tools in parallel. Never drop a result; failed calls get is_error: true.
  • Execute independent parallel calls concurrently (asyncio.gather) with a concurrency cap.
  • The SDKs also ship a beta tool runner that drives this loop for you with hooks for approval and logging; use it when you do not need custom control flow.
  • Forced tool_choice (any / a named tool) is rejected by several current Claude models. Use auto, say in the prompt which tool to use, and verify a call was made.

Stopping conditions

ConditionAction
stop_reason == "end_turn"Done; validate the final answer against the task
A finish / submit_result tool is calledDone; its strict schema is the result contract
Turn limit, token budget, or wall-clock hitAsk for a summary of progress, return partial result, log
Same tool + same arguments 3 timesBreak the loop; likely stuck
Consecutive tool errors > 3Stop and escalate with the error trail

A submit_result tool with a strict schema makes completion explicit and machine-checkable; prefer it for autonomous jobs.

5. Approval gates

Classify each tool: read-only, reversible write, irreversible or external effect. In dispatch, require approval for the last class:

REQUIRES_APPROVAL = {"billing_create_refund", "email_send", "db_delete_record"}

def dispatch(name, args):
    if name in REQUIRES_APPROVAL and not approvals.granted(name, args):
        raise ToolError(f"{name} needs user approval. Ask the user to confirm: {summarize_args(args)}")
    return HANDLERS[name](**args)

Enforce permissions in code with the end user’s credentials and scopes. Never rely on the system prompt to stop the model from calling a tool it has.

System prompt for agents

State the goal, the definition of done, the tool strategy (“search before acting; prefer one broad search over many narrow ones”), what to do when stuck (“ask the user” vs. “make a reasonable assumption and note it”), and the budget. Keep it stable across turns so it caches.

6. Test

  • Write 10-30 scripted scenarios with mocked tool backends, including tool errors, empty results, ambiguous requests, and a prompt-injection string inside a tool result.
  • Assert on outcomes (final state, tool calls made, approvals requested), not exact text.
  • Read full transcripts for at least 10 runs. Look for redundant calls, wrong tool choice, and ignored errors; fix descriptions first.
  • Track per-task cost and turn count; regressions there are bugs.

Gotchas

  • Context grows every turn. Large tool results make later turns slow and expensive; summarize or truncate old results, and cache the stable prefix (tools + system).
  • Changing the tool list mid-conversation invalidates prompt caching for everything after the tools block; keep the tool set fixed per session.
  • Models call tools with plausible but invented IDs when a search tool is missing or vague. Provide a lookup tool and say “never guess IDs”.
  • Tool descriptions that say “always use this” get over-used. Scope with “use when”.
  • 30+ tools degrade selection. Group by domain, load only the relevant subset, or use the provider’s tool-search feature.

Output contract

**Agent:** <purpose>; tier justification <why not a workflow>
**Tools:** <name — read|write|irreversible — approval rule>
**Loop limits:** <turns>, <tokens/$>, <timeout>; finish via <end_turn | submit_result>
**Tests:** <n> scenarios incl. errors/injection; pass <x>/<n>; median turns <t>, cost <$>

About this skill

Written and maintained by the Claude Skills editors to the same editorial standard as every skill in the directory: a selective trigger, a real workflow, known pitfalls and a verification step. First published , last updated . MIT licensed.

esc