claude_agent/subagents/
loader.rs

1use std::path::Path;
2
3use serde::{Deserialize, Serialize};
4
5use super::SubagentDefinition;
6use crate::common::{DocumentLoader, SourceType, is_markdown, parse_frontmatter};
7
8#[derive(Debug, Clone, Serialize, Deserialize)]
9pub struct SubagentFrontmatter {
10    pub name: String,
11    pub description: String,
12    #[serde(default)]
13    pub tools: Option<String>,
14    #[serde(default)]
15    pub model: Option<String>,
16    #[serde(default)]
17    pub skills: Option<String>,
18    #[serde(default, rename = "source-type")]
19    pub source_type: Option<String>,
20}
21
22#[derive(Debug, Clone, Copy, Default)]
23pub struct SubagentLoader;
24
25impl SubagentLoader {
26    pub fn new() -> Self {
27        Self
28    }
29
30    fn build_subagent(
31        &self,
32        fm: SubagentFrontmatter,
33        body: String,
34        _path: Option<&Path>,
35    ) -> crate::Result<SubagentDefinition> {
36        let source = SourceType::from_str_opt(fm.source_type.as_deref());
37
38        let tools: Vec<String> = fm
39            .tools
40            .map(|t| t.split(',').map(|s| s.trim().to_string()).collect())
41            .unwrap_or_default();
42
43        let skills: Vec<String> = fm
44            .skills
45            .map(|s| s.split(',').map(|s| s.trim().to_string()).collect())
46            .unwrap_or_default();
47
48        let mut subagent = SubagentDefinition::new(fm.name, fm.description, body)
49            .with_source_type(source)
50            .with_tools(tools)
51            .with_skills(skills);
52
53        if let Some(model) = fm.model {
54            subagent = subagent.with_model(model);
55        }
56
57        Ok(subagent)
58    }
59}
60
61impl DocumentLoader<SubagentDefinition> for SubagentLoader {
62    fn parse_content(
63        &self,
64        content: &str,
65        path: Option<&Path>,
66    ) -> crate::Result<SubagentDefinition> {
67        let doc = parse_frontmatter::<SubagentFrontmatter>(content)?;
68        self.build_subagent(doc.frontmatter, doc.body, path)
69    }
70
71    fn doc_type_name(&self) -> &'static str {
72        "subagent"
73    }
74
75    fn file_filter(&self) -> fn(&Path) -> bool {
76        is_markdown
77    }
78}
79
80#[cfg(test)]
81mod tests {
82    use super::*;
83
84    #[test]
85    fn test_parse_subagent() {
86        let content = r#"---
87name: code-reviewer
88description: Expert code reviewer for quality checks
89tools: Read, Grep, Glob
90model: haiku
91---
92
93You are a senior code reviewer focusing on:
94- Code quality and best practices
95- Security vulnerabilities
96"#;
97
98        let loader = SubagentLoader::new();
99        let subagent = loader.parse_content(content, None).unwrap();
100
101        assert_eq!(subagent.name, "code-reviewer");
102        assert_eq!(subagent.tools, vec!["Read", "Grep", "Glob"]);
103        assert_eq!(subagent.model, Some("haiku".to_string()));
104        assert!(subagent.prompt.contains("senior code reviewer"));
105    }
106
107    #[test]
108    fn test_parse_subagent_with_skills() {
109        let content = r#"---
110name: full-agent
111description: Full featured agent
112tools: Read, Write, Bash(git:*)
113model: sonnet
114skills: security-check, linting
115---
116
117Full agent prompt.
118"#;
119
120        let loader = SubagentLoader::new();
121        let subagent = loader.parse_content(content, None).unwrap();
122
123        assert_eq!(subagent.skills, vec!["security-check", "linting"]);
124        assert_eq!(subagent.model, Some("sonnet".to_string()));
125    }
126
127    #[test]
128    fn test_parse_without_frontmatter() {
129        let content = "Just content without frontmatter";
130        let loader = SubagentLoader::new();
131        assert!(loader.parse_content(content, None).is_err());
132    }
133}