Solving tips
- Compaction has three fixed parts: the system message you always keep, a recent window you never touch, and the old middle you collapse into one summary. Get those boundaries right and the rest is bookkeeping.
- Only summarize when you are actually over budget and there is a non-empty middle to collapse. Summarizing a conversation that already fits just burns a model call and loses detail.
- Take both the tokenizer and the summarizer as injected callables so the whole function is deterministic and unit-testable without a real model or a real token count.
A long-running agent cannot keep every turn forever: the context window is finite and every token costs money and latency. The standard fix is compaction. Once the transcript grows past a budget, you collapse the oldest turns into a short summary and keep only the system prompt plus the last few messages verbatim. This exercise is that control logic.
How compaction works
The history is a list of {"role", "content"} messages. You split it into three regions:
- The system message — if
messages[0]hasrole == "system", it is the durable instruction set and is always kept as the first message. - The recent window — the last
keep_recentmessages. These are the live context the model needs and are kept verbatim. - The old middle — everything between the system message and the recent window. When you are over budget, this whole region is replaced by one summary message.
You only compact when the transcript is actually over budget: sum count_tokens(m) across every message and compare against token_budget. If the history already fits, return it untouched. If it is over budget but the middle region is empty (there is nothing to collapse), also return it untouched — you cannot save space that is not there.
Both count_tokens and summarize are injected callables. That keeps the function deterministic: the same inputs always produce the same compacted list, with no real tokenizer or model in the loop.
Task
Complete compact_history(messages, summarize, count_tokens, token_budget, keep_recent=2):
- Compute the total token count. If it is not greater than
token_budget, returnmessagesunchanged. - Peel off the system message:
head = [messages[0]]if its role is"system", elsehead = []. The rest is thebody. - Split the body into
recent(its lastkeep_recentmessages) andold(everything before them). Handlekeep_recent == 0, whererecentis empty andoldis the whole body. - If
oldis empty, there is nothing to summarize — returnmessagesunchanged. - Otherwise build
summary = {"role": "system", "content": summarize(old)}and returnhead + [summary] + recent.
Example
messages = [
{"role": "system", "content": "You are a helpful assistant."}, # 5 tokens
{"role": "user", "content": "one two three four"}, # 4 tokens
{"role": "assistant", "content": "reply a"}, # 2 tokens
{"role": "user", "content": "five six seven"}, # 3 tokens
{"role": "assistant", "content": "reply b"}, # 2 tokens
]
count_tokens = lambda m: len(m["content"].split()) # total = 16
summarize = lambda old: f"earlier conversation ({len(old)} messages)"
compact_history(messages, summarize, count_tokens, token_budget=12, keep_recent=2)
# -> [
# {"role": "system", "content": "You are a helpful assistant."},
# {"role": "system", "content": "earlier conversation (2 messages)"},
# {"role": "user", "content": "five six seven"},
# {"role": "assistant", "content": "reply b"},
# ]
# already within budget -> returned unchanged
compact_history(messages, summarize, count_tokens, token_budget=100, keep_recent=2) is messages
# -> True
Constraints
- Compact only when the total token count is strictly greater than
token_budget. - Never drop or rewrite the system message, and never rewrite the recent window — only the old middle is summarized.
- The summary is a single message
{"role": "system", "content": <summary string>}. keep_recentmay be0(summarize everything after the system message) and may be larger than the body (then nothing is old, so return unchanged).- Do not call any real model, tokenizer, or network —
summarizeandcount_tokensare provided.