Observed since 20 Oct, 22:14 UTC
No superseded metadata states collected. Unchanged observations do not create new revisions.
agentic-context-compaction
Automatic context window management for AI agent SDKs
Automatic context window management for AI agent SDKs.
Long-running agent conversations exceed model context windows, causing:
This library provides automatic, intelligent context compaction with:
# Core (no dependencies)
pip install context-compactor
# With pydantic-ai support
pip install context-compactor[pydantic-ai]
# With all SDKs
pip install context-compactor[all-sdks]
from pydantic_ai import Agent
from context_compactor import ContextCompactor, LoggingHook
from context_compactor.adapters.pydantic_ai import pydantic_ai_adapter
from context_compactor.strategies import KeepRecentMessages
from context_compactor.tokenizers.pydantic_ai import PydanticAITokenCounter
compactor = ContextCompactor(
max_context_tokens=128_000,
strategy=KeepRecentMessages(keep_count=20),
token_counter=PydanticAITokenCounter(),
hooks=[LoggingHook()], # Optional: log compaction events
)
agent = Agent(
'openai:gpt-4o',
history_processors=[pydantic_ai_adapter(compactor)],
)
# Compaction happens automatically when context approaches limit
result = await agent.run("Continue...", message_history=long_history)
from agents import Agent, Runner
from context_compactor import ContextCompactor
from context_compactor.adapters.openai_agents import openai_agents_adapter
from context_compactor.strategies import SlidingWindow
from context_compactor.tokenizers.openai_agents import OpenAIAgentsTokenCounter
compactor = ContextCompactor(
max_context_tokens=128_000,
strategy=SlidingWindow(),
token_counter=OpenAIAgentsTokenCounter(),
)
result = await Runner.run(
agent,
input="Hello",
hooks=openai_agents_adapter(compactor),
)
from claude_agent_sdk import ClaudeSDKClient, ClaudeAgentOptions
from context_compactor import ContextCompactor
from context_compactor.adapters.claude_agent import claude_agent_adapter
from context_compactor.strategies import KeepFirstLast
from context_compactor.tokenizers.claude_agent import ClaudeAgentTokenCounter
compactor = ContextCompactor(
max_context_tokens=200_000,
strategy=KeepFirstLast(keep_first=2, keep_last=10),
token_counter=ClaudeAgentTokenCounter(),
)
hook_event, hook_matchers = claude_agent_adapter(compactor)
options = ClaudeAgentOptions(hooks={hook_event: hook_matchers})
async with ClaudeSDKClient(options=options) as client:
await client.query("Help me with this large codebase...")
Hooks let you react to compaction events—perfect for showing UI feedback like Cursor's "Summarizing conversation..." spinner.
from context_compactor import LoggingHook, CallbackHook
# LoggingHook - prints to stdout
compactor = ContextCompactor(
...,
hooks=[LoggingHook(prefix="[MyApp]")],
)
# CallbackHook - call your own async functions
async def on_start():
await show_spinner("Summarizing context...")
async def on_end(result):
await hide_spinner()
print(f"Saved {result.tokens_saved} tokens")
compactor = ContextCompactor(
...,
hooks=[CallbackHook(on_start_callback=on_start, on_end_callback=on_end)],
)
Implement the CompactionHook protocol:
from dataclasses import dataclass
from context_compactor import CompactionResult
@dataclass
class WebhookHook:
"""Send compaction events to your backend."""
webhook_url: str
async def on_start(self) -> None:
async with httpx.AsyncClient() as client:
await client.post(self.webhook_url, json={
"type": "compaction_started",
"message": "Summarizing context..."
})
async def on_end(self, result: CompactionResult) -> None:
async with httpx.AsyncClient() as client:
await client.post(self.webhook_url, json={
"type": "compaction_completed",
"tokens_saved": result.tokens_saved,
})
# Use multiple hooks
compactor = ContextCompactor(
max_context_tokens=128_000,
strategy=KeepRecentMessages(keep_count=20),
token_counter=PydanticAITokenCounter(),
hooks=[
WebhookHook(webhook_url="https://your-app.com/events"),
LoggingHook(),
],
)
Hooks fire synchronously before/after compaction, ensuring correct ordering with streaming:
1. User sends message
2. history_processor runs
→ hook.on_start() fires → UI shows spinner
→ compaction happens
→ hook.on_end() fires → UI hides spinner
3. Stream begins → tokens flow to UI
| Strategy | Description | Best For |
|---|---|---|
KeepRecentMessages | Keep last N messages | Simple truncation |
KeepFirstLast | Keep first N + last M, drop middle | Preserve initial context |
SlidingWindow | Fit as many recent as token budget allows | Token-efficient |
DropOldestUntilFits | Remove oldest until under budget | Minimal dropping |
SummarizeMiddle | Keep first/last, LLM-summarize middle | Best preservation |
Write type-safe strategies that work with native SDK message types:
from pydantic_ai.messages import ModelRequest, ModelResponse, TextPart, ToolCallPart
class KeepToolCalls:
"""Keep all tool interactions, drop regular text."""
async def compact(
self,
messages: list[ModelRequest | ModelResponse],
target_tokens: int,
token_counter,
) -> list[ModelRequest | ModelResponse]:
result = []
for msg in messages:
if isinstance(msg, ModelResponse):
tool_parts = [p for p in msg.parts if isinstance(p, ToolCallPart)]
if tool_parts:
result.append(ModelResponse(parts=tool_parts))
return result
See the examples/ directory for complete working examples:
| SDK | Examples |
|---|---|
| pydantic-ai | keep_recent, keep_first_last, sliding_window, summarize_middle, streaming_hooks |
| openai-agents | keep_recent, sliding_window |
| claude-agent-sdk | keep_recent |
ContextCompactorContextCompactor(
max_context_tokens: int, # Model's context window
strategy: CompactionStrategy, # How to compact
token_counter: TokenCounter, # How to count tokens
trigger_at_percent: float = 0.8, # Compact at 80% full
verbose: bool = False, # Print debug info
hooks: list[CompactionHook] = [], # Lifecycle hooks
)
await compactor.maybe_compact(messages) — Compact if over thresholdcompactor.get_stats() — Get compaction statisticscompactor.reset_stats() — Reset statisticsCompactionResultPassed to hook.on_end():
@dataclass
class CompactionResult:
original_tokens: int # Tokens before compaction
compacted_tokens: int # Tokens after compaction
tokens_saved: int # original - compacted
original_message_count: int
compacted_message_count: int
strategy_name: str # e.g., "KeepRecentMessages"
# Clone and setup
git clone ...
cd context-compactor
pip install -e ".[dev]"
# Run tests
pytest tests/ -v
# Lint
ruff check .
# Type check
ty check context_compactor/
MIT
Download counts and popularity will appear here as the collector receives them.
| Package / target | Requirement | Relationship |
|---|---|---|
claude-agent-sdkEnvironment{
"extras": "",
"marker": "extra == \"all-sdks\"",
"requires_python": ">=3.11",
"evaluation": "NOT_EVALUATED"
} | claude-agent-sdk>=0.0.20; extra == "all-sdks" | runtime |
openai-agentsEnvironment{
"extras": "",
"marker": "extra == \"all-sdks\"",
"requires_python": ">=3.11",
"evaluation": "NOT_EVALUATED"
} | openai-agents>=0.0.3; extra == "all-sdks" | runtime |
pydantic-aiEnvironment{
"extras": "",
"marker": "extra == \"all-sdks\"",
"requires_python": ">=3.11",
"evaluation": "NOT_EVALUATED"
} | pydantic-ai>=0.1.0; extra == "all-sdks" | runtime |
claude-agent-sdkEnvironment{
"extras": "",
"marker": "extra == \"claude-agent\"",
"requires_python": ">=3.11",
"evaluation": "NOT_EVALUATED"
} | claude-agent-sdk>=0.0.20; extra == "claude-agent" | runtime |
pytest-asyncioEnvironment{
"extras": "",
"marker": "extra == \"dev\"",
"requires_python": ">=3.11",
"evaluation": "NOT_EVALUATED"
} | pytest-asyncio>=0.24.0; extra == "dev" | runtime |
pytestEnvironment{
"extras": "",
"marker": "extra == \"dev\"",
"requires_python": ">=3.11",
"evaluation": "NOT_EVALUATED"
} | pytest>=8.0.0; extra == "dev" | runtime |
ruffEnvironment{
"extras": "",
"marker": "extra == \"dev\"",
"requires_python": ">=3.11",
"evaluation": "NOT_EVALUATED"
} | ruff>=0.8.0; extra == "dev" | runtime |
context-compactorEnvironment{
"extras": "all-sdks,dev,litellm,tiktoken",
"marker": "extra == \"full\"",
"requires_python": ">=3.11",
"evaluation": "NOT_EVALUATED"
} | context-compactor[all-sdks,dev,litellm,tiktoken]; extra == "full" | runtime |
litellmEnvironment{
"extras": "",
"marker": "extra == \"litellm\"",
"requires_python": ">=3.11",
"evaluation": "NOT_EVALUATED"
} | litellm>=1.50.0; extra == "litellm" | runtime |
openai-agentsEnvironment{
"extras": "",
"marker": "extra == \"openai-agents\"",
"requires_python": ">=3.11",
"evaluation": "NOT_EVALUATED"
} | openai-agents>=0.0.3; extra == "openai-agents" | runtime |
pydantic-aiEnvironment{
"extras": "",
"marker": "extra == \"pydantic-ai\"",
"requires_python": ">=3.11",
"evaluation": "NOT_EVALUATED"
} | pydantic-ai>=0.1.0; extra == "pydantic-ai" | runtime |
tiktokenEnvironment{
"extras": "",
"marker": "extra == \"tiktoken\"",
"requires_python": ">=3.11",
"evaluation": "NOT_EVALUATED"
} | tiktoken>=0.7.0; extra == "tiktoken" | runtime |
bdist_wheel · 25.21 KB · Python py3
sdist · 30.74 KB · Python source
Shared repository data · GitHub
Observed since 20 Oct, 22:14 UTC
No superseded metadata states collected. Unchanged observations do not create new revisions.
package_version:617aea6c8bccef89a11bc9945c317b85e6a75ec3271595faf4d6a68e98737416