ambi 0.3.8

A flexible, multi-backend, customizable AI agent framework, entirely based on Rust.
Documentation
// src/agent/core/history.rs

use crate::types::Message;
use serde::{Deserialize, Serialize};
use std::sync::Arc;

/// A structure storing dialogue turns and tracking aggregate token usage.
///
/// Note: `ChatHistory` now exclusively stores conversational turns (`User`, `Assistant`, `Tool`).
/// System instructions and dynamic contexts are handled separately in the `Agent` blueprint
/// and `AgentState`, ensuring pure FIFO eviction logic and optimal KV Cache performance.
#[derive(Clone, Serialize, Deserialize, Default)]
pub struct ChatHistory {
    messages: Vec<(Arc<Message>, usize)>,
    total_tokens: usize,
}

impl ChatHistory {
    /// Initializes an empty chat history.
    pub fn new() -> Self {
        Self {
            messages: Vec::new(),
            total_tokens: 0,
        }
    }

    /// # Accessors
    pub fn all(&self) -> &[(Arc<Message>, usize)] {
        &self.messages
    }

    /// Returns the raw total message count currently held in history.
    pub fn len(&self) -> usize {
        self.messages.len()
    }

    /// Returns the current total token count for all messages in history.
    pub fn total_tokens(&self) -> usize {
        self.total_tokens
    }

    /// Checks if the chat history contains zero messages.
    pub fn is_empty(&self) -> bool {
        self.messages.is_empty()
    }

    /// # Modifiers
    pub fn push(&mut self, msg: Message, exact_tokens: usize) {
        self.total_tokens += exact_tokens;
        self.messages.push((Arc::new(msg), exact_tokens));
    }

    /// Clears the history completely and resets the token counter to zero.
    pub fn clear(&mut self) {
        self.messages.clear();
        self.total_tokens = 0;
    }

    /// Truncates the message list to the specified length and recalculates tokens.
    pub fn truncate(&mut self, len: usize) {
        if len < self.messages.len() {
            self.messages.truncate(len);
            self.recalculate_len();
        }
    }

    /// The robust Context Eviction algorithm. Pops the oldest messages until safe.
    /// Pure FIFO implementation, as System prompts are completely decoupled from ChatHistory.
    pub fn evict_old_messages(
        &mut self,
        max_safe_tokens: usize, // Maximum allowed number of tokens (excluding prompt overhead)
        prompt_overhead: usize, // Number of tokens for system prompts and other fixed overhead
    ) -> Vec<Arc<Message>> {
        let mut target_tokens = self.total_tokens + prompt_overhead;

        if target_tokens <= max_safe_tokens || self.messages.is_empty() {
            return Vec::new();
        }

        let mut evict_count = 0;
        let mut tokens_to_remove = 0;

        for (_, tokens) in &self.messages {
            evict_count += 1;
            tokens_to_remove += tokens;
            target_tokens -= tokens;

            if target_tokens <= max_safe_tokens {
                break;
            }
        }

        let evicted: Vec<Arc<Message>> = self
            .messages
            .drain(0..evict_count)
            .map(|(m, _)| m)
            .collect();

        self.total_tokens -= tokens_to_remove;

        if !evicted.is_empty() {
            log::warn!(
                "Token limit exceeded (Max: {}). Evicted {} oldest messages.",
                max_safe_tokens,
                evicted.len()
            );
        }

        evicted
    }

    /// # Internal Helpers
    fn recalculate_len(&mut self) {
        self.total_tokens = self.messages.iter().map(|(_, t)| *t).sum();
    }

    /// Convenience helper: Full-text keyword search.
    /// Returns all messages containing the specified keyword.
    /// Useful for lightweight context sniffing at the business logic layer.
    pub fn search_by_keyword(&self, keyword: &str) -> Vec<Arc<Message>> {
        self.messages
            .iter()
            .filter(|(msg, _)| msg.to_string().contains(keyword))
            .map(|(msg, _)| Arc::clone(msg))
            .collect()
    }

    /// Convenience helper: Retrieves the most recent user input message.
    /// Useful for interceptors or pre-generation checks that need to analyze the user's latest intent.
    pub fn last_user_message(&self) -> Option<Arc<Message>> {
        self.messages.iter().rev().find_map(|(msg, _)| {
            if matches!(**msg, Message::User { .. }) {
                Some(Arc::clone(msg))
            } else {
                None
            }
        })
    }

    /// Convenience helper: Retrieves the most recent response generated by the assistant.
    pub fn last_assistant_message(&self) -> Option<Arc<Message>> {
        self.messages.iter().rev().find_map(|(msg, _)| {
            if matches!(**msg, Message::Assistant { .. }) {
                Some(Arc::clone(msg))
            } else {
                None
            }
        })
    }
}