Skip to main content
A long coding or research session fills the context window with old tool calls. Drop them and the agent forgets what it learned: which files it read, which approaches failed, which facts it confirmed. This recipe replaces the old part of the history with a summary from ScaleDown Summarize and keeps the recent turns word for word, so the agent remembers what it learned in a fraction of the tokens. Summarize does one job: it reads one block of text and returns a summary. One call over a whole history drops facts, so a long history gets summarized in chunks. Goal: Write a maybeCompact function. While the history is under budget, it returns it unchanged. Once it’s over, it returns the system prompt, one summary message, and the most recent turns. Outcome: The history stays under budget however many times it’s compacted. Facts from the summarized turns can still be recalled afterwards. The kept turns never start with an orphaned tool result, so the next model call isn’t rejected, and a failed summarize call leaves the history as it was.
Prerequisites:
  • Your existing TypeScript agent loop runs on Node.js 20+ or Bun as an ES module, with @types/node or @types/bun installed, and keeps its message history in an array you can rewrite between steps.
  • You’ve configured OPENROUTER_API_KEY and installed zod 4.

1. Call Summarize

summarize sends your instructions as the system message and the text as the user message, and returns the plain-text summary with its usage. The instructions tell the model what to keep: the goal, the decisions made and why, the files and commands the agent touched, the approaches that failed, and the facts it confirmed. They also tell it to copy names exactly, such as file paths, table names, and ticket IDs, because our summaries lost many of them otherwise. Longer histories get split into chunks before they reach summarize, so each summary stays short and focused.
Each summarize call is one paid request billed to your OpenRouter account at the scaledown/summarize rate. In our run, compacting an 82k-character history took six calls and cost $0.00087.

2. Turn old messages into text

Summarize reads one block of text and rejects tool messages with a 400, so turn the old messages into text first. Keep the tool calls and their results, since that’s where most of what the agent learned lives. The message type below is the OpenAI chat format; swap in your own.

3. Summarize in chunks

One call can’t hold everything in a long history, so it drops facts. Split the old turns into chunks of about 16k characters (4k to 5k tokens), summarize each chunk, and join the results. Each chunk gets its own summary, and the calls run in parallel.

4. Compact when the history gets big

maybeCompact keeps the system prompt and the last KEEP_RECENT turns, and replaces everything in between with one summary message. Three details keep it safe over a long run:
  • Cut between whole turns. If the kept turns started with a tool result whose tool call had been summarized away, the next model call would be rejected. So turnBoundary moves the cut back until the kept turns start with a user or assistant message. If that reaches the first turn, which happens when one assistant turn made more than KEEP_RECENT tool calls, it moves forward instead, so that whole batch gets summarized.
  • Carry the summary forward. On the next compaction, the earlier summary is kept and the new one is added after it. Once the combined summary passes SUMMARY_MAX_CHARS, it’s summarized again in FOLD_WINDOW_CHARS chunks. One call over the whole summary would drop facts the same way one call over the whole history does.
  • Keep the full history if a call fails. On any error, maybeCompact logs it and returns the history unchanged. The run carries on with a bigger context instead of a gap in its memory.
Call it at the start of each loop step. This sketch isn’t part of the runnable file below; agentStep stands for your own loop body:
Put anything the agent must never lose, like the user’s original task and its constraints, in the system prompt. The system prompt is never summarized.

Worked example

The session below is made up, but it’s the one the captured output came from, so you can run it yourself. syntheticSession builds a 100-step coding session (202 messages, 86k characters) in which the agent reads one file per step, with ten facts planted along the way to check what survives: a config path, a failed approach, a rate limit, a table name, a ticket ID, a branch name, a test command, the root cause, a reviewer, and an environment variable. Save steps 1 to 4, without the loop sketch, and these two blocks as one file and run it with bun run compact.ts.
Captured output. The first compaction’s log line from maybeCompact, and the first two of the six parts of its summary:
If your summaries stop mid-sentence, lower CHUNK_CHARS. We then appended syntheticSession(80, 200).slice(2), 80 more steps, and compacted again, then once more after another 80 (syntheticSession(80, 400).slice(2)). The second compaction carried the first summary forward and grew it to 11.8k characters, just under SUMMARY_MAX_CHARS. The third crossed the limit, so the carried summary was folded as well: 11 calls instead of 5, and the summary came back at 10.5k to 10.8k characters, since each 4k window returned about 1.7k. Facts after the third compaction: 8 of 10 in one run, 10 of 10 in the other. Folding re-summarizes a summary, which is where facts go missing, so keep the limit as high as your context allows. With SUMMARY_MAX_CHARS lowered to 6_000, the fold ran on both the second and third compaction (8 and 9 calls), the summary stayed between 2.8k and 4.8k characters, and 10 of 10 facts survived in that run, so a fold is a risk, not a certain loss.

Check your work

  • Shape. After compaction, the history is your system prompt, then a user message starting with SUMMARY_PREFIX, then a user or assistant message, never a tool result.
  • Budget. historySize after compaction is well under COMPACT_AT_CHARS: 7k against 50k in our run.
  • Memory. Ask the agent about something that only appears in the summarized turns, like a file it read or an approach that failed. It should answer correctly. Our summaries kept 9 or 10 of the 10 hidden facts, so expect to lose one now and then. Keep anything that must never be lost in your own state, not in the summary.
  • Repeat. Compact twice more and check which early facts survive. Ours kept 8 to 10 of 10 after the third compaction, the one that folds the summary.
  • Fail safe. Break the API key. maybeCompact logs the error and returns the history unchanged.

Variations

With different instructions and no chunking, the same summarize call can turn a meeting transcript into decisions and open questions, condense a support ticket’s history before an agent picks it up, or boil a long email thread down to one paragraph. Once the input is longer than a few thousand tokens, chunk it as in step 3.

Next steps