1use std::collections::HashMap;
2use std::path::{Path, PathBuf};
3use std::str::FromStr;
4use std::sync::Arc;
5
6use ai_agents_context::ContextManager;
7use ai_agents_core::{AgentError, AgentStorage, LLMFeature, LLMProvider, Result, Tool};
8use ai_agents_hitl::{ApprovalHandler, HITLEngine, RejectAllHandler};
9use ai_agents_hooks::{AgentHooks, CompositeHooks};
10use ai_agents_llm::LLMRegistry;
11use ai_agents_llm::providers::{ProviderType, UnifiedLLMProvider};
12use ai_agents_memory::{
13 CompactingMemory, InMemoryStore, LLMSummarizer, Memory, NoopSummarizer, Summarizer,
14};
15use ai_agents_observability::{
16 ObservabilityConfig, ObservabilityHooks, ObservabilityManager, ObservedLLMProvider,
17 ObservedTool,
18};
19use ai_agents_process::ProcessProcessor;
20use ai_agents_reasoning::{ReasoningConfig, ReflectionConfig};
21use ai_agents_recovery::{MessageFilter, RecoveryManager};
22use ai_agents_relationships::{
23 RelationshipEvaluator, RelationshipEvaluatorTrait, RelationshipManager,
24};
25use ai_agents_skills::{SkillDefinition, SkillLoader};
26use ai_agents_state::{LLMTransitionEvaluator, StateMachine, TransitionEvaluator};
27use ai_agents_template::{TemplateInheritance, TemplateLoader, TemplateRenderer};
28use ai_agents_tools::mcp::view::MCPViewTool;
29use ai_agents_tools::mcp::wrapper::MCPWrapperTool;
30use ai_agents_tools::{ToolRegistry, ToolSecurityEngine, create_builtin_registry};
31
32use super::AgentInfo;
33use super::StreamingConfig;
34use super::runtime::{RuntimeAgent, ToolResourceLocks};
35use crate::spec::{AgentSpec, StorageConfig};
36
37fn feature_overrides_from_config(config: &crate::spec::LLMConfig) -> HashMap<LLMFeature, bool> {
38 let mut overrides = HashMap::new();
39 if let Some(enabled) = config.function_calling {
40 overrides.insert(LLMFeature::FunctionCalling, enabled);
41 }
42 if let Some(enabled) = config.vision {
43 overrides.insert(LLMFeature::Vision, enabled);
44 }
45 if let Some(enabled) = config.json_mode {
46 overrides.insert(LLMFeature::JsonMode, enabled);
47 }
48 overrides
49}
50
51fn model_by_alias_from_spec(spec: Option<&AgentSpec>) -> HashMap<String, String> {
53 spec.map(|spec| {
54 let mut models: HashMap<String, String> = spec
55 .llms
56 .iter()
57 .map(|(alias, config)| (alias.clone(), config.model.clone()))
58 .collect();
59 if let Some(config) = spec.llm.as_config() {
60 models.insert("default".to_string(), config.model.clone());
61 }
62 models
63 })
64 .unwrap_or_default()
65}
66
67fn wrap_registry_with_observability(
69 registry: LLMRegistry,
70 manager: Arc<ObservabilityManager>,
71 model_by_alias: &HashMap<String, String>,
72) -> LLMRegistry {
73 registry.map_providers(|alias, provider| {
74 let provider_name = provider.provider_name().to_string();
75 let model = model_by_alias
76 .get(alias)
77 .cloned()
78 .unwrap_or_else(|| alias.to_string());
79 Arc::new(ObservedLLMProvider::new(
80 provider,
81 Arc::clone(&manager),
82 Some(alias.to_string()),
83 provider_name,
84 model,
85 )) as Arc<dyn LLMProvider>
86 })
87}
88
89pub struct AgentBuilder {
90 spec: Option<AgentSpec>,
91 llm: Option<Arc<dyn LLMProvider>>,
92 llm_registry: Option<LLMRegistry>,
93 memory: Option<Arc<dyn Memory>>,
94 tools: Option<ToolRegistry>,
95 skills: Vec<SkillDefinition>,
96 skill_loader: Option<SkillLoader>,
97 yaml_dir: Option<PathBuf>,
98 system_prompt: Option<String>,
99 tools_prompt: Option<String>,
100 auto_tools_prompt: bool,
101 max_iterations: Option<u32>,
102 max_context_tokens: Option<u32>,
103 recovery_manager: Option<RecoveryManager>,
104 tool_security: Option<ToolSecurityEngine>,
105 process_processor: Option<ProcessProcessor>,
106 message_filters: HashMap<String, Arc<dyn MessageFilter>>,
107 context_manager: Option<Arc<ContextManager>>,
108 state_machine: Option<Arc<StateMachine>>,
109 transition_evaluator: Option<Arc<dyn TransitionEvaluator>>,
110 hooks: Option<Arc<dyn AgentHooks>>,
111 hitl_engine: Option<HITLEngine>,
112 approval_handler: Option<Arc<dyn ApprovalHandler>>,
113 storage_config: Option<StorageConfig>,
114 storage: Option<Arc<dyn AgentStorage>>,
115 reasoning: Option<ReasoningConfig>,
116 reflection: Option<ReflectionConfig>,
117 streaming: Option<StreamingConfig>,
118 spawner: Option<Arc<crate::spawner::AgentSpawner>>,
119 spawner_registry: Option<Arc<crate::spawner::AgentRegistry>>,
120 persona_manager: Option<Arc<ai_agents_persona::PersonaManager>>,
121 persona_templates: Option<Arc<ai_agents_persona::PersonaTemplateRegistry>>,
122 observability_manager: Option<Arc<ObservabilityManager>>,
123 resource_locks: Option<ToolResourceLocks>,
124 llm_registry_observed: bool,
125}
126
127impl AgentBuilder {
128 pub fn new() -> Self {
129 Self {
130 reasoning: None,
131 reflection: None,
132 spec: None,
133 llm: None,
134 llm_registry: None,
135 memory: None,
136 tools: None,
137 skills: Vec::new(),
138 skill_loader: None,
139 yaml_dir: None,
140 system_prompt: None,
141 tools_prompt: None,
142 auto_tools_prompt: true,
143 max_iterations: None,
144 max_context_tokens: None,
145 recovery_manager: None,
146 tool_security: None,
147 process_processor: None,
148 message_filters: HashMap::new(),
149 context_manager: None,
150 state_machine: None,
151 transition_evaluator: None,
152 hooks: None,
153 hitl_engine: None,
154 approval_handler: None,
155 storage_config: None,
156 storage: None,
157 streaming: None,
158 spawner: None,
159 spawner_registry: None,
160 persona_manager: None,
161 persona_templates: None,
162 observability_manager: None,
163 resource_locks: None,
164 llm_registry_observed: false,
165 }
166 }
167
168 pub fn from_spec(spec: AgentSpec) -> Self {
169 let system_prompt = spec.system_prompt.clone();
170 let max_iterations = Some(spec.max_iterations);
171 let max_context_tokens = Some(spec.max_context_tokens);
172 let reasoning = Some(spec.reasoning.clone());
173 let reflection = Some(spec.reflection.clone());
174
175 Self {
176 spec: Some(spec),
177 llm: None,
178 llm_registry: None,
179 memory: None,
180 tools: None,
181 skills: Vec::new(),
182 skill_loader: None,
183 yaml_dir: None,
184 system_prompt: Some(system_prompt),
185 tools_prompt: None,
186 auto_tools_prompt: true,
187 max_iterations,
188 max_context_tokens,
189 recovery_manager: None,
190 tool_security: None,
191 process_processor: None,
192 message_filters: HashMap::new(),
193 context_manager: None,
194 state_machine: None,
195 transition_evaluator: None,
196 hooks: None,
197 hitl_engine: None,
198 approval_handler: None,
199 storage_config: None,
200 storage: None,
201 reasoning,
202 reflection,
203 streaming: None,
204 spawner: None,
205 spawner_registry: None,
206 persona_manager: None,
207 persona_templates: None,
208 observability_manager: None,
209 resource_locks: None,
210 llm_registry_observed: false,
211 }
212 }
213
214 pub fn from_spec_with_base_dir(spec: AgentSpec, base_dir: impl Into<PathBuf>) -> Self {
216 let mut builder = Self::from_spec(spec);
217 builder.yaml_dir = Some(base_dir.into());
218 builder
219 }
220
221 pub fn from_yaml(yaml_content: &str) -> Result<Self> {
222 let spec = AgentSpec::from_yaml_strict(yaml_content)?;
223 spec.validate()?;
224 Ok(Self::from_spec(spec))
225 }
226
227 pub(crate) fn shared_resource_locks(&mut self) -> ToolResourceLocks {
228 self.resource_locks
229 .get_or_insert_with(|| Arc::new(parking_lot::RwLock::new(HashMap::new())))
230 .clone()
231 }
232
233 pub(crate) fn with_shared_resource_locks(mut self, locks: ToolResourceLocks) -> Self {
234 self.resource_locks = Some(locks);
235 self
236 }
237
238 pub fn from_yaml_file(path: impl AsRef<Path>) -> Result<Self> {
239 let path = path.as_ref();
240 let content = std::fs::read_to_string(path).map_err(AgentError::IoError)?;
241 let spec = AgentSpec::from_yaml_strict(&content)?;
242 spec.validate()?;
243 Ok(match path.parent() {
244 Some(parent) => Self::from_spec_with_base_dir(spec, parent),
245 None => Self::from_spec(spec),
246 })
247 }
248
249 pub fn from_template(template_name: &str) -> Result<Self> {
250 let loader = TemplateLoader::new();
251 Self::from_template_with_loader(template_name, &loader)
252 }
253
254 pub fn from_template_with_loader(template_name: &str, loader: &TemplateLoader) -> Result<Self> {
255 let renderer = TemplateRenderer::new();
256 let variables = loader.variables();
257
258 let load_and_render = |name: &str| -> Result<String> {
259 let content = loader.load_template(name)?;
260 renderer.render(&content, variables)
261 };
262
263 let rendered_root = load_and_render(template_name)?;
264 let processed = TemplateInheritance::process(&rendered_root, load_and_render)?;
265 let spec = AgentSpec::from_yaml_strict(&processed)?;
266 spec.validate()?;
267 Ok(Self::from_spec(spec))
268 }
269
270 pub fn auto_configure_llms(mut self) -> Result<Self> {
271 let spec = self
272 .spec
273 .as_ref()
274 .ok_or_else(|| AgentError::Config("Cannot auto-configure LLMs without spec".into()))?;
275
276 if !spec.llms.is_empty() {
277 let mut registry = LLMRegistry::new();
278
279 for (alias, config) in &spec.llms {
280 let provider_type = ProviderType::from_str(&config.provider)
281 .map_err(|e| AgentError::Config(e.to_string()))?;
282
283 let core_config = ai_agents_core::LLMConfig {
284 temperature: Some(config.temperature),
285 max_tokens: Some(config.max_tokens),
286 top_p: config.top_p,
287 top_k: None,
288 frequency_penalty: None,
289 presence_penalty: None,
290 stop_sequences: None,
291 timeout_seconds: config.timeout_seconds,
292 reasoning: config.reasoning,
293 reasoning_effort: config.reasoning_effort.clone(),
294 reasoning_budget_tokens: config.reasoning_budget_tokens,
295 extra: config.extra.clone(),
296 };
297 let base_url = config.base_url.clone().or_else(|| {
299 config
300 .extra
301 .get("base_url")
302 .and_then(|v| v.as_str())
303 .map(|s| s.to_string())
304 });
305
306 let api_key = config
308 .api_key_env
309 .as_ref()
310 .and_then(|env_var| std::env::var(env_var).ok());
311
312 let mut provider = UnifiedLLMProvider::from_spec_config(
313 provider_type,
314 &config.model,
315 api_key,
316 base_url,
317 core_config,
318 )
319 .map_err(|e| AgentError::LLM(e.to_string()))?
320 .with_feature_overrides(feature_overrides_from_config(config));
321 if let Some(choice) = config.tool_choice.clone() {
322 provider = provider.with_tool_choice(choice);
323 }
324
325 registry.register(alias, Arc::new(provider));
326 }
327
328 let default_alias = spec.llm.get_default_alias();
329 let router_alias = spec.llm.get_router_alias();
330
331 registry.set_default(&default_alias);
332 if let Some(router) = router_alias {
333 registry.set_router(&router);
334 }
335
336 self.llm_registry = Some(registry);
337 self.llm_registry_observed = false;
338 } else if let Some(config) = spec.llm.as_config() {
339 let provider_type = ProviderType::from_str(&config.provider)
340 .map_err(|e| AgentError::Config(e.to_string()))?;
341
342 let core_config = ai_agents_core::LLMConfig {
343 temperature: Some(config.temperature),
344 max_tokens: Some(config.max_tokens),
345 top_p: config.top_p,
346 top_k: None,
347 frequency_penalty: None,
348 presence_penalty: None,
349 stop_sequences: None,
350 timeout_seconds: config.timeout_seconds,
351 reasoning: config.reasoning,
352 reasoning_effort: config.reasoning_effort.clone(),
353 reasoning_budget_tokens: config.reasoning_budget_tokens,
354 extra: config.extra.clone(),
355 };
356 let base_url = config.base_url.clone().or_else(|| {
358 config
359 .extra
360 .get("base_url")
361 .and_then(|v| v.as_str())
362 .map(|s| s.to_string())
363 });
364
365 let api_key = config
367 .api_key_env
368 .as_ref()
369 .and_then(|env_var| std::env::var(env_var).ok());
370
371 let mut provider = UnifiedLLMProvider::from_spec_config(
372 provider_type,
373 &config.model,
374 api_key,
375 base_url,
376 core_config,
377 )
378 .map_err(|e| AgentError::LLM(e.to_string()))?
379 .with_feature_overrides(feature_overrides_from_config(config));
380 if let Some(choice) = config.tool_choice.clone() {
381 provider = provider.with_tool_choice(choice);
382 }
383
384 self.llm = Some(Arc::new(provider));
385 self.llm_registry_observed = false;
386 }
387
388 Ok(self)
389 }
390
391 pub fn auto_configure_features(mut self) -> Result<Self> {
408 if let Some(ref spec) = self.spec {
409 self.recovery_manager = Some(RecoveryManager::try_new(spec.error_recovery.clone())?);
410 self.tool_security = Some(ToolSecurityEngine::try_new(spec.tool_security.clone())?);
411
412 if spec.has_process() {
413 let mut processor = ProcessProcessor::new(spec.process.clone());
414 if let Some(ref registry) = self.llm_registry {
415 processor = processor.with_llm_registry(Arc::new(registry.clone()));
416 }
417 self.process_processor = Some(processor);
418 }
419
420 if self.tools.is_none() {
422 self.tools = Some(create_builtin_registry());
423 }
424 }
425 Ok(self)
426 }
427
428 pub async fn auto_configure_mcp(mut self) -> Result<Self> {
435 if let Some(ref spec) = self.spec {
436 let mcp_configs: Vec<_> = spec
438 .tools
439 .as_ref()
440 .map(|tools| {
441 tools
442 .iter()
443 .filter_map(|entry| entry.to_mcp_config())
444 .collect()
445 })
446 .unwrap_or_default();
447
448 if !mcp_configs.is_empty() {
449 let registry = self.tools.get_or_insert_with(create_builtin_registry);
450
451 for config in mcp_configs {
452 let tool_name = config.name.clone();
453 let timeout_ms = config.startup_timeout_ms;
454 let views_config = config.views.clone();
455
456 let wrapper = MCPWrapperTool::new(config);
457
458 match tokio::time::timeout(
460 std::time::Duration::from_millis(timeout_ms),
461 wrapper.initialized(),
462 )
463 .await
464 {
465 Ok(Ok(initialized_tool)) => {
466 tracing::info!(
467 tool = %tool_name,
468 functions = initialized_tool.function_count(),
469 "MCP wrapper tool registered"
470 );
471
472 let parent = Arc::new(initialized_tool);
473
474 registry.register(parent.clone()).map_err(|e| {
476 AgentError::Config(format!(
477 "Failed to register MCP tool '{}': {}",
478 tool_name, e
479 ))
480 })?;
481
482 for (view_name, view_config) in &views_config {
484 let view_tool = MCPViewTool::new(
485 view_name.clone(),
486 parent.clone(),
487 view_config.functions.clone(),
488 view_config.description.clone(),
489 )
490 .map_err(|e| {
491 AgentError::Config(format!(
492 "Failed to create MCP view '{}': {}",
493 view_name, e
494 ))
495 })?;
496
497 tracing::info!(
498 view = %view_name,
499 parent = %tool_name,
500 functions = view_config.functions.len(),
501 "MCP view tool registered"
502 );
503
504 registry.register(Arc::new(view_tool)).map_err(|e| {
505 AgentError::Config(format!(
506 "Failed to register MCP view '{}': {}",
507 view_name, e
508 ))
509 })?;
510 }
511 }
512 Ok(Err(e)) => {
513 return Err(AgentError::Config(format!(
514 "MCP tool '{}' initialization failed: {}",
515 tool_name, e
516 )));
517 }
518 Err(_) => {
519 return Err(AgentError::Config(format!(
520 "MCP tool '{}' timed out after {}ms",
521 tool_name, timeout_ms
522 )));
523 }
524 }
525 }
526 }
527 }
528 Ok(self)
529 }
530
531 pub fn llm(mut self, llm: Arc<dyn LLMProvider>) -> Self {
532 self.llm = Some(llm);
533 self
534 }
535
536 pub fn llm_alias(mut self, alias: impl Into<String>, provider: Arc<dyn LLMProvider>) -> Self {
537 if self.llm_registry.is_none() {
538 self.llm_registry = Some(LLMRegistry::new());
539 }
540 if let Some(ref mut registry) = self.llm_registry {
541 registry.register(alias, provider);
542 }
543 self
544 }
545
546 pub fn llm_registry(mut self, registry: LLMRegistry) -> Self {
548 self.llm_registry = Some(registry);
549 self.llm_registry_observed = false;
550 self
551 }
552
553 pub(crate) fn authoritative_llm_registry(
554 mut self,
555 registry: LLMRegistry,
556 observed: bool,
557 ) -> Self {
558 self.llm_registry = Some(registry);
559 self.llm_registry_observed = observed;
560 self
561 }
562
563 pub fn memory(mut self, memory: Arc<dyn Memory>) -> Self {
564 self.memory = Some(memory);
565 self
566 }
567
568 pub fn tools(mut self, tools: ToolRegistry) -> Self {
572 self.tools = Some(tools);
573 self
574 }
575
576 pub fn tool(mut self, tool: Arc<dyn Tool>) -> Self {
581 let registry = self.tools.get_or_insert_with(ToolRegistry::new);
582 let _ = registry.register(tool);
583 self
584 }
585
586 pub fn extend_tools(mut self, additional: ToolRegistry) -> Self {
591 let registry = self.tools.get_or_insert_with(ToolRegistry::new);
592 for id in additional.list_ids() {
593 if registry.get(&id).is_none()
594 && let Some(tool) = additional.get(&id)
595 {
596 let _ = registry.register(tool);
597 }
598 }
599 self
600 }
601
602 pub fn skill(mut self, skill: SkillDefinition) -> Self {
603 self.skills.push(skill);
604 self
605 }
606
607 pub fn skills(mut self, skills: Vec<SkillDefinition>) -> Self {
608 self.skills.extend(skills);
609 self
610 }
611
612 pub fn skill_loader(mut self, loader: SkillLoader) -> Self {
613 self.skill_loader = Some(loader);
614 self
615 }
616
617 pub fn system_prompt(mut self, prompt: impl Into<String>) -> Self {
618 self.system_prompt = Some(prompt.into());
619 self
620 }
621
622 pub fn tools_prompt(mut self, prompt: impl Into<String>) -> Self {
623 self.tools_prompt = Some(prompt.into());
624 self.auto_tools_prompt = false;
625 self
626 }
627
628 pub fn auto_tools_prompt(mut self, auto: bool) -> Self {
629 self.auto_tools_prompt = auto;
630 self
631 }
632
633 pub fn max_iterations(mut self, max: u32) -> Self {
634 self.max_iterations = Some(max);
635 self
636 }
637
638 pub fn max_context_tokens(mut self, tokens: u32) -> Self {
639 self.max_context_tokens = Some(tokens);
640 self
641 }
642
643 pub fn recovery_manager(mut self, manager: RecoveryManager) -> Self {
644 self.recovery_manager = Some(manager);
645 self
646 }
647
648 pub fn tool_security(mut self, engine: ToolSecurityEngine) -> Self {
649 self.tool_security = Some(engine);
650 self
651 }
652
653 pub fn process_processor(mut self, processor: ProcessProcessor) -> Self {
654 self.process_processor = Some(processor);
655 self
656 }
657
658 pub fn message_filter(
659 mut self,
660 name: impl Into<String>,
661 filter: Arc<dyn MessageFilter>,
662 ) -> Self {
663 self.message_filters.insert(name.into(), filter);
664 self
665 }
666
667 pub fn context_manager(mut self, manager: Arc<ContextManager>) -> Self {
668 self.context_manager = Some(manager);
669 self
670 }
671
672 pub fn state_machine(mut self, machine: Arc<StateMachine>) -> Self {
673 self.state_machine = Some(machine);
674 self
675 }
676
677 pub fn transition_evaluator(mut self, evaluator: Arc<dyn TransitionEvaluator>) -> Self {
678 self.transition_evaluator = Some(evaluator);
679 self
680 }
681
682 pub fn hooks(mut self, hooks: Arc<dyn AgentHooks>) -> Self {
683 self.hooks = Some(hooks);
684 self
685 }
686
687 pub fn approval_handler(mut self, handler: Arc<dyn ApprovalHandler>) -> Self {
688 self.approval_handler = Some(handler);
689 self
690 }
691
692 pub fn hitl_engine(mut self, engine: HITLEngine) -> Self {
693 self.hitl_engine = Some(engine);
694 self
695 }
696
697 pub fn storage_config(mut self, config: StorageConfig) -> Self {
698 self.storage_config = Some(config);
699 self
700 }
701
702 pub fn storage(mut self, storage: Arc<dyn AgentStorage>) -> Self {
703 self.storage = Some(storage);
704 self
705 }
706
707 pub fn reasoning(mut self, config: ReasoningConfig) -> Self {
708 self.reasoning = Some(config);
709 self
710 }
711
712 pub fn reflection(mut self, config: ReflectionConfig) -> Self {
713 self.reflection = Some(config);
714 self
715 }
716
717 pub fn persona(mut self, manager: Arc<ai_agents_persona::PersonaManager>) -> Self {
719 self.persona_manager = Some(manager);
720 self
721 }
722
723 pub fn persona_templates(
725 mut self,
726 registry: Arc<ai_agents_persona::PersonaTemplateRegistry>,
727 ) -> Self {
728 self.persona_templates = Some(registry);
729 self
730 }
731
732 pub fn observability(mut self, manager: Arc<ObservabilityManager>) -> Self {
734 self.observability_manager = Some(manager);
735 self
736 }
737
738 fn ensure_observability_manager(&mut self) -> Result<Option<Arc<ObservabilityManager>>> {
740 if let Some(manager) = self.observability_manager.as_ref() {
741 return Ok(Some(Arc::clone(manager)));
742 }
743 let Some(ref spec) = self.spec else {
744 return Ok(None);
745 };
746 if !spec.observability.enabled {
747 return Ok(None);
748 }
749 let config = self.observability_config_with_pricing(&spec.observability)?;
750 config
751 .validate()
752 .map_err(|e| AgentError::Config(e.to_string()))?;
753 let manager = ObservabilityManager::new(config);
754 self.observability_manager = Some(Arc::clone(&manager));
755 Ok(Some(manager))
756 }
757
758 fn observability_config_with_pricing(
760 &self,
761 config: &ObservabilityConfig,
762 ) -> Result<ObservabilityConfig> {
763 config
764 .clone()
765 .with_pricing_file_loaded(self.yaml_dir.as_deref())
766 .map_err(|e| AgentError::Config(e.to_string()))
767 }
768
769 fn wrap_llm_registry_for_observability(&mut self) -> Result<()> {
771 if self.llm_registry_observed {
772 return Ok(());
773 }
774 let Some(manager) = self.ensure_observability_manager()? else {
775 return Ok(());
776 };
777 let Some(registry) = self.llm_registry.take() else {
778 return Ok(());
779 };
780 let model_by_alias = model_by_alias_from_spec(self.spec.as_ref());
781 let wrapped = wrap_registry_with_observability(registry, manager, &model_by_alias);
782 let wrapped_arc = Arc::new(wrapped.clone());
783 if let Some(processor) = self.process_processor.take() {
784 self.process_processor = Some(processor.with_llm_registry(wrapped_arc));
785 }
786 self.llm_registry = Some(wrapped);
787 self.llm_registry_observed = true;
788 Ok(())
789 }
790
791 pub fn streaming(mut self, enabled: bool) -> Self {
792 let mut config = self.streaming.unwrap_or_default();
793 config.enabled = enabled;
794 self.streaming = Some(config);
795 self
796 }
797
798 pub async fn auto_configure_spawner(mut self) -> Result<Self> {
801 let spawner_config = match self.spec.as_ref().and_then(|s| s.spawner.as_ref()) {
802 Some(c) => c.clone(),
803 None => return Ok(self),
804 };
805
806 if spawner_config.shared_llms && self.llm_registry.is_none() {
807 let provider = self.llm.as_ref().cloned().ok_or_else(|| {
808 AgentError::Config(
809 "spawner.shared_llms requires the parent LLM provider to be configured"
810 .to_string(),
811 )
812 })?;
813 let mut registry = LLMRegistry::new();
814 registry.register("default", provider);
815 registry.set_default("default");
816 self.llm_registry = Some(registry);
817 self.llm_registry_observed = false;
818 }
819
820 self.wrap_llm_registry_for_observability()?;
821 let observability_manager = self.observability_manager.clone();
822
823 use crate::spawner::{
824 AgentRegistry, AgentSpawner,
825 config::{configure_spawner_tools, resolve_templates},
826 };
827
828 let mut spawner = AgentSpawner::new();
829
830 if let Some(ref manager) = observability_manager {
831 spawner = spawner.with_observability(Arc::clone(manager));
832 }
833 spawner = spawner.with_resource_locks(self.shared_resource_locks());
834
835 if spawner_config.shared_llms {
836 let reg = self.llm_registry.as_ref().ok_or_else(|| {
837 AgentError::Config(
838 "spawner.shared_llms requires the parent LLM registry to be configured"
839 .to_string(),
840 )
841 })?;
842 spawner = if self.llm_registry_observed {
843 spawner.with_shared_observed_llms(reg.clone())
844 } else {
845 spawner.with_shared_llms(reg.clone())
846 };
847 }
848
849 if !spawner_config.shared_context.is_empty() {
850 spawner = spawner.with_shared_context_map(spawner_config.shared_context.clone());
851 }
852
853 if let Some(max) = spawner_config.max_agents {
854 spawner = spawner.with_max_agents(max);
855 }
856
857 if let Some(ref prefix) = spawner_config.name_prefix {
858 spawner = spawner.with_name_prefix(prefix.clone())?;
859 }
860
861 if !spawner_config.templates.is_empty() {
863 let resolved = resolve_templates(&spawner_config.templates, self.yaml_dir.as_deref())?;
864 spawner = spawner.with_templates(resolved);
865 }
866
867 if let Some(ref allowed) = spawner_config.allowed_tools {
868 spawner = spawner.with_allowed_tools(allowed.clone());
869 }
870
871 if let Some(ref sc) = spawner_config.shared_storage {
873 let converted = crate::spec::storage::to_storage_config(sc);
874 if let Some(st) = ai_agents_storage::create_storage(&converted).await? {
875 spawner = spawner.with_shared_storage(Arc::clone(&st));
876
877 let parent_has_storage = self.storage.is_some()
879 || self.storage_config.is_some()
880 || self.spec.as_ref().is_some_and(|s| s.has_storage());
881 if !parent_has_storage {
882 self.storage = Some(st);
883 }
884 }
885 }
886
887 let spawner = Arc::new(spawner);
888 let registry = Arc::new(AgentRegistry::new());
889
890 self.spawner = Some(Arc::clone(&spawner));
891 self.spawner_registry = Some(Arc::clone(®istry));
892 let llm_for_tools = Arc::new(self.llm_registry.clone().unwrap_or_default());
893 let agent_name = self
894 .spec
895 .as_ref()
896 .map(|s| s.name.clone())
897 .unwrap_or_default();
898
899 let tools = configure_spawner_tools(
900 Arc::clone(&spawner),
901 Arc::clone(®istry),
902 Arc::clone(&llm_for_tools),
903 &agent_name,
904 );
905
906 let tool_registry = self.tools.get_or_insert_with(create_builtin_registry);
907 for tool in tools {
908 let _ = tool_registry.register(tool);
909 }
910
911 tracing::info!("Spawner tools registered");
912
913 if spawner_config.orchestration_tools.is_enabled() {
915 let orch_tools = crate::orchestration::tools::configure_orchestration_tools(
916 &spawner_config.orchestration_tools,
917 Arc::clone(®istry),
918 Arc::clone(&llm_for_tools),
919 );
920 let tool_registry = self.tools.get_or_insert_with(create_builtin_registry);
921 for tool in orch_tools {
922 let _ = tool_registry.register(tool);
923 }
924 tracing::info!("Orchestration tools registered");
925 }
926
927 for entry in &spawner_config.auto_spawn {
928 let yaml_path = if let Some(ref dir) = self.yaml_dir {
929 dir.join(&entry.agent)
930 } else {
931 std::path::PathBuf::from(&entry.agent)
932 };
933
934 tracing::info!(id = %entry.id, path = %yaml_path.display(), "Auto-spawning agent");
935 let spawned = spawner
936 .spawn_from_yaml_file_with_id(entry.id.clone(), &yaml_path)
937 .await
938 .map_err(|error| {
939 AgentError::Config(format!(
940 "Failed to auto-spawn agent '{}' from '{}': {}",
941 entry.id,
942 yaml_path.display(),
943 error
944 ))
945 })?;
946 registry.register(spawned).await.map_err(|error| {
947 AgentError::Config(format!(
948 "Failed to register auto-spawned agent '{}': {}",
949 entry.id, error
950 ))
951 })?;
952 tracing::info!(id = %entry.id, "Auto-spawned agent registered");
953 }
954
955 if let Some(ref spec) = self.spec
957 && let Some(ref state_config) = spec.states
958 {
959 let refs = collect_orchestration_refs(&state_config.states);
960 let mut missing: Vec<String> = Vec::new();
961
962 for (agent_id, state_name, pattern) in &refs {
963 if !registry.contains(agent_id) {
964 missing.push(format!(
965 " - '{}' (referenced by state '{}' via {})",
966 agent_id, state_name, pattern
967 ));
968 }
969 }
970
971 if !missing.is_empty() {
972 missing.sort();
973 missing.dedup();
974 return Err(AgentError::Config(format!(
975 "Auto-spawn validation failed. These agents are referenced by \
976 orchestration states but were not successfully spawned:\n\n{}\n\n\
977 Check that agent YAML files exist and contain valid specs.",
978 missing.join("\n")
979 )));
980 }
981 }
982
983 Ok(self)
984 }
985
986 pub fn build(mut self) -> Result<RuntimeAgent> {
987 let resource_locks = self.shared_resource_locks();
988 let actor_memory_config = self
990 .spec
991 .as_ref()
992 .and_then(|s| s.memory.actor_memory.clone());
993 let facts_config = self.spec.as_ref().and_then(|s| s.memory.facts.clone());
994 let relationships_config = self
995 .spec
996 .as_ref()
997 .and_then(|s| s.memory.relationships.clone());
998
999 let observability_manager = self.ensure_observability_manager()?;
1000
1001 let base_prompt = self
1002 .system_prompt
1003 .ok_or_else(|| AgentError::Config("System prompt is required".into()))?;
1004
1005 let mut tools = self.tools.unwrap_or_default();
1006
1007 let system_prompt = base_prompt;
1010
1011 let max_iterations = self.max_iterations.unwrap_or(10);
1012
1013 let info = if let Some(ref spec) = self.spec {
1014 AgentInfo::new(&spec.name, &spec.name, &spec.version)
1015 .with_description(spec.description.clone().unwrap_or_default())
1016 } else {
1017 AgentInfo::new("agent", "Agent", "1.0.0")
1018 };
1019
1020 if let Some(ref spec) = self.spec
1021 && !spec.skills.is_empty()
1022 {
1023 let mut loader = self.skill_loader.take().unwrap_or_default();
1024 if let Some(ref dir) = self.yaml_dir {
1025 loader.set_base_dir(dir);
1026 }
1027 let loaded_skills = loader.load_refs(&spec.skills)?;
1028 self.skills.extend(loaded_skills);
1029 }
1030
1031 let mut llm_registry = self.llm_registry.unwrap_or_default();
1032
1033 if let Some(llm) = self.llm
1034 && !llm_registry.has("default")
1035 {
1036 let provider = if self.llm_registry_observed {
1037 if let Some(ref manager) = observability_manager {
1038 Arc::new(ObservedLLMProvider::new(
1039 llm.clone(),
1040 Arc::clone(manager),
1041 Some("default".to_string()),
1042 llm.provider_name().to_string(),
1043 model_by_alias_from_spec(self.spec.as_ref())
1044 .get("default")
1045 .cloned()
1046 .unwrap_or_else(|| "default".to_string()),
1047 )) as Arc<dyn LLMProvider>
1048 } else {
1049 llm.clone()
1050 }
1051 } else {
1052 llm.clone()
1053 };
1054 llm_registry.register("default", provider);
1055 }
1056
1057 if let Some(ref spec) = self.spec {
1058 let default_alias = spec.llm.get_default_alias();
1059 let router_alias = spec.llm.get_router_alias();
1060
1061 llm_registry.set_default(&default_alias);
1062 if let Some(router) = router_alias {
1063 llm_registry.set_router(&router);
1064 }
1065 }
1066
1067 if llm_registry.is_empty() {
1068 return Err(AgentError::Config(
1069 "At least one LLM provider is required".into(),
1070 ));
1071 }
1072
1073 if let Some(ref manager) = observability_manager
1074 && !self.llm_registry_observed
1075 {
1076 let model_by_alias = model_by_alias_from_spec(self.spec.as_ref());
1077 llm_registry = wrap_registry_with_observability(
1078 llm_registry,
1079 Arc::clone(manager),
1080 &model_by_alias,
1081 );
1082 self.llm_registry_observed = true;
1083 }
1084
1085 let memory = self.memory.unwrap_or_else(|| {
1087 if let Some(ref spec) = self.spec {
1088 if spec.memory.is_compacting() {
1089 let summarizer_llm = spec
1090 .memory
1091 .summarizer_llm
1092 .as_ref()
1093 .and_then(|alias| llm_registry.get(alias).ok())
1094 .or_else(|| llm_registry.router().ok())
1095 .or_else(|| llm_registry.default().ok());
1096
1097 let summarizer: Arc<dyn Summarizer> = match summarizer_llm {
1098 Some(llm) => Arc::new(LLMSummarizer::new(llm)),
1099 None => Arc::new(NoopSummarizer),
1100 };
1101 let config = spec.memory.to_compacting_config();
1102 return Arc::new(CompactingMemory::new(summarizer, config));
1103 }
1104 Arc::new(InMemoryStore::new(spec.memory.max_messages))
1105 } else {
1106 Arc::new(InMemoryStore::new(100))
1107 }
1108 });
1109
1110 let persona_manager: Option<Arc<ai_agents_persona::PersonaManager>> =
1112 if let Some(pm) = self.persona_manager.take() {
1113 Some(pm)
1114 } else if let Some(ref spec) = self.spec {
1115 if spec.has_persona() {
1116 let persona_config = spec.persona.clone().unwrap();
1117 let renderer = ai_agents_context::TemplateRenderer::new();
1118 let registry = self.persona_templates.clone();
1119 let manager = ai_agents_persona::PersonaManager::from_config(
1120 persona_config,
1121 registry,
1122 renderer,
1123 )
1124 .map_err(|e| {
1125 AgentError::Config(format!("Failed to create PersonaManager: {}", e))
1126 })?;
1127 Some(Arc::new(manager))
1128 } else {
1129 None
1130 }
1131 } else {
1132 None
1133 };
1134
1135 if let Some(ref pm) = persona_manager
1137 && pm.should_register_evolve_tool()
1138 {
1139 let evolve_tool = ai_agents_persona::PersonaEvolveTool::new(pm.clone());
1140 let _ = tools.register(Arc::new(evolve_tool));
1141 }
1142
1143 if let Some(ref manager) = observability_manager {
1144 tools = tools.map_tools(|tool| {
1145 Arc::new(ObservedTool::new(tool, Arc::clone(manager))) as Arc<dyn Tool>
1146 });
1147 }
1148
1149 let relationship_manager: Option<Arc<RelationshipManager>> =
1150 if let Some(ref config) = relationships_config {
1151 if config.enabled {
1152 let evaluator: Option<Arc<dyn RelationshipEvaluatorTrait>> =
1153 if config.auto_update.enabled {
1154 let llm = config
1155 .auto_update
1156 .llm
1157 .as_ref()
1158 .and_then(|alias| llm_registry.get(alias).ok())
1159 .or_else(|| llm_registry.router().ok())
1160 .or_else(|| llm_registry.default().ok());
1161 llm.map(|llm| {
1162 Arc::new(RelationshipEvaluator::new(llm))
1163 as Arc<dyn RelationshipEvaluatorTrait>
1164 })
1165 } else {
1166 None
1167 };
1168 Some(Arc::new(RelationshipManager::from_config_with_evaluator(
1169 config.clone(),
1170 evaluator,
1171 )?))
1172 } else {
1173 None
1174 }
1175 } else {
1176 None
1177 };
1178
1179 let tools_arc = Arc::new(tools);
1180 let llm_registry_arc = Arc::new(llm_registry);
1181 tools_arc.set_web_fetch_extractor(
1182 llm_registry_arc
1183 .router()
1184 .ok()
1185 .or_else(|| llm_registry_arc.default().ok()),
1186 );
1187
1188 let declared_tool_ids: Option<Vec<String>> = Some(if self.spec.is_none() {
1191 tools_arc.list_ids()
1192 } else {
1193 let mut ids: Vec<String> = self
1194 .spec
1195 .as_ref()
1196 .and_then(|s| s.tools.as_ref())
1197 .map(|tools| {
1198 let mut ids: Vec<String> = tools
1199 .iter()
1200 .filter_map(|t| {
1201 tools_arc
1202 .canonical_id(t.name())
1203 .or_else(|| Some(t.name().to_string()))
1204 })
1205 .collect();
1206
1207 for entry in tools {
1208 if let Some(mcp_config) = entry.to_mcp_config() {
1209 for view_name in mcp_config.views.keys() {
1210 ids.push(
1211 tools_arc
1212 .canonical_id(view_name)
1213 .unwrap_or_else(|| view_name.clone()),
1214 );
1215 }
1216 }
1217 }
1218
1219 ids
1220 })
1221 .unwrap_or_default();
1222
1223 if let Some(ref spec) = self.spec
1224 && let Some(ref spawner) = spec.spawner
1225 {
1226 ids.extend(spawner.management_tools.granted_management_tool_ids());
1228 ids.extend(spawner.orchestration_tools.granted_orchestration_tool_ids());
1229 }
1230
1231 if persona_manager
1232 .as_ref()
1233 .is_some_and(|pm| pm.should_register_evolve_tool())
1234 {
1235 ids.push("persona_evolve".to_string());
1237 }
1238
1239 ids.sort();
1240 ids.dedup();
1241 ids
1242 });
1243
1244 if let Some(ref ids) = declared_tool_ids {
1249 let mcp_names: Vec<String> = self
1250 .spec
1251 .as_ref()
1252 .and_then(|s| s.tools.as_ref())
1253 .map(|tools| {
1254 let mut names = Vec::new();
1255 for entry in tools {
1256 if entry.is_mcp() {
1257 names.push(entry.name().to_string());
1258 if let Some(cfg) = entry.to_mcp_config() {
1259 names.extend(cfg.views.keys().cloned());
1260 }
1261 }
1262 }
1263 names
1264 })
1265 .unwrap_or_default();
1266
1267 let missing: Vec<&str> = ids
1268 .iter()
1269 .filter(|id| !mcp_names.contains(id))
1270 .filter(|id| tools_arc.get(id).is_none())
1271 .map(|s| s.as_str())
1272 .collect();
1273
1274 if !missing.is_empty() {
1275 return Err(AgentError::Config(format!(
1276 "Tools granted by YAML but not registered: [{}]. \
1277 Register them via .tool(Arc::new(...)) or the matching auto_configure_* method before .build(), \
1278 or remove the grant from YAML.",
1279 missing.join(", ")
1280 )));
1281 }
1282 }
1283
1284 let mut agent = RuntimeAgent::new(
1285 info,
1286 llm_registry_arc.clone(),
1287 memory,
1288 tools_arc,
1289 self.skills,
1290 system_prompt,
1291 max_iterations,
1292 )
1293 .with_shared_resource_locks(resource_locks)
1294 .with_declared_tool_ids(declared_tool_ids);
1295
1296 if let Some(tokens) = self.max_context_tokens {
1297 agent = agent.with_max_context_tokens(tokens);
1298 }
1299
1300 if let Some(manager) = self.recovery_manager {
1301 agent = agent.with_recovery_manager(manager);
1302 } else if let Some(ref spec) = self.spec {
1303 agent =
1304 agent.with_recovery_manager(RecoveryManager::try_new(spec.error_recovery.clone())?);
1305 }
1306
1307 if let Some(engine) = self.tool_security {
1308 agent = agent.with_tool_security(engine);
1309 } else if let Some(ref spec) = self.spec {
1310 agent =
1311 agent.with_tool_security(ToolSecurityEngine::try_new(spec.tool_security.clone())?);
1312 }
1313
1314 if let Some(processor) = self.process_processor {
1315 agent =
1316 agent.with_process_processor(processor.with_llm_registry(llm_registry_arc.clone()));
1317 } else if let Some(ref spec) = self.spec
1318 && spec.has_process()
1319 {
1320 let processor = ProcessProcessor::new(spec.process.clone())
1321 .with_llm_registry(llm_registry_arc.clone());
1322 agent = agent.with_process_processor(processor);
1323 }
1324
1325 for (name, filter) in self.message_filters {
1326 agent.register_message_filter(name, filter);
1327 }
1328
1329 if let Some(state_machine) = self.state_machine {
1331 let evaluator = self.transition_evaluator.unwrap_or_else(|| {
1332 let eval_llm = llm_registry_arc
1333 .get("evaluator")
1334 .or_else(|_| llm_registry_arc.router())
1335 .or_else(|_| llm_registry_arc.default())
1336 .expect("At least one LLM required for transition evaluator");
1337 Arc::new(LLMTransitionEvaluator::new(eval_llm))
1338 });
1339 agent = agent.with_state_machine(state_machine, evaluator);
1340 } else if let Some(ref spec) = self.spec
1341 && let Some(ref state_config) = spec.states
1342 {
1343 let state_machine = StateMachine::new(state_config.clone())?;
1344 let evaluator = self.transition_evaluator.unwrap_or_else(|| {
1345 let eval_llm = llm_registry_arc
1346 .get("evaluator")
1347 .or_else(|_| llm_registry_arc.router())
1348 .or_else(|_| llm_registry_arc.default())
1349 .expect("At least one LLM required for transition evaluator");
1350 Arc::new(LLMTransitionEvaluator::new(eval_llm))
1351 });
1352 agent = agent.with_state_machine(Arc::new(state_machine), evaluator);
1353 }
1354
1355 if let Some(context_manager) = self.context_manager {
1357 agent = agent.with_context_manager(context_manager);
1358 } else if let Some(ref spec) = self.spec
1359 && !spec.context.is_empty()
1360 {
1361 let context_manager = ContextManager::new(
1362 spec.context.clone(),
1363 spec.name.clone(),
1364 spec.version.clone(),
1365 );
1366 agent = agent.with_context_manager(Arc::new(context_manager));
1367 }
1368
1369 if let Some(ref spec) = self.spec {
1371 agent = agent.with_parallel_tools(spec.parallel_tools.clone());
1372 let streaming_config = self
1373 .streaming
1374 .clone()
1375 .unwrap_or_else(|| spec.streaming.clone());
1376 agent = agent.with_streaming(streaming_config);
1377
1378 agent = agent.with_runtime_config(spec.runtime.clone());
1379
1380 if let Some(ref budget) = spec.memory.token_budget {
1382 agent = agent.with_memory_token_budget(budget.clone());
1383 }
1384
1385 if self.storage_config.is_none() && spec.has_storage() {
1387 agent = agent.with_storage_config(spec.storage.clone());
1388 }
1389 }
1390
1391 if let Some(storage_config) = self.storage_config {
1393 agent = agent.with_storage_config(storage_config);
1394 }
1395 if let Some(storage) = self.storage {
1396 agent = agent.with_storage(storage);
1397 }
1398
1399 if let (Some(spawner), Some(registry)) = (self.spawner, self.spawner_registry) {
1401 agent = agent.with_spawner_handles(spawner, registry);
1402 }
1403
1404 if let Some(manager) = observability_manager {
1406 agent = agent.with_observability(Arc::clone(&manager));
1407 let observability_hooks: Arc<dyn AgentHooks> =
1408 Arc::new(ObservabilityHooks::new(manager));
1409 let hooks: Arc<dyn AgentHooks> = if let Some(user_hooks) = self.hooks {
1410 Arc::new(
1411 CompositeHooks::new()
1412 .add(user_hooks)
1413 .add(observability_hooks),
1414 )
1415 } else {
1416 observability_hooks
1417 };
1418 agent = agent.with_hooks(hooks);
1419 } else if let Some(hooks) = self.hooks {
1420 agent = agent.with_hooks(hooks);
1421 }
1422
1423 if let Some(hitl_engine) = self.hitl_engine {
1425 let handler = self
1426 .approval_handler
1427 .unwrap_or_else(|| Arc::new(RejectAllHandler::new()));
1428 agent = agent.with_hitl(hitl_engine, handler);
1429 } else if let Some(ref spec) = self.spec
1430 && let Some(ref hitl_config) = spec.hitl
1431 {
1432 let hitl_engine = HITLEngine::new(hitl_config.clone());
1433 let handler = self
1434 .approval_handler
1435 .unwrap_or_else(|| Arc::new(RejectAllHandler::new()));
1436 agent = agent.with_hitl(hitl_engine, handler);
1437 }
1438
1439 if let Some(reasoning) = self.reasoning {
1440 agent = agent.with_reasoning(reasoning);
1441 } else if let Some(ref spec) = self.spec {
1442 agent = agent.with_reasoning(spec.reasoning.clone());
1443 }
1444
1445 if let Some(reflection) = self.reflection {
1446 agent = agent.with_reflection(reflection);
1447 } else if let Some(ref spec) = self.spec {
1448 agent = agent.with_reflection(spec.reflection.clone());
1449 }
1450
1451 if let Some(ref spec) = self.spec
1453 && spec.disambiguation.is_enabled()
1454 {
1455 agent = agent.with_disambiguation(spec.disambiguation.clone());
1456 }
1457
1458 if let Some(pm) = persona_manager {
1460 agent = agent.with_persona(pm);
1461 }
1462
1463 if let Some(relationship_manager) = relationship_manager {
1464 agent = agent.with_relationships(relationship_manager);
1465 }
1466
1467 if actor_memory_config.is_some() || facts_config.is_some() {
1473 agent = agent.with_facts_config(actor_memory_config, facts_config);
1474 }
1475
1476 Ok(agent)
1477 }
1478}
1479
1480fn collect_orchestration_refs(
1482 states: &std::collections::HashMap<String, ai_agents_state::StateDefinition>,
1483) -> Vec<(String, String, &'static str)> {
1484 let mut refs = Vec::new();
1485
1486 for (state_name, def) in states {
1487 if let Some(ref delegate_id) = def.delegate {
1488 refs.push((delegate_id.clone(), state_name.clone(), "delegate"));
1489 }
1490 if let Some(ref concurrent) = def.concurrent {
1491 for agent_ref in &concurrent.agents {
1492 refs.push((agent_ref.id().to_string(), state_name.clone(), "concurrent"));
1493 }
1494 }
1495 if let Some(ref gc) = def.group_chat {
1496 for participant in &gc.participants {
1497 refs.push((participant.id.clone(), state_name.clone(), "group_chat"));
1498 }
1499 }
1500 if let Some(ref pipeline) = def.pipeline {
1501 for stage in &pipeline.stages {
1502 refs.push((stage.id().to_string(), state_name.clone(), "pipeline"));
1503 }
1504 }
1505 if let Some(ref handoff) = def.handoff {
1506 refs.push((handoff.initial_agent.clone(), state_name.clone(), "handoff"));
1507 for agent_id in &handoff.available_agents {
1508 refs.push((agent_id.clone(), state_name.clone(), "handoff"));
1509 }
1510 }
1511
1512 if let Some(ref sub_states) = def.states {
1514 refs.extend(collect_orchestration_refs(sub_states));
1515 }
1516 }
1517
1518 refs
1519}
1520
1521impl Default for AgentBuilder {
1522 fn default() -> Self {
1523 Self::new()
1524 }
1525}
1526
1527#[cfg(test)]
1528mod tests {
1529 use super::*;
1530
1531 #[test]
1532 fn test_builder_new() {
1533 let builder = AgentBuilder::new();
1534 assert!(builder.spec.is_none());
1535 assert!(builder.system_prompt.is_none());
1536 }
1537
1538 #[test]
1539 fn test_builder_from_yaml() {
1540 let yaml = r#"
1541name: TestAgent
1542system_prompt: "You are helpful."
1543llm:
1544 provider: openai
1545 model: gpt-4
1546"#;
1547 let builder = AgentBuilder::from_yaml(yaml).unwrap();
1548 assert!(builder.spec.is_some());
1549 assert_eq!(builder.spec.as_ref().unwrap().name, "TestAgent");
1550 }
1551
1552 #[test]
1553 fn test_builder_from_yaml_rejects_nested_unknown_path() {
1554 let yaml = r#"
1555name: TestAgent
1556system_prompt: test
1557runtime:
1558 optimization:
1559 max_parallel_runtime_task: 4
1560"#;
1561 let error = match AgentBuilder::from_yaml(yaml) {
1562 Ok(_) => panic!("expected strict parse failure"),
1563 Err(error) => error.to_string(),
1564 };
1565 assert!(
1566 error.contains("runtime.optimization.max_parallel_runtime_task"),
1567 "{error}"
1568 );
1569 }
1570
1571 #[test]
1572 fn test_feature_override_single_llm_builder_path() {
1573 let yaml = r#"
1574name: LocalAgent
1575system_prompt: "You are helpful."
1576llm:
1577 provider: ollama
1578 model: llama3.1
1579 function_calling: true
1580"#;
1581 let builder = AgentBuilder::from_yaml(yaml)
1582 .unwrap()
1583 .auto_configure_llms()
1584 .unwrap();
1585
1586 let llm = builder.llm.as_ref().unwrap();
1587 assert!(llm.supports(LLMFeature::FunctionCalling));
1588 }
1589
1590 #[test]
1591 fn test_feature_override_named_llms_builder_path() {
1592 let yaml = r#"
1593name: LocalAgent
1594system_prompt: "You are helpful."
1595llms:
1596 default:
1597 provider: openai-compatible
1598 model: qwen3:8b
1599 base_url: http://localhost:11434/v1
1600 json_mode: true
1601llm:
1602 default: default
1603"#;
1604 let builder = AgentBuilder::from_yaml(yaml)
1605 .unwrap()
1606 .auto_configure_llms()
1607 .unwrap();
1608
1609 let registry = builder.llm_registry.as_ref().unwrap();
1610 let llm = registry.get("default").unwrap();
1611 assert!(llm.supports(LLMFeature::JsonMode));
1612 }
1613
1614 #[test]
1615 fn test_builder_from_yaml_with_tool_security() {
1616 let yaml = r#"
1617name: SecureAgent
1618system_prompt: "You are helpful."
1619llm:
1620 provider: openai
1621 model: gpt-4
1622max_context_tokens: 8192
1623error_recovery:
1624 default:
1625 max_retries: 5
1626tool_security:
1627 enabled: true
1628 tools:
1629 http:
1630 rate_limit: 10
1631"#;
1632 let builder = AgentBuilder::from_yaml(yaml).unwrap();
1633 assert!(builder.spec.is_some());
1634 let spec = builder.spec.as_ref().unwrap();
1635 assert_eq!(spec.max_context_tokens, 8192);
1636 assert_eq!(spec.error_recovery.default.max_retries, 5);
1637 assert!(spec.tool_security.enabled);
1638 }
1639
1640 #[test]
1641 fn test_builder_from_yaml_rejects_zero_max_results() {
1642 let yaml = r#"
1643name: SecureAgent
1644system_prompt: "You are helpful."
1645tool_security:
1646 enabled: true
1647 tools:
1648 web_search:
1649 max_results: 0
1650"#;
1651 let error = AgentBuilder::from_yaml(yaml).err().unwrap();
1652 assert!(
1653 error
1654 .to_string()
1655 .contains("tool_security.tools.web_search.max_results must be greater than 0")
1656 );
1657 }
1658
1659 #[test]
1660 fn test_builder_from_yaml_with_skills() {
1661 let yaml = r#"
1662name: SkillAgent
1663system_prompt: "You are helpful."
1664llm:
1665 provider: openai
1666 model: gpt-4
1667skills:
1668 - id: greeting
1669 description: "Greet users"
1670 trigger: "When user says hello"
1671 steps:
1672 - prompt: "Hello!"
1673"#;
1674 let builder = AgentBuilder::from_yaml(yaml).unwrap();
1675 assert!(builder.spec.is_some());
1676 assert!(!builder.spec.as_ref().unwrap().skills.is_empty());
1677 }
1678
1679 #[test]
1680 fn test_builder_from_spec() {
1681 let spec = AgentSpec {
1682 name: "test".to_string(),
1683 version: "1.0".to_string(),
1684 description: Some("Test agent".to_string()),
1685 system_prompt: "You are helpful".to_string(),
1686 ..Default::default()
1687 };
1688
1689 let builder = AgentBuilder::from_spec(spec);
1690 assert!(builder.spec.is_some());
1691 assert_eq!(builder.system_prompt, Some("You are helpful".to_string()));
1692 }
1693
1694 #[test]
1695 fn test_builder_rejects_invalid_programmatic_tool_security() {
1696 use ai_agents_llm::mock::MockLLMProvider;
1697
1698 let mut spec = AgentSpec::default();
1699 spec.tool_security.tools.insert(
1700 "web_search".to_string(),
1701 ai_agents_tools::ToolPolicyConfig {
1702 max_results: Some(0),
1703 ..Default::default()
1704 },
1705 );
1706
1707 let error = AgentBuilder::from_spec(spec)
1708 .llm(Arc::new(MockLLMProvider::new("test")))
1709 .build()
1710 .unwrap_err();
1711 assert!(
1712 error
1713 .to_string()
1714 .contains("max_results must be greater than 0")
1715 );
1716 }
1717
1718 #[test]
1719 fn test_builder_from_spec_with_base_dir_preserves_mutations() {
1720 let yaml = r#"
1721name: OriginalAgent
1722system_prompt: "Original prompt"
1723max_iterations: 10
1724llm:
1725 provider: openai
1726 model: gpt-4
1727"#;
1728 let mut spec = AgentBuilder::from_yaml(yaml).unwrap().spec.unwrap();
1729 spec.name = "RewrittenAgent".to_string();
1730 spec.system_prompt = "Rewritten prompt".to_string();
1731 spec.max_iterations = 37;
1732 let base_dir = PathBuf::from("rewritten-agent-dir");
1733
1734 let builder = AgentBuilder::from_spec_with_base_dir(spec, &base_dir);
1735
1736 let stored_spec = builder.spec.as_ref().unwrap();
1737 assert_eq!(stored_spec.name, "RewrittenAgent");
1738 assert_eq!(stored_spec.system_prompt, "Rewritten prompt");
1739 assert_eq!(stored_spec.max_iterations, 37);
1740 assert_eq!(builder.system_prompt.as_deref(), Some("Rewritten prompt"));
1741 assert_eq!(builder.max_iterations, Some(37));
1742 assert_eq!(builder.yaml_dir.as_deref(), Some(base_dir.as_path()));
1743 }
1744
1745 #[tokio::test]
1746 async fn test_builder_from_spec_with_base_dir_resolves_spawner_paths() {
1747 use ai_agents_llm::mock::MockLLMProvider;
1748
1749 let base_dir = std::env::temp_dir().join(format!(
1750 "ai-agents-builder-base-dir-{}",
1751 uuid::Uuid::new_v4()
1752 ));
1753 let templates_dir = base_dir.join("templates");
1754 let agents_dir = base_dir.join("agents");
1755 std::fs::create_dir_all(&templates_dir).unwrap();
1756 std::fs::create_dir_all(&agents_dir).unwrap();
1757
1758 let template_content = "name: {{ name }}\nsystem_prompt: Template prompt\n";
1759 std::fs::write(templates_dir.join("worker.yaml"), template_content).unwrap();
1760 std::fs::write(
1761 agents_dir.join("child.yaml"),
1762 r#"
1763name: ChildAgent
1764system_prompt: "Child prompt"
1765llm:
1766 provider: definitely-not-a-provider
1767 model: unavailable
1768"#,
1769 )
1770 .unwrap();
1771
1772 let yaml = r#"
1773name: ParentAgent
1774system_prompt: "Parent prompt"
1775llm:
1776 provider: openai
1777 model: gpt-4
1778spawner:
1779 shared_llms: true
1780 templates:
1781 worker:
1782 path: templates/worker.yaml
1783 auto_spawn:
1784 - id: child
1785 agent: agents/child.yaml
1786"#;
1787 let spec = AgentBuilder::from_yaml(yaml).unwrap().spec.unwrap();
1788
1789 let builder = AgentBuilder::from_spec_with_base_dir(spec, &base_dir)
1790 .llm(Arc::new(MockLLMProvider::new("test")))
1791 .auto_configure_spawner()
1792 .await
1793 .unwrap();
1794
1795 let template = builder
1796 .spawner
1797 .as_ref()
1798 .unwrap()
1799 .templates()
1800 .get("worker")
1801 .unwrap();
1802 assert_eq!(template.content, template_content);
1803 assert!(builder.spawner_registry.as_ref().unwrap().contains("child"));
1804
1805 std::fs::remove_dir_all(base_dir).unwrap();
1806 }
1807
1808 #[tokio::test]
1809 async fn test_builder_auto_spawn_fails_on_any_declared_child_error() {
1810 use ai_agents_llm::mock::MockLLMProvider;
1811
1812 let base_dir = std::env::temp_dir().join(format!(
1813 "ai-agents-builder-child-failure-{}",
1814 uuid::Uuid::new_v4()
1815 ));
1816 std::fs::create_dir_all(&base_dir).unwrap();
1817 std::fs::write(
1818 base_dir.join("valid.yaml"),
1819 "name: ValidChild\nsystem_prompt: valid\n",
1820 )
1821 .unwrap();
1822
1823 let yaml = r#"
1824name: ParentAgent
1825system_prompt: parent
1826llm:
1827 default: default
1828spawner:
1829 shared_llms: true
1830 auto_spawn:
1831 - id: valid
1832 agent: valid.yaml
1833 - id: missing
1834 agent: missing.yaml
1835"#;
1836 let spec = AgentBuilder::from_yaml(yaml).unwrap().spec.unwrap();
1837 let mut registry = LLMRegistry::new();
1838 registry.register("default", Arc::new(MockLLMProvider::new("test")));
1839 registry.set_default("default");
1840
1841 let error = AgentBuilder::from_spec_with_base_dir(spec, &base_dir)
1842 .llm_registry(registry)
1843 .auto_configure_spawner()
1844 .await
1845 .err()
1846 .unwrap()
1847 .to_string();
1848 assert!(error.contains("missing"), "{error}");
1849 assert!(error.contains("missing.yaml"), "{error}");
1850
1851 std::fs::remove_dir_all(base_dir).unwrap();
1852 }
1853
1854 #[test]
1855 fn test_builder_chain() {
1856 let builder = AgentBuilder::new()
1857 .system_prompt("Test prompt")
1858 .max_iterations(5)
1859 .max_context_tokens(4096);
1860
1861 assert_eq!(builder.system_prompt, Some("Test prompt".to_string()));
1862 assert_eq!(builder.max_iterations, Some(5));
1863 assert_eq!(builder.max_context_tokens, Some(4096));
1864 }
1865
1866 #[test]
1867 fn test_builder_skills() {
1868 use ai_agents_skills::{SkillDefinition, SkillStep};
1869
1870 let skill = SkillDefinition {
1871 id: "test".to_string(),
1872 description: "Test skill".to_string(),
1873 trigger: "When testing".to_string(),
1874 steps: vec![SkillStep::Prompt {
1875 prompt: "Hello".to_string(),
1876 llm: None,
1877 }],
1878 reasoning: None,
1879 reflection: None,
1880 disambiguation: None,
1881 };
1882
1883 let builder = AgentBuilder::new().skill(skill.clone()).skills(vec![skill]);
1884
1885 assert_eq!(builder.skills.len(), 2);
1886 }
1887
1888 #[test]
1889 fn test_builder_from_yaml_with_states() {
1890 let yaml = r#"
1891name: StatefulAgent
1892system_prompt: "You are helpful."
1893llm:
1894 provider: openai
1895 model: gpt-4
1896states:
1897 initial: greeting
1898 states:
1899 greeting:
1900 prompt: "Welcome!"
1901 transitions:
1902 - to: support
1903 when: "user needs help"
1904 support:
1905 prompt: "How can I help?"
1906"#;
1907 let builder = AgentBuilder::from_yaml(yaml).unwrap();
1908 assert!(builder.spec.is_some());
1909 let spec = builder.spec.as_ref().unwrap();
1910 assert!(spec.has_states());
1911 assert!(spec.states.is_some());
1912 let states = spec.states.as_ref().unwrap();
1913 assert_eq!(states.initial, "greeting");
1914 assert_eq!(states.states.len(), 2);
1915 }
1916
1917 #[test]
1918 fn test_builder_from_yaml_with_context() {
1919 let yaml = r#"
1920name: ContextAgent
1921system_prompt: "Hello, {{ context.user.name }}!"
1922llm:
1923 provider: openai
1924 model: gpt-4
1925context:
1926 user:
1927 type: runtime
1928 required: true
1929 time:
1930 type: builtin
1931 source: datetime
1932 refresh: per_turn
1933"#;
1934 let builder = AgentBuilder::from_yaml(yaml).unwrap();
1935 assert!(builder.spec.is_some());
1936 let spec = builder.spec.as_ref().unwrap();
1937 assert!(spec.has_context());
1938 assert_eq!(spec.context.len(), 2);
1939 assert!(spec.context.contains_key("user"));
1940 assert!(spec.context.contains_key("time"));
1941 }
1942
1943 #[test]
1944 fn test_builder_from_yaml_with_full_v04_features() {
1945 let yaml = r#"
1946name: FullFeaturedAgent
1947version: "0.4.0"
1948system_prompt: |
1949 You are a helpful assistant.
1950 User: {{ context.user.name }}
1951 Language: {{ context.user.language }}
1952llm:
1953 provider: openai
1954 model: gpt-4
1955context:
1956 user:
1957 type: runtime
1958 required: true
1959 default:
1960 name: "Guest"
1961 language: "en"
1962 time:
1963 type: builtin
1964 source: datetime
1965 refresh: per_turn
1966states:
1967 initial: greeting
1968 states:
1969 greeting:
1970 prompt: "Welcome to our service!"
1971 prompt_mode: append
1972 transitions:
1973 - to: support
1974 when: "user needs help"
1975 auto: true
1976 priority: 10
1977 support:
1978 prompt: "I'm here to help you."
1979 max_turns: 5
1980 timeout_to: escalation
1981 transitions:
1982 - to: closing
1983 when: "issue resolved"
1984 auto: true
1985 escalation:
1986 prompt: "Let me connect you with a human agent."
1987 closing:
1988 prompt: "Thank you for using our service!"
1989"#;
1990 let builder = AgentBuilder::from_yaml(yaml).unwrap();
1991 assert!(builder.spec.is_some());
1992 let spec = builder.spec.as_ref().unwrap();
1993
1994 assert!(spec.has_context());
1996 assert_eq!(spec.context.len(), 2);
1997
1998 assert!(spec.has_states());
2000 let states = spec.states.as_ref().unwrap();
2001 assert_eq!(states.initial, "greeting");
2002 assert_eq!(states.states.len(), 4);
2003
2004 let greeting = states.states.get("greeting").unwrap();
2006 assert!(greeting.prompt.is_some());
2007 assert_eq!(greeting.transitions.len(), 1);
2008 assert_eq!(greeting.transitions[0].to, "support");
2009 assert!(greeting.transitions[0].auto);
2010
2011 let support = states.states.get("support").unwrap();
2013 assert_eq!(support.max_turns, Some(5));
2014 assert_eq!(support.timeout_to, Some("escalation".to_string()));
2015 }
2016
2017 #[test]
2018 fn test_builder_from_yaml_with_hitl() {
2019 let yaml = r#"
2020name: HITLAgent
2021system_prompt: "You are a secure assistant."
2022llm:
2023 provider: openai
2024 model: gpt-4
2025hitl:
2026 default_timeout_seconds: 600
2027 on_timeout: reject
2028 tools:
2029 send_payment:
2030 require_approval: true
2031 approval_context:
2032 - amount
2033 - recipient
2034 approval_message: "Approve payment?"
2035 delete_record:
2036 require_approval: true
2037 conditions:
2038 - name: high_value
2039 when: "amount > 1000"
2040 require_approval: true
2041 approval_message: "High value transaction"
2042 states:
2043 escalation:
2044 on_enter: require_approval
2045 approval_message: "Escalate to human?"
2046"#;
2047 let builder = AgentBuilder::from_yaml(yaml).unwrap();
2048 assert!(builder.spec.is_some());
2049 let spec = builder.spec.as_ref().unwrap();
2050
2051 assert!(spec.has_hitl());
2052 let hitl = spec.hitl.as_ref().unwrap();
2053 assert_eq!(hitl.default_timeout_seconds, 600);
2054 assert_eq!(hitl.tools.len(), 2);
2055 assert!(hitl.tools.get("send_payment").unwrap().require_approval);
2056 assert_eq!(hitl.conditions.len(), 1);
2057 assert_eq!(hitl.conditions[0].name, "high_value");
2058 assert_eq!(hitl.states.len(), 1);
2059 }
2060
2061 #[test]
2062 fn test_builder_from_yaml_with_compacting_memory() {
2063 let yaml = r#"
2064name: CompactingAgent
2065system_prompt: "You are a helpful assistant."
2066memory:
2067 type: compacting
2068 max_messages: 100
2069 max_recent_messages: 20
2070 compress_threshold: 15
2071 summarize_batch_size: 5
2072 summarizer_llm: router
2073llm:
2074 provider: openai
2075 model: gpt-4
2076"#;
2077 let builder = AgentBuilder::from_yaml(yaml).unwrap();
2078 assert!(builder.spec.is_some());
2079 let spec = builder.spec.as_ref().unwrap();
2080
2081 assert!(spec.memory.is_compacting());
2082 assert_eq!(spec.memory.max_recent_messages, Some(20));
2083 assert_eq!(spec.memory.compress_threshold, Some(15));
2084 assert_eq!(spec.memory.summarize_batch_size, Some(5));
2085 assert_eq!(spec.memory.summarizer_llm, Some("router".to_string()));
2086
2087 let compacting_config = spec.memory.to_compacting_config();
2088 assert_eq!(compacting_config.max_recent_messages, 20);
2089 assert_eq!(compacting_config.compress_threshold, 15);
2090 assert_eq!(compacting_config.summarize_batch_size, 5);
2091 }
2092
2093 #[test]
2094 fn test_builder_from_yaml_with_token_budget() {
2095 let yaml = r#"
2096name: BudgetAgent
2097system_prompt: "You are a helpful assistant."
2098memory:
2099 type: compacting
2100 max_messages: 100
2101 token_budget:
2102 total: 8192
2103 allocation:
2104 summary: 2048
2105 recent_messages: 4096
2106 facts: 1024
2107 overflow_strategy: summarize_more
2108 warn_at_percent: 75
2109llm:
2110 provider: openai
2111 model: gpt-4
2112"#;
2113 let builder = AgentBuilder::from_yaml(yaml).unwrap();
2114 assert!(builder.spec.is_some());
2115 let spec = builder.spec.as_ref().unwrap();
2116
2117 assert!(spec.memory.token_budget.is_some());
2118 let budget = spec.memory.token_budget.as_ref().unwrap();
2119 assert_eq!(budget.total, 8192);
2120 assert_eq!(budget.allocation.summary, 2048);
2121 assert_eq!(budget.allocation.recent_messages, 4096);
2122 assert_eq!(budget.allocation.facts, 1024);
2123 assert_eq!(budget.warn_at_percent, 75);
2124 }
2125
2126 #[test]
2127 fn test_builder_from_yaml_with_overflow_strategies() {
2128 use ai_agents_memory::OverflowStrategy;
2129
2130 let yaml = r#"
2131name: TruncateAgent
2132system_prompt: "You are helpful."
2133memory:
2134 type: compacting
2135 token_budget:
2136 total: 4096
2137 overflow_strategy: truncate_oldest
2138llm:
2139 provider: openai
2140 model: gpt-4
2141"#;
2142 let builder = AgentBuilder::from_yaml(yaml).unwrap();
2143 let budget = builder
2144 .spec
2145 .as_ref()
2146 .unwrap()
2147 .memory
2148 .token_budget
2149 .as_ref()
2150 .unwrap();
2151 assert_eq!(budget.overflow_strategy, OverflowStrategy::TruncateOldest);
2152
2153 let yaml = r#"
2154name: ErrorAgent
2155system_prompt: "You are helpful."
2156memory:
2157 type: compacting
2158 token_budget:
2159 total: 4096
2160 overflow_strategy: error
2161llm:
2162 provider: openai
2163 model: gpt-4
2164"#;
2165 let builder = AgentBuilder::from_yaml(yaml).unwrap();
2166 let budget = builder
2167 .spec
2168 .as_ref()
2169 .unwrap()
2170 .memory
2171 .token_budget
2172 .as_ref()
2173 .unwrap();
2174 assert_eq!(budget.overflow_strategy, OverflowStrategy::Error);
2175 }
2176
2177 #[test]
2178 fn test_builder_from_yaml_with_storage_file() {
2179 let yaml = r#"
2180name: PersistentAgent
2181system_prompt: "You are helpful."
2182llm:
2183 provider: openai
2184 model: gpt-4
2185storage:
2186 type: file
2187 path: "./data/sessions"
2188"#;
2189 let builder = AgentBuilder::from_yaml(yaml).unwrap();
2190 let spec = builder.spec.as_ref().unwrap();
2191 assert!(spec.has_storage());
2192 assert!(spec.storage.is_file());
2193 assert_eq!(spec.storage.get_path(), Some("./data/sessions"));
2194 }
2195
2196 #[test]
2197 fn test_builder_from_yaml_with_storage_sqlite() {
2198 let yaml = r#"
2199name: PersistentAgent
2200system_prompt: "You are helpful."
2201llm:
2202 provider: openai
2203 model: gpt-4
2204storage:
2205 type: sqlite
2206 path: "./data/sessions.db"
2207"#;
2208 let builder = AgentBuilder::from_yaml(yaml).unwrap();
2209 let spec = builder.spec.as_ref().unwrap();
2210 assert!(spec.has_storage());
2211 assert!(spec.storage.is_sqlite());
2212 }
2213
2214 #[test]
2215 fn test_builder_from_yaml_with_storage_redis() {
2216 let yaml = r#"
2217name: PersistentAgent
2218system_prompt: "You are helpful."
2219llm:
2220 provider: openai
2221 model: gpt-4
2222storage:
2223 type: redis
2224 url: "redis://localhost:6379"
2225 prefix: "myagent:"
2226 ttl_seconds: 86400
2227"#;
2228 let builder = AgentBuilder::from_yaml(yaml).unwrap();
2229 let spec = builder.spec.as_ref().unwrap();
2230 assert!(spec.has_storage());
2231 assert!(spec.storage.is_redis());
2232 assert_eq!(spec.storage.get_prefix(), "myagent:");
2233 assert_eq!(spec.storage.get_ttl(), Some(86400));
2234 }
2235
2236 #[test]
2237 fn test_builder_no_storage_by_default() {
2238 let yaml = r#"
2239name: SimpleAgent
2240system_prompt: "You are helpful."
2241llm:
2242 provider: openai
2243 model: gpt-4
2244"#;
2245 let builder = AgentBuilder::from_yaml(yaml).unwrap();
2246 let spec = builder.spec.as_ref().unwrap();
2247 assert!(!spec.has_storage());
2248 }
2249
2250 #[test]
2251 fn test_build_fails_on_missing_declared_tool() {
2252 use ai_agents_llm::mock::MockLLMProvider;
2253
2254 let yaml = r#"
2255name: ToolAgent
2256system_prompt: "You are helpful."
2257llm:
2258 provider: openai
2259 model: gpt-4
2260tools:
2261 - name: lookup_order
2262 - name: calculator
2263"#;
2264 let llm = Arc::new(MockLLMProvider::new("test"));
2265 let result = AgentBuilder::from_yaml(yaml)
2266 .unwrap()
2267 .llm(llm)
2268 .auto_configure_features()
2269 .unwrap()
2270 .build();
2272
2273 assert!(result.is_err());
2274 let err = result.unwrap_err().to_string();
2275 assert!(
2276 err.contains("lookup_order"),
2277 "error should name the missing tool: {}",
2278 err
2279 );
2280 assert!(
2282 !err.contains("calculator"),
2283 "calculator is registered, should not be missing: {}",
2284 err
2285 );
2286 }
2287
2288 #[test]
2289 fn test_build_succeeds_when_declared_tool_is_registered() {
2290 use ai_agents_core::Tool;
2291 use ai_agents_llm::mock::MockLLMProvider;
2292
2293 struct FakeTool;
2294 #[async_trait::async_trait]
2295 impl Tool for FakeTool {
2296 fn id(&self) -> &str {
2297 "lookup_order"
2298 }
2299 fn name(&self) -> &str {
2300 "Order Lookup"
2301 }
2302 fn description(&self) -> &str {
2303 "Look up an order"
2304 }
2305 fn input_schema(&self) -> serde_json::Value {
2306 serde_json::json!({})
2307 }
2308 async fn execute(
2309 &self,
2310 _args: serde_json::Value,
2311 _ctx: ai_agents_core::ToolExecutionContext,
2312 ) -> ai_agents_core::ToolResult {
2313 ai_agents_core::ToolResult::ok("ok")
2314 }
2315 }
2316
2317 let yaml = r#"
2318name: ToolAgent
2319system_prompt: "You are helpful."
2320llm:
2321 provider: openai
2322 model: gpt-4
2323tools:
2324 - name: lookup_order
2325 - name: calculator
2326"#;
2327 let llm = Arc::new(MockLLMProvider::new("test"));
2328 let result = AgentBuilder::from_yaml(yaml)
2329 .unwrap()
2330 .llm(llm)
2331 .auto_configure_features()
2332 .unwrap()
2333 .tool(Arc::new(FakeTool))
2334 .build();
2335
2336 assert!(
2337 result.is_ok(),
2338 "build should succeed when all declared tools are registered: {:?}",
2339 result.err()
2340 );
2341 }
2342
2343 #[test]
2344 fn test_spawner_config_deserializes_shared_storage() {
2345 let yaml = r#"
2346name: TestAgent
2347system_prompt: "Test"
2348llm:
2349 provider: openai
2350 model: gpt-4
2351spawner:
2352 shared_llms: true
2353 shared_storage:
2354 type: sqlite
2355 path: ./data/test.db
2356 max_agents: 5
2357"#;
2358 let builder = AgentBuilder::from_yaml(yaml).unwrap();
2359 let spec = builder.spec.as_ref().unwrap();
2360 let sc = spec.spawner.as_ref().unwrap();
2361 assert!(sc.shared_storage.is_some());
2362 assert!(sc.shared_storage.as_ref().unwrap().is_sqlite());
2363 }
2364
2365 #[test]
2366 fn test_build_succeeds_with_no_tools_declared() {
2367 use ai_agents_llm::mock::MockLLMProvider;
2368
2369 let yaml = r#"
2370name: SimpleAgent
2371system_prompt: "You are helpful."
2372llm:
2373 provider: openai
2374 model: gpt-4
2375"#;
2376 let llm = Arc::new(MockLLMProvider::new("test"));
2377 let result = AgentBuilder::from_yaml(yaml)
2378 .unwrap()
2379 .llm(llm)
2380 .auto_configure_features()
2381 .unwrap()
2382 .build();
2383
2384 assert!(
2385 result.is_ok(),
2386 "no tools: section means no validation needed: {:?}",
2387 result.err()
2388 );
2389 }
2390}