use crate::agent::AgentContext;
use crate::error::{ConfigError, Error};
use crate::ids::ThreadId;
use crate::llm::{Message, Role};
use crate::summarize::{
summarize_history, visible_window_tokens, SummarizeOptions, WINDOW_MULTIPLE_OF_TRIGGER,
};
const SUMMARY_MESSAGE_PREFIX: &str = "[compacted summary of earlier turns]\n";
pub(crate) async fn maybe_compact(
ctx: &AgentContext,
thread: &ThreadId,
compaction: Option<&SummarizeOptions>,
max_history_tokens: usize,
) -> Result<(), Error> {
let Some(opts) = compaction else {
return Ok(());
};
if opts.trigger_token_budget == 0 {
return Err(Error::Config(ConfigError::InvalidValue {
key: "compaction.trigger_token_budget".into(),
reason: "must be non-zero when compaction is enabled; disable via \
RunOptions::without_compaction() instead of setting a zero budget"
.into(),
}));
}
let window = visible_window_tokens(opts);
if window < max_history_tokens {
return Err(Error::Config(ConfigError::InvalidValue {
key: "compaction.trigger_token_budget".into(),
reason: format!(
"trigger_token_budget * {WINDOW_MULTIPLE_OF_TRIGGER} = {window} is below \
max_history_tokens = {max_history_tokens}, so a thread the request and resume \
paths can restore would exceed the window summarize_history can see; history \
past the window is never summarised and would then be discarded by compaction. \
Raise compaction.trigger_token_budget to at least \
{min_trigger}, or lower max_history_tokens via \
RunOptions::with_max_history_tokens()",
min_trigger = max_history_tokens.div_ceil(WINDOW_MULTIPLE_OF_TRIGGER),
),
}));
}
match summarize_history(ctx, thread.clone(), opts.clone()).await? {
Some(summary) => rewrite_short_term_with_summary(ctx, thread, opts, summary).await,
None => Ok(()),
}
}
async fn rewrite_short_term_with_summary(
ctx: &AgentContext,
thread: &ThreadId,
opts: &SummarizeOptions,
summary: String,
) -> Result<(), Error> {
let history = ctx
.short_term
.load(thread.clone(), visible_window_tokens(opts))
.await?;
let split = history.len().saturating_sub(opts.keep_recent_messages);
let tail = &history[split..];
let mut rewritten = Vec::with_capacity(tail.len() + 1);
rewritten.push(Message {
role: Role::System,
content: format!("{SUMMARY_MESSAGE_PREFIX}{summary}"),
tool_calls: vec![],
tool_call_id: None,
});
rewritten.extend_from_slice(tail);
ctx.short_term.clear(thread.clone()).await?;
ctx.short_term
.append_batch(thread.clone(), rewritten)
.await?;
Ok(())
}
#[cfg(test)]
#[path = "compaction_tests.rs"]
mod tests;