use std::sync::Arc;
use crate::core::capability::CapabilitySource;
use crate::tools::Tool;
#[derive(Debug, Clone, PartialEq)]
pub struct AgentDef {
pub name: String,
pub description: String,
pub tools: Option<Vec<String>>,
pub model: Option<String>,
pub system_prompt: String,
}
pub fn parse_agent_md(content: &str, fallback_name: &str) -> AgentDef {
let mut name = fallback_name.to_string();
let mut description = String::new();
let mut tools = None;
let mut model = None;
let mut body = content;
if let Some(rest) = content.strip_prefix("---") {
if let Some(end) = rest.find("\n---") {
let front = &rest[..end];
body = rest[end + 4..].trim_start_matches('\n');
for line in front.lines() {
let Some((k, v)) = line.split_once(':') else {
continue;
};
let (k, v) = (k.trim(), v.trim());
match k {
"name" => name = v.to_string(),
"description" => description = v.to_string(),
"model" => model = Some(v.to_string()),
"tools" => {
tools = Some(
v.split(',')
.map(|s| s.trim().to_string())
.filter(|s| !s.is_empty())
.collect(),
)
}
_ => {}
}
}
}
}
AgentDef {
name,
description,
tools,
model,
system_prompt: body.trim().to_string(),
}
}
pub fn load_agents(dir: &std::path::Path) -> Vec<AgentDef> {
let mut defs = Vec::new();
let Ok(entries) = std::fs::read_dir(dir) else {
return defs;
};
for entry in entries.flatten() {
let path = entry.path();
if path.extension().and_then(|e| e.to_str()) != Some("md") {
continue;
}
let stem = path.file_stem().and_then(|s| s.to_str()).unwrap_or("agent");
if let Ok(content) = std::fs::read_to_string(&path) {
defs.push(parse_agent_md(&content, stem));
}
}
defs
}
pub struct AgentRegistry {
defs: Arc<Vec<AgentDef>>,
task_tool: Arc<dyn Tool>,
}
impl AgentRegistry {
pub fn new(defs: Arc<Vec<AgentDef>>, task_tool: Arc<dyn Tool>) -> Self {
AgentRegistry { defs, task_tool }
}
}
impl CapabilitySource for AgentRegistry {
fn tools(&self) -> Vec<Arc<dyn Tool>> {
vec![self.task_tool.clone()]
}
fn prompt_fragment(&self) -> Option<String> {
if self.defs.is_empty() {
return None;
}
let mut s = String::from("## Sub-agents (spawn via the `task` tool)\n");
for d in self.defs.iter() {
s.push_str(&format!("- {}: {}\n", d.name, d.description));
}
Some(s)
}
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn parses_frontmatter_and_body() {
let md = "---\nname: reviewer\ndescription: reviews diffs\ntools: read, grep\nmodel: gpt-4o\n---\nYou review code carefully.";
let def = parse_agent_md(md, "fallback");
assert_eq!(def.name, "reviewer");
assert_eq!(def.description, "reviews diffs");
assert_eq!(
def.tools,
Some(vec!["read".to_string(), "grep".to_string()])
);
assert_eq!(def.model.as_deref(), Some("gpt-4o"));
assert_eq!(def.system_prompt, "You review code carefully.");
}
#[test]
fn no_frontmatter_uses_fallback_name_and_full_body() {
let def = parse_agent_md("just a prompt", "helper");
assert_eq!(def.name, "helper");
assert_eq!(def.tools, None);
assert_eq!(def.system_prompt, "just a prompt");
}
}