- MCP Server ToolsUpdated
- Managed MCP with chat()Updated
- Manual MCPUpdated
- MCP Type GenerationUpdated
- MCP Client InputUpdated
- MCP Server Tools: AdvancedNew
- MCP EventsNew
- MCP AppsUpdated
- Serve Tools over HTTPUpdated
- Serve Resources and PromptsUpdated
- Ask for InputUpdated
- Draft Text from a ToolUpdated
- MCP Server TasksUpdated
- MCP Server AuthUpdated
- MCP Server SessionsUpdated
- MCP Server on stdioUpdated
Persistence (Advanced)
- GenerationsUpdated
- Realtime Voice ChatUpdated
- Text-to-SpeechUpdated
- TranscriptionUpdated
- Transcription: AdvancedNew
- Voice CreationUpdated
- Audio RecordingUpdated
- Audio GenerationUpdated
- Image GenerationUpdated
- Image Generation: AdvancedNew
- Video GenerationUpdated
- Video Generation: AdvancedNew
- Live GenerationUpdated
- World GenerationUpdated
- Generation HooksUpdated
- Build your first harnessUpdated
- Host optionsNew
- Send and show mediaUpdated
- Connect clientsUpdated
- Write a client in any languageNew
- Access and tenantsNew
- Run in the terminalUpdated
- Deploy a harnessUpdated
- Send inputs safelyUpdated
- Run a tool without a model
- Durable sessionsUpdated
- Run background work on workersNew
- List, rename, and fork sessionsUpdated
- Lifecycle and retentionNew
- Run side effects onceUpdated
- Wait for a personNew
- Build on the session logUpdated
- Control how a turn endsUpdated
- Store settings per threadUpdated
- Compact a long sessionUpdated
- Fork and reset a threadUpdated
- One log for a session treeUpdated
- Write a pluginUpdated
- Track usage and costUpdated
- Ask before risky tool callsUpdated
- Give your agent coding toolsUpdated
- Sandbox and extend the coding toolsUpdated
- Run the coding tools for many threadsNew
- Undo a turnUpdated
- Build a coding agentUpdated
- Delegate to coding agentsUpdated
- Switch between agent profilesUpdated
- Auth and connectorsUpdated
- Share a thread between peopleUpdated
- Connect model providersUpdated
- Build your own UIUpdated
- Code mode in a harnessUpdated
- Work until a goal is metUpdated
- Run agents from a harnessUpdated
- Hand off a conversationNew
- Add skillsUpdated
- Use MCP serversUpdated
- Give your agent a computerNew
- Use from any MCP clientUpdated
- Use a harness from an editor (ACP)Updated
- OverviewUpdated
- Quick StartUpdated
- ProvidersUpdated
- HarnessesUpdated
- Harness AuthUpdated
- WorkspaceUpdated
- ToolsUpdated
- PolicyUpdated
- Lifecycle & SnapshotsUpdated
- Portable SnapshotsUpdated
- Keep Files After ReloadUpdated
- Save a Named VersionUpdated
- Branch From a VersionUpdated
- Send a Frozen FileUpdated
- Let the Agent Save and ForkUpdated
- Pick Which Files to KeepUpdated
- What a Snapshot StoresUpdated
- Instance DurabilityUpdated
- Durable RunsUpdated
- EventsUpdated
Sandboxes (Advanced)
- Runtime ContextUpdated
- Bring Your Own KeyUpdated
- Keep Keys on the ServerNew
- Debug LoggingUpdated
- Multimodal ContentUpdated
- Files APIUpdated
- Per-Model Type SafetyUpdated
- Runtime Adapter SwitchingUpdated
- Tree-ShakingUpdated
- Extend AdapterUpdated
- Extend Adapter: AdvancedNew
- Typed Pre-Configured OptionsUpdated
- Test with a Fake ModelUpdated
- Prompt CachingUpdated
- Mid-Conversation ChangesUpdated
- Approval Flow ProcessingUpdated
Compaction
Compaction shortens a transcript before it passes the model context limit. A long chat or agent loop otherwise fails on the next call.
Add withCompaction to your chat() call. It runs before each model call.
Quick setup#
1. Install the package#
npm i @tanstack/ai-compactionpnpm add @tanstack/ai-compactionyarn add @tanstack/ai-compactionbun add @tanstack/ai-compaction2. Add the middleware to your chat endpoint#
import {
chat,
chatParamsFromRequest,
toServerSentEventsResponse,
} from "@tanstack/ai";
import { openaiText } from "@tanstack/ai-openai";
import { withCompaction } from "@tanstack/ai-compaction";
export async function POST(request: Request) {
const { messages, threadId, runId } = await chatParamsFromRequest(request);
const stream = chat({
adapter: openaiText("gpt-6.1-sol"),
messages,
threadId,
runId,
middleware: [withCompaction({ maxTokens: 100_000 })],
});
return toServerSentEventsResponse(stream);
}The client does not change. Your useChat code stays the same.
3. Watch it run#
Use a small maxTokens and an onCompact log while you try it:
import { withCompaction } from "@tanstack/ai-compaction";
const compaction = withCompaction({
maxTokens: 2_000,
onCompact: (info) => {
console.log(`Compacted from ${info.before} to ${info.after} tokens`);
},
});Put compaction in the middleware array and chat for a few turns. The server logs a line each time the history shrinks, and the chat keeps answering.
How it works#
- withCompaction is a chat middleware. Before each model call, it estimates the token count of the messages (characters / 4).
- When the count passes maxTokens, a strategy rewrites the messages that go to the provider.
- The full transcript does not change. Persistence and the UI still see every message.
- The system prompt is never dropped. chat() keeps it separate from messages.
- Tool calls stay paired with their results, so the request stays valid.
Pick a strategy#
Pass strategy to change how the history shrinks. Three are built in:
| Strategy | What it does | Cost |
|---|---|---|
| evictOldest (default) | Drop the oldest messages and leave a marker | No extra model call |
| summarizeOldest | Replace the oldest messages with a summary from a model | One summary call (up to two with cut: 'turn') |
| clearToolResults | Replace the content of old tool results with a stub, keep the messages | No extra model call |
evictOldest#
This strategy is the cheapest. It keeps the recent messages, drops the older ones, and puts a short marker in their place. It is the default, so name it only to set keepRecentTokens:
import { withCompaction, evictOldest } from "@tanstack/ai-compaction";
withCompaction({
maxTokens: 100_000,
strategy: evictOldest({ keepRecentTokens: 40_000 }),
});summarizeOldest#
This strategy keeps the meaning of old turns and costs one summary call. Give it a summarize function. The function gets the messages to drop and an input with the threadId of the conversation. It returns the summary. Give the summary call a thread id of its own, so it does not share state with the conversation:
import {
chat,
chatParamsFromRequest,
summarize,
toServerSentEventsResponse,
} from "@tanstack/ai";
import type { ModelMessage } from "@tanstack/ai";
import { openaiSummarize, openaiText } from "@tanstack/ai-openai";
import {
summarizeOldest,
withCompaction,
type SummarizeInput,
} from "@tanstack/ai-compaction";
async function summarizeHistory(
messages: Array<ModelMessage>,
{ threadId }: SummarizeInput,
) {
const text = messages
.map((m) => `${m.role}: ${typeof m.content === "string" ? m.content : ""}`)
.join("\n");
// Return the whole result, so compaction can report its usage.
return summarize({
adapter: openaiSummarize("gpt-6.1-sol"),
text,
threadId: `${threadId}:compaction`,
});
}
export async function POST(request: Request) {
const { messages, threadId, runId } = await chatParamsFromRequest(request);
const stream = chat({
adapter: openaiText("gpt-6.1-sol"),
messages,
threadId,
runId,
middleware: [
withCompaction({
maxTokens: 100_000,
strategy: summarizeOldest({ summarize: summarizeHistory }),
}),
],
});
return toServerSentEventsResponse(stream);
}summarize can return the summary text, or an object with summary and usage. Return the summarize() result as it is, so that compaction can report the cost of the summary.
For a coding agent, use the ready summarizer in Better summaries.
clearToolResults#
This strategy is the best fit for agent loops. Tool output (file reads, command output) is usually most of the tokens. The strategy replaces the content of old tool results with a stub. Every message and every tool-call pair stays in place.
import { withCompaction, clearToolResults } from "@tanstack/ai-compaction";
withCompaction({
maxTokens: 100_000,
// Keep the 5 most recent tool results in full. Stub the older ones.
strategy: clearToolResults({ keepRecentToolResults: 5 }),
});Combine strategies#
composeStrategies runs several strategies in order. It stops as soon as the result is under maxTokens. Put the cheap, targeted strategy first and a broad fallback last.
This example clears old tool output first. It drops old messages only when that is not enough:
import {
withCompaction,
composeStrategies,
clearToolResults,
evictOldest,
} from "@tanstack/ai-compaction";
withCompaction({
maxTokens: 100_000,
strategy: composeStrategies(clearToolResults(), evictOldest()),
});Let the provider compact#
Some providers can compact the conversation on their own servers. The model then keeps more of the old turns than a marker gives. It costs one compaction call.
Pass the adapter of the chat() call as native. When the count passes maxTokens, the compact method of the adapter runs in place of strategy:
import {
chat,
chatParamsFromRequest,
toServerSentEventsResponse,
} from "@tanstack/ai";
import { openaiText } from "@tanstack/ai-openai";
import { withCompaction } from "@tanstack/ai-compaction";
// Create the adapter once, outside the request handler.
const adapter = openaiText("gpt-6.1-sol");
export async function POST(request: Request) {
const { messages, threadId, runId } = await chatParamsFromRequest(request);
const stream = chat({
adapter,
messages,
threadId,
runId,
middleware: [withCompaction({ maxTokens: 100_000, native: adapter })],
});
return toServerSentEventsResponse(stream);
}- An adapter with no compact method uses strategy.
- When compact fails, strategy runs in its place.
The OpenAI Responses adapters have compact. See Native compaction. For what happens on each kind of failure, see Native compaction failures.
When the chat is still too long#
The token count is an estimate. So sometimes a call is still too long for the model, and the provider rejects it.
You do not have to catch that error. When a call fails with CONTEXT_OVERFLOW_CODE, withCompaction compacts and calls the model again, in the same run. Your code only sees the answer. This also works with auto: false.
It retries once for each model call. If the compacted call is still too long, the run ends with a RUN_ERROR:
- code is CONTEXT_OVERFLOW_CODE ('context_overflow').
- message is the text from the provider.
These adapters set this code:
- OpenAI, Anthropic, Gemini, and Bedrock.
- OpenRouter, Grok, Cloudflare, and BytePlus.
- Other OpenAI-compatible adapters, when the provider sends context_length_exceeded.
To make sure that the retry always has something to drop, see Give the retry something to cut.
Compaction and persistence#
Compaction and server-side withPersistence use two views of the messages:
- messages is the full transcript. Persistence saves this view.
- providerMessages is the context for the model. Compaction rewrites this view.
The order of the middleware does not change this split. Dropped, summarized, and stubbed content stays in the message store.
If the persistence adapter has a metadata store, compaction also saves a small checkpoint. The next request then starts from the last compacted result. See Checkpoints for the details.
Advanced#
Compaction: Advanced has the deep details:
- Every option of withCompaction and of each strategy.
- Your own strategy, and compaction on your own schedule.
- Counts from the real usage of the provider, and summaries that run in the background.
- DevTools events, checkpoints, and session logs.