1use arc_swap::ArcSwap;
35#[cfg(unix)]
36type ConfigFsNotify = notify::INotifyWatcher;
37#[cfg(windows)]
38type ConfigFsNotify = notify::ReadDirectoryChangesWatcher;
39#[cfg(not(any(unix, windows)))]
40type ConfigFsNotify = notify::PollWatcher;
41use notify::{Event, RecursiveMode, Watcher};
42use serde::{Deserialize, Serialize};
43use std::any::TypeId;
44use std::collections::HashMap;
45use std::fs;
46use std::path::{Path, PathBuf};
47use std::sync::Arc;
48use toon_format::{decode_default, encode_default, ToonError};
49use tracing::{debug, error, info, warn};
50
51#[derive(Debug, Clone, Serialize, Deserialize, PartialEq)]
61pub struct ToonAgentConfig {
62 pub name: String,
64
65 #[serde(default = "default_version")]
68 pub version: String,
69
70 pub model: String,
72
73 #[serde(default)]
75 pub system_prompt: Option<String>,
76
77 #[serde(default)]
79 pub tools: Vec<String>,
80
81 #[serde(default, skip_serializing_if = "Option::is_none")]
84 pub allowed_tools: Option<Vec<String>>,
85
86 #[serde(default = "default_max_tool_iterations")]
88 pub max_tool_iterations: usize,
89
90 #[serde(default)]
92 pub parallel_tools: bool,
93
94 #[serde(flatten)]
96 pub extra: HashMap<String, serde_json::Value>,
97}
98
99fn default_version() -> String {
100 "0.1.0".to_string()
101}
102
103fn default_max_tool_iterations() -> usize {
104 10
105}
106
107impl ToonAgentConfig {
108 pub fn new(name: impl Into<String>, model: impl Into<String>) -> Self {
110 Self {
111 name: name.into(),
112 version: default_version(),
113 model: model.into(),
114 system_prompt: None,
115 tools: Vec::new(),
116 allowed_tools: None,
117 max_tool_iterations: default_max_tool_iterations(),
118 parallel_tools: false,
119 extra: HashMap::new(),
120 }
121 }
122
123 pub fn with_system_prompt(mut self, prompt: impl Into<String>) -> Self {
125 self.system_prompt = Some(prompt.into());
126 self
127 }
128
129 pub fn with_tools(mut self, tools: Vec<String>) -> Self {
131 self.tools = tools;
132 self
133 }
134
135 pub fn to_toon(&self) -> Result<String, ToonConfigError> {
137 encode_default(self).map_err(ToonConfigError::from)
138 }
139
140 pub fn from_toon(toon: &str) -> Result<Self, ToonConfigError> {
142 decode_default(toon).map_err(ToonConfigError::from)
143 }
144}
145
146#[derive(Debug, Clone, Serialize, Deserialize, PartialEq)]
153pub struct ToonModelConfig {
154 pub name: String,
156
157 pub provider: String,
159
160 pub model: String,
162
163 #[serde(default = "default_temperature")]
165 pub temperature: f32,
166
167 #[serde(default = "default_max_tokens")]
169 pub max_tokens: u32,
170
171 #[serde(default)]
173 pub top_p: Option<f32>,
174
175 #[serde(default)]
177 pub frequency_penalty: Option<f32>,
178
179 #[serde(default)]
181 pub presence_penalty: Option<f32>,
182}
183
184fn default_temperature() -> f32 {
185 0.7
186}
187
188fn default_max_tokens() -> u32 {
189 512
190}
191
192impl ToonModelConfig {
193 pub fn new(
195 name: impl Into<String>,
196 provider: impl Into<String>,
197 model: impl Into<String>,
198 ) -> Self {
199 Self {
200 name: name.into(),
201 provider: provider.into(),
202 model: model.into(),
203 temperature: default_temperature(),
204 max_tokens: default_max_tokens(),
205 top_p: None,
206 frequency_penalty: None,
207 presence_penalty: None,
208 }
209 }
210
211 pub fn to_toon(&self) -> Result<String, ToonConfigError> {
213 encode_default(self).map_err(ToonConfigError::from)
214 }
215
216 pub fn from_toon(toon: &str) -> Result<Self, ToonConfigError> {
218 decode_default(toon).map_err(ToonConfigError::from)
219 }
220}
221
222#[derive(Debug, Clone, Serialize, Deserialize, PartialEq)]
228pub struct ToonToolConfig {
229 pub name: String,
231
232 #[serde(default = "default_true")]
234 pub enabled: bool,
235
236 #[serde(default)]
238 pub description: Option<String>,
239
240 #[serde(default = "default_timeout")]
242 pub timeout_secs: u64,
243
244 #[serde(flatten)]
246 pub extra: HashMap<String, serde_json::Value>,
247}
248
249fn default_true() -> bool {
250 true
251}
252
253fn default_timeout() -> u64 {
254 30
255}
256
257impl ToonToolConfig {
258 pub fn new(name: impl Into<String>) -> Self {
260 Self {
261 name: name.into(),
262 enabled: default_true(),
263 description: None,
264 timeout_secs: default_timeout(),
265 extra: HashMap::new(),
266 }
267 }
268
269 pub fn to_toon(&self) -> Result<String, ToonConfigError> {
271 encode_default(self).map_err(ToonConfigError::from)
272 }
273
274 pub fn from_toon(toon: &str) -> Result<Self, ToonConfigError> {
276 decode_default(toon).map_err(ToonConfigError::from)
277 }
278}
279
280#[derive(Debug, Clone, Serialize, Deserialize, PartialEq)]
287pub struct ToonWorkflowConfig {
288 pub name: String,
290
291 pub entry_agent: String,
293
294 #[serde(default)]
296 pub fallback_agent: Option<String>,
297
298 #[serde(default = "default_max_depth")]
300 pub max_depth: u8,
301
302 #[serde(default = "default_max_iterations")]
304 pub max_iterations: u8,
305
306 #[serde(default)]
308 pub parallel_subagents: bool,
309}
310
311fn default_max_depth() -> u8 {
312 3
313}
314
315fn default_max_iterations() -> u8 {
316 5
317}
318
319impl ToonWorkflowConfig {
320 pub fn new(name: impl Into<String>, entry_agent: impl Into<String>) -> Self {
322 Self {
323 name: name.into(),
324 entry_agent: entry_agent.into(),
325 fallback_agent: None,
326 max_depth: default_max_depth(),
327 max_iterations: default_max_iterations(),
328 parallel_subagents: false,
329 }
330 }
331
332 pub fn to_toon(&self) -> Result<String, ToonConfigError> {
334 encode_default(self).map_err(ToonConfigError::from)
335 }
336
337 pub fn from_toon(toon: &str) -> Result<Self, ToonConfigError> {
339 decode_default(toon).map_err(ToonConfigError::from)
340 }
341}
342
343#[derive(Debug, Clone, Serialize, Deserialize, PartialEq)]
350pub struct ToonMcpConfig {
351 pub name: String,
353
354 #[serde(default = "default_true")]
356 pub enabled: bool,
357
358 #[serde(default)]
360 pub command: Option<String>,
361
362 #[serde(default)]
364 pub args: Vec<String>,
365
366 #[serde(default)]
368 pub env: HashMap<String, String>,
369
370 #[serde(default = "default_timeout")]
372 pub timeout_secs: u64,
373}
374
375impl ToonMcpConfig {
376 pub fn new(name: impl Into<String>, command: impl Into<String>) -> Self {
378 Self {
379 name: name.into(),
380 enabled: default_true(),
381 command: Some(command.into()),
382 args: Vec::new(),
383 env: HashMap::new(),
384 timeout_secs: default_timeout(),
385 }
386 }
387
388 pub fn to_toon(&self) -> Result<String, ToonConfigError> {
390 encode_default(self).map_err(ToonConfigError::from)
391 }
392
393 pub fn from_toon(toon: &str) -> Result<Self, ToonConfigError> {
395 decode_default(toon).map_err(ToonConfigError::from)
396 }
397}
398
399#[derive(Debug, Clone, Default)]
407pub struct DynamicConfig {
408 pub agents: HashMap<String, ToonAgentConfig>,
410 pub models: HashMap<String, ToonModelConfig>,
412 pub tools: HashMap<String, ToonToolConfig>,
414 pub workflows: HashMap<String, ToonWorkflowConfig>,
416 pub mcps: HashMap<String, ToonMcpConfig>,
418}
419
420impl DynamicConfig {
421 pub fn load(
423 agents_dir: &Path,
424 models_dir: &Path,
425 tools_dir: &Path,
426 workflows_dir: &Path,
427 mcps_dir: &Path,
428 ) -> Result<Self, ToonConfigError> {
429 let agents = load_configs_from_dir::<ToonAgentConfig>(agents_dir, "agents")?;
430 let models = load_configs_from_dir::<ToonModelConfig>(models_dir, "models")?;
431 let tools = load_configs_from_dir::<ToonToolConfig>(tools_dir, "tools")?;
432 let workflows = load_configs_from_dir::<ToonWorkflowConfig>(workflows_dir, "workflows")?;
433 let mcps = load_configs_from_dir::<ToonMcpConfig>(mcps_dir, "mcps")?;
434
435 info!(
436 "Loaded dynamic config: {} agents, {} models, {} tools, {} workflows, {} mcps",
437 agents.len(),
438 models.len(),
439 tools.len(),
440 workflows.len(),
441 mcps.len()
442 );
443
444 Ok(Self {
445 agents,
446 models,
447 tools,
448 workflows,
449 mcps,
450 })
451 }
452
453 pub fn get_agent(&self, name: &str) -> Option<&ToonAgentConfig> {
455 self.agents.get(name)
456 }
457
458 pub fn get_model(&self, name: &str) -> Option<&ToonModelConfig> {
460 self.models.get(name)
461 }
462
463 pub fn get_tool(&self, name: &str) -> Option<&ToonToolConfig> {
465 self.tools.get(name)
466 }
467
468 pub fn get_workflow(&self, name: &str) -> Option<&ToonWorkflowConfig> {
470 self.workflows.get(name)
471 }
472
473 pub fn get_mcp(&self, name: &str) -> Option<&ToonMcpConfig> {
475 self.mcps.get(name)
476 }
477
478 pub fn agent_names(&self) -> Vec<&str> {
480 self.agents.keys().map(|s| s.as_str()).collect()
481 }
482
483 pub fn model_names(&self) -> Vec<&str> {
485 self.models.keys().map(|s| s.as_str()).collect()
486 }
487
488 pub fn tool_names(&self) -> Vec<&str> {
490 self.tools.keys().map(|s| s.as_str()).collect()
491 }
492
493 pub fn workflow_names(&self) -> Vec<&str> {
495 self.workflows.keys().map(|s| s.as_str()).collect()
496 }
497
498 pub fn mcp_names(&self) -> Vec<&str> {
500 self.mcps.keys().map(|s| s.as_str()).collect()
501 }
502
503 pub fn validate(&self) -> Result<Vec<ConfigWarning>, ToonConfigError> {
505 let mut warnings = Vec::new();
506
507 for (agent_name, agent) in &self.agents {
509 if !self.models.contains_key(&agent.model) {
510 return Err(ToonConfigError::Validation(format!(
511 "Agent '{}' references unknown model '{}'",
512 agent_name, agent.model
513 )));
514 }
515
516 for tool_name in &agent.tools {
518 if !self.tools.contains_key(tool_name) {
519 return Err(ToonConfigError::Validation(format!(
520 "Agent '{}' references unknown tool '{}'",
521 agent_name, tool_name
522 )));
523 }
524 }
525 }
526
527 for (workflow_name, workflow) in &self.workflows {
529 if !self.agents.contains_key(&workflow.entry_agent) {
530 return Err(ToonConfigError::Validation(format!(
531 "Workflow '{}' references unknown entry agent '{}'",
532 workflow_name, workflow.entry_agent
533 )));
534 }
535
536 if let Some(ref fallback) = workflow.fallback_agent {
537 if !self.agents.contains_key(fallback) {
538 return Err(ToonConfigError::Validation(format!(
539 "Workflow '{}' references unknown fallback agent '{}'",
540 workflow_name, fallback
541 )));
542 }
543 }
544 }
545
546 let used_models: std::collections::HashSet<_> =
548 self.agents.values().map(|a| &a.model).collect();
549 for model_name in self.models.keys() {
550 if !used_models.contains(model_name) {
551 warnings.push(ConfigWarning {
552 kind: WarningKind::UnusedModel,
553 message: format!("Model '{}' is not used by any agent", model_name),
554 });
555 }
556 }
557
558 let used_tools: std::collections::HashSet<_> =
560 self.agents.values().flat_map(|a| a.tools.iter()).collect();
561 for tool_name in self.tools.keys() {
562 if !used_tools.contains(tool_name) {
563 warnings.push(ConfigWarning {
564 kind: WarningKind::UnusedTool,
565 message: format!("Tool '{}' is not used by any agent", tool_name),
566 });
567 }
568 }
569
570 Ok(warnings)
571 }
572}
573
574pub trait HasName {
581 fn name(&self) -> &str;
583}
584
585impl HasName for ToonAgentConfig {
586 fn name(&self) -> &str {
587 &self.name
588 }
589}
590
591impl HasName for ToonModelConfig {
592 fn name(&self) -> &str {
593 &self.name
594 }
595}
596
597impl HasName for ToonToolConfig {
598 fn name(&self) -> &str {
599 &self.name
600 }
601}
602
603impl HasName for ToonWorkflowConfig {
604 fn name(&self) -> &str {
605 &self.name
606 }
607}
608
609impl HasName for ToonMcpConfig {
610 fn name(&self) -> &str {
611 &self.name
612 }
613}
614
615fn load_configs_from_dir<T>(
617 dir: &Path,
618 config_type: &str,
619) -> Result<HashMap<String, T>, ToonConfigError>
620where
621 T: for<'de> Deserialize<'de> + HasName,
622{
623 let mut configs = HashMap::new();
624
625 if !dir.exists() {
626 debug!("Config directory does not exist: {:?}", dir);
627 return Ok(configs);
628 }
629
630 let entries = fs::read_dir(dir).map_err(|e| {
631 ToonConfigError::Io(std::io::Error::new(
632 e.kind(),
633 format!("Failed to read {} directory {:?}: {}", config_type, dir, e),
634 ))
635 })?;
636
637 for entry in entries {
638 let entry = entry.map_err(ToonConfigError::Io)?;
639 let path = entry.path();
640
641 if path.extension().and_then(|e| e.to_str()) != Some("toon") {
643 continue;
644 }
645
646 match load_toon_file::<T>(&path) {
647 Ok(config) => {
648 let name = config.name().to_string();
649 debug!("Loaded {} config: {}", config_type, name);
650 configs.insert(name, config);
651 }
652 Err(e) => {
653 warn!("Failed to load {} from {:?}: {}", config_type, path, e);
654 }
655 }
656 }
657
658 Ok(configs)
659}
660
661fn load_toon_file<T>(path: &Path) -> Result<T, ToonConfigError>
663where
664 T: for<'de> Deserialize<'de>,
665{
666 let content = fs::read_to_string(path).map_err(|e| {
667 ToonConfigError::Io(std::io::Error::new(
668 e.kind(),
669 format!("Failed to read {:?}: {}", path, e),
670 ))
671 })?;
672
673 match decode_default(&content) {
674 Ok(config) => Ok(config),
675 Err(toon_error) => toml::from_str(&content).map_err(|toml_error| {
676 ToonConfigError::Parse(format!(
677 "Failed to parse {:?} as TOON ({}) or TOML ({})",
678 path, toon_error, toml_error
679 ))
680 }),
681 }
682}
683
684#[derive(Debug, thiserror::Error)]
688pub enum ToonConfigError {
689 #[error("IO error: {0}")]
691 Io(#[from] std::io::Error),
692
693 #[error("TOON parse error: {0}")]
695 Parse(String),
696
697 #[error("Validation error: {0}")]
699 Validation(String),
700
701 #[error("Watch error: {0}")]
703 Watch(#[from] notify::Error),
704}
705
706impl From<ToonError> for ToonConfigError {
707 fn from(e: ToonError) -> Self {
708 ToonConfigError::Parse(e.to_string())
709 }
710}
711
712#[derive(Debug, Clone)]
714pub struct ConfigWarning {
715 pub kind: WarningKind,
717
718 pub message: String,
720}
721
722#[derive(Debug, Clone, PartialEq)]
724pub enum WarningKind {
725 UnusedModel,
727
728 UnusedTool,
730
731 UnusedAgent,
733
734 UnusedWorkflow,
736
737 UnusedMcp,
739}
740
741impl std::fmt::Display for ConfigWarning {
742 fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
743 write!(f, "{}", self.message)
744 }
745}
746
747pub struct DynamicConfigManager {
775 config: Arc<ArcSwap<DynamicConfig>>,
776 agents_dir: PathBuf,
777 models_dir: PathBuf,
778 tools_dir: PathBuf,
779 workflows_dir: PathBuf,
780 mcps_dir: PathBuf,
781 _watcher: Option<ConfigFsNotify>,
782 version_tx:
786 Arc<std::sync::Mutex<Option<tokio::sync::mpsc::UnboundedSender<Vec<ToonAgentConfig>>>>>,
787}
788
789impl DynamicConfigManager {
790 pub fn from_config(
795 config: &crate::overlay::AresConfig,
796 ) -> Result<Self, ToonConfigError> {
797 let agents_dir = PathBuf::from(&config.config.agents_dir);
798 let models_dir = PathBuf::from(&config.config.models_dir);
799 let tools_dir = PathBuf::from(&config.config.tools_dir);
800 let workflows_dir = PathBuf::from(&config.config.workflows_dir);
801 let mcps_dir = PathBuf::from(&config.config.mcps_dir);
802
803 Self::new(
804 agents_dir,
805 models_dir,
806 tools_dir,
807 workflows_dir,
808 mcps_dir,
809 false, )
811 }
812
813 pub fn reload(&self) -> Result<Vec<ConfigWarning>, ToonConfigError> {
815 let new_config = DynamicConfig::load(
816 &self.agents_dir,
817 &self.models_dir,
818 &self.tools_dir,
819 &self.workflows_dir,
820 &self.mcps_dir,
821 )?;
822 match new_config.validate() {
823 Ok(warnings) => {
824 for warning in &warnings {
825 warn!("Config warning: {}", warning);
826 }
827 let agents: Vec<ToonAgentConfig> = new_config.agents.values().cloned().collect();
828 if let Ok(guard) = self.version_tx.lock() {
829 if let Some(tx) = guard.as_ref() {
830 let _ = tx.send(agents);
831 }
832 }
833 self.config.store(Arc::new(new_config));
834 info!("Config reloaded successfully");
835 Ok(warnings)
836 }
837 Err(e) => {
838 error!("Config validation failed, keeping old config: {}", e);
839 Err(e)
840 }
841 }
842 }
843
844 pub fn new(
854 agents_dir: PathBuf,
855 models_dir: PathBuf,
856 tools_dir: PathBuf,
857 workflows_dir: PathBuf,
858 mcps_dir: PathBuf,
859 hot_reload: bool,
860 ) -> Result<Self, ToonConfigError> {
861 let initial_config = DynamicConfig::load(
863 &agents_dir,
864 &models_dir,
865 &tools_dir,
866 &workflows_dir,
867 &mcps_dir,
868 )?;
869
870 let config = Arc::new(ArcSwap::from_pointee(initial_config));
871
872 let version_tx: Arc<
874 std::sync::Mutex<Option<tokio::sync::mpsc::UnboundedSender<Vec<ToonAgentConfig>>>>,
875 > = Arc::new(std::sync::Mutex::new(None));
876
877 let _ = hot_reload;
880 let watcher = None;
881
882 Ok(Self {
883 config,
884 agents_dir,
885 models_dir,
886 tools_dir,
887 workflows_dir,
888 mcps_dir,
889 _watcher: watcher,
890 version_tx,
891 })
892 }
893
894 pub fn set_version_tx(&self, tx: tokio::sync::mpsc::UnboundedSender<Vec<ToonAgentConfig>>) {
897 if let Ok(mut guard) = self.version_tx.lock() {
898 *guard = Some(tx);
899 }
900 }
901
902 #[allow(dead_code)]
904 fn setup_watcher(
905 config: Arc<ArcSwap<DynamicConfig>>,
906 agents_dir: PathBuf,
907 models_dir: PathBuf,
908 tools_dir: PathBuf,
909 workflows_dir: PathBuf,
910 mcps_dir: PathBuf,
911 version_tx: Arc<
912 std::sync::Mutex<Option<tokio::sync::mpsc::UnboundedSender<Vec<ToonAgentConfig>>>>,
913 >,
914 ) -> Result<ConfigFsNotify, ToonConfigError> {
915 let agents_dir_clone = agents_dir.clone();
916 let models_dir_clone = models_dir.clone();
917 let tools_dir_clone = tools_dir.clone();
918 let workflows_dir_clone = workflows_dir.clone();
919 let mcps_dir_clone = mcps_dir.clone();
920
921 let mut watcher = notify::recommended_watcher(move |res: Result<Event, notify::Error>| {
922 match res {
923 Ok(event) => {
924 if matches!(
926 event.kind,
927 notify::EventKind::Create(_)
928 | notify::EventKind::Modify(_)
929 | notify::EventKind::Remove(_)
930 ) {
931 info!("Config change detected, reloading...");
932
933 match DynamicConfig::load(
934 &agents_dir_clone,
935 &models_dir_clone,
936 &tools_dir_clone,
937 &workflows_dir_clone,
938 &mcps_dir_clone,
939 ) {
940 Ok(new_config) => {
941 match new_config.validate() {
943 Ok(warnings) => {
944 for warning in warnings {
945 warn!("Config warning: {}", warning);
946 }
947 let agents: Vec<ToonAgentConfig> =
949 new_config.agents.values().cloned().collect();
950 if let Ok(guard) = version_tx.lock() {
951 if let Some(tx) = guard.as_ref() {
952 let _ = tx.send(agents);
953 }
954 }
955 config.store(Arc::new(new_config));
956 info!("Config reloaded successfully");
957 }
958 Err(e) => {
959 error!(
960 "Config validation failed, keeping old config: {}",
961 e
962 );
963 }
964 }
965 }
966 Err(e) => {
967 error!("Failed to reload config: {}", e);
968 }
969 }
970 }
971 }
972 Err(e) => {
973 error!("Watch error: {:?}", e);
974 }
975 }
976 })?;
977
978 for dir in [
980 &agents_dir,
981 &models_dir,
982 &tools_dir,
983 &workflows_dir,
984 &mcps_dir,
985 ] {
986 if dir.exists() {
987 watcher.watch(dir, RecursiveMode::Recursive)?;
988 debug!("Watching directory: {:?}", dir);
989 }
990 }
991
992 Ok(watcher)
993 }
994
995 pub fn config(&self) -> arc_swap::Guard<Arc<DynamicConfig>> {
997 self.config.load()
998 }
999
1000 pub fn agent(&self, name: &str) -> Option<ToonAgentConfig> {
1002 self.config.load().get_agent(name).cloned()
1003 }
1004
1005 pub fn model(&self, name: &str) -> Option<ToonModelConfig> {
1007 self.config.load().get_model(name).cloned()
1008 }
1009
1010 pub fn tool(&self, name: &str) -> Option<ToonToolConfig> {
1012 self.config.load().get_tool(name).cloned()
1013 }
1014
1015 pub fn workflow(&self, name: &str) -> Option<ToonWorkflowConfig> {
1017 self.config.load().get_workflow(name).cloned()
1018 }
1019
1020 pub fn mcp(&self, name: &str) -> Option<ToonMcpConfig> {
1022 self.config.load().get_mcp(name).cloned()
1023 }
1024
1025 pub fn agents(&self) -> Vec<ToonAgentConfig> {
1027 self.config.load().agents.values().cloned().collect()
1028 }
1029
1030 pub fn models(&self) -> Vec<ToonModelConfig> {
1032 self.config.load().models.values().cloned().collect()
1033 }
1034
1035 pub fn tools(&self) -> Vec<ToonToolConfig> {
1037 self.config.load().tools.values().cloned().collect()
1038 }
1039
1040 pub fn workflows(&self) -> Vec<ToonWorkflowConfig> {
1042 self.config.load().workflows.values().cloned().collect()
1043 }
1044
1045 pub fn mcps(&self) -> Vec<ToonMcpConfig> {
1047 self.config.load().mcps.values().cloned().collect()
1048 }
1049
1050 pub fn agent_names(&self) -> Vec<String> {
1052 self.config
1053 .load()
1054 .agent_names()
1055 .into_iter()
1056 .map(String::from)
1057 .collect()
1058 }
1059
1060 pub fn model_names(&self) -> Vec<String> {
1062 self.config
1063 .load()
1064 .model_names()
1065 .into_iter()
1066 .map(String::from)
1067 .collect()
1068 }
1069
1070 pub fn tool_names(&self) -> Vec<String> {
1072 self.config
1073 .load()
1074 .tool_names()
1075 .into_iter()
1076 .map(String::from)
1077 .collect()
1078 }
1079
1080 pub fn workflow_names(&self) -> Vec<String> {
1082 self.config
1083 .load()
1084 .workflow_names()
1085 .into_iter()
1086 .map(String::from)
1087 .collect()
1088 }
1089
1090 pub fn mcp_names(&self) -> Vec<String> {
1092 self.config
1093 .load()
1094 .mcp_names()
1095 .into_iter()
1096 .map(String::from)
1097 .collect()
1098 }
1099
1100 pub fn upsert_agent(&self, agent: ToonAgentConfig) {
1103 let current = self.config.load();
1104 let mut new_agents = current.agents.clone();
1105 new_agents.insert(agent.name.clone(), agent);
1106 let new_config = DynamicConfig {
1107 agents: new_agents,
1108 models: current.models.clone(),
1109 tools: current.tools.clone(),
1110 workflows: current.workflows.clone(),
1111 mcps: current.mcps.clone(),
1112 };
1113 self.config.store(Arc::new(new_config));
1114 }
1115}
1116
1117impl cordis::Service for DynamicConfigManager {
1118 fn name(&self) -> &'static str { "dynamic_config_manager" }
1119 fn init(&self, _ctx: &std::sync::Arc<cordis::Context>) -> cordis::ServiceInitFuture<'_> {
1120 Box::pin(async { Ok(None) })
1122 }
1123 fn check(&self) -> bool { true }
1124}
1125
1126pub(crate) fn notify_tools_and_execute(ctx: &Arc<cordis::Context>) {
1131 if let Some(reflect) = ctx.get::<cordis::ReflectService>() {
1132 reflect.notify(TypeId::of::<ares_tools::Tools>());
1133 reflect.notify(TypeId::of::<ares_agent::Execute>());
1134 }
1135}
1136
1137fn json_extra_to_toml(extra: &HashMap<String, serde_json::Value>) -> HashMap<String, toml::Value> {
1138 extra
1139 .iter()
1140 .filter_map(|(k, v)| {
1141 serde_json::from_value::<toml::Value>(v.clone())
1142 .ok()
1143 .map(|tv| (k.clone(), tv))
1144 })
1145 .collect()
1146}
1147
1148fn toon_to_agent_config(t: &ToonAgentConfig) -> ares_agent::AgentConfig {
1149 ares_agent::AgentConfig {
1150 model: t.model.clone(),
1151 system_prompt: t.system_prompt.clone(),
1152 tools: t.tools.clone(),
1153 allowed_tools: t.allowed_tools.clone(),
1154 max_tool_iterations: t.max_tool_iterations,
1155 parallel_tools: t.parallel_tools,
1156 compaction_enabled: None,
1157 extra: json_extra_to_toml(&t.extra),
1158 }
1159}
1160
1161impl ares_agent::ToonAgents for DynamicConfigManager {
1162 fn get(&self, name: &str) -> Option<ares_agent::AgentConfig> {
1163 self.agent(name).as_ref().map(toon_to_agent_config)
1164 }
1165 fn names(&self) -> Vec<String> {
1166 self.agent_names()
1167 }
1168}
1169
1170#[cfg(test)]
1173mod tests {
1174 use super::*;
1175 use tempfile::TempDir;
1176
1177 #[test]
1178 fn test_agent_config_roundtrip() {
1179 let agent = ToonAgentConfig::new("test-agent", "fast")
1180 .with_system_prompt("You are a test agent.")
1181 .with_tools(vec!["calculator".to_string(), "web_search".to_string()]);
1182
1183 let toon = agent.to_toon().expect("Failed to encode");
1184 let decoded = ToonAgentConfig::from_toon(&toon).expect("Failed to decode");
1185
1186 assert_eq!(agent.name, decoded.name);
1187 assert_eq!(agent.model, decoded.model);
1188 assert_eq!(agent.system_prompt, decoded.system_prompt);
1189 assert_eq!(agent.tools, decoded.tools);
1190 }
1191
1192 #[test]
1193 fn test_model_config_roundtrip() {
1194 let model = ToonModelConfig::new("fast", "ollama-local", "ministral-3:3b");
1195
1196 let toon = model.to_toon().expect("Failed to encode");
1197 let decoded = ToonModelConfig::from_toon(&toon).expect("Failed to decode");
1198
1199 assert_eq!(model.name, decoded.name);
1200 assert_eq!(model.provider, decoded.provider);
1201 assert_eq!(model.model, decoded.model);
1202 assert_eq!(model.temperature, decoded.temperature);
1203 assert_eq!(model.max_tokens, decoded.max_tokens);
1204 }
1205
1206 #[test]
1207 fn test_tool_config_roundtrip() {
1208 let mut tool = ToonToolConfig::new("calculator");
1209 tool.description = Some("Performs arithmetic operations".to_string());
1210 tool.timeout_secs = 10;
1211
1212 let toon = tool.to_toon().expect("Failed to encode");
1213 let decoded = ToonToolConfig::from_toon(&toon).expect("Failed to decode");
1214
1215 assert_eq!(tool.name, decoded.name);
1216 assert_eq!(tool.enabled, decoded.enabled);
1217 assert_eq!(tool.description, decoded.description);
1218 assert_eq!(tool.timeout_secs, decoded.timeout_secs);
1219 }
1220
1221 #[test]
1222 fn test_workflow_config_roundtrip() {
1223 let mut workflow = ToonWorkflowConfig::new("default", "router");
1224 workflow.fallback_agent = Some("orchestrator".to_string());
1225 workflow.max_depth = 3;
1226 workflow.max_iterations = 5;
1227
1228 let toon = workflow.to_toon().expect("Failed to encode");
1229 let decoded = ToonWorkflowConfig::from_toon(&toon).expect("Failed to decode");
1230
1231 assert_eq!(workflow.name, decoded.name);
1232 assert_eq!(workflow.entry_agent, decoded.entry_agent);
1233 assert_eq!(workflow.fallback_agent, decoded.fallback_agent);
1234 assert_eq!(workflow.max_depth, decoded.max_depth);
1235 assert_eq!(workflow.max_iterations, decoded.max_iterations);
1236 }
1237
1238 #[test]
1239 fn test_mcp_config_roundtrip() {
1240 let mut mcp = ToonMcpConfig::new("filesystem", "npx");
1241 mcp.args = vec![
1242 "-y".to_string(),
1243 "@modelcontextprotocol/server-filesystem".to_string(),
1244 "/home".to_string(),
1245 "/tmp".to_string(),
1246 ];
1247 mcp.env
1248 .insert("NODE_ENV".to_string(), "production".to_string());
1249 mcp.timeout_secs = 30;
1250
1251 let toon = mcp.to_toon().expect("Failed to encode");
1252 let decoded = ToonMcpConfig::from_toon(&toon).expect("Failed to decode");
1253
1254 assert_eq!(mcp.name, decoded.name);
1255 assert_eq!(mcp.command, decoded.command);
1256 assert_eq!(mcp.args, decoded.args);
1257 assert_eq!(mcp.env, decoded.env);
1258 assert_eq!(mcp.timeout_secs, decoded.timeout_secs);
1259 }
1260
1261 #[test]
1262 fn test_load_configs_from_dir() {
1263 let temp_dir = TempDir::new().expect("Failed to create temp dir");
1264 let agents_dir = temp_dir.path().join("agents");
1265 fs::create_dir_all(&agents_dir).expect("Failed to create agents dir");
1266
1267 let agent_content = r#"name: test-agent
1269model: fast
1270max_tool_iterations: 5
1271parallel_tools: false
1272tools[0]:
1273system_prompt: Test agent prompt"#;
1274
1275 fs::write(agents_dir.join("test-agent.toon"), agent_content)
1276 .expect("Failed to write agent file");
1277
1278 let agents = load_configs_from_dir::<ToonAgentConfig>(&agents_dir, "agents")
1279 .expect("Failed to load agents");
1280
1281 assert_eq!(agents.len(), 1);
1282 let agent = agents.get("test-agent").expect("Agent not found");
1283 assert_eq!(agent.name, "test-agent");
1284 assert_eq!(agent.model, "fast");
1285 assert_eq!(agent.max_tool_iterations, 5);
1286 }
1287
1288 #[test]
1289 fn test_load_toml_shaped_toon_config_from_dir() {
1290 let temp_dir = TempDir::new().expect("Failed to create temp dir");
1291 let mcps_dir = temp_dir.path().join("mcps");
1292 fs::create_dir_all(&mcps_dir).expect("Failed to create mcps dir");
1293
1294 let mcp_content = r#"name = "eruka"
1295enabled = true
1296endpoint = "https://eruka.dirmacs.com/mcp"
1297transport = "http"
1298timeout_secs = 30
1299"#;
1300
1301 fs::write(mcps_dir.join("eruka.toon"), mcp_content).expect("Failed to write mcp file");
1302
1303 let mcps =
1304 load_configs_from_dir::<ToonMcpConfig>(&mcps_dir, "mcps").expect("Failed to load mcps");
1305
1306 let mcp = mcps.get("eruka").expect("MCP not found");
1307 assert_eq!(mcp.name, "eruka");
1308 assert!(mcp.enabled);
1309 assert_eq!(mcp.timeout_secs, 30);
1310 }
1311
1312 #[test]
1313 fn test_dynamic_config_validation() {
1314 let mut config = DynamicConfig::default();
1315
1316 config.models.insert(
1318 "fast".to_string(),
1319 ToonModelConfig::new("fast", "ollama-local", "ministral-3:3b"),
1320 );
1321
1322 config
1324 .tools
1325 .insert("calculator".to_string(), ToonToolConfig::new("calculator"));
1326
1327 let mut agent = ToonAgentConfig::new("router", "fast");
1329 agent.tools = vec!["calculator".to_string()];
1330 config.agents.insert("router".to_string(), agent);
1331
1332 config.workflows.insert(
1334 "default".to_string(),
1335 ToonWorkflowConfig::new("default", "router"),
1336 );
1337
1338 let warnings = config.validate().expect("Validation failed");
1340 assert!(warnings.is_empty());
1341 }
1342
1343 #[test]
1344 fn test_dynamic_config_validation_missing_model() {
1345 let mut config = DynamicConfig::default();
1346
1347 let agent = ToonAgentConfig::new("router", "non-existent-model");
1349 config.agents.insert("router".to_string(), agent);
1350
1351 let err = config.validate().expect_err("expected validation error");
1352 assert_eq!(
1353 err.to_string(),
1354 "Validation error: Agent 'router' references unknown model 'non-existent-model'"
1355 );
1356 match err {
1357 ToonConfigError::Validation(msg) => {
1358 assert_eq!(
1359 msg,
1360 "Agent 'router' references unknown model 'non-existent-model'"
1361 );
1362 }
1363 other => panic!("expected Validation error, got {other:?}"),
1364 }
1365 }
1366
1367 #[test]
1368 fn test_dynamic_config_validation_missing_tool() {
1369 let mut config = DynamicConfig::default();
1370
1371 config.models.insert(
1373 "fast".to_string(),
1374 ToonModelConfig::new("fast", "ollama-local", "ministral-3:3b"),
1375 );
1376
1377 let mut agent = ToonAgentConfig::new("router", "fast");
1379 agent.tools = vec!["non-existent-tool".to_string()];
1380 config.agents.insert("router".to_string(), agent);
1381
1382 let err = config.validate().expect_err("expected validation error");
1383 match err {
1384 ToonConfigError::Validation(msg) => {
1385 assert_eq!(
1386 msg,
1387 "Agent 'router' references unknown tool 'non-existent-tool'"
1388 );
1389 }
1390 other => panic!("expected Validation error, got {other:?}"),
1391 }
1392 }
1393
1394 #[test]
1395 fn test_dynamic_config_validation_missing_entry_agent() {
1396 let mut config = DynamicConfig::default();
1397 config.workflows.insert(
1398 "default".to_string(),
1399 ToonWorkflowConfig::new("default", "missing-agent"),
1400 );
1401
1402 let err = config.validate().expect_err("expected validation error");
1403 assert_eq!(
1404 err.to_string(),
1405 "Validation error: Workflow 'default' references unknown entry agent 'missing-agent'"
1406 );
1407 }
1408
1409 #[test]
1410 fn test_dynamic_config_validation_missing_fallback_agent() {
1411 let mut config = DynamicConfig::default();
1412 config
1413 .agents
1414 .insert("router".to_string(), ToonAgentConfig::new("router", "fast"));
1415 config.models.insert(
1416 "fast".to_string(),
1417 ToonModelConfig::new("fast", "ollama", "llama3"),
1418 );
1419
1420 let mut workflow = ToonWorkflowConfig::new("default", "router");
1421 workflow.fallback_agent = Some("missing-fallback".to_string());
1422 config.workflows.insert("default".to_string(), workflow);
1423
1424 let err = config.validate().expect_err("expected validation error");
1425 assert_eq!(
1426 err.to_string(),
1427 "Validation error: Workflow 'default' references unknown fallback agent 'missing-fallback'"
1428 );
1429 }
1430
1431 #[test]
1432 fn test_dynamic_config_validation_unused_model_and_tool_warnings() {
1433 let mut config = DynamicConfig::default();
1434 config.models.insert(
1435 "fast".to_string(),
1436 ToonModelConfig::new("fast", "ollama", "llama3"),
1437 );
1438 config.models.insert(
1439 "slow".to_string(),
1440 ToonModelConfig::new("slow", "ollama", "llama3:70b"),
1441 );
1442 config
1443 .tools
1444 .insert("calc".to_string(), ToonToolConfig::new("calc"));
1445 config
1446 .tools
1447 .insert("search".to_string(), ToonToolConfig::new("search"));
1448
1449 let mut agent = ToonAgentConfig::new("router", "fast");
1450 agent.tools = vec!["calc".to_string()];
1451 config.agents.insert("router".to_string(), agent);
1452
1453 let warnings = config.validate().expect("validation should succeed");
1454 assert_eq!(warnings.len(), 2);
1455
1456 let unused_model = warnings
1457 .iter()
1458 .find(|w| w.kind == WarningKind::UnusedModel)
1459 .expect("unused model warning");
1460 assert_eq!(
1461 unused_model.message,
1462 "Model 'slow' is not used by any agent"
1463 );
1464 assert_eq!(unused_model.to_string(), unused_model.message);
1465
1466 let unused_tool = warnings
1467 .iter()
1468 .find(|w| w.kind == WarningKind::UnusedTool)
1469 .expect("unused tool warning");
1470 assert_eq!(
1471 unused_tool.message,
1472 "Tool 'search' is not used by any agent"
1473 );
1474 }
1475
1476 #[test]
1477 fn test_parse_agent_from_toon_string() {
1478 let toon = r#"name: router
1479model: fast
1480max_tool_iterations: 1
1481parallel_tools: false
1482tools[0]:
1483system_prompt: You are a routing agent."#;
1484
1485 let agent = ToonAgentConfig::from_toon(toon).expect("Failed to parse");
1486 assert_eq!(agent.name, "router");
1487 assert_eq!(agent.model, "fast");
1488 assert_eq!(agent.max_tool_iterations, 1);
1489 assert!(!agent.parallel_tools);
1490 assert!(agent.tools.is_empty());
1491 }
1492
1493 #[test]
1494 fn test_parse_model_from_toon_string() {
1495 let toon = r#"name: fast
1496provider: ollama-local
1497model: ministral-3:3b
1498temperature: 0.7
1499max_tokens: 256"#;
1500
1501 let model = ToonModelConfig::from_toon(toon).expect("Failed to parse");
1502 assert_eq!(model.name, "fast");
1503 assert_eq!(model.provider, "ollama-local");
1504 assert_eq!(model.model, "ministral-3:3b");
1505 assert!((model.temperature - 0.7).abs() < 0.01);
1506 assert_eq!(model.max_tokens, 256);
1507 }
1508 #[test]
1509 fn test_toon_agent_config_defaults() {
1510 let agent = ToonAgentConfig::new("router", "fast");
1511 assert_eq!(agent.version, "0.1.0");
1512 assert_eq!(agent.max_tool_iterations, 10);
1513 assert!(!agent.parallel_tools);
1514 assert!(agent.tools.is_empty());
1515 assert!(agent.system_prompt.is_none());
1516 }
1517
1518 #[test]
1519 fn test_toon_model_config_defaults() {
1520 let model = ToonModelConfig::new("fast", "ollama", "llama3");
1521 assert!((model.temperature - 0.7).abs() < 0.01);
1522 assert_eq!(model.max_tokens, 512);
1523 }
1524
1525 #[test]
1526 fn test_toon_tool_config_defaults() {
1527 let tool = ToonToolConfig::new("calc");
1528 assert!(tool.enabled);
1529 assert_eq!(tool.timeout_secs, 30);
1530 }
1531
1532 #[test]
1533 fn test_toon_workflow_config_defaults() {
1534 let wf = ToonWorkflowConfig::new("main", "router");
1535 assert_eq!(wf.max_depth, 3);
1536 assert_eq!(wf.max_iterations, 5);
1537 assert!(wf.fallback_agent.is_none());
1538 }
1539
1540 #[test]
1541 fn test_parse_tool_from_toon_string() {
1542 let toon = r#"name: calculator
1543enabled: true
1544timeout_secs: 15
1545description: Performs arithmetic"#;
1546
1547 let tool = ToonToolConfig::from_toon(toon).expect("Failed to parse");
1548 assert_eq!(tool.name, "calculator");
1549 assert!(tool.enabled);
1550 assert_eq!(tool.timeout_secs, 15);
1551 assert_eq!(
1552 tool.description.as_deref(),
1553 Some("Performs arithmetic")
1554 );
1555 }
1556
1557 #[test]
1558 fn test_parse_workflow_from_toon_string() {
1559 let toon = r#"name: default
1560entry_agent: router
1561fallback_agent: orchestrator
1562max_depth: 2
1563max_iterations: 4
1564parallel_subagents: true"#;
1565
1566 let workflow = ToonWorkflowConfig::from_toon(toon).expect("Failed to parse");
1567 assert_eq!(workflow.name, "default");
1568 assert_eq!(workflow.entry_agent, "router");
1569 assert_eq!(workflow.fallback_agent.as_deref(), Some("orchestrator"));
1570 assert_eq!(workflow.max_depth, 2);
1571 assert_eq!(workflow.max_iterations, 4);
1572 assert!(workflow.parallel_subagents);
1573 }
1574
1575 #[test]
1576 fn test_parse_agent_applies_serde_defaults_from_toon() {
1577 let toon = r#"name: minimal
1578model: fast"#;
1579
1580 let agent = ToonAgentConfig::from_toon(toon).expect("Failed to parse");
1581 assert_eq!(agent.version, "0.1.0");
1582 assert_eq!(agent.max_tool_iterations, 10);
1583 assert!(!agent.parallel_tools);
1584 assert!(agent.tools.is_empty());
1585 }
1586
1587 #[test]
1588 fn test_parse_invalid_toon_returns_parse_error() {
1589 let err = ToonAgentConfig::from_toon("name: [unclosed").expect_err("expected parse error");
1590 assert!(err.to_string().starts_with("TOON parse error: "));
1591 match err {
1592 ToonConfigError::Parse(msg) => assert!(!msg.is_empty()),
1593 other => panic!("expected Parse error, got {other:?}"),
1594 }
1595 }
1596
1597 #[test]
1598 fn test_load_toon_file_invalid_content() {
1599 let temp_dir = TempDir::new().expect("Failed to create temp dir");
1600 let path = temp_dir.path().join("broken.toon");
1601 fs::write(&path, "name: [unclosed").expect("Failed to write broken toon file");
1602
1603 let err = load_toon_file::<ToonAgentConfig>(&path).expect_err("expected parse error");
1604 let msg = err.to_string();
1605 assert!(msg.contains("Failed to parse"), "{msg}");
1606 assert!(msg.contains("broken.toon"), "{msg}");
1607 assert!(msg.contains("TOON"), "{msg}");
1608 assert!(msg.contains("TOML"), "{msg}");
1609 }
1610
1611 #[test]
1612 fn test_load_configs_from_dir_missing_directory_returns_empty() {
1613 let temp_dir = TempDir::new().expect("Failed to create temp dir");
1614 let missing = temp_dir.path().join("does-not-exist");
1615
1616 let agents =
1617 load_configs_from_dir::<ToonAgentConfig>(&missing, "agents").expect("should succeed");
1618 assert!(agents.is_empty());
1619 }
1620
1621 #[test]
1622 fn test_load_configs_from_dir_skips_non_toon_files() {
1623 let temp_dir = TempDir::new().expect("Failed to create temp dir");
1624 let agents_dir = temp_dir.path().join("agents");
1625 fs::create_dir_all(&agents_dir).expect("Failed to create agents dir");
1626
1627 fs::write(agents_dir.join("notes.txt"), "ignore me").expect("Failed to write txt file");
1628 fs::write(
1629 agents_dir.join("valid.toon"),
1630 "name: router
1631model: fast
1632",
1633 )
1634 .expect("Failed to write toon file");
1635
1636 let agents = load_configs_from_dir::<ToonAgentConfig>(&agents_dir, "agents")
1637 .expect("Failed to load agents");
1638 assert_eq!(agents.len(), 1);
1639 assert!(agents.contains_key("router"));
1640 }
1641
1642 #[test]
1643 fn test_toon_mcp_config_defaults() {
1644 let mcp = ToonMcpConfig::new("filesystem", "npx");
1645 assert!(mcp.enabled);
1646 assert_eq!(mcp.timeout_secs, 30);
1647 assert_eq!(mcp.command.as_deref(), Some("npx"));
1648 assert!(mcp.args.is_empty());
1649 assert!(mcp.env.is_empty());
1650 }
1651
1652 #[test]
1653 fn test_dynamic_config_paths_resolve_under_cwd() {
1654 let paths = crate::overlay::DynamicConfigPaths::default();
1655 assert!(paths.agents_dir.is_relative());
1656 assert!(paths.hot_reload);
1657 }
1658
1659
1660 #[test]
1661 fn test_dynamic_config_load_from_directories() {
1662 let temp_dir = TempDir::new().expect("Failed to create temp dir");
1663 let root = temp_dir.path();
1664
1665 let agents_dir = root.join("agents");
1666 let models_dir = root.join("models");
1667 let tools_dir = root.join("tools");
1668 let workflows_dir = root.join("workflows");
1669 let mcps_dir = root.join("mcps");
1670 for dir in [&agents_dir, &models_dir, &tools_dir, &workflows_dir, &mcps_dir] {
1671 fs::create_dir_all(dir).expect("Failed to create config dir");
1672 }
1673
1674 fs::write(
1675 agents_dir.join("router.toon"),
1676 "name: router\nmodel: fast\ntools[1]: calc\n",
1677 )
1678 .expect("Failed to write agent");
1679 fs::write(
1680 models_dir.join("fast.toon"),
1681 "name: fast\nprovider: ollama\nmodel: llama3\n",
1682 )
1683 .expect("Failed to write model");
1684 fs::write(tools_dir.join("calc.toon"), "name: calc\n").expect("Failed to write tool");
1685 fs::write(
1686 workflows_dir.join("default.toon"),
1687 "name: default\nentry_agent: router\n",
1688 )
1689 .expect("Failed to write workflow");
1690 fs::write(mcps_dir.join("fs.toon"), "name: fs\ncommand: npx\n")
1691 .expect("Failed to write mcp");
1692
1693 let config = DynamicConfig::load(
1694 &agents_dir,
1695 &models_dir,
1696 &tools_dir,
1697 &workflows_dir,
1698 &mcps_dir,
1699 )
1700 .expect("Failed to load dynamic config");
1701
1702 assert_eq!(config.agents.len(), 1);
1703 assert_eq!(config.models.len(), 1);
1704 assert_eq!(config.tools.len(), 1);
1705 assert_eq!(config.workflows.len(), 1);
1706 assert_eq!(config.mcps.len(), 1);
1707 assert!(config.validate().expect("validation failed").is_empty());
1708 }
1709
1710 #[test]
1711 fn test_dynamic_config_accessors_and_name_lists() {
1712 let mut config = DynamicConfig::default();
1713 config
1714 .agents
1715 .insert("router".to_string(), ToonAgentConfig::new("router", "fast"));
1716 config.models.insert(
1717 "fast".to_string(),
1718 ToonModelConfig::new("fast", "ollama", "llama3"),
1719 );
1720 config
1721 .tools
1722 .insert("calc".to_string(), ToonToolConfig::new("calc"));
1723 config.workflows.insert(
1724 "default".to_string(),
1725 ToonWorkflowConfig::new("default", "router"),
1726 );
1727 config
1728 .mcps
1729 .insert("fs".to_string(), ToonMcpConfig::new("fs", "npx"));
1730
1731 assert_eq!(config.get_agent("router").unwrap().name, "router");
1732 assert_eq!(config.get_model("fast").unwrap().provider, "ollama");
1733 assert_eq!(config.get_tool("calc").unwrap().name, "calc");
1734 assert_eq!(config.get_workflow("default").unwrap().entry_agent, "router");
1735 assert_eq!(config.get_mcp("fs").unwrap().name, "fs");
1736 assert!(config.get_agent("missing").is_none());
1737
1738 assert_eq!(config.agent_names(), vec!["router"]);
1739 assert_eq!(config.model_names(), vec!["fast"]);
1740 assert_eq!(config.tool_names(), vec!["calc"]);
1741 assert_eq!(config.workflow_names(), vec!["default"]);
1742 assert_eq!(config.mcp_names(), vec!["fs"]);
1743 }
1744
1745 #[test]
1746 fn test_dynamic_config_manager_without_hot_reload() {
1747 let temp_dir = TempDir::new().expect("Failed to create temp dir");
1748 let root = temp_dir.path();
1749
1750 let agents_dir = root.join("agents");
1751 let models_dir = root.join("models");
1752 let tools_dir = root.join("tools");
1753 let workflows_dir = root.join("workflows");
1754 let mcps_dir = root.join("mcps");
1755 fs::create_dir_all(&agents_dir).expect("Failed to create agents dir");
1756 fs::create_dir_all(&models_dir).expect("Failed to create models dir");
1757 fs::create_dir_all(&tools_dir).expect("Failed to create tools dir");
1758 fs::create_dir_all(&workflows_dir).expect("Failed to create workflows dir");
1759 fs::create_dir_all(&mcps_dir).expect("Failed to create mcps dir");
1760
1761 fs::write(
1762 agents_dir.join("router.toon"),
1763 "name: router\nmodel: fast\n",
1764 )
1765 .expect("Failed to write agent");
1766 fs::write(
1767 models_dir.join("fast.toon"),
1768 "name: fast\nprovider: ollama\nmodel: llama3\n",
1769 )
1770 .expect("Failed to write model");
1771
1772 let manager = DynamicConfigManager::new(
1773 agents_dir,
1774 models_dir,
1775 tools_dir,
1776 workflows_dir,
1777 mcps_dir,
1778 false,
1779 )
1780 .expect("Failed to create manager");
1781
1782 let agent = manager.agent("router").expect("router agent missing");
1783 assert_eq!(agent.model, "fast");
1784 assert_eq!(manager.agent_names(), vec!["router"]);
1785 assert_eq!(manager.model_names(), vec!["fast"]);
1786 assert!(manager.tool_names().is_empty());
1787 assert!(manager.workflow_names().is_empty());
1788 assert!(manager.mcps().is_empty());
1789
1790 let warnings = manager.reload().expect("reload failed");
1791 assert!(warnings.is_empty());
1792 }
1793
1794 #[test]
1795 fn dynamic_config_manager_readable_via_cordis() {
1796 use cordis::Service;
1797 let dir = TempDir::new().unwrap();
1798 let manager = DynamicConfigManager::new(
1799 dir.path().join("agents"),
1800 dir.path().join("models"),
1801 dir.path().join("tools"),
1802 dir.path().join("workflows"),
1803 dir.path().join("mcps"),
1804 false,
1805 )
1806 .expect("empty dynamic config");
1807 let ctx = std::sync::Arc::new(cordis::Context::new_root());
1808 ctx.provide(manager);
1809 let got = ctx.get::<DynamicConfigManager>().expect("provided");
1810 assert_eq!(got.name(), "dynamic_config_manager");
1811 assert!(got.check());
1812 }
1813
1814 #[test]
1815 fn toon_changes_notify_tools_and_execute_type_ids() {
1816 let ctx = cordis::Context::new_root();
1817 let reflect = ctx.provide(cordis::ReflectService::new());
1818 let mut tools_rx = reflect.ensure_notifier(TypeId::of::<ares_tools::Tools>());
1819 let mut execute_rx = reflect.ensure_notifier(TypeId::of::<ares_agent::Execute>());
1820 let _ = tools_rx.borrow_and_update();
1821 let _ = execute_rx.borrow_and_update();
1822
1823 super::notify_tools_and_execute(&ctx);
1824
1825 assert!(
1826 tools_rx.has_changed().expect("tools watch"),
1827 "TOON notify must signal TypeId::of::<Tools>()"
1828 );
1829 assert!(
1830 execute_rx.has_changed().expect("execute watch"),
1831 "TOON notify must signal TypeId::of::<Execute>()"
1832 );
1833 }
1834
1835}