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, LLMProvider, Result, Tool};
8use ai_agents_hitl::{ApprovalHandler, HITLEngine, RejectAllHandler};
9use ai_agents_hooks::AgentHooks;
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_process::ProcessProcessor;
16use ai_agents_reasoning::{ReasoningConfig, ReflectionConfig};
17use ai_agents_recovery::{MessageFilter, RecoveryManager};
18use ai_agents_skills::{SkillDefinition, SkillLoader};
19use ai_agents_state::{LLMTransitionEvaluator, StateMachine, TransitionEvaluator};
20use ai_agents_template::{TemplateInheritance, TemplateLoader, TemplateRenderer};
21use ai_agents_tools::mcp::view::MCPViewTool;
22use ai_agents_tools::mcp::wrapper::MCPWrapperTool;
23use ai_agents_tools::{ToolRegistry, ToolSecurityEngine, create_builtin_registry};
24
25use super::AgentInfo;
26use super::StreamingConfig;
27use super::runtime::RuntimeAgent;
28use crate::spec::{AgentSpec, StorageConfig};
29
30pub struct AgentBuilder {
31 spec: Option<AgentSpec>,
32 llm: Option<Arc<dyn LLMProvider>>,
33 llm_registry: Option<LLMRegistry>,
34 memory: Option<Arc<dyn Memory>>,
35 tools: Option<ToolRegistry>,
36 skills: Vec<SkillDefinition>,
37 skill_loader: Option<SkillLoader>,
38 yaml_dir: Option<PathBuf>,
39 system_prompt: Option<String>,
40 tools_prompt: Option<String>,
41 auto_tools_prompt: bool,
42 max_iterations: Option<u32>,
43 max_context_tokens: Option<u32>,
44 recovery_manager: Option<RecoveryManager>,
45 tool_security: Option<ToolSecurityEngine>,
46 process_processor: Option<ProcessProcessor>,
47 message_filters: HashMap<String, Arc<dyn MessageFilter>>,
48 context_manager: Option<Arc<ContextManager>>,
49 state_machine: Option<Arc<StateMachine>>,
50 transition_evaluator: Option<Arc<dyn TransitionEvaluator>>,
51 hooks: Option<Arc<dyn AgentHooks>>,
52 hitl_engine: Option<HITLEngine>,
53 approval_handler: Option<Arc<dyn ApprovalHandler>>,
54 storage_config: Option<StorageConfig>,
55 storage: Option<Arc<dyn AgentStorage>>,
56 reasoning: Option<ReasoningConfig>,
57 reflection: Option<ReflectionConfig>,
58 streaming: Option<StreamingConfig>,
59 spawner: Option<Arc<crate::spawner::AgentSpawner>>,
60 spawner_registry: Option<Arc<crate::spawner::AgentRegistry>>,
61 persona_manager: Option<Arc<ai_agents_persona::PersonaManager>>,
62 persona_templates: Option<Arc<ai_agents_persona::PersonaTemplateRegistry>>,
63}
64
65impl AgentBuilder {
66 pub fn new() -> Self {
67 Self {
68 reasoning: None,
69 reflection: None,
70 spec: None,
71 llm: None,
72 llm_registry: None,
73 memory: None,
74 tools: None,
75 skills: Vec::new(),
76 skill_loader: None,
77 yaml_dir: None,
78 system_prompt: None,
79 tools_prompt: None,
80 auto_tools_prompt: true,
81 max_iterations: None,
82 max_context_tokens: None,
83 recovery_manager: None,
84 tool_security: None,
85 process_processor: None,
86 message_filters: HashMap::new(),
87 context_manager: None,
88 state_machine: None,
89 transition_evaluator: None,
90 hooks: None,
91 hitl_engine: None,
92 approval_handler: None,
93 storage_config: None,
94 storage: None,
95 streaming: None,
96 spawner: None,
97 spawner_registry: None,
98 persona_manager: None,
99 persona_templates: None,
100 }
101 }
102
103 pub fn from_spec(spec: AgentSpec) -> Self {
104 let system_prompt = spec.system_prompt.clone();
105 let max_iterations = Some(spec.max_iterations);
106 let max_context_tokens = Some(spec.max_context_tokens);
107 let reasoning = Some(spec.reasoning.clone());
108 let reflection = Some(spec.reflection.clone());
109
110 Self {
111 spec: Some(spec),
112 llm: None,
113 llm_registry: None,
114 memory: None,
115 tools: None,
116 skills: Vec::new(),
117 skill_loader: None,
118 yaml_dir: None,
119 system_prompt: Some(system_prompt),
120 tools_prompt: None,
121 auto_tools_prompt: true,
122 max_iterations,
123 max_context_tokens,
124 recovery_manager: None,
125 tool_security: None,
126 process_processor: None,
127 message_filters: HashMap::new(),
128 context_manager: None,
129 state_machine: None,
130 transition_evaluator: None,
131 hooks: None,
132 hitl_engine: None,
133 approval_handler: None,
134 storage_config: None,
135 storage: None,
136 reasoning,
137 reflection,
138 streaming: None,
139 spawner: None,
140 spawner_registry: None,
141 persona_manager: None,
142 persona_templates: None,
143 }
144 }
145
146 pub fn from_yaml(yaml_content: &str) -> Result<Self> {
147 let spec: AgentSpec = serde_yaml::from_str(yaml_content)?;
148 spec.validate()?;
149 Ok(Self::from_spec(spec))
150 }
151
152 pub fn from_yaml_file(path: impl AsRef<Path>) -> Result<Self> {
153 let path = path.as_ref();
154 let content = std::fs::read_to_string(path).map_err(AgentError::IoError)?;
155 let mut builder = Self::from_yaml(&content)?;
156 if let Some(parent) = path.parent() {
157 builder.yaml_dir = Some(parent.to_path_buf());
158 }
159 Ok(builder)
160 }
161
162 pub fn from_template(template_name: &str) -> Result<Self> {
163 let loader = TemplateLoader::new();
164 Self::from_template_with_loader(template_name, &loader)
165 }
166
167 pub fn from_template_with_loader(template_name: &str, loader: &TemplateLoader) -> Result<Self> {
168 let renderer = TemplateRenderer::new();
169 let variables = loader.variables();
170
171 let load_and_render = |name: &str| -> Result<String> {
172 let content = loader.load_template(name)?;
173 renderer.render(&content, variables)
174 };
175
176 let rendered_root = load_and_render(template_name)?;
177 let processed = TemplateInheritance::process(&rendered_root, load_and_render)?;
178 let spec: AgentSpec = serde_yaml::from_str(&processed)?;
179 spec.validate()?;
180 Ok(Self::from_spec(spec))
181 }
182
183 pub fn auto_configure_llms(mut self) -> Result<Self> {
184 let spec = self
185 .spec
186 .as_ref()
187 .ok_or_else(|| AgentError::Config("Cannot auto-configure LLMs without spec".into()))?;
188
189 if !spec.llms.is_empty() {
190 let mut registry = LLMRegistry::new();
191
192 for (alias, config) in &spec.llms {
193 let provider_type = ProviderType::from_str(&config.provider)
194 .map_err(|e| AgentError::Config(e.to_string()))?;
195
196 let core_config = ai_agents_core::LLMConfig {
197 temperature: Some(config.temperature),
198 max_tokens: Some(config.max_tokens),
199 top_p: config.top_p,
200 top_k: None,
201 frequency_penalty: None,
202 presence_penalty: None,
203 stop_sequences: None,
204 timeout_seconds: config.timeout_seconds,
205 reasoning: config.reasoning,
206 reasoning_effort: config.reasoning_effort.clone(),
207 reasoning_budget_tokens: config.reasoning_budget_tokens,
208 extra: config.extra.clone(),
209 };
210 let base_url = config.base_url.clone().or_else(|| {
212 config
213 .extra
214 .get("base_url")
215 .and_then(|v| v.as_str())
216 .map(|s| s.to_string())
217 });
218
219 let api_key = config
221 .api_key_env
222 .as_ref()
223 .and_then(|env_var| std::env::var(env_var).ok());
224
225 let provider = UnifiedLLMProvider::from_spec_config(
226 provider_type,
227 &config.model,
228 api_key,
229 base_url,
230 core_config,
231 )
232 .map_err(|e| AgentError::LLM(e.to_string()))?;
233
234 registry.register(alias, Arc::new(provider));
235 }
236
237 let default_alias = spec.llm.get_default_alias();
238 let router_alias = spec.llm.get_router_alias();
239
240 registry.set_default(&default_alias);
241 if let Some(router) = router_alias {
242 registry.set_router(&router);
243 }
244
245 self.llm_registry = Some(registry);
246 } else if let Some(config) = spec.llm.as_config() {
247 let provider_type = ProviderType::from_str(&config.provider)
248 .map_err(|e| AgentError::Config(e.to_string()))?;
249
250 let core_config = ai_agents_core::LLMConfig {
251 temperature: Some(config.temperature),
252 max_tokens: Some(config.max_tokens),
253 top_p: config.top_p,
254 top_k: None,
255 frequency_penalty: None,
256 presence_penalty: None,
257 stop_sequences: None,
258 timeout_seconds: config.timeout_seconds,
259 reasoning: config.reasoning,
260 reasoning_effort: config.reasoning_effort.clone(),
261 reasoning_budget_tokens: config.reasoning_budget_tokens,
262 extra: config.extra.clone(),
263 };
264 let base_url = config.base_url.clone().or_else(|| {
266 config
267 .extra
268 .get("base_url")
269 .and_then(|v| v.as_str())
270 .map(|s| s.to_string())
271 });
272
273 let api_key = config
275 .api_key_env
276 .as_ref()
277 .and_then(|env_var| std::env::var(env_var).ok());
278
279 let provider = UnifiedLLMProvider::from_spec_config(
280 provider_type,
281 &config.model,
282 api_key,
283 base_url,
284 core_config,
285 )
286 .map_err(|e| AgentError::LLM(e.to_string()))?;
287
288 self.llm = Some(Arc::new(provider));
289 }
290
291 Ok(self)
292 }
293
294 pub fn auto_configure_features(mut self) -> Result<Self> {
311 if let Some(ref spec) = self.spec {
312 self.recovery_manager = Some(RecoveryManager::new(spec.error_recovery.clone()));
313 self.tool_security = Some(ToolSecurityEngine::new(spec.tool_security.clone()));
314
315 if spec.has_process() {
316 let mut processor = ProcessProcessor::new(spec.process.clone());
317 if let Some(ref registry) = self.llm_registry {
318 processor = processor.with_llm_registry(Arc::new(registry.clone()));
319 }
320 self.process_processor = Some(processor);
321 }
322
323 if self.tools.is_none() {
325 self.tools = Some(create_builtin_registry());
326 }
327 }
328 Ok(self)
329 }
330
331 pub async fn auto_configure_mcp(mut self) -> Result<Self> {
338 if let Some(ref spec) = self.spec {
339 let mcp_configs: Vec<_> = spec
341 .tools
342 .as_ref()
343 .map(|tools| {
344 tools
345 .iter()
346 .filter_map(|entry| entry.to_mcp_config())
347 .collect()
348 })
349 .unwrap_or_default();
350
351 if !mcp_configs.is_empty() {
352 let registry = self.tools.get_or_insert_with(create_builtin_registry);
353
354 for config in mcp_configs {
355 let tool_name = config.name.clone();
356 let timeout_ms = config.startup_timeout_ms;
357 let views_config = config.views.clone();
358
359 let wrapper = MCPWrapperTool::new(config);
360
361 match tokio::time::timeout(
363 std::time::Duration::from_millis(timeout_ms),
364 wrapper.initialized(),
365 )
366 .await
367 {
368 Ok(Ok(initialized_tool)) => {
369 tracing::info!(
370 tool = %tool_name,
371 functions = initialized_tool.function_count(),
372 "MCP wrapper tool registered"
373 );
374
375 let parent = Arc::new(initialized_tool);
376
377 registry.register(parent.clone()).map_err(|e| {
379 AgentError::Config(format!(
380 "Failed to register MCP tool '{}': {}",
381 tool_name, e
382 ))
383 })?;
384
385 for (view_name, view_config) in &views_config {
387 let view_tool = MCPViewTool::new(
388 view_name.clone(),
389 parent.clone(),
390 view_config.functions.clone(),
391 view_config.description.clone(),
392 )
393 .map_err(|e| {
394 AgentError::Config(format!(
395 "Failed to create MCP view '{}': {}",
396 view_name, e
397 ))
398 })?;
399
400 tracing::info!(
401 view = %view_name,
402 parent = %tool_name,
403 functions = view_config.functions.len(),
404 "MCP view tool registered"
405 );
406
407 registry.register(Arc::new(view_tool)).map_err(|e| {
408 AgentError::Config(format!(
409 "Failed to register MCP view '{}': {}",
410 view_name, e
411 ))
412 })?;
413 }
414 }
415 Ok(Err(e)) => {
416 return Err(AgentError::Config(format!(
417 "MCP tool '{}' initialization failed: {}",
418 tool_name, e
419 )));
420 }
421 Err(_) => {
422 return Err(AgentError::Config(format!(
423 "MCP tool '{}' timed out after {}ms",
424 tool_name, timeout_ms
425 )));
426 }
427 }
428 }
429 }
430 }
431 Ok(self)
432 }
433
434 pub fn llm(mut self, llm: Arc<dyn LLMProvider>) -> Self {
435 self.llm = Some(llm);
436 self
437 }
438
439 pub fn llm_alias(mut self, alias: impl Into<String>, provider: Arc<dyn LLMProvider>) -> Self {
440 if self.llm_registry.is_none() {
441 self.llm_registry = Some(LLMRegistry::new());
442 }
443 if let Some(ref mut registry) = self.llm_registry {
444 registry.register(alias, provider);
445 }
446 self
447 }
448
449 pub fn llm_registry(mut self, registry: LLMRegistry) -> Self {
450 self.llm_registry = Some(registry);
451 self
452 }
453
454 pub fn memory(mut self, memory: Arc<dyn Memory>) -> Self {
455 self.memory = Some(memory);
456 self
457 }
458
459 pub fn tools(mut self, tools: ToolRegistry) -> Self {
463 self.tools = Some(tools);
464 self
465 }
466
467 pub fn tool(mut self, tool: Arc<dyn Tool>) -> Self {
472 let registry = self.tools.get_or_insert_with(ToolRegistry::new);
473 let _ = registry.register(tool);
474 self
475 }
476
477 pub fn extend_tools(mut self, additional: ToolRegistry) -> Self {
482 let registry = self.tools.get_or_insert_with(ToolRegistry::new);
483 for id in additional.list_ids() {
484 if registry.get(&id).is_none() {
485 if let Some(tool) = additional.get(&id) {
486 let _ = registry.register(tool);
487 }
488 }
489 }
490 self
491 }
492
493 pub fn skill(mut self, skill: SkillDefinition) -> Self {
494 self.skills.push(skill);
495 self
496 }
497
498 pub fn skills(mut self, skills: Vec<SkillDefinition>) -> Self {
499 self.skills.extend(skills);
500 self
501 }
502
503 pub fn skill_loader(mut self, loader: SkillLoader) -> Self {
504 self.skill_loader = Some(loader);
505 self
506 }
507
508 pub fn system_prompt(mut self, prompt: impl Into<String>) -> Self {
509 self.system_prompt = Some(prompt.into());
510 self
511 }
512
513 pub fn tools_prompt(mut self, prompt: impl Into<String>) -> Self {
514 self.tools_prompt = Some(prompt.into());
515 self.auto_tools_prompt = false;
516 self
517 }
518
519 pub fn auto_tools_prompt(mut self, auto: bool) -> Self {
520 self.auto_tools_prompt = auto;
521 self
522 }
523
524 pub fn max_iterations(mut self, max: u32) -> Self {
525 self.max_iterations = Some(max);
526 self
527 }
528
529 pub fn max_context_tokens(mut self, tokens: u32) -> Self {
530 self.max_context_tokens = Some(tokens);
531 self
532 }
533
534 pub fn recovery_manager(mut self, manager: RecoveryManager) -> Self {
535 self.recovery_manager = Some(manager);
536 self
537 }
538
539 pub fn tool_security(mut self, engine: ToolSecurityEngine) -> Self {
540 self.tool_security = Some(engine);
541 self
542 }
543
544 pub fn process_processor(mut self, processor: ProcessProcessor) -> Self {
545 self.process_processor = Some(processor);
546 self
547 }
548
549 pub fn message_filter(
550 mut self,
551 name: impl Into<String>,
552 filter: Arc<dyn MessageFilter>,
553 ) -> Self {
554 self.message_filters.insert(name.into(), filter);
555 self
556 }
557
558 pub fn context_manager(mut self, manager: Arc<ContextManager>) -> Self {
559 self.context_manager = Some(manager);
560 self
561 }
562
563 pub fn state_machine(mut self, machine: Arc<StateMachine>) -> Self {
564 self.state_machine = Some(machine);
565 self
566 }
567
568 pub fn transition_evaluator(mut self, evaluator: Arc<dyn TransitionEvaluator>) -> Self {
569 self.transition_evaluator = Some(evaluator);
570 self
571 }
572
573 pub fn hooks(mut self, hooks: Arc<dyn AgentHooks>) -> Self {
574 self.hooks = Some(hooks);
575 self
576 }
577
578 pub fn approval_handler(mut self, handler: Arc<dyn ApprovalHandler>) -> Self {
579 self.approval_handler = Some(handler);
580 self
581 }
582
583 pub fn hitl_engine(mut self, engine: HITLEngine) -> Self {
584 self.hitl_engine = Some(engine);
585 self
586 }
587
588 pub fn storage_config(mut self, config: StorageConfig) -> Self {
589 self.storage_config = Some(config);
590 self
591 }
592
593 pub fn storage(mut self, storage: Arc<dyn AgentStorage>) -> Self {
594 self.storage = Some(storage);
595 self
596 }
597
598 pub fn reasoning(mut self, config: ReasoningConfig) -> Self {
599 self.reasoning = Some(config);
600 self
601 }
602
603 pub fn reflection(mut self, config: ReflectionConfig) -> Self {
604 self.reflection = Some(config);
605 self
606 }
607
608 pub fn persona(mut self, manager: Arc<ai_agents_persona::PersonaManager>) -> Self {
610 self.persona_manager = Some(manager);
611 self
612 }
613
614 pub fn persona_templates(
616 mut self,
617 registry: Arc<ai_agents_persona::PersonaTemplateRegistry>,
618 ) -> Self {
619 self.persona_templates = Some(registry);
620 self
621 }
622
623 pub fn streaming(mut self, enabled: bool) -> Self {
624 let mut config = self.streaming.unwrap_or_default();
625 config.enabled = enabled;
626 self.streaming = Some(config);
627 self
628 }
629
630 pub async fn auto_configure_spawner(mut self) -> Result<Self> {
633 let spawner_config = match self.spec.as_ref().and_then(|s| s.spawner.as_ref()) {
634 Some(c) => c.clone(),
635 None => return Ok(self),
636 };
637
638 use crate::spawner::{
639 AgentRegistry, AgentSpawner,
640 config::{configure_spawner_tools, resolve_templates},
641 };
642
643 let mut spawner = AgentSpawner::new();
644
645 if spawner_config.shared_llms {
646 if let Some(ref reg) = self.llm_registry {
647 spawner = spawner.with_shared_llms(reg.clone());
648 }
649 }
650
651 if !spawner_config.shared_context.is_empty() {
652 spawner = spawner.with_shared_context_map(spawner_config.shared_context.clone());
653 }
654
655 if let Some(max) = spawner_config.max_agents {
656 spawner = spawner.with_max_agents(max);
657 }
658
659 if let Some(ref prefix) = spawner_config.name_prefix {
660 spawner = spawner.with_name_prefix(prefix.clone());
661 }
662
663 if !spawner_config.templates.is_empty() {
665 let resolved = resolve_templates(&spawner_config.templates, self.yaml_dir.as_deref())?;
666 spawner = spawner.with_templates(resolved);
667 }
668
669 if let Some(ref allowed) = spawner_config.allowed_tools {
670 spawner = spawner.with_allowed_tools(allowed.clone());
671 }
672
673 if let Some(ref sc) = spawner_config.shared_storage {
675 let converted = crate::spec::storage::to_storage_config(sc);
676 if let Some(st) = ai_agents_storage::create_storage(&converted).await? {
677 spawner = spawner.with_shared_storage(Arc::clone(&st));
678
679 let parent_has_storage = self.storage.is_some()
681 || self.storage_config.is_some()
682 || self.spec.as_ref().map_or(false, |s| s.has_storage());
683 if !parent_has_storage {
684 self.storage = Some(st);
685 }
686 }
687 }
688
689 let spawner = Arc::new(spawner);
690 let registry = Arc::new(AgentRegistry::new());
691
692 self.spawner = Some(Arc::clone(&spawner));
693 self.spawner_registry = Some(Arc::clone(®istry));
694 let llm_for_tools = Arc::new(self.llm_registry.clone().unwrap_or_default());
695 let agent_name = self
696 .spec
697 .as_ref()
698 .map(|s| s.name.clone())
699 .unwrap_or_default();
700
701 let tools = configure_spawner_tools(
702 Arc::clone(&spawner),
703 Arc::clone(®istry),
704 Arc::clone(&llm_for_tools),
705 &agent_name,
706 );
707
708 let tool_registry = self.tools.get_or_insert_with(create_builtin_registry);
709 for tool in tools {
710 let _ = tool_registry.register(tool);
711 }
712
713 tracing::info!("Spawner tools registered");
714
715 if spawner_config.orchestration_tools.is_enabled() {
717 let orch_tools = crate::orchestration::tools::configure_orchestration_tools(
718 &spawner_config.orchestration_tools,
719 Arc::clone(®istry),
720 Arc::clone(&llm_for_tools),
721 );
722 let tool_registry = self.tools.get_or_insert_with(create_builtin_registry);
723 for tool in orch_tools {
724 let _ = tool_registry.register(tool);
725 }
726 tracing::info!("Orchestration tools registered");
727 }
728
729 for entry in &spawner_config.auto_spawn {
731 let yaml_path = if let Some(ref dir) = self.yaml_dir {
732 dir.join(&entry.agent)
733 } else {
734 std::path::PathBuf::from(&entry.agent)
735 };
736
737 tracing::info!(id = %entry.id, path = %yaml_path.display(), "Auto-spawning agent");
738
739 match AgentBuilder::from_yaml_file(&yaml_path) {
740 Ok(mut sub_builder) => {
741 if spawner_config.shared_llms {
742 if let Some(ref reg) = self.llm_registry {
743 sub_builder = sub_builder.llm_registry(reg.clone());
744 }
745 }
746 match sub_builder
747 .auto_configure_llms()
748 .and_then(|b| b.auto_configure_features())
749 {
750 Ok(configured) => match configured.build() {
751 Ok(agent) => {
752 let spec_yaml =
753 std::fs::read_to_string(&yaml_path).unwrap_or_default();
754 let spec: crate::spec::AgentSpec =
755 serde_yaml::from_str(&spec_yaml).unwrap_or_default();
756
757 let spawned = crate::spawner::spawner::SpawnedAgent {
758 id: entry.id.clone(),
759 agent: Arc::new(agent),
760 spec,
761 spawned_at: chrono::Utc::now(),
762 };
763
764 if let Err(e) = registry.register(spawned).await {
765 tracing::warn!(
766 id = %entry.id,
767 error = %e,
768 "Failed to register auto-spawned agent"
769 );
770 } else {
771 tracing::info!(id = %entry.id, "Auto-spawned agent registered");
772 }
773 }
774 Err(e) => {
775 tracing::warn!(
776 id = %entry.id,
777 error = %e,
778 "Failed to build auto-spawn agent"
779 );
780 }
781 },
782 Err(e) => {
783 tracing::warn!(
784 id = %entry.id,
785 error = %e,
786 "Failed to configure auto-spawn agent"
787 );
788 }
789 }
790 }
791 Err(e) => {
792 tracing::warn!(
793 id = %entry.id,
794 path = %yaml_path.display(),
795 error = %e,
796 "Failed to load auto-spawn YAML"
797 );
798 }
799 }
800 }
801
802 if let Some(ref spec) = self.spec {
804 if let Some(ref state_config) = spec.states {
805 let refs = collect_orchestration_refs(&state_config.states);
806 let mut missing: Vec<String> = Vec::new();
807
808 for (agent_id, state_name, pattern) in &refs {
809 if !registry.contains(agent_id) {
810 missing.push(format!(
811 " - '{}' (referenced by state '{}' via {})",
812 agent_id, state_name, pattern
813 ));
814 }
815 }
816
817 if !missing.is_empty() {
818 missing.sort();
819 missing.dedup();
820 return Err(AgentError::Config(format!(
821 "Auto-spawn validation failed. These agents are referenced by \
822 orchestration states but were not successfully spawned:\n\n{}\n\n\
823 Check that agent YAML files exist and contain valid specs.",
824 missing.join("\n")
825 )));
826 }
827 }
828 }
829
830 Ok(self)
831 }
832
833 pub fn build(mut self) -> Result<RuntimeAgent> {
834 let base_prompt = self
835 .system_prompt
836 .ok_or_else(|| AgentError::Config("System prompt is required".into()))?;
837
838 let mut tools = self.tools.unwrap_or_else(ToolRegistry::new);
839
840 let system_prompt = base_prompt;
843
844 let max_iterations = self.max_iterations.unwrap_or(10);
845
846 let info = if let Some(ref spec) = self.spec {
847 AgentInfo::new(&spec.name, &spec.name, &spec.version)
848 .with_description(spec.description.clone().unwrap_or_default())
849 } else {
850 AgentInfo::new("agent", "Agent", "1.0.0")
851 };
852
853 if let Some(ref spec) = self.spec {
854 if !spec.skills.is_empty() {
855 let mut loader = self.skill_loader.take().unwrap_or_else(SkillLoader::new);
856 if let Some(ref dir) = self.yaml_dir {
857 loader.set_base_dir(dir);
858 }
859 let loaded_skills = loader.load_refs(&spec.skills)?;
860 self.skills.extend(loaded_skills);
861 }
862 }
863
864 let mut llm_registry = self.llm_registry.unwrap_or_else(LLMRegistry::new);
865
866 if let Some(llm) = self.llm {
867 if !llm_registry.has("default") {
868 llm_registry.register("default", llm.clone());
869 }
870 }
871
872 if let Some(ref spec) = self.spec {
873 let default_alias = spec.llm.get_default_alias();
874 let router_alias = spec.llm.get_router_alias();
875
876 llm_registry.set_default(&default_alias);
877 if let Some(router) = router_alias {
878 llm_registry.set_router(&router);
879 }
880 }
881
882 if llm_registry.is_empty() {
883 return Err(AgentError::Config(
884 "At least one LLM provider is required".into(),
885 ));
886 }
887
888 let memory = self.memory.unwrap_or_else(|| {
890 if let Some(ref spec) = self.spec {
891 if spec.memory.is_compacting() {
892 let summarizer_llm = spec
893 .memory
894 .summarizer_llm
895 .as_ref()
896 .and_then(|alias| llm_registry.get(alias).ok())
897 .or_else(|| llm_registry.router().ok())
898 .or_else(|| llm_registry.default().ok());
899
900 let summarizer: Arc<dyn Summarizer> = match summarizer_llm {
901 Some(llm) => Arc::new(LLMSummarizer::new(llm)),
902 None => Arc::new(NoopSummarizer),
903 };
904 let config = spec.memory.to_compacting_config();
905 return Arc::new(CompactingMemory::new(summarizer, config));
906 }
907 Arc::new(InMemoryStore::new(spec.memory.max_messages))
908 } else {
909 Arc::new(InMemoryStore::new(100))
910 }
911 });
912
913 let persona_manager: Option<Arc<ai_agents_persona::PersonaManager>> =
915 if let Some(pm) = self.persona_manager.take() {
916 Some(pm)
917 } else if let Some(ref spec) = self.spec {
918 if spec.has_persona() {
919 let persona_config = spec.persona.clone().unwrap();
920 let renderer = ai_agents_context::TemplateRenderer::new();
921 let registry = self.persona_templates.clone();
922 let manager = ai_agents_persona::PersonaManager::from_config(
923 persona_config,
924 registry,
925 renderer,
926 )
927 .map_err(|e| {
928 AgentError::Config(format!("Failed to create PersonaManager: {}", e))
929 })?;
930 Some(Arc::new(manager))
931 } else {
932 None
933 }
934 } else {
935 None
936 };
937
938 if let Some(ref pm) = persona_manager {
940 if pm.should_register_evolve_tool() {
941 let evolve_tool = ai_agents_persona::PersonaEvolveTool::new(pm.clone());
942 let _ = tools.register(Arc::new(evolve_tool));
943 }
944 }
945
946 let tools_arc = Arc::new(tools);
947 let llm_registry_arc = Arc::new(llm_registry);
948
949 let declared_tool_ids: Option<Vec<String>> = self.spec.as_ref().and_then(|s| {
954 s.tools.as_ref().map(|tools| {
955 let mut ids: Vec<String> = tools.iter().map(|t| t.name().to_string()).collect();
956
957 for entry in tools {
959 if let Some(mcp_config) = entry.to_mcp_config() {
960 for view_name in mcp_config.views.keys() {
961 ids.push(view_name.clone());
962 }
963 }
964 }
965
966 if persona_manager
969 .as_ref()
970 .is_some_and(|pm| pm.should_register_evolve_tool())
971 {
972 ids.push("persona_evolve".to_string());
973 }
974
975 ids
976 })
977 });
978
979 if let Some(ref ids) = declared_tool_ids {
983 let mcp_names: Vec<String> = self
984 .spec
985 .as_ref()
986 .and_then(|s| s.tools.as_ref())
987 .map(|tools| {
988 let mut names = Vec::new();
989 for entry in tools {
990 if entry.is_mcp() {
991 names.push(entry.name().to_string());
992 if let Some(cfg) = entry.to_mcp_config() {
993 names.extend(cfg.views.keys().cloned());
994 }
995 }
996 }
997 names
998 })
999 .unwrap_or_default();
1000
1001 let missing: Vec<&str> = ids
1002 .iter()
1003 .filter(|id| !mcp_names.contains(id))
1004 .filter(|id| tools_arc.get(id).is_none())
1005 .map(|s| s.as_str())
1006 .collect();
1007
1008 if !missing.is_empty() {
1009 return Err(AgentError::Config(format!(
1010 "Tools declared in YAML but not registered: [{}]. \
1011 Register them via .tool(Arc::new(...)) before .build(), \
1012 or remove them from the YAML tools: list.",
1013 missing.join(", ")
1014 )));
1015 }
1016 }
1017
1018 let mut agent = RuntimeAgent::new(
1019 info,
1020 llm_registry_arc.clone(),
1021 memory,
1022 tools_arc,
1023 self.skills,
1024 system_prompt,
1025 max_iterations,
1026 )
1027 .with_declared_tool_ids(declared_tool_ids);
1028
1029 if let Some(tokens) = self.max_context_tokens {
1030 agent = agent.with_max_context_tokens(tokens);
1031 }
1032
1033 if let Some(manager) = self.recovery_manager {
1034 agent = agent.with_recovery_manager(manager);
1035 } else if let Some(ref spec) = self.spec {
1036 agent = agent.with_recovery_manager(RecoveryManager::new(spec.error_recovery.clone()));
1037 }
1038
1039 if let Some(engine) = self.tool_security {
1040 agent = agent.with_tool_security(engine);
1041 } else if let Some(ref spec) = self.spec {
1042 agent = agent.with_tool_security(ToolSecurityEngine::new(spec.tool_security.clone()));
1043 }
1044
1045 if let Some(processor) = self.process_processor {
1046 agent = agent.with_process_processor(processor);
1047 } else if let Some(ref spec) = self.spec {
1048 if spec.has_process() {
1049 let processor = ProcessProcessor::new(spec.process.clone())
1050 .with_llm_registry(llm_registry_arc.clone());
1051 agent = agent.with_process_processor(processor);
1052 }
1053 }
1054
1055 for (name, filter) in self.message_filters {
1056 agent.register_message_filter(name, filter);
1057 }
1058
1059 if let Some(state_machine) = self.state_machine {
1061 let evaluator = self.transition_evaluator.unwrap_or_else(|| {
1062 let eval_llm = llm_registry_arc
1063 .get("evaluator")
1064 .or_else(|_| llm_registry_arc.router())
1065 .or_else(|_| llm_registry_arc.default())
1066 .expect("At least one LLM required for transition evaluator");
1067 Arc::new(LLMTransitionEvaluator::new(eval_llm))
1068 });
1069 agent = agent.with_state_machine(state_machine, evaluator);
1070 } else if let Some(ref spec) = self.spec {
1071 if let Some(ref state_config) = spec.states {
1072 let state_machine = StateMachine::new(state_config.clone())?;
1073 let evaluator = self.transition_evaluator.unwrap_or_else(|| {
1074 let eval_llm = llm_registry_arc
1075 .get("evaluator")
1076 .or_else(|_| llm_registry_arc.router())
1077 .or_else(|_| llm_registry_arc.default())
1078 .expect("At least one LLM required for transition evaluator");
1079 Arc::new(LLMTransitionEvaluator::new(eval_llm))
1080 });
1081 agent = agent.with_state_machine(Arc::new(state_machine), evaluator);
1082 }
1083 }
1084
1085 if let Some(context_manager) = self.context_manager {
1087 agent = agent.with_context_manager(context_manager);
1088 } else if let Some(ref spec) = self.spec {
1089 if !spec.context.is_empty() {
1090 let context_manager = ContextManager::new(
1091 spec.context.clone(),
1092 spec.name.clone(),
1093 spec.version.clone(),
1094 );
1095 agent = agent.with_context_manager(Arc::new(context_manager));
1096 }
1097 }
1098
1099 if let Some(ref spec) = self.spec {
1101 agent = agent.with_parallel_tools(spec.parallel_tools.clone());
1102 let streaming_config = self
1103 .streaming
1104 .clone()
1105 .unwrap_or_else(|| spec.streaming.clone());
1106 agent = agent.with_streaming(streaming_config);
1107
1108 if let Some(ref budget) = spec.memory.token_budget {
1110 agent = agent.with_memory_token_budget(budget.clone());
1111 }
1112
1113 if self.storage_config.is_none() && spec.has_storage() {
1115 agent = agent.with_storage_config(spec.storage.clone());
1116 }
1117 }
1118
1119 if let Some(storage_config) = self.storage_config {
1121 agent = agent.with_storage_config(storage_config);
1122 }
1123 if let Some(storage) = self.storage {
1124 agent = agent.with_storage(storage);
1125 }
1126
1127 if let (Some(spawner), Some(registry)) = (self.spawner, self.spawner_registry) {
1129 agent = agent.with_spawner_handles(spawner, registry);
1130 }
1131
1132 if let Some(hooks) = self.hooks {
1134 agent = agent.with_hooks(hooks);
1135 }
1136
1137 if let Some(hitl_engine) = self.hitl_engine {
1139 let handler = self
1140 .approval_handler
1141 .unwrap_or_else(|| Arc::new(RejectAllHandler::new()));
1142 agent = agent.with_hitl(hitl_engine, handler);
1143 } else if let Some(ref spec) = self.spec {
1144 if let Some(ref hitl_config) = spec.hitl {
1145 let hitl_engine = HITLEngine::new(hitl_config.clone());
1146 let handler = self
1147 .approval_handler
1148 .unwrap_or_else(|| Arc::new(RejectAllHandler::new()));
1149 agent = agent.with_hitl(hitl_engine, handler);
1150 }
1151 }
1152
1153 if let Some(reasoning) = self.reasoning {
1154 agent = agent.with_reasoning(reasoning);
1155 } else if let Some(ref spec) = self.spec {
1156 agent = agent.with_reasoning(spec.reasoning.clone());
1157 }
1158
1159 if let Some(reflection) = self.reflection {
1160 agent = agent.with_reflection(reflection);
1161 } else if let Some(ref spec) = self.spec {
1162 agent = agent.with_reflection(spec.reflection.clone());
1163 }
1164
1165 if let Some(ref spec) = self.spec {
1167 if spec.disambiguation.is_enabled() {
1168 agent = agent.with_disambiguation(spec.disambiguation.clone());
1169 }
1170 }
1171
1172 if let Some(pm) = persona_manager {
1174 agent = agent.with_persona(pm);
1175 }
1176
1177 Ok(agent)
1178 }
1179}
1180
1181fn collect_orchestration_refs(
1183 states: &std::collections::HashMap<String, ai_agents_state::StateDefinition>,
1184) -> Vec<(String, String, &'static str)> {
1185 let mut refs = Vec::new();
1186
1187 for (state_name, def) in states {
1188 if let Some(ref delegate_id) = def.delegate {
1189 refs.push((delegate_id.clone(), state_name.clone(), "delegate"));
1190 }
1191 if let Some(ref concurrent) = def.concurrent {
1192 for agent_ref in &concurrent.agents {
1193 refs.push((agent_ref.id().to_string(), state_name.clone(), "concurrent"));
1194 }
1195 }
1196 if let Some(ref gc) = def.group_chat {
1197 for participant in &gc.participants {
1198 refs.push((participant.id.clone(), state_name.clone(), "group_chat"));
1199 }
1200 }
1201 if let Some(ref pipeline) = def.pipeline {
1202 for stage in &pipeline.stages {
1203 refs.push((stage.id().to_string(), state_name.clone(), "pipeline"));
1204 }
1205 }
1206 if let Some(ref handoff) = def.handoff {
1207 refs.push((handoff.initial_agent.clone(), state_name.clone(), "handoff"));
1208 for agent_id in &handoff.available_agents {
1209 refs.push((agent_id.clone(), state_name.clone(), "handoff"));
1210 }
1211 }
1212
1213 if let Some(ref sub_states) = def.states {
1215 refs.extend(collect_orchestration_refs(sub_states));
1216 }
1217 }
1218
1219 refs
1220}
1221
1222impl Default for AgentBuilder {
1223 fn default() -> Self {
1224 Self::new()
1225 }
1226}
1227
1228#[cfg(test)]
1229mod tests {
1230 use super::*;
1231
1232 #[test]
1233 fn test_builder_new() {
1234 let builder = AgentBuilder::new();
1235 assert!(builder.spec.is_none());
1236 assert!(builder.system_prompt.is_none());
1237 }
1238
1239 #[test]
1240 fn test_builder_from_yaml() {
1241 let yaml = r#"
1242name: TestAgent
1243system_prompt: "You are helpful."
1244llm:
1245 provider: openai
1246 model: gpt-4
1247"#;
1248 let builder = AgentBuilder::from_yaml(yaml).unwrap();
1249 assert!(builder.spec.is_some());
1250 assert_eq!(builder.spec.as_ref().unwrap().name, "TestAgent");
1251 }
1252
1253 #[test]
1254 fn test_builder_from_yaml_with_tool_security() {
1255 let yaml = r#"
1256name: SecureAgent
1257system_prompt: "You are helpful."
1258llm:
1259 provider: openai
1260 model: gpt-4
1261max_context_tokens: 8192
1262error_recovery:
1263 default:
1264 max_retries: 5
1265tool_security:
1266 enabled: true
1267 tools:
1268 http:
1269 rate_limit: 10
1270"#;
1271 let builder = AgentBuilder::from_yaml(yaml).unwrap();
1272 assert!(builder.spec.is_some());
1273 let spec = builder.spec.as_ref().unwrap();
1274 assert_eq!(spec.max_context_tokens, 8192);
1275 assert_eq!(spec.error_recovery.default.max_retries, 5);
1276 assert!(spec.tool_security.enabled);
1277 }
1278
1279 #[test]
1280 fn test_builder_from_yaml_with_skills() {
1281 let yaml = r#"
1282name: SkillAgent
1283system_prompt: "You are helpful."
1284llm:
1285 provider: openai
1286 model: gpt-4
1287skills:
1288 - id: greeting
1289 description: "Greet users"
1290 trigger: "When user says hello"
1291 steps:
1292 - prompt: "Hello!"
1293"#;
1294 let builder = AgentBuilder::from_yaml(yaml).unwrap();
1295 assert!(builder.spec.is_some());
1296 assert!(!builder.spec.as_ref().unwrap().skills.is_empty());
1297 }
1298
1299 #[test]
1300 fn test_builder_from_spec() {
1301 let spec = AgentSpec {
1302 name: "test".to_string(),
1303 version: "1.0".to_string(),
1304 description: Some("Test agent".to_string()),
1305 system_prompt: "You are helpful".to_string(),
1306 ..Default::default()
1307 };
1308
1309 let builder = AgentBuilder::from_spec(spec);
1310 assert!(builder.spec.is_some());
1311 assert_eq!(builder.system_prompt, Some("You are helpful".to_string()));
1312 }
1313
1314 #[test]
1315 fn test_builder_chain() {
1316 let builder = AgentBuilder::new()
1317 .system_prompt("Test prompt")
1318 .max_iterations(5)
1319 .max_context_tokens(4096);
1320
1321 assert_eq!(builder.system_prompt, Some("Test prompt".to_string()));
1322 assert_eq!(builder.max_iterations, Some(5));
1323 assert_eq!(builder.max_context_tokens, Some(4096));
1324 }
1325
1326 #[test]
1327 fn test_builder_skills() {
1328 use ai_agents_skills::{SkillDefinition, SkillStep};
1329
1330 let skill = SkillDefinition {
1331 id: "test".to_string(),
1332 description: "Test skill".to_string(),
1333 trigger: "When testing".to_string(),
1334 steps: vec![SkillStep::Prompt {
1335 prompt: "Hello".to_string(),
1336 llm: None,
1337 }],
1338 reasoning: None,
1339 reflection: None,
1340 disambiguation: None,
1341 };
1342
1343 let builder = AgentBuilder::new().skill(skill.clone()).skills(vec![skill]);
1344
1345 assert_eq!(builder.skills.len(), 2);
1346 }
1347
1348 #[test]
1349 fn test_builder_from_yaml_with_states() {
1350 let yaml = r#"
1351name: StatefulAgent
1352system_prompt: "You are helpful."
1353llm:
1354 provider: openai
1355 model: gpt-4
1356states:
1357 initial: greeting
1358 states:
1359 greeting:
1360 prompt: "Welcome!"
1361 transitions:
1362 - to: support
1363 when: "user needs help"
1364 support:
1365 prompt: "How can I help?"
1366"#;
1367 let builder = AgentBuilder::from_yaml(yaml).unwrap();
1368 assert!(builder.spec.is_some());
1369 let spec = builder.spec.as_ref().unwrap();
1370 assert!(spec.has_states());
1371 assert!(spec.states.is_some());
1372 let states = spec.states.as_ref().unwrap();
1373 assert_eq!(states.initial, "greeting");
1374 assert_eq!(states.states.len(), 2);
1375 }
1376
1377 #[test]
1378 fn test_builder_from_yaml_with_context() {
1379 let yaml = r#"
1380name: ContextAgent
1381system_prompt: "Hello, {{ context.user.name }}!"
1382llm:
1383 provider: openai
1384 model: gpt-4
1385context:
1386 user:
1387 type: runtime
1388 required: true
1389 time:
1390 type: builtin
1391 source: datetime
1392 refresh: per_turn
1393"#;
1394 let builder = AgentBuilder::from_yaml(yaml).unwrap();
1395 assert!(builder.spec.is_some());
1396 let spec = builder.spec.as_ref().unwrap();
1397 assert!(spec.has_context());
1398 assert_eq!(spec.context.len(), 2);
1399 assert!(spec.context.contains_key("user"));
1400 assert!(spec.context.contains_key("time"));
1401 }
1402
1403 #[test]
1404 fn test_builder_from_yaml_with_full_v04_features() {
1405 let yaml = r#"
1406name: FullFeaturedAgent
1407version: "0.4.0"
1408system_prompt: |
1409 You are a helpful assistant.
1410 User: {{ context.user.name }}
1411 Language: {{ context.user.language }}
1412llm:
1413 provider: openai
1414 model: gpt-4
1415context:
1416 user:
1417 type: runtime
1418 required: true
1419 default:
1420 name: "Guest"
1421 language: "en"
1422 time:
1423 type: builtin
1424 source: datetime
1425 refresh: per_turn
1426states:
1427 initial: greeting
1428 states:
1429 greeting:
1430 prompt: "Welcome to our service!"
1431 prompt_mode: append
1432 transitions:
1433 - to: support
1434 when: "user needs help"
1435 auto: true
1436 priority: 10
1437 support:
1438 prompt: "I'm here to help you."
1439 max_turns: 5
1440 timeout_to: escalation
1441 transitions:
1442 - to: closing
1443 when: "issue resolved"
1444 auto: true
1445 escalation:
1446 prompt: "Let me connect you with a human agent."
1447 closing:
1448 prompt: "Thank you for using our service!"
1449"#;
1450 let builder = AgentBuilder::from_yaml(yaml).unwrap();
1451 assert!(builder.spec.is_some());
1452 let spec = builder.spec.as_ref().unwrap();
1453
1454 assert!(spec.has_context());
1456 assert_eq!(spec.context.len(), 2);
1457
1458 assert!(spec.has_states());
1460 let states = spec.states.as_ref().unwrap();
1461 assert_eq!(states.initial, "greeting");
1462 assert_eq!(states.states.len(), 4);
1463
1464 let greeting = states.states.get("greeting").unwrap();
1466 assert!(greeting.prompt.is_some());
1467 assert_eq!(greeting.transitions.len(), 1);
1468 assert_eq!(greeting.transitions[0].to, "support");
1469 assert!(greeting.transitions[0].auto);
1470
1471 let support = states.states.get("support").unwrap();
1473 assert_eq!(support.max_turns, Some(5));
1474 assert_eq!(support.timeout_to, Some("escalation".to_string()));
1475 }
1476
1477 #[test]
1478 fn test_builder_from_yaml_with_hitl() {
1479 let yaml = r#"
1480name: HITLAgent
1481system_prompt: "You are a secure assistant."
1482llm:
1483 provider: openai
1484 model: gpt-4
1485hitl:
1486 default_timeout_seconds: 600
1487 on_timeout: reject
1488 tools:
1489 send_payment:
1490 require_approval: true
1491 approval_context:
1492 - amount
1493 - recipient
1494 approval_message: "Approve payment?"
1495 delete_record:
1496 require_approval: true
1497 conditions:
1498 - name: high_value
1499 when: "amount > 1000"
1500 require_approval: true
1501 approval_message: "High value transaction"
1502 states:
1503 escalation:
1504 on_enter: require_approval
1505 approval_message: "Escalate to human?"
1506"#;
1507 let builder = AgentBuilder::from_yaml(yaml).unwrap();
1508 assert!(builder.spec.is_some());
1509 let spec = builder.spec.as_ref().unwrap();
1510
1511 assert!(spec.has_hitl());
1512 let hitl = spec.hitl.as_ref().unwrap();
1513 assert_eq!(hitl.default_timeout_seconds, 600);
1514 assert_eq!(hitl.tools.len(), 2);
1515 assert!(hitl.tools.get("send_payment").unwrap().require_approval);
1516 assert_eq!(hitl.conditions.len(), 1);
1517 assert_eq!(hitl.conditions[0].name, "high_value");
1518 assert_eq!(hitl.states.len(), 1);
1519 }
1520
1521 #[test]
1522 fn test_builder_from_yaml_with_compacting_memory() {
1523 let yaml = r#"
1524name: CompactingAgent
1525system_prompt: "You are a helpful assistant."
1526memory:
1527 type: compacting
1528 max_messages: 100
1529 max_recent_messages: 20
1530 compress_threshold: 15
1531 summarize_batch_size: 5
1532 summarizer_llm: router
1533llm:
1534 provider: openai
1535 model: gpt-4
1536"#;
1537 let builder = AgentBuilder::from_yaml(yaml).unwrap();
1538 assert!(builder.spec.is_some());
1539 let spec = builder.spec.as_ref().unwrap();
1540
1541 assert!(spec.memory.is_compacting());
1542 assert_eq!(spec.memory.max_recent_messages, Some(20));
1543 assert_eq!(spec.memory.compress_threshold, Some(15));
1544 assert_eq!(spec.memory.summarize_batch_size, Some(5));
1545 assert_eq!(spec.memory.summarizer_llm, Some("router".to_string()));
1546
1547 let compacting_config = spec.memory.to_compacting_config();
1548 assert_eq!(compacting_config.max_recent_messages, 20);
1549 assert_eq!(compacting_config.compress_threshold, 15);
1550 assert_eq!(compacting_config.summarize_batch_size, 5);
1551 }
1552
1553 #[test]
1554 fn test_builder_from_yaml_with_token_budget() {
1555 let yaml = r#"
1556name: BudgetAgent
1557system_prompt: "You are a helpful assistant."
1558memory:
1559 type: compacting
1560 max_messages: 100
1561 token_budget:
1562 total: 8192
1563 allocation:
1564 summary: 2048
1565 recent_messages: 4096
1566 facts: 1024
1567 overflow_strategy: summarize_more
1568 warn_at_percent: 75
1569llm:
1570 provider: openai
1571 model: gpt-4
1572"#;
1573 let builder = AgentBuilder::from_yaml(yaml).unwrap();
1574 assert!(builder.spec.is_some());
1575 let spec = builder.spec.as_ref().unwrap();
1576
1577 assert!(spec.memory.token_budget.is_some());
1578 let budget = spec.memory.token_budget.as_ref().unwrap();
1579 assert_eq!(budget.total, 8192);
1580 assert_eq!(budget.allocation.summary, 2048);
1581 assert_eq!(budget.allocation.recent_messages, 4096);
1582 assert_eq!(budget.allocation.facts, 1024);
1583 assert_eq!(budget.warn_at_percent, 75);
1584 }
1585
1586 #[test]
1587 fn test_builder_from_yaml_with_overflow_strategies() {
1588 use ai_agents_memory::OverflowStrategy;
1589
1590 let yaml = r#"
1591name: TruncateAgent
1592system_prompt: "You are helpful."
1593memory:
1594 type: compacting
1595 token_budget:
1596 total: 4096
1597 overflow_strategy: truncate_oldest
1598llm:
1599 provider: openai
1600 model: gpt-4
1601"#;
1602 let builder = AgentBuilder::from_yaml(yaml).unwrap();
1603 let budget = builder
1604 .spec
1605 .as_ref()
1606 .unwrap()
1607 .memory
1608 .token_budget
1609 .as_ref()
1610 .unwrap();
1611 assert_eq!(budget.overflow_strategy, OverflowStrategy::TruncateOldest);
1612
1613 let yaml = r#"
1614name: ErrorAgent
1615system_prompt: "You are helpful."
1616memory:
1617 type: compacting
1618 token_budget:
1619 total: 4096
1620 overflow_strategy: error
1621llm:
1622 provider: openai
1623 model: gpt-4
1624"#;
1625 let builder = AgentBuilder::from_yaml(yaml).unwrap();
1626 let budget = builder
1627 .spec
1628 .as_ref()
1629 .unwrap()
1630 .memory
1631 .token_budget
1632 .as_ref()
1633 .unwrap();
1634 assert_eq!(budget.overflow_strategy, OverflowStrategy::Error);
1635 }
1636
1637 #[test]
1638 fn test_builder_from_yaml_with_storage_file() {
1639 let yaml = r#"
1640name: PersistentAgent
1641system_prompt: "You are helpful."
1642llm:
1643 provider: openai
1644 model: gpt-4
1645storage:
1646 type: file
1647 path: "./data/sessions"
1648"#;
1649 let builder = AgentBuilder::from_yaml(yaml).unwrap();
1650 let spec = builder.spec.as_ref().unwrap();
1651 assert!(spec.has_storage());
1652 assert!(spec.storage.is_file());
1653 assert_eq!(spec.storage.get_path(), Some("./data/sessions"));
1654 }
1655
1656 #[test]
1657 fn test_builder_from_yaml_with_storage_sqlite() {
1658 let yaml = r#"
1659name: PersistentAgent
1660system_prompt: "You are helpful."
1661llm:
1662 provider: openai
1663 model: gpt-4
1664storage:
1665 type: sqlite
1666 path: "./data/sessions.db"
1667"#;
1668 let builder = AgentBuilder::from_yaml(yaml).unwrap();
1669 let spec = builder.spec.as_ref().unwrap();
1670 assert!(spec.has_storage());
1671 assert!(spec.storage.is_sqlite());
1672 }
1673
1674 #[test]
1675 fn test_builder_from_yaml_with_storage_redis() {
1676 let yaml = r#"
1677name: PersistentAgent
1678system_prompt: "You are helpful."
1679llm:
1680 provider: openai
1681 model: gpt-4
1682storage:
1683 type: redis
1684 url: "redis://localhost:6379"
1685 prefix: "myagent:"
1686 ttl_seconds: 86400
1687"#;
1688 let builder = AgentBuilder::from_yaml(yaml).unwrap();
1689 let spec = builder.spec.as_ref().unwrap();
1690 assert!(spec.has_storage());
1691 assert!(spec.storage.is_redis());
1692 assert_eq!(spec.storage.get_prefix(), "myagent:");
1693 assert_eq!(spec.storage.get_ttl(), Some(86400));
1694 }
1695
1696 #[test]
1697 fn test_builder_no_storage_by_default() {
1698 let yaml = r#"
1699name: SimpleAgent
1700system_prompt: "You are helpful."
1701llm:
1702 provider: openai
1703 model: gpt-4
1704"#;
1705 let builder = AgentBuilder::from_yaml(yaml).unwrap();
1706 let spec = builder.spec.as_ref().unwrap();
1707 assert!(!spec.has_storage());
1708 }
1709
1710 #[test]
1711 fn test_build_fails_on_missing_declared_tool() {
1712 use ai_agents_llm::mock::MockLLMProvider;
1713
1714 let yaml = r#"
1715name: ToolAgent
1716system_prompt: "You are helpful."
1717llm:
1718 provider: openai
1719 model: gpt-4
1720tools:
1721 - name: lookup_order
1722 - name: calculator
1723"#;
1724 let llm = Arc::new(MockLLMProvider::new("test"));
1725 let result = AgentBuilder::from_yaml(yaml)
1726 .unwrap()
1727 .llm(llm)
1728 .auto_configure_features()
1729 .unwrap()
1730 .build();
1732
1733 assert!(result.is_err());
1734 let err = result.unwrap_err().to_string();
1735 assert!(
1736 err.contains("lookup_order"),
1737 "error should name the missing tool: {}",
1738 err
1739 );
1740 assert!(
1742 !err.contains("calculator"),
1743 "calculator is registered, should not be missing: {}",
1744 err
1745 );
1746 }
1747
1748 #[test]
1749 fn test_build_succeeds_when_declared_tool_is_registered() {
1750 use ai_agents_core::Tool;
1751 use ai_agents_llm::mock::MockLLMProvider;
1752
1753 struct FakeTool;
1754 #[async_trait::async_trait]
1755 impl Tool for FakeTool {
1756 fn id(&self) -> &str {
1757 "lookup_order"
1758 }
1759 fn name(&self) -> &str {
1760 "Order Lookup"
1761 }
1762 fn description(&self) -> &str {
1763 "Look up an order"
1764 }
1765 fn input_schema(&self) -> serde_json::Value {
1766 serde_json::json!({})
1767 }
1768 async fn execute(&self, _args: serde_json::Value) -> ai_agents_core::ToolResult {
1769 ai_agents_core::ToolResult::ok("ok")
1770 }
1771 }
1772
1773 let yaml = r#"
1774name: ToolAgent
1775system_prompt: "You are helpful."
1776llm:
1777 provider: openai
1778 model: gpt-4
1779tools:
1780 - name: lookup_order
1781 - name: calculator
1782"#;
1783 let llm = Arc::new(MockLLMProvider::new("test"));
1784 let result = AgentBuilder::from_yaml(yaml)
1785 .unwrap()
1786 .llm(llm)
1787 .auto_configure_features()
1788 .unwrap()
1789 .tool(Arc::new(FakeTool))
1790 .build();
1791
1792 assert!(
1793 result.is_ok(),
1794 "build should succeed when all declared tools are registered: {:?}",
1795 result.err()
1796 );
1797 }
1798
1799 #[test]
1800 fn test_spawner_config_deserializes_shared_storage() {
1801 let yaml = r#"
1802name: TestAgent
1803system_prompt: "Test"
1804llm:
1805 provider: openai
1806 model: gpt-4
1807spawner:
1808 shared_llms: true
1809 shared_storage:
1810 type: sqlite
1811 path: ./data/test.db
1812 max_agents: 5
1813"#;
1814 let builder = AgentBuilder::from_yaml(yaml).unwrap();
1815 let spec = builder.spec.as_ref().unwrap();
1816 let sc = spec.spawner.as_ref().unwrap();
1817 assert!(sc.shared_storage.is_some());
1818 assert!(sc.shared_storage.as_ref().unwrap().is_sqlite());
1819 }
1820
1821 #[test]
1822 fn test_build_succeeds_with_no_tools_declared() {
1823 use ai_agents_llm::mock::MockLLMProvider;
1824
1825 let yaml = r#"
1826name: SimpleAgent
1827system_prompt: "You are helpful."
1828llm:
1829 provider: openai
1830 model: gpt-4
1831"#;
1832 let llm = Arc::new(MockLLMProvider::new("test"));
1833 let result = AgentBuilder::from_yaml(yaml)
1834 .unwrap()
1835 .llm(llm)
1836 .auto_configure_features()
1837 .unwrap()
1838 .build();
1839
1840 assert!(
1841 result.is_ok(),
1842 "no tools: section means no validation needed: {:?}",
1843 result.err()
1844 );
1845 }
1846}