1use serde::{Deserialize, Serialize};
12use std::collections::HashMap;
13
14#[derive(Debug, Clone, Serialize, Deserialize)]
16pub struct AgentFleet {
17 #[serde(rename = "apiVersion")]
19 pub api_version: String,
20
21 pub kind: String,
23
24 pub metadata: FleetMetadata,
26
27 pub spec: FleetSpec,
29}
30
31#[derive(Debug, Clone, Serialize, Deserialize)]
33pub struct FleetMetadata {
34 pub name: String,
36
37 #[serde(default)]
39 pub namespace: Option<String>,
40
41 #[serde(default)]
43 pub labels: HashMap<String, String>,
44
45 #[serde(default)]
47 pub annotations: HashMap<String, String>,
48}
49
50#[derive(Debug, Clone, Serialize, Deserialize)]
52pub struct FleetSpec {
53 pub agents: Vec<FleetAgent>,
55
56 #[serde(default)]
58 pub coordination: CoordinationConfig,
59
60 #[serde(default)]
62 pub shared: Option<SharedResources>,
63
64 #[serde(default)]
66 pub communication: Option<CommunicationConfig>,
67
68 #[serde(default)]
70 pub scaling: Option<ScalingConfig>,
71}
72
73#[derive(Debug, Clone, Serialize, Deserialize)]
75pub struct FleetAgent {
76 pub name: String,
78
79 #[serde(default)]
81 pub config: Option<String>,
82
83 #[serde(default)]
85 pub spec: Option<FleetAgentSpec>,
86
87 #[serde(default = "default_replicas")]
89 pub replicas: u32,
90
91 #[serde(default)]
93 pub role: AgentRole,
94
95 #[serde(default)]
97 pub labels: HashMap<String, String>,
98
99 #[serde(default)]
103 pub tier: Option<u32>,
104
105 #[serde(default)]
107 pub weight: Option<f32>,
108}
109
110fn default_replicas() -> u32 {
111 1
112}
113
114#[derive(Debug, Clone, Serialize, Deserialize)]
116pub struct FleetAgentSpec {
117 pub model: String,
119
120 #[serde(default)]
122 pub instructions: Option<String>,
123
124 #[serde(default)]
127 pub tools: Vec<crate::agent::ToolSpec>,
128
129 #[serde(default)]
131 pub mcp_servers: Vec<crate::McpServerConfig>,
132
133 #[serde(default)]
135 pub max_iterations: Option<u32>,
136
137 #[serde(default)]
139 pub temperature: Option<f32>,
140}
141
142#[derive(Debug, Clone, Copy, Serialize, Deserialize, Default, PartialEq, Eq)]
144#[serde(rename_all = "lowercase")]
145pub enum AgentRole {
146 #[default]
148 Worker,
149 Manager,
151 Specialist,
153 Validator,
155}
156
157#[derive(Debug, Clone, Serialize, Deserialize)]
159pub struct CoordinationConfig {
160 #[serde(default)]
162 pub mode: CoordinationMode,
163
164 #[serde(default)]
166 pub manager: Option<String>,
167
168 #[serde(default)]
170 pub distribution: TaskDistribution,
171
172 #[serde(default)]
174 pub consensus: Option<ConsensusConfig>,
175
176 #[serde(default)]
181 pub aggregation: Option<FinalAggregation>,
182
183 #[serde(default)]
185 pub tiered: Option<TieredConfig>,
186
187 #[serde(default)]
189 pub deep: Option<DeepConfig>,
190}
191
192#[derive(Debug, Clone, Serialize, Deserialize, Default)]
194pub struct TieredConfig {
195 #[serde(default)]
198 pub tier_consensus: HashMap<String, ConsensusConfig>,
199
200 #[serde(default)]
202 pub pass_all_results: bool,
203
204 #[serde(default)]
206 pub final_aggregation: FinalAggregation,
207}
208
209#[derive(Debug, Clone, Serialize, Deserialize)]
221pub struct DeepConfig {
222 #[serde(default = "default_max_iterations")]
224 pub max_iterations: u32,
225
226 #[serde(default = "default_true")]
228 pub planning: bool,
229
230 #[serde(default = "default_true")]
232 pub memory: bool,
233
234 #[serde(default)]
236 pub planner_model: Option<String>,
237
238 #[serde(default)]
240 pub planner_prompt: Option<String>,
241
242 #[serde(default)]
244 pub synthesizer_prompt: Option<String>,
245}
246
247fn default_max_iterations() -> u32 {
248 10
249}
250
251fn default_true() -> bool {
252 true
253}
254
255impl Default for DeepConfig {
256 fn default() -> Self {
257 Self {
258 max_iterations: default_max_iterations(),
259 planning: default_true(),
260 memory: default_true(),
261 planner_model: None,
262 planner_prompt: None,
263 synthesizer_prompt: None,
264 }
265 }
266}
267
268#[derive(Debug, Clone, Copy, Serialize, Deserialize, Default, PartialEq, Eq)]
270#[serde(rename_all = "snake_case")]
271pub enum FinalAggregation {
272 #[default]
274 Consensus,
275 Merge,
277 ManagerSynthesis,
279}
280
281impl Default for CoordinationConfig {
282 fn default() -> Self {
283 Self {
284 mode: CoordinationMode::Peer,
285 manager: None,
286 distribution: TaskDistribution::RoundRobin,
287 consensus: None,
288 aggregation: None,
289 tiered: None,
290 deep: None,
291 }
292 }
293}
294
295#[derive(Debug, Clone, Copy, Serialize, Deserialize, Default, PartialEq, Eq)]
312#[serde(rename_all = "lowercase")]
313pub enum CoordinationMode {
314 Hierarchical,
316 #[default]
318 Peer,
319 Swarm,
321 Pipeline,
323 Tiered,
327 Deep,
330}
331
332#[derive(Debug, Clone, Copy, Serialize, Deserialize, Default, PartialEq, Eq)]
334#[serde(rename_all = "kebab-case")]
335pub enum TaskDistribution {
336 #[default]
338 RoundRobin,
339 LeastLoaded,
341 Random,
343 SkillBased,
345 Sticky,
347}
348
349#[derive(Debug, Clone, Serialize, Deserialize)]
364pub struct ConsensusConfig {
365 #[serde(default)]
367 pub algorithm: ConsensusAlgorithm,
368
369 #[serde(default)]
371 pub min_votes: Option<u32>,
372
373 #[serde(default)]
375 pub timeout_ms: Option<u64>,
376
377 #[serde(default)]
379 pub allow_partial: bool,
380
381 #[serde(default)]
384 pub weights: HashMap<String, f32>,
385
386 #[serde(default)]
389 pub min_confidence: Option<f32>,
390}
391
392#[derive(Debug, Clone, Copy, Serialize, Deserialize, Default, PartialEq, Eq)]
401#[serde(rename_all = "snake_case")]
402pub enum ConsensusAlgorithm {
403 #[default]
405 Majority,
406 Unanimous,
408 Weighted,
410 FirstWins,
412 HumanReview,
414}
415
416#[derive(Debug, Clone, Serialize, Deserialize, Default)]
418pub struct SharedResources {
419 #[serde(default)]
421 pub memory: Option<SharedMemoryConfig>,
422
423 #[serde(default)]
425 pub tools: Vec<SharedToolConfig>,
426
427 #[serde(default)]
429 pub knowledge: Option<SharedKnowledgeConfig>,
430}
431
432#[derive(Debug, Clone, Serialize, Deserialize)]
434pub struct SharedMemoryConfig {
435 #[serde(rename = "type")]
437 pub memory_type: SharedMemoryType,
438
439 #[serde(default)]
441 pub url: Option<String>,
442
443 #[serde(default)]
445 pub namespace: Option<String>,
446
447 #[serde(default)]
449 pub ttl: Option<u64>,
450}
451
452#[derive(Debug, Clone, Copy, Serialize, Deserialize, Default, PartialEq, Eq)]
454#[serde(rename_all = "lowercase")]
455pub enum SharedMemoryType {
456 #[default]
458 InMemory,
459 Redis,
461 Sqlite,
463 Postgres,
465}
466
467#[derive(Debug, Clone, Serialize, Deserialize)]
469pub struct SharedToolConfig {
470 #[serde(rename = "mcp-server")]
472 pub mcp_server: Option<String>,
473
474 pub tool: Option<String>,
476
477 #[serde(default)]
479 pub config: HashMap<String, serde_json::Value>,
480}
481
482#[derive(Debug, Clone, Serialize, Deserialize)]
484pub struct SharedKnowledgeConfig {
485 #[serde(rename = "type")]
487 pub kb_type: String,
488
489 pub source: String,
491
492 #[serde(default)]
494 pub config: HashMap<String, serde_json::Value>,
495}
496
497#[derive(Debug, Clone, Serialize, Deserialize)]
499pub struct CommunicationConfig {
500 #[serde(default)]
502 pub pattern: MessagePattern,
503
504 #[serde(default)]
506 pub queue: Option<QueueConfig>,
507
508 #[serde(default)]
510 pub broadcast: Option<BroadcastConfig>,
511}
512
513#[derive(Debug, Clone, Copy, Serialize, Deserialize, Default, PartialEq, Eq)]
515#[serde(rename_all = "kebab-case")]
516pub enum MessagePattern {
517 #[default]
519 Direct,
520 PubSub,
522 RequestReply,
524 Broadcast,
526}
527
528#[derive(Debug, Clone, Serialize, Deserialize)]
530pub struct QueueConfig {
531 #[serde(rename = "type")]
533 pub queue_type: String,
534
535 pub url: String,
537
538 pub name: String,
540}
541
542#[derive(Debug, Clone, Serialize, Deserialize)]
544pub struct BroadcastConfig {
545 pub channel: String,
547
548 #[serde(default)]
550 pub include_sender: bool,
551}
552
553#[derive(Debug, Clone, Serialize, Deserialize)]
555pub struct ScalingConfig {
556 #[serde(default)]
558 pub min_replicas: Option<u32>,
559
560 #[serde(default)]
562 pub max_replicas: Option<u32>,
563
564 #[serde(default)]
566 pub auto_scale: bool,
567
568 #[serde(default)]
570 pub metrics: Vec<ScalingMetric>,
571}
572
573#[derive(Debug, Clone, Serialize, Deserialize)]
575pub struct ScalingMetric {
576 pub name: String,
578
579 pub target: f64,
581
582 #[serde(rename = "type")]
584 pub metric_type: String,
585}
586
587#[derive(Debug, Clone, Serialize, Deserialize)]
589pub struct FleetState {
590 pub fleet_name: String,
592
593 pub status: FleetStatus,
595
596 pub agents: HashMap<String, AgentInstanceState>,
598
599 pub active_tasks: Vec<FleetTask>,
601
602 pub completed_tasks: Vec<FleetTask>,
604
605 pub metrics: FleetMetrics,
607
608 pub started_at: Option<chrono::DateTime<chrono::Utc>>,
610}
611
612impl FleetState {
613 pub fn new(fleet_name: &str) -> Self {
615 Self {
616 fleet_name: fleet_name.to_string(),
617 status: FleetStatus::Initializing,
618 agents: HashMap::new(),
619 active_tasks: Vec::new(),
620 completed_tasks: Vec::new(),
621 metrics: FleetMetrics::default(),
622 started_at: None,
623 }
624 }
625}
626
627#[derive(Debug, Clone, Copy, Serialize, Deserialize, Default, PartialEq, Eq)]
629#[serde(rename_all = "lowercase")]
630pub enum FleetStatus {
631 #[default]
633 Initializing,
634 Ready,
636 Active,
638 Paused,
640 Failed,
642 ShuttingDown,
644}
645
646#[derive(Debug, Clone, Serialize, Deserialize)]
648pub struct AgentInstanceState {
649 pub instance_id: String,
651
652 pub agent_name: String,
654
655 pub replica_index: u32,
657
658 pub status: AgentInstanceStatus,
660
661 pub current_task: Option<String>,
663
664 pub tasks_processed: u64,
666
667 pub last_activity: Option<chrono::DateTime<chrono::Utc>>,
669}
670
671#[derive(Debug, Clone, Copy, Serialize, Deserialize, Default, PartialEq, Eq)]
673#[serde(rename_all = "lowercase")]
674pub enum AgentInstanceStatus {
675 #[default]
677 Starting,
678 Idle,
680 Busy,
682 Failed,
684 Stopped,
686}
687
688#[derive(Debug, Clone, Serialize, Deserialize)]
690pub struct FleetTask {
691 pub task_id: String,
693
694 pub input: serde_json::Value,
696
697 pub assigned_to: Option<String>,
699
700 pub status: FleetTaskStatus,
702
703 pub result: Option<serde_json::Value>,
705
706 pub error: Option<String>,
708
709 pub created_at: chrono::DateTime<chrono::Utc>,
711
712 pub started_at: Option<chrono::DateTime<chrono::Utc>>,
714
715 pub completed_at: Option<chrono::DateTime<chrono::Utc>>,
717}
718
719#[derive(Debug, Clone, Copy, Serialize, Deserialize, Default, PartialEq, Eq)]
721#[serde(rename_all = "lowercase")]
722pub enum FleetTaskStatus {
723 #[default]
725 Pending,
726 Assigned,
728 Running,
730 Completed,
732 Failed,
734 Cancelled,
736}
737
738#[derive(Debug, Clone, Serialize, Deserialize, Default)]
740pub struct FleetMetrics {
741 pub total_tasks: u64,
743
744 pub completed_tasks: u64,
746
747 pub failed_tasks: u64,
749
750 pub avg_task_duration_ms: f64,
752
753 pub active_agents: u32,
755
756 pub total_agents: u32,
758
759 pub messages_exchanged: u64,
761
762 pub consensus_rounds: u64,
764}
765
766impl AgentFleet {
767 pub fn from_yaml(yaml: &str) -> Result<Self, crate::AofError> {
769 serde_yaml::from_str(yaml).map_err(|e| crate::AofError::config(format!("Failed to parse fleet YAML: {}", e)))
770 }
771
772 pub fn from_file(path: &str) -> Result<Self, crate::AofError> {
774 let content = std::fs::read_to_string(path)
775 .map_err(|e| crate::AofError::config(format!("Failed to read fleet file: {}", e)))?;
776 Self::from_yaml(&content)
777 }
778
779 pub fn get_agent(&self, name: &str) -> Option<&FleetAgent> {
781 self.spec.agents.iter().find(|a| a.name == name)
782 }
783
784 pub fn get_agents_by_role(&self, role: AgentRole) -> Vec<&FleetAgent> {
786 self.spec.agents.iter().filter(|a| a.role == role).collect()
787 }
788
789 pub fn get_manager(&self) -> Option<&FleetAgent> {
791 if let Some(ref manager_name) = self.spec.coordination.manager {
792 self.get_agent(manager_name)
793 } else {
794 self.get_agents_by_role(AgentRole::Manager).first().copied()
795 }
796 }
797
798 pub fn total_replicas(&self) -> u32 {
800 self.spec.agents.iter().map(|a| a.replicas).sum()
801 }
802
803 pub fn validate(&self) -> Result<(), crate::AofError> {
805 let mut names = std::collections::HashSet::new();
807 for agent in &self.spec.agents {
808 if !names.insert(&agent.name) {
809 return Err(crate::AofError::config(format!(
810 "Duplicate agent name in fleet: {}",
811 agent.name
812 )));
813 }
814 }
815
816 if self.spec.coordination.mode == CoordinationMode::Hierarchical {
818 if self.get_manager().is_none() {
819 return Err(crate::AofError::config(
820 "Hierarchical mode requires a manager agent".to_string(),
821 ));
822 }
823 }
824
825 if self.spec.coordination.mode == CoordinationMode::Tiered {
827 let tiers = self.get_tiers();
828 if tiers.is_empty() {
829 return Err(crate::AofError::config(
830 "Tiered mode requires at least one agent with a tier assignment".to_string(),
831 ));
832 }
833 if tiers.len() < 2 {
835 tracing::warn!(
837 "Tiered mode with only one tier ({}) - consider using peer mode instead",
838 tiers[0]
839 );
840 }
841 }
842
843 for agent in &self.spec.agents {
845 if agent.config.is_none() && agent.spec.is_none() {
846 return Err(crate::AofError::config(format!(
847 "Agent '{}' must have either 'config' or 'spec' defined",
848 agent.name
849 )));
850 }
851 }
852
853 Ok(())
854 }
855
856 pub fn get_tiers(&self) -> Vec<u32> {
858 let mut tiers: Vec<u32> = self
859 .spec
860 .agents
861 .iter()
862 .map(|a| a.tier.unwrap_or(1))
863 .collect::<std::collections::HashSet<_>>()
864 .into_iter()
865 .collect();
866 tiers.sort();
867 tiers
868 }
869
870 pub fn get_agents_by_tier(&self, tier: u32) -> Vec<&FleetAgent> {
872 self.spec
873 .agents
874 .iter()
875 .filter(|a| a.tier.unwrap_or(1) == tier)
876 .collect()
877 }
878
879 pub fn get_agent_weight(&self, agent_name: &str) -> f32 {
881 if let Some(agent) = self.get_agent(agent_name) {
883 if let Some(weight) = agent.weight {
884 return weight;
885 }
886 }
887 if let Some(ref consensus) = self.spec.coordination.consensus {
889 if let Some(weight) = consensus.weights.get(agent_name) {
890 return *weight;
891 }
892 }
893 1.0
895 }
896}
897
898#[cfg(test)]
899mod tests {
900 use super::*;
901
902 #[test]
903 fn test_parse_fleet_yaml() {
904 let yaml = r#"
905apiVersion: aof.dev/v1
906kind: AgentFleet
907metadata:
908 name: incident-team
909 labels:
910 team: sre
911spec:
912 agents:
913 - name: detector
914 config: ./agents/detector.yaml
915 replicas: 2
916 role: worker
917 - name: analyzer
918 config: ./agents/analyzer.yaml
919 replicas: 1
920 role: specialist
921 - name: coordinator
922 config: ./agents/coordinator.yaml
923 replicas: 1
924 role: manager
925 coordination:
926 mode: hierarchical
927 manager: coordinator
928 distribution: skill-based
929 shared:
930 memory:
931 type: redis
932 url: redis://localhost:6379
933 tools:
934 - mcp-server: kubectl-ai
935"#;
936
937 let fleet = AgentFleet::from_yaml(yaml).unwrap();
938 assert_eq!(fleet.metadata.name, "incident-team");
939 assert_eq!(fleet.spec.agents.len(), 3);
940 assert_eq!(fleet.spec.coordination.mode, CoordinationMode::Hierarchical);
941 assert_eq!(fleet.total_replicas(), 4);
942
943 let manager = fleet.get_manager().unwrap();
944 assert_eq!(manager.name, "coordinator");
945 }
946
947 #[test]
948 fn test_peer_mode_fleet() {
949 let yaml = r#"
950apiVersion: aof.dev/v1
951kind: AgentFleet
952metadata:
953 name: review-team
954spec:
955 agents:
956 - name: reviewer-1
957 config: ./reviewer.yaml
958 - name: reviewer-2
959 config: ./reviewer.yaml
960 - name: reviewer-3
961 config: ./reviewer.yaml
962 coordination:
963 mode: peer
964 distribution: round-robin
965 consensus:
966 algorithm: majority
967 minVotes: 2
968"#;
969
970 let fleet = AgentFleet::from_yaml(yaml).unwrap();
971 assert_eq!(fleet.spec.coordination.mode, CoordinationMode::Peer);
972 assert!(fleet.spec.coordination.consensus.is_some());
973 }
974
975 #[test]
976 fn test_fleet_validation() {
977 let yaml = r#"
979apiVersion: aof.dev/v1
980kind: AgentFleet
981metadata:
982 name: test-fleet
983spec:
984 agents:
985 - name: agent-1
986 config: ./agent.yaml
987"#;
988 let fleet = AgentFleet::from_yaml(yaml).unwrap();
989 assert!(fleet.validate().is_ok());
990
991 let yaml = r#"
993apiVersion: aof.dev/v1
994kind: AgentFleet
995metadata:
996 name: test-fleet
997spec:
998 agents:
999 - name: agent-1
1000 config: ./agent.yaml
1001 coordination:
1002 mode: hierarchical
1003"#;
1004 let fleet = AgentFleet::from_yaml(yaml).unwrap();
1005 assert!(fleet.validate().is_err());
1006 }
1007
1008 #[test]
1009 fn test_fleet_state() {
1010 let mut state = FleetState::new("test-fleet");
1011 assert_eq!(state.status, FleetStatus::Initializing);
1012
1013 state.status = FleetStatus::Ready;
1014 state.metrics.total_agents = 3;
1015 state.metrics.active_agents = 3;
1016
1017 assert_eq!(state.metrics.total_agents, 3);
1018 }
1019
1020 #[test]
1021 fn test_tiered_mode_fleet() {
1022 let yaml = r#"
1023apiVersion: aof.dev/v1
1024kind: AgentFleet
1025metadata:
1026 name: rca-team
1027spec:
1028 agents:
1029 # Tier 1: Data collectors (cheap models)
1030 - name: loki-collector
1031 config: ./agents/loki.yaml
1032 tier: 1
1033 - name: prometheus-collector
1034 config: ./agents/prometheus.yaml
1035 tier: 1
1036 - name: k8s-collector
1037 config: ./agents/k8s.yaml
1038 tier: 1
1039 # Tier 2: Reasoning models
1040 - name: claude-analyzer
1041 config: ./agents/claude.yaml
1042 tier: 2
1043 weight: 2.0
1044 - name: gemini-analyzer
1045 config: ./agents/gemini.yaml
1046 tier: 2
1047 # Tier 3: Synthesizer
1048 - name: rca-coordinator
1049 config: ./agents/coordinator.yaml
1050 tier: 3
1051 role: manager
1052 coordination:
1053 mode: tiered
1054 consensus:
1055 algorithm: weighted
1056 min_votes: 2
1057 min_confidence: 0.7
1058 tiered:
1059 pass_all_results: true
1060 final_aggregation: manager_synthesis
1061"#;
1062
1063 let fleet = AgentFleet::from_yaml(yaml).unwrap();
1064 assert_eq!(fleet.metadata.name, "rca-team");
1065 assert_eq!(fleet.spec.coordination.mode, CoordinationMode::Tiered);
1066
1067 let tiers = fleet.get_tiers();
1069 assert_eq!(tiers, vec![1, 2, 3]);
1070
1071 let tier1_agents = fleet.get_agents_by_tier(1);
1073 assert_eq!(tier1_agents.len(), 3);
1074
1075 let tier2_agents = fleet.get_agents_by_tier(2);
1076 assert_eq!(tier2_agents.len(), 2);
1077
1078 let tier3_agents = fleet.get_agents_by_tier(3);
1079 assert_eq!(tier3_agents.len(), 1);
1080
1081 assert_eq!(fleet.get_agent_weight("claude-analyzer"), 2.0);
1083 assert_eq!(fleet.get_agent_weight("gemini-analyzer"), 1.0); assert!(fleet.validate().is_ok());
1087 }
1088
1089 #[test]
1090 fn test_consensus_algorithms() {
1091 let algorithms = vec![
1093 ("majority", ConsensusAlgorithm::Majority),
1094 ("unanimous", ConsensusAlgorithm::Unanimous),
1095 ("weighted", ConsensusAlgorithm::Weighted),
1096 ("first_wins", ConsensusAlgorithm::FirstWins),
1097 ("human_review", ConsensusAlgorithm::HumanReview),
1098 ];
1099
1100 for (yaml_value, expected) in algorithms {
1101 let yaml = format!(r#"
1102apiVersion: aof.dev/v1
1103kind: AgentFleet
1104metadata:
1105 name: test
1106spec:
1107 agents:
1108 - name: agent-1
1109 config: ./agent.yaml
1110 coordination:
1111 mode: peer
1112 consensus:
1113 algorithm: {}
1114"#, yaml_value);
1115
1116 let fleet = AgentFleet::from_yaml(&yaml).unwrap();
1117 assert_eq!(
1118 fleet.spec.coordination.consensus.as_ref().unwrap().algorithm,
1119 expected,
1120 "Failed for algorithm: {}",
1121 yaml_value
1122 );
1123 }
1124 }
1125
1126 #[test]
1127 fn test_weighted_consensus_config() {
1128 let yaml = r#"
1129apiVersion: aof.dev/v1
1130kind: AgentFleet
1131metadata:
1132 name: weighted-team
1133spec:
1134 agents:
1135 - name: senior-reviewer
1136 config: ./reviewer.yaml
1137 weight: 2.0
1138 - name: junior-reviewer
1139 config: ./reviewer.yaml
1140 coordination:
1141 mode: peer
1142 consensus:
1143 algorithm: weighted
1144 min_votes: 2
1145 min_confidence: 0.8
1146 weights:
1147 senior-reviewer: 2.0
1148 junior-reviewer: 1.0
1149"#;
1150
1151 let fleet = AgentFleet::from_yaml(yaml).unwrap();
1152 let consensus = fleet.spec.coordination.consensus.as_ref().unwrap();
1153
1154 assert_eq!(consensus.algorithm, ConsensusAlgorithm::Weighted);
1155 assert_eq!(consensus.min_votes, Some(2));
1156 assert_eq!(consensus.min_confidence, Some(0.8));
1157 assert_eq!(consensus.weights.get("senior-reviewer"), Some(&2.0));
1158 assert_eq!(consensus.weights.get("junior-reviewer"), Some(&1.0));
1159
1160 assert_eq!(fleet.get_agent_weight("senior-reviewer"), 2.0);
1162 }
1163
1164 #[test]
1165 fn test_human_review_consensus() {
1166 let yaml = r#"
1167apiVersion: aof.dev/v1
1168kind: AgentFleet
1169metadata:
1170 name: critical-review
1171spec:
1172 agents:
1173 - name: analyzer-1
1174 config: ./analyzer.yaml
1175 - name: analyzer-2
1176 config: ./analyzer.yaml
1177 coordination:
1178 mode: peer
1179 consensus:
1180 algorithm: human_review
1181 timeout_ms: 300000
1182 min_confidence: 0.9
1183"#;
1184
1185 let fleet = AgentFleet::from_yaml(yaml).unwrap();
1186 let consensus = fleet.spec.coordination.consensus.as_ref().unwrap();
1187
1188 assert_eq!(consensus.algorithm, ConsensusAlgorithm::HumanReview);
1189 assert_eq!(consensus.timeout_ms, Some(300000));
1190 assert_eq!(consensus.min_confidence, Some(0.9));
1191 }
1192
1193 #[test]
1194 fn test_all_coordination_modes() {
1195 let modes = vec![
1196 ("peer", CoordinationMode::Peer),
1197 ("hierarchical", CoordinationMode::Hierarchical),
1198 ("pipeline", CoordinationMode::Pipeline),
1199 ("swarm", CoordinationMode::Swarm),
1200 ("tiered", CoordinationMode::Tiered),
1201 ("deep", CoordinationMode::Deep),
1202 ];
1203
1204 for (yaml_value, expected) in modes {
1205 let yaml = format!(r#"
1206apiVersion: aof.dev/v1
1207kind: AgentFleet
1208metadata:
1209 name: test
1210spec:
1211 agents:
1212 - name: agent-1
1213 config: ./agent.yaml
1214 role: manager
1215 tier: 1
1216 - name: agent-2
1217 config: ./agent.yaml
1218 tier: 2
1219 coordination:
1220 mode: {}
1221 manager: agent-1
1222"#, yaml_value);
1223
1224 let fleet = AgentFleet::from_yaml(&yaml).unwrap();
1225 assert_eq!(
1226 fleet.spec.coordination.mode,
1227 expected,
1228 "Failed for mode: {}",
1229 yaml_value
1230 );
1231 }
1232 }
1233
1234 #[test]
1235 fn test_tiered_config() {
1236 let yaml = r#"
1237apiVersion: aof.dev/v1
1238kind: AgentFleet
1239metadata:
1240 name: tiered-team
1241spec:
1242 agents:
1243 - name: collector
1244 config: ./collector.yaml
1245 tier: 1
1246 - name: reasoner
1247 config: ./reasoner.yaml
1248 tier: 2
1249 coordination:
1250 mode: tiered
1251 tiered:
1252 pass_all_results: true
1253 final_aggregation: merge
1254 tier_consensus:
1255 "1":
1256 algorithm: first_wins
1257 "2":
1258 algorithm: majority
1259 min_votes: 1
1260"#;
1261
1262 let fleet = AgentFleet::from_yaml(yaml).unwrap();
1263 let tiered = fleet.spec.coordination.tiered.as_ref().unwrap();
1264
1265 assert!(tiered.pass_all_results);
1266 assert_eq!(tiered.final_aggregation, FinalAggregation::Merge);
1267
1268 let tier1_consensus = tiered.tier_consensus.get("1").unwrap();
1269 assert_eq!(tier1_consensus.algorithm, ConsensusAlgorithm::FirstWins);
1270
1271 let tier2_consensus = tiered.tier_consensus.get("2").unwrap();
1272 assert_eq!(tier2_consensus.algorithm, ConsensusAlgorithm::Majority);
1273 }
1274
1275 #[test]
1276 fn test_final_aggregation_modes() {
1277 let modes = vec![
1278 ("consensus", FinalAggregation::Consensus),
1279 ("merge", FinalAggregation::Merge),
1280 ("manager_synthesis", FinalAggregation::ManagerSynthesis),
1281 ];
1282
1283 for (yaml_value, expected) in modes {
1284 let yaml = format!(r#"
1285apiVersion: aof.dev/v1
1286kind: AgentFleet
1287metadata:
1288 name: test
1289spec:
1290 agents:
1291 - name: agent-1
1292 config: ./agent.yaml
1293 tier: 1
1294 - name: agent-2
1295 config: ./agent.yaml
1296 tier: 2
1297 coordination:
1298 mode: tiered
1299 tiered:
1300 final_aggregation: {}
1301"#, yaml_value);
1302
1303 let fleet = AgentFleet::from_yaml(&yaml).unwrap();
1304 let tiered = fleet.spec.coordination.tiered.as_ref().unwrap();
1305 assert_eq!(
1306 tiered.final_aggregation,
1307 expected,
1308 "Failed for aggregation: {}",
1309 yaml_value
1310 );
1311 }
1312 }
1313
1314 #[test]
1315 fn test_existing_simple_fleet_unchanged() {
1316 let yaml = r#"
1318apiVersion: aof.dev/v1
1319kind: AgentFleet
1320metadata:
1321 name: simple-fleet
1322spec:
1323 agents:
1324 - name: worker
1325 config: ./worker.yaml
1326"#;
1327
1328 let fleet = AgentFleet::from_yaml(yaml).unwrap();
1329 assert_eq!(fleet.spec.coordination.mode, CoordinationMode::Peer); assert!(fleet.validate().is_ok());
1331 }
1332
1333 #[test]
1334 fn test_pipeline_mode_unchanged() {
1335 let yaml = r#"
1336apiVersion: aof.dev/v1
1337kind: AgentFleet
1338metadata:
1339 name: pipeline-fleet
1340spec:
1341 agents:
1342 - name: stage1
1343 config: ./stage1.yaml
1344 - name: stage2
1345 config: ./stage2.yaml
1346 - name: stage3
1347 config: ./stage3.yaml
1348 coordination:
1349 mode: pipeline
1350"#;
1351
1352 let fleet = AgentFleet::from_yaml(yaml).unwrap();
1353 assert_eq!(fleet.spec.coordination.mode, CoordinationMode::Pipeline);
1354 assert!(fleet.validate().is_ok());
1355 }
1356
1357 #[test]
1358 fn test_deep_mode_config() {
1359 let yaml = r#"
1360apiVersion: aof.dev/v1
1361kind: AgentFleet
1362metadata:
1363 name: deep-fleet
1364spec:
1365 agents:
1366 - name: investigator
1367 spec:
1368 model: openai:gpt-4
1369 instructions: "Deep investigator"
1370 tools: []
1371 coordination:
1372 mode: deep
1373 deep:
1374 max_iterations: 15
1375 planning: true
1376 memory: true
1377 planner_model: anthropic:claude-sonnet-4
1378 planner_prompt: "Generate investigation steps."
1379 synthesizer_prompt: "Synthesize findings into a report."
1380"#;
1381
1382 let fleet = AgentFleet::from_yaml(yaml).unwrap();
1383 assert_eq!(fleet.spec.coordination.mode, CoordinationMode::Deep);
1384
1385 let deep_config = fleet.spec.coordination.deep.as_ref().unwrap();
1386 assert_eq!(deep_config.max_iterations, 15);
1387 assert!(deep_config.planning);
1388 assert!(deep_config.memory);
1389 assert_eq!(deep_config.planner_model.as_deref(), Some("anthropic:claude-sonnet-4"));
1390 assert!(deep_config.planner_prompt.is_some());
1391 assert!(deep_config.synthesizer_prompt.is_some());
1392 }
1393
1394 #[test]
1395 fn test_deep_mode_defaults() {
1396 let yaml = r#"
1397apiVersion: aof.dev/v1
1398kind: AgentFleet
1399metadata:
1400 name: deep-fleet-defaults
1401spec:
1402 agents:
1403 - name: agent
1404 config: ./agent.yaml
1405 coordination:
1406 mode: deep
1407"#;
1408
1409 let fleet = AgentFleet::from_yaml(yaml).unwrap();
1410 assert_eq!(fleet.spec.coordination.mode, CoordinationMode::Deep);
1411
1412 let deep_config = fleet.spec.coordination.deep.unwrap_or_default();
1414 assert_eq!(deep_config.max_iterations, 10); assert!(deep_config.planning); assert!(deep_config.memory); assert!(deep_config.planner_model.is_none());
1418 assert!(deep_config.planner_prompt.is_none());
1419 assert!(deep_config.synthesizer_prompt.is_none());
1420 }
1421}