pi's backpack, and the helper that empties it
What we built, what happened when we tried it, and why it is still a bit rough. Real numbers, real examples, five-year-old words.
The backpack keeps getting heavier.
Every time pi reads a file or runs a command, the answer goes into its backpack (the context). It never takes anything out. Three tasks in a row on a small Rust project, and the backpack goes from 40k to 90k tokens.
Backpack weight after every step, pruning off (three tasks, one session)
Heavy backpack = pi forgets what matters, gets slower, and eventually pi has to squash everything (compaction). We want to delay that.
The helper asks a tiny brain: “will pi need this again?”
Right before pi thinks, the helper looks at old tool outputs and asks Jev (a small, fast model) how likely pi is to need each one again. If Jev says “nah”, the output is swapped for a sticky note. pi can always ask for the real thing back with recall.
Find tool outputs older than 3 steps and bigger than 300 tokens.
One question per output: “Will the agent need the full output again to finish the task?” → a probability p.
p below 0.25 → sticky note. Otherwise keep and ask again later.
Only swap at good moments (new user message, backpack nearly full), and swap several at once.
A real sticky note from one of the runs
The original stays in the session file. Only what is sent to the model changes.
What exactly do we send Jev? A tiny card for each old output, plus what pi is doing right now.
Jev never sees the backpack. It sees a summary card for each candidate — the first 300 characters, the last 100, how old it is, and the first thing pi said after reading it — plus the task (the user's message) and now (pi's latest words). One question per card, all in one request. This is a real request from the main run, rebuilt with the same code.
The question (asked once per card) and how Jev is told to judge it
| answer | when |
|---|---|
| yes, keep | |
| no, prune |
The state: task and now at this moment (step , window “”)
task (last user message, ≤ 1000 chars)
now (pi's latest text, ≤ 1000 chars)
The 16 cards, and what Jev answered (green = would prune at 0.25)
Raw request JSON (state + one question), as sent to client.systemOne()
Notice: now names get.rs and output.rs as the files pi is about to edit → Jev keeps them (0.57, 0.62) even though they're 51 steps old. That's the task-awareness we wanted.
Also notice two weaknesses of the card: the nine parallel reads all carry the same agentReaction (it's the text after the whole step, not per file), and the bash outputs have none at all (pi went straight to the next tool call). Jev is judging those mostly from head and arg. And targets.rs at 0.62 is an over-keep — nothing in now points at it.
Plumbing
One HTTPS call to TypeSafe: systemOne({ model: "jev-latest", state, questions }) with a 2 s timeout and no retries; on timeout the window is skipped and retried at the next one. Cards are batched so state + questions stay under 24k tokens (this request: tokens estimated). Questions are keyed c0…c15 and mapped back to toolCallIds; each answer is a number 0–1 (noul). Latency in the live runs: p50 ≈ 300 ms, max 1.1 s.
Here is a real backpack, with and without the helper.
Same three-task session, pruning on, snapshot at the end of task 2. Top row: everything pi had collected. Bottom row: what was actually sent — green slivers are sticky notes. Hover a block to see what it was.
Context at the end of task 2
The bottom row is what pi actually carries. The gap on the right is the room we made.
Grown-up numbers
Did it help? Yes, a little — and more the longer pi works.
Task 1 is the same in every mode (nothing is old enough to prune yet). By task 3, the helper has been cleaning for a while, so pi starts the task with a lighter backpack.
Heaviest the backpack got during each task
Whole-session peak: . Task 3 was the only task that was also cheaper with pruning.
Grown-up numbers
Each cell is one run (n = 1). The middle task is high-variance — pi did anywhere from 37 to 85 tool calls depending on its mood — so read “calls” and “$” as noise and “peak” as signal.
Did we ever throw away something pi needed? No.
If pi needed a pruned thing, it would call recall. Across every live run, it never did. And every file, function and struct name pi wrote into its reports actually exists in the code — it did not make things up to fill the gaps.
Everything that got pruned in the main run (pruning on, threshold 0.25)
Test files read “for style”, rg/grep exploration output, and files pi re-read after editing them. All reasonable.
How sure is Jev, really? Very sure at the bottom, guessing in the middle.
We replayed 14 old sessions and checked, for every output Jev scored: did pi later use something that only that output contained? (That's our stand-in for “needed it”.) Blue = Jev's score. Orange = the ones pi did use later.
What Jev said (p) vs. what pi actually used later
Below p = 0.15, 1 in 28 was used later. From 0.15 to 0.30, about 1 in 4 — the same as picking at random ( of everything gets used later). Jev is only reliable at the very bottom. That is why the cutoff is low, and why the savings are modest.
Moving the cutoff: how much we save vs. how often we're wrong
At 0.35 (the original default) Jev is exactly as wrong as random. At 0.25 it's half as wrong and still saves ~half of what's possible.
Grown-up numbers
Rough #1: on a small project, Jev thinks everything might be needed.
In the old sessions (long, messy real work) Jev's average score was 0.28. In the live runs (“read this whole small repo, then fix things”) it was ~0.5. When every file is a possible target, Jev — correctly — refuses to bet. So at cutoff 0.25, almost nothing gets pruned.
Jev's first score per output: replayed sessions vs. live runs
Same outputs, asked again and again as the task moves on (live, pruning on)
Scores are stable and drift down slowly — then jump back up when task 3 (“trace how mansk get works”) makes github.rs and get.rs relevant again. Jev is consistent and task-aware; it just isn't confident on a small repo. The helper will matter more on long, wandering sessions than on tidy small ones.
Rough #2: every swap means repacking the whole backpack.
The model provider remembers (“caches”) the start of the backpack so pi doesn't pay full price for it each time. Swap one thing in the middle and everything after it must be re-sent and re-remembered — 1.25× the normal price for those tokens. Saving a small thing this way costs more than it saves.
Tokens re-remembered (cache writes) per step — orange = a repack caused by pruning, grey = new user turn or natural cache miss
Pruning one item at a time (every-call) repacks constantly. Our fix: collect prunes until they add up to ≥5% of the budget, then swap them all in one repack. Same lighter backpack, half the repacks.
When does a swap pay for itself?
Repack costs 1.25 × R. Each later step saves 0.1 × S (cached tokens cost 10%). Break-even after N = 12.5 × R ⁄ S steps. Default slider = the real first swap in the main run: it needed ~70 more steps to pay off — the whole rest of the session.
Grown-up numbers
Rough #3: we asked Jev the same question hundreds of times.
Once the backpack is nearly full, the helper checks at every step. The first version re-asked about every old output every step — and Jev kept saying “keep”. That's wasted calls and wasted latency.
Fixed partly (re-ask only every 3 steps once over budget), but the ratio is still ~14 questions per item. A back-off (3, 6, 12 steps…) would cut it further; the scores barely move between asks anyway.
Rough #4: a rule with no brain did much of the useful work.
“pi read main.rs, then edited main.rs, then read it again → the first read is dead.” No model needed, never wrong. In several runs this rule pruned as much as Jev did.
What decided each prune, all live runs
If a dumb rule gets you most of the way, the expensive part has to earn its keep. Jev does — in the low tail — but the gap is smaller than we hoped.
Rough #5: it's hard to know if we were wrong.
The obvious check — “did pi read this file again later?” — is useless: pi never re-reads what is still in its backpack ( re-reads out of ~190). So we invented a stand-in: did pi later type a name that only this output contained? It works, but it over-counts and under-counts. Here are the “mistakes” it found:
Pruned in replay, but the stand-in says pi used something from it later
Some are real misses (a name pi needed). Some are the proxy being picky (pi had already copied the name into its own notes). Only the live recall count is honest — and we have 13 runs of it, all zero. Need many more.
So… is it good?
As a tool: 6/10. Safe, never lost anything, makes the backpack ~15% lighter on long sessions, costs about the same. As a study: 8/10. We now know where Jev's signal is, what pruning costs, and that a dumb rule gets you far.
It decides but doesn't act. Check /prune show on a long session and see if you agree with it.
An 8k-token file read where 400 tokens mattered — filter it before it enters the backpack. No repacking cost at all.
If pi has been idle past the cache lifetime, repacking is free. Do the swaps then.