1use std::collections::{HashMap, HashSet, VecDeque};
10use std::sync::Arc;
11
12use serde_json::Value;
13use tokio::sync::mpsc;
14
15use super::blocks;
16use crate::drone::data_flow::ExecutionScope;
17use crate::drone::types::{BlockKind, BlockState, FlowEdge, FlowNode, DroneGraph};
18
19#[derive(Debug, Clone, serde::Serialize, ts_rs::TS)]
21#[ts(export, export_to = "../../../frontend/types/rpc/")]
22#[serde(tag = "kind", rename_all = "snake_case")]
23pub enum RunEvent {
24 RunStarted { run_id: String, drone_id: String },
25 BlockStarted { run_id: String, block_id: String },
26 BlockDone {
27 run_id: String,
28 block_id: String,
29 #[ts(type = "unknown")]
30 output: Value,
31 },
32 BlockError {
33 run_id: String,
34 block_id: String,
35 error: String,
36 },
37 RunDone {
38 run_id: String,
39 #[ts(type = "unknown")]
40 output: Value,
41 },
42 RunFailed { run_id: String, error: String },
43}
44
45pub struct RunHandle {
46 pub run_id: String,
47 pub events: mpsc::UnboundedReceiver<RunEvent>,
48 pub final_states: Arc<tokio::sync::Mutex<HashMap<String, BlockState>>>,
49}
50
51pub async fn run_drone(
55 drone_id: String,
56 graph: DroneGraph,
57) -> Result<RunHandle, String> {
58 let run_id = uuid::Uuid::new_v4().to_string();
59 let (tx, rx) = mpsc::unbounded_channel();
60 let final_states: Arc<tokio::sync::Mutex<HashMap<String, BlockState>>> =
61 Arc::new(tokio::sync::Mutex::new(HashMap::new()));
62
63 let states_for_task = final_states.clone();
64 let drone_id_for_evt = drone_id.clone();
65 let run_id_for_task = run_id.clone();
66 let tx_for_task = tx.clone();
67 tokio::spawn(async move {
68 let _ = tx_for_task.send(RunEvent::RunStarted {
69 run_id: run_id_for_task.clone(),
70 drone_id: drone_id_for_evt,
71 });
72 match execute(&run_id_for_task, &graph, &tx_for_task, &states_for_task).await {
73 Ok(output) => {
74 let _ = tx_for_task.send(RunEvent::RunDone {
75 run_id: run_id_for_task,
76 output,
77 });
78 }
79 Err(e) => {
80 let _ = tx_for_task.send(RunEvent::RunFailed {
81 run_id: run_id_for_task,
82 error: e,
83 });
84 }
85 }
86 });
87
88 Ok(RunHandle {
89 run_id,
90 events: rx,
91 final_states,
92 })
93}
94
95async fn execute(
96 run_id: &str,
97 graph: &DroneGraph,
98 tx: &mpsc::UnboundedSender<RunEvent>,
99 final_states: &Arc<tokio::sync::Mutex<HashMap<String, BlockState>>>,
100) -> Result<Value, String> {
101 if graph.nodes.is_empty() {
102 return Err("drone has no blocks".to_string());
103 }
104
105 let layers = topological_layers(graph)?;
106 let mut scope = ExecutionScope::new();
107 let mut response_output: Option<Value> = None;
108 let nodes_by_id: HashMap<String, &FlowNode> =
109 graph.nodes.iter().map(|n| (n.id.clone(), n)).collect();
110 let mut skipped: HashSet<String> = HashSet::new();
116
117 for layer in layers {
118 for block_id in layer {
119 let node = nodes_by_id
120 .get(&block_id)
121 .ok_or_else(|| format!("internal: missing node {block_id}"))?;
122 let kind = block_kind_of(node)?;
123
124 if should_skip(&block_id, graph, &nodes_by_id, &scope, &skipped) {
125 skipped.insert(block_id.clone());
126 mark_state(
127 final_states,
128 &block_id,
129 BlockState {
130 status: "skipped".to_string(),
131 output: None,
132 error: None,
133 started_at: None,
134 completed_at: Some(now_ms()),
135 },
136 )
137 .await;
138 continue;
139 }
140
141 let _ = tx.send(RunEvent::BlockStarted {
142 run_id: run_id.to_string(),
143 block_id: block_id.clone(),
144 });
145 let started_at = now_ms();
149 mark_state(
150 final_states,
151 &block_id,
152 BlockState {
153 status: "running".to_string(),
154 output: None,
155 error: None,
156 started_at: Some(started_at),
157 completed_at: None,
158 },
159 )
160 .await;
161
162 let result = match kind {
163 BlockKind::Variables => blocks::variables::run(node, &mut scope).await,
164 BlockKind::Api => blocks::api::run(node, &scope).await,
165 BlockKind::Condition => blocks::condition::run(node, &scope).await,
166 BlockKind::Response => blocks::response::run(node, &scope).await,
167 BlockKind::Agent => blocks::agent::run(node, &scope).await,
168 };
169
170 match result {
171 Ok(output) => {
172 scope.outputs.insert(block_id.clone(), output.clone());
173 let _ = tx.send(RunEvent::BlockDone {
174 run_id: run_id.to_string(),
175 block_id: block_id.clone(),
176 output: output.clone(),
177 });
178 mark_state(
179 final_states,
180 &block_id,
181 BlockState {
182 status: "done".to_string(),
183 output: Some(output.clone()),
184 error: None,
185 started_at: Some(started_at),
186 completed_at: Some(now_ms()),
187 },
188 )
189 .await;
190 if matches!(kind, BlockKind::Response) {
191 response_output = Some(
197 output
198 .get("value")
199 .cloned()
200 .unwrap_or(output),
201 );
202 }
203 }
204 Err(e) => {
205 let _ = tx.send(RunEvent::BlockError {
206 run_id: run_id.to_string(),
207 block_id: block_id.clone(),
208 error: e.clone(),
209 });
210 mark_state(
211 final_states,
212 &block_id,
213 BlockState {
214 status: "error".to_string(),
215 output: None,
216 error: Some(e.clone()),
217 started_at: Some(started_at),
218 completed_at: Some(now_ms()),
219 },
220 )
221 .await;
222 return Err(format!("block {block_id} failed: {e}"));
223 }
224 }
225 }
226 }
227
228 Ok(response_output.unwrap_or(Value::Null))
229}
230
231async fn mark_state(
232 states: &Arc<tokio::sync::Mutex<HashMap<String, BlockState>>>,
233 id: &str,
234 state: BlockState,
235) {
236 let mut g = states.lock().await;
237 g.insert(id.to_string(), state);
238}
239
240use agentmux_common::time::now_ms;
241
242pub fn topological_layers(graph: &DroneGraph) -> Result<Vec<Vec<String>>, String> {
246 let mut indegree: HashMap<String, usize> = HashMap::new();
247 let mut outedges: HashMap<String, Vec<String>> = HashMap::new();
248 let known: HashSet<String> = graph.nodes.iter().map(|n| n.id.clone()).collect();
249 for n in &graph.nodes {
250 indegree.entry(n.id.clone()).or_insert(0);
251 outedges.entry(n.id.clone()).or_insert_with(Vec::new);
252 }
253 for e in &graph.edges {
254 if !known.contains(&e.source) || !known.contains(&e.target) {
255 return Err(format!(
256 "edge references unknown node ({} → {})",
257 e.source, e.target
258 ));
259 }
260 *indegree.entry(e.target.clone()).or_insert(0) += 1;
261 outedges
262 .entry(e.source.clone())
263 .or_insert_with(Vec::new)
264 .push(e.target.clone());
265 }
266
267 let mut layers: Vec<Vec<String>> = Vec::new();
268 let mut frontier: VecDeque<String> = indegree
269 .iter()
270 .filter_map(|(id, deg)| if *deg == 0 { Some(id.clone()) } else { None })
271 .collect();
272
273 while !frontier.is_empty() {
274 let mut layer: Vec<String> = Vec::new();
275 let next_frontier: Vec<String> = frontier.drain(..).collect();
276 for id in &next_frontier {
277 layer.push(id.clone());
278 }
279 layer.sort();
281 for id in &layer {
282 if let Some(targets) = outedges.get(id).cloned() {
283 for t in targets {
284 if let Some(d) = indegree.get_mut(&t) {
285 if *d > 0 {
286 *d -= 1;
287 if *d == 0 {
288 frontier.push_back(t);
289 }
290 }
291 }
292 }
293 }
294 }
295 layers.push(layer);
296 }
297
298 let scheduled: usize = layers.iter().map(|l| l.len()).sum();
299 if scheduled != graph.nodes.len() {
300 return Err("drone contains a cycle".to_string());
301 }
302 Ok(layers)
303}
304
305fn block_kind_of(node: &FlowNode) -> Result<BlockKind, String> {
306 let kind = node
307 .data
308 .get("kind")
309 .and_then(|v| v.as_str())
310 .ok_or_else(|| format!("node {} has no `kind` field", node.id))?;
311 BlockKind::parse(kind).ok_or_else(|| format!("unknown block kind: {kind}"))
312}
313
314fn should_skip(
326 node_id: &str,
327 graph: &DroneGraph,
328 nodes_by_id: &HashMap<String, &FlowNode>,
329 scope: &ExecutionScope,
330 skipped: &HashSet<String>,
331) -> bool {
332 let mut had_incoming = false;
333 let mut had_active = false;
334 for edge in &graph.edges {
335 if edge.target != node_id {
336 continue;
337 }
338 had_incoming = true;
339 if edge_is_active(edge, nodes_by_id, scope, skipped) {
340 had_active = true;
341 break;
342 }
343 }
344 had_incoming && !had_active
345}
346
347fn edge_is_active(
350 edge: &FlowEdge,
351 nodes_by_id: &HashMap<String, &FlowNode>,
352 scope: &ExecutionScope,
353 skipped: &HashSet<String>,
354) -> bool {
355 if skipped.contains(&edge.source) {
356 return false;
357 }
358 let src_node = match nodes_by_id.get(&edge.source) {
359 Some(n) => n,
360 None => return true,
361 };
362 let is_condition = src_node
363 .data
364 .get("kind")
365 .and_then(|v| v.as_str())
366 == Some("condition");
367 if !is_condition {
368 return true;
369 }
370 let cond_result = scope
371 .outputs
372 .get(&edge.source)
373 .and_then(|v| v.get("result"))
374 .and_then(|v| v.as_bool());
375 match (edge.source_handle.as_deref(), cond_result) {
376 (Some("true"), Some(r)) => r,
377 (Some("false"), Some(r)) => !r,
378 _ => true,
382 }
383}
384
385#[cfg(test)]
386mod tests {
387 use super::*;
388 use crate::drone::types::{FlowEdge, FlowNode, NodePosition};
389 use serde_json::json;
390
391 fn n(id: &str, kind: &str) -> FlowNode {
392 FlowNode {
393 id: id.to_string(),
394 position: NodePosition::default(),
395 data: json!({ "kind": kind }),
396 node_type: None,
397 }
398 }
399
400 fn e(id: &str, src: &str, dst: &str) -> FlowEdge {
401 FlowEdge {
402 id: id.to_string(),
403 source: src.to_string(),
404 target: dst.to_string(),
405 source_handle: None,
406 target_handle: None,
407 }
408 }
409
410 #[test]
411 fn topo_orders_diamond() {
412 let g = DroneGraph {
414 nodes: vec![n("a", "variables"), n("b", "api"), n("c", "api"), n("d", "response")],
415 edges: vec![
416 e("e1", "a", "b"),
417 e("e2", "a", "c"),
418 e("e3", "b", "d"),
419 e("e4", "c", "d"),
420 ],
421 };
422 let layers = topological_layers(&g).unwrap();
423 assert_eq!(layers.len(), 3);
424 assert_eq!(layers[0], vec!["a"]);
425 assert_eq!(layers[1], vec!["b", "c"]);
426 assert_eq!(layers[2], vec!["d"]);
427 }
428
429 #[test]
430 fn topo_rejects_cycle() {
431 let g = DroneGraph {
432 nodes: vec![n("a", "variables"), n("b", "api")],
433 edges: vec![e("e1", "a", "b"), e("e2", "b", "a")],
434 };
435 assert!(topological_layers(&g).is_err());
436 }
437
438 #[test]
439 fn topo_rejects_unknown_node_in_edge() {
440 let g = DroneGraph {
441 nodes: vec![n("a", "variables")],
442 edges: vec![e("e1", "a", "ghost")],
443 };
444 assert!(topological_layers(&g).is_err());
445 }
446
447 #[test]
448 fn topo_single_node_one_layer() {
449 let g = DroneGraph {
450 nodes: vec![n("a", "response")],
451 edges: vec![],
452 };
453 let layers = topological_layers(&g).unwrap();
454 assert_eq!(layers, vec![vec!["a".to_string()]]);
455 }
456
457 fn eh(id: &str, src: &str, dst: &str, handle: Option<&str>) -> FlowEdge {
462 FlowEdge {
463 id: id.to_string(),
464 source: src.to_string(),
465 target: dst.to_string(),
466 source_handle: handle.map(|s| s.to_string()),
467 target_handle: None,
468 }
469 }
470
471 fn nodes_map<'a>(g: &'a DroneGraph) -> HashMap<String, &'a FlowNode> {
472 g.nodes.iter().map(|n| (n.id.clone(), n)).collect()
473 }
474
475 fn scope_with_cond(cond_id: &str, result: bool) -> ExecutionScope {
476 let mut scope = ExecutionScope::new();
477 scope
478 .outputs
479 .insert(cond_id.to_string(), json!({ "result": result }));
480 scope
481 }
482
483 #[test]
484 fn skip_root_node_runs() {
485 let g = DroneGraph {
487 nodes: vec![n("a", "variables")],
488 edges: vec![],
489 };
490 let nodes = nodes_map(&g);
491 assert!(!should_skip(
492 "a",
493 &g,
494 &nodes,
495 &ExecutionScope::new(),
496 &HashSet::new()
497 ));
498 }
499
500 #[test]
501 fn skip_unconditional_chain_runs() {
502 let g = DroneGraph {
504 nodes: vec![n("a", "variables"), n("b", "api")],
505 edges: vec![e("e1", "a", "b")],
506 };
507 let nodes = nodes_map(&g);
508 assert!(!should_skip(
509 "b",
510 &g,
511 &nodes,
512 &ExecutionScope::new(),
513 &HashSet::new()
514 ));
515 }
516
517 #[test]
518 fn skip_condition_false_branch_when_result_true() {
519 let g = DroneGraph {
522 nodes: vec![n("c", "condition"), n("t", "api"), n("f", "api")],
523 edges: vec![
524 eh("e1", "c", "t", Some("true")),
525 eh("e2", "c", "f", Some("false")),
526 ],
527 };
528 let nodes = nodes_map(&g);
529 let scope = scope_with_cond("c", true);
530 let skipped = HashSet::new();
531 assert!(!should_skip("t", &g, &nodes, &scope, &skipped));
532 assert!(should_skip("f", &g, &nodes, &scope, &skipped));
533 }
534
535 #[test]
536 fn skip_condition_true_branch_when_result_false() {
537 let g = DroneGraph {
538 nodes: vec![n("c", "condition"), n("t", "api"), n("f", "api")],
539 edges: vec![
540 eh("e1", "c", "t", Some("true")),
541 eh("e2", "c", "f", Some("false")),
542 ],
543 };
544 let nodes = nodes_map(&g);
545 let scope = scope_with_cond("c", false);
546 let skipped = HashSet::new();
547 assert!(should_skip("t", &g, &nodes, &scope, &skipped));
548 assert!(!should_skip("f", &g, &nodes, &scope, &skipped));
549 }
550
551 #[test]
552 fn skip_transitive_through_skipped_source() {
553 let g = DroneGraph {
557 nodes: vec![n("c", "condition"), n("f", "api"), n("x", "agent")],
558 edges: vec![
559 eh("e1", "c", "f", Some("false")),
560 e("e2", "f", "x"),
561 ],
562 };
563 let nodes = nodes_map(&g);
564 let scope = scope_with_cond("c", true);
565 let mut skipped = HashSet::new();
566 skipped.insert("f".to_string());
567 assert!(should_skip("x", &g, &nodes, &scope, &skipped));
568 }
569
570 #[test]
571 fn join_runs_if_any_incoming_active() {
572 let g = DroneGraph {
575 nodes: vec![n("a", "variables"), n("b", "api"), n("d", "response")],
576 edges: vec![e("e1", "a", "d"), e("e2", "b", "d")],
577 };
578 let nodes = nodes_map(&g);
579 let mut skipped = HashSet::new();
580 skipped.insert("b".to_string());
581 assert!(!should_skip(
582 "d",
583 &g,
584 &nodes,
585 &ExecutionScope::new(),
586 &skipped
587 ));
588 }
589
590 #[test]
591 fn condition_edge_without_handle_is_permissive() {
592 let g = DroneGraph {
596 nodes: vec![n("c", "condition"), n("x", "api")],
597 edges: vec![eh("e1", "c", "x", None)],
598 };
599 let nodes = nodes_map(&g);
600 let scope = scope_with_cond("c", false);
601 assert!(!should_skip("x", &g, &nodes, &scope, &HashSet::new()));
602 }
603
604 #[tokio::test]
605 async fn execute_preserves_started_at_in_block_state() {
606 let mut vars_node = n("v1", "variables");
611 vars_node.data = json!({
617 "kind": "variables",
618 "entries": [{ "name": "v", "value": 1 }]
619 });
620 let mut resp_node = n("r1", "response");
621 resp_node.data = json!({ "kind": "response", "template": "done" });
622 let g = DroneGraph {
623 nodes: vec![vars_node, resp_node],
624 edges: vec![e("e1", "v1", "r1")],
625 };
626
627 let handle = run_drone("wf1".to_string(), g).await.unwrap();
628 let mut rx = handle.events;
629 while let Some(ev) = rx.recv().await {
630 if matches!(ev, RunEvent::RunDone { .. } | RunEvent::RunFailed { .. }) {
631 break;
632 }
633 }
634
635 let states = handle.final_states.lock().await;
636 let v1 = states.get("v1").expect("v1 state");
637 let r1 = states.get("r1").expect("r1 state");
638 assert_eq!(v1.status, "done");
639 assert!(
640 v1.started_at.is_some() && v1.started_at.unwrap() > 0,
641 "v1 started_at must survive the done transition; got {:?}",
642 v1.started_at
643 );
644 assert!(v1.completed_at.is_some());
645 assert!(v1.completed_at.unwrap() >= v1.started_at.unwrap());
646 assert!(r1.started_at.is_some());
647 assert!(r1.completed_at.is_some());
648 }
649
650 #[test]
651 fn flow_edge_serializes_with_camelcase_handle_fields() {
652 let edge = FlowEdge {
657 id: "e1".to_string(),
658 source: "a".to_string(),
659 target: "b".to_string(),
660 source_handle: Some("true".to_string()),
661 target_handle: None,
662 };
663 let json = serde_json::to_string(&edge).unwrap();
664 assert!(
665 json.contains("\"sourceHandle\":\"true\""),
666 "expected sourceHandle in JSON; got {json}"
667 );
668 let parsed: FlowEdge = serde_json::from_str(&json).unwrap();
670 assert_eq!(parsed.source_handle.as_deref(), Some("true"));
671 }
672
673 #[tokio::test]
674 async fn execute_skips_pruned_branch_end_to_end() {
675 let mut vars_node = n("v1", "variables");
686 vars_node.data = json!({
687 "kind": "variables",
688 "entries": [{ "name": "v", "value": 10 }]
689 });
690 let mut cond_node = n("c1", "condition");
691 cond_node.data = json!({
692 "kind": "condition",
693 "expr": "{{var.v}} < 5"
694 });
695 let mut t_resp = n("rt", "response");
696 t_resp.data = json!({
697 "kind": "response",
698 "template": "hit_true"
699 });
700 let mut f_resp = n("rf", "response");
701 f_resp.data = json!({
702 "kind": "response",
703 "template": "hit_false"
704 });
705 let g = DroneGraph {
706 nodes: vec![vars_node, cond_node, t_resp, f_resp],
707 edges: vec![
708 e("e1", "v1", "c1"),
709 eh("e2", "c1", "rt", Some("true")),
710 eh("e3", "c1", "rf", Some("false")),
711 ],
712 };
713
714 let handle = run_drone("wf1".to_string(), g).await.unwrap();
715 let mut rx = handle.events;
717 let mut got_done_ids: Vec<String> = Vec::new();
718 let mut final_output: Option<Value> = None;
719 while let Some(ev) = rx.recv().await {
720 match ev {
721 RunEvent::BlockDone { block_id, .. } => got_done_ids.push(block_id),
722 RunEvent::RunDone { output, .. } => {
723 final_output = Some(output);
724 break;
725 }
726 RunEvent::RunFailed { error, .. } => panic!("run failed: {error}"),
727 _ => {}
728 }
729 }
730
731 assert!(got_done_ids.contains(&"rf".to_string()));
733 assert!(
734 !got_done_ids.contains(&"rt".to_string()),
735 "true branch must NOT run when condition is false; got: {got_done_ids:?}"
736 );
737
738 let states = handle.final_states.lock().await;
740 let rt_state = states.get("rt").expect("rt state recorded");
741 assert_eq!(rt_state.status, "skipped");
742
743 assert_eq!(final_output, Some(json!("hit_false")));
747 }
748}