use std::path::Path;
use anyhow::Result;
use crate::feedback;
use crate::gatekeeper_l1_path;
use crate::gatekeeper_l3_content;
use crate::EvolutionLlm;
use crate::EvolutionMessage;
use super::infer;
use super::parse;
use super::query;
use super::refine;
use super::repair;
use super::scan;
use super::validate;
use super::SkillMeta;
use super::MAX_EVOLVED_SKILLS;
use super::SKILL_GENERATION_FROM_FAILURES_PROMPT;
use super::SKILL_GENERATION_PROMPT;
pub(super) type SuccessQueryData = (String, String);
pub(super) type FailureQueryData = (String, String);
pub(super) async fn generate_skill<L: EvolutionLlm>(
chat_root: &Path,
skills_root: &Path,
llm: &L,
model: &str,
txn_id: &str,
min_pattern_count: u32,
pre_fetched: Option<SuccessQueryData>,
) -> Result<Option<String>> {
let evolved_dir = skills_root.join("_evolved");
let pending_dir = evolved_dir.join("_pending");
let current_count = infer::count_active_evolved_skills(&evolved_dir);
if current_count >= MAX_EVOLVED_SKILLS {
tracing::debug!(
"Evolved skill cap reached ({}/{}), skipping generation",
current_count,
MAX_EVOLVED_SKILLS
);
return Ok(None);
}
let (patterns, executions) = match pre_fetched {
Some((p, e)) => (p, e),
None => {
let conn = feedback::open_evolution_db(chat_root)?;
let (patterns_display, pattern_descs) =
query::query_repeated_patterns(&conn, min_pattern_count)?;
let executions = if !pattern_descs.is_empty() {
query::query_pattern_executions(&conn, &pattern_descs)?
} else {
String::new()
};
(patterns_display, executions)
}
};
if patterns.is_empty() {
return Ok(None);
}
let existing_skills = infer::list_existing_skill_names(skills_root);
let prompt = SKILL_GENERATION_PROMPT
.replace("{{repeated_patterns}}", &patterns)
.replace("{{successful_executions}}", &executions)
.replace("{{existing_skills}}", &existing_skills);
let messages = vec![EvolutionMessage::user(&prompt)];
let parsed = match parse::parse_skill_generation_with_retry(llm, model, &messages).await {
Ok(Some(s)) => s,
Ok(None) => return Ok(None),
Err(e) => {
tracing::warn!(
"Failed to parse skill generation output (after retry): {}",
e
);
return Ok(None);
}
};
let name = generate_skill_inner(
parsed,
chat_root,
skills_root,
&pending_dir,
txn_id,
llm,
model,
)
.await?;
if let Some(ref n) = name {
tracing::info!("Generated evolved skill (pending confirmation): {}", n);
}
Ok(name)
}
pub(super) async fn generate_skill_inner<L: EvolutionLlm>(
parsed: parse::GeneratedSkill,
chat_root: &Path,
skills_root: &Path,
pending_dir: &Path,
txn_id: &str,
llm: &L,
model: &str,
) -> Result<Option<String>> {
if let Err(e) = gatekeeper_l3_content(&parsed.script_content) {
tracing::warn!("L3 rejected generated skill script: {}", e);
return Ok(None);
}
if let Err(e) = gatekeeper_l3_content(&parsed.skill_md_content) {
tracing::warn!("L3 rejected generated SKILL.md: {}", e);
return Ok(None);
}
if let Some(doc_err) = validate::check_skill_md_completeness_heuristic(&parsed.skill_md_content)
{
tracing::warn!(
"Generated SKILL.md for '{}' incomplete ({}), skipping write to avoid later validate failure",
parsed.name, doc_err
);
return Ok(None);
}
let skill_dir = pending_dir.join(&parsed.name);
if skill_dir.exists() && skill_dir.join("SKILL.md").exists() {
tracing::debug!(
"Skill '{}' already in pending (same name), skipping to avoid duplicate",
parsed.name
);
return Ok(None);
}
let dedup_desc = std::env::var("SKILLLITE_SKILL_DEDUP_DESCRIPTION")
.map(|v| v != "0")
.unwrap_or(true);
if dedup_desc {
for (existing_name, existing_desc) in infer::list_pending_skill_descriptions(pending_dir) {
if infer::is_description_similar(&parsed.description, &existing_desc) {
tracing::debug!(
"Skill '{}' description similar to pending '{}', skipping duplicate",
parsed.name,
existing_name
);
return Ok(None);
}
}
}
if !gatekeeper_l1_path(chat_root, &skill_dir, Some(skills_root)) {
tracing::warn!("L1 rejected skill directory: {}", skill_dir.display());
return Ok(None);
}
std::fs::create_dir_all(&skill_dir)?;
let script_path = skill_dir.join(&parsed.entry_point);
let skill_md_path = skill_dir.join("SKILL.md");
let needs_network = scan::skill_md_needs_network(&parsed.skill_md_content);
let scan_result = scan::run_l4_scan(&parsed.script_content, &script_path, needs_network)?;
if !scan_result {
let refined = refine::refine_loop(
llm,
model,
&skill_dir,
&parsed.name,
&parsed.description,
&parsed.entry_point,
&parsed.script_content,
"Security scan found critical/high issues",
"security_scan",
needs_network,
)
.await?;
match refined {
Some(fixed_script) => {
write_skill_files(
&skill_dir,
&skill_md_path,
&script_path,
&parsed.skill_md_content,
&fixed_script,
&parsed.name,
txn_id,
false,
)?;
let (ep, ti) = infer::infer_skill_execution(llm, model, &skill_dir)
.await
.unwrap_or_else(|_| (parsed.entry_point.clone(), "{}".to_string()));
let _ =
repair::repair_one_skill(llm, model, &skill_dir, &parsed.name, &ep, &ti, None)
.await?;
}
None => {
let final_script = parsed.script_content.clone();
write_skill_files(
&skill_dir,
&skill_md_path,
&script_path,
&parsed.skill_md_content,
&final_script,
&parsed.name,
txn_id,
true,
)?;
tracing::info!(
"Skill '{}' saved as draft (L4 未通过,需人工审核后 confirm)",
parsed.name
);
}
}
} else {
let final_script = parsed.script_content.clone();
write_skill_files(
&skill_dir,
&skill_md_path,
&script_path,
&parsed.skill_md_content,
&final_script,
&parsed.name,
txn_id,
false,
)?;
let (ep, ti) = infer::infer_skill_execution(llm, model, &skill_dir)
.await
.unwrap_or_else(|_| (parsed.entry_point.clone(), "{}".to_string()));
let _ =
repair::repair_one_skill(llm, model, &skill_dir, &parsed.name, &ep, &ti, None).await?;
}
Ok(Some(parsed.name))
}
pub(super) async fn generate_skill_from_failures<L: EvolutionLlm>(
chat_root: &Path,
skills_root: &Path,
llm: &L,
model: &str,
txn_id: &str,
pre_fetched: Option<FailureQueryData>,
) -> Result<Option<String>> {
let evolved_dir = skills_root.join("_evolved");
let pending_dir = evolved_dir.join("_pending");
if infer::count_active_evolved_skills(&evolved_dir) >= MAX_EVOLVED_SKILLS {
return Ok(None);
}
let (failed_patterns, failed_executions) = match pre_fetched {
Some((p, e)) => (p, e),
None => {
let conn = feedback::open_evolution_db(chat_root)?;
let patterns = query::query_failed_patterns(&conn, 2)?;
let executions = query::query_failed_executions(&conn)?;
(patterns, executions)
}
};
if failed_patterns.is_empty() {
return Ok(None);
}
let existing_skills = infer::list_existing_skill_names(skills_root);
let prompt = SKILL_GENERATION_FROM_FAILURES_PROMPT
.replace("{{failed_patterns}}", &failed_patterns)
.replace("{{failed_executions}}", &failed_executions)
.replace("{{existing_skills}}", &existing_skills);
let messages = vec![EvolutionMessage::user(&prompt)];
let parsed = match parse::parse_skill_generation_with_retry(llm, model, &messages).await {
Ok(Some(s)) => s,
Ok(None) => return Ok(None),
Err(e) => {
tracing::warn!("Failed to parse failure-driven skill output: {}", e);
return Ok(None);
}
};
let name = generate_skill_inner(
parsed,
chat_root,
skills_root,
&pending_dir,
txn_id,
llm,
model,
)
.await?;
if let Some(ref n) = name {
tracing::info!("Generated failure-driven skill (补全): {}", n);
}
Ok(name)
}
#[allow(clippy::too_many_arguments)]
fn write_skill_files(
skill_dir: &Path,
skill_md_path: &Path,
script_path: &Path,
skill_md: &str,
script: &str,
name: &str,
txn_id: &str,
needs_review: bool,
) -> Result<()> {
skilllite_fs::write_file(skill_md_path, skill_md)?;
skilllite_fs::write_file(script_path, script)?;
#[cfg(unix)]
{
use std::os::unix::fs::PermissionsExt;
let _ = std::fs::set_permissions(script_path, std::fs::Permissions::from_mode(0o755));
}
let meta = SkillMeta {
name: name.to_string(),
source_session: String::new(),
created_at: chrono::Utc::now().to_rfc3339(),
success_count: 0,
failure_count: 0,
call_count: 0,
last_used: None,
archived: false,
generation_txn: txn_id.to_string(),
needs_review,
};
let meta_path = skill_dir.join(".meta.json");
skilllite_fs::write_file(&meta_path, &serde_json::to_string_pretty(&meta)?)?;
Ok(())
}