Skip to main content

agentic_time/
schedule.rs

1//! Scheduling with recurrence and conflict detection.
2
3use chrono::{DateTime, Duration as ChronoDuration, Utc};
4use serde::{Deserialize, Serialize};
5
6use crate::TemporalId;
7
8/// A scheduled event or task.
9#[derive(Debug, Clone, Serialize, Deserialize)]
10pub struct Schedule {
11    /// Unique identifier.
12    pub id: TemporalId,
13
14    /// What is scheduled.
15    pub label: String,
16
17    /// When it starts.
18    pub start_at: DateTime<Utc>,
19
20    /// Expected duration in seconds.
21    pub duration_secs: i64,
22
23    /// Recurrence pattern (if any).
24    pub recurrence: Option<Recurrence>,
25
26    /// Priority level.
27    pub priority: Priority,
28
29    /// Can this be moved?
30    pub flexible: bool,
31
32    /// Buffer time before (seconds).
33    pub buffer_before_secs: Option<i64>,
34
35    /// Buffer time after (seconds).
36    pub buffer_after_secs: Option<i64>,
37
38    /// Dependencies (must complete before this).
39    pub dependencies: Vec<TemporalId>,
40
41    /// Blocked by (cannot start until these complete).
42    pub blocked_by: Vec<TemporalId>,
43
44    /// Status.
45    pub status: ScheduleStatus,
46
47    /// When created.
48    pub created_at: DateTime<Utc>,
49
50    /// Tags.
51    pub tags: Vec<String>,
52}
53
54/// Recurrence configuration.
55#[derive(Debug, Clone, Serialize, Deserialize)]
56pub struct Recurrence {
57    /// Pattern type.
58    pub pattern: RecurrencePattern,
59
60    /// End condition.
61    pub ends: RecurrenceEnd,
62
63    /// Exceptions (dates to skip).
64    pub exceptions: Vec<DateTime<Utc>>,
65}
66
67/// Recurrence pattern.
68#[derive(Debug, Clone, Serialize, Deserialize)]
69pub enum RecurrencePattern {
70    /// Every N days.
71    Daily {
72        /// Interval in days.
73        interval: u32,
74    },
75
76    /// Every N weeks on specific days.
77    Weekly {
78        /// Interval in weeks.
79        interval: u32,
80        /// Days of the week (0=Mon, 6=Sun).
81        days: Vec<u32>,
82    },
83
84    /// Every N months on specific day.
85    Monthly {
86        /// Interval in months.
87        interval: u32,
88        /// Day of month (1-31).
89        day_of_month: u32,
90    },
91
92    /// Custom cron expression.
93    Cron {
94        /// Cron expression string.
95        expression: String,
96    },
97}
98
99/// When recurrence ends.
100#[derive(Debug, Clone, Serialize, Deserialize)]
101pub enum RecurrenceEnd {
102    /// Never ends.
103    Never,
104    /// Ends after N occurrences.
105    After {
106        /// Number of occurrences.
107        count: u32,
108    },
109    /// Ends on specific date.
110    On {
111        /// End date.
112        date: DateTime<Utc>,
113    },
114}
115
116/// Priority level.
117#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize)]
118pub enum Priority {
119    /// Critical priority.
120    Critical,
121    /// High priority.
122    High,
123    /// Medium priority.
124    Medium,
125    /// Low priority.
126    Low,
127    /// Optional.
128    Optional,
129}
130
131/// Schedule status.
132#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize)]
133pub enum ScheduleStatus {
134    /// Scheduled but not started.
135    Scheduled,
136    /// Currently in progress.
137    InProgress,
138    /// Completed.
139    Completed,
140    /// Cancelled.
141    Cancelled,
142    /// Rescheduled to a new time.
143    Rescheduled,
144}
145
146impl Schedule {
147    /// Create a new schedule.
148    pub fn new(label: impl Into<String>, start_at: DateTime<Utc>, duration_secs: i64) -> Self {
149        Self {
150            id: TemporalId::new(),
151            label: label.into(),
152            start_at,
153            duration_secs,
154            recurrence: None,
155            priority: Priority::Medium,
156            flexible: true,
157            buffer_before_secs: None,
158            buffer_after_secs: None,
159            dependencies: Vec::new(),
160            blocked_by: Vec::new(),
161            status: ScheduleStatus::Scheduled,
162            created_at: Utc::now(),
163            tags: Vec::new(),
164        }
165    }
166
167    /// Calculate end time.
168    pub fn end_at(&self) -> DateTime<Utc> {
169        self.start_at + ChronoDuration::seconds(self.duration_secs)
170    }
171
172    /// Check if conflicts with another schedule.
173    pub fn conflicts_with(&self, other: &Schedule) -> bool {
174        let self_end = self.end_at();
175        let other_end = other.end_at();
176        self.start_at < other_end && self_end > other.start_at
177    }
178
179    /// Get next occurrence (for recurring schedules).
180    pub fn next_occurrence(&self, after: DateTime<Utc>) -> Option<DateTime<Utc>> {
181        self.recurrence.as_ref().map(|r| match &r.pattern {
182            RecurrencePattern::Daily { interval } => {
183                let days_since = (after - self.start_at).num_days();
184                let next_interval = ((days_since / *interval as i64) + 1) * *interval as i64;
185                self.start_at + ChronoDuration::days(next_interval)
186            }
187            RecurrencePattern::Weekly { interval, .. } => {
188                let weeks_since = (after - self.start_at).num_weeks();
189                let next_interval = ((weeks_since / *interval as i64) + 1) * *interval as i64;
190                self.start_at + ChronoDuration::weeks(next_interval)
191            }
192            RecurrencePattern::Monthly { interval, .. } => {
193                // Simplified: add N months worth of days
194                let months_since = (after - self.start_at).num_days() / 30;
195                let next_interval = ((months_since / *interval as i64) + 1) * *interval as i64;
196                self.start_at + ChronoDuration::days(next_interval * 30)
197            }
198            RecurrencePattern::Cron { .. } => {
199                // Cron parsing would require a cron crate — return next day as fallback
200                after + ChronoDuration::days(1)
201            }
202        })
203    }
204}
205
206#[cfg(test)]
207mod tests {
208    use super::*;
209
210    #[test]
211    fn test_end_at() {
212        let start = Utc::now();
213        let s = Schedule::new("Standup", start, 1800); // 30 min
214        assert_eq!(s.end_at(), start + ChronoDuration::seconds(1800));
215    }
216
217    #[test]
218    fn test_conflicts() {
219        let now = Utc::now();
220        let a = Schedule::new("A", now, 3600);
221        let b = Schedule::new("B", now + ChronoDuration::seconds(1800), 3600);
222        assert!(a.conflicts_with(&b));
223    }
224
225    #[test]
226    fn test_no_conflict() {
227        let now = Utc::now();
228        let a = Schedule::new("A", now, 3600);
229        let b = Schedule::new("B", now + ChronoDuration::seconds(7200), 3600);
230        assert!(!a.conflicts_with(&b));
231    }
232}