Harpe · context budget

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.

Try it

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.

Running harpe 0.1.0-alpha.4 in your browser (WebAssembly). Loads 59 KB when you start it; nothing is sent anywhere.
ItemTokensPriorityResult
policy4200 (pinned)not run yet
meeting3,2005not run yet
email2,6004not run yet
contract1,1502not run yet
org3005not run yet
incident1,4003not run yet
style7004not run yet
spec9002not run yet
api1,0503not run yet
decisions3801not run yet

How it decides

  1. 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 TokenCounter trait for exact counts.
  2. Room for the answer and for tool calls is reserved first (reserved_completion, reserved_tools, safety_margin).
  3. Pinned items (priority 0) are always kept.
  4. 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.
  5. You get back the kept items, the dropped items, tokens used and tokens left.

Install

Rust: Cargo.toml
[dependencies]
harpe = "=0.1.0-alpha.4"
Python
pip install "harpe==0.1.0a4"

Version 0.1.0-alpha.4 is on crates.io and PyPI.

Rust (tested)
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(())
}
Python (tested)
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

MeasurementMedianRangeRaw 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 µs1.64–1.72run1 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 µs34.51–35.93run1 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 runrun1 run2 run3 run4 run5 · source · details
Simulated agent turns
One run of the long-running context simulation
100,000 turnssame in every runrun1 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 runrun1 run2 run3 run4 run5 · source · details
Turns that went over the budget
100,000 turns, 1,200-token budget per turn
0 turnssame in every runrun1 run2 run3 run4 run5 · source · details
Average Harpe time per simulated turn
100,000 turns; includes building each turn's item list
44.9 µs44.6–45.3run1 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_tokens if 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 LeastRelevant policy behaves exactly like TruncateTail (input order), and TruncateTail drops whole items; it does not shorten text.
  • Deterministic. No randomness, no clock, no network: the same items and budget always give the same report.