Skip to main content

systemprompt_sync/diff/
agents.rs

1use super::{compute_agent_hash, compute_db_agent_hash};
2use crate::models::{AgentDiffItem, AgentsDiffResult, DiffStatus, DiskAgent};
3use anyhow::{Result, anyhow};
4use std::collections::HashMap;
5use std::path::Path;
6use systemprompt_agent::models::Agent;
7use systemprompt_agent::repository::content::AgentRepository;
8use systemprompt_database::DbPool;
9use systemprompt_identifiers::AgentId;
10use systemprompt_models::{AGENT_CONFIG_FILENAME, DiskAgentConfig, strip_frontmatter};
11use tracing::warn;
12
13#[derive(Debug)]
14pub struct AgentsDiffCalculator {
15    agent_repo: AgentRepository,
16}
17
18impl AgentsDiffCalculator {
19    pub fn new(db: &DbPool) -> Result<Self> {
20        Ok(Self {
21            agent_repo: AgentRepository::new(db)?,
22        })
23    }
24
25    pub async fn calculate_diff(&self, agents_path: &Path) -> Result<AgentsDiffResult> {
26        let db_agents = self.agent_repo.list_all().await?;
27        let db_map: HashMap<AgentId, Agent> =
28            db_agents.into_iter().map(|a| (a.id.clone(), a)).collect();
29
30        let disk_agents = Self::scan_disk_agents(agents_path)?;
31
32        let mut result = AgentsDiffResult::default();
33
34        for (agent_id, disk_agent) in &disk_agents {
35            let disk_hash = compute_agent_hash(disk_agent);
36
37            match db_map.get(agent_id) {
38                None => {
39                    result.added.push(AgentDiffItem {
40                        agent_id: agent_id.clone(),
41                        name: disk_agent.name.clone(),
42                        status: DiffStatus::Added,
43                        disk_hash: Some(disk_hash),
44                        db_hash: None,
45                    });
46                },
47                Some(db_agent) => {
48                    let db_hash = compute_db_agent_hash(db_agent);
49                    if db_hash == disk_hash {
50                        result.unchanged += 1;
51                    } else {
52                        result.modified.push(AgentDiffItem {
53                            agent_id: agent_id.clone(),
54                            name: disk_agent.name.clone(),
55                            status: DiffStatus::Modified,
56                            disk_hash: Some(disk_hash),
57                            db_hash: Some(db_hash),
58                        });
59                    }
60                },
61            }
62        }
63
64        for (agent_id, db_agent) in &db_map {
65            if !disk_agents.contains_key(agent_id) {
66                result.removed.push(AgentDiffItem {
67                    agent_id: agent_id.clone(),
68                    name: db_agent.name.clone(),
69                    status: DiffStatus::Removed,
70                    disk_hash: None,
71                    db_hash: Some(compute_db_agent_hash(db_agent)),
72                });
73            }
74        }
75
76        Ok(result)
77    }
78
79    fn scan_disk_agents(path: &Path) -> Result<HashMap<AgentId, DiskAgent>> {
80        let mut agents = HashMap::new();
81
82        if !path.exists() {
83            return Ok(agents);
84        }
85
86        for entry in std::fs::read_dir(path)? {
87            let entry = entry?;
88            let agent_path = entry.path();
89
90            if !agent_path.is_dir() {
91                continue;
92            }
93
94            let config_path = agent_path.join(AGENT_CONFIG_FILENAME);
95            if !config_path.exists() {
96                continue;
97            }
98
99            match parse_agent_dir(&config_path, &agent_path) {
100                Ok(agent) => {
101                    agents.insert(agent.agent_id.clone(), agent);
102                },
103                Err(e) => {
104                    warn!("Failed to parse agent at {}: {}", agent_path.display(), e);
105                },
106            }
107        }
108
109        Ok(agents)
110    }
111}
112
113fn parse_agent_dir(config_path: &Path, agent_dir: &Path) -> Result<DiskAgent> {
114    let config_text = std::fs::read_to_string(config_path)?;
115    let config: DiskAgentConfig = serde_yaml::from_str(&config_text)?;
116
117    let dir_name = agent_dir
118        .file_name()
119        .and_then(|n| n.to_str())
120        .ok_or_else(|| anyhow!("Invalid agent directory name"))?;
121
122    let agent_id = config
123        .id
124        .clone()
125        .unwrap_or_else(|| AgentId::new(dir_name.replace('-', "_")));
126
127    let system_prompt_path = agent_dir.join(config.system_prompt_file());
128    let system_prompt = if system_prompt_path.exists() {
129        let raw = std::fs::read_to_string(&system_prompt_path)?;
130        Some(strip_frontmatter(&raw))
131    } else {
132        None
133    };
134
135    Ok(DiskAgent {
136        agent_id,
137        name: config.name,
138        display_name: config.display_name,
139        description: config.description,
140        system_prompt,
141        port: config.port,
142    })
143}