Skip to content

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() from core.py, which prefers each provider's own counter — Anthropic messages.count_tokens, Google models.count_tokens, OpenAI's token-counting API — and falls back to a local tiktoken encoding 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): summarize and combined compact 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_config mode 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 turn no_reasoning, no_tool_results, messages_only and compact_tool_results silently 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_tokens value 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, combined and sliding_window compact 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 setting keep_recent_messages to at least 4–6 when tools are active.
  • Truncation notice: When sliding_window_include_truncation_notice is True (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 functionCall without its thought signature. That holds at both compaction levels, for every history mode and every keep_recent_messages value.
  • Shrinking, not in every case. Keeping a tool exchange whole can mean keeping more than keep_recent_messages messages. A single turn whose own tool loop fills the context window is not reduced by sliding_window alone — combine it with tool_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) and clear_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 summarize stage.

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. generateContent rejects any request ending on a model turn with Requests 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.

  • summarize summarises 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.
  • combined runs its tool_result_clearing step as before. Its sliding_window step follows the rule below. Its summarize step summarises only what precedes the current turn, and is skipped when nothing does.
  • sliding_window returns 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_clearing used on its own.
  • max_compactions_per_session and 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_tokens for floats, or threshold directly 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_compaction messages exist.
  • max_compactions_per_session has 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_history and archive_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 an archive_sink returned 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 like tool_result_clearing can 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

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.