Skip to content
echogeco
The harness, in code · Build it

Budgeted compaction

Implement the compaction planner that keeps the newest turns verbatim, folds any earlier summary, and refuses when the kept turns leave no room.

codemediumExercise · 3 min · 71 of 77
On this page

Compaction is a model call, but deciding what to compact is not. Planned in code, the decision is testable, and the two ways it loses a run's memory become test cases.

The situation

A research agent adds about 3,500 tokens of history a turn on a 6,000-token system prompt and tool set, in a 200k-token window with 8,000 reserved for output. When the history reaches 105,000 tokens, which is turn 30, the harness compacts it to a budget of 37,000, keeping the last 10 turns verbatim and summarising the rest, the mechanism memory and context management describes.

The first version had two bugs, both found in one week of traces. At the second compaction, on turn 50, it summarised turns 21 to 40 and dropped the summary of turns 1 to 20, and the agent fetched again 14 pages it had already read. On a run whose last ten turns included three 20,000-token tool results, the kept turns alone came to 84,500 tokens, the summary's cap came out at −47,500, and the API rejected the summariser call, ending the run in an error.

The question

Implement the planner so that the second compaction folds the first summary, and a history whose kept turns overflow the budget is refused rather than cut.

def solve(turns, budget, keep_last):
    """turns: [(label, tokens), ...], oldest first; a first entry labelled
    "summary" is the summary an earlier compaction left.
    budget: the tokens the history may hold after compaction, summary included.
    keep_last: how many of the newest turns stay verbatim.
    Returns (fold, keep, slot): the labels sent to the summariser, oldest first,
    with an earlier summary always among them; the labels kept verbatim; and
    the summary's token cap, budget minus the kept tokens. A history that
    already fits returns ([], every label, 0). Returns None when there is
    nothing to fold or the kept turns leave no room for a summary."""
T30 = [(f"t{i}", 3500) for i in range(1, 31)]
SECOND = [("summary", 2000)] + [(f"t{i}", 3500) for i in range(21, 51)]

solve(T30, 37000, 10)
    -> ([f"t{i}" for i in range(1, 21)], [f"t{i}" for i in range(21, 31)], 2000)
solve(SECOND, 37000, 10)
    -> (["summary"] + [f"t{i}" for i in range(21, 41)], [f"t{i}" for i in range(41, 51)], 2000)
solve([("t1", 20000), ("t2", 20000), ("t3", 20000)], 37000, 2)
    -> None

A strong answer · Rubric · Where answers go wrong · Solution

Write your answer first, then open