systemprompt_sync/diff/
agents.rs1use 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}