Skip to main content

a3s_code_core/task/
idle.rs

1//! Idle Task - Explicit Memory Consolidation
2//!
3//! Provides explicit representation of idle-time memory consolidation tasks.
4//!
5//! ## Idle Phases
6//!
7//! 1. `Starting` - Idle task initialized
8//! 2. `Consolidating` - Memory consolidation in progress
9//! 3. `Updating` - Updating semantic/ episodic memory
10//! 4. `Completed` - Idle finished, changes committed
11//!
12//! ## Example
13//!
14//! ```rust,ignore
15//! use a3s_code_core::task::idle::{IdleTask, IdlePhase, IdleTurn};
16//!
17//! let mut idle = IdleTask::new("no_activity".to_string());
18//! idle.add_turn(IdleTurn::default());
19//! idle.transition(IdlePhase::Updating);
20//! let update = idle.complete();
21//! ```
22
23use serde::{Deserialize, Serialize};
24use std::path::PathBuf;
25
26/// Idle task phase
27#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize)]
28#[serde(rename_all = "snake_case")]
29#[derive(Default)]
30pub enum IdlePhase {
31    /// Idle task just started
32    #[default]
33    Starting,
34    /// Active memory consolidation
35    Consolidating,
36    /// Updating memory stores
37    Updating,
38    /// Idle completed
39    Completed,
40}
41
42impl IdlePhase {
43    /// Check if phase is terminal
44    pub fn is_terminal(&self) -> bool {
45        matches!(self, IdlePhase::Completed)
46    }
47}
48
49impl std::fmt::Display for IdlePhase {
50    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
51        match self {
52            IdlePhase::Starting => write!(f, "starting"),
53            IdlePhase::Consolidating => write!(f, "consolidating"),
54            IdlePhase::Updating => write!(f, "updating"),
55            IdlePhase::Completed => write!(f, "completed"),
56        }
57    }
58}
59
60/// A single turn in the idle execution
61#[derive(Debug, Clone, Default, Serialize, Deserialize)]
62pub struct IdleTurn {
63    /// Turn text (assistant message)
64    pub text: String,
65    /// Tool calls made during this turn
66    pub tool_calls: Vec<IdleToolCall>,
67    /// Files touched during this turn
68    pub touched_files: Vec<PathBuf>,
69    /// Token usage for this turn
70    pub input_tokens: u64,
71    pub output_tokens: u64,
72}
73
74impl IdleTurn {
75    /// Create a new idle turn
76    pub fn new(text: impl Into<String>) -> Self {
77        Self {
78            text: text.into(),
79            ..Default::default()
80        }
81    }
82
83    /// Add a tool call
84    pub fn add_tool_call(&mut self, call: IdleToolCall) {
85        self.tool_calls.push(call);
86    }
87
88    /// Add a touched file
89    pub fn add_touched_file(&mut self, path: impl Into<PathBuf>) {
90        self.touched_files.push(path.into());
91    }
92}
93
94/// Tool call in idle
95#[derive(Debug, Clone, Serialize, Deserialize)]
96pub struct IdleToolCall {
97    /// Tool name
98    pub name: String,
99    /// Arguments (truncated)
100    pub args_summary: String,
101    /// Success/failure
102    pub success: bool,
103}
104
105/// Memory update produced by idle completion
106#[derive(Debug, Clone, Serialize, Deserialize)]
107pub struct MemoryUpdate {
108    /// Semantic memory updates
109    pub semantic_facts: Vec<String>,
110    /// Episodic memory entries
111    pub episodic_entries: Vec<EpisodicEntry>,
112    /// Procedural memory updates
113    pub procedural_updates: Vec<String>,
114    /// Total tokens consumed
115    pub total_tokens: u64,
116    /// Duration in milliseconds
117    pub duration_ms: u64,
118}
119
120/// Episodic memory entry
121#[derive(Debug, Clone, Serialize, Deserialize)]
122pub struct EpisodicEntry {
123    /// When the event occurred
124    pub timestamp: chrono::DateTime<chrono::Utc>,
125    /// Event description
126    pub description: String,
127    /// Related files
128    pub related_files: Vec<PathBuf>,
129    /// Importance score (0-1)
130    pub importance: f32,
131}
132
133/// Idle task state
134#[derive(Debug, Clone, Serialize, Deserialize)]
135pub struct IdleTask {
136    /// Unique task ID
137    pub id: uuid::Uuid,
138    /// Current phase
139    pub phase: IdlePhase,
140    /// Reason for idle
141    pub reason: String,
142    /// Idle execution turns
143    pub turns: Vec<IdleTurn>,
144    /// All files touched during idle
145    pub touched_files: Vec<PathBuf>,
146    /// When idle started
147    pub start_time: std::time::SystemTime,
148    /// When idle ended
149    pub end_time: Option<std::time::SystemTime>,
150    /// Memory update produced (set on completion)
151    pub memory_update: Option<MemoryUpdate>,
152    /// Error message if failed
153    pub error: Option<String>,
154    /// Maximum turns to keep (sliding window)
155    max_turns: usize,
156}
157
158impl IdleTask {
159    /// Create a new idle task
160    pub fn new(reason: impl Into<String>) -> Self {
161        Self {
162            id: uuid::Uuid::new_v4(),
163            phase: IdlePhase::Starting,
164            reason: reason.into(),
165            turns: Vec::new(),
166            touched_files: Vec::new(),
167            start_time: std::time::SystemTime::now(),
168            end_time: None,
169            memory_update: None,
170            error: None,
171            max_turns: 30,
172        }
173    }
174
175    /// Add a turn to the idle
176    pub fn add_turn(&mut self, mut turn: IdleTurn) {
177        // Update touched files
178        for file in turn.touched_files.drain(..) {
179            if !self.touched_files.contains(&file) {
180                self.touched_files.push(file);
181            }
182        }
183
184        self.turns.push(turn);
185
186        // Trim to max turns (sliding window)
187        while self.turns.len() > self.max_turns {
188            self.turns.remove(0);
189        }
190    }
191
192    /// Transition to a new phase
193    pub fn transition(&mut self, new_phase: IdlePhase) {
194        if new_phase.is_terminal() && !self.phase.is_terminal() {
195            self.end_time = Some(std::time::SystemTime::now());
196        }
197        self.phase = new_phase;
198    }
199
200    /// Start consolidation phase
201    pub fn start_consolidation(&mut self) {
202        self.transition(IdlePhase::Consolidating);
203    }
204
205    /// Start update phase
206    pub fn start_update(&mut self) {
207        self.transition(IdlePhase::Updating);
208    }
209
210    /// Complete the idle and produce memory update
211    ///
212    /// Returns the memory update that should be applied to memory stores.
213    pub fn complete(mut self) -> MemoryUpdate {
214        self.transition(IdlePhase::Completed);
215
216        let duration_ms = self
217            .end_time
218            .unwrap_or_else(std::time::SystemTime::now)
219            .duration_since(self.start_time)
220            .map(|d| d.as_millis() as u64)
221            .unwrap_or(0);
222
223        let total_tokens: u64 = self
224            .turns
225            .iter()
226            .map(|t| t.input_tokens + t.output_tokens)
227            .sum();
228
229        // Extract semantic facts from turns
230        let semantic_facts: Vec<String> = self
231            .turns
232            .iter()
233            .filter_map(|t| {
234                if t.text.len() > 10 {
235                    Some(t.text.clone())
236                } else {
237                    None
238                }
239            })
240            .take(10)
241            .collect();
242
243        // Create episodic entries
244        let episodic_entries: Vec<EpisodicEntry> = self
245            .touched_files
246            .iter()
247            .map(|f| EpisodicEntry {
248                timestamp: chrono::Utc::now(),
249                description: format!("File accessed: {}", f.display()),
250                related_files: vec![f.clone()],
251                importance: 0.5,
252            })
253            .collect();
254
255        let update = MemoryUpdate {
256            semantic_facts,
257            episodic_entries,
258            procedural_updates: Vec::new(),
259            total_tokens,
260            duration_ms,
261        };
262
263        self.memory_update = Some(update.clone());
264        update
265    }
266
267    /// Mark idle as failed
268    pub fn fail(mut self, error: impl Into<String>) -> Self {
269        self.transition(IdlePhase::Completed);
270        self.error = Some(error.into());
271        self
272    }
273
274    /// Check if idle is completed
275    pub fn is_completed(&self) -> bool {
276        self.phase.is_terminal()
277    }
278
279    /// Get recent turns (for UI display)
280    pub fn recent_turns(&self, count: usize) -> &[IdleTurn] {
281        let start = self.turns.len().saturating_sub(count);
282        &self.turns[start..]
283    }
284
285    /// Get duration in milliseconds
286    pub fn duration_ms(&self) -> Option<u64> {
287        self.end_time
288            .or_else(|| {
289                if self.phase.is_terminal() {
290                    Some(std::time::SystemTime::now())
291                } else {
292                    None
293                }
294            })
295            .and_then(|end| end.duration_since(self.start_time).ok())
296            .map(|d| d.as_millis() as u64)
297    }
298}
299
300#[cfg(test)]
301mod tests {
302    use super::*;
303
304    #[test]
305    fn test_idle_lifecycle() {
306        let mut idle = IdleTask::new("idle_timeout");
307
308        assert_eq!(idle.phase, IdlePhase::Starting);
309
310        idle.start_consolidation();
311        assert_eq!(idle.phase, IdlePhase::Consolidating);
312
313        idle.add_turn(IdleTurn::new("Consolidating memory..."));
314        assert_eq!(idle.turns.len(), 1);
315
316        idle.start_update();
317        assert_eq!(idle.phase, IdlePhase::Updating);
318
319        idle.add_turn(IdleTurn::new("Updating semantic memory"));
320        assert_eq!(idle.turns.len(), 2);
321
322        let update = idle.complete();
323        assert!(!update.semantic_facts.is_empty() || !update.episodic_entries.is_empty());
324    }
325
326    #[test]
327    fn test_idle_sliding_window() {
328        let mut idle = IdleTask::new("test");
329        idle.max_turns = 3;
330
331        for i in 0..5 {
332            idle.add_turn(IdleTurn::new(format!("Turn {}", i)));
333        }
334
335        assert_eq!(idle.turns.len(), 3);
336        assert_eq!(idle.turns[0].text, "Turn 2");
337        assert_eq!(idle.turns[2].text, "Turn 4");
338    }
339
340    #[test]
341    fn test_idle_touched_files() {
342        let mut idle = IdleTask::new("test");
343
344        let mut turn1 = IdleTurn::new("First turn");
345        turn1.add_touched_file("/path/to/file1.rs");
346        turn1.add_touched_file("/path/to/file2.rs");
347
348        let mut turn2 = IdleTurn::new("Second turn");
349        turn2.add_touched_file("/path/to/file1.rs"); // duplicate
350        turn2.add_touched_file("/path/to/file3.rs");
351
352        idle.add_turn(turn1);
353        idle.add_turn(turn2);
354
355        // Should have only unique files
356        assert_eq!(idle.touched_files.len(), 3);
357        assert!(idle
358            .touched_files
359            .contains(&PathBuf::from("/path/to/file1.rs")));
360        assert!(idle
361            .touched_files
362            .contains(&PathBuf::from("/path/to/file2.rs")));
363        assert!(idle
364            .touched_files
365            .contains(&PathBuf::from("/path/to/file3.rs")));
366    }
367}