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