Context Window Management

20 min read Module 7 of 12 Topic 7 of 12

What you'll learn

  • Understand why context overflow is a critical agent failure mode
  • Implement conversation summarisation to compress long histories
  • Use selective retrieval to bring in only relevant context
  • Track token usage in real-time to avoid hitting limits
Building this at your company? For enterprise and company teams taking this to production: book a 30-minute session with our AI engineers for architecture guidance, code review, and a rollout plan for your use case.
Book a Team Session

The Context Window Is Like RAM

Your computer’s RAM holds currently-active data. When RAM fills up, the computer slows to a crawl (swapping to disk) or crashes. The context window is similar: it’s the agent’s working memory, holding everything it’s currently thinking about.

A long-running agent task generates a lot of text:

  • System prompt: ~500 tokens
  • User goal: ~100 tokens
  • Tool call 1 and result: ~500 tokens
  • Tool call 2 and result: ~2,000 tokens (web search results are verbose)
  • Tool call 3 and result: ~1,500 tokens
  • … after 20 tool calls: easily 30,000-50,000 tokens

For a 128k context window, this is manageable. But for longer tasks, multi-day pipelines, or models with smaller windows, context overflow is a real failure mode.

The solution is context management, actively monitoring and trimming context before it hits the limit.


Tracking Token Usage

First, always track how much context you’re using:

import tiktoken
from openai import OpenAI

client = OpenAI()
enc = tiktoken.encoding_for_model('gpt-5.6-sol')

def count_tokens(messages: list[dict]) -> int:
    """Count tokens in a messages array."""
    total = 0
    for msg in messages:
        # Each message has overhead tokens (role, formatting)
        total += 4
        
        content = msg.get('content', '')
        if isinstance(content, str):
            total += len(enc.encode(content))
        elif isinstance(content, list):
            # Multi-modal content (text + images)
            for block in content:
                if block.get('type') == 'text':
                    total += len(enc.encode(block['text']))
    
    return total

Use this before every API call to check headroom:

MAX_TOKENS      = 128_000
SAFETY_MARGIN   = 10_000   # always leave room for the response

def check_context_headroom(messages: list[dict]) -> int:
    used = count_tokens(messages)
    available = MAX_TOKENS - SAFETY_MARGIN - used
    print(f"Context: {used:,} / {MAX_TOKENS:,} tokens ({available:,} remaining)")
    return available

When available drops below a threshold (say, 20,000 tokens), trigger compression.


Strategy 1: Conversation Summarisation

The most effective technique: periodically compress older messages into a summary.

def summarise_history(messages: list[dict], keep_last: int = 4) -> list[dict]:
    """
    Compress older messages into a summary.
    Keeps the system prompt + last N messages intact.
    Summarises everything in between.
    """
    if len(messages) <= keep_last + 1:
        return messages   # nothing to summarise
    
    system_msg = messages[0]       # always keep system prompt
    recent = messages[-keep_last:] # always keep recent messages
    to_compress = messages[1:-keep_last]  # compress the middle
    
    if not to_compress:
        return messages

Now ask the LLM to summarise the middle section:

    compress_text = '\n'.join([
        f"{m['role'].upper()}: {m.get('content', '')[:500]}"
        for m in to_compress
        if isinstance(m.get('content'), str)
    ])
    
    summary_response = client.chat.completions.create(
        model='gpt-5.6-luna',   # use cheaper model for summarisation
        messages=[{
            'role': 'user',
            'content': f"""Summarise this conversation history concisely.
Preserve all key facts, decisions, tool results, and progress made.
Be specific: include numbers, names, and conclusions.

HISTORY:
{compress_text}

Provide a 3-5 sentence summary:"""
        }],
        max_tokens=300,
        temperature=0.0,
    )
    
    summary_text = summary_response.choices[0].message.content
    
    summary_message = {
        'role': 'system',
        'content': f'[Compressed history summary]\n{summary_text}'
    }
    
    return [system_msg, summary_message] + recent

Now integrate this into your agent loop:

def agent_with_context_management(user_message: str) -> str:
    messages = [
        {'role': 'system', 'content': AGENT_SYSTEM_PROMPT},
        {'role': 'user',   'content': user_message},
    ]
    
    while True:
        # Check context before each call
        if check_context_headroom(messages) < 20_000:
            print("⚠ Context getting full, compressing history...")
            messages = summarise_history(messages, keep_last=6)
        
        response = client.chat.completions.create(
            model='gpt-5.6-sol',
            messages=messages,
            tools=tools,
        )
        
        msg = response.choices[0].message
        messages.append(msg)
        
        if not msg.tool_calls:
            return msg.content
        
        # Handle tool calls...

Strategy 2: Selective Retrieval Instead of Full History

For agents that accumulate large amounts of past context (multi-session, knowledge-base agents), instead of loading everything, load only what’s relevant:

def build_selective_context(query: str, all_sessions: list[dict]) -> str:
    """Retrieve only relevant past sessions for this query."""
    
    # Embed the current query
    query_embedding = client.embeddings.create(
        model='text-embedding-3-small',
        input=query
    ).data[0].embedding
    
    # Score each past session by relevance
    def relevance_score(session: dict) -> float:
        session_embedding = session.get('embedding', [])
        if not session_embedding:
            return 0.0
        # Cosine similarity between query and session
        import numpy as np
        a, b = np.array(query_embedding), np.array(session_embedding)
        return float(np.dot(a, b) / (np.linalg.norm(a) * np.linalg.norm(b)))
    
    # Sort by relevance and take top 5
    scored = sorted(all_sessions, key=relevance_score, reverse=True)
    top_sessions = scored[:5]
    
    # Build a compact context string
    context_parts = []
    for s in top_sessions:
        context_parts.append(f"[Past session {s['date']}]: {s['summary']}")
    
    return '\n'.join(context_parts)

Strategy 3: Chunking Large Tool Results

Sometimes a single tool returns enormous results (reading a 10,000-word article, scraping a large page). Truncate or chunk before adding to context:

def trim_tool_result(result: str, max_tokens: int = 2000) -> str:
    """Trim a tool result to fit within a token budget."""
    tokens = enc.encode(result)
    if len(tokens) <= max_tokens:
        return result
    
    # Truncate and add a note
    trimmed = enc.decode(tokens[:max_tokens])
    return trimmed + f"\n\n[Result truncated. Full content was {len(tokens)} tokens.]"

Apply this when adding tool results to the messages array:

messages.append({
    'role':         'tool',
    'tool_call_id': tool_call.id,
    'content':      trim_tool_result(json.dumps(result), max_tokens=2000)
})

A web search result with 50 articles is probably 30,000 tokens. Trimming to 2,000 per result keeps the most relevant content while keeping your context budget under control.

Exercise: Implement the token tracking function and add it to an agent you built in a previous module. Run a long task (ask the agent to research 10 topics in sequence) and log the token count before each API call. At what step does it start getting close to the limit? Now add the summarisation strategy and compare: can you run the same 10-topic task without hitting the limit?

Knowledge Check

3 questions to test your understanding

1 What happens to an agent's performance as conversation history grows close to the context limit?

2 What is the summarisation strategy for context management?

3 When would selective retrieval be better than full conversation history?

Discussion

Questions and notes from learners on this topic

Loading discussion…

Go further with expert guidance

Ready to build production AI?
Talk to our R&D team.

These courses give you the foundation. Our embedded AI teams take you from prototype to production in 30–90 days, with your team, your codebase, your goals. Book a free strategy call to see how we can accelerate your AI initiative.

30 minutes · No obligation · Expert AI engineers, not sales reps

AI Architecture Review

Audit your current stack and identify high-impact improvements

Project Review

Get expert feedback on your AI implementation and codebase

Team Mentoring

Upskill your engineers with hands-on AI coaching sessions

AI Strategy

Define your AI roadmap, prioritization, and implementation plan