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 !readme_path.exists() {
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(
97    workspace: &Path,
98    config: &vtcode_config::DynamicContextConfig,
99) -> Result<()> {
100    if !config.enabled || !config.sync_mcp_tools {
101        return Ok(());
102    }
103
104    let dirs = DynamicContextDirs::from_workspace(workspace);
105    if let Err(e) = fs::create_dir_all(&dirs.mcp_tools).await {
106        warn!(
107            path = %dirs.mcp_tools.display(),
108            error = %e,
109            "Failed to create MCP dynamic context directory"
110        );
111        return Ok(());
112    }
113    create_initial_mcp_index(&dirs.mcp_tools).await;
114    Ok(())
115}
116
117/// Ensure skills dynamic-context directories exist once skills are activated.
118pub async fn ensure_skills_dynamic_context(
119    workspace: &Path,
120    config: &vtcode_config::DynamicContextConfig,
121) -> Result<()> {
122    if !config.enabled || !config.sync_skills {
123        return Ok(());
124    }
125
126    let dirs = DynamicContextDirs::from_workspace(workspace);
127    if let Err(e) = fs::create_dir_all(&dirs.skills).await {
128        warn!(
129            path = %dirs.skills.display(),
130            error = %e,
131            "Failed to create skills dynamic context directory"
132        );
133        return Ok(());
134    }
135    create_initial_skills_index(&dirs.skills).await;
136    Ok(())
137}
138
139/// Generate README content for .vtcode directory
140fn generate_vtcode_readme() -> String {
141    r#"# VT Code Dynamic Context Directory
142
143This directory contains dynamic context files for VT Code agent operations.
144
145## Directory Structure
146
147```
148.vtcode/
149  context/
150    tool_outputs/     # Large tool outputs spooled to files
151  history/            # Conversation history during summarization
152  mcp/
153    tools/            # MCP tool descriptions and schemas
154    status.json       # MCP provider status
155  skills/
156    INDEX.md          # Available skills index
157    {skill_name}/     # Individual skill directories
158  terminals/
159    INDEX.md          # Terminal sessions index
160    {session_id}.txt  # Terminal session output
161```
162
163## Purpose
164
165These files implement **dynamic context discovery** - a pattern where large outputs
166are written to files instead of being truncated. This allows the agent to:
167
1681. Retrieve full tool outputs on demand via `unified_file`
1692. Search through outputs using `unified_search`
1703. Recover conversation details lost during summarization
1714. Discover available skills and MCP tools efficiently
172
173## Configuration
174
175Configure in `vtcode.toml`:
176
177```toml
178[context.dynamic]
179enabled = true
180tool_output_threshold = 8192  # Bytes before spooling
181sync_terminals = true
182persist_history = true
183sync_mcp_tools = true
184sync_skills = true
185```
186
187---
188*This directory is managed by VT Code. Files may be automatically created, updated, or cleaned up.*
189"#
190    .to_string()
191}
192
193/// Create initial skills INDEX.md
194async fn create_initial_skills_index(skills_dir: &Path) {
195    let index_path = skills_dir.join("INDEX.md");
196    if index_path.exists() {
197        return;
198    }
199
200    let content = r#"# Skills Index
201
202This file lists all available skills for dynamic discovery.
203Use `unified_file` (action='read') on individual skill directories for full documentation.
204
205*No skills available yet.*
206
207Create skills using the `save_skill` tool.
208
209---
210*Generated automatically. Do not edit manually.*
211"#;
212
213    if let Err(e) = fs::write(&index_path, content).await {
214        warn!(error = %e, "Failed to create initial skills index");
215    }
216}
217
218/// Create initial terminals INDEX.md
219async fn create_initial_terminals_index(terminals_dir: &Path) {
220    let index_path = terminals_dir.join("INDEX.md");
221    if index_path.exists() {
222        return;
223    }
224
225    let content = r#"# Terminal Sessions Index
226
227This file lists all active terminal sessions for dynamic discovery.
228Use `unified_file` (action='read') on individual session files for full output.
229
230*No active terminal sessions.*
231
232---
233*Generated automatically. Do not edit manually.*
234"#;
235
236    if let Err(e) = fs::write(&index_path, content).await {
237        warn!(error = %e, "Failed to create initial terminals index");
238    }
239}
240
241/// Create initial MCP tools INDEX.md
242async fn create_initial_mcp_index(mcp_tools_dir: &Path) {
243    let index_path = mcp_tools_dir.join("INDEX.md");
244    if index_path.exists() {
245        return;
246    }
247
248    let content = r#"# MCP Tools Index
249
250This file lists all available MCP tools for dynamic discovery.
251Use `unified_file` (action='read') on individual tool files for full schema details.
252
253*No MCP tools available.*
254
255Configure MCP servers in `vtcode.toml` or `.mcp.json`.
256
257---
258*Generated automatically. Do not edit manually.*
259"#;
260
261    if let Err(e) = fs::write(&index_path, content).await {
262        warn!(error = %e, "Failed to create initial MCP tools index");
263    }
264}
265
266/// Check if dynamic context directories exist
267pub fn check_dynamic_context_exists(workspace: &Path) -> bool {
268    let dirs = DynamicContextDirs::from_workspace(workspace);
269    dirs.vtcode_dir.exists()
270}
271
272#[cfg(test)]
273mod tests {
274    use super::*;
275    use tempfile::tempdir;
276
277    #[tokio::test]
278    async fn test_initialize_dynamic_context() {
279        let temp = tempdir().unwrap();
280        let config = vtcode_config::DynamicContextConfig::default();
281
282        let dirs = initialize_dynamic_context(temp.path(), &config)
283            .await
284            .unwrap();
285
286        assert!(dirs.vtcode_dir.exists());
287        assert!(dirs.tool_outputs.exists());
288        assert!(dirs.history.exists());
289        assert!(dirs.terminals.exists());
290        assert!(!dirs.skills.exists());
291        assert!(!dirs.mcp_tools.exists());
292
293        // Check README was created
294        assert!(dirs.vtcode_dir.join("README.md").exists());
295        let readme = fs::read_to_string(dirs.vtcode_dir.join("README.md"))
296            .await
297            .unwrap();
298        assert!(readme.contains("`unified_file`"));
299        assert!(readme.contains("`unified_search`"));
300
301        // Check startup index files were created
302        assert!(dirs.terminals.join("INDEX.md").exists());
303        let terminals_index = fs::read_to_string(dirs.terminals.join("INDEX.md"))
304            .await
305            .unwrap();
306        assert!(terminals_index.contains("`unified_file` (action='read')"));
307        assert!(!dirs.skills.join("INDEX.md").exists());
308        assert!(!dirs.mcp_tools.join("INDEX.md").exists());
309    }
310
311    #[tokio::test]
312    async fn test_disabled_skips_creation() {
313        let temp = tempdir().unwrap();
314        let config = vtcode_config::DynamicContextConfig {
315            enabled: false,
316            ..Default::default()
317        };
318
319        let dirs = initialize_dynamic_context(temp.path(), &config)
320            .await
321            .unwrap();
322
323        // Directories should not be created when disabled
324        assert!(!dirs.vtcode_dir.exists());
325    }
326
327    #[tokio::test]
328    async fn test_ensure_mcp_dynamic_context() {
329        let temp = tempdir().unwrap();
330        let config = vtcode_config::DynamicContextConfig::default();
331
332        ensure_mcp_dynamic_context(temp.path(), &config)
333            .await
334            .unwrap();
335
336        let dirs = DynamicContextDirs::from_workspace(temp.path());
337        assert!(dirs.mcp_tools.exists());
338        assert!(dirs.mcp_tools.join("INDEX.md").exists());
339        let index = fs::read_to_string(dirs.mcp_tools.join("INDEX.md"))
340            .await
341            .unwrap();
342        assert!(index.contains("`unified_file` (action='read')"));
343    }
344
345    #[tokio::test]
346    async fn test_ensure_skills_dynamic_context() {
347        let temp = tempdir().unwrap();
348        let config = vtcode_config::DynamicContextConfig::default();
349
350        ensure_skills_dynamic_context(temp.path(), &config)
351            .await
352            .unwrap();
353
354        let dirs = DynamicContextDirs::from_workspace(temp.path());
355        assert!(dirs.skills.exists());
356        assert!(dirs.skills.join("INDEX.md").exists());
357        let index = fs::read_to_string(dirs.skills.join("INDEX.md"))
358            .await
359            .unwrap();
360        assert!(index.contains("`unified_file` (action='read')"));
361    }
362}