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 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}