use chrono::{DateTime, Utc};
use serde::{Deserialize, Serialize};
use std::collections::HashMap;
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct SessionContext {
pub agent_type: String,
pub extraction_started: DateTime<Utc>,
pub extraction_completed: Option<DateTime<Utc>>,
pub session_id: Option<String>,
pub conversation: Vec<ConversationMessage>,
pub decisions: Vec<Decision>,
pub files: Vec<FileInfo>,
pub tasks: Vec<TaskInfo>,
pub insights: Vec<String>,
pub errors: Vec<ErrorInfo>,
pub subagent_executions: Vec<SubagentExecution>,
pub commands_run: Vec<String>,
pub custom: HashMap<String, serde_json::Value>,
pub extraction_source: String,
pub reliability_score: f32,
}
impl Default for SessionContext {
fn default() -> Self {
Self {
agent_type: String::new(),
extraction_started: Utc::now(),
extraction_completed: None,
session_id: None,
conversation: Vec::new(),
decisions: Vec::new(),
files: Vec::new(),
tasks: Vec::new(),
insights: Vec::new(),
errors: Vec::new(),
subagent_executions: Vec::new(),
commands_run: Vec::new(),
custom: HashMap::new(),
extraction_source: "unknown".to_string(),
reliability_score: 1.0,
}
}
}
impl SessionContext {
pub fn new(agent_type: impl Into<String>) -> Self {
Self {
agent_type: agent_type.into(),
..Default::default()
}
}
pub fn with_source(mut self, source: impl Into<String>) -> Self {
self.extraction_source = source.into();
self
}
pub fn with_reliability(mut self, score: f32) -> Self {
self.reliability_score = score.clamp(0.0, 1.0);
self
}
pub fn complete(&mut self) {
self.extraction_completed = Some(Utc::now());
}
pub fn add_message(&mut self, role: impl Into<String>, content: impl Into<String>) {
self.conversation.push(ConversationMessage {
role: role.into(),
content: content.into(),
timestamp: Utc::now(),
});
}
pub fn add_decision(&mut self, decision: Decision) {
self.decisions.push(decision);
}
pub fn add_file(&mut self, file: FileInfo) {
self.files.push(file);
}
pub fn add_task(&mut self, task: TaskInfo) {
self.tasks.push(task);
}
pub fn add_insight(&mut self, insight: impl Into<String>) {
self.insights.push(insight.into());
}
pub fn add_error(&mut self, error: ErrorInfo) {
self.errors.push(error);
}
pub fn add_command(&mut self, command: impl Into<String>) {
self.commands_run.push(command.into());
}
pub fn add_custom(&mut self, key: impl Into<String>, value: serde_json::Value) {
self.custom.insert(key.into(), value);
}
pub fn is_empty(&self) -> bool {
self.conversation.is_empty()
&& self.decisions.is_empty()
&& self.files.is_empty()
&& self.tasks.is_empty()
&& self.insights.is_empty()
&& self.errors.is_empty()
&& self.commands_run.is_empty()
}
pub fn stats(&self) -> SessionStats {
SessionStats {
message_count: self.conversation.len(),
decision_count: self.decisions.len(),
file_count: self.files.len(),
task_count: self.tasks.len(),
insight_count: self.insights.len(),
error_count: self.errors.len(),
command_count: self.commands_run.len(),
}
}
pub fn to_memory_content(&self) -> String {
let mut parts = Vec::new();
parts.push(format!("Agent: {}", self.agent_type));
parts.push(format!("Source: {}", self.extraction_source));
parts.push(format!(
"Reliability: {:.0}%",
self.reliability_score * 100.0
));
if !self.conversation.is_empty() {
parts.push(format!(
"\nConversation: {} messages",
self.conversation.len()
));
}
if !self.decisions.is_empty() {
parts.push(format!("\nDecisions: {}", self.decisions.len()));
for decision in &self.decisions {
parts.push(format!(" - {}", decision.summary));
}
}
if !self.files.is_empty() {
parts.push(format!("\nFiles: {}", self.files.len()));
for file in &self.files {
parts.push(format!(" - {} ({})", file.path, file.action));
}
}
if !self.insights.is_empty() {
parts.push(format!("\nInsights: {}", self.insights.len()));
for insight in &self.insights {
parts.push(format!(" - {}", insight));
}
}
if !self.errors.is_empty() {
parts.push(format!("\nErrors: {}", self.errors.len()));
for error in &self.errors {
parts.push(format!(" - {}: {}", error.error_type, error.message));
}
}
parts.join("\n")
}
}
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct ConversationMessage {
pub role: String,
pub content: String,
pub timestamp: DateTime<Utc>,
}
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct Decision {
pub summary: String,
pub rationale: Option<String>,
pub alternatives: Vec<String>,
pub impact: Option<String>,
pub timestamp: DateTime<Utc>,
}
impl Decision {
pub fn new(summary: impl Into<String>) -> Self {
Self {
summary: summary.into(),
rationale: None,
alternatives: Vec::new(),
impact: None,
timestamp: Utc::now(),
}
}
}
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct FileInfo {
pub path: String,
pub action: FileAction,
pub lines_added: Option<usize>,
pub lines_removed: Option<usize>,
pub timestamp: DateTime<Utc>,
}
impl FileInfo {
pub fn new(path: impl Into<String>, action: FileAction) -> Self {
Self {
path: path.into(),
action,
lines_added: None,
lines_removed: None,
timestamp: Utc::now(),
}
}
}
#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize)]
#[serde(rename_all = "snake_case")]
pub enum FileAction {
Created,
Modified,
Deleted,
Read,
}
impl std::fmt::Display for FileAction {
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
match self {
FileAction::Created => write!(f, "created"),
FileAction::Modified => write!(f, "modified"),
FileAction::Deleted => write!(f, "deleted"),
FileAction::Read => write!(f, "read"),
}
}
}
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct TaskInfo {
pub description: String,
pub status: TaskStatus,
pub started_at: Option<DateTime<Utc>>,
pub completed_at: Option<DateTime<Utc>>,
pub subagent: Option<String>,
}
impl TaskInfo {
pub fn new(description: impl Into<String>) -> Self {
Self {
description: description.into(),
status: TaskStatus::Pending,
started_at: None,
completed_at: None,
subagent: None,
}
}
}
#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize)]
#[serde(rename_all = "snake_case")]
pub enum TaskStatus {
Pending,
InProgress,
Completed,
Failed,
}
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct ErrorInfo {
pub error_type: String,
pub message: String,
pub stack_trace: Option<String>,
pub timestamp: DateTime<Utc>,
}
impl ErrorInfo {
pub fn new(error_type: impl Into<String>, message: impl Into<String>) -> Self {
Self {
error_type: error_type.into(),
message: message.into(),
stack_trace: None,
timestamp: Utc::now(),
}
}
}
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct SubagentExecution {
pub subagent_type: String,
pub task: String,
pub status: String,
pub started_at: DateTime<Utc>,
pub completed_at: Option<DateTime<Utc>>,
pub result_summary: Option<String>,
}
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct SessionStats {
pub message_count: usize,
pub decision_count: usize,
pub file_count: usize,
pub task_count: usize,
pub insight_count: usize,
pub error_count: usize,
pub command_count: usize,
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn test_session_context_new() {
let ctx = SessionContext::new("claude-code");
assert_eq!(ctx.agent_type, "claude-code");
assert!(ctx.is_empty());
}
#[test]
fn test_session_context_add_items() {
let mut ctx = SessionContext::new("test");
ctx.add_message("user", "Hello");
ctx.add_message("assistant", "Hi there!");
ctx.add_decision(Decision::new("Use Rust"));
ctx.add_file(FileInfo::new("/src/main.rs", FileAction::Created));
ctx.add_insight("Rust is fast");
ctx.add_command("cargo build");
assert!(!ctx.is_empty());
assert_eq!(ctx.conversation.len(), 2);
assert_eq!(ctx.decisions.len(), 1);
assert_eq!(ctx.files.len(), 1);
assert_eq!(ctx.insights.len(), 1);
assert_eq!(ctx.commands_run.len(), 1);
}
#[test]
fn test_session_context_to_memory_content() {
let mut ctx = SessionContext::new("claude-code")
.with_source("native")
.with_reliability(0.95);
ctx.add_insight("Test insight");
let content = ctx.to_memory_content();
assert!(content.contains("claude-code"));
assert!(content.contains("native"));
assert!(content.contains("95%"));
assert!(content.contains("Test insight"));
}
#[test]
fn test_session_stats() {
let mut ctx = SessionContext::new("test");
ctx.add_message("user", "test");
ctx.add_decision(Decision::new("decide"));
ctx.add_error(ErrorInfo::new("test", "error"));
let stats = ctx.stats();
assert_eq!(stats.message_count, 1);
assert_eq!(stats.decision_count, 1);
assert_eq!(stats.error_count, 1);
}
}