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::{anyhow, Result};
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::{strip_frontmatter, DiskAgentConfig, AGENT_CONFIG_FILENAME};
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> = db_agents
28            .into_iter()
29            .map(|a| (a.agent_id.clone(), a))
30            .collect();
31
32        let disk_agents = Self::scan_disk_agents(agents_path)?;
33
34        let mut result = AgentsDiffResult::default();
35
36        for (agent_id, disk_agent) in &disk_agents {
37            let disk_hash = compute_agent_hash(disk_agent);
38
39            match db_map.get(agent_id) {
40                None => {
41                    result.added.push(AgentDiffItem {
42                        agent_id: agent_id.clone(),
43                        name: disk_agent.name.clone(),
44                        status: DiffStatus::Added,
45                        disk_hash: Some(disk_hash),
46                        db_hash: None,
47                    });
48                },
49                Some(db_agent) => {
50                    let db_hash = compute_db_agent_hash(db_agent);
51                    if db_hash == disk_hash {
52                        result.unchanged += 1;
53                    } else {
54                        result.modified.push(AgentDiffItem {
55                            agent_id: agent_id.clone(),
56                            name: disk_agent.name.clone(),
57                            status: DiffStatus::Modified,
58                            disk_hash: Some(disk_hash),
59                            db_hash: Some(db_hash),
60                        });
61                    }
62                },
63            }
64        }
65
66        for (agent_id, db_agent) in &db_map {
67            if !disk_agents.contains_key(agent_id) {
68                result.removed.push(AgentDiffItem {
69                    agent_id: agent_id.clone(),
70                    name: db_agent.name.clone(),
71                    status: DiffStatus::Removed,
72                    disk_hash: None,
73                    db_hash: Some(compute_db_agent_hash(db_agent)),
74                });
75            }
76        }
77
78        Ok(result)
79    }
80
81    fn scan_disk_agents(path: &Path) -> Result<HashMap<AgentId, DiskAgent>> {
82        let mut agents = HashMap::new();
83
84        if !path.exists() {
85            return Ok(agents);
86        }
87
88        for entry in std::fs::read_dir(path)? {
89            let entry = entry?;
90            let agent_path = entry.path();
91
92            if !agent_path.is_dir() {
93                continue;
94            }
95
96            let config_path = agent_path.join(AGENT_CONFIG_FILENAME);
97            if !config_path.exists() {
98                continue;
99            }
100
101            match parse_agent_dir(&config_path, &agent_path) {
102                Ok(agent) => {
103                    agents.insert(agent.agent_id.clone(), agent);
104                },
105                Err(e) => {
106                    warn!("Failed to parse agent at {}: {}", agent_path.display(), e);
107                },
108            }
109        }
110
111        Ok(agents)
112    }
113}
114
115fn parse_agent_dir(config_path: &Path, agent_dir: &Path) -> Result<DiskAgent> {
116    let config_text = std::fs::read_to_string(config_path)?;
117    let config: DiskAgentConfig = serde_yaml::from_str(&config_text)?;
118
119    let dir_name = agent_dir
120        .file_name()
121        .and_then(|n| n.to_str())
122        .ok_or_else(|| anyhow!("Invalid agent directory name"))?;
123
124    let agent_id_str = if config.id.is_empty() {
125        dir_name.replace('-', "_")
126    } else {
127        config.id.clone()
128    };
129    let agent_id = AgentId::new(agent_id_str);
130
131    let system_prompt_path = agent_dir.join(config.system_prompt_file());
132    let system_prompt = if system_prompt_path.exists() {
133        let raw = std::fs::read_to_string(&system_prompt_path)?;
134        Some(strip_frontmatter(&raw))
135    } else {
136        None
137    };
138
139    Ok(DiskAgent {
140        agent_id,
141        name: config.name,
142        display_name: config.display_name,
143        description: config.description,
144        system_prompt,
145        port: config.port,
146    })
147}