Skip to main content

systemprompt_sync/diff/
agents.rs

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