Skip to main content

ainl_context_compiler/
capability.rs

1//! Capability tier auto-detection.
2//!
3//! Tiers light up additively as the host injects optional dependencies. The orchestrator
4//! consults the active tier per-call and auto-degrades on any failure — never blocks startup,
5//! never fails a turn because Tier 1/2 was unavailable.
6
7use serde::{Deserialize, Serialize};
8
9/// Active capability tier for one [`crate::ContextCompiler::compose`] call.
10#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash, Serialize, Deserialize, Default)]
11#[serde(rename_all = "snake_case")]
12pub enum Tier {
13    /// Heuristic-only (always available, offline-safe).
14    #[default]
15    Heuristic,
16    /// Heuristic + LLM-driven anchored summarization (M2).
17    HeuristicSummarization,
18    /// Heuristic + summarization + embedding-based relevance rerank (M3).
19    HeuristicSummarizationEmbedding,
20}
21
22impl Tier {
23    /// Stable label for telemetry.
24    #[must_use]
25    pub fn as_str(self) -> &'static str {
26        match self {
27            Self::Heuristic => "heuristic",
28            Self::HeuristicSummarization => "heuristic_summarization",
29            Self::HeuristicSummarizationEmbedding => "heuristic_summarization_embedding",
30        }
31    }
32}
33
34/// Probe result describing which optional capabilities the host wired in.
35#[derive(Debug, Clone, Copy, PartialEq, Eq, Default)]
36pub struct CapabilityProbe {
37    /// Whether a [`crate::Summarizer`] is available.
38    pub summarizer: bool,
39    /// Whether an embedder is available (Tier 2).
40    pub embedder: bool,
41}
42
43impl CapabilityProbe {
44    /// Construct a probe with no optional capabilities (offline default).
45    #[must_use]
46    pub fn offline() -> Self {
47        Self::default()
48    }
49
50    /// Highest tier the probe authorizes.
51    #[must_use]
52    pub fn active_tier(self) -> Tier {
53        match (self.summarizer, self.embedder) {
54            (true, true) => Tier::HeuristicSummarizationEmbedding,
55            (true, false) => Tier::HeuristicSummarization,
56            (false, true) => Tier::HeuristicSummarizationEmbedding,
57            (false, false) => Tier::Heuristic,
58        }
59    }
60
61    /// Human-readable reason for telemetry (`reason` field of `TierSelected` event).
62    #[must_use]
63    pub fn reason(self) -> &'static str {
64        match (self.summarizer, self.embedder) {
65            (true, true) => "summarizer_and_embedder_present",
66            (true, false) => "summarizer_present",
67            (false, true) => "embedder_present",
68            (false, false) => "heuristic_only",
69        }
70    }
71}
72
73#[cfg(test)]
74mod tests {
75    use super::*;
76
77    #[test]
78    fn offline_is_heuristic() {
79        assert_eq!(CapabilityProbe::offline().active_tier(), Tier::Heuristic);
80    }
81
82    #[test]
83    fn summarizer_only_unlocks_tier1() {
84        let p = CapabilityProbe {
85            summarizer: true,
86            embedder: false,
87        };
88        assert_eq!(p.active_tier(), Tier::HeuristicSummarization);
89    }
90
91    #[test]
92    fn both_unlocks_tier2() {
93        let p = CapabilityProbe {
94            summarizer: true,
95            embedder: true,
96        };
97        assert_eq!(p.active_tier(), Tier::HeuristicSummarizationEmbedding);
98    }
99
100    #[test]
101    fn embedder_only_unlocks_embedding_tier() {
102        let p = CapabilityProbe {
103            summarizer: false,
104            embedder: true,
105        };
106        assert_eq!(p.active_tier(), Tier::HeuristicSummarizationEmbedding);
107    }
108}