1use chrono::{DateTime, Utc};
4use serde::{Deserialize, Serialize};
5use std::collections::HashMap;
6
7#[derive(Debug, Clone, Serialize, Deserialize)]
9pub struct SessionContext {
10 pub agent_type: String,
12
13 pub extraction_started: DateTime<Utc>,
15
16 pub extraction_completed: Option<DateTime<Utc>>,
18
19 pub session_id: Option<String>,
21
22 pub conversation: Vec<ConversationMessage>,
24
25 pub decisions: Vec<Decision>,
27
28 pub files: Vec<FileInfo>,
30
31 pub tasks: Vec<TaskInfo>,
33
34 pub insights: Vec<String>,
36
37 pub errors: Vec<ErrorInfo>,
39
40 pub subagent_executions: Vec<SubagentExecution>,
42
43 pub commands_run: Vec<String>,
45
46 pub custom: HashMap<String, serde_json::Value>,
48
49 pub extraction_source: String,
51
52 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 pub fn new(agent_type: impl Into<String>) -> Self {
81 Self {
82 agent_type: agent_type.into(),
83 ..Default::default()
84 }
85 }
86
87 pub fn with_source(mut self, source: impl Into<String>) -> Self {
89 self.extraction_source = source.into();
90 self
91 }
92
93 pub fn with_reliability(mut self, score: f32) -> Self {
95 self.reliability_score = score.clamp(0.0, 1.0);
96 self
97 }
98
99 pub fn complete(&mut self) {
101 self.extraction_completed = Some(Utc::now());
102 }
103
104 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 pub fn add_decision(&mut self, decision: Decision) {
115 self.decisions.push(decision);
116 }
117
118 pub fn add_file(&mut self, file: FileInfo) {
120 self.files.push(file);
121 }
122
123 pub fn add_task(&mut self, task: TaskInfo) {
125 self.tasks.push(task);
126 }
127
128 pub fn add_insight(&mut self, insight: impl Into<String>) {
130 self.insights.push(insight.into());
131 }
132
133 pub fn add_error(&mut self, error: ErrorInfo) {
135 self.errors.push(error);
136 }
137
138 pub fn add_command(&mut self, command: impl Into<String>) {
140 self.commands_run.push(command.into());
141 }
142
143 pub fn add_custom(&mut self, key: impl Into<String>, value: serde_json::Value) {
145 self.custom.insert(key.into(), value);
146 }
147
148 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 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 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#[derive(Debug, Clone, Serialize, Deserialize)]
224pub struct ConversationMessage {
225 pub role: String,
226 pub content: String,
227 pub timestamp: DateTime<Utc>,
228}
229
230#[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#[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#[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#[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#[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#[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#[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#[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}