Skip to main content

pawan/agent/
session.rs

1//! Session persistence — save and resume conversations
2
3use crate::agent::Message;
4use crate::{PawanError, Result};
5use serde::{Deserialize, Serialize};
6use std::path::PathBuf;
7
8/// A saved conversation session
9#[derive(Debug, Serialize, Deserialize)]
10pub struct Session {
11    /// Unique session ID
12    pub id: String,
13    /// Model used for this session
14    pub model: String,
15    /// When the session was created
16    pub created_at: String,
17    /// When the session was last updated
18    pub updated_at: String,
19    /// Conversation messages
20    pub messages: Vec<Message>,
21    /// Total tokens used in this session
22    #[serde(default)]
23    pub total_tokens: u64,
24    /// Number of iterations completed
25    #[serde(default)]
26    pub iteration_count: u32,
27}
28
29impl Session {
30    /// Create a new session
31    pub fn new(model: &str) -> Self {
32        let id = uuid::Uuid::new_v4().to_string()[..8].to_string();
33        let now = chrono::Utc::now().to_rfc3339();
34        Self {
35            id,
36            model: model.to_string(),
37            created_at: now.clone(),
38            updated_at: now,
39            messages: Vec::new(),
40            total_tokens: 0,
41            iteration_count: 0,
42        }
43    }
44
45    /// Get the sessions directory (~/.pawan/sessions/)
46    pub fn sessions_dir() -> Result<PathBuf> {
47        let home = std::env::var("HOME").unwrap_or_else(|_| "/root".to_string());
48        let dir = PathBuf::from(home).join(".pawan").join("sessions");
49        if !dir.exists() {
50            std::fs::create_dir_all(&dir)
51                .map_err(|e| PawanError::Config(format!("Failed to create sessions dir: {}", e)))?;
52        }
53        Ok(dir)
54    }
55
56    /// Save session to disk
57    pub fn save(&mut self) -> Result<PathBuf> {
58        self.updated_at = chrono::Utc::now().to_rfc3339();
59        let dir = Self::sessions_dir()?;
60        let path = dir.join(format!("{}.json", self.id));
61        let json = serde_json::to_string_pretty(self)
62            .map_err(|e| PawanError::Config(format!("Failed to serialize session: {}", e)))?;
63        std::fs::write(&path, json)
64            .map_err(|e| PawanError::Config(format!("Failed to write session: {}", e)))?;
65        Ok(path)
66    }
67
68    /// Load a session from disk by ID
69    pub fn load(id: &str) -> Result<Self> {
70        let dir = Self::sessions_dir()?;
71        let path = dir.join(format!("{}.json", id));
72        if !path.exists() {
73            return Err(PawanError::NotFound(format!("Session not found: {}", id)));
74        }
75        let content = std::fs::read_to_string(&path)
76            .map_err(|e| PawanError::Config(format!("Failed to read session: {}", e)))?;
77        serde_json::from_str(&content)
78            .map_err(|e| PawanError::Config(format!("Failed to parse session: {}", e)))
79    }
80
81    /// List all saved sessions (sorted by updated_at, newest first)
82    pub fn list() -> Result<Vec<SessionSummary>> {
83        let dir = Self::sessions_dir()?;
84        let mut sessions = Vec::new();
85
86        if let Ok(entries) = std::fs::read_dir(&dir) {
87            for entry in entries.flatten() {
88                let path = entry.path();
89                if path.extension().is_some_and(|ext| ext == "json") {
90                    if let Ok(content) = std::fs::read_to_string(&path) {
91                        if let Ok(session) = serde_json::from_str::<Session>(&content) {
92                            sessions.push(SessionSummary {
93                                id: session.id,
94                                model: session.model,
95                                created_at: session.created_at,
96                                updated_at: session.updated_at,
97                                message_count: session.messages.len(),
98                            });
99                        }
100                    }
101                }
102            }
103        }
104
105        sessions.sort_by(|a, b| b.updated_at.cmp(&a.updated_at));
106        Ok(sessions)
107    }
108}
109
110/// Summary of a saved session (for listing)
111#[derive(Debug, Serialize, Deserialize)]
112pub struct SessionSummary {
113    pub id: String,
114    pub model: String,
115    pub created_at: String,
116    pub updated_at: String,
117    pub message_count: usize,
118}