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 through shell inspection or `code_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    fn validate(&self) -> Result<()> {
68        ensure!(self.tool_output_threshold >= 1024, "Tool output threshold must be at least 1024 bytes");
69        ensure!(self.max_spooled_files > 0, "Max spooled files must be greater than zero");
70        ensure!(self.retained_user_messages > 0, "Retained user messages must be greater than zero");
71        Ok(())
72    }
73}
74
75fn default_dynamic_enabled() -> bool {
76    true
77}
78
79fn default_tool_output_threshold() -> usize {
80    8192 // 8KB
81}
82
83fn default_sync_terminals() -> bool {
84    true
85}
86
87fn default_persist_history() -> bool {
88    true
89}
90
91fn default_retained_user_messages() -> usize {
92    4
93}
94
95fn default_sync_mcp_tools() -> bool {
96    true
97}
98
99fn default_sync_skills() -> bool {
100    true
101}
102
103fn default_spool_max_age_secs() -> u64 {
104    3600 // 1 hour
105}
106
107fn default_max_spooled_files() -> usize {
108    100
109}
110
111#[cfg_attr(feature = "schema", derive(schemars::JsonSchema))]
112#[derive(Debug, Clone, Deserialize, Serialize)]
113pub struct LedgerConfig {
114    #[serde(default = "default_enabled")]
115    pub enabled: bool,
116    #[serde(default = "default_max_entries")]
117    pub max_entries: usize,
118    /// Inject ledger into the system prompt each turn
119    #[serde(default = "default_include_in_prompt")]
120    include_in_prompt: bool,
121    /// Preserve ledger entries during context compression
122    #[serde(default = "default_preserve_in_compression")]
123    preserve_in_compression: bool,
124}
125
126impl Default for LedgerConfig {
127    fn default() -> Self {
128        Self {
129            enabled: default_enabled(),
130            max_entries: default_max_entries(),
131            include_in_prompt: default_include_in_prompt(),
132            preserve_in_compression: default_preserve_in_compression(),
133        }
134    }
135}
136
137impl LedgerConfig {
138    fn validate(&self) -> Result<()> {
139        ensure!(self.max_entries > 0, "Ledger max_entries must be greater than zero");
140        Ok(())
141    }
142}
143
144#[cfg_attr(feature = "schema", derive(schemars::JsonSchema))]
145#[derive(Debug, Clone, Deserialize, Serialize)]
146pub struct ContextFeaturesConfig {
147    /// Session context safety ceiling. Zero automatically uses the resolved
148    /// model capacity. Compaction reserves the next response within this limit.
149    #[serde(default = "default_max_context_tokens")]
150    pub max_context_tokens: usize,
151
152    /// Percentage to trim context to when it gets too large
153    /// This field is maintained for compatibility but no longer used for trimming
154    #[serde(default = "default_trim_to_percent")]
155    trim_to_percent: u8,
156
157    /// Preserve recent turns during context management
158    /// This field is maintained for compatibility but no longer used for trimming
159    #[serde(default = "default_preserve_recent_turns")]
160    pub preserve_recent_turns: usize,
161
162    #[serde(default)]
163    pub ledger: LedgerConfig,
164
165    /// Dynamic context discovery settings (Cursor-style)
166    #[serde(default)]
167    pub dynamic: DynamicContextConfig,
168}
169
170impl Default for ContextFeaturesConfig {
171    fn default() -> Self {
172        Self {
173            max_context_tokens: default_max_context_tokens(),
174            trim_to_percent: default_trim_to_percent(),
175            preserve_recent_turns: default_preserve_recent_turns(),
176            ledger: LedgerConfig::default(),
177            dynamic: DynamicContextConfig::default(),
178        }
179    }
180}
181
182impl ContextFeaturesConfig {
183    pub(crate) fn validate(&self) -> Result<()> {
184        self.ledger.validate().context("Invalid ledger configuration")?;
185        self.dynamic.validate().context("Invalid dynamic context configuration")?;
186        Ok(())
187    }
188}
189
190fn default_max_entries() -> usize {
191    12
192}
193fn default_include_in_prompt() -> bool {
194    true
195}
196pub fn default_max_context_tokens() -> usize {
197    0
198}
199
200fn default_trim_to_percent() -> u8 {
201    60
202}
203
204fn default_preserve_recent_turns() -> usize {
205    10
206}
207
208fn default_preserve_in_compression() -> bool {
209    true
210}
211
212#[cfg(test)]
213mod tests {
214    use super::DynamicContextConfig;
215
216    #[test]
217    fn dynamic_context_defaults_retain_four_user_messages() {
218        let config = DynamicContextConfig::default();
219
220        assert_eq!(config.retained_user_messages, 4);
221    }
222
223    #[test]
224    fn dynamic_context_validation_rejects_zero_retained_user_messages() {
225        let config = DynamicContextConfig {
226            retained_user_messages: 0,
227            ..DynamicContextConfig::default()
228        };
229
230        assert!(config.validate().is_err());
231    }
232}