use std::fmt::Write as _;
use serde::Serialize;
use crate::maintenance::MaintenanceReport;
use crate::portfolio::{Portfolio, PortfolioAction, Verdict};
#[derive(Debug, Clone, PartialEq, Eq, Serialize)]
pub struct ConciergeAnswer {
pub answer: String,
pub summary: String,
#[serde(skip_serializing_if = "Option::is_none")]
pub model_used: Option<String>,
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",
}
}
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
}
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")
}
const MODEL_FAMILIES: [&str; 9] = [
"llama", "gpt", "mistral", "gemma", "deepseek", "phi", "claude", "grok", "qwen",
];
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."
);
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());
assert_eq!(
ungrounded_ids("Try Mistral Large or parslee/openrouter/fast-2.", evidence),
vec![
"parslee/openrouter/fast-2".to_string(),
"mistral (family)".to_string()
]
);
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}"
);
}
}