use saya_agent::{CONTEXT_COMPACT_PERCENT, context_utilisation_percent};
use saya_config::CompactionMode;
pub(crate) struct AutoCompactInput {
pub(crate) mode: CompactionMode,
pub(crate) answering_input: Option<u64>,
pub(crate) window: Option<u64>,
pub(crate) continued: bool,
pub(crate) compact_running: bool,
pub(crate) auto_failed: bool,
}
pub(crate) fn should_auto_compact(input: &AutoCompactInput) -> bool {
if input.mode != CompactionMode::Auto {
return false;
}
if input.continued || input.compact_running || input.auto_failed {
return false;
}
let Some(percent) = context_utilisation_percent(input.answering_input, input.window) else {
return false;
};
percent >= CONTEXT_COMPACT_PERCENT
}
pub(crate) fn auto_success_message(compacted_turns: usize, summary: &str) -> String {
format!(
"Automatic compaction: {}",
super::session_compact::success_message(compacted_turns, summary)
)
}
pub(crate) fn auto_failure_message(reason: &str) -> String {
format!(
"Automatic compaction: {}. It will not retry on its own; run /compact to try again.",
super::session_compact::failure_message(reason)
)
}
pub(crate) fn warning_enabled(mode: CompactionMode) -> bool {
mode != CompactionMode::Off
}
#[cfg(test)]
mod tests {
use super::*;
fn input(answering_input: Option<u64>, window: Option<u64>) -> AutoCompactInput {
AutoCompactInput {
mode: CompactionMode::Auto,
answering_input,
window,
continued: false,
compact_running: false,
auto_failed: false,
}
}
#[test]
fn crossing_95_fires() {
assert!(should_auto_compact(&input(Some(121_600), Some(128_000))));
}
#[test]
fn past_100_is_a_real_case_that_fires() {
assert!(should_auto_compact(&input(Some(200_000), Some(128_000))));
}
#[test]
fn below_95_does_not_fire() {
assert!(!should_auto_compact(&input(Some(64_000), Some(128_000))));
}
#[test]
fn unknown_window_never_fires() {
assert!(!should_auto_compact(&input(Some(122_880), None)));
}
#[test]
fn no_provider_report_never_fires_absent_is_not_zero() {
assert!(!should_auto_compact(&input(None, Some(128_000))));
}
#[test]
fn manual_and_off_never_fire() {
for mode in [CompactionMode::Manual, CompactionMode::Off] {
assert!(
!should_auto_compact(&AutoCompactInput {
mode,
..input(Some(122_880), Some(128_000))
}),
"{mode:?} must never fire automatically"
);
}
}
#[test]
fn mid_continuation_never_fires() {
assert!(
!should_auto_compact(&AutoCompactInput {
continued: true,
..input(Some(122_880), Some(128_000))
}),
"a continued turn must not compact: the resume anchors live in earlier tool results"
);
}
#[test]
fn a_running_compaction_and_a_failed_one_both_suppress() {
assert!(!should_auto_compact(&AutoCompactInput {
compact_running: true,
..input(Some(122_880), Some(128_000))
}));
assert!(!should_auto_compact(&AutoCompactInput {
auto_failed: true,
..input(Some(122_880), Some(128_000))
}));
}
#[test]
fn the_automatic_strings_reuse_the_manual_path_with_origin_stated() {
let manual = super::super::session_compact::success_message(3, "older turns at length");
assert_eq!(
auto_success_message(3, "older turns at length"),
format!("Automatic compaction: {manual}")
);
let manual_fail =
super::super::session_compact::failure_message("the summariser timed out");
let auto = auto_failure_message("the summariser timed out");
assert!(auto.starts_with("Automatic compaction: "));
assert!(auto.contains(&manual_fail));
assert!(auto.contains("/compact"));
}
#[test]
fn off_silences_the_warning_while_other_modes_warn() {
assert!(!warning_enabled(CompactionMode::Off));
assert!(warning_enabled(CompactionMode::Auto));
assert!(warning_enabled(CompactionMode::Manual));
}
}