EmpryoEmpryo.beta
← Blog
engineeringJune 1, 2025 · 3 min read

Why an AI agent should edit symbols, not strings

A failed edit costs three tool calls: the miss, the re-read, the retry. Empryo's ast_edit names a symbol and mutates the syntax tree — no search string, no line numbers, nothing to mismatch. It started as my Master's thesis.

PProxySoul

Watch an AI agent fail an edit and you'll almost always find the same autopsy: the model asked for find-and-replace, the search string had two spaces where the file had a tab, and the match came back empty. The agent re-reads the file, rebuilds the string, tries again. Three tool calls, a few thousand tokens, to change one line.

The edit didn't fail because the model misunderstood the code. It failed because the model was asked to reproduce bytes, and reproducing bytes is the one thing a language model is bad at.

Disclosure up front, as always: I build Empryo, solo and unfunded, and this post is about one of its tools. The idea is older than the product — more on that at the end.

What find-and-replace actually asks of a model

A string edit makes the model do three jobs at once, and punishes it for missing any of them:

  • Byte-exact recall. Tab vs. spaces, CRLF vs. LF, a trailing space it never saw. Any mismatch and the edit silently misses.
  • Disambiguation. return null; appears five times in the file. Which one did you mean? The tool can't know; the model has to pad the search string with context until it's unique, and every padded line is another chance to typo.
  • Syntax bookkeeping. The replacement lands mid-expression, a brace goes missing, and nothing notices until the typechecker runs — if it runs.

None of this is exotic. It's the default failure surface of text-level editing on any file longer than a toy, and it gets worse as the session goes on, because the file keeps drifting away from whatever version the model last read.

Name the symbol, skip the bytes

Empryo's ast_edit drops the search string entirely. You address a symbol by kind and name, and an operation mutates the parsed tree:

ast_edit({
  path: "src/auth.ts",
  action: "set_body",
  target: "function",
  name: "validateToken",
  newCode: "return verify(token, key);",
})

There is nothing here to mismatch. No whitespace to reproduce, no line number to go stale, no five-way ambiguity — the file has one validateToken, and ts-morph finds it no matter how the file was reformatted since the model last read it. The Genome already lists every symbol with its kind and location, so the agent doesn't spend a read locating the target first.

Micro-edits shrink to almost nothing. Changing a return type is one operation and a handful of tokens:

ast_edit({
  path: "src/api.ts",
  action: "set_return_type",
  target: "function",
  name: "fetchUser",
  value: "Promise<User>",
})

And multi-step changes — make it async, add a parameter, import the type — run as one atomic batch. If any operation fails, none apply. The file is never left half-edited:

ast_edit({
  path: "src/api.ts",
  operations: [
    { action: "set_async", target: "function", name: "fetchUser", value: "true" },
    { action: "add_parameter", target: "function", name: "fetchUser", value: "cache: boolean" },
    { action: "add_named_import", value: "./types", newCode: "User" },
  ],
})

Even the errors are built for a model rather than a human. Ask for a symbol that isn't there and the failure lists every symbol of that kind in the file — the retry is a lookup, not another guess.

The 60th edit is as reliable as the first

String editing degrades over a session: the more the agent has changed the file, the less its mental copy matches the bytes on disk, and the more matches miss. Tree addressing doesn't care. validateToken is validateToken after forty edits, a reformat, and an import reshuffle.

An agent that trusts its own edits stops re-reading files to confirm them. That's where the real savings live — not the tokens of one edit, but the verification loop you no longer need.

This started as a thesis

The approach predates Empryo. My Master's thesis, *Typed vs Untyped Programming Languages* (with Artur Matusiak, 2022), showed that JavaScript-to-TypeScript conversion could run entirely through ts-morph AST mutations — no string manipulation anywhere in the pipeline. ast_edit is that idea grown up and handed to an agent: if a program can rewrite code structurally, a model never has to reproduce bytes at all.