A receipt for
every token.
Treat the context window like a packed suitcase, not a string you concatenate. Declare blocks with priorities, assemble to a token budget deterministically — and get an honest receipt of what was kept, shrunk, or dropped.
from cendor.contextkit import Context, Block ctx = Context(budget_tokens=8000, model="gpt-4o", reserve_output=500, order="attention") ctx.add(Block(SYSTEM_PROMPT, priority=10, pin=True, role="system")) ctx.add(Block(retrieved_docs, priority=5, evict="compress")) ctx.add(Block(messages=chat_history, priority=3, evict="drop_oldest")) ctx.add(Block(user_msg, priority=9, pin=True, role="user")) messages = ctx.assemble() # fits the budget, deterministically print(ctx.report()) # kept / shrunk / dropped + token math
How a block loses gracefully.
Every block declares its priority and how it prefers to lose. When the budget is tight, higher priority holds its ground and lower priority gives way — using the eviction strategy the block chose, not an arbitrary string chop.
"…[truncated]" markeraassemble()) shrinks the blockPinned blocks are never evicted — if pins alone overflow, assemble() raises BudgetError instead of silently mangling your prompt. Ordering modes: default · attention (strongest context on the edges — the lost-in-the-middle effect) · cache (stable prefix to maximize prompt-cache hits).
Shrink the budget. Watch the receipt.
This is the packer, live. Drag the slider — blocks fit in priority order against the usable budget (budget minus reserve_output), and the receipt updates to show exactly what was kept, shrunk, or dropped.
▸ drag the budget — every block refits, in priority order · pins never move
used 7,200 of 8,000 budget · reserve_output 500 · within budget ✓
Same inputs, same budget → same output, every time. The receipt is emitted on the event bus, so acttrace can audit what the model actually saw.
What-if, adapters, images.
Preview before you commit
See what would be kept, shrunk, or dropped at a tighter budget — a dry-run receipt, without committing anything.
One context, any provider
The same assembled context, reshaped per provider — each adapter returns the messages in that provider's expected format.
Images have a budget too
Multimodal parts are budgeted per image with your image_tokens figure, so image-bearing blocks pack against the same budget as text.
Where the edges are — by design.
used == core.tokens.count(assemble()) holds exactly under the offline counting model and approximately under real tokenizers — per-message framing isn't perfectly linear.
Image parts use your image_tokens figure; a text-only recount can't see them, so don't assert equality on multimodal contexts.
If a managed runtime owns context internally, there's nothing for contextkit to shape — the budget and audit tools still work.
Packing is deterministic; a summarize callback that isn't deterministic makes the assembled output vary with it.
Stop concatenating. Start packing.
contextkit docs → · attention-ordering cookbook → · composes with the Cendor stack