Skip to main content

skilllite_evolution/skill_synth/
mod.rs

1//! Skill synthesis: auto-generate, refine, and retire skills (EVO-4).
2//!
3//! - **Generate**: 既总结成功经验,也总结失败经验
4//!   - 成功驱动:高成功率重复模式 → SKILL.md + script
5//!   - 失败驱动:持续失败模式 → 补全能力缺口的 Skill
6//! - **Refine**: failed skill → analyze error trace → LLM fix → retry (max 2 rounds)
7//! - **Retire**: low success rate or unused skills → archive
8//!
9//! ## React / Check / Retry
10//! 重度依赖大模型能力,每个环节都有校验与重试:
11//! - **Check**: L3 内容门禁、L4 安全扫描、test_skill_invoke 实测
12//! - **Retry**: 任何 LLM 输出 JSON 解析失败时,将错误反馈给大模型并重试 1 次
13//! - 代码修改/修复仅由大模型完成,不使用正则或模式匹配
14//!
15//! All evolved skills live in `chat/skills/_evolved/` with `.meta.json` metadata.
16//! A10: Newly generated skills go to `_evolved/_pending/` until user confirms.
17
18mod env_helper;
19mod generate;
20mod infer;
21mod parse;
22mod query;
23mod refine;
24mod repair;
25mod scan;
26mod validate;
27
28use std::collections::HashSet;
29use std::path::Path;
30
31use anyhow::Result;
32use tokio::task::block_in_place;
33
34use crate::EvolutionLlm;
35
36// ─── Constants (shared across submodules) ────────────────────────────────────
37
38pub(super) const SKILL_GENERATION_PROMPT: &str =
39    include_str!("../seed/evolution_prompts/skill_generation.seed.md");
40pub(super) const SKILL_GENERATION_FROM_FAILURES_PROMPT: &str =
41    include_str!("../seed/evolution_prompts/skill_generation_from_failures.seed.md");
42pub(super) const SKILL_REFINEMENT_PROMPT: &str =
43    include_str!("../seed/evolution_prompts/skill_refinement.seed.md");
44pub(super) const SKILL_EXECUTION_INFERENCE_PROMPT: &str =
45    include_str!("../seed/evolution_prompts/skill_execution_inference.seed.md");
46
47pub(super) const MAX_EVOLVED_SKILLS: usize = 20;
48pub(super) const MAX_REFINE_ROUNDS: usize = 3;
49pub(super) const MAX_PARSE_RETRIES: usize = 1;
50pub(super) const RETIRE_UNUSED_DAYS: i64 = 30;
51pub(super) const RETIRE_LOW_SUCCESS_RATE: f64 = 0.30;
52
53// ─── Skill metadata ───────────────────────────────────────────────────────────
54
55#[derive(Debug, Clone, serde::Serialize, serde::Deserialize)]
56pub struct SkillMeta {
57    pub name: String,
58    pub source_session: String,
59    pub created_at: String,
60    pub success_count: u32,
61    pub failure_count: u32,
62    pub call_count: u32,
63    pub last_used: Option<String>,
64    #[serde(default)]
65    pub archived: bool,
66    #[serde(default)]
67    pub generation_txn: String,
68    #[serde(default)]
69    pub needs_review: bool,
70}
71
72impl SkillMeta {
73    pub fn success_rate(&self) -> f64 {
74        if self.call_count == 0 {
75            return 1.0;
76        }
77        self.success_count as f64 / self.call_count as f64
78    }
79}
80
81// ─── Main entry: evolve skills ────────────────────────────────────────────────
82
83/// Run skill evolution: generate new skills or refine existing ones.
84pub async fn evolve_skills<L: EvolutionLlm>(
85    chat_root: &Path,
86    skills_root: Option<&Path>,
87    llm: &L,
88    model: &str,
89    txn_id: &str,
90    generate: bool,
91    force: bool,
92) -> Result<Vec<(String, String)>> {
93    let Some(skills_root) = skills_root else {
94        return Ok(Vec::new());
95    };
96    let mut changes = Vec::new();
97
98    let try_generate = generate || force;
99    let min_pattern_count: u32 = std::env::var("SKILLLITE_MIN_PATTERN_COUNT")
100        .ok()
101        .and_then(|v| v.parse().ok())
102        .unwrap_or(if force { 2 } else { 3 });
103
104    if try_generate {
105        // 单次 conn 预取成功/失败数据并执行 retire,减少 DB 打开次数
106        let (success_data, failure_data, retired) = block_in_place(|| {
107            let conn = crate::feedback::open_evolution_db(chat_root)?;
108            let (patterns_display, pattern_descs) =
109                query::query_repeated_patterns(&conn, min_pattern_count)?;
110            let success_executions = if pattern_descs.is_empty() {
111                String::new()
112            } else {
113                query::query_pattern_executions(&conn, &pattern_descs)?
114            };
115            let failed_patterns = query::query_failed_patterns(&conn, 2)?;
116            let failed_executions = query::query_failed_executions(&conn)?;
117            let retired = refine::retire_skills_with_conn(chat_root, skills_root, txn_id, &conn)?;
118            Ok::<_, anyhow::Error>((
119                (patterns_display, success_executions),
120                (failed_patterns, failed_executions),
121                retired,
122            ))
123        })?;
124        changes.extend(retired);
125        if let Ok(Some(name)) = generate::generate_skill_from_failures(
126            chat_root,
127            skills_root,
128            llm,
129            model,
130            txn_id,
131            Some(failure_data),
132        )
133        .await
134        {
135            changes.push(("skill_pending".to_string(), name));
136        }
137        if let Ok(Some(name)) = generate::generate_skill(
138            chat_root,
139            skills_root,
140            llm,
141            model,
142            txn_id,
143            min_pattern_count,
144            Some(success_data),
145        )
146        .await
147        {
148            changes.push(("skill_pending".to_string(), name));
149        }
150        if changes.is_empty() {
151            if let Ok(Some(name)) =
152                refine::refine_weakest_skill(chat_root, skills_root, llm, model, txn_id).await
153            {
154                changes.push(("skill_refined".to_string(), name));
155            }
156        }
157    } else {
158        let (retired, _) = block_in_place(|| {
159            let conn = crate::feedback::open_evolution_db(chat_root)?;
160            let retired = refine::retire_skills_with_conn(chat_root, skills_root, txn_id, &conn)?;
161            Ok::<_, anyhow::Error>((retired, ()))
162        })?;
163        changes.extend(retired);
164        match refine::refine_weakest_skill(chat_root, skills_root, llm, model, txn_id).await {
165            Ok(Some(name)) => changes.push(("skill_refined".to_string(), name)),
166            Ok(None) => {}
167            Err(e) => tracing::warn!("Skill refinement failed: {}", e),
168        }
169    }
170
171    // 同轮内名称去重:同一 name 的 skill_pending / skill_refined 只保留首次出现
172    let mut seen: HashSet<String> = HashSet::new();
173    changes.retain(|(t, id)| {
174        if t == "skill_pending" || t == "skill_refined" {
175            seen.insert(id.clone())
176        } else {
177            true
178        }
179    });
180
181    Ok(changes)
182}
183
184// ─── A10: Pending skill confirmation ─────────────────────────────────────────
185
186pub fn list_pending_skills(skills_root: &Path) -> Vec<String> {
187    list_pending_skills_with_review(skills_root)
188        .into_iter()
189        .map(|(name, _)| name)
190        .collect()
191}
192
193pub fn list_pending_skills_with_review(skills_root: &Path) -> Vec<(String, bool)> {
194    let pending_dir = skills_root.join("_evolved").join("_pending");
195    if !pending_dir.exists() {
196        return Vec::new();
197    }
198    std::fs::read_dir(&pending_dir)
199        .ok()
200        .into_iter()
201        .flatten()
202        .filter_map(|e| e.ok())
203        .filter(|e| e.path().is_dir() && e.path().join("SKILL.md").exists())
204        .map(|e| {
205            let name = e.file_name().to_string_lossy().to_string();
206            let needs_review = std::fs::read_to_string(e.path().join(".meta.json"))
207                .ok()
208                .and_then(|s| serde_json::from_str::<SkillMeta>(&s).ok())
209                .map(|m| m.needs_review)
210                .unwrap_or(false);
211            (name, needs_review)
212        })
213        .collect()
214}
215
216pub fn confirm_pending_skill(skills_root: &Path, skill_name: &str) -> Result<()> {
217    let pending_dir = skills_root.join("_evolved").join("_pending");
218    let evolved_dir = skills_root.join("_evolved");
219    let src = pending_dir.join(skill_name);
220    let dst = evolved_dir.join(skill_name);
221
222    if !src.exists() {
223        anyhow::bail!("待确认 Skill '{}' 不存在", skill_name);
224    }
225    if dst.exists() {
226        anyhow::bail!("Skill '{}' 已存在,请先删除或重命名", skill_name);
227    }
228
229    std::fs::rename(&src, &dst)?;
230    tracing::info!("Skill '{}' 已确认加入", skill_name);
231    Ok(())
232}
233
234pub fn reject_pending_skill(skills_root: &Path, skill_name: &str) -> Result<()> {
235    let pending_dir = skills_root.join("_evolved").join("_pending");
236    let src = pending_dir.join(skill_name);
237
238    if !src.exists() {
239        anyhow::bail!("待确认 Skill '{}' 不存在", skill_name);
240    }
241
242    std::fs::remove_dir_all(&src)?;
243    tracing::info!("Skill '{}' 已拒绝", skill_name);
244    Ok(())
245}
246
247// ─── Re-exports ──────────────────────────────────────────────────────────────
248
249pub use repair::{repair_one_skill, repair_skills};
250pub use scan::track_skill_usage;
251pub use validate::{validate_skills, SkillValidation};