- 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
Compact a harness session
A harness session can run for days. Its transcript grows until a model call passes the context limit. Compaction shrinks what the model sees. In a durable harness, the result must also survive a restart. A session that opens again must see the same context as before.
Set durable: true. Compaction then writes a tanstack.compaction record into the session log. The host folds that record into the messages with projectCompaction, live and when the session opens again.
Add compaction to a harness#
- Create the compaction middleware with durable: true.
- Add it to middleware in defineHarness.
- Give the harness projectCompaction in its durability.addRecordsToModelContext option.
import { createHarnessHost, defineHarness } from '@tanstack/ai-harness'
import { memoryPersistence } from '@tanstack/ai-persistence'
import { openaiText } from '@tanstack/ai-openai'
import {
conversationSummarizer,
projectCompaction,
summarizeOldest,
withCompaction,
} from '@tanstack/ai-compaction'
const compaction = withCompaction({
maxTokens: 180_000,
contextWindow: 200_000,
countTokens: 'usage',
durable: true,
strategy: summarizeOldest({
cut: 'turn',
keepRecentTokens: 8_000,
summarize: conversationSummarizer({ adapter: openaiText('gpt-6.1-sol') }),
}),
})
const assistant = defineHarness({
name: 'acme/assistant',
adapter: openaiText('gpt-6.1-sol'),
middleware: [compaction],
durability: { addRecordsToModelContext: { version: 'v1', record: projectCompaction } },
})
const host = createHarnessHost({ persistence: memoryPersistence() })- Compaction runs before a model call when the count passes maxTokens. If the usage of the last model call of a turn passed maxTokens or contextWindow, compaction also runs right after that call. This check needs countTokens: 'usage'.
- The log only grows, so the original messages stay in the store. The fold that the model gets starts with the summary.
- Readers still get every message. session.transcript(), GET transcript, and the chat hydration show the full history, not the summary.
- A thread can change its model. To give each model its own limit, pass a function, like maxTokens: ({ model }) => .... See A limit for each model.
Prepare the summary in the background#
A summary call takes time. When compaction runs at maxTokens, the user waits for that call before the model answers. Set background to prepare the summary earlier, while the session goes on:
const earlyCompaction = withCompaction({
maxTokens: 180_000,
contextWindow: 200_000,
countTokens: 'usage',
durable: true,
background: { atTokens: 150_000 },
strategy: summarizeOldest({
cut: 'turn',
keepRecentTokens: 8_000,
summarize: conversationSummarizer({ adapter: openaiText('gpt-6.1-sol') }),
}),
})- atTokens must be below maxTokens. If it is not, withCompaction throws.
- When the count at a model call passes atTokens, the summary starts beside the turn. The turn does not wait for it.
- The summary applies at the first model call of the next run, never in the middle of a run. It goes into the log as a tanstack.compaction record with reason: 'background'.
- A model call over maxTokens while the summary runs waits for it and applies it. It compacts inline only when the messages are still over maxTokens.
- If you cancel the run during that wait, the wait stops. The summary continues and applies at the next run.
Sometimes a summary does not apply:
- A newer compaction cut past the message that the summary keeps. The summary is dropped. onCompact and compaction:ended report it with stale: true.
- The next run calls another model than the one that started the summary. The summary is dropped and reported with stale: true.
- The summary call failed. Nothing applies. onCompact and compaction:ended report it with error. The next model call past atTokens starts a new summary.
Fold your own records too#
If your harness already folds records of its own, call projectCompaction first:
import type { ProjectOptions } from '@tanstack/ai-harness'
const project: ProjectOptions = {
version: 'v2',
record: (input) => {
const compacted = projectCompaction(input)
if (compacted) return compacted
const { record, messages } = input
if (record.type === 'app.signal' && typeof record.text === 'string') {
return [...messages, { role: 'user', content: record.text }]
}
return undefined
},
}Change version when you change the function. A log snapshot with another version is not used, so the host folds the log again from the first record.
Retry after a context overflow#
A model call can still fail because the context is too long. withCompaction handles it for you. When a call fails with CONTEXT_OVERFLOW_CODE ('context_overflow'), the middleware compacts and calls the model again, inside the chat() call of the turn. That compaction also writes its record to the log.
- It retries once per model call. This also works with auto: false.
- turn.onModelError sees the overflow only when the compacted call is still too long. Then error.code is 'context_overflow', and the turn fails unless your hook returns 'retry'.
- To compact the next call yourself, for example before you switch the thread to a model with a smaller context window, call compaction.compactNext(session.threadId).
For a strategy that always finds something to cut on the retry, see Give the retry something to cut.
What you have now#
- A session that compacts before it passes the context limit, counted with the real usage of each call.
- The same context after a restart, because the compaction is in the log.
- With background, a summary that is ready before the session reaches maxTokens.
- One retry after an overflow, with a compaction first, and no code of your own.