claude_agent/subagents/
loader.rs1use 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}