Design a tool a model can't misuse
Exact match first, fuzzy match second, loud errors, and one queue per file.
- Problem
Models call edit tools with curly quotes, trailing spaces, snippets that appear twice and overlapping changes, and a naive find-and-replace silently edits the wrong place or corrupts the file.
- Idea
Match exactly first, fuzzy-match only on normalized text, reject anything ambiguous with an error the model can act on, and serialize writes to the same file.
An edit tool for an LLM looks trivial: find oldText, replace it with newText. Then real models use it. They copy text with curly quotes where the file has straight ones, drop trailing spaces, send a snippet that appears twice, send two edits that overlap, and call it twice on the same file at once. A naive find-and-replace edits the wrong place, or corrupts the file, and says "done".
This repo's edit tool is built around those failures.
Try it: five edit calls a model really sends
the same matching rules as the real tool, on a small file
An exact, unique match is applied. A near miss (straight quotes against curly ones) is matched on normalized text, and only the touched line is rewritten. Text that appears twice, overlapping edits and text that isn't there are rejected with an error that says how to fix the call.
The matching pipeline
- Normalize what the model can't seeLine endings are converted to LF, and a UTF-8 BOM is stripped first, because "the model will not include an invisible BOM in oldText." Both are restored when writing.
- Exact match firstEvery edit is matched against the original content, not after earlier edits are applied, so the result doesn't depend on edit order.
- Fuzzy match only on normalized textIf an exact match fails, both sides are normalized: NFKC, trailing whitespace per line, smart quotes, Unicode dashes and special spaces to ASCII. The match happens in that space.
- Write back only what changedA fuzzy match must not rewrite the whole file in normalized form. Each replacement is widened to the lines it touches, and every other line is copied back from the original, byte for byte.
Errors are part of the interface
When the tool refuses, the message tells the model what to send next:
Found 2 occurrences of edits[0] in app.ts. Each oldText must be unique.
Please provide more context to make it unique.
edits[0] and edits[1] overlap in app.ts. Merge them into one edit
or target disjoint regions.
The tool description carries the same rules up front: one call with several non-overlapping edits per file, each matched against the original. Errors that are specific are errors a model can recover from in one retry.
One queue per file
Agents run tools in parallel. Two edits to the same file at the same time would read the same original and the second write would silently drop the first. So every mutation goes through a queue keyed by the file's real path (symlinks resolved): calls on the same file run one after another, calls on different files still run in parallel.
Measured, not guessed
A script reads real session transcripts and groups failed edit calls by kind: file not found, text not found, multiple occurrences, no changes, invalid input, overlapping edits. It also measures how much unchanged context models pad their edits with. The fixes in the tool's history (fuzzy quotes and dashes, BOMs, preserving untouched lines in fuzzy edits) are the kind of failure that data surfaces.
Steal this
Design tools for sloppy but well-meaning callers. Match exactly first and fuzzily only on normalized text, write back only what changed, reject ambiguity with an error that says what to send instead, serialize writes per resource, and measure failures from real transcripts.
Use it in your code
A prompt for your coding agent
Run it from the root of your repo. It checks whether the idea fits before it changes anything.
Harden the tools this project exposes to an LLM so a model can't misuse them.
The idea: treat model input as sloppy but well-meaning. Validate strictly, match exactly first and fuzzily only on normalized text, reject ambiguous calls with an error that tells the model how to fix them, preserve everything you didn't mean to change, and serialize side effects that touch the same resource.
1. Find the tools or functions this project lets a model call (edit, write, search, SQL, API actions). If there are none, tell me and stop.
2. For the most dangerous one, list how a model could misuse it: ambiguous targets, duplicates, overlapping operations, whitespace or Unicode differences (smart quotes, dashes, non-breaking spaces, line endings, BOMs), concurrent calls on the same resource, and no-op calls.
3. Propose fixes: exact match first, then a fuzzy match on normalized text that still rewrites only the lines it touched; errors that say what was wrong and what to send instead ("found 3 occurrences, add more context"); and a per-resource queue so two calls on the same file or row can't interleave.
4. Implement them, with tests for each misuse case. If the tool's calls are logged, add a small script that groups failures by error kind, so you can see which mistakes models actually make.
Keep the tool's interface unchanged unless a change removes a whole class of errors.Read the source
Where this comes from
Pi (earendil-works/pi), the minimal, extensible agent harness. The files behind this lesson: