Skip to main content

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 heuristic;
16pub mod manage;
17
18#[cfg(feature = "llama")]
19pub mod llama;
20
21use kintsugi_core::{Class, ProposedCommand};
22
23pub use heuristic::HeuristicScorer;
24pub use manage::{select_spec, ModelSpec, MODEL_FALLBACK, MODEL_PRIMARY};
25
26pub const VERSION: &str = env!("CARGO_PKG_VERSION");
27
28/// The model's structured output for one command.
29#[derive(Debug, Clone, PartialEq, Eq)]
30pub struct ModelOutput {
31    /// One plain-English sentence describing what the command does.
32    pub summary: String,
33    /// Severity score, 0..=100. Only meaningful for the ambiguous band.
34    pub risk: u8,
35}
36
37/// A Tier-2 scorer. Kept warm in the daemon and shared across requests.
38pub trait Scorer: Send + Sync {
39    /// A stable identifier for the backend (`"heuristic"`, `"llama:qwen2.5-3b"`, …).
40    fn name(&self) -> &str;
41
42    /// Explain and score a command. `rule` is the Tier-1 rule id that fired, used
43    /// for a faithful summary. The score is only consulted for the ambiguous band.
44    fn score(&self, cmd: &ProposedCommand, class: Class, rule: &str) -> ModelOutput;
45}
46
47/// Whether a real (non-heuristic) model backend is compiled in.
48pub fn model_available() -> bool {
49    cfg!(feature = "llama")
50}
51
52/// Construct the best available scorer: the real model if the `llama` feature is
53/// on and weights load, otherwise the heuristic scorer.
54pub fn default_scorer() -> Box<dyn Scorer> {
55    #[cfg(feature = "llama")]
56    {
57        match llama::LlamaScorer::autoload() {
58            Ok(s) => return Box::new(s),
59            Err(e) => eprintln!("kintsugi-model: falling back to heuristic scorer: {e}"),
60        }
61    }
62    Box::new(HeuristicScorer::new())
63}
64
65#[cfg(test)]
66mod tests {
67    use super::*;
68
69    #[test]
70    fn default_scorer_is_usable() {
71        let s = default_scorer();
72        let cmd = ProposedCommand::new("t", "/tmp", vec!["rm".into()], "rm -rf build");
73        let out = s.score(&cmd, Class::Ambiguous, "ambiguous:rm");
74        assert!(!out.summary.is_empty());
75        assert!(out.risk <= 100);
76    }
77
78    #[test]
79    fn model_available_tracks_feature() {
80        assert_eq!(model_available(), cfg!(feature = "llama"));
81    }
82}