The "Apply" Problem: Writing Code Without Breaking It
In the previous post, I covered how librarian parses code into an AST-aware SQLite catalog.
That solves the "Find" problem.
But finding code is only half the battle. The other half—and arguably the harder half—is editing it.
Every developer who has used an LLM knows the pain: the model writes perfect Python logic, but it hallucinates the line numbers. Or it tries to apply a patch to a file that has changed since the model last saw it.
The result is broken builds, "hunk failed" errors, and a lot of manual cleanup. To fix this, we need a smarter way to apply changes.
The Problem with Line Numbers
Standard diffs rely on line numbers. But line numbers are brittle. If you add a docstring at the top of the file, every single line number shifts down. If your agent is working off a cached index (which it is, to save tokens), its mental model of "Line 50" might actually be "Line 55."
We need to move from Coordinate-Based Editing (Line 50-60) to Semantic-Based Editing (Function process_data).
Symbol-Based Application
In librarian, I implemented a strategy called apply_from_symbol. Instead of telling the tool "replace lines 10-20",
the agent says: "Replace the body of the function `process_data`."
The tool then does the hard work of resolving what "process_data" means right now.
The Fallback Ladder
The most robust part of the system is how it handles stale indexes. Code changes fast. If the agent is working, the database might be 5 minutes old.
To prevent failures, I implemented a tiered resolution strategy in apply.rs:
1. The Index Lookup (Fastest)
First, check the symbol_index table. If we have a fresh record for `fn:process_data` pointing to lines 50-60, we use it.
This covers 90% of cases and costs microseconds.
2. The Blob Lookup (Stale-Safe)
If the symbol index is missing (maybe we haven't run a full re-index yet), we check the blobs table.
Did we index this function as a standalone blob recently?
3. Source Inference (The Safety Net)
If the database is totally out of whack, we don't give up. We read the actual file from disk right now, parse it with Tree-sitter on the fly, and find the symbol.
// src/ops/apply.rs
fn resolve_symbol_target(...) -> anyhow::Result
Enforcing Clean State
Even with semantic lookups, race conditions happen. To prevent an agent from overwriting changes it didn't see,
librarian supports an --enforce-clean flag.
Before applying a patch, it calculates the SHA-256 hash of the target file (or symbol range) and compares it to what the agent thought it was editing. If the hashes mismatch, the tool rejects the edit with a helpful error: "File has changed since index; re-ingest."
This forces the agent to stop, re-read the ground truth, and try again—rather than silently corrupting the file.
Conclusion
Building reliable agent workflows isn't just about better prompts. It's about building tools that are tolerant of the agent's limitations. By moving from line numbers to symbols, and implementing aggressive fallback strategies, we turn "hallucinated edits" into "valid transactions."