Skip to main content

envoy/
dependency.rs

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
17/// Stateless store for agent-to-agent dependency tracking.
18/// Follows the MessageStore pattern: methods take `&SqliteGraph` as first parameter.
19pub 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    /// Create a dependency: dependent is blocked on blocker.
33    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        // Check for duplicate
49        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    /// Find all unresolved dependencies where the given agent is the blocker.
101    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    /// Find all unresolved dependencies where the given agent is blocked.
128    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    /// Resolve (close) a dependency by ID.
155    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}