Building a Tiny Agent Context Compactor in TypeScript
- Authors

- Name
- Nino
- Occupation
- Senior Tech Editor
An autonomous AI agent loop has an expensive, invisible habit: every time it makes an API call to an LLM, it sends the entire conversation history again. The system prompt, the user's request, every historic tool call, and every raw tool execution log are re-sent continuously.
A 20,000-token build log or test execution output is not paid for once. It is billed repeatedly on every subsequent turn of the agent lifecycle.
Recent research into agent architecture highlights this exact bottleneck. In September 2026, researchers published An Empirical Study of Harness Design for Coding Agents. Across 176 matched evaluation environments, they found that context management strategy dictates agent success as context windows fill up—and rule-based elision prior to LLM-driven summarization yielded the highest cost-to-accuracy efficiency.
Around the same time, teams behind frameworks like Strands Agents reported 28% lower token costs across standard benchmarks using automatic tool result capping above 1,500 tokens. Meanwhile, CliffCompaction demonstrated up to 50% cost reductions using strict content truncation without altering historical wording. Even DeepSeek's open-source Harness v0.2.1 highlighted how context trimming drastically alters agent token efficiency across Claude 3.5 Sonnet, OpenAI o3, and DeepSeek-V3 workloads.
Reducing token usage is easy—doing so without discarding the single log line containing a crucial stack trace is the real challenge.
In this tutorial, we will build a minimal, deterministic context compactor in TypeScript. It requires no external dependencies, no API keys, and no model calls.
Architectural Principle: Truth vs. View
The core design principle of an efficient agent harness separates the immutable log from the ephemeral model view:
- Full Log (Truth): An unedited, append-only append log containing every original tool output and system turn.
- Cap Layer: Truncates massive tool results into a preview snippet while preserving a reference identifier.
- Pinning Layer: Ensures failure signals (such as
FAILorERROR) survive truncation regardless of where they sit in the log. - Elision Layer: Replaces old tool results with lightweight single-line stubs once they age past a configured turn threshold.
- Drop Layer: Drops the oldest whole messages when the calculated payload exceeds 85% of the model's context window.
- Model View: The ephemeral slice passed to models like Claude 3.5 Sonnet or DeepSeek-V3 on the current turn.
Full Log (Original Truth)
↓
Cap: Large outputs converted to Preview + Ref
↓
Keep: Critical failure lines pinned to preview
↓
Elide: Tool results older than N turns replaced with stubs
↓
Drop: Evict oldest unpinned messages if > 85% window capacity
↓
Model View (Current Request Payload)
Implementation: Building compact.ts
Create a clean directory and initialize your TypeScript project:
mkdir tiny-context-compactor && cd tiny-context-compactor
npm init -y
npm install -D tsx typescript @types/node
Save the following code blocks sequentially into compact.ts.
Step 1: Types and Mocked Agent Execution
We begin by defining message structures, a lightweight token estimation heuristic (~4 characters per token), and a multi-step agent run where a coding agent attempts to fix a broken checkout test suite.
// compact.ts: A deterministic agent context compactor
type Msg = {
turn: number;
role: "system" | "user" | "assistant" | "tool";
tool?: string;
text: string
};
// Heuristic token counter (approx 4 chars per token)
const tokens = (s: string): number => Math.ceil(s.length / 4);
const lines = (n: number, f: (i: number) => string) =>
Array.from({ length: n }, (_, i) => f(i)).join("
");
const FAIL_LINE = "FAIL src/checkout.test.ts > applies 10% coupon: expected 90, received 100";
const testLog = (failAt: number | null) =>
lines(1800, (i) => (i === failAt ? FAIL_LINE : `PASS src/suite-${i % 97}.test.ts > case $\{i\} ($\{(i * 37) % 90 + 3\} ms)`));
const sourceCode = (name: string, n: number) =>
lines(n, (i) => ` const ${name}$\{i\} = applyRule(cart.items[${i % 12}], rules.$\{name\}); // line $\{i + 1\}`);
// Mocked 9-step execution history of a coding agent
const RUN: [tool: string, call: string, output: string][] = [
["list_files