collet 0.1.1

Relentless agentic coding orchestrator with zero-drop agent loops
Documentation
//! Config advisor — generates and applies optimization suggestions.
//!
//! Loads bench.jsonl, computes per-model stats, runs the analyzer,
//! and produces a popup-ready suggestion list for the active model.

use std::path::Path;

use crate::config::Config;
use crate::optimizer::analyzer::{self, Suggestion};
use crate::optimizer::metrics;

/// Result of running the advisor for a specific model.
#[derive(Debug)]
pub struct AdvisorResult {
    /// Model name analyzed.
    pub model: String,
    /// Number of sessions used for analysis.
    pub session_count: usize,
    /// Optimization suggestions (already filtered by confidence ≥ 0.7).
    pub suggestions: Vec<Suggestion>,
}

/// Run the advisor for the currently active model.
///
/// Returns `None` if:
/// - bench.jsonl doesn't exist or has too few entries
/// - The current model has fewer than `MIN_SESSIONS` completed tasks
/// - No suggestions pass the confidence threshold
pub fn advise(bench_path: &Path, model: &str, config: &Config) -> Option<AdvisorResult> {
    let entries = metrics::load_bench_entries(bench_path);
    if entries.is_empty() {
        return None;
    }

    let groups = metrics::group_by_model(&entries);
    let model_entries = groups.get(model)?;
    let stats = metrics::compute_model_stats(model, model_entries)?;

    // Log a compact summary of all collected stat fields for diagnostics.
    tracing::debug!(
        model = %stats.model,
        sessions = stats.session_count,
        duration_p50_secs = stats.duration_secs.p50,
        tool_calls_p50 = stats.tool_calls.p50,
        tool_success_p50 = stats.tool_success_rate.p50,
        tokens_in_p50 = stats.tokens_in.p50,
        tokens_out_p50 = stats.tokens_out.p50,
        cache_pct_p50 = stats.cache_pct.p50,
        "ModelStats collected for advisor"
    );

    let suggestions = analyzer::analyze(&stats, config);

    if suggestions.is_empty() {
        return None;
    }

    Some(AdvisorResult {
        model: model.to_string(),
        session_count: stats.session_count,
        suggestions,
    })
}

/// Apply a single suggestion to the config, returning the updated value as string.
/// Returns `Err` if the key is unknown or the value is invalid.
pub fn apply_suggestion(config: &mut Config, suggestion: &Suggestion) -> Result<(), String> {
    match suggestion.key.as_str() {
        "max_iterations" => {
            let val: u32 = suggestion
                .suggested
                .parse()
                .map_err(|_| "Invalid max_iterations value")?;
            config.max_iterations = val;
        }
        "tool_timeout_secs" => {
            let val: u64 = suggestion
                .suggested
                .trim_end_matches('s')
                .parse()
                .map_err(|_| "Invalid tool_timeout value")?;
            config.tool_timeout_secs = val;
        }
        "compaction_threshold" => {
            let val: f32 = suggestion
                .suggested
                .parse()
                .map_err(|_| "Invalid compaction_threshold value")?;
            config.compaction_threshold = val.clamp(0.40, 0.95);
        }
        "stream_max_retries" => {
            let val: u32 = suggestion
                .suggested
                .parse()
                .map_err(|_| "Invalid stream_max_retries value")?;
            config.stream_max_retries = val;
        }
        "iteration_delay_ms" => {
            let val: u64 = suggestion
                .suggested
                .trim_end_matches("ms")
                .parse()
                .map_err(|_| "Invalid iteration_delay value")?;
            config.iteration_delay_ms = val;
        }
        _ => return Err(format!("Unknown config key: {}", suggestion.key)),
    }
    Ok(())
}