Skip to main content

agentic_time/
sequence.rs

1//! Sequence constraints — ordered temporal events with dependencies.
2
3use chrono::{DateTime, Duration as ChronoDuration, Utc};
4use serde::{Deserialize, Serialize};
5
6use crate::TemporalId;
7
8/// A sequence of ordered temporal events.
9#[derive(Debug, Clone, Serialize, Deserialize)]
10pub struct Sequence {
11    /// Unique identifier.
12    pub id: TemporalId,
13
14    /// Sequence name.
15    pub label: String,
16
17    /// Ordered steps.
18    pub steps: Vec<SequenceStep>,
19
20    /// Current step index.
21    pub current_step: usize,
22
23    /// Overall status.
24    pub status: SequenceStatus,
25
26    /// When sequence started.
27    pub started_at: Option<DateTime<Utc>>,
28
29    /// When sequence completed.
30    pub completed_at: Option<DateTime<Utc>>,
31
32    /// Allow parallel execution?
33    pub allow_parallel: bool,
34
35    /// Created at.
36    pub created_at: DateTime<Utc>,
37
38    /// Tags.
39    pub tags: Vec<String>,
40}
41
42/// A single step in a sequence.
43#[derive(Debug, Clone, Serialize, Deserialize)]
44pub struct SequenceStep {
45    /// Step identifier.
46    pub id: TemporalId,
47
48    /// Step label.
49    pub label: String,
50
51    /// Order in sequence (0-indexed).
52    pub order: u32,
53
54    /// Expected duration in seconds.
55    pub duration_secs: Option<i64>,
56
57    /// Step status.
58    pub status: StepStatus,
59
60    /// Can be done in parallel with next step?
61    pub parallel_with_next: bool,
62
63    /// Dependencies within sequence (order indices).
64    pub depends_on: Vec<u32>,
65
66    /// When step started.
67    pub started_at: Option<DateTime<Utc>>,
68
69    /// When step completed.
70    pub completed_at: Option<DateTime<Utc>>,
71}
72
73/// Overall sequence status.
74#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash, Serialize, Deserialize)]
75pub enum SequenceStatus {
76    /// Not yet started.
77    NotStarted,
78    /// Currently in progress.
79    InProgress,
80    /// All steps completed.
81    Completed,
82    /// A step failed.
83    Failed,
84    /// Cancelled.
85    Cancelled,
86}
87
88/// Individual step status.
89#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize)]
90pub enum StepStatus {
91    /// Waiting for dependencies.
92    Pending,
93    /// Dependencies met, can start.
94    Ready,
95    /// Currently running.
96    InProgress,
97    /// Done.
98    Completed,
99    /// Skipped.
100    Skipped,
101    /// Failed.
102    Failed,
103}
104
105impl Sequence {
106    /// Create a new sequence.
107    pub fn new(label: impl Into<String>, steps: Vec<SequenceStep>) -> Self {
108        Self {
109            id: TemporalId::new(),
110            label: label.into(),
111            steps,
112            current_step: 0,
113            status: SequenceStatus::NotStarted,
114            started_at: None,
115            completed_at: None,
116            allow_parallel: false,
117            created_at: Utc::now(),
118            tags: Vec::new(),
119        }
120    }
121
122    /// Get steps that are ready to execute.
123    pub fn ready_steps(&self) -> Vec<&SequenceStep> {
124        self.steps
125            .iter()
126            .filter(|s| {
127                s.status == StepStatus::Ready
128                    || (s.status == StepStatus::Pending && self.dependencies_met(s))
129            })
130            .collect()
131    }
132
133    /// Check if all dependencies for a step are met.
134    pub fn dependencies_met(&self, step: &SequenceStep) -> bool {
135        step.depends_on.iter().all(|dep_order| {
136            self.steps
137                .iter()
138                .find(|s| s.order == *dep_order)
139                .map(|s| s.status == StepStatus::Completed)
140                .unwrap_or(true)
141        })
142    }
143
144    /// Calculate total expected duration.
145    pub fn total_duration(&self) -> ChronoDuration {
146        if self.allow_parallel {
147            self.steps
148                .iter()
149                .filter_map(|s| s.duration_secs)
150                .map(ChronoDuration::seconds)
151                .max()
152                .unwrap_or(ChronoDuration::zero())
153        } else {
154            self.steps
155                .iter()
156                .filter_map(|s| s.duration_secs)
157                .map(ChronoDuration::seconds)
158                .fold(ChronoDuration::zero(), |acc, d| acc + d)
159        }
160    }
161
162    /// Complete the current step and advance.
163    pub fn complete_current_step(&mut self) {
164        if self.status == SequenceStatus::NotStarted {
165            self.status = SequenceStatus::InProgress;
166            self.started_at = Some(Utc::now());
167        }
168
169        if let Some(step) = self.steps.get_mut(self.current_step) {
170            step.status = StepStatus::Completed;
171            step.completed_at = Some(Utc::now());
172        }
173
174        self.current_step += 1;
175
176        if self.current_step >= self.steps.len() {
177            self.status = SequenceStatus::Completed;
178            self.completed_at = Some(Utc::now());
179        }
180
181        self.update_ready_steps();
182    }
183
184    /// Update which steps are ready based on dependency state.
185    pub fn update_ready_steps(&mut self) {
186        let completed_orders: Vec<u32> = self
187            .steps
188            .iter()
189            .filter(|s| s.status == StepStatus::Completed)
190            .map(|s| s.order)
191            .collect();
192
193        for step in &mut self.steps {
194            if step.status == StepStatus::Pending {
195                let deps_met = step
196                    .depends_on
197                    .iter()
198                    .all(|dep| completed_orders.contains(dep));
199                if deps_met {
200                    step.status = StepStatus::Ready;
201                }
202            }
203        }
204    }
205}
206
207impl SequenceStep {
208    /// Create a new sequence step.
209    pub fn new(label: impl Into<String>, order: u32) -> Self {
210        Self {
211            id: TemporalId::new(),
212            label: label.into(),
213            order,
214            duration_secs: None,
215            status: StepStatus::Pending,
216            parallel_with_next: false,
217            depends_on: Vec::new(),
218            started_at: None,
219            completed_at: None,
220        }
221    }
222}
223
224#[cfg(test)]
225mod tests {
226    use super::*;
227
228    #[test]
229    fn test_sequence_advance() {
230        let steps = vec![
231            SequenceStep::new("Step 1", 0),
232            SequenceStep::new("Step 2", 1),
233        ];
234        let mut seq = Sequence::new("Deploy", steps);
235        assert_eq!(seq.status, SequenceStatus::NotStarted);
236
237        seq.complete_current_step();
238        assert_eq!(seq.status, SequenceStatus::InProgress);
239        assert_eq!(seq.steps[0].status, StepStatus::Completed);
240
241        seq.complete_current_step();
242        assert_eq!(seq.status, SequenceStatus::Completed);
243    }
244
245    #[test]
246    fn test_total_duration_sequential() {
247        let mut steps = vec![
248            SequenceStep::new("Step 1", 0),
249            SequenceStep::new("Step 2", 1),
250        ];
251        steps[0].duration_secs = Some(60);
252        steps[1].duration_secs = Some(120);
253
254        let seq = Sequence::new("Pipeline", steps);
255        assert_eq!(seq.total_duration().num_seconds(), 180);
256    }
257}