Lesson 5 / 29

Reading and Searching a Codebase

Understand repo maps, grep-style search and why agents read selectively.

Find the right files first

A real repository is far larger than a context window, so an agent must find the relevant code before it can change it. Typical tools: file listing and glob (by name), text search (grep or ripgrep, for symbols and strings), read file (often by line range to save tokens), and a repo map: a compact outline of files and their classes and functions, built by parsing the code, that lets the model see the project's shape in a few hundred tokens and decide what to open. Some tools add semantic search with embeddings, or language-server features such as "go to definition" and "find references". Good agents read selectively and iteratively: search, open a small range, follow a reference, then edit. Reading too little causes wrong edits; reading everything wastes context and money.

See, edit, run, record

A small set of reliable tools — search, read, edit, run and git — does most of the work.

Four toolsets: read, edit, run, share.
Figure 2.1 — Read, edit, run and share.

Building a repo map with ast, run

I ran this with plain Python 3 (standard library only), using a throwaway project created in a temporary folder. The tiny sample project used in several examples is created by the make_project helper shown in the first example that uses it; later examples start with the same helper. The script parses every Python file and prints classes, methods and functions: eight lines that let a model see the whole project's shape. The sample project has a small pricing module, a cart and tests.

import os, tempfile, textwrap

def make_project(root):
    files = {
        "shop/__init__.py": "",
        "shop/pricing.py": textwrap.dedent("""
            def apply_discount(price, percent):
                \"\"\"Return price after a percentage discount.\"\"\"
                return price - price * percent / 10

            def add_tax(price, rate=0.18):
                return round(price * (1 + rate), 2)
        """),
        "shop/cart.py": textwrap.dedent("""
            from shop.pricing import apply_discount, add_tax

            class Cart:
                def __init__(self):
                    self.items = []

                def add(self, name, price):
                    self.items.append((name, price))

                def total(self, discount_percent=0):
                    subtotal = sum(p for _, p in self.items)
                    return add_tax(apply_discount(subtotal, discount_percent))
        """),
        "tests/__init__.py": "",
        "tests/test_pricing.py": textwrap.dedent("""
            import unittest
            from shop.pricing import apply_discount, add_tax

            class PricingTests(unittest.TestCase):
                def test_discount_ten_percent(self):
                    self.assertEqual(apply_discount(200, 10), 180)

                def test_discount_zero(self):
                    self.assertEqual(apply_discount(200, 0), 200)

                def test_tax(self):
                    self.assertEqual(add_tax(100), 118.0)
        """),
    }
    for rel, text in files.items():
        path = os.path.join(root, rel)
        os.makedirs(os.path.dirname(path), exist_ok=True)
        with open(path, "w") as f:
            f.write(text.lstrip("\n"))

import ast

def outline(root):
    lines = []
    for dirpath, _, names in sorted(os.walk(root)):
        for name in sorted(names):
            if not name.endswith(".py"): continue
            rel = os.path.relpath(os.path.join(dirpath, name), root)
            tree = ast.parse(open(os.path.join(dirpath, name)).read())
            symbols = []
            for node in tree.body:
                if isinstance(node, ast.FunctionDef):
                    symbols.append(f"def {node.name}({', '.join(a.arg for a in node.args.args)})")
                elif isinstance(node, ast.ClassDef):
                    methods = [n.name for n in node.body if isinstance(n, ast.FunctionDef)]
                    symbols.append(f"class {node.name}: {', '.join(methods)}")
            if symbols:
                lines.append(rel + "\n  " + "\n  ".join(symbols))
    return "\n".join(lines)

with tempfile.TemporaryDirectory() as root:
    make_project(root)
    print(outline(root))

Output:

shop/cart.py
  class Cart: __init__, add, total
shop/pricing.py
  def apply_discount(price, percent)
  def add_tax(price, rate)
tests/test_pricing.py
  class PricingTests: test_discount_ten_percent, test_discount_zero, test_tax

Quick check: What is a repo map for?

  • Encrypting the repository
  • Showing the project's shape compactly so the model can choose what to open
  • Running the tests
  • Deleting dead code
Answer

Showing the project's shape compactly so the model can choose what to open — A compact outline helps navigation without spending the whole context window.