use crate::agent::identity::{AgentIdentity, AgentRole, ValidationResult};
use crate::agent::profile::prompt_contract::{
KEY_SYSTEM_PROMPT, KEY_USER_PROMPT_DIRECTORY, KEY_USER_PROMPT_FILE,
};
use crate::agent::profile::AgentConfig;
use crate::agent::storage::AgentStorage;
use crate::error::ApiError;
use std::collections::HashMap;
use std::path::PathBuf;
use std::sync::Arc;
use toml;
pub struct AgentRegistry {
agents: HashMap<String, AgentIdentity>,
storage: Arc<dyn AgentStorage>,
}
impl AgentRegistry {
pub fn new() -> Self {
Self::with_storage(Arc::new(crate::agent::storage::XdgAgentStorage::new()))
}
pub fn with_storage(storage: Arc<dyn AgentStorage>) -> Self {
Self {
agents: HashMap::new(),
storage,
}
}
pub fn register(&mut self, identity: AgentIdentity) {
self.agents.insert(identity.agent_id.clone(), identity);
}
pub fn get(&self, agent_id: &str) -> Option<&AgentIdentity> {
self.agents.get(agent_id)
}
pub fn get_or_error(&self, agent_id: &str) -> Result<&AgentIdentity, ApiError> {
self.get(agent_id)
.ok_or_else(|| ApiError::Unauthorized(format!("Agent not found: {}", agent_id)))
}
pub fn list_all(&self) -> Vec<&AgentIdentity> {
self.agents.values().collect()
}
pub fn remove(&mut self, agent_id: &str) {
self.agents.remove(agent_id);
}
pub fn load_from_config(
&mut self,
config: &crate::config::MerkleConfig,
) -> Result<(), ApiError> {
for (_, agent_config) in &config.agents {
let mut identity = AgentIdentity::new(agent_config.agent_id.clone(), agent_config.role);
if let Some(system_prompt) = &agent_config.system_prompt {
identity
.metadata
.insert(KEY_SYSTEM_PROMPT.to_string(), system_prompt.clone());
}
for (key, value) in &agent_config.metadata {
identity.metadata.insert(key.clone(), value.clone());
}
self.register(identity);
}
Ok(())
}
pub fn load_from_xdg(&mut self) -> Result<(), ApiError> {
for stored in self.storage.list()? {
let mut identity =
AgentIdentity::new(stored.config.agent_id.clone(), stored.config.role);
if let Some(prompt) = stored.resolved_system_prompt {
if !prompt.is_empty() {
identity
.metadata
.insert(KEY_SYSTEM_PROMPT.to_string(), prompt);
}
}
for (key, value) in &stored.config.metadata {
identity.metadata.insert(key.clone(), value.clone());
}
self.agents.insert(stored.agent_id.clone(), identity);
}
Ok(())
}
pub fn list_by_role(&self, role: Option<AgentRole>) -> Vec<&AgentIdentity> {
if let Some(filter_role) = role {
self.agents
.values()
.filter(|agent| agent.role == filter_role)
.collect()
} else {
self.list_all()
}
}
pub fn agent_config_path(&self, agent_id: &str) -> Result<PathBuf, ApiError> {
self.storage.path_for(agent_id)
}
pub fn save_agent_config(&self, agent_id: &str, config: &AgentConfig) -> Result<(), ApiError> {
self.storage.save(agent_id, config)
}
pub fn delete_agent_config(&self, agent_id: &str) -> Result<(), ApiError> {
self.storage.delete(agent_id)
}
pub fn agents_dir(&self) -> Result<PathBuf, ApiError> {
self.storage.agents_dir()
}
pub fn validate_agent(&self, agent_id: &str) -> Result<ValidationResult, ApiError> {
let mut result = ValidationResult::new(agent_id.to_string());
let agent = match self.get(agent_id) {
Some(a) => a,
None => {
result.add_error("Agent not found in registry".to_string());
return Ok(result);
}
};
let config_path = self.agent_config_path(agent_id)?;
if !config_path.exists() {
result.add_error(format!("Config file not found: {}", config_path.display()));
return Ok(result);
}
let expected_filename = format!("{}.toml", agent_id);
if config_path
.file_name()
.and_then(|n| n.to_str())
.map(|n| n == expected_filename)
.unwrap_or(false)
{
result.add_check("Agent ID matches filename", true);
} else {
result.add_error(format!(
"Agent ID '{}' doesn't match filename '{}'",
agent_id,
config_path
.file_name()
.and_then(|n| n.to_str())
.unwrap_or("unknown")
));
}
result.add_check("Role is valid", true);
let content = match std::fs::read_to_string(&config_path) {
Ok(c) => c,
Err(e) => {
result.add_error(format!("Failed to read config file: {}", e));
return Ok(result);
}
};
let agent_config: AgentConfig = match toml::from_str(&content) {
Ok(config) => config,
Err(e) => {
result.add_error(format!("Failed to parse config file: {}", e));
return Ok(result);
}
};
if agent.role != AgentRole::Reader {
if let Some(ref prompt_path) = agent_config.system_prompt_path {
let base_dir = crate::config::xdg::config_home()?.join("meld");
match crate::agent::prompt::resolve_prompt_path(prompt_path, &base_dir) {
Ok(resolved_path) => {
if resolved_path.exists() {
result.add_check("Prompt file exists", true);
match std::fs::metadata(&resolved_path) {
Ok(_) => result.add_check("Prompt file is readable", true),
Err(e) => {
result.add_error(format!("Prompt file not readable: {}", e))
}
}
match std::fs::read_to_string(&resolved_path) {
Ok(_) => result.add_check("Prompt file is valid UTF-8", true),
Err(e) => result
.add_error(format!("Prompt file is not valid UTF-8: {}", e)),
}
} else {
result.add_error(format!(
"Prompt file not found: {}",
resolved_path.display()
));
}
}
Err(e) => {
result.add_error(format!("Failed to resolve prompt path: {}", e));
}
}
} else {
result.add_error("Missing system_prompt_path for non-reader role".to_string());
}
if agent.metadata.get(KEY_USER_PROMPT_FILE).is_some() {
result.add_check("user_prompt_file template present", true);
} else {
result.add_error(
"Missing user_prompt_file in metadata for non-reader role".to_string(),
);
}
if agent.metadata.get(KEY_USER_PROMPT_DIRECTORY).is_some() {
result.add_check("user_prompt_directory template present", true);
} else {
result.add_error(
"Missing user_prompt_directory in metadata for non-reader role".to_string(),
);
}
} else {
result.add_check("Reader agent (no prompt required)", true);
}
Ok(result)
}
}
impl Default for AgentRegistry {
fn default() -> Self {
Self::new()
}
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn test_reader_agent() {
let agent = AgentIdentity::new("reader-1".to_string(), AgentRole::Reader);
assert!(agent.can_read());
assert!(!agent.can_write());
assert!(agent.verify_read().is_ok());
assert!(agent.verify_write().is_err());
}
#[test]
fn test_writer_agent() {
let agent = AgentIdentity::new("writer-1".to_string(), AgentRole::Writer);
assert!(agent.can_read());
assert!(agent.can_write());
assert!(agent.verify_read().is_ok());
assert!(agent.verify_write().is_ok());
}
#[test]
fn test_agent_registry() {
let mut registry = AgentRegistry::new();
let agent1 = AgentIdentity::new("agent-1".to_string(), AgentRole::Reader);
let agent2 = AgentIdentity::new("agent-2".to_string(), AgentRole::Writer);
registry.register(agent1);
registry.register(agent2);
assert!(registry.get("agent-1").is_some());
assert!(registry.get("agent-2").is_some());
assert!(registry.get("agent-3").is_none());
assert!(registry.get_or_error("agent-1").is_ok());
assert!(registry.get_or_error("agent-3").is_err());
}
#[test]
fn test_agent_registry_list_all() {
let mut registry = AgentRegistry::new();
let agent1 = AgentIdentity::new("agent-1".to_string(), AgentRole::Reader);
let agent2 = AgentIdentity::new("agent-2".to_string(), AgentRole::Writer);
let agent3 = AgentIdentity::new("agent-3".to_string(), AgentRole::Writer);
registry.register(agent1);
registry.register(agent2);
registry.register(agent3);
let all_agents = registry.list_all();
assert_eq!(all_agents.len(), 3);
let agent_ids: Vec<String> = all_agents.iter().map(|a| a.agent_id.clone()).collect();
assert!(agent_ids.contains(&"agent-1".to_string()));
assert!(agent_ids.contains(&"agent-2".to_string()));
assert!(agent_ids.contains(&"agent-3".to_string()));
}
#[test]
fn test_list_by_role() {
let mut registry = AgentRegistry::new();
let agent1 = AgentIdentity::new("agent-1".to_string(), AgentRole::Reader);
let agent2 = AgentIdentity::new("agent-2".to_string(), AgentRole::Writer);
let agent3 = AgentIdentity::new("agent-3".to_string(), AgentRole::Writer);
let agent4 = AgentIdentity::new("agent-4".to_string(), AgentRole::Writer);
registry.register(agent1);
registry.register(agent2);
registry.register(agent3);
registry.register(agent4);
let readers = registry.list_by_role(Some(AgentRole::Reader));
assert_eq!(readers.len(), 1);
assert_eq!(readers[0].agent_id, "agent-1");
let writers = registry.list_by_role(Some(AgentRole::Writer));
assert_eq!(writers.len(), 3);
let writer_ids: Vec<String> = writers.iter().map(|a| a.agent_id.clone()).collect();
assert!(writer_ids.contains(&"agent-2".to_string()));
assert!(writer_ids.contains(&"agent-3".to_string()));
assert!(writer_ids.contains(&"agent-4".to_string()));
let all = registry.list_by_role(None);
assert_eq!(all.len(), 4);
}
#[test]
fn test_get_agent_config_path() {
let registry = AgentRegistry::new();
let path = registry.agent_config_path("test-agent");
assert!(path.is_ok());
let path = path.unwrap();
assert!(path.to_string_lossy().contains("test-agent"));
assert!(path.to_string_lossy().ends_with(".toml"));
}
#[test]
fn test_validation_result() {
let mut result = ValidationResult::new("test-agent".to_string());
assert_eq!(result.agent_id, "test-agent");
assert!(result.is_valid());
assert_eq!(result.total_checks(), 0);
assert_eq!(result.passed_checks(), 0);
result.add_check("Test check 1", true);
result.add_check("Test check 2", true);
result.add_check("Test check 3", false);
assert!(!result.is_valid());
assert_eq!(result.total_checks(), 3);
assert_eq!(result.passed_checks(), 2);
result.add_error("Test error".to_string());
assert!(!result.is_valid());
assert_eq!(result.errors.len(), 1);
}
}