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 #[serde(default = "default_version")]
61 pub version: String,
62
63 pub model: String,
65
66 #[serde(default)]
68 pub system_prompt: Option<String>,
69
70 #[serde(default)]
72 pub tools: Vec<String>,
73
74 #[serde(default = "default_max_tool_iterations")]
76 pub max_tool_iterations: usize,
77
78 #[serde(default)]
80 pub parallel_tools: bool,
81
82 #[serde(flatten)]
84 pub extra: HashMap<String, serde_json::Value>,
85}
86
87fn default_version() -> String {
88 "0.1.0".to_string()
89}
90
91fn default_max_tool_iterations() -> usize {
92 10
93}
94
95impl ToonAgentConfig {
96 pub fn new(name: impl Into<String>, model: impl Into<String>) -> Self {
98 Self {
99 name: name.into(),
100 version: default_version(),
101 model: model.into(),
102 system_prompt: None,
103 tools: Vec::new(),
104 max_tool_iterations: default_max_tool_iterations(),
105 parallel_tools: false,
106 extra: HashMap::new(),
107 }
108 }
109
110 pub fn with_system_prompt(mut self, prompt: impl Into<String>) -> Self {
112 self.system_prompt = Some(prompt.into());
113 self
114 }
115
116 pub fn with_tools(mut self, tools: Vec<String>) -> Self {
118 self.tools = tools;
119 self
120 }
121
122 pub fn to_toon(&self) -> Result<String, ToonConfigError> {
124 encode_default(self).map_err(ToonConfigError::from)
125 }
126
127 pub fn from_toon(toon: &str) -> Result<Self, ToonConfigError> {
129 decode_default(toon).map_err(ToonConfigError::from)
130 }
131}
132
133#[derive(Debug, Clone, Serialize, Deserialize, PartialEq)]
140pub struct ToonModelConfig {
141 pub name: String,
143
144 pub provider: String,
146
147 pub model: String,
149
150 #[serde(default = "default_temperature")]
152 pub temperature: f32,
153
154 #[serde(default = "default_max_tokens")]
156 pub max_tokens: u32,
157
158 #[serde(default)]
160 pub top_p: Option<f32>,
161
162 #[serde(default)]
164 pub frequency_penalty: Option<f32>,
165
166 #[serde(default)]
168 pub presence_penalty: Option<f32>,
169}
170
171fn default_temperature() -> f32 {
172 0.7
173}
174
175fn default_max_tokens() -> u32 {
176 512
177}
178
179impl ToonModelConfig {
180 pub fn new(
182 name: impl Into<String>,
183 provider: impl Into<String>,
184 model: impl Into<String>,
185 ) -> Self {
186 Self {
187 name: name.into(),
188 provider: provider.into(),
189 model: model.into(),
190 temperature: default_temperature(),
191 max_tokens: default_max_tokens(),
192 top_p: None,
193 frequency_penalty: None,
194 presence_penalty: None,
195 }
196 }
197
198 pub fn to_toon(&self) -> Result<String, ToonConfigError> {
200 encode_default(self).map_err(ToonConfigError::from)
201 }
202
203 pub fn from_toon(toon: &str) -> Result<Self, ToonConfigError> {
205 decode_default(toon).map_err(ToonConfigError::from)
206 }
207}
208
209#[derive(Debug, Clone, Serialize, Deserialize, PartialEq)]
215pub struct ToonToolConfig {
216 pub name: String,
218
219 #[serde(default = "default_true")]
221 pub enabled: bool,
222
223 #[serde(default)]
225 pub description: Option<String>,
226
227 #[serde(default = "default_timeout")]
229 pub timeout_secs: u64,
230
231 #[serde(flatten)]
233 pub extra: HashMap<String, serde_json::Value>,
234}
235
236fn default_true() -> bool {
237 true
238}
239
240fn default_timeout() -> u64 {
241 30
242}
243
244impl ToonToolConfig {
245 pub fn new(name: impl Into<String>) -> Self {
247 Self {
248 name: name.into(),
249 enabled: default_true(),
250 description: None,
251 timeout_secs: default_timeout(),
252 extra: HashMap::new(),
253 }
254 }
255
256 pub fn to_toon(&self) -> Result<String, ToonConfigError> {
258 encode_default(self).map_err(ToonConfigError::from)
259 }
260
261 pub fn from_toon(toon: &str) -> Result<Self, ToonConfigError> {
263 decode_default(toon).map_err(ToonConfigError::from)
264 }
265}
266
267#[derive(Debug, Clone, Serialize, Deserialize, PartialEq)]
274pub struct ToonWorkflowConfig {
275 pub name: String,
277
278 pub entry_agent: String,
280
281 #[serde(default)]
283 pub fallback_agent: Option<String>,
284
285 #[serde(default = "default_max_depth")]
287 pub max_depth: u8,
288
289 #[serde(default = "default_max_iterations")]
291 pub max_iterations: u8,
292
293 #[serde(default)]
295 pub parallel_subagents: bool,
296}
297
298fn default_max_depth() -> u8 {
299 3
300}
301
302fn default_max_iterations() -> u8 {
303 5
304}
305
306impl ToonWorkflowConfig {
307 pub fn new(name: impl Into<String>, entry_agent: impl Into<String>) -> Self {
309 Self {
310 name: name.into(),
311 entry_agent: entry_agent.into(),
312 fallback_agent: None,
313 max_depth: default_max_depth(),
314 max_iterations: default_max_iterations(),
315 parallel_subagents: false,
316 }
317 }
318
319 pub fn to_toon(&self) -> Result<String, ToonConfigError> {
321 encode_default(self).map_err(ToonConfigError::from)
322 }
323
324 pub fn from_toon(toon: &str) -> Result<Self, ToonConfigError> {
326 decode_default(toon).map_err(ToonConfigError::from)
327 }
328}
329
330#[derive(Debug, Clone, Serialize, Deserialize, PartialEq)]
337pub struct ToonMcpConfig {
338 pub name: String,
340
341 #[serde(default = "default_true")]
343 pub enabled: bool,
344
345 #[serde(default)]
347 pub command: Option<String>,
348
349 #[serde(default)]
351 pub args: Vec<String>,
352
353 #[serde(default)]
355 pub env: HashMap<String, String>,
356
357 #[serde(default = "default_timeout")]
359 pub timeout_secs: u64,
360}
361
362impl ToonMcpConfig {
363 pub fn new(name: impl Into<String>, command: impl Into<String>) -> Self {
365 Self {
366 name: name.into(),
367 enabled: default_true(),
368 command: Some(command.into()),
369 args: Vec::new(),
370 env: HashMap::new(),
371 timeout_secs: default_timeout(),
372 }
373 }
374
375 pub fn to_toon(&self) -> Result<String, ToonConfigError> {
377 encode_default(self).map_err(ToonConfigError::from)
378 }
379
380 pub fn from_toon(toon: &str) -> Result<Self, ToonConfigError> {
382 decode_default(toon).map_err(ToonConfigError::from)
383 }
384}
385
386#[derive(Debug, Clone, Default)]
394pub struct DynamicConfig {
395 pub agents: HashMap<String, ToonAgentConfig>,
397 pub models: HashMap<String, ToonModelConfig>,
399 pub tools: HashMap<String, ToonToolConfig>,
401 pub workflows: HashMap<String, ToonWorkflowConfig>,
403 pub mcps: HashMap<String, ToonMcpConfig>,
405}
406
407impl DynamicConfig {
408 pub fn load(
410 agents_dir: &Path,
411 models_dir: &Path,
412 tools_dir: &Path,
413 workflows_dir: &Path,
414 mcps_dir: &Path,
415 ) -> Result<Self, ToonConfigError> {
416 let agents = load_configs_from_dir::<ToonAgentConfig>(agents_dir, "agents")?;
417 let models = load_configs_from_dir::<ToonModelConfig>(models_dir, "models")?;
418 let tools = load_configs_from_dir::<ToonToolConfig>(tools_dir, "tools")?;
419 let workflows = load_configs_from_dir::<ToonWorkflowConfig>(workflows_dir, "workflows")?;
420 let mcps = load_configs_from_dir::<ToonMcpConfig>(mcps_dir, "mcps")?;
421
422 info!(
423 "Loaded dynamic config: {} agents, {} models, {} tools, {} workflows, {} mcps",
424 agents.len(),
425 models.len(),
426 tools.len(),
427 workflows.len(),
428 mcps.len()
429 );
430
431 Ok(Self {
432 agents,
433 models,
434 tools,
435 workflows,
436 mcps,
437 })
438 }
439
440 pub fn get_agent(&self, name: &str) -> Option<&ToonAgentConfig> {
442 self.agents.get(name)
443 }
444
445 pub fn get_model(&self, name: &str) -> Option<&ToonModelConfig> {
447 self.models.get(name)
448 }
449
450 pub fn get_tool(&self, name: &str) -> Option<&ToonToolConfig> {
452 self.tools.get(name)
453 }
454
455 pub fn get_workflow(&self, name: &str) -> Option<&ToonWorkflowConfig> {
457 self.workflows.get(name)
458 }
459
460 pub fn get_mcp(&self, name: &str) -> Option<&ToonMcpConfig> {
462 self.mcps.get(name)
463 }
464
465 pub fn agent_names(&self) -> Vec<&str> {
467 self.agents.keys().map(|s| s.as_str()).collect()
468 }
469
470 pub fn model_names(&self) -> Vec<&str> {
472 self.models.keys().map(|s| s.as_str()).collect()
473 }
474
475 pub fn tool_names(&self) -> Vec<&str> {
477 self.tools.keys().map(|s| s.as_str()).collect()
478 }
479
480 pub fn workflow_names(&self) -> Vec<&str> {
482 self.workflows.keys().map(|s| s.as_str()).collect()
483 }
484
485 pub fn mcp_names(&self) -> Vec<&str> {
487 self.mcps.keys().map(|s| s.as_str()).collect()
488 }
489
490 pub fn validate(&self) -> Result<Vec<ConfigWarning>, ToonConfigError> {
492 let mut warnings = Vec::new();
493
494 for (agent_name, agent) in &self.agents {
496 if !self.models.contains_key(&agent.model) {
497 return Err(ToonConfigError::Validation(format!(
498 "Agent '{}' references unknown model '{}'",
499 agent_name, agent.model
500 )));
501 }
502
503 for tool_name in &agent.tools {
505 if !self.tools.contains_key(tool_name) {
506 return Err(ToonConfigError::Validation(format!(
507 "Agent '{}' references unknown tool '{}'",
508 agent_name, tool_name
509 )));
510 }
511 }
512 }
513
514 for (workflow_name, workflow) in &self.workflows {
516 if !self.agents.contains_key(&workflow.entry_agent) {
517 return Err(ToonConfigError::Validation(format!(
518 "Workflow '{}' references unknown entry agent '{}'",
519 workflow_name, workflow.entry_agent
520 )));
521 }
522
523 if let Some(ref fallback) = workflow.fallback_agent {
524 if !self.agents.contains_key(fallback) {
525 return Err(ToonConfigError::Validation(format!(
526 "Workflow '{}' references unknown fallback agent '{}'",
527 workflow_name, fallback
528 )));
529 }
530 }
531 }
532
533 let used_models: std::collections::HashSet<_> =
535 self.agents.values().map(|a| &a.model).collect();
536 for model_name in self.models.keys() {
537 if !used_models.contains(model_name) {
538 warnings.push(ConfigWarning {
539 kind: WarningKind::UnusedModel,
540 message: format!("Model '{}' is not used by any agent", model_name),
541 });
542 }
543 }
544
545 let used_tools: std::collections::HashSet<_> =
547 self.agents.values().flat_map(|a| a.tools.iter()).collect();
548 for tool_name in self.tools.keys() {
549 if !used_tools.contains(tool_name) {
550 warnings.push(ConfigWarning {
551 kind: WarningKind::UnusedTool,
552 message: format!("Tool '{}' is not used by any agent", tool_name),
553 });
554 }
555 }
556
557 Ok(warnings)
558 }
559}
560
561pub trait HasName {
568 fn name(&self) -> &str;
570}
571
572impl HasName for ToonAgentConfig {
573 fn name(&self) -> &str {
574 &self.name
575 }
576}
577
578impl HasName for ToonModelConfig {
579 fn name(&self) -> &str {
580 &self.name
581 }
582}
583
584impl HasName for ToonToolConfig {
585 fn name(&self) -> &str {
586 &self.name
587 }
588}
589
590impl HasName for ToonWorkflowConfig {
591 fn name(&self) -> &str {
592 &self.name
593 }
594}
595
596impl HasName for ToonMcpConfig {
597 fn name(&self) -> &str {
598 &self.name
599 }
600}
601
602fn load_configs_from_dir<T>(
604 dir: &Path,
605 config_type: &str,
606) -> Result<HashMap<String, T>, ToonConfigError>
607where
608 T: for<'de> Deserialize<'de> + HasName,
609{
610 let mut configs = HashMap::new();
611
612 if !dir.exists() {
613 debug!("Config directory does not exist: {:?}", dir);
614 return Ok(configs);
615 }
616
617 let entries = fs::read_dir(dir).map_err(|e| {
618 ToonConfigError::Io(std::io::Error::new(
619 e.kind(),
620 format!("Failed to read {} directory {:?}: {}", config_type, dir, e),
621 ))
622 })?;
623
624 for entry in entries {
625 let entry = entry.map_err(ToonConfigError::Io)?;
626 let path = entry.path();
627
628 if path.extension().and_then(|e| e.to_str()) != Some("toon") {
630 continue;
631 }
632
633 match load_toon_file::<T>(&path) {
634 Ok(config) => {
635 let name = config.name().to_string();
636 debug!("Loaded {} config: {}", config_type, name);
637 configs.insert(name, config);
638 }
639 Err(e) => {
640 warn!("Failed to load {} from {:?}: {}", config_type, path, e);
641 }
642 }
643 }
644
645 Ok(configs)
646}
647
648fn load_toon_file<T>(path: &Path) -> Result<T, ToonConfigError>
650where
651 T: for<'de> Deserialize<'de>,
652{
653 let content = fs::read_to_string(path).map_err(|e| {
654 ToonConfigError::Io(std::io::Error::new(
655 e.kind(),
656 format!("Failed to read {:?}: {}", path, e),
657 ))
658 })?;
659
660 decode_default(&content)
661 .map_err(|e| ToonConfigError::Parse(format!("Failed to parse {:?}: {}", path, e)))
662}
663
664#[derive(Debug, thiserror::Error)]
668pub enum ToonConfigError {
669 #[error("IO error: {0}")]
671 Io(#[from] std::io::Error),
672
673 #[error("TOON parse error: {0}")]
675 Parse(String),
676
677 #[error("Validation error: {0}")]
679 Validation(String),
680
681 #[error("Watch error: {0}")]
683 Watch(#[from] notify::Error),
684}
685
686impl From<ToonError> for ToonConfigError {
687 fn from(e: ToonError) -> Self {
688 ToonConfigError::Parse(e.to_string())
689 }
690}
691
692#[derive(Debug, Clone)]
694pub struct ConfigWarning {
695 pub kind: WarningKind,
697
698 pub message: String,
700}
701
702#[derive(Debug, Clone, PartialEq)]
704pub enum WarningKind {
705 UnusedModel,
707
708 UnusedTool,
710
711 UnusedAgent,
713
714 UnusedWorkflow,
716
717 UnusedMcp,
719}
720
721impl std::fmt::Display for ConfigWarning {
722 fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
723 write!(f, "{}", self.message)
724 }
725}
726
727pub struct DynamicConfigManager {
755 config: Arc<ArcSwap<DynamicConfig>>,
756 agents_dir: PathBuf,
757 models_dir: PathBuf,
758 tools_dir: PathBuf,
759 workflows_dir: PathBuf,
760 mcps_dir: PathBuf,
761 _watcher: Option<RecommendedWatcher>,
762 version_tx: Arc<std::sync::Mutex<Option<tokio::sync::mpsc::UnboundedSender<Vec<ToonAgentConfig>>>>>,
766}
767
768impl DynamicConfigManager {
769 pub fn from_config(
774 config: &crate::utils::toml_config::AresConfig,
775 ) -> Result<Self, ToonConfigError> {
776 let agents_dir = PathBuf::from(&config.config.agents_dir);
777 let models_dir = PathBuf::from(&config.config.models_dir);
778 let tools_dir = PathBuf::from(&config.config.tools_dir);
779 let workflows_dir = PathBuf::from(&config.config.workflows_dir);
780 let mcps_dir = PathBuf::from(&config.config.mcps_dir);
781
782 Self::new(
783 agents_dir,
784 models_dir,
785 tools_dir,
786 workflows_dir,
787 mcps_dir,
788 true, )
790 }
791
792 pub fn new(
802 agents_dir: PathBuf,
803 models_dir: PathBuf,
804 tools_dir: PathBuf,
805 workflows_dir: PathBuf,
806 mcps_dir: PathBuf,
807 hot_reload: bool,
808 ) -> Result<Self, ToonConfigError> {
809 let initial_config = DynamicConfig::load(
811 &agents_dir,
812 &models_dir,
813 &tools_dir,
814 &workflows_dir,
815 &mcps_dir,
816 )?;
817
818 let config = Arc::new(ArcSwap::from_pointee(initial_config));
819
820 let version_tx: Arc<std::sync::Mutex<Option<tokio::sync::mpsc::UnboundedSender<Vec<ToonAgentConfig>>>>> =
822 Arc::new(std::sync::Mutex::new(None));
823
824 let watcher = if hot_reload {
826 Some(Self::setup_watcher(
827 config.clone(),
828 agents_dir.clone(),
829 models_dir.clone(),
830 tools_dir.clone(),
831 workflows_dir.clone(),
832 mcps_dir.clone(),
833 version_tx.clone(),
834 )?)
835 } else {
836 None
837 };
838
839 Ok(Self {
840 config,
841 agents_dir,
842 models_dir,
843 tools_dir,
844 workflows_dir,
845 mcps_dir,
846 _watcher: watcher,
847 version_tx,
848 })
849 }
850
851 pub fn set_version_tx(&self, tx: tokio::sync::mpsc::UnboundedSender<Vec<ToonAgentConfig>>) {
854 if let Ok(mut guard) = self.version_tx.lock() {
855 *guard = Some(tx);
856 }
857 }
858
859 fn setup_watcher(
861 config: Arc<ArcSwap<DynamicConfig>>,
862 agents_dir: PathBuf,
863 models_dir: PathBuf,
864 tools_dir: PathBuf,
865 workflows_dir: PathBuf,
866 mcps_dir: PathBuf,
867 version_tx: Arc<std::sync::Mutex<Option<tokio::sync::mpsc::UnboundedSender<Vec<ToonAgentConfig>>>>>,
868 ) -> Result<RecommendedWatcher, ToonConfigError> {
869 let agents_dir_clone = agents_dir.clone();
870 let models_dir_clone = models_dir.clone();
871 let tools_dir_clone = tools_dir.clone();
872 let workflows_dir_clone = workflows_dir.clone();
873 let mcps_dir_clone = mcps_dir.clone();
874
875 let mut watcher = notify::recommended_watcher(move |res: Result<Event, notify::Error>| {
876 match res {
877 Ok(event) => {
878 if matches!(
880 event.kind,
881 notify::EventKind::Create(_)
882 | notify::EventKind::Modify(_)
883 | notify::EventKind::Remove(_)
884 ) {
885 info!("Config change detected, reloading...");
886
887 match DynamicConfig::load(
888 &agents_dir_clone,
889 &models_dir_clone,
890 &tools_dir_clone,
891 &workflows_dir_clone,
892 &mcps_dir_clone,
893 ) {
894 Ok(new_config) => {
895 match new_config.validate() {
897 Ok(warnings) => {
898 for warning in warnings {
899 warn!("Config warning: {}", warning);
900 }
901 let agents: Vec<ToonAgentConfig> =
903 new_config.agents.values().cloned().collect();
904 if let Ok(guard) = version_tx.lock() {
905 if let Some(tx) = guard.as_ref() {
906 let _ = tx.send(agents);
907 }
908 }
909 config.store(Arc::new(new_config));
910 info!("Config reloaded successfully");
911 }
912 Err(e) => {
913 error!(
914 "Config validation failed, keeping old config: {}",
915 e
916 );
917 }
918 }
919 }
920 Err(e) => {
921 error!("Failed to reload config: {}", e);
922 }
923 }
924 }
925 }
926 Err(e) => {
927 error!("Watch error: {:?}", e);
928 }
929 }
930 })?;
931
932 for dir in [
934 &agents_dir,
935 &models_dir,
936 &tools_dir,
937 &workflows_dir,
938 &mcps_dir,
939 ] {
940 if dir.exists() {
941 watcher.watch(dir, RecursiveMode::Recursive)?;
942 debug!("Watching directory: {:?}", dir);
943 }
944 }
945
946 Ok(watcher)
947 }
948
949 pub fn config(&self) -> arc_swap::Guard<Arc<DynamicConfig>> {
951 self.config.load()
952 }
953
954 pub fn agent(&self, name: &str) -> Option<ToonAgentConfig> {
956 self.config.load().get_agent(name).cloned()
957 }
958
959 pub fn model(&self, name: &str) -> Option<ToonModelConfig> {
961 self.config.load().get_model(name).cloned()
962 }
963
964 pub fn tool(&self, name: &str) -> Option<ToonToolConfig> {
966 self.config.load().get_tool(name).cloned()
967 }
968
969 pub fn workflow(&self, name: &str) -> Option<ToonWorkflowConfig> {
971 self.config.load().get_workflow(name).cloned()
972 }
973
974 pub fn mcp(&self, name: &str) -> Option<ToonMcpConfig> {
976 self.config.load().get_mcp(name).cloned()
977 }
978
979 pub fn agents(&self) -> Vec<ToonAgentConfig> {
981 self.config.load().agents.values().cloned().collect()
982 }
983
984 pub fn models(&self) -> Vec<ToonModelConfig> {
986 self.config.load().models.values().cloned().collect()
987 }
988
989 pub fn tools(&self) -> Vec<ToonToolConfig> {
991 self.config.load().tools.values().cloned().collect()
992 }
993
994 pub fn workflows(&self) -> Vec<ToonWorkflowConfig> {
996 self.config.load().workflows.values().cloned().collect()
997 }
998
999 pub fn mcps(&self) -> Vec<ToonMcpConfig> {
1001 self.config.load().mcps.values().cloned().collect()
1002 }
1003
1004 pub fn agent_names(&self) -> Vec<String> {
1006 self.config
1007 .load()
1008 .agent_names()
1009 .into_iter()
1010 .map(String::from)
1011 .collect()
1012 }
1013
1014 pub fn model_names(&self) -> Vec<String> {
1016 self.config
1017 .load()
1018 .model_names()
1019 .into_iter()
1020 .map(String::from)
1021 .collect()
1022 }
1023
1024 pub fn tool_names(&self) -> Vec<String> {
1026 self.config
1027 .load()
1028 .tool_names()
1029 .into_iter()
1030 .map(String::from)
1031 .collect()
1032 }
1033
1034 pub fn workflow_names(&self) -> Vec<String> {
1036 self.config
1037 .load()
1038 .workflow_names()
1039 .into_iter()
1040 .map(String::from)
1041 .collect()
1042 }
1043
1044 pub fn mcp_names(&self) -> Vec<String> {
1046 self.config
1047 .load()
1048 .mcp_names()
1049 .into_iter()
1050 .map(String::from)
1051 .collect()
1052 }
1053
1054 pub fn upsert_agent(&self, agent: ToonAgentConfig) {
1057 let current = self.config.load();
1058 let mut new_agents = current.agents.clone();
1059 new_agents.insert(agent.name.clone(), agent);
1060 let new_config = DynamicConfig {
1061 agents: new_agents,
1062 models: current.models.clone(),
1063 tools: current.tools.clone(),
1064 workflows: current.workflows.clone(),
1065 mcps: current.mcps.clone(),
1066 };
1067 self.config.store(Arc::new(new_config));
1068 }
1069
1070 pub fn reload(&self) -> Result<Vec<ConfigWarning>, ToonConfigError> {
1072 let new_config = DynamicConfig::load(
1073 &self.agents_dir,
1074 &self.models_dir,
1075 &self.tools_dir,
1076 &self.workflows_dir,
1077 &self.mcps_dir,
1078 )?;
1079
1080 let warnings = new_config.validate()?;
1081 self.config.store(Arc::new(new_config));
1082 Ok(warnings)
1083 }
1084}
1085
1086#[cfg(test)]
1089mod tests {
1090 use super::*;
1091 use tempfile::TempDir;
1092
1093 #[test]
1094 fn test_agent_config_roundtrip() {
1095 let agent = ToonAgentConfig::new("test-agent", "fast")
1096 .with_system_prompt("You are a test agent.")
1097 .with_tools(vec!["calculator".to_string(), "web_search".to_string()]);
1098
1099 let toon = agent.to_toon().expect("Failed to encode");
1100 let decoded = ToonAgentConfig::from_toon(&toon).expect("Failed to decode");
1101
1102 assert_eq!(agent.name, decoded.name);
1103 assert_eq!(agent.model, decoded.model);
1104 assert_eq!(agent.system_prompt, decoded.system_prompt);
1105 assert_eq!(agent.tools, decoded.tools);
1106 }
1107
1108 #[test]
1109 fn test_model_config_roundtrip() {
1110 let model = ToonModelConfig::new("fast", "ollama-local", "ministral-3:3b");
1111
1112 let toon = model.to_toon().expect("Failed to encode");
1113 let decoded = ToonModelConfig::from_toon(&toon).expect("Failed to decode");
1114
1115 assert_eq!(model.name, decoded.name);
1116 assert_eq!(model.provider, decoded.provider);
1117 assert_eq!(model.model, decoded.model);
1118 assert_eq!(model.temperature, decoded.temperature);
1119 assert_eq!(model.max_tokens, decoded.max_tokens);
1120 }
1121
1122 #[test]
1123 fn test_tool_config_roundtrip() {
1124 let mut tool = ToonToolConfig::new("calculator");
1125 tool.description = Some("Performs arithmetic operations".to_string());
1126 tool.timeout_secs = 10;
1127
1128 let toon = tool.to_toon().expect("Failed to encode");
1129 let decoded = ToonToolConfig::from_toon(&toon).expect("Failed to decode");
1130
1131 assert_eq!(tool.name, decoded.name);
1132 assert_eq!(tool.enabled, decoded.enabled);
1133 assert_eq!(tool.description, decoded.description);
1134 assert_eq!(tool.timeout_secs, decoded.timeout_secs);
1135 }
1136
1137 #[test]
1138 fn test_workflow_config_roundtrip() {
1139 let mut workflow = ToonWorkflowConfig::new("default", "router");
1140 workflow.fallback_agent = Some("orchestrator".to_string());
1141 workflow.max_depth = 3;
1142 workflow.max_iterations = 5;
1143
1144 let toon = workflow.to_toon().expect("Failed to encode");
1145 let decoded = ToonWorkflowConfig::from_toon(&toon).expect("Failed to decode");
1146
1147 assert_eq!(workflow.name, decoded.name);
1148 assert_eq!(workflow.entry_agent, decoded.entry_agent);
1149 assert_eq!(workflow.fallback_agent, decoded.fallback_agent);
1150 assert_eq!(workflow.max_depth, decoded.max_depth);
1151 assert_eq!(workflow.max_iterations, decoded.max_iterations);
1152 }
1153
1154 #[test]
1155 fn test_mcp_config_roundtrip() {
1156 let mut mcp = ToonMcpConfig::new("filesystem", "npx");
1157 mcp.args = vec![
1158 "-y".to_string(),
1159 "@modelcontextprotocol/server-filesystem".to_string(),
1160 "/home".to_string(),
1161 "/tmp".to_string(),
1162 ];
1163 mcp.env
1164 .insert("NODE_ENV".to_string(), "production".to_string());
1165 mcp.timeout_secs = 30;
1166
1167 let toon = mcp.to_toon().expect("Failed to encode");
1168 let decoded = ToonMcpConfig::from_toon(&toon).expect("Failed to decode");
1169
1170 assert_eq!(mcp.name, decoded.name);
1171 assert_eq!(mcp.command, decoded.command);
1172 assert_eq!(mcp.args, decoded.args);
1173 assert_eq!(mcp.env, decoded.env);
1174 assert_eq!(mcp.timeout_secs, decoded.timeout_secs);
1175 }
1176
1177 #[test]
1178 fn test_load_configs_from_dir() {
1179 let temp_dir = TempDir::new().expect("Failed to create temp dir");
1180 let agents_dir = temp_dir.path().join("agents");
1181 fs::create_dir_all(&agents_dir).expect("Failed to create agents dir");
1182
1183 let agent_content = r#"name: test-agent
1185model: fast
1186max_tool_iterations: 5
1187parallel_tools: false
1188tools[0]:
1189system_prompt: Test agent prompt"#;
1190
1191 fs::write(agents_dir.join("test-agent.toon"), agent_content)
1192 .expect("Failed to write agent file");
1193
1194 let agents = load_configs_from_dir::<ToonAgentConfig>(&agents_dir, "agents")
1195 .expect("Failed to load agents");
1196
1197 assert_eq!(agents.len(), 1);
1198 let agent = agents.get("test-agent").expect("Agent not found");
1199 assert_eq!(agent.name, "test-agent");
1200 assert_eq!(agent.model, "fast");
1201 assert_eq!(agent.max_tool_iterations, 5);
1202 }
1203
1204 #[test]
1205 fn test_dynamic_config_validation() {
1206 let mut config = DynamicConfig::default();
1207
1208 config.models.insert(
1210 "fast".to_string(),
1211 ToonModelConfig::new("fast", "ollama-local", "ministral-3:3b"),
1212 );
1213
1214 config
1216 .tools
1217 .insert("calculator".to_string(), ToonToolConfig::new("calculator"));
1218
1219 let mut agent = ToonAgentConfig::new("router", "fast");
1221 agent.tools = vec!["calculator".to_string()];
1222 config.agents.insert("router".to_string(), agent);
1223
1224 config.workflows.insert(
1226 "default".to_string(),
1227 ToonWorkflowConfig::new("default", "router"),
1228 );
1229
1230 let warnings = config.validate().expect("Validation failed");
1232 assert!(warnings.is_empty());
1233 }
1234
1235 #[test]
1236 fn test_dynamic_config_validation_missing_model() {
1237 let mut config = DynamicConfig::default();
1238
1239 let agent = ToonAgentConfig::new("router", "non-existent-model");
1241 config.agents.insert("router".to_string(), agent);
1242
1243 let result = config.validate();
1245 assert!(result.is_err());
1246 assert!(result.unwrap_err().to_string().contains("unknown model"));
1247 }
1248
1249 #[test]
1250 fn test_dynamic_config_validation_missing_tool() {
1251 let mut config = DynamicConfig::default();
1252
1253 config.models.insert(
1255 "fast".to_string(),
1256 ToonModelConfig::new("fast", "ollama-local", "ministral-3:3b"),
1257 );
1258
1259 let mut agent = ToonAgentConfig::new("router", "fast");
1261 agent.tools = vec!["non-existent-tool".to_string()];
1262 config.agents.insert("router".to_string(), agent);
1263
1264 let result = config.validate();
1266 assert!(result.is_err());
1267 assert!(result.unwrap_err().to_string().contains("unknown tool"));
1268 }
1269
1270 #[test]
1271 fn test_parse_agent_from_toon_string() {
1272 let toon = r#"name: router
1273model: fast
1274max_tool_iterations: 1
1275parallel_tools: false
1276tools[0]:
1277system_prompt: You are a routing agent."#;
1278
1279 let agent = ToonAgentConfig::from_toon(toon).expect("Failed to parse");
1280 assert_eq!(agent.name, "router");
1281 assert_eq!(agent.model, "fast");
1282 assert_eq!(agent.max_tool_iterations, 1);
1283 assert!(!agent.parallel_tools);
1284 assert!(agent.tools.is_empty());
1285 }
1286
1287 #[test]
1288 fn test_parse_model_from_toon_string() {
1289 let toon = r#"name: fast
1290provider: ollama-local
1291model: ministral-3:3b
1292temperature: 0.7
1293max_tokens: 256"#;
1294
1295 let model = ToonModelConfig::from_toon(toon).expect("Failed to parse");
1296 assert_eq!(model.name, "fast");
1297 assert_eq!(model.provider, "ollama-local");
1298 assert_eq!(model.model, "ministral-3:3b");
1299 assert!((model.temperature - 0.7).abs() < 0.01);
1300 assert_eq!(model.max_tokens, 256);
1301 }
1302}