Skip to main content

ai_agents_runtime/spec/
mod.rs

1//! Agent specification types
2
3mod 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    /// Dynamic agent spawning configuration (optional).
119    #[serde(default, skip_serializing_if = "Option::is_none")]
120    pub spawner: Option<SpawnerConfig>,
121
122    /// Agent persona configuration (optional).
123    #[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        // Resolve workspace root: this crate is at crates/ai-agents-runtime
926        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}