Skip to main content

nexus_memory_hooks/
session.rs

1//! Session context for extracted data
2
3use chrono::{DateTime, Utc};
4use serde::{Deserialize, Serialize};
5use std::collections::HashMap;
6
7/// Extracted session context from an agent
8#[derive(Debug, Clone, Serialize, Deserialize)]
9pub struct SessionContext {
10    /// Agent type that created this context
11    pub agent_type: String,
12
13    /// When extraction started
14    pub extraction_started: DateTime<Utc>,
15
16    /// When extraction completed
17    pub extraction_completed: Option<DateTime<Utc>>,
18
19    /// Session ID (if available)
20    pub session_id: Option<String>,
21
22    /// Conversation messages
23    pub conversation: Vec<ConversationMessage>,
24
25    /// Decisions made during session
26    pub decisions: Vec<Decision>,
27
28    /// Files worked on
29    pub files: Vec<FileInfo>,
30
31    /// Tasks completed
32    pub tasks: Vec<TaskInfo>,
33
34    /// Key insights/learnings
35    pub insights: Vec<String>,
36
37    /// Errors encountered
38    pub errors: Vec<ErrorInfo>,
39
40    /// Subagent executions (for pi-mono, oh-my-pi)
41    pub subagent_executions: Vec<SubagentExecution>,
42
43    /// Commands run
44    pub commands_run: Vec<String>,
45
46    /// Custom context data
47    pub custom: HashMap<String, serde_json::Value>,
48
49    /// Source of extraction
50    pub extraction_source: String,
51
52    /// Reliability score (0.0-1.0)
53    pub reliability_score: f32,
54}
55
56impl Default for SessionContext {
57    fn default() -> Self {
58        Self {
59            agent_type: String::new(),
60            extraction_started: Utc::now(),
61            extraction_completed: None,
62            session_id: None,
63            conversation: Vec::new(),
64            decisions: Vec::new(),
65            files: Vec::new(),
66            tasks: Vec::new(),
67            insights: Vec::new(),
68            errors: Vec::new(),
69            subagent_executions: Vec::new(),
70            commands_run: Vec::new(),
71            custom: HashMap::new(),
72            extraction_source: "unknown".to_string(),
73            reliability_score: 1.0,
74        }
75    }
76}
77
78impl SessionContext {
79    /// Create a new session context for an agent
80    pub fn new(agent_type: impl Into<String>) -> Self {
81        Self {
82            agent_type: agent_type.into(),
83            ..Default::default()
84        }
85    }
86
87    /// Create with extraction source
88    pub fn with_source(mut self, source: impl Into<String>) -> Self {
89        self.extraction_source = source.into();
90        self
91    }
92
93    /// Create with reliability score
94    pub fn with_reliability(mut self, score: f32) -> Self {
95        self.reliability_score = score.clamp(0.0, 1.0);
96        self
97    }
98
99    /// Mark extraction as complete
100    pub fn complete(&mut self) {
101        self.extraction_completed = Some(Utc::now());
102    }
103
104    /// Add a conversation message
105    pub fn add_message(&mut self, role: impl Into<String>, content: impl Into<String>) {
106        self.conversation.push(ConversationMessage {
107            role: role.into(),
108            content: content.into(),
109            timestamp: Utc::now(),
110        });
111    }
112
113    /// Add a decision
114    pub fn add_decision(&mut self, decision: Decision) {
115        self.decisions.push(decision);
116    }
117
118    /// Add a file
119    pub fn add_file(&mut self, file: FileInfo) {
120        self.files.push(file);
121    }
122
123    /// Add a task
124    pub fn add_task(&mut self, task: TaskInfo) {
125        self.tasks.push(task);
126    }
127
128    /// Add an insight
129    pub fn add_insight(&mut self, insight: impl Into<String>) {
130        self.insights.push(insight.into());
131    }
132
133    /// Add an error
134    pub fn add_error(&mut self, error: ErrorInfo) {
135        self.errors.push(error);
136    }
137
138    /// Add a command
139    pub fn add_command(&mut self, command: impl Into<String>) {
140        self.commands_run.push(command.into());
141    }
142
143    /// Add custom data
144    pub fn add_custom(&mut self, key: impl Into<String>, value: serde_json::Value) {
145        self.custom.insert(key.into(), value);
146    }
147
148    /// Check if context is empty
149    pub fn is_empty(&self) -> bool {
150        self.conversation.is_empty()
151            && self.decisions.is_empty()
152            && self.files.is_empty()
153            && self.tasks.is_empty()
154            && self.insights.is_empty()
155            && self.errors.is_empty()
156            && self.commands_run.is_empty()
157    }
158
159    /// Get summary statistics
160    pub fn stats(&self) -> SessionStats {
161        SessionStats {
162            message_count: self.conversation.len(),
163            decision_count: self.decisions.len(),
164            file_count: self.files.len(),
165            task_count: self.tasks.len(),
166            insight_count: self.insights.len(),
167            error_count: self.errors.len(),
168            command_count: self.commands_run.len(),
169        }
170    }
171
172    /// Convert to memory content string
173    pub fn to_memory_content(&self) -> String {
174        let mut parts = Vec::new();
175
176        parts.push(format!("Agent: {}", self.agent_type));
177        parts.push(format!("Source: {}", self.extraction_source));
178        parts.push(format!(
179            "Reliability: {:.0}%",
180            self.reliability_score * 100.0
181        ));
182
183        if !self.conversation.is_empty() {
184            parts.push(format!(
185                "\nConversation: {} messages",
186                self.conversation.len()
187            ));
188        }
189
190        if !self.decisions.is_empty() {
191            parts.push(format!("\nDecisions: {}", self.decisions.len()));
192            for decision in &self.decisions {
193                parts.push(format!("  - {}", decision.summary));
194            }
195        }
196
197        if !self.files.is_empty() {
198            parts.push(format!("\nFiles: {}", self.files.len()));
199            for file in &self.files {
200                parts.push(format!("  - {} ({})", file.path, file.action));
201            }
202        }
203
204        if !self.insights.is_empty() {
205            parts.push(format!("\nInsights: {}", self.insights.len()));
206            for insight in &self.insights {
207                parts.push(format!("  - {}", insight));
208            }
209        }
210
211        if !self.errors.is_empty() {
212            parts.push(format!("\nErrors: {}", self.errors.len()));
213            for error in &self.errors {
214                parts.push(format!("  - {}: {}", error.error_type, error.message));
215            }
216        }
217
218        parts.join("\n")
219    }
220}
221
222/// Conversation message
223#[derive(Debug, Clone, Serialize, Deserialize)]
224pub struct ConversationMessage {
225    pub role: String,
226    pub content: String,
227    pub timestamp: DateTime<Utc>,
228}
229
230/// Decision made during session
231#[derive(Debug, Clone, Serialize, Deserialize)]
232pub struct Decision {
233    pub summary: String,
234    pub rationale: Option<String>,
235    pub alternatives: Vec<String>,
236    pub impact: Option<String>,
237    pub timestamp: DateTime<Utc>,
238}
239
240impl Decision {
241    pub fn new(summary: impl Into<String>) -> Self {
242        Self {
243            summary: summary.into(),
244            rationale: None,
245            alternatives: Vec::new(),
246            impact: None,
247            timestamp: Utc::now(),
248        }
249    }
250}
251
252/// File operation info
253#[derive(Debug, Clone, Serialize, Deserialize)]
254pub struct FileInfo {
255    pub path: String,
256    pub action: FileAction,
257    pub lines_added: Option<usize>,
258    pub lines_removed: Option<usize>,
259    pub timestamp: DateTime<Utc>,
260}
261
262impl FileInfo {
263    pub fn new(path: impl Into<String>, action: FileAction) -> Self {
264        Self {
265            path: path.into(),
266            action,
267            lines_added: None,
268            lines_removed: None,
269            timestamp: Utc::now(),
270        }
271    }
272}
273
274/// File action type
275#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize)]
276#[serde(rename_all = "snake_case")]
277pub enum FileAction {
278    Created,
279    Modified,
280    Deleted,
281    Read,
282}
283
284impl std::fmt::Display for FileAction {
285    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
286        match self {
287            FileAction::Created => write!(f, "created"),
288            FileAction::Modified => write!(f, "modified"),
289            FileAction::Deleted => write!(f, "deleted"),
290            FileAction::Read => write!(f, "read"),
291        }
292    }
293}
294
295/// Task info
296#[derive(Debug, Clone, Serialize, Deserialize)]
297pub struct TaskInfo {
298    pub description: String,
299    pub status: TaskStatus,
300    pub started_at: Option<DateTime<Utc>>,
301    pub completed_at: Option<DateTime<Utc>>,
302    pub subagent: Option<String>,
303}
304
305impl TaskInfo {
306    pub fn new(description: impl Into<String>) -> Self {
307        Self {
308            description: description.into(),
309            status: TaskStatus::Pending,
310            started_at: None,
311            completed_at: None,
312            subagent: None,
313        }
314    }
315}
316
317/// Task status
318#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize)]
319#[serde(rename_all = "snake_case")]
320pub enum TaskStatus {
321    Pending,
322    InProgress,
323    Completed,
324    Failed,
325}
326
327/// Error info
328#[derive(Debug, Clone, Serialize, Deserialize)]
329pub struct ErrorInfo {
330    pub error_type: String,
331    pub message: String,
332    pub stack_trace: Option<String>,
333    pub timestamp: DateTime<Utc>,
334}
335
336impl ErrorInfo {
337    pub fn new(error_type: impl Into<String>, message: impl Into<String>) -> Self {
338        Self {
339            error_type: error_type.into(),
340            message: message.into(),
341            stack_trace: None,
342            timestamp: Utc::now(),
343        }
344    }
345}
346
347/// Subagent execution (for pi-mono, oh-my-pi)
348#[derive(Debug, Clone, Serialize, Deserialize)]
349pub struct SubagentExecution {
350    pub subagent_type: String,
351    pub task: String,
352    pub status: String,
353    pub started_at: DateTime<Utc>,
354    pub completed_at: Option<DateTime<Utc>>,
355    pub result_summary: Option<String>,
356}
357
358/// Session statistics
359#[derive(Debug, Clone, Serialize, Deserialize)]
360pub struct SessionStats {
361    pub message_count: usize,
362    pub decision_count: usize,
363    pub file_count: usize,
364    pub task_count: usize,
365    pub insight_count: usize,
366    pub error_count: usize,
367    pub command_count: usize,
368}
369
370#[cfg(test)]
371mod tests {
372    use super::*;
373
374    #[test]
375    fn test_session_context_new() {
376        let ctx = SessionContext::new("claude-code");
377        assert_eq!(ctx.agent_type, "claude-code");
378        assert!(ctx.is_empty());
379    }
380
381    #[test]
382    fn test_session_context_add_items() {
383        let mut ctx = SessionContext::new("test");
384
385        ctx.add_message("user", "Hello");
386        ctx.add_message("assistant", "Hi there!");
387        ctx.add_decision(Decision::new("Use Rust"));
388        ctx.add_file(FileInfo::new("/src/main.rs", FileAction::Created));
389        ctx.add_insight("Rust is fast");
390        ctx.add_command("cargo build");
391
392        assert!(!ctx.is_empty());
393        assert_eq!(ctx.conversation.len(), 2);
394        assert_eq!(ctx.decisions.len(), 1);
395        assert_eq!(ctx.files.len(), 1);
396        assert_eq!(ctx.insights.len(), 1);
397        assert_eq!(ctx.commands_run.len(), 1);
398    }
399
400    #[test]
401    fn test_session_context_to_memory_content() {
402        let mut ctx = SessionContext::new("claude-code")
403            .with_source("native")
404            .with_reliability(0.95);
405
406        ctx.add_insight("Test insight");
407
408        let content = ctx.to_memory_content();
409        assert!(content.contains("claude-code"));
410        assert!(content.contains("native"));
411        assert!(content.contains("95%"));
412        assert!(content.contains("Test insight"));
413    }
414
415    #[test]
416    fn test_session_stats() {
417        let mut ctx = SessionContext::new("test");
418        ctx.add_message("user", "test");
419        ctx.add_decision(Decision::new("decide"));
420        ctx.add_error(ErrorInfo::new("test", "error"));
421
422        let stats = ctx.stats();
423        assert_eq!(stats.message_count, 1);
424        assert_eq!(stats.decision_count, 1);
425        assert_eq!(stats.error_count, 1);
426    }
427}