use anyhow::Result;
use serde::{Deserialize, Serialize};
use std::collections::HashMap;
use std::path::{Path, PathBuf};
use std::time::{SystemTime, UNIX_EPOCH};
use super::self_edit::{ImprovementCategory, ImprovementRecord};
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct StrategyScore {
pub category: ImprovementCategory,
pub attempts: usize,
pub successes: usize,
pub avg_effectiveness: f64,
pub last_attempted: u64,
pub cooldown_until: u64,
}
impl StrategyScore {
pub fn new(category: ImprovementCategory) -> Self {
Self {
category,
attempts: 0,
successes: 0,
avg_effectiveness: 0.0,
last_attempted: 0,
cooldown_until: 0,
}
}
pub fn success_rate(&self) -> f64 {
if self.attempts == 0 {
0.5 } else {
self.successes as f64 / self.attempts as f64
}
}
pub fn in_cooldown(&self) -> bool {
let now = SystemTime::now()
.duration_since(UNIX_EPOCH)
.unwrap_or_default()
.as_secs();
now < self.cooldown_until
}
pub fn priority_weight(&self) -> f64 {
if self.in_cooldown() {
return 0.0;
}
let exploration_bonus = if self.attempts < 3 { 0.2 } else { 0.0 };
0.5 * self.success_rate() + 0.5 * self.avg_effectiveness.max(0.0) + exploration_bonus
}
}
pub struct MetaLearner {
scores: HashMap<ImprovementCategory, StrategyScore>,
alpha: f64,
cooldown_secs: u64,
persist_path: PathBuf,
}
impl MetaLearner {
pub fn new() -> Self {
let persist_path = dirs::data_local_dir()
.unwrap_or_else(|| PathBuf::from("."))
.join("selfware")
.join("improvements")
.join("meta_learner.json");
let scores = Self::load_scores(&persist_path).unwrap_or_default();
Self {
scores,
alpha: 0.3,
cooldown_secs: 3600, persist_path,
}
}
pub fn update_weights(&mut self, record: &ImprovementRecord) {
let score = self
.scores
.entry(record.category.clone())
.or_insert_with(|| StrategyScore::new(record.category.clone()));
score.attempts += 1;
score.last_attempted = record.completed_at;
if record.verified && !record.rolled_back && record.effectiveness_score > 0.0 {
score.successes += 1;
}
if score.attempts == 1 {
score.avg_effectiveness = record.effectiveness_score;
} else {
score.avg_effectiveness = self.alpha * record.effectiveness_score
+ (1.0 - self.alpha) * score.avg_effectiveness;
}
if record.rolled_back || record.effectiveness_score < 0.0 {
let now = SystemTime::now()
.duration_since(UNIX_EPOCH)
.unwrap_or_default()
.as_secs();
score.cooldown_until = now + self.cooldown_secs;
}
if let Err(e) = self.save() {
tracing::warn!("Failed to persist meta-learner state: {}", e);
}
}
pub fn analyze_strategies(&self) -> Vec<(ImprovementCategory, f64)> {
let mut ranked: Vec<_> = self
.scores
.iter()
.map(|(cat, score)| (cat.clone(), score.priority_weight()))
.collect();
let all_categories = vec![
ImprovementCategory::PromptTemplate,
ImprovementCategory::ToolPipeline,
ImprovementCategory::ErrorHandling,
ImprovementCategory::VerificationLogic,
ImprovementCategory::ContextManagement,
ImprovementCategory::CodeQuality,
ImprovementCategory::NewCapability,
];
for cat in all_categories {
if !self.scores.contains_key(&cat) {
ranked.push((cat, 0.7));
}
}
ranked.sort_by(|a, b| b.1.partial_cmp(&a.1).unwrap_or(std::cmp::Ordering::Equal));
ranked
}
pub fn weight_priority(&self, category: &ImprovementCategory, base_priority: f64) -> f64 {
let weight = self
.scores
.get(category)
.map(|s| s.priority_weight())
.unwrap_or(0.7); base_priority * weight
}
pub fn get_score(&self, category: &ImprovementCategory) -> Option<&StrategyScore> {
self.scores.get(category)
}
fn save(&self) -> Result<()> {
if let Some(parent) = self.persist_path.parent() {
std::fs::create_dir_all(parent)?;
}
let content = serde_json::to_string_pretty(&self.scores)?;
std::fs::write(&self.persist_path, content)?;
Ok(())
}
fn load_scores(path: &Path) -> Result<HashMap<ImprovementCategory, StrategyScore>> {
if !path.exists() {
return Ok(HashMap::new());
}
let content = std::fs::read_to_string(path)?;
let scores = serde_json::from_str(&content)?;
Ok(scores)
}
}
impl Default for MetaLearner {
fn default() -> Self {
Self::new()
}
}
#[cfg(test)]
#[path = "../../tests/unit/cognitive/meta_learning/meta_learning_test.rs"]
mod tests;