The files are not the answer
An agent asked to change one function opens three files and spends most of its context on code that has nothing to do with the task. A slice is the other shape of the same question: the declarations the function depends on, and nothing that merely lives beside them.

On this page · 4 sections
Ask an agent to change one function and watch what it reads. It opens the file the function is in, most of which is about something else. Then the file holding a helper it calls. Then the one behind that. For the function this post takes as its example, that is three files and 157 545 characters, to answer a question whose real answer is 18 707.
The rest is not noise in the ordinary sense. It is perfectly good code that has nothing to do with the task, and it arrives because files are how we store code, not how code depends on code.
The other shape of the question
code_slice takes a symbol — the name, as the case for
addressing a tool by symbol rather than by position argues
it should — and returns the declarations it depends on, grouped by the file they happen to live
in, each one labelled with how it was reached. The paths below are shortened after the first
mention; the tool prints them in full:
$ prod-code slice signature::change
Function change at crates/prod-code-mcp/src/signature.rs:521:14
slice of `change`: 16 item(s), 18707 bytes from 157545 bytes of source (88% smaller)
outside the workspace, not followed: Ok, Option, Path, PathBuf, Result, SocketAddr, Some,
String, Url, Vec, and_then, anyhow, and 60 more
=== crates/prod-code-mcp/src/remote_fs.rs
[function] uri_to_path remote_fs.rs:52-55 (depth 2, used by references)
=== crates/prod-code-mcp/src/signature.rs
[enum] Param signature.rs:26-35 (depth 1, used by change)
[class] Reference signature.rs:38-38 (depth 2, used by references)
[struct] Declared signature.rs:42-47 (depth 1, used by change)
[struct] SignatureChange signature.rs:51-71 (depth 1, used by change)
[function] split_at_top_level signature.rs:198-231 (depth 2, used by parse_declared)
[function] offset_of signature.rs:234-248 (depth 1, used by change)
[function] param_span signature.rs:252-308 (depth 1, used by change)
[function] split_params signature.rs:312-340 (depth 2, used by parse_declared)
[function] parse_declared signature.rs:343-364 (depth 1, used by change)
[struct] Plan signature.rs:370-374 (depth 1, used by change)
[function] plan signature.rs:377-419 (depth 1, used by change)
[function] change signature.rs:521-733 (the seed)
[function] references signature.rs:790-831 (depth 1, used by change)
[function] line_col_at signature.rs:833-842 (depth 1, used by change)
=== crates/prod-code-mcp/src/tools.rs
[function] execute_lsp_query tools.rs:1972-1982 (depth 2, used by references)
Sixteen declarations instead of three files. The three files are 157 545 bytes; the slice is 18 707, and it carries the whole of each declaration — signature, body, doc comment — because a declaration cut in half is worse than no declaration at all.
Note the outside the workspace line. String, Vec, Result, anyhow and sixty-eight more
are named and not followed: they are outside the workspace, the agent already knows them, and
following them would drag the standard library into a context window to explain a function that
merely returns a Result.
The edges are the point
Every line says how the item got there. parse_declared is at depth 1 because change calls
it; split_params is at depth 2 because parse_declared calls it. That is not decoration —
it is the difference between a slice and a pile.
An agent reading (depth 2, used by references) next to uri_to_path knows, without opening
anything, that this function is two hops away and only matters through the reference lookup. It
can decide to ignore it. With three files open it cannot decide anything, because nothing in a
file says why it is on screen.
The depth is also the knob:
| items | slice | of source | ||
|---|---|---|---|---|
--depth 1 |
11 | 15 703 B | 37 817 B | 58% smaller |
--depth 2 (default) |
16 | 18 707 B | 157 545 B | 88% smaller |
--depth 3 |
18 | 20 869 B | 172 968 B | 88% smaller |
Depth 1 stays inside one file, so the comparison is against that file alone and the saving looks smaller. Depth 2 reaches into two more files and the saving jumps, which is the useful reading of that table: the further the dependencies spread, the more a slice is worth. Depth 3 adds two items and 2 kB — the graph is nearly closed by then, which is the usual shape.
There is a byte budget too, for when a symbol’s neighbourhood is genuinely large:
$ prod-code slice signature::change --max-bytes 4096
slice of `change`: 1 item(s), 8269 bytes from 37817 bytes of source (78% smaller)
The budget stops the walk; it does not truncate a declaration. One item came back and it is 8 269 bytes, because that is how big the seed is. A budget that cut the seed in half would produce something that compiles in nobody’s head.
Neither of the two tools that already exist answers this question, and it is worth being precise about why. An outline lists what a file contains. It is a table of contents: useful for orienting, useless for dependency, and it stops at the file’s edge — the two items above that live in other files would never appear.
Grep finds text. It would find parse_declared in change’s body, and also in a comment, in a
test name, and in the unrelated function of another crate that happens to use the same word.
Then it hands back lines, and a line is not a declaration: the agent still has to open the file
to see the body it matched inside.
The slice walks the edges the analyzer already resolved while type-checking. parse_declared
is in the slice because the compiler knows change calls that parse_declared, and the
function’s whole text comes with it.
What it does not do
- The unit is a declaration, not a statement. This is not data-flow slicing: ask for a function and you get the functions and types it uses, not “the six lines that affect this variable”. Inside a body, everything comes.
- Foreign code is named, not followed. A dependency’s type is listed on that line and left
there. If the thing you actually need to read is inside
serde, a slice will not bring it;code_sourcewill, and that is a different tool on purpose. - A macro hides its edges. What a declarative macro expands into is not walked, so a dependency that exists only after expansion is missing from the slice. The analyzer knows it; the slicer does not ask yet.
- Rust and Go are the languages it has been run against in anger. The mechanism is the analyzer’s definition edges, so it applies anywhere those exist, but the depth heuristics were tuned on these two.
What it costs
$ time prod-code slice signature::change
real 0.41 # 0.44 on a second run
Almost all of that is now the slice. A bare prod-code status, which asks a gateway nothing
about any workspace, costs 0.02 s on the same machine; the binary starting up on its own is
0.01 s, and the LAN round trip is a third of a millisecond.
That status row read 0.53 s while this post was being written, and the slice above took
0.83 s, for a reason that had nothing to do with either. Every invocation asks a gateway two
questions before it sends a query — what does the cluster look like, and which node holds this
checkout — and the gateway answered both by probing for the language servers it can run. One of
those probes asks npm where its global modules live, which is 213 ms of node starting up. Per
question. On every command. It is
fixed: the probe runs once at startup and once
a minute after that, and a status request answers in 0.2 ms, measured on the node itself, where it used to take 437.
What the slice itself costs is the difference between those rows — nearly all of the 0.41 s.
Each edge is a textDocument/definition against an analyzer that already holds the answer in memory. Cold,
the workspace load comes first — the same forty-five seconds everything else in this series pays
once per worktree.
The saving is the point, and it is worth stating plainly rather than as a percentage. Eighteen thousand characters instead of a hundred and fifty-seven thousand is roughly five thousand tokens instead of thirty-nine thousand. That is not a nicer number. That is the difference between an agent that can hold the task and the code at the same time and one that reads the code, forgets the task, and asks you what it was doing.
Cite this article
Alexander Panasenko (2026-09-28). The files are not the answer. https://prod.codes/blog/the-files-are-not-the-answer/