use serde::{Deserialize, Serialize};
use std::collections::HashMap;
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct AgentFleet {
#[serde(rename = "apiVersion")]
pub api_version: String,
pub kind: String,
pub metadata: FleetMetadata,
pub spec: FleetSpec,
}
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct FleetMetadata {
pub name: String,
#[serde(default)]
pub namespace: Option<String>,
#[serde(default)]
pub labels: HashMap<String, String>,
#[serde(default)]
pub annotations: HashMap<String, String>,
}
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct FleetSpec {
pub agents: Vec<FleetAgent>,
#[serde(default)]
pub coordination: CoordinationConfig,
#[serde(default)]
pub shared: Option<SharedResources>,
#[serde(default)]
pub communication: Option<CommunicationConfig>,
#[serde(default)]
pub scaling: Option<ScalingConfig>,
}
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct FleetAgent {
pub name: String,
#[serde(default)]
pub config: Option<String>,
#[serde(default)]
pub spec: Option<FleetAgentSpec>,
#[serde(default = "default_replicas")]
pub replicas: u32,
#[serde(default)]
pub role: AgentRole,
#[serde(default)]
pub labels: HashMap<String, String>,
#[serde(default)]
pub tier: Option<u32>,
#[serde(default)]
pub weight: Option<f32>,
}
fn default_replicas() -> u32 {
1
}
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct FleetAgentSpec {
pub model: String,
#[serde(default)]
pub instructions: Option<String>,
#[serde(default)]
pub tools: Vec<crate::agent::ToolSpec>,
#[serde(default)]
pub mcp_servers: Vec<crate::McpServerConfig>,
#[serde(default)]
pub max_iterations: Option<u32>,
#[serde(default)]
pub temperature: Option<f32>,
}
#[derive(Debug, Clone, Copy, Serialize, Deserialize, Default, PartialEq, Eq)]
#[serde(rename_all = "lowercase")]
pub enum AgentRole {
#[default]
Worker,
Manager,
Specialist,
Validator,
}
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct CoordinationConfig {
#[serde(default)]
pub mode: CoordinationMode,
#[serde(default)]
pub manager: Option<String>,
#[serde(default)]
pub distribution: TaskDistribution,
#[serde(default)]
pub consensus: Option<ConsensusConfig>,
#[serde(default)]
pub aggregation: Option<FinalAggregation>,
#[serde(default)]
pub tiered: Option<TieredConfig>,
#[serde(default)]
pub deep: Option<DeepConfig>,
}
#[derive(Debug, Clone, Serialize, Deserialize, Default)]
pub struct TieredConfig {
#[serde(default)]
pub tier_consensus: HashMap<String, ConsensusConfig>,
#[serde(default)]
pub pass_all_results: bool,
#[serde(default)]
pub final_aggregation: FinalAggregation,
}
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct DeepConfig {
#[serde(default = "default_max_iterations")]
pub max_iterations: u32,
#[serde(default = "default_true")]
pub planning: bool,
#[serde(default = "default_true")]
pub memory: bool,
#[serde(default)]
pub planner_model: Option<String>,
#[serde(default)]
pub planner_prompt: Option<String>,
#[serde(default)]
pub synthesizer_prompt: Option<String>,
}
fn default_max_iterations() -> u32 {
10
}
fn default_true() -> bool {
true
}
impl Default for DeepConfig {
fn default() -> Self {
Self {
max_iterations: default_max_iterations(),
planning: default_true(),
memory: default_true(),
planner_model: None,
planner_prompt: None,
synthesizer_prompt: None,
}
}
}
#[derive(Debug, Clone, Copy, Serialize, Deserialize, Default, PartialEq, Eq)]
#[serde(rename_all = "snake_case")]
pub enum FinalAggregation {
#[default]
Consensus,
Merge,
ManagerSynthesis,
}
impl Default for CoordinationConfig {
fn default() -> Self {
Self {
mode: CoordinationMode::Peer,
manager: None,
distribution: TaskDistribution::RoundRobin,
consensus: None,
aggregation: None,
tiered: None,
deep: None,
}
}
}
#[derive(Debug, Clone, Copy, Serialize, Deserialize, Default, PartialEq, Eq)]
#[serde(rename_all = "lowercase")]
pub enum CoordinationMode {
Hierarchical,
#[default]
Peer,
Swarm,
Pipeline,
Tiered,
Deep,
}
#[derive(Debug, Clone, Copy, Serialize, Deserialize, Default, PartialEq, Eq)]
#[serde(rename_all = "kebab-case")]
pub enum TaskDistribution {
#[default]
RoundRobin,
LeastLoaded,
Random,
SkillBased,
Sticky,
}
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct ConsensusConfig {
#[serde(default)]
pub algorithm: ConsensusAlgorithm,
#[serde(default)]
pub min_votes: Option<u32>,
#[serde(default)]
pub timeout_ms: Option<u64>,
#[serde(default)]
pub allow_partial: bool,
#[serde(default)]
pub weights: HashMap<String, f32>,
#[serde(default)]
pub min_confidence: Option<f32>,
}
#[derive(Debug, Clone, Copy, Serialize, Deserialize, Default, PartialEq, Eq)]
#[serde(rename_all = "snake_case")]
pub enum ConsensusAlgorithm {
#[default]
Majority,
Unanimous,
Weighted,
FirstWins,
HumanReview,
}
#[derive(Debug, Clone, Serialize, Deserialize, Default)]
pub struct SharedResources {
#[serde(default)]
pub memory: Option<SharedMemoryConfig>,
#[serde(default)]
pub tools: Vec<SharedToolConfig>,
#[serde(default)]
pub knowledge: Option<SharedKnowledgeConfig>,
}
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct SharedMemoryConfig {
#[serde(rename = "type")]
pub memory_type: SharedMemoryType,
#[serde(default)]
pub url: Option<String>,
#[serde(default)]
pub namespace: Option<String>,
#[serde(default)]
pub ttl: Option<u64>,
}
#[derive(Debug, Clone, Copy, Serialize, Deserialize, Default, PartialEq, Eq)]
#[serde(rename_all = "lowercase")]
pub enum SharedMemoryType {
#[default]
InMemory,
Redis,
Sqlite,
Postgres,
}
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct SharedToolConfig {
#[serde(rename = "mcp-server")]
pub mcp_server: Option<String>,
pub tool: Option<String>,
#[serde(default)]
pub config: HashMap<String, serde_json::Value>,
}
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct SharedKnowledgeConfig {
#[serde(rename = "type")]
pub kb_type: String,
pub source: String,
#[serde(default)]
pub config: HashMap<String, serde_json::Value>,
}
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct CommunicationConfig {
#[serde(default)]
pub pattern: MessagePattern,
#[serde(default)]
pub queue: Option<QueueConfig>,
#[serde(default)]
pub broadcast: Option<BroadcastConfig>,
}
#[derive(Debug, Clone, Copy, Serialize, Deserialize, Default, PartialEq, Eq)]
#[serde(rename_all = "kebab-case")]
pub enum MessagePattern {
#[default]
Direct,
PubSub,
RequestReply,
Broadcast,
}
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct QueueConfig {
#[serde(rename = "type")]
pub queue_type: String,
pub url: String,
pub name: String,
}
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct BroadcastConfig {
pub channel: String,
#[serde(default)]
pub include_sender: bool,
}
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct ScalingConfig {
#[serde(default)]
pub min_replicas: Option<u32>,
#[serde(default)]
pub max_replicas: Option<u32>,
#[serde(default)]
pub auto_scale: bool,
#[serde(default)]
pub metrics: Vec<ScalingMetric>,
}
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct ScalingMetric {
pub name: String,
pub target: f64,
#[serde(rename = "type")]
pub metric_type: String,
}
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct FleetState {
pub fleet_name: String,
pub status: FleetStatus,
pub agents: HashMap<String, AgentInstanceState>,
pub active_tasks: Vec<FleetTask>,
pub completed_tasks: Vec<FleetTask>,
pub metrics: FleetMetrics,
pub started_at: Option<chrono::DateTime<chrono::Utc>>,
}
impl FleetState {
pub fn new(fleet_name: &str) -> Self {
Self {
fleet_name: fleet_name.to_string(),
status: FleetStatus::Initializing,
agents: HashMap::new(),
active_tasks: Vec::new(),
completed_tasks: Vec::new(),
metrics: FleetMetrics::default(),
started_at: None,
}
}
}
#[derive(Debug, Clone, Copy, Serialize, Deserialize, Default, PartialEq, Eq)]
#[serde(rename_all = "lowercase")]
pub enum FleetStatus {
#[default]
Initializing,
Ready,
Active,
Paused,
Failed,
ShuttingDown,
}
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct AgentInstanceState {
pub instance_id: String,
pub agent_name: String,
pub replica_index: u32,
pub status: AgentInstanceStatus,
pub current_task: Option<String>,
pub tasks_processed: u64,
pub last_activity: Option<chrono::DateTime<chrono::Utc>>,
}
#[derive(Debug, Clone, Copy, Serialize, Deserialize, Default, PartialEq, Eq)]
#[serde(rename_all = "lowercase")]
pub enum AgentInstanceStatus {
#[default]
Starting,
Idle,
Busy,
Failed,
Stopped,
}
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct FleetTask {
pub task_id: String,
pub input: serde_json::Value,
pub assigned_to: Option<String>,
pub status: FleetTaskStatus,
pub result: Option<serde_json::Value>,
pub error: Option<String>,
pub created_at: chrono::DateTime<chrono::Utc>,
pub started_at: Option<chrono::DateTime<chrono::Utc>>,
pub completed_at: Option<chrono::DateTime<chrono::Utc>>,
}
#[derive(Debug, Clone, Copy, Serialize, Deserialize, Default, PartialEq, Eq)]
#[serde(rename_all = "lowercase")]
pub enum FleetTaskStatus {
#[default]
Pending,
Assigned,
Running,
Completed,
Failed,
Cancelled,
}
#[derive(Debug, Clone, Serialize, Deserialize, Default)]
pub struct FleetMetrics {
pub total_tasks: u64,
pub completed_tasks: u64,
pub failed_tasks: u64,
pub avg_task_duration_ms: f64,
pub active_agents: u32,
pub total_agents: u32,
pub messages_exchanged: u64,
pub consensus_rounds: u64,
}
impl AgentFleet {
pub fn from_yaml(yaml: &str) -> Result<Self, crate::AofError> {
serde_yaml::from_str(yaml).map_err(|e| crate::AofError::config(format!("Failed to parse fleet YAML: {}", e)))
}
pub fn from_file(path: &str) -> Result<Self, crate::AofError> {
let content = std::fs::read_to_string(path)
.map_err(|e| crate::AofError::config(format!("Failed to read fleet file: {}", e)))?;
Self::from_yaml(&content)
}
pub fn get_agent(&self, name: &str) -> Option<&FleetAgent> {
self.spec.agents.iter().find(|a| a.name == name)
}
pub fn get_agents_by_role(&self, role: AgentRole) -> Vec<&FleetAgent> {
self.spec.agents.iter().filter(|a| a.role == role).collect()
}
pub fn get_manager(&self) -> Option<&FleetAgent> {
if let Some(ref manager_name) = self.spec.coordination.manager {
self.get_agent(manager_name)
} else {
self.get_agents_by_role(AgentRole::Manager).first().copied()
}
}
pub fn total_replicas(&self) -> u32 {
self.spec.agents.iter().map(|a| a.replicas).sum()
}
pub fn validate(&self) -> Result<(), crate::AofError> {
let mut names = std::collections::HashSet::new();
for agent in &self.spec.agents {
if !names.insert(&agent.name) {
return Err(crate::AofError::config(format!(
"Duplicate agent name in fleet: {}",
agent.name
)));
}
}
if self.spec.coordination.mode == CoordinationMode::Hierarchical {
if self.get_manager().is_none() {
return Err(crate::AofError::config(
"Hierarchical mode requires a manager agent".to_string(),
));
}
}
if self.spec.coordination.mode == CoordinationMode::Tiered {
let tiers = self.get_tiers();
if tiers.is_empty() {
return Err(crate::AofError::config(
"Tiered mode requires at least one agent with a tier assignment".to_string(),
));
}
if tiers.len() < 2 {
tracing::warn!(
"Tiered mode with only one tier ({}) - consider using peer mode instead",
tiers[0]
);
}
}
for agent in &self.spec.agents {
if agent.config.is_none() && agent.spec.is_none() {
return Err(crate::AofError::config(format!(
"Agent '{}' must have either 'config' or 'spec' defined",
agent.name
)));
}
}
Ok(())
}
pub fn get_tiers(&self) -> Vec<u32> {
let mut tiers: Vec<u32> = self
.spec
.agents
.iter()
.map(|a| a.tier.unwrap_or(1))
.collect::<std::collections::HashSet<_>>()
.into_iter()
.collect();
tiers.sort();
tiers
}
pub fn get_agents_by_tier(&self, tier: u32) -> Vec<&FleetAgent> {
self.spec
.agents
.iter()
.filter(|a| a.tier.unwrap_or(1) == tier)
.collect()
}
pub fn get_agent_weight(&self, agent_name: &str) -> f32 {
if let Some(agent) = self.get_agent(agent_name) {
if let Some(weight) = agent.weight {
return weight;
}
}
if let Some(ref consensus) = self.spec.coordination.consensus {
if let Some(weight) = consensus.weights.get(agent_name) {
return *weight;
}
}
1.0
}
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn test_parse_fleet_yaml() {
let yaml = r#"
apiVersion: aof.dev/v1
kind: AgentFleet
metadata:
name: incident-team
labels:
team: sre
spec:
agents:
- name: detector
config: ./agents/detector.yaml
replicas: 2
role: worker
- name: analyzer
config: ./agents/analyzer.yaml
replicas: 1
role: specialist
- name: coordinator
config: ./agents/coordinator.yaml
replicas: 1
role: manager
coordination:
mode: hierarchical
manager: coordinator
distribution: skill-based
shared:
memory:
type: redis
url: redis://localhost:6379
tools:
- mcp-server: kubectl-ai
"#;
let fleet = AgentFleet::from_yaml(yaml).unwrap();
assert_eq!(fleet.metadata.name, "incident-team");
assert_eq!(fleet.spec.agents.len(), 3);
assert_eq!(fleet.spec.coordination.mode, CoordinationMode::Hierarchical);
assert_eq!(fleet.total_replicas(), 4);
let manager = fleet.get_manager().unwrap();
assert_eq!(manager.name, "coordinator");
}
#[test]
fn test_peer_mode_fleet() {
let yaml = r#"
apiVersion: aof.dev/v1
kind: AgentFleet
metadata:
name: review-team
spec:
agents:
- name: reviewer-1
config: ./reviewer.yaml
- name: reviewer-2
config: ./reviewer.yaml
- name: reviewer-3
config: ./reviewer.yaml
coordination:
mode: peer
distribution: round-robin
consensus:
algorithm: majority
minVotes: 2
"#;
let fleet = AgentFleet::from_yaml(yaml).unwrap();
assert_eq!(fleet.spec.coordination.mode, CoordinationMode::Peer);
assert!(fleet.spec.coordination.consensus.is_some());
}
#[test]
fn test_fleet_validation() {
let yaml = r#"
apiVersion: aof.dev/v1
kind: AgentFleet
metadata:
name: test-fleet
spec:
agents:
- name: agent-1
config: ./agent.yaml
"#;
let fleet = AgentFleet::from_yaml(yaml).unwrap();
assert!(fleet.validate().is_ok());
let yaml = r#"
apiVersion: aof.dev/v1
kind: AgentFleet
metadata:
name: test-fleet
spec:
agents:
- name: agent-1
config: ./agent.yaml
coordination:
mode: hierarchical
"#;
let fleet = AgentFleet::from_yaml(yaml).unwrap();
assert!(fleet.validate().is_err());
}
#[test]
fn test_fleet_state() {
let mut state = FleetState::new("test-fleet");
assert_eq!(state.status, FleetStatus::Initializing);
state.status = FleetStatus::Ready;
state.metrics.total_agents = 3;
state.metrics.active_agents = 3;
assert_eq!(state.metrics.total_agents, 3);
}
#[test]
fn test_tiered_mode_fleet() {
let yaml = r#"
apiVersion: aof.dev/v1
kind: AgentFleet
metadata:
name: rca-team
spec:
agents:
# Tier 1: Data collectors (cheap models)
- name: loki-collector
config: ./agents/loki.yaml
tier: 1
- name: prometheus-collector
config: ./agents/prometheus.yaml
tier: 1
- name: k8s-collector
config: ./agents/k8s.yaml
tier: 1
# Tier 2: Reasoning models
- name: claude-analyzer
config: ./agents/claude.yaml
tier: 2
weight: 2.0
- name: gemini-analyzer
config: ./agents/gemini.yaml
tier: 2
# Tier 3: Synthesizer
- name: rca-coordinator
config: ./agents/coordinator.yaml
tier: 3
role: manager
coordination:
mode: tiered
consensus:
algorithm: weighted
min_votes: 2
min_confidence: 0.7
tiered:
pass_all_results: true
final_aggregation: manager_synthesis
"#;
let fleet = AgentFleet::from_yaml(yaml).unwrap();
assert_eq!(fleet.metadata.name, "rca-team");
assert_eq!(fleet.spec.coordination.mode, CoordinationMode::Tiered);
let tiers = fleet.get_tiers();
assert_eq!(tiers, vec![1, 2, 3]);
let tier1_agents = fleet.get_agents_by_tier(1);
assert_eq!(tier1_agents.len(), 3);
let tier2_agents = fleet.get_agents_by_tier(2);
assert_eq!(tier2_agents.len(), 2);
let tier3_agents = fleet.get_agents_by_tier(3);
assert_eq!(tier3_agents.len(), 1);
assert_eq!(fleet.get_agent_weight("claude-analyzer"), 2.0);
assert_eq!(fleet.get_agent_weight("gemini-analyzer"), 1.0);
assert!(fleet.validate().is_ok());
}
#[test]
fn test_consensus_algorithms() {
let algorithms = vec![
("majority", ConsensusAlgorithm::Majority),
("unanimous", ConsensusAlgorithm::Unanimous),
("weighted", ConsensusAlgorithm::Weighted),
("first_wins", ConsensusAlgorithm::FirstWins),
("human_review", ConsensusAlgorithm::HumanReview),
];
for (yaml_value, expected) in algorithms {
let yaml = format!(r#"
apiVersion: aof.dev/v1
kind: AgentFleet
metadata:
name: test
spec:
agents:
- name: agent-1
config: ./agent.yaml
coordination:
mode: peer
consensus:
algorithm: {}
"#, yaml_value);
let fleet = AgentFleet::from_yaml(&yaml).unwrap();
assert_eq!(
fleet.spec.coordination.consensus.as_ref().unwrap().algorithm,
expected,
"Failed for algorithm: {}",
yaml_value
);
}
}
#[test]
fn test_weighted_consensus_config() {
let yaml = r#"
apiVersion: aof.dev/v1
kind: AgentFleet
metadata:
name: weighted-team
spec:
agents:
- name: senior-reviewer
config: ./reviewer.yaml
weight: 2.0
- name: junior-reviewer
config: ./reviewer.yaml
coordination:
mode: peer
consensus:
algorithm: weighted
min_votes: 2
min_confidence: 0.8
weights:
senior-reviewer: 2.0
junior-reviewer: 1.0
"#;
let fleet = AgentFleet::from_yaml(yaml).unwrap();
let consensus = fleet.spec.coordination.consensus.as_ref().unwrap();
assert_eq!(consensus.algorithm, ConsensusAlgorithm::Weighted);
assert_eq!(consensus.min_votes, Some(2));
assert_eq!(consensus.min_confidence, Some(0.8));
assert_eq!(consensus.weights.get("senior-reviewer"), Some(&2.0));
assert_eq!(consensus.weights.get("junior-reviewer"), Some(&1.0));
assert_eq!(fleet.get_agent_weight("senior-reviewer"), 2.0);
}
#[test]
fn test_human_review_consensus() {
let yaml = r#"
apiVersion: aof.dev/v1
kind: AgentFleet
metadata:
name: critical-review
spec:
agents:
- name: analyzer-1
config: ./analyzer.yaml
- name: analyzer-2
config: ./analyzer.yaml
coordination:
mode: peer
consensus:
algorithm: human_review
timeout_ms: 300000
min_confidence: 0.9
"#;
let fleet = AgentFleet::from_yaml(yaml).unwrap();
let consensus = fleet.spec.coordination.consensus.as_ref().unwrap();
assert_eq!(consensus.algorithm, ConsensusAlgorithm::HumanReview);
assert_eq!(consensus.timeout_ms, Some(300000));
assert_eq!(consensus.min_confidence, Some(0.9));
}
#[test]
fn test_all_coordination_modes() {
let modes = vec![
("peer", CoordinationMode::Peer),
("hierarchical", CoordinationMode::Hierarchical),
("pipeline", CoordinationMode::Pipeline),
("swarm", CoordinationMode::Swarm),
("tiered", CoordinationMode::Tiered),
("deep", CoordinationMode::Deep),
];
for (yaml_value, expected) in modes {
let yaml = format!(r#"
apiVersion: aof.dev/v1
kind: AgentFleet
metadata:
name: test
spec:
agents:
- name: agent-1
config: ./agent.yaml
role: manager
tier: 1
- name: agent-2
config: ./agent.yaml
tier: 2
coordination:
mode: {}
manager: agent-1
"#, yaml_value);
let fleet = AgentFleet::from_yaml(&yaml).unwrap();
assert_eq!(
fleet.spec.coordination.mode,
expected,
"Failed for mode: {}",
yaml_value
);
}
}
#[test]
fn test_tiered_config() {
let yaml = r#"
apiVersion: aof.dev/v1
kind: AgentFleet
metadata:
name: tiered-team
spec:
agents:
- name: collector
config: ./collector.yaml
tier: 1
- name: reasoner
config: ./reasoner.yaml
tier: 2
coordination:
mode: tiered
tiered:
pass_all_results: true
final_aggregation: merge
tier_consensus:
"1":
algorithm: first_wins
"2":
algorithm: majority
min_votes: 1
"#;
let fleet = AgentFleet::from_yaml(yaml).unwrap();
let tiered = fleet.spec.coordination.tiered.as_ref().unwrap();
assert!(tiered.pass_all_results);
assert_eq!(tiered.final_aggregation, FinalAggregation::Merge);
let tier1_consensus = tiered.tier_consensus.get("1").unwrap();
assert_eq!(tier1_consensus.algorithm, ConsensusAlgorithm::FirstWins);
let tier2_consensus = tiered.tier_consensus.get("2").unwrap();
assert_eq!(tier2_consensus.algorithm, ConsensusAlgorithm::Majority);
}
#[test]
fn test_final_aggregation_modes() {
let modes = vec![
("consensus", FinalAggregation::Consensus),
("merge", FinalAggregation::Merge),
("manager_synthesis", FinalAggregation::ManagerSynthesis),
];
for (yaml_value, expected) in modes {
let yaml = format!(r#"
apiVersion: aof.dev/v1
kind: AgentFleet
metadata:
name: test
spec:
agents:
- name: agent-1
config: ./agent.yaml
tier: 1
- name: agent-2
config: ./agent.yaml
tier: 2
coordination:
mode: tiered
tiered:
final_aggregation: {}
"#, yaml_value);
let fleet = AgentFleet::from_yaml(&yaml).unwrap();
let tiered = fleet.spec.coordination.tiered.as_ref().unwrap();
assert_eq!(
tiered.final_aggregation,
expected,
"Failed for aggregation: {}",
yaml_value
);
}
}
#[test]
fn test_existing_simple_fleet_unchanged() {
let yaml = r#"
apiVersion: aof.dev/v1
kind: AgentFleet
metadata:
name: simple-fleet
spec:
agents:
- name: worker
config: ./worker.yaml
"#;
let fleet = AgentFleet::from_yaml(yaml).unwrap();
assert_eq!(fleet.spec.coordination.mode, CoordinationMode::Peer); assert!(fleet.validate().is_ok());
}
#[test]
fn test_pipeline_mode_unchanged() {
let yaml = r#"
apiVersion: aof.dev/v1
kind: AgentFleet
metadata:
name: pipeline-fleet
spec:
agents:
- name: stage1
config: ./stage1.yaml
- name: stage2
config: ./stage2.yaml
- name: stage3
config: ./stage3.yaml
coordination:
mode: pipeline
"#;
let fleet = AgentFleet::from_yaml(yaml).unwrap();
assert_eq!(fleet.spec.coordination.mode, CoordinationMode::Pipeline);
assert!(fleet.validate().is_ok());
}
#[test]
fn test_deep_mode_config() {
let yaml = r#"
apiVersion: aof.dev/v1
kind: AgentFleet
metadata:
name: deep-fleet
spec:
agents:
- name: investigator
spec:
model: openai:gpt-4
instructions: "Deep investigator"
tools: []
coordination:
mode: deep
deep:
max_iterations: 15
planning: true
memory: true
planner_model: anthropic:claude-sonnet-4
planner_prompt: "Generate investigation steps."
synthesizer_prompt: "Synthesize findings into a report."
"#;
let fleet = AgentFleet::from_yaml(yaml).unwrap();
assert_eq!(fleet.spec.coordination.mode, CoordinationMode::Deep);
let deep_config = fleet.spec.coordination.deep.as_ref().unwrap();
assert_eq!(deep_config.max_iterations, 15);
assert!(deep_config.planning);
assert!(deep_config.memory);
assert_eq!(deep_config.planner_model.as_deref(), Some("anthropic:claude-sonnet-4"));
assert!(deep_config.planner_prompt.is_some());
assert!(deep_config.synthesizer_prompt.is_some());
}
#[test]
fn test_deep_mode_defaults() {
let yaml = r#"
apiVersion: aof.dev/v1
kind: AgentFleet
metadata:
name: deep-fleet-defaults
spec:
agents:
- name: agent
config: ./agent.yaml
coordination:
mode: deep
"#;
let fleet = AgentFleet::from_yaml(yaml).unwrap();
assert_eq!(fleet.spec.coordination.mode, CoordinationMode::Deep);
let deep_config = fleet.spec.coordination.deep.unwrap_or_default();
assert_eq!(deep_config.max_iterations, 10); assert!(deep_config.planning); assert!(deep_config.memory); assert!(deep_config.planner_model.is_none());
assert!(deep_config.planner_prompt.is_none());
assert!(deep_config.synthesizer_prompt.is_none());
}
}