use std::fs;
use std::path::{Path, PathBuf};
use anyhow::{Context, Result};
use crate::agent::memory::{MemDocTier, MemoryDoc, MemoryStore};
const MIN_CLUSTER_SIZE: usize = 3;
const CLUSTER_SIMILARITY: f32 = 0.75;
pub fn find_cluster(
store: &MemoryStore,
doc_id: &str,
scope: &str,
) -> Result<Option<Vec<MemoryDoc>>> {
let source = store
.get_sync(doc_id)
.context("source doc not found in store")?
.clone();
if source.tier != MemDocTier::Core
|| source.scope != scope
|| source.tags.contains(&"crystallized".to_string())
{
return Ok(None);
}
let neighbours =
store.find_near_duplicates(doc_id, Some(scope), CLUSTER_SIMILARITY)?;
let mut cluster: Vec<MemoryDoc> = neighbours
.into_iter()
.filter(|(doc, _sim)| {
doc.tier == MemDocTier::Core
&& doc.scope == scope
&& !doc.tags.contains(&"crystallized".to_string())
})
.map(|(doc, _sim)| doc)
.collect();
cluster.insert(0, source);
if cluster.len() < MIN_CLUSTER_SIZE {
return Ok(None);
}
Ok(Some(cluster))
}
pub fn build_distill_prompt(cluster: &[MemoryDoc]) -> String {
let mut prompt = String::with_capacity(8192);
prompt.push_str(
"You are a skill-engineering expert. Below are related memory documents \
from an AI agent's long-term memory store. Distill them into a single \
SKILL.md file following the Anthropic skill-creator standard.\n\n\
\
## SKILL.md Standard\n\
\
**Frontmatter** (required fields):\n\
```yaml\n\
---\n\
name: skill-name-in-kebab-case\n\
description: >\n\
What the skill does AND when to invoke it. Be slightly pushy so the\n\
agent does not undertrigger. Example: \"How to do X. Use this skill\n\
whenever the user asks about X, Y, or Z, even if not phrased explicitly.\"\n\
---\n\
```\n\n\
\
**Body** (Markdown, imperative language, under 500 lines):\n\
- Use numbered steps or headers to structure the workflow.\n\
- Explain *why* each step matters, not just *what* to do.\n\
- Include a short example (Input / Output) where it helps.\n\
- If the skill needs a reusable helper script, note it as:\n\
`See scripts/helper.py — run with: python scripts/helper.py <args>`\n\
(do NOT write the script here; the caller will create it separately)\n\
- If the skill references large external docs, note them as:\n\
`See references/guide.md for detailed field descriptions`\n\n\
\
**Rules**:\n\
- Do not invent information beyond what the memory documents contain.\n\
- Merge overlapping facts; prefer the most-accessed version.\n\
- Use imperative voice: \"Check the config\", not \"You should check\".\n\
- Avoid ALL-CAPS MUST/NEVER; explain reasoning instead.\n\
- Keep total length under 300 lines unless complexity demands more.\n\n\
\
=== MEMORY DOCUMENTS ===\n\n",
);
for (i, doc) in cluster.iter().enumerate() {
prompt.push_str(&format!(
"--- Memory {} (access_count={}) ---\nKind: {}\nText:\n{}\n\n",
i + 1,
doc.access_count,
doc.kind,
doc.text,
));
}
prompt.push_str(
"=== END OF MEMORIES ===\n\n\
Produce ONLY the SKILL.md content — frontmatter + body. \
No explanation, no commentary outside the file.",
);
prompt
}
pub fn write_skill(skills_dir: &Path, slug: &str, content: &str) -> Result<PathBuf> {
let dir = skills_dir.join(slug);
fs::create_dir_all(&dir)
.with_context(|| format!("failed to create skill directory: {}", dir.display()))?;
let path = dir.join("SKILL.md");
fs::write(&path, content)
.with_context(|| format!("failed to write SKILL.md at {}", path.display()))?;
Ok(path)
}
pub fn slugify(name: &str) -> String {
let lower = name.to_lowercase();
let slug: String = lower
.chars()
.map(|c| if c.is_alphanumeric() { c } else { '-' })
.collect();
let mut result = String::with_capacity(slug.len());
let mut prev_hyphen = true; for ch in slug.chars() {
if ch == '-' {
if !prev_hyphen {
result.push('-');
}
prev_hyphen = true;
} else {
result.push(ch);
prev_hyphen = false;
}
}
if result.ends_with('-') {
result.pop();
}
if result.is_empty() {
"unnamed-skill".to_owned()
} else {
result
}
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn slugify_basic() {
assert_eq!(slugify("Web Search Pattern"), "web-search-pattern");
}
#[test]
fn slugify_special_chars() {
assert_eq!(slugify(" LLM--Retry Logic! "), "llm-retry-logic");
}
#[test]
fn slugify_already_clean() {
assert_eq!(slugify("hello-world"), "hello-world");
}
#[test]
fn slugify_empty() {
assert_eq!(slugify(""), "unnamed-skill");
}
}