Context Compaction¶
Context compaction automatically compresses conversation history when it approaches the model's context window limit, allowing long-running agent sessions to continue without hitting token limits.
Why compaction matters¶
As agents iterate through tool calls and accumulate results, the conversation history can grow beyond the model's context window. Without compaction, this results in API errors or silently truncated context. Compaction keeps the agent running by proactively reducing history size using configurable strategies.
Two-level architecture¶
Compaction operates at two independent, complementary levels:
Level 1 — Pre-call compaction¶
- When: Before any API call, inside
_execute_run(). - What it handles: Cross-run session growth — accumulated history from previous
run()calls that could exceed the context window before the first API call. - Token estimation: Uses
count_tokens()fromcore.py, which prefers each provider's own counter — Anthropicmessages.count_tokens, Googlemodels.count_tokens, OpenAI's token-counting API — and falls back to a localtiktokenencoding chosen from the model's registry row, then to a text approximation (~4 chars/token). Which rung answered is reported; see Cost Tracking. - Applies to: Both function-calling and structured-output code paths.
- Supported strategies: All four —
sliding_window,summarize,tool_result_clearing,combined. - The pending question is not summarised (0.5.1):
summarizeandcombinedcompact the history before the question the run answers and keep the question verbatim after the recap. See Preserving the pending turn. - History mode: the session's
session_history_configmode is re-applied to the compacted history before the request is built. Until this release the post-compaction re-prepare skipped it, so on a compacting turnno_reasoning,no_tool_results,messages_onlyandcompact_tool_resultssilently stopped applying to that one request — see Sessions.
Level 2 — Intra-run compaction¶
- When: Inside
_run_tool_calling_loop_*()functions, after each API response, between complete tool roundtrips. - What it handles: Within-run growth — conversation growing during a single
run()due to many tool iterations. - Token estimation: Uses the
input_tokensvalue reported by the API response. - Applies to: Function-calling path only (structured output has no loop).
- Supported strategies: All four —
sliding_window,summarize,tool_result_clearing,combined. - The turn in progress is kept verbatim:
summarize,combinedandsliding_windowcompact only what came before the current turn's request. See The turn in progress is kept verbatim.
Both levels can trigger in the same session. The compaction_count in AgentResult and session.metadata reflects the total across both levels.
Strategy support matrix¶
| Strategy | Level 1 (pre-call) | Level 2 (intra-run) | Notes |
|---|---|---|---|
summarize |
✅ | ✅ | Requires a separate LLM call |
sliding_window |
✅ | ✅ | Includes orphan fix and optional truncation notice |
tool_result_clearing |
✅ | ✅ | Level 1 operates on provider-native session history |
combined |
✅ | ✅ | Applies all sub-strategies at both levels |
Strategies¶
summarize (default)¶
Calls an LLM to generate a structured summary of the conversation, then replaces old messages with the summary. Best for preserving semantic context.
- Uses a configurable model (defaults to the agent's own model).
- Uses a configurable prompt (inline string, file path, or built-in default).
- The summarization call is a separate LLM API call using the toolkit's client infrastructure.
- The pending question survives, and is never summarised. Before a run, the summary covers the conversation before the question the run answers, and the question follows it verbatim (0.5.1; see Preserving the pending turn). Inside a tool loop the summary covers only the turns before the current one, and the current turn is kept as it is — see The turn in progress is kept verbatim.
sliding_window¶
Keeps the last N messages verbatim and discards everything before. Simple and fast, but loses older context entirely.
- Configured via
keep_recent_messages(default:5). - Orphan fix: If the cut point falls inside a tool-call/tool-result sequence, the window automatically advances forward to avoid sending orphaned tool results without their matching tool call (which causes API errors). This means the effective retention may be fewer messages than
keep_recent_messages. Recommend settingkeep_recent_messagesto at least 4–6 when tools are active. - Truncation notice: When
sliding_window_include_truncation_noticeisTrue(default), a synthetic user message is prepended to notify the model that earlier context was removed:"[Earlier conversation context (N messages) has been truncated by sliding window compaction. Some prior context may be missing.]" - Inside a tool loop the window never opens after the current turn's request. If the window would drop the request, it opens on the request instead. See The turn in progress is kept verbatim.
What sliding_window guarantees¶
The cut point is never allowed to split a tool exchange. It is first walked forward past any tool
call or tool result at the boundary; if that walk runs off the end of the conversation — which
happens when the whole retained window is tool traffic, i.e. a turn with three or more tool round
trips at the default keep_recent_messages=5, or as few as two at keep_recent_messages 1–3 — it is
walked backward instead, to the most recent message that is neither a tool call nor a tool result.
Anything still unpaired after the slice (a tool result whose call was truncated, or a call whose
result was) is removed before the request is built.
The guarantee is therefore about validity, not size:
- Validity, in every case. No request the toolkit sends can contain a tool result without its call,
a retained call without its result, or — on Gemini — a current-turn
functionCallwithout its thought signature. That holds at both compaction levels, for every history mode and everykeep_recent_messagesvalue. - Shrinking, not in every case. Keeping a tool exchange whole can mean keeping more than
keep_recent_messagesmessages. A single turn whose own tool loop fills the context window is not reduced bysliding_windowalone — combine it withtool_result_clearing, which clears the contents of old tool results while keeping every call/result pair intact. If the conversation holds no message that is neither a call nor a result, compaction declines and logs a warning: sending the conversation uncompacted is strictly better than sending an invalid request.
Before this release the walk only went forward, and a "never return an empty list" guard then kept
exactly the last message — which mid-loop is always a tool result whose functionCall had just been
cut. On Gemini that request failed validation outright; on Anthropic a 13-message conversation could
collapse to a lone truncation notice.
tool_result_clearing¶
Replaces old tool results (and optionally tool inputs) with placeholder text, keeping message structure intact. Good for conversations with large tool outputs.
- Configured via
keep_recent_tool_results(default:3) andclear_tool_inputs(default:False). - Works with all 4 provider message formats (Anthropic, OpenAI, Google, Fireworks).
- Level 1 and Level 2 — works at both levels. Level 1 operates on provider-native session history messages; Level 2 operates on in-run provider messages.
combined¶
Applies strategies in sequence: tool_result_clearing → sliding_window → summarize. Each stage reduces tokens; summarization only runs if still above threshold after prior stages.
- Order is configurable via
combined_order. - At Level 1, all strategies in the combined order are applied, including
tool_result_clearing. - Inherits the pending-turn guarantee below from its
summarizestage.
Preserving the pending turn¶
Before a run, the question is kept out of the summary (0.5.1). A session appends the question
a run answers as the last history message before the run. summarize, and a combined order with
a summarize step, compact only the messages before it, and the question follows the recap
verbatim. The request is [marker, summary] and then the real user turn, as before, and the session
stores the question once.
Until 0.5.1 the question was summarised with the rest and re-appended afterwards. The summariser
then often answered it, and the model, seeing the answer in the recap, replied that it had "already
provided" the answer "in the conversation summary above". That was the W10 e2e failure of
compaction_combined on claude-haiku-4-5. There is no switch back: nothing relied on a summary
that answered the question. A tail that is not a plain question (a tool result, an assistant turn)
is summarised as before. A summarize that fails falls back to sliding_window on the whole history, as before; in
combined a failed summarize step leaves the window of the history before the question, followed by
the question.
The re-append described next is the guard for a rewrite that did drop the question. At Level 1 the
strategies above keep it, so the guard finds it in place and adds nothing. In-loop it runs only under
DPT_COMPACTION_PROTECT_CURRENT_TURN=off; by default an in-loop compaction keeps the whole current
turn instead (see The turn in progress is kept verbatim).
summarize and combined discard the message list and rebuild it from the summary, which ends the
conversation on a model turn. That strands the user turn the current call was about to answer:
- Google hard-fails.
generateContentrejects any request ending on a model turn withRequests ending with a model turn are not supported(400). - The other providers answer the wrong thing. With no question left in the list, the model responds to the summary instead of to the user.
So after compaction a trailing plain-text user message is re-appended, and the request ends on
the question again. sliding_window and tool_result_clearing keep the tail already, so this is a
no-op for them. The decision is "is the compacted tail already the pending message?", not "is the
tail user-role?": Google's recap is itself a single user-role turn (a request may not end on a
model turn), so the question follows it as a second consecutive user turn, which Gemini accepts.
Only plain-text user turns are re-appended. Inside a tool loop the trailing message is a tool result,
and by default it is never summarised: the whole current turn is kept verbatim (see
The turn in progress is kept verbatim). Only with
DPT_COMPACTION_PROTECT_CURRENT_TURN=off is a trailing tool-result message folded into the
summary and left there — on Anthropic (tool_result blocks) and Google (function_response parts)
those also ride user-role messages, so the distinction is by content, not by role. Re-appending one
would orphan it from the tool_use that produced it, and would make an aggressive mid-loop
compaction re-pose the same question on every pass, looping the model until max_iterations.
At Level 1 the restore is conditional on the strategy having actually removed the pending turn,
compared by value. sliding_window and tool_result_clearing keep the question you just asked, so
re-appending it unconditionally — as every Level-1 compaction on those strategies used to — sent,
billed and stored the same question twice.
A pending turn that carries no text at all — a pure image or pure file turn — used to lose the summary instead. With no text to restore, the assistant summary became the last history entry and was then cut off the very request the compaction had just been billed for, sending that turn with no history whatsoever. Now the session's own empty turn is kept after the recap (the guard would append an empty slot in its place), so the summary survives the slice every consumer applies.
A compacted request never ends on a model turn¶
A request whose last message is an assistant turn is an assistant prefill. Anthropic rejects one whenever extended or adaptive thinking is enabled, and Google rejects one outright. By default an in-loop compaction keeps the current turn, so the request still ends on the pending tool result and nothing needs to be added.
With DPT_COMPACTION_PROTECT_CURRENT_TURN=off, summarize and combined fold the tool result into
the summary, and there is no plain-text question to restore. They then append a short synthetic user
continuation, whose text is doc_processing_toolkit.agent.compaction.COMPACTION_CONTINUATION_TEXT.
It costs roughly 15 tokens on the requests where it fires, and fires nowhere else.
A compaction never rewrites the tool round a pending result belongs to¶
Anthropic rejects any change to the most recent assistant message while tool_result blocks are
being returned for it. tool_result_clearing with clear_tool_inputs=True could reach into that
turn and blank a tool_use input; it is restored verbatim, and everything earlier keeps the
compaction's savings. The rule is provider-neutral — no strategy has a reason to rewrite the turn
the pending results answer — and it is a no-op unless one does.
The guard now covers the whole open round, not just the assistant turn that opened it.
keep_recent_tool_results counts tool results, so a round with more parallel calls than the budget
used to straddle the boundary: one result kept its payload while its sibling was replaced by
"[Tool result cleared during compaction]", inside the round the model was about to answer. Nothing
failed, because the request was perfectly valid — the model was simply asked to reason about evidence
it had never been shown.
Google conversations are read, not skipped¶
Every text consumer — the summarize transcript, the local token estimate, the Level-1 trigger and
the text_summary history mode — reached for msg["content"], which a Gemini turn does not have:
it carries parts. A Google session therefore flattened to a column of bare ROLE: labels, so
summarize summarised an empty transcript, the token estimate collapsed to its minimum, the Level-1
trigger lost its only text signal, and text_summary sent the model the shape of the conversation
and none of its words. One shared reader now handles all four, skipping thought-summary parts and
rendering tool traffic with the same [tool_call: …] / [tool_result: …] markers used for the other
providers. Anthropic and Fireworks histories are read byte for byte as before.
OpenAI tool-loop histories are read, not skipped¶
Inside a tool loop the OpenAI provider keeps its history in Responses shapes. The user's request is
stored as input_text blocks, the model's draft as output_text blocks on a message item, each
tool call as a function_call item with no content, and each tool result as a
function_call_output. The shared reader used to read none of the text blocks and rendered a
function_call as nothing. A mid-loop summarize or combined compaction on OpenAI therefore sent
the summarizer only the bare tool outputs. The September 2026 e2e run recorded the summary
"Only a tool result is present: "Word count: 221." No substantive conversation content … were
provided." The local token estimate behind tokens_after also collapsed to a handful of tokens.
The reader now reads input_text and output_text blocks, and renders a function_call as
[tool_call: name(arguments)] and a function_call_output as [tool_result: …]. It skips
reasoning items, as it skips Google thought parts. The OpenAI summary recap, whose marker is an
input_text block, is now visible to a later compaction too. Level 1 on OpenAI was never affected,
because session storage is normalised before it is read.
The turn in progress is kept verbatim¶
Inside a tool loop (Level 2), the current turn is everything from the user's request onwards: the
request, the draft the model has already written, and the tool round it is waiting on. summarize,
the summarise step of combined and sliding_window compact only the messages before that
request. The turn itself is kept exactly as it was sent.
summarizesummarises the earlier turns and places the recap in front of the current turn. The request therefore still ends on the pending tool result, and no "Continue the task" turn is needed. On Google the recap is a user-role turn, so the request follows it as a second user turn, which Gemini accepts. Tool calls and their results stay paired on every provider.combinedruns itstool_result_clearingstep as before. Itssliding_windowstep follows the rule below. Itssummarizestep summarises only what precedes the current turn, and is skipped when nothing does.sliding_windowreturns its ordinary window when that window still holds the request, so these requests are unchanged. When the window would drop the request, it opens on the request instead. Before this change, OpenAI and Gemini runs could send a window that held the tool round but not the question.
The current turn starts at the last plain-text user message. Tool results, which on Anthropic and
Google also travel in user-role messages, do not count. When nothing precedes the current turn,
the in-loop compaction falls back to tool_result_clearing on the current turn and logs why at INFO.
This covers three cases: a single-turn run, a prefix that is only an earlier compaction's recap or
truncation notice, and a list with no plain-text user message. The result then reports
strategy="tool_result_clearing". The fallback never summarises a summary again, so a run whose
current turn stays above the threshold does not pay for a summarisation call on every iteration.
combined keeps its own strategy name and skips the steps that would lose the turn.
Before this change the current turn was summarised along with everything else, and a synthetic continuation ("Continue the task from the summary above") was appended. The request survived only as the summary's account of it, and the draft existed only inside the summary. Summaries of a nearly finished turn tend to record the task as done, so the model often answered with a message about the task, such as "the previous task has already been completed". The September 2026 e2e run recorded this on Anthropic, OpenAI, Google and Fireworks.
What stays the same:
- Pre-call (Level 1) compaction, which has its own form of the rule: see Preserving the pending turn.
tool_result_clearingused on its own.max_compactions_per_sessionand the archive.
A current turn larger than the threshold stays large
Keeping the turn means a single turn whose own tool loop fills the context window is reduced
only by clearing old tool results inside it. Lower keep_recent_tool_results for long tool
loops. max_compactions_per_session still bounds how often compaction fires.
Thinking blocks of the kept turn follow a rewritten prefix
On Anthropic the kept turn carries its thinking blocks behind a summary that replaced the
earlier turns. sliding_window has always done the same. On the rows that check a thinking
block's conversation prefix, the toolkit's default drop_block binding tells the API to drop
such a block rather than reject the request. If you set anthropic_block_binding="error",
strict_prefix_binding=True or DPT_ANTHROPIC_BLOCK_BINDING=error, an in-loop summarize on
those rows fails in the same way. See Thinking.
A run whose thinking is turned off with between_tools cannot send that binding. On such a run
the toolkit removes the run's thinking blocks from the history an in-loop compaction has just
rewritten, and logs a WARNING naming the strategy; earlier turns' blocks are not sent on such a
run in the first place. See
Thinking off on a model that checks the thinking prefix.
DPT_COMPACTION_PROTECT_CURRENT_TURN=off restores the previous folding byte for byte. See
Configuration.
Trigger mechanism¶
Compaction checks whether the current token usage exceeds a configurable threshold:
- Threshold: Supports both a percentage of context window (float 0.0–1.0) and an absolute token count (int). Default:
0.8(80%). - Resolution:
effective_threshold = threshold × context_window_tokensfor floats, orthresholddirectly for ints.
Context window resolution¶
Every trigger check — all four provider loops at Level 2, plus the Level-1 pre-call check — resolves
the context window through one shared function, compaction.resolve_context_window(model_name,
provider, logger). It reads limits.<provider>.models.<model>.context_window_tokens from
model_information.json.
On a registry miss it warns loudly, naming the exact JSON path to add, and falls back to 128,000 tokens:
No context window found in model_information.json at
limits.google.models['some-unregistered-model'].context_window_tokens; falling back to 128000 tokens.
Compaction may trigger prematurely for larger-context models — add the model's limits entry to fix this.
Two things to know about that fallback:
- It warns rather than raises. Compaction is an optional resilience feature; aborting a long run over a missing registry row would trade a recoverable inefficiency for a total failure.
- It errs early, never late. 128,000 is a floor, so a model missing from the registry compacts sooner than necessary rather than overflowing its context window.
This replaced four divergent inline fallbacks (200,000 in the provider loops, 128,000 at Level 1)
that silently decayed as the model lineup changed: million-token models were compacting at roughly
15–20% utilization, while sub-128k models could overflow before compaction ever fired. If you see
the warning above, add the model's limits entry rather than tuning trigger_threshold around it.
Compaction does not trigger when:
- The first request is already above the threshold (let the provider API handle the error).
- Fewer than
min_messages_before_compactionmessages exist. max_compactions_per_sessionhas been reached.
Archive¶
Before compacting, the original conversation history is written to a JSON file:
- Path format:
{archive_dir}/compaction_{session_id}_{timestamp}_{compaction_number}.json - Multimodal handling: Long base64 strings are replaced with placeholder references (e.g.,
"[base64 image data: 12345 bytes, image/png]"). - Archival is configurable via
archive_historyandarchive_dir.
Send the archive somewhere else¶
If your process runs where that file will not outlive it — a sandbox, a short-lived container — set
compaction_config={"archive_sink": fn} instead. The callable receives the same sanitised,
JSON-serialisable message list the file would have held, and replaces the file rather than adding
to it. Return a string and it is used wherever the path would have been, including
AgentResult.compaction_archives and the summary's ## Reference section, so an object key or a URL
reads back exactly where a path did.
def archive_sink(messages: list) -> str:
key = f"conversations/{uuid4()}.json"
s3.put_object(Bucket="my-bucket", Key=key, Body=json.dumps(messages).encode())
return key
agent = Agent(model="claude-sonnet-5", compaction_config={"archive_sink": archive_sink})
A sink that raises never fails the run: the toolkit logs a warning and carries on, and that warning
names the exception type and nothing else, because anything an archive touches is conversation
content. archive_history=False still switches archiving off entirely and wins over the sink. A
callable has no YAML spelling, so this one is Python-only — agent/config.py rejects a non-callable
at load time.
Summary template¶
When using the summarize strategy, the compacted history is replaced with a structured summary:
<compaction_summary>
## Conversation Summary (Compaction #1)
{LLM-generated summary}
## Reference
The full conversation history prior to this compaction has been saved to: `./compaction_archives/compaction_sess1_20260302_120000_1.json`
You may refer back to this file if you need exact details from the original conversation.
## Current Task Context
Continue working on the task described above. The summary preserves the key context needed to proceed.
</compaction_summary>
The ## Reference section is only included when include_archive_path_in_summary=True.
compaction_config parameters¶
| Parameter | Type | Default | Description |
|---|---|---|---|
strategy |
str |
"summarize" |
"summarize", "sliding_window", "tool_result_clearing", or "combined" |
trigger_threshold |
float \| int |
0.8 |
Float (0.0–1.0) = percentage of context window; int = absolute token count |
summary_model |
str \| None |
None |
Model for summarization (defaults to agent's model) |
summary_prompt_file |
str \| None |
None |
File path to custom summarization prompt |
summary_prompt |
str \| None |
None |
Inline custom prompt (takes precedence over summary_prompt_file) |
keep_recent_messages |
int |
5 |
Messages to preserve verbatim in sliding_window/combined |
keep_recent_tool_results |
int |
3 |
Tool results to keep in tool_result_clearing/combined |
clear_tool_inputs |
bool |
False |
Also clear tool call parameters (tool_result_clearing) |
archive_history |
bool |
True |
Write original conversation to file before compacting |
archive_dir |
str |
"./compaction_archives" |
Directory for archive files |
archive_sink |
Callable \| None |
None |
Receives the archived conversation instead of writing a file; a string it returns is used wherever the path was. Python-only |
include_archive_path_in_summary |
bool |
True |
Include file path in summary output |
combined_order |
list[str] |
["tool_result_clearing", "sliding_window", "summarize"] |
Order of strategies for combined |
min_messages_before_compaction |
int |
4 |
Minimum messages before compaction can trigger |
max_compactions_per_session |
int \| None |
None |
Limit on compaction count (None = unlimited) |
summary_max_tokens |
int |
4096 |
Maximum output tokens for the summarization LLM call |
sliding_window_include_truncation_notice |
bool |
True |
Prepend a notice message when sliding window truncates history |
Enabling compaction¶
agent = Agent(
model="claude-sonnet-5",
compaction=True,
compaction_config={
"strategy": "combined",
"trigger_threshold": 0.75,
},
)
from doc_processing_toolkit import AgentBuilder
agent = (
AgentBuilder()
.with_compaction(
strategy="sliding_window",
keep_recent_messages=10,
)
.build()
)
# Or via Agent.builder():
# agent = Agent.builder().with_compaction(...).build()
model: claude-sonnet-5
compaction: true
compaction_config:
strategy: summarize
trigger_threshold: 0.8
archive_dir: ./my_archives
Cost tracking¶
Summarization tokens and cost are tracked separately in AgentResult:
| Field | Type | Description |
|---|---|---|
compaction_count |
int |
Number of compaction events during a run |
compaction_input_tokens |
int |
Total input tokens for summarization calls |
compaction_output_tokens |
int |
Total output tokens for summarization calls |
compaction_cost |
float |
Total cost of summarization calls |
compaction_archives |
list[str] |
File paths to archived conversation histories |
Per-iteration metrics also include compaction fields:
| Field | Type | Description |
|---|---|---|
compaction_triggered |
bool |
Whether compaction occurred in this iteration |
compaction_strategy |
str \| None |
Strategy that was applied |
compaction_tokens_cleared |
int |
Approximate tokens removed |
How the reported token figures are computed¶
CompactionResult carries three token fields, and it is worth knowing exactly how much to trust
each one:
| Field | Source | Accuracy |
|---|---|---|
tokens_before |
The API-reported input_tokens (Level 2) or count_tokens() estimate (Level 1) |
Best available |
tokens_after |
tokens_before scaled by the local pre/post shrink ratio |
Approximate |
tokens_cleared |
tokens_before - tokens_after |
Approximate |
The compaction entry points have no API client in hand, so no exact recount is possible after
compacting. Instead the toolkit flattens the message list to text and estimates it at roughly four
characters per token — before and after — and applies that shrink ratio to the API-scale
tokens_before. Mixing a raw local estimate with an API-reported count would compare two
incompatible scales; scaling by the ratio keeps every reported figure in one unit system.
Behaviour change
tokens_cleared previously reported tokens_before verbatim — i.e. it claimed every token
had been cleared, on every strategy, including tool_result_clearing runs that only replaced a
few tool payloads. Dashboards or budget logic that treated the old value as a real measurement
will now see much smaller, and much more honest, numbers. tokens_after was never populated at
all and stayed 0; it now carries a real estimate.
Both levels log the same shape at INFO:
Compaction #1 complete (strategy=sliding_window): ~48210 tokens -> ~9642 tokens
(cleared ~38568; 6 messages retained)
Session integration¶
AgentSession tracks compaction across multiple runs:
metadata["compaction_count"]accumulates the total compaction count.metadata["compaction_archives"]collects all archive file paths — or whatever anarchive_sinkreturned in their place.- A compaction rewrites the conversation, so the session's recorded
run boundaries are emptied and
metadata["boundaries_reset_at_run"]stamps the run that did it, rather than leaving message indices pointing at the wrong messages. - Session history stores provider-native messages (including tool calls, tool results, and reasoning items) — not just text-only
{role, content}pairs. This means Level 1 compaction strategies liketool_result_clearingcan operate directly on the session history. - Cross-run growth of session history is handled by Level 1 pre-call compaction on subsequent
run()calls. - When intra-run (Level 2) compaction occurs, the session replaces its stored history with the compacted result, which becomes the authoritative history for subsequent turns.
Provider notes¶
Anthropic native compaction
If you enable both Anthropic's native context_management (via provider_kwargs) and the toolkit's compaction=True, both run independently. No conflict resolution is performed — both mechanisms may trigger compaction at different points.
Not every row that lists the context_editing built-in accepts the server-side compaction
edit: claude-haiku-4-5 answers compact_20260112 with a 400 (observed live on 2026-09-23)
while accepting the clear_tool_uses and clear_thinking edits. Every Anthropic row now carries
descriptions.<model>.supports_server_compaction, set from Anthropic's
threshold-compaction compatibility list:
true on Claude Fable 5.1 / 5, Opus 5.5 / 5 / 4.8 / 4.7 / 4.6, Sonnet 5.5 / 5 / 4.6 and Haiku 5.5; false
on Haiku 4.5, Opus 4.5 and Sonnet 4.5. On a false row the toolkit leaves the
compact_20260112 edit (and its compact-2026-01-12 beta) out of the request and logs one
WARNING naming the key; the other context_editing edits are still sent, so the run completes
instead of returning the provider's 400. Use the toolkit's own compaction= on those rows.
The edits Anthropic reports having applied — the cleared tool uses and thinking turns, logged at
INFO and kept in the run's built-in tool usage as context_cleared_tool_uses /
context_cleared_thinking_turns with their token counts — are read from the SDK's typed
context_management response on both Anthropic paths. Earlier builds read that block as a dict,
so any context-editing run whose response carried it failed with an "Unexpected error" and no
data.
Fireworks
Fireworks has no native compaction support. Only toolkit-managed compaction is available.
OpenAI
OpenAI has no native compaction support in the toolkit. Only toolkit-managed compaction is available.
Google has no native compaction support in the toolkit. Only toolkit-managed compaction is available.
The summary is emitted as one user-role Content carrying the marker and the summary text
together, not as a [user marker, model summary] pair. generateContent rejects any request
ending on a model turn, and a compaction can legitimately leave the summary as the last
message — for instance a mid-loop compaction under
DPT_COMPACTION_PROTECT_CURRENT_TURN=off, which folds a trailing tool result into the summary
rather than re-appending it. By default a mid-loop recap is followed by the kept current turn.
A user-role recap is always a valid tail, and Gemini accepts
the consecutive user turns that result when a pending question follows it.