ccswarm 0.9.2

AI Agent Workflow DevOps toolchain complementing Claude Code Agent Teams
Documentation
use chrono::{DateTime, Utc};
use serde::{Deserialize, Serialize};
use std::collections::HashMap;

use super::types::ExecutionSessionId;

#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct SessionContext {
    pub session_id: ExecutionSessionId,
    messages: Vec<Message>,
    total_messages_added: usize,
    active_tokens: usize,
    metadata: HashMap<String, serde_json::Value>,
}

impl SessionContext {
    pub fn new(session_id: ExecutionSessionId) -> Self {
        Self {
            session_id,
            messages: Vec::new(),
            total_messages_added: 0,
            active_tokens: 0,
            metadata: HashMap::new(),
        }
    }

    pub fn add_message_raw(&mut self, role: MessageRole, content: String) {
        let token_count = estimate_tokens(&content);
        self.messages.push(Message {
            role,
            content,
            timestamp: Utc::now(),
            token_count,
        });
        self.total_messages_added += 1;
        self.active_tokens = self.active_tokens.saturating_add(token_count);
        self.compact_if_needed();
    }

    pub async fn compress_context(&mut self) {
        self.compact_if_needed();
    }

    pub fn get_recent_messages(&self, n: usize) -> Vec<&Message> {
        let count = self.messages.len();
        if n >= count {
            self.messages.iter().collect()
        } else {
            self.messages.iter().skip(count - n).collect()
        }
    }

    pub fn get_compression_stats(&self) -> CompressionStats {
        CompressionStats {
            total_messages_added: self.total_messages_added,
            active_messages: self.messages.len(),
            compressed_messages: self
                .total_messages_added
                .saturating_sub(self.messages.len()),
            active_tokens: self.active_tokens,
            tokens_saved: 0,
            compressed_bytes: 0,
            compression_ratio: 0.0,
        }
    }

    fn compact_if_needed(&mut self) {
        const KEEP_RECENT: usize = 40;
        if self.messages.len() <= KEEP_RECENT {
            return;
        }
        let remove_count = self.messages.len() - KEEP_RECENT;
        let removed_tokens: usize = self
            .messages
            .drain(..remove_count)
            .map(|message| message.token_count)
            .sum();
        self.active_tokens = self.active_tokens.saturating_sub(removed_tokens);
    }
}

fn estimate_tokens(content: &str) -> usize {
    if content.is_empty() {
        return 1;
    }
    let word_count = content.split_whitespace().count();
    let char_count = content.chars().count();
    let special_chars = content
        .chars()
        .filter(|c| !c.is_alphanumeric() && !c.is_whitespace())
        .count();
    ((word_count as f64 * 1.3) as usize + special_chars + 2)
        .max(char_count / 4)
        .max(1)
}

#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct Message {
    pub role: MessageRole,
    pub content: String,
    pub timestamp: DateTime<Utc>,
    pub token_count: usize,
}

#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize)]
pub enum MessageRole {
    System,
    User,
    Assistant,
    Tool,
}

#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct CompressionStats {
    pub total_messages_added: usize,
    pub active_messages: usize,
    pub compressed_messages: usize,
    pub active_tokens: usize,
    pub tokens_saved: usize,
    pub compressed_bytes: usize,
    pub compression_ratio: f64,
}