skilllite_evolution/skill_synth/
mod.rs1mod 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
36pub(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#[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
81pub 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 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 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
184pub 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
247pub use repair::{repair_one_skill, repair_skills};
250pub use scan::track_skill_usage;
251pub use validate::{validate_skills, SkillValidation};