Skip to main content

scud/attractor/
outcome.rs

1//! Stage execution outcomes.
2
3use serde::{Deserialize, Serialize};
4use std::collections::HashMap;
5
6/// Status of a stage execution.
7#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
8#[serde(rename_all = "snake_case")]
9pub enum StageStatus {
10    /// Stage completed successfully.
11    Success,
12    /// Stage failed.
13    Failure,
14    /// Stage was skipped.
15    Skipped,
16    /// Stage is waiting for human input.
17    WaitingForHuman,
18    /// Stage timed out.
19    Timeout,
20    /// Stage was cancelled.
21    Cancelled,
22}
23
24impl StageStatus {
25    pub fn is_success(&self) -> bool {
26        matches!(self, StageStatus::Success)
27    }
28
29    pub fn as_str(&self) -> &str {
30        match self {
31            StageStatus::Success => "success",
32            StageStatus::Failure => "failure",
33            StageStatus::Skipped => "skipped",
34            StageStatus::WaitingForHuman => "waiting_for_human",
35            StageStatus::Timeout => "timeout",
36            StageStatus::Cancelled => "cancelled",
37        }
38    }
39}
40
41/// Outcome of executing a pipeline node handler.
42#[derive(Debug, Clone, Serialize, Deserialize)]
43pub struct Outcome {
44    /// The execution status.
45    pub status: StageStatus,
46    /// The preferred label for edge selection (e.g., "success", "fail", "approve").
47    pub preferred_label: Option<String>,
48    /// Suggested next node IDs (used in edge selection step 3).
49    pub suggested_next: Vec<String>,
50    /// Context updates to apply after this node completes.
51    pub context_updates: HashMap<String, serde_json::Value>,
52    /// Response text from the handler (e.g., LLM output).
53    pub response_text: Option<String>,
54    /// Human-readable summary of what happened.
55    pub summary: Option<String>,
56}
57
58impl Outcome {
59    /// Create a simple success outcome.
60    pub fn success() -> Self {
61        Self {
62            status: StageStatus::Success,
63            preferred_label: None,
64            suggested_next: vec![],
65            context_updates: HashMap::new(),
66            response_text: None,
67            summary: None,
68        }
69    }
70
71    /// Create a failure outcome with a message.
72    pub fn failure(message: impl Into<String>) -> Self {
73        Self {
74            status: StageStatus::Failure,
75            preferred_label: None,
76            suggested_next: vec![],
77            context_updates: HashMap::new(),
78            response_text: None,
79            summary: Some(message.into()),
80        }
81    }
82
83    /// Create a success outcome with a preferred label for edge routing.
84    pub fn success_with_label(label: impl Into<String>) -> Self {
85        Self {
86            status: StageStatus::Success,
87            preferred_label: Some(label.into()),
88            suggested_next: vec![],
89            context_updates: HashMap::new(),
90            response_text: None,
91            summary: None,
92        }
93    }
94
95    /// Set context updates on this outcome.
96    pub fn with_context(mut self, updates: HashMap<String, serde_json::Value>) -> Self {
97        self.context_updates = updates;
98        self
99    }
100
101    /// Set response text on this outcome.
102    pub fn with_response(mut self, text: impl Into<String>) -> Self {
103        self.response_text = Some(text.into());
104        self
105    }
106}
107
108#[cfg(test)]
109mod tests {
110    use super::*;
111
112    #[test]
113    fn test_success_outcome() {
114        let o = Outcome::success();
115        assert!(o.status.is_success());
116        assert!(o.preferred_label.is_none());
117        assert!(o.context_updates.is_empty());
118    }
119
120    #[test]
121    fn test_failure_outcome() {
122        let o = Outcome::failure("bad things");
123        assert!(!o.status.is_success());
124        assert_eq!(o.summary.as_deref(), Some("bad things"));
125    }
126
127    #[test]
128    fn test_success_with_label() {
129        let o = Outcome::success_with_label("approve");
130        assert!(o.status.is_success());
131        assert_eq!(o.preferred_label.as_deref(), Some("approve"));
132    }
133
134    #[test]
135    fn test_with_context() {
136        let mut ctx = HashMap::new();
137        ctx.insert("key".into(), serde_json::json!("value"));
138        let o = Outcome::success().with_context(ctx);
139        assert_eq!(o.context_updates.len(), 1);
140    }
141}