mod env_helper;
mod generate;
mod infer;
mod parse;
mod query;
mod refine;
mod repair;
mod scan;
mod validate;
use std::collections::HashSet;
use std::path::Path;
use anyhow::Result;
use tokio::task::block_in_place;
use crate::EvolutionLlm;
pub(super) const SKILL_GENERATION_PROMPT: &str =
include_str!("../seed/evolution_prompts/skill_generation.seed.md");
pub(super) const SKILL_GENERATION_FROM_FAILURES_PROMPT: &str =
include_str!("../seed/evolution_prompts/skill_generation_from_failures.seed.md");
pub(super) const SKILL_REFINEMENT_PROMPT: &str =
include_str!("../seed/evolution_prompts/skill_refinement.seed.md");
pub(super) const SKILL_EXECUTION_INFERENCE_PROMPT: &str =
include_str!("../seed/evolution_prompts/skill_execution_inference.seed.md");
pub(super) const MAX_EVOLVED_SKILLS: usize = 20;
pub(super) const MAX_REFINE_ROUNDS: usize = 3;
pub(super) const MAX_PARSE_RETRIES: usize = 1;
pub(super) const RETIRE_UNUSED_DAYS: i64 = 30;
pub(super) const RETIRE_LOW_SUCCESS_RATE: f64 = 0.30;
#[derive(Debug, Clone, serde::Serialize, serde::Deserialize)]
pub struct SkillMeta {
pub name: String,
pub source_session: String,
pub created_at: String,
pub success_count: u32,
pub failure_count: u32,
pub call_count: u32,
pub last_used: Option<String>,
#[serde(default)]
pub archived: bool,
#[serde(default)]
pub generation_txn: String,
#[serde(default)]
pub needs_review: bool,
}
impl SkillMeta {
pub fn success_rate(&self) -> f64 {
if self.call_count == 0 {
return 1.0;
}
self.success_count as f64 / self.call_count as f64
}
}
pub async fn evolve_skills<L: EvolutionLlm>(
chat_root: &Path,
skills_root: Option<&Path>,
llm: &L,
model: &str,
txn_id: &str,
generate: bool,
force: bool,
) -> Result<Vec<(String, String)>> {
let Some(skills_root) = skills_root else {
return Ok(Vec::new());
};
let mut changes = Vec::new();
let try_generate = generate || force;
let min_pattern_count: u32 = std::env::var("SKILLLITE_MIN_PATTERN_COUNT")
.ok()
.and_then(|v| v.parse().ok())
.unwrap_or(if force { 2 } else { 3 });
if try_generate {
let (success_data, failure_data, retired) = block_in_place(|| {
let conn = crate::feedback::open_evolution_db(chat_root)?;
let (patterns_display, pattern_descs) =
query::query_repeated_patterns(&conn, min_pattern_count)?;
let success_executions = if pattern_descs.is_empty() {
String::new()
} else {
query::query_pattern_executions(&conn, &pattern_descs)?
};
let failed_patterns = query::query_failed_patterns(&conn, 2)?;
let failed_executions = query::query_failed_executions(&conn)?;
let retired = refine::retire_skills_with_conn(chat_root, skills_root, txn_id, &conn)?;
Ok::<_, anyhow::Error>((
(patterns_display, success_executions),
(failed_patterns, failed_executions),
retired,
))
})?;
changes.extend(retired);
if let Ok(Some(name)) = generate::generate_skill_from_failures(
chat_root,
skills_root,
llm,
model,
txn_id,
Some(failure_data),
)
.await
{
changes.push(("skill_pending".to_string(), name));
}
if let Ok(Some(name)) = generate::generate_skill(
chat_root,
skills_root,
llm,
model,
txn_id,
min_pattern_count,
Some(success_data),
)
.await
{
changes.push(("skill_pending".to_string(), name));
}
if changes.is_empty() {
if let Ok(Some(name)) =
refine::refine_weakest_skill(chat_root, skills_root, llm, model, txn_id).await
{
changes.push(("skill_refined".to_string(), name));
}
}
} else {
let (retired, _) = block_in_place(|| {
let conn = crate::feedback::open_evolution_db(chat_root)?;
let retired = refine::retire_skills_with_conn(chat_root, skills_root, txn_id, &conn)?;
Ok::<_, anyhow::Error>((retired, ()))
})?;
changes.extend(retired);
match refine::refine_weakest_skill(chat_root, skills_root, llm, model, txn_id).await {
Ok(Some(name)) => changes.push(("skill_refined".to_string(), name)),
Ok(None) => {}
Err(e) => tracing::warn!("Skill refinement failed: {}", e),
}
}
let mut seen: HashSet<String> = HashSet::new();
changes.retain(|(t, id)| {
if t == "skill_pending" || t == "skill_refined" {
seen.insert(id.clone())
} else {
true
}
});
Ok(changes)
}
pub fn list_pending_skills(skills_root: &Path) -> Vec<String> {
list_pending_skills_with_review(skills_root)
.into_iter()
.map(|(name, _)| name)
.collect()
}
pub fn list_pending_skills_with_review(skills_root: &Path) -> Vec<(String, bool)> {
let pending_dir = skills_root.join("_evolved").join("_pending");
if !pending_dir.exists() {
return Vec::new();
}
std::fs::read_dir(&pending_dir)
.ok()
.into_iter()
.flatten()
.filter_map(|e| e.ok())
.filter(|e| e.path().is_dir() && e.path().join("SKILL.md").exists())
.map(|e| {
let name = e.file_name().to_string_lossy().to_string();
let needs_review = std::fs::read_to_string(e.path().join(".meta.json"))
.ok()
.and_then(|s| serde_json::from_str::<SkillMeta>(&s).ok())
.map(|m| m.needs_review)
.unwrap_or(false);
(name, needs_review)
})
.collect()
}
pub fn confirm_pending_skill(skills_root: &Path, skill_name: &str) -> Result<()> {
let pending_dir = skills_root.join("_evolved").join("_pending");
let evolved_dir = skills_root.join("_evolved");
let src = pending_dir.join(skill_name);
let dst = evolved_dir.join(skill_name);
if !src.exists() {
anyhow::bail!("待确认 Skill '{}' 不存在", skill_name);
}
if dst.exists() {
anyhow::bail!("Skill '{}' 已存在,请先删除或重命名", skill_name);
}
std::fs::rename(&src, &dst)?;
tracing::info!("Skill '{}' 已确认加入", skill_name);
Ok(())
}
pub fn reject_pending_skill(skills_root: &Path, skill_name: &str) -> Result<()> {
let pending_dir = skills_root.join("_evolved").join("_pending");
let src = pending_dir.join(skill_name);
if !src.exists() {
anyhow::bail!("待确认 Skill '{}' 不存在", skill_name);
}
std::fs::remove_dir_all(&src)?;
tracing::info!("Skill '{}' 已拒绝", skill_name);
Ok(())
}
pub use repair::{repair_one_skill, repair_skills};
pub use scan::track_skill_usage;
pub use validate::{validate_skills, SkillValidation};