#[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub struct EpisodicCompactionConfig {
pub trigger_context_tokens: Option<usize>,
pub trigger_unfinished_turns: usize,
pub trigger_age_days: u64,
pub trigger_age_turn_count: usize,
}
impl Default for EpisodicCompactionConfig {
fn default() -> Self {
Self {
trigger_context_tokens: None,
trigger_unfinished_turns: 8,
trigger_age_days: 30,
trigger_age_turn_count: 50,
}
}
}
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub enum EpisodicCompactionTrigger {
ContextPressure,
UnfinishedTurns,
AgeAndTurns,
ManualRecovery,
}
impl EpisodicCompactionTrigger {
pub const fn as_str(self) -> &'static str {
match self {
Self::ContextPressure => "context_pressure",
Self::UnfinishedTurns => "unfinished_turns",
Self::AgeAndTurns => "age_and_turns",
Self::ManualRecovery => "manual_recovery",
}
}
}
#[derive(Debug, Clone, Copy, Default, PartialEq, Eq)]
pub struct EpisodicCompactionPressure {
pub estimated_context_tokens: Option<usize>,
pub user_messages_since_boundary: usize,
pub run_completed_since_boundary: usize,
pub user_messages_after_latest_run_completed: usize,
pub conversation_age_days: u64,
pub total_user_messages: usize,
}
pub fn episodic_compaction_trigger(
pressure: &EpisodicCompactionPressure,
config: &EpisodicCompactionConfig,
) -> Option<EpisodicCompactionTrigger> {
if let (Some(estimated), Some(trigger)) = (
pressure.estimated_context_tokens,
config.trigger_context_tokens,
) {
if estimated >= trigger {
return Some(EpisodicCompactionTrigger::ContextPressure);
}
}
if pressure.user_messages_after_latest_run_completed >= config.trigger_unfinished_turns {
return Some(EpisodicCompactionTrigger::UnfinishedTurns);
}
if pressure.conversation_age_days >= config.trigger_age_days
&& pressure.total_user_messages >= config.trigger_age_turn_count
{
return Some(EpisodicCompactionTrigger::AgeAndTurns);
}
None
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn context_pressure_wins_before_a_provider_overflow() {
let pressure = EpisodicCompactionPressure {
estimated_context_tokens: Some(408_000),
user_messages_since_boundary: 80,
run_completed_since_boundary: 80,
conversation_age_days: 1,
total_user_messages: 80,
..EpisodicCompactionPressure::default()
};
let config = EpisodicCompactionConfig {
trigger_context_tokens: Some(400_000),
..EpisodicCompactionConfig::default()
};
assert_eq!(
episodic_compaction_trigger(&pressure, &config),
Some(EpisodicCompactionTrigger::ContextPressure)
);
}
#[test]
fn unfinished_turns_only_count_after_the_latest_clean_completion() {
let pressure = EpisodicCompactionPressure {
user_messages_since_boundary: 20,
run_completed_since_boundary: 1,
user_messages_after_latest_run_completed: 9,
conversation_age_days: 1,
total_user_messages: 20,
..EpisodicCompactionPressure::default()
};
assert_eq!(
episodic_compaction_trigger(&pressure, &EpisodicCompactionConfig::default()),
Some(EpisodicCompactionTrigger::UnfinishedTurns)
);
}
#[test]
fn a_recent_clean_completion_prevents_the_unfinished_turn_trigger() {
let pressure = EpisodicCompactionPressure {
user_messages_since_boundary: 20,
run_completed_since_boundary: 1,
user_messages_after_latest_run_completed: 2,
conversation_age_days: 1,
total_user_messages: 20,
..EpisodicCompactionPressure::default()
};
assert_eq!(
episodic_compaction_trigger(&pressure, &EpisodicCompactionConfig::default()),
None
);
}
#[test]
fn context_pressure_is_disabled_until_the_runtime_supplies_a_limit() {
let pressure = EpisodicCompactionPressure {
estimated_context_tokens: Some(900_000),
user_messages_since_boundary: 80,
run_completed_since_boundary: 80,
conversation_age_days: 1,
total_user_messages: 80,
..EpisodicCompactionPressure::default()
};
assert_eq!(
episodic_compaction_trigger(&pressure, &EpisodicCompactionConfig::default()),
None
);
}
#[test]
fn age_trigger_requires_both_age_and_turn_count() {
let sparse = EpisodicCompactionPressure {
conversation_age_days: 60,
total_user_messages: 10,
..EpisodicCompactionPressure::default()
};
assert_eq!(
episodic_compaction_trigger(&sparse, &EpisodicCompactionConfig::default()),
None
);
let old_busy = EpisodicCompactionPressure {
conversation_age_days: 60,
total_user_messages: 200,
..EpisodicCompactionPressure::default()
};
assert_eq!(
episodic_compaction_trigger(&old_busy, &EpisodicCompactionConfig::default()),
Some(EpisodicCompactionTrigger::AgeAndTurns)
);
}
#[test]
fn trigger_names_are_stable_for_store_metadata() {
assert_eq!(
EpisodicCompactionTrigger::ContextPressure.as_str(),
"context_pressure"
);
assert_eq!(
EpisodicCompactionTrigger::ManualRecovery.as_str(),
"manual_recovery"
);
}
}