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 !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
95pub 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
114pub 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
136fn 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
190async 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
215async 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
238async 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 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 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 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}