Skip to main content

ai_agent/utils/
session_activity.rs

1// Source: /data/home/swei/claudecode/openclaudecode/src/utils/sessionActivity.ts
2//! Session activity tracking - track and analyze session activity events
3
4use chrono::{DateTime, Utc};
5use serde::{Deserialize, Serialize};
6use std::time::Duration;
7
8/// Types of session activity
9#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize)]
10pub enum ActivityType {
11    Message,
12    ToolUse,
13    Command,
14    FileAccess,
15    SessionStart,
16    SessionEnd,
17    Error,
18    UserInput,
19    Compact,
20    MemoryUpdate,
21}
22
23impl std::fmt::Display for ActivityType {
24    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
25        match self {
26            ActivityType::Message => write!(f, "message"),
27            ActivityType::ToolUse => write!(f, "tool_use"),
28            ActivityType::Command => write!(f, "command"),
29            ActivityType::FileAccess => write!(f, "file_access"),
30            ActivityType::SessionStart => write!(f, "session_start"),
31            ActivityType::SessionEnd => write!(f, "session_end"),
32            ActivityType::Error => write!(f, "error"),
33            ActivityType::UserInput => write!(f, "user_input"),
34            ActivityType::Compact => write!(f, "compact"),
35            ActivityType::MemoryUpdate => write!(f, "memory_update"),
36        }
37    }
38}
39
40/// Session activity event with timestamp
41#[derive(Debug, Clone, Serialize, Deserialize)]
42pub struct SessionActivity {
43    pub activity_type: ActivityType,
44    pub details: Option<String>,
45    pub timestamp: DateTime<Utc>,
46    pub metadata: Option<serde_json::Value>,
47}
48
49impl SessionActivity {
50    pub fn new(activity_type: ActivityType) -> Self {
51        Self {
52            activity_type,
53            details: None,
54            timestamp: Utc::now(),
55            metadata: None,
56        }
57    }
58
59    pub fn with_details(mut self, details: String) -> Self {
60        self.details = Some(details);
61        self
62    }
63
64    pub fn with_metadata(mut self, metadata: serde_json::Value) -> Self {
65        self.metadata = Some(metadata);
66        self
67    }
68
69    pub fn with_timestamp(mut self, timestamp: DateTime<Utc>) -> Self {
70        self.timestamp = timestamp;
71        self
72    }
73
74    /// Get the age of this activity
75    pub fn age(&self) -> Duration {
76        let now = Utc::now();
77        (now - self.timestamp).to_std().unwrap_or(Duration::ZERO)
78    }
79}
80
81/// Track session activities with time-based filtering and statistics
82pub struct SessionActivityTracker {
83    activities: Vec<SessionActivity>,
84    max_capacity: usize,
85}
86
87impl SessionActivityTracker {
88    pub fn new() -> Self {
89        Self {
90            activities: Vec::new(),
91            max_capacity: 10_000,
92        }
93    }
94
95    /// Create a tracker with a custom max capacity
96    pub fn with_capacity(max_capacity: usize) -> Self {
97        Self {
98            activities: Vec::new(),
99            max_capacity,
100        }
101    }
102
103    /// Record an activity
104    pub fn record(&mut self, activity: SessionActivity) {
105        // Evict oldest entries if at capacity
106        if self.activities.len() >= self.max_capacity {
107            let remove_count = self.max_capacity / 4; // Remove 25% when full
108            self.activities.drain(..remove_count);
109        }
110        self.activities.push(activity);
111    }
112
113    /// Record an activity by type
114    pub fn record_type(&mut self, activity_type: ActivityType) {
115        self.record(SessionActivity::new(activity_type));
116    }
117
118    /// Record an activity by type with details
119    pub fn record_type_with_details(&mut self, activity_type: ActivityType, details: String) {
120        self.record(SessionActivity::new(activity_type).with_details(details));
121    }
122
123    /// Get all activities
124    pub fn get_activities(&self) -> &[SessionActivity] {
125        &self.activities
126    }
127
128    /// Get activities within a duration from now
129    pub fn get_recent(&self, duration: Duration) -> Vec<&SessionActivity> {
130        let now = Utc::now();
131        let cutoff = now - chrono::Duration::from_std(duration).unwrap_or(chrono::Duration::zero());
132
133        self.activities
134            .iter()
135            .filter(|a| a.timestamp >= cutoff)
136            .collect()
137    }
138
139    /// Get the count of recent activities within a duration
140    pub fn get_recent_count(&self, duration: Duration) -> usize {
141        self.get_recent(duration).len()
142    }
143
144    /// Get activities of a specific type
145    pub fn get_activities_by_type(&self, activity_type: ActivityType) -> Vec<&SessionActivity> {
146        self.activities
147            .iter()
148            .filter(|a| a.activity_type == activity_type)
149            .collect()
150    }
151
152    /// Get the count of activities by type
153    pub fn count_by_type(&self, activity_type: ActivityType) -> usize {
154        self.get_activities_by_type(activity_type).len()
155    }
156
157    /// Get the last activity
158    pub fn get_last_activity(&self) -> Option<&SessionActivity> {
159        self.activities.last()
160    }
161
162    /// Get the last activity of a specific type
163    pub fn get_last_activity_of_type(
164        &self,
165        activity_type: ActivityType,
166    ) -> Option<&SessionActivity> {
167        self.activities
168            .iter()
169            .rev()
170            .find(|a| a.activity_type == activity_type)
171    }
172
173    /// Get the time since the last activity
174    pub fn time_since_last_activity(&self) -> Option<Duration> {
175        self.activities.last().map(|a| a.age())
176    }
177
178    /// Get the time since the last activity of a specific type
179    pub fn time_since_last_activity_of_type(
180        &self,
181        activity_type: ActivityType,
182    ) -> Option<Duration> {
183        self.get_last_activity_of_type(activity_type)
184            .map(|a| a.age())
185    }
186
187    /// Get activity rate (activities per second) over a duration
188    pub fn get_activity_rate(&self, duration: Duration) -> f64 {
189        let count = self.get_recent_count(duration);
190        let secs = duration.as_secs_f64();
191        if secs > 0.0 { count as f64 / secs } else { 0.0 }
192    }
193
194    /// Check if there has been any activity within a duration
195    pub fn has_recent_activity(&self, duration: Duration) -> bool {
196        self.get_recent_count(duration) > 0
197    }
198
199    /// Get total activity count
200    pub fn total_count(&self) -> usize {
201        self.activities.len()
202    }
203
204    /// Clear all activities
205    pub fn clear(&mut self) {
206        self.activities.clear();
207    }
208
209    /// Export activities for serialization
210    pub fn export_activities(&self) -> Vec<SessionActivity> {
211        self.activities.clone()
212    }
213
214    /// Import activities from serialized data
215    pub fn import_activities(&mut self, activities: Vec<SessionActivity>) {
216        self.activities = activities;
217    }
218
219    /// Get a summary of activity counts by type
220    pub fn get_activity_summary(&self) -> serde_json::Value {
221        let mut summary = serde_json::Map::new();
222        for activity_type in &[
223            ActivityType::Message,
224            ActivityType::ToolUse,
225            ActivityType::Command,
226            ActivityType::FileAccess,
227            ActivityType::SessionStart,
228            ActivityType::SessionEnd,
229            ActivityType::Error,
230            ActivityType::UserInput,
231            ActivityType::Compact,
232            ActivityType::MemoryUpdate,
233        ] {
234            let count = self.count_by_type(*activity_type);
235            summary.insert(
236                activity_type.to_string(),
237                serde_json::Value::Number(count.into()),
238            );
239        }
240        serde_json::Value::Object(summary)
241    }
242}
243
244impl Default for SessionActivityTracker {
245    fn default() -> Self {
246        Self::new()
247    }
248}
249
250#[cfg(test)]
251mod tests {
252    use super::*;
253
254    #[test]
255    fn test_new_tracker() {
256        let tracker = SessionActivityTracker::new();
257        assert_eq!(tracker.total_count(), 0);
258    }
259
260    #[test]
261    fn test_record_activity() {
262        let mut tracker = SessionActivityTracker::new();
263        tracker.record_type(ActivityType::Message);
264        assert_eq!(tracker.total_count(), 1);
265    }
266
267    #[test]
268    fn test_record_with_details() {
269        let mut tracker = SessionActivityTracker::new();
270        tracker.record_type_with_details(ActivityType::ToolUse, "ReadFile".to_string());
271        assert_eq!(tracker.count_by_type(ActivityType::ToolUse), 1);
272    }
273
274    #[test]
275    fn test_get_recent_count() {
276        let mut tracker = SessionActivityTracker::new();
277        tracker.record_type(ActivityType::Message);
278        // Activity just recorded, so it's recent
279        assert!(tracker.get_recent_count(Duration::from_secs(60)) > 0);
280    }
281
282    #[test]
283    fn test_clear() {
284        let mut tracker = SessionActivityTracker::new();
285        tracker.record_type(ActivityType::Message);
286        tracker.record_type(ActivityType::ToolUse);
287        tracker.clear();
288        assert_eq!(tracker.total_count(), 0);
289    }
290
291    #[test]
292    fn test_activity_type_display() {
293        assert_eq!(ActivityType::Message.to_string(), "message");
294        assert_eq!(ActivityType::ToolUse.to_string(), "tool_use");
295        assert_eq!(ActivityType::Command.to_string(), "command");
296    }
297
298    #[test]
299    fn test_last_activity() {
300        let mut tracker = SessionActivityTracker::new();
301        assert!(tracker.get_last_activity().is_none());
302
303        tracker.record_type(ActivityType::Message);
304        assert!(tracker.get_last_activity().is_some());
305    }
306
307    #[test]
308    fn test_time_since_last_activity() {
309        let mut tracker = SessionActivityTracker::new();
310        assert!(tracker.time_since_last_activity().is_none());
311
312        tracker.record_type(ActivityType::Message);
313        assert!(tracker.time_since_last_activity().is_some());
314    }
315
316    #[test]
317    fn test_activity_summary() {
318        let mut tracker = SessionActivityTracker::new();
319        tracker.record_type(ActivityType::Message);
320        tracker.record_type(ActivityType::Message);
321        tracker.record_type(ActivityType::ToolUse);
322
323        let summary = tracker.get_activity_summary();
324        assert!(summary.is_object());
325    }
326
327    #[test]
328    fn test_activity_rate() {
329        let mut tracker = SessionActivityTracker::new();
330        for _ in 0..5 {
331            tracker.record_type(ActivityType::Message);
332        }
333        let rate = tracker.get_activity_rate(Duration::from_secs(60));
334        assert!(rate > 0.0);
335    }
336
337    #[test]
338    fn test_has_recent_activity() {
339        let mut tracker = SessionActivityTracker::new();
340        assert!(!tracker.has_recent_activity(Duration::from_secs(60)));
341
342        tracker.record_type(ActivityType::Message);
343        assert!(tracker.has_recent_activity(Duration::from_secs(60)));
344    }
345
346    #[test]
347    fn test_export_import() {
348        let mut tracker = SessionActivityTracker::new();
349        tracker.record_type(ActivityType::Message);
350        tracker.record_type(ActivityType::ToolUse);
351
352        let exported = tracker.export_activities();
353        let mut tracker2 = SessionActivityTracker::new();
354        tracker2.import_activities(exported);
355
356        assert_eq!(tracker2.total_count(), 2);
357    }
358}