1use crate::config::VTCodeConfig;
4use crate::config::constants::tools;
5use crate::config::models::{ModelId, Provider};
6use crate::config::types::{ReasoningEffortLevel, VerbosityLevel};
7use crate::core::agent::events::EventSink;
8use crate::core::agent::features::FeatureSet;
9use crate::core::agent::session_config::ResolvedSessionConfig;
10use crate::core::agent::steering::SteeringMessage;
11use crate::core::threads::{ThreadBootstrap, ThreadRuntimeHandle, build_thread_archive_metadata};
12use std::path::Path;
13
14#[derive(Clone, Default)]
16pub struct RunnerSettings {
17 pub reasoning_effort: Option<ReasoningEffortLevel>,
19 pub verbosity: Option<VerbosityLevel>,
21}
22
23use crate::core::agent::types::AgentType;
24use crate::core::loop_detector::LoopDetector;
25use crate::exec::events::ThreadEvent;
26use crate::llm::AnyClient;
27use crate::llm::client::ProviderClientAdapter;
28use crate::llm::factory::{ProviderConfig, create_provider_with_config, infer_provider_from_model};
29use crate::llm::provider as uni_provider;
30use crate::primary_agent::ActivePrimaryAgent;
31use crate::prompts::PromptContext;
32use crate::tools::ToolRegistry;
33
34use anyhow::{Context, Result, anyhow};
35use parking_lot::{Mutex, RwLock};
36use std::path::PathBuf;
37use std::str::FromStr;
38use std::sync::Arc;
39use tracing::{info, warn};
40use vtcode_config::auth::OpenAIChatGptAuthHandle;
41
42mod config_helpers;
43mod constants;
44mod continuation;
45mod contract_render;
46mod escalation;
47mod evaluator_types;
48mod execute;
49mod execute_checks;
50mod execute_helpers;
51mod helpers;
52mod orchestration;
53mod output;
54mod planner_helpers;
55mod planner_types;
56pub mod prompt_alignment;
57mod replan_helpers;
58mod retry;
59mod summarize;
60mod summary;
61mod task_setup;
62mod telemetry;
63mod tool_access;
64mod tool_args;
65mod tool_dispatch_common;
66mod tool_exec;
67mod tool_execution_guard;
68mod tool_rejection;
69mod tool_types;
70mod types;
71mod validation;
72mod verification;
73mod workspace_detection;
74
75async fn attach_mcp_client(
80 tool_registry: &ToolRegistry,
81 session_config: &ResolvedSessionConfig,
82 workspace: &Path,
83) -> Result<()> {
84 let config = session_config.effective();
85 let sandbox_context = if config.sandbox.enabled
86 && !matches!(config.sandbox.default_policy, vtcode_config::SandboxPolicy::DangerFullAccess)
87 {
88 let policy = crate::tools::registry::sandbox_policy_from_runtime_config(&config.sandbox, workspace)?;
89 Some(crate::mcp::McpSandboxContext::new(policy, workspace))
90 } else {
91 None
92 };
93
94 let mut client = crate::mcp::McpClient::with_sandbox_context(config.mcp.clone(), sandbox_context);
95 let startup_timeout = std::time::Duration::from_secs(config.mcp.startup_timeout_seconds.unwrap_or(30));
96 let result = tokio::time::timeout(startup_timeout, client.initialize()).await;
97 match result {
98 Ok(Ok(())) => {}
99 Ok(Err(err)) => return Err(err).context("MCP client initialization failed"),
100 Err(_) => anyhow::bail!("MCP client initialization timed out after {} seconds", startup_timeout.as_secs()),
101 }
102
103 let client = Arc::new(client);
104 tool_registry.set_mcp_client(client).await;
105 if let Err(err) = tool_registry.refresh_mcp_tools().await {
106 warn!("Failed to refresh MCP tools after attach: {err:#}");
107 }
108 info!("MCP client attached to runner tool registry");
109 Ok(())
110}
111
112#[cfg(test)]
113mod tests;
114
115type ToolArgTransform = Arc<dyn Fn(&str, serde_json::Value) -> serde_json::Value + Send + Sync>;
116
117pub struct AgentRunner {
119 agent_type: AgentType,
121 client: AnyClient,
123 provider_client: Box<dyn uni_provider::LLMProvider>,
125 tool_registry: ToolRegistry,
127 system_prompt: String,
129 system_prompt_report: crate::prompts::system::SystemPromptReport,
135 session_id: String,
137 bootstrap_messages: Vec<crate::llm::provider::Message>,
139 _workspace: PathBuf,
141 session_config: Arc<ResolvedSessionConfig>,
143 session_tools_config: crate::tools::handlers::SessionToolsConfig,
145 model: String,
147 _api_key: String,
149 reasoning_effort: Option<ReasoningEffortLevel>,
151 verbosity: Option<VerbosityLevel>,
153 quiet: bool,
155 event_sink: Option<EventSink>,
157 thread_handle: ThreadRuntimeHandle,
159 max_turns: usize,
161 loop_detector: Mutex<LoopDetector>,
163 steering_receiver: Mutex<Option<tokio::sync::mpsc::UnboundedReceiver<SteeringMessage>>>,
167 tool_definitions_override: RwLock<Option<Vec<uni_provider::ToolDefinition>>>,
169 tool_arg_transform: Option<ToolArgTransform>,
171 local_tools_only: bool,
172 active_primary_agent: Option<ActivePrimaryAgent>,
174}
175
176impl AgentRunner {
177 fn get_selected_model(&self) -> String {
179 self.model.clone()
180 }
181
182 fn runner_println(&self, args: std::fmt::Arguments) {
183 if !self.quiet {
184 println!("{args}");
185 }
186 }
187
188 pub async fn new(
190 agent_type: AgentType,
191 model: ModelId,
192 api_key: String,
193 workspace: PathBuf,
194 session_id: String,
195 settings: RunnerSettings,
196 steering_receiver: Option<tokio::sync::mpsc::UnboundedReceiver<SteeringMessage>>,
197 ) -> Result<Self> {
198 Box::pin(Self::new_internal(
199 agent_type,
200 model,
201 api_key,
202 workspace,
203 session_id,
204 settings,
205 steering_receiver,
206 ThreadBootstrap::new(None),
207 None,
208 None,
209 ))
210 .await
211 }
212
213 #[allow(
215 clippy::too_many_arguments,
216 reason = "Intentional compatibility, platform, or test-only suppression."
217 )]
218 pub async fn new_with_bootstrap(
219 agent_type: AgentType,
220 model: ModelId,
221 api_key: String,
222 workspace: PathBuf,
223 session_id: String,
224 settings: RunnerSettings,
225 steering_receiver: Option<tokio::sync::mpsc::UnboundedReceiver<SteeringMessage>>,
226 bootstrap: ThreadBootstrap,
227 vt_cfg: Option<VTCodeConfig>,
228 openai_chatgpt_auth: Option<OpenAIChatGptAuthHandle>,
229 ) -> Result<Self> {
230 Box::pin(Self::new_internal(
231 agent_type,
232 model,
233 api_key,
234 workspace,
235 session_id,
236 settings,
237 steering_receiver,
238 bootstrap,
239 vt_cfg,
240 openai_chatgpt_auth,
241 ))
242 .await
243 }
244
245 #[expect(
246 clippy::too_many_arguments,
247 reason = "Intentional compatibility, platform, test, or API-shape suppression."
248 )]
249 async fn new_internal(
250 agent_type: AgentType,
251 model: ModelId,
252 api_key: String,
253 workspace: PathBuf,
254 session_id: String,
255 settings: RunnerSettings,
256 steering_receiver: Option<tokio::sync::mpsc::UnboundedReceiver<SteeringMessage>>,
257 bootstrap: ThreadBootstrap,
258 vt_cfg: Option<VTCodeConfig>,
259 openai_chatgpt_auth: Option<OpenAIChatGptAuthHandle>,
260 ) -> Result<Self> {
261 let session_config = if let Some(vt_cfg) = vt_cfg {
263 ResolvedSessionConfig::from_config(vt_cfg)
264 } else {
265 match ResolvedSessionConfig::load_from_workspace(&workspace) {
266 Ok(session_config) => session_config,
267 Err(err) => {
268 warn!("Failed to load vtcode configuration for system prompt composition: {err:#}");
269 ResolvedSessionConfig::from_config(VTCodeConfig::default())
270 }
271 }
272 };
273 let session_config = Arc::new(session_config);
274 let provider_name = {
275 let configured = session_config.effective().agent.provider.trim();
276 if configured.is_empty() {
277 infer_provider_from_model(&model.as_str())
278 .map(|provider| provider.to_string())
279 .ok_or_else(|| anyhow!("Failed to determine provider for model {model}"))?
280 } else {
281 configured.to_lowercase()
282 }
283 };
284 let provider_config = ProviderConfig {
285 api_key: Some(api_key.clone()),
286 openai_chatgpt_auth: openai_chatgpt_auth.clone(),
287 copilot_auth: Some(session_config.effective().auth.copilot.clone()),
288 base_url: None,
289 model: Some(model.to_string()),
290 prompt_cache: Some(session_config.effective().prompt_cache.clone()),
291 timeouts: None,
292 openai: Some(session_config.effective().provider.openai.clone()),
293 anthropic: Some(session_config.effective().provider.anthropic.clone()),
294 openrouter: Some(session_config.effective().provider.openrouter.clone()),
295 model_behavior: Some(session_config.effective().model.clone()),
296 workspace_root: Some(workspace.clone()),
297 };
298
299 let client: AnyClient = Box::new(ProviderClientAdapter::new(
300 create_provider_with_config(&provider_name, provider_config.clone())
301 .with_context(|| "Failed to create client provider")?,
302 model.to_string(),
303 ));
304 let provider_client = create_provider_with_config(&provider_name, provider_config)
305 .with_context(|| "Failed to create provider client")?;
306 if std::env::var_os("VTCODE_DEBUG_PROVIDER").is_some() {
307 eprintln!(
308 "vtcode-debug: runner provider={} client_provider={} model={}",
309 provider_name,
310 provider_client.name(),
311 model
312 );
313 }
314 let max_repeated_tool_calls = session_config.effective().tools.max_repeated_tool_calls.max(1);
315 let deferred_tool_policy = crate::tools::handlers::deferred_tool_policy_for_runtime(
316 crate::llm::factory::infer_provider(Some(&session_config.effective().agent.provider), &model.as_str()),
317 provider_client.supports_responses_compaction(&model.as_str()),
318 Some(session_config.effective()),
319 );
320 let anthropic_native_memory_enabled = crate::tools::handlers::anthropic_native_memory_enabled_for_runtime(
321 Provider::from_str(provider_client.name()).ok(),
322 &model.as_str(),
323 Some(session_config.effective()),
324 );
325 let tool_registry = ToolRegistry::new(workspace.clone()).await;
326 tool_registry.set_inspection_mode(session_config.effective().agent.harness.inspection.mode);
327 tool_registry.set_harness_session(session_id.clone());
328 tool_registry.set_matrix_coordinator(session_config.effective().default_primary_agent == "coordinator");
329 tool_registry.set_agent_type(agent_type.to_string());
330 tool_registry.initialize_async().await?;
331 if let Err(err) = tool_registry
332 .apply_session_runtime_config(
333 &session_config.effective().commands,
334 &session_config.effective().permissions,
335 &session_config.effective().sandbox,
336 &session_config.effective().timeouts,
337 &session_config.effective().tools,
338 )
339 .await
340 {
341 warn!("Failed to apply tool policies from config: {}", err);
342 }
343 if session_config.effective().mcp.enabled {
344 if let Err(err) = crate::mcp::validate_mcp_config(&session_config.effective().mcp) {
345 warn!("MCP configuration validation error: {err}");
346 }
347 if let Err(err) = attach_mcp_client(&tool_registry, &session_config, &workspace).await {
348 warn!("Failed to attach MCP client: {err:#}");
349 }
350 }
351 if session_config.effective().context.dynamic.enabled
352 && let Err(err) =
353 crate::context::initialize_dynamic_context(&workspace, &session_config.effective().context.dynamic)
354 .await
355 {
356 warn!("Failed to initialize dynamic context directories: {}", err);
357 }
358 let session_tools_config = crate::tools::handlers::SessionToolsConfig {
359 surface: crate::tools::handlers::SessionSurface::AgentRunner,
360 capability_level: crate::config::types::CapabilityLevel::CodeSearch,
361 documentation_mode: session_config.effective().agent.tool_documentation_mode,
362 planning_active: tool_registry.is_planning_active(),
363 request_user_input_enabled: false,
364 model_capabilities: crate::tools::handlers::ToolModelCapabilities::for_model_name(&model.as_str()),
365 deferred_tool_policy,
366 anthropic_native_memory_enabled,
367 tool_profile: session_config.effective().tools.profile,
368 };
369 let available_tools = tool_registry
370 .model_tools(session_tools_config.clone())
371 .await
372 .into_iter()
373 .map(|tool| tool.function_name().to_string())
374 .collect::<Vec<_>>();
375 let mut prompt_context = PromptContext::from_workspace_tools(&workspace, available_tools);
376 prompt_context.set_current_directory(workspace.clone());
377 prompt_context.load_available_skills_async().await;
378 let (system_prompt, system_prompt_report) = helpers::compose_system_prompt_with_appendix(
379 workspace.as_path(),
380 session_config.effective(),
381 &prompt_context,
382 )
383 .await?;
384 let loop_detector = LoopDetector::with_max_repeated_calls(max_repeated_tool_calls);
385 let bootstrap_messages = bootstrap.messages.clone();
386 let mut bootstrap = bootstrap;
387 if bootstrap.metadata.is_none() {
388 bootstrap.metadata = Some(build_thread_archive_metadata(
389 workspace.as_path(),
390 &model.as_str(),
391 &session_config.effective().agent.provider,
392 &session_config.effective().agent.theme,
393 settings
394 .reasoning_effort
395 .unwrap_or(session_config.effective().agent.reasoning_effort)
396 .as_str(),
397 ));
398 }
399 let thread_handle =
400 crate::core::threads::ThreadManager::new().start_thread_with_identifier(session_id.clone(), bootstrap);
401 let max_turns = session_config.effective().automation.full_auto.max_turns.max(1);
402 if session_config.effective().default_primary_agent == "coordinator" {
403 let controller =
404 Box::pin(crate::subagents::SubagentController::new(crate::subagents::SubagentControllerConfig {
405 workspace_root: workspace.clone(),
406 parent_session_id: session_id.clone(),
407 parent_model: model.to_string(),
408 parent_provider: provider_name,
409 parent_reasoning_effort: settings
410 .reasoning_effort
411 .unwrap_or(session_config.effective().agent.reasoning_effort),
412 api_key: api_key.clone(),
413 vt_cfg: session_config.effective().clone(),
414 openai_chatgpt_auth,
415 depth: 0,
416 workspace_gated: session_config
417 .effective()
418 .workspace_lifecycle_hooks
419 .as_ref()
420 .is_some_and(|hooks| !hooks.is_empty()),
421 exec_sessions: tool_registry.exec_session_manager(),
422 pty_manager: tool_registry.pty_manager().clone(),
423 managed_background_runtime: false,
424 }))
425 .await?;
426 tool_registry.set_subagent_controller(Arc::new(controller));
427 }
428
429 Ok(Self {
430 agent_type,
431 client,
432 provider_client,
433 tool_registry,
434 system_prompt,
435 system_prompt_report,
436 session_id,
437 bootstrap_messages,
438 _workspace: workspace,
439 session_config,
440 session_tools_config,
441 model: model.to_string(),
442 _api_key: api_key,
443 reasoning_effort: settings.reasoning_effort,
444 verbosity: settings.verbosity,
445 quiet: false,
446 event_sink: None,
447 thread_handle,
448 max_turns,
449 loop_detector: Mutex::new(loop_detector),
450 steering_receiver: Mutex::new(steering_receiver),
451 tool_definitions_override: RwLock::new(None),
452 tool_arg_transform: None,
453 local_tools_only: false,
454 active_primary_agent: None,
455 })
456 }
457
458 pub fn set_quiet(&mut self, quiet: bool) {
460 self.quiet = quiet;
461 }
462
463 pub fn set_subagent_mode(&self, is_subagent: bool) {
466 self.loop_detector.lock().set_subagent_mode(is_subagent);
467 }
468
469 pub fn set_subagent_controller(&self, controller: Arc<crate::subagents::SubagentController>) {
475 self.tool_registry.set_subagent_controller(controller);
476 }
477
478 pub fn session_messages(&self) -> Vec<crate::llm::provider::Message> {
480 self.thread_handle.messages()
481 }
482
483 pub fn thread_handle(&self) -> ThreadRuntimeHandle {
485 self.thread_handle.clone()
486 }
487
488 pub fn workspace(&self) -> &Path {
490 &self._workspace
491 }
492
493 pub fn enable_planning(&self) {
495 self.tool_registry.enable_planning();
496 }
497
498 pub fn disable_planning(&self) {
500 self.tool_registry.disable_planning();
501 }
502
503 pub fn set_event_handler<F>(&mut self, handler: F)
505 where
506 F: FnMut(&ThreadEvent) + Send + 'static,
507 {
508 self.event_sink = Some(Arc::new(Mutex::new(Box::new(handler))));
509 }
510
511 pub fn clear_event_handler(&mut self) {
513 self.event_sink = None;
514 }
515
516 pub fn set_system_prompt(&mut self, system_prompt: impl Into<String>) {
522 self.system_prompt = system_prompt.into();
523 self.system_prompt_report = crate::prompts::system::SystemPromptReport::measure(
524 &self.system_prompt,
525 self.config().agent.max_system_prompt_tokens,
526 );
527 }
528
529 pub fn tool_registry(&self) -> ToolRegistry {
531 self.tool_registry.clone()
532 }
533
534 pub fn set_tool_definitions_override(&mut self, definitions: Vec<uni_provider::ToolDefinition>) {
536 *self.tool_definitions_override.write() = Some(definitions);
537 }
538
539 pub fn clear_tool_definitions_override(&mut self) {
541 *self.tool_definitions_override.write() = None;
542 }
543
544 pub fn set_tool_arg_transform(&mut self, transform: ToolArgTransform) {
546 self.tool_arg_transform = Some(transform);
547 }
548
549 pub fn restrict_to_local_tools(&mut self) {
551 self.local_tools_only = true;
552 }
553
554 pub fn clear_tool_arg_transform(&mut self) {
556 self.tool_arg_transform = None;
557 }
558
559 pub fn set_active_primary_agent(&mut self, active_primary_agent: ActivePrimaryAgent) {
561 self.tool_registry
562 .set_matrix_coordinator(active_primary_agent.identity.name == "coordinator");
563 self.active_primary_agent = Some(active_primary_agent);
564 }
565
566 pub async fn enable_full_auto(&mut self, allowed_tools: &[String]) {
568 let mut permission_catalog_config = self.session_tools_config.clone();
569 permission_catalog_config.planning_active = allowed_tools
570 .iter()
571 .any(|tool_name| crate::tools::names::canonical_tool_name(tool_name) == tools::CODE_SEARCH);
572 self.tool_registry
573 .enable_full_auto_permission_for_session(allowed_tools, permission_catalog_config)
574 .await;
575 }
576
577 pub async fn review_tool_allowlist(&self, allowed_tools: &[String]) -> Vec<String> {
579 let review_candidates = if allowed_tools.iter().any(|tool| tool.trim() == tools::WILDCARD_ALL) {
580 let mut candidates = self.tool_registry.available_tools().await;
581 if !candidates.iter().any(|tool| tool == tools::CODE_SEARCH) {
582 candidates.push(tools::CODE_SEARCH.to_string());
583 }
584 candidates
585 } else {
586 allowed_tools.to_vec()
587 };
588
589 review_candidates
590 .iter()
591 .filter(|tool_name| {
592 let canonical = crate::tools::names::canonical_tool_name(tool_name);
593
594 !matches!(canonical, tools::REQUEST_USER_INPUT | tools::TASK_TRACKER | tools::START_PLANNING)
595 && !self.tool_registry.is_mutating_tool(tool_name)
596 })
597 .cloned()
598 .collect()
599 }
600
601 fn features(&self) -> FeatureSet {
602 self.session_config.features().clone()
603 }
604}