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

> Discover how Headroom differentiates tool outputs from user messages. Explore SDK source code to understand its unique handling of roles and structural markers for efficient processing.

- Repository: [Tejas Chopra/headroom](https://github.com/chopratejas/headroom)
- Tags: deep-dive
- Published: 2026-06-05

---

**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`](https://github.com/chopratejas/headroom/blob/main/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`](https://github.com/chopratejas/headroom/blob/main/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`](https://github.com/chopratejas/headroom/blob/main/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`](https://github.com/chopratejas/headroom/blob/main/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.

```typescript
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`](https://github.com/chopratejas/headroom/blob/main/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`](https://github.com/chopratejas/headroom/blob/main/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`](https://github.com/chopratejas/headroom/blob/main/sdk/typescript/src/utils/format.ts), `detectFormat` identifies tool messages via `role: "tool"`, `tool_call_id`, and provider-specific structural markers.
- **OpenAI normalization** – `toOpenAI` converts tool results into uniform `tool-result` blocks so the proxy can process them as plain text.
- **Selective compression** – `compress` in [`sdk/typescript/src/compress.ts`](https://github.com/chopratejas/headroom/blob/main/sdk/typescript/src/compress.ts) subjects tool outputs to summarization while leaving `role: "user"` messages unchanged.
- **Format restoration** – `fromOpenAI` rebuilds the native provider format after compression, emitting proper `tool-result` parts for downstream compatibility.
- **Hook extensibility** – `extractToolCalls` in [`sdk/typescript/src/hooks.ts`](https://github.com/chopratejas/headroom/blob/main/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`](https://github.com/chopratejas/headroom/blob/main/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`](https://github.com/chopratejas/headroom/blob/main/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.