car-inference 0.56.1

Local model inference for CAR — Candle backend with Qwen3 models
Documentation
//! What `concierge.ask` answers with — built so the facts never depend on a
//! language model.
//!
//! [`summarize`] states the local model world deterministically: what each
//! installed model is doing, what maintenance would do and why, the memory
//! policy, and what the concierge did on its own. A model then answers the
//! question from that evidence, bounded; [`trim_repetition`] cuts the loops
//! small models fall into, and [`ungrounded_ids`] names any model id the
//! reply mentions that the evidence does not contain, so an invented model
//! is flagged rather than passed off as verified.

use std::fmt::Write as _;

use serde::Serialize;

use crate::maintenance::MaintenanceReport;
use crate::portfolio::{Portfolio, PortfolioAction, Verdict};

/// What `concierge.ask` returns.
#[derive(Debug, Clone, PartialEq, Eq, Serialize)]
pub struct ConciergeAnswer {
    /// The reply: the model's answer to the question, or the facts alone.
    pub answer: String,
    /// The deterministic facts the answer rests on.
    pub summary: String,
    /// The model that answered, when one did.
    #[serde(skip_serializing_if = "Option::is_none")]
    pub model_used: Option<String>,
    /// Model ids the reply named that the evidence does not contain.
    pub ungrounded: Vec<String>,
}

fn gb(bytes: u64) -> f64 {
    bytes as f64 / 1e9
}

fn verdict_label(verdict: Verdict) -> &'static str {
    match verdict {
        Verdict::Protected => "protected",
        Verdict::NeverRunsHere => "cannot run on this machine",
        Verdict::CannotLoad => "never loaded successfully",
        Verdict::Superseded => "replaced by a newer model",
        Verdict::Idle => "unused",
        Verdict::Active => "in use",
        Verdict::Unknown => "not enough history yet",
    }
}

/// The deterministic state of this machine's local models, in plain words.
/// `memory_policy` is the active local-model memory policy as a word
/// (`everyday`, `local_focused`, `custom`).
pub fn summarize(
    portfolio: &Portfolio,
    maintenance: &MaintenanceReport,
    memory_policy: &str,
) -> String {
    let mut out = String::new();
    let installed = portfolio.models.len();
    let _ = writeln!(out, "Installed local models: {installed}.");
    for assessment in &portfolio.models {
        let freed = assessment
            .retire
            .as_ref()
            .filter(|_| assessment.action == PortfolioAction::Retire)
            .map(|r| format!("; retiring it would free {:.1} GB", gb(r.freed_bytes)))
            .unwrap_or_default();
        let _ = writeln!(
            out,
            "- {} ({}): {}{}{}",
            assessment.name,
            assessment.model_id,
            verdict_label(assessment.verdict),
            if assessment.evidence.is_empty() {
                String::new()
            } else {
                format!(" — {}", assessment.evidence.join("; "))
            },
            freed
        );
    }
    if portfolio.reclaimable_bytes > 0 {
        let _ = writeln!(
            out,
            "Retiring every model recommended for it would free {:.1} GB.",
            gb(portfolio.reclaimable_bytes)
        );
    }
    let policy = match maintenance.policy {
        crate::update_prefs::UpdatePolicy::Auto => "auto (CAR maintains local models itself)",
        crate::update_prefs::UpdatePolicy::Notify => "notify (CAR suggests, you decide)",
        crate::update_prefs::UpdatePolicy::Off => "off (no maintenance)",
    };
    let _ = writeln!(out, "Update policy: {policy}.");
    if maintenance.auto_grace_until.is_some() {
        let _ = writeln!(
            out,
            "This is the first week under the default policy, so maintenance only suggests for now."
        );
    }
    if let Some(disk) = maintenance.disk {
        let _ = writeln!(
            out,
            "Disk: {:.0} GB free of {:.0} GB{}.",
            gb(disk.free_bytes),
            gb(disk.total_bytes),
            if maintenance.pressured {
                " — space is short, so unused models may be retired"
            } else {
                ""
            }
        );
    }
    let queued: Vec<String> = maintenance
        .plan
        .act
        .iter()
        .chain(maintenance.plan.suggest.iter())
        .map(|d| format!("{} ({:.1} GB)", d.model_id, gb(d.freed_bytes)))
        .collect();
    if queued.is_empty() {
        let _ = writeln!(out, "Maintenance has nothing to retire right now.");
    } else {
        let _ = writeln!(out, "Maintenance would retire: {}.", queued.join(", "));
    }
    for held in &maintenance.plan.held {
        let _ = writeln!(out, "Kept {}: {}.", held.model_id, held.why);
    }
    let _ = writeln!(out, "Memory policy for local models: {memory_policy}.");
    if let Some(sizing) = &maintenance.resource_policy {
        let _ = writeln!(
            out,
            "The memory policy {} change from {:?} to {:?}: {}.",
            if sizing.applied { "did" } else { "would" },
            sizing.from,
            sizing.to,
            sizing.reason
        );
    }
    if !portfolio.recent_actions.is_empty() {
        let _ = writeln!(
            out,
            "What the concierge did on its own in the last 30 days:"
        );
        for action in &portfolio.recent_actions {
            let _ = writeln!(
                out,
                "- {} {}{}{}",
                action.kind,
                action.subject,
                if action.interrupted {
                    " (interrupted; finished at the next daemon start)"
                } else {
                    ""
                },
                action
                    .undo
                    .as_deref()
                    .map(|undo| format!(" — undo with `{undo}`"))
                    .unwrap_or_default()
            );
        }
    }
    for pin in &portfolio.revoked_pins {
        let named = if pin.pin == pin.model_id {
            String::new()
        } else {
            format!(" (as `{}`)", pin.pin)
        };
        let _ = writeln!(
            out,
            "Its publisher revoked {}; agents still pin it{named}: {}. Point them at another model.",
            pin.model_id,
            pin.agents.join(", ")
        );
    }
    for copy in &portfolio.revoked_copies {
        let _ = writeln!(
            out,
            "{} holds {:.1} GB in a directory named for {}, which its publisher revoked; CAR never removes it on its own.",
            copy.path.display(),
            copy.bytes as f64 / 1e9,
            copy.model_id
        );
    }
    let user_orphans: Vec<&str> = portfolio
        .orphans
        .iter()
        .filter(|o| o.owner == crate::retire::OrphanOwner::User)
        .map(|o| o.repo.as_str())
        .collect();
    if !user_orphans.is_empty() {
        let _ = writeln!(
            out,
            "{} Hugging Face repos no CAR model uses are yours and are never touched.",
            user_orphans.len()
        );
    }
    out
}

/// Cut a reply at the first substantial line it repeats — the loop a small
/// model falls into — and drop trailing blank lines.
pub fn trim_repetition(text: &str) -> String {
    let mut seen = std::collections::HashSet::new();
    let mut kept = Vec::new();
    for line in text.lines() {
        let key = line.trim().to_lowercase();
        if key.len() >= 24 && !seen.insert(key) {
            break;
        }
        kept.push(line);
    }
    while kept.last().is_some_and(|line| line.trim().is_empty()) {
        kept.pop();
    }
    kept.join("\n")
}

/// Model families a reply might name by bare name ("Llama 3.1 70B") rather
/// than by id.
const MODEL_FAMILIES: [&str; 9] = [
    "llama", "gpt", "mistral", "gemma", "deepseek", "phi", "claude", "grok", "qwen",
];

/// What the reply names that the evidence does not contain, deduplicated,
/// in order of appearance: model ids (`owner/name[:tag]`, or more segments,
/// as `parslee/openrouter/<alias>`), then model families named by bare name.
/// A model id's last segment carries a digit (`qwen3-8b`, `Qwen3-4B-4bit`),
/// which is what keeps `and/or` and `read/write` out.
pub fn ungrounded_ids(reply: &str, evidence: &str) -> Vec<String> {
    let evidence = evidence.to_lowercase();
    let mut out: Vec<String> = Vec::new();
    for raw in reply.split(|c: char| c.is_whitespace() || "()[]{}<>,;\"'`*".contains(c)) {
        let token = raw.trim_end_matches(['.', ':', '!', '?']);
        let parts: Vec<&str> = token.split('/').collect();
        let valid = |part: &str| {
            part.len() >= 2
                && part.chars().any(|c| c.is_ascii_alphabetic())
                && part
                    .chars()
                    .all(|c| c.is_ascii_alphanumeric() || "._:-".contains(c))
        };
        let is_id = parts.len() >= 2
            && parts.iter().all(|p| valid(p))
            && parts
                .last()
                .is_some_and(|name| name.chars().any(|c| c.is_ascii_digit()));
        if !is_id {
            continue;
        }
        let lower = token.to_lowercase();
        if !evidence.contains(&lower) && !out.iter().any(|o| o.eq_ignore_ascii_case(token)) {
            out.push(token.to_string());
        }
    }
    let reply = reply.to_lowercase();
    for family in MODEL_FAMILIES {
        let named = reply
            .split(|c: char| !c.is_ascii_alphanumeric())
            .any(|word| word.starts_with(family));
        if named && !evidence.contains(family) {
            out.push(format!("{family} (family)"));
        }
    }
    out
}

#[cfg(test)]
mod tests {
    use super::*;

    #[test]
    fn a_loop_is_cut_at_its_first_repeat() {
        let reply = "Keep qwen3-8b, it is in use.\n\
                     You could retire the vision model to free space.\n\
                     Keep qwen3-8b, it is in use.\n\
                     You could retire the vision model to free space.\n";
        assert_eq!(
            trim_repetition(reply),
            "Keep qwen3-8b, it is in use.\nYou could retire the vision model to free space."
        );
        // Short lines (a bullet, "Yes.") may repeat legitimately.
        assert_eq!(trim_repetition("- a\n- a\nok"), "- a\n- a\nok");
    }

    #[test]
    fn only_ids_missing_from_the_evidence_are_flagged() {
        let evidence = "- Qwen3-8B (mlx/qwen3-8b:4bit): in use";
        let reply = "Keep mlx/qwen3-8b:4bit. Consider meta/llama-4-70b, \
                     and see mlx/qwen3-8b:4bit again; 24/7 uptime; a/b test; \
                     read/write and/or input/output.";
        assert_eq!(
            ungrounded_ids(reply, evidence),
            vec!["meta/llama-4-70b".to_string(), "llama (family)".to_string()]
        );
        assert!(ungrounded_ids("(mlx/qwen3-8b:4bit).", evidence).is_empty());
        // A bare family the evidence never mentions, and a longer id.
        assert_eq!(
            ungrounded_ids("Try Mistral Large or parslee/openrouter/fast-2.", evidence),
            vec![
                "parslee/openrouter/fast-2".to_string(),
                "mistral (family)".to_string()
            ]
        );
        // A family the evidence does contain is grounded.
        assert!(ungrounded_ids("Qwen3 is fine.", evidence).is_empty());
    }

    #[test]
    fn the_summary_states_the_facts_without_a_model() {
        use crate::maintenance::{Decision, MaintenancePlan};
        use crate::portfolio::{
            Assessment, PortfolioPolicy, RecentAction, RetireSummary, RevokedPin,
        };
        let portfolio = Portfolio {
            policy: PortfolioPolicy::new(100, Some(0)),
            models: vec![Assessment {
                model_id: "mlx/old:4bit".into(),
                name: "Old".into(),
                verdict: Verdict::Idle,
                action: PortfolioAction::Retire,
                evidence: vec!["unused 40 days (threshold 30)".into()],
                kept: false,
                last_used: None,
                retire: Some(RetireSummary {
                    freed_bytes: 4_300_000_000,
                    digest: "d".into(),
                    refusals: Vec::new(),
                    also_retires: Vec::new(),
                }),
            }],
            reclaimable_bytes: 4_300_000_000,
            orphans: Vec::new(),
            recent_actions: vec![RecentAction {
                at: 90,
                kind: "retire".into(),
                subject: "mlx/gone:4bit".into(),
                detail: String::new(),
                undo: Some("car models pull mlx/gone:4bit".into()),
                use_case: None,
                prior: None,
                interrupted: false,
            }],
            revoked_pins: vec![RevokedPin {
                pin: "Bad Model".into(),
                model_id: "mlx/bad:4bit".into(),
                agents: vec!["digest".into(), "monthly".into()],
            }],
            revoked_copies: vec![crate::retire::RevokedCopy {
                model_id: "mlx/bad:4bit".into(),
                path: "/models/bad".into(),
                bytes: 2_000_000_000,
            }],
        };
        let report = MaintenanceReport {
            plan: MaintenancePlan {
                suggest: vec![Decision {
                    model_id: "mlx/old:4bit".into(),
                    verdict: Verdict::Idle,
                    freed_bytes: 4_300_000_000,
                    digest: "d".into(),
                    reason: String::new(),
                }],
                ..Default::default()
            },
            ..Default::default()
        };
        let summary = summarize(&portfolio, &report, "everyday");
        assert!(summary.contains("Old (mlx/old:4bit): unused"), "{summary}");
        assert!(summary.contains("free 4.3 GB"), "{summary}");
        assert!(
            summary.contains("Maintenance would retire: mlx/old:4bit (4.3 GB)"),
            "{summary}"
        );
        assert!(
            summary.contains("undo with `car models pull mlx/gone:4bit`"),
            "{summary}"
        );
        assert!(
            summary.contains("Memory policy for local models: everyday"),
            "{summary}"
        );
        assert!(
            summary.contains(
                "revoked mlx/bad:4bit; agents still pin it (as `Bad Model`): digest, monthly."
            ),
            "{summary}"
        );
        assert!(
            summary.contains("/models/bad holds 2.0 GB in a directory named for mlx/bad:4bit"),
            "{summary}"
        );
    }
}