1use serde::Deserialize;
32
33#[derive(Debug, Clone, Deserialize)]
38#[serde(untagged)]
39pub enum AgentDef {
40 From {
42 from: String,
44 },
45
46 External {
48 file: String,
50 },
51
52 Inline {
54 system: String,
56
57 #[serde(default = "default_provider")]
59 provider: String,
60
61 model: Option<String>,
63
64 max_turns: Option<u32>,
66
67 temperature: Option<f32>,
69
70 #[serde(default)]
79 skills: Option<Vec<String>>,
80 },
81}
82
83fn default_provider() -> String {
84 "claude".to_string()
85}
86
87impl AgentDef {
88 pub fn is_external(&self) -> bool {
90 matches!(self, AgentDef::External { .. } | AgentDef::From { .. })
91 }
92
93 pub fn is_inline(&self) -> bool {
95 matches!(self, AgentDef::Inline { .. })
96 }
97
98 pub fn is_from(&self) -> bool {
100 matches!(self, AgentDef::From { .. })
101 }
102
103 pub fn file_path(&self) -> Option<&str> {
105 match self {
106 AgentDef::External { file } => Some(file),
107 AgentDef::From { from } => Some(from),
108 AgentDef::Inline { .. } => None,
109 }
110 }
111
112 pub fn source_path(&self) -> Option<&str> {
114 self.file_path()
115 }
116
117 pub fn skills(&self) -> Option<&Vec<String>> {
122 match self {
123 AgentDef::Inline { skills, .. } => skills.as_ref(),
124 _ => None,
125 }
126 }
127}
128
129#[cfg(test)]
130mod tests {
131 use super::*;
132 use crate::serde_yaml;
133
134 #[test]
135 fn test_agent_def_from() {
136 let yaml = r#"
137from: ./agents/researcher
138"#;
139 let def: AgentDef = serde_yaml::from_str(yaml).unwrap();
140 assert!(def.is_external());
141 assert!(def.is_from());
142 assert_eq!(def.file_path(), Some("./agents/researcher"));
143 }
144
145 #[test]
146 fn test_agent_def_from_md() {
147 let yaml = r#"
148from: ./agents/reviewer.md
149"#;
150 let def: AgentDef = serde_yaml::from_str(yaml).unwrap();
151 assert!(def.is_from());
152 assert_eq!(def.source_path(), Some("./agents/reviewer.md"));
153 }
154
155 #[test]
156 fn test_agent_def_external() {
157 let yaml = r#"
158file: ./agents/researcher.agent.yaml
159"#;
160 let def: AgentDef = serde_yaml::from_str(yaml).unwrap();
161 assert!(def.is_external());
162 assert!(!def.is_from());
163 assert_eq!(def.file_path(), Some("./agents/researcher.agent.yaml"));
164 }
165
166 #[test]
167 fn test_agent_def_inline_minimal() {
168 let yaml = r#"
169system: "You are a helpful assistant."
170"#;
171 let def: AgentDef = serde_yaml::from_str(yaml).unwrap();
172 assert!(def.is_inline());
173 if let AgentDef::Inline {
174 system,
175 provider,
176 model,
177 max_turns,
178 temperature,
179 skills,
180 } = def
181 {
182 assert_eq!(system, "You are a helpful assistant.");
183 assert_eq!(provider, "claude"); assert!(model.is_none());
185 assert!(max_turns.is_none());
186 assert!(temperature.is_none());
187 assert!(skills.is_none()); }
189 }
190
191 #[test]
192 fn test_agent_def_inline_full() {
193 let yaml = r#"
194system: "You are a translator."
195provider: openai
196model: gpt-4o
197max_turns: 5
198temperature: 0.7
199"#;
200 let def: AgentDef = serde_yaml::from_str(yaml).unwrap();
201 assert!(def.is_inline());
202 if let AgentDef::Inline {
203 system,
204 provider,
205 model,
206 max_turns,
207 temperature,
208 skills,
209 } = def
210 {
211 assert_eq!(system, "You are a translator.");
212 assert_eq!(provider, "openai");
213 assert_eq!(model, Some("gpt-4o".to_string()));
214 assert_eq!(max_turns, Some(5));
215 assert_eq!(temperature, Some(0.7));
216 assert!(skills.is_none()); }
218 }
219
220 #[test]
221 fn test_agent_def_is_external() {
222 let external = AgentDef::External {
223 file: "test.yaml".to_string(),
224 };
225 let from = AgentDef::From {
226 from: "./agents/test".to_string(),
227 };
228 let inline = AgentDef::Inline {
229 system: "test".to_string(),
230 provider: "claude".to_string(),
231 model: None,
232 max_turns: None,
233 temperature: None,
234 skills: None,
235 };
236
237 assert!(external.is_external());
238 assert!(!external.is_inline());
239 assert!(!external.is_from());
240
241 assert!(from.is_external());
242 assert!(from.is_from());
243 assert!(!from.is_inline());
244
245 assert!(!inline.is_external());
246 assert!(inline.is_inline());
247 assert!(!inline.is_from());
248 }
249
250 #[test]
251 fn test_agent_def_file_path() {
252 let external = AgentDef::External {
253 file: "path/to/agent.yaml".to_string(),
254 };
255 let from = AgentDef::From {
256 from: "./agents/researcher".to_string(),
257 };
258 let inline = AgentDef::Inline {
259 system: "test".to_string(),
260 provider: "claude".to_string(),
261 model: None,
262 max_turns: None,
263 temperature: None,
264 skills: None,
265 };
266
267 assert_eq!(external.file_path(), Some("path/to/agent.yaml"));
268 assert_eq!(from.file_path(), Some("./agents/researcher"));
269 assert_eq!(from.source_path(), Some("./agents/researcher"));
270 assert_eq!(inline.file_path(), None);
271 }
272
273 #[test]
278 fn test_agent_def_inline_with_skills_array() {
279 let yaml = r#"
280 system: "You are a helpful assistant"
281 provider: claude
282 skills:
283 - ./skills/research.md
284 - ./skills/writing.md
285 "#;
286 let agent: AgentDef = serde_yaml::from_str(yaml).unwrap();
287
288 assert!(agent.is_inline());
289 let skills = agent.skills().expect("skills should be Some");
290 assert_eq!(skills.len(), 2);
291 assert_eq!(skills[0], "./skills/research.md");
292 assert_eq!(skills[1], "./skills/writing.md");
293 }
294
295 #[test]
296 fn test_agent_def_skills_helper_returns_some() {
297 let inline = AgentDef::Inline {
298 system: "test".to_string(),
299 provider: "claude".to_string(),
300 model: None,
301 max_turns: None,
302 temperature: None,
303 skills: Some(vec!["./skill.md".to_string()]),
304 };
305
306 assert!(inline.skills().is_some());
307 assert_eq!(inline.skills().unwrap().len(), 1);
308 }
309
310 #[test]
311 fn test_agent_def_skills_helper_returns_none_for_no_skills() {
312 let inline = AgentDef::Inline {
313 system: "test".to_string(),
314 provider: "claude".to_string(),
315 model: None,
316 max_turns: None,
317 temperature: None,
318 skills: None,
319 };
320
321 assert!(inline.skills().is_none());
322 }
323
324 #[test]
325 fn test_agent_def_skills_helper_returns_none_for_external() {
326 let external = AgentDef::External {
327 file: "agent.yaml".to_string(),
328 };
329
330 assert!(external.skills().is_none());
332 }
333
334 #[test]
335 fn test_agent_def_empty_skills_array() {
336 let yaml = r#"
337 system: "You are a helpful assistant"
338 provider: claude
339 skills: []
340 "#;
341 let agent: AgentDef = serde_yaml::from_str(yaml).unwrap();
342
343 let skills = agent.skills().expect("empty array should still be Some");
344 assert!(skills.is_empty());
345 }
346
347 #[test]
348 fn test_agent_def_skills_with_pkg_uri() {
349 let yaml = r#"
350 system: "You are a helpful assistant"
351 provider: claude
352 skills:
353 - pkg:@supernovae/research@1.0.0/skills/deep-research.md
354 - pkg:@supernovae/writing@2.0.0/skills/technical-writing.md
355 "#;
356 let agent: AgentDef = serde_yaml::from_str(yaml).unwrap();
357
358 let skills = agent.skills().expect("skills should be Some");
359 assert_eq!(skills.len(), 2);
360 assert!(skills[0].starts_with("pkg:@"));
361 assert!(skills[1].starts_with("pkg:@"));
362 assert!(skills[0].contains("@1.0.0"));
363 assert!(skills[1].contains("@2.0.0"));
364 }
365
366 #[test]
367 fn test_agent_def_skills_with_relative_paths() {
368 let yaml = r#"
369 system: "You are a helpful assistant"
370 provider: claude
371 skills:
372 - ./skills/local-skill.md
373 - ../shared/common-skill.md
374 - skills/nested/deep-skill.md
375 "#;
376 let agent: AgentDef = serde_yaml::from_str(yaml).unwrap();
377
378 let skills = agent.skills().expect("skills should be Some");
379 assert_eq!(skills.len(), 3);
380 assert_eq!(skills[0], "./skills/local-skill.md");
381 assert_eq!(skills[1], "../shared/common-skill.md");
382 assert_eq!(skills[2], "skills/nested/deep-skill.md");
383 }
384
385 #[test]
386 fn test_agent_def_skills_mixed_pkg_and_relative() {
387 let yaml = r#"
392 system: "You are a helpful assistant"
393 provider: claude
394 skills:
395 - ./skills/project-specific.md
396 - pkg:@supernovae/research@1.0.0/skills/research.md
397 "#;
398 let agent: AgentDef = serde_yaml::from_str(yaml).unwrap();
399
400 let skills = agent.skills().expect("skills should be Some");
401 assert_eq!(skills.len(), 2);
402
403 assert!(skills[0].starts_with("./"));
405 assert!(skills[1].starts_with("pkg:"));
407 }
408}