vtcode_config/
context.rs

1use crate::constants::context as context_defaults;
2use anyhow::{Context, Result, ensure};
3use serde::{Deserialize, Serialize};
4
5#[cfg_attr(feature = "schema", derive(schemars::JsonSchema))]
6#[derive(Debug, Clone, Deserialize, Serialize)]
7pub struct LedgerConfig {
8    #[serde(default = "default_enabled")]
9    pub enabled: bool,
10    #[serde(default = "default_max_entries")]
11    pub max_entries: usize,
12    /// Inject ledger into the system prompt each turn
13    #[serde(default = "default_include_in_prompt")]
14    pub include_in_prompt: bool,
15    /// Preserve ledger entries during context compression
16    #[serde(default = "default_preserve_in_compression")]
17    pub preserve_in_compression: bool,
18}
19
20impl Default for LedgerConfig {
21    fn default() -> Self {
22        Self {
23            enabled: default_enabled(),
24            max_entries: default_max_entries(),
25            include_in_prompt: default_include_in_prompt(),
26            preserve_in_compression: default_preserve_in_compression(),
27        }
28    }
29}
30
31impl LedgerConfig {
32    pub fn validate(&self) -> Result<()> {
33        ensure!(
34            self.max_entries > 0,
35            "Ledger max_entries must be greater than zero"
36        );
37        Ok(())
38    }
39}
40
41fn default_enabled() -> bool {
42    true
43}
44fn default_max_entries() -> usize {
45    12
46}
47fn default_include_in_prompt() -> bool {
48    true
49}
50fn default_preserve_in_compression() -> bool {
51    true
52}
53
54#[cfg_attr(feature = "schema", derive(schemars::JsonSchema))]
55#[derive(Debug, Clone, Deserialize, Serialize)]
56pub struct TokenBudgetConfig {
57    /// Enable token budget tracking
58    #[serde(default = "default_token_budget_enabled")]
59    pub enabled: bool,
60    /// Model name for tokenizer selection
61    #[serde(default = "default_token_budget_model")]
62    pub model: String,
63    /// Optional override for tokenizer identifier or file path
64    #[serde(default)]
65    pub tokenizer: Option<String>,
66    /// Warning threshold (0.0-1.0)
67    #[serde(default = "default_warning_threshold")]
68    pub warning_threshold: f64,
69    /// Alert threshold (0.0-1.0)
70    #[serde(default = "default_alert_threshold")]
71    pub alert_threshold: f64,
72    /// Enable detailed component tracking
73    #[serde(default = "default_detailed_tracking")]
74    pub detailed_tracking: bool,
75}
76
77impl Default for TokenBudgetConfig {
78    fn default() -> Self {
79        Self {
80            enabled: default_token_budget_enabled(),
81            model: default_token_budget_model(),
82            tokenizer: None,
83            warning_threshold: default_warning_threshold(),
84            alert_threshold: default_alert_threshold(),
85            detailed_tracking: default_detailed_tracking(),
86        }
87    }
88}
89
90impl TokenBudgetConfig {
91    pub fn validate(&self) -> Result<()> {
92        ensure!(
93            (0.0..=1.0).contains(&self.warning_threshold),
94            "Token budget warning_threshold must be between 0.0 and 1.0"
95        );
96        ensure!(
97            (0.0..=1.0).contains(&self.alert_threshold),
98            "Token budget alert_threshold must be between 0.0 and 1.0"
99        );
100        ensure!(
101            self.warning_threshold <= self.alert_threshold,
102            "Token budget warning_threshold must be less than or equal to alert_threshold"
103        );
104
105        if self.enabled {
106            ensure!(
107                !self.model.trim().is_empty(),
108                "Token budget model must be provided when token budgeting is enabled"
109            );
110            if let Some(tokenizer) = &self.tokenizer {
111                ensure!(
112                    !tokenizer.trim().is_empty(),
113                    "Token budget tokenizer override cannot be empty"
114                );
115            }
116        }
117
118        Ok(())
119    }
120}
121
122fn default_token_budget_enabled() -> bool {
123    true
124}
125fn default_token_budget_model() -> String {
126    "gpt-5-nano".to_string()
127}
128fn default_warning_threshold() -> f64 {
129    0.75
130}
131fn default_alert_threshold() -> f64 {
132    0.85
133}
134fn default_detailed_tracking() -> bool {
135    false
136}
137
138#[cfg_attr(feature = "schema", derive(schemars::JsonSchema))]
139#[derive(Debug, Clone, Deserialize, Serialize)]
140pub struct ContextFeaturesConfig {
141    #[serde(default)]
142    pub ledger: LedgerConfig,
143    #[serde(default)]
144    pub token_budget: TokenBudgetConfig,
145    #[serde(default = "default_max_context_tokens")]
146    pub max_context_tokens: usize,
147    #[serde(default = "default_trim_to_percent")]
148    pub trim_to_percent: u8,
149    #[serde(default = "default_preserve_recent_turns")]
150    pub preserve_recent_turns: usize,
151    #[serde(default = "default_semantic_compression_enabled")]
152    pub semantic_compression: bool,
153    #[serde(default = "default_tool_aware_retention_enabled")]
154    pub tool_aware_retention: bool,
155    #[serde(default = "default_max_structural_depth")]
156    pub max_structural_depth: usize,
157    #[serde(default = "default_preserve_recent_tools")]
158    pub preserve_recent_tools: usize,
159    /// Maximum tokens from a single tool result to include before truncation
160    /// Applies token-based head+tail truncation to tool outputs before model input
161    #[serde(default = "default_model_input_token_budget")]
162    pub model_input_token_budget: usize,
163
164    /// Byte failsafe fuse for tool outputs after token truncation
165    /// Protects against pathological cases and ensures reasonable payload sizes
166    #[serde(default = "default_model_input_byte_fuse")]
167    pub model_input_byte_fuse: usize,
168}
169
170impl Default for ContextFeaturesConfig {
171    fn default() -> Self {
172        Self {
173            ledger: LedgerConfig::default(),
174            token_budget: TokenBudgetConfig::default(),
175            max_context_tokens: default_max_context_tokens(),
176            trim_to_percent: default_trim_to_percent(),
177            preserve_recent_turns: default_preserve_recent_turns(),
178            semantic_compression: default_semantic_compression_enabled(),
179            tool_aware_retention: default_tool_aware_retention_enabled(),
180            max_structural_depth: default_max_structural_depth(),
181            preserve_recent_tools: default_preserve_recent_tools(),
182            model_input_token_budget: default_model_input_token_budget(),
183            model_input_byte_fuse: default_model_input_byte_fuse(),
184        }
185    }
186}
187
188impl ContextFeaturesConfig {
189    pub fn validate(&self) -> Result<()> {
190        self.ledger
191            .validate()
192            .context("Invalid ledger configuration")?;
193        self.token_budget
194            .validate()
195            .context("Invalid token_budget configuration")?;
196
197        ensure!(
198            self.max_context_tokens > 0,
199            "Context features max_context_tokens must be greater than zero"
200        );
201        ensure!(
202            (1..=100).contains(&self.trim_to_percent),
203            "Context features trim_to_percent must be between 1 and 100"
204        );
205        ensure!(
206            self.preserve_recent_turns > 0,
207            "Context features preserve_recent_turns must be greater than zero"
208        );
209
210        ensure!(
211            (context_defaults::MIN_STRUCTURAL_DEPTH..=context_defaults::MAX_STRUCTURAL_DEPTH)
212                .contains(&self.max_structural_depth),
213            "Context features max_structural_depth must be between {} and {}",
214            context_defaults::MIN_STRUCTURAL_DEPTH,
215            context_defaults::MAX_STRUCTURAL_DEPTH,
216        );
217
218        ensure!(
219            (context_defaults::MIN_PRESERVE_RECENT_TOOLS
220                ..=context_defaults::MAX_PRESERVE_RECENT_TOOLS)
221                .contains(&self.preserve_recent_tools),
222            "Context features preserve_recent_tools must be between {} and {}",
223            context_defaults::MIN_PRESERVE_RECENT_TOOLS,
224            context_defaults::MAX_PRESERVE_RECENT_TOOLS,
225        );
226
227        Ok(())
228    }
229}
230
231fn default_max_context_tokens() -> usize {
232    context_defaults::DEFAULT_MAX_TOKENS
233}
234
235fn default_trim_to_percent() -> u8 {
236    context_defaults::DEFAULT_TRIM_TO_PERCENT
237}
238
239fn default_preserve_recent_turns() -> usize {
240    context_defaults::DEFAULT_PRESERVE_RECENT_TURNS
241}
242
243fn default_semantic_compression_enabled() -> bool {
244    context_defaults::DEFAULT_SEMANTIC_COMPRESSION_ENABLED
245}
246
247fn default_tool_aware_retention_enabled() -> bool {
248    context_defaults::DEFAULT_TOOL_AWARE_RETENTION_ENABLED
249}
250
251fn default_max_structural_depth() -> usize {
252    context_defaults::DEFAULT_MAX_STRUCTURAL_DEPTH
253}
254
255fn default_preserve_recent_tools() -> usize {
256    context_defaults::DEFAULT_PRESERVE_RECENT_TOOLS
257}
258
259fn default_model_input_token_budget() -> usize {
260    crate::constants::context::DEFAULT_MODEL_INPUT_TOKEN_BUDGET
261}
262
263fn default_model_input_byte_fuse() -> usize {
264    crate::constants::context::DEFAULT_MODEL_INPUT_BYTE_FUSE
265}