Watch a coding agent work for ten minutes and count how much it reads.
My answer: way too much.
It greps, gets 80 hits, opens a 3,000 line file in 800 line chunks, then does the exact same thing for the next file and the one after that, and most of what it reads never touches the change.
That's tokens and time.
It's also quality. Patrick Kelly wrote about context rot, where irrelevant details from all that exploration start to degrade the results. Across thousands of coding sessions he analyzed, 40% to 89% of the files an agent read were never used or edited.
He prunes those files after the turn. A graph helps earlier, the agent asks for the exact lines it needs, so most of those files never land in context in the first place.
When I was working on FreightFloor I realized that we needed a faster way to find changes.
So I wrote ast-graph, a plugin for Antigravity and Claude Code (it's an MCP server plus a CLI, so other harnesses work too). The first commit was October 24.
I built it because I found the agent just scanning thousands of lines of code, being slow, and inefficient. I'm trying to build good things for an economic cost.
What it does
It parses your repo into a local graph (.ast-graph/ast.db, SQLite) of symbols, line spans, calls, property reads and writes, routes, and tests. Then the agent asks the graph instead of the file system.
A few tools do most of the work:
ast_outline: every symbol in a file with its line range, before reading it.ast_refs: where a field is read vs. written.ast_blast_radius: what breaks upstream if you change this.ast_tests_for: the exact test command for what you touched.ast_lineage: a route or schema traced from client to server to Terraform.
Every response is summary first and capped around 3.8 KB, so one query can't flood the context window.
Across my sessions, that works out to roughly 70% to 90% fewer context tokens on search and file reading, and 5x to 15x fewer lines read per task. Every repo and task is different, so treat that as a range.
The numbers below are from FreightFloor, a TypeScript monorepo with 161 source files. Indexing took 14 seconds.
Example 1: changing a shared schema
I want to change JoinRequestSchema, a Zod schema in the shared protocol package.
Plain grep -rn returned 78 lines and 32.6 MB of output across 38 files. Almost all of it was build caches and source maps (a single match in a .js.map file is one 1.8 MB line).
rg is smarter, it respects .gitignore and returns 15 clean lines in 5 files, but then the agent has 6,483 lines of files to open and it still doesn't know the load test and smoke test packages depend on this schema two hops away.
Grep finds strings.
ast_blast_radius returned 3.7 KB: the schema at packages/protocol/src/index.ts#L252-L262, 72 impacted symbols across 4 packages, and one command running the 6 test files that cover it.
That's the whole job, in one call.
For FreightFloor specifically: opening the 5 files rg found is 259 KB of reading, vs. 3.7 KB from the graph (about 98% less).
Example 2: a 3,126 line file
The lobby server is a single index.ts file, 3,126 lines and 110 KB. One function in it (buildLobbyServer) is 1,600 lines long.
ast_outline returns 150 symbols with line ranges in 3.7 KB, about 30x smaller than the file (97% less).
The agent reads getSites at lines 466 to 606 and skips the other 2,985.
Example 3: who sets this field?
maxDrivers lives in the protocol, the lobby, and the client. rg maxDrivers gives 65 lines, reads and writes mixed together.
ast_refs maxDrivers splits them: 4 declarations, 18 writes, 16 reads, each tagged with the enclosing function (reconcileNetworkSites, getSites, presenceView). For a 'who set this' bug, the writes are the only part you care about.
Example 4: running the right tests
FreightFloor has 53 test files. After touching the lobby, ast_tests_for says 4 of them cover it and hands back the npx vitest run ... line.
The agent runs those first, then the full suite once before the PR.
Example 5: following an endpoint
ast_lineage "POST /api/join" finds the handler (index.ts#L2039-L2476) and every caller: the browser client's join and resumeIdentity, plus the load test bot and the Gemini pilot. Static fetch calls get linked to backend routes, schemas, SQL tables, and Terraform resources the same way.
Where it doesn't help
It's static analysis.
I don't intentionally use grep, the model still gets to pick it, but I ask questions every couple days about the effectiveness of the plugin across my sessions to find bugs and improve it.
Dynamic imports, reflection, and URLs built at runtime won't show up in the graph. It indexes symbols, not strings, so for UI copy or a log message, grep is still the right tool. TypeScript, Python, Go, and Rust get the full call and test graph; Java, C#, Ruby, and friends get structural spans and route linking without full compiler resolution.
The index lives in .ast-graph/ and gets added to .git/info/exclude automatically, so git status stays clean.
Try it
``bash claude plugin marketplace add davenportjw/agent-skills claude plugin install ast-graph@davenportjw-agent-skills ``
For Antigravity, clone it and link the folder:
``bash git clone https://github.com/davenportjw/agent-skills cd agent-skills/plugins/ast-graph && npm install --omit=dev ln -sfn "$(pwd)" ~/.gemini/config/plugins/ast-graph ``
Restart, and the full steps are in the README.
Ultimately, the cheapest token is the one your agent never reads.
For companies and engineering leaders, look at where your agent spend actually goes. A lot of it is search and file reading, and that part is fixable with tooling instead of a bigger budget.
For the individual, try it on your messiest repo and watch what the agent stops reading. If it misses something in your stack, open an issue.
What's the worst file your agent keeps rereading? Let me know in the comments!