Open the console

Guides

Context compaction

A long conversation grows until the prompt no longer fits, or costs more than the answer is worth. Compaction replaces a span of old turns with a summary of that span, so the thread stays intelligible while what is sent to the model shrinks. It is off unless your manifest asks for it.

from agentkit_core.manifest import AgentManifest, Compaction

MANIFEST = AgentManifest(
    ...,
    compaction=Compaction(after_tokens=80_000, keep_recent_events=20),
)

It is in MANIFEST rather than elan.yaml on the line the two files are split on: elan.yaml holds what the platform enforces outside your process, and nothing outside a process can enforce how it manages its own session. See elan.yaml and MANIFEST.

The two triggers

FieldAcceptsMeaning
after_tokensgreater than 0Compact once the last model request's prompt reaches this many tokens. Needs keep_recent_events.
keep_recent_events0 or moreHow many of the newest raw events the size trigger leaves alone.
after_invocationsgreater than 0Compact every this many user turns, however small. Needs overlap.
overlap0 or moreHow many earlier turns each summary re-includes, so consecutive summaries overlap at the seam.
summarizera dotted pathYour own summarizer instead of the agent's model. Empty, the default, means the agent's model.

By size

Compaction(after_tokens=80_000, keep_recent_events=20)

The trigger that matches what people mean by the problem. The newest turns are the ones a model most needs verbatim, so keep_recent_events is the floor under how much detail a long session loses. A file the user's message names is sent with every request while the name is in the conversation, and counts toward the prompt this measures.

By turns

Compaction(after_invocations=20, overlap=2)

Every twenty user turns, re-including the two before the span so the thread does not jump at the seam. overlap=0 is legitimate and lossier — a choice, not an absence: leaving it out is refused, setting it to zero is not.

Both, and half of one

Both triggers may be set; whichever condition arrives first fires. Half a pair is refused when your manifest is built, naming the field you did write:

`after_tokens` and `keep_recent_events` go together: the first says when to
compact, the second how much of the recent conversation to leave alone.
Set both, or neither.

So is a Compaction() with no trigger at all, because it would never compact while reading as though it were working. ADK refuses half a pair too, but at pod start and about a configuration you never wrote; the manifest refuses it first.

Which model summarizes

Your agent's own. ADK builds the summarizer from the root agent's model, which is the model the deploy already checked against llm.models — so compaction needs no new field in elan.yaml and puts no second model in play.

Supplying your own

Two reasons to: summarize with something cheaper than the agent runs on, or summarize by rule, with no model at all. This one keeps what people said and drops tool chatter; it costs nothing and is deterministic.

# agents/support_bot/summarizers.py
from google.adk.apps.base_events_summarizer import BaseEventsSummarizer
from google.adk.events.event import Event
from google.adk.events.event_actions import EventActions, EventCompaction
from google.genai import types


class Terse(BaseEventsSummarizer):
    """What people said, without the tool chatter. No model call at all."""

    async def maybe_summarize_events(self, *, events):
        said = [f"{event.author}: {part.text}"
                for event in events if event.content
                for part in event.content.parts or [] if part.text]
        if not said:
            return None
        return Event(author="user", actions=EventActions(
            compaction=EventCompaction(
                start_timestamp=events[0].timestamp,
                end_timestamp=events[-1].timestamp,
                compacted_content=types.Content(
                    role="model", parts=[types.Part(text="\n".join(said))]),
            )))


terse = Terse()
compaction=Compaction(after_tokens=80_000, keep_recent_events=20,
                      summarizer=".summarizers.terse")
  • The path follows the plugin rules: a leading dot is relative to your target's package, a class is built with no arguments, an instance is taken as it stands — see plugins.
  • Unlike a plugin, it is not loaded during the build. A path that does not import, or names something that is not a BaseEventsSummarizer, fails when the pod starts. Check it locally before you push.
  • A summarizer that calls a model you did not declare is calling a host your egress does not allow, and is refused at the proxy — the enforcement working, not a bug to route around.

Agents only

A WorkflowManifest has no compaction field, and declaring one is refused as an unknown field. ADK builds its default summarizer from the root agent's model and raises when the root is not an LlmAgent — which a graph never is — and it raises after the run has answered, so every run past the threshold would be marked failed having worked.

The same applies to an agent whose build() returns something other than an LlmAgent. That cannot be seen from the manifest, so it is refused at pod start instead, with a message saying to remove the block or return an LlmAgent. Declaring your own summarizer lifts the requirement: ADK never asks what the root is when it has one.

What it costs you

  • A model call per compaction, on your agent's model and your own provider key — unless your summarizer makes none. It runs after the run's charges have been settled and outside the platform's plugins, so it is not metered: it is not charged to your users and does not appear in the run's usage. Your provider bills it to your key like any other call.
  • Time at the end of the run. The summary is written after the run's last event, so the reply has already streamed — but the terminal finished event follows the summary, and a client that waits for it, as runtime.run() does, waits for the summary too.
  • Detail. A summary standing in for twelve turns is right for a model and lossy for a reader, and some of those turns may be approvals and refusals. The raw events stay stored — compaction adds a summary rather than deleting history — but what the model sees is the summary.

If a later turn moves the conversation on while a summary is being written, that summary is discarded rather than failing a run that already answered; the next turn re-evaluates.

Alternatives

  • Keep sessions short. A session id is yours to choose, so start a new one when the task changes rather than carrying one thread all day. Free, and lossless. See sessions and runs.
  • Summarize deliberately, as a tool. A tool that writes a précis into session state is compaction you control, at a point you choose — and for a workflow, which cannot use this field, it is the answer, as a node.
  • Cap the growth. An agent looping on tool results is usually what grew the history, and max_llm_calls bounds it per invocation.
  • Keep large documents out of the conversation. A file a tool reads with load_artifact is in the prompt only as far as the tool's result puts it there. A file the user's message names is attached in full to every request while the name is in the conversation.

Reading this as a model? llms.txt indexes every page, and each page has a “Copy for AI” control.