collet 0.1.1

Relentless agentic coding orchestrator with zero-drop agent loops
Documentation
use super::AgentEvent;

/// Lightweight tracker for agent loop performance metrics.
///
/// Uses incremental O(1) aggregation instead of re-scanning raw vectors
/// each iteration, so `build_event()` is constant-time regardless of how
/// many tool calls / iterations have elapsed.
pub(super) struct PerfTracker {
    // Incremental aggregates — O(1) per build_event() call
    tool_latency_sum: u64,
    tool_latency_max: u64,
    tool_call_count: u32,
    api_latency_sum: u64,
    api_latency_max: u64,
    api_call_count: u32,
    tool_successes: u32,
    tool_failures: u32,
    tool_freq: std::collections::HashMap<String, u32>,
    total_tokens: u64,
    iterations: u32,
    // Cached top-3 — rebuilt only when tool_freq changes
    top_tools_cache: Vec<(String, u32)>,
    top_tools_dirty: bool,
}

impl PerfTracker {
    pub(super) fn new() -> Self {
        Self {
            tool_latency_sum: 0,
            tool_latency_max: 0,
            tool_call_count: 0,
            api_latency_sum: 0,
            api_latency_max: 0,
            api_call_count: 0,
            tool_successes: 0,
            tool_failures: 0,
            tool_freq: std::collections::HashMap::new(),
            total_tokens: 0,
            iterations: 0,
            top_tools_cache: Vec::new(),
            top_tools_dirty: false,
        }
    }

    pub(super) fn record_tool_call(&mut self, name: &str, elapsed_ms: u64, success: bool) {
        self.tool_latency_sum += elapsed_ms;
        if elapsed_ms > self.tool_latency_max {
            self.tool_latency_max = elapsed_ms;
        }
        self.tool_call_count += 1;
        *self.tool_freq.entry(name.to_string()).or_insert(0) += 1;
        self.top_tools_dirty = true;
        if success {
            self.tool_successes += 1;
        } else {
            self.tool_failures += 1;
        }
    }

    pub(super) fn record_api_latency(&mut self, elapsed_ms: u64) {
        self.api_latency_sum += elapsed_ms;
        if elapsed_ms > self.api_latency_max {
            self.api_latency_max = elapsed_ms;
        }
        self.api_call_count += 1;
    }

    pub(super) fn record_iteration(&mut self, tokens: u64) {
        self.iterations += 1;
        self.total_tokens += tokens;
    }

    pub(super) fn build_event(&mut self) -> AgentEvent {
        let tool_latency_avg_ms = if self.tool_call_count == 0 {
            0.0
        } else {
            self.tool_latency_sum as f64 / self.tool_call_count as f64
        };

        let api_latency_avg_ms = if self.api_call_count == 0 {
            0.0
        } else {
            self.api_latency_sum as f64 / self.api_call_count as f64
        };

        // Rebuild top-3 cache only when tool_freq has changed
        if self.top_tools_dirty {
            let mut sorted: Vec<(String, u32)> = self
                .tool_freq
                .iter()
                .map(|(k, v)| (k.clone(), *v))
                .collect();
            sorted.sort_unstable_by(|a, b| b.1.cmp(&a.1));
            sorted.truncate(3);
            self.top_tools_cache = sorted;
            self.top_tools_dirty = false;
        }

        AgentEvent::PerformanceUpdate {
            tool_latency_avg_ms,
            tool_latency_max_ms: self.tool_latency_max,
            api_latency_avg_ms,
            api_latency_max_ms: self.api_latency_max,
            tool_success_count: self.tool_successes,
            tool_failure_count: self.tool_failures,
            total_iterations: self.iterations,
            total_tokens_used: self.total_tokens,
            total_tool_calls_made: self.tool_call_count,
            top_tools: self.top_tools_cache.clone(),
        }
    }
}