neuron_runtime/
session.rs1use std::collections::HashMap;
4use std::future::Future;
5use std::path::PathBuf;
6use std::sync::Arc;
7
8use chrono::{DateTime, Utc};
9use serde::{Deserialize, Serialize};
10use tokio::sync::RwLock;
11
12use neuron_types::{Message, StorageError, TokenUsage, WasmCompatSend, WasmCompatSync};
13
14#[derive(Debug, Clone, Serialize, Deserialize)]
16pub struct Session {
17 pub id: String,
19 pub messages: Vec<Message>,
21 pub state: SessionState,
23 pub created_at: DateTime<Utc>,
25 pub updated_at: DateTime<Utc>,
27}
28
29impl Session {
30 #[must_use]
32 pub fn new(id: impl Into<String>, cwd: PathBuf) -> Self {
33 let now = Utc::now();
34 Self {
35 id: id.into(),
36 messages: Vec::new(),
37 state: SessionState {
38 cwd,
39 token_usage: TokenUsage::default(),
40 event_count: 0,
41 custom: HashMap::new(),
42 },
43 created_at: now,
44 updated_at: now,
45 }
46 }
47
48 #[must_use]
50 pub fn summary(&self) -> SessionSummary {
51 SessionSummary {
52 id: self.id.clone(),
53 created_at: self.created_at,
54 updated_at: self.updated_at,
55 message_count: self.messages.len(),
56 }
57 }
58}
59
60#[derive(Debug, Clone, Serialize, Deserialize)]
62pub struct SessionState {
63 pub cwd: PathBuf,
65 pub token_usage: TokenUsage,
67 pub event_count: u64,
69 pub custom: HashMap<String, serde_json::Value>,
71}
72
73#[derive(Debug, Clone, Serialize, Deserialize)]
75pub struct SessionSummary {
76 pub id: String,
78 pub created_at: DateTime<Utc>,
80 pub updated_at: DateTime<Utc>,
82 pub message_count: usize,
84}
85
86pub trait SessionStorage: WasmCompatSend + WasmCompatSync {
100 fn save(
102 &self,
103 session: &Session,
104 ) -> impl Future<Output = Result<(), StorageError>> + WasmCompatSend;
105
106 fn load(
108 &self,
109 id: &str,
110 ) -> impl Future<Output = Result<Session, StorageError>> + WasmCompatSend;
111
112 fn list(
114 &self,
115 ) -> impl Future<Output = Result<Vec<SessionSummary>, StorageError>> + WasmCompatSend;
116
117 fn delete(
119 &self,
120 id: &str,
121 ) -> impl Future<Output = Result<(), StorageError>> + WasmCompatSend;
122}
123
124#[derive(Debug, Clone)]
128pub struct InMemorySessionStorage {
129 sessions: Arc<RwLock<HashMap<String, Session>>>,
130}
131
132impl InMemorySessionStorage {
133 #[must_use]
135 pub fn new() -> Self {
136 Self {
137 sessions: Arc::new(RwLock::new(HashMap::new())),
138 }
139 }
140}
141
142impl Default for InMemorySessionStorage {
143 fn default() -> Self {
144 Self::new()
145 }
146}
147
148impl SessionStorage for InMemorySessionStorage {
149 async fn save(&self, session: &Session) -> Result<(), StorageError> {
150 let mut map = self.sessions.write().await;
151 map.insert(session.id.clone(), session.clone());
152 Ok(())
153 }
154
155 async fn load(&self, id: &str) -> Result<Session, StorageError> {
156 let map = self.sessions.read().await;
157 map.get(id)
158 .cloned()
159 .ok_or_else(|| StorageError::NotFound(id.to_string()))
160 }
161
162 async fn list(&self) -> Result<Vec<SessionSummary>, StorageError> {
163 let map = self.sessions.read().await;
164 Ok(map.values().map(|s| s.summary()).collect())
165 }
166
167 async fn delete(&self, id: &str) -> Result<(), StorageError> {
168 let mut map = self.sessions.write().await;
169 map.remove(id)
170 .ok_or_else(|| StorageError::NotFound(id.to_string()))?;
171 Ok(())
172 }
173}
174
175#[derive(Debug, Clone)]
179pub struct FileSessionStorage {
180 directory: PathBuf,
181}
182
183impl FileSessionStorage {
184 #[must_use]
188 pub fn new(directory: PathBuf) -> Self {
189 Self { directory }
190 }
191
192 fn path_for(&self, id: &str) -> PathBuf {
194 self.directory.join(format!("{id}.json"))
195 }
196}
197
198impl SessionStorage for FileSessionStorage {
199 async fn save(&self, session: &Session) -> Result<(), StorageError> {
200 tokio::fs::create_dir_all(&self.directory).await?;
201 let json = serde_json::to_string_pretty(session)
202 .map_err(|e| StorageError::Serialization(e.to_string()))?;
203 tokio::fs::write(self.path_for(&session.id), json).await?;
204 Ok(())
205 }
206
207 async fn load(&self, id: &str) -> Result<Session, StorageError> {
208 let path = self.path_for(id);
209 let data = tokio::fs::read_to_string(&path).await.map_err(|e| {
210 if e.kind() == std::io::ErrorKind::NotFound {
211 StorageError::NotFound(id.to_string())
212 } else {
213 StorageError::Io(e)
214 }
215 })?;
216 let session: Session = serde_json::from_str(&data)
217 .map_err(|e| StorageError::Serialization(e.to_string()))?;
218 Ok(session)
219 }
220
221 async fn list(&self) -> Result<Vec<SessionSummary>, StorageError> {
222 let mut summaries = Vec::new();
223 let mut entries = match tokio::fs::read_dir(&self.directory).await {
224 Ok(entries) => entries,
225 Err(e) if e.kind() == std::io::ErrorKind::NotFound => return Ok(summaries),
226 Err(e) => return Err(StorageError::Io(e)),
227 };
228 while let Some(entry) = entries.next_entry().await? {
229 let path = entry.path();
230 if path.extension().is_some_and(|ext| ext == "json") {
231 let data = tokio::fs::read_to_string(&path).await?;
232 if let Ok(session) = serde_json::from_str::<Session>(&data) {
233 summaries.push(session.summary());
234 }
235 }
236 }
237 Ok(summaries)
238 }
239
240 async fn delete(&self, id: &str) -> Result<(), StorageError> {
241 let path = self.path_for(id);
242 tokio::fs::remove_file(&path).await.map_err(|e| {
243 if e.kind() == std::io::ErrorKind::NotFound {
244 StorageError::NotFound(id.to_string())
245 } else {
246 StorageError::Io(e)
247 }
248 })
249 }
250}