Compact the history
# grade this stage (from your course directory)
npx theinference test --stage compaction --watchLast step you learned the policy by heart: keep intent, edits, errors, and open tasks; drop verbatim output, intermediate reasoning, and skill descriptions; re-inject startup. Now you encode it. Compaction is graded as a pure classifier — given a list of items by kind, you say which survive and which go. No model judges the summary's prose; the grader checks two deterministic sets. You also build the trip wire that decides when to fire it.
startup → keep (system prompt, tool defs, CLAUDE.md, memory)
user-intent → keep (the original goal)
edit → keep (diffs already applied)
error → keep (failures seen — don't repeat them)
task → keep (open to-do list)
tool-output → drop (verbatim file dumps / bash stdout)
reasoning → drop (intermediate thinking)
skill-description → drop (reloads on demand when invoked)
What to build
Implement compact and needsCompaction
Implement two functions in src/context.ts:
compact(items)— take a list of item kinds (strings) and partition them into{ kept, dropped }. Keepstartup,user-intent,edit,error, andtask; droptool-output,reasoning, andskill-description. Preserve input order within each list, and don't dedupe — classify each item independently.needsCompaction(used, limit, threshold = 75)— returntrueonce pressure reaches the threshold:(used / limit) * 100 >= threshold. Default the threshold to 75 (percent).
The program.ts compact subcommand reads fixtures/case.json ({ items }), calls compact, and prints { kept, dropped } as one JSON line.
const KEEP = new Set(['startup', 'user-intent', 'edit', 'error', 'task'])
export function compact(items: string[]): { kept: string[]; dropped: string[] } {
// partition items by KEEP membership, preserving order in each list
}
export function needsCompaction(used: number, limit: number, threshold = 75): boolean {
// true once (used / limit) * 100 >= threshold
}
The grader compares your kept and dropped sets against the reference exactly, so an item in the wrong bucket fails the step. The two that trip people: skill-description is dropped (it reloads when the skill is invoked, so the body is never load-bearing in history), and task is kept (open work the agent must still finish). startup is kept here because the classifier sees it as a surviving item — re-injection of startup content is the loop's job, not the classifier's.
needsCompaction works in percent: (used / limit) * 100, compared with the threshold default of 75. Use >=, not > — at exactly the threshold you compact. used = 150, limit = 200 is 75% → true; used = 149 is 74.5% → false. Compacting at the line, not past it, is what keeps the next request from overflowing.
Process mode. The harness runs npx tsx program.ts compact fixtures/case.json, which calls compact(case.items) and prints JSON.stringify({ kept, dropped }). The grader compares that line to fixtures/expected.json byte-for-byte — so for items [startup, user-intent, tool-output, reasoning, edit, error, skill-description, task] it must print {"kept":["startup","user-intent","edit","error","task"],"dropped":["tool-output","reasoning","skill-description"]}. Order within each list comes straight from the input. This is a structural check on the policy — no model judges a summary.