vtcode_core/tools/registry/
builder.rs1use std::future::Future;
4use std::path::Path;
5use std::path::PathBuf;
6use std::sync::Arc;
7use std::sync::RwLock;
8
9use parking_lot::Mutex;
10
11use crate::config::{PersistentMemoryConfig, PtyConfig};
12use crate::core::memory_pool::MemoryPool;
13use crate::tool_policy::ToolPolicyManager;
14use crate::tools::handlers::PlanningWorkflowState;
15use crate::tools::output_spooler::{SpoolerConfig, ToolOutputSpooler};
16use crate::tools::safety_gateway::SafetyGateway;
17use vtcode_config::DynamicContextConfig;
18use vtcode_config::loader::ConfigManager;
19
20use super::ToolRegistry;
21use super::assembly::ToolAssembly;
22use super::circuit_breaker;
23use super::distributed::ToolConfigSnapshot;
24use super::execution_history::ToolExecutionHistory;
25use super::harness::HarnessContext;
26use super::inventory::ToolInventory;
27use super::pack::register_builtin_packs;
28use super::policy::ToolPolicyGateway;
29use super::pty;
30use super::resiliency::ResiliencyContext;
31use super::shell_policy::ShellPolicyChecker;
32use super::timeout::ToolTimeoutPolicy;
33
34fn spooler_config_from_dynamic_context(config: &DynamicContextConfig) -> SpoolerConfig {
35 SpoolerConfig {
36 enabled: config.enabled,
37 threshold_bytes: config.tool_output_threshold,
38 max_files: config.max_spooled_files,
39 max_age_secs: config.spool_max_age_secs,
40 include_file_reference: true,
41 }
42}
43
44#[derive(Debug, Default)]
45struct WorkspaceToolConfig {
46 tool: ToolConfigSnapshot,
47 spooler: SpoolerConfig,
48 persistent_memory: PersistentMemoryConfig,
49 persistent_memory_enabled: bool,
50}
51
52fn load_workspace_tool_config(workspace_root: &Path) -> WorkspaceToolConfig {
56 match ConfigManager::load_from_workspace(workspace_root) {
57 Ok(manager) => workspace_tool_config_from_loaded(manager.config()),
58 Err(err) => {
59 tracing::warn!(
60 workspace = %workspace_root.display(),
61 error = %err,
62 "Failed to load workspace config for tool registry; using defaults"
63 );
64 WorkspaceToolConfig::default()
65 }
66 }
67}
68
69fn workspace_tool_config_from_loaded(config: &vtcode_config::loader::VTCodeConfig) -> WorkspaceToolConfig {
70 let persistent_memory_enabled = config.persistent_memory_enabled();
71 let mut persistent_memory = config.agent.persistent_memory.clone();
72 persistent_memory.enabled = persistent_memory_enabled;
73 WorkspaceToolConfig {
74 tool: ToolConfigSnapshot {
75 web_search: config.tools.web_search.clone(),
76 web_fetch: config.tools.web_fetch.clone(),
77 },
78 spooler: spooler_config_from_dynamic_context(&config.context.dynamic),
79 persistent_memory,
80 persistent_memory_enabled,
81 }
82}
83
84impl ToolRegistry {
85 pub fn new(workspace_root: PathBuf) -> impl Future<Output = Self> {
86 Self::build(workspace_root, PtyConfig::default())
87 }
88
89 pub fn new_with_loaded_config(
97 workspace_root: PathBuf,
98 config: &vtcode_config::loader::VTCodeConfig,
99 ) -> impl Future<Output = Self> {
100 let snapshot = workspace_tool_config_from_loaded(config);
101 Self::build_with_snapshot(workspace_root, PtyConfig::default(), None, true, snapshot)
102 }
103
104 pub fn new_for_first_paint_with_loaded_config(
111 workspace_root: PathBuf,
112 config: &vtcode_config::loader::VTCodeConfig,
113 ) -> impl Future<Output = Self> {
114 let snapshot = workspace_tool_config_from_loaded(config);
115 Self::build_with_snapshot(workspace_root, PtyConfig::default(), None, false, snapshot)
116 }
117
118 pub fn new_with_config(workspace_root: PathBuf, pty_config: PtyConfig) -> impl Future<Output = Self> {
119 Self::build(workspace_root, pty_config)
120 }
121
122 pub fn new_for_schema(workspace_root: PathBuf) -> impl Future<Output = Self> {
124 Self::build_with_policy(workspace_root, PtyConfig::default(), None, false)
125 }
126
127 pub fn new_with_custom_policy(
128 workspace_root: PathBuf,
129 policy_manager: ToolPolicyManager,
130 ) -> impl Future<Output = Self> {
131 Self::build_with_policy(workspace_root, PtyConfig::default(), Some(policy_manager), true)
132 }
133
134 pub fn new_with_custom_policy_and_config(
135 workspace_root: PathBuf,
136 pty_config: PtyConfig,
137 policy_manager: ToolPolicyManager,
138 ) -> impl Future<Output = Self> {
139 Self::build_with_policy(workspace_root, pty_config, Some(policy_manager), true)
140 }
141
142 async fn build(workspace_root: PathBuf, pty_config: PtyConfig) -> Self {
143 Self::build_with_policy(workspace_root, pty_config, None, true).await
144 }
145
146 async fn build_with_policy(
147 workspace_root: PathBuf,
148 pty_config: PtyConfig,
149 policy_manager: Option<ToolPolicyManager>,
150 initialize_policy: bool,
151 ) -> Self {
152 let config_workspace = workspace_root.clone();
158 let workspace_config =
159 match tokio::task::spawn_blocking(move || load_workspace_tool_config(&config_workspace)).await {
160 Ok(config) => config,
161 Err(error) => {
162 tracing::warn!(%error, "Workspace tool config task panicked; using defaults");
163 WorkspaceToolConfig::default()
164 }
165 };
166 Self::build_with_snapshot(workspace_root, pty_config, policy_manager, initialize_policy, workspace_config).await
167 }
168
169 async fn build_with_snapshot(
170 workspace_root: PathBuf,
171 pty_config: PtyConfig,
172 policy_manager: Option<ToolPolicyManager>,
173 initialize_policy: bool,
174 workspace_config: WorkspaceToolConfig,
175 ) -> Self {
176 let WorkspaceToolConfig {
177 tool: tool_config,
178 spooler: spooler_config,
179 persistent_memory: persistent_memory_config,
180 persistent_memory_enabled,
181 } = workspace_config;
182 let edited_file_monitor = Arc::new(crate::tools::edited_file_monitor::EditedFileMonitor::new());
183 let inventory = ToolInventory::new(workspace_root.clone(), Arc::clone(&edited_file_monitor));
184 let planning_workflow_state = PlanningWorkflowState::new(workspace_root.clone());
185
186 register_builtin_packs(&inventory, &planning_workflow_state, &tool_config).await;
187
188 let pty_sessions = pty::PtySessionManager::new(workspace_root.clone(), pty_config);
189 let exec_sessions =
190 crate::tools::exec_session::ExecSessionManager::new(workspace_root.clone(), pty_sessions.clone());
191
192 let policy_gateway = match (policy_manager, initialize_policy) {
193 (Some(pm), _) => ToolPolicyGateway::with_policy_manager(pm),
194 (None, true) => ToolPolicyGateway::new(&workspace_root).await,
195 (None, false) => ToolPolicyGateway::without_persistence(),
196 };
197
198 let optimization_config = vtcode_config::OptimizationConfig::default();
199 let metrics = Arc::new(crate::metrics::MetricsCollector::new());
200 let hot_cache_size = std::num::NonZeroUsize::new(optimization_config.tool_registry.hot_cache_size)
201 .unwrap_or(std::num::NonZeroUsize::MIN);
202 let output_spooler = Arc::new(ToolOutputSpooler::with_config(&workspace_root, spooler_config));
203 if let Err(error) = output_spooler.cleanup_expired_files().await {
210 tracing::debug!(%error, "startup spool prune failed");
211 }
212
213 let mcp_tool_index = rustc_hash::FxHashMap::with_capacity_and_hasher(32, rustc_hash::FxBuildHasher);
217 let mcp_reverse_index = rustc_hash::FxHashMap::with_capacity_and_hasher(32, rustc_hash::FxBuildHasher);
218
219 let registry = Self {
220 inventory,
221 persistent_memory_config: Arc::new(persistent_memory_config),
222 persistent_memory_enabled,
223 edited_file_monitor,
224 policy_gateway: Arc::new(policy_gateway),
225 pty_sessions,
226 exec_sessions,
227 mcp_client: Arc::new(parking_lot::RwLock::new(None)),
228 mcp_tool_index: Arc::new(tokio::sync::RwLock::new(mcp_tool_index)),
229 mcp_reverse_index: Arc::new(tokio::sync::RwLock::new(mcp_reverse_index)),
230 timeout_policy: Arc::new(parking_lot::RwLock::new(ToolTimeoutPolicy::default())),
231 execution_history: ToolExecutionHistory::with_workspace_root(100, workspace_root.clone()),
232 patch_recovery_reads: Arc::new(Mutex::new(super::patch_recovery::PatchRecoveryReads::default())),
233 harness_context: HarnessContext::default(),
234 resiliency: Arc::new(Mutex::new(ResiliencyContext::default())),
235 mcp_circuit_breaker: Arc::new(circuit_breaker::McpCircuitBreaker::with_metrics(metrics.clone())),
236 shared_circuit_breaker: Arc::new(RwLock::new(None)),
237 initialized: Arc::new(std::sync::atomic::AtomicBool::new(false)),
238 tool_call_counter: Arc::new(std::sync::atomic::AtomicU64::new(0)),
239 pty_poll_counter: Arc::new(std::sync::atomic::AtomicU64::new(0)),
240 canonical_plans_dir: Arc::new(std::sync::OnceLock::new()),
241 metrics,
242 shell_policy: Arc::new(RwLock::new(ShellPolicyChecker::new())),
243 runtime_sandbox_config: Arc::new(RwLock::new(super::sandbox_facade::runtime_sandbox_config_default())),
244 agent_type: Arc::new(RwLock::new("unknown".to_owned())),
245 cached_available_tools: Arc::new(parking_lot::RwLock::new(None)),
246 active_tool_profile: Arc::new(RwLock::new(crate::config::ToolProfile::default())),
247 progress_callback: Arc::new(RwLock::new(None)),
248 active_pty_sessions: Arc::new(RwLock::new(None)),
249
250 memory_pool: Arc::new(MemoryPool::from_config(&optimization_config.memory_pool)),
251 hot_tool_cache: Arc::new(parking_lot::RwLock::new(lru::LruCache::new(hot_cache_size))),
252 optimization_config,
253 middleware: crate::tools::tool_middleware::MiddlewareChain::new(),
254
255 output_spooler,
256
257 planning_workflow_state,
258 planning_mode_policy_overrides: Arc::new(parking_lot::RwLock::new(None)),
259 safety_gateway: Arc::new(SafetyGateway::default()),
260 cgp_runtime_mode: Arc::new(RwLock::new(None)),
261 tool_assembly: Arc::new(RwLock::new(ToolAssembly::empty())),
262 tool_catalog_state: Arc::new(super::tool_catalog_facade::SessionToolCatalogState::new()),
263 subagent_controller: Arc::new(RwLock::new(None)),
264 matrix_worker: Arc::new(parking_lot::RwLock::new(None)),
265 matrix_coordinator: Arc::new(std::sync::atomic::AtomicBool::new(false)),
266 session_scheduler: Arc::new(tokio::sync::Mutex::new(crate::scheduler::SessionScheduler::new())),
267 session_model_tools: Arc::new(RwLock::new(None)),
268 self_ref: Arc::new(RwLock::new(None)),
269 };
270
271 registry.rebuild_tool_assembly().await;
272 registry.sync_policy_catalog().await;
273 registry.initialize_resiliency_trackers();
274 registry
275 }
276}
277
278#[cfg(test)]
279mod tests {
280 use super::*;
281 use tempfile::tempdir;
282
283 #[tokio::test]
284 async fn tool_registry_uses_workspace_dynamic_context_for_output_spooler() {
285 let temp = tempdir().unwrap();
286 std::fs::write(
287 temp.path().join("vtcode.toml"),
288 r#"[context.dynamic]
289enabled = true
290tool_output_threshold = 4096
291max_spooled_files = 7
292spool_max_age_secs = 12
293
294[workspace]
295use_root_config = true
296"#,
297 )
298 .unwrap();
299
300 let registry = ToolRegistry::new(temp.path().to_path_buf()).await;
301 let config = registry.output_spooler().config();
302
303 assert!(config.enabled);
304 assert_eq!(config.threshold_bytes, 4096);
305 assert_eq!(config.max_files, 7);
306 assert_eq!(config.max_age_secs, 12);
307 }
308
309 #[tokio::test]
310 async fn tool_registry_falls_back_to_default_spooler_config_on_invalid_workspace_config() {
311 let temp = tempdir().unwrap();
312 std::fs::write(
313 temp.path().join("vtcode.toml"),
314 r#"[context.dynamic]
315tool_output_threshold = "oops"
316"#,
317 )
318 .unwrap();
319
320 let registry = ToolRegistry::new(temp.path().to_path_buf()).await;
321 let config = registry.output_spooler().config();
322
323 assert!(config.enabled);
324 assert_eq!(config.threshold_bytes, DynamicContextConfig::default().tool_output_threshold);
325 assert_eq!(config.max_files, SpoolerConfig::default().max_files);
326 assert_eq!(config.max_age_secs, SpoolerConfig::default().max_age_secs);
327 }
328
329 #[tokio::test]
330 async fn tool_registry_captures_effective_memory_config() {
331 let temp = tempdir().expect("workspace");
332 std::fs::write(
333 temp.path().join("vtcode.toml"),
334 "[features]\nmemories = true\n\n[agent.persistent_memory]\nenabled = true\n\n[workspace]\nuse_root_config = true\n",
335 )
336 .expect("workspace config");
337
338 let snapshot = load_workspace_tool_config(temp.path());
339 assert!(snapshot.persistent_memory_enabled);
340 assert!(snapshot.persistent_memory.enabled);
341
342 let registry = ToolRegistry::new(temp.path().to_path_buf()).await;
343 assert!(registry.persistent_memory_enabled);
344 assert!(registry.persistent_memory_config.enabled);
345 }
346
347 #[tokio::test]
348 async fn schema_registry_does_not_create_workspace_policy() {
349 let temp = tempdir().expect("workspace");
350
351 let _registry = ToolRegistry::new_for_schema(temp.path().to_path_buf()).await;
352
353 assert!(!temp.path().join(".vtcode").exists());
354 }
355
356 #[tokio::test]
357 async fn loaded_config_registry_reuses_snapshot_without_workspace_parse() {
358 let temp = tempdir().expect("workspace");
362 std::fs::write(
363 temp.path().join("vtcode.toml"),
364 r#"[context.dynamic]
365enabled = true
366tool_output_threshold = 4096
367max_spooled_files = 7
368spool_max_age_secs = 12
369
370[workspace]
371use_root_config = true
372"#,
373 )
374 .expect("workspace config");
375
376 let mut loaded = vtcode_config::loader::VTCodeConfig::default();
377 loaded.context.dynamic.enabled = true;
378 loaded.context.dynamic.tool_output_threshold = 8192;
379 loaded.context.dynamic.max_spooled_files = 3;
380 loaded.context.dynamic.spool_max_age_secs = 99;
381
382 let registry = ToolRegistry::new_with_loaded_config(temp.path().to_path_buf(), &loaded).await;
383 let config = registry.output_spooler().config();
384
385 assert!(config.enabled);
386 assert_eq!(config.threshold_bytes, 8192);
387 assert_eq!(config.max_files, 3);
388 assert_eq!(config.max_age_secs, 99);
389 }
390
391 #[tokio::test]
392 async fn first_paint_registry_defers_policy_manager_until_ensure() {
393 let temp = tempdir().expect("workspace");
394 let loaded = vtcode_config::loader::VTCodeConfig::default();
395
396 let registry = ToolRegistry::new_for_first_paint_with_loaded_config(temp.path().to_path_buf(), &loaded).await;
397 assert!(!registry.has_policy_manager().await, "first-paint registry must skip policy file I/O");
398
399 registry.ensure_workspace_policy_manager(temp.path()).await;
400 assert!(
401 registry.has_policy_manager().await,
402 "hydration must attach the workspace policy manager before any tool runs"
403 );
404 }
405}