History management
The runtime keeps conversation history for each agent session and sends a provider-facing working history to the model. Two complementary limits operate on different representations:
- Token-budget trimming acts on the provider-facing
ChatMessageworking history and drops oldest whole turns until the estimated context fits the token budget. - Whole-turn retention applies the runtime profile’s configured history
limit to structured
Agent::history(ConversationMessage) and the provider-facingChatMessagehistory used by the legacy agent loop.
Both limits retain turns atomically. A turn starts at a real user message and
includes the assistant response and any tool calls and tool results before the
next user message. Trimming therefore does not split a tool call from its
result. A user message whose text happens to begin with [Tool results] is
treated as part of the turn before it, not as a new turn.
Whole-turn retention
history_trim::trim_to_recent_turns enforces the token budget.
history_trim::trim_to_recent_turn_count and
history_trim::trim_conversation_to_recent_turns enforce the configured turn
limit for the two history representations. Each keeps the newest complete turn
even when the configured value is 0. This is intentional: preserving a
complete current turn is safer than dropping its newest messages or breaking a
tool exchange.
Leading system messages are retained. When no trim is needed, message order and shape are left unchanged.
Token budget
The token budget comes from ResolvedRuntime::effective_context_budget():
- Existing profiles retain the historical 32,000-token budget when neither
max_context_tokensnorcontext_compact_ratiois set, capped by the selected model’s configured capacity when that capacity is smaller. max_context_tokensremains an absolute budget. An explicit value of0disables proactive token-budget trimming.- Setting
context_compact_ratioopts into a model-relative budget: the selected provider alias/model’scontext_window(or a 32,000 fallback when its capacity is unknown) multiplied by the ratio. Values outside(0.0, 1.0]are treated as unset. When both settings are present,max_context_tokensis a downward cap on the ratio-derived budget. - When
history_pruning.enabledis set with a positivehistory_pruning.max_tokens, that value pulls the budget down (never up), letting operators trim earlier. - Every positive effective budget is capped by the selected model’s
configured capacity. The 32,000 fallback is a compatibility stub, not
model truth: when the provider profile declares no
context_windowand the runtime profile sets a positivemax_context_tokens, that budget is honored and also becomes the window operand (so a ratio applies to it) instead of being clamped down to 32,000. The explicit zero sentinel remains zero and continues to disable proactive trimming.
The effective budget is a proactive trimming target, not a hard request limit.
After dropping all eligible older turns, the runtime retains the newest complete
turn even if it remains above that target. It sends the request when the prepared
messages, images, hooks, and tool schemas fit the resolved model context window.
A request that still exceeds that capacity fails before provider dispatch; raising
the proactive target alone cannot make it fit. This capacity check also applies
when proactive trimming is disabled (max_context_tokens = 0) and to the final
summary request after the tool iteration limit is reached.
Capacity and budget are resolved together for the active provider/model route.
Classifier hints and explicit session switches use the same route selection as
provider dispatch, so the next model call, proactive trim, overflow diagnostic,
cost attribution, and client context meter use the selected provider/model and
its resolved pair. If the selected model does not match the model configured on
that provider profile, capacity is treated as unknown instead of borrowing the
profile’s metadata for a different model. The internal 32,000 compatibility
fallback remains available for safety calculations, but wire clients receive no
model_context_window value for an unknown capacity.
Token counts are estimated by history::estimate_history_tokens: roughly four
characters per token plus four framing tokens per message. This is a heuristic,
not a provider tokenizer. Loadable [IMAGE:...] markers are charged a fixed
per-image cost only in messages whose images are dispatched: user turns and the
tool results of the current user turn. A tool result’s images are the
attachments the tool declared, not marker text in its output. Older tool
results are priced as their message bytes, and system and
assistant text is priced as text.
Proactive token-budget trimming runs before the first provider call of a turn when history already exceeds the effective budget and at provider-call boundaries between tool-loop iterations. If a provider reports a context-window overflow, the existing reactive recovery path retries after trimming to two thirds of the current estimated history size. It does not derive a new recovery target from model capacity. Both paths retain whole turns, so neither splits a tool exchange.
Whole-turn limit
max_history_messages is the legacy name of the history setting in an agent’s
runtime profile. In structured agents and the legacy agent loop, its unit is a
complete user turn rather than an individual message row. Tool calls and tool
results remain part of their surrounding turn and do not consume independent
slots.
The default is 50 turns. An explicitly configured value is authoritative,
including 0; the newest complete turn is still retained. The field name is
kept for schema compatibility and can be renamed in a future schema version.
Channel sender caches remain a separate exception: they count individual
message rows and preserve their existing 0-means-default behavior.
Visible trimming
Whenever token-budget trimming or the whole-turn limit drops older turns, the runtime:
- Inserts a breadcrumb before the first retained turn so the model knows that earlier context was omitted.
- Emits
HistoryTrimmedwith the number of dropped messages, retained turns, and a reason identifying the token budget or turn limit.
The event is surfaced through the active client transport and through the observer path used by dashboards and event subscribers. Trimming is therefore not log-only and is not silent to either the model or connected clients.
The legacy agent::run path in loop_.rs reports count-based trimming through
logs only, without the breadcrumb or HistoryTrimmed event. This path serves
interactive use as well as one-shot and non-interactive daemon, cron, subagent,
and SOP callers.
Pairing safety
Whole-turn retention is the primary tool-pairing guarantee: a tool call and its result belong to the same turn and are retained or dropped together. The orphan sweep remains a final safety net for histories that were already inconsistent, such as restored or externally modified sessions.
Tool-result length limits are separate. max_tool_result_chars bounds an
individual result when it is recorded; it does not trim conversation history.
Provider-side context enforcement is also separate, though a provider overflow
can trigger the runtime’s reactive token-budget trim.