1use serde::{Deserialize, Serialize};
6use std::collections::HashMap;
7use std::path::PathBuf;
8
9#[derive(Debug, Clone, Default, Serialize, Deserialize)]
11pub struct AgentState {
12 pub files: HashMap<PathBuf, FileState>,
14
15 pub errors: Vec<ErrorInfo>,
17
18 pub occupied_files: HashMap<PathBuf, u32>,
20
21 pub investigated: HashMap<PathBuf, u32>,
23
24 pub metrics: HashMap<String, f64>,
26
27 pub last_progress_tick: u64,
29
30 pub total_changes: usize,
32}
33
34impl AgentState {
35 pub fn new() -> Self {
37 Self::default()
38 }
39
40 pub fn update_file(&mut self, path: PathBuf, state: FileState) {
42 self.files.insert(path, state);
43 }
44
45 pub fn get_file(&self, path: &PathBuf) -> Option<&FileState> {
47 self.files.get(path)
48 }
49
50 pub fn add_error(&mut self, error: ErrorInfo) {
52 self.errors.push(error);
53 }
54
55 pub fn clear_errors(&mut self) {
57 self.errors.clear();
58 }
59
60 pub fn has_errors(&self) -> bool {
62 !self.errors.is_empty()
63 }
64
65 pub fn error_count(&self) -> usize {
67 self.errors.len()
68 }
69
70 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 pub fn occupy_file(&mut self, path: PathBuf, agent_id: u32) {
81 self.occupied_files.insert(path, agent_id);
82 }
83
84 pub fn release_file(&mut self, path: &PathBuf) {
86 self.occupied_files.remove(path);
87 }
88
89 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 pub fn mark_investigated(&mut self, path: PathBuf, agent_id: u32) {
99 self.investigated.insert(path, agent_id);
100 }
101
102 pub fn is_investigated(&self, path: &PathBuf) -> bool {
104 self.investigated.contains_key(path)
105 }
106
107 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 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 pub fn set_metric(&mut self, key: impl Into<String>, value: f64) {
125 self.metrics.insert(key.into(), value);
126 }
127
128 pub fn get_metric(&self, key: &str) -> Option<f64> {
130 self.metrics.get(key).copied()
131 }
132
133 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 pub fn is_stalled(&self, current_tick: u64, threshold: u64) -> bool {
143 current_tick.saturating_sub(self.last_progress_tick) >= threshold
144 }
145
146 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#[derive(Debug, Clone, Default, Serialize, Deserialize)]
161pub struct FileState {
162 pub lines: usize,
164
165 pub size_bytes: usize,
167
168 pub last_modified_tick: u64,
170
171 pub changes: usize,
173
174 pub read: bool,
176
177 pub modified: bool,
179}
180
181impl FileState {
182 pub fn new(lines: usize, size_bytes: usize) -> Self {
184 Self {
185 lines,
186 size_bytes,
187 ..Default::default()
188 }
189 }
190
191 pub fn mark_read(&mut self) {
193 self.read = true;
194 }
195
196 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#[derive(Debug, Clone, Serialize, Deserialize)]
206pub struct ErrorInfo {
207 pub file: PathBuf,
209
210 pub line: usize,
212
213 pub column: Option<usize>,
215
216 pub message: String,
218
219 pub severity: ErrorSeverity,
221}
222
223impl ErrorInfo {
224 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 pub fn with_column(mut self, column: usize) -> Self {
237 self.column = Some(column);
238 self
239 }
240
241 pub fn with_severity(mut self, severity: ErrorSeverity) -> Self {
243 self.severity = severity;
244 self
245 }
246}
247
248#[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 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 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 state.occupy_file(path.clone(), 0);
289 assert!(!state.is_occupied(&path, 0)); assert!(state.is_occupied(&path, 1)); 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 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 let uninvestigated = state.uninvestigated_files();
313 assert_eq!(uninvestigated.len(), 1);
314 }
315}