Skip to main content

st/context_gatherer/
collab_session.rs

1//! Collaborative Session Detection and Analysis
2//!
3//! This module detects and analyzes collaborative sessions between AI and humans,
4//! tracking engagement patterns, flow states, and building relationship memory.
5//!
6//! "You're easy to work with. You just get me." - The goal we're helping AI achieve.
7
8use anyhow::Result;
9use chrono::{DateTime, Datelike, Duration, Timelike, Utc};
10use serde::{Deserialize, Serialize};
11use std::collections::{HashMap, VecDeque};
12
13use super::{ContextContent, GatheredContext};
14use crate::mem8::wave::{FrequencyBand, MemoryWave};
15
16/// Collaborative session types
17#[derive(Debug, Clone, PartialEq, Eq, Hash, Serialize, Deserialize)]
18pub enum SessionType {
19    /// AI working alone
20    SoloAI,
21    /// Human working alone
22    SoloHuman,
23    /// Both actively engaged (the magic zone!)
24    Tandem,
25    /// Transitioning between states
26    Transitional,
27}
28
29/// Flow state indicators
30#[derive(Debug, Clone, Serialize, Deserialize)]
31pub enum FlowState {
32    /// Smooth back-and-forth, high engagement
33    Flow { depth: f32, sustained_minutes: u32 },
34    /// Getting stuck, confusion markers
35    Whirlpool {
36        confusion_score: f32,
37        repeated_concepts: Vec<String>,
38    },
39    /// Tangential branches, exploring new territory
40    Fork {
41        branch_count: usize,
42        topics: Vec<String>,
43    },
44    /// Normal working state
45    Steady,
46    /// Interruptions or context switches
47    Interrupted { reason: String },
48}
49
50/// Collaborative memory anchor
51#[derive(Debug, Clone, Serialize, Deserialize)]
52pub struct MemoryAnchor {
53    pub id: String,
54    pub origin: CollaborativeOrigin,
55    pub anchor_type: AnchorType,
56    pub context: String,
57    pub keywords: Vec<String>,
58    pub timestamp: DateTime<Utc>,
59    pub relevance_wave: MemoryWave,
60    pub co_created: bool,
61}
62
63#[derive(Debug, Clone, Serialize, Deserialize)]
64pub enum CollaborativeOrigin {
65    /// Single entity created
66    Single(String),
67    /// Co-created by both parties
68    Tandem { human: String, ai: String },
69    /// Emerged from conversation
70    Emergent,
71}
72
73#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
74pub enum AnchorType {
75    PatternInsight,
76    Solution,
77    Breakthrough,
78    LearningMoment,
79    SharedJoke, // Because humor matters in partnerships!
80    TechnicalPattern,
81    ProcessImprovement,
82}
83
84/// Trust and flow score for each session
85#[derive(Debug, Clone, Serialize, Deserialize)]
86pub struct TrustFlowScore {
87    pub clarity: f32,             // 0.0-1.0: How clear is communication
88    pub responsiveness: f32,      // 0.0-1.0: Time between suggestion and action
89    pub autonomy_ratio: f32,      // 0.0-1.0: Balance of AI-led vs Human-led
90    pub frustration_markers: f32, // 0.0-1.0: Detected confusion/repetition
91    pub flow_depth: f32,          // 0.0-1.0: Depth of collaborative flow
92}
93
94/// Rapport index that evolves over time
95#[derive(Debug, Clone, Serialize, Deserialize)]
96pub struct RapportIndex {
97    pub overall_score: f32,
98    pub trust_level: f32,
99    pub communication_efficiency: f32,
100    pub shared_vocabulary_size: usize,
101    pub inside_jokes_count: usize, // The Cheet would approve!
102    pub preferred_working_hours: Vec<u32>,
103    pub avg_session_productivity: f32,
104    pub evolution_trend: f32, // Positive = improving, negative = declining
105}
106
107/// Collaborative session tracker
108pub struct CollaborativeSessionTracker {
109    pub active_session: Option<CollaborativeSession>,
110    pub session_history: VecDeque<CollaborativeSession>,
111    pub memory_anchors: HashMap<String, MemoryAnchor>,
112    pub rapport_indices: HashMap<String, RapportIndex>, // Per AI-human pair
113    pub cross_session_links: HashMap<String, Vec<String>>,
114}
115
116impl Default for CollaborativeSessionTracker {
117    fn default() -> Self {
118        Self::new()
119    }
120}
121
122impl CollaborativeSessionTracker {
123    pub fn new() -> Self {
124        Self {
125            active_session: None,
126            session_history: VecDeque::with_capacity(1000),
127            memory_anchors: HashMap::new(),
128            rapport_indices: HashMap::new(),
129            cross_session_links: HashMap::new(),
130        }
131    }
132
133    /// Process a new context and update session state
134    pub fn process_context(&mut self, context: &GatheredContext) -> Result<()> {
135        // Detect session type from context
136        let session_type = self.detect_session_type(context);
137        let timestamp = context.timestamp;
138
139        // Check if we need to start a new session
140        let should_start_new = if let Some(session) = &self.active_session {
141            Self::should_start_new_session_static(session, &timestamp, &session_type)
142        } else {
143            true
144        };
145
146        if should_start_new {
147            // End current session and start new one
148            if self.active_session.is_some() {
149                self.end_active_session();
150            }
151            self.start_new_session(context, session_type);
152        } else {
153            // Update current session
154            if let Some(session) = &mut self.active_session {
155                session.update(context, session_type);
156            }
157        }
158
159        // Check for flow state changes
160        if let Some(session) = &self.active_session {
161            let flow_state = self.analyze_flow_state(session);
162            if let Some(session) = &mut self.active_session {
163                session.flow_state = flow_state;
164            }
165        }
166
167        Ok(())
168    }
169
170    /// Detect session type from context
171    fn detect_session_type(&self, context: &GatheredContext) -> SessionType {
172        // Analyze context to determine if it's AI-only, human-only, or tandem
173        match &context.content {
174            ContextContent::Json(json) => {
175                if let Some(messages) = json.get("messages").and_then(|m| m.as_array()) {
176                    // Check message patterns
177                    let recent_messages = messages.iter().rev().take(5).collect::<Vec<_>>();
178                    let has_user = recent_messages
179                        .iter()
180                        .any(|m| m.get("role").and_then(|r| r.as_str()) == Some("user"));
181                    let has_assistant = recent_messages
182                        .iter()
183                        .any(|m| m.get("role").and_then(|r| r.as_str()) == Some("assistant"));
184
185                    if has_user && has_assistant {
186                        SessionType::Tandem
187                    } else if has_assistant {
188                        SessionType::SoloAI
189                    } else {
190                        SessionType::SoloHuman
191                    }
192                } else {
193                    SessionType::SoloAI
194                }
195            }
196            _ => SessionType::SoloHuman,
197        }
198    }
199
200    /// Check if we should start a new session
201    fn should_start_new_session_static(
202        current: &CollaborativeSession,
203        timestamp: &DateTime<Utc>,
204        new_type: &SessionType,
205    ) -> bool {
206        let time_gap = *timestamp - current.last_activity;
207
208        // New session if: gap > 30 mins, type change, or tool change
209        time_gap > Duration::minutes(30)
210            || current.session_type != *new_type
211            || (*new_type == SessionType::Tandem && time_gap > Duration::minutes(5))
212    }
213
214    /// Start a new collaborative session
215    fn start_new_session(&mut self, context: &GatheredContext, session_type: SessionType) {
216        let session = CollaborativeSession {
217            id: format!("session_{}", chrono::Utc::now().timestamp()),
218            start_time: context.timestamp,
219            last_activity: context.timestamp,
220            session_type,
221            ai_tool: context.ai_tool.clone(),
222            interactions: vec![context.clone()],
223            flow_state: FlowState::Steady,
224            trust_flow_score: TrustFlowScore::default(),
225            anchored_memories: Vec::new(),
226        };
227
228        self.active_session = Some(session);
229    }
230
231    /// End the active session and move to history
232    pub fn end_active_session(&mut self) {
233        if let Some(mut session) = self.active_session.take() {
234            // Calculate final scores
235            session.trust_flow_score = self.calculate_trust_flow(&session);
236
237            // Update rapport index
238            self.update_rapport_index(&session);
239
240            // Store in history
241            if self.session_history.len() >= 1000 {
242                self.session_history.pop_front();
243            }
244            self.session_history.push_back(session);
245        }
246    }
247
248    /// Analyze current flow state
249    fn analyze_flow_state(&self, session: &CollaborativeSession) -> FlowState {
250        let recent_interactions = session
251            .interactions
252            .iter()
253            .rev()
254            .take(10)
255            .collect::<Vec<_>>();
256
257        if recent_interactions.is_empty() {
258            return FlowState::Steady;
259        }
260
261        // Check for flow indicators
262        let mut back_and_forth_count = 0;
263        let mut repeated_concepts = HashMap::new();
264        let topics = Vec::new();
265
266        for (i, interaction) in recent_interactions.iter().enumerate() {
267            if i > 0 {
268                let prev = &recent_interactions[i - 1];
269                // Check for quick back-and-forth (flow indicator)
270                let time_diff = interaction.timestamp - prev.timestamp;
271                if time_diff < Duration::minutes(2) {
272                    back_and_forth_count += 1;
273                }
274            }
275
276            // Extract concepts (simplified)
277            if let ContextContent::Text(text) = &interaction.content {
278                for word in text.split_whitespace() {
279                    if word.len() > 5 {
280                        *repeated_concepts.entry(word.to_lowercase()).or_insert(0) += 1;
281                    }
282                }
283            }
284        }
285
286        // Determine flow state
287        if back_and_forth_count > 5 {
288            FlowState::Flow {
289                depth: back_and_forth_count as f32 / 10.0,
290                sustained_minutes: (recent_interactions.first().unwrap().timestamp
291                    - recent_interactions.last().unwrap().timestamp)
292                    .num_minutes() as u32,
293            }
294        } else if repeated_concepts.values().any(|&count| count > 3) {
295            let repeated: Vec<String> = repeated_concepts
296                .into_iter()
297                .filter(|(_, count)| *count > 3)
298                .map(|(concept, _)| concept)
299                .collect();
300            FlowState::Whirlpool {
301                confusion_score: repeated.len() as f32 / 10.0,
302                repeated_concepts: repeated,
303            }
304        } else if topics.len() > 3 {
305            FlowState::Fork {
306                branch_count: topics.len(),
307                topics,
308            }
309        } else {
310            FlowState::Steady
311        }
312    }
313
314    /// Calculate trust and flow score for a session
315    fn calculate_trust_flow(&self, session: &CollaborativeSession) -> TrustFlowScore {
316        TrustFlowScore {
317            clarity: self.calculate_clarity(session),
318            responsiveness: self.calculate_responsiveness(session),
319            autonomy_ratio: self.calculate_autonomy_ratio(session),
320            frustration_markers: self.detect_frustration(session),
321            flow_depth: match &session.flow_state {
322                FlowState::Flow { depth, .. } => *depth,
323                FlowState::Whirlpool {
324                    confusion_score, ..
325                } => 1.0 - confusion_score,
326                _ => 0.5,
327            },
328        }
329    }
330
331    /// Update rapport index based on session
332    fn update_rapport_index(&mut self, session: &CollaborativeSession) {
333        let pair_id = format!("{}_{}", "human", session.ai_tool);
334
335        let rapport = self
336            .rapport_indices
337            .entry(pair_id)
338            .or_insert_with(|| RapportIndex {
339                overall_score: 0.5,
340                trust_level: 0.5,
341                communication_efficiency: 0.5,
342                shared_vocabulary_size: 0,
343                inside_jokes_count: 0,
344                preferred_working_hours: Vec::new(),
345                avg_session_productivity: 0.5,
346                evolution_trend: 0.0,
347            });
348
349        // Update rapport based on session performance
350        let session_score = (session.trust_flow_score.clarity
351            + session.trust_flow_score.responsiveness
352            + session.trust_flow_score.flow_depth)
353            / 3.0;
354
355        // Exponential moving average
356        rapport.overall_score = rapport.overall_score * 0.8 + session_score * 0.2;
357
358        // Update trust level
359        if session.trust_flow_score.autonomy_ratio > 0.3 {
360            rapport.trust_level = (rapport.trust_level * 0.9 + 0.1).min(1.0);
361        }
362
363        // Track working hours
364        let hour = session.start_time.hour();
365        if !rapport.preferred_working_hours.contains(&hour) {
366            rapport.preferred_working_hours.push(hour);
367        }
368
369        // Calculate trend
370        rapport.evolution_trend = session_score - rapport.avg_session_productivity;
371        rapport.avg_session_productivity =
372            rapport.avg_session_productivity * 0.9 + session_score * 0.1;
373    }
374
375    /// Create a memory anchor
376    pub fn anchor_memory(
377        &mut self,
378        origin: CollaborativeOrigin,
379        anchor_type: AnchorType,
380        context: String,
381        keywords: Vec<String>,
382    ) -> Result<String> {
383        let id = format!(
384            "anchor_{}",
385            chrono::Utc::now().timestamp_nanos_opt().unwrap_or(0)
386        );
387
388        let co_created = matches!(origin, CollaborativeOrigin::Tandem { .. });
389
390        let anchor = MemoryAnchor {
391            id: id.clone(),
392            origin,
393            anchor_type,
394            context,
395            keywords,
396            timestamp: chrono::Utc::now(),
397            relevance_wave: MemoryWave::new_with_band(
398                FrequencyBand::Beta,
399                1.0, // High amplitude for anchored memories
400                0.0,
401                0.05, // Slow decay rate (20 second time constant) for important memories
402            ),
403            co_created,
404        };
405
406        self.memory_anchors.insert(id.clone(), anchor);
407
408        // Add to current session if active
409        if let Some(ref mut session) = self.active_session {
410            session.anchored_memories.push(id.clone());
411        }
412
413        Ok(id)
414    }
415
416    /// Find relevant anchored memories
417    pub fn find_relevant_anchors(&self, keywords: &[String]) -> Vec<&MemoryAnchor> {
418        let mut relevant = Vec::new();
419
420        for anchor in self.memory_anchors.values() {
421            let relevance = keywords
422                .iter()
423                .filter(|k| anchor.keywords.contains(k))
424                .count() as f32
425                / keywords.len().max(1) as f32;
426
427            if relevance > 0.3 {
428                relevant.push(anchor);
429            }
430        }
431
432        // Sort by relevance and recency
433        relevant.sort_by(|a, b| {
434            let a_score = a.relevance_wave.amplitude;
435            let b_score = b.relevance_wave.amplitude;
436            b_score.partial_cmp(&a_score).unwrap()
437        });
438
439        relevant
440    }
441
442    /// Link sessions across domains
443    pub fn link_cross_session(&mut self, session_id: String, related_ids: Vec<String>) {
444        self.cross_session_links
445            .entry(session_id)
446            .or_default()
447            .extend(related_ids);
448    }
449
450    /// Get rapport index for a pair
451    pub fn get_rapport(&self, ai_tool: &str) -> Option<&RapportIndex> {
452        let pair_id = format!("human_{}", ai_tool);
453        self.rapport_indices.get(&pair_id)
454    }
455
456    // Helper methods
457
458    fn calculate_clarity(&self, session: &CollaborativeSession) -> f32 {
459        // Simplified clarity calculation
460        let tandem_ratio = session
461            .interactions
462            .iter()
463            .filter(|i| matches!(self.detect_session_type(i), SessionType::Tandem))
464            .count() as f32
465            / session.interactions.len().max(1) as f32;
466
467        tandem_ratio
468    }
469
470    fn calculate_responsiveness(&self, session: &CollaborativeSession) -> f32 {
471        // Calculate average response time
472        let mut response_times = Vec::new();
473
474        for i in 1..session.interactions.len() {
475            let time_diff =
476                session.interactions[i].timestamp - session.interactions[i - 1].timestamp;
477            if time_diff < Duration::minutes(5) {
478                response_times.push(time_diff.num_seconds() as f32);
479            }
480        }
481
482        if response_times.is_empty() {
483            return 0.5;
484        }
485
486        let avg_response = response_times.iter().sum::<f32>() / response_times.len() as f32;
487        // Convert to 0-1 scale (faster response = higher score)
488        1.0 - (avg_response / 300.0).min(1.0)
489    }
490
491    fn calculate_autonomy_ratio(&self, session: &CollaborativeSession) -> f32 {
492        // Measure balance of initiative
493        let ai_initiated = session
494            .interactions
495            .iter()
496            .filter(|i| matches!(self.detect_session_type(i), SessionType::SoloAI))
497            .count() as f32;
498        let human_initiated = session
499            .interactions
500            .iter()
501            .filter(|i| matches!(self.detect_session_type(i), SessionType::SoloHuman))
502            .count() as f32;
503
504        let total = ai_initiated + human_initiated;
505        if total == 0.0 {
506            return 0.5;
507        }
508
509        // Perfect balance = 0.5, all AI = 1.0, all human = 0.0
510        ai_initiated / total
511    }
512
513    fn detect_frustration(&self, session: &CollaborativeSession) -> f32 {
514        // Look for frustration patterns
515        let mut frustration_score = 0.0;
516
517        // Check for repeated questions
518        if let FlowState::Whirlpool {
519            confusion_score, ..
520        } = &session.flow_state
521        {
522            frustration_score += confusion_score;
523        }
524
525        // Check for long gaps between interactions
526        for i in 1..session.interactions.len() {
527            let gap = session.interactions[i].timestamp - session.interactions[i - 1].timestamp;
528            if gap > Duration::minutes(10) && gap < Duration::hours(1) {
529                frustration_score += 0.1;
530            }
531        }
532
533        frustration_score.min(1.0)
534    }
535}
536
537/// A collaborative session between AI and human
538#[derive(Debug, Clone, Serialize, Deserialize)]
539pub struct CollaborativeSession {
540    pub id: String,
541    pub start_time: DateTime<Utc>,
542    pub last_activity: DateTime<Utc>,
543    pub session_type: SessionType,
544    pub ai_tool: String,
545    pub interactions: Vec<GatheredContext>,
546    pub flow_state: FlowState,
547    pub trust_flow_score: TrustFlowScore,
548    pub anchored_memories: Vec<String>,
549}
550
551impl CollaborativeSession {
552    fn update(&mut self, context: &GatheredContext, session_type: SessionType) {
553        self.last_activity = context.timestamp;
554        self.session_type = session_type;
555        self.interactions.push(context.clone());
556    }
557}
558
559impl Default for TrustFlowScore {
560    fn default() -> Self {
561        Self {
562            clarity: 0.5,
563            responsiveness: 0.5,
564            autonomy_ratio: 0.5,
565            frustration_markers: 0.0,
566            flow_depth: 0.0,
567        }
568    }
569}
570
571/// Temporal co-engagement heatmap data
572#[derive(Debug, Clone, Serialize, Deserialize)]
573pub struct CoEngagementHeatmap {
574    pub time_slots: Vec<Vec<f32>>, // 24x7 grid (hours x days)
575    pub peak_collaboration_zones: Vec<(usize, usize)>, // (hour, day) pairs
576    pub collaboration_density: f32,
577}
578
579impl CoEngagementHeatmap {
580    pub fn from_sessions(sessions: &[CollaborativeSession]) -> Self {
581        let mut grid = vec![vec![0.0; 7]; 24]; // 24 hours x 7 days
582
583        // Build heatmap
584        for session in sessions {
585            if session.session_type == SessionType::Tandem {
586                let hour = session.start_time.hour() as usize;
587                let day = session.start_time.weekday().num_days_from_monday() as usize;
588                grid[hour][day] += 1.0;
589            }
590        }
591
592        // Normalize
593        let max_val = grid
594            .iter()
595            .flat_map(|row| row.iter())
596            .fold(0.0f32, |a, &b| a.max(b));
597
598        if max_val > 0.0 {
599            for row in &mut grid {
600                for val in row {
601                    *val /= max_val;
602                }
603            }
604        }
605
606        // Find peaks
607        let mut peaks = Vec::new();
608        for (hour, row) in grid.iter().enumerate() {
609            for (day, &val) in row.iter().enumerate() {
610                if val > 0.7 {
611                    peaks.push((hour, day));
612                }
613            }
614        }
615
616        let density = grid
617            .iter()
618            .flat_map(|row| row.iter())
619            .filter(|&&v| v > 0.0)
620            .count() as f32
621            / (24.0 * 7.0);
622
623        Self {
624            time_slots: grid,
625            peak_collaboration_zones: peaks,
626            collaboration_density: density,
627        }
628    }
629}