vtcode_core/context/
dynamic_init.rs1use anyhow::Result;
7use std::path::Path;
8use tokio::fs;
9use tracing::{debug, trace, warn};
10
11pub struct DynamicContextDirs {
13 pub vtcode_dir: std::path::PathBuf,
15 pub tool_outputs: std::path::PathBuf,
17 pub history: std::path::PathBuf,
19 pub mcp_tools: std::path::PathBuf,
21 pub terminals: std::path::PathBuf,
23 pub skills: std::path::PathBuf,
25}
26
27impl DynamicContextDirs {
28 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 pub fn startup_dirs(&self) -> Vec<&std::path::PathBuf> {
43 vec![&self.tool_outputs, &self.history, &self.terminals]
44 }
45}
46
47pub 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 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 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
95pub 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
117pub 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
139fn 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
193async 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
218async 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
241async 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
266pub 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 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 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 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}