1use std::collections::HashMap;
73use std::path::PathBuf;
74use std::sync::Arc;
75use std::time::Duration;
76
77use anyhow::Result;
78use chrono::{DateTime, Utc};
79use dashmap::DashMap;
80use serde::{Deserialize, Serialize};
81use tokio::sync::RwLock;
82use uuid::Uuid;
83
84pub mod attention;
85pub mod headless;
86pub mod lifecycle;
87pub mod process;
88pub mod pty;
89pub mod terminal;
90
91pub use attention::AttentionState;
92use attention::AttentionTracker;
93
94use crate::context::SessionContext;
95use crate::persistence::CommandRecord;
96
97#[derive(Debug, thiserror::Error)]
99pub enum SessionError {
100 #[error("Session not found: {0}")]
101 NotFound(SessionId),
102
103 #[error("Session already exists: {0}")]
104 AlreadyExists(SessionId),
105
106 #[error("PTY error: {0}")]
107 PtyError(String),
108
109 #[error("Process error: {0}")]
110 ProcessError(String),
111
112 #[error("IO error: {0}")]
113 IoError(#[from] std::io::Error),
114
115 #[error("Other error: {0}")]
116 Other(#[from] anyhow::Error),
117}
118
119pub type SessionResult<T> = std::result::Result<T, SessionError>;
121
122#[derive(Debug, Clone, Hash, Eq, PartialEq, Serialize, Deserialize)]
124pub struct SessionId(Uuid);
125
126impl Default for SessionId {
127 fn default() -> Self {
128 Self::new()
129 }
130}
131
132impl SessionId {
133 pub fn new() -> Self {
135 Self(Uuid::new_v4())
136 }
137
138 pub fn new_v4() -> Self {
140 Self(Uuid::new_v4())
141 }
142
143 pub fn parse_str(s: &str) -> Result<Self> {
145 Ok(Self(Uuid::parse_str(s)?))
146 }
147
148 pub fn to_string(&self) -> String {
150 self.0.to_string()
151 }
152
153 pub fn as_uuid(&self) -> &Uuid {
155 &self.0
156 }
157}
158
159impl std::fmt::Display for SessionId {
160 fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
161 write!(f, "{}", self.0)
162 }
163}
164
165#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize, Default)]
167pub enum SessionStatus {
168 #[default]
170 Initializing,
171 Running,
173 Paused,
175 Terminating,
177 Terminated,
179 Error,
181}
182
183#[derive(Debug, Clone, Serialize, Deserialize)]
185#[serde(default)]
186pub struct SessionConfig {
187 pub name: Option<String>,
189 pub working_directory: PathBuf,
191 pub environment: HashMap<String, String>,
193 pub shell: Option<String>,
195 pub shell_command: Option<String>,
197 pub pty_size: (u16, u16),
199 pub output_buffer_size: usize,
201 pub timeout: Option<Duration>,
203 pub compress_output: bool,
205 pub parse_output: bool,
207 pub enable_ai_features: bool,
209 pub context_config: ContextConfig,
211 pub agent_role: Option<String>,
213 pub force_headless: bool,
215 pub allow_headless_fallback: bool,
217}
218
219#[derive(Debug, Clone, Serialize, Deserialize)]
221#[serde(default)]
222pub struct ContextConfig {
223 pub max_tokens: usize,
225 pub compression_threshold: f64,
227}
228
229impl Default for SessionConfig {
230 fn default() -> Self {
231 Self {
232 name: None,
233 working_directory: std::env::current_dir().unwrap_or_else(|_| PathBuf::from("/")),
234 environment: HashMap::new(),
235 shell: None,
236 shell_command: None,
237 pty_size: (24, 80),
238 output_buffer_size: 1024 * 1024, timeout: None,
240 compress_output: true,
241 parse_output: true,
242 enable_ai_features: false,
243 context_config: ContextConfig::default(),
244 agent_role: None,
245 force_headless: false,
246 allow_headless_fallback: true,
247 }
248 }
249}
250
251impl Default for ContextConfig {
252 fn default() -> Self {
253 Self {
254 max_tokens: 4096,
255 compression_threshold: 0.8,
256 }
257 }
258}
259
260pub struct AISession {
262 pub id: SessionId,
264 pub config: SessionConfig,
266 pub status: RwLock<SessionStatus>,
268 pub context: Arc<RwLock<SessionContext>>,
270 process: Arc<RwLock<Option<process::ProcessHandle>>>,
272 terminal: Arc<RwLock<Option<terminal::TerminalHandle>>>,
274 pub created_at: DateTime<Utc>,
276 pub last_activity: Arc<RwLock<DateTime<Utc>>>,
278 pub metadata: Arc<RwLock<HashMap<String, serde_json::Value>>>,
280 pub command_history: Arc<RwLock<Vec<CommandRecord>>>,
282 pub command_count: Arc<RwLock<usize>>,
284 pub total_tokens: Arc<RwLock<usize>>,
286 attention: AttentionTracker,
288}
289
290impl AISession {
291 pub async fn new(config: SessionConfig) -> Result<Self> {
293 let id = SessionId::new();
294 let now = Utc::now();
295
296 Ok(Self {
297 id: id.clone(),
298 config,
299 status: RwLock::new(SessionStatus::Initializing),
300 context: Arc::new(RwLock::new(SessionContext::new(id))),
301 process: Arc::new(RwLock::new(None)),
302 terminal: Arc::new(RwLock::new(None)),
303 created_at: now,
304 last_activity: Arc::new(RwLock::new(now)),
305 metadata: Arc::new(RwLock::new(HashMap::new())),
306 command_history: Arc::new(RwLock::new(Vec::new())),
307 command_count: Arc::new(RwLock::new(0)),
308 total_tokens: Arc::new(RwLock::new(0)),
309 attention: AttentionTracker::new(AttentionState::Idle),
310 })
311 }
312
313 pub async fn new_with_id(
315 id: SessionId,
316 config: SessionConfig,
317 created_at: DateTime<Utc>,
318 ) -> Result<Self> {
319 let now = Utc::now();
320
321 Ok(Self {
322 id: id.clone(),
323 config,
324 status: RwLock::new(SessionStatus::Initializing),
325 context: Arc::new(RwLock::new(SessionContext::new(id))),
326 process: Arc::new(RwLock::new(None)),
327 terminal: Arc::new(RwLock::new(None)),
328 created_at,
329 last_activity: Arc::new(RwLock::new(now)),
330 metadata: Arc::new(RwLock::new(HashMap::new())),
331 command_history: Arc::new(RwLock::new(Vec::new())),
332 command_count: Arc::new(RwLock::new(0)),
333 total_tokens: Arc::new(RwLock::new(0)),
334 attention: AttentionTracker::new(AttentionState::Idle),
335 })
336 }
337
338 pub async fn start(&self) -> Result<()> {
340 lifecycle::start_session(self).await
341 }
342
343 pub async fn stop(&self) -> Result<()> {
345 lifecycle::stop_session(self).await
346 }
347
348 pub async fn send_input(&self, input: &str) -> Result<()> {
350 let terminal_guard = self.terminal.read().await;
351 if let Some(terminal) = terminal_guard.as_ref() {
352 terminal.write(input.as_bytes()).await?;
353 *self.last_activity.write().await = Utc::now();
354 Ok(())
355 } else {
356 Err(anyhow::anyhow!("Session not started"))
357 }
358 }
359
360 pub async fn read_output(&self) -> Result<Vec<u8>> {
362 let terminal = self.terminal.read().await;
363 if let Some(terminal) = terminal.as_ref() {
364 let output = terminal.read().await?;
365 *self.last_activity.write().await = Utc::now();
366 Ok(output)
367 } else {
368 Err(anyhow::anyhow!("Session not started"))
369 }
370 }
371
372 pub async fn status(&self) -> SessionStatus {
374 *self.status.read().await
375 }
376
377 pub async fn set_metadata(&self, key: String, value: serde_json::Value) -> Result<()> {
379 self.metadata.write().await.insert(key, value);
380 Ok(())
381 }
382
383 pub async fn get_metadata(&self, key: &str) -> Option<serde_json::Value> {
385 self.metadata.read().await.get(key).cloned()
386 }
387
388 pub async fn execute_command(&self, command: &str) -> Result<String> {
390 let start_time = Utc::now();
391
392 let shell_env = std::env::var("SHELL").ok();
393 let shell = self
394 .config
395 .shell
396 .as_deref()
397 .or(shell_env.as_deref())
398 .unwrap_or("/bin/sh");
399 let mut cmd = tokio::process::Command::new(shell);
400 cmd.arg("-lc")
401 .arg(command)
402 .current_dir(&self.config.working_directory);
403 for (key, value) in &self.config.environment {
404 cmd.env(key, value);
405 }
406
407 let execution =
408 crate::execution::run_provider_command(cmd, &self.config.working_directory, shell)
409 .await?;
410 *self.last_activity.write().await = Utc::now();
411 let output = if execution.stderr.is_empty() {
412 execution.stdout
413 } else if execution.stdout.is_empty() {
414 execution.stderr
415 } else {
416 format!("{}{}", execution.stdout, execution.stderr)
417 };
418
419 let end_time = Utc::now();
421 let duration_ms = execution
422 .duration_ms
423 .max((end_time - start_time).num_milliseconds() as u64);
424
425 let record = CommandRecord {
426 command: command.to_string(),
427 timestamp: start_time,
428 exit_code: execution.status.code(),
429 output_preview: if output.len() > 200 {
430 format!("{}...", &output[..200])
431 } else {
432 output.clone()
433 },
434 duration_ms,
435 };
436
437 self.command_history.write().await.push(record);
439 *self.command_count.write().await += 1;
440
441 Ok(output)
442 }
443
444 pub async fn add_tokens(&self, token_count: usize) {
446 *self.total_tokens.write().await += token_count;
447 }
448
449 pub async fn get_command_history(&self) -> Vec<CommandRecord> {
451 self.command_history.read().await.clone()
452 }
453
454 pub async fn get_command_count(&self) -> usize {
456 *self.command_count.read().await
457 }
458
459 pub async fn get_total_tokens(&self) -> usize {
461 *self.total_tokens.read().await
462 }
463
464 pub fn attention(&self) -> AttentionState {
466 self.attention.get()
467 }
468
469 pub fn subscribe_attention(&self) -> tokio::sync::watch::Receiver<AttentionState> {
474 self.attention.subscribe()
475 }
476
477 pub fn set_attention(&self, state: AttentionState) -> bool {
479 self.attention.set(state)
480 }
481
482 pub fn update_attention_from_parsed(
486 &self,
487 parsed: &crate::output::ParsedOutput,
488 ) -> Option<AttentionState> {
489 let next = AttentionState::from_parsed(parsed)?;
490 self.attention.set(next);
491 Some(next)
492 }
493
494 pub async fn trim_command_history(&self, keep_recent: usize) {
496 let mut history = self.command_history.write().await;
497 if history.len() > keep_recent {
498 let start_index = history.len() - keep_recent;
499 history.drain(0..start_index);
500 }
501 }
502}
503
504pub struct SessionManager {
598 sessions: Arc<DashMap<SessionId, Arc<AISession>>>,
600 default_config: SessionConfig,
602}
603
604impl SessionManager {
605 pub fn new() -> Self {
607 Self {
608 sessions: Arc::new(DashMap::new()),
609 default_config: SessionConfig::default(),
610 }
611 }
612
613 pub async fn create_session(&self) -> Result<Arc<AISession>> {
615 self.create_session_with_config(self.default_config.clone())
616 .await
617 }
618
619 pub async fn create_session_with_config(
621 &self,
622 config: SessionConfig,
623 ) -> Result<Arc<AISession>> {
624 let session = Arc::new(AISession::new(config).await?);
625 self.sessions.insert(session.id.clone(), session.clone());
626 Ok(session)
627 }
628
629 pub async fn restore_session(
631 &self,
632 id: SessionId,
633 config: SessionConfig,
634 created_at: DateTime<Utc>,
635 ) -> Result<Arc<AISession>> {
636 if self.sessions.contains_key(&id) {
638 return Err(SessionError::AlreadyExists(id).into());
639 }
640
641 let session = Arc::new(AISession::new_with_id(id.clone(), config, created_at).await?);
642 self.sessions.insert(id, session.clone());
643 Ok(session)
644 }
645
646 pub fn get_session(&self, id: &SessionId) -> Option<Arc<AISession>> {
648 self.sessions.get(id).map(|entry| entry.clone())
649 }
650
651 pub fn list_sessions(&self) -> Vec<SessionId> {
653 self.sessions
654 .iter()
655 .map(|entry| entry.key().clone())
656 .collect()
657 }
658
659 pub fn list_session_refs(&self) -> Vec<Arc<AISession>> {
661 self.sessions
662 .iter()
663 .map(|entry| entry.value().clone())
664 .collect()
665 }
666
667 pub async fn remove_session(&self, id: &SessionId) -> Result<()> {
669 if let Some((_, session)) = self.sessions.remove(id) {
670 session.stop().await?;
671 }
672 Ok(())
673 }
674
675 pub async fn cleanup_terminated(&self) -> Result<usize> {
677 let mut removed = 0;
678 let terminated_ids: Vec<SessionId> = self
679 .sessions
680 .iter()
681 .filter(|entry| {
682 let session = entry.value();
683 if let Ok(status) = session.status.try_read() {
684 *status == SessionStatus::Terminated
685 } else {
686 false
687 }
688 })
689 .map(|entry| entry.key().clone())
690 .collect();
691
692 for id in terminated_ids {
693 self.sessions.remove(&id);
694 removed += 1;
695 }
696
697 Ok(removed)
698 }
699
700 pub fn find_session_by_name(&self, name: &str) -> Option<Arc<AISession>> {
708 self.sessions.iter().find_map(|entry| {
709 let session = entry.value();
710 if session.config.name.as_deref() == Some(name) {
711 Some(session.clone())
712 } else {
713 None
714 }
715 })
716 }
717
718 pub fn list_sessions_by_prefix(&self, prefix: &str) -> Vec<Arc<AISession>> {
726 self.sessions
727 .iter()
728 .filter_map(|entry| {
729 let session = entry.value();
730 if session
731 .config
732 .name
733 .as_ref()
734 .map(|n| n.starts_with(prefix))
735 .unwrap_or(false)
736 {
737 Some(session.clone())
738 } else {
739 None
740 }
741 })
742 .collect()
743 }
744
745 pub fn list_sessions_detailed(&self) -> Vec<crate::SessionInfo> {
750 self.sessions
751 .iter()
752 .map(|entry| {
753 let session = entry.value();
754 let status = session
755 .status
756 .try_read()
757 .map(|s| *s)
758 .unwrap_or(SessionStatus::Initializing);
759 let last_activity = session
760 .last_activity
761 .try_read()
762 .map(|t| *t)
763 .unwrap_or(session.created_at);
764 let command_count = session.command_count.try_read().map(|c| *c).unwrap_or(0);
765 let context_tokens = session.total_tokens.try_read().map(|t| *t).unwrap_or(0);
766
767 crate::SessionInfo {
768 id: session.id.clone(),
769 name: session.config.name.clone(),
770 status,
771 created_at: session.created_at,
772 last_activity,
773 working_directory: session.config.working_directory.clone(),
774 ai_features_enabled: session.config.enable_ai_features,
775 context_token_count: context_tokens,
776 command_count,
777 }
778 })
779 .collect()
780 }
781}
782
783impl Default for SessionManager {
784 fn default() -> Self {
785 Self::new()
786 }
787}
788
789#[cfg(test)]
791mod tests {
792 use super::*;
793
794 #[tokio::test]
795 async fn test_session_id() {
796 let id1 = SessionId::new();
797 let id2 = SessionId::new();
798 assert_ne!(id1, id2);
799 }
800
801 #[tokio::test]
802 async fn test_session_manager() {
803 let manager = SessionManager::new();
804 let session = manager.create_session().await.unwrap();
805
806 assert!(manager.get_session(&session.id).is_some());
807 assert_eq!(manager.list_sessions().len(), 1);
808
809 manager.remove_session(&session.id).await.unwrap();
810 assert!(manager.get_session(&session.id).is_none());
811 }
812
813 #[tokio::test]
814 async fn execute_command_runs_in_session_working_directory() {
815 let dir = tempfile::tempdir().unwrap();
816 let config = SessionConfig {
817 working_directory: dir.path().to_path_buf(),
818 shell: Some("/bin/sh".to_string()),
819 ..SessionConfig::default()
820 };
821 let session = AISession::new(config).await.unwrap();
822
823 let output = session
824 .execute_command("printf ai-session-ok && pwd")
825 .await
826 .unwrap();
827
828 assert!(output.contains("ai-session-ok"));
829 assert!(output.contains(&dir.path().to_string_lossy().to_string()));
830
831 let history = session.get_command_history().await;
832 assert_eq!(history.len(), 1);
833 assert_eq!(history[0].exit_code, Some(0));
834 }
835}