Choose what goes into each model call, under a token budget.
An agent collects instructions, memory, documents and tool output faster than a model can read them. Sending everything costs money and eventually fails; cutting by hand drops the wrong things. Harpe makes the cut explicit, repeatable and visible in a report.
Move the budget, watch the cut.
Ten context items, one budget. Move the budget and change the policy; every decision below is made by the harpe crate itself, compiled to WebAssembly.
| Item | Tokens | Priority | Result |
|---|---|---|---|
| policy | 420 | 0 (pinned) | not run yet |
| meeting | 3,200 | 5 | not run yet |
| 2,600 | 4 | not run yet | |
| contract | 1,150 | 2 | not run yet |
| org | 300 | 5 | not run yet |
| incident | 1,400 | 3 | not run yet |
| style | 700 | 4 | not run yet |
| spec | 900 | 2 | not run yet |
| api | 1,050 | 3 | not run yet |
| decisions | 380 | 1 | not run yet |
How it decides
- Every item gets a token count: yours, or an estimate (the larger of its word count and its character count divided by four, rounded up). Plug in your model's tokenizer through the
TokenCountertrait for exact counts. - Room for the answer and for tool calls is reserved first (
reserved_completion,reserved_tools,safety_margin). - Pinned items (priority
0) are always kept. - The rest are taken in policy order, each one kept if it still fits and skipped if it does not: by priority, newest first, or in input order.
- You get back the kept items, the dropped items, tokens used and tokens left.
Install
[dependencies]
harpe = "=0.1.0-alpha.4"pip install "harpe==0.1.0a4"Version 0.1.0-alpha.4 is on crates.io and PyPI.
use harpe::{ContextBudgetEngine, ContextItem, ContextSynthesizer, PrunePolicy, TokenBudget};
fn main() -> Result<(), Box<dyn std::error::Error>> {
let mut items = vec![
ContextItem::new("policy", "Never deploy on Fridays.", 0), // priority 0: always kept
ContextItem::new("decisions", "We chose blue-green deploys in March.", 1),
ContextItem::new("meeting", "Long notes from the weekly sync, mostly unrelated.", 5),
];
items[2].token_count = 900; // or leave 0 and Harpe estimates it
let mut budget = TokenBudget::new(1_000);
budget.reserved_completion = 200; // room for the model's answer
let report = ContextBudgetEngine::default().synthesize(&items, &budget, PrunePolicy::LowestPriority)?;
for it in &report.included_items { println!("keep {}", it.id); }
for it in &report.pruned_items { println!("drop {}", it.id); }
println!("{} tokens used, {} left", report.consumed_tokens, report.budget_remaining);
Ok(())
}from harpe import ContextBudgetEngine, ContextItem, PrunePolicy, TokenBudget
items = [
ContextItem(id="policy", content="Never deploy on Fridays.", priority=0, pinned=True),
ContextItem(id="decisions", content="We chose blue-green deploys in March.", priority=1),
ContextItem(id="meeting", content="Long notes from the weekly sync.", priority=5, token_count=900),
]
budget = TokenBudget(total_budget=1000, reserved_completion=200) # room for the answer
report = ContextBudgetEngine().synthesize(items, budget, PrunePolicy.LOWEST_PRIORITY)
print("keep:", [i.id for i in report.included_items], "drop:", [i.id for i in report.pruned_items])
print(report.consumed_tokens, "tokens used")Measurements
| Measurement | Median | Range | Raw data |
|---|---|---|---|
| Harpe call time, 10 items (median call) 10 context items, 60-token budget, 2,000 calls; p50 of the calls in one run | 1.67 µs | 1.64–1.72 | run1 run2 run3 run4 run5 · source · details |
| Harpe call time, 200 items (median call) 200 context items, 1,500-token budget, 500 calls; p50 of the calls in one run | 34.78 µs | 34.51–35.93 | run1 run2 run3 run4 run5 · source · details |
| Tokens removed to fit the budget, 200-item case 200 generated items against a 1,500-token budget | 69.5% | same in every run | run1 run2 run3 run4 run5 · source · details |
| Simulated agent turns One run of the long-running context simulation | 100,000 turns | same in every run | run1 run2 run3 run4 run5 · source · details |
| Tokens removed over 100,000 simulated turns 100,000 turns, 1,200-token budget per turn, compared with keeping every item | 78.3% | same in every run | run1 run2 run3 run4 run5 · source · details |
| Turns that went over the budget 100,000 turns, 1,200-token budget per turn | 0 turns | same in every run | run1 run2 run3 run4 run5 · source · details |
| Average Harpe time per simulated turn 100,000 turns; includes building each turn's item list | 44.9 µs | 44.6–45.3 | run1 run2 run3 run4 run5 · source · details |
How these numbers were measured. Each figure is the median of 5 runs of the telarch-benchmarks programs at release tag v0.1.0-alpha.4, with the range of all runs beside it. 13th Gen Intel Core i5-13500, 20 logical CPUs, Linux 6.18, rustc 1.97.1, pinned to the performance-core threads (CPUs 0-11). Shared server, not isolated; each run started only after the 1-minute load average was below 4 on two checks 20 s apart. Synthetic data; real workloads will differ. Raw output of every run.
Limits, stated plainly
- Greedy, not optimal. Harpe takes items in order and keeps each one that fits. A different combination could sometimes fill the budget more fully.
- Pinned items can exceed the budget. If the pinned items alone are larger than the budget, Harpe keeps them anyway and reports zero tokens left. Check
consumed_tokensif you need a hard cap. - Estimates are estimates. Without your own counter, token counts approximate the model's tokenizer.
- No relevance scoring yet. In 0.1.0-alpha.4, the
LeastRelevantpolicy behaves exactly likeTruncateTail(input order), andTruncateTaildrops whole items; it does not shorten text. - Deterministic. No randomness, no clock, no network: the same items and budget always give the same report.