Skip to main content

systemprompt_agent/services/agent_orchestration/
database.rs

1//! Database-backed view of agent service state, reconciled against live
2//! processes.
3//!
4//! [`AgentDatabaseService`] wraps the agent-service repository and the
5//! config-driven [`AgentRegistry`], translating stored rows into
6//! [`AgentStatus`] while verifying that recorded PIDs still correspond to
7//! running processes. It is the single source of truth the lifecycle, monitor,
8//! and reconciler services query and mutate.
9//!
10//! Copyright (c) systemprompt.io — Business Source License 1.1.
11//! See <https://systemprompt.io> for licensing details.
12
13use crate::repository::agent_service::AgentServiceRepository;
14use crate::services::agent_orchestration::{
15    AgentStatus, OrchestrationError, OrchestrationResult, process,
16};
17use crate::services::registry::AgentRegistry;
18use systemprompt_models::services::AgentConfig;
19
20#[derive(Debug)]
21pub struct AgentDatabaseService {
22    pub repository: AgentServiceRepository,
23    pub registry: AgentRegistry,
24}
25
26impl AgentDatabaseService {
27    pub fn new(repository: AgentServiceRepository) -> OrchestrationResult<Self> {
28        let registry = AgentRegistry::new().map_err(|e| {
29            OrchestrationError::Database(format!("Failed to load agent registry: {e}"))
30        })?;
31
32        Ok(Self {
33            repository,
34            registry,
35        })
36    }
37
38    #[must_use]
39    pub const fn with_registry(
40        repository: AgentServiceRepository,
41        registry: AgentRegistry,
42    ) -> Self {
43        Self {
44            repository,
45            registry,
46        }
47    }
48
49    pub async fn register_agent(
50        &self,
51        name: &str,
52        pid: u32,
53        port: u16,
54    ) -> OrchestrationResult<String> {
55        self.repository
56            .register_agent(name, pid, port)
57            .await
58            .map_err(|e| OrchestrationError::Database(e.to_string()))
59    }
60
61    pub async fn get_status(&self, agent_name: &str) -> OrchestrationResult<AgentStatus> {
62        let row = self
63            .repository
64            .get_agent_status(agent_name)
65            .await
66            .map_err(|e| OrchestrationError::Database(e.to_string()))?;
67
68        match row {
69            Some(r) => match (r.pid, r.status.as_str()) {
70                (Some(pid), "running") => {
71                    let pid = pid as u32;
72                    if process::process_exists(pid) {
73                        Ok(AgentStatus::Running {
74                            pid,
75                            port: r.port as u16,
76                        })
77                    } else {
78                        self.mark_failed(agent_name).await?;
79                        Ok(AgentStatus::Failed {
80                            reason: "Process died unexpectedly".to_owned(),
81                            last_attempt: None,
82                            retry_count: 0,
83                        })
84                    }
85                },
86                (_, "starting") => Ok(AgentStatus::Failed {
87                    reason: "Agent is starting".to_owned(),
88                    last_attempt: None,
89                    retry_count: 0,
90                }),
91                (_, "failed" | "crashed" | "stopped") => {
92                    let error_msg = self
93                        .get_error_message(agent_name)
94                        .await
95                        .unwrap_or_else(|_| "Unknown failure".to_owned());
96                    Ok(AgentStatus::Failed {
97                        reason: error_msg,
98                        last_attempt: None,
99                        retry_count: 0,
100                    })
101                },
102                _ => {
103                    self.mark_failed(agent_name).await?;
104                    Ok(AgentStatus::Failed {
105                        reason: "Invalid database state".to_owned(),
106                        last_attempt: None,
107                        retry_count: 0,
108                    })
109                },
110            },
111            None => Ok(AgentStatus::Failed {
112                reason: "No service record found".to_owned(),
113                last_attempt: None,
114                retry_count: 0,
115            }),
116        }
117    }
118
119    pub async fn mark_failed(&self, agent_name: &str) -> OrchestrationResult<()> {
120        self.repository
121            .mark_error(agent_name)
122            .await
123            .map_err(|e| OrchestrationError::Database(e.to_string()))?;
124
125        self.repository
126            .mark_crashed(agent_name)
127            .await
128            .map_err(|e| OrchestrationError::Database(e.to_string()))
129    }
130
131    pub async fn mark_crashed(&self, agent_name: &str) -> OrchestrationResult<()> {
132        self.mark_failed(agent_name).await
133    }
134
135    pub async fn get_error_message(&self, agent_name: &str) -> OrchestrationResult<String> {
136        let row = self
137            .repository
138            .get_agent_status(agent_name)
139            .await
140            .map_err(|e| OrchestrationError::Database(e.to_string()))?;
141
142        match row {
143            Some(r) => Ok(format!("Status: {}", r.status)),
144            None => Ok("No service record".to_owned()),
145        }
146    }
147
148    pub async fn mark_error(&self, agent_name: &str) -> OrchestrationResult<()> {
149        self.repository
150            .mark_error(agent_name)
151            .await
152            .map_err(|e| OrchestrationError::Database(e.to_string()))
153    }
154
155    pub async fn list_running_agents(&self) -> OrchestrationResult<Vec<String>> {
156        let rows = self
157            .repository
158            .list_running_agents()
159            .await
160            .map_err(|e| OrchestrationError::Database(e.to_string()))?;
161
162        Ok(rows.into_iter().map(|row| row.name).collect())
163    }
164
165    pub async fn list_all_agents(&self) -> OrchestrationResult<Vec<(String, AgentStatus)>> {
166        let agent_configs = self.registry.list_agents().await.map_err(|e| {
167            OrchestrationError::Database(format!("Failed to list agents from config: {e}"))
168        })?;
169
170        let mut agents = Vec::new();
171
172        for agent_config in agent_configs {
173            let agent_name = &agent_config.name;
174
175            let status = self.get_status(agent_name).await?;
176
177            agents.push((agent_name.clone(), status));
178        }
179
180        Ok(agents)
181    }
182
183    pub async fn agent_exists(&self, agent_name: &str) -> OrchestrationResult<bool> {
184        self.registry
185            .get_agent(agent_name)
186            .await
187            .map(|_| true)
188            .or_else(|_| Ok(false))
189    }
190
191    pub async fn get_agent_config(&self, agent_name: &str) -> OrchestrationResult<AgentConfig> {
192        let agent_config = self.registry.get_agent(agent_name).await.map_err(|e| {
193            OrchestrationError::AgentNotFound(format!(
194                "Agent {} not found in config: {}",
195                agent_name, e
196            ))
197        })?;
198
199        Ok(agent_config)
200    }
201
202    pub async fn cleanup_orphaned_services(&self) -> OrchestrationResult<u64> {
203        let rows = self
204            .repository
205            .list_running_agent_pids()
206            .await
207            .map_err(|e| OrchestrationError::Database(e.to_string()))?;
208
209        let mut cleaned = 0u64;
210
211        for row in rows {
212            let pid = row.pid as u32;
213            if !process::process_exists(pid) {
214                self.mark_crashed(&row.name).await?;
215                cleaned += 1;
216            }
217        }
218
219        Ok(cleaned)
220    }
221
222    pub async fn remove_agent_service(&self, agent_name: &str) -> OrchestrationResult<()> {
223        self.repository
224            .remove_agent_service(agent_name)
225            .await
226            .map_err(|e| OrchestrationError::Database(e.to_string()))
227    }
228
229    pub async fn update_health_status(
230        &self,
231        agent_name: &str,
232        health_status: &str,
233    ) -> OrchestrationResult<()> {
234        self.repository
235            .update_health_status(agent_name, health_status)
236            .await
237            .map_err(|e| OrchestrationError::Database(e.to_string()))
238    }
239
240    pub async fn update_agent_running(
241        &self,
242        agent_name: &str,
243        pid: u32,
244        port: u16,
245    ) -> OrchestrationResult<String> {
246        self.repository
247            .register_agent(agent_name, pid, port)
248            .await
249            .map_err(|e| OrchestrationError::Database(e.to_string()))
250    }
251
252    pub async fn update_agent_stopped(&self, agent_name: &str) -> OrchestrationResult<()> {
253        self.repository
254            .mark_stopped(agent_name)
255            .await
256            .map_err(|e| OrchestrationError::Database(e.to_string()))
257    }
258
259    pub async fn register_agent_starting(
260        &self,
261        agent_name: &str,
262        pid: u32,
263        port: u16,
264    ) -> OrchestrationResult<String> {
265        self.repository
266            .register_agent_starting(agent_name, pid, port)
267            .await
268            .map_err(|e| OrchestrationError::Database(e.to_string()))
269    }
270
271    pub async fn mark_running(&self, agent_name: &str) -> OrchestrationResult<()> {
272        self.repository
273            .mark_running(agent_name)
274            .await
275            .map_err(|e| OrchestrationError::Database(e.to_string()))
276    }
277
278    pub async fn get_unresponsive_agents(&self) -> OrchestrationResult<Vec<(String, Option<u32>)>> {
279        use crate::services::agent_orchestration::monitor::check_a2a_agent_health;
280
281        let agents = self.list_all_agents().await?;
282
283        let mut unresponsive = Vec::new();
284        for (agent_name, status) in agents {
285            if let AgentStatus::Running { pid, port, .. } = status {
286                let is_healthy = check_a2a_agent_health(port, 10).await.unwrap_or(false);
287
288                if !is_healthy {
289                    unresponsive.push((agent_name, Some(pid)));
290                }
291            }
292        }
293
294        Ok(unresponsive)
295    }
296}