Skip to main content

matrixcode_core/compress/
phase_detector.rs

1//! Conversation phase detection for dynamic weight adjustment.
2//!
3//! Analyzes message history to determine the current conversation phase,
4//! which affects scoring weights during compression.
5
6use crate::providers::{ContentBlock, Message, MessageContent, Role};
7
8use super::types::ConversationPhase;
9
10/// Detector for conversation phase.
11pub struct PhaseDetector;
12
13impl PhaseDetector {
14    /// Detect the current conversation phase from message history.
15    pub fn detect(messages: &[Message]) -> ConversationPhase {
16        // Few messages means initial request phase
17        if messages.len() <= 3 {
18            return ConversationPhase::InitialRequest;
19        }
20
21        // Analyze recent messages (last 10)
22        let recent_start = messages.len().saturating_sub(10);
23        let recent = &messages[recent_start..];
24
25        // Check for tool activity
26        let has_tools = recent.iter().any(has_tool_use);
27
28        if has_tools {
29            // Check for finalizing signals
30            if has_finalizing_signals(recent) {
31                return ConversationPhase::Finalizing;
32            }
33            return ConversationPhase::ActiveDevelopment;
34        }
35
36        // Default to initial request if no tools
37        ConversationPhase::InitialRequest
38    }
39
40    /// Detect phase with custom window size.
41    pub fn detect_with_window(messages: &[Message], window_size: usize) -> ConversationPhase {
42        if messages.len() <= 3 {
43            return ConversationPhase::InitialRequest;
44        }
45
46        let recent_start = messages.len().saturating_sub(window_size);
47        let recent = &messages[recent_start..];
48
49        let has_tools = recent.iter().any(has_tool_use);
50
51        if has_tools {
52            if has_finalizing_signals(recent) {
53                return ConversationPhase::Finalizing;
54            }
55            return ConversationPhase::ActiveDevelopment;
56        }
57
58        ConversationPhase::InitialRequest
59    }
60}
61
62/// Check if a message contains ToolUse block.
63fn has_tool_use(message: &Message) -> bool {
64    match &message.content {
65        MessageContent::Blocks(blocks) => blocks
66            .iter()
67            .any(|b| matches!(b, ContentBlock::ToolUse { .. })),
68        _ => false,
69    }
70}
71
72/// Check for signals indicating task is nearing completion.
73fn has_finalizing_signals(messages: &[Message]) -> bool {
74    for msg in messages {
75        // Check for ask tool (user decisions)
76        if has_ask_tool(msg) {
77            return true;
78        }
79
80        // Check for todo completion signals
81        if has_todo_completion(msg) {
82            return true;
83        }
84
85        // Check for user confirmation patterns
86        if has_user_confirmation(msg) {
87            return true;
88        }
89    }
90    false
91}
92
93/// Check if message contains ask tool.
94fn has_ask_tool(message: &Message) -> bool {
95    match &message.content {
96        MessageContent::Blocks(blocks) => blocks.iter().any(|b| {
97            if let ContentBlock::ToolUse { name, .. } = b {
98                name == "ask"
99            } else {
100                false
101            }
102        }),
103        MessageContent::Text(text) => text.contains("AskUserQuestion"),
104    }
105}
106
107/// Check if message indicates todo completion.
108fn has_todo_completion(message: &Message) -> bool {
109    match &message.content {
110        MessageContent::Blocks(blocks) => blocks.iter().any(|b| match b {
111            ContentBlock::ToolUse { name, .. } => name == "todo_write",
112            ContentBlock::ToolResult { content, .. } => {
113                content.contains("completed") || content.contains("done")
114            }
115            _ => false,
116        }),
117        MessageContent::Text(text) => {
118            text.contains("任务完成")
119                || text.contains("task completed")
120                || text.contains("all done")
121        }
122    }
123}
124
125/// Check if message contains user confirmation.
126fn has_user_confirmation(message: &Message) -> bool {
127    if message.role != Role::User {
128        return false;
129    }
130
131    match &message.content {
132        MessageContent::Text(text) => {
133            let lower = text.to_lowercase();
134            lower.contains("好的")
135                || lower.contains("可以")
136                || lower.contains("继续")
137                || lower.contains("yes")
138                || lower.contains("ok")
139                || lower.contains("confirm")
140                || lower.contains("done")
141        }
142        _ => false,
143    }
144}
145
146#[cfg(test)]
147mod tests {
148    use super::*;
149
150    #[test]
151    fn test_detect_initial_request() {
152        let messages = vec![Message {
153            role: Role::User,
154            content: MessageContent::Text("Hello".to_string()),
155        }];
156        assert_eq!(
157            PhaseDetector::detect(&messages),
158            ConversationPhase::InitialRequest
159        );
160    }
161
162    #[test]
163    fn test_detect_active_development() {
164        let messages = vec![
165            Message {
166                role: Role::User,
167                content: MessageContent::Text("Read file".to_string()),
168            },
169            Message {
170                role: Role::Assistant,
171                content: MessageContent::Blocks(vec![ContentBlock::ToolUse {
172                    id: "t1".to_string(),
173                    name: "read".to_string(),
174                    input: serde_json::json!({"path": "test.rs"}),
175                }]),
176            },
177            Message {
178                role: Role::Tool,
179                content: MessageContent::Blocks(vec![ContentBlock::ToolResult {
180                    tool_use_id: "t1".to_string(),
181                    content: "file content".to_string(),
182                }]),
183            },
184        ];
185        assert_eq!(
186            PhaseDetector::detect(&messages),
187            ConversationPhase::InitialRequest
188        );
189    }
190
191    #[test]
192    fn test_detect_finalizing() {
193        let messages = vec![
194            Message {
195                role: Role::User,
196                content: MessageContent::Text("Start task".to_string()),
197            },
198            Message {
199                role: Role::Assistant,
200                content: MessageContent::Blocks(vec![ContentBlock::ToolUse {
201                    id: "t1".to_string(),
202                    name: "read".to_string(),
203                    input: serde_json::json!({"path": "test.rs"}),
204                }]),
205            },
206            Message {
207                role: Role::Assistant,
208                content: MessageContent::Blocks(vec![ContentBlock::ToolUse {
209                    id: "t2".to_string(),
210                    name: "ask".to_string(),
211                    input: serde_json::json!({"question": "Confirm?"}),
212                }]),
213            },
214        ];
215        // With 3 messages, still InitialRequest
216        assert_eq!(
217            PhaseDetector::detect(&messages),
218            ConversationPhase::InitialRequest
219        );
220    }
221}