agtrace-engine 0.7.1

Internal core engine for the agtrace CLI. Not intended for direct use.
Documentation
use super::metrics::{SessionMetrics, compute_metrics};
use crate::AgentSession;

#[derive(Debug, Clone)]
pub struct SessionDigest {
    pub session_id: String,
    pub provider: String,
    pub session: AgentSession,
    pub opening: Option<String>,
    pub activation: Option<String>,
    pub metrics: SessionMetrics,
    pub recency_boost: u32,
    pub selection_reason: Option<String>,
}

impl SessionDigest {
    pub fn new(
        session_id: &str,
        provider: &str,
        session: AgentSession,
        recency_boost: u32,
    ) -> Self {
        let opening = session
            .turns
            .first()
            .map(|t| clean_snippet(&t.user.content.text))
            .filter(|s| !s.is_empty())
            .map(|s| truncate_string(&s, 100));

        let metrics = compute_metrics(&session);
        let activation = find_activation(&session);

        Self {
            session_id: session_id.to_string(),
            provider: provider.to_string(),
            session,
            opening,
            activation,
            metrics,
            recency_boost,
            selection_reason: None,
        }
    }
}

fn clean_snippet(text: &str) -> String {
    let mut cleaned = text.to_string();

    let noise_tags = [
        ("<environment_context>", "</environment_context>"),
        ("<command_output>", "</command_output>"),
        ("<changed_files>", "</changed_files>"),
    ];

    for (start_tag, end_tag) in noise_tags {
        while let Some(start_idx) = cleaned.find(start_tag) {
            if let Some(end_idx) = cleaned[start_idx..].find(end_tag) {
                let absolute_end = start_idx + end_idx + end_tag.len();
                cleaned.replace_range(start_idx..absolute_end, " [..meta..] ");
            } else {
                break;
            }
        }
    }

    let fields: Vec<&str> = cleaned.split_whitespace().collect();
    fields.join(" ")
}

fn find_activation(session: &AgentSession) -> Option<String> {
    if session.turns.is_empty() {
        return None;
    }

    let (best_idx, max_tools) = session
        .turns
        .iter()
        .enumerate()
        .map(|(i, turn)| {
            let tool_count: usize = turn.steps.iter().map(|s| s.tools.len()).sum();
            (i, tool_count)
        })
        .max_by_key(|(_, count)| *count)
        .unwrap_or((0, 0));

    if max_tools < 3 {
        return None;
    }

    session
        .turns
        .get(best_idx)
        .map(|turn| clean_snippet(&turn.user.content.text))
        .map(|s| truncate_string(&s, 120))
}

fn truncate_string(s: &str, max_len: usize) -> String {
    if s.chars().count() <= max_len {
        s.to_string()
    } else {
        let chars: Vec<char> = s.chars().take(max_len).collect();
        chars.iter().collect::<String>() + "..."
    }
}