1use crate::agent::AgentConfig;
10use crate::agentflow::AgentFlow;
11use crate::binding::FlowBinding;
12use crate::context::Context;
13use crate::error::{AofError, AofResult};
14use crate::trigger::Trigger;
15
16use std::collections::HashMap;
17use std::path::Path;
18
19pub trait Registry<T> {
21 fn load_directory(&mut self, path: &Path) -> AofResult<usize>;
23
24 fn get(&self, name: &str) -> Option<&T>;
26
27 fn get_all(&self) -> Vec<&T>;
29
30 fn register(&mut self, resource: T) -> AofResult<()>;
32
33 fn count(&self) -> usize;
35
36 fn exists(&self, name: &str) -> bool {
38 self.get(name).is_some()
39 }
40}
41
42#[derive(Debug, Default)]
48pub struct AgentRegistry {
49 agents: HashMap<String, AgentConfig>,
50}
51
52impl AgentRegistry {
53 pub fn new() -> Self {
54 Self::default()
55 }
56
57 pub fn names(&self) -> Vec<&str> {
59 self.agents.keys().map(|s| s.as_str()).collect()
60 }
61}
62
63impl Registry<AgentConfig> for AgentRegistry {
64 fn load_directory(&mut self, path: &Path) -> AofResult<usize> {
65 if !path.exists() {
66 return Ok(0);
67 }
68
69 let mut count = 0;
70 for entry in std::fs::read_dir(path)? {
71 let entry = entry?;
72 let file_path = entry.path();
73
74 if file_path.extension().map_or(false, |e| e == "yaml" || e == "yml") {
75 if !yaml_file_has_kind(&file_path, "Agent") {
77 tracing::debug!("Skipping non-Agent file: {:?}", file_path);
78 continue;
79 }
80
81 match load_yaml_file::<AgentConfig>(&file_path) {
82 Ok(agent) => {
83 let name = agent.name.clone();
84 self.agents.insert(name.clone(), agent);
85 tracing::debug!("Loaded agent: {}", name);
86 count += 1;
87 }
88 Err(e) => {
89 tracing::warn!("Failed to load agent from {:?}: {}", file_path, e);
90 }
91 }
92 }
93 }
94
95 Ok(count)
96 }
97
98 fn get(&self, name: &str) -> Option<&AgentConfig> {
99 self.agents.get(name)
100 }
101
102 fn get_all(&self) -> Vec<&AgentConfig> {
103 self.agents.values().collect()
104 }
105
106 fn register(&mut self, resource: AgentConfig) -> AofResult<()> {
107 let name = resource.name.clone();
108 self.agents.insert(name, resource);
109 Ok(())
110 }
111
112 fn count(&self) -> usize {
113 self.agents.len()
114 }
115}
116
117#[derive(Debug, Default)]
123pub struct ContextRegistry {
124 contexts: HashMap<String, Context>,
125}
126
127impl ContextRegistry {
128 pub fn new() -> Self {
129 Self::default()
130 }
131
132 pub fn names(&self) -> Vec<&str> {
134 self.contexts.keys().map(|s| s.as_str()).collect()
135 }
136
137 pub fn get_mut(&mut self, name: &str) -> Option<&mut Context> {
139 self.contexts.get_mut(name)
140 }
141
142 pub fn expand_all_env_vars(&mut self) {
144 for context in self.contexts.values_mut() {
145 context.expand_env_vars();
146 }
147 }
148}
149
150impl Registry<Context> for ContextRegistry {
151 fn load_directory(&mut self, path: &Path) -> AofResult<usize> {
152 if !path.exists() {
153 return Ok(0);
154 }
155
156 let mut count = 0;
157 for entry in std::fs::read_dir(path)? {
158 let entry = entry?;
159 let file_path = entry.path();
160
161 if file_path.extension().map_or(false, |e| e == "yaml" || e == "yml") {
162 match load_yaml_file::<Context>(&file_path) {
163 Ok(mut context) => {
164 context.expand_env_vars();
165 if let Err(e) = context.validate() {
166 tracing::warn!("Invalid context in {:?}: {}", file_path, e);
167 continue;
168 }
169 let name = context.metadata.name.clone();
170 self.contexts.insert(name.clone(), context);
171 tracing::debug!("Loaded context: {}", name);
172 count += 1;
173 }
174 Err(e) => {
175 tracing::warn!("Failed to load context from {:?}: {}", file_path, e);
176 }
177 }
178 }
179 }
180
181 Ok(count)
182 }
183
184 fn get(&self, name: &str) -> Option<&Context> {
185 self.contexts.get(name)
186 }
187
188 fn get_all(&self) -> Vec<&Context> {
189 self.contexts.values().collect()
190 }
191
192 fn register(&mut self, resource: Context) -> AofResult<()> {
193 resource.validate().map_err(|e| AofError::Config(e))?;
194 let name = resource.metadata.name.clone();
195 self.contexts.insert(name, resource);
196 Ok(())
197 }
198
199 fn count(&self) -> usize {
200 self.contexts.len()
201 }
202}
203
204#[derive(Debug, Default)]
210pub struct TriggerRegistry {
211 triggers: HashMap<String, Trigger>,
212}
213
214impl TriggerRegistry {
215 pub fn new() -> Self {
216 Self::default()
217 }
218
219 pub fn names(&self) -> Vec<&str> {
221 self.triggers.keys().map(|s| s.as_str()).collect()
222 }
223
224 pub fn get_by_type(&self, trigger_type: crate::trigger::StandaloneTriggerType) -> Vec<&Trigger> {
226 self.triggers
227 .values()
228 .filter(|t| t.spec.trigger_type == trigger_type)
229 .collect()
230 }
231
232 pub fn expand_all_env_vars(&mut self) {
234 for trigger in self.triggers.values_mut() {
235 trigger.expand_env_vars();
236 }
237 }
238}
239
240impl Registry<Trigger> for TriggerRegistry {
241 fn load_directory(&mut self, path: &Path) -> AofResult<usize> {
242 if !path.exists() {
243 return Ok(0);
244 }
245
246 let mut count = 0;
247 for entry in std::fs::read_dir(path)? {
248 let entry = entry?;
249 let file_path = entry.path();
250
251 if file_path.extension().map_or(false, |e| e == "yaml" || e == "yml") {
252 match load_yaml_file::<Trigger>(&file_path) {
253 Ok(mut trigger) => {
254 trigger.expand_env_vars();
255 if let Err(e) = trigger.validate() {
256 tracing::warn!("Invalid trigger in {:?}: {}", file_path, e);
257 continue;
258 }
259 let name = trigger.metadata.name.clone();
260 self.triggers.insert(name.clone(), trigger);
261 tracing::debug!("Loaded trigger: {}", name);
262 count += 1;
263 }
264 Err(e) => {
265 tracing::warn!("Failed to load trigger from {:?}: {}", file_path, e);
266 }
267 }
268 }
269 }
270
271 Ok(count)
272 }
273
274 fn get(&self, name: &str) -> Option<&Trigger> {
275 self.triggers.get(name)
276 }
277
278 fn get_all(&self) -> Vec<&Trigger> {
279 self.triggers.values().collect()
280 }
281
282 fn register(&mut self, resource: Trigger) -> AofResult<()> {
283 resource.validate().map_err(|e| AofError::Config(e))?;
284 let name = resource.metadata.name.clone();
285 self.triggers.insert(name, resource);
286 Ok(())
287 }
288
289 fn count(&self) -> usize {
290 self.triggers.len()
291 }
292}
293
294#[derive(Debug, Default)]
300pub struct FlowRegistry {
301 flows: HashMap<String, AgentFlow>,
302}
303
304impl FlowRegistry {
305 pub fn new() -> Self {
306 Self::default()
307 }
308
309 pub fn names(&self) -> Vec<&str> {
311 self.flows.keys().map(|s| s.as_str()).collect()
312 }
313}
314
315impl Registry<AgentFlow> for FlowRegistry {
316 fn load_directory(&mut self, path: &Path) -> AofResult<usize> {
317 if !path.exists() {
318 return Ok(0);
319 }
320
321 let mut count = 0;
322 for entry in std::fs::read_dir(path)? {
323 let entry = entry?;
324 let file_path = entry.path();
325
326 if file_path.extension().map_or(false, |e| e == "yaml" || e == "yml") {
327 match load_yaml_file::<AgentFlow>(&file_path) {
328 Ok(flow) => {
329 if let Err(e) = flow.validate() {
330 tracing::warn!("Invalid flow in {:?}: {}", file_path, e);
331 continue;
332 }
333 let name = flow.metadata.name.clone();
334 self.flows.insert(name.clone(), flow);
335 tracing::debug!("Loaded flow: {}", name);
336 count += 1;
337 }
338 Err(e) => {
339 tracing::warn!("Failed to load flow from {:?}: {}", file_path, e);
340 }
341 }
342 }
343 }
344
345 Ok(count)
346 }
347
348 fn get(&self, name: &str) -> Option<&AgentFlow> {
349 self.flows.get(name)
350 }
351
352 fn get_all(&self) -> Vec<&AgentFlow> {
353 self.flows.values().collect()
354 }
355
356 fn register(&mut self, resource: AgentFlow) -> AofResult<()> {
357 resource.validate().map_err(|e| AofError::Config(e))?;
358 let name = resource.metadata.name.clone();
359 self.flows.insert(name, resource);
360 Ok(())
361 }
362
363 fn count(&self) -> usize {
364 self.flows.len()
365 }
366}
367
368#[derive(Debug, Default)]
374pub struct BindingRegistry {
375 bindings: HashMap<String, FlowBinding>,
376}
377
378impl BindingRegistry {
379 pub fn new() -> Self {
380 Self::default()
381 }
382
383 pub fn names(&self) -> Vec<&str> {
385 self.bindings.keys().map(|s| s.as_str()).collect()
386 }
387
388 pub fn get_bindings_for_trigger(&self, trigger_name: &str) -> Vec<&FlowBinding> {
390 self.bindings
391 .values()
392 .filter(|b| b.spec.trigger == trigger_name && b.spec.enabled)
393 .collect()
394 }
395
396 pub fn get_bindings_for_context(&self, context_name: &str) -> Vec<&FlowBinding> {
398 self.bindings
399 .values()
400 .filter(|b| b.spec.context.as_deref() == Some(context_name) && b.spec.enabled)
401 .collect()
402 }
403
404 pub fn get_enabled(&self) -> Vec<&FlowBinding> {
406 self.bindings.values().filter(|b| b.spec.enabled).collect()
407 }
408
409 pub fn find_best_match(
411 &self,
412 trigger_name: &str,
413 channel: Option<&str>,
414 user: Option<&str>,
415 text: Option<&str>,
416 ) -> Option<&FlowBinding> {
417 let bindings = self.get_bindings_for_trigger(trigger_name);
418
419 bindings
420 .into_iter()
421 .filter(|b| b.matches(channel, user, text))
422 .max_by_key(|b| b.match_score(channel, user, text))
423 }
424}
425
426impl Registry<FlowBinding> for BindingRegistry {
427 fn load_directory(&mut self, path: &Path) -> AofResult<usize> {
428 if !path.exists() {
429 return Ok(0);
430 }
431
432 let mut count = 0;
433 for entry in std::fs::read_dir(path)? {
434 let entry = entry?;
435 let file_path = entry.path();
436
437 if file_path.extension().map_or(false, |e| e == "yaml" || e == "yml") {
438 match load_yaml_file::<FlowBinding>(&file_path) {
439 Ok(binding) => {
440 if let Err(e) = binding.validate() {
441 tracing::warn!("Invalid binding in {:?}: {}", file_path, e);
442 continue;
443 }
444 let name = binding.metadata.name.clone();
445 self.bindings.insert(name.clone(), binding);
446 tracing::debug!("Loaded binding: {}", name);
447 count += 1;
448 }
449 Err(e) => {
450 tracing::warn!("Failed to load binding from {:?}: {}", file_path, e);
451 }
452 }
453 }
454 }
455
456 Ok(count)
457 }
458
459 fn get(&self, name: &str) -> Option<&FlowBinding> {
460 self.bindings.get(name)
461 }
462
463 fn get_all(&self) -> Vec<&FlowBinding> {
464 self.bindings.values().collect()
465 }
466
467 fn register(&mut self, resource: FlowBinding) -> AofResult<()> {
468 resource.validate().map_err(|e| AofError::Config(e))?;
469 let name = resource.metadata.name.clone();
470 self.bindings.insert(name, resource);
471 Ok(())
472 }
473
474 fn count(&self) -> usize {
475 self.bindings.len()
476 }
477}
478
479#[derive(Debug, Default)]
485pub struct ResourceManager {
486 pub agents: AgentRegistry,
487 pub contexts: ContextRegistry,
488 pub triggers: TriggerRegistry,
489 pub flows: FlowRegistry,
490 pub bindings: BindingRegistry,
491}
492
493impl ResourceManager {
494 pub fn new() -> Self {
495 Self::default()
496 }
497
498 pub fn load_directory(&mut self, root: &Path) -> AofResult<ResourceLoadSummary> {
510 let mut summary = ResourceLoadSummary::default();
511
512 let agents_dir = root.join("agents");
514 if agents_dir.exists() {
515 summary.agents = self.agents.load_directory(&agents_dir)?;
516 }
517
518 let contexts_dir = root.join("contexts");
520 if contexts_dir.exists() {
521 summary.contexts = self.contexts.load_directory(&contexts_dir)?;
522 }
523
524 let triggers_dir = root.join("triggers");
526 if triggers_dir.exists() {
527 summary.triggers = self.triggers.load_directory(&triggers_dir)?;
528 }
529
530 let flows_dir = root.join("flows");
532 if flows_dir.exists() {
533 summary.flows = self.flows.load_directory(&flows_dir)?;
534 }
535
536 let bindings_dir = root.join("bindings");
538 if bindings_dir.exists() {
539 summary.bindings = self.bindings.load_directory(&bindings_dir)?;
540 }
541
542 Ok(summary)
543 }
544
545 pub fn validate_references(&self) -> Vec<ValidationError> {
547 let mut errors = Vec::new();
548
549 for binding in self.bindings.get_all() {
551 if !self.triggers.exists(&binding.spec.trigger) {
553 errors.push(ValidationError {
554 resource_type: "FlowBinding".to_string(),
555 resource_name: binding.metadata.name.clone(),
556 field: "trigger".to_string(),
557 message: format!("Referenced trigger '{}' not found", binding.spec.trigger),
558 });
559 }
560
561 if let Some(ref context_name) = binding.spec.context {
563 if !self.contexts.exists(context_name) {
564 errors.push(ValidationError {
565 resource_type: "FlowBinding".to_string(),
566 resource_name: binding.metadata.name.clone(),
567 field: "context".to_string(),
568 message: format!("Referenced context '{}' not found", context_name),
569 });
570 }
571 }
572
573 if !binding.spec.flow.is_empty() && !self.flows.exists(&binding.spec.flow) {
575 errors.push(ValidationError {
576 resource_type: "FlowBinding".to_string(),
577 resource_name: binding.metadata.name.clone(),
578 field: "flow".to_string(),
579 message: format!("Referenced flow '{}' not found", binding.spec.flow),
580 });
581 }
582
583 if let Some(ref agent_name) = binding.spec.agent {
585 if !self.agents.exists(agent_name) {
586 errors.push(ValidationError {
587 resource_type: "FlowBinding".to_string(),
588 resource_name: binding.metadata.name.clone(),
589 field: "agent".to_string(),
590 message: format!("Referenced agent '{}' not found", agent_name),
591 });
592 }
593 }
594 }
595
596 errors
597 }
598
599 pub fn summary(&self) -> ResourceLoadSummary {
601 ResourceLoadSummary {
602 agents: self.agents.count(),
603 contexts: self.contexts.count(),
604 triggers: self.triggers.count(),
605 flows: self.flows.count(),
606 bindings: self.bindings.count(),
607 }
608 }
609}
610
611#[derive(Debug, Default, Clone)]
613pub struct ResourceLoadSummary {
614 pub agents: usize,
615 pub contexts: usize,
616 pub triggers: usize,
617 pub flows: usize,
618 pub bindings: usize,
619}
620
621impl ResourceLoadSummary {
622 pub fn total(&self) -> usize {
623 self.agents + self.contexts + self.triggers + self.flows + self.bindings
624 }
625}
626
627impl std::fmt::Display for ResourceLoadSummary {
628 fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
629 write!(
630 f,
631 "Loaded {} resources: {} agents, {} contexts, {} triggers, {} flows, {} bindings",
632 self.total(),
633 self.agents,
634 self.contexts,
635 self.triggers,
636 self.flows,
637 self.bindings
638 )
639 }
640}
641
642#[derive(Debug, Clone)]
644pub struct ValidationError {
645 pub resource_type: String,
646 pub resource_name: String,
647 pub field: String,
648 pub message: String,
649}
650
651impl std::fmt::Display for ValidationError {
652 fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
653 write!(
654 f,
655 "{} '{}' field '{}': {}",
656 self.resource_type, self.resource_name, self.field, self.message
657 )
658 }
659}
660
661fn load_yaml_file<T: serde::de::DeserializeOwned>(path: &Path) -> AofResult<T> {
667 let content = std::fs::read_to_string(path)?;
668 let resource: T = serde_yaml::from_str(&content)?;
669 Ok(resource)
670}
671
672fn yaml_file_has_kind(path: &Path, expected_kind: &str) -> bool {
675 #[derive(serde::Deserialize)]
677 struct KindCheck {
678 kind: Option<String>,
679 }
680
681 let content = match std::fs::read_to_string(path) {
682 Ok(c) => c,
683 Err(_) => return false,
684 };
685
686 match serde_yaml::from_str::<KindCheck>(&content) {
687 Ok(check) => check.kind.as_deref() == Some(expected_kind),
688 Err(_) => false,
689 }
690}
691
692#[cfg(test)]
693mod tests {
694 use super::*;
695 use std::io::Write;
696 use tempfile::TempDir;
697
698 #[test]
699 fn test_agent_registry() {
700 let mut registry = AgentRegistry::new();
701
702 let agent = AgentConfig {
703 name: "test-agent".to_string(),
704 model: "google:gemini-2.5-flash".to_string(),
705 system_prompt: Some("Test prompt".to_string()),
706 provider: None,
707 tools: vec![],
708 mcp_servers: vec![],
709 memory: None,
710 max_context_messages: 10,
711 max_iterations: 10,
712 temperature: 0.7,
713 max_tokens: None,
714 output_schema: None,
715 extra: HashMap::new(),
716 };
717
718 registry.register(agent).unwrap();
719 assert_eq!(registry.count(), 1);
720 assert!(registry.exists("test-agent"));
721 assert!(registry.get("test-agent").is_some());
722 }
723
724 #[test]
725 fn test_context_registry() {
726 let mut registry = ContextRegistry::new();
727
728 let yaml = r#"
729apiVersion: aof.dev/v1
730kind: Context
731metadata:
732 name: test-context
733spec:
734 namespace: default
735"#;
736 let context: Context = serde_yaml::from_str(yaml).unwrap();
737
738 registry.register(context).unwrap();
739 assert_eq!(registry.count(), 1);
740 assert!(registry.exists("test-context"));
741 }
742
743 #[test]
744 fn test_trigger_registry() {
745 let mut registry = TriggerRegistry::new();
746
747 let yaml = r#"
748apiVersion: aof.dev/v1
749kind: Trigger
750metadata:
751 name: test-trigger
752spec:
753 type: HTTP
754 config: {}
755"#;
756 let trigger: Trigger = serde_yaml::from_str(yaml).unwrap();
757
758 registry.register(trigger).unwrap();
759 assert_eq!(registry.count(), 1);
760 assert!(registry.exists("test-trigger"));
761 }
762
763 #[test]
764 fn test_binding_registry_find_best_match() {
765 let mut registry = BindingRegistry::new();
766
767 let yaml1 = r#"
769apiVersion: aof.dev/v1
770kind: FlowBinding
771metadata:
772 name: general
773spec:
774 trigger: slack
775 flow: general-flow
776"#;
777 let yaml2 = r#"
779apiVersion: aof.dev/v1
780kind: FlowBinding
781metadata:
782 name: kubectl-specific
783spec:
784 trigger: slack
785 flow: k8s-flow
786 match:
787 patterns: [kubectl]
788 channels: [production]
789"#;
790
791 let binding1: FlowBinding = serde_yaml::from_str(yaml1).unwrap();
792 let binding2: FlowBinding = serde_yaml::from_str(yaml2).unwrap();
793
794 registry.register(binding1).unwrap();
795 registry.register(binding2).unwrap();
796
797 let best = registry.find_best_match(
799 "slack",
800 Some("production"),
801 None,
802 Some("kubectl get pods"),
803 );
804
805 assert!(best.is_some());
806 assert_eq!(best.unwrap().metadata.name, "kubectl-specific");
807 }
808
809 #[test]
810 fn test_resource_manager_load_directory() {
811 let temp_dir = TempDir::new().unwrap();
812 let root = temp_dir.path();
813
814 std::fs::create_dir_all(root.join("agents")).unwrap();
816 std::fs::create_dir_all(root.join("contexts")).unwrap();
817 std::fs::create_dir_all(root.join("triggers")).unwrap();
818 std::fs::create_dir_all(root.join("bindings")).unwrap();
819
820 let agent_yaml = r#"
822apiVersion: aof.dev/v1
823kind: Agent
824metadata:
825 name: test-agent
826spec:
827 model: google:gemini-2.5-flash
828"#;
829 let mut file = std::fs::File::create(root.join("agents/test.yaml")).unwrap();
830 file.write_all(agent_yaml.as_bytes()).unwrap();
831
832 let context_yaml = r#"
834apiVersion: aof.dev/v1
835kind: Context
836metadata:
837 name: prod
838spec:
839 namespace: production
840"#;
841 let mut file = std::fs::File::create(root.join("contexts/prod.yaml")).unwrap();
842 file.write_all(context_yaml.as_bytes()).unwrap();
843
844 let mut manager = ResourceManager::new();
846 let summary = manager.load_directory(root).unwrap();
847
848 assert_eq!(summary.agents, 1);
849 assert_eq!(summary.contexts, 1);
850 assert!(manager.agents.exists("test-agent"));
851 assert!(manager.contexts.exists("prod"));
852 }
853
854 #[test]
855 fn test_validate_references() {
856 let mut manager = ResourceManager::new();
857
858 let trigger_yaml = r#"
860apiVersion: aof.dev/v1
861kind: Trigger
862metadata:
863 name: slack-trigger
864spec:
865 type: HTTP
866 config: {}
867"#;
868 let trigger: Trigger = serde_yaml::from_str(trigger_yaml).unwrap();
869 manager.triggers.register(trigger).unwrap();
870
871 let binding_yaml = r#"
873apiVersion: aof.dev/v1
874kind: FlowBinding
875metadata:
876 name: bad-binding
877spec:
878 trigger: slack-trigger
879 context: non-existent-context
880 flow: non-existent-flow
881"#;
882 let binding: FlowBinding = serde_yaml::from_str(binding_yaml).unwrap();
883 manager.bindings.register(binding).unwrap();
884
885 let errors = manager.validate_references();
886 assert_eq!(errors.len(), 2); }
888}