Skip to main content

vtcode_core/context/
dynamic_init.rs

1//! Dynamic Context Discovery Initialization
2//!
3//! This module handles initialization of dynamic context discovery directories
4//! and index files at agent startup.
5
6use anyhow::Result;
7use std::path::Path;
8use tokio::fs;
9use tracing::{debug, trace, warn};
10
11/// Directory structure for dynamic context discovery
12pub struct DynamicContextDirs {
13    /// Root .vtcode directory
14    pub vtcode_dir: std::path::PathBuf,
15    /// Tool output spool directory
16    pub tool_outputs: std::path::PathBuf,
17    /// Conversation history directory
18    pub history: std::path::PathBuf,
19    /// MCP tools directory
20    pub mcp_tools: std::path::PathBuf,
21    /// Terminal sessions directory
22    pub terminals: std::path::PathBuf,
23    /// Skills directory
24    pub skills: std::path::PathBuf,
25}
26
27impl DynamicContextDirs {
28    /// Create directory structure from workspace root
29    pub fn from_workspace(workspace: &Path) -> Self {
30        let vtcode_dir = workspace.join(".vtcode");
31        Self {
32            tool_outputs: vtcode_dir.join("context").join("tool_outputs"),
33            history: vtcode_dir.join("history"),
34            mcp_tools: vtcode_dir.join("mcp").join("tools"),
35            terminals: vtcode_dir.join("terminals"),
36            skills: vtcode_dir.join("skills"),
37            vtcode_dir,
38        }
39    }
40
41    /// Get directories that should exist at startup.
42    pub fn startup_dirs(&self) -> Vec<&std::path::PathBuf> {
43        vec![&self.tool_outputs, &self.history, &self.terminals]
44    }
45}
46
47/// Initialize dynamic context discovery directories and index files
48///
49/// This should be called at agent startup when dynamic context is enabled.
50pub async fn initialize_dynamic_context(
51    workspace: &Path,
52    config: &vtcode_config::DynamicContextConfig,
53) -> Result<DynamicContextDirs> {
54    if !config.enabled {
55        debug!("Dynamic context discovery is disabled, skipping initialization");
56        return Ok(DynamicContextDirs::from_workspace(workspace));
57    }
58
59    let dirs = DynamicContextDirs::from_workspace(workspace);
60
61    // Create startup directories only; MCP/skills directories are created on demand.
62    for dir in dirs.startup_dirs() {
63        if let Err(e) = fs::create_dir_all(dir).await {
64            warn!(
65                path = %dir.display(),
66                error = %e,
67                "Failed to create dynamic context directory"
68            );
69        } else {
70            trace!(path = %dir.display(), "Created dynamic context directory");
71        }
72    }
73
74    // Create README in .vtcode explaining the directory structure
75    let readme_path = dirs.vtcode_dir.join("README.md");
76    if !fs::try_exists(&readme_path).await.unwrap_or(false) {
77        let readme_content = generate_vtcode_readme();
78        if let Err(e) = fs::write(&readme_path, &readme_content).await {
79            warn!(error = %e, "Failed to create .vtcode/README.md");
80        }
81    }
82
83    if config.sync_terminals {
84        create_initial_terminals_index(&dirs.terminals).await;
85    }
86
87    debug!(
88        workspace = %workspace.display(),
89        "Initialized dynamic context discovery directories"
90    );
91
92    Ok(dirs)
93}
94
95/// Ensure MCP dynamic-context directories exist once MCP is activated.
96pub async fn ensure_mcp_dynamic_context(workspace: &Path, config: &vtcode_config::DynamicContextConfig) -> Result<()> {
97    if !config.enabled || !config.sync_mcp_tools {
98        return Ok(());
99    }
100
101    let dirs = DynamicContextDirs::from_workspace(workspace);
102    if let Err(e) = fs::create_dir_all(&dirs.mcp_tools).await {
103        warn!(
104            path = %dirs.mcp_tools.display(),
105            error = %e,
106            "Failed to create MCP dynamic context directory"
107        );
108        return Ok(());
109    }
110    create_initial_mcp_index(&dirs.mcp_tools).await;
111    Ok(())
112}
113
114/// Ensure skills dynamic-context directories exist once skills are activated.
115pub async fn ensure_skills_dynamic_context(
116    workspace: &Path,
117    config: &vtcode_config::DynamicContextConfig,
118) -> Result<()> {
119    if !config.enabled || !config.sync_skills {
120        return Ok(());
121    }
122
123    let dirs = DynamicContextDirs::from_workspace(workspace);
124    if let Err(e) = fs::create_dir_all(&dirs.skills).await {
125        warn!(
126            path = %dirs.skills.display(),
127            error = %e,
128            "Failed to create skills dynamic context directory"
129        );
130        return Ok(());
131    }
132    create_initial_skills_index(&dirs.skills).await;
133    Ok(())
134}
135
136/// Generate README content for .vtcode directory
137fn generate_vtcode_readme() -> String {
138    r#"# VT Code Dynamic Context Directory
139
140This directory contains dynamic context files for VT Code agent operations.
141
142## Directory Structure
143
144```
145.vtcode/
146  context/
147    tool_outputs/     # Large tool outputs spooled to files
148  history/            # Conversation history during summarization
149  mcp/
150    tools/            # MCP tool descriptions and schemas
151    status.json       # MCP provider status
152  skills/
153    INDEX.md          # Available skills index
154    {skill_name}/     # Individual skill directories
155  terminals/
156    INDEX.md          # Terminal sessions index
157    {session_id}.txt  # Terminal session output
158```
159
160## Purpose
161
162These files implement **dynamic context discovery** - a pattern where large outputs
163are written to files instead of being truncated. This allows the agent to:
164
1651. Inspect full tool-output files on demand with `exec_command.cmd` using `sed`, `cat`, or `rg`
1662. Search code with advanced `code_search`, and search spooled text with `exec_command.cmd` plus `rg`
1673. Recover conversation details lost during summarization
1684. Discover available skills and MCP tools efficiently
169
170## Configuration
171
172Configure in `vtcode.toml`:
173
174```toml
175[context.dynamic]
176enabled = true
177tool_output_threshold = 8192  # Bytes before spooling
178sync_terminals = true
179persist_history = true
180sync_mcp_tools = true
181sync_skills = true
182```
183
184---
185*This directory is managed by VT Code. Files may be automatically created, updated, or cleaned up.*
186"#
187    .to_string()
188}
189
190/// Create initial skills INDEX.md
191async fn create_initial_skills_index(skills_dir: &Path) {
192    let index_path = skills_dir.join("INDEX.md");
193    if fs::try_exists(&index_path).await.unwrap_or(false) {
194        return;
195    }
196
197    let content = r#"# Skills Index
198
199This file lists all available skills for dynamic discovery.
200Use `exec_command.cmd` with `find`, `sed`, or `cat` on individual skill directories for full documentation.
201
202*No skills available yet.*
203
204Create skills using the `save_skill` tool.
205
206---
207*Generated automatically. Do not edit manually.*
208"#;
209
210    if let Err(e) = fs::write(&index_path, content).await {
211        warn!(error = %e, "Failed to create initial skills index");
212    }
213}
214
215/// Create initial terminals INDEX.md
216async fn create_initial_terminals_index(terminals_dir: &Path) {
217    let index_path = terminals_dir.join("INDEX.md");
218    if fs::try_exists(&index_path).await.unwrap_or(false) {
219        return;
220    }
221
222    let content = r#"# Terminal Sessions Index
223
224This file lists all active terminal sessions for dynamic discovery.
225Use `exec_command.cmd` with `sed`, `cat`, or `rg` on individual session files for full output.
226
227*No active terminal sessions.*
228
229---
230*Generated automatically. Do not edit manually.*
231"#;
232
233    if let Err(e) = fs::write(&index_path, content).await {
234        warn!(error = %e, "Failed to create initial terminals index");
235    }
236}
237
238/// Create initial MCP tools INDEX.md
239async fn create_initial_mcp_index(mcp_tools_dir: &Path) {
240    let index_path = mcp_tools_dir.join("INDEX.md");
241    if fs::try_exists(&index_path).await.unwrap_or(false) {
242        return;
243    }
244
245    let content = r#"# MCP Tools Index
246
247This file lists all available MCP tools for dynamic discovery.
248Use `exec_command.cmd` with `sed`, `cat`, or `rg` on individual tool files for full schema details.
249
250*No MCP tools available.*
251
252Configure MCP servers in `vtcode.toml` or `.mcp.json`.
253
254---
255*Generated automatically. Do not edit manually.*
256"#;
257
258    if let Err(e) = fs::write(&index_path, content).await {
259        warn!(error = %e, "Failed to create initial MCP tools index");
260    }
261}
262
263#[cfg(test)]
264mod tests {
265    use super::*;
266    use tempfile::tempdir;
267
268    #[tokio::test]
269    async fn test_initialize_dynamic_context() {
270        let temp = tempdir().unwrap();
271        let config = vtcode_config::DynamicContextConfig::default();
272
273        let dirs = initialize_dynamic_context(temp.path(), &config).await.unwrap();
274
275        assert!(dirs.vtcode_dir.exists());
276        assert!(dirs.tool_outputs.exists());
277        assert!(dirs.history.exists());
278        assert!(dirs.terminals.exists());
279        assert!(!dirs.skills.exists());
280        assert!(!dirs.mcp_tools.exists());
281
282        // Check README was created
283        assert!(dirs.vtcode_dir.join("README.md").exists());
284        let readme = fs::read_to_string(dirs.vtcode_dir.join("README.md")).await.unwrap();
285        assert!(readme.contains("`exec_command.cmd`"));
286        assert!(readme.contains("`code_search`"));
287
288        // Check startup index files were created
289        assert!(dirs.terminals.join("INDEX.md").exists());
290        let terminals_index = fs::read_to_string(dirs.terminals.join("INDEX.md")).await.unwrap();
291        assert!(terminals_index.contains("`exec_command.cmd` with `sed`, `cat`, or `rg`"));
292        assert!(!dirs.skills.join("INDEX.md").exists());
293        assert!(!dirs.mcp_tools.join("INDEX.md").exists());
294    }
295
296    #[tokio::test]
297    async fn test_disabled_skips_creation() {
298        let temp = tempdir().unwrap();
299        let config = vtcode_config::DynamicContextConfig { enabled: false, ..Default::default() };
300
301        let dirs = initialize_dynamic_context(temp.path(), &config).await.unwrap();
302
303        // Directories should not be created when disabled
304        assert!(!dirs.vtcode_dir.exists());
305    }
306
307    #[tokio::test]
308    async fn test_ensure_mcp_dynamic_context() {
309        let temp = tempdir().unwrap();
310        let config = vtcode_config::DynamicContextConfig::default();
311
312        ensure_mcp_dynamic_context(temp.path(), &config).await.unwrap();
313
314        let dirs = DynamicContextDirs::from_workspace(temp.path());
315        assert!(dirs.mcp_tools.exists());
316        assert!(dirs.mcp_tools.join("INDEX.md").exists());
317        let index = fs::read_to_string(dirs.mcp_tools.join("INDEX.md")).await.unwrap();
318        assert!(index.contains("`exec_command.cmd` with `sed`, `cat`, or `rg`"));
319    }
320
321    #[tokio::test]
322    async fn test_ensure_skills_dynamic_context() {
323        let temp = tempdir().unwrap();
324        let config = vtcode_config::DynamicContextConfig::default();
325
326        ensure_skills_dynamic_context(temp.path(), &config).await.unwrap();
327
328        let dirs = DynamicContextDirs::from_workspace(temp.path());
329        assert!(dirs.skills.exists());
330        assert!(dirs.skills.join("INDEX.md").exists());
331        let index = fs::read_to_string(dirs.skills.join("INDEX.md")).await.unwrap();
332        assert!(index.contains("`exec_command.cmd` with `find`, `sed`, or `cat`"));
333    }
334}