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(
106 agent_id: &str,
107 task: &str,
108 label: Option<String>,
109 parent_key: Option<SessionKey>,
110 ) -> Self {
111 let now_ms = now_millis();
112 let run_id = generate_uuid();
113 Self {
114 key: generate_subagent_key(agent_id),
115 agent_id: agent_id.to_string(),
116 kind: SessionKind::Subagent,
117 status: SessionStatus::Active,
118 label,
119 task: Some(task.to_string()),
120 created_ms: now_ms,
121 finished_ms: None,
122 messages: Vec::new(),
123 run_id: Some(run_id),
124 parent_key,
125 }
126 }
127
128 pub fn add_message(&mut self, role: &str, content: &str) {
130 self.messages.push(SessionMessage {
131 role: role.to_string(),
132 content: content.to_string(),
133 timestamp_ms: now_millis(),
134 tool_name: None,
135 });
136
137 if self.messages.len() > 100 {
139 self.messages.remove(0);
140 }
141 }
142
143 pub fn complete(&mut self) {
145 self.status = SessionStatus::Completed;
146 self.finished_ms = Some(now_millis());
147 }
148
149 pub fn error(&mut self) {
151 self.status = SessionStatus::Error;
152 self.finished_ms = Some(now_millis());
153 }
154
155 pub fn runtime_secs(&self) -> u64 {
157 let end = self.finished_ms.unwrap_or_else(now_millis);
158 (end - self.created_ms) / 1000
159 }
160}
161
162pub struct SessionManager {
164 sessions: HashMap<SessionKey, Session>,
165 labels: HashMap<String, SessionKey>,
167}
168
169impl SessionManager {
170 pub fn new() -> Self {
172 Self {
173 sessions: HashMap::new(),
174 labels: HashMap::new(),
175 }
176 }
177
178 pub fn get_or_create_main(&mut self, agent_id: &str) -> &Session {
180 let key = format!("agent:{}:main", agent_id);
181 self.sessions
182 .entry(key.clone())
183 .or_insert_with(|| Session::new_main(agent_id))
184 }
185
186 pub fn spawn_subagent(
188 &mut self,
189 agent_id: &str,
190 task: &str,
191 label: Option<String>,
192 parent_key: Option<SessionKey>,
193 ) -> SessionKey {
194 let session = Session::new_subagent(agent_id, task, label.clone(), parent_key);
195 let key = session.key.clone();
196
197 if let Some(ref lbl) = label {
198 self.labels.insert(lbl.clone(), key.clone());
199 }
200
201 self.sessions.insert(key.clone(), session);
202 key
203 }
204
205 pub fn get(&self, key: &str) -> Option<&Session> {
207 self.sessions.get(key)
208 }
209
210 pub fn get_by_label(&self, label: &str) -> Option<&Session> {
212 self.labels.get(label).and_then(|k| self.sessions.get(k))
213 }
214
215 pub fn get_mut(&mut self, key: &str) -> Option<&mut Session> {
217 self.sessions.get_mut(key)
218 }
219
220 pub fn list(
222 &self,
223 kinds: Option<&[SessionKind]>,
224 active_only: bool,
225 limit: usize,
226 ) -> Vec<&Session> {
227 let mut sessions: Vec<_> = self
228 .sessions
229 .values()
230 .filter(|s| {
231 let kind_match = kinds.map(|ks| ks.contains(&s.kind)).unwrap_or(true);
232 let active_match = !active_only || s.status == SessionStatus::Active;
233 kind_match && active_match
234 })
235 .collect();
236
237 sessions.sort_by(|a, b| b.created_ms.cmp(&a.created_ms));
239 sessions.truncate(limit);
240 sessions
241 }
242
243 pub fn history(
245 &self,
246 key: &str,
247 limit: usize,
248 include_tools: bool,
249 ) -> Option<Vec<&SessionMessage>> {
250 self.sessions.get(key).map(|s| {
251 s.messages
252 .iter()
253 .filter(|m| include_tools || m.role != "tool")
254 .rev()
255 .take(limit)
256 .collect::<Vec<_>>()
257 .into_iter()
258 .rev()
259 .collect()
260 })
261 }
262
263 pub fn send_message(&mut self, key: &str, message: &str) -> Result<(), String> {
265 let session = self
266 .sessions
267 .get_mut(key)
268 .ok_or_else(|| format!("Session not found: {}", key))?;
269
270 if session.status != SessionStatus::Active {
271 return Err(format!("Session is not active: {:?}", session.status));
272 }
273
274 session.add_message("user", message);
275 Ok(())
276 }
277
278 pub fn complete_session(&mut self, key: &str) -> Result<(), String> {
280 let session = self
281 .sessions
282 .get_mut(key)
283 .ok_or_else(|| format!("Session not found: {}", key))?;
284 session.complete();
285 Ok(())
286 }
287}
288
289impl Default for SessionManager {
290 fn default() -> Self {
291 Self::new()
292 }
293}
294
295pub type SharedSessionManager = Arc<Mutex<SessionManager>>;
297
298static SESSION_MANAGER: OnceLock<SharedSessionManager> = OnceLock::new();
300
301pub fn session_manager() -> &'static SharedSessionManager {
303 SESSION_MANAGER.get_or_init(|| Arc::new(Mutex::new(SessionManager::new())))
304}
305
306#[derive(Debug, Clone, Serialize, Deserialize)]
308#[serde(rename_all = "camelCase")]
309pub struct SpawnResult {
310 pub status: String,
311 pub run_id: String,
312 pub session_key: SessionKey,
313 pub message: String,
314}
315
316fn now_millis() -> u64 {
318 SystemTime::now()
319 .duration_since(UNIX_EPOCH)
320 .unwrap_or_default()
321 .as_millis() as u64
322}
323
324#[cfg(test)]
325mod tests {
326 use super::*;
327
328 #[test]
329 fn test_session_creation() {
330 let session = Session::new_main("main");
331 assert_eq!(session.key, "agent:main:main");
332 assert_eq!(session.kind, SessionKind::Main);
333 assert_eq!(session.status, SessionStatus::Active);
334 }
335
336 #[test]
337 fn test_subagent_spawn() {
338 let mut manager = SessionManager::new();
339 let key =
340 manager.spawn_subagent("main", "Research task", Some("research".to_string()), None);
341
342 assert!(key.contains("subagent"));
343
344 let session = manager.get(&key).unwrap();
345 assert_eq!(session.kind, SessionKind::Subagent);
346 assert_eq!(session.task, Some("Research task".to_string()));
347 assert_eq!(session.label, Some("research".to_string()));
348
349 let by_label = manager.get_by_label("research").unwrap();
351 assert_eq!(by_label.key, key);
352 }
353
354 #[test]
355 fn test_message_history() {
356 let mut manager = SessionManager::new();
357 let key = manager.spawn_subagent("main", "Test", None, None);
358
359 manager.send_message(&key, "Hello").unwrap();
360
361 let session = manager.get_mut(&key).unwrap();
362 session.add_message("assistant", "Hi there!");
363
364 let history = manager.history(&key, 10, false).unwrap();
365 assert_eq!(history.len(), 2);
366 assert_eq!(history[0].content, "Hello");
367 assert_eq!(history[1].content, "Hi there!");
368 }
369
370 #[test]
371 fn test_session_listing() {
372 let mut manager = SessionManager::new();
373 manager.get_or_create_main("main");
374 manager.spawn_subagent("main", "Task 1", None, None);
375 manager.spawn_subagent("main", "Task 2", None, None);
376
377 let all = manager.list(None, false, 10);
378 assert_eq!(all.len(), 3);
379
380 let subagents = manager.list(Some(&[SessionKind::Subagent]), false, 10);
381 assert_eq!(subagents.len(), 2);
382 }
383
384 #[test]
385 fn test_subagent_appears_in_active_list() {
386 let mut manager = SessionManager::new();
389
390 let key = manager.spawn_subagent("main", "Test task", Some("test".to_string()), None);
392
393 let active = manager.list(Some(&[SessionKind::Subagent]), true, 10);
395 assert_eq!(active.len(), 1, "Subagent should appear in active list");
396 assert_eq!(active[0].key, key);
397 assert_eq!(active[0].status, SessionStatus::Active);
398 assert_eq!(active[0].label, Some("test".to_string()));
399 }
400}