use std::collections::HashMap;
use serde::{Deserialize, Serialize};
use crate::provider::ModelName;
#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize)]
#[serde(rename_all = "snake_case")]
pub enum AgentMode {
Primary,
Subagent,
}
#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
pub struct PermissionRule {
pub tool: String,
pub resource: String,
#[serde(flatten)]
pub effect: PermissionEffect,
}
#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize)]
#[serde(rename_all = "snake_case")]
pub enum PermissionEffect {
Allow,
Deny,
Ask,
}
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct AgentDefinition {
pub name: String,
pub description: String,
pub mode: AgentMode,
pub system_prompt: String,
#[serde(default)]
pub hidden: bool,
#[serde(default)]
pub permissions: Vec<PermissionRule>,
#[serde(default)]
pub model: Option<ModelName>,
#[serde(default)]
pub max_steps: Option<usize>,
}
impl AgentDefinition {
#[must_use]
pub fn primary(
name: impl Into<String>,
description: impl Into<String>,
system_prompt: impl Into<String>,
) -> Self {
Self {
name: name.into(),
description: description.into(),
mode: AgentMode::Primary,
system_prompt: system_prompt.into(),
hidden: false,
permissions: Vec::new(),
model: None,
max_steps: None,
}
}
#[must_use]
pub fn subagent(
name: impl Into<String>,
description: impl Into<String>,
system_prompt: impl Into<String>,
) -> Self {
Self {
name: name.into(),
description: description.into(),
mode: AgentMode::Subagent,
system_prompt: system_prompt.into(),
hidden: false,
permissions: Vec::new(),
model: None,
max_steps: None,
}
}
#[must_use]
pub fn hidden(mut self) -> Self {
self.hidden = true;
self
}
#[must_use]
pub fn with_permissions(mut self, permissions: Vec<PermissionRule>) -> Self {
self.permissions = permissions;
self
}
#[must_use]
pub fn with_model(mut self, model: ModelName) -> Self {
self.model = Some(model);
self
}
#[must_use]
pub fn with_max_steps(mut self, steps: usize) -> Self {
self.max_steps = Some(steps);
self
}
}
#[derive(Clone, Default)]
pub struct AgentRegistry {
agents: HashMap<String, AgentDefinition>,
default_agent: Option<String>,
}
impl AgentRegistry {
#[must_use]
pub fn new() -> Self {
Self::default()
}
pub fn register(&mut self, agent: AgentDefinition) {
if self.default_agent.is_none() && agent.mode == AgentMode::Primary && !agent.hidden {
self.default_agent = Some(agent.name.clone());
}
self.agents.insert(agent.name.clone(), agent);
}
pub fn set_default(&mut self, name: impl Into<String>) {
self.default_agent = Some(name.into());
}
#[must_use]
pub fn get(&self, name: &str) -> Option<&AgentDefinition> {
self.agents.get(name)
}
#[must_use]
pub fn list(&self) -> Vec<&AgentDefinition> {
let mut agents: Vec<&AgentDefinition> = self.agents.values().collect();
agents.sort_by_key(|a| &a.name);
agents
}
#[must_use]
pub fn list_selectable(&self) -> Vec<&AgentDefinition> {
let mut agents: Vec<&AgentDefinition> = self
.agents
.values()
.filter(|a| a.mode == AgentMode::Primary && !a.hidden)
.collect();
agents.sort_by_key(|a| &a.name);
agents
}
#[must_use]
pub fn default_agent(&self) -> Option<&AgentDefinition> {
self.default_agent
.as_ref()
.and_then(|name| self.agents.get(name))
}
#[must_use]
pub fn is_empty(&self) -> bool {
self.agents.is_empty()
}
#[must_use]
pub fn len(&self) -> usize {
self.agents.len()
}
}
#[cfg(test)]
#[allow(clippy::unwrap_used)]
mod tests {
use super::*;
#[test]
fn registry_should_be_empty_when_new() {
let registry = AgentRegistry::new();
assert!(registry.is_empty());
assert!(registry.default_agent().is_none());
}
#[test]
fn registry_should_register_and_retrieve_agent() {
let mut registry = AgentRegistry::new();
let agent = AgentDefinition::primary("build", "Build agent", "You are a build agent.");
registry.register(agent);
assert_eq!(registry.len(), 1);
let a = registry.get("build").unwrap();
assert_eq!(a.name, "build");
assert_eq!(a.mode, AgentMode::Primary);
}
#[test]
fn registry_should_set_first_primary_as_default() {
let mut registry = AgentRegistry::new();
registry.register(AgentDefinition::primary("build", "Build", "prompt"));
registry.register(AgentDefinition::subagent("general", "General", "prompt"));
let default = registry.default_agent().unwrap();
assert_eq!(default.name, "build");
}
#[test]
fn registry_should_allow_explicit_default() {
let mut registry = AgentRegistry::new();
registry.register(AgentDefinition::primary("a", "A", "prompt"));
registry.register(AgentDefinition::primary("b", "B", "prompt"));
registry.set_default("b");
let default = registry.default_agent().unwrap();
assert_eq!(default.name, "b");
}
#[test]
fn registry_should_list_all_agents() {
let mut registry = AgentRegistry::new();
registry.register(AgentDefinition::primary("build", "Build", "prompt"));
registry.register(AgentDefinition::subagent("general", "General", "prompt").hidden());
let all = registry.list();
assert_eq!(all.len(), 2);
let selectable = registry.list_selectable();
assert_eq!(selectable.len(), 1);
}
#[test]
fn agent_definition_builder() {
let agent = AgentDefinition::primary("echo", "Echo agent", "You echo input.")
.with_model(ModelName::new("gpt-4o-mini"))
.with_max_steps(5)
.hidden();
assert_eq!(agent.name, "echo");
assert!(agent.hidden);
assert_eq!(agent.model.unwrap().as_str(), "gpt-4o-mini");
assert_eq!(agent.max_steps, Some(5));
}
}