Agentic AI

The Reasoning Core

Tool Design

Most teams tune the system prompt for days and write tool descriptions in a hurry. The leverage runs the other way.

JrCodex·8 min read

Jr Codex Agentic AI Notes

Level: Intermediate Prerequisites: Chapter 2: Prompting Patterns for Agents Time to complete: ~25 minutes


Table of Contents

  1. Tools Matter More Than Prompts
  2. The Description Is the Interface
  3. Granularity
  4. Designing the Return Value
  5. Errors That Teach
  6. Safety Properties
  7. A Tool Design Checklist
  8. Summary & Next Steps

1. Tools Matter More Than Prompts

Most teams tune the system prompt for days and write tool descriptions in a hurry. The leverage runs the other way.

Why
─────────────────────────────────────────
  The system prompt says how to decide IN GENERAL.
  The tool descriptions are the ONLY information the
  model has about what its options actually are.

  A model choosing between `search` and `lookup`,
  both described as "finds information", is guessing.
  No amount of policy fixes that — the options
  themselves are indistinguishable.
─────────────────────────────────────────
The Reframe
─────────────────────────────────────────
  You are writing an API for a competent new
  colleague who will never ask you a question,
  cannot read your source, and must choose correctly
  on the first attempt from the docs alone.

  Everything in this chapter follows from that.
─────────────────────────────────────────

2. The Description Is the Interface

# WEAK — the model cannot tell these apart, or know when either applies.
{"name": "search", "description": "Search for information"}
{"name": "lookup", "description": "Look up data"}
# STRONG — each says what it does, when to use it, when NOT to, and what it costs.
{
  "type": "function",
  "function": {
    "name": "search_knowledge_base",
    "description": (
        "Full-text search over internal support articles and runbooks. "
        "USE FOR: how-to questions, policy questions, troubleshooting steps. "
        "DO NOT USE FOR: customer-specific data — use get_customer instead. "
        "Returns up to 5 excerpts with article IDs. "
        "Cost: low (~200ms). Read-only."
    ),
    "parameters": {
      "type": "object",
      "properties": {
        "query": {"type": "string",
                  "description": "Natural-language question. Full sentences work "
                                 "better than keywords."},
        "max_results": {"type": "integer", "minimum": 1, "maximum": 10, "default": 5},
      },
      "required": ["query"],
    },
  },
}
The Five Elements of a Good Description
─────────────────────────────────────────
  1. WHAT it does, concretely
  2. USE FOR — the cases it is right for
  3. DO NOT USE FOR — and what to use instead
     (this line prevents more errors than any other)
  4. WHAT IT RETURNS, in shape and volume
  5. COST and whether it MUTATES anything
─────────────────────────────────────────

Element 3 is the one almost always missing. Naming the sibling tool by name converts a wrong choice into a right one, because the model is told where to go instead.


3. Granularity

How much should one tool do? Both extremes fail, in opposite ways.

TOO FINE
─────────────────────────────────────────
  open_connection, run_query, fetch_row,
  close_connection

  The model must orchestrate plumbing. Four steps
  become four chances to err, four round trips, and
  four tool results in the context.
TOO COARSE
─────────────────────────────────────────
  do_customer_operation(action, params, options)

  Now the DESCRIPTION must explain every mode, the
  schema cannot validate meaningfully, and the model
  guesses at `params`.
THE RIGHT SIZE
─────────────────────────────────────────
  One tool = one thing a USER would ask for.

  get_customer(customer_id)
  list_orders(customer_id, since)
  issue_refund(order_id, amount, reason)

  Each is a complete, meaningful unit of work with a
  schema that fully describes it.
─────────────────────────────────────────
The Test
─────────────────────────────────────────
  "Would a human colleague describe this as one
   task?"

  "Look up the customer" — yes, one tool.
  "Open a database connection" — no, that is
  implementation the agent should never see.
─────────────────────────────────────────

4. Designing the Return Value

Chapter 1 showed tool results consume ~80% of an agent's context. The return value is therefore a design surface, not an afterthought.

def list_orders(customer_id: str, since: str | None = None, limit: int = 20):
    rows = db.query(...)
 
    return {
        "orders": [                                   # only the fields an agent needs
            {"id": r.id, "date": str(r.date), "total": r.total, "status": r.status}
            for r in rows[:limit]
        ],
        "returned": min(len(rows), limit),
        "total_matching": len(rows),                  # so the model knows if it saw ALL
        "truncated": len(rows) > limit,               # EXPLICIT, not inferred
        "next": {"since": str(rows[limit].date)} if len(rows) > limit else None,
    }                                                 # tells it HOW to get the rest
Four Rules for Return Values
─────────────────────────────────────────
  RETURN A SUBSET, NOT THE RECORD. A database row
  has 40 columns; the agent needs 4. Every extra
  field is context spent for nothing.

  SIGNAL TRUNCATION EXPLICITLY. An agent that does
  not know it saw a partial list will confidently
  reason as if it saw everything.

  INCLUDE THE CONTINUATION. `next` turns "I only saw
  20" into an action the model can take.

  PREFER STABLE IDs OVER PROSE. IDs can be passed to
  the next tool; prose cannot.
─────────────────────────────────────────

5. Errors That Teach

Module 1, Chapter 4 established that errors become observations. Their content determines whether the agent recovers.

# USELESS — the agent has nothing to act on and will likely retry identically.
return {"error": "Invalid request"}
 
# USEFUL — states what was wrong, what is valid, and what to do next.
return {
    "error": "invalid_date_format",
    "message": "The `since` argument must be YYYY-MM-DD. Received '03/14/2026'.",
    "hint": "Reformat to '2026-03-14' and call again.",
}
 
# BEST — when you can, answer the question the agent was really asking.
return {
    "error": "customer_not_found",
    "message": "No customer with id 'CUS-4821'.",
    "hint": "Ids are case-sensitive and start with 'cust_'. "
            "Did you mean 'cust_4821'? Use search_customers to find one by name.",
    "suggestions": ["cust_4821"],                # a NEXT ACTION, not just a complaint
}
The Principle
─────────────────────────────────────────
  An error message is a PROMPT. It is text you are
  injecting into the model's context at the exact
  moment it must decide what to do next.

  Write it as instruction to a colleague, not as a
  log line for you.

  Errors that name the fix convert most failures
  into single-retry recoveries. Errors that say
  "invalid request" produce loops.
─────────────────────────────────────────

6. Safety Properties

Three properties of the tool itself, independent of any prompt.

IDEMPOTENCY
─────────────────────────────────────────
  Agents retry. Networks time out ambiguously. A
  non-idempotent tool WILL eventually double-charge
  a customer.

  Fix: accept a caller-supplied idempotency key and
  deduplicate on it.
def issue_refund(order_id: str, amount: float, reason: str, idempotency_key: str):
    if prior := refunds.find(idempotency_key):
        return {**prior, "note": "already processed; returning the original result"}
    ...
LEAST PRIVILEGE
─────────────────────────────────────────
  Give the agent the narrowest tool that does the
  job.

  ✗ run_sql(query)          — unbounded read AND write
  ✓ get_customer(id)        — one row, read-only
  ✓ list_orders(customer_id) — scoped, read-only

  `run_sql` is convenient and is effectively handing
  the model your database. Module 8 develops this.
EXPLICIT DESTRUCTIVENESS
─────────────────────────────────────────
  Mark mutating tools in metadata, not only in prose,
  so the EXECUTOR can enforce a gate that no prompt
  can talk its way past:

    {"name": "issue_refund", "x-destructive": true}

  The prompt says "confirm first". The executor
  MAKES it true. You want both.
─────────────────────────────────────────

7. A Tool Design Checklist

Per Tool
─────────────────────────────────────────
  □ Name is a verb_noun that reads as an action
  □ Description covers what / use for / do NOT use
    for / returns / cost / mutates
  □ "Do not use for" names the correct alternative
  □ Every parameter has a description and, where
    possible, an enum or range
  □ Return value is a minimal subset, not a raw record
  □ Truncation is signalled and a continuation given
  □ Errors state the cause, the valid form, and a
    next action
  □ Mutating tools take an idempotency key
  □ Destructiveness is machine-readable metadata
Per Tool Set
─────────────────────────────────────────
  □ 5-15 tools. Beyond ~20, selection accuracy drops
    sharply — split into sub-agents (Module 6) or
    load tools by task phase
  □ No two descriptions could plausibly answer the
    same request
  □ Read-only and mutating tools are clearly
    distinguishable by name alone
  □ The set has no gap that forces the model to
    improvise with a general-purpose tool
─────────────────────────────────────────

8. Summary & Next Steps

Key Takeaways

  • Tool descriptions are the only information the model has about its options, which makes them higher-leverage than the system prompt.
  • The "do not use for" line, naming the correct alternative, prevents more errors than any other single element.
  • Return values are a context budget decision: return a minimal subset, signal truncation explicitly, and include a continuation.
  • Error messages are prompts injected at the decision moment — those naming a fix produce recoveries, those saying "invalid request" produce loops.

Concept Check

  1. Why does adding a "DO NOT USE FOR" clause to a tool description reduce wrong tool selection more than lengthening the system prompt?
  2. What specifically goes wrong when a tool truncates its results without saying so?
  3. Give two independent reasons to prefer get_customer(id) over run_sql(query).

Next Chapter

Chapter 4: The Model Context Protocol


Jr Codex — 1-on-1 Personalized Coaching | Back to Module Index | Back to Agentic AI Index