tools · diff

git:20260726.1a24604 to git:20260804.4bb97c4

5 added, 0 removed. Audit A to A.

---
name: tools
description: Optimize an agent's OWN tool surface (tools it implements, not an external MCP server). Use when the agent mis-selects tools, fills arguments wrong, or has a confusing/redundant toolset. You may edit tool names, descriptions, parameter docs, in-description examples, the JSON schema/API, the tool code itself, ADD tools (including composite tools that call existing tools), and REMOVE tools — all under an action policy so risky edits can be locked off.
component: capability
argument-hint: "--path DIR"
allowed-tools: Read, Write, Edit, Bash
provides: [candidate]
needs: []
sources: [gepa, tau2bench]
---
# Capability: tools (full control)
This capability treats the agent's **entire tool surface as the optimizable
artifact**. When an agent *owns* its tools — it implements the handlers, defines
the wire schema, and controls every caller — then names, descriptions, parameter
docs, in-description examples, the JSON Schema, *and the implementation code* are
all fair game.
Use this capability when the agent OWNS its tools — it implements the handlers and
controls the wire schema, so the code itself is editable. (When the tools come from an
external server you can only re-describe, not re-implement, the action policy here is
tightened to documentation-only edits.)
## What you can change here
**The tool's docstring AND its return value are what the agent SEES — make both
clear and recovery-oriented.** The doc surface (description, important-notes,
per-param, `Raises:`, examples) drives *which* tool the model calls and *how* it
fills the arguments; the return value (and especially the error text) steers the
*next* turn. Each lever below is an edit class; in ONE pass, apply EVERY edit class
the traces call for — a validation wrapper AND a loop tool AND enriched
returns/errors AND doc fixes across all implicated tools can and should all ship in
the same candidate. (1-line generic examples; worked bodies in
[`references/examples.md`](references/examples.md), depth below and in
[`references/concepts.md`](references/concepts.md).)
**Read this skill in full before editing. Ship MULTIPLE fixes per iteration — but
every one must be REAL (targets a currently-failing task), SAFE (cannot change a
passing task's behavior), and VERIFIED (proven to fix its target).** Several such fixes
beat a long list that includes a speculative edit: one edit that regresses a passing
task can sink the whole candidate at the gate. Quality over churn — never add an edit
to hit a count, and never re-add a rule/tool the run already tried and rejected.
**Per-change SAFETY (the rule that makes multi-change work).** Scope every guard to fire
ONLY on the exact violating condition, and check its blast radius: run it on the args of
1–2 currently-PASSING tasks that use the same tool and confirm it does NOT fire. A guard
that fires on a passing task is a regression — rescope or drop it. This is how you ship
many changes without net-zero churn.
**Pick the lever by failure type — the in-body guard is the DEFAULT strong move:**
- **Edit the BODY of an EXISTING tool (reach for this FIRST for a rule violation).** When
the agent VIOLATES a rule a tool already owns (a wrong field value, an id not on file,
an action on a record whose state forbids it), convert the prose rule into a scoped
in-body guard. This is the highest-yield, lowest-regression edit; expect to touch
SEVERAL existing bodies in one iteration.
- **ADD a NEW code-bearing tool — for a genuine CAPABILITY GAP or action STALL.** A
composite atomic-WRITE tool for a multi-step action the agent narrates/confirms then
fails to execute is the canonical case (the primitive is what it skips; the composite
makes the action un-skippable; then REMOVE the raw primitives). New tools are
available and encouraged WHEN they close a real gap — but add one ONLY if the agent
will actually call it AND it changes the graded outcome. Do NOT add read/compute/summary
helper tools that a guard or a prompt line would subsume, and never add a tool just to
"ship a new tool" — that is churn the gate punishes.
- **DECISION / PERMISSION cluster → a discriminating-predicate guard (the BOUNDED-blast-
radius alternative to a prompt change).** When a cluster is about ACT-vs-REFUSE (the
agent acted where it should have refused/escalated, or vice versa), the WRONG fix is
loosening a global decision rule in the prompt — that has UNBOUNDED blast radius and
regresses every task where the original behavior was gold. The RIGHT fix lives here:
an in-body guard on the tool that owns the action, expressing the EXACT policy
predicate, that refuses/raises ONLY when the qualifying condition is/isn't met. It
fires only on the narrow violating condition, so its blast radius is bounded to the
failing inputs — the SAFEST edit, preferred over any prompt/permission change.
- **HARD-ZERO / capability-gap cluster → a REAL targeted tool, never prose.** A task
scoring 0.00 that needs a compute / composite / discriminating-predicate tool stays
0.00 after any docstring or prompt reword. Ship a tool the agent will CALL that
changes the graded state.
The two failure modes to avoid: (1) leaving a rule the agent keeps breaking as loose
prose instead of a guard, or loosening a global permission rule (unbounded regression)
instead of a scoped guard; (2) padding the candidate with low-value helper tools or
cosmetic rewrites that move no graded task.
1. **Edit the CODE of EXISTING tools to enforce rules deterministically (the most
common high-leverage edit)** — most violated textual rules govern a tool that
ALREADY exists, and the fix is an in-body guard *there*, not a new tool. Bake the
precondition / normalization / actionable refusal into the body so correctness
doesn't depend on the LLM. *Ex:* add `if not rec["cancellable"]: raise
ValueError("not cancellable; reason=...; do X instead")` to the existing
`cancel_record` body. Expect to touch the BODIES of SEVERAL existing tools per
iteration — one per violated rule.
2. **Add a new tool (first-class — reach for it freely)** — give the agent a
capability it lacks or a safe path it keeps skipping, built as a thoughtful
workflow/validation tool, not a thin wrapper. *Ex:* `search_logs` returns only
the relevant lines instead of a raw dump; or a `find_duplicate_records` the agent
has no way to compute today. For a STALL or capability-gap cluster this is the
right move even when a primitive exists — see item 7 (composite atomic-WRITE).
3. **Replace / wrap a tool** — superset an existing tool and route the old behavior
through it. *Ex:* wrap `find_record`+`charge_payment` behind one
`charge_record(record_id)` that resolves then charges.
4. **Improve a tool's documentation** — sharpen description / important-notes /
`Raises:` / per-param docs / examples; rename for least surprise. *Ex:*
`lookup(record)` → `get_record(record_id: str)` with "returns an error object if
not found."
5. **Improve RETURN VALUES for recoverability** — high-signal fields, stable
human-readable ids, and **actionable error text with a next-step hint and what
NOT to do**. *Ex:* error returns "payment method not on file; available:
['card_1'] — pass one of these" instead of a raw traceback.
6. **Add a loop tool** — replace N repeated single-item calls with one list call.
*Ex:* `get_records(ids: [...])` replaces N× `get_record(id)`.
7. **Add a composite atomic-WRITE / workflow tool (first-class — the fix for STALLS
and multi-step writes)** — for a recurring, failure-prone multi-step action
(especially one the agent narrates/confirms then fails to execute, e.g. a
multi-step update that must cancel/undo then re-create a record), a deterministic
tool whose body performs ALL the steps in order via the
existing primitives, then `remove` the raw primitives so the action is
un-skippable. *Ex:* `apply_change_plan(record_id, steps)` validates → applies each
→ returns final state as one reliable call. Reach for this whenever a cluster
STALLS at the action boundary — do not settle for a prose "be sure to act" rule.
8. **Remove-with-replacement** — remove a redundant/overlapping tool *only* after a
replacement preserving its capability exists. *Ex:* drop `query` once
`get_record` + `search_records` cover it.
**Before/after — convert a violated prose rule into an in-body guard (the default
edit).** The rule "a record can only be cancelled when it is cancellable" lives as
prose in the prompt and the agent keeps breaking it. Don't add a tool — edit the
EXISTING `cancel_record` body:
```diff
def cancel_record(record_id):
+ rec = get_record(record_id)
+ if not rec["cancellable"]:
+ raise ValueError(
+ "not cancellable; reason=" + rec.get("status", "unknown")
+ + "; do X instead (e.g. offer a change_record)")
return _backend.cancel(record_id)
```
And the rule "amounts are in whole cents and the method must be on file" — edit the
EXISTING `book` (or `charge`) body to normalize the field then raise an actionable
error:
```diff
def book(record_id, amount, payment_id):
+ amount = int(round(amount)) # normalize: callers pass dollars
+ methods = {m["id"] for m in get_record(record_id)["payment_methods"]}
+ if payment_id not in methods:
+ raise ValueError(
+ f"payment method {payment_id!r} not on file; "
+ f"available={sorted(methods)} — pass one of these")
return _backend.book(record_id, amount, payment_id)
```
Both fixes touch a tool that ALREADY exists; no new tool is needed. A docstring-only
or new-tool-only iteration that leaves these rules as prose is under-used.
**Guardrails (depth below):** encode deterministic logic in code, not prose (a
tool body the model cannot skip beats a sentence it can forget); keep the toolset
small and namespaced (aim **< ~20** active tools); ship correct, bug-free code
(every code edit needs validation + a `validate` run); and **never remove a tool
without a capability-preserving replacement** (add → verify → swap, see the SAFE
TOOL-REPLACEMENT PROTOCOL).
**Generalize, never hardcode.** Every in-code guard must fire on the GENERAL
condition that defines the failure class, never on a literal value from one task.
*Good:* `if payment_id not in user_payment_methods: raise ...`. *Bad:*
`if record_id == "<TASK_SPECIFIC_ID>": raise ...` — that overfits to one task, gets
rejected by the held-out gate, and helps nothing else. Use a failing task's
specifics only to identify the class, then write the general check.
A discriminating-predicate guard for a decision/permission cluster must encode the
GENERAL policy condition that separates the qualifying cases from the rest (e.g.
`if record.tier == "restricted" and action == "modify": raise ...`), NOT a global
behavior flip and NOT a task literal. The guard NARROWS — it fires only on the cases
the policy actually governs — so passing tasks outside that condition keep their
behavior unchanged.
## The highest-leverage edit: deterministic CODE (usually in an EXISTING tool body)
**Start here. A deterministic guard in code beats a sentence in the prompt.** A
docstring or system-prompt rule only makes the model *more likely* to comply — it
can be forgotten, mis-read, or out-reasoned on the next task. Code (a precondition
check, a normalization, a loop) makes the right behavior *the only thing that can
happen* — it cannot be "forgotten." When the traces show a rule the agent keeps
breaking, a multi-step action it keeps fumbling, or — most importantly — an action
it *stalls on and never executes*, **do not just reword a description — put the
behavior in code.**
**Two code-bearing edits, chosen by failure type — both first-class.** For a rule
VIOLATION, enforce the rule in the body of the EXISTING tool that owns it — "only
cancel a cancellable record" governs `cancel_record`, "amounts in whole cents"
governs `book`/`charge`; add an in-body guard there and expect to touch SEVERAL
existing bodies per iteration. For a CAPABILITY GAP or an action STALL, write a NEW
code-bearing tool — a loop tool to collapse a multi-call chain, or (the big one) a
**composite atomic-WRITE tool** to encapsulate a multi-step write the agent keeps
skipping. Do not treat the new tool as a last resort: when a cluster stalls at the
action boundary, the composite is the *correct* fix even though a write primitive
already exists, because the primitive is exactly what the agent declines to call.
(See the before/after diffs above, the three patterns below, and the worked bodies
in [`references/examples.md`](references/examples.md).)
**Prose cannot fix a BEHAVIORAL failure.** There is a sharp distinction the
optimizer must make. If the agent *does not know* something (a format, a rule, a
decision criterion), prose can teach it — that is a KNOWLEDGE gap, and it belongs
in the prompt. But if the agent demonstrably *knows* what to do and still doesn't
do it — it analyzes the situation, explains the plan, even gets the user's
confirmation, and then simply **fails to CALL the action tool and stops** — that
is a BEHAVIORAL failure, and *more prose does not fix it*. You cannot instruct a
model out of a behavior it already "agreed" to and then skipped. The only
reliable fix is to move the behavior into CODE: encapsulate the whole action in a
tool whose body performs it, so executing it is no longer a choice the model can
decline mid-conversation. Telling the agent to "be sure to act" is exactly the
kind of edit the traces show failing.
Three patterns carry almost all the gain (worked bodies below and in
[`references/examples.md`](references/examples.md)):
1. **Validation / rule-enforcement tool (wrap, then delegate).** When a rule or
precondition that today lives only in the prompt is GENERAL — it always
applies, not just to one task — implement it as code in a NEW tool that:
validates / normalizes the inputs → enforces the rule → calls the existing
primitive → returns a clear result (or a clean refusal). Then **remove the raw
primitive from the exposed set** if the agent should only ever reach it
through the safe path. Example: `cancel_record_safely(record_id)` reads the
record, refuses unless it is cancellable, then calls the raw `cancel_record`.
2. **Workflow / loop tool (collapse a recurring multi-step sequence).** When a
small multi-step workflow recurs, or the agent calls one primitive N times in
a row (and drops or mis-threads a result), implement it as ONE tool with real
loops/code that calls the existing tools internally and returns the finished
result. Example: a tool that loops over a list of ids calling `get_record`
once each and returns them aligned, instead of the agent issuing N calls.
3. **Write / workflow COMPOSITE tool — make a stalled action un-skippable
(co-equal PRIMARY pattern).** This is the fix for the single most common
BEHAVIORAL failure: the agent reliably **stalls at the action/write boundary**
— it analyzes, explains, even confirms with the user, then never issues the
write call and stops, leaving the task half-done. The cure is *not* a stronger
rule. Encapsulate the ENTIRE multi-step action (analyze → confirm → act) as
ONE composite tool whose body performs *all* the steps in code — calling the
existing primitives in the right sequence and looping where needed — so the
action completes the moment the tool is called and **cannot be skipped
mid-conversation**. Then **`remove` the raw primitives** so the composite is
the only path; the safe, complete behavior is then the only behavior reachable.
Generic example: an `apply_change_plan(record_id, steps)` that performs a
multi-step update atomically — validating each step, applying them in order via
the existing write primitives, and returning the final state — so the agent
hands over the plan in one call instead of narrating it and then failing to
execute. (Worked body in [`references/examples.md`](references/examples.md) §3e.)
**Lean caveat — replace, don't accumulate.** Every exposed tool enters the
agent's context, and too many tools degrade selection (see §3 of concepts.md).
So PREFER consolidating over piling on: when you add a safer or looped tool,
**`remove` the now-redundant primitive** so the net tool count stays small and
sharp. Do not add many narrow tools. One sharp tool that subsumes a primitive
beats two overlapping ones.
**SAFE TOOL-REPLACEMENT PROTOCOL (never bare-remove a tool).** To replace or
consolidate a tool, follow these steps in order — never delete a tool without a
replacement that subsumes it:
1. **ADD a wrapper tool whose body CALLS the existing tool** — after validation,
normalization, or the extra steps you want guaranteed. The wrapper delegates to
the primitive; it does not re-implement it.
2. **VERIFY** the wrapper (run `validate`; confirm the body actually calls the
primitive and returns a sane result).
3. **Only then SWAP the registration**: `remove` the raw primitive from the active
/ exposed set and register the wrapper as the path the agent uses.
Bare-removing a primitive with no replacement strands every task that needed it
("no applicable tool"); adding a wrapper but leaving the primitive exposed lets the
model route around the guard and reproduce the original failure. The add-verify-swap
order is what makes the safe path the *only* path without a coverage gap.
**You must write the BODY.** A `compose`/`add`/`code` edit whose body is `...`,
a bare `pass`, or docstring-only is NOT this edit — it does nothing. Emit a real
implementation: the loop, the precondition check, the calls to the existing
tools (`get_record(i)` — or `self.get_record(i)` if your adapter binds tools as
methods). Worked examples with full bodies are below under
[Add tools that call existing tools](#add-tools-that-call-existing-tools-the-highest-leverage-edit).
**SECONDARY (last resort): passthrough / "think" / reasoning-only tools.** A tool
whose body just returns (or echoes) its argument, with the actual rule living only
in the *docstring* (e.g. a `think(thought)`/`check_policy(text)` tool that does no
real work), is the WEAKEST form of this edit — it is prose wearing a tool's
costume, and the model can ignore or mis-apply it exactly like any prompt sentence.
**If a rule can be encoded as code, encode it as code** — validate, normalize, and
enforce it in the body (patterns 1 and 2 above), don't leave it as docstring prose.
Reach for a reasoning-only tool only when the behavior genuinely cannot be made
deterministic (e.g. you want to *prompt* a planning step), never as a substitute
for a check you could have written in a few lines of code.
## How agents fail (and how tools fix it)
Map the trace symptom to the code-bearing edit. Each row is a failure the model
*could* "know" how to avoid yet keeps producing — so the fix is code, not prose:
These four rows carry most of the recoverable gain — diagnose for them FIRST, and
verify the fix you ship actually FIRES on the failing trace (run the new body on the
exact arguments from that trajectory; a guard that never triggers on the failing task
is dead code, not a fix). These rows are independent: fix as MANY of them as appear in
the trajectories in one candidate, not just the first — each guarded tool is its own
bounded fix.
| Trace symptom | Fix |
|---------------|-----|
| **Wrong ARGUMENT the tool could validate** — a write whose id / reference / count / unit is not consistent with the agent-visible state (an id not in the record, a count exceeding what's available, the wrong unit). Right tool, bad argument; partial credit or a corrupted write. | **Normalize-then-call wrapper**: wrap the write in a body that RESOLVES / VALIDATES the argument against the current state, and on mismatch returns `available=[...]` (the valid options) or raises an actionable error naming what's wrong and what to pass instead — never let a write proceed on an unvalidated reference. Coerce units, resolve ids, check the field is on file, then delegate to the primitive. |
| **Escalate / bail-out abandoning a REQUIRED, eligible action** — the agent hands off or gives up when it could and should have completed the action itself; this is a behavioral STALL, not a missing capability. | **Composite WRITE tool**: encapsulate the eligible-action batch in ONE tool whose body executes the steps in code (skipping any ineligible item with a recorded reason), then `remove` the raw primitives so completing the batch is the only path. Do NOT add a "don't bail out" prose rule — the agent already chose to bail; only code removes the choice. |
| **Recoverable error that strands the agent** — a tool raises an opaque traceback / bare code, the agent retries the same bad call or gives up. | **Enriched RETURN that aids recovery**: on a recoverable error, return what's wrong + the valid options + the recommended next action (e.g. `{"error": "id not found", "available": [...], "next": "call search_x to resolve the id"}`), so the model self-corrects on the next turn instead of repeating the failure. |
| **Execution stalls at the action boundary** — the agent analyzes, explains, even confirms, then never calls the write tool and stops (the task is left half-done). | **Composite WRITE tool** (pattern 3): one tool whose body performs the whole analyze→confirm→act sequence, then `remove` the raw primitives so the action is un-skippable. |
| **The same primitive called N times in a row** — looping over a list in the agent's own context, dropping or mis-threading results. | **Loop tool** (pattern 2): one tool that takes the list and loops inside a single call. |
| **A rule stated in the prompt but repeatedly violated** — a required order ("read before write"), a precondition the API doesn't enforce. | **Validation wrapper** (pattern 1): enforce the rule in the tool body; `remove` the unguarded primitive. |
The throughline: a failure the agent *knows better than* but still commits is
behavioral, and behavioral failures are fixed by removing the choice — putting the
behavior in code and `remove`-ing the path that let it go wrong.
## When to use this
Reach for `tools` when a trace shows one of these failure signatures:
- **Execution stall at the action/write boundary (BEHAVIORAL)** — the agent
reaches the point of acting, narrates or confirms the action, then **fails to
call the write tool and stops**. This is not a knowledge gap and prose will not
fix it; encapsulate the whole action in a composite WRITE tool and remove the
raw primitives so completing it is the only path (§"highest-leverage edit",
pattern 3).
- **Mis-selection** — the agent calls the wrong tool, or calls none when one
applied, or invents a tool that does not exist. Selection is driven almost
entirely by the tool *name* and *description*, so this is a documentation fix.
- **Bad argument-filling** — the right tool, wrong arguments: a missing required
field, the wrong enum value, a free-text string where a structured object was
expected. This is a parameter-schema and per-parameter-description fix.
- **Repeated multi-call sequences the agent fumbles** — the agent must chain
`search` → `filter` → `fetch` every time and keeps getting the order or the
glue wrong. A single well-named **composite** tool collapses the sequence.
- **The same primitive called N times in a row** — the agent loops over a list
in *its own context* (calls `get_record(id)` once per id, or `search(a,b,date)`
once per date/route combination), burning turns and often dropping or
mis-threading a result. A tool that takes the **list** and loops *inside one
call* collapses N calls into 1.
- **An invariant the model keeps violating** — a required order of operations
("read before write"), a precondition the API does not itself enforce, or a
normalization the model forgets. A tool that **enforces the rule in code**
(validates, normalizes, or performs the steps in the right order) makes the
mistake impossible rather than merely discouraged.
- **A DECISION / PERMISSION the model gets wrong (ACT vs REFUSE)** — the agent acts
where the policy says refuse/escalate (or refuses where it should act). Do NOT fix
this by loosening the global rule in the prompt — that changes behavior for the whole
class and regresses every task where the original behavior was correct (unbounded
blast radius). Encode the EXACT discriminating policy predicate as an in-body guard on
the tool that owns the action: it refuses/raises ONLY on the qualifying condition, so
its blast radius is bounded to the violating cases. This is the SAFE, preferred
alternative to a permission-rule prompt edit.
- **A bloated or overlapping toolset** — too many tools, or several that do
nearly the same thing, distracting the agent. Remove or consolidate.
- **A real behavioral bug in a handler** — the tool returns the wrong thing.
Because you own the code, you can fix it directly.
If the problem is *what the agent is told to do* rather than *what it can do*, it is
out of scope for this capability (it belongs to whatever capability edits the agent's
instructions).
## What can be optimized (default policy = all of these)
| Action | Changes | Why it moves the metric |
|--------|---------|-------------------------|
| `description` | tool-level wording incl. in-desc examples | the single biggest lever on *selection* |
| `params` | per-parameter descriptions / defaults | drives correct *argument-filling* |
| `examples` | example call strings | shows concrete well-formed calls |
| `schema` | the full JSON Schema (types, `required`, `enum`) | constrains/guides the model's output |
| `code` | **the handler body of an EXISTING tool** | **the default high-leverage edit** — convert a violated prose rule into an in-body guard (precondition, normalization, actionable refusal); expect to edit SEVERAL existing bodies per iteration |
| `compose` | add a code-bearing tool that calls existing tools | enforce a rule, collapse a multi-call chain, or perform a whole stalled WRITE action in code — use when no existing tool owns the rule |
| `add` / `remove` | introduce / delete a tool | shape and shrink the toolset (replace primitives; keep it lean) |
The `code` row (editing an EXISTING tool's body) is the **first edit to reach
for**, with `compose`/`add` close behind — a deterministic guard beats a sentence
in a prompt (see below). For each violated rule, first ask "which EXISTING tool
governs this, and what in-body check enforces it?" — usually the answer is editing
that tool's body, not adding a new one. Reword descriptions *after* you've asked
"can this rule be code in the existing body instead?" In ONE pass, apply EVERY edit
class the traces call for — in-body guards across SEVERAL existing tools AND a loop
tool where needed AND enriched returns/errors AND doc fixes across all implicated
tools can and should all ship in the same candidate; do not stop after a single
edit.
Lock any of these off via `inputs/policy.json`. For example, in a frozen-API
deployment you might allow only `["description", "params", "examples"]` so an
optimizer can reword tools but never change the wire contract or the code.
`apply()` refuses anything outside the allowed set and reports the refusal — it
never silently drops or silently applies a disallowed edit.
## How tool-using agents actually read a tool
An LLM never sees your implementation. At call time it sees, for every available
tool, a serialized block of `{name, description, parameters-schema, examples}`
injected into its context (this is literally how the Anthropic and OpenAI tool
APIs work, and how an MCP host presents `tools/list` results). Two decisions
follow, and each is driven by a different part of that block:
1. **Selection** — *which* tool (or none) to call. Decided primarily from the
**name** and **description**. Anthropic's own guidance is blunt: the
description "is by far the most important factor in tool performance," and
they recommend at least 3–4 sentences covering *what the tool does, when to
use it, and when not to*. A name like `lookup` selects worse than
`get_order_by_id`.
2. **Argument-filling** — *how* to populate the call. Decided from the
**parameter schema** (types, `required`, `enum`, descriptions) plus any
**examples**. An `enum` turns "guess a status string" into "pick from this
closed set"; prefer it for every closed value set, and use the provider's
**strict / schema-validated mode** where available so the model adheres to the
schema instead of guessing. A per-field description ("ISO-8601 date, e.g.
2025-06-14"; "amount in whole US cents") turns a malformed argument into a
correct one — always pin **units, format, and default** per parameter. Add
schema-validated **`input_examples`** for complex / nested / format-sensitive
params (a few help; long dumps hurt reasoning models). And **don't make the
model fill arguments you already know** — pass them in code (a wrapper) instead
of asking for them.
**Namespace by service/resource** so selection stays unambiguous as the set grows
(`github_list_prs`, `payments_charge`), and keep the **active toolset small** — aim
for **fewer than ~20 tools per turn** (OpenAI's heuristic); selection degrades
sharply past that. This is the number behind the lean caveat above.
## Adapting to the reader's capability tier
Scale the edit to WHO calls these tools at runtime (see the `THE READER` block in your
instructions, if present). For a **mid/weak** reader, push harder on this skill's
already-preferred **code enforcement** (in-body guards, composite atomic-write tools) — a
weak reader skips a prose rule but a guard fires regardless — and write **literal,
example-bearing per-parameter slot-filling docs** on every tool (name the exact format,
units, and one concrete valid value, e.g. `date: ISO-8601 "2026-07-20"`). A weak reader
mis-fills under-documented arguments far more often, so the marginal value of explicit
parameter docs and a smaller, less-confusable toolset is highest there. For a **frontier**
reader, terser parameter docs and fewer worked examples suffice; spend the budget on
removing redundant tools instead.
## Shape the RESULT, not just the call (output/response design)
What a tool *returns* steers the next turn as much as its description steers
selection. A bloated or opaque result causes hallucinated ids, wasted context, and
redundant calls. Design the response:
- **Return high-signal fields only.** Strip low-value noise (internal uuids, mime
types, 256-px thumbnail urls, audit columns). Return the semantic fields the
agent will actually act on.
- **Use stable, human-readable identifiers, not raw UUIDs.** Models hallucinate and
mis-copy long opaque ids; a `get_order(order_id)` projection should surface
`order_id="A-1042"` over `4f3c…-uuid`. If the backend only has a UUID, attach a
readable handle alongside it.
- **Paginate / filter / truncate with sane defaults**, and offer a
**`verbosity`/`response_format`** control (e.g. `"concise"` vs `"full"`) so the
agent asks for detail only when needed instead of drowning in it.
- **Make error messages ACTIONABLE — they are a steering surface, not just a
failure.** A raw traceback or opaque code teaches the model nothing. Return a
specific, example-bearing message that tells the agent how to recover:
`"payment method not on file; available: ['card_1','gift_4'] — pass one of these"`
or `"date must be ISO-8601 YYYY-MM-DD, got '6/14/25'"`. The model reads the error
and self-corrects on the next call instead of retrying the same bad one. Wrappers
(patterns 1–3) are the natural place to produce these.
## Document every tool comprehensively
A tool's documentation is its contract. Every tool — primitive or wrapper — needs
**all** of these, or the model is left guessing:
- a **crisp description**: what it does, when to use it, and when NOT to (the
boundary against the nearest sibling tool);
- an **"important points"** note for any non-obvious behavior or precondition;
- a **Raises / errors** section listing the failure conditions (keep these — see
below; they are a guard rail, not clutter);
- a **per-parameter description** with units / format / allowed values / default;
- one **generic, always-valid usage example** (the shape of a call, never one
task's literal id/date/city).
**The description is the model's contract, not flavor text.** It is the *only*
information the model has about *which* tool to call and *what argument values
are legal*. A good description always states, in always-true terms (never one
task's specifics):
- **When to use / when not to use** — explicit triggers, and the boundary
against the nearest sibling tool ("use X for a single record by id; use Y to
search across records").
- **Argument semantics** — for each parameter: its meaning, **units**, **allowed
values / format**, and **default**. "amount in whole US cents" beats "the
amount"; "ISO-8601 date `YYYY-MM-DD`" beats "the date".
- **Preconditions and failure modes** — what must be true *before* the call, and
what the tool **raises / returns on error**. This is the model's chance to
avoid a bad call. **Do NOT strip `Raises:`/error-condition text to make the
description "cleaner."** Knowing a call raises `ValueError: gift card balance
too low` is exactly what lets the model pick a different payment method instead
of failing the task. Stripping error info removes a guard rail; it does not
improve selection.
- **A short, always-valid usage example** — one concrete well-formed call that is
correct for *any* input (e.g. the shape of a list element), never a single
benchmark task's literal values.
Selection degrades as the toolset grows: benchmarks like the Berkeley
Function-Calling Leaderboard include a dedicated "relevance detection" category
precisely because models hallucinate calls when no tool fits, and ToolLLM had to
add a *retriever* to cope with thousands of tools. The practical implication for
this capability: **fewer, sharper, non-overlapping tools beat many vague ones.**
## Concrete before/after
**Selection fix (description).** A trace shows the agent calling a generic
`query` tool for order lookups, then failing.
```diff
- "name": "query", "description": "Run a query."
+ "name": "get_order", "description": "Look up a single order by its ID and
+ return status, line items, and shipping. Use when the user references a
+ specific order (an ID, 'my last order', or an order in the current thread).
+ Do NOT use for searching across orders — use search_orders for that."
```
**Argument-filling fix (schema + enum).** The agent keeps sending
`status="done"` when the backend expects `"fulfilled"`.
```diff
"parameters": { "type": "object", "properties": {
- "status": { "type": "string", "description": "the status" }
+ "status": { "type": "string", "enum": ["pending","fulfilled","cancelled"],
+ "description": "Order status to filter by." }
}, "required": ["status"] }
```
**Collapse a fumbled chain (compose).** The agent must `search_orders` then
`get_order` on the first hit, and often forgets the second call.
```json
{ "kind": "compose", "value": {
"name": "find_order",
"description": "Search orders by free text and return the full record of the
best match. Use this instead of search_orders+get_order when you want one order.",
"code": "def find_order(q):\n hit = search_orders(q)[0]\n return get_order(hit['id'])"
}}
```
## Add tools that call existing tools (the highest-leverage edit)
A docstring edit can only make the model *more likely* to do the right thing.
A new tool whose body calls existing tools can make the right thing *the only
thing the model can do*, and can turn many calls into one. These are the two
PRIMARY patterns (§"highest-leverage edit" above) plus the normalize variant —
all benchmark-agnostic, and each shows a REAL body you are expected to write:
1. **Workflow / loop — collapse repeated primitive calls into one list call.** If the agent calls
`get_record(id)` once per id, or `search(origin, dest, date)` once per
route/date combination, add a tool that takes the **list** and loops inside a
single call, returning all results together. The agent makes one call instead
of N; nothing is dropped or mis-threaded.
```json
{ "kind": "compose", "value": {
"name": "get_records",
"description": "Fetch the FULL details of EVERY record in `ids` in one call. Use this instead of calling get_record once per id when you have several ids (e.g. all of a user's records). Returns a list aligned with `ids`; an entry is an error object if that id is not found.",
"parameters": {"type":"object","properties":{"ids":{"type":"array","items":{"type":"string"}}},"required":["ids"]},
"code": "def get_records(ids):\n out = []\n for i in ids:\n try:\n out.append(get_record(i))\n except Exception as e:\n out.append({'id': i, 'error': str(e)})\n return out" }}
```
2. **Validation / rule-enforcement — enforce a rule or required order
deterministically (wrap, then delegate).** If a write must be preceded by a
read, or a precondition the API does not itself check must hold, put the check
in code: validate/normalize the inputs, enforce the rule, then call the
existing primitive — so a violation returns a clear refusal instead of
corrupting state. The model literally cannot skip the step. Then **`remove`
the raw primitive** so the only path is the safe one.
```json
{ "kind": "compose", "value": {
"name": "cancel_record_safely",
"description": "Cancel a record after verifying it is cancellable. Reads the record first and REFUSES (returns an error) if the cancellation preconditions are not met, so you never need to call get_record yourself before cancelling. Use this for every cancellation.",
"parameters": {"type":"object","properties":{"record_id":{"type":"string"}},"required":["record_id"]},
"code": "def cancel_record_safely(record_id):\n rec = get_record(record_id)\n if not rec.get('cancellable'):\n return {'error': 'not cancellable', 'record': rec}\n return cancel_record(record_id)" }}
```
...paired with a `{ "tool": "cancel_record", "kind": "remove" }` so the unsafe
primitive leaves the choice set entirely.
3. **Normalize / return richer, ready-to-use results.** Have the new tool do the
glue the model otherwise improvises (resolve an id, attach the related record,
coerce units), so the model gets a result it can act on directly.
**Then remove the error-prone original** deliberately (the lean caveat). This
`remove` step is not optional polish — it is what makes the safe/complete tool the
*only* path. A wrapper that enforces a rule, or a composite that performs a whole
write, achieves nothing if the raw primitive is still exposed: the model can (and
under pressure will) route around the guard straight to the primitive and
reproduce the exact failure. Observed in real runs, optimizers add wrappers but
**never `remove` the primitives they wrap**, so the unsafe path survives and the
metric barely moves. To *replace* a confusing or now-redundant tool,
`add`/`compose` the clearer one and `remove` the old name so it leaves the choice
set — don't keep both, or selection gets harder (§3 of concepts.md). Net tool
count should stay small and sharp: prefer one tool that subsumes a primitive over
two that overlap. Pair every wrapper/composite with the matching `remove` unless
the primitive is still independently needed for a *different*, safe purpose.
## What good vs bad tool edits look like
Drawn from real runs where the optimizer left most of the gain on the table.
**Bad — what under-performs (do not stop here):**
- **Stripping `Raises:` / error info** "to clean up" the docstring. Observed: an
accepted candidate deleted every `Raises:` section. It barely moved the metric
and left the model blind to why calls fail. Error conditions are guidance, not
clutter — keep them.
- **Cosmetic description rewording** — reflowing sentences, adding commas,
restating the obvious ("Cancel the record" → "Cancel an entire record"). No new
always-true information, so no behavior change.
- **Baking one task's specifics into a description** — naming a particular id,
date, or city. It overfits and can mislead on the next task.
- **Adding tools that duplicate ones the agent already uses fine** — enlarges the
choice set and *hurts* selection for no gain.
**Good — what actually moves accuracy and cuts calls:**
- **A loop/composite tool** that collapses the repeated-primitive pattern from the
traces (e.g. the agent fetching records one id at a time, or sweeping a search across
many parameter combinations) into a single list call.
- **A rule-enforcing tool** that reads-before-writes or validates a precondition
the underlying API does not, turning a silent bad write into a clear refusal.
- **Precise descriptions** that add genuinely new, always-true content: explicit
when/when-not triggers, per-argument units/allowed-values/defaults, retained
failure modes, and one always-valid example call.
- **Replace, don't accumulate** — add the clearer tool and `remove` the
error-prone original so the surface stays small and sharp.
The test: *would this edit help on a task the optimizer has never seen?* A loop
tool, a precondition check, and a unit-pinned argument description pass. A comma
and a deleted `Raises:` line do not.
## Failure modes to avoid
- **Over-describing into contradiction.** Adding a fifth "use when" clause that
conflicts with the first makes selection *worse*. Keep descriptions internally
consistent; state the boundary against the nearest sibling tool, not every tool.
- **Schema changes that break callers.** You own the callers here, but a `code`
edit and a `schema` edit must stay in sync — change both in one batch, then run
`validate`. In a frozen-API setting, lock `schema`/`code` off via policy.
- **Composite-tool sprawl.** A composite is worth it only if the chain is
frequent and error-prone. Adding one for a two-call path the agent already
handles just enlarges the toolset and hurts selection.
- **Removing a tool that's rarely-but-critically needed.** Check the traces:
remove for *overlap/confusion*, not merely for low call-count.
- **Examples that fight reasoning models.** A few examples sharpen formatting, but
long example dumps can degrade reasoning-tuned models — prefer a crisp schema
over many examples.
## The action policy (safety knob)
`inputs/policy.json` is the safety boundary between "reword the docs" and "rewrite
the program." It exists because the same artifact is edited in very different
trust settings: an optimizer you trust to polish descriptions should not be free
to change the wire schema of a tool other systems depend on, or to inject
arbitrary handler code. The default here is the **full** set; tighten it to match
your deployment's blast radius. Every refused edit is reported, so a policy that's
too tight surfaces as visible refusals rather than silent no-ops.
## Artifact + handlers
`tools.json` — a list of `{name, description, parameters, examples, code?}`.
`scripts/abstract.py` provides:
- `materialize(dir)` — flatten the surface into named text components
(`tool.<name>.description`, `.parameters`, `.examples`) for a text optimizer.
- `apply(dir, edits)` — policy-enforced edits incl. `schema`/`code`/`compose`;
returns `{changed, refused}`.
- `validate(dir)` — schema well-formedness, empty-description and duplicate-name
checks.
## How to run
```
python scripts/check.py
python scripts/run.py --path <capability_dir> # candidate + policy + validity
```
## References
- [`references/concepts.md`](references/concepts.md) — the mental model (select vs.
fill, toolset design, the policy) with cited sources.
- [`references/examples.md`](references/examples.md) — worked before/after edits.
- [`references/pitfalls.md`](references/pitfalls.md) — failure modes and how to detect them.
+ - [`references/optimizer-playbook.md`](references/optimizer-playbook.md) — what the
+ authored optimizer INSTRUCTIONS must demand when `tools` is selected: the
+ existing-tool-code mandate, the depth mandate's tools wording, and the two-phase
+ (diagnose fan-out → implement fan-out → merge) subagent pattern. `intake` points
+ here rather than inlining it.