1use arc_swap::ArcSwap;
35use notify::{Event, RecommendedWatcher, RecursiveMode, Watcher};
36use serde::{Deserialize, Serialize};
37use std::collections::HashMap;
38use std::fs;
39use std::path::{Path, PathBuf};
40use std::sync::Arc;
41use toon_format::{decode_default, encode_default, ToonError};
42use tracing::{debug, error, info, warn};
43
44#[derive(Debug, Clone, Serialize, Deserialize, PartialEq)]
54pub struct ToonAgentConfig {
55 pub name: String,
57
58 pub model: String,
60
61 #[serde(default)]
63 pub system_prompt: Option<String>,
64
65 #[serde(default)]
67 pub tools: Vec<String>,
68
69 #[serde(default = "default_max_tool_iterations")]
71 pub max_tool_iterations: usize,
72
73 #[serde(default)]
75 pub parallel_tools: bool,
76
77 #[serde(flatten)]
79 pub extra: HashMap<String, serde_json::Value>,
80}
81
82fn default_max_tool_iterations() -> usize {
83 10
84}
85
86impl ToonAgentConfig {
87 pub fn new(name: impl Into<String>, model: impl Into<String>) -> Self {
89 Self {
90 name: name.into(),
91 model: model.into(),
92 system_prompt: None,
93 tools: Vec::new(),
94 max_tool_iterations: default_max_tool_iterations(),
95 parallel_tools: false,
96 extra: HashMap::new(),
97 }
98 }
99
100 pub fn with_system_prompt(mut self, prompt: impl Into<String>) -> Self {
102 self.system_prompt = Some(prompt.into());
103 self
104 }
105
106 pub fn with_tools(mut self, tools: Vec<String>) -> Self {
108 self.tools = tools;
109 self
110 }
111
112 pub fn to_toon(&self) -> Result<String, ToonConfigError> {
114 encode_default(self).map_err(ToonConfigError::from)
115 }
116
117 pub fn from_toon(toon: &str) -> Result<Self, ToonConfigError> {
119 decode_default(toon).map_err(ToonConfigError::from)
120 }
121}
122
123#[derive(Debug, Clone, Serialize, Deserialize, PartialEq)]
130pub struct ToonModelConfig {
131 pub name: String,
133
134 pub provider: String,
136
137 pub model: String,
139
140 #[serde(default = "default_temperature")]
142 pub temperature: f32,
143
144 #[serde(default = "default_max_tokens")]
146 pub max_tokens: u32,
147
148 #[serde(default)]
150 pub top_p: Option<f32>,
151
152 #[serde(default)]
154 pub frequency_penalty: Option<f32>,
155
156 #[serde(default)]
158 pub presence_penalty: Option<f32>,
159}
160
161fn default_temperature() -> f32 {
162 0.7
163}
164
165fn default_max_tokens() -> u32 {
166 512
167}
168
169impl ToonModelConfig {
170 pub fn new(
172 name: impl Into<String>,
173 provider: impl Into<String>,
174 model: impl Into<String>,
175 ) -> Self {
176 Self {
177 name: name.into(),
178 provider: provider.into(),
179 model: model.into(),
180 temperature: default_temperature(),
181 max_tokens: default_max_tokens(),
182 top_p: None,
183 frequency_penalty: None,
184 presence_penalty: None,
185 }
186 }
187
188 pub fn to_toon(&self) -> Result<String, ToonConfigError> {
190 encode_default(self).map_err(ToonConfigError::from)
191 }
192
193 pub fn from_toon(toon: &str) -> Result<Self, ToonConfigError> {
195 decode_default(toon).map_err(ToonConfigError::from)
196 }
197}
198
199#[derive(Debug, Clone, Serialize, Deserialize, PartialEq)]
205pub struct ToonToolConfig {
206 pub name: String,
208
209 #[serde(default = "default_true")]
211 pub enabled: bool,
212
213 #[serde(default)]
215 pub description: Option<String>,
216
217 #[serde(default = "default_timeout")]
219 pub timeout_secs: u64,
220
221 #[serde(flatten)]
223 pub extra: HashMap<String, serde_json::Value>,
224}
225
226fn default_true() -> bool {
227 true
228}
229
230fn default_timeout() -> u64 {
231 30
232}
233
234impl ToonToolConfig {
235 pub fn new(name: impl Into<String>) -> Self {
237 Self {
238 name: name.into(),
239 enabled: default_true(),
240 description: None,
241 timeout_secs: default_timeout(),
242 extra: HashMap::new(),
243 }
244 }
245
246 pub fn to_toon(&self) -> Result<String, ToonConfigError> {
248 encode_default(self).map_err(ToonConfigError::from)
249 }
250
251 pub fn from_toon(toon: &str) -> Result<Self, ToonConfigError> {
253 decode_default(toon).map_err(ToonConfigError::from)
254 }
255}
256
257#[derive(Debug, Clone, Serialize, Deserialize, PartialEq)]
264pub struct ToonWorkflowConfig {
265 pub name: String,
267
268 pub entry_agent: String,
270
271 #[serde(default)]
273 pub fallback_agent: Option<String>,
274
275 #[serde(default = "default_max_depth")]
277 pub max_depth: u8,
278
279 #[serde(default = "default_max_iterations")]
281 pub max_iterations: u8,
282
283 #[serde(default)]
285 pub parallel_subagents: bool,
286}
287
288fn default_max_depth() -> u8 {
289 3
290}
291
292fn default_max_iterations() -> u8 {
293 5
294}
295
296impl ToonWorkflowConfig {
297 pub fn new(name: impl Into<String>, entry_agent: impl Into<String>) -> Self {
299 Self {
300 name: name.into(),
301 entry_agent: entry_agent.into(),
302 fallback_agent: None,
303 max_depth: default_max_depth(),
304 max_iterations: default_max_iterations(),
305 parallel_subagents: false,
306 }
307 }
308
309 pub fn to_toon(&self) -> Result<String, ToonConfigError> {
311 encode_default(self).map_err(ToonConfigError::from)
312 }
313
314 pub fn from_toon(toon: &str) -> Result<Self, ToonConfigError> {
316 decode_default(toon).map_err(ToonConfigError::from)
317 }
318}
319
320#[derive(Debug, Clone, Serialize, Deserialize, PartialEq)]
327pub struct ToonMcpConfig {
328 pub name: String,
330
331 #[serde(default = "default_true")]
333 pub enabled: bool,
334
335 pub command: String,
337
338 #[serde(default)]
340 pub args: Vec<String>,
341
342 #[serde(default)]
344 pub env: HashMap<String, String>,
345
346 #[serde(default = "default_timeout")]
348 pub timeout_secs: u64,
349}
350
351impl ToonMcpConfig {
352 pub fn new(name: impl Into<String>, command: impl Into<String>) -> Self {
354 Self {
355 name: name.into(),
356 enabled: default_true(),
357 command: command.into(),
358 args: Vec::new(),
359 env: HashMap::new(),
360 timeout_secs: default_timeout(),
361 }
362 }
363
364 pub fn to_toon(&self) -> Result<String, ToonConfigError> {
366 encode_default(self).map_err(ToonConfigError::from)
367 }
368
369 pub fn from_toon(toon: &str) -> Result<Self, ToonConfigError> {
371 decode_default(toon).map_err(ToonConfigError::from)
372 }
373}
374
375#[derive(Debug, Clone, Default)]
383pub struct DynamicConfig {
384 pub agents: HashMap<String, ToonAgentConfig>,
386 pub models: HashMap<String, ToonModelConfig>,
388 pub tools: HashMap<String, ToonToolConfig>,
390 pub workflows: HashMap<String, ToonWorkflowConfig>,
392 pub mcps: HashMap<String, ToonMcpConfig>,
394}
395
396impl DynamicConfig {
397 pub fn load(
399 agents_dir: &Path,
400 models_dir: &Path,
401 tools_dir: &Path,
402 workflows_dir: &Path,
403 mcps_dir: &Path,
404 ) -> Result<Self, ToonConfigError> {
405 let agents = load_configs_from_dir::<ToonAgentConfig>(agents_dir, "agents")?;
406 let models = load_configs_from_dir::<ToonModelConfig>(models_dir, "models")?;
407 let tools = load_configs_from_dir::<ToonToolConfig>(tools_dir, "tools")?;
408 let workflows = load_configs_from_dir::<ToonWorkflowConfig>(workflows_dir, "workflows")?;
409 let mcps = load_configs_from_dir::<ToonMcpConfig>(mcps_dir, "mcps")?;
410
411 info!(
412 "Loaded dynamic config: {} agents, {} models, {} tools, {} workflows, {} mcps",
413 agents.len(),
414 models.len(),
415 tools.len(),
416 workflows.len(),
417 mcps.len()
418 );
419
420 Ok(Self {
421 agents,
422 models,
423 tools,
424 workflows,
425 mcps,
426 })
427 }
428
429 pub fn get_agent(&self, name: &str) -> Option<&ToonAgentConfig> {
431 self.agents.get(name)
432 }
433
434 pub fn get_model(&self, name: &str) -> Option<&ToonModelConfig> {
436 self.models.get(name)
437 }
438
439 pub fn get_tool(&self, name: &str) -> Option<&ToonToolConfig> {
441 self.tools.get(name)
442 }
443
444 pub fn get_workflow(&self, name: &str) -> Option<&ToonWorkflowConfig> {
446 self.workflows.get(name)
447 }
448
449 pub fn get_mcp(&self, name: &str) -> Option<&ToonMcpConfig> {
451 self.mcps.get(name)
452 }
453
454 pub fn agent_names(&self) -> Vec<&str> {
456 self.agents.keys().map(|s| s.as_str()).collect()
457 }
458
459 pub fn model_names(&self) -> Vec<&str> {
461 self.models.keys().map(|s| s.as_str()).collect()
462 }
463
464 pub fn tool_names(&self) -> Vec<&str> {
466 self.tools.keys().map(|s| s.as_str()).collect()
467 }
468
469 pub fn workflow_names(&self) -> Vec<&str> {
471 self.workflows.keys().map(|s| s.as_str()).collect()
472 }
473
474 pub fn mcp_names(&self) -> Vec<&str> {
476 self.mcps.keys().map(|s| s.as_str()).collect()
477 }
478
479 pub fn validate(&self) -> Result<Vec<ConfigWarning>, ToonConfigError> {
481 let mut warnings = Vec::new();
482
483 for (agent_name, agent) in &self.agents {
485 if !self.models.contains_key(&agent.model) {
486 return Err(ToonConfigError::Validation(format!(
487 "Agent '{}' references unknown model '{}'",
488 agent_name, agent.model
489 )));
490 }
491
492 for tool_name in &agent.tools {
494 if !self.tools.contains_key(tool_name) {
495 return Err(ToonConfigError::Validation(format!(
496 "Agent '{}' references unknown tool '{}'",
497 agent_name, tool_name
498 )));
499 }
500 }
501 }
502
503 for (workflow_name, workflow) in &self.workflows {
505 if !self.agents.contains_key(&workflow.entry_agent) {
506 return Err(ToonConfigError::Validation(format!(
507 "Workflow '{}' references unknown entry agent '{}'",
508 workflow_name, workflow.entry_agent
509 )));
510 }
511
512 if let Some(ref fallback) = workflow.fallback_agent {
513 if !self.agents.contains_key(fallback) {
514 return Err(ToonConfigError::Validation(format!(
515 "Workflow '{}' references unknown fallback agent '{}'",
516 workflow_name, fallback
517 )));
518 }
519 }
520 }
521
522 let used_models: std::collections::HashSet<_> =
524 self.agents.values().map(|a| &a.model).collect();
525 for model_name in self.models.keys() {
526 if !used_models.contains(model_name) {
527 warnings.push(ConfigWarning {
528 kind: WarningKind::UnusedModel,
529 message: format!("Model '{}' is not used by any agent", model_name),
530 });
531 }
532 }
533
534 let used_tools: std::collections::HashSet<_> =
536 self.agents.values().flat_map(|a| a.tools.iter()).collect();
537 for tool_name in self.tools.keys() {
538 if !used_tools.contains(tool_name) {
539 warnings.push(ConfigWarning {
540 kind: WarningKind::UnusedTool,
541 message: format!("Tool '{}' is not used by any agent", tool_name),
542 });
543 }
544 }
545
546 Ok(warnings)
547 }
548}
549
550pub trait HasName {
557 fn name(&self) -> &str;
559}
560
561impl HasName for ToonAgentConfig {
562 fn name(&self) -> &str {
563 &self.name
564 }
565}
566
567impl HasName for ToonModelConfig {
568 fn name(&self) -> &str {
569 &self.name
570 }
571}
572
573impl HasName for ToonToolConfig {
574 fn name(&self) -> &str {
575 &self.name
576 }
577}
578
579impl HasName for ToonWorkflowConfig {
580 fn name(&self) -> &str {
581 &self.name
582 }
583}
584
585impl HasName for ToonMcpConfig {
586 fn name(&self) -> &str {
587 &self.name
588 }
589}
590
591fn load_configs_from_dir<T>(
593 dir: &Path,
594 config_type: &str,
595) -> Result<HashMap<String, T>, ToonConfigError>
596where
597 T: for<'de> Deserialize<'de> + HasName,
598{
599 let mut configs = HashMap::new();
600
601 if !dir.exists() {
602 debug!("Config directory does not exist: {:?}", dir);
603 return Ok(configs);
604 }
605
606 let entries = fs::read_dir(dir).map_err(|e| {
607 ToonConfigError::Io(std::io::Error::new(
608 e.kind(),
609 format!("Failed to read {} directory {:?}: {}", config_type, dir, e),
610 ))
611 })?;
612
613 for entry in entries {
614 let entry = entry.map_err(ToonConfigError::Io)?;
615 let path = entry.path();
616
617 if path.extension().and_then(|e| e.to_str()) != Some("toon") {
619 continue;
620 }
621
622 match load_toon_file::<T>(&path) {
623 Ok(config) => {
624 let name = config.name().to_string();
625 debug!("Loaded {} config: {}", config_type, name);
626 configs.insert(name, config);
627 }
628 Err(e) => {
629 warn!("Failed to load {} from {:?}: {}", config_type, path, e);
630 }
631 }
632 }
633
634 Ok(configs)
635}
636
637fn load_toon_file<T>(path: &Path) -> Result<T, ToonConfigError>
639where
640 T: for<'de> Deserialize<'de>,
641{
642 let content = fs::read_to_string(path).map_err(|e| {
643 ToonConfigError::Io(std::io::Error::new(
644 e.kind(),
645 format!("Failed to read {:?}: {}", path, e),
646 ))
647 })?;
648
649 decode_default(&content)
650 .map_err(|e| ToonConfigError::Parse(format!("Failed to parse {:?}: {}", path, e)))
651}
652
653#[derive(Debug, thiserror::Error)]
657pub enum ToonConfigError {
658 #[error("IO error: {0}")]
660 Io(#[from] std::io::Error),
661
662 #[error("TOON parse error: {0}")]
664 Parse(String),
665
666 #[error("Validation error: {0}")]
668 Validation(String),
669
670 #[error("Watch error: {0}")]
672 Watch(#[from] notify::Error),
673}
674
675impl From<ToonError> for ToonConfigError {
676 fn from(e: ToonError) -> Self {
677 ToonConfigError::Parse(e.to_string())
678 }
679}
680
681#[derive(Debug, Clone)]
683pub struct ConfigWarning {
684 pub kind: WarningKind,
686
687 pub message: String,
689}
690
691#[derive(Debug, Clone, PartialEq)]
693pub enum WarningKind {
694 UnusedModel,
696
697 UnusedTool,
699
700 UnusedAgent,
702
703 UnusedWorkflow,
705
706 UnusedMcp,
708}
709
710impl std::fmt::Display for ConfigWarning {
711 fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
712 write!(f, "{}", self.message)
713 }
714}
715
716pub struct DynamicConfigManager {
744 config: Arc<ArcSwap<DynamicConfig>>,
745 agents_dir: PathBuf,
746 models_dir: PathBuf,
747 tools_dir: PathBuf,
748 workflows_dir: PathBuf,
749 mcps_dir: PathBuf,
750 _watcher: Option<RecommendedWatcher>,
751}
752
753impl DynamicConfigManager {
754 pub fn from_config(
759 config: &crate::utils::toml_config::AresConfig,
760 ) -> Result<Self, ToonConfigError> {
761 let agents_dir = PathBuf::from(&config.config.agents_dir);
762 let models_dir = PathBuf::from(&config.config.models_dir);
763 let tools_dir = PathBuf::from(&config.config.tools_dir);
764 let workflows_dir = PathBuf::from(&config.config.workflows_dir);
765 let mcps_dir = PathBuf::from(&config.config.mcps_dir);
766
767 Self::new(
768 agents_dir,
769 models_dir,
770 tools_dir,
771 workflows_dir,
772 mcps_dir,
773 true, )
775 }
776
777 pub fn new(
787 agents_dir: PathBuf,
788 models_dir: PathBuf,
789 tools_dir: PathBuf,
790 workflows_dir: PathBuf,
791 mcps_dir: PathBuf,
792 hot_reload: bool,
793 ) -> Result<Self, ToonConfigError> {
794 let initial_config = DynamicConfig::load(
796 &agents_dir,
797 &models_dir,
798 &tools_dir,
799 &workflows_dir,
800 &mcps_dir,
801 )?;
802
803 let config = Arc::new(ArcSwap::from_pointee(initial_config));
804
805 let watcher = if hot_reload {
807 Some(Self::setup_watcher(
808 config.clone(),
809 agents_dir.clone(),
810 models_dir.clone(),
811 tools_dir.clone(),
812 workflows_dir.clone(),
813 mcps_dir.clone(),
814 )?)
815 } else {
816 None
817 };
818
819 Ok(Self {
820 config,
821 agents_dir,
822 models_dir,
823 tools_dir,
824 workflows_dir,
825 mcps_dir,
826 _watcher: watcher,
827 })
828 }
829
830 fn setup_watcher(
832 config: Arc<ArcSwap<DynamicConfig>>,
833 agents_dir: PathBuf,
834 models_dir: PathBuf,
835 tools_dir: PathBuf,
836 workflows_dir: PathBuf,
837 mcps_dir: PathBuf,
838 ) -> Result<RecommendedWatcher, ToonConfigError> {
839 let agents_dir_clone = agents_dir.clone();
840 let models_dir_clone = models_dir.clone();
841 let tools_dir_clone = tools_dir.clone();
842 let workflows_dir_clone = workflows_dir.clone();
843 let mcps_dir_clone = mcps_dir.clone();
844
845 let mut watcher = notify::recommended_watcher(move |res: Result<Event, notify::Error>| {
846 match res {
847 Ok(event) => {
848 if matches!(
850 event.kind,
851 notify::EventKind::Create(_)
852 | notify::EventKind::Modify(_)
853 | notify::EventKind::Remove(_)
854 ) {
855 info!("Config change detected, reloading...");
856
857 match DynamicConfig::load(
858 &agents_dir_clone,
859 &models_dir_clone,
860 &tools_dir_clone,
861 &workflows_dir_clone,
862 &mcps_dir_clone,
863 ) {
864 Ok(new_config) => {
865 match new_config.validate() {
867 Ok(warnings) => {
868 for warning in warnings {
869 warn!("Config warning: {}", warning);
870 }
871 config.store(Arc::new(new_config));
872 info!("Config reloaded successfully");
873 }
874 Err(e) => {
875 error!(
876 "Config validation failed, keeping old config: {}",
877 e
878 );
879 }
880 }
881 }
882 Err(e) => {
883 error!("Failed to reload config: {}", e);
884 }
885 }
886 }
887 }
888 Err(e) => {
889 error!("Watch error: {:?}", e);
890 }
891 }
892 })?;
893
894 for dir in [
896 &agents_dir,
897 &models_dir,
898 &tools_dir,
899 &workflows_dir,
900 &mcps_dir,
901 ] {
902 if dir.exists() {
903 watcher.watch(dir, RecursiveMode::Recursive)?;
904 debug!("Watching directory: {:?}", dir);
905 }
906 }
907
908 Ok(watcher)
909 }
910
911 pub fn config(&self) -> arc_swap::Guard<Arc<DynamicConfig>> {
913 self.config.load()
914 }
915
916 pub fn agent(&self, name: &str) -> Option<ToonAgentConfig> {
918 self.config.load().get_agent(name).cloned()
919 }
920
921 pub fn model(&self, name: &str) -> Option<ToonModelConfig> {
923 self.config.load().get_model(name).cloned()
924 }
925
926 pub fn tool(&self, name: &str) -> Option<ToonToolConfig> {
928 self.config.load().get_tool(name).cloned()
929 }
930
931 pub fn workflow(&self, name: &str) -> Option<ToonWorkflowConfig> {
933 self.config.load().get_workflow(name).cloned()
934 }
935
936 pub fn mcp(&self, name: &str) -> Option<ToonMcpConfig> {
938 self.config.load().get_mcp(name).cloned()
939 }
940
941 pub fn agents(&self) -> Vec<ToonAgentConfig> {
943 self.config.load().agents.values().cloned().collect()
944 }
945
946 pub fn models(&self) -> Vec<ToonModelConfig> {
948 self.config.load().models.values().cloned().collect()
949 }
950
951 pub fn tools(&self) -> Vec<ToonToolConfig> {
953 self.config.load().tools.values().cloned().collect()
954 }
955
956 pub fn workflows(&self) -> Vec<ToonWorkflowConfig> {
958 self.config.load().workflows.values().cloned().collect()
959 }
960
961 pub fn mcps(&self) -> Vec<ToonMcpConfig> {
963 self.config.load().mcps.values().cloned().collect()
964 }
965
966 pub fn agent_names(&self) -> Vec<String> {
968 self.config
969 .load()
970 .agent_names()
971 .into_iter()
972 .map(String::from)
973 .collect()
974 }
975
976 pub fn model_names(&self) -> Vec<String> {
978 self.config
979 .load()
980 .model_names()
981 .into_iter()
982 .map(String::from)
983 .collect()
984 }
985
986 pub fn tool_names(&self) -> Vec<String> {
988 self.config
989 .load()
990 .tool_names()
991 .into_iter()
992 .map(String::from)
993 .collect()
994 }
995
996 pub fn workflow_names(&self) -> Vec<String> {
998 self.config
999 .load()
1000 .workflow_names()
1001 .into_iter()
1002 .map(String::from)
1003 .collect()
1004 }
1005
1006 pub fn mcp_names(&self) -> Vec<String> {
1008 self.config
1009 .load()
1010 .mcp_names()
1011 .into_iter()
1012 .map(String::from)
1013 .collect()
1014 }
1015
1016 pub fn reload(&self) -> Result<Vec<ConfigWarning>, ToonConfigError> {
1018 let new_config = DynamicConfig::load(
1019 &self.agents_dir,
1020 &self.models_dir,
1021 &self.tools_dir,
1022 &self.workflows_dir,
1023 &self.mcps_dir,
1024 )?;
1025
1026 let warnings = new_config.validate()?;
1027 self.config.store(Arc::new(new_config));
1028 Ok(warnings)
1029 }
1030}
1031
1032#[cfg(test)]
1035mod tests {
1036 use super::*;
1037 use tempfile::TempDir;
1038
1039 #[test]
1040 fn test_agent_config_roundtrip() {
1041 let agent = ToonAgentConfig::new("test-agent", "fast")
1042 .with_system_prompt("You are a test agent.")
1043 .with_tools(vec!["calculator".to_string(), "web_search".to_string()]);
1044
1045 let toon = agent.to_toon().expect("Failed to encode");
1046 let decoded = ToonAgentConfig::from_toon(&toon).expect("Failed to decode");
1047
1048 assert_eq!(agent.name, decoded.name);
1049 assert_eq!(agent.model, decoded.model);
1050 assert_eq!(agent.system_prompt, decoded.system_prompt);
1051 assert_eq!(agent.tools, decoded.tools);
1052 }
1053
1054 #[test]
1055 fn test_model_config_roundtrip() {
1056 let model = ToonModelConfig::new("fast", "ollama-local", "ministral-3:3b");
1057
1058 let toon = model.to_toon().expect("Failed to encode");
1059 let decoded = ToonModelConfig::from_toon(&toon).expect("Failed to decode");
1060
1061 assert_eq!(model.name, decoded.name);
1062 assert_eq!(model.provider, decoded.provider);
1063 assert_eq!(model.model, decoded.model);
1064 assert_eq!(model.temperature, decoded.temperature);
1065 assert_eq!(model.max_tokens, decoded.max_tokens);
1066 }
1067
1068 #[test]
1069 fn test_tool_config_roundtrip() {
1070 let mut tool = ToonToolConfig::new("calculator");
1071 tool.description = Some("Performs arithmetic operations".to_string());
1072 tool.timeout_secs = 10;
1073
1074 let toon = tool.to_toon().expect("Failed to encode");
1075 let decoded = ToonToolConfig::from_toon(&toon).expect("Failed to decode");
1076
1077 assert_eq!(tool.name, decoded.name);
1078 assert_eq!(tool.enabled, decoded.enabled);
1079 assert_eq!(tool.description, decoded.description);
1080 assert_eq!(tool.timeout_secs, decoded.timeout_secs);
1081 }
1082
1083 #[test]
1084 fn test_workflow_config_roundtrip() {
1085 let mut workflow = ToonWorkflowConfig::new("default", "router");
1086 workflow.fallback_agent = Some("orchestrator".to_string());
1087 workflow.max_depth = 3;
1088 workflow.max_iterations = 5;
1089
1090 let toon = workflow.to_toon().expect("Failed to encode");
1091 let decoded = ToonWorkflowConfig::from_toon(&toon).expect("Failed to decode");
1092
1093 assert_eq!(workflow.name, decoded.name);
1094 assert_eq!(workflow.entry_agent, decoded.entry_agent);
1095 assert_eq!(workflow.fallback_agent, decoded.fallback_agent);
1096 assert_eq!(workflow.max_depth, decoded.max_depth);
1097 assert_eq!(workflow.max_iterations, decoded.max_iterations);
1098 }
1099
1100 #[test]
1101 fn test_mcp_config_roundtrip() {
1102 let mut mcp = ToonMcpConfig::new("filesystem", "npx");
1103 mcp.args = vec![
1104 "-y".to_string(),
1105 "@modelcontextprotocol/server-filesystem".to_string(),
1106 "/home".to_string(),
1107 "/tmp".to_string(),
1108 ];
1109 mcp.env
1110 .insert("NODE_ENV".to_string(), "production".to_string());
1111 mcp.timeout_secs = 30;
1112
1113 let toon = mcp.to_toon().expect("Failed to encode");
1114 let decoded = ToonMcpConfig::from_toon(&toon).expect("Failed to decode");
1115
1116 assert_eq!(mcp.name, decoded.name);
1117 assert_eq!(mcp.command, decoded.command);
1118 assert_eq!(mcp.args, decoded.args);
1119 assert_eq!(mcp.env, decoded.env);
1120 assert_eq!(mcp.timeout_secs, decoded.timeout_secs);
1121 }
1122
1123 #[test]
1124 fn test_load_configs_from_dir() {
1125 let temp_dir = TempDir::new().expect("Failed to create temp dir");
1126 let agents_dir = temp_dir.path().join("agents");
1127 fs::create_dir_all(&agents_dir).expect("Failed to create agents dir");
1128
1129 let agent_content = r#"name: test-agent
1131model: fast
1132max_tool_iterations: 5
1133parallel_tools: false
1134tools[0]:
1135system_prompt: Test agent prompt"#;
1136
1137 fs::write(agents_dir.join("test-agent.toon"), agent_content)
1138 .expect("Failed to write agent file");
1139
1140 let agents = load_configs_from_dir::<ToonAgentConfig>(&agents_dir, "agents")
1141 .expect("Failed to load agents");
1142
1143 assert_eq!(agents.len(), 1);
1144 let agent = agents.get("test-agent").expect("Agent not found");
1145 assert_eq!(agent.name, "test-agent");
1146 assert_eq!(agent.model, "fast");
1147 assert_eq!(agent.max_tool_iterations, 5);
1148 }
1149
1150 #[test]
1151 fn test_dynamic_config_validation() {
1152 let mut config = DynamicConfig::default();
1153
1154 config.models.insert(
1156 "fast".to_string(),
1157 ToonModelConfig::new("fast", "ollama-local", "ministral-3:3b"),
1158 );
1159
1160 config
1162 .tools
1163 .insert("calculator".to_string(), ToonToolConfig::new("calculator"));
1164
1165 let mut agent = ToonAgentConfig::new("router", "fast");
1167 agent.tools = vec!["calculator".to_string()];
1168 config.agents.insert("router".to_string(), agent);
1169
1170 config.workflows.insert(
1172 "default".to_string(),
1173 ToonWorkflowConfig::new("default", "router"),
1174 );
1175
1176 let warnings = config.validate().expect("Validation failed");
1178 assert!(warnings.is_empty());
1179 }
1180
1181 #[test]
1182 fn test_dynamic_config_validation_missing_model() {
1183 let mut config = DynamicConfig::default();
1184
1185 let agent = ToonAgentConfig::new("router", "non-existent-model");
1187 config.agents.insert("router".to_string(), agent);
1188
1189 let result = config.validate();
1191 assert!(result.is_err());
1192 assert!(result.unwrap_err().to_string().contains("unknown model"));
1193 }
1194
1195 #[test]
1196 fn test_dynamic_config_validation_missing_tool() {
1197 let mut config = DynamicConfig::default();
1198
1199 config.models.insert(
1201 "fast".to_string(),
1202 ToonModelConfig::new("fast", "ollama-local", "ministral-3:3b"),
1203 );
1204
1205 let mut agent = ToonAgentConfig::new("router", "fast");
1207 agent.tools = vec!["non-existent-tool".to_string()];
1208 config.agents.insert("router".to_string(), agent);
1209
1210 let result = config.validate();
1212 assert!(result.is_err());
1213 assert!(result.unwrap_err().to_string().contains("unknown tool"));
1214 }
1215
1216 #[test]
1217 fn test_parse_agent_from_toon_string() {
1218 let toon = r#"name: router
1219model: fast
1220max_tool_iterations: 1
1221parallel_tools: false
1222tools[0]:
1223system_prompt: You are a routing agent."#;
1224
1225 let agent = ToonAgentConfig::from_toon(toon).expect("Failed to parse");
1226 assert_eq!(agent.name, "router");
1227 assert_eq!(agent.model, "fast");
1228 assert_eq!(agent.max_tool_iterations, 1);
1229 assert!(!agent.parallel_tools);
1230 assert!(agent.tools.is_empty());
1231 }
1232
1233 #[test]
1234 fn test_parse_model_from_toon_string() {
1235 let toon = r#"name: fast
1236provider: ollama-local
1237model: ministral-3:3b
1238temperature: 0.7
1239max_tokens: 256"#;
1240
1241 let model = ToonModelConfig::from_toon(toon).expect("Failed to parse");
1242 assert_eq!(model.name, "fast");
1243 assert_eq!(model.provider, "ollama-local");
1244 assert_eq!(model.model, "ministral-3:3b");
1245 assert!((model.temperature - 0.7).abs() < 0.01);
1246 assert_eq!(model.max_tokens, 256);
1247 }
1248}