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