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};
12
13#[derive(Clone, Default)]
15pub struct RunnerSettings {
16 pub reasoning_effort: Option<ReasoningEffortLevel>,
18 pub verbosity: Option<VerbosityLevel>,
20}
21
22use crate::core::agent::types::AgentType;
23use crate::core::loop_detector::LoopDetector;
24use crate::exec::events::ThreadEvent;
25use crate::llm::AnyClient;
26use crate::llm::client::ProviderClientAdapter;
27use crate::llm::factory::{ProviderConfig, create_provider_with_config, infer_provider_from_model};
28use crate::llm::provider as uni_provider;
29use crate::primary_agent::ActivePrimaryAgent;
30use crate::prompts::PromptContext;
31use crate::tools::ToolRegistry;
32
33use anyhow::{Context, Result, anyhow};
34use parking_lot::{Mutex, RwLock};
35use std::path::PathBuf;
36use std::str::FromStr;
37use std::sync::Arc;
38use tracing::{info, warn};
39use vtcode_config::auth::OpenAIChatGptAuthHandle;
40
41mod config_helpers;
42mod constants;
43mod continuation;
44mod execute;
45mod execute_checks;
46mod helpers;
47mod orchestration;
48mod output;
49pub mod prompt_alignment;
50mod retry;
51mod summarize;
52mod summary;
53mod telemetry;
54mod tool_access;
55mod tool_args;
56mod tool_exec;
57mod tool_execution_guard;
58mod types;
59mod validation;
60mod workspace_detection;
61
62#[cfg(test)]
63mod tests;
64
65type ToolArgTransform = Arc<dyn Fn(&str, serde_json::Value) -> serde_json::Value + Send + Sync>;
66
67pub struct AgentRunner {
69 agent_type: AgentType,
71 client: AnyClient,
73 provider_client: Box<dyn uni_provider::LLMProvider>,
75 tool_registry: ToolRegistry,
77 system_prompt: String,
79 session_id: String,
81 bootstrap_messages: Vec<crate::llm::provider::Message>,
83 _workspace: PathBuf,
85 session_config: Arc<ResolvedSessionConfig>,
87 model: String,
89 _api_key: String,
91 reasoning_effort: Option<ReasoningEffortLevel>,
93 verbosity: Option<VerbosityLevel>,
95 quiet: bool,
97 event_sink: Option<EventSink>,
99 thread_handle: ThreadRuntimeHandle,
101 max_turns: usize,
103 loop_detector: Mutex<LoopDetector>,
105 steering_receiver: Mutex<Option<tokio::sync::mpsc::UnboundedReceiver<SteeringMessage>>>,
109 tool_definitions_override: RwLock<Option<Vec<uni_provider::ToolDefinition>>>,
111 tool_arg_transform: Option<ToolArgTransform>,
113 active_primary_agent: Option<ActivePrimaryAgent>,
115}
116
117impl AgentRunner {
118 fn get_selected_model(&self) -> String {
120 self.model.clone()
121 }
122
123 fn runner_println(&self, args: std::fmt::Arguments) {
124 if !self.quiet {
125 println!("{args}");
126 }
127 }
128
129 pub async fn new(
131 agent_type: AgentType,
132 model: ModelId,
133 api_key: String,
134 workspace: PathBuf,
135 session_id: String,
136 settings: RunnerSettings,
137 steering_receiver: Option<tokio::sync::mpsc::UnboundedReceiver<SteeringMessage>>,
138 ) -> Result<Self> {
139 Self::new_internal(
140 agent_type,
141 model,
142 api_key,
143 workspace,
144 session_id,
145 settings,
146 steering_receiver,
147 ThreadBootstrap::new(None),
148 None,
149 None,
150 )
151 .await
152 }
153
154 #[allow(clippy::too_many_arguments)]
156 pub async fn new_with_bootstrap(
157 agent_type: AgentType,
158 model: ModelId,
159 api_key: String,
160 workspace: PathBuf,
161 session_id: String,
162 settings: RunnerSettings,
163 steering_receiver: Option<tokio::sync::mpsc::UnboundedReceiver<SteeringMessage>>,
164 bootstrap: ThreadBootstrap,
165 vt_cfg: Option<VTCodeConfig>,
166 openai_chatgpt_auth: Option<OpenAIChatGptAuthHandle>,
167 ) -> Result<Self> {
168 Self::new_internal(
169 agent_type,
170 model,
171 api_key,
172 workspace,
173 session_id,
174 settings,
175 steering_receiver,
176 bootstrap,
177 vt_cfg,
178 openai_chatgpt_auth,
179 )
180 .await
181 }
182
183 #[expect(clippy::too_many_arguments)]
184 async fn new_internal(
185 agent_type: AgentType,
186 model: ModelId,
187 api_key: String,
188 workspace: PathBuf,
189 session_id: String,
190 settings: RunnerSettings,
191 steering_receiver: Option<tokio::sync::mpsc::UnboundedReceiver<SteeringMessage>>,
192 bootstrap: ThreadBootstrap,
193 vt_cfg: Option<VTCodeConfig>,
194 openai_chatgpt_auth: Option<OpenAIChatGptAuthHandle>,
195 ) -> Result<Self> {
196 let session_config = if let Some(vt_cfg) = vt_cfg {
198 ResolvedSessionConfig::from_config(vt_cfg)
199 } else {
200 match ResolvedSessionConfig::load_from_workspace(&workspace) {
201 Ok(session_config) => session_config,
202 Err(err) => {
203 warn!(
204 "Failed to load vtcode configuration for system prompt composition: {err:#}"
205 );
206 ResolvedSessionConfig::from_config(VTCodeConfig::default())
207 }
208 }
209 };
210 let session_config = Arc::new(session_config);
211 let provider_name = {
212 let configured = session_config.effective().agent.provider.trim();
213 if configured.is_empty() {
214 infer_provider_from_model(model.as_str())
215 .map(|provider| provider.to_string())
216 .ok_or_else(|| anyhow!("Failed to determine provider for model {}", model))?
217 } else {
218 configured.to_lowercase()
219 }
220 };
221 let provider_config = ProviderConfig {
222 api_key: Some(api_key.clone()),
223 openai_chatgpt_auth: openai_chatgpt_auth.clone(),
224 copilot_auth: Some(session_config.effective().auth.copilot.clone()),
225 base_url: None,
226 model: Some(model.to_string()),
227 prompt_cache: Some(session_config.effective().prompt_cache.clone()),
228 timeouts: None,
229 openai: Some(session_config.effective().provider.openai.clone()),
230 anthropic: Some(session_config.effective().provider.anthropic.clone()),
231 model_behavior: Some(session_config.effective().model.clone()),
232 workspace_root: Some(workspace.clone()),
233 };
234
235 let client: AnyClient = Box::new(ProviderClientAdapter::new(
236 create_provider_with_config(&provider_name, provider_config.clone())
237 .with_context(|| "Failed to create client provider")?,
238 model.to_string(),
239 ));
240 let provider_client = create_provider_with_config(&provider_name, provider_config)
241 .with_context(|| "Failed to create provider client")?;
242 if std::env::var_os("VTCODE_DEBUG_PROVIDER").is_some() {
243 eprintln!(
244 "vtcode-debug: runner provider={} client_provider={} model={}",
245 provider_name,
246 provider_client.name(),
247 model
248 );
249 }
250 let max_repeated_tool_calls = session_config
251 .effective()
252 .tools
253 .max_repeated_tool_calls
254 .max(1);
255 let deferred_tool_policy = crate::tools::handlers::deferred_tool_policy_for_runtime(
256 crate::llm::factory::infer_provider(
257 Some(&session_config.effective().agent.provider),
258 model.as_str(),
259 ),
260 provider_client.supports_responses_compaction(model.as_str()),
261 Some(session_config.effective()),
262 );
263 let anthropic_native_memory_enabled =
264 crate::tools::handlers::anthropic_native_memory_enabled_for_runtime(
265 Provider::from_str(provider_client.name()).ok(),
266 model.as_str(),
267 Some(session_config.effective()),
268 );
269 let tool_registry = ToolRegistry::new(workspace.clone()).await;
270 tool_registry.set_harness_session(session_id.clone());
271 tool_registry.set_agent_type(agent_type.to_string());
272 tool_registry.initialize_async().await?;
273 if let Err(err) = tool_registry
274 .apply_session_runtime_config(
275 &session_config.effective().commands,
276 &session_config.effective().permissions,
277 &session_config.effective().sandbox,
278 &session_config.effective().timeouts,
279 &session_config.effective().tools,
280 )
281 .await
282 {
283 warn!("Failed to apply tool policies from config: {}", err);
284 }
285 if session_config.effective().mcp.enabled {
286 if let Err(err) = crate::mcp::validate_mcp_config(&session_config.effective().mcp) {
287 warn!("MCP configuration validation error: {err}");
288 }
289 info!("Deferring MCP client initialization to on-demand activation");
290 }
291 if session_config.effective().context.dynamic.enabled
292 && let Err(err) = crate::context::initialize_dynamic_context(
293 &workspace,
294 &session_config.effective().context.dynamic,
295 )
296 .await
297 {
298 warn!("Failed to initialize dynamic context directories: {}", err);
299 }
300 let available_tools = tool_registry
301 .model_tools(crate::tools::handlers::SessionToolsConfig {
302 surface: crate::tools::handlers::SessionSurface::AgentRunner,
303 capability_level: crate::config::types::CapabilityLevel::CodeSearch,
304 documentation_mode: session_config.effective().agent.tool_documentation_mode,
305 planning_active: tool_registry.is_planning_active(),
306 request_user_input_enabled: false,
307 model_capabilities: crate::tools::handlers::ToolModelCapabilities::for_model_name(
308 model.as_str(),
309 ),
310 deferred_tool_policy,
311 anthropic_native_memory_enabled,
312 })
313 .await
314 .into_iter()
315 .map(|tool| tool.function_name().to_string())
316 .collect::<Vec<_>>();
317 let mut prompt_context = PromptContext::from_workspace_tools(&workspace, available_tools);
318 prompt_context.load_available_skills();
319 let system_prompt = helpers::compose_system_prompt_with_appendix(
320 workspace.as_path(),
321 session_config.effective(),
322 &prompt_context,
323 )
324 .await?;
325 let loop_detector = LoopDetector::with_max_repeated_calls(max_repeated_tool_calls);
326 let bootstrap_messages = bootstrap.messages.clone();
327 let mut bootstrap = bootstrap;
328 if bootstrap.metadata.is_none() {
329 bootstrap.metadata = Some(build_thread_archive_metadata(
330 workspace.as_path(),
331 model.as_str(),
332 &session_config.effective().agent.provider,
333 &session_config.effective().agent.theme,
334 settings
335 .reasoning_effort
336 .unwrap_or(session_config.effective().agent.reasoning_effort)
337 .as_str(),
338 ));
339 }
340 let thread_handle = crate::core::threads::ThreadManager::new()
341 .start_thread_with_identifier(session_id.clone(), bootstrap);
342 let max_turns = session_config
343 .effective()
344 .automation
345 .full_auto
346 .max_turns
347 .max(1);
348
349 Ok(Self {
350 agent_type,
351 client,
352 provider_client,
353 tool_registry,
354 system_prompt,
355 session_id,
356 bootstrap_messages,
357 _workspace: workspace,
358 session_config,
359 model: model.to_string(),
360 _api_key: api_key,
361 reasoning_effort: settings.reasoning_effort,
362 verbosity: settings.verbosity,
363 quiet: false,
364 event_sink: None,
365 thread_handle,
366 max_turns,
367 loop_detector: Mutex::new(loop_detector),
368 steering_receiver: Mutex::new(steering_receiver),
369 tool_definitions_override: RwLock::new(None),
370 tool_arg_transform: None,
371 active_primary_agent: None,
372 })
373 }
374
375 pub fn set_quiet(&mut self, quiet: bool) {
377 self.quiet = quiet;
378 }
379
380 pub fn session_messages(&self) -> Vec<crate::llm::provider::Message> {
382 self.thread_handle.messages()
383 }
384
385 pub fn thread_handle(&self) -> ThreadRuntimeHandle {
387 self.thread_handle.clone()
388 }
389
390 pub fn enable_planning(&self) {
392 self.tool_registry.enable_planning();
393 }
394
395 pub fn disable_planning(&self) {
396 self.tool_registry.disable_planning();
397 }
398
399 pub fn set_event_handler<F>(&mut self, handler: F)
401 where
402 F: FnMut(&ThreadEvent) + Send + 'static,
403 {
404 self.event_sink = Some(Arc::new(Mutex::new(Box::new(handler))));
405 }
406
407 pub fn clear_event_handler(&mut self) {
409 self.event_sink = None;
410 }
411
412 pub fn set_system_prompt(&mut self, system_prompt: impl Into<String>) {
414 self.system_prompt = system_prompt.into();
415 }
416
417 pub fn tool_registry(&self) -> ToolRegistry {
419 self.tool_registry.clone()
420 }
421
422 pub fn set_tool_definitions_override(
423 &mut self,
424 definitions: Vec<uni_provider::ToolDefinition>,
425 ) {
426 *self.tool_definitions_override.write() = Some(definitions);
427 }
428
429 pub fn clear_tool_definitions_override(&mut self) {
430 *self.tool_definitions_override.write() = None;
431 }
432
433 pub fn set_tool_arg_transform(&mut self, transform: ToolArgTransform) {
434 self.tool_arg_transform = Some(transform);
435 }
436
437 pub fn clear_tool_arg_transform(&mut self) {
438 self.tool_arg_transform = None;
439 }
440
441 pub fn set_active_primary_agent(&mut self, active_primary_agent: ActivePrimaryAgent) {
443 self.active_primary_agent = Some(active_primary_agent);
444 }
445
446 pub async fn enable_full_auto(&mut self, allowed_tools: &[String]) {
448 self.tool_registry
449 .enable_full_auto_permission(allowed_tools)
450 .await;
451 }
452
453 pub async fn review_tool_allowlist(&self, allowed_tools: &[String]) -> Vec<String> {
455 let review_candidates = if allowed_tools
456 .iter()
457 .any(|tool| tool.trim() == tools::WILDCARD_ALL)
458 {
459 self.tool_registry.available_tools().await
460 } else {
461 allowed_tools.to_vec()
462 };
463
464 review_candidates
465 .iter()
466 .filter(|tool_name| {
467 let canonical = crate::tools::names::canonical_tool_name(tool_name);
468
469 !matches!(
470 canonical,
471 tools::REQUEST_USER_INPUT
472 | tools::TASK_TRACKER
473 | tools::START_PLANNING
474 | tools::FINISH_PLANNING
475 ) && (canonical == tools::UNIFIED_FILE
476 || !self.tool_registry.is_mutating_tool(tool_name))
477 })
478 .cloned()
479 .collect()
480 }
481
482 pub(crate) fn features(&self) -> FeatureSet {
483 self.session_config.features().clone()
484 }
485}