Context Management
Long agent runs pile up tool results, model replies and subagent messages. Left alone, that history can overflow the model's context window and cause errors or truncation. B4.run keeps it in check two ways:
- Tool-output offloading is on by default.
- Summarization is off until you turn it on.
Tool-output offloadingCopy link to section: Tool-output offloading
When a tool returns a large result, B4.run writes the full output to workspace/tool-outputs/ and puts a short stub in the context. The stub has a few preview lines (you choose how many) and a file path. When the agent needs the full content, it calls readFile with that path, and the content goes into the next model turn.
Offloading turns on as soon as a workspace/ directory exists at the app root. There's nothing else to configure.
What the model seesCopy link to section: What the model sees
The stub looks like this:
[Tool output offloaded — 12,345 chars exceeded the 1,500-char limit.
Full output saved to: tool-outputs/abc123.txt
Preview (first 10 lines):
line 1 …
line 2 …
…
Read the full output with the readFile tool at the path above.]The model can work from the preview or call readFile for the rest. The full content stays on disk, so it's there on later turns without using context tokens.
ConfigurationCopy link to section: Configuration
Here are the toolOutput options with their defaults:
export default {
toolOutput: {
offloadThresholdChars: 40000, // default
previewLines: 10, // default
maxBytes: 268435456, // default (256 MB)
ttlMs: 10800000, // default (3 h)
gcThrottleMs: 10000, // default (10 s)
noOffloadTools: [], // merged with built-in exempt set
},
}| Key | Type | Default | Description |
|---|---|---|---|
offloadThresholdChars | number | 40000 | Offload results longer than this many characters (serialized). |
previewLines | number | 10 | Lines kept in the stub. |
maxBytes | number | 268435456 | Total bytes kept in workspace/tool-outputs/. Past that, the oldest files go first. |
ttlMs | number | 10800000 | Delete offloaded files older than this (3 hours). |
gcThrottleMs | number | 10000 | Minimum time between cleanup passes (10 seconds). |
noOffloadTools | string[] | [] | More tools to never offload, on top of readFile and listDir. |
Offloading needs a filesystem, so the build turns toolOutput off for the hono edge target. A runtime with no filesystem fails each request with B4_E1005 rather than silently ignoring the settings. Summarization needs no filesystem and runs on every target.
ExemptionsCopy link to section: Exemptions
readFile and listDir are never offloaded, so the agent can read an offloaded file back without it getting offloaded again. Use noOffloadTools to exempt more tools, such as one that already returns a short summary.
Garbage collectionCopy link to section: Garbage collection
At most once per gcThrottleMs, B4.run runs a cleanup pass in the background. It deletes files older than ttlMs. If the total is still over maxBytes, it deletes the oldest files until it fits. Agent turns never wait on it.
Conversation summarizationCopy link to section: Conversation summarization
Once a thread's token count passes a threshold, summarization shrinks the older messages. The most recent turns stay word for word. Everything older is folded into a running summary, which goes at the start of the thread on the next model call.
Let's enable it in b4.config.ts:
export default {
summarization: {
enabled: true,
},
}ConfigurationCopy link to section: Configuration
| Key | Type | Default | Description |
|---|---|---|---|
enabled | boolean | false | Turns summarization on. |
maxTokens | number | 12000 | Summarize older history past this many tokens. |
keepRecentTurns | number | 6 | Recent turns (each starting at a HumanMessage) that are never summarized. |
model | string | Route's model | Model that writes the summary. |
tokenCounter | (text: string) => number | Promise<number> | gpt-tokenizer (o200k_base), loaded on demand | Your own token counter. |
summarize | (args) => Promise<string> | One model call | Your own summarizer. Receives messages, model, previousSummary, signal, and maxRetries (the route's retry.maxAttempts minus one). |
Use tokenCounter to plug in a different tokenizer. summarize replaces the whole summary step, for example to use a cheaper model, compress in a way that suits your domain, or call a different provider.
How they composeCopy link to section: How they compose
Offloading and summarization handle different kinds of growth:
- Tool-output offloading works on one tool result at a time. It keeps a single large result from filling the context.
- Summarization works on the whole thread. It shrinks history that built up over many turns.
You can use both at once. Here's a typical setup for a long-running research agent:
export default {
toolOutput: {
offloadThresholdChars: 1500, // tight threshold for a research agent
previewLines: 10,
},
summarization: {
enabled: true,
maxTokens: 12000,
keepRecentTurns: 6,
},
}