// src/agent/runtime/duplicate_detector.rs // // 同质调用检测器:检测重复的工具调用,防止死循环。 // // 历史实现是单槽(只记住上一次调用),A/B 交替死循环 // (call A → call B → call A → call B ...)检测不到。 // 现改为"连续计数 + 滑动窗口计数"双通道: // - 连续通道:连续相同调用 ≥ threshold(原语义,立即触发) // - 窗口通道:最近 threshold*2 次调用中同一调用出现 ≥ threshold+2 次 // (捕获 A/B 交替;门槛略高以放过合理的重复只读调用) use std::collections::VecDeque; /// 同质调用检测器 #[derive(Debug, Default)] pub struct DuplicateDetector { /// 最近调用的滑动窗口(name, arguments) recent: VecDeque<(String, String)>, } impl DuplicateDetector { /// 记录一次调用,返回是否检测到死循环 pub fn record(&mut self, tool_name: &str, arguments: &str, threshold: usize) -> bool { let key = (tool_name.to_string(), arguments.to_string()); let window = (threshold * 2).max(4); self.recent.push_back(key.clone()); while self.recent.len() > window { self.recent.pop_front(); } // 连续通道:尾部连续出现次数 let consecutive = self.recent.iter().rev().take_while(|k| *k == &key).count(); if threshold >= 2 && consecutive >= threshold { return true; } // 窗口通道:同一调用在窗口内出现次数(捕获 A/B 交替)。 // 门槛与 threshold 相同:纯交替下窗口内单键最多出现 window/2 = // threshold 次,恰好可达;合理的间隔重复(≤ threshold-1 次)不受影响。 let occurrences = self.recent.iter().filter(|k| **k == key).count(); threshold >= 2 && occurrences >= threshold } } #[cfg(test)] mod tests { use super::*; #[test] fn test_consecutive_duplicates_detected() { let mut det = DuplicateDetector::default(); assert!(!det.record("search_papers", "q=1", 3)); assert!(!det.record("search_papers", "q=1", 3)); assert!(det.record("search_papers", "q=1", 3)); } #[test] fn test_alternating_loop_detected() { // A/B 交替:单槽实现检测不到,窗口通道应捕获 let mut det = DuplicateDetector::default(); let mut tripped = false; for i in 0..6 { let a = det.record("tool_a", "{}", 3); let b = det.record("tool_b", "{}", 3); if a || b { // 交替 5 次出现(threshold+2 = 5)后触发 assert!(i >= 1, "不应过早触发 (round {})", i); tripped = true; break; } } assert!(tripped, "A/B 交替死循环应被检测到"); } #[test] fn test_spaced_repeats_not_detected() { // 合理的重复只读调用(间隔其他调用)不应触发 let mut det = DuplicateDetector::default(); det.record("read_file", "a.rs", 3); det.record("grep_files", "pat", 3); det.record("read_file", "b.rs", 3); det.record("glob_files", "*.rs", 3); let tripped = det.record("read_file", "a.rs", 3); assert!(!tripped); } #[test] fn test_different_args_not_duplicates() { let mut det = DuplicateDetector::default(); for q in ["q=1", "q=2", "q=3", "q=4", "q=5", "q=6", "q=7", "q=8"] { assert!(!det.record("search_papers", q, 3)); } } }