1use std::collections::{BTreeMap, HashSet, VecDeque};
14
15use serde::{Deserialize, Serialize};
16
17use crate::config::StepConfig;
18
19#[derive(Debug, Clone, Serialize, Deserialize, PartialEq)]
21pub struct DagNode {
22 pub id: String,
24
25 pub step: StepConfig,
27
28 #[serde(default)]
30 pub depends_on: Vec<String>,
31
32 #[serde(default)]
34 pub children: Vec<String>,
35
36 #[serde(default)]
38 pub level: usize,
39}
40
41#[derive(Debug, Clone, Serialize, Deserialize, PartialEq)]
49pub struct WorkflowDag {
50 pub name: String,
52
53 pub nodes: BTreeMap<String, DagNode>,
59
60 pub levels: Vec<Vec<String>>,
64
65 pub roots: Vec<String>,
67
68 pub leaves: Vec<String>,
70}
71
72impl WorkflowDag {
73 pub fn new(name: impl Into<String>) -> Self {
75 Self {
76 name: name.into(),
77 nodes: BTreeMap::new(),
78 levels: Vec::new(),
79 roots: Vec::new(),
80 leaves: Vec::new(),
81 }
82 }
83
84 pub fn add_node(&mut self, node: DagNode) {
86 self.nodes.insert(node.id.clone(), node);
87 }
88
89 pub fn build(&mut self) {
96 let children_map: BTreeMap<String, Vec<String>> = {
98 let mut map: BTreeMap<String, Vec<String>> = BTreeMap::new();
99 for node in self.nodes.values() {
100 for parent_id in &node.depends_on {
101 map.entry(parent_id.clone())
102 .or_default()
103 .push(node.id.clone());
104 }
105 }
106 map
107 };
108
109 for (parent_id, children) in &children_map {
111 if let Some(parent) = self.nodes.get_mut(parent_id) {
112 for child_id in children {
113 if !parent.children.contains(child_id) {
114 parent.children.push(child_id.clone());
115 }
116 }
117 }
118 }
119
120 self.roots = self
122 .nodes
123 .values()
124 .filter(|n| n.depends_on.is_empty())
125 .map(|n| n.id.clone())
126 .collect();
127
128 self.leaves = self
130 .nodes
131 .values()
132 .filter(|n| n.children.is_empty())
133 .map(|n| n.id.clone())
134 .collect();
135
136 self.compute_levels();
138 }
139
140 fn compute_levels(&mut self) {
145 self.levels.clear();
146
147 let mut assigned: HashSet<String> = HashSet::new();
149 let mut current_level: Vec<String> = self.roots.clone();
150
151 if current_level.is_empty() && !self.nodes.is_empty() {
152 return;
155 }
156
157 while !current_level.is_empty() {
158 for node_id in ¤t_level {
160 if let Some(node) = self.nodes.get_mut(node_id) {
161 node.level = self.levels.len();
162 }
163 assigned.insert(node_id.clone());
164 }
165
166 self.levels.push(current_level.clone());
167
168 let mut next_level: Vec<String> = Vec::new();
170 for (id, node) in &self.nodes {
171 if assigned.contains(id) {
172 continue;
173 }
174 let all_deps_done = node.depends_on.iter().all(|dep| assigned.contains(dep));
176 if all_deps_done {
177 next_level.push(id.clone());
178 }
179 }
180
181 current_level = next_level;
182 }
183 }
184
185 pub fn get_node(&self, id: &str) -> Option<&DagNode> {
187 self.nodes.get(id)
188 }
189
190 pub fn node_count(&self) -> usize {
192 self.nodes.len()
193 }
194
195 pub fn level_count(&self) -> usize {
197 self.levels.len()
198 }
199
200 pub fn level_nodes(&self, level: usize) -> Vec<&DagNode> {
202 if level >= self.levels.len() {
203 return Vec::new();
204 }
205 self.levels[level]
206 .iter()
207 .filter_map(|id| self.nodes.get(id))
208 .collect()
209 }
210
211 pub fn topological_order(&self) -> Vec<String> {
213 self.levels.iter().flatten().cloned().collect()
214 }
215
216 pub fn has_cycle(&self) -> bool {
221 let mut in_degree: BTreeMap<String, usize> =
222 self.nodes.keys().map(|id| (id.clone(), 0)).collect();
223
224 for node in self.nodes.values() {
225 in_degree.insert(node.id.clone(), node.depends_on.len());
226 }
227
228 let mut queue: VecDeque<String> = in_degree
229 .iter()
230 .filter(|(_, deg)| **deg == 0)
231 .map(|(id, _)| id.clone())
232 .collect();
233
234 let mut visited_count = 0;
235 while let Some(id) = queue.pop_front() {
236 visited_count += 1;
237 if let Some(node) = self.nodes.get(&id) {
238 for child_id in &node.children {
239 if let Some(deg) = in_degree.get_mut(child_id) {
240 *deg -= 1;
241 if *deg == 0 {
242 queue.push_back(child_id.clone());
243 }
244 }
245 }
246 }
247 }
248
249 visited_count != self.nodes.len()
250 }
251
252 pub fn transitive_deps(&self, node_id: &str) -> HashSet<String> {
254 let mut deps = HashSet::new();
255 let mut to_visit = VecDeque::new();
256
257 if let Some(node) = self.nodes.get(node_id) {
258 for dep_id in &node.depends_on {
259 to_visit.push_back(dep_id.clone());
260 }
261 }
262
263 while let Some(id) = to_visit.pop_front() {
264 if deps.contains(&id) {
265 continue;
266 }
267 deps.insert(id.clone());
268 if let Some(node) = self.nodes.get(&id) {
269 for dep_id in &node.depends_on {
270 if !deps.contains(dep_id) {
271 to_visit.push_back(dep_id.clone());
272 }
273 }
274 }
275 }
276
277 deps
278 }
279}
280
281#[cfg(test)]
282mod tests {
283 use super::*;
284 use crate::config::StepConfig;
285 use std::collections::HashMap;
286
287 fn make_step(id: &str, depends_on: Vec<&str>) -> StepConfig {
288 StepConfig {
289 id: id.to_string(),
290 description: format!("Step {}", id),
291 depends_on: depends_on.iter().map(|s| s.to_string()).collect(),
292 tool: None,
293 tool_args: None,
294 prompt: None,
295 requires_approval: false,
296 timeout_secs: None,
297 retry_policy: None,
298 metadata: HashMap::new(),
299 }
300 }
301
302 fn make_node(id: &str, depends_on: Vec<&str>) -> DagNode {
303 let depends_on_owned: Vec<String> = depends_on.iter().map(|s| s.to_string()).collect();
304 DagNode {
305 id: id.to_string(),
306 step: make_step(id, depends_on),
307 depends_on: depends_on_owned,
308 children: Vec::new(),
309 level: 0,
310 }
311 }
312
313 #[test]
314 fn test_simple_dag() {
315 let mut dag = WorkflowDag::new("test");
316 dag.add_node(make_node("a", vec![]));
317 dag.add_node(make_node("b", vec!["a"]));
318 dag.add_node(make_node("c", vec!["b"]));
319 dag.build();
320
321 assert_eq!(dag.node_count(), 3);
322 assert_eq!(dag.level_count(), 3);
323 assert_eq!(dag.roots, vec!["a"]);
324 assert_eq!(dag.leaves, vec!["c"]);
325 assert_eq!(dag.levels[0], vec!["a"]);
326 assert_eq!(dag.levels[1], vec!["b"]);
327 assert_eq!(dag.levels[2], vec!["c"]);
328 }
329
330 #[test]
331 fn test_parallel_dag() {
332 let mut dag = WorkflowDag::new("test");
333 dag.add_node(make_node("a", vec![]));
334 dag.add_node(make_node("b", vec![]));
335 dag.add_node(make_node("c", vec!["a", "b"]));
336 dag.build();
337
338 assert_eq!(dag.level_count(), 2);
339 assert_eq!(dag.levels[0], vec!["a", "b"]);
342 assert_eq!(dag.levels[1], vec!["c"]);
343 assert_eq!(dag.roots, vec!["a", "b"]);
345 assert_eq!(dag.leaves, vec!["c"]);
346 }
347
348 #[test]
349 fn test_diamond_dag() {
350 let mut dag = WorkflowDag::new("test");
353 dag.add_node(make_node("a", vec![]));
354 dag.add_node(make_node("b", vec!["a"]));
355 dag.add_node(make_node("c", vec!["a"]));
356 dag.add_node(make_node("d", vec!["b", "c"]));
357 dag.build();
358
359 assert_eq!(dag.level_count(), 3);
360 assert_eq!(dag.levels[0], vec!["a"]);
361 assert_eq!(dag.levels[1], vec!["b", "c"]);
364 assert_eq!(dag.levels[2], vec!["d"]);
365 }
366
367 #[test]
368 fn test_dag_cycle_detection() {
369 let mut dag = WorkflowDag::new("test");
370 dag.add_node(make_node("a", vec!["b"]));
371 dag.add_node(make_node("b", vec!["a"]));
372 dag.build();
373
374 assert!(dag.has_cycle());
375 }
376
377 #[test]
378 fn test_dag_no_cycle() {
379 let mut dag = WorkflowDag::new("test");
380 dag.add_node(make_node("a", vec![]));
381 dag.add_node(make_node("b", vec!["a"]));
382 dag.build();
383
384 assert!(!dag.has_cycle());
385 }
386
387 #[test]
388 fn test_dag_topological_order() {
389 let mut dag = WorkflowDag::new("test");
390 dag.add_node(make_node("a", vec![]));
391 dag.add_node(make_node("b", vec!["a"]));
392 dag.add_node(make_node("c", vec!["a"]));
393 dag.add_node(make_node("d", vec!["b", "c"]));
394 dag.build();
395
396 let order = dag.topological_order();
397 assert_eq!(order.len(), 4);
398 let a_pos = order.iter().position(|x| x == "a").unwrap();
400 let b_pos = order.iter().position(|x| x == "b").unwrap();
401 let c_pos = order.iter().position(|x| x == "c").unwrap();
402 let d_pos = order.iter().position(|x| x == "d").unwrap();
403 assert!(a_pos < b_pos);
404 assert!(a_pos < c_pos);
405 assert!(b_pos < d_pos);
406 assert!(c_pos < d_pos);
407 }
408
409 #[test]
410 fn test_dag_transitive_deps() {
411 let mut dag = WorkflowDag::new("test");
412 dag.add_node(make_node("a", vec![]));
413 dag.add_node(make_node("b", vec!["a"]));
414 dag.add_node(make_node("c", vec!["b"]));
415 dag.build();
416
417 let deps = dag.transitive_deps("c");
418 assert!(deps.contains("a"));
419 assert!(deps.contains("b"));
420 assert_eq!(deps.len(), 2);
421 }
422
423 #[test]
424 fn test_dag_level_nodes() {
425 let mut dag = WorkflowDag::new("test");
426 dag.add_node(make_node("a", vec![]));
427 dag.add_node(make_node("b", vec!["a"]));
428 dag.build();
429
430 let level0 = dag.level_nodes(0);
431 assert_eq!(level0.len(), 1);
432 assert_eq!(level0[0].id, "a");
433
434 let level1 = dag.level_nodes(1);
435 assert_eq!(level1.len(), 1);
436 assert_eq!(level1[0].id, "b");
437 }
438}