kintsugi_model/lib.rs
1//! Kintsugi Tier-2 model wrapper.
2//!
3//! The model's only jobs are to **explain** (a one-sentence summary) and to
4//! **score** the ambiguous band (a `risk` 0..=100). It is never in the path for a
5//! catastrophic command, and its influence is escalation-only — it can add
6//! caution but can never unlock a rule-based block (see `CLAUDE.md`).
7//!
8//! Two backends behind one [`Scorer`] trait:
9//! - [`HeuristicScorer`] — deterministic, dependency-free, always available. This
10//! is also the graceful-degradation path when no real model is present.
11//! - `LlamaScorer` (feature `llama`) — real CPU GGUF inference via `llama.cpp`.
12
13#![forbid(unsafe_code)]
14
15pub mod config;
16pub mod heuristic;
17pub mod manage;
18
19#[cfg(feature = "llama")]
20pub mod llama;
21
22use kintsugi_core::{Class, ProposedCommand};
23
24pub use heuristic::HeuristicScorer;
25pub use manage::{select_spec, ModelSpec, MODEL_FALLBACK, MODEL_PRIMARY};
26
27pub const VERSION: &str = env!("CARGO_PKG_VERSION");
28
29/// The model's structured output for one command.
30#[derive(Debug, Clone, PartialEq, Eq)]
31pub struct ModelOutput {
32 /// One plain-English sentence describing what the command does.
33 pub summary: String,
34 /// Severity score, 0..=100. Only meaningful for the ambiguous band.
35 pub risk: u8,
36}
37
38/// A Tier-2 scorer. Kept warm in the daemon and shared across requests.
39pub trait Scorer: Send + Sync {
40 /// A stable identifier for the backend (`"heuristic"`, `"llama:qwen2.5-3b"`, …).
41 fn name(&self) -> &str;
42
43 /// Explain and score a command. `rule` is the Tier-1 rule id that fired, used
44 /// for a faithful summary. The score is only consulted for the ambiguous band.
45 fn score(&self, cmd: &ProposedCommand, class: Class, rule: &str) -> ModelOutput;
46}
47
48/// Whether a real (non-heuristic) model backend is compiled in.
49pub fn model_available() -> bool {
50 cfg!(feature = "llama")
51}
52
53/// Construct the best available scorer: the real model if the `llama` feature is
54/// on and weights load, otherwise the heuristic scorer.
55pub fn default_scorer() -> Box<dyn Scorer> {
56 #[cfg(feature = "llama")]
57 {
58 match llama::LlamaScorer::autoload() {
59 Ok(s) => return Box::new(s),
60 Err(e) => eprintln!("kintsugi-model: falling back to heuristic scorer: {e}"),
61 }
62 }
63 Box::new(HeuristicScorer::new())
64}
65
66#[cfg(test)]
67mod tests {
68 use super::*;
69
70 #[test]
71 fn default_scorer_is_usable() {
72 let s = default_scorer();
73 let cmd = ProposedCommand::new("t", "/tmp", vec!["rm".into()], "rm -rf build");
74 let out = s.score(&cmd, Class::Ambiguous, "ambiguous:rm");
75 assert!(!out.summary.is_empty());
76 assert!(out.risk <= 100);
77 }
78
79 #[test]
80 fn model_available_tracks_feature() {
81 assert_eq!(model_available(), cfg!(feature = "llama"));
82 }
83}