Skip to main content

zeph_common/
fidelity.rs

1// SPDX-FileCopyrightText: 2026 Andrei G <bug-ops>
2// SPDX-License-Identifier: MIT OR Apache-2.0
3
4//! Fidelity types for Context-Adaptive Memory (CAM).
5//!
6//! [`ContextFidelity`] is a three-level representation that replaces the binary
7//! keep/discard approach used by compaction. [`PlannedToolHint`] carries lookahead
8//! hints from the orchestration DAG so the fidelity scorer can bias toward messages
9//! that are relevant to upcoming tool calls.
10
11use serde::{Deserialize, Serialize};
12
13/// Fidelity level assigned to a message in the context window.
14///
15/// Determines how a historical message is rendered before sending to the LLM.
16/// Assigned by `FidelityScorer` based on relevance signals; stored in
17/// `MessageMetadata.fidelity_tag` for debug tracing and compaction filtering.
18///
19/// Distinct from `zeph_memory::optical_forgetting::ContentFidelity`, which tracks
20/// long-term memory store preservation (optical forgetting): `ContextFidelity` governs
21/// rendering in the active context window; `ContentFidelity` governs what is durably
22/// stored in the `SQLite` memory store.
23///
24/// # Examples
25///
26/// ```
27/// use zeph_common::fidelity::ContextFidelity;
28///
29/// let level = ContextFidelity::default();
30/// assert_eq!(level, ContextFidelity::Full);
31///
32/// let compressed: u8 = ContextFidelity::Compressed as u8;
33/// assert_eq!(compressed, 1);
34///
35/// assert_eq!(ContextFidelity::from_u8(0), ContextFidelity::Full);
36/// assert_eq!(ContextFidelity::from_u8(1), ContextFidelity::Compressed);
37/// assert_eq!(ContextFidelity::from_u8(255), ContextFidelity::Full); // unknown → Full
38/// ```
39#[non_exhaustive]
40#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash, Default, Serialize, Deserialize)]
41#[repr(u8)]
42pub enum ContextFidelity {
43    /// Original message content, unchanged.
44    #[default]
45    Full = 0,
46    /// Content truncated to `compressed_max_tokens` tokens (or replaced by
47    /// `deferred_summary` when available).
48    Compressed = 1,
49    /// Content replaced by a compact placeholder tag; no semantic content
50    /// survives.
51    Placeholder = 2,
52}
53
54impl ContextFidelity {
55    /// Convert a database integer to a fidelity level.
56    ///
57    /// Unknown values map to [`Full`](ContextFidelity::Full) as the safe default,
58    /// preserving forward compatibility when new variants are added.
59    ///
60    /// **Note for variant authors:** update this match when adding new variants,
61    /// or old code will silently upgrade persisted values to `Full`.
62    ///
63    /// # Examples
64    ///
65    /// ```
66    /// use zeph_common::fidelity::ContextFidelity;
67    ///
68    /// assert_eq!(ContextFidelity::from_u8(0), ContextFidelity::Full);
69    /// assert_eq!(ContextFidelity::from_u8(1), ContextFidelity::Compressed);
70    /// assert_eq!(ContextFidelity::from_u8(2), ContextFidelity::Placeholder);
71    /// assert_eq!(ContextFidelity::from_u8(99), ContextFidelity::Full);
72    /// ```
73    #[must_use]
74    pub fn from_u8(v: u8) -> Self {
75        match v {
76            1 => Self::Compressed,
77            2 => Self::Placeholder,
78            _ => Self::Full, // 0 = Full (default); unknown values also map to Full
79        }
80    }
81}
82
83/// Hint about an upcoming tool call derived from the orchestration DAG.
84///
85/// Used by `FidelityScorer` to bias relevance scores toward messages that
86/// contain context useful for the next planned operations. In the v0.21 MVP
87/// the hints are populated by callers that have access to the DAG lookahead;
88/// an empty slice is always safe and disables the plan signal.
89///
90/// # Examples
91///
92/// ```
93/// use zeph_common::fidelity::PlannedToolHint;
94///
95/// let hint = PlannedToolHint::new("shell", vec!["cargo".to_string(), "build".to_string()], 1);
96/// assert_eq!(hint.tool_name, "shell");
97/// assert_eq!(hint.distance_from_current, 1);
98/// ```
99#[non_exhaustive]
100#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
101pub struct PlannedToolHint {
102    /// Name of the planned tool.
103    pub tool_name: String,
104    /// Keywords extracted from the tool's planned arguments (best-effort).
105    pub keywords: Vec<String>,
106    /// Steps until this tool is scheduled. 1 = immediately next, capped at 5.
107    pub distance_from_current: u8,
108}
109
110impl PlannedToolHint {
111    /// Creates a new [`PlannedToolHint`].
112    pub fn new(
113        tool_name: impl Into<String>,
114        keywords: Vec<String>,
115        distance_from_current: u8,
116    ) -> Self {
117        Self {
118            tool_name: tool_name.into(),
119            keywords,
120            distance_from_current,
121        }
122    }
123}
124
125#[cfg(test)]
126mod tests {
127    use super::ContextFidelity;
128
129    #[test]
130    fn from_u8_round_trip() {
131        assert_eq!(ContextFidelity::from_u8(0), ContextFidelity::Full);
132        assert_eq!(ContextFidelity::from_u8(1), ContextFidelity::Compressed);
133        assert_eq!(ContextFidelity::from_u8(2), ContextFidelity::Placeholder);
134    }
135
136    #[test]
137    fn from_u8_unknown_defaults_to_full() {
138        assert_eq!(ContextFidelity::from_u8(3), ContextFidelity::Full);
139        assert_eq!(ContextFidelity::from_u8(255), ContextFidelity::Full);
140    }
141
142    #[test]
143    fn as_u8_matches_from_u8() {
144        for level in [
145            ContextFidelity::Full,
146            ContextFidelity::Compressed,
147            ContextFidelity::Placeholder,
148        ] {
149            assert_eq!(ContextFidelity::from_u8(level as u8), level);
150        }
151    }
152}