use std::collections::HashMap;
use std::path::PathBuf;
use std::sync::Arc;
use std::time::Duration;
use anyhow::Result;
use chrono::{DateTime, Utc};
use dashmap::DashMap;
use serde::{Deserialize, Serialize};
use tokio::sync::RwLock;
use uuid::Uuid;
pub mod headless;
pub mod lifecycle;
pub mod process;
pub mod pty;
pub mod terminal;
use crate::context::SessionContext;
use crate::persistence::CommandRecord;
#[derive(Debug, thiserror::Error)]
pub enum SessionError {
#[error("Session not found: {0}")]
NotFound(SessionId),
#[error("Session already exists: {0}")]
AlreadyExists(SessionId),
#[error("PTY error: {0}")]
PtyError(String),
#[error("Process error: {0}")]
ProcessError(String),
#[error("IO error: {0}")]
IoError(#[from] std::io::Error),
#[error("Other error: {0}")]
Other(#[from] anyhow::Error),
}
pub type SessionResult<T> = std::result::Result<T, SessionError>;
#[derive(Debug, Clone, Hash, Eq, PartialEq, Serialize, Deserialize)]
pub struct SessionId(Uuid);
impl Default for SessionId {
fn default() -> Self {
Self::new()
}
}
impl SessionId {
pub fn new() -> Self {
Self(Uuid::new_v4())
}
pub fn new_v4() -> Self {
Self(Uuid::new_v4())
}
pub fn parse_str(s: &str) -> Result<Self> {
Ok(Self(Uuid::parse_str(s)?))
}
pub fn to_string(&self) -> String {
self.0.to_string()
}
pub fn as_uuid(&self) -> &Uuid {
&self.0
}
}
impl std::fmt::Display for SessionId {
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
write!(f, "{}", self.0)
}
}
#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize, Default)]
pub enum SessionStatus {
#[default]
Initializing,
Running,
Paused,
Terminating,
Terminated,
Error,
}
#[derive(Debug, Clone, Serialize, Deserialize)]
#[serde(default)]
pub struct SessionConfig {
pub name: Option<String>,
pub working_directory: PathBuf,
pub environment: HashMap<String, String>,
pub shell: Option<String>,
pub shell_command: Option<String>,
pub pty_size: (u16, u16),
pub output_buffer_size: usize,
pub timeout: Option<Duration>,
pub compress_output: bool,
pub parse_output: bool,
pub enable_ai_features: bool,
pub context_config: ContextConfig,
pub agent_role: Option<String>,
pub force_headless: bool,
pub allow_headless_fallback: bool,
}
#[derive(Debug, Clone, Serialize, Deserialize)]
#[serde(default)]
pub struct ContextConfig {
pub max_tokens: usize,
pub compression_threshold: f64,
}
impl Default for SessionConfig {
fn default() -> Self {
Self {
name: None,
working_directory: std::env::current_dir().unwrap_or_else(|_| PathBuf::from("/")),
environment: HashMap::new(),
shell: None,
shell_command: None,
pty_size: (24, 80),
output_buffer_size: 1024 * 1024, timeout: None,
compress_output: true,
parse_output: true,
enable_ai_features: false,
context_config: ContextConfig::default(),
agent_role: None,
force_headless: false,
allow_headless_fallback: true,
}
}
}
impl Default for ContextConfig {
fn default() -> Self {
Self {
max_tokens: 4096,
compression_threshold: 0.8,
}
}
}
pub struct AISession {
pub id: SessionId,
pub config: SessionConfig,
pub status: RwLock<SessionStatus>,
pub context: Arc<RwLock<SessionContext>>,
process: Arc<RwLock<Option<process::ProcessHandle>>>,
terminal: Arc<RwLock<Option<terminal::TerminalHandle>>>,
pub created_at: DateTime<Utc>,
pub last_activity: Arc<RwLock<DateTime<Utc>>>,
pub metadata: Arc<RwLock<HashMap<String, serde_json::Value>>>,
pub command_history: Arc<RwLock<Vec<CommandRecord>>>,
pub command_count: Arc<RwLock<usize>>,
pub total_tokens: Arc<RwLock<usize>>,
}
impl AISession {
pub async fn new(config: SessionConfig) -> Result<Self> {
let id = SessionId::new();
let now = Utc::now();
Ok(Self {
id: id.clone(),
config,
status: RwLock::new(SessionStatus::Initializing),
context: Arc::new(RwLock::new(SessionContext::new(id))),
process: Arc::new(RwLock::new(None)),
terminal: Arc::new(RwLock::new(None)),
created_at: now,
last_activity: Arc::new(RwLock::new(now)),
metadata: Arc::new(RwLock::new(HashMap::new())),
command_history: Arc::new(RwLock::new(Vec::new())),
command_count: Arc::new(RwLock::new(0)),
total_tokens: Arc::new(RwLock::new(0)),
})
}
pub async fn new_with_id(
id: SessionId,
config: SessionConfig,
created_at: DateTime<Utc>,
) -> Result<Self> {
let now = Utc::now();
Ok(Self {
id: id.clone(),
config,
status: RwLock::new(SessionStatus::Initializing),
context: Arc::new(RwLock::new(SessionContext::new(id))),
process: Arc::new(RwLock::new(None)),
terminal: Arc::new(RwLock::new(None)),
created_at,
last_activity: Arc::new(RwLock::new(now)),
metadata: Arc::new(RwLock::new(HashMap::new())),
command_history: Arc::new(RwLock::new(Vec::new())),
command_count: Arc::new(RwLock::new(0)),
total_tokens: Arc::new(RwLock::new(0)),
})
}
pub async fn start(&self) -> Result<()> {
lifecycle::start_session(self).await
}
pub async fn stop(&self) -> Result<()> {
lifecycle::stop_session(self).await
}
pub async fn send_input(&self, input: &str) -> Result<()> {
let terminal_guard = self.terminal.read().await;
if let Some(terminal) = terminal_guard.as_ref() {
terminal.write(input.as_bytes()).await?;
*self.last_activity.write().await = Utc::now();
Ok(())
} else {
Err(anyhow::anyhow!("Session not started"))
}
}
pub async fn read_output(&self) -> Result<Vec<u8>> {
let terminal = self.terminal.read().await;
if let Some(terminal) = terminal.as_ref() {
let output = terminal.read().await?;
*self.last_activity.write().await = Utc::now();
Ok(output)
} else {
Err(anyhow::anyhow!("Session not started"))
}
}
pub async fn status(&self) -> SessionStatus {
*self.status.read().await
}
pub async fn set_metadata(&self, key: String, value: serde_json::Value) -> Result<()> {
self.metadata.write().await.insert(key, value);
Ok(())
}
pub async fn get_metadata(&self, key: &str) -> Option<serde_json::Value> {
self.metadata.read().await.get(key).cloned()
}
pub async fn execute_command(&self, command: &str) -> Result<String> {
let start_time = Utc::now();
self.send_input(&format!("{}\n", command)).await?;
tokio::time::sleep(Duration::from_millis(500)).await;
let output_bytes = self.read_output().await?;
let output = String::from_utf8_lossy(&output_bytes).to_string();
let end_time = Utc::now();
let duration_ms = (end_time - start_time).num_milliseconds() as u64;
let record = CommandRecord {
command: command.to_string(),
timestamp: start_time,
exit_code: None, output_preview: if output.len() > 200 {
format!("{}...", &output[..200])
} else {
output.clone()
},
duration_ms,
};
self.command_history.write().await.push(record);
*self.command_count.write().await += 1;
Ok(output)
}
pub async fn add_tokens(&self, token_count: usize) {
*self.total_tokens.write().await += token_count;
}
pub async fn get_command_history(&self) -> Vec<CommandRecord> {
self.command_history.read().await.clone()
}
pub async fn get_command_count(&self) -> usize {
*self.command_count.read().await
}
pub async fn get_total_tokens(&self) -> usize {
*self.total_tokens.read().await
}
pub async fn trim_command_history(&self, keep_recent: usize) {
let mut history = self.command_history.write().await;
if history.len() > keep_recent {
let start_index = history.len() - keep_recent;
history.drain(0..start_index);
}
}
}
pub struct SessionManager {
sessions: Arc<DashMap<SessionId, Arc<AISession>>>,
default_config: SessionConfig,
}
impl SessionManager {
pub fn new() -> Self {
Self {
sessions: Arc::new(DashMap::new()),
default_config: SessionConfig::default(),
}
}
pub async fn create_session(&self) -> Result<Arc<AISession>> {
self.create_session_with_config(self.default_config.clone())
.await
}
pub async fn create_session_with_config(
&self,
config: SessionConfig,
) -> Result<Arc<AISession>> {
let session = Arc::new(AISession::new(config).await?);
self.sessions.insert(session.id.clone(), session.clone());
Ok(session)
}
pub async fn restore_session(
&self,
id: SessionId,
config: SessionConfig,
created_at: DateTime<Utc>,
) -> Result<Arc<AISession>> {
if self.sessions.contains_key(&id) {
return Err(SessionError::AlreadyExists(id).into());
}
let session = Arc::new(AISession::new_with_id(id.clone(), config, created_at).await?);
self.sessions.insert(id, session.clone());
Ok(session)
}
pub fn get_session(&self, id: &SessionId) -> Option<Arc<AISession>> {
self.sessions.get(id).map(|entry| entry.clone())
}
pub fn list_sessions(&self) -> Vec<SessionId> {
self.sessions
.iter()
.map(|entry| entry.key().clone())
.collect()
}
pub fn list_session_refs(&self) -> Vec<Arc<AISession>> {
self.sessions
.iter()
.map(|entry| entry.value().clone())
.collect()
}
pub async fn remove_session(&self, id: &SessionId) -> Result<()> {
if let Some((_, session)) = self.sessions.remove(id) {
session.stop().await?;
}
Ok(())
}
pub async fn cleanup_terminated(&self) -> Result<usize> {
let mut removed = 0;
let terminated_ids: Vec<SessionId> = self
.sessions
.iter()
.filter(|entry| {
let session = entry.value();
if let Ok(status) = session.status.try_read() {
*status == SessionStatus::Terminated
} else {
false
}
})
.map(|entry| entry.key().clone())
.collect();
for id in terminated_ids {
self.sessions.remove(&id);
removed += 1;
}
Ok(removed)
}
pub fn find_session_by_name(&self, name: &str) -> Option<Arc<AISession>> {
self.sessions.iter().find_map(|entry| {
let session = entry.value();
if session.config.name.as_deref() == Some(name) {
Some(session.clone())
} else {
None
}
})
}
pub fn list_sessions_by_prefix(&self, prefix: &str) -> Vec<Arc<AISession>> {
self.sessions
.iter()
.filter_map(|entry| {
let session = entry.value();
if session
.config
.name
.as_ref()
.map(|n| n.starts_with(prefix))
.unwrap_or(false)
{
Some(session.clone())
} else {
None
}
})
.collect()
}
pub fn list_sessions_detailed(&self) -> Vec<crate::SessionInfo> {
self.sessions
.iter()
.map(|entry| {
let session = entry.value();
let status = session
.status
.try_read()
.map(|s| *s)
.unwrap_or(SessionStatus::Initializing);
let last_activity = session
.last_activity
.try_read()
.map(|t| *t)
.unwrap_or(session.created_at);
let command_count = session.command_count.try_read().map(|c| *c).unwrap_or(0);
let context_tokens = session.total_tokens.try_read().map(|t| *t).unwrap_or(0);
crate::SessionInfo {
id: session.id.clone(),
name: session.config.name.clone(),
status,
created_at: session.created_at,
last_activity,
working_directory: session.config.working_directory.clone(),
ai_features_enabled: session.config.enable_ai_features,
context_token_count: context_tokens,
command_count,
}
})
.collect()
}
}
impl Default for SessionManager {
fn default() -> Self {
Self::new()
}
}
#[cfg(test)]
mod tests {
use super::*;
#[tokio::test]
async fn test_session_id() {
let id1 = SessionId::new();
let id2 = SessionId::new();
assert_ne!(id1, id2);
}
#[tokio::test]
async fn test_session_manager() {
let manager = SessionManager::new();
let session = manager.create_session().await.unwrap();
assert!(manager.get_session(&session.id).is_some());
assert_eq!(manager.list_sessions().len(), 1);
manager.remove_session(&session.id).await.unwrap();
assert!(manager.get_session(&session.id).is_none());
}
}