Agent Context Window Management: Sliding Windows, Summarization, and Priority Eviction for Production Systems

The core problem: Every multi-turn agent session drifts toward context window overflow. Tool call results pile up, retrieved documents stack, conversation history accumulates — and the LLM starts dropping information silently. A production agent needs a structured context management strategy that decides what to keep, what to compress, and what to evict, all while staying within the model’s context budget.
This guide covers three battle-tested patterns for agent context window management — sliding windows, rolling summarization, and priority-based eviction — with Python implementations, token budgeting strategies, and configuration guidelines for production deployments.
Why Agent Context Management Is Different
Chatbot context management is simple: keep recent messages, drop old ones. Agent context management is harder because the content is heterogeneous — system prompts, conversation turns, tool call results (potentially large JSON blobs), retrieved documents, and intermediate reasoning steps all compete for the same limited window 1.
A typical agent turn might involve:
- System prompt: 500–2,000 tokens
- User message: 100–500 tokens
- Retrieved documents: 1,000–8,000 tokens
- Tool call + result: 200–4,000 tokens (results can be enormous)
- Assistant response: 200–1,000 tokens
After 5–10 turns, even a 200K context window fills up. The LLM doesn’t warn you — it just starts producing worse outputs as relevant context gets pushed out.
Strategy 1: Token-Aware Sliding Window
The simplest production pattern is a sliding window that maintains a fixed token budget and drops the oldest messages when the budget is exceeded.
Implementation
import tiktoken
class TokenSlidingWindow:
"""Fixed-budget sliding window for agent conversation history."""
def __init__(self, max_tokens: int = 32_000, model: str = "gpt-4o"):
self.max_tokens = max_tokens
self.encoder = tiktoken.encoding_for_model(model)
self.messages: list[dict] = []
self._token_count = 0
def count_tokens(self, text: str) -> int:
return len(self.encoder.encode(text))
def add(self, role: str, content: str, metadata: dict | None = None) -> None:
"""Add a message and evict oldest if over budget."""
tokens = self.count_tokens(content)
self.messages.append({
"role": role,
"content": content,
"tokens": tokens,
"metadata": metadata or {},
})
self._token_count += tokens
self._evict()
def _evict(self) -> None:
"""Drop oldest messages until under budget."""
# Always keep the system prompt (assumed first message)
while self._token_count > self.max_tokens and len(self.messages) > 2:
removed = self.messages.pop(1) # skip system prompt at index 0
self._token_count -= removed["tokens"]
def get_context(self) -> list[dict]:
"""Return messages formatted for the LLM API."""
return [{"role": m["role"], "content": m["content"]} for m in self.messages]
Key configuration tips:
- Set
max_tokensto 70–80% of the model’s rated limit to leave room for generation output 2 - Always pin the system prompt — it’s the most critical message and should never be evicted
- Track token counts accurately:
tiktokenper-model encoders are more reliable than character-based estimates
Strategy 2: Rolling Summarization
When the sliding window drops too much useful context, rolling summarization compresses older messages instead of discarding them entirely.
Architecture
[Turn 1] [Turn 2] [Turn 3] ... [Turn N-5]
↓ ↓ ↓
Summary ← Summarizer LLM ← Older turns
↓
[Summary] [Turn N-4] [Turn N-3] ... [Turn N]
The summarizer runs when the token budget is exceeded. It compresses all messages older than a threshold into a single summary message that preserves key facts, decisions, and action outcomes 3.
class RollingSummarizer:
"""Compresses older context into summaries instead of dropping it."""
def __init__(
self,
summarizer_fn: callable,
max_tokens: int = 48_000,
summary_threshold: float = 0.75,
model: str = "gpt-4o",
):
self.max_tokens = max_tokens
self.summary_threshold = int(max_tokens * summary_threshold)
self.summarizer = summarizer_fn
self.encoder = tiktoken.encoding_for_model(model)
self.messages: list[dict] = []
self.summary: str | None = None
self._token_count = 0
def add(self, role: str, content: str) -> None:
tokens = len(self.encoder.encode(content))
self.messages.append({"role": role, "content": content, "tokens": tokens})
self._token_count += tokens
if self._token_count > self.summary_threshold:
self._compress()
def _compress(self) -> None:
"""Summarize older messages, keep recent ones intact."""
# Keep last 3 turns intact, summarize everything before that
keep_count = 6 # 3 turns × 2 messages each (user + assistant)
if len(self.messages) <= keep_count:
return
to_summarize = self.messages[:-keep_count]
recent = self.messages[-keep_count:]
summary_text = self._generate_summary(to_summarize)
old_tokens = sum(m["tokens"] for m in to_summarize)
new_tokens = len(self.encoder.encode(summary_text))
saved = old_tokens - new_tokens
self.summary = summary_text
self.messages = [
{"role": "system", "content": f"[Previous context summary: {summary_text}]", "tokens": new_tokens}
] + recent
self._token_count = new_tokens + sum(m["tokens"] for m in recent)
def _generate_summary(self, messages: list[dict]) -> str:
"""Call the summarizer LLM to compress message history."""
text = "\n".join(f"{m['role']}: {m['content'][:500]}" for m in messages)
prompt = (
"Summarize the key facts, decisions, completed actions, "
"and user preferences from this agent conversation history. "
"Be concise. Include specific data points and outcomes:\n\n" + text
)
return self.summarizer(prompt)
Best practices:
- Run summarization when context reaches 75% of the budget, not at 100% — compression takes a turn and you need headroom 3
- Keep the last 2–3 interaction turns intact for immediate conversational coherence
- Include extracted facts, decisions, and action outcomes in the summary — not vague “the user discussed X” statements
- Benchmark summary fidelity periodically: sample compressed conversations and verify the LLM can still answer questions about the summarized portion
Strategy 3: Priority-Based Eviction
Not all context is equal. Tool call results from CRUD operations (which return large data dumps) are less valuable than the user’s stated preferences or a confirmed decision. Priority-based eviction scores each message and drops the lowest-value content first.
Priority scoring function
def score_message(msg: dict) -> float:
"""Score a message by its retention priority (higher = keep longer)."""
role = msg.get("role", "")
content = msg.get("content", "")
# System prompt: highest priority
if role == "system":
return 100.0
# User messages with explicit instructions
if role == "user":
priority = 50.0
# Boost messages that look like instructions or preferences
if any(word in content.lower() for word in ["remember", "always", "prefer", "never", "my name", "set"]):
priority += 30.0
# Boost short, dense user instructions
if 50 < len(content) < 500:
priority += 10.0
return priority
# Tool results: lowest priority unless marked critical
if role == "tool":
base = 10.0
# Error results deserve retention (they're rare and important)
if "error" in content.lower() or "exception" in content.lower():
base += 40.0
# Large results are costly to keep
if len(content) > 2000:
base -= 20.0
return base
# Assistant messages
if role == "assistant":
priority = 30.0
# Boost messages with tool calls
if "tool_calls" in msg or "</tool>" in content:
priority += 15.0
return priority
return 20.0
class PriorityContextManager:
"""Evicts lowest-priority content when budget is exceeded."""
def __init__(self, max_tokens: int = 48_000, model: str = "gpt-4o"):
self.max_tokens = max_tokens
self.encoder = tiktoken.encoding_for_model(model)
self.messages: list[dict] = []
def add(self, role: str, content: str) -> None:
tokens = len(self.encoder.encode(content))
msg = {"role": role, "content": content, "tokens": tokens}
msg["priority"] = score_message(msg)
self.messages.append(msg)
self._evict()
def _evict(self) -> None:
"""Evict lowest-priority messages until under budget."""
total = sum(m["tokens"] for m in self.messages)
if total <= self.max_tokens:
return
# Sort by priority (ascending), evict from bottom
# Never evict system prompt (index 0)
candidates = list(enumerate(self.messages[1:], start=1))
candidates.sort(key=lambda x: (x[1]["priority"], -x[1]["tokens"]))
to_remove = set()
for idx, msg in candidates:
if total <= self.max_tokens:
break
total -= msg["tokens"]
to_remove.add(idx)
self.messages = [
m for i, m in enumerate(self.messages) if i not in to_remove
]
When to use priority eviction: Systems where context diversity is high — agents that switch between retrieval, code generation, and database queries in a single session. The priority system naturally retains user instructions and error results while discarding large but low-signal tool outputs 4.
Combining Strategies in Production
A production agent rarely uses just one strategy. The recommended architecture layers all three:
TokenSlidingWindow (budget enforcement)
→ RollingSummarizer (compression when budget tight)
→ PriorityEviction (fine-grained selection of what to drop)
→ LLM API call
class ProductionContextManager:
"""Layered context management for production agents."""
def __init__(
self,
max_tokens: int = 48_000,
summarizer_fn: callable = None,
model: str = "gpt-4o",
):
self.max_tokens = max_tokens
self.sliding = TokenSlidingWindow(max_tokens, model)
self.summarizer = RollingSummarizer(
summarizer_fn or self._default_summarize,
max_tokens,
0.70,
model,
)
self.priority = PriorityContextManager(max_tokens, model)
self.encoder = tiktoken.encoding_for_model(model)
def _default_summarize(self, text: str) -> str:
"""Minimal fallback summarizer — replace with an LLM call in production."""
lines = text.split("\n")
return "\n".join(lines[:5]) if len(lines) > 10 else text
def add_turn(self, user_msg: str, assistant_msg: str, tool_results: list[dict] | None = None) -> None:
"""Add a full interaction turn through all three layers."""
# Layer 1: Sliding window for basic budget enforcement
self.sliding.add("user", user_msg)
if tool_results:
for tr in tool_results:
self.sliding.add("tool", tr.get("content", str(tr)))
self.sliding.add("assistant", assistant_msg)
# Layer 2: Summarization at threshold
if self.sliding._token_count > int(self.sliding.max_tokens * 0.7):
# Feed sliding window messages into summarizer
for msg in self.sliding.messages[1:]: # skip system
self.summarizer.add(msg["role"], msg["content"])
# Layer 3: Priority eviction if still over budget
if self.summarizer._token_count > self.summarizer.summary_threshold:
for msg in self.summarizer.messages:
self.priority.add(msg["role"], msg["content"])
def get_context(self) -> list[dict]:
"""Return the best available context."""
if self.priority.messages:
return [{"role": m["role"], "content": m["content"]} for m in self.priority.messages]
if self.summarizer.messages:
return [{"role": m["role"], "content": m["content"]} for m in self.summarizer.messages]
return self.sliding.get_context()
Token Budget Planning
Set per-component budgets before your agent’s first turn:
| Component | Budget (% of limit) | Notes |
|---|---|---|
| System prompt | 5% | Keep under 2K tokens |
| Conversation history | 20–30% | Sliding window allocation |
| Tool call results | 15–20% | Most variable component |
| Retrieved documents | 15–20% | Apply relevance filtering before insertion |
| Generation output | 15–25% | Leave room for the model to write |
| Reserved headroom | 10% | Buffer for unexpected growth |
A 128K context window on GPT-4o or Claude 4 Opus should reserve ~100K for content and leave 28K for generation output 5.
Monitoring and Alerting
Add observability to your context management layer:
def log_context_stats(manager, turn_count: int) -> dict:
"""Log context window utilization for monitoring."""
total = manager.sliding._token_count
limit = manager.sliding.max_tokens
return {
"turn_count": turn_count,
"tokens_used": total,
"pct_used": round(total / limit * 100, 1),
"message_count": len(manager.sliding.messages),
"has_summary": manager.summarizer.summary is not None,
"eviction_count": sum(
1 for m in manager.priority.messages
if m.get("priority", 0) < 20
),
}
Alert when pct_used exceeds 90% consistently — it signals that your budget or eviction strategy needs tuning. Track token utilization per session in your metrics system (Prometheus, OpenTelemetry) to identify conversation patterns that overload the window 1.
Summary
The three patterns form a progression in sophistication and cost:
| Strategy | Token overhead | Context fidelity | Implementation cost |
|---|---|---|---|
| Sliding window | None | Low (drops context) | Minimal |
| Rolling summarization | ~200 tokens per summary | High (compresses) | Medium (LLM call per compression) |
| Priority eviction | None | Medium (drops low-value) | Medium (scoring logic) |
Start with a token-aware sliding window on day one. Add rolling summarization when you observe context-dependent quality regression. Add priority-based eviction when you need fine-grained control over heterogeneous context.
1 Anthropic. “The Claude Context Window.” November 2024. 2 OpenAI. “Managing Context Windows with System Messages.” 2025. 3 LangChain. “How to manage conversation history with LLMs.” 2026. 4 Pinecone. “Context Management for AI Agents: Best Practices.” May 2026. 5 Zylos Research. “LLM Context Optimization: Budget Allocation Patterns for Production Agent Systems.” March 2026.
← Back to all posts

