tools
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, calls the same tool N times in a row, or has a confusing, redundant, or oversized toolset. Covers tool names and descriptions, para
Install
npx skills add https://github.com/skillberry-ai/cap-evolve/tree/main/skills/capabilities/tools
claude plugin marketplace add https://llmmart.ai/marketplace.json && claude plugin install skillberry-ai-cap-evolve@llmmart
git clone https://github.com/skillberry-ai/cap-evolve.git
The skills CLI installs just this skill, for any of its supported agents. Claude Code installs the whole skillberry-ai/cap-evolve collection as a plugin from our marketplace. Git is the plain clone.
Skill manifest
Capability: tools (full control)
This capability treats the agent's entire tool surface as the optimizable
artifact. It applies when the agent owns its tools — it implements the handlers,
defines the wire schema, and controls every caller — so names, descriptions,
parameter docs, in-description examples, the JSON Schema, and the implementation
code are all fair game. (When the tools come from an external server you can only
re-describe, not re-implement: that is mcp-tool, whose policy is tightened to
documentation-only edits.)
What you can change here
The tool's documentation AND its return value are what the agent SEES — make both
clear and recovery-oriented. The doc surface (description, important-notes,
per-param, error/Raises text, 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. Confirm which parts of a docstring your runtime actually SENDS before
writing into it — some frameworks discard whole sections, and guidance written into
that void does nothing (references/field-notes.md §1).
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 sinks the whole candidate at the val gate. Never add an edit to hit a count, and never re-add a rule or 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.
Pick the lever by failure type
Each item is an edit class. In ONE pass, apply EVERY 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 ship in the same candidate. The in-body guard is the default strong move; reach for a documentation edit only after asking "can this rule be code in the existing body instead?"
- Edit the CODE of an EXISTING tool (reach for this FIRST for a rule violation).
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, or
actionable refusal into the body so correctness does not depend on the LLM. Ex:
add
if not rec["cancellable"]: raise ValueError("not cancellable; reason=...; do X instead")to the existingcancel_recordbody. Expect to touch the BODIES of SEVERAL existing tools per iteration — one per violated rule. A deterministic guard beats a sentence in a prompt: prose makes the model more likely to comply, code makes the right behavior the only thing that can happen. - Add a composite atomic-WRITE tool — for a stalled or abandoned multi-step action,
encapsulate the ENTIRE action in one tool whose body performs all the steps in order
via the existing primitives, then
removethe raw primitives so the action is un-skippable. Ex:apply_change_plan(record_id, steps)validates → applies each → returns final state as one call. Reach for this even though a write primitive already exists: the primitive is exactly what the agent declines to call. - Add a discriminating-predicate guard for an ACT-vs-REFUSE cluster — an in-body guard on the tool that owns the action, expressing the EXACT policy predicate, that refuses only when the qualifying condition is (or is not) met. It is the narrowest edit available here and the tool-side alternative to changing a global rule.
- Add a real targeted tool for a capability gap — a task that needs a compute /
composite / predicate tool it does not have stays failing after any docstring reword.
Ship a tool the agent will CALL that changes the graded state — Ex: a
find_duplicate_recordsit has no way to compute today, or asearch_logsthat returns the relevant lines instead of a raw dump. - Add a loop tool — replace N repeated single-item calls with one list call. Ex:
get_records(ids: [...])replaces N×get_record(id). - Replace / wrap a tool — superset an existing tool and route the old behavior
through it. Ex: wrap
find_record+charge_paymentbehind onecharge_record(record_id)that resolves then charges. - Improve a tool's documentation — sharpen description / important-notes / error
conditions / per-param docs / examples; rename for least surprise. Ex:
lookup(record)→get_record(record_id: str)with "returns an error object if not found." - 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: an error returning "payment method not on file; available: ['card_1'] —
pass one of these" instead of a raw traceback. Adding to a return is not free,
though: it is re-read every turn, so treat enrichment as a hypothesis to gate, not a
free win (
references/field-notes.md§2). - Remove-with-replacement — remove a redundant/overlapping tool only after a
replacement preserving its capability exists. Ex: drop
queryonceget_record+search_recordscover it.
The two ways to waste an iteration: leaving a rule the agent keeps breaking as loose prose instead of a guard (or loosening a global permission rule instead of scoping a guard); and padding the candidate with low-value helper tools or cosmetic rewrites that move no graded task.
Guardrails
- Encode deterministic logic in code, not prose — a tool body the model cannot skip
beats a sentence it can forget. A tool whose body enforces nothing (a
think()/check_policy()passthrough with the rule only in its docstring) is prose in a tool's costume; reach for it only when the behavior genuinely cannot be made deterministic. - You must write the BODY. A
compose/add/codeedit whose body is..., a barepass, or docstring-only is not this edit — it does nothing. Emit the real loop, the real precondition check, the real calls to existing tools (get_record(i)— orself.get_record(i)if your adapter binds tools as methods). - Never remove a tool without a capability-preserving replacement. Add → verify →
swap (
references/concepts.md§8). Bare-removing strands every task that needed it; adding a wrapper but leaving the primitive exposed lets the model route around the guard and reproduce the original failure. - Keep the toolset small and namespaced — aim for < ~20 active tools; selection
degrades sharply past that. Prefer consolidating over piling on: when you add a safer
or looped tool,
removethe now-redundant primitive. - Ship correct, bug-free code — every code edit needs validation plus a
validaterun, and proof the toolset still registers (an import check is not a registration check —references/field-notes.md§3). - Generalize, never hardcode. Every 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, 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. The test for any edit: would this help on a task the optimizer has never seen?
How agents fail (and how tools fix it)
Map the trace symptom to the edit. This table is the single canonical statement of what to ship for what failure. The 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. 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).
| Trace symptom | Fix |
|---|---|
| Wrong ARGUMENT the tool could validate — a write whose id / reference / count / unit is not consistent with the agent-visible state. 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 current state, and on mismatch returns available=[...] or raises an actionable error naming what is wrong and what to pass instead. Never let a write proceed on an unvalidated reference. |
| The action never happens — the agent analyzes, explains, even confirms, then never calls the write tool and stops; or it hands off / gives up on an action it could have completed. Task left half-done. | Composite WRITE tool (lever 2): one tool whose body performs the whole sequence — or the whole eligible-action batch, skipping any ineligible item with a recorded reason — then remove the raw primitives so completing it is the only path. Not a "be sure to act" prose rule. |
narrated_without_action — the strong form of the row above, and diagnose names it as its own cluster: the final message REPORTS the change as done, with specifics, and the trace holds no mutating call at all. The agent took the user's confirmation as its completion signal. |
Make "confirmed" and "executed" ONE call (lever 2, structurally): a single write whose body performs the approved change, sharing the code path the confirmation handling already uses, then remove the primitives that let the two come apart. A prose reminder to call the tool has been tried on this class and rejected — the agent knows the rule and violates it. |
| 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 is wrong + the valid options + the recommended next action ({"error": "id not found", "available": [...], "next": "call search_x to resolve the id"}) so the model self-corrects next turn. |
| The same primitive called N times in a row — looping over a list in the agent's own context, burning turns and dropping or mis-threading results. | Loop tool (lever 5): 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 does not enforce, a normalization the model forgets. | In-body guard / validation wrapper (lever 1): enforce the rule in the body of the tool that owns it; remove the unguarded primitive if the safe path must be the only one. |
| A wrong ACT-vs-REFUSE call — the agent acts where policy says refuse/escalate, or refuses where it should act. | Discriminating-predicate guard (lever 3) on the tool that owns the action. |
| Mis-selection — the agent calls the wrong tool, calls none when one applied, or invents a tool that does not exist. | Name + description fix: selection is driven almost entirely by the name and description — sharpen what/when/when-not and the boundary against the nearest sibling (references/doc-contract.md). |
| Bad argument-filling — right tool, wrong arguments: a missing required field, the wrong enum value, free text where a structured object was expected. | Schema + per-parameter docs: close the value set with an enum, pin units/format/default per parameter, add an in-description example. |
| A bloated or overlapping toolset — too many tools, or several that do nearly the same thing, distracting the agent. | Consolidate then remove the originals (lever 9). Remove for overlap/confusion, not for low call-count. |
| A bug in a handler — the tool returns the wrong thing. | code edit: because you own the code, fix it directly. |
The throughline: a failure the agent knows better than but still commits is fixed by
removing the choice — putting the behavior in code and remove-ing the path that let it
go wrong.
If the problem is what the agent is told to do rather than what it can do, it belongs to whatever capability edits the agent's instructions, not here.
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; expect to edit SEVERAL 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 |
add / remove |
introduce / delete a tool | shape and shrink the toolset (replace primitives; keep it lean) |
policy.json in the capability dir is the safety boundary between "reword the docs" and "rewrite the
program" — the same artifact is edited in very different trust settings, so tighten the
allowed set to match your deployment's blast radius (a frozen-API deployment might allow
only ["description", "params", "examples"]). apply() refuses anything outside the
allowed set and reports the refusal, so an over-tight policy surfaces as visible
refusals rather than silent no-ops.
Adapting to the runtime reader's capability tier
Scale the edit to WHO calls these tools at runtime (if your instructions state the
runtime reader's capability tier, use it). 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
(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 explicit parameter
docs and a smaller, less-confusable toolset are worth most there. For a frontier
reader, terser parameter docs and fewer worked examples suffice; spend the budget on
removing redundant tools instead.
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.is_empty(dir)— whether the artifact is an empty seed (no tools yet).
How to run
python scripts/check.py
python scripts/run.py --path <capability_dir> # candidate + policy + validity
References
Each is standalone — read the one that matches what you are about to do.
references/examples.md— worked before/after edits with full bodies, ordered by leverage: in-body guards (§0), description/schema fixes (§1–§2), loop and composite/write tools (§3b–§3e), result shaping (§3f), the doc contract in practice (§3g), removal and refusals (§4–§7). Load when you are about to write an edit and want the exact JSON and a real body to model.references/concepts.md— how an LLM turns tool definitions into a call (select from name+description, fill from schema+examples), toolset-size limits, response shaping, the safe replacement protocol, and the action-policy model, with cited sources. Read once per project, before your first candidate.references/doc-contract.md— the full documentation contract for one tool: what/when/when-not, important-points, error conditions, per-parameter units and formats, one always-valid example. Load when the fix is documentation rather than code.references/pitfalls.md— edits that look like improvements and regress (stripped error conditions, cosmetic rewording, task-overfitted descriptions, composite sprawl, example dumps, an exposed primitive behind a wrapper), each with how to detect it. Read before shipping a docs-only candidate, and when an accepted candidate barely moved the metric.references/field-notes.md— observations from real runs: how much of a docstring the runtime actually delivers, why enriching a return is not free, a docstring header that broke tool registration, and a return shape that corrupted the feedback signal. Read before your first candidate on a new harness, and whenever an edit "verified" green without an explanation you can point at.references/optimizer-playbook.md— what the authored optimizer INSTRUCTIONS must demand whentoolsis 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. Read when authoring or reviewing those instructions (intakepoints here rather than inlining it).
Files (cap-evolve)
-
references
-
concepts.md 11.1 KB
# Concepts — optimizing an agent's own tool surface > The mental model behind the `tools` capability: how an LLM turns a set of tool > definitions into a tool call, what each part of a definition controls, and why > the editable surface is governed by an action policy. Grounded in provider tool > docs, function-calling benchmarks, and prompt-optimization research. ## Contents - 1. The model never sees your code — it sees the definitions - 2. Two decisions, two levers: SELECT, then FILL - 3. More tools is not better - 4. Composite tools collapse fragile chains - 5. The action policy is the safety boundary - 6. Automatic optimization of tool text - 7. Output / response shaping - 8. The safe tool-replacement protocol - Sources ## 1. The model never sees your code — it sees the definitions At call time the model is given, for every available tool, a serialized block: ``` name an identifier (e.g. get_order) description plaintext: what it does, when to use it, when NOT to parameters a JSON Schema (types, properties, required, enum, descriptions) examples optional concrete example calls / inputs ``` This is exactly the contract of both major provider APIs and of the Model Context Protocol: a tool is `{name, description, input_schema/inputSchema}`. The implementation is invisible to the model. **Therefore every selection and argument error is, first, a *definition* problem — not a code problem** (unless the handler is genuinely buggy, which `code` edits address). ## 2. Two decisions, two levers: SELECT, then FILL - **Selection** — *which* tool, or none. Driven by **name + description**. Anthropic's tool-use guidance states the description "is by far the most important factor in tool performance" and recommends ≥3–4 sentences covering what / when / when-not. OpenAI's guide gives the same advice and adds: describe each parameter and its format, and use the prompt to say when *not* to call a function. - **Argument-filling** — *how* to populate the call. Driven by the **parameter schema** (types, `required`, `enum`) and **examples**. A JSON-Schema `enum` restricts a value to a fixed set, so the model picks rather than guesses; structured-output research shows a schema can *guarantee* conformance rather than merely suggest it. Mapped to the action kinds this capability exposes: `description` → selection; `params`/`schema`/`examples` → filling; `code` → behavior; `add`/`remove`/ `compose` → the shape of the choice set itself. ## 3. More tools is not better Selection accuracy degrades as the toolset grows and as tools overlap: - The **Berkeley Function-Calling Leaderboard** evaluates name/required-param/ type correctness and includes a dedicated *relevance-detection* category — measuring whether a model hallucinates a call when no tool fits. - **Gorilla** and **ToolLLM/ToolBench** found that even strong models hallucinate API usage at scale; ToolLLM had to add a neural *retriever* to navigate 16k+ tools, and Gorilla showed a document retriever sharply cuts hallucination. - **MetaTool** separates "is a tool needed?" from "which tool?" and finds selection is the harder, still-unsolved half. Design implication: prefer **fewer, sharper, non-overlapping tools.** Anthropic's "Writing tools for agents" makes the same point — overlapping tools "distract agents," a single tool can "consolidate functionality… under the hood," and even small description refinements "yield dramatic improvements." A concrete budget: OpenAI's function-calling guide recommends keeping the **active set under ~20 tools** per turn. **Namespacing** by service/resource (`orders_search` vs `users_search`, `payments_charge` vs `payments_refund`) reduces selection ambiguity as the library grows, and `user_id` selects better than a bare `user`. Argument-filling reliability scales with how *closed* the schema is: use **`enum`** for every closed value set, the provider's **strict / schema-validated mode** where available (so output conforms rather than merely suggesting), per-parameter **units/format/default**, and **`input_examples`** for nested or format-sensitive params. Don't ask the model to fill an argument the code already knows — bind it in a wrapper. ## 4. Composite tools collapse fragile chains When a trace repeatedly shows the same multi-call sequence going wrong (wrong order, forgotten step, mis-threaded IDs), a `compose` edit adds one higher-level tool whose code calls the existing handlers. This trades a brittle multi-turn plan the model must reconstruct each time for a single deterministic call. It is only worth it when the chain is *frequent and error-prone* — otherwise it just enlarges the choice set and hurts selection (§3). Three sub-cases, all benchmark-agnostic, recur in practice: - **Loop-in-one-call.** When the agent calls the *same* primitive N times in its own context — once per id, once per date, once per route — a tool that takes the list and loops inside one call removes N−1 turns and the chance of dropping a result. This is the single most common waste in real traces. - **Rule/invariant enforcement.** When the backend does not itself enforce a precondition or a required order (read-before-write, "only if cancellable"), put the check in the composite's code. A violation becomes a clean refusal the model can react to, instead of a silent wrong-state write. - **Normalization / richer return.** Resolve ids, attach related records, or coerce units inside the tool so the model gets a ready-to-use result. Keeping error information matters here too: a tool's documented failure modes (what it `Raises`/returns on error) are part of the contract the model reasons over — "Tool Documentation Enables Zero-Shot Tool-Usage" (arXiv:2308.00675) finds documentation, not examples, is what carries usage. Deleting error conditions to shorten a description removes guidance and is a common *non-improving* edit. ## 5. The action policy is the safety boundary `inputs/policy.json` lists the allowed edit kinds. It exists because the same artifact is edited in different trust settings. Rewording a description is low risk; rewriting a handler's `code` or changing a `schema` other systems depend on is high risk. The policy lets you grant exactly the blast radius you intend: - Frozen API / shared schema → allow `["description","params","examples"]` only. - You own everything → allow the full set (the default in this capability). This mirrors the *mutation-lock* idea from agent-optimization tooling: let an automatic optimizer change the safe surface, forbid the rest. `apply()` reports every refusal, so an over-tight policy is visible, not silent. ## 6. Automatic optimization of tool text The same loop a human runs here — propose a description/schema edit, score it on a held-out task set, keep what helps — is what automatic prompt/instruction optimizers do. **GEPA** evolves prompt/instruction text by reflecting in natural language over sampled trajectories and reports beating RL (GRPO) and DSPy's MIPROv2 on several tasks; **DSPy** optimizers (MIPROv2, GEPA) tune instructions and demonstrations against a metric. Tool descriptions and examples are exactly this kind of optimizable text, which is why this capability `materialize`s them as named components an optimizer can rewrite. ## 7. Output / response shaping Selection and filling decide the *call*; the **response** decides the next turn. Anthropic's "Writing tools for agents" treats response design as a first-class lever: - **Return high-signal fields, drop noise.** Internal uuids, mime types, thumbnail urls, and audit columns inflate context and distract. Project to the fields the agent acts on. - **Stable, human-readable ids over raw UUIDs.** Long opaque identifiers are mis-copied and hallucinated; surface a readable handle (`order_id="A-1042"`) and keep the UUID only if a later call truly needs it. - **Pagination / filtering / truncation with sane defaults**, plus a **`verbosity` / `response_format`** control so the model can request `concise` vs `full` rather than always paying for the largest payload. - **Errors are a steering surface.** An `isError` result with a specific, example-bearing message ("amount must be whole cents; got 12.99 → pass 1299") lets the model self-correct; an opaque traceback or code teaches it nothing and it retries the same bad call. Treat the error string as instructions to the next turn, not a log line. (Tool Documentation Enables Zero-Shot Tool-Usage, arXiv:2308.00675, finds documented behavior — including failure modes — is what carries usage.) ## 8. The safe tool-replacement protocol Observed in real runs: optimizers add wrappers but never remove the primitives, so the unsafe path survives — or they bare-remove a tool and strand the tasks that needed it. Both are regressions. The safe sequence to replace/consolidate a tool: 1. **ADD a wrapper** whose body *calls* the existing primitive after the extra validation/normalization/steps you want guaranteed (it delegates, never re-implements). 2. **VERIFY** it (`validate`; confirm the body calls the primitive and returns a sane result). 3. **SWAP the registration** — `remove` the raw primitive from the exposed set and expose the wrapper. Never bare-remove without a replacement that calls the original. Add-verify-swap makes the safe path the only path with no coverage gap (and keeps the count lean — one tool subsuming a primitive beats two overlapping ones). ## Sources - Anthropic — Define tools / tool-use implementation (descriptions are the #1 factor; ≥3–4 sentences; `input_examples`): https://platform.claude.com/docs/en/docs/agents-and-tools/tool-use/implement-tool-use - Anthropic — Tool use overview (auto selection from descriptions; tools injected into the system prompt): https://platform.claude.com/docs/en/docs/build-with-claude/tool-use/overview - Anthropic Engineering — Writing effective tools for agents (consolidation, overlap distracts, namespacing, small refinements help): https://www.anthropic.com/engineering/writing-tools-for-agents - OpenAI — Function calling guide (name/description/parameters; describe each param; say when not to call; aim for <20 functions): https://developers.openai.com/api/docs/guides/function-calling - OpenAI — Structured outputs (a JSON Schema can guarantee conformance): https://developers.openai.com/api/docs/guides/structured-outputs - JSON Schema — `enum` (restrict a value to a fixed set): https://json-schema.org/understanding-json-schema/reference/enum - Berkeley Function-Calling Leaderboard (AST eval + relevance detection): https://gorilla.cs.berkeley.edu/leaderboard.html - Gorilla: LLM Connected with Massive APIs (arXiv:2305.15334): https://arxiv.org/abs/2305.15334 - ToolLLM: Mastering 16000+ Real-world APIs (arXiv:2307.16789): https://arxiv.org/abs/2307.16789 - MetaTool: Deciding Whether and Which Tool to Use (arXiv:2310.03128): https://arxiv.org/abs/2310.03128 - Tool Documentation Enables Zero-Shot Tool-Usage (arXiv:2308.00675): https://arxiv.org/abs/2308.00675 - GEPA: Reflective Prompt Evolution (arXiv:2507.19457): https://arxiv.org/abs/2507.19457 - DSPy — optimizers tune instructions/demos against a metric: https://dspy.ai/ -
doc-contract.md 2.8 KB
# The documentation contract for one tool Load this when the fix is documentation rather than code — you are sharpening a description, per-parameter docs, or error text and want to know what a complete tool doc contains. A worked, fully-documented tool is `examples.md` §3g. ## Every tool needs all of these 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. ## Why fewer, sharper tools beat many vague ones 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** — so a documentation pass that reduces overlap between two siblings is worth more than one that polishes either alone. -
examples.md 23.7 KB
# Examples — worked tool edits Each example is an edit you would emit to `apply()`. Edit shape: `{"tool": <name>, "kind": <action>, "value": <...>}`. For `add`/`compose` the value is a full tool def; for `remove` the value is ignored. ## Contents - [0. Turning N prose rules into N in-body checks (the DEFAULT edit)](#0-turning-n-prose-rules-into-n-in-body-checks-the-default-edit) - [1. Selection fix — sharpen a vague description](#1-selection-fix--sharpen-a-vague-description) - [2. Argument-filling fix — close the value set with an enum](#2-argument-filling-fix--close-the-value-set-with-an-enum) - [3. Collapse a fumbled chain — compose](#3-collapse-a-fumbled-chain--compose) - [3b. Collapse repeated primitive calls — a loop-in-one-call tool](#3b-collapse-repeated-primitive-calls--a-loop-in-one-call-tool) - [3c. Validation / rule-enforcement tool — wrap, then delegate](#3c-validation--rule-enforcement-tool--wrap-then-delegate-then-remove-the-primitive) - [3c-bis. Validate-and-normalize inputs before a primitive](#3c-bis-validate-and-normalize-inputs-before-a-primitive) - [3c-ter. Wrong ARGUMENT the tool could validate](#3c-ter-wrong-argument-the-tool-could-validate--resolvevalidate-against-state-return-available) - [3c-quater. A required, eligible action abandoned via bail-out](#3c-quater-a-required-eligible-action-abandoned-via-bail-out--escalation--encapsulate-the-batch-as-a-composite-write) - [3d. Keep failure modes — improve, do not delete, `Raises:`](#3d-keep-failure-modes--improve-do-not-delete-raises) - [3e. Make a STALLED action un-skippable — a composite WRITE tool](#3e-make-a-stalled-action-un-skippable--a-composite-write-tool-then-remove-the-primitives) - [3f. Shape the result — high-signal fields, readable ids, actionable errors](#3f-shape-the-result--high-signal-fields-readable-ids-actionable-errors) - [3g. A comprehensively documented tool (the doc contract)](#3g-a-comprehensively-documented-tool-the-doc-contract) - [4. Shrink an overlapping toolset — remove + consolidate](#4-shrink-an-overlapping-toolset--remove--consolidate) - [5. Behavior bug — code edit](#5-behavior-bug--code-edit) - [6. A policy refusal (what tightening looks like)](#6-a-policy-refusal-what-tightening-looks-like) - [7. SECONDARY (last resort) — a passthrough / reasoning-only tool](#7-secondary-last-resort--a-passthrough--reasoning-only-tool) **Ordered by leverage.** The DEFAULT, most common edit is §0 — editing the BODY of an EXISTING tool to convert a violated prose rule into an in-body check. The other PRIMARY edits are the code-bearing tools in §3b (workflow/loop) and §3c (validation/rule-enforcement) — a deterministic body beats a prompt sentence. Reach for the description/schema edits (§1, §2) *after* asking "can this rule be code in the existing body instead?" A passthrough / reasoning-only tool (§7) is the SECONDARY, last-resort form — prose in a tool's costume. **First-class failure→fix patterns** (diagnose for these first): a wrong ARGUMENT the tool could validate → resolve/validate against state, return `available=[...]` (§3c-ter); a required eligible action abandoned via bail-out / transfer-to-human → encapsulate the batch as a COMPOSITE WRITE (§3c-quater, §3e); a recoverable error that strands the agent → an enriched RETURN that names what's wrong + the valid options + the next action (§3f). Always VERIFY the fix fires on the exact failing-trace arguments before shipping it. ## 0. Turning N prose rules into N in-body checks (the DEFAULT edit) The most common high-leverage edit is NOT adding a tool — it is editing the BODIES of EXISTING tools so a rule the agent keeps violating becomes code it cannot skip. Most violated textual rules govern a tool that already exists, so expect to emit SEVERAL `code` edits per iteration, one per violated rule. Each example below edits an existing handler in place (no new tool, no `remove`). **0a. Precondition guard — refuse when the rule is not met.** Prose rule: "only cancel a record that is still cancellable." Edit the existing `cancel_record` body to enforce it: ```json { "tool": "cancel_record", "kind": "code", "value": "def cancel_record(record_id):\n rec = get_record(record_id)\n if not rec.get('cancellable'):\n raise ValueError(\"not cancellable; reason=\" + rec.get('status','unknown') + \"; offer a change_record instead\")\n return _backend.cancel(record_id)" } ``` **0b. Unit / format normalization — coerce the field, then validate.** Prose rule: "amounts are in whole US cents." Edit the existing `charge` body to normalize the field so a dollars-vs-cents mistake can't corrupt the write: ```json { "tool": "charge", "kind": "code", "value": "def charge(record_id, amount):\n amount = int(round(amount))\n if amount <= 0:\n raise ValueError(f\"amount must be a positive integer in whole US cents, got {amount!r}\")\n return _backend.charge(record_id, amount)" } ``` **0c. Actionable error — name the valid options on refusal.** Prose rule: "the payment method must already be on the record." Edit the existing `book` body to check it and raise an error the model can recover from on the next turn: ```json { "tool": "book", "kind": "code", "value": "def book(record_id, payment_id):\n methods = {m['id'] for m in get_record(record_id)['payment_methods']}\n if payment_id not in methods:\n raise ValueError(f\"payment method {payment_id!r} not on file; available={sorted(methods)} — pass one of these\")\n return _backend.book(record_id, payment_id)" } ``` **0d. Required-order guard — enforce read-before-write in the body.** Prose rule: "never update a record you have not fetched this turn." Edit the existing `update_record` body so the read is part of the write and the staleness check is guaranteed: ```json { "tool": "update_record", "kind": "code", "value": "def update_record(record_id, field, value):\n rec = get_record(record_id)\n if rec.get('locked'):\n raise ValueError(f\"record {record_id} is locked (status={rec.get('status')}); unlock or escalate before updating\")\n return _backend.update(record_id, field, value)" } ``` These four counterbalance the new-tool-heavy examples below: each takes ONE prose rule and lands it as an in-body check on the tool that already owns it. A docstring-only or new-tool-only iteration that leaves these rules as prose is under-used. ## 1. Selection fix — sharpen a vague description Trace symptom: agent calls a generic `query` tool for everything and fails on order lookups. ```json { "tool": "query", "kind": "description", "value": "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 already in this thread). Do NOT use for free-text search across orders — use search_orders for that." } ``` Why it works: the description now states *what*, *when*, and *when not*, and names the sibling tool to disambiguate (§1, §3 of concepts.md). ## 2. Argument-filling fix — close the value set with an enum Trace symptom: agent sends `status="done"`, backend expects `"fulfilled"`. ```json { "tool": "search_orders", "kind": "schema", "value": { "type": "object", "properties": { "status": { "type": "string", "enum": ["pending","fulfilled","cancelled"], "description": "Order status to filter by." }, "since": { "type": "string", "description": "ISO-8601 date, e.g. 2025-06-14." } }, "required": ["status"] } } ``` Why it works: `enum` turns a guess into a pick; the `since` description pins the exact format. (Requires `schema` to be allowed by policy.) ## 3. Collapse a fumbled chain — compose Trace symptom: 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 exactly one order.", "parameters": { "type": "object", "properties": { "q": { "type": "string" } }, "required": ["q"] }, "code": "def find_order(q):\n hit = search_orders(q)[0]\n return get_order(hit['id'])" } } ``` ## 3b. Collapse repeated primitive calls — a loop-in-one-call tool Trace symptom: the agent calls `get_record(id)` once per id (e.g. fetching every record a user owns, one at a time), or calls `search(origin, dest, date)` once per route/date combination — many calls, results sometimes 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 {\"id\":..., \"error\":...} 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" } } ``` Why it works: N fragile turns become 1 deterministic call; the loop and the error handling live in code the model cannot get wrong. ## 3c. Validation / rule-enforcement tool — wrap, then delegate (then remove the primitive) Trace symptom: a write that must be preceded by a read, or whose precondition the backend does not itself enforce, keeps producing wrong-state failures (the agent skips the check or mis-reads it). The rule is GENERAL (it always applies), so it belongs in code, not in a prompt sentence the model can forget. Emit two edits together: `compose` the safe wrapper, then `remove` the raw primitive so the only reachable path is the validated 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 read the 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)" } }, { "tool": "cancel_record", "kind": "remove" } ] ``` Why it works: the read-before-write order and the precondition are guaranteed in code; a violation is a clean refusal, not a corrupted write. Removing the raw `cancel_record` keeps the surface lean and makes the unsafe path unreachable — the model cannot route around the check. ## 3c-bis. Validate-and-normalize inputs before a primitive Trace symptom: the agent calls `charge_payment` with the amount in the wrong unit, or against a method that isn't on file, and the raw primitive happily errors mid-task. A wrapper normalizes the input, checks the precondition, then delegates. ```json { "kind": "compose", "value": { "name": "charge_payment_safely", "description": "Charge `amount` (whole US cents) to `payment_id` after confirming the method is on the user's profile. Normalizes the amount and refuses (returns an error) if the method is not on file, so the charge never errors mid-task.", "parameters": { "type": "object", "properties": { "payment_id": { "type": "string" }, "amount": { "type": "integer" } }, "required": ["payment_id", "amount"] }, "code": "def charge_payment_safely(payment_id, amount):\n amount = int(round(amount))\n methods = {m['id'] for m in get_user_details()['payment_methods']}\n if payment_id not in methods:\n return {'error': 'payment method not on file', 'available': sorted(methods)}\n return charge_payment(payment_id=payment_id, amount=amount)" } } ``` Why it works: validate → normalize → enforce → delegate, all in code; the model hands over an id and an amount and cannot mis-route the call. ## 3c-ter. Wrong ARGUMENT the tool could validate — resolve/validate against state, return `available=[...]` Trace symptom (FIRST-CLASS): the agent calls a write with an id / reference / count that is NOT consistent with the agent-visible state — an item id not in the record, a quantity exceeding what's available, a reference to a resource that doesn't exist for this entity. The right tool, the wrong argument. Never let the write proceed on an unvalidated reference: wrap it in a body that RESOLVES/VALIDATES the argument against the current state and, on mismatch, returns the valid options (`available=[...]`) or raises an actionable error. ```json { "kind": "compose", "value": { "name": "remove_item_safely", "description": "Remove `item_id` from `record_id` after confirming the item is actually on the record. Validates the id against the record's current items and refuses (returning the valid options) if it is not present, so the write never corrupts state on a stale or wrong id.", "parameters": { "type": "object", "properties": { "record_id": { "type": "string" }, "item_id": { "type": "string" } }, "required": ["record_id", "item_id"] }, "code": "def remove_item_safely(record_id, item_id):\n items = {it['id'] for it in get_record(record_id)['items']}\n if item_id not in items:\n return {'error': f'item {item_id!r} not on record {record_id!r}', 'available': sorted(items), 'next': 'pass one of available'}\n return remove_item(record_id, item_id)" } } ``` Why it works: the argument is resolved against the live state in code; a wrong/stale reference becomes a clean refusal that NAMES the valid options, so the model corrects on the next turn instead of corrupting the record. (Verify the fix: run this body on the exact id from the failing trace and confirm it returns `available` rather than calling through.) ## 3c-quater. A required, eligible action abandoned via bail-out / escalation — encapsulate the batch as a COMPOSITE WRITE Trace symptom (FIRST-CLASS, behavioral): the agent escalates / hands off to a human / bails out instead of performing a REQUIRED action it was eligible to do itself — often a batch of similar writes (cancel each eligible line, refund each qualifying charge). This is a behavioral STALL, not a missing capability; a "don't bail out" prose rule does not fix it. Encapsulate the eligible-action batch in ONE composite WRITE tool whose body executes the steps in code, skipping ineligible items with a recorded reason — then `remove` the raw primitives so the batch is the path. ```json [ { "kind": "compose", "value": { "name": "process_eligible_items", "description": "Process EVERY eligible item on `record_id` in one call: applies the action to each item that meets the precondition and SKIPS the rest with a reason, returning a per-item result. Use this instead of escalating or handing off when the items are processable — there is no separate per-item write tool.", "parameters": { "type": "object", "properties": { "record_id": { "type": "string" } }, "required": ["record_id"] }, "code": "def process_eligible_items(record_id):\n rec = get_record(record_id)\n results = []\n for it in rec['items']:\n if not it.get('eligible'):\n results.append({'id': it['id'], 'skipped': it.get('reason', 'ineligible')})\n continue\n results.append({'id': it['id'], 'result': process_item(record_id, it['id'])})\n return {'record_id': record_id, 'processed': results}" } }, { "tool": "process_item", "kind": "remove" } ] ``` Why it works: the whole eligible-action batch runs the moment the tool is called, so the agent can no longer hand off a task it was equipped to finish; ineligible items are skipped with a reason rather than blocking the batch. (Verify: run the body on the record from the bail-out trace and confirm it processes the eligible items.) ## 3d. Keep failure modes — improve, do not delete, `Raises:` Anti-symptom: an optimizer "cleaned up" a description by deleting its `Raises:` section. Do the opposite — keep the error conditions and pair each with the recovery action. ```json { "tool": "charge_payment", "kind": "description", "value": "Charge `amount` (whole US cents) to the payment method `payment_id` from the user's profile. Use after the user confirms the total. Fails if the payment method is not on file (pick another from get_user_details) or if a gift-card balance is below `amount` (split across methods or choose a card). Example: charge_payment(payment_id='gift_card_42', amount=1299)." } ``` Why it works: the model now knows the units (cents), the precondition (method on file), and exactly what to do on each failure — instead of retrying the same bad call. ## 3e. Make a STALLED action un-skippable — a composite WRITE tool (then remove the primitives) Trace symptom (the most common behavioral failure): the agent analyzes a multi-step change, explains the plan, sometimes even gets the user's confirmation — and then **never issues the write calls and stops**, leaving the task half-done. No prose rule ("be sure to apply the change", "always act after confirming") reliably fixes this; it is behavioral, not a knowledge gap. The fix is to encapsulate the WHOLE action as one tool whose body performs every step in code, so the moment the agent calls it the action is complete and cannot be skipped mid-conversation. Then `remove` the raw write primitives so the composite is the only path. ```json [ { "kind": "compose", "value": { "name": "apply_change_plan", "description": "Apply an ENTIRE multi-step change to one record in a single call: validates every step, then performs them in order via the underlying writes, and returns the final record. Use this for any change of one or more steps instead of issuing the writes yourself — there is no separate per-step write tool.", "parameters": { "type": "object", "properties": { "record_id": { "type": "string" }, "steps": { "type": "array", "items": { "type": "object", "properties": { "op": { "type": "string", "enum": ["add","remove","update"] }, "field": { "type": "string" }, "value": {} }, "required": ["op","field"] } } }, "required": ["record_id","steps"] }, "code": "def apply_change_plan(record_id, steps):\n rec = get_record(record_id)\n for s in steps:\n if s['op'] not in ('add','remove','update'):\n return {'error': 'bad op', 'step': s}\n for s in steps:\n rec = update_record(record_id, s['op'], s['field'], s.get('value'))\n return {'record_id': record_id, 'applied': len(steps), 'record': rec}" } }, { "tool": "update_record", "kind": "remove" } ] ``` Why it works: the analyze→apply sequence lives entirely in the tool body, so a single call performs all of it — the agent can no longer narrate a plan and then fail to execute it. Removing the raw `update_record` makes the composite the only reachable write path, so the stall cannot recur by routing around it. ## 3f. Shape the result — high-signal fields, readable ids, actionable errors Trace symptom: a tool returns the raw row (uuids, mime, audit columns); the model hallucinates ids and the response floods context. And when a call is invalid, the handler raises an opaque traceback the model can't recover from. ```json { "tool": "get_order", "kind": "code", "value": "def get_order(order_id):\n row = db.orders.find(order_id)\n if row is None:\n return {'error': f\"no order {order_id!r}; search with search_orders(query=...) to find the id\"}\n return {\n 'order_id': row['public_ref'], # stable, human-readable, not the uuid\n 'status': row['status'],\n 'items': [{'sku': i['sku'], 'qty': i['qty']} for i in row['items']],\n 'total_cents': row['total_cents'],\n }" } ``` Why it works: the projection drops noise, returns a readable `order_id`, and the not-found path is an **actionable** message that names the recovery tool — the model self-corrects instead of retrying the same bad call. Add a `verbosity`/`response_format` param when callers sometimes need the full row. ## 3g. A comprehensively documented tool (the doc contract) Every tool — primitive or wrapper — should carry: a crisp what/when/when-not, an "important points" note, a Raises/errors section, per-parameter docs with units/format/default, and one generic example. ```json { "tool": "charge_payment", "kind": "description", "value": "Charge `amount` to a payment method on the user's profile and return the receipt.\nUse after the user confirms the total; do NOT use to quote a price (use get_quote).\nImportant: amounts are in WHOLE US CENTS (1299 = $12.99); the method must already be on file.\nRaises: 'method not on file' — pick another id from get_user_details; 'gift-card balance below amount' — split across methods or choose a card.\nExample: charge_payment(payment_id='card_1', amount=1299)" } ``` Pair it with a schema whose `amount` param description says "Whole US cents (integer), e.g. 1299 for $12.99" and a `payment_id` with an `input_examples` entry. ## 4. Shrink an overlapping toolset — remove + consolidate Trace symptom: `create_pr`, `review_pr`, `merge_pr` all present; agent keeps choosing the wrong one. Consolidate into one tool with an `action` parameter, then remove the three originals. ```json [ { "kind": "add", "value": { "name": "pull_request", "description": "Create, review, or merge a pull request. Set action to choose the operation.", "parameters": { "type": "object", "properties": { "action": { "type": "string", "enum": ["create","review","merge"] }, "id": { "type": "string" } }, "required": ["action"] } } }, { "tool": "create_pr", "kind": "remove" }, { "tool": "review_pr", "kind": "remove" }, { "tool": "merge_pr", "kind": "remove" } ] ``` ## 5. Behavior bug — code edit Trace symptom: `get_order` returns the raw DB row including internal fields the model then leaks. Fix the handler to return a clean projection. ```json { "tool": "get_order", "kind": "code", "value": "def get_order(order_id):\n row = db.orders.find(order_id)\n return {k: row[k] for k in ('id','status','items','shipping')}" } ``` ## 6. A policy refusal (what tightening looks like) With `inputs/policy.json = {"allow": ["description","params","examples"]}`, the schema edit in example 2 is refused: ```json { "edit": {"tool":"search_orders","kind":"schema", ...}, "reason": "action 'schema' not allowed by policy" } ``` The fix is either to widen the policy deliberately or to express the change as an allowed edit (e.g. add the enum guidance via a `params` description instead). ## 7. SECONDARY (last resort) — a passthrough / reasoning-only tool Trace symptom: the agent keeps skipping a rule. The WEAK fix is a tool whose body does no real work — it returns its argument and parks the rule in the docstring: ```json { "kind": "compose", "value": { "name": "check_cancellable", "description": "Before cancelling, state here whether the record is cancellable and why.", "parameters": { "type": "object", "properties": { "reasoning": { "type": "string" } }, "required": ["reasoning"] }, "code": "def check_cancellable(reasoning):\n return {'noted': reasoning}" } } ``` Why it under-performs: the body enforces nothing — the model can write any `reasoning` and still proceed, exactly like ignoring a prompt sentence. **Prefer the §3c code-bearing wrapper** (`cancel_record_safely` reads the record and refuses in code, then `remove` the raw primitive) so the rule is guaranteed, not merely requested. Only keep a reasoning-only tool when the step genuinely cannot be made deterministic. -
field-notes.md 5.6 KB
# Field notes — observations from real optimization runs Evidence gathered while optimizing tool surfaces on specific harnesses and runners. These are **observations, not rules**: the numbers belong to the runs that produced them, and the harness details (Python docstrings, one framework's schema builder) are that framework's, not this capability's. Read them for the *mechanism*, then verify the equivalent on your own runtime. Read before your first candidate on a new harness, and whenever an edit "verified" green without an explanation you can point at. ## Contents - [1. Not all of a tool's documentation reaches the model](#1-not-all-of-a-tools-documentation-reaches-the-model) - [2. Enriching a return is a hypothesis, not a free win](#2-enriching-a-return-is-a-hypothesis-not-a-free-win) - [3. A docstring header can break tool REGISTRATION](#3-a-docstring-header-can-break-tool-registration) - [4. Changing a return SHAPE can corrupt the learning signal](#4-changing-a-return-shape-can-corrupt-the-learning-signal) ## 1. Not all of a tool's documentation reaches the model A docstring is not the wire schema. One benchmark harness built each tool's schema `description` from the docstring **summary plus the prose before `Args:`**, and dropped the `Returns:` section entirely. Measured across a 14-tool toolset there: **5469 of 12929 docstring characters (42%) never reached the model**, and on the one tool whose return had been documented most carefully it was **1791 of 1906 — 94% dropped**. Rounds of behavioural guidance had been written into that void, and one edit credited as "verified" turned out to work only because the return **VALUE** changed shape (which the model does see at call time), not because anything documented it. So on that harness there were three delivered surfaces — the summary and pre-`Args:` prose, the per-parameter `Args:` descriptions, and the returned value itself — and one that looked identical and did nothing. **Render the live toolset the way the runtime builds it and count the delivered characters per candidate** rather than trusting the file. Your runtime will have its own cut line; find it before you write into it. ## 2. Enriching a return is a hypothesis, not a free win A tool return is re-read on every later turn, so adding to it is not free — measure it. Measured on a multi-turn tool-use benchmark with a mid-tier runner: one round accepted an edit that CONSTRAINED behaviour (in-code preconditions, val 0.5889 → 0.6778, +8.9pp) while **four separate edits that ADDED information all landed at or below the same parent**: richer docs + derived facts merged onto the winner **0.6444**, composite tools **0.6556**, argument derivation **0.6666** (identical to that round's null control), and a structural policy rewrite **0.5777** (also identical to the control). Multi-turn rollouts have a step budget and the whole conversation is re-read each turn, so verbose returns crowd out the signal they were meant to supply. Keep what the agent ACTS on — amounts, eligibility, the corrective hint — and cut what it can read off the object it already has. When in doubt, run the subtraction as its own gated candidate; it is as legitimate a hypothesis as the addition, and here the additive ones lost. ## 3. A docstring header can break tool REGISTRATION Observed: adding a worked example under an `Example:` / `Examples:` header made `docstring_parser` return a `DocstringExample` object; that harness's `Tool` model required `examples: list[str]`, so building the environment raised and **all 90 rollouts of that candidate died as `INFRASTRUCTURE_ERROR`** — an entire evaluation spent on a parse error, not on the edit. Keep the example text, but put it under ordinary prose (e.g. "A correct call looks like:"). **An import check is NOT a registration check** — the file imported fine; it was registration that failed. Prove the toolset still builds the way the runtime builds it before you spend rollouts. If the adapter exposes a render/validate helper, call that and keep it in the loop. Otherwise construct it directly, substituting your own runner's construction call for the last line, which is harness-specific: ```bash python -c "import sys; sys.path.insert(0,'<project>/adapters'); from adapter import Adapter; \ from pathlib import Path; Adapter().apply(Path('<candidate_dir>')); \ tools = <your harness's get_tools() call>; \ print(len(tools), sorted(t.name for t in tools))" ``` The same render is what lets you count delivered characters (§1). ## 4. Changing a return SHAPE can corrupt the learning signal This one is a caveat about the *measurement*, not about tool surfaces — keep it in mind when a defect appears without a cause. Optimizer-side code that parses tool returns to build feedback is written against the PRISTINE shape, and a candidate is entitled to change it. Observed: a candidate that nested summary objects under a list key the feedback code read as bare ids made that code `str()` the dicts, so its feedback claimed the id was *"not among"* the held ids — for calls whose id was perfectly valid. The reward was never affected (it came from the harness's own DB/action checks, which never read a tool's return), but one optimizer spent a whole iteration hunting a scoring bug that did not exist, and another concluded the key name was capping its score. Two lessons, in order: **audit the measurement before you believe a defect**, and make return-parsing tolerant of shapes a candidate may legitimately introduce (extract ids from `str` *or* `dict` entries) rather than forbidding the enrichment. The signal degrades exactly when the candidate is most interesting. -
optimizer-playbook.md 2.6 KB
# Optimizer playbook for the `tools` capability What the **authored optimizer instructions** must demand when `tools` is among the selected capabilities. `intake` writes those instructions (`.capevolve/project/optimizer/INSTRUCTIONS.md`) but stays capability-agnostic; this file is the `tools`-specific half it points at. Encode every item below into the authored INSTRUCTIONS — verbatim or tightened for the benchmark at hand. - [Depth mandate (tools wording)](#depth-mandate-tools-wording) - [The EXISTING-tool-code mandate](#the-existing-tool-code-mandate) - [The explicit TWO-PHASE subagent pattern](#the-explicit-two-phase-subagent-pattern) ## Depth mandate (tools wording) `intake` demands a substantial multi-root-cause pass in capability-neutral terms. When `tools` is selected, make that demand concrete with this snippet: > "Each iteration is a substantial, multi-root-cause pass. Diagnose ALL clusters > and fix as many as possible in ONE candidate — improve multiple tools' code, > validation, and return values/errors; add new tools; sharpen many tool docs; > and fix the prompt (only if `system-prompt` is ALSO among the selected > capabilities — on a `tools`-only run drop this clause and leave the prompt > alone) — together. Scope each fix to protect passing tasks; do NOT > trade breadth for caution. A single small edit is an under-used iteration." ## The EXISTING-tool-code mandate Demand: convert violated textual rules into in-code checks across MANY EXISTING tool bodies — most violated rules govern a tool that already exists, so the fix is an in-body guard there, not a new tool. State plainly: *a docstring-only iteration (or one that only adds a single new tool + rewords docstrings, leaving rules as prose) is under-used.* The edit classes this mandate ranges over are in [`../SKILL.md`](../SKILL.md) ("What you can change here", "Pick the lever by failure type"), which also points at the worked before/after diffs showing an in-body guard replacing a prose rule. ## The explicit TWO-PHASE subagent pattern Require: 1. **Phase 1 — diagnose fan-out.** One read-only subagent per trajectory-group → a tight issue list; the main agent dedups those into clusters. 2. **Phase 2 — implement fan-out.** One edit-subagent per ISSUE, each in its own worktree, each PREFERRING to edit the EXISTING tool's code body to enforce its rule. 3. **Merge.** The main agent merges all edits into ONE candidate. Point the optimizer at `./guidance/optimizer/<name>.md` for that agent's concrete trigger phrasing. Authored INSTRUCTIONS fail this playbook when they omit either mandate above. -
pitfalls.md 8.4 KB
# Pitfalls — editing a tool surface Failure modes that make a tool edit a regression rather than an improvement, and how to detect each from traces or from `validate`. ## Stripping error info / `Raises:` to "clean up" the description The error conditions a tool can raise are *guidance for the model*, not clutter. Knowing a call raises "balance too low" or "record not found" is what lets the model pick a different argument or a different tool instead of failing. Deleting that text removes a guard rail and typically does not improve selection at all. - **Detect:** an edit's only change is removing `Raises:`/error lines or other failure-mode text; the metric is flat or the model now makes the same bad call the error described. - **Fix:** keep failure modes in the description. If anything, make them more precise and pair each with what the model should do instead. ## Trying to fix a BEHAVIORAL stall with prose The agent analyzes, confirms, then fails to call the write tool and stops. This is the single most common — and most expensive — failure, and it is *behavioral*: the model already "knows" what to do and skips it. Rewording a docstring or adding "always act after confirming" does not fix a behavior the model already declined; the traces show those edits failing. - **Detect:** failing tasks where the trace contains the analysis/confirmation but no write call; "be sure to act"-style edits that don't move the metric. - **Fix:** move the whole action into a composite WRITE tool whose body performs every step (examples §3e), then `remove` the raw write primitives so completing the action is the only path. ## Wrapping a primitive but leaving it exposed A validation wrapper or composite achieves nothing if the raw primitive it wraps is still in the toolset — the model can call the primitive directly and reproduce the exact failure the wrapper was meant to prevent. Observed: optimizers add safe wrappers but never `remove` the primitives, so the unsafe path survives. - **Detect:** a wrapper/composite was added but the primitive it delegates to is still exposed; traces still show direct calls to the primitive. - **Fix:** pair every wrapper/composite with a `remove` of the primitive, unless the primitive is still independently needed for a different, safe purpose. ## Wrong arguments to a write (partial-credit failures) A task can fail partway — the right write tool called with the wrong unit, a missing field, or an unresolved id — scoring partial credit, not zero. These are easy to overlook if you only look at fully-failing tasks. - **Detect:** partial-credit tasks whose feedback names a malformed write argument (wrong unit, id not on file, missing required field). - **Fix:** a normalize-then-call wrapper that coerces units, resolves ids, and checks the field/method is on file *before* calling the primitive, turning a corrupted write into a clean refusal (examples §3c-bis). ## Cosmetic rewording that adds no always-true information Reflowing sentences, adding commas, or restating the obvious changes the text without changing what the model knows. It will not move behavior. - **Detect:** the diff has no new trigger, unit, allowed-value, default, or failure mode — just reworded prose. - **Fix:** add genuinely new, always-true content (when/when-not, argument semantics, an always-valid example), or reach for a loop/rule/composite tool. ## Overfitting a description to one task Putting a specific id, date, or city from a single task into a description overfits and can mislead on the next input. - **Detect:** the description names literal values that came from one trace. - **Fix:** describe the *shape* and *rules* that hold for every input; if you show an example, make it a generic well-formed one. ## Over-describing into contradiction Piling on "use when" clauses until two of them conflict makes selection *worse*, not better. A model resolves contradictory instructions unpredictably. - **Detect:** description has multiple, overlapping trigger conditions; selection is now inconsistent across near-identical inputs. - **Fix:** one crisp paragraph — what / when / when-not — and state the boundary only against the *nearest sibling* tool, not against every other tool. ## Schema and code drift apart You own the callers, so a `schema` change is "safe" — but only if the handler `code` matches it. A renamed/retyped parameter in the schema with an unchanged handler yields runtime errors the model can't recover from. - **Detect:** the tool starts erroring on well-formed calls after a schema edit. - **Fix:** change `schema` and `code` in the *same* edit batch, then run `validate`. In a frozen-API setting, lock both off via policy. ## Composite-tool sprawl A `compose` tool is only worth its slot in the choice set if the chain it replaces is frequent and error-prone. Adding composites for paths the agent already handles enlarges the toolset and *degrades* selection (more tools → worse relevance detection; see concepts.md §3). - **Detect:** new composite tools are rarely chosen, or selection accuracy on *other* tools dropped after you added them. - **Fix:** remove composites that don't earn their place; keep the surface small. ## Removing a rarely-but-critically-needed tool Low call-count is not the same as low value. A tool used in 2% of traces may be the only correct action in those traces. - **Detect:** removed a low-frequency tool; a previously-passing task class now fails with "no applicable tool." - **Fix:** remove for *overlap/confusion*, not for low frequency. Re-add and instead disambiguate via descriptions. ## Example dumps that hurt reasoning models A few examples sharpen formatting, but long blocks of examples can degrade reasoning-tuned models and crowd the context. - **Detect:** adding many `examples` lowered accuracy on a model that reasons. - **Fix:** encode the constraint in the *schema* (types, `enum`, formats) and keep one or two examples, not ten. ## Opaque errors and UUID-heavy / bloated responses A handler that raises a raw traceback (or returns a low-signal blob full of uuids, mime types, and audit columns) leaves the model blind: it hallucinates ids, re-fetches, and retries the same invalid call because the error told it nothing. - **Detect:** failing tasks where the trace shows the model copying a wrong id, or re-issuing the identical bad call after an error with an opaque message. - **Fix:** project to high-signal fields, surface a stable human-readable id (not the raw UUID), and return an **actionable** error that names the correct format or the recovery tool ("payment method not on file; available: [...]"). Errors are a steering surface (examples §3f). ## Vague names defeat good descriptions `lookup`, `query`, `do_it` select poorly no matter how good the description is — the name is read first and weighs heavily. - **Detect:** mis-selection persists after a description rewrite. - **Fix:** rename to a verb-noun that states the action and object (`get_order`, `search_orders`); namespace when domains overlap. ## Silent policy mismatch (avoided by design) `apply()` never silently drops a disallowed edit — it records it under `refused`. An optimizer that "did nothing" is usually hitting a too-tight policy. - **Detect:** `apply()` returns `changed: []` and a non-empty `refused`. - **Fix:** widen `inputs/policy.json` deliberately, or re-express the change as an allowed edit kind. ## The positive mirror — what actually moves accuracy and cuts calls Every pitfall above has a shape that works. When an accepted candidate barely moved the metric, check it against this list: - **A loop/composite tool** that collapses the repeated-primitive pattern from the traces (fetching records one id at a time, 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.
-
-
scripts
-
abstract.py 1.4 KB
"""tools capability — optimize an agent's OWN tool surface (the full action set). This capability owns the tool code, so its DEFAULT_POLICY allows every edit kind: reword descriptions, change parameter schema, edit tool ``code``, compose new tools from existing ones, and add/remove tools. (Contrast ``mcp-tool``, whose server is external and so forbids schema/code edits.) The artifact is ``tools.json``; the materialize/apply/validate mechanics are shared in ``cap_evolve.tool_surface`` — this module only declares the policy. """ from __future__ import annotations from pathlib import Path import _bootstrap # noqa: F401 from cap_evolve import tool_surface # tools owns its code → the FULL action set is allowed by default. DEFAULT_POLICY = {"allow": ["description", "params", "examples", "schema", "code", "add", "compose", "remove"]} def load_policy(capability_dir: Path) -> dict: return tool_surface.load_policy(capability_dir, DEFAULT_POLICY) def materialize(capability_dir: Path) -> dict: return tool_surface.materialize(capability_dir) def apply(capability_dir: Path, edits: list[dict] | None = None) -> dict: return tool_surface.apply(capability_dir, DEFAULT_POLICY, edits) def is_empty(capability_dir: Path) -> bool: return tool_surface.is_empty(capability_dir) def validate(capability_dir: Path) -> dict: return tool_surface.validate(capability_dir) -
check.py 3.2 KB
"""tools: by default the FULL action set is allowed (docs, schema, code, add/compose, remove).""" from __future__ import annotations import json import sys import tempfile from pathlib import Path import _bootstrap # noqa: F401 import abstract def main() -> int: report = {"skill": "tools", "ok": False, "problems": [], "notes": []} with tempfile.TemporaryDirectory() as d: cap = Path(d) (cap / "tools.json").write_text(json.dumps({"tools": [ {"name": "search", "description": "Search the web.", "parameters": {"type": "object", "properties": {"q": {"type": "string"}}}, "examples": ["search(q='weather')"]}, ]}), encoding="utf-8") # no policy.json -> default (full) policy rep = abstract.apply(cap, [ {"tool": "search", "kind": "schema", "value": {"type": "object", "properties": {"q": {"type": "string"}, "n": {"type": "integer"}}}}, {"tool": "search", "kind": "code", "value": "def search(q, n=10): ..."}, {"kind": "compose", "value": {"name": "search_top", "description": "search then top-n", "code": "def search_top(q): return search(q, 1)"}}, # remove-with-replacement: search_top subsumes search, so drop the primitive. {"tool": "search", "kind": "remove"}, ]) if rep["refused"]: report["problems"].append(f"full policy refused allowed edits: {rep['refused']}") if "schema:search" not in rep["changed"] or not any(c.startswith("compose") for c in rep["changed"]): report["problems"].append(f"expected schema+compose edits applied, got {rep['changed']}") if "remove:search" not in rep["changed"]: report["problems"].append(f"expected remove:search applied, got {rep['changed']}") names = [t.get("name") for t in json.loads((cap / "tools.json").read_text())["tools"]] if names != ["search_top"]: report["problems"].append(f"after compose+remove expected ['search_top'], got {names}") v = abstract.validate(cap) if not v["ok"]: report["problems"].append(f"validate failed: {v['problems']}") report["notes"].append("full action set (schema/code/compose/remove) allowed by default") # A tightened policy must REFUSE, not silently drop or silently apply. (cap / "policy.json").write_text( json.dumps({"allow": ["description"]}), encoding="utf-8") rep2 = abstract.apply(cap, [ {"tool": "search_top", "kind": "description", "value": "Search and return the top hit."}, {"tool": "search_top", "kind": "code", "value": "def search_top(q): return 1"}, ]) if not rep2["refused"]: report["problems"].append("tightened policy did not refuse a 'code' edit") if "description:search_top" not in rep2["changed"]: report["problems"].append(f"tightened policy dropped an allowed edit: {rep2['changed']}") report["notes"].append("tightened policy refuses disallowed kinds and reports them") report["ok"] = not report["problems"] print(json.dumps(report, indent=2)) return 0 if report["ok"] else 1 if __name__ == "__main__": sys.exit(main()) -
run.py 883 B
"""Expose a tools artifact as a Candidate and report the action policy + validity.""" from __future__ import annotations import argparse import json import sys from pathlib import Path import _bootstrap # noqa: F401 from cap_evolve import Candidate import abstract def main(argv=None) -> int: p = argparse.ArgumentParser(prog="tools") p.add_argument("--path", required=True, help="capability dir with tools.json (+ policy.json)") args = p.parse_args(argv) parts = abstract.materialize(Path(args.path)) policy = abstract.load_policy(Path(args.path)) v = abstract.validate(Path(args.path)) cand = Candidate(id="seed", component="tools", text_parts=parts, dir=str(args.path)) print(json.dumps({"candidate": cand.to_dict(), "policy": policy, "valid": v}, indent=2)) return 0 if v["ok"] else 1 if __name__ == "__main__": sys.exit(main()) -
_bootstrap.py 3.6 KB
"""Thin shim: locate cap_evolve, then defer to cap_evolve._bootstrap. Skill scripts ``import _bootstrap`` first. The real path-resolution logic lives ONCE in ``cap_evolve._bootstrap`` (so it can't drift across skills); this shim only has to find that package, which means a minimal upward walk for ``core/`` — the single bit of bootstrapping that genuinely must run before cap_evolve is importable. Everything else delegates. """ from __future__ import annotations import os import sys from pathlib import Path def _seed_path() -> None: """Minimal: put a dir containing the cap_evolve package on sys.path. ``CAPEVOLVE_CORE`` is honoured BEFORE any ambient import. An editable install of a *different* cap-evolve checkout registers a ``sys.meta_path`` finder, which outranks both ``sys.path`` and ``PYTHONPATH`` — so "cap_evolve imports fine" is not evidence that it imports the checkout you are standing in. Deferring to the ambient package here made an explicit override unreachable, and the symptom was a stale core silently answering for this one (``ModuleNotFoundError: cap_evolve.constraints`` from a checkout that predates that module). An explicit env var wins. """ env = os.environ.get("CAPEVOLVE_CORE") want = Path(env).resolve() if env else None if want and (want / "cap_evolve" / "__init__.py").exists(): loaded = sys.modules.get("cap_evolve") already = getattr(loaded, "__file__", None) if already and Path(already).resolve().parent.parent == want: return # right checkout already imported: touch nothing p = str(want) if p in sys.path: sys.path.remove(p) sys.path.insert(0, p) if loaded is not None: # Evicting a module makes a re-import yield a DIFFERENT object, so anything # already holding a reference fails an `is` check. Only ever do it when the # loaded package really is the wrong checkout — otherwise this "fix" becomes # the bug (it broke two identity assertions in core/tests exactly once). for name in [m for m in sys.modules if m == "cap_evolve" or m.startswith("cap_evolve.")]: sys.modules.pop(name, None) for finder in list(sys.meta_path): if "cap_evolve" in getattr(finder, "MAPPING", {}): sys.meta_path.remove(finder) return # A checkout's own core outranks an ambient install. Without this, a skill script run # from checkout X silently executed against checkout Y's cap_evolve (an editable install # registers a sys.meta_path finder, which outranks sys.path), and the only symptom was # missing modules — or, worse, a green result measured against the wrong tree. here = Path(__file__).resolve() own = next((p / "core" for p in here.parents if (p / "core" / "cap_evolve" / "__init__.py").exists()), None) if own is not None: os.environ.setdefault("CAPEVOLVE_CORE", str(own)) return _seed_path() try: import cap_evolve # noqa: F401 return except Exception: pass cands = [] for parent in here.parents: cands.append(parent / "core") cands.append(parent) for c in cands: if (c / "cap_evolve" / "__init__.py").exists(): p = str(c) if p not in sys.path: sys.path.insert(0, p) return _seed_path() from cap_evolve._bootstrap import ensure_core # noqa: E402 # Anchor the upward walk at THIS skill script's location (not the core module's). ensure_core(Path(__file__).resolve())
-
-
meta.yaml 353 B
component: capability name: tools summary: Optimize an agent's own tool surface — docs, parameter schema/API, tool code, composing tools that call existing tools, adding/removing tools. entry: scripts/run.py abstract: scripts/abstract.py check: scripts/check.py needs: [] provides: [candidate] compatible_with: optimizers: ["*"] algorithms: ["*"] -
SKILL.md 18.2 KB
--- 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, calls the same tool N times in a row, or has a confusing, redundant, or oversized toolset. Covers tool names and descriptions, parameter docs, tool schemas, handler code, function-calling accuracy, and adding or removing tools. 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**. It applies when the agent *owns* its tools — it implements the handlers, defines the wire schema, and controls every caller — so names, descriptions, parameter docs, in-description examples, the JSON Schema, *and the implementation code* are all fair game. (When the tools come from an external server you can only re-describe, not re-implement: that is `mcp-tool`, whose policy is tightened to documentation-only edits.) ## What you can change here **The tool's documentation AND its return value are what the agent SEES — make both clear and recovery-oriented.** The doc surface (description, important-notes, per-param, error/`Raises` text, 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. Confirm which parts of a docstring your runtime actually SENDS before writing into it — some frameworks discard whole sections, and guidance written into that void does nothing (`references/field-notes.md` §1). **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 sinks the whole candidate at the val gate. Never add an edit to hit a count, and never re-add a rule or 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. ## Pick the lever by failure type Each item is an edit class. In ONE pass, apply EVERY 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 ship in the same candidate. The in-body guard is the default strong move; reach for a documentation edit only after asking "can this rule be code in the existing body instead?" 1. **Edit the CODE of an EXISTING tool (reach for this FIRST for a rule violation).** 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, or actionable refusal into the body so correctness does not 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. A deterministic guard beats a sentence in a prompt: prose makes the model *more likely* to comply, code makes the right behavior the only thing that can happen. 2. **Add a composite atomic-WRITE tool** — for a stalled or abandoned multi-step action, encapsulate the ENTIRE action in one 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 call. Reach for this even though a write primitive already exists: the primitive is exactly what the agent declines to call. 3. **Add a discriminating-predicate guard** for an ACT-vs-REFUSE cluster — an in-body guard on the tool that owns the action, expressing the EXACT policy predicate, that refuses only when the qualifying condition is (or is not) met. It is the narrowest edit available here and the tool-side alternative to changing a global rule. 4. **Add a real targeted tool for a capability gap** — a task that needs a compute / composite / predicate tool it does not have stays failing after any docstring reword. Ship a tool the agent will CALL that changes the graded state — *Ex:* a `find_duplicate_records` it has no way to compute today, or a `search_logs` that returns the relevant lines instead of a raw dump. 5. **Add a loop tool** — replace N repeated single-item calls with one list call. *Ex:* `get_records(ids: [...])` replaces N× `get_record(id)`. 6. **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. 7. **Improve a tool's documentation** — sharpen description / important-notes / error conditions / per-param docs / examples; rename for least surprise. *Ex:* `lookup(record)` → `get_record(record_id: str)` with "returns an error object if not found." 8. **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:* an error returning "payment method not on file; available: ['card_1'] — pass one of these" instead of a raw traceback. Adding to a return is not free, though: it is re-read every turn, so treat enrichment as a hypothesis to gate, not a free win (`references/field-notes.md` §2). 9. **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. The two ways to waste an iteration: leaving a rule the agent keeps breaking as loose prose instead of a guard (or loosening a global permission rule instead of scoping a guard); and padding the candidate with low-value helper tools or cosmetic rewrites that move no graded task. ## Guardrails - **Encode deterministic logic in code, not prose** — a tool body the model cannot skip beats a sentence it can forget. A tool whose body enforces nothing (a `think()` / `check_policy()` passthrough with the rule only in its docstring) is prose in a tool's costume; reach for it only when the behavior genuinely cannot be made deterministic. - **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 the real loop, the real precondition check, the real calls to existing tools (`get_record(i)` — or `self.get_record(i)` if your adapter binds tools as methods). - **Never remove a tool without a capability-preserving replacement.** Add → verify → swap (`references/concepts.md` §8). Bare-removing strands every task that needed it; adding a wrapper but leaving the primitive exposed lets the model route around the guard and reproduce the original failure. - **Keep the toolset small and namespaced** — aim for **< ~20** active tools; selection degrades sharply past that. Prefer consolidating over piling on: when you add a safer or looped tool, `remove` the now-redundant primitive. - **Ship correct, bug-free code** — every code edit needs validation plus a `validate` run, and proof the toolset still *registers* (an import check is not a registration check — `references/field-notes.md` §3). - **Generalize, never hardcode.** Every 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, 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. The test for any edit: *would this help on a task the optimizer has never seen?* ## How agents fail (and how tools fix it) Map the trace symptom to the edit. This table is the single canonical statement of what to ship for what failure. The 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. 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). | Trace symptom | Fix | |---------------|-----| | **Wrong ARGUMENT the tool could validate** — a write whose id / reference / count / unit is not consistent with the agent-visible state. 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 current state, and on mismatch returns `available=[...]` or raises an actionable error naming what is wrong and what to pass instead. Never let a write proceed on an unvalidated reference. | | **The action never happens** — the agent analyzes, explains, even confirms, then never calls the write tool and stops; or it hands off / gives up on an action it could have completed. Task left half-done. | **Composite WRITE tool** (lever 2): one tool whose body performs the whole sequence — or the whole eligible-action batch, skipping any ineligible item with a recorded reason — then `remove` the raw primitives so completing it is the only path. Not a "be sure to act" prose rule. | | **`narrated_without_action`** — the strong form of the row above, and `diagnose` names it as its own cluster: the final message REPORTS the change as done, with specifics, and the trace holds no mutating call at all. The agent took the user's confirmation as its completion signal. | **Make "confirmed" and "executed" ONE call** (lever 2, structurally): a single write whose body performs the approved change, sharing the code path the confirmation handling already uses, then `remove` the primitives that let the two come apart. A prose reminder to call the tool has been tried on this class and rejected — the agent knows the rule and violates it. | | **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 is wrong + the valid options + the recommended next action (`{"error": "id not found", "available": [...], "next": "call search_x to resolve the id"}`) so the model self-corrects next turn. | | **The same primitive called N times in a row** — looping over a list in the agent's own context, burning turns and dropping or mis-threading results. | **Loop tool** (lever 5): 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 does not enforce, a normalization the model forgets. | **In-body guard / validation wrapper** (lever 1): enforce the rule in the body of the tool that owns it; `remove` the unguarded primitive if the safe path must be the only one. | | **A wrong ACT-vs-REFUSE call** — the agent acts where policy says refuse/escalate, or refuses where it should act. | **Discriminating-predicate guard** (lever 3) on the tool that owns the action. | | **Mis-selection** — the agent calls the wrong tool, calls none when one applied, or invents a tool that does not exist. | **Name + description fix**: selection is driven almost entirely by the name and description — sharpen what/when/when-not and the boundary against the nearest sibling (`references/doc-contract.md`). | | **Bad argument-filling** — right tool, wrong arguments: a missing required field, the wrong enum value, free text where a structured object was expected. | **Schema + per-parameter docs**: close the value set with an `enum`, pin units/format/default per parameter, add an in-description example. | | **A bloated or overlapping toolset** — too many tools, or several that do nearly the same thing, distracting the agent. | **Consolidate then `remove`** the originals (lever 9). Remove for *overlap/confusion*, not for low call-count. | | **A bug in a handler** — the tool returns the wrong thing. | **`code` edit**: because you own the code, fix it directly. | The throughline: a failure the agent *knows better than* but still commits is fixed by removing the choice — putting the behavior in code and `remove`-ing the path that let it go wrong. If the problem is *what the agent is told to do* rather than *what it can do*, it belongs to whatever capability edits the agent's instructions, not here. ## 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; expect to edit SEVERAL 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 | | `add` / `remove` | introduce / delete a tool | shape and shrink the toolset (replace primitives; keep it lean) | `policy.json` in the capability dir is the safety boundary between "reword the docs" and "rewrite the program" — the same artifact is edited in very different trust settings, so tighten the allowed set to match your deployment's blast radius (a frozen-API deployment might allow only `["description", "params", "examples"]`). `apply()` refuses anything outside the allowed set and *reports* the refusal, so an over-tight policy surfaces as visible refusals rather than silent no-ops. ## Adapting to the runtime reader's capability tier Scale the edit to WHO calls these tools at runtime (if your instructions state the runtime reader's capability tier, use it). 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 (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 explicit parameter docs and a smaller, less-confusable toolset are worth most there. For a **frontier** reader, terser parameter docs and fewer worked examples suffice; spend the budget on removing redundant tools instead. ## 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. - `is_empty(dir)` — whether the artifact is an empty seed (no tools yet). ## How to run ``` python scripts/check.py python scripts/run.py --path <capability_dir> # candidate + policy + validity ``` ## References Each is standalone — read the one that matches what you are about to do. - [`references/examples.md`](references/examples.md) — worked before/after edits with full bodies, ordered by leverage: in-body guards (§0), description/schema fixes (§1–§2), loop and composite/write tools (§3b–§3e), result shaping (§3f), the doc contract in practice (§3g), removal and refusals (§4–§7). **Load when you are about to write an edit** and want the exact JSON and a real body to model. - [`references/concepts.md`](references/concepts.md) — how an LLM turns tool definitions into a call (select from name+description, fill from schema+examples), toolset-size limits, response shaping, the safe replacement protocol, and the action-policy model, with cited sources. **Read once per project**, before your first candidate. - [`references/doc-contract.md`](references/doc-contract.md) — the full documentation contract for one tool: what/when/when-not, important-points, error conditions, per-parameter units and formats, one always-valid example. **Load when the fix is documentation rather than code.** - [`references/pitfalls.md`](references/pitfalls.md) — edits that look like improvements and regress (stripped error conditions, cosmetic rewording, task-overfitted descriptions, composite sprawl, example dumps, an exposed primitive behind a wrapper), each with how to detect it. **Read before shipping a docs-only candidate**, and when an accepted candidate barely moved the metric. - [`references/field-notes.md`](references/field-notes.md) — observations from real runs: how much of a docstring the runtime actually delivers, why enriching a return is not free, a docstring header that broke tool registration, and a return shape that corrupted the feedback signal. **Read before your first candidate on a new harness**, and whenever an edit "verified" green without an explanation you can point at. - [`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. **Read when authoring or reviewing those instructions** (`intake` points here rather than inlining it).
Comments (0)
Sign in to join the conversation.
Reviews (0)
No reviews yet.
No comments yet.