codeprism_mcp/context/
session.rs

1//! Session management for MCP tools
2//!
3//! Provides session state tracking, analysis history, and workflow stage detection
4//! to enable intelligent tool guidance and reduce redundant analysis.
5
6use anyhow::Result;
7use serde::{Deserialize, Serialize};
8use std::collections::HashMap;
9use std::sync::{Arc, RwLock};
10use std::time::{Duration, SystemTime, UNIX_EPOCH};
11
12/// Unique identifier for a session
13#[derive(Debug, Clone, PartialEq, Eq, Hash, Serialize, Deserialize)]
14pub struct SessionId(pub String);
15
16impl SessionId {
17    /// Generate a new unique session ID
18    pub fn new() -> Self {
19        let timestamp = SystemTime::now()
20            .duration_since(UNIX_EPOCH)
21            .unwrap_or(Duration::from_secs(0))
22            .as_millis();
23        let random: u32 = rand::random();
24        Self(format!("session_{}_{}", timestamp, random))
25    }
26}
27
28impl Default for SessionId {
29    fn default() -> Self {
30        Self::new()
31    }
32}
33
34/// Current workflow stage of analysis
35#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
36pub enum WorkflowStage {
37    /// Initial exploration and discovery
38    Discovery,
39    /// Understanding relationships and structure
40    Mapping,
41    /// Detailed analysis of specific areas
42    DeepDive,
43    /// Putting findings together
44    Synthesis,
45}
46
47impl WorkflowStage {
48    /// Get appropriate tools for this stage
49    pub fn recommended_tools(&self) -> Vec<&'static str> {
50        match self {
51            WorkflowStage::Discovery => vec![
52                "repository_stats",
53                "search_content",
54                "find_files",
55                "content_stats",
56            ],
57            WorkflowStage::Mapping => vec![
58                "search_symbols",
59                "find_dependencies",
60                "detect_patterns",
61                "trace_path",
62            ],
63            WorkflowStage::DeepDive => vec![
64                "explain_symbol",
65                "trace_inheritance",
66                "analyze_decorators",
67                "find_references",
68            ],
69            WorkflowStage::Synthesis => vec!["analyze_complexity"],
70        }
71    }
72
73    /// Get next logical stage
74    pub fn next_stage(&self) -> Option<WorkflowStage> {
75        match self {
76            WorkflowStage::Discovery => Some(WorkflowStage::Mapping),
77            WorkflowStage::Mapping => Some(WorkflowStage::DeepDive),
78            WorkflowStage::DeepDive => Some(WorkflowStage::Synthesis),
79            WorkflowStage::Synthesis => None,
80        }
81    }
82}
83
84/// Analysis operation record
85#[derive(Debug, Clone, Serialize, Deserialize)]
86pub struct AnalysisRecord {
87    /// Tool that was called
88    pub tool_name: String,
89    /// Parameters used
90    pub parameters: serde_json::Value,
91    /// Timestamp of the analysis
92    pub timestamp: u64,
93    /// Success or failure
94    pub success: bool,
95    /// Summary of results
96    pub result_summary: Option<String>,
97    /// Symbols analyzed (if applicable)
98    pub symbols_analyzed: Vec<String>,
99}
100
101impl AnalysisRecord {
102    /// Create a new analysis record
103    pub fn new(
104        tool_name: String,
105        parameters: serde_json::Value,
106        success: bool,
107        result_summary: Option<String>,
108        symbols_analyzed: Vec<String>,
109    ) -> Self {
110        let timestamp = SystemTime::now()
111            .duration_since(UNIX_EPOCH)
112            .unwrap_or(Duration::from_secs(0))
113            .as_secs();
114
115        Self {
116            tool_name,
117            parameters,
118            timestamp,
119            success,
120            result_summary,
121            symbols_analyzed,
122        }
123    }
124}
125
126/// History of analysis operations in a session
127#[derive(Debug, Clone, Default, Serialize, Deserialize)]
128pub struct AnalysisHistory {
129    /// All analysis records in chronological order
130    pub records: Vec<AnalysisRecord>,
131    /// Symbols that have been analyzed
132    pub analyzed_symbols: std::collections::HashSet<String>,
133    /// Patterns discovered
134    pub discovered_patterns: Vec<String>,
135    /// Current focus areas
136    pub focus_areas: Vec<String>,
137}
138
139impl AnalysisHistory {
140    /// Add a new analysis record
141    pub fn add_record(&mut self, record: AnalysisRecord) {
142        // Update analyzed symbols
143        for symbol in &record.symbols_analyzed {
144            self.analyzed_symbols.insert(symbol.clone());
145        }
146
147        self.records.push(record);
148    }
149
150    /// Check if a symbol has been analyzed recently
151    pub fn was_recently_analyzed(&self, symbol: &str, within_minutes: u64) -> bool {
152        let cutoff_time = SystemTime::now()
153            .duration_since(UNIX_EPOCH)
154            .unwrap_or(Duration::from_secs(0))
155            .as_secs()
156            - (within_minutes * 60);
157
158        self.records.iter().any(|record| {
159            record.timestamp > cutoff_time && record.symbols_analyzed.contains(&symbol.to_string())
160        })
161    }
162
163    /// Get recent tools used
164    pub fn recent_tools(&self, within_minutes: u64) -> Vec<String> {
165        let cutoff_time = SystemTime::now()
166            .duration_since(UNIX_EPOCH)
167            .unwrap_or(Duration::from_secs(0))
168            .as_secs()
169            - (within_minutes * 60);
170
171        self.records
172            .iter()
173            .filter(|record| record.timestamp > cutoff_time)
174            .map(|record| record.tool_name.clone())
175            .collect::<std::collections::HashSet<_>>()
176            .into_iter()
177            .collect()
178    }
179
180    /// Detect current workflow stage based on recent activity
181    pub fn detect_workflow_stage(&self) -> WorkflowStage {
182        let recent_tools = self.recent_tools(30); // Last 30 minutes
183
184        // Count tools by category
185        let discovery_tools = [
186            "repository_stats",
187            "search_content",
188            "find_files",
189            "content_stats",
190        ];
191        let mapping_tools = [
192            "search_symbols",
193            "find_dependencies",
194            "detect_patterns",
195            "trace_path",
196        ];
197        let deepdive_tools = [
198            "explain_symbol",
199            "trace_inheritance",
200            "analyze_decorators",
201            "find_references",
202        ];
203        let synthesis_tools = ["analyze_complexity"];
204
205        let discovery_count = recent_tools
206            .iter()
207            .filter(|t| discovery_tools.contains(&t.as_str()))
208            .count();
209        let mapping_count = recent_tools
210            .iter()
211            .filter(|t| mapping_tools.contains(&t.as_str()))
212            .count();
213        let deepdive_count = recent_tools
214            .iter()
215            .filter(|t| deepdive_tools.contains(&t.as_str()))
216            .count();
217        let synthesis_count = recent_tools
218            .iter()
219            .filter(|t| synthesis_tools.contains(&t.as_str()))
220            .count();
221
222        // Determine stage based on dominant activity
223        if synthesis_count > 0 {
224            WorkflowStage::Synthesis
225        } else if deepdive_count > mapping_count && deepdive_count > discovery_count {
226            WorkflowStage::DeepDive
227        } else if mapping_count > discovery_count {
228            WorkflowStage::Mapping
229        } else {
230            WorkflowStage::Discovery
231        }
232    }
233}
234
235/// Current state of a session
236#[derive(Debug, Clone, Serialize, Deserialize)]
237pub struct SessionState {
238    /// Unique session identifier
239    pub id: SessionId,
240    /// When the session was created
241    pub created_at: u64,
242    /// Last activity timestamp
243    pub last_activity: u64,
244    /// Analysis history
245    pub history: AnalysisHistory,
246    /// Current workflow stage
247    pub current_stage: WorkflowStage,
248    /// Session metadata
249    pub metadata: HashMap<String, serde_json::Value>,
250}
251
252impl SessionState {
253    /// Create a new session state
254    pub fn new() -> Self {
255        let now = SystemTime::now()
256            .duration_since(UNIX_EPOCH)
257            .unwrap_or(Duration::from_secs(0))
258            .as_secs();
259
260        Self {
261            id: SessionId::new(),
262            created_at: now,
263            last_activity: now,
264            history: AnalysisHistory::default(),
265            current_stage: WorkflowStage::Discovery,
266            metadata: HashMap::new(),
267        }
268    }
269
270    /// Update last activity timestamp
271    pub fn touch(&mut self) {
272        self.last_activity = SystemTime::now()
273            .duration_since(UNIX_EPOCH)
274            .unwrap_or(Duration::from_secs(0))
275            .as_secs();
276
277        // Update workflow stage based on recent activity
278        self.current_stage = self.history.detect_workflow_stage();
279    }
280
281    /// Add an analysis record and update state
282    pub fn record_analysis(
283        &mut self,
284        tool_name: String,
285        parameters: serde_json::Value,
286        success: bool,
287        result_summary: Option<String>,
288        symbols_analyzed: Vec<String>,
289    ) {
290        let record = AnalysisRecord::new(
291            tool_name,
292            parameters,
293            success,
294            result_summary,
295            symbols_analyzed,
296        );
297
298        self.history.add_record(record);
299        self.touch();
300    }
301
302    /// Check if session is expired (inactive for over 1 hour)
303    pub fn is_expired(&self) -> bool {
304        let now = SystemTime::now()
305            .duration_since(UNIX_EPOCH)
306            .unwrap_or(Duration::from_secs(0))
307            .as_secs();
308
309        now - self.last_activity > 3600 // 1 hour
310    }
311}
312
313impl Default for SessionState {
314    fn default() -> Self {
315        Self::new()
316    }
317}
318
319/// Manages multiple sessions and provides session lifecycle management
320#[derive(Debug)]
321pub struct SessionManager {
322    /// Active sessions
323    sessions: Arc<RwLock<HashMap<SessionId, SessionState>>>,
324}
325
326impl SessionManager {
327    /// Create a new session manager
328    pub fn new() -> Self {
329        Self {
330            sessions: Arc::new(RwLock::new(HashMap::new())),
331        }
332    }
333
334    /// Create a new session
335    pub fn create_session(&self) -> Result<SessionId> {
336        let session = SessionState::new();
337        let session_id = session.id.clone();
338
339        let mut sessions = self
340            .sessions
341            .write()
342            .map_err(|_| anyhow::anyhow!("Failed to acquire write lock on sessions"))?;
343
344        sessions.insert(session_id.clone(), session);
345        Ok(session_id)
346    }
347
348    /// Get a session by ID (create if not exists)
349    pub fn get_or_create_session(&self, session_id: Option<SessionId>) -> Result<SessionId> {
350        match session_id {
351            Some(id) => {
352                let sessions = self
353                    .sessions
354                    .read()
355                    .map_err(|_| anyhow::anyhow!("Failed to acquire read lock on sessions"))?;
356
357                if sessions.contains_key(&id) {
358                    Ok(id)
359                } else {
360                    drop(sessions);
361                    self.create_session()
362                }
363            }
364            None => self.create_session(),
365        }
366    }
367
368    /// Get session state (readonly)
369    pub fn get_session(&self, session_id: &SessionId) -> Result<Option<SessionState>> {
370        let sessions = self
371            .sessions
372            .read()
373            .map_err(|_| anyhow::anyhow!("Failed to acquire read lock on sessions"))?;
374
375        Ok(sessions.get(session_id).cloned())
376    }
377
378    /// Update session with analysis record
379    pub fn record_analysis(
380        &self,
381        session_id: &SessionId,
382        tool_name: String,
383        parameters: serde_json::Value,
384        success: bool,
385        result_summary: Option<String>,
386        symbols_analyzed: Vec<String>,
387    ) -> Result<()> {
388        let mut sessions = self
389            .sessions
390            .write()
391            .map_err(|_| anyhow::anyhow!("Failed to acquire write lock on sessions"))?;
392
393        if let Some(session) = sessions.get_mut(session_id) {
394            session.record_analysis(
395                tool_name,
396                parameters,
397                success,
398                result_summary,
399                symbols_analyzed,
400            );
401        }
402
403        Ok(())
404    }
405
406    /// Clean up expired sessions
407    pub fn cleanup_expired_sessions(&self) -> Result<usize> {
408        let mut sessions = self
409            .sessions
410            .write()
411            .map_err(|_| anyhow::anyhow!("Failed to acquire write lock on sessions"))?;
412
413        let initial_count = sessions.len();
414        sessions.retain(|_, session| !session.is_expired());
415
416        Ok(initial_count - sessions.len())
417    }
418
419    /// Get active session count
420    pub fn active_session_count(&self) -> Result<usize> {
421        let sessions = self
422            .sessions
423            .read()
424            .map_err(|_| anyhow::anyhow!("Failed to acquire read lock on sessions"))?;
425
426        Ok(sessions.len())
427    }
428}
429
430impl Default for SessionManager {
431    fn default() -> Self {
432        Self::new()
433    }
434}
435
436#[cfg(test)]
437mod tests {
438    use super::*;
439
440    #[test]
441    fn test_session_id_generation() {
442        let id1 = SessionId::new();
443        let id2 = SessionId::new();
444        assert_ne!(id1, id2);
445        assert!(id1.0.starts_with("session_"));
446    }
447
448    #[test]
449    fn test_workflow_stage_progression() {
450        assert_eq!(
451            WorkflowStage::Discovery.next_stage(),
452            Some(WorkflowStage::Mapping)
453        );
454        assert_eq!(
455            WorkflowStage::Mapping.next_stage(),
456            Some(WorkflowStage::DeepDive)
457        );
458        assert_eq!(
459            WorkflowStage::DeepDive.next_stage(),
460            Some(WorkflowStage::Synthesis)
461        );
462        assert_eq!(WorkflowStage::Synthesis.next_stage(), None);
463    }
464
465    #[test]
466    fn test_workflow_stage_tools() {
467        let discovery_tools = WorkflowStage::Discovery.recommended_tools();
468        assert!(discovery_tools.contains(&"repository_stats"));
469        assert!(discovery_tools.contains(&"search_content"));
470    }
471
472    #[test]
473    fn test_analysis_history() {
474        let mut history = AnalysisHistory::default();
475
476        let record = AnalysisRecord::new(
477            "explain_symbol".to_string(),
478            serde_json::json!({"symbol_id": "test123"}),
479            true,
480            Some("Symbol explained successfully".to_string()),
481            vec!["test123".to_string()],
482        );
483
484        history.add_record(record);
485        assert_eq!(history.records.len(), 1);
486        assert!(history.analyzed_symbols.contains("test123"));
487    }
488
489    #[test]
490    fn test_session_state_creation() {
491        let session = SessionState::new();
492        assert_eq!(session.current_stage, WorkflowStage::Discovery);
493        assert!(session.history.records.is_empty());
494    }
495
496    #[test]
497    fn test_session_manager() {
498        let manager = SessionManager::new();
499
500        // Create a session
501        let session_id = manager.create_session().unwrap();
502
503        // Verify it exists
504        let session = manager.get_session(&session_id).unwrap();
505        assert!(session.is_some());
506
507        // Record an analysis
508        manager
509            .record_analysis(
510                &session_id,
511                "test_tool".to_string(),
512                serde_json::json!({}),
513                true,
514                None,
515                vec![],
516            )
517            .unwrap();
518
519        // Verify the record was added
520        let updated_session = manager.get_session(&session_id).unwrap().unwrap();
521        assert_eq!(updated_session.history.records.len(), 1);
522    }
523
524    #[test]
525    fn test_workflow_stage_detection() {
526        let mut history = AnalysisHistory::default();
527
528        // Add some mapping stage tools
529        for tool in ["search_symbols", "find_dependencies"] {
530            history.add_record(AnalysisRecord::new(
531                tool.to_string(),
532                serde_json::json!({}),
533                true,
534                None,
535                vec![],
536            ));
537        }
538
539        let stage = history.detect_workflow_stage();
540        assert_eq!(stage, WorkflowStage::Mapping);
541    }
542}