use anyhow::Result;
use std::collections::HashSet;
use std::sync::Arc;
use crate::agent::memory::{MemDocTier, MemoryStore};
use crate::provider::registry::ProviderRegistry;
pub struct MeditationConfig {
pub dedup_threshold: f32,
pub batch_size: usize,
pub crystallized_ttl_days: u32,
}
impl Default for MeditationConfig {
fn default() -> Self {
let evo = crate::agent::evolution::evolution_config();
Self {
dedup_threshold: evo.meditation.dedup_threshold,
batch_size: 50,
crystallized_ttl_days: evo.meditation.crystallized_ttl_days,
}
}
}
#[derive(Debug, Default)]
pub struct MeditationReport {
pub duplicates_merged: usize,
pub crystallized_cleaned: usize,
pub skills_crystallized: usize,
pub total_processed: usize,
}
pub async fn meditate(
store: &mut MemoryStore,
scope: &str,
config: &MeditationConfig,
) -> Result<MeditationReport> {
let mut report = MeditationReport::default();
let merged = dedup_phase(store, scope, config).await?;
report.duplicates_merged = merged;
let cleaned = cleanup_phase(store, scope, config).await?;
report.crystallized_cleaned = cleaned;
report.total_processed = merged + cleaned;
Ok(report)
}
async fn dedup_phase(
store: &mut MemoryStore,
scope: &str,
config: &MeditationConfig,
) -> Result<usize> {
let candidate_ids: Vec<String> = store
.find_by_tier(&MemDocTier::Core, Some(scope))
.into_iter()
.take(config.batch_size)
.map(|d| d.id.clone())
.collect();
let mut merged: usize = 0;
let mut seen: HashSet<String> = HashSet::new();
for doc_id in &candidate_ids {
if seen.contains(doc_id) {
continue;
}
let duplicates = store.find_near_duplicates(doc_id, Some(scope), config.dedup_threshold)?;
for (dup_doc, _sim) in &duplicates {
if seen.contains(&dup_doc.id) {
continue;
}
let src = store.get_sync(doc_id);
let src_importance = src.map(|d| d.importance).unwrap_or(0.0);
let (keep_id, remove_id) = if src_importance >= dup_doc.importance {
(doc_id.as_str(), dup_doc.id.as_str())
} else {
(dup_doc.id.as_str(), doc_id.as_str())
};
seen.insert(keep_id.to_owned());
seen.insert(remove_id.to_owned());
store.delete(remove_id).await?;
merged += 1;
}
seen.insert(doc_id.clone());
}
Ok(merged)
}
pub async fn crystallize_phase(
store: &Arc<tokio::sync::Mutex<MemoryStore>>,
scope: &str,
providers: &Arc<ProviderRegistry>,
flash_model: &str,
skills_dir: &std::path::Path,
) -> Result<usize> {
let max_per_cycle = crate::agent::evolution::evolution_config()
.meditation
.max_per_cycle;
let candidates: Vec<String> = {
let s = store.lock().await;
s.find_by_tier(&MemDocTier::Core, Some(scope))
.into_iter()
.filter(|d| !d.tags.iter().any(|t| t == "crystallized"))
.take(max_per_cycle)
.map(|d| d.id.clone())
.collect()
};
let mut written = 0usize;
for doc_id in candidates {
match crate::skill::crystallizer::crystallize_one(
store,
&doc_id,
scope,
providers,
flash_model,
skills_dir,
)
.await
{
Ok(Some(_path)) => written += 1,
Ok(None) => {} Err(e) => {
tracing::warn!(doc_id, "crystallize_phase hard failure: {e:#}");
}
}
}
Ok(written)
}
async fn cleanup_phase(
store: &mut MemoryStore,
scope: &str,
config: &MeditationConfig,
) -> Result<usize> {
let now_secs = std::time::SystemTime::now()
.duration_since(std::time::UNIX_EPOCH)
.unwrap_or_default()
.as_secs() as i64;
let ttl_secs = i64::from(config.crystallized_ttl_days) * 86_400;
let mut to_demote: Vec<String> = Vec::new();
for tier in &[MemDocTier::Core, MemDocTier::Working] {
let docs = store.find_by_tier(tier, Some(scope));
for doc in docs {
if doc.tags.iter().any(|t| t == "crystallized") {
let age_secs = now_secs - doc.created_at;
if age_secs > ttl_secs {
to_demote.push(doc.id.clone());
}
}
}
}
let mut cleaned: usize = 0;
for id in &to_demote {
let current = store.get_sync(id).map(|d| d.importance).unwrap_or(0.5);
let delta = 0.01 - current;
store.adjust_importance(id, delta).await?;
cleaned += 1;
}
Ok(cleaned)
}