Context Garbage Collector (CGC) for LangGraph agents — virtual memory for LLM context windows
Project description
L1-Pager — Virtual Memory for LLM Context Windows
Your agent never forgets. It just pages.
L1-Pager is a drop-in context garbage collector (CGC) for LangGraph agents. It watches your conversation, evicts large old messages to a heap, and restores them on demand — exactly like OS virtual memory, but for LLM context windows.
Without L1-Pager With L1-Pager
───────────────── ──────────────────────────────
┌──────────────────┐ ┌──────────────────────────────┐
│ System prompt │ │ System prompt │
│ Tool result 3KB │ ← fills fast │ [ptr: tool result evicted] │ ← 12 chars
│ Tool result 2KB │ │ [ptr: tool result evicted] │
│ Tool result 4KB │ │ Tool result 4KB │ ← recent, kept
│ ...12 more turns │ │ ...12 more turns │
│ Recency bias │ ✗ │ Active window stays tight │ ✓
└──────────────────┘ └──────────────────────────────┘
When the model needs an evicted value, it calls fetch_evicted_page(page_id=...) and gets the raw content back — zero data loss, transparent to the rest of your graph.
How it works
Turn N Turn N+3 Turn N+6
────────────── ────────────── ──────────────
Tool result → Mark → [ptr: abc123…] → Model sees ptr
(3 000 tokens) phase stored in heap calls fetch_evicted_page
↓
Raw content re-injected
Model answers with exact value
- Mark — O(n) scan each turn. Candidates:
> 500 tokensAND> 3 turns old. - Sweep — concurrent heap writes. Pointer replaces original message in-place (same
id→ LangGraphadd_messagesdeduplication works correctly). - Page fault — model calls
fetch_evicted_page, interceptor handles it transparently before returning to your graph.
Overhead: p99 < 1ms on 400-message arrays (benchmarked, not estimated).
Install
Python
pip install l1-pager
# Optional: Redis heap backend
pip install "l1-pager[redis]"
TypeScript / JavaScript
npm install l1-pager-core
Quickstart
Python — drop-in create_react_agent replacement
from langchain_anthropic import ChatAnthropic
from l1_pager import EvictionConfig, build_heap
from l1_pager import create_l1_pager_react_agent
model = ChatAnthropic(model="claude-sonnet-4-6")
config = EvictionConfig(min_tokens=500, min_turns_old=3)
heap = build_heap(config)
# Exact same API as LangGraph's create_react_agent
agent = create_l1_pager_react_agent(model, tools=[...], heap=heap, config=config)
result = await agent.ainvoke({"messages": [HumanMessage(content="...")]})
Python — wrap your existing model
from l1_pager import L1Pager, EvictionConfig, build_heap
config = EvictionConfig()
heap = build_heap(config)
pager = L1Pager(model, heap, config)
# Use pager.ainvoke() anywhere you'd call model.invoke()
response = await pager.ainvoke(messages, current_turn=turn_count)
TypeScript — LangGraph node
import { createL1PagerReactAgent } from "@l1-pager/core";
const agent = createL1PagerReactAgent(model, tools);
const result = await agent.invoke({
messages: [new HumanMessage("...")],
});
TypeScript — lower-level
import { L1Pager, buildHeap, DEFAULT_EVICTION_CONFIG } from "@l1-pager/core";
const heap = buildHeap(DEFAULT_EVICTION_CONFIG);
const pager = new L1Pager(model, heap, DEFAULT_EVICTION_CONFIG);
const { aiMessage, pageFaults, tokensFreed } = await pager.ainvoke(messages, turn);
Configuration
from l1_pager import EvictionConfig, EvictionPolicy
config = EvictionConfig(
min_tokens = 500, # don't evict messages smaller than this
min_turns_old = 3, # don't evict messages newer than this
max_heap_entries = 128, # LRU cap on in-memory heap
entry_ttl_seconds = 3_600, # evicted pages expire after 1 hour
max_page_fault_depth = 3, # max re-fetch loops per model call
policy = EvictionPolicy.LRU, # LRU | IMPORTANCE | HYBRID
)
import { EvictionConfig } from "@l1-pager/core";
const config: EvictionConfig = {
minTokens: 500,
minTurnsOld: 3,
maxHeapEntries: 128,
entryTtlSeconds: 3600,
maxPageFaultDepth: 3,
policy: "lru",
};
Heap backends
In-memory (default)
heap = build_heap(config, backend="memory")
Zero dependencies, LRU eviction, lives in process memory.
Redis (persistent, shared across workers)
heap = await build_heap_async(config, backend="redis", redis_url="redis://localhost:6379")
import { buildHeapAsync } from "@l1-pager/core";
const heap = await buildHeapAsync(config, "redis", { redisUrl: "redis://localhost:6379" });
Each evicted page is stored as a Redis hash with a TTL. Access resets the TTL (LRU behaviour without a sorted set).
State schema
L1-Pager tracks its own counters as first-class graph state. These accumulate across turns and are accessible for observability:
| Field | Type | Description |
|---|---|---|
l1_turn_count |
int |
Turns processed |
l1_tokens_freed_total |
int |
Estimated tokens freed across all sweeps |
l1_page_fault_total |
int |
Total page faults resolved |
l1_eviction_log |
list[EvictionRecord] |
Full audit trail of every eviction |
System prompt guidance
For best results, add this block to your system prompt so the model understands the compression format:
This conversation uses L1-Pager context compression.
Compressed messages appear as: <PAGE_FAULT_ID: {hex} | SUMMARY: {text}>
If a question asks for SPECIFIC DATA inside a compressed reference,
call fetch_evicted_page(page_id="{hex}") to retrieve it first.
Do NOT guess — always fetch before answering precise values.
Benchmarks
Run against synthetic conversations on Apple M-series (no Redis):
| Messages | p50 | p95 | p99 |
|---|---|---|---|
| 52 | 0.04ms | 0.07ms | 0.09ms |
| 102 | 0.08ms | 0.13ms | 0.17ms |
| 202 | 0.18ms | 0.26ms | 0.32ms |
| 402 | 0.38ms | 0.49ms | 0.55ms |
SLA target: 15ms. Actual worst-case p99: 0.55ms (27× headroom).
Project structure
l1-pager/
├── python/ # PyPI package: l1-pager
│ ├── l1_pager/
│ │ ├── schema.py # types, pointer helpers
│ │ ├── mark.py # O(n) mark phase
│ │ ├── heap.py # InMemoryHeap + RedisHeap
│ │ ├── summarizer.py # extractive one-line summaries
│ │ ├── sweep.py # concurrent eviction
│ │ ├── tools.py # fetch_evicted_page tool schema
│ │ ├── interceptor.py # L1Pager — the main class
│ │ ├── langgraph_integration.py
│ │ └── wrap.py # @l1_pager_hook decorator
│ └── tests/
└── typescript/ # npm package: @l1-pager/core
└── src/
├── schema.ts
├── mark.ts
├── heap.ts
├── redis_heap.ts
├── summarizer.ts
├── sweep.ts
├── tools.ts
├── interceptor.ts
├── langgraph_node.ts
└── index.ts
Requirements
Python
- Python ≥ 3.11
langchain-core >= 0.3langgraph >= 0.2redis >= 4.2(optional, for Redis heap)
TypeScript
- Node.js ≥ 18
@langchain/core >= 0.3(peer dependency)@langchain/langgraph >= 0.2(peer dependency)redis >= 4.0(optional, for Redis heap)
License
MIT
Project details
Release history Release notifications | RSS feed
Download files
Download the file for your platform. If you're not sure which to choose, learn more about installing packages.
Source Distribution
Built Distribution
Filter files by name, interpreter, ABI, and platform.
If you're not sure about the file name format, learn more about wheel file names.
Copy a direct link to the current filters
File details
Details for the file l1_pager-1.1.1.tar.gz.
File metadata
- Download URL: l1_pager-1.1.1.tar.gz
- Upload date:
- Size: 38.6 kB
- Tags: Source
- Uploaded using Trusted Publishing? No
- Uploaded via: twine/6.2.0 CPython/3.14.4
File hashes
| Algorithm | Hash digest | |
|---|---|---|
| SHA256 |
88bb79869ddc5723bb3133fd57aae480d1ea60d044e355e33d8048e51325c076
|
|
| MD5 |
653657799e70791c27a2a4b924c82202
|
|
| BLAKE2b-256 |
5ed45156d5b21ee3f9f0cc58060d9f056d8be47f6e1afed9a54fac7ab9a44c59
|
File details
Details for the file l1_pager-1.1.1-py3-none-any.whl.
File metadata
- Download URL: l1_pager-1.1.1-py3-none-any.whl
- Upload date:
- Size: 28.1 kB
- Tags: Python 3
- Uploaded using Trusted Publishing? No
- Uploaded via: twine/6.2.0 CPython/3.14.4
File hashes
| Algorithm | Hash digest | |
|---|---|---|
| SHA256 |
eb99fbbf8c0db8a725ca3007f733514db77aeedbc8ccb4f1354ae1079d116688
|
|
| MD5 |
285402331af7b9ae60db24fe34b50677
|
|
| BLAKE2b-256 |
f5f4be8900958c0c53ee78d39ad4dbf5a5bf9b6ac43a045a40a338571599eac1
|