| use async_trait::async_trait; |
| use derive_setters::Setters; |
| use forge_domain::{ |
| ContextMessage, Conversation, EventData, EventHandle, RequestPayload, Role, TextMessage, |
| ToolCallArguments, ToolName, |
| }; |
| use forge_template::Element; |
| use tracing::warn; |
|
|
| use crate::TemplateEngine; |
|
|
| |
| |
| |
| |
| |
| |
| |
| |
| |
| |
| |
| |
| #[derive(Debug, Clone, Setters)] |
| pub struct DoomLoopDetector { |
| |
| |
| threshold: usize, |
| } |
|
|
| impl Default for DoomLoopDetector { |
| fn default() -> Self { |
| Self::new() |
| } |
| } |
|
|
| impl DoomLoopDetector { |
| const DEFAULT_THRESHOLD: usize = 3; |
|
|
| |
| pub fn new() -> Self { |
| Self { threshold: Self::DEFAULT_THRESHOLD } |
| } |
|
|
| |
| |
| |
| |
| |
| |
| |
| pub fn detect_from_conversation(&self, conversation: &Conversation) -> Option<usize> { |
| let all_signatures = self.extract_tool_signatures(conversation); |
|
|
| let (_, count) = self.check_repeating_pattern(&all_signatures)?; |
|
|
| Some(count) |
| } |
|
|
| fn extract_tool_signatures( |
| &self, |
| conversation: &Conversation, |
| ) -> Vec<(ToolName, ToolCallArguments)> { |
| let assistant_messages = conversation |
| .context |
| .as_ref() |
| .map(|ctx| { |
| Self::extract_assistant_messages(ctx.messages.iter().map(|entry| &entry.message)) |
| }) |
| .unwrap_or_default(); |
|
|
| assistant_messages |
| .iter() |
| .filter_map(|msg| msg.tool_calls.as_ref()) |
| .flat_map(|calls| calls.iter()) |
| .map(|call| (call.name.clone(), call.arguments.clone())) |
| .collect() |
| } |
|
|
| |
| fn check_repeating_pattern<T>(&self, sequence: &[T]) -> Option<(usize, usize)> |
| where |
| T: Eq, |
| { |
| if sequence.is_empty() { |
| return None; |
| } |
|
|
| if sequence.len() < self.threshold { |
| return None; |
| } |
|
|
| for pattern_length in 1..sequence.len() { |
| let complete_repetitions = |
| self.count_recent_pattern_repetitions(sequence, pattern_length); |
|
|
| if complete_repetitions >= self.threshold { |
| let pattern_offset = complete_repetitions.checked_mul(pattern_length)?; |
| let pattern_start_idx = sequence.len().checked_sub(pattern_offset)?; |
|
|
| if sequence.get(pattern_start_idx).is_some() { |
| return Some((pattern_start_idx, complete_repetitions)); |
| } |
| } |
| } |
|
|
| None |
| } |
|
|
| |
| |
| |
| |
| |
| |
| |
| fn count_recent_pattern_repetitions<T>(&self, sequence: &[T], pattern_length: usize) -> usize |
| where |
| T: Eq, |
| { |
| if pattern_length == 0 || sequence.len() < pattern_length { |
| return 0; |
| } |
|
|
| |
| let total_len = sequence.len(); |
| let mut repetitions = 0; |
|
|
| |
| |
| let mut check_len = total_len; |
|
|
| |
| |
| if !total_len.is_multiple_of(pattern_length) { |
| let partial_len = total_len % pattern_length; |
| |
| |
| if total_len < pattern_length + partial_len { |
| return 0; |
| } |
|
|
| let pattern_start = total_len - partial_len - pattern_length; |
| let pattern_end = pattern_start + pattern_length; |
| let partial_start = total_len - partial_len; |
|
|
| let Some(pattern) = sequence.get(pattern_start..pattern_end) else { |
| return 0; |
| }; |
| let Some(partial) = sequence.get(partial_start..total_len) else { |
| return 0; |
| }; |
| let Some(pattern_prefix) = pattern.get(..partial_len) else { |
| return 0; |
| }; |
|
|
| if partial == pattern_prefix { |
| repetitions += 1; |
| check_len = total_len - partial_len; |
| } else { |
| |
| return 0; |
| } |
| } |
|
|
| |
| if check_len < pattern_length { |
| return repetitions; |
| } |
|
|
| |
| let pattern_start = check_len - pattern_length; |
| let Some(pattern) = sequence.get(pattern_start..check_len) else { |
| return repetitions; |
| }; |
| repetitions += 1; |
|
|
| |
| let mut pos = pattern_start; |
| while pos >= pattern_length { |
| pos -= pattern_length; |
| let Some(chunk) = sequence.get(pos..pos + pattern_length) else { |
| break; |
| }; |
|
|
| if chunk == pattern { |
| repetitions += 1; |
| } else { |
| |
| break; |
| } |
| } |
|
|
| repetitions |
| } |
|
|
| |
| |
| |
| pub fn extract_assistant_messages<'a>( |
| messages: impl Iterator<Item = &'a ContextMessage> + 'a, |
| ) -> Vec<&'a TextMessage> { |
| messages |
| .filter_map(|msg| { |
| if let ContextMessage::Text(text_msg) = msg |
| && text_msg.role == Role::Assistant |
| { |
| return Some(text_msg); |
| } |
| None |
| }) |
| .collect() |
| } |
| } |
|
|
| |
| |
| |
| |
| |
| #[async_trait] |
| impl EventHandle<EventData<RequestPayload>> for DoomLoopDetector { |
| async fn handle( |
| &self, |
| event: &EventData<RequestPayload>, |
| conversation: &mut Conversation, |
| ) -> anyhow::Result<()> { |
| if let Some(consecutive_calls) = self.detect_from_conversation(conversation) { |
| warn!( |
| agent_id = %event.agent.id, |
| request_count = event.payload.request_count, |
| consecutive_calls, |
| "Doom loop detected from conversation context before next request" |
| ); |
|
|
| if let Some(context) = conversation.context.as_mut() { |
| let reminder = TemplateEngine::default().render( |
| "forge-doom-loop-reminder.md", |
| &serde_json::json!({"consecutive_calls": consecutive_calls}), |
| )?; |
| let content = Element::new("system_reminder").cdata(reminder); |
| context |
| .messages |
| .push(ContextMessage::user(content, None).into()); |
| } |
| } |
|
|
| Ok(()) |
| } |
| } |
|
|
| #[cfg(test)] |
| mod tests { |
| use forge_domain::{ |
| Context, ContextMessage, ConversationId, MessageEntry, ToolCallArguments, ToolCallFull, |
| }; |
| use pretty_assertions::assert_eq; |
|
|
| use super::*; |
|
|
| fn create_assistant_message(tool_call: &ToolCallFull) -> TextMessage { |
| TextMessage { |
| role: Role::Assistant, |
| content: String::new(), |
| raw_content: None, |
| tool_calls: Some(vec![tool_call.clone()]), |
| thought_signature: None, |
| model: None, |
| reasoning_details: None, |
| droppable: false, |
| phase: None, |
| } |
| } |
|
|
| fn create_conversation_with_messages(messages: Vec<TextMessage>) -> Conversation { |
| let context_messages: Vec<MessageEntry> = messages |
| .into_iter() |
| .map(|msg| MessageEntry::from(ContextMessage::Text(msg))) |
| .collect(); |
|
|
| let context = Context::default().messages(context_messages); |
|
|
| Conversation { |
| id: ConversationId::generate(), |
| title: None, |
| context: Some(context), |
| metrics: Default::default(), |
| metadata: forge_domain::MetaData::new(chrono::Utc::now()), |
| } |
| } |
|
|
| #[test] |
| fn test_doom_loop_detector_detects_identical_calls() { |
| let detector = DoomLoopDetector::new(); |
|
|
| let tool_call = ToolCallFull::new("read") |
| .arguments(ToolCallArguments::from_json(r#"{"path": "file.txt"}"#)); |
|
|
| |
| let msg1 = create_assistant_message(&tool_call); |
| let msg2 = create_assistant_message(&tool_call); |
| let msg3 = create_assistant_message(&tool_call); |
| let conversation = create_conversation_with_messages(vec![msg1, msg2, msg3]); |
|
|
| |
| let actual = detector.detect_from_conversation(&conversation); |
| let expected = Some(3); |
| assert_eq!(actual, expected); |
| } |
|
|
| #[test] |
| fn test_doom_loop_detector_no_loop_with_two_calls() { |
| let detector = DoomLoopDetector::new(); |
|
|
| let tool_call = ToolCallFull::new("read") |
| .arguments(ToolCallArguments::from_json(r#"{"path": "file.txt"}"#)); |
|
|
| |
| let msg1 = create_assistant_message(&tool_call); |
| let conversation = create_conversation_with_messages(vec![msg1]); |
|
|
| |
| let actual = detector.detect_from_conversation(&conversation); |
| assert_eq!(actual, None); |
| } |
|
|
| #[test] |
| fn test_doom_loop_detector_resets_on_different_arguments() { |
| let detector = DoomLoopDetector::new(); |
|
|
| let tool_call_1 = ToolCallFull::new("read") |
| .arguments(ToolCallArguments::from_json(r#"{"path": "file1.txt"}"#)); |
| let tool_call_2 = ToolCallFull::new("read") |
| .arguments(ToolCallArguments::from_json(r#"{"path": "file2.txt"}"#)); |
|
|
| |
| let msg1 = create_assistant_message(&tool_call_1); |
| let msg2 = create_assistant_message(&tool_call_1); |
| let msg3 = create_assistant_message(&tool_call_2); |
| let conversation = create_conversation_with_messages(vec![msg1, msg2, msg3]); |
|
|
| |
| let actual = detector.detect_from_conversation(&conversation); |
| assert_eq!(actual, None); |
| } |
|
|
| #[test] |
| fn test_doom_loop_detector_resets_on_different_tool() { |
| let detector = DoomLoopDetector::new(); |
|
|
| let tool_call_1 = ToolCallFull::new("read") |
| .arguments(ToolCallArguments::from_json(r#"{"path": "file.txt"}"#)); |
| let tool_call_2 = ToolCallFull::new("write") |
| .arguments(ToolCallArguments::from_json(r#"{"path": "file.txt"}"#)); |
|
|
| |
| let msg1 = create_assistant_message(&tool_call_1); |
| let msg2 = create_assistant_message(&tool_call_1); |
| let msg3 = create_assistant_message(&tool_call_2); |
| let conversation = create_conversation_with_messages(vec![msg1, msg2, msg3]); |
|
|
| |
| let actual = detector.detect_from_conversation(&conversation); |
| assert_eq!(actual, None); |
| } |
|
|
| #[test] |
| fn test_doom_loop_detector_custom_threshold() { |
| let detector = DoomLoopDetector::new().threshold(2); |
|
|
| let tool_call = ToolCallFull::new("read") |
| .arguments(ToolCallArguments::from_json(r#"{"path": "file.txt"}"#)); |
|
|
| |
| let msg1 = create_assistant_message(&tool_call); |
| let msg2 = create_assistant_message(&tool_call); |
| let conversation = create_conversation_with_messages(vec![msg1, msg2]); |
|
|
| |
| let actual = detector.detect_from_conversation(&conversation); |
| let expected = Some(2); |
| assert_eq!(actual, expected); |
| } |
|
|
| #[test] |
| fn test_doom_loop_detector_empty_history() { |
| let detector = DoomLoopDetector::new(); |
|
|
| |
| let conversation = create_conversation_with_messages(vec![]); |
|
|
| let actual = detector.detect_from_conversation(&conversation); |
| assert_eq!(actual, None); |
| } |
|
|
| #[test] |
| fn test_extract_assistant_messages() { |
| let assistant_msg_1 = TextMessage { |
| role: Role::Assistant, |
| content: "Response 1".to_string(), |
| raw_content: None, |
| tool_calls: None, |
| thought_signature: None, |
| model: None, |
| reasoning_details: None, |
| droppable: false, |
| phase: None, |
| }; |
|
|
| let user_msg = TextMessage { |
| role: Role::User, |
| content: "Question".to_string(), |
| raw_content: None, |
| tool_calls: None, |
| thought_signature: None, |
| model: None, |
| reasoning_details: None, |
| droppable: false, |
| phase: None, |
| }; |
|
|
| let assistant_msg_2 = TextMessage { |
| role: Role::Assistant, |
| content: "Response 2".to_string(), |
| raw_content: None, |
| tool_calls: None, |
| thought_signature: None, |
| model: None, |
| reasoning_details: None, |
| droppable: false, |
| phase: None, |
| }; |
|
|
| let messages = [ |
| ContextMessage::Text(assistant_msg_1.clone()), |
| ContextMessage::Text(user_msg), |
| ContextMessage::Text(assistant_msg_2.clone()), |
| ]; |
|
|
| let result = DoomLoopDetector::extract_assistant_messages(messages.iter()); |
|
|
| assert_eq!(result.len(), 2); |
| assert_eq!(result[0].content, "Response 1"); |
| assert_eq!(result[1].content, "Response 2"); |
| } |
|
|
| #[test] |
| fn test_detect_pattern_start_with_integers_for_123_123_123() { |
| let detector = DoomLoopDetector::new(); |
| let fixture = vec![1, 2, 3, 1, 2, 3, 1, 2, 3]; |
|
|
| let actual = detector.check_repeating_pattern(&fixture); |
| let expected = Some((0, 3)); |
| assert_eq!(actual, expected); |
| } |
|
|
| #[test] |
| fn test_detect_pattern_start_with_integers_detects_recent_suffix_pattern() { |
| let detector = DoomLoopDetector::new(); |
| let fixture = vec![1, 2, 3, 1, 2, 3, 4, 5, 4, 5, 4, 5]; |
|
|
| let actual = detector.check_repeating_pattern(&fixture); |
| let expected = Some((6, 3)); |
| assert_eq!(actual, expected); |
| } |
|
|
| #[test] |
| fn test_detect_pattern_start_with_integers_detects_consecutive_identical() { |
| let detector = DoomLoopDetector::new(); |
| let fixture = vec![1, 2, 3, 3, 3]; |
|
|
| let actual = detector.check_repeating_pattern(&fixture); |
| let expected = Some((2, 3)); |
| assert_eq!(actual, expected); |
| } |
|
|
| #[test] |
| fn test_doom_loop_detector_detects_repeating_pattern_123_123_123() { |
| let detector = DoomLoopDetector::new(); |
|
|
| let tool_call_1 = ToolCallFull::new("read") |
| .arguments(ToolCallArguments::from_json(r#"{"path": "file1.txt"}"#)); |
| let tool_call_2 = ToolCallFull::new("write") |
| .arguments(ToolCallArguments::from_json(r#"{"path": "file2.txt"}"#)); |
| let tool_call_3 = ToolCallFull::new("patch") |
| .arguments(ToolCallArguments::from_json(r#"{"path": "file3.txt"}"#)); |
|
|
| |
| let msg1 = create_assistant_message(&tool_call_1); |
| let msg2 = create_assistant_message(&tool_call_2); |
| let msg3 = create_assistant_message(&tool_call_3); |
| let msg4 = create_assistant_message(&tool_call_1); |
| let msg5 = create_assistant_message(&tool_call_2); |
| let msg6 = create_assistant_message(&tool_call_3); |
| let msg7 = create_assistant_message(&tool_call_1); |
| let msg8 = create_assistant_message(&tool_call_2); |
| let msg9 = create_assistant_message(&tool_call_3); |
|
|
| let conversation = create_conversation_with_messages(vec![ |
| msg1, msg2, msg3, msg4, msg5, msg6, msg7, msg8, msg9, |
| ]); |
|
|
| let actual = detector.detect_from_conversation(&conversation); |
|
|
| |
| assert_eq!(actual, Some(3)); |
| } |
|
|
| #[test] |
| fn test_doom_loop_detector_detects_repeating_pattern_12_12_12() { |
| let detector = DoomLoopDetector::new(); |
|
|
| let tool_call_1 = ToolCallFull::new("read") |
| .arguments(ToolCallArguments::from_json(r#"{"path": "file1.txt"}"#)); |
| let tool_call_2 = ToolCallFull::new("write") |
| .arguments(ToolCallArguments::from_json(r#"{"path": "file2.txt"}"#)); |
|
|
| |
| let msg1 = create_assistant_message(&tool_call_1); |
| let msg2 = create_assistant_message(&tool_call_2); |
| let msg3 = create_assistant_message(&tool_call_1); |
| let msg4 = create_assistant_message(&tool_call_2); |
| let msg5 = create_assistant_message(&tool_call_1); |
| let msg6 = create_assistant_message(&tool_call_2); |
|
|
| let conversation = |
| create_conversation_with_messages(vec![msg1, msg2, msg3, msg4, msg5, msg6]); |
|
|
| let actual = detector.detect_from_conversation(&conversation); |
|
|
| |
| assert_eq!(actual, Some(3)); |
| } |
|
|
| #[test] |
| fn test_doom_loop_detector_no_pattern_with_partial_repetition() { |
| let detector = DoomLoopDetector::new(); |
|
|
| let tool_call_1 = ToolCallFull::new("read") |
| .arguments(ToolCallArguments::from_json(r#"{"path": "file1.txt"}"#)); |
| let tool_call_2 = ToolCallFull::new("write") |
| .arguments(ToolCallArguments::from_json(r#"{"path": "file2.txt"}"#)); |
| let tool_call_3 = ToolCallFull::new("patch") |
| .arguments(ToolCallArguments::from_json(r#"{"path": "file3.txt"}"#)); |
|
|
| |
| let msg1 = create_assistant_message(&tool_call_1); |
| let msg2 = create_assistant_message(&tool_call_2); |
| let msg3 = create_assistant_message(&tool_call_3); |
| let msg4 = create_assistant_message(&tool_call_1); |
| let msg5 = create_assistant_message(&tool_call_2); |
|
|
| let conversation = create_conversation_with_messages(vec![msg1, msg2, msg3, msg4, msg5]); |
|
|
| |
| let actual = detector.detect_from_conversation(&conversation); |
|
|
| |
| assert_eq!(actual, None); |
| } |
|
|
| #[test] |
| fn test_doom_loop_detector_pattern_with_custom_threshold() { |
| let detector = DoomLoopDetector::new().threshold(2); |
|
|
| let tool_call_1 = ToolCallFull::new("read") |
| .arguments(ToolCallArguments::from_json(r#"{"path": "file1.txt"}"#)); |
| let tool_call_2 = ToolCallFull::new("write") |
| .arguments(ToolCallArguments::from_json(r#"{"path": "file2.txt"}"#)); |
|
|
| |
| let msg1 = create_assistant_message(&tool_call_1); |
| let msg2 = create_assistant_message(&tool_call_2); |
| let msg3 = create_assistant_message(&tool_call_1); |
| let msg4 = create_assistant_message(&tool_call_2); |
|
|
| let conversation = create_conversation_with_messages(vec![msg1, msg2, msg3, msg4]); |
|
|
| let actual = detector.detect_from_conversation(&conversation); |
|
|
| |
| assert_eq!(actual, Some(2)); |
| } |
|
|
| #[test] |
| fn test_doom_loop_detector_consecutive_identical_takes_precedence() { |
| let detector = DoomLoopDetector::new(); |
|
|
| let tool_call_1 = ToolCallFull::new("read") |
| .arguments(ToolCallArguments::from_json(r#"{"path": "file1.txt"}"#)); |
|
|
| |
| let msg1 = create_assistant_message(&tool_call_1); |
| let msg2 = create_assistant_message(&tool_call_1); |
| let msg3 = create_assistant_message(&tool_call_1); |
|
|
| let conversation = create_conversation_with_messages(vec![msg1, msg2, msg3]); |
|
|
| |
| let actual = detector.detect_from_conversation(&conversation); |
|
|
| assert_eq!(actual, Some(3)); |
| } |
|
|
| #[test] |
| fn test_doom_loop_detector_complex_pattern_1234_1234_1234() { |
| let detector = DoomLoopDetector::new(); |
|
|
| let tool_call_1 = ToolCallFull::new("read") |
| .arguments(ToolCallArguments::from_json(r#"{"path": "file1.txt"}"#)); |
| let tool_call_2 = ToolCallFull::new("write") |
| .arguments(ToolCallArguments::from_json(r#"{"path": "file2.txt"}"#)); |
| let tool_call_3 = ToolCallFull::new("patch") |
| .arguments(ToolCallArguments::from_json(r#"{"path": "file3.txt"}"#)); |
| let tool_call_4 = ToolCallFull::new("shell") |
| .arguments(ToolCallArguments::from_json(r#"{"command": "ls"}"#)); |
|
|
| |
| let msg1 = create_assistant_message(&tool_call_1); |
| let msg2 = create_assistant_message(&tool_call_2); |
| let msg3 = create_assistant_message(&tool_call_3); |
| let msg4 = create_assistant_message(&tool_call_4); |
| let msg5 = create_assistant_message(&tool_call_1); |
| let msg6 = create_assistant_message(&tool_call_2); |
| let msg7 = create_assistant_message(&tool_call_3); |
| let msg8 = create_assistant_message(&tool_call_4); |
| let msg9 = create_assistant_message(&tool_call_1); |
| let msg10 = create_assistant_message(&tool_call_2); |
| let msg11 = create_assistant_message(&tool_call_3); |
| let msg12 = create_assistant_message(&tool_call_4); |
|
|
| let conversation = create_conversation_with_messages(vec![ |
| msg1, msg2, msg3, msg4, msg5, msg6, msg7, msg8, msg9, msg10, msg11, msg12, |
| ]); |
|
|
| let actual = detector.detect_from_conversation(&conversation); |
|
|
| |
| assert_eq!(actual, Some(3)); |
| } |
|
|
| #[test] |
| fn test_doom_loop_detector_real_world_scenario() { |
| let detector = DoomLoopDetector::new(); |
|
|
| |
| let read_call = ToolCallFull::new("read") |
| .arguments(ToolCallArguments::from_json(r#"{"path": "src/main.rs"}"#)); |
| let diagnostics_call = ToolCallFull::new("mcp_forge_extension_tool_get_diagnostics") |
| .arguments(ToolCallArguments::from_json(r#"{"severity": "error"}"#)); |
| let patch_call = ToolCallFull::new("patch").arguments(ToolCallArguments::from_json( |
| r#"{"path": "src/main.rs", "old": "foo", "new": "bar"}"#, |
| )); |
|
|
| |
| let msg1 = create_assistant_message(&read_call); |
| let msg2 = create_assistant_message(&diagnostics_call); |
| let msg3 = create_assistant_message(&patch_call); |
| let msg4 = create_assistant_message(&read_call); |
| let msg5 = create_assistant_message(&diagnostics_call); |
| let msg6 = create_assistant_message(&patch_call); |
| let msg7 = create_assistant_message(&read_call); |
| let msg8 = create_assistant_message(&diagnostics_call); |
| let msg9 = create_assistant_message(&patch_call); |
|
|
| let conversation = create_conversation_with_messages(vec![ |
| msg1, msg2, msg3, msg4, msg5, msg6, msg7, msg8, msg9, |
| ]); |
|
|
| let actual = detector.detect_from_conversation(&conversation); |
|
|
| |
| assert_eq!(actual, Some(3)); |
| } |
|
|
| #[test] |
| fn test_doom_loop_detector_pattern_changes_midway_123123454545() { |
| let detector = DoomLoopDetector::new(); |
|
|
| let tool_call_1 = ToolCallFull::new("read") |
| .arguments(ToolCallArguments::from_json(r#"{"path": "file1.txt"}"#)); |
| let tool_call_2 = ToolCallFull::new("write") |
| .arguments(ToolCallArguments::from_json(r#"{"path": "file2.txt"}"#)); |
| let tool_call_3 = ToolCallFull::new("patch") |
| .arguments(ToolCallArguments::from_json(r#"{"path": "file3.txt"}"#)); |
| let tool_call_4 = ToolCallFull::new("shell") |
| .arguments(ToolCallArguments::from_json(r#"{"command": "ls"}"#)); |
| let tool_call_5 = ToolCallFull::new("fs_search") |
| .arguments(ToolCallArguments::from_json(r#"{"pattern": "test"}"#)); |
|
|
| |
| |
| let msg1 = create_assistant_message(&tool_call_1); |
| let msg2 = create_assistant_message(&tool_call_2); |
| let msg3 = create_assistant_message(&tool_call_3); |
| let msg4 = create_assistant_message(&tool_call_1); |
| let msg5 = create_assistant_message(&tool_call_2); |
| let msg6 = create_assistant_message(&tool_call_3); |
| let msg7 = create_assistant_message(&tool_call_4); |
| let msg8 = create_assistant_message(&tool_call_5); |
| let msg9 = create_assistant_message(&tool_call_4); |
| let msg10 = create_assistant_message(&tool_call_5); |
| let msg11 = create_assistant_message(&tool_call_4); |
| let msg12 = create_assistant_message(&tool_call_5); |
|
|
| let conversation = create_conversation_with_messages(vec![ |
| msg1, msg2, msg3, msg4, msg5, msg6, msg7, msg8, msg9, msg10, msg11, msg12, |
| ]); |
|
|
| |
| let actual = detector.detect_from_conversation(&conversation); |
|
|
| |
| |
| |
| |
| assert_eq!(actual, Some(3)); |
| } |
|
|
| #[test] |
| fn test_doom_loop_detector_sequence_1234546454545_step_by_step() { |
| let detector = DoomLoopDetector::new(); |
|
|
| |
| let tool_1 = ToolCallFull::new("read") |
| .arguments(ToolCallArguments::from_json(r#"{"path": "file1.txt"}"#)); |
| let tool_2 = ToolCallFull::new("write") |
| .arguments(ToolCallArguments::from_json(r#"{"path": "file2.txt"}"#)); |
| let tool_3 = ToolCallFull::new("patch") |
| .arguments(ToolCallArguments::from_json(r#"{"path": "file3.txt"}"#)); |
| let tool_4 = ToolCallFull::new("shell") |
| .arguments(ToolCallArguments::from_json(r#"{"command": "ls"}"#)); |
| let tool_5 = ToolCallFull::new("fs_search") |
| .arguments(ToolCallArguments::from_json(r#"{"pattern": "test"}"#)); |
| let tool_6 = ToolCallFull::new("sem_search") |
| .arguments(ToolCallArguments::from_json(r#"{"queries": []}"#)); |
|
|
| |
| |
| let mut messages = vec![]; |
|
|
| |
| messages.push(create_assistant_message(&tool_1)); |
| let conv = create_conversation_with_messages(messages.clone()); |
| assert_eq!(detector.detect_from_conversation(&conv), None); |
|
|
| |
| messages.push(create_assistant_message(&tool_2)); |
| let conv = create_conversation_with_messages(messages.clone()); |
| assert_eq!(detector.detect_from_conversation(&conv), None); |
|
|
| |
| messages.push(create_assistant_message(&tool_3)); |
| let conv = create_conversation_with_messages(messages.clone()); |
| assert_eq!(detector.detect_from_conversation(&conv), None); |
|
|
| |
| messages.push(create_assistant_message(&tool_4)); |
| let conv = create_conversation_with_messages(messages.clone()); |
| assert_eq!(detector.detect_from_conversation(&conv), None); |
|
|
| |
| messages.push(create_assistant_message(&tool_5)); |
| let conv = create_conversation_with_messages(messages.clone()); |
| assert_eq!(detector.detect_from_conversation(&conv), None); |
|
|
| |
| messages.push(create_assistant_message(&tool_4)); |
| let conv = create_conversation_with_messages(messages.clone()); |
| assert_eq!(detector.detect_from_conversation(&conv), None); |
|
|
| |
| messages.push(create_assistant_message(&tool_6)); |
| let conv = create_conversation_with_messages(messages.clone()); |
| assert_eq!(detector.detect_from_conversation(&conv), None); |
|
|
| |
| messages.push(create_assistant_message(&tool_4)); |
| let conv = create_conversation_with_messages(messages.clone()); |
| assert_eq!(detector.detect_from_conversation(&conv), None); |
|
|
| |
| messages.push(create_assistant_message(&tool_5)); |
| let conv = create_conversation_with_messages(messages.clone()); |
| assert_eq!(detector.detect_from_conversation(&conv), None); |
|
|
| |
| messages.push(create_assistant_message(&tool_4)); |
| let conv = create_conversation_with_messages(messages.clone()); |
| assert_eq!(detector.detect_from_conversation(&conv), None); |
|
|
| |
| messages.push(create_assistant_message(&tool_5)); |
| let conv = create_conversation_with_messages(messages.clone()); |
| assert_eq!(detector.detect_from_conversation(&conv), None); |
|
|
| |
| messages.push(create_assistant_message(&tool_4)); |
| let conv = create_conversation_with_messages(messages.clone()); |
| assert_eq!(detector.detect_from_conversation(&conv), None); |
|
|
| |
| |
| messages.push(create_assistant_message(&tool_5)); |
| let conv = create_conversation_with_messages(messages.clone()); |
|
|
| let result = detector.detect_from_conversation(&conv); |
|
|
| |
| assert_eq!(result, Some(3)); |
| } |
| } |
|
|