use serde::{Deserialize, Serialize};
use crate::defaults::{default_sqlite_path_field, default_true};
use crate::providers::ProviderName;
fn default_sqlite_pool_size() -> u32 {
5
}
fn default_max_history() -> usize {
100
}
fn default_title_max_chars() -> usize {
60
}
fn default_document_collection() -> String {
"zeph_documents".into()
}
fn default_document_chunk_size() -> usize {
1000
}
fn default_document_chunk_overlap() -> usize {
100
}
fn default_document_top_k() -> usize {
3
}
fn default_autosave_min_length() -> usize {
20
}
fn default_tool_call_cutoff() -> usize {
6
}
fn default_token_safety_margin() -> f32 {
1.0
}
fn default_redact_credentials() -> bool {
true
}
fn default_qdrant_url() -> String {
"http://localhost:6334".into()
}
fn default_summarization_threshold() -> usize {
50
}
fn default_context_budget_tokens() -> usize {
0
}
fn default_soft_compaction_threshold() -> f32 {
0.60
}
fn default_hard_compaction_threshold() -> f32 {
0.90
}
fn default_compaction_preserve_tail() -> usize {
6
}
fn default_compaction_cooldown_turns() -> u8 {
2
}
fn default_auto_budget() -> bool {
true
}
fn default_prune_protect_tokens() -> usize {
40_000
}
fn default_cross_session_score_threshold() -> f32 {
0.35
}
fn default_temporal_decay_half_life_days() -> u32 {
30
}
fn default_mmr_lambda() -> f32 {
0.7
}
fn default_semantic_enabled() -> bool {
true
}
fn default_recall_limit() -> usize {
5
}
fn default_vector_weight() -> f64 {
0.7
}
fn default_keyword_weight() -> f64 {
0.3
}
fn default_graph_max_entities_per_message() -> usize {
10
}
fn default_graph_max_edges_per_message() -> usize {
15
}
fn default_graph_community_refresh_interval() -> usize {
100
}
fn default_graph_community_summary_max_prompt_bytes() -> usize {
8192
}
fn default_graph_community_summary_concurrency() -> usize {
4
}
fn default_lpa_edge_chunk_size() -> usize {
10_000
}
fn default_graph_entity_similarity_threshold() -> f32 {
0.85
}
fn default_graph_entity_ambiguous_threshold() -> f32 {
0.70
}
fn default_graph_extraction_timeout_secs() -> u64 {
15
}
fn default_graph_max_hops() -> u32 {
2
}
fn default_graph_recall_limit() -> usize {
10
}
fn default_graph_expired_edge_retention_days() -> u32 {
90
}
fn default_graph_temporal_decay_rate() -> f64 {
0.0
}
fn default_graph_edge_history_limit() -> usize {
100
}
fn default_spreading_activation_decay_lambda() -> f32 {
0.85
}
fn default_spreading_activation_max_hops() -> u32 {
3
}
fn default_spreading_activation_activation_threshold() -> f32 {
0.1
}
fn default_spreading_activation_inhibition_threshold() -> f32 {
0.8
}
fn default_spreading_activation_max_activated_nodes() -> usize {
50
}
fn default_spreading_activation_recall_timeout_ms() -> u64 {
1000
}
fn default_note_linking_similarity_threshold() -> f32 {
0.85
}
fn default_note_linking_top_k() -> usize {
10
}
fn default_note_linking_timeout_secs() -> u64 {
5
}
fn default_shutdown_summary() -> bool {
true
}
fn default_shutdown_summary_min_messages() -> usize {
4
}
fn default_shutdown_summary_max_messages() -> usize {
20
}
fn default_shutdown_summary_timeout_secs() -> u64 {
10
}
fn validate_tier_similarity_threshold<'de, D>(deserializer: D) -> Result<f32, D::Error>
where
D: serde::Deserializer<'de>,
{
let value = <f32 as serde::Deserialize>::deserialize(deserializer)?;
if value.is_nan() || value.is_infinite() {
return Err(serde::de::Error::custom(
"similarity_threshold must be a finite number",
));
}
if !(0.5..=1.0).contains(&value) {
return Err(serde::de::Error::custom(
"similarity_threshold must be in [0.5, 1.0]",
));
}
Ok(value)
}
fn validate_tier_promotion_min_sessions<'de, D>(deserializer: D) -> Result<u32, D::Error>
where
D: serde::Deserializer<'de>,
{
let value = <u32 as serde::Deserialize>::deserialize(deserializer)?;
if value < 2 {
return Err(serde::de::Error::custom(
"promotion_min_sessions must be >= 2",
));
}
Ok(value)
}
fn validate_tier_sweep_batch_size<'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("sweep_batch_size must be >= 1"));
}
Ok(value)
}
fn default_tier_promotion_min_sessions() -> u32 {
3
}
fn default_tier_similarity_threshold() -> f32 {
0.92
}
fn default_tier_sweep_interval_secs() -> u64 {
3600
}
fn default_tier_sweep_batch_size() -> usize {
100
}
fn default_scene_similarity_threshold() -> f32 {
0.80
}
fn default_scene_batch_size() -> usize {
50
}
fn validate_scene_similarity_threshold<'de, D>(deserializer: D) -> Result<f32, D::Error>
where
D: serde::Deserializer<'de>,
{
let value = <f32 as serde::Deserialize>::deserialize(deserializer)?;
if value.is_nan() || value.is_infinite() {
return Err(serde::de::Error::custom(
"scene_similarity_threshold must be a finite number",
));
}
if !(0.5..=1.0).contains(&value) {
return Err(serde::de::Error::custom(
"scene_similarity_threshold must be in [0.5, 1.0]",
));
}
Ok(value)
}
fn validate_scene_batch_size<'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("scene_batch_size must be >= 1"));
}
Ok(value)
}
#[derive(Debug, Clone, serde::Deserialize, serde::Serialize)]
#[serde(default)]
pub struct TierConfig {
pub enabled: bool,
#[serde(deserialize_with = "validate_tier_promotion_min_sessions")]
pub promotion_min_sessions: u32,
#[serde(deserialize_with = "validate_tier_similarity_threshold")]
pub similarity_threshold: f32,
pub sweep_interval_secs: u64,
#[serde(deserialize_with = "validate_tier_sweep_batch_size")]
pub sweep_batch_size: usize,
pub scene_enabled: bool,
#[serde(deserialize_with = "validate_scene_similarity_threshold")]
pub scene_similarity_threshold: f32,
#[serde(deserialize_with = "validate_scene_batch_size")]
pub scene_batch_size: usize,
pub scene_provider: ProviderName,
pub scene_sweep_interval_secs: u64,
}
fn default_scene_sweep_interval_secs() -> u64 {
7200
}
impl Default for TierConfig {
fn default() -> Self {
Self {
enabled: false,
promotion_min_sessions: default_tier_promotion_min_sessions(),
similarity_threshold: default_tier_similarity_threshold(),
sweep_interval_secs: default_tier_sweep_interval_secs(),
sweep_batch_size: default_tier_sweep_batch_size(),
scene_enabled: false,
scene_similarity_threshold: default_scene_similarity_threshold(),
scene_batch_size: default_scene_batch_size(),
scene_provider: ProviderName::default(),
scene_sweep_interval_secs: default_scene_sweep_interval_secs(),
}
}
}
fn validate_temporal_decay_rate<'de, D>(deserializer: D) -> Result<f64, D::Error>
where
D: serde::Deserializer<'de>,
{
let value = <f64 as serde::Deserialize>::deserialize(deserializer)?;
if value.is_nan() || value.is_infinite() {
return Err(serde::de::Error::custom(
"temporal_decay_rate must be a finite number",
));
}
if !(0.0..=10.0).contains(&value) {
return Err(serde::de::Error::custom(
"temporal_decay_rate must be in [0.0, 10.0]",
));
}
Ok(value)
}
fn validate_similarity_threshold<'de, D>(deserializer: D) -> Result<f32, D::Error>
where
D: serde::Deserializer<'de>,
{
let value = <f32 as serde::Deserialize>::deserialize(deserializer)?;
if value.is_nan() || value.is_infinite() {
return Err(serde::de::Error::custom(
"similarity_threshold must be a finite number",
));
}
if !(0.0..=1.0).contains(&value) {
return Err(serde::de::Error::custom(
"similarity_threshold must be in [0.0, 1.0]",
));
}
Ok(value)
}
fn validate_importance_weight<'de, D>(deserializer: D) -> Result<f64, D::Error>
where
D: serde::Deserializer<'de>,
{
let value = <f64 as serde::Deserialize>::deserialize(deserializer)?;
if value.is_nan() || value.is_infinite() {
return Err(serde::de::Error::custom(
"importance_weight must be a finite number",
));
}
if value < 0.0 {
return Err(serde::de::Error::custom(
"importance_weight must be non-negative",
));
}
if value > 1.0 {
return Err(serde::de::Error::custom("importance_weight must be <= 1.0"));
}
Ok(value)
}
fn default_importance_weight() -> f64 {
0.15
}
#[derive(Debug, Clone, Deserialize, Serialize)]
#[serde(default)]
pub struct SpreadingActivationConfig {
pub enabled: bool,
#[serde(deserialize_with = "validate_decay_lambda")]
pub decay_lambda: f32,
#[serde(deserialize_with = "validate_max_hops")]
pub max_hops: u32,
pub activation_threshold: f32,
pub inhibition_threshold: f32,
pub max_activated_nodes: usize,
#[serde(default = "default_seed_structural_weight")]
pub seed_structural_weight: f32,
#[serde(default = "default_seed_community_cap")]
pub seed_community_cap: usize,
#[serde(default = "default_spreading_activation_recall_timeout_ms")]
pub recall_timeout_ms: u64,
}
fn validate_decay_lambda<'de, D>(deserializer: D) -> Result<f32, D::Error>
where
D: serde::Deserializer<'de>,
{
let value = <f32 as serde::Deserialize>::deserialize(deserializer)?;
if value.is_nan() || value.is_infinite() {
return Err(serde::de::Error::custom(
"decay_lambda must be a finite number",
));
}
if !(value > 0.0 && value <= 1.0) {
return Err(serde::de::Error::custom(
"decay_lambda must be in (0.0, 1.0]",
));
}
Ok(value)
}
fn validate_max_hops<'de, D>(deserializer: D) -> Result<u32, D::Error>
where
D: serde::Deserializer<'de>,
{
let value = <u32 as serde::Deserialize>::deserialize(deserializer)?;
if value == 0 {
return Err(serde::de::Error::custom("max_hops must be >= 1"));
}
Ok(value)
}
impl SpreadingActivationConfig {
pub fn validate(&self) -> Result<(), String> {
if self.activation_threshold >= self.inhibition_threshold {
return Err(format!(
"activation_threshold ({}) must be < inhibition_threshold ({})",
self.activation_threshold, self.inhibition_threshold
));
}
Ok(())
}
}
fn default_seed_structural_weight() -> f32 {
0.4
}
fn default_seed_community_cap() -> usize {
3
}
impl Default for SpreadingActivationConfig {
fn default() -> Self {
Self {
enabled: false,
decay_lambda: default_spreading_activation_decay_lambda(),
max_hops: default_spreading_activation_max_hops(),
activation_threshold: default_spreading_activation_activation_threshold(),
inhibition_threshold: default_spreading_activation_inhibition_threshold(),
max_activated_nodes: default_spreading_activation_max_activated_nodes(),
seed_structural_weight: default_seed_structural_weight(),
seed_community_cap: default_seed_community_cap(),
recall_timeout_ms: default_spreading_activation_recall_timeout_ms(),
}
}
}
#[derive(Debug, Clone, Deserialize, Serialize)]
#[serde(default)]
pub struct BeliefRevisionConfig {
pub enabled: bool,
#[serde(deserialize_with = "validate_similarity_threshold")]
pub similarity_threshold: f32,
}
fn default_belief_revision_similarity_threshold() -> f32 {
0.85
}
impl Default for BeliefRevisionConfig {
fn default() -> Self {
Self {
enabled: false,
similarity_threshold: default_belief_revision_similarity_threshold(),
}
}
}
#[derive(Debug, Clone, Deserialize, Serialize)]
#[serde(default)]
pub struct RpeConfig {
pub enabled: bool,
#[serde(deserialize_with = "validate_similarity_threshold")]
pub threshold: f32,
pub max_skip_turns: u32,
}
fn default_rpe_threshold() -> f32 {
0.3
}
fn default_rpe_max_skip_turns() -> u32 {
5
}
impl Default for RpeConfig {
fn default() -> Self {
Self {
enabled: false,
threshold: default_rpe_threshold(),
max_skip_turns: default_rpe_max_skip_turns(),
}
}
}
#[derive(Debug, Clone, Deserialize, Serialize)]
#[serde(default)]
pub struct NoteLinkingConfig {
pub enabled: bool,
#[serde(deserialize_with = "validate_similarity_threshold")]
pub similarity_threshold: f32,
pub top_k: usize,
pub timeout_secs: u64,
}
impl Default for NoteLinkingConfig {
fn default() -> Self {
Self {
enabled: false,
similarity_threshold: default_note_linking_similarity_threshold(),
top_k: default_note_linking_top_k(),
timeout_secs: default_note_linking_timeout_secs(),
}
}
}
#[derive(Debug, Clone, Copy, PartialEq, Eq, Default, Deserialize, Serialize)]
#[serde(rename_all = "lowercase")]
pub enum VectorBackend {
Qdrant,
#[default]
Sqlite,
}
impl VectorBackend {
#[must_use]
pub fn as_str(&self) -> &'static str {
match self {
Self::Qdrant => "qdrant",
Self::Sqlite => "sqlite",
}
}
}
#[derive(Debug, Deserialize, Serialize)]
#[allow(clippy::struct_excessive_bools)]
pub struct MemoryConfig {
#[serde(default)]
pub compression_guidelines: zeph_memory::CompressionGuidelinesConfig,
#[serde(default = "default_sqlite_path_field")]
pub sqlite_path: String,
pub history_limit: u32,
#[serde(default = "default_qdrant_url")]
pub qdrant_url: String,
#[serde(default)]
pub semantic: SemanticConfig,
#[serde(default = "default_summarization_threshold")]
pub summarization_threshold: usize,
#[serde(default = "default_context_budget_tokens")]
pub context_budget_tokens: usize,
#[serde(default = "default_soft_compaction_threshold")]
pub soft_compaction_threshold: f32,
#[serde(
default = "default_hard_compaction_threshold",
alias = "compaction_threshold"
)]
pub hard_compaction_threshold: f32,
#[serde(default = "default_compaction_preserve_tail")]
pub compaction_preserve_tail: usize,
#[serde(default = "default_compaction_cooldown_turns")]
pub compaction_cooldown_turns: u8,
#[serde(default = "default_auto_budget")]
pub auto_budget: bool,
#[serde(default = "default_prune_protect_tokens")]
pub prune_protect_tokens: usize,
#[serde(default = "default_cross_session_score_threshold")]
pub cross_session_score_threshold: f32,
#[serde(default)]
pub vector_backend: VectorBackend,
#[serde(default = "default_token_safety_margin")]
pub token_safety_margin: f32,
#[serde(default = "default_redact_credentials")]
pub redact_credentials: bool,
#[serde(default = "default_true")]
pub autosave_assistant: bool,
#[serde(default = "default_autosave_min_length")]
pub autosave_min_length: usize,
#[serde(default = "default_tool_call_cutoff")]
pub tool_call_cutoff: usize,
#[serde(default = "default_sqlite_pool_size")]
pub sqlite_pool_size: u32,
#[serde(default)]
pub sessions: SessionsConfig,
#[serde(default)]
pub documents: DocumentConfig,
#[serde(default)]
pub eviction: zeph_memory::EvictionConfig,
#[serde(default)]
pub compression: CompressionConfig,
#[serde(default)]
pub sidequest: SidequestConfig,
#[serde(default)]
pub graph: GraphConfig,
#[serde(default = "default_shutdown_summary")]
pub shutdown_summary: bool,
#[serde(default = "default_shutdown_summary_min_messages")]
pub shutdown_summary_min_messages: usize,
#[serde(default = "default_shutdown_summary_max_messages")]
pub shutdown_summary_max_messages: usize,
#[serde(default = "default_shutdown_summary_timeout_secs")]
pub shutdown_summary_timeout_secs: u64,
#[serde(default)]
pub structured_summaries: bool,
#[serde(default)]
pub tiers: TierConfig,
#[serde(default)]
pub admission: AdmissionConfig,
#[serde(default)]
pub digest: DigestConfig,
#[serde(default)]
pub context_strategy: ContextStrategy,
#[serde(default = "default_crossover_turn_threshold")]
pub crossover_turn_threshold: u32,
#[serde(default)]
pub consolidation: ConsolidationConfig,
#[serde(default)]
pub forgetting: ForgettingConfig,
#[serde(default)]
pub database_url: Option<String>,
#[serde(default)]
pub store_routing: StoreRoutingConfig,
#[serde(default)]
pub persona: PersonaConfig,
#[serde(default)]
pub trajectory: TrajectoryConfig,
#[serde(default)]
pub category: CategoryConfig,
#[serde(default)]
pub tree: TreeConfig,
#[serde(default)]
pub microcompact: MicrocompactConfig,
#[serde(default)]
pub autodream: AutoDreamConfig,
#[serde(default = "default_key_facts_dedup_threshold")]
pub key_facts_dedup_threshold: f32,
}
fn default_crossover_turn_threshold() -> u32 {
20
}
fn default_key_facts_dedup_threshold() -> f32 {
0.95
}
#[derive(Debug, Clone, Deserialize, Serialize)]
#[serde(default)]
pub struct DigestConfig {
pub enabled: bool,
pub provider: String,
pub max_tokens: usize,
pub max_input_messages: usize,
}
impl Default for DigestConfig {
fn default() -> Self {
Self {
enabled: false,
provider: String::new(),
max_tokens: 500,
max_input_messages: 50,
}
}
}
#[derive(Debug, Clone, Copy, Default, Deserialize, Serialize, PartialEq, Eq)]
#[serde(rename_all = "snake_case")]
pub enum ContextStrategy {
#[default]
FullHistory,
MemoryFirst,
Adaptive,
}
#[derive(Debug, Clone, Deserialize, Serialize)]
#[serde(default)]
pub struct SessionsConfig {
#[serde(default = "default_max_history")]
pub max_history: usize,
#[serde(default = "default_title_max_chars")]
pub title_max_chars: usize,
}
impl Default for SessionsConfig {
fn default() -> Self {
Self {
max_history: default_max_history(),
title_max_chars: default_title_max_chars(),
}
}
}
#[derive(Debug, Clone, Deserialize, Serialize)]
pub struct DocumentConfig {
#[serde(default = "default_document_collection")]
pub collection: String,
#[serde(default = "default_document_chunk_size")]
pub chunk_size: usize,
#[serde(default = "default_document_chunk_overlap")]
pub chunk_overlap: usize,
#[serde(default = "default_document_top_k")]
pub top_k: usize,
#[serde(default)]
pub rag_enabled: bool,
}
impl Default for DocumentConfig {
fn default() -> Self {
Self {
collection: default_document_collection(),
chunk_size: default_document_chunk_size(),
chunk_overlap: default_document_chunk_overlap(),
top_k: default_document_top_k(),
rag_enabled: false,
}
}
}
#[derive(Debug, Deserialize, Serialize)]
#[allow(clippy::struct_excessive_bools)]
pub struct SemanticConfig {
#[serde(default = "default_semantic_enabled")]
pub enabled: bool,
#[serde(default = "default_recall_limit")]
pub recall_limit: usize,
#[serde(default = "default_vector_weight")]
pub vector_weight: f64,
#[serde(default = "default_keyword_weight")]
pub keyword_weight: f64,
#[serde(default = "default_true")]
pub temporal_decay_enabled: bool,
#[serde(default = "default_temporal_decay_half_life_days")]
pub temporal_decay_half_life_days: u32,
#[serde(default = "default_true")]
pub mmr_enabled: bool,
#[serde(default = "default_mmr_lambda")]
pub mmr_lambda: f32,
#[serde(default = "default_true")]
pub importance_enabled: bool,
#[serde(
default = "default_importance_weight",
deserialize_with = "validate_importance_weight"
)]
pub importance_weight: f64,
#[serde(default)]
pub embed_provider: Option<String>,
}
impl Default for SemanticConfig {
fn default() -> Self {
Self {
enabled: default_semantic_enabled(),
recall_limit: default_recall_limit(),
vector_weight: default_vector_weight(),
keyword_weight: default_keyword_weight(),
temporal_decay_enabled: true,
temporal_decay_half_life_days: default_temporal_decay_half_life_days(),
mmr_enabled: true,
mmr_lambda: default_mmr_lambda(),
importance_enabled: true,
importance_weight: default_importance_weight(),
embed_provider: None,
}
}
}
#[derive(Debug, Clone, Default, Deserialize, Serialize, PartialEq)]
#[serde(tag = "strategy", rename_all = "snake_case")]
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")]
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 CompressionPredictorConfig {
pub enabled: bool,
pub min_samples: u64,
pub candidate_ratios: Vec<f32>,
pub retrain_interval: u64,
pub max_training_samples: usize,
}
impl Default for CompressionPredictorConfig {
fn default() -> Self {
Self {
enabled: false,
min_samples: 10,
candidate_ratios: vec![0.2, 0.3, 0.4, 0.5, 0.6, 0.7, 0.8, 0.9],
retrain_interval: 5,
max_training_samples: 200,
}
}
}
#[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: zeph_memory::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 predictor: CompressionPredictorConfig,
}
fn default_sidequest_interval_turns() -> u32 {
4
}
fn default_sidequest_max_eviction_ratio() -> f32 {
0.5
}
fn default_sidequest_max_cursors() -> usize {
30
}
fn default_sidequest_min_cursor_tokens() -> usize {
100
}
#[derive(Debug, Clone, Deserialize, Serialize)]
#[serde(default)]
pub struct SidequestConfig {
pub enabled: bool,
#[serde(default = "default_sidequest_interval_turns")]
pub interval_turns: u32,
#[serde(default = "default_sidequest_max_eviction_ratio")]
pub max_eviction_ratio: f32,
#[serde(default = "default_sidequest_max_cursors")]
pub max_cursors: usize,
#[serde(default = "default_sidequest_min_cursor_tokens")]
pub min_cursor_tokens: usize,
}
impl Default for SidequestConfig {
fn default() -> Self {
Self {
enabled: false,
interval_turns: default_sidequest_interval_turns(),
max_eviction_ratio: default_sidequest_max_eviction_ratio(),
max_cursors: default_sidequest_max_cursors(),
min_cursor_tokens: default_sidequest_min_cursor_tokens(),
}
}
}
#[derive(Debug, Clone, Deserialize, Serialize)]
#[serde(default)]
pub struct GraphConfig {
pub enabled: bool,
pub extract_model: String,
#[serde(default = "default_graph_max_entities_per_message")]
pub max_entities_per_message: usize,
#[serde(default = "default_graph_max_edges_per_message")]
pub max_edges_per_message: usize,
#[serde(default = "default_graph_community_refresh_interval")]
pub community_refresh_interval: usize,
#[serde(default = "default_graph_entity_similarity_threshold")]
pub entity_similarity_threshold: f32,
#[serde(default = "default_graph_extraction_timeout_secs")]
pub extraction_timeout_secs: u64,
#[serde(default)]
pub use_embedding_resolution: bool,
#[serde(default = "default_graph_entity_ambiguous_threshold")]
pub entity_ambiguous_threshold: f32,
#[serde(default = "default_graph_max_hops")]
pub max_hops: u32,
#[serde(default = "default_graph_recall_limit")]
pub recall_limit: usize,
#[serde(default = "default_graph_expired_edge_retention_days")]
pub expired_edge_retention_days: u32,
#[serde(default)]
pub max_entities: usize,
#[serde(default = "default_graph_community_summary_max_prompt_bytes")]
pub community_summary_max_prompt_bytes: usize,
#[serde(default = "default_graph_community_summary_concurrency")]
pub community_summary_concurrency: usize,
#[serde(default = "default_lpa_edge_chunk_size")]
pub lpa_edge_chunk_size: usize,
#[serde(
default = "default_graph_temporal_decay_rate",
deserialize_with = "validate_temporal_decay_rate"
)]
pub temporal_decay_rate: f64,
#[serde(default = "default_graph_edge_history_limit")]
pub edge_history_limit: usize,
#[serde(default)]
pub note_linking: NoteLinkingConfig,
#[serde(default)]
pub spreading_activation: SpreadingActivationConfig,
#[serde(
default = "default_link_weight_decay_lambda",
deserialize_with = "validate_link_weight_decay_lambda"
)]
pub link_weight_decay_lambda: f64,
#[serde(default = "default_link_weight_decay_interval_secs")]
pub link_weight_decay_interval_secs: u64,
#[serde(default)]
pub belief_revision: BeliefRevisionConfig,
#[serde(default)]
pub rpe: RpeConfig,
#[serde(default = "default_graph_pool_size")]
pub pool_size: u32,
}
fn default_graph_pool_size() -> u32 {
3
}
impl Default for GraphConfig {
fn default() -> Self {
Self {
enabled: false,
extract_model: String::new(),
max_entities_per_message: default_graph_max_entities_per_message(),
max_edges_per_message: default_graph_max_edges_per_message(),
community_refresh_interval: default_graph_community_refresh_interval(),
entity_similarity_threshold: default_graph_entity_similarity_threshold(),
extraction_timeout_secs: default_graph_extraction_timeout_secs(),
use_embedding_resolution: false,
entity_ambiguous_threshold: default_graph_entity_ambiguous_threshold(),
max_hops: default_graph_max_hops(),
recall_limit: default_graph_recall_limit(),
expired_edge_retention_days: default_graph_expired_edge_retention_days(),
max_entities: 0,
community_summary_max_prompt_bytes: default_graph_community_summary_max_prompt_bytes(),
community_summary_concurrency: default_graph_community_summary_concurrency(),
lpa_edge_chunk_size: default_lpa_edge_chunk_size(),
temporal_decay_rate: default_graph_temporal_decay_rate(),
edge_history_limit: default_graph_edge_history_limit(),
note_linking: NoteLinkingConfig::default(),
spreading_activation: SpreadingActivationConfig::default(),
link_weight_decay_lambda: default_link_weight_decay_lambda(),
link_weight_decay_interval_secs: default_link_weight_decay_interval_secs(),
belief_revision: BeliefRevisionConfig::default(),
rpe: RpeConfig::default(),
pool_size: default_graph_pool_size(),
}
}
}
fn default_consolidation_confidence_threshold() -> f32 {
0.7
}
fn default_consolidation_sweep_interval_secs() -> u64 {
3600
}
fn default_consolidation_sweep_batch_size() -> usize {
50
}
fn default_consolidation_similarity_threshold() -> f32 {
0.85
}
#[derive(Debug, Clone, serde::Serialize, serde::Deserialize)]
#[serde(default)]
pub struct ConsolidationConfig {
pub enabled: bool,
#[serde(default)]
pub consolidation_provider: ProviderName,
#[serde(default = "default_consolidation_confidence_threshold")]
pub confidence_threshold: f32,
#[serde(default = "default_consolidation_sweep_interval_secs")]
pub sweep_interval_secs: u64,
#[serde(default = "default_consolidation_sweep_batch_size")]
pub sweep_batch_size: usize,
#[serde(default = "default_consolidation_similarity_threshold")]
pub similarity_threshold: f32,
}
impl Default for ConsolidationConfig {
fn default() -> Self {
Self {
enabled: false,
consolidation_provider: ProviderName::default(),
confidence_threshold: default_consolidation_confidence_threshold(),
sweep_interval_secs: default_consolidation_sweep_interval_secs(),
sweep_batch_size: default_consolidation_sweep_batch_size(),
similarity_threshold: default_consolidation_similarity_threshold(),
}
}
}
fn default_link_weight_decay_lambda() -> f64 {
0.95
}
fn default_link_weight_decay_interval_secs() -> u64 {
86400
}
fn validate_link_weight_decay_lambda<'de, D>(deserializer: D) -> Result<f64, D::Error>
where
D: serde::Deserializer<'de>,
{
let value = <f64 as serde::Deserialize>::deserialize(deserializer)?;
if value.is_nan() || value.is_infinite() {
return Err(serde::de::Error::custom(
"link_weight_decay_lambda must be a finite number",
));
}
if !(value > 0.0 && value <= 1.0) {
return Err(serde::de::Error::custom(
"link_weight_decay_lambda must be in (0.0, 1.0]",
));
}
Ok(value)
}
fn validate_admission_threshold<'de, D>(deserializer: D) -> Result<f32, D::Error>
where
D: serde::Deserializer<'de>,
{
let value = <f32 as serde::Deserialize>::deserialize(deserializer)?;
if value.is_nan() || value.is_infinite() {
return Err(serde::de::Error::custom(
"threshold must be a finite number",
));
}
if !(0.0..=1.0).contains(&value) {
return Err(serde::de::Error::custom("threshold must be in [0.0, 1.0]"));
}
Ok(value)
}
fn validate_admission_fast_path_margin<'de, D>(deserializer: D) -> Result<f32, D::Error>
where
D: serde::Deserializer<'de>,
{
let value = <f32 as serde::Deserialize>::deserialize(deserializer)?;
if value.is_nan() || value.is_infinite() {
return Err(serde::de::Error::custom(
"fast_path_margin must be a finite number",
));
}
if !(0.0..=1.0).contains(&value) {
return Err(serde::de::Error::custom(
"fast_path_margin must be in [0.0, 1.0]",
));
}
Ok(value)
}
fn default_admission_threshold() -> f32 {
0.40
}
fn default_admission_fast_path_margin() -> f32 {
0.15
}
fn default_rl_min_samples() -> u32 {
500
}
fn default_rl_retrain_interval_secs() -> u64 {
3600
}
#[derive(Debug, Clone, Default, PartialEq, Eq, serde::Deserialize, serde::Serialize)]
#[serde(rename_all = "snake_case")]
pub enum AdmissionStrategy {
#[default]
Heuristic,
Rl,
}
fn validate_admission_weight<'de, D>(deserializer: D) -> Result<f32, D::Error>
where
D: serde::Deserializer<'de>,
{
let value = <f32 as serde::Deserialize>::deserialize(deserializer)?;
if value < 0.0 {
return Err(serde::de::Error::custom(
"admission weight must be non-negative (>= 0.0)",
));
}
Ok(value)
}
#[derive(Debug, Clone, Deserialize, Serialize)]
#[serde(default)]
pub struct AdmissionWeights {
#[serde(deserialize_with = "validate_admission_weight")]
pub future_utility: f32,
#[serde(deserialize_with = "validate_admission_weight")]
pub factual_confidence: f32,
#[serde(deserialize_with = "validate_admission_weight")]
pub semantic_novelty: f32,
#[serde(deserialize_with = "validate_admission_weight")]
pub temporal_recency: f32,
#[serde(deserialize_with = "validate_admission_weight")]
pub content_type_prior: f32,
#[serde(deserialize_with = "validate_admission_weight")]
pub goal_utility: f32,
}
impl Default for AdmissionWeights {
fn default() -> Self {
Self {
future_utility: 0.30,
factual_confidence: 0.15,
semantic_novelty: 0.30,
temporal_recency: 0.10,
content_type_prior: 0.15,
goal_utility: 0.0,
}
}
}
impl AdmissionWeights {
#[must_use]
pub fn normalized(&self) -> Self {
let sum = self.future_utility
+ self.factual_confidence
+ self.semantic_novelty
+ self.temporal_recency
+ self.content_type_prior
+ self.goal_utility;
if sum <= f32::EPSILON {
return Self::default();
}
Self {
future_utility: self.future_utility / sum,
factual_confidence: self.factual_confidence / sum,
semantic_novelty: self.semantic_novelty / sum,
temporal_recency: self.temporal_recency / sum,
content_type_prior: self.content_type_prior / sum,
goal_utility: self.goal_utility / sum,
}
}
}
#[derive(Debug, Clone, Deserialize, Serialize)]
#[serde(default)]
pub struct AdmissionConfig {
pub enabled: bool,
#[serde(deserialize_with = "validate_admission_threshold")]
pub threshold: f32,
#[serde(deserialize_with = "validate_admission_fast_path_margin")]
pub fast_path_margin: f32,
pub admission_provider: ProviderName,
pub weights: AdmissionWeights,
#[serde(default)]
pub admission_strategy: AdmissionStrategy,
#[serde(default = "default_rl_min_samples")]
pub rl_min_samples: u32,
#[serde(default = "default_rl_retrain_interval_secs")]
pub rl_retrain_interval_secs: u64,
#[serde(default)]
pub goal_conditioned_write: bool,
#[serde(default)]
pub goal_utility_provider: ProviderName,
#[serde(default = "default_goal_utility_threshold")]
pub goal_utility_threshold: f32,
#[serde(default = "default_goal_utility_weight")]
pub goal_utility_weight: f32,
}
fn default_goal_utility_threshold() -> f32 {
0.4
}
fn default_goal_utility_weight() -> f32 {
0.25
}
impl Default for AdmissionConfig {
fn default() -> Self {
Self {
enabled: false,
threshold: default_admission_threshold(),
fast_path_margin: default_admission_fast_path_margin(),
admission_provider: ProviderName::default(),
weights: AdmissionWeights::default(),
admission_strategy: AdmissionStrategy::default(),
rl_min_samples: default_rl_min_samples(),
rl_retrain_interval_secs: default_rl_retrain_interval_secs(),
goal_conditioned_write: false,
goal_utility_provider: ProviderName::default(),
goal_utility_threshold: default_goal_utility_threshold(),
goal_utility_weight: default_goal_utility_weight(),
}
}
}
#[derive(Debug, Clone, Copy, Default, PartialEq, Eq, Deserialize, Serialize)]
#[serde(rename_all = "snake_case")]
pub enum StoreRoutingStrategy {
#[default]
Heuristic,
Llm,
Hybrid,
}
#[derive(Debug, Clone, Deserialize, Serialize)]
#[serde(default)]
pub struct StoreRoutingConfig {
pub enabled: bool,
pub strategy: StoreRoutingStrategy,
pub routing_classifier_provider: ProviderName,
pub fallback_route: String,
pub confidence_threshold: f32,
}
impl Default for StoreRoutingConfig {
fn default() -> Self {
Self {
enabled: false,
strategy: StoreRoutingStrategy::Heuristic,
routing_classifier_provider: ProviderName::default(),
fallback_route: "hybrid".into(),
confidence_threshold: 0.7,
}
}
}
#[derive(Debug, Clone, Deserialize, Serialize)]
#[serde(default)]
pub struct PersonaConfig {
pub enabled: bool,
pub persona_provider: ProviderName,
pub min_confidence: f64,
pub min_messages: usize,
pub max_messages: usize,
pub extraction_timeout_secs: u64,
pub context_budget_tokens: usize,
}
impl Default for PersonaConfig {
fn default() -> Self {
Self {
enabled: false,
persona_provider: ProviderName::default(),
min_confidence: 0.6,
min_messages: 3,
max_messages: 10,
extraction_timeout_secs: 10,
context_budget_tokens: 500,
}
}
}
#[derive(Debug, Clone, Deserialize, Serialize)]
#[serde(default)]
pub struct TrajectoryConfig {
pub enabled: bool,
pub trajectory_provider: ProviderName,
pub context_budget_tokens: usize,
pub max_messages: usize,
pub extraction_timeout_secs: u64,
pub recall_top_k: usize,
pub min_confidence: f64,
}
impl Default for TrajectoryConfig {
fn default() -> Self {
Self {
enabled: false,
trajectory_provider: ProviderName::default(),
context_budget_tokens: 400,
max_messages: 10,
extraction_timeout_secs: 10,
recall_top_k: 5,
min_confidence: 0.6,
}
}
}
#[derive(Debug, Clone, Deserialize, Serialize)]
#[serde(default)]
pub struct CategoryConfig {
pub enabled: bool,
pub auto_tag: bool,
}
impl Default for CategoryConfig {
fn default() -> Self {
Self {
enabled: false,
auto_tag: true,
}
}
}
#[derive(Debug, Clone, Deserialize, Serialize)]
#[serde(default)]
pub struct TreeConfig {
pub enabled: bool,
pub consolidation_provider: ProviderName,
pub sweep_interval_secs: u64,
pub batch_size: usize,
pub similarity_threshold: f32,
pub max_level: u32,
pub context_budget_tokens: usize,
pub recall_top_k: usize,
pub min_cluster_size: usize,
}
impl Default for TreeConfig {
fn default() -> Self {
Self {
enabled: false,
consolidation_provider: ProviderName::default(),
sweep_interval_secs: 300,
batch_size: 20,
similarity_threshold: 0.8,
max_level: 3,
context_budget_tokens: 400,
recall_top_k: 5,
min_cluster_size: 2,
}
}
}
#[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, serde::Deserialize, serde::Serialize)]
#[serde(default)]
pub struct AutoDreamConfig {
pub enabled: bool,
pub min_sessions: u32,
pub min_hours: u32,
pub consolidation_provider: ProviderName,
pub max_iterations: u8,
}
impl Default for AutoDreamConfig {
fn default() -> Self {
Self {
enabled: false,
min_sessions: 3,
min_hours: 24,
consolidation_provider: ProviderName::default(),
max_iterations: 8,
}
}
}
#[derive(Debug, Clone, serde::Deserialize, serde::Serialize)]
#[serde(default)]
pub struct MagicDocsConfig {
pub enabled: bool,
pub min_turns_between_updates: u32,
pub update_provider: ProviderName,
pub max_iterations: u8,
}
impl Default for MagicDocsConfig {
fn default() -> Self {
Self {
enabled: false,
min_turns_between_updates: 5,
update_provider: ProviderName::default(),
max_iterations: 4,
}
}
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn pruning_strategy_toml_task_aware_mig_falls_back_to_reactive() {
#[derive(serde::Deserialize)]
struct Wrapper {
#[allow(dead_code)]
pruning_strategy: PruningStrategy,
}
let toml = r#"pruning_strategy = "task_aware_mig""#;
let w: Wrapper = toml::from_str(toml).expect("should deserialize without error");
assert_eq!(
w.pruning_strategy,
PruningStrategy::Reactive,
"task_aware_mig must fall back to Reactive"
);
}
#[test]
fn pruning_strategy_toml_round_trip() {
#[derive(serde::Deserialize)]
struct Wrapper {
#[allow(dead_code)]
pruning_strategy: PruningStrategy,
}
for (input, expected) in [
("reactive", PruningStrategy::Reactive),
("task_aware", PruningStrategy::TaskAware),
("mig", PruningStrategy::Mig),
] {
let toml = format!(r#"pruning_strategy = "{input}""#);
let w: Wrapper = toml::from_str(&toml)
.unwrap_or_else(|e| panic!("failed to deserialize `{input}`: {e}"));
assert_eq!(w.pruning_strategy, expected, "mismatch for `{input}`");
}
}
#[test]
fn pruning_strategy_toml_unknown_value_errors() {
#[derive(serde::Deserialize)]
#[allow(dead_code)]
struct Wrapper {
pruning_strategy: PruningStrategy,
}
let toml = r#"pruning_strategy = "nonexistent_strategy""#;
assert!(
toml::from_str::<Wrapper>(toml).is_err(),
"unknown strategy must produce an error"
);
}
#[test]
fn tier_config_defaults_are_correct() {
let cfg = TierConfig::default();
assert!(!cfg.enabled);
assert_eq!(cfg.promotion_min_sessions, 3);
assert!((cfg.similarity_threshold - 0.92).abs() < f32::EPSILON);
assert_eq!(cfg.sweep_interval_secs, 3600);
assert_eq!(cfg.sweep_batch_size, 100);
}
#[test]
fn tier_config_rejects_min_sessions_below_2() {
let toml = "promotion_min_sessions = 1";
assert!(toml::from_str::<TierConfig>(toml).is_err());
}
#[test]
fn tier_config_rejects_similarity_threshold_below_0_5() {
let toml = "similarity_threshold = 0.4";
assert!(toml::from_str::<TierConfig>(toml).is_err());
}
#[test]
fn tier_config_rejects_zero_sweep_batch_size() {
let toml = "sweep_batch_size = 0";
assert!(toml::from_str::<TierConfig>(toml).is_err());
}
fn deserialize_importance_weight(toml_val: &str) -> Result<SemanticConfig, toml::de::Error> {
let input = format!("importance_weight = {toml_val}");
toml::from_str::<SemanticConfig>(&input)
}
#[test]
fn importance_weight_default_is_0_15() {
let cfg = SemanticConfig::default();
assert!((cfg.importance_weight - 0.15).abs() < f64::EPSILON);
}
#[test]
fn importance_weight_valid_zero() {
let cfg = deserialize_importance_weight("0.0").unwrap();
assert!((cfg.importance_weight - 0.0_f64).abs() < f64::EPSILON);
}
#[test]
fn importance_weight_valid_one() {
let cfg = deserialize_importance_weight("1.0").unwrap();
assert!((cfg.importance_weight - 1.0_f64).abs() < f64::EPSILON);
}
#[test]
fn importance_weight_rejects_near_zero_negative() {
let result = deserialize_importance_weight("-0.01");
assert!(
result.is_err(),
"negative importance_weight must be rejected"
);
}
#[test]
fn importance_weight_rejects_negative() {
let result = deserialize_importance_weight("-1.0");
assert!(result.is_err(), "negative value must be rejected");
}
#[test]
fn importance_weight_rejects_greater_than_one() {
let result = deserialize_importance_weight("1.01");
assert!(result.is_err(), "value > 1.0 must be rejected");
}
#[test]
fn admission_weights_normalized_sums_to_one() {
let w = AdmissionWeights {
future_utility: 2.0,
factual_confidence: 1.0,
semantic_novelty: 3.0,
temporal_recency: 1.0,
content_type_prior: 3.0,
goal_utility: 0.0,
};
let n = w.normalized();
let sum = n.future_utility
+ n.factual_confidence
+ n.semantic_novelty
+ n.temporal_recency
+ n.content_type_prior;
assert!(
(sum - 1.0).abs() < 0.001,
"normalized weights must sum to 1.0, got {sum}"
);
}
#[test]
fn admission_weights_normalized_preserves_already_unit_sum() {
let w = AdmissionWeights::default();
let n = w.normalized();
let sum = n.future_utility
+ n.factual_confidence
+ n.semantic_novelty
+ n.temporal_recency
+ n.content_type_prior;
assert!(
(sum - 1.0).abs() < 0.001,
"default weights sum to ~1.0 after normalization"
);
}
#[test]
fn admission_weights_normalized_zero_sum_falls_back_to_default() {
let w = AdmissionWeights {
future_utility: 0.0,
factual_confidence: 0.0,
semantic_novelty: 0.0,
temporal_recency: 0.0,
content_type_prior: 0.0,
goal_utility: 0.0,
};
let n = w.normalized();
let default = AdmissionWeights::default();
assert!(
(n.future_utility - default.future_utility).abs() < 0.001,
"zero-sum weights must fall back to defaults"
);
}
#[test]
fn admission_config_defaults() {
let cfg = AdmissionConfig::default();
assert!(!cfg.enabled);
assert!((cfg.threshold - 0.40).abs() < 0.001);
assert!((cfg.fast_path_margin - 0.15).abs() < 0.001);
assert!(cfg.admission_provider.is_empty());
}
#[test]
fn spreading_activation_default_recall_timeout_ms_is_1000() {
let cfg = SpreadingActivationConfig::default();
assert_eq!(
cfg.recall_timeout_ms, 1000,
"default recall_timeout_ms must be 1000ms"
);
}
#[test]
fn spreading_activation_toml_recall_timeout_ms_round_trip() {
#[derive(serde::Deserialize)]
struct Wrapper {
recall_timeout_ms: u64,
}
let toml = "recall_timeout_ms = 500";
let w: Wrapper = toml::from_str(toml).unwrap();
assert_eq!(w.recall_timeout_ms, 500);
}
#[test]
fn spreading_activation_validate_cross_field_constraints() {
let mut cfg = SpreadingActivationConfig::default();
assert!(cfg.validate().is_ok());
cfg.activation_threshold = 0.5;
cfg.inhibition_threshold = 0.5;
assert!(cfg.validate().is_err());
}
#[test]
fn compression_config_focus_strategy_deserializes() {
let toml = r#"strategy = "focus""#;
let cfg: CompressionConfig = toml::from_str(toml).unwrap();
assert_eq!(cfg.strategy, CompressionStrategy::Focus);
}
#[test]
fn compression_config_density_budget_defaults_on_deserialize() {
let toml = r#"strategy = "reactive""#;
let cfg: CompressionConfig = toml::from_str(toml).unwrap();
assert!((cfg.high_density_budget - 0.7).abs() < 1e-6);
assert!((cfg.low_density_budget - 0.3).abs() < 1e-6);
}
#[test]
fn compression_config_density_budget_round_trip() {
let toml = "strategy = \"reactive\"\nhigh_density_budget = 0.6\nlow_density_budget = 0.4";
let cfg: CompressionConfig = toml::from_str(toml).unwrap();
assert!((cfg.high_density_budget - 0.6).abs() < f32::EPSILON);
assert!((cfg.low_density_budget - 0.4).abs() < f32::EPSILON);
}
#[test]
fn compression_config_focus_scorer_provider_default_empty() {
let cfg = CompressionConfig::default();
assert!(cfg.focus_scorer_provider.is_empty());
}
#[test]
fn compression_config_focus_scorer_provider_round_trip() {
let toml = "strategy = \"focus\"\nfocus_scorer_provider = \"fast\"";
let cfg: CompressionConfig = toml::from_str(toml).unwrap();
assert_eq!(cfg.focus_scorer_provider.as_str(), "fast");
}
}