◉CLAUDE LABJP
●2.1.301 — Adds the vim % motion, LaTeX math rendering in the VS Code panel, and a retry-watchdog limit for usage-limit waits (10/15)●2.1.299 — Conversations over 200K tokens left idle about 54 minutes get compacted; MCP allow rules now match only the server they name●SONNET 4.5 — Retires on the Claude API on 11/30, 45 days left. Migrate to Sonnet 5.5●WINDOWS — Reports say hooks break silently only on Japanese-locale Windows; check the character encoding first●FAST MODE — Opus 5.5 fast mode is out of research preview and no longer needs the beta header (10/15)●NEW — I Put a Ten-Line Promise File at the To…●2.1.301 — Adds the vim % motion, LaTeX math rendering in the VS Code panel, and a retry-watchdog limit for usage-limit waits (10/15)●2.1.299 — Conversations over 200K tokens left idle about 54 minutes get compacted; MCP allow rules now match only the server they name●SONNET 4.5 — Retires on the Claude API on 11/30, 45 days left. Migrate to Sonnet 5.5●WINDOWS — Reports say hooks break silently only on Japanese-locale Windows; check the character encoding first●FAST MODE — Opus 5.5 fast mode is out of research preview and no longer needs the beta header (10/15)●NEW — I Put a Ten-Line Promise File at the To…
Articles/API & SDK
⬡ API & SDK/2026-05-20Advanced

Compressing Tool Results in Claude Agents — Aggregating Large Responses Without Bloating Context

When a database returns 8,000 rows, a scrape returns 200KB of HTML, or a file read returns several megabytes, dropping the raw payload into your Claude tool result wrecks both cost and quality. This guide presents a three-layer compression architecture — schema projection, summarization, and reference handles — with TypeScript examples from a production agent pipeline.

claude-agenttool-use24context-management5mcp20production111

✦ Premium Article

What happens when your database tool returns 8,000 rows in a single call, or your web fetch tool drops 200KB of HTML into the conversation? In my own multi-site auto-posting pipeline as an indie developer running six sites alongside an app business that has crossed 50 million cumulative downloads, this was the first wall I hit. A single tool call would consume more than 60,000 tokens, get called three or four times per loop, and saturate the context window before the agent could finish its job. Cost went up, but more painfully, output quality went down.

Tool result compression is a quiet but decisive part of agent design that compounds as you run agents longer. Anthropic's official docs do a great job explaining how to define tools, but the question of how to constrain the return side is essentially left to the implementer. Here I want to share the three-layer architecture I run in production for content generation, content auditing, and revenue analysis agents — with TypeScript samples you can adapt.

Three failure modes that bloated tool results trigger

The reasoning "the context window is bigger now, so raw payloads are fine" is wrong on both cost and quality grounds. Three things degrade at once:

  1. Direct token cost: Claude Sonnet 4.6 at $3 per million input tokens, multiplied by 60,000 tokens × 4 tool calls = $0.72 per request. A pipeline doing 200 such requests per day burns roughly $4,300 per month in pure overhead
  2. Instruction-following degradation: The longer the input, the weaker the relative weight of your system prompt and instructions. In my own runs, the rate of style-guide violations roughly doubles once cumulative tool results pass 30,000 tokens
  3. Cache efficiency collapse: With prompt caching enabled, tool results that change every call destroy your cache-hit rate, dragging it down in proportion to how many tools you call

So compression is not just a cost-optimization concern. It is a reliability-level architectural decision for the whole agent.

The three-layer compression model

The taxonomy I use in production looks like this. The top layer is safest (least information loss), the bottom layer is the most powerful (highest compression ratio). In practice I pick one layer per tool, and sometimes stack two.

Layer 1: Schema projection (return only the fields that matter)

The least lossy form of compression is to never include unnecessary fields. A Stripe Charge object has 50+ fields, but an agent doing spend analysis usually only needs id, amount, currency, created, and description.

async function searchCharges(query: SearchQuery) {
  const charges = await stripe.charges.search({
    query: query.text,
    limit: 100,
  });
 
  // 50+ fields × 100 rows ~= 100k+ tokens → 5 fields × 100 rows ~= 10k tokens
  return {
    data: charges.data.map((c) => ({
      id: c.id,
      amount: c.amount,
      currency: c.currency,
      created: c.created,
      description: c.description,
    })),
    has_more: charges.has_more,
    next_cursor: charges.data[charges.data.length - 1]?.id,
  };
}

Projection alone usually buys 70–90% token reduction. When you write MCP servers that wrap external APIs, ask yourself which fields exist for human dashboard consumption vs. which fields the agent actually uses to decide. That distinction lands you in this layer naturally.

Layer 2: Summarization (have Haiku digest the payload first)

Some tools return free-form text that schema projection cannot squeeze. Web search, file reads, log retrieval — all of these. The pattern that works best for me is a cascade where Haiku compresses the payload before it ever reaches the Sonnet/Opus loop.

import Anthropic from "@anthropic-ai/sdk";
const client = new Anthropic();
 
async function fetchAndSummarize(url: string, focus: string) {
  const html = await fetch(url).then((r) => r.text());
  const text = stripHtml(html).slice(0, 50_000); // about 25,000 tokens
 
  // 5,000-token input → 400-token summary
  const summary = await client.messages.create({
    model: "claude-haiku-4-5-20251001",
    max_tokens: 400,
    system: `Summarize the following web page. Focus angle: ${focus}.
Facts only. No speculation. Keep all important numbers verbatim.`,
    messages: [{ role: "user", content: text }],
  });
 
  return {
    url,
    focus,
    summary: summary.content[0].type === "text" ? summary.content[0].text : "",
    original_length: text.length,
  };
}

The critical detail is passing the focus as a tool parameter. When Claude itself declares what it wants to know at the moment of calling, Haiku produces summaries aligned with the caller's intent, which dramatically reduces follow-up turns.

This mirrors a pattern Dolice (my indie app studio operating since 2014) has used on the ad-monetization side: a cheap layer screens, an expensive layer commits. The same economic logic applies to agent design — a Haiku cascade in front of Sonnet/Opus is the AdMob mediation of LLM orchestration.

Layer 3: Reference handles (return only an ID, not the data)

When the agent doesn't need to read the payload, only act on it, the strongest move is to make the tool return "saved, here is the handle" and let Claude drill down via separate tools.

const blobStore = new Map<string, unknown>();
 
async function executeQuery(sql: string) {
  const rows = await db.query(sql);
  const handle = `query-${crypto.randomUUID()}`;
  blobStore.set(handle, rows);
 
  // Don't return thousands of rows; return the handle plus a tiny preview
  return {
    handle,
    row_count: rows.length,
    sample: rows.slice(0, 3),
    columns: rows[0] ? Object.keys(rows[0]) : [],
    note: "use `query_result_aggregate` or `query_result_filter` with the handle to drill down",
  };
}
 
async function queryResultAggregate(handle: string, groupBy: string, agg: string) {
  const rows = (blobStore.get(handle) ?? []) as Record<string, unknown>[];
  return aggregate(rows, groupBy, agg);
}

The reference-handle pattern positions Claude as an analyst issuing aggregation directives rather than someone scrolling the raw data. In production I back blobStore with Cloudflare KV or Durable Objects with a one-hour TTL.

✦

Thank you for reading this far.

Continue Reading

What follows includes implementation code, benchmarks, and practical content we hope you'll find useful. This site runs without ads — server and development costs are supported entirely by members like you. If it's been helpful, we'd be truly grateful for your support.

WHAT YOU'LL LEARN
✦Quantify the three failure modes that bloated tool results cause — token cost, instruction-following degradation, and cache breakdown
✦Adopt a three-layer compression model (projection / summarization / reference handle) with TypeScript code you can paste in
✦Walk away with rules of thumb for choosing the right compression layer per tool, plus production metrics to track
Secure payment via Stripe · Cancel anytime
✦

Unlock This Article

Get full access to the rest of this article. Buy once, read anytime. This site is ad-free — your support goes directly toward keeping it running.

or
Unlock all articles with Membership →
Share

Thank You for Reading

Claude Lab is ad-free, supported entirely by members like you. We publish practical guides daily with implementation code, benchmarks, and production-ready patterns. If you've found it useful, we'd love to have you on board.

  • ✦Copy-paste ready implementation code
  • ✦New advanced guides published daily
  • ✦$5/mo or $10 for lifetime access
View Membership →

Related Articles

⬡ API & SDK2026-07-28
The Line That Disappears at 100K: Measuring What Tool-Output Spill Actually Keeps
When agent tool output passes 100,000 characters, the full text spills to a file and the model sees only a head-truncated preview. Here are measured survival rates from a real repository, and the output envelope I built to push decision-relevant lines to the front.
⬡ API & SDK2026-05-26
Stabilizing Claude API Structured Responses in Production — Notes on tool_use, JSON Schema, and Layered Validation
Getting Claude to return JSON takes a few lines. Keeping that JSON usable in production is a different problem. Here is the layered design I landed on after running a wallpaper classification pipeline through Claude API, built around tool_use, JSON Schema, and domain validation.
⬡ API & SDK2026-05-22
Why tool_result could not be submitted Keeps Coming Back, and How to Build a Recovery Handler That Actually Holds
Run a Claude agent long enough and one day it starts: 'tool_result could not be submitted', back to back, and retries change nothing. The error message hides four completely different root causes. Here is what I learned debugging it on the always-on agent jobs I run as an indie developer, with the TypeScript recovery handler I now ship in production.