ras_agent/domain/
loop_detector.rs1use sha2::{Digest, Sha256};
2use url::Url;
3
4#[derive(Debug, Clone, PartialEq, Eq, Hash)]
5pub struct PageFingerprint(String);
6
7impl PageFingerprint {
8 #[must_use]
9 pub fn new(url: &Url, title: &str, clickable_count: u32, text_chars: u32) -> Self {
10 let mut h = Sha256::new();
11 h.update(url.as_str().as_bytes());
12 h.update(b"|");
13 h.update(title.as_bytes());
14 h.update(b"|");
15 h.update(clickable_count.to_le_bytes());
16 h.update(b"|");
17 h.update(text_chars.to_le_bytes());
18 Self(format!("{:x}", h.finalize()))
19 }
20
21 #[must_use]
22 pub fn as_str(&self) -> &str {
23 &self.0
24 }
25}
26
27#[derive(Debug, Default)]
28pub struct ActionLoopDetector {
29 history: Vec<String>,
30 pages: Vec<PageFingerprint>,
31 pub action_threshold: u32,
32 pub stagnation_threshold: u32,
33}
34
35impl ActionLoopDetector {
36 #[must_use]
37 pub fn new() -> Self {
38 Self {
39 history: Vec::new(),
40 pages: Vec::new(),
41 action_threshold: 5,
42 stagnation_threshold: 5,
43 }
44 }
45
46 pub fn record_action(&mut self, action_hash: impl Into<String>) {
47 self.history.push(action_hash.into());
48 if self.history.len() > 32 {
49 self.history.remove(0);
50 }
51 }
52
53 pub fn record_page(&mut self, fingerprint: PageFingerprint) {
54 self.pages.push(fingerprint);
55 if self.pages.len() > 32 {
56 self.pages.remove(0);
57 }
58 }
59
60 #[must_use]
61 pub fn action_loop_detected(&self) -> bool {
62 let n = self.action_threshold as usize;
63 if self.history.len() < n {
64 return false;
65 }
66 let last = &self.history[self.history.len() - 1];
67 self.history.iter().rev().take(n).all(|h| h == last)
68 }
69
70 #[must_use]
71 pub fn page_stagnation_detected(&self) -> bool {
72 let n = self.stagnation_threshold as usize;
73 if self.pages.len() < n {
74 return false;
75 }
76 let last = &self.pages[self.pages.len() - 1];
77 self.pages.iter().rev().take(n).all(|p| p == last)
78 }
79}