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.
Jr Codex Agentic AI Notes
Level: Intermediate Prerequisites: Chapter 2: Prompting Patterns for Agents Time to complete: ~25 minutes
Table of Contents
- Tools Matter More Than Prompts
- The Description Is the Interface
- Granularity
- Designing the Return Value
- Errors That Teach
- Safety Properties
- A Tool Design Checklist
- 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 restFour 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
- Why does adding a "DO NOT USE FOR" clause to a tool description reduce wrong tool selection more than lengthening the system prompt?
- What specifically goes wrong when a tool truncates its results without saying so?
- Give two independent reasons to prefer
get_customer(id)overrun_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