Cross-Agent Token Deduplication Bus — eliminate 65-82% of redundant tokens across multi-agent LLM pipelines
Project description
EchoKill
Cross-Agent Token Deduplication Bus — eliminate 65–82% of redundant tokens across multi-agent LLM pipelines.
Table of Contents
- The Problem
- The Solution
- Installation
- Quick Start
- Tokenizers
- Provider-Aware Cache Optimization
- Cost Tracking
- Conversation History Compaction
- Multi-Provider Middleware
- Async Support
- Multi-User / Multi-Tenant Architecture
- Production Deployment
- Configuration Reference
- Security & Data Isolation
- Monitoring & Observability
- API Reference
- Using the Middleware
- Scaling & Performance
- Project Structure
- Key Concepts
- Estimated Savings
- Running Tests
- Contributing
- License
The Problem
In multi-agent systems (AutoGen, CrewAI, LangGraph, etc.), every agent independently receives the full conversation history, system prompt, and shared documents. In a 5-agent workflow processing a 10-page document:
- The same 4,000-token document gets sent 5× = 20,000 tokens wasted
- System prompts (500–2,000 tokens each) are re-sent on every turn
- Over a multi-turn conversation, 40–70% of total tokens are pure duplication
At production scale with hundreds of concurrent users, this becomes:
- $10K–$50K/month in wasted API spend for mid-sized deployments
- Unnecessary latency — longer prompts = slower inference
- Rate-limit pressure — burning TPM quota on duplicated content
No existing framework addresses this.
The Solution
EchoKill provides a shared semantic memory bus using content-addressed storage. Agents reference shared context blocks by hash instead of re-sending them. The system:
- Deduplicates shared context into a common prefix block
- Orders agent-specific content after the shared prefix (maximizes provider prefix-cache hits)
- Tracks which blocks each agent has already "seen" and sends only deltas
- Isolates per-user/tenant state for safe multi-tenant operation
┌─────────────────────────────────────────────────────────────────┐
│ EchoKill Multi-Tenant Bus │
│ │
│ ┌───────────────────────────────────────────────────────────┐ │
│ │ Content-Addressed Store (Redis Cluster) │ │
│ │ hash(block) → block_content │ │
│ │ tenant:user1:abc123 → [system prompt, 800 tokens] │ │
│ │ tenant:user1:def456 → [document chunk, 2000 tokens] │ │
│ │ tenant:user2:abc123 → [same hash, shared across tenant] │ │
│ └───────────────────────────────────────────────────────────┘ │
│ │ │
│ ┌─── User Session 1 ──┼──── User Session 2 ────────────────┐ │
│ │ │ │ │
│ │ ┌──────────┐ ┌──────────┐ ┌──────────┐ ┌──────────┐│ │
│ │ │ Agent A │ │ Agent B │ │ Agent A │ │ Agent C ││ │
│ │ │seen:{abc, │ │seen:{abc} │ │seen:{} │ │seen:{abc} ││ │
│ │ │ def} │ │ │ │ │ │ ││ │
│ │ └──────────┘ └──────────┘ └──────────┘ └──────────┘│ │
│ └───────────────────────────────────────────────────────────┘ │
│ │
│ Prompt Compiler (per-session): │
│ [shared_prefix: abc123, def456] │
│ [agent_specific: role + new delta only] │
│ [query: new user message] │
└─────────────────────────────────────────────────────────────────┘
Installation
Basic (single-process, development)
pip install -e .
Production (Redis-backed, multi-process)
pip install -e ".[redis]"
With accurate token counting (tiktoken)
pip install -e ".[tiktoken]"
With Anthropic cache optimization
pip install -e ".[anthropic]"
With OpenAI provider middleware
pip install -e ".[openai]"
Full (all providers + dev/test dependencies)
pip install -e ".[all,dev]"
System Requirements
| Component | Minimum | Recommended (Production) |
|---|---|---|
| Python | 3.10+ | 3.11+ (faster startup) |
| Redis | 6.2+ | 7.0+ (with Redis Functions) |
| Memory | 512 MB per worker | 2 GB per worker |
| CPU | 1 core | 4+ cores (async I/O bound) |
Quick Start
from echokill import MemoryBus
# Create the bus
bus = MemoryBus()
# Register agents
bus.register_agent("security_reviewer", role="You review code for security vulnerabilities.")
bus.register_agent("style_reviewer", role="You review code style and readability.")
bus.register_agent("perf_reviewer", role="You review code for performance issues.")
# Share context once (stored once, referenced by all agents)
bus.add_shared_block(code_diff_text)
bus.add_shared_block(project_guidelines_text)
# Build deduplicated prompts
for agent_id in bus.agent_ids:
prompt = bus.build_prompt(agent_id, new_message="Review the code above.")
# prompt.messages → send directly to any chat completions API
# prompt.tokens_saved_estimate → tokens eliminated via dedup
# Check savings
print(bus.metrics.summary())
# {'total_calls': 3, 'total_tokens_sent': 7500, 'total_tokens_saved': 17500, 'savings_ratio': '70.0%'}
Tokenizers
EchoKill includes a multi-backend tokenizer system for accurate token counting across providers.
Supported Backends
| Backend | Accuracy | Dependency | Best For |
|---|---|---|---|
TiktokenTokenizer |
Exact | tiktoken |
OpenAI models (GPT-5.x, GPT-4o, o-series) |
AnthropicTokenizer |
~95% | None | Claude models (~3.5 chars/token) |
ApproximateTokenizer |
~90% | None | Zero-dependency fallback (~4 chars/token) |
Usage
from echokill.tokenizers import get_tokenizer, count_tokens
# Auto-selects the best tokenizer for the model
tokenizer = get_tokenizer("gpt-4o") # → TiktokenTokenizer (if tiktoken installed)
tokenizer = get_tokenizer("claude-sonnet-4-5") # → AnthropicTokenizer
# Quick one-liner
count = count_tokens("Hello, world!", model="gpt-4o")
# Or use directly
from echokill.tokenizers import TiktokenTokenizer, ApproximateTokenizer
exact = TiktokenTokenizer(model="gpt-4o")
approx = ApproximateTokenizer(chars_per_token=4.0)
print(exact.count("def hello(): pass")) # exact BPE count
print(approx.count("def hello(): pass")) # len(text) // 4
Automatic Fallback
get_tokenizer() gracefully falls back to approximate counting if tiktoken is not installed — no code changes needed.
Provider-Aware Cache Optimization
EchoKill generates provider-specific prompt formats that maximize LLM provider cache hit rates — stacking on top of EchoKill's own deduplication for additional savings.
How It Works
┌──────────────────────────────────────────────────────────────────────────┐
│ Savings Stack │
│ │
│ Layer 1: EchoKill Dedup — eliminates redundant blocks (~65-70%) │
│ Layer 2: Provider Caching — cached prefix reads at 50-90% discount │
│ Combined — up to 82% effective token reduction │
└──────────────────────────────────────────────────────────────────────────┘
Anthropic (Explicit Cache Breakpoints)
from echokill.cache_optimizer import CacheOptimizer, Provider
optimizer = CacheOptimizer(provider=Provider.ANTHROPIC)
optimized = optimizer.optimize(compiled_prompt)
# Result contains:
# - messages with cache_control markers at optimal positions
# - prewarm_payload for cache pre-warming (max_tokens=0 technique)
# - cached_prefix_tokens / dynamic_suffix_tokens breakdown
print(optimized.cached_prefix_tokens) # tokens served from cache
print(optimized.prewarm_payload) # send to Anthropic to pre-warm
OpenAI (Automatic Prefix Caching)
optimizer = CacheOptimizer(provider=Provider.OPENAI)
optimized = optimizer.optimize(compiled_prompt)
# OpenAI automatically caches stable prefixes ≥1024 tokens.
# EchoKill ensures deterministic block ordering to maximize hit rates.
# - System prompt → shared blocks (sorted) → agent role → delta → message
Cache Pre-Warming (Anthropic)
Pre-load expensive shared context into the provider cache before actual queries:
from echokill.providers import AnthropicProviderMiddleware
import anthropic
mw = AnthropicProviderMiddleware(
client=anthropic.Anthropic(),
model="claude-sonnet-4-5",
)
mw.register("analyst", role="You analyze financial data.")
mw.share(large_quarterly_report) # 10K+ tokens
# Pre-warm: loads shared prefix into Anthropic's cache
mw.prewarm("analyst")
# Subsequent calls read from cache at 90% discount ($0.30/MTok vs $3.00/MTok)
result = mw.call("analyst", message="What are the key risks?")
Supported Providers
| Provider | Caching Type | Discount | Threshold | EchoKill Integration |
|---|---|---|---|---|
| OpenAI | Automatic prefix | 50% off input | ≥1024 tokens | Deterministic prefix ordering |
| Anthropic | Explicit breakpoints | 90% off reads | ≥1024 tokens | cache_control markers + pre-warm |
| Azure OpenAI | Same as OpenAI | 50% off input | ≥1024 tokens | Same prefix ordering |
| Context caching | 75% off reads | ≥1024 tokens | Stable prefix extraction |
Cost Tracking
Track actual dollar savings with model-specific pricing (July 2026 prices).
Quick Start
from echokill.cost import CostTracker
tracker = CostTracker(model="gpt-4o")
tracker.record(agent_id="reviewer", tokens_sent=2500, tokens_saved=5500)
tracker.record(agent_id="analyst", tokens_sent=3000, tokens_saved=7000)
print(tracker.summary())
# {
# 'model': 'gpt-4o',
# 'total_calls': 2,
# 'total_cost_usd': '$0.0138',
# 'total_saved_usd': '$0.0313',
# 'naive_cost_usd': '$0.0450',
# 'savings_percentage': '69.4%',
# 'roi_multiplier': '2.3x',
# 'pricing': {'input_per_mtok': '$2.5', 'cached_read_per_mtok': '$1.25'}
# }
Pricing Database
Built-in pricing for all major current models (per 1M tokens):
| Model | Input $/MTok | Cached Read $/MTok | Cache Write $/MTok | Output $/MTok |
|---|---|---|---|---|
| GPT-5.6 (sol) | $5.00 | $0.50 | $5.00 | $30.00 |
| GPT-5.6 (terra) | $2.50 | $0.25 | $2.50 | $15.00 |
| GPT-5.6 (luna) | $1.00 | $0.10 | $1.00 | $6.00 |
| GPT-5.4 | $2.50 | $0.25 | $2.50 | $15.00 |
| GPT-5.4-mini | $0.75 | $0.075 | $0.75 | $4.50 |
| GPT-5.4-nano | $0.20 | $0.02 | $0.20 | $1.25 |
| Claude Opus 4.5 / 4.8 | $5.00 | $0.50 | $6.25 | $25.00 |
| Claude Sonnet 5 * | $2.00 | $0.20 | $2.50 | $10.00 |
| Claude Sonnet 4.5 | $3.00 | $0.30 | $3.75 | $15.00 |
| Claude Haiku 4.5 | $1.00 | $0.10 | $1.25 | $5.00 |
| Gemini 3.1 Pro | $2.00 | $0.20 | $2.00 | $12.00 |
| Gemini 3.5 Flash | $1.50 | $0.15 | $1.50 | $9.00 |
| Gemini 2.5 Pro | $1.25 | $0.125 | $1.25 | $10.00 |
| Gemini 2.5 Flash | $0.30 | $0.03 | $0.30 | $2.50 |
| Gemini 2.5 Flash-Lite | $0.10 | $0.01 | $0.10 | $0.40 |
| GPT-4o (legacy) | $2.50 | $1.25 | $2.50 | $10.00 |
| GPT-4o-mini (legacy) | $0.15 | $0.075 | $0.15 | $0.60 |
* Claude Sonnet 5 introductory pricing is in effect through 2026-08-31; it reverts to $3.00 input / $15.00 output on 2026-09-01. Legacy models (gpt-4-turbo, o1, o3-mini, Claude 4 / 3.5, Gemini 1.5) remain in the pricing DB for back-compat.
Per-Agent Breakdown
print(tracker.agent_cost("reviewer"))
# {'agent_id': 'reviewer', 'calls': 1, 'cost_usd': 0.00625, 'saved_usd': 0.01375}
print(f"ROI: {tracker.roi_multiplier:.1f}x") # savings / cost ratio
With Cache-Aware Tracking
tracker.record(
agent_id="analyst",
tokens_sent=5000,
tokens_saved=10000,
tokens_cached_read=3000, # served from provider cache
tokens_cache_write=2000, # written to provider cache
)
# Accounts for discounted cache reads + cache write costs
Conversation History Compaction
As conversations grow, older turns become less relevant. The HistoryCompactor intelligently compresses history while preserving critical context.
Strategies
| Strategy | Description | Best For |
|---|---|---|
SLIDING_WINDOW |
Keep only the last N turns | Simple chat bots |
TOKEN_BUDGET |
Keep history within a token limit | Context window management |
SUMMARY |
Replace old turns with a summary | Long-running conversations |
PRIORITY |
Always keep tool results + system prompts | Agentic workflows |
Usage
from echokill.compaction import HistoryCompactor, CompactionStrategy
# Token budget strategy — keep history under 8K tokens
compactor = HistoryCompactor(
strategy=CompactionStrategy.TOKEN_BUDGET,
token_budget=8000,
)
result = compactor.compact(history_blocks)
print(f"Kept: {len(result.kept_blocks)} blocks")
print(f"Removed: {len(result.removed_blocks)} blocks")
print(f"Compaction ratio: {result.compaction_ratio:.0%}")
# e.g., "Compaction ratio: 45%" — removed 45% of tokens
Sliding Window
compactor = HistoryCompactor(
strategy=CompactionStrategy.SLIDING_WINDOW,
max_turns=20, # keep only last 20 blocks
)
result = compactor.compact(all_blocks)
Summary Compaction
Replace removed turns with a generated summary:
def my_summarizer(blocks: list[Block]) -> str:
"""Call an LLM to summarize old turns."""
text = "\n".join(b.content for b in blocks)
return llm_summarize(text)
compactor = HistoryCompactor(
strategy=CompactionStrategy.SUMMARY,
token_budget=8000,
summary_fn=my_summarizer,
)
result = compactor.compact(history)
# result.summary_block contains the generated summary
Priority-Based Compaction
Keep important block types regardless of age:
from echokill.block import BlockType
compactor = HistoryCompactor(
strategy=CompactionStrategy.PRIORITY,
token_budget=8000,
priority_types={BlockType.TOOL_RESULT, BlockType.SYSTEM_PROMPT},
)
# Tool results and system prompts are never evicted
Multi-Provider Middleware
Provider-specific middleware combines EchoKill deduplication with native provider caching for maximum cost reduction.
OpenAI
from openai import OpenAI
from echokill.providers import OpenAIProviderMiddleware
mw = OpenAIProviderMiddleware(client=OpenAI(), model="gpt-4o")
mw.register("reviewer", role="You review code for bugs.")
mw.share(code_diff)
result = mw.call("reviewer", message="Find critical bugs.")
print(result["response"].choices[0].message.content)
print(result["cost"]) # real dollar breakdown
Anthropic (with Cache Pre-Warming)
import anthropic
from echokill.providers import AnthropicProviderMiddleware
mw = AnthropicProviderMiddleware(
client=anthropic.Anthropic(),
model="claude-sonnet-4-5",
)
mw.register("analyst", role="You are a financial analyst.")
mw.share(quarterly_report) # 10K+ tokens
# Pre-warm the cache (loads prefix at write cost, subsequent reads at 90% discount)
mw.prewarm("analyst")
# All subsequent calls benefit from cached prefix
result = mw.call("analyst", message="Summarize revenue trends.")
result = mw.call("analyst", message="Identify top 3 risks.")
Azure OpenAI
from openai import AzureOpenAI
from echokill.providers import AzureOpenAIProviderMiddleware
client = AzureOpenAI(
azure_endpoint="https://your-resource.openai.azure.com/",
api_version="2024-06-01",
)
mw = AzureOpenAIProviderMiddleware(client=client, model="gpt-4o")
mw.register("reviewer", role="Enterprise code reviewer.")
mw.share(code_diff)
result = mw.call("reviewer", message="Check for security issues.")
Combined Cost Summary
print(mw.summary())
# {
# 'total_calls': 3,
# 'total_tokens_sent': 7500,
# 'total_tokens_saved': 17500,
# 'cost': {
# 'total_cost_usd': '$0.019',
# 'total_saved_usd': '$0.044',
# 'roi_multiplier': '2.3x',
# }
# }
Async Support
AsyncMemoryBus provides the same API as MemoryBus but with async/await for use in FastAPI, aiohttp, and other async frameworks.
Usage
from echokill.aio import AsyncMemoryBus, AsyncRedisBlockStore
import redis.asyncio as aioredis
# With async Redis
redis_client = aioredis.from_url("redis://localhost:6379/0")
store = AsyncRedisBlockStore(redis_client, ttl=86400)
bus = AsyncMemoryBus(store=store)
await bus.register_agent("reviewer", role="You review code.")
await bus.add_shared_block(code_diff)
prompt = await bus.build_prompt("reviewer", new_message="Review this code.")
# prompt.messages → send to provider
FastAPI Integration
from fastapi import FastAPI, Depends
from echokill.aio import AsyncMemoryBus, AsyncRedisBlockStore
import redis.asyncio as aioredis
app = FastAPI()
redis_pool = aioredis.from_url("redis://localhost:6379/0")
async def get_bus(session_id: str) -> AsyncMemoryBus:
store = AsyncRedisBlockStore(redis_pool, key_prefix=f"echokill:{session_id}:")
return AsyncMemoryBus(store=store)
@app.post("/review")
async def review_code(code: str, session_id: str):
bus = await get_bus(session_id)
await bus.register_agent("reviewer", role="Code reviewer.")
await bus.add_shared_block(code)
prompt = await bus.build_prompt("reviewer", new_message="Review this.")
# Send prompt.messages to your LLM provider
return {"tokens_saved": prompt.tokens_saved_estimate}
Multi-User / Multi-Tenant Architecture
EchoKill supports concurrent users through tenant-isolated bus instances backed by a shared Redis store. Each user/session gets its own MemoryBus with independent agent state while benefiting from cross-session content deduplication at the storage layer.
Architecture Overview
┌─────────────────────────────────────────────────────────────────┐
│ Application Layer (FastAPI / Flask) │
│ │
│ Request → Auth → TenantResolver → BusFactory → MemoryBus │
└─────────────────────────────┬───────────────────────────────────┘
│
┌─────────────────────────────▼───────────────────────────────────┐
│ EchoKill Bus Pool │
│ │
│ ┌──────────────┐ ┌──────────────┐ ┌──────────────┐ │
│ │ Bus: user_1 │ │ Bus: user_2 │ │ Bus: user_N │ │
│ │ session: abc │ │ session: def │ │ session: xyz │ │
│ │ agents: 3 │ │ agents: 5 │ │ agents: 2 │ │
│ └──────┬───────┘ └──────┬───────┘ └──────┬───────┘ │
│ │ │ │ │
└──────────┼──────────────────┼──────────────────┼─────────────────┘
│ │ │
┌──────────▼──────────────────▼──────────────────▼─────────────────┐
│ Shared Redis Block Store (Cluster Mode) │
│ │
│ Key schema: echokill:{tenant_id}:block:{hash} │
│ TTL: configurable per tenant (default 24h for sessions) │
│ Eviction: LRU on idle sessions, pin high-value shared blocks │
└───────────────────────────────────────────────────────────────────┘
Per-User Session Management
import redis
from echokill import MemoryBus
from echokill.store import RedisBlockStore
# --- Shared Redis connection pool (create once at app startup) ---
redis_pool = redis.ConnectionPool(
host="redis-cluster.internal",
port=6379,
password="<from-secrets-manager>",
max_connections=100,
decode_responses=True,
socket_timeout=5,
socket_connect_timeout=2,
retry_on_timeout=True,
)
def create_user_bus(tenant_id: str, session_id: str) -> MemoryBus:
"""Factory: create an isolated bus per user session.
Each user gets:
- Isolated agent states (no cross-user leakage)
- Tenant-scoped Redis keys (namespace isolation)
- Independent metrics tracking
- Configurable TTL for automatic session cleanup
"""
client = redis.Redis(connection_pool=redis_pool)
store = RedisBlockStore(
redis_client=client,
ttl=86400, # 24-hour session TTL
# Namespace isolation: each tenant's blocks are prefixed
# key_prefix=f"echokill:{tenant_id}:{session_id}:"
)
return MemoryBus(store=store)
FastAPI Multi-Tenant Integration
from fastapi import FastAPI, Depends, HTTPException, Request
from fastapi.security import HTTPBearer, HTTPAuthorizationCredentials
from pydantic import BaseModel
from contextlib import asynccontextmanager
import uuid
app = FastAPI(title="EchoKill Multi-Agent Service")
security = HTTPBearer()
# --- Bus pool: manages per-session bus instances ---
class BusPool:
"""Thread-safe pool of MemoryBus instances keyed by session."""
def __init__(self, redis_pool, max_sessions: int = 10_000):
self._pool: dict[str, MemoryBus] = {}
self._redis_pool = redis_pool
self._max_sessions = max_sessions
def get_or_create(self, tenant_id: str, session_id: str) -> MemoryBus:
key = f"{tenant_id}:{session_id}"
if key not in self._pool:
if len(self._pool) >= self._max_sessions:
self._evict_oldest()
self._pool[key] = create_user_bus(tenant_id, session_id)
return self._pool[key]
def destroy_session(self, tenant_id: str, session_id: str) -> None:
key = f"{tenant_id}:{session_id}"
self._pool.pop(key, None)
def _evict_oldest(self):
# LRU eviction — remove least recently used session
oldest_key = next(iter(self._pool))
del self._pool[oldest_key]
bus_pool = BusPool(redis_pool=redis_pool)
# --- Auth & tenant resolution ---
async def get_current_user(credentials: HTTPAuthorizationCredentials = Depends(security)):
"""Validate JWT and extract tenant/user info."""
token = credentials.credentials
# In production: validate JWT signature, expiry, claims
payload = validate_jwt(token) # your JWT validation logic
return {
"tenant_id": payload["tenant_id"],
"user_id": payload["user_id"],
"session_id": payload.get("session_id", str(uuid.uuid4())),
}
# --- Request models ---
class AgentRequest(BaseModel):
agent_id: str
message: str
shared_context: list[str] | None = None
class AgentResponse(BaseModel):
messages: list[dict[str, str]]
tokens_saved: int
total_tokens: int
session_id: str
# --- Endpoints ---
@app.post("/v1/agents/register")
async def register_agent(
agent_id: str,
role: str,
user=Depends(get_current_user),
):
bus = bus_pool.get_or_create(user["tenant_id"], user["session_id"])
bus.register_agent(agent_id, role=role)
return {"status": "registered", "agent_id": agent_id}
@app.post("/v1/prompts/build", response_model=AgentResponse)
async def build_prompt(
request: AgentRequest,
user=Depends(get_current_user),
):
bus = bus_pool.get_or_create(user["tenant_id"], user["session_id"])
# Add any new shared context
if request.shared_context:
for ctx in request.shared_context:
bus.add_shared_block(ctx)
# Build the deduplicated prompt
compiled = bus.build_prompt(request.agent_id, new_message=request.message)
return AgentResponse(
messages=compiled.messages,
tokens_saved=compiled.tokens_saved_estimate,
total_tokens=compiled.total_tokens_estimate,
session_id=user["session_id"],
)
@app.get("/v1/sessions/{session_id}/metrics")
async def get_session_metrics(session_id: str, user=Depends(get_current_user)):
bus = bus_pool.get_or_create(user["tenant_id"], session_id)
return bus.metrics.summary()
@app.delete("/v1/sessions/{session_id}")
async def destroy_session(session_id: str, user=Depends(get_current_user)):
bus_pool.destroy_session(user["tenant_id"], session_id)
return {"status": "destroyed"}
Extracting User Identity from Webhooks / Message Queues
# For async pipelines (Celery, RabbitMQ, SQS)
from celery import Celery
celery_app = Celery("echokill_worker")
@celery_app.task(bind=True, max_retries=3)
def process_agent_request(self, tenant_id: str, session_id: str, agent_id: str, message: str):
"""Async task: build prompt for a specific user's session."""
bus = create_user_bus(tenant_id, session_id)
# Restore session state from Redis (blocks persist via RedisBlockStore)
compiled = bus.build_prompt(agent_id, new_message=message)
return {
"messages": compiled.messages,
"tokens_saved": compiled.tokens_saved_estimate,
}
Production Deployment
Docker
# Dockerfile
FROM python:3.11-slim AS base
WORKDIR /app
# Install system deps
RUN apt-get update && apt-get install -y --no-install-recommends \
curl \
&& rm -rf /var/lib/apt/lists/*
# Install Python deps
COPY pyproject.toml .
RUN pip install --no-cache-dir -e ".[redis]" gunicorn uvicorn[standard]
COPY . .
# Non-root user for security
RUN useradd -m -r echokill && chown -R echokill:echokill /app
USER echokill
# Health check
HEALTHCHECK --interval=30s --timeout=5s --start-period=10s --retries=3 \
CMD curl -f http://localhost:8000/health || exit 1
EXPOSE 8000
CMD ["gunicorn", "app:app", "-w", "4", "-k", "uvicorn.workers.UvicornWorker", \
"--bind", "0.0.0.0:8000", "--timeout", "120", "--graceful-timeout", "30"]
Docker Compose (local development)
# docker-compose.yml
version: "3.9"
services:
echokill-api:
build: .
ports:
- "8000:8000"
environment:
- ECHOKILL_REDIS_URL=redis://redis:6379/0
- ECHOKILL_LOG_LEVEL=info
- ECHOKILL_MAX_SESSIONS=10000
- ECHOKILL_BLOCK_TTL=86400
depends_on:
redis:
condition: service_healthy
deploy:
replicas: 2
resources:
limits:
memory: 2G
cpus: "2.0"
redis:
image: redis:7-alpine
ports:
- "6379:6379"
command: >
redis-server
--maxmemory 1gb
--maxmemory-policy allkeys-lru
--save 60 1000
--appendonly yes
volumes:
- redis-data:/data
healthcheck:
test: ["CMD", "redis-cli", "ping"]
interval: 10s
timeout: 3s
retries: 3
prometheus:
image: prom/prometheus:latest
ports:
- "9090:9090"
volumes:
- ./monitoring/prometheus.yml:/etc/prometheus/prometheus.yml
grafana:
image: grafana/grafana:latest
ports:
- "3000:3000"
depends_on:
- prometheus
volumes:
redis-data:
Kubernetes Deployment
# k8s/deployment.yaml
apiVersion: apps/v1
kind: Deployment
metadata:
name: echokill-api
labels:
app: echokill
component: api
spec:
replicas: 3
strategy:
type: RollingUpdate
rollingUpdate:
maxSurge: 1
maxUnavailable: 0
selector:
matchLabels:
app: echokill
template:
metadata:
labels:
app: echokill
component: api
annotations:
prometheus.io/scrape: "true"
prometheus.io/port: "8000"
prometheus.io/path: "/metrics"
spec:
serviceAccountName: echokill-sa
securityContext:
runAsNonRoot: true
runAsUser: 1000
containers:
- name: echokill
image: your-registry/echokill:latest
ports:
- containerPort: 8000
protocol: TCP
env:
- name: ECHOKILL_REDIS_URL
valueFrom:
secretKeyRef:
name: echokill-secrets
key: redis-url
- name: ECHOKILL_MAX_SESSIONS
value: "50000"
- name: ECHOKILL_BLOCK_TTL
value: "86400"
resources:
requests:
memory: "512Mi"
cpu: "500m"
limits:
memory: "2Gi"
cpu: "2000m"
readinessProbe:
httpGet:
path: /health
port: 8000
initialDelaySeconds: 5
periodSeconds: 10
livenessProbe:
httpGet:
path: /health
port: 8000
initialDelaySeconds: 15
periodSeconds: 20
---
apiVersion: v1
kind: Service
metadata:
name: echokill-api
spec:
type: ClusterIP
selector:
app: echokill
ports:
- port: 80
targetPort: 8000
---
apiVersion: autoscaling/v2
kind: HorizontalPodAutoscaler
metadata:
name: echokill-hpa
spec:
scaleTargetRef:
apiVersion: apps/v1
kind: Deployment
name: echokill-api
minReplicas: 3
maxReplicas: 20
metrics:
- type: Resource
resource:
name: cpu
target:
type: Utilization
averageUtilization: 70
- type: Pods
pods:
metric:
name: echokill_active_sessions
target:
type: AverageValue
averageValue: "5000"
Configuration Reference
All settings can be provided via environment variables or a config file.
| Variable | Default | Description |
|---|---|---|
ECHOKILL_REDIS_URL |
redis://localhost:6379/0 |
Redis connection string |
ECHOKILL_REDIS_PASSWORD |
— | Redis auth password (use secrets manager) |
ECHOKILL_REDIS_SSL |
false |
Enable TLS for Redis connections |
ECHOKILL_REDIS_POOL_SIZE |
100 |
Max Redis connection pool size |
ECHOKILL_MAX_SESSIONS |
10000 |
Max concurrent user sessions in memory |
ECHOKILL_BLOCK_TTL |
86400 |
Block expiry in seconds (0 = no expiry) |
ECHOKILL_MAX_CONTEXT_TOKENS |
128000 |
Max tokens per compiled prompt |
ECHOKILL_LOG_LEVEL |
info |
Logging level (debug/info/warning/error) |
ECHOKILL_METRICS_ENABLED |
true |
Enable Prometheus metrics export |
ECHOKILL_METRICS_PORT |
8000 |
Port for /metrics endpoint |
ECHOKILL_TENANT_ISOLATION |
strict |
Isolation mode: strict / shared |
ECHOKILL_RATE_LIMIT_RPM |
1000 |
Requests per minute per tenant |
ECHOKILL_SESSION_IDLE_TIMEOUT |
3600 |
Auto-destroy idle sessions (seconds) |
Example Configuration File
# config/echokill.yaml
redis:
url: redis://redis-cluster.internal:6379/0
password: ${ECHOKILL_REDIS_PASSWORD}
ssl: true
pool_size: 200
socket_timeout: 5
retry_on_timeout: true
sessions:
max_concurrent: 50000
idle_timeout: 3600
block_ttl: 86400
compiler:
max_context_tokens: 128000
security:
tenant_isolation: strict
rate_limit_rpm: 1000
api_key_required: true
observability:
log_level: info
metrics_enabled: true
tracing_enabled: true
trace_sample_rate: 0.1
Security & Data Isolation
Tenant Isolation Model
EchoKill enforces strict tenant isolation by default:
| Layer | Mechanism |
|---|---|
| Storage | Redis key namespacing: echokill:{tenant_id}:block:{hash} |
| Memory | Each session gets its own MemoryBus instance — no shared mutable state |
| Network | TLS for Redis connections; mTLS between service pods |
| Auth | JWT validation with tenant claim extraction at the gateway |
| Eviction | Sessions destroyed on logout or after idle timeout |
Data Flow Security
User Request
│
▼
┌──────────────────┐
│ API Gateway │ ← TLS termination, rate limiting, JWT validation
│ (Kong / Envoy) │
└────────┬─────────┘
│ (authenticated, tenant_id extracted)
▼
┌──────────────────┐
│ EchoKill API │ ← Input validation, size limits, content sanitization
│ (FastAPI) │
└────────┬─────────┘
│ (tenant-scoped operations only)
▼
┌──────────────────┐
│ Redis Store │ ← ACL per-tenant, encrypted at rest, network-isolated
│ (Cluster) │
└──────────────────┘
Security Checklist for Production
- Authentication: JWT or OAuth2 on all endpoints; no anonymous access
- Authorization: Tenant-scoped access — users can only access their own sessions
- Input validation: Max block size (default 100KB), max blocks per session (1000)
- Rate limiting: Per-tenant RPM limits at the gateway level
- TLS: All Redis connections use TLS in production
- Secrets management: Redis passwords, API keys stored in Vault/AWS Secrets Manager
- Audit logging: Log session create/destroy, agent registration, block additions
- Data retention: Configure TTL for GDPR/compliance; support
DELETE /sessions/{id} - No PII in blocks: Document guidelines that shared blocks should not contain PII
- Redis ACL: Use Redis 6+ ACLs to restrict key patterns per service account
Input Validation
# Recommended limits (enforce at API layer)
MAX_BLOCK_SIZE_BYTES = 100 * 1024 # 100 KB per block
MAX_BLOCKS_PER_SESSION = 1000 # prevent memory abuse
MAX_AGENTS_PER_SESSION = 50 # reasonable agent limit
MAX_MESSAGE_SIZE_BYTES = 50 * 1024 # 50 KB per user message
ALLOWED_BLOCK_TYPES = {"document", "system_prompt", "custom"}
Monitoring & Observability
Prometheus Metrics
EchoKill exports the following metrics for production monitoring:
# Key metrics exported at /metrics
echokill_prompts_compiled_total # Counter: total prompts built
echokill_tokens_saved_total # Counter: cumulative tokens saved
echokill_tokens_sent_total # Counter: cumulative tokens actually sent
echokill_savings_ratio # Gauge: current savings ratio (0.0-1.0)
echokill_active_sessions # Gauge: current live session count
echokill_blocks_stored_total # Gauge: unique blocks in store
echokill_prompt_compile_duration_seconds # Histogram: prompt compilation latency
echokill_redis_operation_duration_seconds # Histogram: Redis op latency
echokill_session_lifetime_seconds # Histogram: session durations
echokill_block_size_bytes # Histogram: block sizes
Grafana Dashboard (recommended panels)
| Panel | Metric | Purpose |
|---|---|---|
| Token Savings Rate | echokill_savings_ratio |
Are we meeting the 65%+ target? |
| Prompt Throughput | rate(echokill_prompts_compiled_total[5m]) |
Requests per second |
| Compilation Latency p99 | histogram_quantile(0.99, echokill_prompt_compile_duration_seconds) |
Ensure <50ms |
| Active Sessions | echokill_active_sessions |
Capacity planning |
| Redis Latency | histogram_quantile(0.95, echokill_redis_operation_duration_seconds) |
Backend health |
| Cost Savings ($) | echokill_tokens_saved_total * $0.000003 |
Business value |
Structured Logging
{
"timestamp": "2026-06-09T14:30:00Z",
"level": "info",
"event": "prompt_compiled",
"tenant_id": "tenant_abc",
"session_id": "sess_123",
"agent_id": "security_reviewer",
"tokens_sent": 2500,
"tokens_saved": 5500,
"savings_ratio": 0.69,
"shared_blocks": 3,
"delta_blocks": 1,
"compile_duration_ms": 12.3
}
Health Check Endpoint
@app.get("/health")
async def health():
"""Liveness + readiness probe."""
checks = {
"redis": await check_redis_connection(),
"memory": get_memory_usage_mb() < MAX_MEMORY_MB,
"sessions": bus_pool.active_count() < bus_pool.max_sessions,
}
status = "healthy" if all(checks.values()) else "degraded"
code = 200 if status == "healthy" else 503
return JSONResponse({"status": status, "checks": checks}, status_code=code)
API Reference
Core Classes
MemoryBus
The central orchestrator. One instance per user session.
bus = MemoryBus(store=RedisBlockStore(...), compiler=PromptCompiler(max_context_tokens=128000))
| Method | Description |
|---|---|
register_agent(agent_id, role=None) |
Register an agent with optional role persona |
add_shared_block(content, block_type=DOCUMENT) |
Add content shared across all agents |
add_agent_block(agent_id, content) |
Add content visible only to one agent |
build_prompt(agent_id, new_message=None) → CompiledPrompt |
Build a deduplicated prompt |
metrics → BusMetrics |
Access token savings analytics |
summary() → dict |
Human-readable bus state |
CompiledPrompt
Returned by build_prompt(). Send .messages directly to any LLM API.
| Attribute | Type | Description |
|---|---|---|
messages |
list[dict[str, str]] |
Ready-to-send message list |
shared_block_hashes |
list[str] |
Hashes of shared blocks included |
delta_block_hashes |
list[str] |
Hashes of agent-specific delta blocks |
total_tokens_estimate |
int |
Estimated tokens in compiled prompt |
tokens_saved_estimate |
int |
Tokens avoided via deduplication |
RedisBlockStore
Production-grade block storage with Redis.
store = RedisBlockStore(redis_client=client, ttl=86400)
| Method | Description |
|---|---|
put(block) → str |
Store block, return hash (idempotent) |
get(hash) → Block | None |
Retrieve by hash |
has(hash) → bool |
Check existence |
all_hashes() → set[str] |
List all stored hashes |
size() → int |
Count of unique blocks |
BusMetrics
Token savings analytics.
| Method/Property | Description |
|---|---|
total_tokens_sent |
Cumulative tokens actually sent |
total_tokens_saved |
Cumulative tokens eliminated |
savings_ratio |
Float 0.0–1.0 representing % saved |
agent_stats(agent_id) |
Per-agent breakdown |
summary() |
Dict with all aggregate stats |
Using the Middleware
For drop-in integration with any LLM call function:
from echokill.middleware import EchoKillMiddleware
mw = EchoKillMiddleware()
mw.register("analyst", role="You are a financial analyst.")
mw.register("compliance", role="You are a compliance officer.")
mw.share(quarterly_report)
result = mw.call("analyst", message="What are the key risks?", llm_fn=my_llm_call)
print(result["prompt_info"]) # token savings metrics
OpenAI Integration
from openai import OpenAI
from echokill.middleware import OpenAIMiddleware
client = OpenAI()
mw = OpenAIMiddleware(client=client, model="gpt-4o-mini")
mw.register("reviewer", role="You are a code reviewer.")
mw.share(code_diff)
result = mw.call_openai("reviewer", message="Review this code.")
Azure OpenAI Integration
from openai import AzureOpenAI
from echokill.middleware import OpenAIMiddleware
client = AzureOpenAI(
azure_endpoint="https://your-resource.openai.azure.com/",
api_version="2024-06-01",
# api_key loaded from environment or managed identity
)
mw = OpenAIMiddleware(client=client, model="gpt-4o")
mw.register("reviewer", role="You review enterprise code.")
mw.share(code_diff)
result = mw.call_openai("reviewer", message="Identify security vulnerabilities.")
Multi-User Middleware Pattern
from echokill.middleware import EchoKillMiddleware
from echokill.store import RedisBlockStore
from echokill.bus import MemoryBus
class MultiUserMiddleware:
"""Production pattern: one middleware per user session."""
def __init__(self, redis_client):
self._redis = redis_client
self._sessions: dict[str, EchoKillMiddleware] = {}
def get_session(self, user_id: str, session_id: str) -> EchoKillMiddleware:
key = f"{user_id}:{session_id}"
if key not in self._sessions:
store = RedisBlockStore(redis_client=self._redis, ttl=86400)
bus = MemoryBus(store=store)
self._sessions[key] = EchoKillMiddleware(bus=bus)
return self._sessions[key]
def cleanup_session(self, user_id: str, session_id: str) -> dict:
key = f"{user_id}:{session_id}"
if key in self._sessions:
metrics = self._sessions[key].metrics_summary()
del self._sessions[key]
return metrics
return {}
Scaling & Performance
Horizontal Scaling Strategy
┌─── Load Balancer (sticky sessions optional) ───┐
│ │
┌──────────▼──────────┐ ┌──────────▼──────────┐
│ EchoKill Worker 1 │ │ EchoKill Worker N │
│ (stateless API) │ │ (stateless API) │
└──────────┬──────────┘ └──────────┬──────────┘
│ │
└────────────────┬───────────────────────────┘
│
┌────────────────▼────────────────┐
│ Redis Cluster (6+ nodes) │
│ │
│ Shard 1 │ Shard 2 │ Shard 3 │
│ (blocks)│ (blocks)│ (blocks) │
└──────────────────────────────────┘
Performance Characteristics
| Operation | Latency (p99) | Throughput |
|---|---|---|
build_prompt() (in-memory) |
<5 ms | 10,000 req/s per core |
build_prompt() (Redis) |
<25 ms | 2,000 req/s per core |
add_shared_block() |
<10 ms | 5,000 req/s |
| Block store lookup (Redis) | <3 ms | 50,000 ops/s |
| Session creation | <5 ms | 10,000/s |
Capacity Planning
| Concurrent Users | Workers | Redis Memory | Redis Nodes | Est. Monthly Cost Savings |
|---|---|---|---|---|
| 100 | 2 | 512 MB | 1 | $500–$2,000 |
| 1,000 | 4 | 2 GB | 3 | $5,000–$20,000 |
| 10,000 | 8 | 8 GB | 6 | $50,000–$200,000 |
| 100,000 | 20+ | 32 GB | 12+ | $500,000+ |
Cost savings assume average 70% token reduction at ~$2.50/1M input tokens (GPT-5.4 / GPT-4o pricing).
Optimization Tips
- Enable Redis pipelining for batch block operations
- Use Redis Cluster for >10GB data or >50K concurrent sessions
- Set appropriate TTLs — blocks from completed sessions should expire
- Use connection pooling — never create per-request Redis connections
- Pin shared prefix order — deterministic ordering maximizes LLM provider prefix caching
- Batch agent prompts — compile all agent prompts in a session together to amortize Redis reads
Project Structure
echokill/
├── __init__.py # Public API exports
├── block.py # Content block primitives (Block, BlockType)
├── store.py # Content-addressed storage (InMemory + Redis)
├── agent_state.py # Per-agent state tracking (seen-set, deltas)
├── compiler.py # Prompt compiler (dedup + prefix ordering)
├── bus.py # MemoryBus — main orchestrator
├── metrics.py # Token savings analytics
├── middleware.py # Framework adapters (generic + OpenAI)
├── tokenizers.py # Multi-backend token counting (tiktoken, approximate)
├── cache_optimizer.py # Provider-aware cache optimization
├── cost.py # Dollar cost tracking with pricing DB
├── compaction.py # Conversation history compaction strategies
├── providers.py # Provider-specific middleware (OpenAI, Anthropic, Azure)
├── aio.py # Async bus + async Redis store
├── pool.py # Multi-session BusPool with LRU eviction
├── config.py # Environment-based configuration
├── validation.py # Input validation for blocks and prompts
├── health.py # Health check endpoint
├── logging.py # Structured logging setup
└── py.typed # PEP 561 marker for type checkers
tests/
├── test_block.py
├── test_store.py
├── test_bus.py
├── test_compiler.py
├── test_middleware.py
├── test_features_v03.py # v0.3.0 feature tests (tokenizers, cache, cost, etc.)
└── test_pool.py
examples/
├── code_review_pipeline.py # 5-agent code review demo
└── middleware_usage.py # Middleware with mock LLM
Key Concepts
| Concept | Description |
|---|---|
| Block | Immutable, content-addressed chunk of context (SHA-256 keyed) |
| BlockStore | Deduplicating storage — identical content stored exactly once |
| AgentState | Tracks what each agent has seen; computes deltas |
| PromptCompiler | Assembles shared prefix + agent delta + new message |
| MemoryBus | Top-level orchestrator tying everything together |
| Tenant | Organizational unit — each tenant's data is fully isolated |
| Session | Per-user conversation context; owns a bus instance and agents |
| BusPool | Manages lifecycle of per-session bus instances with LRU eviction |
| CacheOptimizer | Generates provider-specific prompt formats for cache maximization |
| CostTracker | Tracks actual USD savings with model-specific pricing |
| HistoryCompactor | Compresses conversation history using configurable strategies |
| Tokenizer | Multi-backend token counting (tiktoken, approximate, Anthropic) |
| ProviderMiddleware | End-to-end middleware combining dedup + provider caching + cost tracking |
| AsyncMemoryBus | Async version of MemoryBus for non-blocking I/O frameworks |
Estimated Savings
| Scenario | Without EchoKill | With EchoKill | Reduction |
|---|---|---|---|
| 5 agents, 10-turn, 4K shared doc | ~200K input tokens | ~70K tokens | 65% |
| + provider prefix caching (OpenAI) | — | ~49K effective | 75% |
| + Anthropic cache breakpoints | — | ~35K effective | 82% |
| 5 agents, 1-turn code review | 25K tokens | 7.5K tokens | 70% |
| 100 users × 5 agents × 10 turns/day | 20M tokens/day | 6M tokens/day | 70% |
| + provider caching stacked | — | 3M effective/day | 85% |
| Annual savings (100 users, GPT-5.4) | $21,900/year | $6,570/year | $15,330 saved |
| Annual savings (100 users, Claude Sonnet 4.5) | $26,280/year | $3,942/year | $22,338 saved |
Anthropic cache reads are 90% cheaper, making Claude models particularly cost-effective with EchoKill.
Running Tests
pip install -e ".[dev]"
pytest
With coverage
pytest --cov=echokill --cov-report=html
Integration tests (requires Redis)
ECHOKILL_REDIS_URL=redis://localhost:6379/0 pytest tests/ -m integration
Load testing
pip install locust
locust -f tests/load/locustfile.py --host=http://localhost:8000
Running Examples
python examples/code_review_pipeline.py
python examples/middleware_usage.py
Contributing
- Fork the repository
- Create a feature branch:
git checkout -b feature/your-feature - Install dev dependencies:
pip install -e ".[dev]" - Run tests:
pytest - Run linter:
ruff check . - Submit a pull request
Code Quality Requirements
- All PRs must pass
pytestandruff check - New features require tests with >90% coverage
- Breaking API changes require a migration guide in the PR description
Changelog
v0.3.0
- Tokenizers: Multi-backend token counting (tiktoken, Anthropic, approximate fallback)
- Provider-Aware Cache Optimization: Anthropic
cache_controlmarkers, OpenAI prefix caching, pre-warming support - Cost Tracking: Real USD savings with built-in pricing DB for GPT-4o, Claude Sonnet 4, Gemini, and more
- Conversation Compaction: 4 strategies (sliding window, token budget, summary, priority-based)
- Multi-Provider Middleware: OpenAI, Anthropic (with prewarm), Azure OpenAI provider-specific adapters
- Async Support:
AsyncMemoryBusandAsyncRedisBlockStorefor async frameworks - Stacked savings: EchoKill dedup + provider caching achieves up to 85% effective reduction
v0.2.0
- Multi-user / multi-tenant architecture with
BusPool - Redis-backed
RedisBlockStorewith TTL and key prefixes - Environment-based configuration (
EchoKillConfig) - Input validation for blocks and prompts
- Prometheus metrics integration
- Health check endpoints
- Structured logging
py.typedmarker for PEP 561
v0.1.0 (Initial Release)
- Core deduplication engine with content-addressed blocks
- InMemory and Redis block store backends
- Per-agent state tracking with seen-set and delta computation
- Prompt compiler with deterministic prefix ordering
- Generic middleware + OpenAI-specific adapter
- Token savings metrics and analytics
- 65–82% token reduction in multi-agent pipelines
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 echokill-0.3.0.tar.gz.
File metadata
- Download URL: echokill-0.3.0.tar.gz
- Upload date:
- Size: 85.3 kB
- Tags: Source
- Uploaded using Trusted Publishing? No
- Uploaded via: twine/6.2.0 CPython/3.12.4
File hashes
| Algorithm | Hash digest | |
|---|---|---|
| SHA256 |
38b6e74c818e8a6146de17bfe9197854b4efa047d45858a00c2655915eb1c4b6
|
|
| MD5 |
1ee48f4cd10c0fef4e811ac048880295
|
|
| BLAKE2b-256 |
dd337f8f27ae6abfb8a583e6fbc0b398c413bf13d503eac9eda7b7af0ac26ecb
|
File details
Details for the file echokill-0.3.0-py3-none-any.whl.
File metadata
- Download URL: echokill-0.3.0-py3-none-any.whl
- Upload date:
- Size: 55.5 kB
- Tags: Python 3
- Uploaded using Trusted Publishing? No
- Uploaded via: twine/6.2.0 CPython/3.12.4
File hashes
| Algorithm | Hash digest | |
|---|---|---|
| SHA256 |
baa6d4047db16c9ae25a3a060439c2a21ff14b2e4bffe347924ee2578fd827f1
|
|
| MD5 |
600943803dc4bb2cd0ffeed6520499a2
|
|
| BLAKE2b-256 |
67e0734aedbea54c1d062cf3bb788c12d1dc0cec83389d022b251c1bea21ac31
|