use serde::{Deserialize, Serialize};
use std::fmt;
use std::time::{Duration, SystemTime, UNIX_EPOCH};
#[derive(Debug, Clone, PartialEq)]
pub enum FileOp {
Read,
Write,
Edit,
}
impl fmt::Display for FileOp {
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
match self {
FileOp::Read => write!(f, "R"),
FileOp::Write => write!(f, "W"),
FileOp::Edit => write!(f, "E"),
}
}
}
#[derive(Debug, Clone)]
pub struct FileAccess {
pub path: String,
pub operation: FileOp,
#[allow(dead_code)]
pub turn_index: u32,
}
pub const MAX_FILE_ACCESSES: usize = 1000;
#[derive(Debug, Clone, Default, Serialize)]
pub struct RateLimitInfo {
pub source: String,
pub five_hour_pct: Option<f64>,
pub five_hour_resets_at: Option<u64>,
pub seven_day_pct: Option<f64>,
pub seven_day_resets_at: Option<u64>,
pub updated_at: Option<u64>,
}
#[derive(Debug, Clone, PartialEq, Serialize)]
pub enum SessionStatus {
Thinking,
Executing,
Waiting,
Unknown,
RateLimited,
Done,
}
impl SessionStatus {
pub fn is_active(&self) -> bool {
matches!(self, SessionStatus::Thinking | SessionStatus::Executing)
}
}
#[derive(Debug, Clone, Serialize)]
pub struct ChildProcess {
pub pid: u32,
pub command: String,
pub mem_kb: u64,
pub port: Option<u16>,
}
#[derive(Debug, Clone, Serialize)]
pub struct OrphanPort {
pub port: u16,
pub pid: u32,
pub command: String,
pub project_name: String,
}
#[derive(Debug, Clone)]
pub struct SubAgent {
pub name: String,
pub status: String,
pub tokens: u64,
}
#[derive(Debug, Clone)]
pub struct ToolCall {
pub name: String,
pub arg: String,
pub duration_ms: u64,
}
#[derive(Debug, Clone, PartialEq)]
pub enum ChatRole {
User,
Assistant,
}
#[derive(Debug, Clone, PartialEq)]
pub struct ChatMessage {
pub role: ChatRole,
pub text: String,
}
pub const MAX_CHAT_MESSAGES: usize = 12;
#[derive(Debug, Clone)]
pub struct AgentSession {
pub agent_cli: &'static str,
pub pid: u32,
pub session_id: String,
pub cwd: String,
pub project_name: String,
pub started_at: u64,
pub status: SessionStatus,
pub model: String,
pub effort: String,
pub context_percent: f64,
pub total_input_tokens: u64,
pub total_output_tokens: u64,
pub total_cache_read: u64,
pub total_cache_create: u64,
pub turn_count: u32,
pub current_tasks: Vec<String>,
pub mem_mb: u64,
pub version: String,
pub git_branch: String,
pub git_added: u32,
pub git_modified: u32,
pub token_history: Vec<u64>,
pub context_history: Vec<u64>,
pub compaction_count: u32,
pub context_window: u64,
pub subagents: Vec<SubAgent>,
pub mem_file_count: u32,
pub mem_line_count: u32,
pub children: Vec<ChildProcess>,
pub initial_prompt: String,
pub first_assistant_text: String,
pub chat_messages: Vec<ChatMessage>,
pub tool_calls: Vec<ToolCall>,
pub pending_since_ms: u64,
pub thinking_since_ms: u64,
pub file_accesses: Vec<FileAccess>,
pub config_root: String,
}
impl AgentSession {
pub fn total_tokens(&self) -> u64 {
self.total_input_tokens
+ self.total_output_tokens
+ self.total_cache_read
+ self.total_cache_create
}
pub fn active_tokens(&self) -> u64 {
self.total_input_tokens + self.total_output_tokens + self.total_cache_create
}
pub fn elapsed(&self) -> Duration {
let now = SystemTime::now()
.duration_since(UNIX_EPOCH)
.unwrap_or_default()
.as_millis() as u64;
Duration::from_millis(now.saturating_sub(self.started_at))
}
pub fn elapsed_display(&self) -> String {
let secs = self.elapsed().as_secs();
if secs < 60 {
format!("{}s", secs)
} else if secs < 3600 {
format!("{}m", secs / 60)
} else {
format!("{}h {}m", secs / 3600, (secs % 3600) / 60)
}
}
}
#[derive(Debug, Deserialize)]
pub struct SessionFile {
pub pid: u32,
#[serde(rename = "sessionId")]
pub session_id: String,
pub cwd: String,
#[serde(rename = "startedAt")]
pub started_at: u64,
}
impl SessionFile {
pub fn sanitize(&mut self) {
truncate_string(&mut self.session_id, 256);
truncate_string(&mut self.cwd, 4096);
}
}
fn truncate_string(s: &mut String, max_bytes: usize) {
if s.len() > max_bytes {
let mut end = max_bytes;
while end > 0 && !s.is_char_boundary(end) {
end -= 1;
}
s.truncate(end);
}
}
#[cfg(test)]
mod tests {
use super::*;
fn make_session(input: u64, output: u64, cache_read: u64, cache_create: u64) -> AgentSession {
AgentSession {
agent_cli: "claude",
pid: 0,
session_id: String::new(),
cwd: String::new(),
project_name: String::new(),
started_at: 0,
status: SessionStatus::Waiting,
model: String::new(),
effort: String::new(),
context_percent: 0.0,
total_input_tokens: input,
total_output_tokens: output,
total_cache_read: cache_read,
total_cache_create: cache_create,
turn_count: 0,
current_tasks: Vec::new(),
mem_mb: 0,
version: String::new(),
git_branch: String::new(),
git_added: 0,
git_modified: 0,
token_history: Vec::new(),
context_history: Vec::new(),
compaction_count: 0,
context_window: 0,
subagents: Vec::new(),
mem_file_count: 0,
mem_line_count: 0,
children: Vec::new(),
initial_prompt: String::new(),
first_assistant_text: String::new(),
chat_messages: Vec::new(),
tool_calls: Vec::new(),
pending_since_ms: 0,
thinking_since_ms: 0,
file_accesses: Vec::new(),
config_root: String::new(),
}
}
#[test]
fn test_total_tokens() {
let session = make_session(100, 50, 200, 30);
assert_eq!(session.total_tokens(), 380); }
#[test]
fn test_active_tokens() {
let session = make_session(100, 50, 200, 30);
assert_eq!(session.active_tokens(), 180); }
}