DEV Community

Dakota Wu
Dakota Wu

Posted on

AST-Aware Context Pruning: Cut AI Tokens 60-80%

Sending a whole source file to an AI coding model is the fastest way to burn a limited allowance—most tokens go to imports, docstrings, and formatting the model already understands. I cut that context by 60–80% with an AST-based pruner that keeps only the symbols that change the answer: function signatures, class bases, and top-level assignments.

This workflow targets developers who use a free AI coding tier and want to make it last. MonkeyCode provides a free model tier and a free server for agent sessions, and this pruning technique works well on both. Disclosure: This article was prepared as part of MonkeyCode's product outreach.

Why Context Size Matters on a Free AI Tier

Every token I send costs part of my allowance. Large contexts also increase latency, because the model processes the whole prompt before generating a single token. On a free tier the allowance is finite, and the server may be shared. Wasting tokens on boilerplate is not just inefficient; it shortens the number of tasks I can complete in a session.

A source file is mostly boilerplate. Imports, type aliases, and long docstrings rarely change the model's answer. What the model needs is structure: function names, parameters, class hierarchy, and key globals. That structure is exactly what an Abstract Syntax Tree (AST) contains, and Python's standard-library ast module parses source into those nodes without a third-party parser.

I treat the following as signal, and everything else as noise:

  • Function names and parameter lists
  • Class names and base classes
  • Top-level assignments that mark important globals
  • A short intent hint from the first-line docstring

On a typical 300-line module, the skeleton usually fits in 30 lines. Character count is only a proxy for tokens, but a 60–80% reduction is consistent enough that I treat it as four to five times more tasks per allowance. Compared with pasting the full file, the skeleton is smaller, faster to send, and still names every symbol the model must locate.

Build a Symbol Skeleton in About Twenty Lines

The pruner parses a Python file and emits a compact skeleton. It keeps function signatures, class definitions, and top-level assignments. It drops everything else. The output is valid enough for a model to understand the codebase's shape without seeing the full implementation.

#!/usr/bin/env python3
"""Extract a compact symbol skeleton from a Python file."""
import ast
import sys
from pathlib import Path

def prune(source: str) -> str:
    tree = ast.parse(source)
    lines = []

    for node in tree.body:
        if isinstance(node, ast.FunctionDef):
            args = ", ".join(a.arg for a in node.args.args)
            lines.append(f"def {node.name}({args}):")
            if node.body:
                first = node.body[0]
                if (isinstance(first, ast.Expr)
                        and isinstance(first.value, ast.Constant)
                        and isinstance(first.value.value, str)):
                    lines.append(f"    # {first.value.value.strip()[:80]}")
        elif isinstance(node, ast.ClassDef):
            bases = ", ".join(b.id for b in node.bases if isinstance(b, ast.Name))
            lines.append(f"class {node.name}({bases}):")
        elif isinstance(node, ast.Assign):
            targets = ", ".join(t.id for t in node.targets if isinstance(t, ast.Name))
            if targets:
                lines.append(f"{targets} = ...")
    return "\n".join(lines)

if __name__ == "__main__":
    source = Path(sys.argv[1]).read_text()
    print(prune(source))
Enter fullscreen mode Exit fullscreen mode

Run it on any Python file:

python prune.py your_file.py
Enter fullscreen mode Exit fullscreen mode

The output is a few lines per symbol. For a 300-line module, the skeleton usually fits in 30 lines. The contrast for a single function looks like this:

  • Full source: decorators, body-level branches, logging, and a multi-paragraph docstring
  • Skeleton: def parse_range(start, end): plus # Slice a half-open interval and clamp to bounds

The second form is enough when the task is "fix the off-by-one error in the slice."

I walk tree.body so I only see top-level statements, not local variables or nested expressions. The AST abstract grammar lists every node type if I later want to keep async def or annotated assignments. If I collect symbols into a set, order can be lost; for a prompt, order rarely matters. If it does, I keep this sequential walk.

The pruner emits three kinds of lines:

  1. Functions — parameter names tell the model what each function expects
  2. Classes — base classes reveal inheritance
  3. Top-level assignmentsname = ... flags globals without their values

Docstrings are truncated to the first 80 characters. That keeps the skeleton readable while preserving intent. I skip imports, method bodies, non-docstring comments, and literals on purpose: the model already knows common libraries, and I do not want implementation noise in the prompt.

Prompt, Measure, and Compare Before You Send

With a skeleton in hand, I stop embedding the whole file. I embed the skeleton and describe the task:

def build_prompt(file_path, skeleton, task):
    return f"""You are editing {file_path}.

Symbol skeleton:
{skeleton}

Task: {task}

Return only the code changes as a diff."""

# Usage
skeleton = prune(Path("parser.py").read_text())
prompt = build_prompt("parser.py", skeleton, "Fix the off-by-one error in the slice.")
Enter fullscreen mode Exit fullscreen mode

The model receives the structure it needs to locate the bug, without the noise of the full implementation. This works because most coding tasks are local: a bug in one function, a feature that mirrors an existing pattern, a rename that touches a few call sites.

My prompt checklist:

  1. Name the file so the model can emit a correct diff header
  2. Paste the skeleton, not the source
  3. State the task in one sentence
  4. Ask for a diff only, so the reply stays small too
  5. If the task needs a literal (a regex or an error message), paste those few lines as a supplement

To know whether pruning helps, I measure prompt size before and after. A full tokenizer requires an external library, but character count is a decent proxy:

#!/usr/bin/env python3
"""Compare raw source vs pruned skeleton size."""
import sys
from pathlib import Path
from prune import prune

def main():
    source = Path(sys.argv[1]).read_text()
    skeleton = prune(source)
    print(f"raw source: {len(source)} chars")
    print(f"skeleton:   {len(skeleton)} chars")
    print(f"reduction:  {100 * (1 - len(skeleton) / len(source)):.1f}%")

if __name__ == "__main__":
    main()
Enter fullscreen mode Exit fullscreen mode

Run it:

python compare.py your_file.py
Enter fullscreen mode Exit fullscreen mode

On a typical module, the reduction lands between 60% and 80%. That means four to five times more tasks per allowance. The exact number depends on how much of the file is docstrings and imports, so I measure on my own codebase. I record three numbers each time: raw character count, skeleton character count, and reduction percentage.

When to Skip Pruning—Then Try It on One File

Pruning works best for small, local edits. A model that sees only a skeleton cannot reason about cross-function data flow, subtle state mutations, or type interactions that span multiple modules. For those tasks, the full context is necessary.

The skeleton also loses comments that are not docstrings, and it drops all string literals. If the task involves changing a message or a regex, the model needs the original value. In that case, I include the specific lines as a supplement.

I skip pruning when:

  • The bug is a data-flow issue across several functions
  • I am changing string literals, regexes, or magic numbers
  • The file is already tiny
  • I need architectural reasoning, not a mechanical edit

Teams working under strict compliance rules should still check whether sending a skeleton is acceptable. The skeleton is derived from the source, so it can leak names and structure. The free server executes your code on infrastructure you do not control; treat the skeleton as sensitive data.

This workflow is not for everyone. If the codebase is tiny, or the tasks require deep architectural understanding, pruning will hurt more than help. I use it for the long tail of mechanical edits: fixing bugs, adding parameters, updating call sites, and writing tests.

A finite allowance changes the economics of AI coding. The cheapest token is the one I never send. A ten-line pruner can double or triple the useful work I get from a free tier, and it runs in seconds on a free server.

If you already use MonkeyCode's free model and server, add this script to your workflow and watch your task count climb. Run prune.py on one module you actually edit, then run compare.py and write down the reduction. If the cut is above 60% and your next task is a local fix, send the skeleton instead of the file—and share the percentage you measured, or the case where the skeleton was not enough, so the next pass is grounded in real files.

Top comments (0)