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