ai_agent/utils/
session_storage.rs1use crate::constants::env::system;
5use crate::session::SessionData;
6use serde::{Deserialize, Serialize};
7use std::path::PathBuf;
8
9pub fn get_session_storage_dir() -> PathBuf {
11 let home = std::env::var(system::HOME)
12 .or_else(|_| std::env::var(system::USERPROFILE))
13 .unwrap_or_else(|_| "/tmp".to_string());
14 PathBuf::from(home)
15 .join(".open-agent-sdk")
16 .join("session_storage")
17}
18
19pub fn get_transcript_path(session_id: &str) -> PathBuf {
21 get_session_storage_dir()
22 .join(session_id)
23 .join("transcript.json")
24}
25
26pub fn get_session_state_path(session_id: &str) -> PathBuf {
28 get_session_storage_dir()
29 .join(session_id)
30 .join("state.json")
31}
32
33fn ensure_storage_dir() -> std::io::Result<()> {
35 std::fs::create_dir_all(get_session_storage_dir())
36}
37
38fn ensure_session_dir(session_id: &str) -> std::io::Result<()> {
40 std::fs::create_dir_all(get_session_storage_dir().join(session_id))
41}
42
43pub fn session_exists(session_id: &str) -> bool {
45 get_transcript_path(session_id).exists()
46}
47
48#[derive(Debug, Clone, Serialize, Deserialize)]
50pub struct TranscriptEntry {
51 pub role: String,
52 pub content: String,
53 #[serde(default)]
54 pub timestamp: Option<String>,
55}
56
57pub fn load_transcript(session_id: &str) -> Vec<String> {
59 let path = get_transcript_path(session_id);
60 if !path.exists() {
61 return vec![];
62 }
63
64 match std::fs::read_to_string(&path) {
65 Ok(content) => {
66 match serde_json::from_str::<Vec<TranscriptEntry>>(&content) {
67 Ok(entries) => entries.into_iter().map(|e| e.content).collect(),
68 Err(_) => {
69 serde_json::from_str::<Vec<String>>(&content).unwrap_or_default()
71 }
72 }
73 }
74 Err(_) => vec![],
75 }
76}
77
78pub fn load_transcript_with_metadata(session_id: &str) -> Result<Vec<TranscriptEntry>, String> {
80 let path = get_transcript_path(session_id);
81 if !path.exists() {
82 return Err(format!("Transcript not found for session: {}", session_id));
83 }
84
85 let content =
86 std::fs::read_to_string(&path).map_err(|e| format!("Failed to read transcript: {}", e))?;
87
88 serde_json::from_str::<Vec<TranscriptEntry>>(&content)
89 .map_err(|e| format!("Failed to parse transcript: {}", e))
90}
91
92pub fn save_transcript(session_id: &str, transcript: &[String]) -> Result<(), String> {
94 ensure_session_dir(session_id).map_err(|e| format!("Failed to create session dir: {}", e))?;
95
96 let entries: Vec<TranscriptEntry> = transcript
97 .iter()
98 .map(|content| TranscriptEntry {
99 role: "assistant".to_string(),
100 content: content.clone(),
101 timestamp: Some(chrono::Utc::now().to_rfc3339()),
102 })
103 .collect();
104
105 let json = serde_json::to_string_pretty(&entries)
106 .map_err(|e| format!("Failed to serialize transcript: {}", e))?;
107
108 let path = get_transcript_path(session_id);
109 std::fs::write(&path, json).map_err(|e| format!("Failed to write transcript: {}", e))?;
110
111 Ok(())
112}
113
114pub fn append_to_transcript(session_id: &str, role: &str, content: &str) -> Result<(), String> {
116 let path = get_transcript_path(session_id);
117
118 let mut entries = if path.exists() {
119 let existing = std::fs::read_to_string(&path)
120 .map_err(|e| format!("Failed to read existing transcript: {}", e))?;
121 serde_json::from_str::<Vec<TranscriptEntry>>(&existing)
122 .map_err(|e| format!("Failed to parse existing transcript: {}", e))?
123 } else {
124 ensure_session_dir(session_id)
125 .map_err(|e| format!("Failed to create session dir: {}", e))?;
126 vec![]
127 };
128
129 entries.push(TranscriptEntry {
130 role: role.to_string(),
131 content: content.to_string(),
132 timestamp: Some(chrono::Utc::now().to_rfc3339()),
133 });
134
135 let json = serde_json::to_string_pretty(&entries)
136 .map_err(|e| format!("Failed to serialize transcript: {}", e))?;
137
138 std::fs::write(&path, json).map_err(|e| format!("Failed to write transcript: {}", e))?;
139
140 Ok(())
141}
142
143pub fn delete_session_storage(session_id: &str) -> Result<(), String> {
145 let session_dir = get_session_storage_dir().join(session_id);
146 if session_dir.exists() {
147 std::fs::remove_dir_all(&session_dir)
148 .map_err(|e| format!("Failed to delete session storage: {}", e))?;
149 }
150 Ok(())
151}
152
153pub fn flush_session_storage() -> Result<(), String> {
160 let session_dir = get_session_storage_dir();
161 if !session_dir.exists() {
162 return Ok(()); }
164
165 for entry in std::fs::read_dir(&session_dir)
166 .map_err(|e| format!("Failed to read session storage dir: {}", e))?
167 {
168 let entry = entry.map_err(|e| format!("Failed to read entry: {}", e))?;
169 if entry.file_type().map(|t| t.is_dir()).unwrap_or(false) {
170 let transcript = entry.path().join("transcript.json");
172 if transcript.exists() {
173 let mut f = std::fs::OpenOptions::new()
174 .write(true)
175 .open(&transcript)
176 .map_err(|e| format!("Failed to open {}: {}", transcript.display(), e))?;
177 let _ = f.sync_all(); }
179 let state = entry.path().join("state.json");
181 if state.exists() {
182 let mut f = std::fs::OpenOptions::new()
183 .write(true)
184 .open(&state)
185 .map_err(|e| format!("Failed to open {}: {}", state.display(), e))?;
186 let _ = f.sync_all();
187 }
188 }
189 }
190 Ok(())
191}
192
193pub fn list_stored_sessions() -> Vec<String> {
195 let dir = get_session_storage_dir();
196 if !dir.exists() {
197 return vec![];
198 }
199
200 let mut sessions = vec![];
201 if let Ok(entries) = std::fs::read_dir(&dir) {
202 for entry in entries.flatten() {
203 if entry.path().is_dir() {
204 if let Some(name) = entry.file_name().to_str() {
205 let transcript_path = entry.path().join("transcript.json");
206 if transcript_path.exists() {
207 sessions.push(name.to_string());
208 }
209 }
210 }
211 }
212 }
213 sessions
214}
215
216pub fn get_transcript_size(session_id: &str) -> u64 {
218 let path = get_transcript_path(session_id);
219 if !path.exists() {
220 return 0;
221 }
222 std::fs::metadata(&path).map(|m| m.len()).unwrap_or(0)
223}
224
225pub fn is_session_data_valid(session_id: &str) -> bool {
227 let path = get_transcript_path(session_id);
228 if !path.exists() {
229 return false;
230 }
231
232 match std::fs::read_to_string(&path) {
233 Ok(content) => {
234 serde_json::from_str::<Vec<TranscriptEntry>>(&content).is_ok()
235 || serde_json::from_str::<Vec<String>>(&content).is_ok()
236 }
237 Err(_) => false,
238 }
239}
240
241#[cfg(test)]
242mod tests {
243 use super::*;
244
245 #[test]
246 fn test_get_transcript_path() {
247 let path = get_transcript_path("test-session-123");
248 assert!(path.to_string_lossy().contains("test-session-123"));
249 assert!(path.to_string_lossy().contains("transcript.json"));
250 }
251
252 #[test]
253 fn test_get_session_state_path() {
254 let path = get_session_state_path("test-session-456");
255 assert!(path.to_string_lossy().contains("test-session-456"));
256 assert!(path.to_string_lossy().contains("state.json"));
257 }
258
259 #[test]
260 fn test_session_not_exists() {
261 assert!(!session_exists("nonexistent-session-xyz"));
262 }
263
264 #[test]
265 fn test_list_stored_sessions_empty() {
266 let sessions = list_stored_sessions();
267 assert!(sessions.is_empty());
268 }
269
270 #[test]
271 fn test_load_transcript_nonexistent() {
272 let result = load_transcript("nonexistent");
273 assert!(result.is_empty());
274 }
275
276 #[test]
277 fn test_get_transcript_size_nonexistent() {
278 let size = get_transcript_size("nonexistent");
279 assert_eq!(size, 0);
280 }
281
282 #[test]
283 fn test_is_session_data_valid_nonexistent() {
284 assert!(!is_session_data_valid("nonexistent"));
285 }
286}