ctxfit — drop-in context-window optimizer for any agent loop
Attention cost scales quadratically: doubling context roughly quadruples compute, and every extra chunk makes the agent slower, more expensive, and less accurate. Fifty turns into a run you hit the limit — and each tool result sits uncompressed in context for the rest of the run.
ctxfit is the standalone layer every agent team hand-rolls and nobody ships: structural context compression with budget enforcement. No LLM calls, no framework lock-in, deterministic.
import ctxfit
fitted, report = ctxfit.fit(messages, total_budget=128_000)
print(report.summary())
# est. tokens: 96,400 → 41,150 (−57.3%) [heuristic]
# retrieved 71,200 → 18,400
# history 24,100 → 21,650
# truncate_tool_output: −52,800 est. tokens, 3 message(s)
# within budget ✓
Companion story with ContextRouter: "ContextRouter picks what goes in, ctxfit keeps it lean."
- PyPI:
ctxfit· npm:@decnutz/ctxfit
What it does
Structural compression (no API call, no latency, reproducible):
| Strategy | What it does |
|---|---|
Deduplicate |
Exact-duplicate message bodies → pointer to the first occurrence |
CompactJson |
JSON arrays → markdown tables; nested objects → flat key paths (only when it saves tokens) |
TruncateToolOutput |
Oversized tool results → head + tail with an explicit omission marker |
SlidingWindow |
Oldest turns dropped while history overflows (pinned + latest turn never dropped) |
DropOldestUnpinned |
Aggressive-mode last resort: oldest-first dropping until the total fits |
Budget enforcement: declare a total budget and per-section split —
default system 10% / retrieved 55% / history 25% / scratchpad 10%.
Strategies compress toward the budgets; surviving overruns produce warnings
(strict=True raises BudgetOverflow instead).
Token estimation: heuristic by default (chars/4, CJK-adjusted, zero
dependencies); automatically upgrades to tiktoken's cl100k_base when
installed. Pluggable — implement count(text) for your own tokenizer.
Framework adapters: raw message-list dicts are first-class
(ctxfit.fit accepts them directly). OpenAI-shaped (also covers
OpenAI-compatible endpoints like Ollama/freellmapi), Anthropic block-shaped,
and duck-typed LangChain-shaped helpers convert at the boundary; the core
never sees framework shapes.
Install
pip install ctxfit
# optional: better estimates
pip install "ctxfit[tiktoken]"
CLI
# Breakdown without changing anything (exit 2 if over budget — CI-friendly)
ctxfit analyze convo.json --budget 128000
ctxfit analyze convo.json --format json --top 20
# Compress to fit, write fitted messages + report
ctxfit fit convo.json --out fitted.json --report report.json --aggressive
# Section overrides + strict mode
ctxfit --budget 64000 --budgets retrieved=0.6,history=0.2 --strict fit convo.json
# Quick estimate
ctxfit estimate "some text"
convo.json is a JSON array of message dicts (generic {"role","content"} or
OpenAI-shaped; --flavor overrides auto-detection).
Python API
import ctxfit
from ctxfit import Budget, FitPipeline, TruncateToolOutput
# One call
fitted, report = ctxfit.fit(
messages,
total_budget=128_000,
budgets={"retrieved": 0.6, "history": 0.2}, # partial overrides OK
strategies="default", # or "aggressive", or [Strategy(...), ...]
estimator="auto", # "heuristic" | "tiktoken" | your own
strict=False, # True -> raise instead of warn
flavor="auto", # "openai" | "anthropic" | "langchain" | "generic"
)
# Full control
pipeline = FitPipeline(
strategies=[TruncateToolOutput(max_chars=8000, when="always")],
estimator="heuristic",
budget=Budget(total=64_000),
aggressive=True,
)
fitted, report = pipeline.fit(messages, flavor="openai")
# Measure-only
info = pipeline.analyze(messages) # section usage, warnings, top consumers
FitReport carries tokens_before/after per section, per-strategy savings,
warnings, over_budget, plus summary() and to_dict().
Custom strategies
from ctxfit import Strategy, Message
class RedactSecrets(Strategy):
name = "redact_secrets"
target_sections = ("retrieved", "history")
def _apply(self, messages, ctx, report):
out = []
for m in messages:
new = m.content.replace("sk-live-", "sk-live-[REDACTED]")
if new != m.content:
report.log("redacted secret-like token", 0)
out.append(Message(role=m.role, content=new, kind=m.kind,
name=m.name, call_id=m.call_id,
section=m.section, pinned=m.pinned, meta=m.meta))
return out, report
Contract: pure function (never mutate inputs — return new objects), preserve
message order, never modify tool_call content, never orphan half of a
tool_call/tool_result pair (the pipeline raises PairingBroken if you do),
and respect pinned.
Pinning
System messages are pinned automatically. Pin anything that must survive:
{"role": "user", "content": task_brief, "pinned": True}
Pinned messages are never dropped; truncated only with explicit opt-in
(TruncateToolOutput(truncate_pinned=True)).
Safety rules (load-bearing)
- Pair-atomicity. Tool calls and results are linked by ID and kept,
truncated, or dropped together.
validate_pairing()runs before and after the pipeline; violations raisePairingBroken. - Tool-call arguments are sacred. No strategy ever modifies
tool_callcontent — API validity is non-negotiable. - Soft enforcement by default. ctxfit degrades to warnings, never crashes
your agent loop. Opt into
strict=Trueif you want exceptions. - Honest numbers. Every figure is an estimate, labeled as such.
Limitations (read before trusting it blindly)
- Estimates, not tokens. The heuristic is roughly ±25% on mixed prose/code and worse on CJK/emoji-heavy text. Budget 80–90% of your model's true window as a safety margin. tiktoken (optional) narrows the error but no heuristic equals your model's actual tokenizer.
- Lossy by design. ctxfit deletes and rewrites content to fit budgets. Pin anything that must survive verbatim.
- Head+tail truncation assumes the middle matters least. If the critical line is item #500 of 1,000, it will be lost. Pin or pre-filter such payloads.
- JSON compaction is whole-message only (no substring extraction);
non-uniform object arrays are left alone; table cells truncate at 120 chars
(marked
…); only applied when it actually saves tokens. - Exact-match dedup only. Near-duplicates are not merged — deliberate; fuzzy matching is unexplainable and risky.
- No LLM summarization, ever. Structural compression only. (Subclass
Strategywith your own summarizer if you want semantic compression — that's your API bill, not ours.) - Turn counting is user-message-led. Tool-heavy loops with few user turns
should use
aggressive=True. to_anthropicemits system messages as{"role": "system"}dicts — hoist them to the API's top-levelsystemparameter yourself.- Section overrides are a power tool. Mislabeling sections mostly hurts you (wrong protection, wrong truncation eligibility); assignment from role/kind is deterministic and never inferred from content length.
Development
python -m venv .venv && .venv/bin/pip install -e ".[dev]"
.venv/bin/python -m pytest # 104 tests
.venv/bin/ruff check src tests
.venv/bin/ruff format --check src tests
License
MIT — Calas Automations Inc.
Metadata
Release files for ctxfit 0.1.1
For a detailed explanation of source distributions (sdists) and built distributions (wheels), please see the package formats documentation.
Source distribution (sdist)
| File | Size | Uploaded | Provenance | |
|---|---|---|---|---|
| ctxfit-0.1.1.tar.gz | 36.4 kB | Without provenance | Details |
Built distribution (wheel)
| File | Interpreter | ABI | Platform | Reset |
|---|---|---|---|---|
| ctxfit-0.1.1-py3-none-any.whl | Python 3 | none | any | Details |
Total release size: 66.1 kB
Release files / ctxfit-0.1.1.tar.gz
| Download URL | ctxfit-0.1.1.tar.gz |
|---|---|
| Size | 36.4 kB |
| Tags | Source |
|
SHA-256 checksum How to use checksums |
6605157e539e76aab7ea03e0861870c44bd6606d56fd6f01a755e53d1831e5d4
|
|
BLAKE2b-256 checksum How to use checksums |
b91a6e837904700f1e67a50e3e434af233e1afed00fcbdb543b4bf52f9fca55d
|
| Upload date | |
|
Uploaded using Trusted Publishing? What is trusted publishing? |
No |
| Uploaded via |
twine/7.0.0 CPython/3.11.15
|
Release files / ctxfit-0.1.1-py3-none-any.whl
| Download URL | ctxfit-0.1.1-py3-none-any.whl |
|---|---|
| Size | 29.7 kB |
| Tags | Python 3 |
|
SHA-256 checksum How to use checksums |
0d51bd9afd6090f48901ec696fe0fd8e25199582b03e666b86fafb628545c04a
|
|
BLAKE2b-256 checksum How to use checksums |
e99c2afebb988e64c69bd8669bb26fe34d22caec2e40d047730da94e86be0111
|
| Upload date | |
|
Uploaded using Trusted Publishing? What is trusted publishing? |
No |
| Uploaded via |
twine/7.0.0 CPython/3.11.15
|
Security for ctxfit 0.1.1
Provenance
Provenance describes where a file came from. On PyPI, provenance is shared via attestations, which provide a verifiable record of the build or publishing details. View details, limitations and caveats.
No provenance found
None of the files in this release were uploaded with provenance metadata.
A note on package security
PyPI is a repository for distributing packages, not a service for auditing them. While third-party security researchers and our staff do their best to detect and remove malicious packages, PyPI cannot guarantee the quality, security or suitability of any given package.