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 read_file/grep_file.
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    /// Enable syncing MCP tool descriptions to .vtcode/mcp/tools/
29    #[serde(default = "default_sync_mcp_tools")]
30    pub sync_mcp_tools: bool,
31
32    /// Enable generating skill index in .vtcode/skills/INDEX.md
33    #[serde(default = "default_sync_skills")]
34    pub sync_skills: bool,
35
36    /// Maximum age in seconds for spooled tool output files before cleanup
37    #[serde(default = "default_spool_max_age_secs")]
38    pub spool_max_age_secs: u64,
39
40    /// Maximum number of spooled files to keep
41    #[serde(default = "default_max_spooled_files")]
42    pub max_spooled_files: usize,
43}
44
45impl Default for DynamicContextConfig {
46    fn default() -> Self {
47        Self {
48            enabled: default_dynamic_enabled(),
49            tool_output_threshold: default_tool_output_threshold(),
50            sync_terminals: default_sync_terminals(),
51            persist_history: default_persist_history(),
52            sync_mcp_tools: default_sync_mcp_tools(),
53            sync_skills: default_sync_skills(),
54            spool_max_age_secs: default_spool_max_age_secs(),
55            max_spooled_files: default_max_spooled_files(),
56        }
57    }
58}
59
60impl DynamicContextConfig {
61    pub fn validate(&self) -> Result<()> {
62        ensure!(
63            self.tool_output_threshold >= 1024,
64            "Tool output threshold must be at least 1024 bytes"
65        );
66        ensure!(
67            self.max_spooled_files > 0,
68            "Max spooled files must be greater than zero"
69        );
70        Ok(())
71    }
72}
73
74fn default_dynamic_enabled() -> bool {
75    true
76}
77
78fn default_tool_output_threshold() -> usize {
79    8192 // 8KB
80}
81
82fn default_sync_terminals() -> bool {
83    true
84}
85
86fn default_persist_history() -> bool {
87    true
88}
89
90fn default_sync_mcp_tools() -> bool {
91    true
92}
93
94fn default_sync_skills() -> bool {
95    true
96}
97
98fn default_spool_max_age_secs() -> u64 {
99    3600 // 1 hour
100}
101
102fn default_max_spooled_files() -> usize {
103    100
104}
105
106#[cfg_attr(feature = "schema", derive(schemars::JsonSchema))]
107#[derive(Debug, Clone, Deserialize, Serialize)]
108pub struct LedgerConfig {
109    #[serde(default = "default_enabled")]
110    pub enabled: bool,
111    #[serde(default = "default_max_entries")]
112    pub max_entries: usize,
113    /// Inject ledger into the system prompt each turn
114    #[serde(default = "default_include_in_prompt")]
115    pub include_in_prompt: bool,
116    /// Preserve ledger entries during context compression
117    #[serde(default = "default_preserve_in_compression")]
118    pub preserve_in_compression: bool,
119}
120
121impl Default for LedgerConfig {
122    fn default() -> Self {
123        Self {
124            enabled: default_enabled(),
125            max_entries: default_max_entries(),
126            include_in_prompt: default_include_in_prompt(),
127            preserve_in_compression: default_preserve_in_compression(),
128        }
129    }
130}
131
132impl LedgerConfig {
133    pub fn validate(&self) -> Result<()> {
134        ensure!(
135            self.max_entries > 0,
136            "Ledger max_entries must be greater than zero"
137        );
138        Ok(())
139    }
140}
141
142#[cfg_attr(feature = "schema", derive(schemars::JsonSchema))]
143#[derive(Debug, Clone, Deserialize, Serialize)]
144pub struct ContextFeaturesConfig {
145    /// Maximum tokens to keep in context (affects model cost and performance)
146    /// Higher values preserve more context but cost more and may hit token limits
147    /// This field is maintained for compatibility but no longer used for trimming
148    #[serde(default = "default_max_context_tokens")]
149    pub max_context_tokens: usize,
150
151    /// Percentage to trim context to when it gets too large
152    /// This field is maintained for compatibility but no longer used for trimming
153    #[serde(default = "default_trim_to_percent")]
154    pub trim_to_percent: u8,
155
156    /// Preserve recent turns during context management
157    /// This field is maintained for compatibility but no longer used for trimming
158    #[serde(default = "default_preserve_recent_turns")]
159    pub preserve_recent_turns: usize,
160
161    #[serde(default)]
162    pub ledger: LedgerConfig,
163
164    /// Dynamic context discovery settings (Cursor-style)
165    #[serde(default)]
166    pub dynamic: DynamicContextConfig,
167}
168
169impl Default for ContextFeaturesConfig {
170    fn default() -> Self {
171        Self {
172            max_context_tokens: default_max_context_tokens(),
173            trim_to_percent: default_trim_to_percent(),
174            preserve_recent_turns: default_preserve_recent_turns(),
175            ledger: LedgerConfig::default(),
176            dynamic: DynamicContextConfig::default(),
177        }
178    }
179}
180
181impl ContextFeaturesConfig {
182    pub fn validate(&self) -> Result<()> {
183        self.ledger
184            .validate()
185            .context("Invalid ledger configuration")?;
186        self.dynamic
187            .validate()
188            .context("Invalid dynamic context configuration")?;
189        Ok(())
190    }
191}
192
193fn default_enabled() -> bool {
194    true
195}
196fn default_max_entries() -> usize {
197    12
198}
199fn default_include_in_prompt() -> bool {
200    true
201}
202fn default_max_context_tokens() -> usize {
203    90000
204}
205
206fn default_trim_to_percent() -> u8 {
207    60
208}
209
210fn default_preserve_recent_turns() -> usize {
211    10
212}
213
214fn default_preserve_in_compression() -> bool {
215    true
216}