pub struct Compactor {
pub threshold_chars: usize,
pub threshold_prompt_tokens: Option<u32>,
pub keep_recent_n: usize,
}Expand description
Configuration for LLM-driven transcript compaction.
Fields§
§threshold_chars: usizeCharacter-count threshold above which compaction is triggered.
Defaults to usize::MAX (disabled).
This estimate is reliable for English content (~4 chars/token) but
underestimates token density in CJK languages. When actual
prompt_tokens from the API are available, threshold_prompt_tokens
is checked first and takes priority.
threshold_prompt_tokens: Option<u32>Token-count threshold: if the last turn’s prompt_tokens from the
API response meets or exceeds this value, compaction is triggered.
When Some and the last prompt_tokens is non-zero, this check takes
priority over threshold_chars. This eliminates the 4-char/token
assumption that fails for CJK content. Set to None to rely solely
on the char-based heuristic.
keep_recent_n: usizeNumber of most-recent messages to keep verbatim during compaction.
Implementations§
Source§impl Compactor
impl Compactor
Sourcepub fn new(threshold_chars: usize) -> Self
pub fn new(threshold_chars: usize) -> Self
Create a new compactor with the given threshold and default keep_recent_n (8).
Sourcepub fn threshold_prompt_tokens(self, n: u32) -> Self
pub fn threshold_prompt_tokens(self, n: u32) -> Self
Set the token-count threshold for compaction.
When the last turn’s prompt_tokens (from the API response) meets or
exceeds this value, compaction is triggered instead of relying on the
character-count estimate. Prefer this for non-English workloads.
Sourcepub fn keep_recent_n(self, n: usize) -> Self
pub fn keep_recent_n(self, n: usize) -> Self
Set the number of recent messages to preserve verbatim.
Sourcepub fn estimate_bytes(transcript: &[Message]) -> usize
pub fn estimate_bytes(transcript: &[Message]) -> usize
Estimate the prompt byte count of a transcript.
This is a rough proxy for token count (≈4 bytes/token for English; over-counts CJK ~3× because CJK chars are 3 UTF-8 bytes each). The agent uses this to decide whether compaction is needed before the next LLM call. It is intentionally byte-based and kept in bytes — the char/byte threshold calibration depends on it. Includes tool_calls arguments and reasoning_content which were previously omitted, causing the threshold to be systematically underestimated.
Sourcepub fn should_compact(
&self,
estimate_bytes: usize,
last_prompt_tokens: u32,
) -> bool
pub fn should_compact( &self, estimate_bytes: usize, last_prompt_tokens: u32, ) -> bool
Decide whether compaction should run, preferring the token-based
threshold (accurate, CJK-safe) when actual API prompt_tokens are
available, falling back to the byte estimate.
last_prompt_tokens of 0 means “no API reading yet” (first step,
or a provider that never reports usage) — the byte estimate is used.
Sourcepub fn safe_split_point(transcript: &[Message], keep_n: usize) -> usize
pub fn safe_split_point(transcript: &[Message], keep_n: usize) -> usize
Compute the safe split point for compaction: the index at which to divide “older messages to summarize” from “recent messages to keep”.
The retained (“kept”) segment must start with a User (or System) message, because OpenAI/Anthropic require that the first non-System message after the compaction summary is a User message. Two cases can violate this invariant:
- The split point lands on a
Toolresult message — backing up preserves tool-call / tool-result pairs. - The split point lands on an
Assistantmessage that carriestool_calls— backing up avoids starting the kept segment with an Assistant-with-tool-calls, which would produce an invalid[System(summary), Assistant(tool_calls), Tool, …]sequence.
Sourcepub fn would_compact(&self, transcript: &[Message]) -> bool
pub fn would_compact(&self, transcript: &[Message]) -> bool
Returns true iff [apply_to_transcript] would actually compact this
transcript (return Ok(Some(..))): long enough AND the older slice has
enough real conversation. Pure — no provider call, no mutation.
Proactive-compaction callers should consult this before dispatching
PreCompact so that PreCompact and PostCompact stay balanced (both
fire when compaction runs, neither fires when it is rejected). Without
this guard the Goal 345 degenerate-slice rejection made PreCompact
fire without a matching PostCompact. (Goal 345)
pub async fn apply_to_transcript( &self, provider: &dyn ChatProvider, transcript: &mut Vec<Message>, step: usize, ) -> Result<Option<(usize, usize)>>
Sourcepub async fn compact(
&self,
provider: &dyn ChatProvider,
transcript: &[Message],
step: usize,
) -> Result<Message>
pub async fn compact( &self, provider: &dyn ChatProvider, transcript: &[Message], step: usize, ) -> Result<Message>
Compact the transcript: summarize older messages into a single system
message, keeping the last keep_recent_n messages verbatim.
step is the current turn number and is embedded in the compaction
header for debuggability.
Returns the summary Message that should replace the older portion.
The caller is responsible for splicing it into the transcript.
If the summarisation call returns a context-window-exceeded error,
the oldest message groups are dropped (via truncate_head_for_retry)
and the call is retried up to MAX_PTL_RETRIES times.