use crate::providers::ProviderName;
use schemars::JsonSchema;
use serde::{Deserialize, Serialize};
use super::CompactionProbeConfig;
#[derive(Debug, Clone, Deserialize, Serialize)]
#[serde(default)]
pub struct OpticalForgettingConfig {
pub enabled: bool,
pub compress_provider: ProviderName,
pub compress_after_turns: u32,
pub summarize_after_turns: u32,
pub sweep_interval_secs: u64,
pub sweep_batch_size: usize,
}
impl Default for OpticalForgettingConfig {
fn default() -> Self {
Self {
enabled: false,
compress_provider: ProviderName::default(),
compress_after_turns: 100,
summarize_after_turns: 500,
sweep_interval_secs: 3600,
sweep_batch_size: 50,
}
}
}
#[derive(Debug, Clone, Deserialize, Serialize)]
#[serde(default)]
pub struct DigestConfig {
pub enabled: bool,
#[serde(default)]
pub provider: Option<ProviderName>,
pub max_tokens: usize,
pub max_input_messages: usize,
}
impl Default for DigestConfig {
fn default() -> Self {
Self {
enabled: false,
provider: None,
max_tokens: 500,
max_input_messages: 50,
}
}
}
#[derive(Debug, Clone, Default, Deserialize, Serialize, PartialEq)]
#[serde(tag = "strategy", rename_all = "snake_case")]
#[non_exhaustive]
pub enum CompressionStrategy {
#[default]
Reactive,
Proactive {
threshold_tokens: usize,
max_summary_tokens: usize,
},
Autonomous,
Focus,
}
#[derive(Debug, Clone, Copy, Default, Serialize, PartialEq, Eq)]
#[serde(rename_all = "snake_case")]
#[non_exhaustive]
pub enum PruningStrategy {
#[default]
Reactive,
TaskAware,
Mig,
Subgoal,
SubgoalMig,
}
impl PruningStrategy {
#[must_use]
pub fn is_subgoal(self) -> bool {
matches!(self, Self::Subgoal | Self::SubgoalMig)
}
}
impl<'de> serde::Deserialize<'de> for PruningStrategy {
fn deserialize<D: serde::Deserializer<'de>>(deserializer: D) -> Result<Self, D::Error> {
let s = String::deserialize(deserializer)?;
s.parse().map_err(serde::de::Error::custom)
}
}
impl std::str::FromStr for PruningStrategy {
type Err = String;
fn from_str(s: &str) -> Result<Self, Self::Err> {
match s {
"reactive" => Ok(Self::Reactive),
"task_aware" | "task-aware" => Ok(Self::TaskAware),
"mig" => Ok(Self::Mig),
"task_aware_mig" | "task-aware-mig" => {
tracing::warn!(
"pruning strategy `task_aware_mig` has been removed; \
falling back to `reactive`. Use `task_aware` or `mig` instead."
);
Ok(Self::Reactive)
}
"subgoal" => Ok(Self::Subgoal),
"subgoal_mig" | "subgoal-mig" => Ok(Self::SubgoalMig),
other => Err(format!(
"unknown pruning strategy `{other}`, expected \
reactive|task_aware|mig|subgoal|subgoal_mig"
)),
}
}
}
fn default_high_density_budget() -> f32 {
0.7
}
fn default_low_density_budget() -> f32 {
0.3
}
#[derive(Debug, Clone, Deserialize, Serialize)]
#[serde(default)]
pub struct ForgettingConfig {
pub enabled: bool,
pub decay_rate: f32,
pub forgetting_floor: f32,
pub sweep_interval_secs: u64,
pub sweep_batch_size: usize,
pub replay_window_hours: u32,
pub replay_min_access_count: u32,
pub protect_recent_hours: u32,
pub protect_min_access_count: u32,
}
impl Default for ForgettingConfig {
fn default() -> Self {
Self {
enabled: false,
decay_rate: 0.1,
forgetting_floor: 0.05,
sweep_interval_secs: 7200,
sweep_batch_size: 500,
replay_window_hours: 24,
replay_min_access_count: 3,
protect_recent_hours: 24,
protect_min_access_count: 3,
}
}
}
#[derive(Debug, Clone, Default, Deserialize, Serialize)]
#[serde(default)]
pub struct CompressionConfig {
#[serde(flatten)]
pub strategy: CompressionStrategy,
pub pruning_strategy: PruningStrategy,
pub model: String,
pub compress_provider: ProviderName,
#[serde(default)]
pub probe: CompactionProbeConfig,
#[serde(default)]
pub archive_tool_outputs: bool,
pub focus_scorer_provider: ProviderName,
#[serde(default = "default_high_density_budget")]
pub high_density_budget: f32,
#[serde(default = "default_low_density_budget")]
pub low_density_budget: f32,
#[serde(default)]
pub typed_pages: TypedPagesConfig,
#[serde(default)]
pub acon: AconConfig,
#[serde(default)]
pub arc: ArcCompactionConfig,
}
fn default_acon_passthrough_threshold() -> usize {
2000
}
fn default_acon_summarize_threshold() -> usize {
4000
}
fn default_acon_total_budget() -> usize {
8000
}
fn validate_acon_passthrough_threshold<'de, D>(deserializer: D) -> Result<usize, D::Error>
where
D: serde::Deserializer<'de>,
{
let value = <usize as serde::Deserialize>::deserialize(deserializer)?;
if value == 0 {
return Err(serde::de::Error::custom(
"acon.passthrough_threshold must be >= 1",
));
}
Ok(value)
}
fn validate_acon_summarize_threshold<'de, D>(deserializer: D) -> Result<usize, D::Error>
where
D: serde::Deserializer<'de>,
{
let value = <usize as serde::Deserialize>::deserialize(deserializer)?;
if value == 0 {
return Err(serde::de::Error::custom(
"acon.summarize_threshold must be >= 1",
));
}
Ok(value)
}
fn validate_acon_total_budget<'de, D>(deserializer: D) -> Result<usize, D::Error>
where
D: serde::Deserializer<'de>,
{
let value = <usize as serde::Deserialize>::deserialize(deserializer)?;
if value == 0 {
return Err(serde::de::Error::custom("acon.total_budget must be >= 1"));
}
Ok(value)
}
#[derive(Debug, Clone, Deserialize, Serialize)]
#[serde(default)]
pub struct AconConfig {
pub enabled: bool,
#[serde(default = "default_acon_passthrough_threshold")]
#[serde(deserialize_with = "validate_acon_passthrough_threshold")]
pub passthrough_threshold: usize,
#[serde(default = "default_acon_summarize_threshold")]
#[serde(deserialize_with = "validate_acon_summarize_threshold")]
pub summarize_threshold: usize,
#[serde(default = "default_acon_total_budget")]
#[serde(deserialize_with = "validate_acon_total_budget")]
pub total_budget: usize,
#[serde(default)]
pub summarize_provider: ProviderName,
}
impl AconConfig {
#[must_use = "validation result must be checked"]
pub fn validate(&self) -> Result<(), String> {
if self.passthrough_threshold >= self.summarize_threshold {
return Err(format!(
"acon: passthrough_threshold ({}) must be < summarize_threshold ({})",
self.passthrough_threshold, self.summarize_threshold
));
}
if self.summarize_threshold > self.total_budget {
return Err(format!(
"acon: summarize_threshold ({}) must be <= total_budget ({})",
self.summarize_threshold, self.total_budget
));
}
Ok(())
}
}
impl Default for AconConfig {
fn default() -> Self {
Self {
enabled: true,
passthrough_threshold: default_acon_passthrough_threshold(),
summarize_threshold: default_acon_summarize_threshold(),
total_budget: default_acon_total_budget(),
summarize_provider: ProviderName::default(),
}
}
}
#[derive(Debug, Clone, Deserialize, Serialize)]
#[serde(default)]
pub struct ArcCompactionConfig {
pub allow_agent_compaction: bool,
}
impl Default for ArcCompactionConfig {
fn default() -> Self {
Self {
allow_agent_compaction: true,
}
}
}
#[derive(Debug, Clone, Deserialize, Serialize, JsonSchema)]
#[serde(default)]
pub struct TypedPagesConfig {
pub enabled: bool,
pub enforcement: TypedPagesEnforcement,
pub audit_path: String,
pub audit_channel_capacity: usize,
}
impl Default for TypedPagesConfig {
fn default() -> Self {
Self {
enabled: false,
enforcement: TypedPagesEnforcement::Observe,
audit_path: String::new(),
audit_channel_capacity: 256,
}
}
}
#[derive(Debug, Clone, Copy, PartialEq, Eq, Default, Deserialize, Serialize, JsonSchema)]
#[serde(rename_all = "snake_case")]
#[non_exhaustive]
pub enum TypedPagesEnforcement {
#[default]
Observe,
Active,
}
#[derive(Debug, Clone, serde::Deserialize, serde::Serialize)]
#[serde(default)]
pub struct MicrocompactConfig {
pub enabled: bool,
pub gap_threshold_minutes: u32,
pub keep_recent: usize,
}
impl Default for MicrocompactConfig {
fn default() -> Self {
Self {
enabled: false,
gap_threshold_minutes: 60,
keep_recent: 3,
}
}
}
#[derive(Debug, Clone, Copy, PartialEq, Eq, Default, Deserialize, Serialize)]
#[serde(rename_all = "lowercase")]
#[non_exhaustive]
pub enum EvictionPolicy {
#[default]
Ebbinghaus,
}
#[derive(Debug, Clone, Deserialize, Serialize)]
pub struct EvictionConfig {
pub policy: EvictionPolicy,
pub max_entries: usize,
pub sweep_interval_secs: u64,
}
impl Default for EvictionConfig {
fn default() -> Self {
Self {
policy: EvictionPolicy::Ebbinghaus,
max_entries: 0,
sweep_interval_secs: 3600,
}
}
}
#[derive(Debug, Clone, Deserialize, Serialize)]
#[serde(default)]
pub struct CompressionGuidelinesConfig {
pub enabled: bool,
pub update_threshold: u16,
pub max_guidelines_tokens: usize,
pub max_pairs_per_update: usize,
pub detection_window_turns: u64,
pub update_interval_secs: u64,
pub max_stored_pairs: usize,
#[serde(default, skip_serializing_if = "Option::is_none")]
pub guidelines_provider: Option<ProviderName>,
#[serde(default)]
pub categorized_guidelines: bool,
}
impl Default for CompressionGuidelinesConfig {
fn default() -> Self {
Self {
enabled: false,
update_threshold: 5,
max_guidelines_tokens: 500,
max_pairs_per_update: 10,
detection_window_turns: 10,
update_interval_secs: 300,
max_stored_pairs: 100,
guidelines_provider: None,
categorized_guidelines: false,
}
}
}