Skip to main content

vtcode_config/
context.rs

1use anyhow::{Context, Result, ensure};
2use serde::{Deserialize, Serialize};
3
4/// Configuration for dynamic context discovery
5///
6/// This implements Cursor-style dynamic context discovery patterns where
7/// large outputs are written to files instead of being truncated, allowing
8/// agents to retrieve them on demand via unified_file/unified_search.
9#[cfg_attr(feature = "schema", derive(schemars::JsonSchema))]
10#[derive(Debug, Clone, Deserialize, Serialize)]
11pub struct DynamicContextConfig {
12    /// Enable dynamic context discovery features
13    #[serde(default = "default_dynamic_enabled")]
14    pub enabled: bool,
15
16    /// Threshold in bytes above which tool outputs are spooled to files
17    #[serde(default = "default_tool_output_threshold")]
18    pub tool_output_threshold: usize,
19
20    /// Enable syncing terminal sessions to .vtcode/terminals/ files
21    #[serde(default = "default_sync_terminals")]
22    pub sync_terminals: bool,
23
24    /// Enable persisting conversation history during summarization
25    #[serde(default = "default_persist_history")]
26    pub persist_history: bool,
27
28    /// Maximum number of recent user messages to retain verbatim during local compaction
29    #[serde(default = "default_retained_user_messages")]
30    pub retained_user_messages: usize,
31
32    /// Enable syncing MCP tool descriptions to .vtcode/mcp/tools/
33    #[serde(default = "default_sync_mcp_tools")]
34    pub sync_mcp_tools: bool,
35
36    /// Enable generating skill index in .agents/skills/INDEX.md
37    #[serde(default = "default_sync_skills")]
38    pub sync_skills: bool,
39
40    /// Maximum age in seconds for spooled tool output files before cleanup
41    #[serde(default = "default_spool_max_age_secs")]
42    pub spool_max_age_secs: u64,
43
44    /// Maximum number of spooled files to keep
45    #[serde(default = "default_max_spooled_files")]
46    pub max_spooled_files: usize,
47}
48
49impl Default for DynamicContextConfig {
50    fn default() -> Self {
51        Self {
52            enabled: default_dynamic_enabled(),
53            tool_output_threshold: default_tool_output_threshold(),
54            sync_terminals: default_sync_terminals(),
55            persist_history: default_persist_history(),
56            retained_user_messages: default_retained_user_messages(),
57            sync_mcp_tools: default_sync_mcp_tools(),
58            sync_skills: default_sync_skills(),
59            spool_max_age_secs: default_spool_max_age_secs(),
60            max_spooled_files: default_max_spooled_files(),
61        }
62    }
63}
64
65impl DynamicContextConfig {
66    pub fn validate(&self) -> Result<()> {
67        ensure!(
68            self.tool_output_threshold >= 1024,
69            "Tool output threshold must be at least 1024 bytes"
70        );
71        ensure!(
72            self.max_spooled_files > 0,
73            "Max spooled files must be greater than zero"
74        );
75        ensure!(
76            self.retained_user_messages > 0,
77            "Retained user messages must be greater than zero"
78        );
79        Ok(())
80    }
81}
82
83fn default_dynamic_enabled() -> bool {
84    true
85}
86
87fn default_tool_output_threshold() -> usize {
88    8192 // 8KB
89}
90
91fn default_sync_terminals() -> bool {
92    true
93}
94
95fn default_persist_history() -> bool {
96    true
97}
98
99fn default_retained_user_messages() -> usize {
100    4
101}
102
103fn default_sync_mcp_tools() -> bool {
104    true
105}
106
107fn default_sync_skills() -> bool {
108    true
109}
110
111fn default_spool_max_age_secs() -> u64 {
112    3600 // 1 hour
113}
114
115fn default_max_spooled_files() -> usize {
116    100
117}
118
119#[cfg_attr(feature = "schema", derive(schemars::JsonSchema))]
120#[derive(Debug, Clone, Deserialize, Serialize)]
121pub struct LedgerConfig {
122    #[serde(default = "default_enabled")]
123    pub enabled: bool,
124    #[serde(default = "default_max_entries")]
125    pub max_entries: usize,
126    /// Inject ledger into the system prompt each turn
127    #[serde(default = "default_include_in_prompt")]
128    pub include_in_prompt: bool,
129    /// Preserve ledger entries during context compression
130    #[serde(default = "default_preserve_in_compression")]
131    pub preserve_in_compression: bool,
132}
133
134impl Default for LedgerConfig {
135    fn default() -> Self {
136        Self {
137            enabled: default_enabled(),
138            max_entries: default_max_entries(),
139            include_in_prompt: default_include_in_prompt(),
140            preserve_in_compression: default_preserve_in_compression(),
141        }
142    }
143}
144
145impl LedgerConfig {
146    pub fn validate(&self) -> Result<()> {
147        ensure!(
148            self.max_entries > 0,
149            "Ledger max_entries must be greater than zero"
150        );
151        Ok(())
152    }
153}
154
155#[cfg_attr(feature = "schema", derive(schemars::JsonSchema))]
156#[derive(Debug, Clone, Deserialize, Serialize)]
157pub struct ContextFeaturesConfig {
158    /// Maximum tokens to keep in context (affects model cost and performance)
159    /// Higher values preserve more context but cost more and may hit token limits
160    /// This field is maintained for compatibility but no longer used for trimming
161    #[serde(default = "default_max_context_tokens")]
162    pub max_context_tokens: usize,
163
164    /// Percentage to trim context to when it gets too large
165    /// This field is maintained for compatibility but no longer used for trimming
166    #[serde(default = "default_trim_to_percent")]
167    pub trim_to_percent: u8,
168
169    /// Preserve recent turns during context management
170    /// This field is maintained for compatibility but no longer used for trimming
171    #[serde(default = "default_preserve_recent_turns")]
172    pub preserve_recent_turns: usize,
173
174    #[serde(default)]
175    pub ledger: LedgerConfig,
176
177    /// Dynamic context discovery settings (Cursor-style)
178    #[serde(default)]
179    pub dynamic: DynamicContextConfig,
180}
181
182impl Default for ContextFeaturesConfig {
183    fn default() -> Self {
184        Self {
185            max_context_tokens: default_max_context_tokens(),
186            trim_to_percent: default_trim_to_percent(),
187            preserve_recent_turns: default_preserve_recent_turns(),
188            ledger: LedgerConfig::default(),
189            dynamic: DynamicContextConfig::default(),
190        }
191    }
192}
193
194impl ContextFeaturesConfig {
195    pub fn validate(&self) -> Result<()> {
196        self.ledger
197            .validate()
198            .context("Invalid ledger configuration")?;
199        self.dynamic
200            .validate()
201            .context("Invalid dynamic context configuration")?;
202        Ok(())
203    }
204}
205
206fn default_enabled() -> bool {
207    true
208}
209fn default_max_entries() -> usize {
210    12
211}
212fn default_include_in_prompt() -> bool {
213    true
214}
215pub fn default_max_context_tokens() -> usize {
216    90000
217}
218
219fn default_trim_to_percent() -> u8 {
220    60
221}
222
223fn default_preserve_recent_turns() -> usize {
224    10
225}
226
227fn default_preserve_in_compression() -> bool {
228    true
229}
230
231#[cfg(test)]
232mod tests {
233    use super::DynamicContextConfig;
234
235    #[test]
236    fn dynamic_context_defaults_retain_four_user_messages() {
237        let config = DynamicContextConfig::default();
238
239        assert_eq!(config.retained_user_messages, 4);
240    }
241
242    #[test]
243    fn dynamic_context_validation_rejects_zero_retained_user_messages() {
244        let config = DynamicContextConfig {
245            retained_user_messages: 0,
246            ..DynamicContextConfig::default()
247        };
248
249        assert!(config.validate().is_err());
250    }
251}