1mod llm;
4mod memory;
5mod provider;
6pub mod spawner;
7pub(crate) mod storage;
8mod tool;
9
10pub use llm::{CliHitlMetadata, CliHitlStyle, CliMetadata, CliPromptStyle, LLMConfig, LLMSelector};
11pub use memory::MemoryConfig;
12pub use provider::{
13 BuiltinProviderConfig, ProviderPolicyConfig, ProviderSecurityConfig, ProvidersConfig,
14 ToolAliasesConfig, ToolPolicyConfig, YamlProviderConfig, YamlToolConfig,
15};
16pub use spawner::{AutoSpawnEntry, OrchestrationToolsConfig, SpawnerConfig, TemplateSource};
17pub use storage::{FileStorageConfig, RedisStorageConfig, SqliteStorageConfig, StorageConfig};
18pub use tool::{StructuredToolEntry, ToolConfig, ToolEntry};
19
20use serde::{Deserialize, Serialize};
21use std::collections::HashMap;
22
23use ai_agents_context::ContextSource;
24use ai_agents_core::{AgentError, Result};
25use ai_agents_disambiguation::DisambiguationConfig;
26use ai_agents_hitl::HITLConfig;
27use ai_agents_persona::PersonaConfig;
28use ai_agents_process::ProcessConfig;
29use ai_agents_reasoning::{ReasoningConfig, ReflectionConfig};
30use ai_agents_recovery::ErrorRecoveryConfig;
31use ai_agents_skills::SkillRef;
32use ai_agents_state::StateConfig;
33use ai_agents_tools::ToolSecurityConfig;
34
35use super::{ParallelToolsConfig, StreamingConfig};
36
37#[derive(Debug, Clone, Serialize, Deserialize)]
38pub struct AgentSpec {
39 pub name: String,
40
41 #[serde(default = "default_version")]
42 pub version: String,
43
44 #[serde(default, skip_serializing_if = "Option::is_none")]
45 pub description: Option<String>,
46
47 pub system_prompt: String,
48
49 #[serde(default)]
50 pub llm: LLMConfigOrSelector,
51
52 #[serde(default)]
53 pub llms: HashMap<String, LLMConfig>,
54
55 #[serde(default)]
56 pub skills: Vec<SkillRef>,
57
58 #[serde(default)]
59 pub memory: MemoryConfig,
60
61 #[serde(default)]
62 pub storage: StorageConfig,
63
64 #[serde(default, skip_serializing_if = "Option::is_none")]
65 pub tools: Option<Vec<ToolConfig>>,
66
67 #[serde(default = "default_max_iterations")]
68 pub max_iterations: u32,
69
70 #[serde(default = "default_max_context_tokens")]
71 pub max_context_tokens: u32,
72
73 #[serde(default)]
74 pub error_recovery: ErrorRecoveryConfig,
75
76 #[serde(default)]
77 pub tool_security: ToolSecurityConfig,
78
79 #[serde(default)]
80 pub process: ProcessConfig,
81
82 #[serde(default)]
83 pub context: HashMap<String, ContextSource>,
84
85 #[serde(default)]
86 pub states: Option<StateConfig>,
87
88 #[serde(default)]
89 pub parallel_tools: ParallelToolsConfig,
90
91 #[serde(default)]
92 pub streaming: StreamingConfig,
93
94 #[serde(default)]
95 pub hitl: Option<HITLConfig>,
96
97 #[serde(default)]
98 pub reasoning: ReasoningConfig,
99
100 #[serde(default)]
101 pub reflection: ReflectionConfig,
102
103 #[serde(default)]
104 pub disambiguation: DisambiguationConfig,
105
106 #[serde(default)]
107 pub providers: ProvidersConfig,
108
109 #[serde(default)]
110 pub provider_security: ProviderSecurityConfig,
111
112 #[serde(default)]
113 pub tool_aliases: ToolAliasesConfig,
114
115 #[serde(skip_serializing_if = "Option::is_none")]
116 pub metadata: Option<serde_json::Value>,
117
118 #[serde(default, skip_serializing_if = "Option::is_none")]
120 pub spawner: Option<SpawnerConfig>,
121
122 #[serde(default, skip_serializing_if = "Option::is_none")]
124 pub persona: Option<PersonaConfig>,
125}
126
127#[derive(Debug, Clone, Serialize, Deserialize)]
128#[serde(untagged)]
129pub enum LLMConfigOrSelector {
130 Config(LLMConfig),
131 Selector(LLMSelector),
132}
133
134impl Default for LLMConfigOrSelector {
135 fn default() -> Self {
136 LLMConfigOrSelector::Config(LLMConfig::default())
137 }
138}
139
140impl LLMConfigOrSelector {
141 pub fn as_config(&self) -> Option<&LLMConfig> {
142 match self {
143 LLMConfigOrSelector::Config(c) => Some(c),
144 LLMConfigOrSelector::Selector(_) => None,
145 }
146 }
147
148 pub fn as_selector(&self) -> Option<&LLMSelector> {
149 match self {
150 LLMConfigOrSelector::Config(_) => None,
151 LLMConfigOrSelector::Selector(s) => Some(s),
152 }
153 }
154
155 pub fn get_default_alias(&self) -> String {
156 match self {
157 LLMConfigOrSelector::Config(_) => "default".to_string(),
158 LLMConfigOrSelector::Selector(s) => s.default.clone(),
159 }
160 }
161
162 pub fn get_router_alias(&self) -> Option<String> {
163 match self {
164 LLMConfigOrSelector::Config(_) => None,
165 LLMConfigOrSelector::Selector(s) => s.router.clone(),
166 }
167 }
168}
169
170fn default_version() -> String {
171 "1.0.0".to_string()
172}
173
174fn default_max_iterations() -> u32 {
175 10
176}
177
178fn default_max_context_tokens() -> u32 {
179 4096
180}
181
182impl Default for AgentSpec {
183 fn default() -> Self {
184 Self {
185 name: "Agent".to_string(),
186 version: default_version(),
187 description: None,
188 system_prompt: "You are a helpful assistant.".to_string(),
189 llm: LLMConfigOrSelector::default(),
190 llms: HashMap::new(),
191 skills: vec![],
192 memory: MemoryConfig::default(),
193 storage: StorageConfig::default(),
194 tools: None,
195 max_iterations: default_max_iterations(),
196 max_context_tokens: default_max_context_tokens(),
197 error_recovery: ErrorRecoveryConfig::default(),
198 tool_security: ToolSecurityConfig::default(),
199 process: ProcessConfig::default(),
200 context: HashMap::new(),
201 states: None,
202 parallel_tools: ParallelToolsConfig::default(),
203 streaming: StreamingConfig::default(),
204 hitl: None,
205 reasoning: ReasoningConfig::default(),
206 reflection: ReflectionConfig::default(),
207 disambiguation: DisambiguationConfig::default(),
208 providers: ProvidersConfig::default(),
209 provider_security: ProviderSecurityConfig::default(),
210 tool_aliases: ToolAliasesConfig::default(),
211 metadata: None,
212 spawner: None,
213 persona: None,
214 }
215 }
216}
217
218impl AgentSpec {
219 pub fn validate(&self) -> Result<()> {
220 if self.name.is_empty() {
221 return Err(AgentError::InvalidSpec(
222 "Agent name cannot be empty".to_string(),
223 ));
224 }
225
226 if self.system_prompt.is_empty() {
227 return Err(AgentError::InvalidSpec(
228 "System prompt cannot be empty".to_string(),
229 ));
230 }
231
232 if self.max_iterations == 0 {
233 return Err(AgentError::InvalidSpec(
234 "Max iterations must be greater than 0".to_string(),
235 ));
236 }
237
238 if let Some(ref states) = self.states {
239 states.validate()?;
240 }
241
242 Ok(())
243 }
244
245 pub fn has_multi_llm(&self) -> bool {
246 !self.llms.is_empty()
247 }
248
249 pub fn has_skills(&self) -> bool {
250 !self.skills.is_empty()
251 }
252
253 pub fn has_process(&self) -> bool {
254 !self.process.input.is_empty() || !self.process.output.is_empty()
255 }
256
257 pub fn has_tool_security(&self) -> bool {
258 self.tool_security.enabled
259 }
260
261 pub fn has_states(&self) -> bool {
262 self.states.is_some()
263 }
264
265 pub fn has_context(&self) -> bool {
266 !self.context.is_empty()
267 }
268
269 pub fn has_parallel_tools(&self) -> bool {
270 self.parallel_tools.enabled
271 }
272
273 pub fn has_streaming(&self) -> bool {
274 self.streaming.enabled
275 }
276
277 pub fn has_hitl(&self) -> bool {
278 self.hitl.is_some()
279 }
280
281 pub fn has_storage(&self) -> bool {
282 !self.storage.is_none()
283 }
284
285 pub fn has_providers(&self) -> bool {
286 self.providers.yaml.is_some()
287 }
288
289 pub fn has_tool_aliases(&self) -> bool {
290 !self.tool_aliases.tools.is_empty()
291 }
292
293 pub fn has_reasoning(&self) -> bool {
294 self.reasoning.is_enabled()
295 }
296
297 pub fn has_reflection(&self) -> bool {
298 self.reflection.requires_evaluation()
299 }
300
301 pub fn has_disambiguation(&self) -> bool {
302 self.disambiguation.is_enabled()
303 }
304
305 pub fn has_persona(&self) -> bool {
306 self.persona.as_ref().map_or(false, |p| p.is_configured())
307 }
308}
309
310#[cfg(test)]
311mod tests {
312 use super::*;
313
314 #[test]
315 fn test_agent_spec_minimal() {
316 let yaml = r#"
317name: TestAgent
318system_prompt: "You are a helpful assistant."
319llm:
320 provider: openai
321 model: gpt-4
322"#;
323 let spec: AgentSpec = serde_yaml::from_str(yaml).unwrap();
324 assert_eq!(spec.name, "TestAgent");
325 assert_eq!(spec.version, "1.0.0");
326 assert_eq!(spec.max_iterations, 10);
327 assert!(spec.validate().is_ok());
328 }
329
330 #[test]
331 fn test_agent_spec_with_states() {
332 let yaml = r#"
333name: StatefulAgent
334system_prompt: "You are helpful."
335llm:
336 provider: openai
337 model: gpt-4
338states:
339 initial: greeting
340 states:
341 greeting:
342 prompt: "Welcome!"
343 transitions:
344 - to: support
345 when: "user needs help"
346 support:
347 prompt: "How can I help?"
348"#;
349 let spec: AgentSpec = serde_yaml::from_str(yaml).unwrap();
350 assert!(spec.has_states());
351 assert!(spec.validate().is_ok());
352 }
353
354 #[test]
355 fn test_agent_spec_with_context() {
356 let yaml = r#"
357name: ContextAgent
358system_prompt: "Hello, {{ context.user.name }}!"
359llm:
360 provider: openai
361 model: gpt-4
362context:
363 user:
364 type: runtime
365 required: true
366 time:
367 type: builtin
368 source: datetime
369 refresh: per_turn
370"#;
371 let spec: AgentSpec = serde_yaml::from_str(yaml).unwrap();
372 assert!(spec.has_context());
373 assert_eq!(spec.context.len(), 2);
374 }
375
376 #[test]
377 fn test_agent_spec_with_tool_security() {
378 let yaml = r#"
379name: SecureAgent
380version: 2.0.0
381system_prompt: "You are an advanced AI."
382llm:
383 provider: openai
384 model: gpt-4
385max_context_tokens: 8192
386error_recovery:
387 default:
388 max_retries: 5
389tool_security:
390 enabled: true
391 default_timeout_ms: 10000
392 tools:
393 http:
394 rate_limit: 10
395 blocked_domains:
396 - evil.com
397process:
398 input:
399 - type: normalize
400 config:
401 trim: true
402"#;
403 let spec: AgentSpec = serde_yaml::from_str(yaml).unwrap();
404 assert_eq!(spec.name, "SecureAgent");
405 assert_eq!(spec.max_context_tokens, 8192);
406 assert_eq!(spec.error_recovery.default.max_retries, 5);
407 assert!(spec.tool_security.enabled);
408 assert!(spec.has_tool_security());
409 assert!(!spec.process.input.is_empty());
410 assert!(spec.has_process());
411 }
412
413 #[test]
414 fn test_agent_spec_with_multi_llm() {
415 let yaml = r#"
416name: MultiLLMAgent
417system_prompt: "You are helpful."
418llms:
419 default:
420 provider: openai
421 model: gpt-4.1-nano
422 router:
423 provider: openai
424 model: gpt-4.1-nano
425llm:
426 default: default
427 router: router
428"#;
429 let spec: AgentSpec = serde_yaml::from_str(yaml).unwrap();
430 assert!(spec.has_multi_llm());
431 assert_eq!(spec.llms.len(), 2);
432 assert!(spec.llms.contains_key("default"));
433 assert!(spec.llms.contains_key("router"));
434 }
435
436 #[test]
437 fn test_agent_spec_with_skills() {
438 let yaml = r#"
439name: SkillAgent
440system_prompt: "You are helpful."
441llm:
442 provider: openai
443 model: gpt-4
444skills:
445 - weather_clothes
446 - file: ./custom.yaml
447 - id: inline_skill
448 description: "An inline skill"
449 trigger: "When user asks"
450 steps:
451 - prompt: "Hello"
452"#;
453 let spec: AgentSpec = serde_yaml::from_str(yaml).unwrap();
454 assert!(spec.has_skills());
455 assert_eq!(spec.skills.len(), 3);
456 }
457
458 #[test]
459 fn test_agent_spec_validation_empty_name() {
460 let mut spec = AgentSpec::default();
461 spec.name = "".to_string();
462 assert!(spec.validate().is_err());
463
464 spec.name = "Valid".to_string();
465 assert!(spec.validate().is_ok());
466 }
467
468 #[test]
469 fn test_agent_spec_validation_empty_prompt() {
470 let mut spec = AgentSpec::default();
471 spec.system_prompt = "".to_string();
472 assert!(spec.validate().is_err());
473
474 spec.system_prompt = "Valid prompt".to_string();
475 assert!(spec.validate().is_ok());
476 }
477
478 #[test]
479 fn test_agent_spec_validation_zero_iterations() {
480 let mut spec = AgentSpec::default();
481 spec.max_iterations = 0;
482 assert!(spec.validate().is_err());
483
484 spec.max_iterations = 5;
485 assert!(spec.validate().is_ok());
486 }
487
488 #[test]
489 fn test_agent_spec_with_parallel_tools() {
490 let yaml = r#"
491name: ParallelAgent
492system_prompt: "You are helpful."
493llm:
494 provider: openai
495 model: gpt-4
496parallel_tools:
497 enabled: true
498 max_parallel: 10
499"#;
500 let spec: AgentSpec = serde_yaml::from_str(yaml).unwrap();
501 assert!(spec.has_parallel_tools());
502 assert_eq!(spec.parallel_tools.max_parallel, 10);
503 }
504
505 #[test]
506 fn test_agent_spec_with_streaming() {
507 let yaml = r#"
508name: StreamingAgent
509system_prompt: "You are helpful."
510llm:
511 provider: openai
512 model: gpt-4
513streaming:
514 enabled: true
515 buffer_size: 64
516 include_tool_events: true
517"#;
518 let spec: AgentSpec = serde_yaml::from_str(yaml).unwrap();
519 assert!(spec.has_streaming());
520 assert_eq!(spec.streaming.buffer_size, 64);
521 }
522
523 #[test]
524 fn test_agent_spec_defaults() {
525 let spec = AgentSpec::default();
526 assert!(spec.parallel_tools.enabled);
527 assert_eq!(spec.parallel_tools.max_parallel, 5);
528 assert!(spec.streaming.enabled);
529 assert!(!spec.has_hitl());
530 }
531
532 #[test]
533 fn test_agent_spec_with_hitl() {
534 let yaml = r#"
535name: HITLAgent
536system_prompt: "You are helpful."
537llm:
538 provider: openai
539 model: gpt-4
540hitl:
541 default_timeout_seconds: 600
542 on_timeout: reject
543 tools:
544 send_payment:
545 require_approval: true
546 approval_context:
547 - amount
548 - recipient
549 approval_message: "Approve payment?"
550 conditions:
551 - name: high_value
552 when: "amount > 1000"
553 require_approval: true
554 states:
555 escalation:
556 on_enter: require_approval
557"#;
558 let spec: AgentSpec = serde_yaml::from_str(yaml).unwrap();
559 assert!(spec.has_hitl());
560 let hitl = spec.hitl.as_ref().unwrap();
561 assert_eq!(hitl.default_timeout_seconds, 600);
562 assert_eq!(hitl.tools.len(), 1);
563 assert_eq!(hitl.conditions.len(), 1);
564 assert_eq!(hitl.states.len(), 1);
565 }
566
567 #[test]
568 fn test_agent_spec_with_storage_file() {
569 let yaml = r#"
570name: PersistentAgent
571system_prompt: "You are helpful."
572llm:
573 provider: openai
574 model: gpt-4
575storage:
576 type: file
577 path: "./data/sessions"
578"#;
579 let spec: AgentSpec = serde_yaml::from_str(yaml).unwrap();
580 assert!(spec.has_storage());
581 assert!(spec.storage.is_file());
582 assert_eq!(spec.storage.get_path(), Some("./data/sessions"));
583 }
584
585 #[test]
586 fn test_agent_spec_with_storage_sqlite() {
587 let yaml = r#"
588name: PersistentAgent
589system_prompt: "You are helpful."
590llm:
591 provider: openai
592 model: gpt-4
593storage:
594 type: sqlite
595 path: "./data/sessions.db"
596"#;
597 let spec: AgentSpec = serde_yaml::from_str(yaml).unwrap();
598 assert!(spec.has_storage());
599 assert!(spec.storage.is_sqlite());
600 }
601
602 #[test]
603 fn test_agent_spec_with_storage_redis() {
604 let yaml = r#"
605name: PersistentAgent
606system_prompt: "You are helpful."
607llm:
608 provider: openai
609 model: gpt-4
610storage:
611 type: redis
612 url: "redis://localhost:6379"
613 prefix: "myagent:"
614 ttl_seconds: 86400
615"#;
616 let spec: AgentSpec = serde_yaml::from_str(yaml).unwrap();
617 assert!(spec.has_storage());
618 assert!(spec.storage.is_redis());
619 assert_eq!(spec.storage.get_url(), Some("redis://localhost:6379"));
620 assert_eq!(spec.storage.get_prefix(), "myagent:");
621 assert_eq!(spec.storage.get_ttl(), Some(86400));
622 }
623
624 #[test]
625 fn test_agent_spec_no_storage_by_default() {
626 let spec = AgentSpec::default();
627 assert!(!spec.has_storage());
628 assert!(spec.storage.is_none());
629 }
630
631 #[test]
632 fn test_agent_spec_with_providers() {
633 let yaml = r#"
634name: ProviderAgent
635system_prompt: "You are helpful."
636llm:
637 provider: openai
638 model: gpt-4
639providers:
640 builtin:
641 enabled: true
642 yaml:
643 enabled: true
644 tools:
645 - id: custom_search
646 name: Custom Search
647 description: Search custom API
648 implementation:
649 type: http
650 url: https://api.example.com/search
651 method: GET
652"#;
653 let spec: AgentSpec = serde_yaml::from_str(yaml).unwrap();
654 assert!(spec.has_providers());
655 assert!(spec.providers.builtin.enabled);
656 assert!(spec.providers.yaml.is_some());
657 }
658
659 #[test]
660 fn test_agent_spec_with_tool_aliases() {
661 let yaml = r#"
662name: AliasAgent
663system_prompt: "You are helpful."
664llm:
665 provider: openai
666 model: gpt-4
667tool_aliases:
668 calculator:
669 names:
670 ko: 계산기
671 ja: 計算機
672 descriptions:
673 ko: 수학 계산을 합니다
674"#;
675 let spec: AgentSpec = serde_yaml::from_str(yaml).unwrap();
676 assert!(spec.has_tool_aliases());
677 let calc_aliases = spec.tool_aliases.tools.get("calculator").unwrap();
678 assert_eq!(calc_aliases.get_name("ko"), Some("계산기"));
679 }
680
681 #[test]
682 fn test_agent_spec_with_reasoning() {
683 let yaml = r#"
684 name: ReasoningAgent
685 system_prompt: "You are helpful."
686 llm:
687 provider: openai
688 model: gpt-4
689 reasoning:
690 mode: cot
691 judge_llm: router
692 output: tagged
693 max_iterations: 8
694 "#;
695 let spec: AgentSpec = serde_yaml::from_str(yaml).unwrap();
696 assert!(spec.has_reasoning());
697 assert_eq!(spec.reasoning.max_iterations, 8);
698 }
699
700 #[test]
701 fn test_agent_spec_with_reflection() {
702 let yaml = r#"
703 name: ReflectionAgent
704 system_prompt: "You are helpful."
705 llm:
706 provider: openai
707 model: gpt-4
708 reflection:
709 enabled: auto
710 evaluator_llm: router
711 max_retries: 3
712 pass_threshold: 0.8
713 criteria:
714 - "Response addresses the question"
715 - "Response is accurate"
716 "#;
717 let spec: AgentSpec = serde_yaml::from_str(yaml).unwrap();
718 assert!(spec.has_reflection());
719 assert_eq!(spec.reflection.max_retries, 3);
720 assert_eq!(spec.reflection.criteria.len(), 2);
721 }
722
723 #[test]
724 fn test_agent_spec_with_plan_and_execute() {
725 let yaml = r#"
726 name: PlanningAgent
727 system_prompt: "You are helpful."
728 llm:
729 provider: openai
730 model: gpt-4
731 reasoning:
732 mode: plan_and_execute
733 planning:
734 planner_llm: router
735 max_steps: 15
736 available:
737 tools: all
738 skills:
739 - analyze
740 - summarize
741 reflection:
742 enabled: true
743 on_step_failure: replan
744 max_replans: 3
745 "#;
746 let spec: AgentSpec = serde_yaml::from_str(yaml).unwrap();
747 assert!(spec.has_reasoning());
748 let planning = spec.reasoning.planning.as_ref().unwrap();
749 assert_eq!(planning.max_steps, 15);
750 assert!(planning.reflection.enabled);
751 }
752
753 #[test]
754 fn test_agent_spec_reasoning_defaults() {
755 let spec = AgentSpec::default();
756 assert!(!spec.has_reasoning());
757 assert!(!spec.has_reflection());
758 }
759
760 #[test]
761 fn test_agent_spec_state_level_reasoning_override() {
762 let yaml = r#"
763 name: StateReasoningAgent
764 system_prompt: "You are helpful."
765 llm:
766 provider: openai
767 model: gpt-4
768 reasoning:
769 mode: auto
770 states:
771 initial: greeting
772 states:
773 greeting:
774 prompt: "Welcome!"
775 reasoning:
776 mode: none
777 complex_analysis:
778 prompt: "Analyze this"
779 reasoning:
780 mode: cot
781 output: tagged
782 reflection:
783 enabled: true
784 criteria:
785 - "Analysis is thorough"
786 "#;
787 let spec: AgentSpec = serde_yaml::from_str(yaml).unwrap();
788 assert!(spec.has_reasoning());
789 assert!(spec.has_states());
790
791 let states = spec.states.as_ref().unwrap();
792 let greeting = states.states.get("greeting").unwrap();
793 assert!(greeting.reasoning.is_some());
794 let greeting_reasoning = greeting.reasoning.as_ref().unwrap();
795 assert_eq!(
796 greeting_reasoning.mode,
797 ai_agents_reasoning::ReasoningMode::None
798 );
799
800 let analysis = states.states.get("complex_analysis").unwrap();
801 assert!(analysis.reasoning.is_some());
802 assert!(analysis.reflection.is_some());
803 let analysis_reasoning = analysis.reasoning.as_ref().unwrap();
804 assert_eq!(
805 analysis_reasoning.mode,
806 ai_agents_reasoning::ReasoningMode::CoT
807 );
808 }
809
810 #[test]
811 fn test_agent_spec_skill_level_reasoning_override() {
812 use ai_agents_skills::SkillDefinition;
813
814 let skill_yaml = r#"
815id: complex_analysis
816description: "Analyze data"
817trigger: "When user asks for analysis"
818reasoning:
819 mode: cot
820reflection:
821 enabled: true
822 criteria:
823 - "Analysis covers all aspects"
824steps:
825 - prompt: "Analyze the input"
826"#;
827 let skill_def: SkillDefinition = serde_yaml::from_str(skill_yaml).unwrap();
828 assert!(skill_def.reasoning.is_some());
829 assert!(skill_def.reflection.is_some());
830 let reasoning = skill_def.reasoning.as_ref().unwrap();
831 assert_eq!(reasoning.mode, ai_agents_reasoning::ReasoningMode::CoT);
832 let reflection = skill_def.reflection.as_ref().unwrap();
833 assert!(reflection.is_enabled());
834
835 let simple_yaml = r#"
836id: simple_lookup
837description: "Look up simple facts"
838trigger: "When user asks for facts"
839reasoning:
840 mode: none
841reflection:
842 enabled: false
843steps:
844 - prompt: "Look up the fact"
845"#;
846 let simple_def: SkillDefinition = serde_yaml::from_str(simple_yaml).unwrap();
847 assert!(simple_def.reasoning.is_some());
848 let simple_reasoning = simple_def.reasoning.as_ref().unwrap();
849 assert_eq!(
850 simple_reasoning.mode,
851 ai_agents_reasoning::ReasoningMode::None
852 );
853 }
854
855 #[test]
856 fn test_agent_spec_with_disambiguation() {
857 let yaml = r#"
858name: DisambiguatingAgent
859system_prompt: "You are a helpful assistant."
860disambiguation:
861 enabled: true
862 detection:
863 llm: router
864 threshold: 0.8
865 aspects:
866 - missing_target
867 - vague_references
868 clarification:
869 style: auto
870 max_attempts: 3
871 on_max_attempts: proceed_with_best_guess
872 skip_when:
873 - type: social
874 - type: short_input
875 max_chars: 10
876llms:
877 default:
878 provider: openai
879 model: gpt-4.1-nano
880"#;
881 let spec: AgentSpec = serde_yaml::from_str(yaml).unwrap();
882 assert!(spec.has_disambiguation());
883 assert!(spec.disambiguation.is_enabled());
884 assert_eq!(spec.disambiguation.detection.threshold, 0.8);
885 assert_eq!(spec.disambiguation.clarification.max_attempts, 3);
886 assert_eq!(spec.disambiguation.skip_when.len(), 2);
887 }
888
889 #[test]
890 fn test_agent_spec_disambiguation_minimal() {
891 let yaml = r#"
892name: MinimalDisambiguatingAgent
893system_prompt: "You are helpful."
894disambiguation:
895 enabled: true
896llms:
897 default:
898 provider: openai
899 model: gpt-4.1-nano
900"#;
901 let spec: AgentSpec = serde_yaml::from_str(yaml).unwrap();
902 assert!(spec.has_disambiguation());
903 assert_eq!(spec.disambiguation.detection.llm, "router");
904 assert_eq!(spec.disambiguation.detection.threshold, 0.7);
905 assert_eq!(spec.disambiguation.clarification.max_attempts, 2);
906 }
907
908 #[test]
909 fn test_agent_spec_no_disambiguation_by_default() {
910 let yaml = r#"
911name: SimpleAgent
912system_prompt: "You are helpful."
913llms:
914 default:
915 provider: openai
916 model: gpt-4.1-nano
917"#;
918 let spec: AgentSpec = serde_yaml::from_str(yaml).unwrap();
919 assert!(!spec.has_disambiguation());
920 assert!(!spec.disambiguation.is_enabled());
921 }
922
923 #[test]
924 fn test_state_machine_examples_parse() {
925 let workspace_root = std::path::Path::new(env!("CARGO_MANIFEST_DIR"))
927 .parent()
928 .unwrap()
929 .parent()
930 .unwrap();
931 let examples = [
932 "examples/yaml/state-machine/two_state_greeting.yaml",
933 "examples/yaml/state-machine/guard_transitions.yaml",
934 "examples/yaml/state-machine/nested_states.yaml",
935 "examples/yaml/state-machine/state_with_tools.yaml",
936 "examples/yaml/state-machine/state_lifecycle.yaml",
937 "examples/yaml/state-machine/support_state_machine.yaml",
938 ];
939 for rel_path in &examples {
940 let path = workspace_root.join(rel_path);
941 let content = std::fs::read_to_string(&path)
942 .unwrap_or_else(|_| panic!("Failed to read {}", path.display()));
943 let spec: AgentSpec = serde_yaml::from_str(&content)
944 .unwrap_or_else(|e| panic!("Failed to parse {}: {}", path.display(), e));
945 if let Some(ref states) = spec.states {
946 states
947 .validate()
948 .unwrap_or_else(|e| panic!("Validation failed for {}: {}", path.display(), e));
949 }
950 }
951 }
952}