1use serde::{Deserialize, Serialize};
7use std::collections::HashMap;
8use std::sync::{Arc, Mutex, OnceLock};
9use std::time::{SystemTime, UNIX_EPOCH};
10
11pub type SessionKey = String;
13
14fn generate_subagent_key(agent_id: &str) -> SessionKey {
16 let uuid = generate_uuid();
17 format!("agent:{}:subagent:{}", agent_id, uuid)
18}
19
20fn generate_uuid() -> String {
22 let timestamp = SystemTime::now()
23 .duration_since(UNIX_EPOCH)
24 .unwrap_or_default()
25 .as_nanos();
26 format!("{:x}", timestamp)
27}
28
29#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
31#[serde(rename_all = "camelCase")]
32pub enum SessionStatus {
33 Active,
34 Completed,
35 Error,
36 Timeout,
37 Stopped,
38}
39
40#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
42#[serde(rename_all = "camelCase")]
43pub enum SessionKind {
44 Main,
45 Subagent,
46 Cron,
47}
48
49#[derive(Debug, Clone, Serialize, Deserialize)]
51#[serde(rename_all = "camelCase")]
52pub struct SessionMessage {
53 pub role: String, pub content: String,
55 pub timestamp_ms: u64,
56 #[serde(skip_serializing_if = "Option::is_none")]
57 pub tool_name: Option<String>,
58}
59
60#[derive(Debug, Clone, Serialize, Deserialize)]
62#[serde(rename_all = "camelCase")]
63pub struct Session {
64 pub key: SessionKey,
65 pub agent_id: String,
66 pub kind: SessionKind,
67 pub status: SessionStatus,
68 #[serde(skip_serializing_if = "Option::is_none")]
69 pub label: Option<String>,
70 #[serde(skip_serializing_if = "Option::is_none")]
71 pub task: Option<String>,
72 pub created_ms: u64,
73 #[serde(skip_serializing_if = "Option::is_none")]
74 pub finished_ms: Option<u64>,
75 pub messages: Vec<SessionMessage>,
77 #[serde(skip_serializing_if = "Option::is_none")]
79 pub run_id: Option<String>,
80 #[serde(skip_serializing_if = "Option::is_none")]
82 pub parent_key: Option<SessionKey>,
83}
84
85impl Session {
86 pub fn new_main(agent_id: &str) -> Self {
88 let now_ms = now_millis();
89 Self {
90 key: format!("agent:{}:main", agent_id),
91 agent_id: agent_id.to_string(),
92 kind: SessionKind::Main,
93 status: SessionStatus::Active,
94 label: None,
95 task: None,
96 created_ms: now_ms,
97 finished_ms: None,
98 messages: Vec::new(),
99 run_id: None,
100 parent_key: None,
101 }
102 }
103
104 pub fn new_subagent(agent_id: &str, task: &str, label: Option<String>, parent_key: Option<SessionKey>) -> Self {
106 let now_ms = now_millis();
107 let run_id = generate_uuid();
108 Self {
109 key: generate_subagent_key(agent_id),
110 agent_id: agent_id.to_string(),
111 kind: SessionKind::Subagent,
112 status: SessionStatus::Active,
113 label,
114 task: Some(task.to_string()),
115 created_ms: now_ms,
116 finished_ms: None,
117 messages: Vec::new(),
118 run_id: Some(run_id),
119 parent_key,
120 }
121 }
122
123 pub fn add_message(&mut self, role: &str, content: &str) {
125 self.messages.push(SessionMessage {
126 role: role.to_string(),
127 content: content.to_string(),
128 timestamp_ms: now_millis(),
129 tool_name: None,
130 });
131
132 if self.messages.len() > 100 {
134 self.messages.remove(0);
135 }
136 }
137
138 pub fn complete(&mut self) {
140 self.status = SessionStatus::Completed;
141 self.finished_ms = Some(now_millis());
142 }
143
144 pub fn error(&mut self) {
146 self.status = SessionStatus::Error;
147 self.finished_ms = Some(now_millis());
148 }
149
150 pub fn runtime_secs(&self) -> u64 {
152 let end = self.finished_ms.unwrap_or_else(now_millis);
153 (end - self.created_ms) / 1000
154 }
155}
156
157pub struct SessionManager {
159 sessions: HashMap<SessionKey, Session>,
160 labels: HashMap<String, SessionKey>,
162}
163
164impl SessionManager {
165 pub fn new() -> Self {
167 Self {
168 sessions: HashMap::new(),
169 labels: HashMap::new(),
170 }
171 }
172
173 pub fn get_or_create_main(&mut self, agent_id: &str) -> &Session {
175 let key = format!("agent:{}:main", agent_id);
176 self.sessions
177 .entry(key.clone())
178 .or_insert_with(|| Session::new_main(agent_id))
179 }
180
181 pub fn spawn_subagent(
183 &mut self,
184 agent_id: &str,
185 task: &str,
186 label: Option<String>,
187 parent_key: Option<SessionKey>,
188 ) -> SessionKey {
189 let session = Session::new_subagent(agent_id, task, label.clone(), parent_key);
190 let key = session.key.clone();
191
192 if let Some(ref lbl) = label {
193 self.labels.insert(lbl.clone(), key.clone());
194 }
195
196 self.sessions.insert(key.clone(), session);
197 key
198 }
199
200 pub fn get(&self, key: &str) -> Option<&Session> {
202 self.sessions.get(key)
203 }
204
205 pub fn get_by_label(&self, label: &str) -> Option<&Session> {
207 self.labels.get(label).and_then(|k| self.sessions.get(k))
208 }
209
210 pub fn get_mut(&mut self, key: &str) -> Option<&mut Session> {
212 self.sessions.get_mut(key)
213 }
214
215 pub fn list(&self, kinds: Option<&[SessionKind]>, active_only: bool, limit: usize) -> Vec<&Session> {
217 let mut sessions: Vec<_> = self
218 .sessions
219 .values()
220 .filter(|s| {
221 let kind_match = kinds
222 .map(|ks| ks.contains(&s.kind))
223 .unwrap_or(true);
224 let active_match = !active_only || s.status == SessionStatus::Active;
225 kind_match && active_match
226 })
227 .collect();
228
229 sessions.sort_by(|a, b| b.created_ms.cmp(&a.created_ms));
231 sessions.truncate(limit);
232 sessions
233 }
234
235 pub fn history(&self, key: &str, limit: usize, include_tools: bool) -> Option<Vec<&SessionMessage>> {
237 self.sessions.get(key).map(|s| {
238 s.messages
239 .iter()
240 .filter(|m| include_tools || m.role != "tool")
241 .rev()
242 .take(limit)
243 .collect::<Vec<_>>()
244 .into_iter()
245 .rev()
246 .collect()
247 })
248 }
249
250 pub fn send_message(&mut self, key: &str, message: &str) -> Result<(), String> {
252 let session = self
253 .sessions
254 .get_mut(key)
255 .ok_or_else(|| format!("Session not found: {}", key))?;
256
257 if session.status != SessionStatus::Active {
258 return Err(format!("Session is not active: {:?}", session.status));
259 }
260
261 session.add_message("user", message);
262 Ok(())
263 }
264
265 pub fn complete_session(&mut self, key: &str) -> Result<(), String> {
267 let session = self
268 .sessions
269 .get_mut(key)
270 .ok_or_else(|| format!("Session not found: {}", key))?;
271 session.complete();
272 Ok(())
273 }
274}
275
276impl Default for SessionManager {
277 fn default() -> Self {
278 Self::new()
279 }
280}
281
282pub type SharedSessionManager = Arc<Mutex<SessionManager>>;
284
285static SESSION_MANAGER: OnceLock<SharedSessionManager> = OnceLock::new();
287
288pub fn session_manager() -> &'static SharedSessionManager {
290 SESSION_MANAGER.get_or_init(|| Arc::new(Mutex::new(SessionManager::new())))
291}
292
293#[derive(Debug, Clone, Serialize, Deserialize)]
295#[serde(rename_all = "camelCase")]
296pub struct SpawnResult {
297 pub status: String,
298 pub run_id: String,
299 pub session_key: SessionKey,
300 pub message: String,
301}
302
303fn now_millis() -> u64 {
305 SystemTime::now()
306 .duration_since(UNIX_EPOCH)
307 .unwrap_or_default()
308 .as_millis() as u64
309}
310
311#[cfg(test)]
312mod tests {
313 use super::*;
314
315 #[test]
316 fn test_session_creation() {
317 let session = Session::new_main("main");
318 assert_eq!(session.key, "agent:main:main");
319 assert_eq!(session.kind, SessionKind::Main);
320 assert_eq!(session.status, SessionStatus::Active);
321 }
322
323 #[test]
324 fn test_subagent_spawn() {
325 let mut manager = SessionManager::new();
326 let key = manager.spawn_subagent("main", "Research task", Some("research".to_string()), None);
327
328 assert!(key.contains("subagent"));
329
330 let session = manager.get(&key).unwrap();
331 assert_eq!(session.kind, SessionKind::Subagent);
332 assert_eq!(session.task, Some("Research task".to_string()));
333 assert_eq!(session.label, Some("research".to_string()));
334
335 let by_label = manager.get_by_label("research").unwrap();
337 assert_eq!(by_label.key, key);
338 }
339
340 #[test]
341 fn test_message_history() {
342 let mut manager = SessionManager::new();
343 let key = manager.spawn_subagent("main", "Test", None, None);
344
345 manager.send_message(&key, "Hello").unwrap();
346
347 let session = manager.get_mut(&key).unwrap();
348 session.add_message("assistant", "Hi there!");
349
350 let history = manager.history(&key, 10, false).unwrap();
351 assert_eq!(history.len(), 2);
352 assert_eq!(history[0].content, "Hello");
353 assert_eq!(history[1].content, "Hi there!");
354 }
355
356 #[test]
357 fn test_session_listing() {
358 let mut manager = SessionManager::new();
359 manager.get_or_create_main("main");
360 manager.spawn_subagent("main", "Task 1", None, None);
361 manager.spawn_subagent("main", "Task 2", None, None);
362
363 let all = manager.list(None, false, 10);
364 assert_eq!(all.len(), 3);
365
366 let subagents = manager.list(Some(&[SessionKind::Subagent]), false, 10);
367 assert_eq!(subagents.len(), 2);
368 }
369}