Skip to main content

ryo_executor/decider/
state.rs

1//! AgentState: Shared state for agent decision-making
2//!
3//! This represents the "world" as seen by agents - files, errors, etc.
4
5use serde::{Deserialize, Serialize};
6use std::collections::HashMap;
7use std::path::PathBuf;
8
9/// State of the agent's world
10#[derive(Debug, Clone, Default, Serialize, Deserialize)]
11pub struct AgentState {
12    /// Files that have been loaded/modified
13    pub files: HashMap<PathBuf, FileState>,
14
15    /// Current errors (compile, lint, etc.)
16    pub errors: Vec<ErrorInfo>,
17
18    /// Files currently being investigated by other agents
19    pub occupied_files: HashMap<PathBuf, u32>,
20
21    /// Completed investigation targets
22    pub investigated: HashMap<PathBuf, u32>,
23
24    /// Custom metrics
25    pub metrics: HashMap<String, f64>,
26
27    /// Last progress tick (for stall detection)
28    pub last_progress_tick: u64,
29
30    /// Total changes made
31    pub total_changes: usize,
32}
33
34impl AgentState {
35    /// Create a new empty state
36    pub fn new() -> Self {
37        Self::default()
38    }
39
40    /// Add or update a file's state
41    pub fn update_file(&mut self, path: PathBuf, state: FileState) {
42        self.files.insert(path, state);
43    }
44
45    /// Get a file's state
46    pub fn get_file(&self, path: &PathBuf) -> Option<&FileState> {
47        self.files.get(path)
48    }
49
50    /// Add an error
51    pub fn add_error(&mut self, error: ErrorInfo) {
52        self.errors.push(error);
53    }
54
55    /// Clear all errors
56    pub fn clear_errors(&mut self) {
57        self.errors.clear();
58    }
59
60    /// Check if there are any errors
61    pub fn has_errors(&self) -> bool {
62        !self.errors.is_empty()
63    }
64
65    /// Get the number of errors
66    pub fn error_count(&self) -> usize {
67        self.errors.len()
68    }
69
70    /// Get the file with the most errors
71    pub fn most_errored_file(&self) -> Option<&PathBuf> {
72        let mut counts: HashMap<&PathBuf, usize> = HashMap::new();
73        for error in &self.errors {
74            *counts.entry(&error.file).or_insert(0) += 1;
75        }
76        counts.into_iter().max_by_key(|(_, c)| *c).map(|(f, _)| f)
77    }
78
79    /// Mark a file as occupied by an agent
80    pub fn occupy_file(&mut self, path: PathBuf, agent_id: u32) {
81        self.occupied_files.insert(path, agent_id);
82    }
83
84    /// Release a file
85    pub fn release_file(&mut self, path: &PathBuf) {
86        self.occupied_files.remove(path);
87    }
88
89    /// Check if a file is occupied (by another agent)
90    pub fn is_occupied(&self, path: &PathBuf, exclude_agent: u32) -> bool {
91        self.occupied_files
92            .get(path)
93            .map(|&id| id != exclude_agent)
94            .unwrap_or(false)
95    }
96
97    /// Mark a file as investigated
98    pub fn mark_investigated(&mut self, path: PathBuf, agent_id: u32) {
99        self.investigated.insert(path, agent_id);
100    }
101
102    /// Check if a file has been investigated
103    pub fn is_investigated(&self, path: &PathBuf) -> bool {
104        self.investigated.contains_key(path)
105    }
106
107    /// Get uninvestigated files
108    pub fn uninvestigated_files(&self) -> Vec<&PathBuf> {
109        self.files
110            .keys()
111            .filter(|p| !self.investigated.contains_key(*p))
112            .collect()
113    }
114
115    /// Get available files for an agent (not occupied, not investigated)
116    pub fn available_files(&self, agent_id: u32) -> Vec<&PathBuf> {
117        self.files
118            .keys()
119            .filter(|p| !self.investigated.contains_key(*p) && !self.is_occupied(p, agent_id))
120            .collect()
121    }
122
123    /// Set a metric
124    pub fn set_metric(&mut self, key: impl Into<String>, value: f64) {
125        self.metrics.insert(key.into(), value);
126    }
127
128    /// Get a metric
129    pub fn get_metric(&self, key: &str) -> Option<f64> {
130        self.metrics.get(key).copied()
131    }
132
133    /// Record progress at current tick
134    pub fn record_progress(&mut self, tick: u64, changes: usize) {
135        if changes > 0 {
136            self.last_progress_tick = tick;
137            self.total_changes += changes;
138        }
139    }
140
141    /// Check if progress is stalled
142    pub fn is_stalled(&self, current_tick: u64, threshold: u64) -> bool {
143        current_tick.saturating_sub(self.last_progress_tick) >= threshold
144    }
145
146    /// Generate a summary for logging
147    pub fn summary(&self) -> String {
148        format!(
149            "Files: {}, Errors: {}, Investigated: {}/{}, Changes: {}",
150            self.files.len(),
151            self.errors.len(),
152            self.investigated.len(),
153            self.files.len(),
154            self.total_changes,
155        )
156    }
157}
158
159/// State of a single file
160#[derive(Debug, Clone, Default, Serialize, Deserialize)]
161pub struct FileState {
162    /// Number of lines
163    pub lines: usize,
164
165    /// File size in bytes
166    pub size_bytes: usize,
167
168    /// Last modification tick
169    pub last_modified_tick: u64,
170
171    /// Number of changes made
172    pub changes: usize,
173
174    /// Whether the file has been read
175    pub read: bool,
176
177    /// Whether the file has been modified
178    pub modified: bool,
179}
180
181impl FileState {
182    /// Create a new file state
183    pub fn new(lines: usize, size_bytes: usize) -> Self {
184        Self {
185            lines,
186            size_bytes,
187            ..Default::default()
188        }
189    }
190
191    /// Mark as read
192    pub fn mark_read(&mut self) {
193        self.read = true;
194    }
195
196    /// Mark as modified
197    pub fn mark_modified(&mut self, tick: u64, changes: usize) {
198        self.modified = true;
199        self.last_modified_tick = tick;
200        self.changes += changes;
201    }
202}
203
204/// Error information
205#[derive(Debug, Clone, Serialize, Deserialize)]
206pub struct ErrorInfo {
207    /// File containing the error
208    pub file: PathBuf,
209
210    /// Line number
211    pub line: usize,
212
213    /// Column number
214    pub column: Option<usize>,
215
216    /// Error message
217    pub message: String,
218
219    /// Error severity
220    pub severity: ErrorSeverity,
221}
222
223impl ErrorInfo {
224    /// Create a new error
225    pub fn new(file: PathBuf, line: usize, message: impl Into<String>) -> Self {
226        Self {
227            file,
228            line,
229            column: None,
230            message: message.into(),
231            severity: ErrorSeverity::Error,
232        }
233    }
234
235    /// Set the column
236    pub fn with_column(mut self, column: usize) -> Self {
237        self.column = Some(column);
238        self
239    }
240
241    /// Set the severity
242    pub fn with_severity(mut self, severity: ErrorSeverity) -> Self {
243        self.severity = severity;
244        self
245    }
246}
247
248/// Error severity
249#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize)]
250pub enum ErrorSeverity {
251    Warning,
252    Error,
253    Critical,
254}
255
256#[cfg(test)]
257mod tests {
258    use super::*;
259
260    #[test]
261    fn test_agent_state() {
262        let mut state = AgentState::new();
263
264        // Add files
265        state.update_file(PathBuf::from("a.rs"), FileState::new(100, 2000));
266        state.update_file(PathBuf::from("b.rs"), FileState::new(50, 1000));
267
268        assert_eq!(state.files.len(), 2);
269
270        // Add errors
271        state.add_error(ErrorInfo::new(PathBuf::from("a.rs"), 10, "error 1"));
272        state.add_error(ErrorInfo::new(PathBuf::from("a.rs"), 20, "error 2"));
273        state.add_error(ErrorInfo::new(PathBuf::from("b.rs"), 5, "error 3"));
274
275        assert!(state.has_errors());
276        assert_eq!(state.error_count(), 3);
277        assert_eq!(state.most_errored_file(), Some(&PathBuf::from("a.rs")));
278    }
279
280    #[test]
281    fn test_occupation() {
282        let mut state = AgentState::new();
283        state.update_file(PathBuf::from("a.rs"), FileState::new(100, 2000));
284
285        let path = PathBuf::from("a.rs");
286
287        // Agent 0 occupies the file
288        state.occupy_file(path.clone(), 0);
289        assert!(!state.is_occupied(&path, 0)); // Not occupied for agent 0
290        assert!(state.is_occupied(&path, 1)); // Occupied for agent 1
291
292        // Release
293        state.release_file(&path);
294        assert!(!state.is_occupied(&path, 1));
295    }
296
297    #[test]
298    fn test_investigation() {
299        let mut state = AgentState::new();
300        state.update_file(PathBuf::from("a.rs"), FileState::new(100, 2000));
301        state.update_file(PathBuf::from("b.rs"), FileState::new(50, 1000));
302
303        let path_a = PathBuf::from("a.rs");
304
305        // Mark a.rs as investigated
306        state.mark_investigated(path_a.clone(), 0);
307
308        assert!(state.is_investigated(&path_a));
309        assert!(!state.is_investigated(&PathBuf::from("b.rs")));
310
311        // Only b.rs should be uninvestigated
312        let uninvestigated = state.uninvestigated_files();
313        assert_eq!(uninvestigated.len(), 1);
314    }
315}