1use serde::{Deserialize, Serialize};
2
3use crate::error::{EnvoyError, Result};
4
5const KIND_DEPENDENCY: &str = "EnvoyDependency";
6
7#[derive(Debug, Clone, Serialize, Deserialize)]
8pub struct AgentDependency {
9 pub dependency_id: String,
10 pub dependent_agent: String,
11 pub blocker_agent: String,
12 pub reason: String,
13 pub created_at: String,
14 pub resolved: bool,
15}
16
17pub struct DependencyStore;
20
21impl Default for DependencyStore {
22 fn default() -> Self {
23 Self::new()
24 }
25}
26
27impl DependencyStore {
28 pub fn new() -> Self {
29 Self
30 }
31
32 pub fn create(
34 &self,
35 graph: &sqlitegraph::SqliteGraph,
36 dependent_agent: String,
37 blocker_agent: String,
38 reason: String,
39 ) -> Result<AgentDependency> {
40 use sqlitegraph::GraphEntity;
41
42 if dependent_agent == blocker_agent {
43 return Err(EnvoyError::InvalidEntity(
44 "cannot depend on self".to_string(),
45 ));
46 }
47
48 let existing = graph.find_entities_by_kind(KIND_DEPENDENCY)?;
50 for e in &existing {
51 let dep = e
52 .data
53 .get("dependent_agent")
54 .and_then(|v| v.as_str())
55 .unwrap_or("");
56 let blk = e
57 .data
58 .get("blocker_agent")
59 .and_then(|v| v.as_str())
60 .unwrap_or("");
61 let res = e
62 .data
63 .get("resolved")
64 .and_then(|v| v.as_bool())
65 .unwrap_or(false);
66 if dep == dependent_agent && blk == blocker_agent && !res {
67 return Err(EnvoyError::DuplicateDependency {
68 dependent: dependent_agent,
69 blocker: blocker_agent,
70 });
71 }
72 }
73
74 let timestamp = chrono::Utc::now().to_rfc3339();
75 let entity = GraphEntity {
76 id: 0,
77 kind: KIND_DEPENDENCY.to_string(),
78 name: format!("dep-{}", uuid::Uuid::new_v4()),
79 file_path: None,
80 data: serde_json::json!({
81 "dependent_agent": dependent_agent,
82 "blocker_agent": blocker_agent,
83 "reason": reason,
84 "created_at": timestamp,
85 "resolved": false,
86 }),
87 };
88 let id = graph.insert_entity(&entity)?;
89
90 Ok(AgentDependency {
91 dependency_id: id.to_string(),
92 dependent_agent,
93 blocker_agent,
94 reason,
95 created_at: timestamp,
96 resolved: false,
97 })
98 }
99
100 pub fn find_by_blocker(
102 &self,
103 graph: &sqlitegraph::SqliteGraph,
104 blocker_agent: &str,
105 ) -> Result<Vec<AgentDependency>> {
106 let entities = graph.find_entities_by_kind(KIND_DEPENDENCY)?;
107 Ok(entities
108 .iter()
109 .filter(|e| {
110 let blk = e
111 .data
112 .get("blocker_agent")
113 .and_then(|v| v.as_str())
114 .unwrap_or("");
115 let res = e
116 .data
117 .get("resolved")
118 .and_then(|v| v.as_bool())
119 .unwrap_or(false);
120 blk == blocker_agent && !res
121 })
122 .map(entity_to_dependency)
123 .filter_map(|r| r.ok())
124 .collect())
125 }
126
127 pub fn find_by_dependent(
129 &self,
130 graph: &sqlitegraph::SqliteGraph,
131 dependent_agent: &str,
132 ) -> Result<Vec<AgentDependency>> {
133 let entities = graph.find_entities_by_kind(KIND_DEPENDENCY)?;
134 Ok(entities
135 .iter()
136 .filter(|e| {
137 let dep = e
138 .data
139 .get("dependent_agent")
140 .and_then(|v| v.as_str())
141 .unwrap_or("");
142 let res = e
143 .data
144 .get("resolved")
145 .and_then(|v| v.as_bool())
146 .unwrap_or(false);
147 dep == dependent_agent && !res
148 })
149 .map(entity_to_dependency)
150 .filter_map(|r| r.ok())
151 .collect())
152 }
153
154 pub fn resolve(
156 &self,
157 graph: &sqlitegraph::SqliteGraph,
158 dependency_id: &str,
159 ) -> Result<AgentDependency> {
160 let id: i64 = dependency_id
161 .parse()
162 .map_err(|_| EnvoyError::DependencyNotFound(dependency_id.to_string()))?;
163 let mut entity = graph
164 .get_entity(id)
165 .map_err(|_| EnvoyError::DependencyNotFound(dependency_id.to_string()))?;
166 if entity.kind != KIND_DEPENDENCY {
167 return Err(EnvoyError::DependencyNotFound(dependency_id.to_string()));
168 }
169 entity.data["resolved"] = serde_json::json!(true);
170 graph.update_entity(&entity)?;
171 entity_to_dependency(&entity)
172 }
173}
174
175fn entity_to_dependency(entity: &sqlitegraph::GraphEntity) -> Result<AgentDependency> {
176 Ok(AgentDependency {
177 dependency_id: entity.id.to_string(),
178 dependent_agent: read_str(&entity.data, "dependent_agent"),
179 blocker_agent: read_str(&entity.data, "blocker_agent"),
180 reason: read_str(&entity.data, "reason"),
181 created_at: read_str(&entity.data, "created_at"),
182 resolved: entity
183 .data
184 .get("resolved")
185 .and_then(|v| v.as_bool())
186 .unwrap_or(false),
187 })
188}
189
190fn read_str(data: &serde_json::Value, key: &str) -> String {
191 data.get(key)
192 .and_then(|v| v.as_str())
193 .unwrap_or("")
194 .to_string()
195}
196
197#[cfg(test)]
198mod tests {
199 use super::*;
200 use crate::engine::Engine;
201
202 #[test]
203 fn create_and_find_dependency() {
204 let engine = Engine::open_in_memory().unwrap();
205 let graph = engine.graph();
206 let store = DependencyStore::new();
207
208 let dep = store
209 .create(
210 graph,
211 "agent-a".into(),
212 "agent-b".into(),
213 "waiting for fix".into(),
214 )
215 .unwrap();
216 assert!(!dep.dependency_id.is_empty());
217 assert!(!dep.resolved);
218
219 let by_blocker = store.find_by_blocker(graph, "agent-b").unwrap();
220 assert_eq!(by_blocker.len(), 1);
221 assert_eq!(by_blocker[0].dependent_agent, "agent-a");
222
223 let by_dependent = store.find_by_dependent(graph, "agent-a").unwrap();
224 assert_eq!(by_dependent.len(), 1);
225 }
226
227 #[test]
228 fn resolve_dependency() {
229 let engine = Engine::open_in_memory().unwrap();
230 let graph = engine.graph();
231 let store = DependencyStore::new();
232
233 let dep = store
234 .create(graph, "agent-a".into(), "agent-b".into(), "waiting".into())
235 .unwrap();
236 let resolved = store.resolve(graph, &dep.dependency_id).unwrap();
237 assert!(resolved.resolved);
238
239 let by_blocker = store.find_by_blocker(graph, "agent-b").unwrap();
240 assert!(by_blocker.is_empty());
241 }
242
243 #[test]
244 fn reject_duplicate_dependency() {
245 let engine = Engine::open_in_memory().unwrap();
246 let graph = engine.graph();
247 let store = DependencyStore::new();
248
249 store
250 .create(graph, "a".into(), "b".into(), "reason".into())
251 .unwrap();
252 let result = store.create(graph, "a".into(), "b".into(), "reason2".into());
253 assert!(result.is_err());
254 }
255
256 #[test]
257 fn reject_self_dependency() {
258 let engine = Engine::open_in_memory().unwrap();
259 let graph = engine.graph();
260 let store = DependencyStore::new();
261
262 let result = store.create(graph, "a".into(), "a".into(), "self".into());
263 assert!(result.is_err());
264 }
265}