Docs / Core Concepts
Context Window
The agent loop can only grow its history: every tool batch appends an assistant message and
one tool result per call. AgentCore owns the loop but ships no automatic compression
policy — nothing compacts unless you supply a context_compactor. It is invoked once per
loop iteration, before the model call:
agent = AgentCore(bindings, context_compactor=my_compactor) # constructor default
response = await agent.chat(prompt) # uses the defaultresponse = await agent.chat(prompt, context_compactor=other) # per-call overrideresponse = await agent.chat(prompt, context_compactor=None) # disabled for this callThe compactor is a sync or async callable receiving a CompactionContext. It can return:
| Return | Meaning |
|---|---|
CompactionPlan | Recommended. Declare what to compact; core materializes the replacement history |
list[CLIverMessage] | Escape hatch: return a complete, already-validated history |
None | Leave the history unchanged — the steady-state path |
chat(), stream(), run_skill(), and chat_structured() all accept the same per-call
parameter.
What the Compactor Sees
Section titled “What the Compactor Sees”from cliver_core import CompactionContext
async def my_compactor(context: CompactionContext): used = context.last_usage.input_tokens if context.last_usage else 0 if used < 60_000: return None return compact(context)| Field / method | Meaning |
|---|---|
messages | The run history excluding the core-owned system message — a fresh list; treat the message objects as read-only |
iteration | Zero-based loop iteration about to call the model |
run_id | The run’s correlation id (same one on events and the response) |
model | The active llm binding’s model before failover for this call |
last_usage | The previous iteration’s usage for this run; None on the first iteration |
record_usage(usage) | Fold usage from a model call the compactor made (for example a summarization call) into the run total — the plan path does this for you |
split_at_user_turn(keep_turns) | Split the history before the Nth-from-last user turn; returns a CompactionSplit |
render(messages) | Format messages as compact text for a summarizer prompt |
last_usage.input_tokens is the previous model call’s input — the best available signal that
the next call may exceed the window. It is per call, not cumulative.
Declarative Plans
Section titled “Declarative Plans”A plan describes the change; core performs it. That keeps the structural invariants — cutting at a safe boundary, preserving tool call/result pairs, framing the summary, and accounting for its usage — in one place instead of in every application’s compactor.
from cliver_core import CompactionContext, CompactionPlan
SUMMARIZE = ( "Summarize the earlier part of this agent run for the same assistant. " "Keep facts, decisions, tool results that still matter, and open tasks. " "Be compact and do not invent.")
async def my_compactor(context: CompactionContext) -> CompactionPlan | None: used = context.last_usage.input_tokens if context.last_usage else 0 if used < 60_000: return None
split = context.split_at_user_turn(keep_turns=2) if not split.older: # nothing to drop yet return None
response = await summarizer.chat(f"{SUMMARIZE}\n\n{context.render(split.older)}") if not response.message.content: return None
return split.plan(summary=response.message.content, usage=response.usage_total)CompactionPlan field | Meaning |
|---|---|
keep_from | Index of the first retained message in context.messages; must be positive and point at a user message |
summary | Optional replacement text; core prepends it as one user message beginning with [Compacted earlier context] |
usage | Optional usage from your summarization call; core adds it to the run’s usage_total once the resulting history is accepted |
What core does with a plan:
- Rejects a
keep_fromoutside the history, or one that does not point at ausermessage —ValueErrorbefore the model call. - Builds
[summary_message, *messages[keep_from:]](the summary message is omitted whensummaryisNone). - Validates the result (tool pairs intact, ends with
userortool). - Records
plan.usageinto the run’s usage total.
split_at_user_turn(keep_turns) returns a CompactionSplit with keep_from, older, and
recent. Its boundary is always a user message, so a suffix that starts there can never contain
a dangling tool call/result pair. split.plan(...) binds that same boundary to the summary, so
the plan cannot drift from the text the summarizer actually saw. It raises ValueError when the
split drops nothing (keep_from == 0) — check split.older first. keep_turns must be at
least 1.
Built-in LlmCompactor
Section titled “Built-in LlmCompactor”cliver-core ships the pattern above as an opt-in class. Core never installs it; pass it
explicitly:
from cliver_core import AgentCore, LlmCompactor, ModelBindings, presets
# One summarizer per worker / event loop, like any other AgentCore.summarizer = AgentCore(ModelBindings(llm=presets.deepseek(api_key="...")))
compactor = LlmCompactor(summarizer, threshold_tokens=60_000, keep_turns=2)agent = AgentCore(bindings, context_compactor=compactor)- Triggers when
last_usage.input_tokens >= threshold_tokens. It is silent on the first iteration and whenever the history holds no complete older turn. - Keeps the last
keep_turnsuser turns verbatim; the summarized prefix is exactlysplit.older, rendered withcontext.render(...). - Returns a plan, so the summarization call’s
usage_totalis recorded automatically. An empty or missing summary leaves the history unchanged (its usage is still recorded). - The summarizer is anything with an async
chat(prompt)returning aCLIverResponse— usually a secondAgentCorebound to a cheaper model. Its run is a separate run with its ownrun_idand events. - Import as
from cliver_core import LlmCompactor;CompactionPlanandCompactionSplitare exported alongside it.
Escape Hatch: Return a Full History
Section titled “Escape Hatch: Return a Full History”Returning list[CLIverMessage] still works — it is the path for a deterministic trim, a
non-LLM policy, or a history loaded from your own store. A returned list is validated before it
can reach a provider. ValueError is raised — before the model call — when it:
- is empty, or contains a
systemmessage; - has a tool result with no matching assistant tool call;
- leaves an assistant tool call without all of its tool results;
- does not end with a
userortoolmessage, so the model has a turn to answer.
Core re-attaches the system message to whatever history you return. If you take this path and
make your own model call, record its usage with context.record_usage(...); the plan path is
the one that does it for you.
Invocation Contract
Section titled “Invocation Contract”- Once per loop iteration, before the model call. Failover retries inside that iteration reuse the compacted history; they do not re-run the compactor.
- The first iteration included. A caller-supplied conversation that is already too large can
be trimmed before the first call, but
last_usageisNonethere — trigger on a message or character budget if that matters to you. - Inside the run deadline.
run_timeoutcovers compaction model calls too. Exceptions propagate; a slow summarizer consumes the run’s budget. - The system message stays core-owned. It is never passed to the compactor and is re-attached to whatever history results.
- Streaming uses the same placement. The replacement is visible to the next model call; the
terminal trace’s entries carry a
usage_totalthat includes recorded compaction usage.
Usage Accounting
Section titled “Usage Accounting”- Non-streaming: usage recorded through
context.record_usage(...)— andplan.usageon the plan path — is included inresponse.usage_total. - Streaming: each terminal trace entry has a
usage_totalas of that iteration. - The compactor’s own
AgentCorerun (the summarizer) is a separate run with its own events and usage;LlmCompactorattaches itsusage_totalto the plan so the caller’s meter stays complete.
Operational Notes
Section titled “Operational Notes”- The context window is application knowledge. Core does not maintain a model →
window-size table.
max_tokenscaps output, not the window; set the threshold per model in your application. - Bound tool output separately. A single huge tool result can blow the window before any turn boundary is available to cut at; cap it at the tool boundary.
- Persist summaries yourself. Core is stateless. If the next request should start from a
compacted history, store the summary in your conversation store and pass it as
conversation=. - Decide the failure policy. A summarizer failure propagates and fails the run. If you
prefer a fallback, catch it inside the compactor and return a deterministic trim (or
None) instead. - Keep the steady-state path cheap. The compactor runs before every model call; a token
check that returns
Noneshould not make a network request. - Do not store per-run state on the agent. Use the context,
run_id, or application-owned storage keyed by your conversation id. The agent stays stateless and loop-safe, not thread-safe — see Concurrency & Lifecycle.