use std::io::Write;
use std::path::{Path, PathBuf};
use anyhow::{Context, Result};
use regex::Regex;
use std::sync::LazyLock;
use super::types::EvoSkillMeta;
static FRONTMATTER_RE: LazyLock<Regex> =
LazyLock::new(|| Regex::new(r"(?s)\A---\s*\n(.*?)\n---").unwrap());
pub struct AgentWorkspace {
pub root: PathBuf,
pub prompts_dir: PathBuf,
pub skills_dir: PathBuf,
pub drafts_dir: PathBuf,
pub tools_dir: PathBuf,
pub memory_dir: PathBuf,
pub evolution_dir: PathBuf,
}
impl AgentWorkspace {
pub fn new(root: impl AsRef<Path>) -> Self {
let root = root.as_ref().to_path_buf();
Self {
prompts_dir: root.join("prompts"),
skills_dir: root.join("skills"),
drafts_dir: root.join("skills").join("_drafts"),
tools_dir: root.join("tools"),
memory_dir: root.join("memory"),
evolution_dir: root.join("evolution"),
root,
}
}
pub fn ensure_dirs(&self) -> Result<()> {
for dir in [
&self.prompts_dir,
&self.skills_dir,
&self.tools_dir,
&self.memory_dir,
&self.evolution_dir,
] {
std::fs::create_dir_all(dir)
.with_context(|| format!("Failed to create {}", dir.display()))?;
}
Ok(())
}
pub fn read_prompt(&self) -> Result<String> {
let path = self.prompts_dir.join("system.md");
if path.exists() {
Ok(std::fs::read_to_string(&path)?)
} else {
Ok(String::new())
}
}
pub fn write_prompt(&self, content: &str) -> Result<()> {
std::fs::create_dir_all(&self.prompts_dir)?;
std::fs::write(self.prompts_dir.join("system.md"), content)?;
Ok(())
}
pub fn read_fragment(&self, name: &str) -> Result<String> {
let path = self.prompts_dir.join("fragments").join(name);
if path.exists() {
Ok(std::fs::read_to_string(&path)?)
} else {
Ok(String::new())
}
}
pub fn write_fragment(&self, name: &str, content: &str) -> Result<()> {
let frag_dir = self.prompts_dir.join("fragments");
std::fs::create_dir_all(&frag_dir)?;
std::fs::write(frag_dir.join(name), content)?;
Ok(())
}
pub fn list_fragments(&self) -> Result<Vec<String>> {
let frag_dir = self.prompts_dir.join("fragments");
if !frag_dir.exists() {
return Ok(Vec::new());
}
let mut names: Vec<String> = std::fs::read_dir(&frag_dir)?
.filter_map(|e| e.ok())
.filter(|e| e.path().is_file())
.filter_map(|e| e.file_name().to_str().map(String::from))
.collect();
names.sort();
Ok(names)
}
pub fn list_skills(&self) -> Vec<EvoSkillMeta> {
if !self.skills_dir.exists() {
return Vec::new();
}
let mut skills = Vec::new();
let mut entries: Vec<_> = std::fs::read_dir(&self.skills_dir)
.into_iter()
.flatten()
.flatten()
.filter(|e| e.path().is_dir())
.filter(|e| {
!e.file_name()
.to_str()
.map(|n| n.starts_with('_'))
.unwrap_or(true)
})
.collect();
entries.sort_by_key(|e| e.file_name());
for entry in entries {
let skill_file = entry.path().join("SKILL.md");
if skill_file.exists() {
let mut meta = parse_skill_frontmatter(&skill_file);
if let Ok(rel) = entry.path().strip_prefix(&self.root) {
meta.path = rel.display().to_string();
}
skills.push(meta);
}
}
skills
}
pub fn read_skill(&self, name: &str) -> Result<String> {
let path = self.skills_dir.join(name).join("SKILL.md");
if path.exists() {
Ok(std::fs::read_to_string(&path)?)
} else {
Ok(String::new())
}
}
pub fn write_skill(&self, name: &str, content: &str) -> Result<()> {
let skill_dir = self.skills_dir.join(name);
std::fs::create_dir_all(&skill_dir)?;
std::fs::write(skill_dir.join("SKILL.md"), content)?;
Ok(())
}
pub fn delete_skill(&self, name: &str) -> Result<()> {
let skill_dir = self.skills_dir.join(name);
if skill_dir.exists() {
std::fs::remove_dir_all(&skill_dir)?;
}
Ok(())
}
pub fn list_drafts(&self) -> Vec<(String, String)> {
if !self.drafts_dir.exists() {
return Vec::new();
}
let mut drafts = Vec::new();
if let Ok(entries) = std::fs::read_dir(&self.drafts_dir) {
for entry in entries.flatten() {
let path = entry.path();
if path.extension().and_then(|e| e.to_str()) == Some("md")
&& let (Some(name), Ok(content)) = (
path.file_stem().and_then(|s| s.to_str()).map(String::from),
std::fs::read_to_string(&path),
)
{
drafts.push((name, content));
}
}
}
drafts.sort_by(|a, b| a.0.cmp(&b.0));
drafts
}
pub fn write_draft(&self, name: &str, content: &str) -> Result<()> {
std::fs::create_dir_all(&self.drafts_dir)?;
std::fs::write(self.drafts_dir.join(format!("{name}.md")), content)?;
Ok(())
}
pub fn clear_drafts(&self) -> Result<()> {
if self.drafts_dir.exists() {
for entry in std::fs::read_dir(&self.drafts_dir)?.flatten() {
let path = entry.path();
if path.extension().and_then(|e| e.to_str()) == Some("md") {
std::fs::remove_file(path)?;
}
}
}
Ok(())
}
pub fn add_memory(&self, entry: &serde_json::Value, category: &str) -> Result<()> {
std::fs::create_dir_all(&self.memory_dir)?;
let path = self.memory_dir.join(format!("{category}.jsonl"));
let mut file = std::fs::OpenOptions::new()
.create(true)
.append(true)
.open(&path)?;
serde_json::to_writer(&mut file, entry)?;
writeln!(file)?;
Ok(())
}
pub fn read_memories(&self, category: &str, limit: usize) -> Result<Vec<serde_json::Value>> {
let path = self.memory_dir.join(format!("{category}.jsonl"));
if !path.exists() {
return Ok(Vec::new());
}
let content = std::fs::read_to_string(&path)?;
let entries: Vec<serde_json::Value> = content
.lines()
.filter(|l| !l.trim().is_empty())
.filter_map(|l| serde_json::from_str(l).ok())
.collect();
let start = entries.len().saturating_sub(limit);
Ok(entries[start..].to_vec())
}
pub fn read_all_memories(&self, limit: usize) -> Result<Vec<serde_json::Value>> {
if !self.memory_dir.exists() {
return Ok(Vec::new());
}
let mut all = Vec::new();
let mut paths: Vec<_> = std::fs::read_dir(&self.memory_dir)?
.filter_map(|e| e.ok())
.filter(|e| e.path().extension().and_then(|ext| ext.to_str()) == Some("jsonl"))
.map(|e| e.path())
.collect();
paths.sort();
for path in paths {
let category = path
.file_stem()
.and_then(|s| s.to_str())
.unwrap_or("unknown")
.to_string();
let content = std::fs::read_to_string(&path)?;
for line in content.lines() {
if !line.trim().is_empty()
&& let Ok(mut entry) = serde_json::from_str::<serde_json::Value>(line)
{
if let Some(obj) = entry.as_object_mut() {
obj.entry("_category")
.or_insert_with(|| serde_json::Value::String(category.clone()));
}
all.push(entry);
}
}
}
let start = all.len().saturating_sub(limit);
Ok(all[start..].to_vec())
}
pub fn read_evolution_history(&self) -> Result<Vec<serde_json::Value>> {
let path = self.evolution_dir.join("history.jsonl");
if !path.exists() {
return Ok(Vec::new());
}
let content = std::fs::read_to_string(&path)?;
Ok(content
.lines()
.filter(|l| !l.trim().is_empty())
.filter_map(|l| serde_json::from_str(l).ok())
.collect())
}
pub fn read_evolution_metrics(&self) -> Result<serde_json::Value> {
let path = self.evolution_dir.join("metrics.json");
if !path.exists() {
return Ok(serde_json::json!({}));
}
Ok(serde_json::from_str(&std::fs::read_to_string(&path)?)?)
}
}
fn parse_skill_frontmatter(path: &Path) -> EvoSkillMeta {
let text = match std::fs::read_to_string(path) {
Ok(t) => t,
Err(_) => {
return EvoSkillMeta {
name: dir_name(path),
description: String::new(),
path: String::new(),
};
}
};
if let Some(captures) = FRONTMATTER_RE.captures(&text)
&& let Some(yaml_str) = captures.get(1)
{
let mut name_val: Option<String> = None;
let mut desc_val: Option<String> = None;
for line in yaml_str.as_str().lines() {
let line = line.trim();
if let Some(v) = line.strip_prefix("name:") {
name_val = Some(v.trim().trim_matches('"').trim_matches('\'').to_string());
} else if let Some(v) = line.strip_prefix("description:") {
desc_val = Some(v.trim().trim_matches('"').trim_matches('\'').to_string());
}
}
if name_val.is_some() || desc_val.is_some() {
return EvoSkillMeta {
name: name_val.unwrap_or_else(|| dir_name(path)),
description: desc_val.unwrap_or_default(),
path: String::new(),
};
}
}
EvoSkillMeta {
name: dir_name(path),
description: String::new(),
path: String::new(),
}
}
fn dir_name(path: &Path) -> String {
path.parent()
.and_then(|p| p.file_name())
.and_then(|n| n.to_str())
.map(String::from)
.unwrap_or_default()
}