context-prune

Context window management: context budgeting, priority-based pruning, conversation summarization, reference replacement, token counting, strategic truncation, and context window optimization for Claude/GPT models.

Context Prune

Techniques for managing context windows in LLM-assisted development: context budgeting, priority-based pruning, conversation summarization, reference replacement, token counting, strategic truncation, and optimizing context for Claude, GPT, and other models.

When to Use

  • Context window approaching its limit during a long session
  • Session contains many irrelevant earlier messages
  • Need to continue working on a large codebase in one session
  • Transferring context between sessions (handoff)
  • Optimizing prompts for cost or latency
  • Working with large files that consume significant context budget
  • Model starts losing track of earlier instructions

Core Concepts

1. Context Window Budgets

ModelContext WindowRecommended Budget (70% for work)
Claude 3.5 Sonnet200K tokens140K tokens
Claude 3 Opus200K tokens140K tokens
GPT-4o128K tokens90K tokens
GPT-4 Turbo128K tokens90K tokens
Claude 3 Haiku200K tokens140K tokens

Token budget allocation:

┌─────────────────────────────────────────────────────────────┐
│                    CONTEXT BUDGET                           │
│                                                             │
│  System prompt    │  5-10%   │  7K-14K tokens               │
│  Instructions     │  5-10%   │  7K-14K tokens               │
│  Active files     │  40-50%  │  56K-70K tokens              │
│  Conversation     │  20-25%  │  28K-35K tokens              │
│  Reserved (output)│  10%     │  14K tokens (generation)     │
└─────────────────────────────────────────────────────────────┘

2. Token Estimation

Quick estimation rules:

  • 1 token ≈ 4 characters (English text)
  • 1 token ≈ 0.75 words
  • Code is denser: ~3 chars/token (more symbols, less natural language)
  • File paths: ~1 token per path segment
def estimate_tokens(text: str) -> int:
    """Rough token estimation (4 chars ≈ 1 token for English, 3 for code)."""
    if any(c in text for c in "{}();<>=[]"):
        return len(text) // 3  # Code-like content
    return len(text) // 4      # English text


def estimate_file_tokens(filepath: str) -> int:
    """Estimate tokens for a file's contents."""
    try:
        with open(filepath, "r") as f:
            content = f.read()
        return estimate_tokens(content)
    except (OSError, IOError):
        return 0


def context_budget(files: list[str], max_tokens: int = 140_000) -> dict:
    """Calculate context budget and warn if approaching limit."""
    total = 0
    file_costs = {}

    for f in files:
        cost = estimate_file_tokens(f)
        file_costs[f] = cost
        total += cost

    system_overhead = 15_000  # System prompt + instructions
    output_reserve = 14_000   # Reserve for model output
    available = max_tokens - system_overhead - output_reserve

    return {
        "total_file_tokens": total,
        "available_budget": available,
        "remaining": available - total,
        "usage_pct": round(total / available * 100, 1),
        "file_costs": file_costs,
        "at_risk": total > available,
    }

3. Priority-Based Pruning

Not all context is equally valuable. Prune by priority:

Priority 1 (KEEP):     Current task, active file, most recent exchange
Priority 2 (SUMMARIZE): Earlier relevant files, important decisions
Priority 3 (REPLACE):   Full file contents → file paths + brief descriptions
Priority 4 (DISCARD):   Explored-but-irrelevant files, failed attempts, debug output

Pruning algorithm:

from dataclasses import dataclass, field
from enum import Enum


class ContextPriority(Enum):
    CRITICAL = 1    # Must keep verbatim
    HIGH = 2        # Keep, but can summarize
    MEDIUM = 3      # Replace with reference + summary
    LOW = 4         # Safe to discard


@dataclass
class ContextItem:
    content: str
    priority: ContextPriority
    description: str  # Brief description for replacement
    token_count: int
    age: int = 0       # How many messages ago this was added


def prune_context(
    items: list[ContextItem],
    max_tokens: int = 120_000,
) -> list[ContextItem]:
    """
    Prune context items to stay within token budget.

    Strategy:
    1. Keep all CRITICAL items
    2. Summarize HIGH items if over budget
    3. Replace MEDIUM items with references
    4. Discard LOW items first
    """
    total = sum(item.token_count for item in items)

    if total <= max_tokens:
        return items  # No pruning needed

    # Phase 1: Discard LOW priority items
    pruned = [i for i in items if i.priority != ContextPriority.LOW]
    total = sum(i.token_count for i in pruned)

    if total <= max_tokens:
        return pruned

    # Phase 2: Replace MEDIUM with references
    result = []
    for item in pruned:
        if item.priority == ContextPriority.MEDIUM:
            # Replace full content with path + 1-line description
            replacement = f"[Reference: {item.description}]"
            result.append(ContextItem(
                content=replacement,
                priority=item.priority,
                description=item.description,
                token_count=len(replacement) // 4,
            ))
        else:
            result.append(item)

    total = sum(i.token_count for i in result)

    if total <= max_tokens:
        return result

    # Phase 3: Summarize HIGH priority items
    final = []
    for item in result:
        if item.priority == ContextPriority.HIGH:
            summary = summarize_content(item.content, max_sentences=3)
            final.append(ContextItem(
                content=summary,
                priority=item.priority,
                description=item.description,
                token_count=len(summary) // 4,
            ))
        else:
            final.append(item)

    return final


def summarize_content(content: str, max_sentences: int = 3) -> str:
    """Extractive summarization: keep first N sentences."""
    sentences = content.split(". ")
    return ". ".join(sentences[:max_sentences]) + "."

4. Conversation Summarization

When the conversation history grows large, summarize earlier exchanges:

Before pruning (verbose, ~2000 tokens):

User: Can you help me refactor the auth module? It's getting too complex.
      The main file is src/auth/service.py and it has about 800 lines.
      I think we should split it into separate modules for token management,
      user validation, and session handling.

Assistant: I'll help you refactor the auth module. Let me first read the
           current file to understand the structure...
           [reads 800-line file]
           I can see three clear responsibilities here...

After summarization (~200 tokens):

Context: Refactoring src/auth/service.py (800 lines) into 3 modules:
  - token_management.py
  - user_validation.py
  - session_handler.py
  Assistant analyzed file, identified 3 responsibility boundaries.

Python — Session Summarizer:

@dataclass
class Message:
    role: str  # "user" or "assistant"
    content: str
    token_count: int


def summarize_conversation(
    messages: list[Message],
    max_tokens: int = 20_000,
    keep_last: int = 5,  # Always keep the last N messages verbatim
) -> list[Message]:
    """
    Summarize earlier messages in a conversation to reduce token usage.

    Preserves:
    - The system message (always)
    - The last `keep_last` messages (always)
    - Summarizes everything in between
    """
    if len(messages) <= keep_last + 1:
        return messages  # No summarization needed

    total = sum(m.token_count for m in messages)
    if total <= max_tokens:
        return messages  # Under budget

    # Extract messages to summarize (everything between system and keep_last)
    system_msg = messages[0] if messages[0].role == "system" else None
    recent = messages[-keep_last:]
    to_summarize = messages[1:-keep_last] if system_msg else messages[:-keep_last]

    # Build summary of earlier conversation
    summary_parts = []
    current_topic = None
    topic_actions = []

    for msg in to_summarize:
        # Extract key actions/decisions from each message
        first_line = msg.content.split("\n")[0][:200]
        topic_actions.append(f"{msg.role}: {first_line}")

    summary_content = (
        f"Earlier conversation summary ({len(to_summarize)} messages):\n"
        + "\n".join(topic_actions[:10])  # Cap at 10 summary points
        + (f"\n... and {len(topic_actions) - 10} more exchanges" if len(topic_actions) > 10 else "")
    )

    summary_msg = Message(
        role="summary",
        content=summary_content,
        token_count=len(summary_content) // 4,
    )

    result = []
    if system_msg:
        result.append(system_msg)
    result.append(summary_msg)
    result.extend(recent)

    return result

5. Reference Replacement Pattern

Replace full file contents with structured references. The model can always ask to read a file if it needs the details.

Before (full file, ~3000 tokens):

Here's the contents of src/api/users.py:

[800 lines of code...]

After (reference replacement, ~50 tokens):

File: src/api/users.py (342 lines, 8 functions, 2 classes)
  - Class: UserRouter (FastAPI router, 12 endpoints)
  - Class: UserCreate (Pydantic model, 4 fields)
  - Functions: create_user, get_user, list_users, update_user, delete_user, ...
  Key imports: app.db, app.models.user, app.services.auth

Bash — Generate File Reference:

#!/usr/bin/env bash
# file-ref.sh — Generate a compact reference for a file
# Usage: file-ref src/api/users.py

file="$1"
if [[ ! -f "$file" ]]; then
  echo "File not found: $file"
  exit 1
fi

lines=$(wc -l < "$file")
chars=$(wc -c < "$file")

echo "File: $file ($lines lines, $chars chars)"

# Python: extract classes and functions
if [[ "$file" == *.py ]]; then
  echo "  Classes: $(grep -c '^class ' "$file")"
  echo "  Functions: $(grep -c '^def \|^async def ' "$file")"
  grep '^class \|^def \|^async def ' "$file" | head -20 | sed 's/^/    - /'

# TypeScript: extract exports, classes, functions
elif [[ "$file" == *.ts || "$file" == *.tsx ]]; then
  echo "  Exports: $(grep -c 'export ' "$file")"
  echo "  Functions: $(grep -c 'function \|const.*=.*(' "$file")"
  grep 'export \|function \|^const.*= ' "$file" | head -20 | sed 's/^/    - /'
fi

# Top imports
echo "  Imports:"
if [[ "$file" == *.py ]]; then
  grep '^import \|^from ' "$file" | head -10 | sed 's/^/    - /'
elif [[ "$file" == *.ts || "$file" == *.tsx ]]; then
  grep '^import ' "$file" | head -10 | sed 's/^/    - /'
fi

6. Strategic Truncation

When context is critically over budget, truncate systematically:

def truncate_for_context(
    files: list[str],
    max_tokens: int,
    priority_files: list[str] | None = None,
) -> dict[str, str]:
    """
    Truncate file contents to fit within token budget.

    Strategy:
    1. Always include priority_files in full
    2. For other files, include only the first N lines + function signatures
    3. Truncate large files to their public API (class defs, function signatures)
    """
    priority_files = priority_files or []
    result = {}
    budget_remaining = max_tokens

    # Phase 1: Include priority files in full
    for filepath in priority_files:
        try:
            with open(filepath) as f:
                content = f.read()
            tokens = estimate_tokens(content)
            if tokens <= budget_remaining:
                result[filepath] = content
                budget_remaining -= tokens
        except (OSError, IOError):
            pass

    # Phase 2: Include truncated versions of other files
    for filepath in files:
        if filepath in result or filepath in priority_files:
            continue

        try:
            with open(filepath) as f:
                lines = f.readlines()
        except (OSError, IOError):
            continue

        # Keep first 30 lines (imports, module docstring) + last 10 lines
        # This preserves the "shape" of the file
        if len(lines) > 50:
            truncated = "".join(lines[:30]) + "\n... [truncated] ...\n" + "".join(lines[-10:])
        else:
            truncated = "".join(lines)

        tokens = estimate_tokens(truncated)
        if tokens <= budget_remaining:
            result[filepath] = truncated
            budget_remaining -= tokens

    return result

7. Context Handoff Between Sessions

When switching sessions or handing off to another developer:

# Session Handoff Template

## Current Task
Refactor `src/auth/service.py` into 3 modules.

## What's Done
- [x] Analyzed original file (800 lines, 15 functions)
- [x] Identified 3 responsibility boundaries
- [x] Created `token_management.py` (200 lines)
- [x] Created `user_validation.py` (150 lines)

## What's In Progress
- [ ] Create `session_handler.py` (estimated 180 lines)
- [ ] Update imports in `src/auth/__init__.py`
- [ ] Run full test suite

## Key Context
- Original file: src/auth/service.py (kept for reference, not modifying)
- New modules go in: src/auth/token_management.py, src/auth/user_validation.py, src/auth/session_handler.py
- Tests are in: tests/auth/test_service.py (needs splitting too)
- Framework: FastAPI + SQLAlchemy + Pydantic
- All new modules must pass: ruff check, mypy, pytest

## Decisions Made
- Split by responsibility (not by size)
- Keep UserCreate schema in user_validation.py (it's validation logic)
- Token refresh stays in token_management.py (not session_handler)

## Files to Read Next
1. src/auth/service.py (original, for session_handler extraction)
2. tests/auth/test_service.py (for test splitting)

Anti-Patterns

Anti-PatternSymptomFix
No pruningContext window fills, model loses trackPrune by priority every 50K tokens
Truncating active filesModel can't see the code it's editingNever truncate files currently being modified
Summarizing too aggressivelyModel loses critical detailsSummarize only MEDIUM/LOW priority items
Keeping all debug outputWastes 10K+ tokens on stack tracesDiscard debug output after issue resolved
No handoff documentNext session starts from zeroAlways write a session handoff
File paths without contentModel can't reference the codeInclude first 30 lines + signatures as minimum

Pruning Decision Flowchart

Is this item part of the current task?
  │
  ├── No ──▶ DISCARD
  │
  Yes
  │
  Is it a file the model is actively editing?
  │
  ├── Yes ──▶ KEEP VERBATIM (Priority 1)
  │
  No
  │
  Is it a file referenced in the current task?
  │
  ├── Yes ──▶ Keep full if under 200 lines, else replace with reference (Priority 2/3)
  │
  No
  │
  Is it an earlier conversation exchange?
  │
  ├── Yes ──▶ Summarize to 3-5 key points (Priority 3)
  │
  No
  │
  Is it system/instruction context?
  │
  ├── Yes ──▶ KEEP (but trim verbose examples)
  │
  No
  │
  DISCARD

Success Checklist

  • Context token budget calculated and tracked
  • Priority levels defined (CRITICAL, HIGH, MEDIUM, LOW)
  • Low-priority items discarded when over budget
  • Medium-priority items replaced with references
  • High-priority items summarized when necessary
  • Critical items (active files) kept verbatim
  • Conversation summarized before session handoff
  • File reference script available (file-ref.sh)
  • Session handoff template documented
  • Output tokens reserved (10% of budget)