How Headroom Handles Tool Outputs Differently from User Messages: SDK Source Code Deep Dive

Headroom identifies tool-result messages by their role: "tool" and structural markers, normalizes them into OpenAI-format blocks that the compression proxy can summarize, and preserves user messages unchanged unless a developer overrides the default behavior with custom hooks.

If you are building agentic applications with large language models, understanding how Headroom handles tool outputs differently from user messages is critical to preserving context window integrity. According to the chopratejas/headroom source code, the SDK treats tool results as a distinct compressible message type while shielding user messages from modification by default. This design allows large JSON payloads returned by functions to be summarized without distorting the original user intent.

How Headroom Detects Tool Messages Versus User Messages

In sdk/typescript/src/utils/format.ts, the detectFormat utility scans the message array for structural markers that separate tool traffic from user traffic. It checks for OpenAI-style tool_calls arrays, Vercel-style tool-call and tool-result parts, and—most importantly—the presence of role: "tool" together with a tool_call_id (lines 33–46).

Converting Provider Formats to OpenAI Tool-Result Blocks

Once Headroom detects a tool message, it normalizes the array into an OpenAI lingua franca before compression. In sdk/typescript/src/utils/format.ts, the toOpenAI router dispatches messages to format-specific helpers such as vercelToOpenAI and anthropicToOpenAI (lines 36–44). For a tool-result message, the helper creates a tool-result block that carries the raw JSON or string payload alongside the originating tool_call_id. This normalization ensures that the downstream proxy can treat all tool outputs as uniform text content regardless of their original provider.

Compression Proxy: Why Tool Outputs Are Summarized but User Messages Are Preserved

The actual compression happens in sdk/typescript/src/compress.ts. The compress function forwards the OpenAI-converted messages to the Headroom proxy (lines 58–62). Because the proxy receives tool results as ordinary text inside tool-result blocks, it can freely summarize or truncate large JSON payloads. In contrast, user messages retain role: "user" and are shielded by the default protective bias: the SDK does not apply the "protect-user-message" logic to role: "tool" messages. Consequently, only tool outputs are compressed while user prompts remain intact.

Preserving Tool Semantics on the Return Trip

After the proxy returns compressed OpenAI messages, sdk/typescript/src/utils/format.ts reconstructs the original provider format via fromOpenAI (lines 73–79). When it encounters a compressed tool-result block, it emits a Vercel-style tool-result part—or the equivalent Anthropic, Gemini, or other provider shape—so that downstream agent code still receives a proper tool response. This round-trip conversion guarantees that format normalization never leaks tool semantics.

Practical Example: Compressing Tool Results Without Modifying User Messages

The following TypeScript snippet demonstrates a realistic message flow that includes a user prompt, an assistant tool call, and a large tool-result payload. When you pass this array to compress, Headroom leaves the user message untouched and targets only the tool output for summarization.

import { compress, CompressionHooks } from "headroom";

const messages = [
  { role: "user", content: "Show me the latest sales numbers." },
  {
    role: "assistant",
    content: "",
    tool_calls: [
      {
        id: "tc_1",
        type: "function",
        function: { name: "fetch_sales", arguments: "{}" },
      },
    ],
  },
  {
    role: "tool",
    content: JSON.stringify({ jan: 1200, feb: 1350, mar: 1420 }),
    tool_call_id: "tc_1",
  },
];

compress(messages, {
  hooks: new CompressionHooks(),
  tokenBudget: 500,
}).then((result) => {
  console.log(result.messages);
});

When you run this code, Headroom executes four distinct steps:

  1. detectFormat recognizes the mixed message flow and identifies the tool-result payload.
  2. toOpenAI converts the tool result into a standardized tool-result block that includes the tool_call_id.
  3. The Headroom proxy summarizes the large JSON payload because tool blocks are treated as ordinary compressible text.
  4. fromOpenAI restores the shortened payload back into a Vercel-style tool-result part for downstream consumption.

For a production-ready illustration, see sdk/typescript/examples/tool-calling-agent.ts, which shows an agent where tool results are automatically compressed during active conversation turns.

Customizing Tool Output Handling with CompressionHooks

While the default behavior compresses tool outputs and protects user messages, developers can override this logic through hooks. In sdk/typescript/src/hooks.ts, the extractToolCalls helper records every tool name that appears—whether from role: "tool" messages or tool-call parts—around lines 95–101. You can feed this metadata into a custom CompressionHooks instance to apply per-tool bias, skip summarization for specific functions, or run post-processing on selected payloads.

Summary

  • Role detection – In sdk/typescript/src/utils/format.ts, detectFormat identifies tool messages via role: "tool", tool_call_id, and provider-specific structural markers.
  • OpenAI normalizationtoOpenAI converts tool results into uniform tool-result blocks so the proxy can process them as plain text.
  • Selective compressioncompress in sdk/typescript/src/compress.ts subjects tool outputs to summarization while leaving role: "user" messages unchanged.
  • Format restorationfromOpenAI rebuilds the native provider format after compression, emitting proper tool-result parts for downstream compatibility.
  • Hook extensibilityextractToolCalls in sdk/typescript/src/hooks.ts exposes tool metadata for custom bias and conditional processing.

Frequently Asked Questions

Does Headroom compress user messages by default?

No. Headroom's default CompressionHooks apply a protective bias to user messages, leaving them intact during a compression run. Only tool-result payloads are treated as freely compressible text unless you explicitly customize the hook behavior.

How does Headroom detect a tool result across different LLM providers?

The detectFormat utility in sdk/typescript/src/utils/format.ts inspects the message array for provider-specific markers. These include OpenAI's tool_calls arrays, Vercel's tool-call and tool-result parts, and the role: "tool" field paired with a tool_call_id.

Can developers prevent specific tool outputs from being summarized?

Yes. You can supply custom hooks to the compress function. The extractToolCalls helper in sdk/typescript/src/hooks.ts surfaces every tool name that appears in the conversation, enabling you to write conditional logic that protects selected tool results or applies specialized post-processing.

What happens to the tool_call_id during the compression pipeline?

The tool_call_id is preserved throughout the round trip. toOpenAI embeds it into the normalized tool-result block, and fromOpenAI carries it back into the provider-native format so the link between the assistant's tool request and the tool's response remains intact.

Have a question about this repo?

These articles cover the highlights, but your codebase questions are specific. Give your agent direct access to the source. Share this with your agent to get started:

Share the following with your agent to get started:
curl -s "https://instagit.com/install.md"

Works with
Claude Codex Cursor VS Code OpenClaw Any MCP Client

Maintain an open-source project? Get it listed too →