Skip to main content

agentic_time/
write_engine.rs

1//! Write engine — create and update temporal entities.
2
3use chrono::{DateTime, Utc};
4
5use crate::deadline::{Deadline, DeadlineStatus};
6use crate::decay::DecayModel;
7use crate::duration::DurationEstimate;
8use crate::error::{TimeError, TimeResult};
9use crate::file_format::{EntityType, TimeFile};
10use crate::schedule::{Schedule, ScheduleStatus};
11use crate::sequence::Sequence;
12use crate::TemporalId;
13
14/// Write engine for temporal operations.
15pub struct WriteEngine {
16    file: TimeFile,
17}
18
19impl WriteEngine {
20    /// Create a new write engine wrapping a TimeFile.
21    pub fn new(file: TimeFile) -> Self {
22        Self { file }
23    }
24
25    /// Consume the engine, returning the underlying TimeFile.
26    pub fn into_file(self) -> TimeFile {
27        self.file
28    }
29
30    /// Get a reference to the underlying TimeFile.
31    pub fn file(&self) -> &TimeFile {
32        &self.file
33    }
34
35    /// Get a mutable reference to the underlying TimeFile.
36    pub fn file_mut(&mut self) -> &mut TimeFile {
37        &mut self.file
38    }
39
40    // --- Durations ---
41
42    /// Add a duration estimate.
43    pub fn add_duration(&mut self, duration: DurationEstimate) -> TimeResult<TemporalId> {
44        let id = duration.id;
45        self.file.add(EntityType::Duration, id, &duration)?;
46        self.file.save()?;
47        Ok(id)
48    }
49
50    /// Update an existing duration estimate.
51    pub fn update_duration(&mut self, duration: DurationEstimate) -> TimeResult<()> {
52        self.file
53            .add(EntityType::Duration, duration.id, &duration)?;
54        self.file.save()?;
55        Ok(())
56    }
57
58    // --- Deadlines ---
59
60    /// Add a deadline.
61    pub fn add_deadline(&mut self, deadline: Deadline) -> TimeResult<TemporalId> {
62        let id = deadline.id;
63        self.file.add(EntityType::Deadline, id, &deadline)?;
64        self.file.save()?;
65        Ok(id)
66    }
67
68    /// Update an existing deadline.
69    pub fn update_deadline(&mut self, deadline: Deadline) -> TimeResult<()> {
70        self.file
71            .add(EntityType::Deadline, deadline.id, &deadline)?;
72        self.file.save()?;
73        Ok(())
74    }
75
76    /// Complete a deadline.
77    pub fn complete_deadline(&mut self, id: &TemporalId) -> TimeResult<()> {
78        let mut deadline: Deadline = self
79            .file
80            .get(id)?
81            .ok_or_else(|| TimeError::NotFound(id.0.to_string()))?;
82        deadline.complete();
83        self.update_deadline(deadline)
84    }
85
86    /// Cancel a deadline.
87    pub fn cancel_deadline(&mut self, id: &TemporalId) -> TimeResult<()> {
88        let mut deadline: Deadline = self
89            .file
90            .get(id)?
91            .ok_or_else(|| TimeError::NotFound(id.0.to_string()))?;
92        deadline.status = DeadlineStatus::Cancelled;
93        self.update_deadline(deadline)
94    }
95
96    // --- Schedules ---
97
98    /// Add a schedule. Returns error if it conflicts with existing non-flexible schedules.
99    pub fn add_schedule(&mut self, schedule: Schedule) -> TimeResult<TemporalId> {
100        let conflicts = self.check_schedule_conflicts(&schedule)?;
101        if !conflicts.is_empty() && !schedule.flexible {
102            return Err(TimeError::ScheduleConflict(
103                schedule.label.clone(),
104                conflicts[0].label.clone(),
105            ));
106        }
107
108        let id = schedule.id;
109        self.file.add(EntityType::Schedule, id, &schedule)?;
110        self.file.save()?;
111        Ok(id)
112    }
113
114    /// Update an existing schedule.
115    pub fn update_schedule(&mut self, schedule: Schedule) -> TimeResult<()> {
116        self.file
117            .add(EntityType::Schedule, schedule.id, &schedule)?;
118        self.file.save()?;
119        Ok(())
120    }
121
122    /// Reschedule to a new start time.
123    pub fn reschedule(&mut self, id: &TemporalId, new_start: DateTime<Utc>) -> TimeResult<()> {
124        let mut schedule: Schedule = self
125            .file
126            .get(id)?
127            .ok_or_else(|| TimeError::NotFound(id.0.to_string()))?;
128        schedule.start_at = new_start;
129        schedule.status = ScheduleStatus::Rescheduled;
130        self.update_schedule(schedule)
131    }
132
133    /// Check for conflicting schedules.
134    fn check_schedule_conflicts(&self, schedule: &Schedule) -> TimeResult<Vec<Schedule>> {
135        let mut conflicts = Vec::new();
136
137        for block in self.file.list_by_type(EntityType::Schedule) {
138            let existing: Schedule = block.deserialize()?;
139            if existing.id != schedule.id
140                && existing.status == ScheduleStatus::Scheduled
141                && schedule.conflicts_with(&existing)
142            {
143                conflicts.push(existing);
144            }
145        }
146
147        Ok(conflicts)
148    }
149
150    // --- Sequences ---
151
152    /// Add a sequence.
153    pub fn add_sequence(&mut self, sequence: Sequence) -> TimeResult<TemporalId> {
154        let id = sequence.id;
155        self.file.add(EntityType::Sequence, id, &sequence)?;
156        self.file.save()?;
157        Ok(id)
158    }
159
160    /// Update an existing sequence.
161    pub fn update_sequence(&mut self, sequence: Sequence) -> TimeResult<()> {
162        self.file
163            .add(EntityType::Sequence, sequence.id, &sequence)?;
164        self.file.save()?;
165        Ok(())
166    }
167
168    /// Advance a sequence to the next step.
169    pub fn advance_sequence(&mut self, id: &TemporalId) -> TimeResult<()> {
170        let mut sequence: Sequence = self
171            .file
172            .get(id)?
173            .ok_or_else(|| TimeError::NotFound(id.0.to_string()))?;
174        sequence.complete_current_step();
175        self.update_sequence(sequence)
176    }
177
178    // --- Decay ---
179
180    /// Add a decay model.
181    pub fn add_decay(&mut self, decay: DecayModel) -> TimeResult<TemporalId> {
182        let id = decay.id;
183        self.file.add(EntityType::Decay, id, &decay)?;
184        self.file.save()?;
185        Ok(id)
186    }
187
188    /// Update an existing decay model.
189    pub fn update_decay(&mut self, decay: DecayModel) -> TimeResult<()> {
190        self.file.add(EntityType::Decay, decay.id, &decay)?;
191        self.file.save()?;
192        Ok(())
193    }
194
195    /// Refresh a decay model (recalculate current value).
196    pub fn refresh_decay(&mut self, id: &TemporalId) -> TimeResult<f64> {
197        let mut decay: DecayModel = self
198            .file
199            .get(id)?
200            .ok_or_else(|| TimeError::NotFound(id.0.to_string()))?;
201        decay.update();
202        let value = decay.current_value;
203        self.update_decay(decay)?;
204        Ok(value)
205    }
206
207    // --- Batch operations ---
208
209    /// Update all statuses (deadlines + decays) based on current time.
210    pub fn update_all_statuses(&mut self) -> TimeResult<UpdateReport> {
211        let mut report = UpdateReport::default();
212
213        // Collect deadline blocks to avoid borrow issues
214        let deadline_blocks: Vec<_> = self
215            .file
216            .list_by_type(EntityType::Deadline)
217            .into_iter()
218            .cloned()
219            .collect();
220
221        for block in deadline_blocks {
222            let mut deadline: Deadline = block.deserialize()?;
223            let old_status = deadline.status;
224            deadline.update_status();
225            if deadline.status != old_status {
226                self.file
227                    .add(EntityType::Deadline, deadline.id, &deadline)?;
228                report.deadlines_updated += 1;
229                if deadline.status == DeadlineStatus::Overdue {
230                    report.new_overdue.push(deadline.id);
231                }
232            }
233        }
234
235        // Collect decay blocks
236        let decay_blocks: Vec<_> = self
237            .file
238            .list_by_type(EntityType::Decay)
239            .into_iter()
240            .cloned()
241            .collect();
242
243        for block in decay_blocks {
244            let mut decay: DecayModel = block.deserialize()?;
245            decay.update();
246            self.file.add(EntityType::Decay, decay.id, &decay)?;
247            report.decays_updated += 1;
248        }
249
250        self.file.save()?;
251        Ok(report)
252    }
253}
254
255/// Report from `update_all_statuses`.
256#[derive(Debug, Default)]
257pub struct UpdateReport {
258    /// Number of deadlines whose status changed.
259    pub deadlines_updated: usize,
260    /// Number of decay models recalculated.
261    pub decays_updated: usize,
262    /// IDs of newly overdue deadlines.
263    pub new_overdue: Vec<TemporalId>,
264}
265
266#[cfg(test)]
267mod tests {
268    use super::*;
269    use crate::deadline::Deadline;
270    use chrono::Duration as ChronoDuration;
271    use tempfile::tempdir;
272
273    #[test]
274    fn test_add_and_complete_deadline() {
275        let dir = tempdir().unwrap();
276        let path = dir.path().join("test.atime");
277        let tf = TimeFile::create(&path).unwrap();
278        let mut engine = WriteEngine::new(tf);
279
280        let d = Deadline::new("Test deadline", Utc::now() + ChronoDuration::hours(24));
281        let id = engine.add_deadline(d).unwrap();
282
283        engine.complete_deadline(&id).unwrap();
284
285        let loaded: Deadline = engine.file().get(&id).unwrap().unwrap();
286        assert_eq!(loaded.status, DeadlineStatus::Completed);
287    }
288}