Skip to main content

vtcode_config/
context.rs

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