use std::collections::HashMap;
use super::config::MultiAgentConfig;
use super::path::AgentPath;
#[derive(Clone, Debug, PartialEq, Eq)]
pub enum AgentStatus {
Idle,
Running,
Done,
}
#[derive(Clone, Debug)]
pub struct AgentEntry {
pub path: AgentPath,
pub status: AgentStatus,
pub depth: i32,
pub tool_count: usize,
}
#[derive(Clone, Debug, PartialEq, Eq)]
pub enum SpawnError {
MaxAgentsReached { max: usize },
DepthLimitReached { max: i32, attempted: i32 },
AlreadyExists,
}
impl std::fmt::Display for SpawnError {
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
match self {
Self::MaxAgentsReached { max } => {
write!(f, "max agents reached (limit: {})", max)
}
Self::DepthLimitReached { max, attempted } => {
write!(
f,
"agent depth limit reached (max: {}, attempted: {})",
max, attempted
)
}
Self::AlreadyExists => {
write!(f, "agent with this path already exists")
}
}
}
}
pub struct AgentRegistry {
config: MultiAgentConfig,
agents: HashMap<AgentPath, AgentEntry>,
}
impl AgentRegistry {
pub fn new(config: MultiAgentConfig) -> Self {
Self {
config,
agents: HashMap::new(),
}
}
pub fn can_spawn(&self, depth: i32) -> Result<(), SpawnError> {
if self.config.enabled && self.agents.len() >= self.config.max_sub_agents {
return Err(SpawnError::MaxAgentsReached {
max: self.config.max_sub_agents,
});
}
if self.config.enabled && depth > self.config.max_agent_depth {
return Err(SpawnError::DepthLimitReached {
max: self.config.max_agent_depth,
attempted: depth,
});
}
Ok(())
}
pub fn register(
&mut self,
path: &AgentPath,
depth: i32,
tool_count: usize,
) -> Result<(), SpawnError> {
self.can_spawn(depth)?;
if self.agents.contains_key(path) {
return Err(SpawnError::AlreadyExists);
}
self.agents.insert(
path.clone(),
AgentEntry {
path: path.clone(),
status: AgentStatus::Idle,
depth,
tool_count,
},
);
Ok(())
}
pub fn close(&mut self, path: &AgentPath) -> Option<AgentEntry> {
self.agents.remove(path)
}
pub fn set_status(&mut self, path: &AgentPath, status: AgentStatus) -> bool {
match self.agents.get_mut(path) {
Some(entry) => {
entry.status = status;
true
}
None => false,
}
}
pub fn get(&self, path: &AgentPath) -> Option<&AgentEntry> {
self.agents.get(path)
}
pub fn list(&self) -> Vec<&AgentEntry> {
let mut entries: Vec<&AgentEntry> = self.agents.values().collect();
entries.sort_by(|a, b| a.path.cmp(&b.path));
entries
}
pub fn count(&self) -> usize {
self.agents.len()
}
pub fn count_by_status(&self, status: &AgentStatus) -> usize {
self.agents.values().filter(|e| e.status == *status).count()
}
pub fn is_empty(&self) -> bool {
self.agents.is_empty()
}
pub fn contains(&self, path: &AgentPath) -> bool {
self.agents.contains_key(path)
}
pub fn config(&self) -> &MultiAgentConfig {
&self.config
}
}
#[cfg(test)]
mod tests {
use super::*;
fn test_config() -> MultiAgentConfig {
MultiAgentConfig::enabled()
}
fn test_path(name: &str) -> AgentPath {
AgentPath::root().join(name)
}
#[test]
fn register_and_close() {
let mut reg = AgentRegistry::new(test_config());
let path = test_path("worker");
assert!(reg.register(&path, 1, 5).is_ok());
assert_eq!(reg.count(), 1);
assert!(reg.contains(&path));
let entry = reg.get(&path).unwrap();
assert_eq!(entry.status, AgentStatus::Idle);
assert_eq!(entry.depth, 1);
assert_eq!(entry.tool_count, 5);
let closed = reg.close(&path).unwrap();
assert_eq!(closed.path, path);
assert_eq!(reg.count(), 0);
assert!(!reg.contains(&path));
}
#[test]
fn duplicate_register_fails() {
let mut reg = AgentRegistry::new(test_config());
let path = test_path("worker");
assert!(reg.register(&path, 1, 3).is_ok());
assert_eq!(
reg.register(&path, 1, 3).unwrap_err(),
SpawnError::AlreadyExists
);
}
#[test]
fn max_agents_limit() {
let config = MultiAgentConfig::with_limits(2, 1);
let mut reg = AgentRegistry::new(config);
assert!(reg.register(&test_path("a"), 1, 1).is_ok());
assert!(reg.register(&test_path("b"), 1, 1).is_ok());
assert_eq!(
reg.register(&test_path("c"), 1, 1).unwrap_err(),
SpawnError::MaxAgentsReached { max: 2 }
);
}
#[test]
fn depth_limit() {
let config = MultiAgentConfig::with_limits(8, 1);
let reg = AgentRegistry::new(config);
assert!(reg.can_spawn(1).is_ok());
assert_eq!(
reg.can_spawn(2).unwrap_err(),
SpawnError::DepthLimitReached {
max: 1,
attempted: 2
}
);
}
#[test]
fn lifecycle_states() {
let mut reg = AgentRegistry::new(test_config());
let path = test_path("worker");
reg.register(&path, 1, 3).unwrap();
assert_eq!(reg.count_by_status(&AgentStatus::Idle), 1);
reg.set_status(&path, AgentStatus::Running);
assert_eq!(reg.count_by_status(&AgentStatus::Idle), 0);
assert_eq!(reg.count_by_status(&AgentStatus::Running), 1);
assert_eq!(reg.get(&path).unwrap().status, AgentStatus::Running);
reg.set_status(&path, AgentStatus::Done);
assert_eq!(reg.count_by_status(&AgentStatus::Running), 0);
assert_eq!(reg.count_by_status(&AgentStatus::Done), 1);
assert_eq!(reg.count(), 1);
}
#[test]
fn close_frees_quota() {
let config = MultiAgentConfig::with_limits(1, 1);
let mut reg = AgentRegistry::new(config);
let path = test_path("worker");
reg.register(&path, 1, 3).unwrap();
assert_eq!(reg.count(), 1);
assert!(reg.can_spawn(1).is_err());
reg.close(&path);
assert_eq!(reg.count(), 0);
assert!(reg.can_spawn(1).is_ok());
}
#[test]
fn list_sorted() {
let mut reg = AgentRegistry::new(test_config());
reg.register(&test_path("b"), 1, 1).unwrap();
reg.register(&test_path("a"), 1, 1).unwrap();
let list = reg.list();
assert_eq!(list.len(), 2);
assert_eq!(list[0].path.name(), "a");
assert_eq!(list[1].path.name(), "b");
}
#[test]
fn set_status_nonexistent() {
let mut reg = AgentRegistry::new(test_config());
assert!(!reg.set_status(&test_path("ghost"), AgentStatus::Running));
}
#[test]
fn disabled_config_allows_spawn() {
let config = MultiAgentConfig {
enabled: false,
..MultiAgentConfig::default()
};
let reg = AgentRegistry::new(config);
assert!(reg.can_spawn(999).is_ok());
}
#[test]
fn spawn_error_display() {
assert_eq!(
SpawnError::MaxAgentsReached { max: 8 }.to_string(),
"max agents reached (limit: 8)"
);
assert_eq!(
SpawnError::DepthLimitReached {
max: 1,
attempted: 2
}
.to_string(),
"agent depth limit reached (max: 1, attempted: 2)"
);
assert_eq!(
SpawnError::AlreadyExists.to_string(),
"agent with this path already exists"
);
}
}