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 #[serde(skip)]
57 pub rescorer: Option<std::sync::Arc<crate::rescorer::SessionRescorer>>,
58}
59
60impl Default for SessionContext {
61 fn default() -> Self {
62 Self {
63 agent_type: String::new(),
64 extraction_started: Utc::now(),
65 extraction_completed: None,
66 session_id: None,
67 conversation: Vec::new(),
68 decisions: Vec::new(),
69 files: Vec::new(),
70 tasks: Vec::new(),
71 insights: Vec::new(),
72 errors: Vec::new(),
73 subagent_executions: Vec::new(),
74 commands_run: Vec::new(),
75 custom: HashMap::new(),
76 extraction_source: "unknown".to_string(),
77 reliability_score: 1.0,
78 rescorer: None,
79 }
80 }
81}
82
83impl SessionContext {
84 pub fn new(agent_type: impl Into<String>) -> Self {
86 Self {
87 agent_type: agent_type.into(),
88 ..Default::default()
89 }
90 }
91
92 pub fn with_source(mut self, source: impl Into<String>) -> Self {
94 self.extraction_source = source.into();
95 self
96 }
97
98 pub fn with_reliability(mut self, score: f32) -> Self {
100 self.reliability_score = score.clamp(0.0, 1.0);
101 self
102 }
103
104 pub fn complete(&mut self) {
106 self.extraction_completed = Some(Utc::now());
107 }
108
109 pub fn add_message(&mut self, role: impl Into<String>, content: impl Into<String>) {
111 let content_str = content.into();
112 self.conversation.push(ConversationMessage {
113 role: role.into(),
114 content: content_str.clone(),
115 timestamp: Utc::now(),
116 });
117
118 if let Some(rescorer) = self.rescorer.as_ref() {
120 let rescorer = rescorer.clone();
121 let agent_type = self.agent_type.clone();
122 if let Ok(handle) = tokio::runtime::Handle::try_current() {
123 handle.spawn(async move {
124 let config = nexus_core::Config::from_env().unwrap_or_default();
125 let embeddings = if config.embedding.enabled {
126 nexus_agent::runtime::create_embedding_service(&config).await
127 } else {
128 None
129 };
130 if rescorer
131 .on_turn(&content_str, embeddings.as_deref())
132 .await
133 .is_some()
134 {
135 let _ = rescorer.rescore(embeddings.as_deref(), &agent_type).await;
136 }
137 });
138 }
139 }
140 }
141
142 pub fn add_decision(&mut self, decision: Decision) {
144 self.decisions.push(decision);
145 }
146
147 pub fn add_file(&mut self, file: FileInfo) {
149 self.files.push(file);
150 }
151
152 pub fn add_task(&mut self, task: TaskInfo) {
154 self.tasks.push(task);
155 }
156
157 pub fn add_insight(&mut self, insight: impl Into<String>) {
159 self.insights.push(insight.into());
160 }
161
162 pub fn add_error(&mut self, error: ErrorInfo) {
164 self.errors.push(error);
165 }
166
167 pub fn add_command(&mut self, command: impl Into<String>) {
169 self.commands_run.push(command.into());
170 }
171
172 pub fn add_custom(&mut self, key: impl Into<String>, value: serde_json::Value) {
174 self.custom.insert(key.into(), value);
175 }
176
177 pub fn is_empty(&self) -> bool {
179 self.conversation.is_empty()
180 && self.decisions.is_empty()
181 && self.files.is_empty()
182 && self.tasks.is_empty()
183 && self.insights.is_empty()
184 && self.errors.is_empty()
185 && self.commands_run.is_empty()
186 }
187
188 pub fn stats(&self) -> SessionStats {
190 SessionStats {
191 message_count: self.conversation.len(),
192 decision_count: self.decisions.len(),
193 file_count: self.files.len(),
194 task_count: self.tasks.len(),
195 insight_count: self.insights.len(),
196 error_count: self.errors.len(),
197 command_count: self.commands_run.len(),
198 }
199 }
200
201 pub fn to_memory_content(&self) -> String {
203 let mut parts = Vec::new();
204
205 parts.push(format!("Agent: {}", self.agent_type));
206 parts.push(format!("Source: {}", self.extraction_source));
207 parts.push(format!(
208 "Reliability: {:.0}%",
209 self.reliability_score * 100.0
210 ));
211
212 if !self.conversation.is_empty() {
213 parts.push(format!(
214 "\nConversation: {} messages",
215 self.conversation.len()
216 ));
217 }
218
219 if !self.decisions.is_empty() {
220 parts.push(format!("\nDecisions: {}", self.decisions.len()));
221 for decision in &self.decisions {
222 parts.push(format!(" - {}", decision.summary));
223 }
224 }
225
226 if !self.files.is_empty() {
227 parts.push(format!("\nFiles: {}", self.files.len()));
228 for file in &self.files {
229 parts.push(format!(" - {} ({})", file.path, file.action));
230 }
231 }
232
233 if !self.insights.is_empty() {
234 parts.push(format!("\nInsights: {}", self.insights.len()));
235 for insight in &self.insights {
236 parts.push(format!(" - {}", insight));
237 }
238 }
239
240 if !self.errors.is_empty() {
241 parts.push(format!("\nErrors: {}", self.errors.len()));
242 for error in &self.errors {
243 parts.push(format!(" - {}: {}", error.error_type, error.message));
244 }
245 }
246
247 parts.join("\n")
248 }
249}
250
251#[derive(Debug, Clone, Serialize, Deserialize)]
253pub struct ConversationMessage {
254 pub role: String,
255 pub content: String,
256 pub timestamp: DateTime<Utc>,
257}
258
259#[derive(Debug, Clone, Serialize, Deserialize)]
261pub struct Decision {
262 pub summary: String,
263 pub rationale: Option<String>,
264 pub alternatives: Vec<String>,
265 pub impact: Option<String>,
266 pub timestamp: DateTime<Utc>,
267}
268
269impl Decision {
270 pub fn new(summary: impl Into<String>) -> Self {
271 Self {
272 summary: summary.into(),
273 rationale: None,
274 alternatives: Vec::new(),
275 impact: None,
276 timestamp: Utc::now(),
277 }
278 }
279}
280
281#[derive(Debug, Clone, Serialize, Deserialize)]
283pub struct FileInfo {
284 pub path: String,
285 pub action: FileAction,
286 pub lines_added: Option<usize>,
287 pub lines_removed: Option<usize>,
288 pub timestamp: DateTime<Utc>,
289}
290
291impl FileInfo {
292 pub fn new(path: impl Into<String>, action: FileAction) -> Self {
293 Self {
294 path: path.into(),
295 action,
296 lines_added: None,
297 lines_removed: None,
298 timestamp: Utc::now(),
299 }
300 }
301}
302
303#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize)]
305#[serde(rename_all = "snake_case")]
306pub enum FileAction {
307 Created,
308 Modified,
309 Deleted,
310 Read,
311}
312
313impl std::fmt::Display for FileAction {
314 fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
315 match self {
316 FileAction::Created => write!(f, "created"),
317 FileAction::Modified => write!(f, "modified"),
318 FileAction::Deleted => write!(f, "deleted"),
319 FileAction::Read => write!(f, "read"),
320 }
321 }
322}
323
324#[derive(Debug, Clone, Serialize, Deserialize)]
326pub struct TaskInfo {
327 pub description: String,
328 pub status: TaskStatus,
329 pub started_at: Option<DateTime<Utc>>,
330 pub completed_at: Option<DateTime<Utc>>,
331 pub subagent: Option<String>,
332}
333
334impl TaskInfo {
335 pub fn new(description: impl Into<String>) -> Self {
336 Self {
337 description: description.into(),
338 status: TaskStatus::Pending,
339 started_at: None,
340 completed_at: None,
341 subagent: None,
342 }
343 }
344}
345
346#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize)]
348#[serde(rename_all = "snake_case")]
349pub enum TaskStatus {
350 Pending,
351 InProgress,
352 Completed,
353 Failed,
354}
355
356#[derive(Debug, Clone, Serialize, Deserialize)]
358pub struct ErrorInfo {
359 pub error_type: String,
360 pub message: String,
361 pub stack_trace: Option<String>,
362 pub timestamp: DateTime<Utc>,
363}
364
365impl ErrorInfo {
366 pub fn new(error_type: impl Into<String>, message: impl Into<String>) -> Self {
367 Self {
368 error_type: error_type.into(),
369 message: message.into(),
370 stack_trace: None,
371 timestamp: Utc::now(),
372 }
373 }
374}
375
376#[derive(Debug, Clone, Serialize, Deserialize)]
378pub struct SubagentExecution {
379 pub subagent_type: String,
380 pub task: String,
381 pub status: String,
382 pub started_at: DateTime<Utc>,
383 pub completed_at: Option<DateTime<Utc>>,
384 pub result_summary: Option<String>,
385}
386
387#[derive(Debug, Clone, Serialize, Deserialize)]
389pub struct SessionStats {
390 pub message_count: usize,
391 pub decision_count: usize,
392 pub file_count: usize,
393 pub task_count: usize,
394 pub insight_count: usize,
395 pub error_count: usize,
396 pub command_count: usize,
397}
398
399#[cfg(test)]
400mod tests {
401 use super::*;
402
403 #[test]
404 fn test_session_context_new() {
405 let ctx = SessionContext::new("claude-code");
406 assert_eq!(ctx.agent_type, "claude-code");
407 assert!(ctx.is_empty());
408 }
409
410 #[test]
411 fn test_session_context_add_items() {
412 let mut ctx = SessionContext::new("test");
413
414 ctx.add_message("user", "Hello");
415 ctx.add_message("assistant", "Hi there!");
416 ctx.add_decision(Decision::new("Use Rust"));
417 ctx.add_file(FileInfo::new("/src/main.rs", FileAction::Created));
418 ctx.add_insight("Rust is fast");
419 ctx.add_command("cargo build");
420
421 assert!(!ctx.is_empty());
422 assert_eq!(ctx.conversation.len(), 2);
423 assert_eq!(ctx.decisions.len(), 1);
424 assert_eq!(ctx.files.len(), 1);
425 assert_eq!(ctx.insights.len(), 1);
426 assert_eq!(ctx.commands_run.len(), 1);
427 }
428
429 #[test]
430 fn test_session_context_to_memory_content() {
431 let mut ctx = SessionContext::new("claude-code")
432 .with_source("native")
433 .with_reliability(0.95);
434
435 ctx.add_insight("Test insight");
436
437 let content = ctx.to_memory_content();
438 assert!(content.contains("claude-code"));
439 assert!(content.contains("native"));
440 assert!(content.contains("95%"));
441 assert!(content.contains("Test insight"));
442 }
443
444 #[test]
445 fn test_session_stats() {
446 let mut ctx = SessionContext::new("test");
447 ctx.add_message("user", "test");
448 ctx.add_decision(Decision::new("decide"));
449 ctx.add_error(ErrorInfo::new("test", "error"));
450
451 let stats = ctx.stats();
452 assert_eq!(stats.message_count, 1);
453 assert_eq!(stats.decision_count, 1);
454 assert_eq!(stats.error_count, 1);
455 }
456}