Code and structure-aware context

TL;DR. Code has structure that prose does not: functions call functions, files import files, classes inherit classes. So you do not compress a codebase by ranking or trimming lines (the way Chapter 3 ranked sentences); you parse it into a tree, build a call graph, and follow the call edges from the target function to collect the exact slice of code it depends on. Everything outside that slice is noise you can drop. This chapter builds the idea from scratch with Python's standard-library ast module, cutting a toy module from 181 tokens to 65 (a 64% reduction) while keeping every function the target actually calls, then shows the same selection running in real tools (Aider's repo map, lean-ctx, Claude Code) on real repositories.

Contents

Chapter 3 shrank prose by scoring sentences and keeping the top ones. Code does not work that way. You cannot drop "the least important line" of a function and hope the rest still makes sense, and you cannot rank lines by similarity to a question the way you would rank paragraphs. Code has structure: functions call functions, files import files, classes inherit classes. The right way to compress a codebase for a model is to use that structure to select only the parts the task touches, and leave the rest out entirely.

The reason this matters is a lesson people tend to learn the expensive way. The lazy reflex, when a model needs to reason about one function, is to dump the whole file (or the whole repo) into the prompt and let attention sort it out. That fails on two counts. It spends tokens on functions the task never touches, which is wasteful even when it works; and on a real repository it does not fit in the context window at all, so the request errors out before the model reads a line. Dumping whole files does not scale. The fix is to select by structure: figure out what the target actually depends on, and send only that.

This chapter is about selecting code by structure instead of by file. The purpose is narrow and practical: given a target (the function the model must edit, explain, or reason about), produce the smallest set of code that makes the target understandable on its own, and send only that. The benefit follows directly. The window stays lean, the token bill drops, and on a repository too large to send whole the request that would have failed now fits. The rest of the chapter is how to compute that set, first by hand and then with the tools that do it at scale.

The unit of code is not the line

To talk about structure we need the thing that exposes it. When Python (or any language) reads source text, the first step is to turn that flat string into a tree that mirrors the grammar of the language. That tree is the Abstract Syntax Tree (AST). "Abstract" because it throws away surface details like whitespace, comments, and the exact punctuation, and keeps the meaningful shape: this is a function definition, its body is a list of statements, this statement is a call, the thing being called has this name. Each node is one grammatical piece of the program, and a node's children are the pieces nested inside it, so the tree as a whole records how the program is built up from its parts. Python hands you the AST through the standard-library ast module, so we can work with the structure of code using nothing but the standard library.

It is worth being clear about why we parse a tree instead of pattern-matching the text, because the difference is the whole reason this approach is reliable. A regular expression (a regex, the usual tool for "find me lines that look like X") sees only characters. It has no idea whether a foo( it matched is a real call, a call written inside a comment, the letters foo( sitting in a string literal, or the tail of a longer name like do_foo(. It cannot tell a function definition from a variable that happens to be named def_something, and it cannot follow a call across a line break or through a decorator. The parser, by contrast, has already decided all of that: by the time you hold the AST, "this is a call and the thing called is named summarize" is a fact about a node, not a guess about some characters. Parsing beats regex for finding code structure because the parser understands the grammar and the regex only sees the spelling. That correctness is the foundation everything below stands on; a call graph built from regex matches would inherit every one of those mistakes.

Two node types carry most of what we need:

  • A FunctionDef node is one def. It knows the function's name and holds its body as a list of child nodes.
  • A Call node is one function call, like summarize(rows). Its .func child tells you what is being called; when the call is a plain name, that child is a Name node whose .id is the string "summarize".

So "which functions does this function call" is not a text search for parentheses. It is a walk over the Call nodes inside a FunctionDef, reading the name off each one. The AST makes that exact and immune to the things that fool a regex (a call inside a comment, a string that happens to contain foo(, a name that is a substring of another).

The call graph

Once you can list every function and, for each one, the functions it calls, you have a call graph: a set of nodes (the functions) and directed edges (A calls B means an arrow from A to B). It is worth saying plainly what the two parts are, because the call graph is the central object of this chapter. The nodes are the definitions: one node per function (a class or method would be a node too). The edges are the calls: you draw an arrow from a function to each function it invokes in its body. Building it is the loop you already have the pieces for. Walk the AST once to collect every FunctionDef, which gives you the nodes; then for each FunctionDef, walk the Call nodes inside its body and read off the name being called, which gives you that node's outgoing edges. Two passes over the tree and the graph is done. The call graph is the map of how the code's pieces depend on each other, and it is what lets you answer the question that matters for context: if I want the model to understand make_report, what else does it need to see?

The answer is the transitive dependencies of the target. "Transitive" means you follow the arrows all the way, not just one hop. If make_report calls clean, and clean called something else, you would need that too, and so on until nothing new turns up. Formally, the transitive dependencies of a target $t$ are every node reachable from $t$ by following call edges:

$$\text{deps}(t) = {, t ,} \cup {, v : t \rightsquigarrow v ,}$$

where $t \rightsquigarrow v$ means "there is a path of call edges from $t$ to $v$." You compute this with an ordinary graph search. Start with the target, look at everything it calls, add anything you have not seen, and repeat until the frontier is empty. That is a breadth-first search (BFS), and it is the whole selection algorithm: the set it returns is exactly the slice of code the model needs, and everything outside the set is noise you can drop. The "follow the arrows all the way" part is what makes it correct. If you stopped at one hop you would hand the model make_report and the functions it calls directly, but those functions might call others that they in turn rely on, and the model would hit a name with no definition. Transitive closure is exactly the property "include everything reachable," so the slice is self-contained: every name the included code uses is also included.

Remember. The call graph is nodes (definitions) and edges (calls). The slice the model needs is the target plus its transitive dependencies, the set of all nodes reachable from the target by following call edges. Compute it with a breadth-first search from the target. The set is self-contained by construction, and everything outside it can be left out of the context.

The demo

The script below carries a small Python module as a string: eight functions where the target make_report transitively needs only three of them (load_rows, clean, summarize) and the other four (send_email, connect_smtp, resize_image, slugify) are unrelated. It parses the module with ast, records each function's exact source text, builds the call graph by walking Call nodes, runs a BFS from the target to collect its transitive dependencies, and emits only those functions as the context slice. Then it prints the before/after token counts (the same words * 1.3 estimate from Chapter 2) and verifies that the target and all its real dependencies are present while the unrelated functions are gone.

"""Select code by STRUCTURE (a call graph), not by file.

When an LLM needs to reason about one function, the lazy move is to paste the
whole module and let the model sort it out. That spends tokens on functions the
task never touches. The structure-aware move is to parse the code, find which
functions the target actually CALLS (directly or indirectly), and send only
those. This script does exactly that with the standard-library `ast` module.

Pipeline:
  1. parse the module text into an Abstract Syntax Tree (AST)
  2. record each top-level function's name and exact source text
  3. build a CALL GRAPH: for each function, which other functions it calls
  4. from the target, walk the graph to collect its transitive dependencies
  5. emit only those functions as the "context slice"

Standard library only. Run:  python3 code_context.py
"""

import ast


def est_tokens(text: str) -> int:
    """Rough token estimate, same rule as chapter 2: ~1.3 tokens per whitespace
    word. Code fragments more than prose, so this UNDER-counts real code tokens,
    but it is consistent across the before and after, which is what we compare."""
    return round(len(text.split()) * 1.3)


# A small module. `make_report` (the target) transitively needs `load_rows`,
# `clean`, and `summarize`. The other four functions are unrelated noise that a
# whole-file dump would carry along for nothing.
MODULE = '''\
def load_rows(path):
    raw = path.read_text()
    return [line.split(",") for line in raw.splitlines()]


def clean(rows):
    return [[cell.strip() for cell in row] for row in rows]


def summarize(rows):
    total = sum(float(row[1]) for row in rows)
    return {"count": len(rows), "total": total}


def make_report(path):
    rows = clean(load_rows(path))
    stats = summarize(rows)
    return f"{stats['count']} rows, total {stats['total']}"


def send_email(addr, body):
    server = connect_smtp()
    message = build_message(addr, body)
    receipt = server.deliver(addr, message)
    log_delivery(addr, receipt)
    return receipt


def connect_smtp():
    host = read_config("smtp_host")
    port = read_config("smtp_port")
    return open_socket(host, port)


def resize_image(img, width, height):
    ratio = min(width / img.width, height / img.height)
    new_w = int(img.width * ratio)
    new_h = int(img.height * ratio)
    canvas = blank_canvas(width, height)
    scaled = img.scaled(new_w, new_h)
    return paste_centered(canvas, scaled)


def slugify(title):
    lowered = title.lower().strip()
    safe = "".join(ch for ch in lowered if ch.isalnum() or ch == " ")
    collapsed = " ".join(safe.split())
    return collapsed.replace(" ", "-")
'''


def function_table(module_src):
    """Parse the module and return {name: source_text} for every top-level
    function. `ast.parse` turns the text into a tree of nodes; a `FunctionDef`
    node is one `def`. `ast.get_source_segment` hands back the EXACT slice of
    the original text for that node, so we keep formatting and comments."""
    tree = ast.parse(module_src)
    funcs = {}
    for node in tree.body:
        if isinstance(node, ast.FunctionDef):
            funcs[node.name] = ast.get_source_segment(module_src, node)
    return funcs


def build_call_graph(module_src, names):
    """Return {name: set_of_names_it_calls}. We walk each function's subtree
    looking for `Call` nodes. A call like `summarize(rows)` parses as a `Call`
    whose `.func` is a `Name` node with `id == "summarize"`. We only keep callees
    that are themselves functions defined in this module (ignore builtins like
    `sum`, `len`, `float`, and methods like `.strip()`)."""
    tree = ast.parse(module_src)
    graph = {name: set() for name in names}
    for node in tree.body:
        if isinstance(node, ast.FunctionDef):
            for sub in ast.walk(node):
                if isinstance(sub, ast.Call) and isinstance(sub.func, ast.Name):
                    callee = sub.func.id
                    if callee in names:
                        graph[node.name].add(callee)
    return graph


def transitive_deps(graph, target):
    """Breadth-first walk of the call graph from `target`. Start with the target,
    then repeatedly add everything its current members call, until nothing new
    appears. The result is the target plus every function reachable from it: its
    transitive dependencies. The order is the discovery order (target first)."""
    seen = []
    queue = [target]
    in_set = {target}
    while queue:
        name = queue.pop(0)
        seen.append(name)
        for callee in sorted(graph[name]):
            if callee not in in_set:
                in_set.add(callee)
                queue.append(callee)
    return seen


def main():
    funcs = function_table(MODULE)
    names = set(funcs)
    graph = build_call_graph(MODULE, names)
    target = "make_report"

    needed = transitive_deps(graph, target)
    dropped = [n for n in funcs if n not in needed]

    slice_src = "\n\n\n".join(funcs[n] for n in funcs if n in needed)

    full_tok = est_tokens(MODULE)
    slice_tok = est_tokens(slice_src)

    print("=== The call graph (who calls whom) ===")
    for name in funcs:
        calls = sorted(graph[name])
        print(f"  {name:14s} -> {', '.join(calls) if calls else '(no in-module calls)'}")
    print()

    print(f"=== Slice for target: {target}() ===")
    print(f"  included ({len(needed)}): {', '.join(needed)}")
    print(f"  dropped  ({len(dropped)}): {', '.join(dropped)}")
    print()

    print("=== Tokens: whole module vs structure-aware slice ===")
    print(f"  whole module : {full_tok:4d} tokens ({len(funcs)} functions)")
    print(f"  slice        : {slice_tok:4d} tokens ({len(needed)} functions)")
    reduction = 100 * (full_tok - slice_tok) / full_tok
    print(f"  reduction    : {reduction:.0f}% fewer tokens sent")
    print()

    print("=== Verify the slice is correct ===")
    expected_deps = {"load_rows", "clean", "summarize"}
    target_present = target in needed
    deps_present = expected_deps.issubset(set(needed))
    unrelated = {"send_email", "connect_smtp", "resize_image", "slugify"}
    unrelated_absent = unrelated.isdisjoint(set(needed))
    print(f"  target present              : {target_present}")
    print(f"  all real dependencies present: {deps_present}  {sorted(expected_deps)}")
    print(f"  unrelated functions absent   : {unrelated_absent}  {sorted(unrelated)}")
    print(f"  ALL CHECKS PASS              : {target_present and deps_present and unrelated_absent}")


if __name__ == "__main__":
    main()

Running it:

=== The call graph (who calls whom) ===
  load_rows      -> (no in-module calls)
  clean          -> (no in-module calls)
  summarize      -> (no in-module calls)
  make_report    -> clean, load_rows, summarize
  send_email     -> connect_smtp
  connect_smtp   -> (no in-module calls)
  resize_image   -> (no in-module calls)
  slugify        -> (no in-module calls)

=== Slice for target: make_report() ===
  included (4): make_report, clean, load_rows, summarize
  dropped  (4): send_email, connect_smtp, resize_image, slugify

=== Tokens: whole module vs structure-aware slice ===
  whole module :  181 tokens (8 functions)
  slice        :   65 tokens (4 functions)
  reduction    : 64% fewer tokens sent

=== Verify the slice is correct ===
  target present              : True
  all real dependencies present: True  ['clean', 'load_rows', 'summarize']
  unrelated functions absent   : True  ['connect_smtp', 'resize_image', 'send_email', 'slugify']
  ALL CHECKS PASS              : True

Read the call graph first. The only function with in-module callees is make_report, which points at clean, load_rows, and summarize. There is a second little cluster, send_email calling connect_smtp, but no arrow connects it to the target, so the search from make_report never reaches it. That is why the slice contains four functions and drops the other four, and why the token count falls from 181 to 65, a 64% cut, while the verification block confirms nothing the target needs went missing and nothing it does not need came along.

Notice what the selection did not rely on. It did not look at what the functions are named, what they appear to be about, or whether their text resembles some query. slugify and resize_image were dropped not because they seemed irrelevant but because no path of call edges reaches them from the target. The structure decided, and the structure cannot be fooled by a suggestive name.

Don't be confused. Structural relevance and semantic relevance are different filters, and they answer different questions. The retrieval in Chapter 3 and the memory in Chapter 9 use semantic relevance: embed the query and the candidates, and keep the ones whose meanings are close. That finds code that is about the same topic. The call graph here uses structural relevance: keep the code the target actually executes. Those can disagree. A helper named _x7 that make_report calls is structurally essential and semantically invisible; a beautifully named generate_report_pdf that nothing calls is semantically tempting and structurally useless. For "give the model what it needs to understand this function," structure is the correct filter, because correctness depends on what the code calls, not on what it sounds like.

Where this is used in the wild

The toy here is a single-file call graph, but the idea scales to real tools that select code by structure instead of dumping it.

  • CodeCompressor is code-aware prompt compression: instead of treating a file as a bag of lines, it respects code structure when deciding what to cut, so the compressed prompt is still parseable code rather than a shredded fragment.
  • lean-ctx builds context from a code graph, assembling the relevant slice for a task from the dependency structure rather than pasting whole files.
  • tree-sitter based selectors use the tree-sitter parser, which produces an AST for dozens of languages, to pull out specific definitions (this function, this class) instead of whole files. It is the same FunctionDef-and-Call idea generalized past Python.
  • Aider's "repo map" gives a coding agent a compact, structure-derived index of a repository: the symbols (functions, classes) and how they relate, ranked by relevance to the current task, so the model gets a map of the codebase rather than its full text.

Two of those bullets deserve unpacking, because they are how the toy in this chapter turns into something you would actually run on a million-line repository.

Our demo used Python's ast, which only parses Python. tree-sitter is what removes that limit. It is a parser generator: feed it a grammar for a language and it produces a parser that turns source in that language into a syntax tree of the same shape we have been using, nodes for definitions and calls and the rest. Grammars exist for dozens of languages (Python, JavaScript, Go, Rust, Java, C, and on), so a single tool can build a call graph across a polyglot codebase where one service is in Go and another in TypeScript. The FunctionDef-and-Call logic does not change; only the parser underneath it does. That is why real selectors are built on tree-sitter rather than on each language's own ast: one query language, many languages parsed.

A repo map is the artifact you get when you run that idea over a whole repository and then summarize it for the model. Instead of the source of every file, the map lists each file's definitions (the function and class names, often with their signatures, the line that says what a function takes and returns) and leaves out the bodies. It is a table of contents for the codebase that fits in a few hundred or few thousand tokens, where the raw source would be millions. The map is not flat: the definitions are ranked, so the ones most relevant to the current task float to the top and the long tail is dropped to fit a token budget. That ranking is itself a graph computation over which files reference which, a close cousin of the call-graph BFS, applied to "what is worth showing" instead of "what is strictly required." This is what makes repository question-answering and PR review tractable: the model reads a map, decides which few definitions it needs in full, and only those bodies get pulled in.

The common thread is that every one of these sends the model a selected, structured slice rather than raw bulk. The selection is what keeps the context lean (the second property from Chapter 1) on inputs far too large to send whole. It is also the answer to the lesson from the top of the chapter: dumping whole files does not scale, so you select by structure, and the repo map is that selection made into a reusable index.

'with Claude' note (illustrative). A coding agent like Claude Code does not paste the repository into the prompt. It navigates: it greps for a symbol, reads the specific files a task names, follows a call or import to the next file, and stops when it has what it needs. The effect is the same selection this chapter computes, done incrementally as the task reveals which symbols matter, which keeps the window lean even on a large codebase.

The use cases line up with how people actually deploy this. A coding agent editing one function needs that function and its dependencies, not the repo. Repository question-answering ("where do we validate the JWT?") wants the few functions on the path to the answer. PR review wants the changed functions plus what they call and what calls them, which is the call graph extended one hop in the other direction, so the reviewer model sees the blast radius of the change and nothing else.

In practice the strongest systems combine the two filters rather than choosing one. Structural relevance, the call graph, tells you what the target needs to run: the functions it actually executes, which you must include for correctness. Semantic relevance, embeddings (the technique from Chapter 3, where you turn text into vectors and keep the ones whose meaning is close to the query), tells you what is about the same thing: the functions a person would think of even when no call edge connects them. They are complementary because they catch different misses. Structure finds the ugly helper the target calls that no keyword search would surface; semantics finds the related validator in another module that the target does not call yet but the task is really about. A repository question-answering tool often runs an embedding search to find candidate files, then walks the call graph from the hits to pull in the dependencies those candidates need to make sense. Use structure for "what must be here to be correct" and semantics for "what else is worth looking at," and the union beats either alone. One more reason to prefer a stable, structure-derived slice: when the same dependency set leads a sequence of requests, the model can reuse its internal work on that shared prefix instead of recomputing it, which is the subject of Chapter 6. A lean slice is cheaper to send and, kept stable, cheaper to send again.

Using the real tool: commands and before/after proof

The demo above computed the slice by hand. The two tools below do the same selection on a real repository, with commands you can run. Both replace the lazy "paste the whole file" habit with "send only the parts the task touches."

Start with Claude Code, because it is the clearest example of the principle.

A worked Claude Code session

Claude Code is Anthropic's command-line coding agent. The -p flag (for print) runs it once, non-interactively: you hand it a single instruction, it works the task, prints the result, and exits, with no back-and-forth chat. Picture a repository whose deploy.py has a test failing on an assertion inside the deploy() function. You run, from the repo root:

# -p runs Claude Code once and prints the result (no interactive session).
claude -p "fix the failing assertion in deploy()"

What it does next is the whole point of this chapter, watched live. Claude Code does not read the repository into the prompt and ask the model to hunt for deploy in a wall of text. It navigates the way a person would, one targeted lookup at a time. Here is an annotated trace of the tool calls it makes (the bracketed notes are mine, not part of the output):

> grep -n "def deploy" .          [grep: a text search. Finds where deploy lives.]
  deploy.py:42:def deploy(env, artifact):

> read deploy.py:42-78            [reads ONLY deploy(), not the 600-line file.]
  ...def deploy(env, artifact):
       cfg = load_config(env)
       url = build_url(cfg, artifact)
       assert url.startswith("https://")   <- the failing assertion
       return push(url) ...

> grep -n "def load_config\|def build_url" .   [the two functions deploy() calls]
  config.py:12:def load_config(env)
  config.py:30:def build_url(cfg, artifact)

> read config.py:12-44            [reads only those two callees, not config.py whole]
  ...def build_url(cfg, artifact):
       return cfg["scheme"] + "://" + cfg["host"] + "/" + artifact   <- scheme is "http"

Notice the shape of that walk. It greps for the symbol to locate the file and line, reads only deploy(), sees that deploy() calls load_config and build_url, and reads only those two. Those callees are the transitive dependencies of deploy(): the functions it reaches by following call edges, the same set the BFS in the AST demo computed. It stops there. It never opens push() (the bug is upstream of it), never touches the unrelated functions in the file, and never reads the rest of the repo. The fix (a wrong scheme default of "http") is visible in exactly the slice the call graph predicts.

You can watch the window stay lean while this happens. Inside an interactive session, /context shows what is loaded; /cost shows tokens spent so far. After the trace above it reads roughly:

> /context
  Loaded: deploy.py (1 function), config.py (2 functions), CLAUDE.md
  Context: ~3.2k / 200k tokens used

Three small function bodies, not three whole files. Contrast the naive alternative: open deploy.py and paste it whole, or worse, cat src/*.py into the prompt. On a small project that spends tokens on functions the bug never touches; on a real repository the dump does not fit in the window at all, so the request fails before the model reads a line. /context is how you see the difference: a few functions loaded versus a flooded (or overflowed) window.

You can compress even the targeted reads it does make. lean-ctx (the tool from the "in the wild" list above) runs as an MCP server: MCP, the Model Context Protocol, is a standard way to register an external program that exposes extra tools to the agent. You add it once,

claude mcp add lean-ctx -- lean-ctx serve

and from then on Claude Code can reach for lean-ctx's leaner context tools, which assemble a slice from the code graph and run command output through a compressor that, on its reported numbers, strips 60 to 90 percent of the tokens a raw dump would carry. The navigation stays the same; each read just costs less. That is the same trade as the from-scratch demo, which cut its module from 181 tokens to 65, a 64% reduction, applied to every read the live agent makes rather than to one module by hand.

The second tool is Aider, an open-source command-line coding assistant. It builds a repo map: a compact index of the repository's definitions (functions, classes, and their call signatures) rather than their full bodies, ranked by relevance to the current task with a graph algorithm over the file dependency structure. Under the hood the map is built with tree-sitter, the same parser the "wild" section mentioned, which gives Aider an AST for dozens of languages instead of the single-file Python ast of our demo.

pip install aider-chat
aider                       # builds a repo map of the relevant definitions, not whole files
aider --map-tokens 1024     # bound the map to roughly 1024 tokens

The --map-tokens option is the lever that matters here. It caps how many tokens the repo map may occupy, so Aider keeps the highest-ranked definitions and drops the rest to fit the budget. That is the same trade the call-graph BFS makes (keep what the task needs, drop the rest), spent as an explicit token budget instead of a reachability set.

Now prove the win. The metric is input tokens, and the exact count comes from client.messages.count_tokens, not a word-count estimate (the Anthropic SDK; the model id this book uses is claude-opus-4-8). Count the prompt twice, once with the whole file or repo dumped in and once with just the targeted slice (the function plus its dependencies, exactly what the from-scratch demo selected), and read .input_tokens off each result:

from anthropic import Anthropic

client = Anthropic()

def input_tokens(code: str) -> int:
    resp = client.messages.count_tokens(
        model="claude-opus-4-8",
        messages=[{"role": "user", "content": code}],
    )
    return resp.input_tokens

whole = input_tokens(open("module.py").read())   # everything
slice_ = input_tokens(selected_slice_text)        # target + its deps only
print(whole, slice_, f"{1 - slice_/whole:.0%} fewer")
whole module ~500 tokens -> structure-aware slice ~150 tokens (70% fewer)

Those two numbers are illustrative: the shapes to expect, not a measurement from this box (count_tokens needs a network call and an API key, which the verification setup does not have). The measurement we did run is the AST demo above, which cut its module from 181 tokens to 65 (a 64% reduction) and verified that nothing the target calls went missing. That on-box result is the proof; count_tokens is how you reproduce the same before/after on a real file or repo, with exact counts instead of an estimate.

Further reading

  • Python ast module (docs.python.org). The standard-library reference for the exact node types this chapter uses, FunctionDef, Call, Name, and the walkers (ast.walk, ast.NodeVisitor) that traverse them. It is the documentation behind the from-scratch demo.
  • tree-sitter (tree-sitter.github.io). The parser-generator that produces ASTs for dozens of languages and the query syntax for pulling out specific definitions. This is what the multi-language selectors and Aider's repo map are built on.
  • Aider repo map (aider.chat). Aider's own documentation for how it builds the repo map, ranks definitions by relevance, and bounds the result with --map-tokens. A concrete writeup of the structure-derived index described above.
  • lean-ctx (github.com/yvgude/lean-ctx). The code-graph context tool used in the worked session, including its MCP server. Read it for how a graph slice plus a compressor combine in a live agent.

Takeaways

  • Code has structure (calls, imports, inheritance) that prose does not, so you compress it by selecting whole units along that structure, not by ranking or trimming lines.
  • The AST turns source text into a tree you can query precisely. A FunctionDef is a def; a Call node's name tells you what it invokes. The standard-library ast module is all you need for Python.
  • A call graph plus a graph search from the target yields its transitive dependencies: the exact slice the model needs. The demo cut a module from 181 to 65 tokens (64%) while keeping every function the target actually calls.
  • Structural relevance (what the code calls) and semantic relevance (what the code is about) are different filters. For "what does this function need to run," structure is the correct one; a suggestive name is not evidence and an ugly name is not disqualifying.
  • Real tools do this at scale: CodeCompressor, lean-ctx, tree-sitter selectors, and Aider's repo map all send a structured slice, and coding agents navigate to targeted files instead of pasting the repo.

👉 Selecting code by structure is one case of a larger lever: choosing which of a whole corpus ever reaches the window at all. The next chapter generalizes it to retrieval and chunking, then the caching family follows. Continue to Retrieval and chunking.