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