Claude Skill

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

LLM Mart · 0 points · 14 views 0 listing impressions 0 install-command copies
Virus-scanned Reviewed automatically before listing.

Full trust report

Download skillberry-ai-cap-evolve-skills_capabilities_tools-49fcedb.zip · 35 KB
Part of skillberry-ai/cap-evolve — 22 skills

Install

skills CLI npx skills add https://github.com/skillberry-ai/cap-evolve/tree/main/skills/capabilities/tools
Claude Code claude plugin marketplace add https://llmmart.ai/marketplace.json && claude plugin install skillberry-ai-cap-evolve@llmmart
Git 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?"

  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 — 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 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).
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.

No comments yet.

Reviews (0)

No reviews yet.

Related