1use std::collections::{HashMap, HashSet};
52use std::future::Future;
53use std::sync::Arc;
54
55use serde_json::Value;
56use tokio::sync::Notify;
57use tokio::task::JoinSet;
58
59use crate::error::AgentError;
60use crate::state::State;
61use crate::text::TextAgent;
62use crate::{AsyncSourceFn, StatePredicate};
63
64#[derive(Debug, thiserror::Error)]
70pub enum WorkflowError {
71 #[error("Workflow cycle detected: {0}")]
73 CycleDetected(String),
74
75 #[error("Unknown dependency: {0}")]
77 UnknownDependency(String),
78
79 #[error("Duplicate node ID: {0}")]
81 DuplicateNodeId(String),
82
83 #[error("Node '{node_id}' failed: {reason}")]
85 NodeFailed {
86 node_id: String,
88 reason: String,
90 },
91
92 #[error("Approval node '{node_id}' rejected: {reason}")]
94 Rejected {
95 node_id: String,
97 reason: String,
99 },
100
101 #[error("Workflow deadlock: no ready nodes but work remains")]
103 Deadlock,
104
105 #[error("Approval node '{0}' requires run_with(state, controller)")]
108 ApprovalWithoutController(String),
109
110 #[error("Agent error in node '{node_id}': {source}")]
112 AgentError {
113 node_id: String,
115 source: AgentError,
117 },
118}
119
120#[derive(Debug, Clone)]
126pub struct WorkflowRun {
127 pub outputs: HashMap<String, Value>,
129 pub skipped: Vec<String>,
131}
132
133#[derive(Debug, Default)]
140pub struct WorkflowController {
141 decisions: tokio::sync::RwLock<HashMap<String, Result<(), String>>>,
143 waiters: tokio::sync::RwLock<HashMap<String, Arc<Notify>>>,
145}
146
147impl WorkflowController {
148 pub fn new() -> Arc<Self> {
150 Arc::new(Self::default())
151 }
152
153 pub async fn approve(&self, node_id: &str) {
156 self.decide(node_id, Ok(())).await;
157 }
158
159 pub async fn reject(&self, node_id: &str, reason: impl Into<String>) {
162 self.decide(node_id, Err(reason.into())).await;
163 }
164
165 async fn decide(&self, node_id: &str, decision: Result<(), String>) {
166 self.decisions
167 .write()
168 .await
169 .insert(node_id.to_string(), decision);
170 if let Some(notify) = self.waiters.read().await.get(node_id) {
171 notify.notify_one();
172 }
173 }
174
175 async fn wait_for_decision(&self, node_id: &str) -> Result<(), String> {
178 let notify = Arc::new(Notify::new());
179 self.waiters
180 .write()
181 .await
182 .insert(node_id.to_string(), notify.clone());
183 loop {
184 let notified = notify.notified();
187 if let Some(decision) = self.decisions.read().await.get(node_id) {
188 self.waiters.write().await.remove(node_id);
189 return decision.clone();
190 }
191 notified.await;
192 }
193 }
194}
195
196enum NodeKind {
202 Agent(Arc<dyn TextAgent>),
203 Function(AsyncSourceFn),
204 Approval,
205}
206
207struct WorkflowNode {
209 id: String,
210 kind: NodeKind,
211 dependencies: Vec<String>,
212 when: Option<StatePredicate>,
213 join_any: bool,
214}
215
216pub struct WorkflowBuilder {
225 nodes: Vec<WorkflowNode>,
226}
227
228impl WorkflowBuilder {
229 pub fn new() -> Self {
231 Self { nodes: Vec::new() }
232 }
233
234 pub fn agent(mut self, id: &str, agent: Arc<dyn TextAgent>) -> Self {
236 self.nodes.push(WorkflowNode {
237 id: id.to_string(),
238 kind: NodeKind::Agent(agent),
239 dependencies: Vec::new(),
240 when: None,
241 join_any: false,
242 });
243 self
244 }
245
246 pub fn function<F, Fut>(mut self, id: &str, f: F) -> Self
248 where
249 F: Fn(State) -> Fut + Send + Sync + 'static,
250 Fut: Future<Output = Result<Value, String>> + Send + 'static,
251 {
252 let wrapped: AsyncSourceFn = Arc::new(move |state| Box::pin(f(state)));
253
254 self.nodes.push(WorkflowNode {
255 id: id.to_string(),
256 kind: NodeKind::Function(wrapped),
257 dependencies: Vec::new(),
258 when: None,
259 join_any: false,
260 });
261 self
262 }
263
264 pub fn approval(mut self, id: &str) -> Self {
266 self.nodes.push(WorkflowNode {
267 id: id.to_string(),
268 kind: NodeKind::Approval,
269 dependencies: Vec::new(),
270 when: None,
271 join_any: false,
272 });
273 self
274 }
275
276 pub fn after(mut self, deps: &[&str]) -> Self {
278 if let Some(node) = self.nodes.last_mut() {
279 node.dependencies = deps.iter().map(std::string::ToString::to_string).collect();
280 }
281 self
282 }
283
284 pub fn when<G>(mut self, guard: G) -> Self
286 where
287 G: Fn(&State) -> bool + Send + Sync + 'static,
288 {
289 if let Some(node) = self.nodes.last_mut() {
290 node.when = Some(Arc::new(guard));
291 }
292 self
293 }
294
295 pub fn join_any(mut self) -> Self {
298 if let Some(node) = self.nodes.last_mut() {
299 node.join_any = true;
300 }
301 self
302 }
303
304 pub fn build(self) -> Result<Workflow, WorkflowError> {
307 let mut seen_ids = HashSet::new();
309 for node in &self.nodes {
310 if !seen_ids.insert(&node.id) {
311 return Err(WorkflowError::DuplicateNodeId(node.id.clone()));
312 }
313 }
314
315 let id_set: HashSet<&str> = self.nodes.iter().map(|n| n.id.as_str()).collect();
317 for node in &self.nodes {
318 for dep in &node.dependencies {
319 if !id_set.contains(dep.as_str()) {
320 return Err(WorkflowError::UnknownDependency(dep.clone()));
321 }
322 }
323 }
324
325 self.check_acyclic()?;
327
328 Ok(Workflow { nodes: self.nodes })
329 }
330
331 fn check_acyclic(&self) -> Result<(), WorkflowError> {
333 let mut visited = HashSet::new();
334 let mut rec_stack = HashSet::new();
335
336 for node in &self.nodes {
337 if !visited.contains(&node.id) {
338 self.dfs(&node.id, &mut visited, &mut rec_stack)?;
339 }
340 }
341 Ok(())
342 }
343
344 fn dfs(
346 &self,
347 node_id: &str,
348 visited: &mut HashSet<String>,
349 rec_stack: &mut HashSet<String>,
350 ) -> Result<(), WorkflowError> {
351 visited.insert(node_id.to_string());
352 rec_stack.insert(node_id.to_string());
353
354 let node = self.nodes.iter().find(|n| n.id == node_id);
355 if let Some(node) = node {
356 for dep in &node.dependencies {
357 if !visited.contains(dep) {
358 self.dfs(dep, visited, rec_stack)?;
359 } else if rec_stack.contains(dep) {
360 return Err(WorkflowError::CycleDetected(format!("{node_id} -> {dep}")));
361 }
362 }
363 }
364
365 rec_stack.remove(node_id);
366 Ok(())
367 }
368}
369
370impl Default for WorkflowBuilder {
371 fn default() -> Self {
372 Self::new()
373 }
374}
375
376pub struct Workflow {
382 nodes: Vec<WorkflowNode>,
383}
384
385impl Workflow {
386 pub fn builder() -> WorkflowBuilder {
388 WorkflowBuilder::new()
389 }
390
391 pub async fn run(&self, state: &State) -> Result<WorkflowRun, WorkflowError> {
395 if let Some(node) = self
396 .nodes
397 .iter()
398 .find(|n| matches!(n.kind, NodeKind::Approval))
399 {
400 return Err(WorkflowError::ApprovalWithoutController(node.id.clone()));
401 }
402 self.run_with(state, WorkflowController::new()).await
403 }
404
405 pub async fn run_with(
411 &self,
412 state: &State,
413 controller: Arc<WorkflowController>,
414 ) -> Result<WorkflowRun, WorkflowError> {
415 let mut outputs: HashMap<String, Value> = HashMap::new();
416 let mut skipped: Vec<String> = Vec::new();
417 let mut finished: HashSet<String> = HashSet::new();
418 let mut running: HashSet<String> = HashSet::new();
419 let mut join_set: JoinSet<(String, Result<Value, String>)> = JoinSet::new();
420
421 loop {
422 loop {
426 let mut changed = false;
427 for node in &self.nodes {
428 if finished.contains(&node.id)
429 || skipped.iter().any(|s| s == &node.id)
430 || running.contains(&node.id)
431 {
432 continue;
433 }
434 let is_terminal =
435 |d: &String| finished.contains(d) || skipped.iter().any(|s| s == d);
436 let deps_ready = if node.join_any {
437 node.dependencies.is_empty()
440 || node.dependencies.iter().any(|d| finished.contains(d))
441 } else {
442 node.dependencies.iter().all(&is_terminal)
443 };
444 let all_deps_skipped = !node.dependencies.is_empty()
449 && node.dependencies.iter().all(&is_terminal)
450 && !node.dependencies.iter().any(|d| finished.contains(d));
451 if all_deps_skipped {
452 skipped.push(node.id.clone());
453 changed = true;
454 continue;
455 }
456 if !deps_ready {
457 continue;
458 }
459 if node.when.as_ref().is_some_and(|guard| !guard(state)) {
460 skipped.push(node.id.clone());
461 changed = true;
462 continue;
463 }
464
465 running.insert(node.id.clone());
467 changed = true;
468 let node_id = node.id.clone();
469 let state_clone = state.clone();
470 match &node.kind {
471 NodeKind::Agent(agent) => {
472 let agent = agent.clone();
473 join_set.spawn(async move {
474 let value = match agent.run(&state_clone).await {
475 Ok(s) => Ok(Value::String(s)),
476 Err(e) => Err(e.to_string()),
477 };
478 (node_id, value)
479 });
480 }
481 NodeKind::Function(f) => {
482 let f = f.clone();
483 join_set.spawn(async move {
484 let result = f(state_clone).await;
485 (node_id, result)
486 });
487 }
488 NodeKind::Approval => {
489 let controller = controller.clone();
490 join_set.spawn(async move {
491 let value = match controller.wait_for_decision(&node_id).await {
492 Ok(()) => Ok(Value::Bool(true)),
493 Err(reason) => Err(format!("Rejected: {reason}")),
494 };
495 (node_id, value)
496 });
497 }
498 }
499 }
500 if !changed {
501 break;
502 }
503 }
504
505 if running.is_empty() {
506 if finished.len() + skipped.len() < self.nodes.len() {
507 return Err(WorkflowError::Deadlock);
508 }
509 break; }
511
512 match join_set.join_next().await {
515 Some(Ok((node_id, value_result))) => {
516 running.remove(&node_id);
517 match value_result {
518 Ok(value) => {
519 outputs.insert(node_id.clone(), value.clone());
520 let _ = state.set(format!("workflow:{node_id}"), value);
522 finished.insert(node_id);
523 }
524 Err(e) => {
525 if let Some(reason) = e.strip_prefix("Rejected: ") {
527 return Err(WorkflowError::Rejected {
528 node_id,
529 reason: reason.to_string(),
530 });
531 }
532 return Err(WorkflowError::NodeFailed { node_id, reason: e });
533 }
534 }
535 }
536 Some(Err(e)) => {
537 return Err(WorkflowError::NodeFailed {
538 node_id: "unknown".to_string(),
539 reason: e.to_string(),
540 });
541 }
542 None => unreachable!("running non-empty implies join_set non-empty"),
543 }
544 }
545
546 Ok(WorkflowRun { outputs, skipped })
547 }
548}
549
550#[cfg(test)]
555struct EchoAgent;
557
558#[cfg(test)]
559#[async_trait::async_trait]
560impl TextAgent for EchoAgent {
561 fn name(&self) -> &str {
562 "echo"
563 }
564
565 async fn run(&self, state: &State) -> Result<String, AgentError> {
566 let input: String = state.get("input").unwrap_or_else(|| "empty".to_string());
567 Ok(format!("Echo: {input}"))
568 }
569}
570
571#[cfg(test)]
576mod tests {
577 use super::*;
578 use serde_json::json;
579
580 #[tokio::test]
581 async fn test_diamond_graph() {
582 let wf = Workflow::builder()
584 .function("a", |state| async move {
585 state.set("a_val", "value_a").map_err(|e| e.to_string())?;
586 Ok(json!({"a": "done"}))
587 })
588 .function("b", |state| async move {
589 let a_val: Option<String> = state.get("a_val");
590 let b_val = format!("value_b_from_{a_val:?}");
591 state.set("b_val", b_val).map_err(|e| e.to_string())?;
592 Ok(json!({"b": "done"}))
593 })
594 .after(&["a"])
595 .function("c", |state| async move {
596 let a_val: Option<String> = state.get("a_val");
597 let c_val = format!("value_c_from_{a_val:?}");
598 state.set("c_val", c_val).map_err(|e| e.to_string())?;
599 Ok(json!({"c": "done"}))
600 })
601 .after(&["a"])
602 .function("d", |state| async move {
603 let b_val: Option<String> = state.get("b_val");
604 let c_val: Option<String> = state.get("c_val");
605 let d_val = format!("value_d_from_b_{b_val:?}_c_{c_val:?}");
606 state.set("d_val", d_val).map_err(|e| e.to_string())?;
607 Ok(json!({"d": "done"}))
608 })
609 .after(&["b", "c"])
610 .build()
611 .expect("valid workflow");
612
613 let state = State::new();
614 let run = wf.run(&state).await.expect("run succeeds");
615
616 assert_eq!(run.outputs.len(), 4);
617 assert!(run.outputs.contains_key("a"));
618 assert!(run.outputs.contains_key("b"));
619 assert!(run.outputs.contains_key("c"));
620 assert!(run.outputs.contains_key("d"));
621 assert!(run.skipped.is_empty());
622 }
623
624 #[tokio::test]
625 async fn test_when_guard_skips_node_and_cascades() {
626 let wf = Workflow::builder()
627 .function("a", |_state| async move { Ok(json!({"a": "done"})) })
628 .function("b", |_state| async move { Ok(json!({"b": "done"})) })
629 .after(&["a"])
630 .when(|_state| false) .function("c", |_state| async move { Ok(json!({"c": "done"})) })
632 .after(&["b"])
633 .build()
634 .expect("valid workflow");
635
636 let state = State::new();
637 let run = wf.run(&state).await.expect("run succeeds");
638
639 assert_eq!(run.outputs.len(), 1);
642 assert!(run.outputs.contains_key("a"));
643 assert!(!run.outputs.contains_key("b"));
644 assert!(!run.outputs.contains_key("c"));
645 assert_eq!(run.skipped, vec!["b", "c"]);
646 }
647
648 #[tokio::test]
649 async fn test_join_any() {
650 let wf = Workflow::builder()
651 .function("a", |_state| async move { Ok(json!({"a": "done"})) })
652 .function("b", |_state| async move { Ok(json!({"b": "done"})) })
653 .when(|_state| false) .function("c", |_state| async move { Ok(json!({"c": "done"})) })
655 .after(&["a", "b"])
656 .join_any()
657 .build()
658 .expect("valid workflow");
659
660 let state = State::new();
661 let run = wf.run(&state).await.expect("run succeeds");
662
663 assert!(run.outputs.contains_key("a"));
664 assert!(!run.outputs.contains_key("b")); assert!(run.outputs.contains_key("c")); assert_eq!(run.skipped, vec!["b"]);
667 }
668
669 #[tokio::test]
670 async fn test_unknown_dep_rejected_at_build() {
671 let result = Workflow::builder()
672 .function("a", |_state| async move { Ok(json!({})) })
673 .function("b", |_state| async move { Ok(json!({})) })
674 .after(&["unknown_node"])
675 .build();
676
677 assert!(matches!(result, Err(WorkflowError::UnknownDependency(_))));
678 }
679
680 #[tokio::test]
681 async fn test_duplicate_id_rejected_at_build() {
682 let result = Workflow::builder()
683 .function("a", |_state| async move { Ok(json!({})) })
684 .function("a", |_state| async move { Ok(json!({})) }) .build();
686
687 assert!(matches!(result, Err(WorkflowError::DuplicateNodeId(_))));
688 }
689
690 #[tokio::test]
691 async fn test_approval_approve() {
692 let wf = Workflow::builder()
693 .function("a", |_state| async move { Ok(json!({"a": "done"})) })
694 .approval("review")
695 .after(&["a"])
696 .build()
697 .expect("valid workflow");
698
699 let state = State::new();
700 let controller = WorkflowController::new();
701 let controller_clone = controller.clone();
702
703 let run_task = tokio::spawn(async move { wf.run_with(&state, controller_clone).await });
705
706 tokio::time::sleep(tokio::time::Duration::from_millis(100)).await;
708
709 controller.approve("review").await;
711
712 let run = run_task.await.expect("task panicked");
714 assert!(run.is_ok());
715 let run = run.unwrap();
716 assert!(run.outputs.contains_key("a"));
717 assert!(run.outputs.contains_key("review"));
718 }
719
720 #[tokio::test]
721 async fn test_approval_reject() {
722 let wf = Workflow::builder()
723 .function("a", |_state| async move { Ok(json!({"a": "done"})) })
724 .approval("review")
725 .after(&["a"])
726 .build()
727 .expect("valid workflow");
728
729 let state = State::new();
730 let controller = WorkflowController::new();
731 let controller_clone = controller.clone();
732
733 let run_task = tokio::spawn(async move { wf.run_with(&state, controller_clone).await });
734
735 tokio::time::sleep(tokio::time::Duration::from_millis(100)).await;
736 controller.reject("review", "not approved").await;
737
738 let run = run_task.await.expect("task panicked");
739 assert!(run.is_err());
740 match run.unwrap_err() {
741 WorkflowError::Rejected { node_id, .. } => {
742 assert_eq!(node_id, "review");
743 }
744 e => panic!("Expected Rejected, got {e:?}"),
745 }
746 }
747
748 #[tokio::test]
749 async fn test_run_rejects_approval_without_controller() {
750 let wf = Workflow::builder()
751 .approval("gate")
752 .build()
753 .expect("valid workflow");
754
755 let state = State::new();
756 match wf.run(&state).await.unwrap_err() {
758 WorkflowError::ApprovalWithoutController(id) => assert_eq!(id, "gate"),
759 e => panic!("Expected ApprovalWithoutController, got {e:?}"),
760 }
761 }
762
763 #[tokio::test]
764 async fn test_early_approval_is_not_lost() {
765 let wf = Workflow::builder()
766 .function("a", |_state| async move { Ok(json!({"a": "done"})) })
767 .approval("review")
768 .after(&["a"])
769 .build()
770 .expect("valid workflow");
771
772 let state = State::new();
773 let controller = WorkflowController::new();
774 controller.approve("review").await;
777
778 let run = tokio::time::timeout(
779 tokio::time::Duration::from_secs(5),
780 wf.run_with(&state, controller),
781 )
782 .await
783 .expect("run must not hang on an early approval")
784 .expect("run succeeds");
785 assert!(run.outputs.contains_key("review"));
786 }
787
788 #[tokio::test]
789 async fn test_early_rejection_is_not_lost() {
790 let wf = Workflow::builder()
791 .approval("review")
792 .build()
793 .expect("valid workflow");
794
795 let state = State::new();
796 let controller = WorkflowController::new();
797 controller.reject("review", "denied up front").await;
798
799 let result = tokio::time::timeout(
800 tokio::time::Duration::from_secs(5),
801 wf.run_with(&state, controller),
802 )
803 .await
804 .expect("run must not hang on an early rejection");
805 assert!(matches!(result, Err(WorkflowError::Rejected { .. })));
806 }
807
808 #[tokio::test]
809 async fn test_join_any_starts_before_slow_sibling_completes() {
810 let wf = Workflow::builder()
815 .function("a", |_state| async move { Ok(json!({"a": "done"})) })
816 .function("b", |state| async move {
817 for _ in 0..500 {
818 if state.get::<bool>("d_ran").unwrap_or(false) {
819 return Ok(json!({"b": "done"}));
820 }
821 tokio::time::sleep(tokio::time::Duration::from_millis(10)).await;
822 }
823 Err("d never ran while b was in flight".to_string())
824 })
825 .function("d", |state| async move {
826 state.set("d_ran", true).map_err(|e| e.to_string())?;
827 Ok(json!({"d": "done"}))
828 })
829 .after(&["a", "b"])
830 .join_any()
831 .build()
832 .expect("valid workflow");
833
834 let state = State::new();
835 let run = tokio::time::timeout(tokio::time::Duration::from_secs(10), wf.run(&state))
836 .await
837 .expect("join_any dependent must start before the slow sibling")
838 .expect("run succeeds");
839 assert!(run.outputs.contains_key("d"));
840 assert!(run.outputs.contains_key("b"));
841 }
842
843 #[tokio::test]
844 async fn test_agent_node() {
845 let echo = Arc::new(EchoAgent);
846
847 let wf = Workflow::builder()
848 .agent("echo_node", echo)
849 .build()
850 .expect("valid workflow");
851
852 let state = State::new();
853 state.set("input", "hello").expect("state set succeeds");
854
855 let run = wf.run(&state).await.expect("run succeeds");
856 assert!(run.outputs.contains_key("echo_node"));
857 let output = &run.outputs["echo_node"];
858 assert!(output.is_string());
859 let s = output.as_str().unwrap();
860 assert!(s.contains("Echo") && s.contains("hello"));
861 }
862}