use std::collections::BTreeMap;
use std::path::Path;
use std::sync::Arc;
use machi_types::{ErrorCode, MachiError};
use crate::builtin::builtin_definitions;
use crate::definition::AgentDefinition;
use crate::discovery::{discover_in_dir, discover_project, resolve_agents};
#[derive(Debug, Clone, Default)]
pub struct AgentRegistry {
inner: Arc<BTreeMap<String, AgentDefinition>>,
}
impl AgentRegistry {
#[must_use]
pub fn new() -> Self {
Self::default()
}
#[must_use]
pub fn with_builtins() -> Self {
Self::from_definitions(builtin_definitions())
}
pub fn resolve_from_cwd(cwd: impl AsRef<Path>) -> Result<Self, MachiError> {
Ok(Self::from_definitions(resolve_agents(cwd)?))
}
#[must_use]
pub fn from_definitions(defs: impl IntoIterator<Item = AgentDefinition>) -> Self {
let mut map = BTreeMap::new();
for def in defs {
if !def.enabled {
continue;
}
map.insert(def.name.clone(), def);
}
Self {
inner: Arc::new(map),
}
}
#[must_use]
pub fn insert(&self, def: AgentDefinition) -> Self {
let mut map = (*self.inner).clone();
if def.enabled {
map.insert(def.name.clone(), def);
} else {
map.remove(&def.name);
}
Self {
inner: Arc::new(map),
}
}
#[must_use]
pub fn merge(&self, other: &Self) -> Self {
let mut map = (*self.inner).clone();
for (k, v) in other.inner.iter() {
map.insert(k.clone(), v.clone());
}
Self {
inner: Arc::new(map),
}
}
#[must_use]
pub fn len(&self) -> usize {
self.inner.len()
}
#[must_use]
pub fn is_empty(&self) -> bool {
self.inner.is_empty()
}
#[must_use]
pub fn get(&self, name: &str) -> Option<&AgentDefinition> {
self.inner.get(name).filter(|d| d.enabled)
}
pub fn require(&self, name: &str) -> Result<&AgentDefinition, MachiError> {
self.get(name).ok_or_else(|| {
MachiError::new(
ErrorCode::AgentNotFound,
format!("agent_type '{name}' not registered"),
)
})
}
#[must_use]
pub fn names(&self) -> Vec<String> {
self.inner.keys().cloned().collect()
}
pub fn discover_dir(self, root: impl AsRef<Path>, strict: bool) -> Result<Self, MachiError> {
let found = discover_in_dir(root, strict)?;
Ok(self.merge(&Self::from_definitions(found)))
}
pub fn discover_project(self, cwd: impl AsRef<Path>) -> Result<Self, MachiError> {
let found = discover_project(cwd)?;
Ok(self.merge(&Self::from_definitions(found)))
}
pub fn resolve_layers(self, cwd: impl AsRef<Path>) -> Result<Self, MachiError> {
let _ = self;
Self::resolve_from_cwd(cwd)
}
}
#[cfg(test)]
mod tests {
use std::fs;
use std::io::Write;
use tempfile::tempdir;
use super::*;
use crate::definition::Instructions;
fn def(name: &str) -> AgentDefinition {
let mut d = AgentDefinition::new(name);
d.instructions = Instructions::Static("hi".into());
d.model = "mock".into();
d.max_steps = 4;
d
}
#[test]
fn insert_and_require() {
let reg = AgentRegistry::new().insert(def("worker"));
assert_eq!(reg.require("worker").expect("get").name, "worker");
assert_eq!(
reg.require("missing").expect_err("nf").code(),
ErrorCode::AgentNotFound
);
}
#[test]
fn discover_merges() {
let dir = tempdir().expect("tmp");
let path = dir.path().join("helper.md");
let mut f = fs::File::create(&path).expect("create");
write!(f, "---\nname: helper\nmodel: m\n---\n\nHelp.\n").expect("write");
let reg = AgentRegistry::new()
.insert(def("base"))
.discover_dir(dir.path(), true)
.expect("disc");
assert!(reg.get("base").is_some());
assert!(reg.get("helper").is_some());
assert_eq!(reg.len(), 2);
}
}