str_replace (Edit)
# grade this stage (from your course directory)
npx theinference test --stage str-replace --watchTime to build the edit tool itself. strReplace(path, old, new) reads a file, finds old in it, and replaces that one occurrence with new — then writes the result back. The whole contract is in the previous step: the match must be exact (every byte, including whitespace and newlines) and unique (present exactly once). Absent means error. Ambiguous means error. Exactly one match means apply it. No fuzzy matching, no "closest line" — the agent earns a clean edit by handing you bytes that pin down exactly one site.
old appears once → replace it, write back, return "edited <path>"
old appears zero → error: old_str not found
old appears twice+ → error: old_str is not unique
What to build
Implement Tools.strReplace and wire the str-replace command
In src/tools.ts, implement strReplace(path, old, new). Read the file's current bytes, count occurrences of old. If there are zero, return an error result (old_str not found). If there are two or more, return an error result (old_str is not unique). If there is exactly one, replace it with new, write the file back, and return a success result (edited <path>). Then make program.ts str-replace <fixture> replay the view then edit steps from the fixture's case.json and print one JSON result per step.
// src/tools.ts
export function strReplace(path: string, old: string, neu: string): ToolResult {
const text = readFile(path);
const first = text.indexOf(old);
if (first === -1) return { content: 'old_str not found', is_error: true };
if (text.indexOf(old, first + 1) !== -1)
return { content: 'old_str is not unique', is_error: true };
writeFile(path, text.slice(0, first) + neu + text.slice(first + old.length));
return { content: `edited ${path}` };
}
Every space, tab, and \n in old has to be present in the file in the same place — a single trailing space the model didn't copy turns a real match into zero matches. This is the #1 bug in edit tools. Don't "helpfully" trim, normalize indentation, or collapse blank lines to make a near-match work: that's exactly the silent overwrite the exact-match rule exists to prevent. A mismatch must surface a clear old_str not found, never a quiet no-op or a forced fuzzy hit.
This is a process step. The grader runs program.ts str-replace <fixture>, which seeds the fixture's files, then replays a view followed by an edit from case.json. It checks that your printed per-step results (one JSON object each — view returns the line-numbered contents, edit returns {content:"edited <path>"}) match expected.json, and that the file's final bytes equal the expected content. A botched replacement, a wrong error string, or trimmed whitespace fails the comparison.