use meerkat_core::Config;
use meerkat_core::config::CompactionRuntimeConfig;
pub const COMPACTION_POLICY_KEYS: [&str; 4] = [
"auto_compact_threshold",
"recent_turn_budget",
"max_summary_tokens",
"min_turns_between_compactions",
];
pub fn parse_compaction_policy(
value: &serde_json::Value,
) -> Result<CompactionRuntimeConfig, String> {
let object = value
.as_object()
.ok_or_else(|| "must be a JSON object".to_string())?;
let unsupported = object
.keys()
.filter(|key| !COMPACTION_POLICY_KEYS.contains(&key.as_str()))
.map(String::as_str)
.collect::<Vec<_>>();
if !unsupported.is_empty() {
return Err(format!(
"carries unsupported fields: {}",
unsupported.join(", ")
));
}
let policy: CompactionRuntimeConfig =
serde_json::from_value(value.clone()).map_err(|error| format!("is invalid: {error}"))?;
validate_compaction_policy(&policy)?;
Ok(policy)
}
pub fn validate_compaction_policy(policy: &CompactionRuntimeConfig) -> Result<(), String> {
if policy.auto_compact_threshold == 0 {
return Err("auto_compact_threshold must be greater than 0".to_string());
}
Ok(())
}
pub fn apply_compaction_policy(
config: &mut Config,
policy: &CompactionRuntimeConfig,
) -> Result<(), String> {
validate_compaction_policy(policy)?;
tracing::info!(
auto_compact_threshold = policy.auto_compact_threshold,
threshold_pinned = policy.auto_compact_threshold_explicit,
recent_turn_budget = policy.recent_turn_budget,
max_summary_tokens = policy.max_summary_tokens,
min_turns_between_compactions = policy.min_turns_between_compactions,
"host compaction policy applied to the session-build config"
);
config.compaction = policy.clone();
Ok(())
}
#[cfg(test)]
#[allow(clippy::expect_used, clippy::panic)]
mod tests {
use super::*;
use serde_json::json;
#[test]
fn declared_threshold_pins_against_model_aware_scaling() {
let policy =
parse_compaction_policy(&json!({ "auto_compact_threshold": 120_000 })).expect("parses");
assert_eq!(policy.auto_compact_threshold, 120_000);
assert!(
policy.auto_compact_threshold_explicit,
"a declared threshold must be explicit or meerkat rescales it to the model window",
);
let mut config = Config::default();
assert!(
!config.compaction.auto_compact_threshold_explicit,
"the un-configured baseline is the inheriting form",
);
apply_compaction_policy(&mut config, &policy).expect("applies");
assert_eq!(config.compaction.auto_compact_threshold, 120_000);
assert!(config.compaction.auto_compact_threshold_explicit);
}
#[test]
fn omitted_threshold_keeps_inheriting() {
let policy = parse_compaction_policy(&json!({ "recent_turn_budget": 8 })).expect("parses");
assert_eq!(policy.recent_turn_budget, 8);
assert!(!policy.auto_compact_threshold_explicit);
assert_eq!(
policy.auto_compact_threshold,
CompactionRuntimeConfig::default().auto_compact_threshold,
);
}
#[test]
fn zero_threshold_is_refused() {
let error = parse_compaction_policy(&json!({ "auto_compact_threshold": 0 }))
.expect_err("zero must fail closed");
assert!(error.contains("greater than 0"), "{error}");
}
#[test]
fn unknown_keys_are_refused() {
let error = parse_compaction_policy(&json!({ "auto_compact_treshold": 100 }))
.expect_err("typos must fail closed");
assert!(error.contains("auto_compact_treshold"), "{error}");
let error = parse_compaction_policy(&json!("100000"))
.expect_err("a scalar is not a compaction declaration");
assert!(error.contains("JSON object"), "{error}");
}
#[test]
fn wrong_field_type_is_refused() {
let error = parse_compaction_policy(&json!({ "auto_compact_threshold": "lots" }))
.expect_err("a string threshold must fail closed");
assert!(error.contains("is invalid"), "{error}");
}
#[test]
fn every_field_round_trips() {
let policy = parse_compaction_policy(&json!({
"auto_compact_threshold": 90_000,
"recent_turn_budget": 6,
"max_summary_tokens": 8192,
"min_turns_between_compactions": 5,
}))
.expect("parses");
assert_eq!(policy.auto_compact_threshold, 90_000);
assert_eq!(policy.recent_turn_budget, 6);
assert_eq!(policy.max_summary_tokens, 8192);
assert_eq!(policy.min_turns_between_compactions, 5);
}
}