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}