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(&self, id: &str) -> impl Future<Output = Result<(), StorageError>> + WasmCompatSend;
119}
120
121#[derive(Debug, Clone)]
125pub struct InMemorySessionStorage {
126 sessions: Arc<RwLock<HashMap<String, Session>>>,
127}
128
129impl InMemorySessionStorage {
130 #[must_use]
132 pub fn new() -> Self {
133 Self {
134 sessions: Arc::new(RwLock::new(HashMap::new())),
135 }
136 }
137}
138
139impl Default for InMemorySessionStorage {
140 fn default() -> Self {
141 Self::new()
142 }
143}
144
145impl SessionStorage for InMemorySessionStorage {
146 async fn save(&self, session: &Session) -> Result<(), StorageError> {
147 let mut map = self.sessions.write().await;
148 map.insert(session.id.clone(), session.clone());
149 Ok(())
150 }
151
152 async fn load(&self, id: &str) -> Result<Session, StorageError> {
153 let map = self.sessions.read().await;
154 map.get(id)
155 .cloned()
156 .ok_or_else(|| StorageError::NotFound(id.to_string()))
157 }
158
159 async fn list(&self) -> Result<Vec<SessionSummary>, StorageError> {
160 let map = self.sessions.read().await;
161 Ok(map.values().map(|s| s.summary()).collect())
162 }
163
164 async fn delete(&self, id: &str) -> Result<(), StorageError> {
165 let mut map = self.sessions.write().await;
166 map.remove(id)
167 .ok_or_else(|| StorageError::NotFound(id.to_string()))?;
168 Ok(())
169 }
170}
171
172#[derive(Debug, Clone)]
176pub struct FileSessionStorage {
177 directory: PathBuf,
178}
179
180impl FileSessionStorage {
181 #[must_use]
185 pub fn new(directory: PathBuf) -> Self {
186 Self { directory }
187 }
188
189 fn path_for(&self, id: &str) -> PathBuf {
191 self.directory.join(format!("{id}.json"))
192 }
193}
194
195impl SessionStorage for FileSessionStorage {
196 async fn save(&self, session: &Session) -> Result<(), StorageError> {
197 tokio::fs::create_dir_all(&self.directory).await?;
198 let json = serde_json::to_string_pretty(session)
199 .map_err(|e| StorageError::Serialization(e.to_string()))?;
200 tokio::fs::write(self.path_for(&session.id), json).await?;
201 Ok(())
202 }
203
204 async fn load(&self, id: &str) -> Result<Session, StorageError> {
205 let path = self.path_for(id);
206 let data = tokio::fs::read_to_string(&path).await.map_err(|e| {
207 if e.kind() == std::io::ErrorKind::NotFound {
208 StorageError::NotFound(id.to_string())
209 } else {
210 StorageError::Io(e)
211 }
212 })?;
213 let session: Session =
214 serde_json::from_str(&data).map_err(|e| StorageError::Serialization(e.to_string()))?;
215 Ok(session)
216 }
217
218 async fn list(&self) -> Result<Vec<SessionSummary>, StorageError> {
219 let mut summaries = Vec::new();
220 let mut entries = match tokio::fs::read_dir(&self.directory).await {
221 Ok(entries) => entries,
222 Err(e) if e.kind() == std::io::ErrorKind::NotFound => return Ok(summaries),
223 Err(e) => return Err(StorageError::Io(e)),
224 };
225 while let Some(entry) = entries.next_entry().await? {
226 let path = entry.path();
227 if path.extension().is_some_and(|ext| ext == "json") {
228 let data = tokio::fs::read_to_string(&path).await?;
229 if let Ok(session) = serde_json::from_str::<Session>(&data) {
230 summaries.push(session.summary());
231 }
232 }
233 }
234 Ok(summaries)
235 }
236
237 async fn delete(&self, id: &str) -> Result<(), StorageError> {
238 let path = self.path_for(id);
239 tokio::fs::remove_file(&path).await.map_err(|e| {
240 if e.kind() == std::io::ErrorKind::NotFound {
241 StorageError::NotFound(id.to_string())
242 } else {
243 StorageError::Io(e)
244 }
245 })
246 }
247}