vtcode_core/core/agent/
runner.rs

1//! Agent runner for executing individual agent instances
2
3use crate::config::VTCodeConfig;
4use crate::config::constants::tools;
5use crate::config::loader::ConfigManager;
6use crate::config::models::{ModelId, Provider as ModelProvider};
7use crate::config::types::ReasoningEffortLevel;
8use crate::core::agent::types::AgentType;
9use crate::gemini::{Content, Part, Tool};
10use crate::llm::factory::create_provider_for_model;
11use crate::llm::provider as uni_provider;
12use crate::llm::provider::{FunctionDefinition, LLMRequest, Message, MessageRole, ToolDefinition};
13use crate::llm::{AnyClient, make_client};
14use crate::mcp_client::McpClient;
15use crate::tools::{ToolRegistry, build_function_declarations};
16use anyhow::{Result, anyhow};
17use console::style;
18use serde::{Deserialize, Serialize};
19use serde_json::Value;
20use std::path::PathBuf;
21use std::sync::Arc;
22use tokio::time::{Duration, timeout};
23use tracing::{info, warn};
24
25/// Individual agent runner for executing specialized agent tasks
26pub struct AgentRunner {
27    /// Agent type and configuration
28    agent_type: AgentType,
29    /// LLM client for this agent
30    client: AnyClient,
31    /// Unified provider client (OpenAI/Anthropic/Gemini) for tool-calling
32    provider_client: Box<dyn uni_provider::LLMProvider>,
33    /// Tool registry with restricted access
34    tool_registry: ToolRegistry,
35    /// System prompt content
36    system_prompt: String,
37    /// Session information
38    _session_id: String,
39    /// Workspace path
40    _workspace: PathBuf,
41    /// Model identifier
42    model: String,
43    /// API key (for provider client construction in future flows)
44    _api_key: String,
45    /// Reasoning effort level for models that support it
46    reasoning_effort: Option<ReasoningEffortLevel>,
47}
48
49impl AgentRunner {
50    fn print_compact_response(agent: AgentType, text: &str) {
51        use console::style;
52        const MAX_CHARS: usize = 1200;
53        const HEAD_CHARS: usize = 800;
54        const TAIL_CHARS: usize = 200;
55        let clean = text.trim();
56        if clean.chars().count() <= MAX_CHARS {
57            println!(
58                "{} [{}]: {}",
59                style("[RESPONSE]").cyan().bold(),
60                agent,
61                clean
62            );
63            return;
64        }
65        let mut out = String::new();
66        let mut count = 0;
67        for ch in clean.chars() {
68            if count >= HEAD_CHARS {
69                break;
70            }
71            out.push(ch);
72            count += 1;
73        }
74        out.push_str("\n…\n");
75        // tail
76        let total = clean.chars().count();
77        let start_tail = total.saturating_sub(TAIL_CHARS);
78        let tail: String = clean.chars().skip(start_tail).collect();
79        out.push_str(&tail);
80        println!("{} [{}]: {}", style("[RESPONSE]").cyan().bold(), agent, out);
81        println!(
82            "{} truncated long response ({} chars).",
83            style("[NOTE]").dim(),
84            total
85        );
86    }
87    /// Create informative progress message based on operation type
88    fn create_progress_message(&self, operation: &str, details: Option<&str>) -> String {
89        match operation {
90            "thinking" => "Analyzing request and planning approach...".to_string(),
91            "processing" => format!("Processing turn with {} model", self.client.model_id()),
92            "tool_call" => {
93                if let Some(tool) = details {
94                    format!("Executing {} tool for task completion", tool)
95                } else {
96                    "Executing tool to gather information".to_string()
97                }
98            }
99            "file_read" => {
100                if let Some(file) = details {
101                    format!("Reading {} to understand structure", file)
102                } else {
103                    "Reading file to analyze content".to_string()
104                }
105            }
106            "file_write" => {
107                if let Some(file) = details {
108                    format!("Writing changes to {}", file)
109                } else {
110                    "Writing file with requested changes".to_string()
111                }
112            }
113            "search" => {
114                if let Some(pattern) = details {
115                    format!("Searching codebase for '{}'", pattern)
116                } else {
117                    "Searching codebase for relevant information".to_string()
118                }
119            }
120            "terminal" => {
121                if let Some(cmd) = details {
122                    format!(
123                        "Running terminal command: {}",
124                        cmd.split(' ').next().unwrap_or(cmd)
125                    )
126                } else {
127                    "Executing terminal command".to_string()
128                }
129            }
130            "completed" => "Task completed successfully!".to_string(),
131            "error" => {
132                if let Some(err) = details {
133                    format!("Error encountered: {}", err)
134                } else {
135                    "An error occurred during execution".to_string()
136                }
137            }
138            _ => format!("{}...", operation),
139        }
140    }
141
142    /// Create a new agent runner
143    pub fn new(
144        agent_type: AgentType,
145        model: ModelId,
146        api_key: String,
147        workspace: PathBuf,
148        session_id: String,
149        reasoning_effort: Option<ReasoningEffortLevel>,
150    ) -> Result<Self> {
151        // Create client based on model
152        let client: AnyClient = make_client(api_key.clone(), model.clone());
153
154        // Create unified provider client for tool calling
155        let provider_client = create_provider_for_model(model.as_str(), api_key.clone(), None)
156            .map_err(|e| anyhow!("Failed to create provider client: {}", e))?;
157
158        // Create system prompt for single agent
159        let system_prompt = crate::prompts::read_system_prompt_from_md()
160            .unwrap_or_else(|_| crate::prompts::system::default_system_prompt().to_string());
161
162        Ok(Self {
163            agent_type,
164            client,
165            provider_client,
166            tool_registry: ToolRegistry::new(workspace.clone()),
167            system_prompt,
168            _session_id: session_id,
169            _workspace: workspace,
170            model: model.as_str().to_string(),
171            _api_key: api_key,
172            reasoning_effort,
173        })
174    }
175
176    /// Enable full-auto execution with the provided allow-list.
177    pub fn enable_full_auto(&mut self, allowed_tools: &[String]) {
178        self.tool_registry.enable_full_auto_mode(allowed_tools);
179    }
180
181    /// Apply workspace configuration to the tool registry, including tool policies and MCP setup.
182    pub async fn apply_workspace_configuration(&mut self, vt_cfg: &VTCodeConfig) -> Result<()> {
183        self.tool_registry.initialize_async().await?;
184
185        if let Err(err) = self.tool_registry.apply_config_policies(&vt_cfg.tools) {
186            eprintln!(
187                "Warning: Failed to apply tool policies from config: {}",
188                err
189            );
190        }
191
192        if vt_cfg.mcp.enabled {
193            let mut mcp_client = McpClient::new(vt_cfg.mcp.clone());
194            match timeout(Duration::from_secs(30), mcp_client.initialize()).await {
195                Ok(Ok(())) => {
196                    let mcp_client = Arc::new(mcp_client);
197                    self.tool_registry.set_mcp_client(Arc::clone(&mcp_client));
198                    if let Err(err) = self.tool_registry.refresh_mcp_tools().await {
199                        warn!("Failed to refresh MCP tools: {}", err);
200                    }
201                }
202                Ok(Err(err)) => {
203                    warn!("MCP client initialization failed: {}", err);
204                }
205                Err(_) => {
206                    warn!("MCP client initialization timed out after 30 seconds");
207                }
208            }
209        }
210
211        Ok(())
212    }
213
214    /// Execute a task with this agent
215    pub async fn execute_task(
216        &mut self,
217        task: &Task,
218        contexts: &[ContextItem],
219    ) -> Result<TaskResults> {
220        // Agent execution status
221        let agent_prefix = format!("[{}]", self.agent_type);
222        println!(
223            "{} {}",
224            agent_prefix,
225            self.create_progress_message("thinking", None)
226        );
227
228        println!(
229            "{} Executing {} task: {}",
230            style("[AGENT]").blue().bold().on_black(),
231            self.agent_type,
232            task.title
233        );
234
235        // Prepare conversation with task context
236        let mut conversation = Vec::new();
237
238        // Add system instruction as the first message
239        let system_content = self.build_system_instruction(task, contexts)?;
240        conversation.push(Content::user_text(system_content));
241
242        // Add task description
243        conversation.push(Content::user_text(format!(
244            "Task: {}\nDescription: {}",
245            task.title, task.description
246        )));
247
248        if let Some(instructions) = task.instructions.as_ref() {
249            conversation.push(Content::user_text(instructions.clone()));
250        }
251
252        // Add context items if any
253        if !contexts.is_empty() {
254            let context_content: Vec<String> = contexts
255                .iter()
256                .map(|ctx| format!("Context [{}]: {}", ctx.id, ctx.content))
257                .collect();
258            conversation.push(Content::user_text(format!(
259                "Relevant Context:\n{}",
260                context_content.join("\n")
261            )));
262        }
263
264        // Build available tools for this agent
265        let gemini_tools = self.build_agent_tools()?;
266
267        // Convert Gemini tools to universal ToolDefinition format
268        let tools: Vec<ToolDefinition> = gemini_tools
269            .into_iter()
270            .flat_map(|tool| tool.function_declarations)
271            .map(|decl| ToolDefinition {
272                tool_type: "function".to_string(),
273                function: FunctionDefinition {
274                    name: decl.name,
275                    description: decl.description,
276                    parameters: decl.parameters,
277                },
278            })
279            .collect();
280
281        // Track execution results
282        let created_contexts = Vec::new();
283        let mut modified_files = Vec::new();
284        let mut executed_commands = Vec::new();
285        let mut warnings = Vec::new();
286        let mut has_completed = false;
287
288        // Determine max loops via configuration
289        let cfg = ConfigManager::load()
290            .or_else(|_| ConfigManager::load_from_workspace("."))
291            .or_else(|_| ConfigManager::load_from_file("vtcode.toml"))
292            .map(|cm| cm.config().clone())
293            .unwrap_or_default();
294        let max_tool_loops = cfg.tools.max_tool_loops.max(1);
295
296        // Agent execution loop uses global tool loop guard
297        for turn in 0..max_tool_loops {
298            if has_completed {
299                break;
300            }
301
302            println!(
303                "{} {} is processing turn {}...",
304                agent_prefix,
305                style("(PROC)").yellow().bold(),
306                turn + 1
307            );
308
309            let request = LLMRequest {
310                messages: conversation
311                    .iter()
312                    .map(|content| {
313                        // Convert Gemini Content to LLM Message
314                        let role = match content.role.as_str() {
315                            "user" => MessageRole::User,
316                            "model" => MessageRole::Assistant,
317                            _ => MessageRole::User,
318                        };
319                        let content_text = content
320                            .parts
321                            .iter()
322                            .filter_map(|part| match part {
323                                crate::gemini::Part::Text { text } => Some(text.clone()),
324                                _ => None,
325                            })
326                            .collect::<Vec<_>>()
327                            .join("\n");
328                        Message {
329                            role,
330                            content: content_text,
331                            tool_calls: None,
332                            tool_call_id: None,
333                        }
334                    })
335                    .collect(),
336                system_prompt: None,
337                tools: Some(tools.clone()),
338                model: self.model.clone(),
339                max_tokens: Some(2000),
340                temperature: Some(0.7),
341                stream: false,
342                tool_choice: None,
343                parallel_tool_calls: None,
344                parallel_tool_config: Some(
345                    crate::llm::provider::ParallelToolConfig::anthropic_optimized(),
346                ),
347                reasoning_effort: if self.provider_client.supports_reasoning_effort(&self.model) {
348                    self.reasoning_effort
349                } else {
350                    None
351                },
352            };
353
354            // Use provider-specific client for OpenAI/Anthropic (and generic support for others)
355            // Prepare for provider-specific vs Gemini handling
356            #[allow(unused_assignments)]
357            let mut response_opt: Option<crate::llm::types::LLMResponse> = None;
358            let provider_kind = self
359                .model
360                .parse::<ModelId>()
361                .map(|m| m.provider())
362                .unwrap_or(ModelProvider::Gemini);
363
364            if matches!(
365                provider_kind,
366                ModelProvider::OpenAI | ModelProvider::Anthropic | ModelProvider::DeepSeek
367            ) {
368                let resp = self
369                    .provider_client
370                    .generate(request.clone())
371                    .await
372                    .map_err(|e| {
373                        println!(
374                            "{} {} Failed",
375                            agent_prefix,
376                            style("(ERROR)").red().bold().on_black()
377                        );
378                        anyhow!(
379                            "Agent {} execution failed at turn {}: {}",
380                            self.agent_type,
381                            turn,
382                            e
383                        )
384                    })?;
385
386                // Update progress for successful response
387                println!(
388                    "{} {}",
389                    agent_prefix,
390                    format!(
391                        "{} {} received response, processing...",
392                        self.agent_type,
393                        style("(RECV)").green().bold()
394                    )
395                );
396
397                let mut had_tool_call = false;
398
399                if let Some(tool_calls) = resp.tool_calls.as_ref() {
400                    if !tool_calls.is_empty() {
401                        had_tool_call = true;
402                        for call in tool_calls {
403                            let name = call.function.name.as_str();
404                            println!(
405                                "{} [{}] Calling tool: {}",
406                                style("[TOOL_CALL]").blue().bold(),
407                                self.agent_type,
408                                name
409                            );
410                            let args = call
411                                .parsed_arguments()
412                                .unwrap_or_else(|_| serde_json::json!({}));
413                            match self.execute_tool(name, &args).await {
414                                Ok(result) => {
415                                    println!(
416                                        "{} {}",
417                                        agent_prefix,
418                                        format!(
419                                            "{} {} tool executed successfully",
420                                            style("(OK)").green(),
421                                            name
422                                        )
423                                    );
424                                    let tool_result = serde_json::to_string(&result)?;
425                                    conversation.push(Content {
426                                        role: "user".to_string(),
427                                        parts: vec![Part::Text {
428                                            text: format!("Tool {} result: {}", name, tool_result),
429                                        }],
430                                    });
431                                    executed_commands.push(name.to_string());
432                                    if name == tools::WRITE_FILE {
433                                        if let Some(filepath) =
434                                            args.get("path").and_then(|p| p.as_str())
435                                        {
436                                            modified_files.push(filepath.to_string());
437                                        }
438                                    }
439                                }
440                                Err(e) => {
441                                    println!(
442                                        "{} {}",
443                                        agent_prefix,
444                                        format!(
445                                            "{} {} tool failed: {}",
446                                            style("(ERR)").red(),
447                                            name,
448                                            e
449                                        )
450                                    );
451                                    warnings.push(format!("Tool {} failed: {}", name, e));
452                                    conversation.push(Content {
453                                        role: "user".to_string(),
454                                        parts: vec![Part::Text {
455                                            text: format!("Tool {} failed: {}", name, e),
456                                        }],
457                                    });
458                                }
459                            }
460                        }
461                    }
462                }
463
464                // If no tool calls, treat as regular content
465                let response_text = resp.content.clone().unwrap_or_default();
466                if !had_tool_call {
467                    if !response_text.trim().is_empty() {
468                        Self::print_compact_response(self.agent_type, &response_text);
469                        conversation.push(Content {
470                            role: "model".to_string(),
471                            parts: vec![Part::Text {
472                                text: response_text.clone(),
473                            }],
474                        });
475                    }
476                }
477
478                // Completion detection
479                if !has_completed {
480                    let response_lower = response_text.to_lowercase();
481                    let completion_indicators = [
482                        "task completed",
483                        "task done",
484                        "finished",
485                        "complete",
486                        "summary",
487                        "i have successfully",
488                        "i've completed",
489                        "i have finished",
490                        "task accomplished",
491                        "mission accomplished",
492                        "objective achieved",
493                        "work is done",
494                        "all done",
495                        "completed successfully",
496                        "task execution complete",
497                        "operation finished",
498                    ];
499                    let is_completed = completion_indicators
500                        .iter()
501                        .any(|&indicator| response_lower.contains(indicator));
502                    let has_explicit_completion = response_lower.contains("the task is complete")
503                        || response_lower.contains("task has been completed")
504                        || response_lower.contains("i am done")
505                        || response_lower.contains("that's all")
506                        || response_lower.contains("no more actions needed");
507                    if is_completed || has_explicit_completion {
508                        has_completed = true;
509                        println!(
510                            "{} {}",
511                            agent_prefix,
512                            format!(
513                                "{} {} completed task successfully",
514                                self.agent_type,
515                                style("(SUCCESS)").green().bold()
516                            )
517                        );
518                    }
519                }
520
521                let should_continue = had_tool_call || (!has_completed && turn < 9);
522                if !should_continue {
523                    if has_completed {
524                        println!(
525                            "{} {}",
526                            agent_prefix,
527                            format!(
528                                "{} {} finished - task completed",
529                                self.agent_type,
530                                style("(SUCCESS)").green().bold()
531                            )
532                        );
533                    } else if turn >= 9 {
534                        println!(
535                            "{} {}",
536                            agent_prefix,
537                            format!(
538                                "{} {} finished - maximum turns reached",
539                                self.agent_type,
540                                style("(TIME)").yellow().bold()
541                            )
542                        );
543                    } else {
544                        println!(
545                            "{} {}",
546                            agent_prefix,
547                            format!(
548                                "{} {} finished",
549                                self.agent_type,
550                                style("(FINISH)").blue().bold()
551                            )
552                        );
553                    }
554                    break;
555                }
556
557                // Continue loop for tool results
558                continue;
559            } else {
560                // Gemini path (existing flow)
561                let response = self
562                    .client
563                    .generate(&serde_json::to_string(&request)?)
564                    .await
565                    .map_err(|e| {
566                        println!(
567                            "{} {} Failed",
568                            agent_prefix,
569                            style("(ERROR)").red().bold().on_black()
570                        );
571                        anyhow!(
572                            "Agent {} execution failed at turn {}: {}",
573                            self.agent_type,
574                            turn,
575                            e
576                        )
577                    })?;
578                response_opt = Some(response);
579            }
580
581            // For Gemini path: use original response handling
582            let response = response_opt.expect("response should be set for Gemini path");
583
584            // Update progress for successful response
585            println!(
586                "{} {}",
587                agent_prefix,
588                format!(
589                    "{} {} received response, processing...",
590                    self.agent_type,
591                    style("(RECV)").green().bold()
592                )
593            );
594
595            // Use response content directly
596            if !response.content.is_empty() {
597                // Try to parse the response as JSON to check for tool calls
598                let mut had_tool_call = false;
599
600                // Try to parse as a tool call response
601                if let Ok(tool_call_response) = serde_json::from_str::<Value>(&response.content) {
602                    // Check for standard tool_calls format
603                    if let Some(tool_calls) = tool_call_response
604                        .get("tool_calls")
605                        .and_then(|tc| tc.as_array())
606                    {
607                        had_tool_call = true;
608
609                        // Process each tool call
610                        for tool_call in tool_calls {
611                            if let Some(function) = tool_call.get("function") {
612                                if let (Some(name), Some(arguments)) = (
613                                    function.get("name").and_then(|n| n.as_str()),
614                                    function.get("arguments"),
615                                ) {
616                                    println!(
617                                        "{} [{}] Calling tool: {}",
618                                        style("[TOOL_CALL]").blue().bold(),
619                                        self.agent_type,
620                                        name
621                                    );
622
623                                    // Execute the tool
624                                    match self.execute_tool(name, &arguments.clone()).await {
625                                        Ok(result) => {
626                                            println!(
627                                                "{} {}",
628                                                agent_prefix,
629                                                format!(
630                                                    "{} {} tool executed successfully",
631                                                    style("(OK)").green(),
632                                                    name
633                                                )
634                                            );
635
636                                            // Add tool result to conversation
637                                            let tool_result = serde_json::to_string(&result)?;
638                                            conversation.push(Content {
639                                                role: "user".to_string(), // Gemini API only accepts "user" and "model"
640                                                parts: vec![Part::Text {
641                                                    text: format!(
642                                                        "Tool {} result: {}",
643                                                        name, tool_result
644                                                    ),
645                                                }],
646                                            });
647
648                                            // Track what the agent did
649                                            executed_commands.push(name.to_string());
650
651                                            // Special handling for certain tools
652                                            if name == tools::WRITE_FILE {
653                                                if let Some(filepath) =
654                                                    arguments.get("path").and_then(|p| p.as_str())
655                                                {
656                                                    modified_files.push(filepath.to_string());
657                                                }
658                                            }
659                                        }
660                                        Err(e) => {
661                                            println!(
662                                                "{} {}",
663                                                agent_prefix,
664                                                format!(
665                                                    "{} {} tool failed: {}",
666                                                    style("(ERR)").red(),
667                                                    name,
668                                                    e
669                                                )
670                                            );
671                                            warnings.push(format!("Tool {} failed: {}", name, e));
672                                            conversation.push(Content {
673                                                role: "user".to_string(), // Gemini API only accepts "user" and "model"
674                                                parts: vec![Part::Text {
675                                                    text: format!("Tool {} failed: {}", name, e),
676                                                }],
677                                            });
678                                        }
679                                    }
680                                }
681                            }
682                        }
683                    }
684                    // Check for Gemini functionCall format
685                    else if let Some(function_call) = tool_call_response.get("functionCall") {
686                        had_tool_call = true;
687
688                        if let (Some(name), Some(args)) = (
689                            function_call.get("name").and_then(|n| n.as_str()),
690                            function_call.get("args"),
691                        ) {
692                            println!(
693                                "{} [{}] Calling tool: {}",
694                                style("[TOOL_CALL]").blue().bold(),
695                                self.agent_type,
696                                name
697                            );
698
699                            // Execute the tool
700                            match self.execute_tool(name, args).await {
701                                Ok(result) => {
702                                    println!(
703                                        "{} {}",
704                                        agent_prefix,
705                                        format!(
706                                            "{} {} tool executed successfully",
707                                            style("(OK)").green(),
708                                            name
709                                        )
710                                    );
711
712                                    // Add tool result to conversation
713                                    let tool_result = serde_json::to_string(&result)?;
714                                    conversation.push(Content {
715                                        role: "user".to_string(), // Gemini API only accepts "user" and "model"
716                                        parts: vec![Part::Text {
717                                            text: format!("Tool {} result: {}", name, tool_result),
718                                        }],
719                                    });
720
721                                    // Track what the agent did
722                                    executed_commands.push(name.to_string());
723
724                                    // Special handling for certain tools
725                                    if name == tools::WRITE_FILE {
726                                        if let Some(filepath) =
727                                            args.get("path").and_then(|p| p.as_str())
728                                        {
729                                            modified_files.push(filepath.to_string());
730                                        }
731                                    }
732                                }
733                                Err(e) => {
734                                    println!(
735                                        "{} {}",
736                                        agent_prefix,
737                                        format!(
738                                            "{} {} tool failed: {}",
739                                            style("(ERR)").red().bold(),
740                                            name,
741                                            e
742                                        )
743                                    );
744                                    warnings.push(format!("Tool {} failed: {}", name, e));
745                                    conversation.push(Content {
746                                        role: "user".to_string(), // Gemini API only accepts "user" and "model"
747                                        parts: vec![Part::Text {
748                                            text: format!("Tool {} failed: {}", name, e),
749                                        }],
750                                    });
751                                }
752                            }
753                        }
754                    }
755                    // Check for tool_code format (what agents are actually producing)
756                    else if let Some(tool_code) = tool_call_response
757                        .get("tool_code")
758                        .and_then(|tc| tc.as_str())
759                    {
760                        had_tool_call = true;
761
762                        println!(
763                            "{} [{}] Executing tool code: {}",
764                            style("[TOOL_EXEC]").cyan().bold().on_black(),
765                            self.agent_type,
766                            tool_code
767                        );
768
769                        // Try to parse the tool_code as a function call
770                        // This is a simplified parser for the format: function_name(args)
771                        if let Some((func_name, args_str)) = parse_tool_code(tool_code) {
772                            println!(
773                                "{} [{}] Parsed tool: {} with args: {}",
774                                style("[TOOL_PARSE]").yellow().bold().on_black(),
775                                self.agent_type,
776                                func_name,
777                                args_str
778                            );
779
780                            // Parse arguments as JSON
781                            match serde_json::from_str::<Value>(&args_str) {
782                                Ok(arguments) => {
783                                    // Execute the tool
784                                    match self.execute_tool(&func_name, &arguments).await {
785                                        Ok(result) => {
786                                            println!(
787                                                "{} {}",
788                                                agent_prefix,
789                                                format!(
790                                                    "{} {} tool executed successfully",
791                                                    style("(OK)").green(),
792                                                    func_name
793                                                )
794                                            );
795
796                                            // Add tool result to conversation
797                                            let tool_result = serde_json::to_string(&result)?;
798                                            conversation.push(Content {
799                                                role: "user".to_string(), // Gemini API only accepts "user" and "model"
800                                                parts: vec![Part::Text {
801                                                    text: format!(
802                                                        "Tool {} result: {}",
803                                                        func_name, tool_result
804                                                    ),
805                                                }],
806                                            });
807
808                                            // Track what the agent did
809                                            executed_commands.push(func_name.to_string());
810
811                                            // Special handling for certain tools
812                                            if func_name == tools::WRITE_FILE {
813                                                if let Some(filepath) =
814                                                    arguments.get("path").and_then(|p| p.as_str())
815                                                {
816                                                    modified_files.push(filepath.to_string());
817                                                }
818                                            }
819                                        }
820                                        Err(e) => {
821                                            println!(
822                                                "{} {}",
823                                                agent_prefix,
824                                                format!(
825                                                    "{} {} tool failed: {}",
826                                                    style("(ERROR)").red().bold(),
827                                                    func_name,
828                                                    e
829                                                )
830                                            );
831                                            warnings
832                                                .push(format!("Tool {} failed: {}", func_name, e));
833                                            conversation.push(Content {
834                                                role: "user".to_string(), // Gemini API only accepts "user" and "model"
835                                                parts: vec![Part::Text {
836                                                    text: format!(
837                                                        "Tool {} failed: {}",
838                                                        func_name, e
839                                                    ),
840                                                }],
841                                            });
842                                        }
843                                    }
844                                }
845                                Err(e) => {
846                                    let error_msg = format!(
847                                        "Failed to parse tool arguments '{}': {}",
848                                        args_str, e
849                                    );
850                                    warnings.push(error_msg.clone());
851                                    conversation.push(Content {
852                                        role: "user".to_string(), // Gemini API only accepts "user" and "model"
853                                        parts: vec![Part::Text { text: error_msg }],
854                                    });
855                                }
856                            }
857                        } else {
858                            let error_msg = format!("Failed to parse tool code: {}", tool_code);
859                            warnings.push(error_msg.clone());
860                            conversation.push(Content {
861                                role: "user".to_string(), // Gemini API only accepts "user" and "model"
862                                parts: vec![Part::Text { text: error_msg }],
863                            });
864                        }
865                    }
866                    // Check for tool_name format (alternative format)
867                    else if let Some(tool_name) = tool_call_response
868                        .get("tool_name")
869                        .and_then(|tn| tn.as_str())
870                    {
871                        had_tool_call = true;
872
873                        println!(
874                            "{} [{}] Calling tool: {}",
875                            style("[TOOL_CALL]").blue().bold().on_black(),
876                            self.agent_type,
877                            tool_name
878                        );
879
880                        if let Some(parameters) = tool_call_response.get("parameters") {
881                            // Execute the tool
882                            match self.execute_tool(tool_name, parameters).await {
883                                Ok(result) => {
884                                    println!(
885                                        "{} {}",
886                                        agent_prefix,
887                                        format!(
888                                            "{} {} tool executed successfully",
889                                            style("(SUCCESS)").green().bold(),
890                                            tool_name
891                                        )
892                                    );
893
894                                    // Add tool result to conversation
895                                    let tool_result = serde_json::to_string(&result)?;
896                                    conversation.push(Content {
897                                        role: "user".to_string(), // Gemini API only accepts "user" and "model"
898                                        parts: vec![Part::Text {
899                                            text: format!(
900                                                "Tool {} result: {}",
901                                                tool_name, tool_result
902                                            ),
903                                        }],
904                                    });
905
906                                    // Track what the agent did
907                                    executed_commands.push(tool_name.to_string());
908
909                                    // Special handling for certain tools
910                                    if tool_name == tools::WRITE_FILE {
911                                        if let Some(filepath) =
912                                            parameters.get("path").and_then(|p| p.as_str())
913                                        {
914                                            modified_files.push(filepath.to_string());
915                                        }
916                                    }
917                                }
918                                Err(e) => {
919                                    println!(
920                                        "{} {}",
921                                        agent_prefix,
922                                        format!(
923                                            "{} {} tool failed: {}",
924                                            style("(ERROR)").red().bold(),
925                                            tool_name,
926                                            e
927                                        )
928                                    );
929                                    warnings.push(format!("Tool {} failed: {}", tool_name, e));
930                                    conversation.push(Content {
931                                        role: "user".to_string(), // Gemini API only accepts "user" and "model"
932                                        parts: vec![Part::Text {
933                                            text: format!("Tool {} failed: {}", tool_name, e),
934                                        }],
935                                    });
936                                }
937                            }
938                        }
939                    } else {
940                        // Regular content response
941                        Self::print_compact_response(self.agent_type, response.content.trim());
942                        conversation.push(Content {
943                            role: "model".to_string(),
944                            parts: vec![Part::Text {
945                                text: response.content.clone(),
946                            }],
947                        });
948                    }
949                } else {
950                    // Regular text response
951                    Self::print_compact_response(self.agent_type, response.content.trim());
952                    conversation.push(Content {
953                        role: "model".to_string(),
954                        parts: vec![Part::Text {
955                            text: response.content.clone(),
956                        }],
957                    });
958                }
959
960                // Check for task completion indicators in the response
961                if !has_completed {
962                    let response_lower = response.content.to_lowercase();
963
964                    // More comprehensive completion detection
965                    let completion_indicators = [
966                        "task completed",
967                        "task done",
968                        "finished",
969                        "complete",
970                        "summary",
971                        "i have successfully",
972                        "i've completed",
973                        "i have finished",
974                        "task accomplished",
975                        "mission accomplished",
976                        "objective achieved",
977                        "work is done",
978                        "all done",
979                        "completed successfully",
980                        "task execution complete",
981                        "operation finished",
982                    ];
983
984                    // Check if any completion indicator is present
985                    let is_completed = completion_indicators
986                        .iter()
987                        .any(|&indicator| response_lower.contains(indicator));
988
989                    // Also check for explicit completion statements
990                    let has_explicit_completion = response_lower.contains("the task is complete")
991                        || response_lower.contains("task has been completed")
992                        || response_lower.contains("i am done")
993                        || response_lower.contains("that's all")
994                        || response_lower.contains("no more actions needed");
995
996                    if is_completed || has_explicit_completion {
997                        has_completed = true;
998                        println!(
999                            "{} {}",
1000                            agent_prefix,
1001                            format!(
1002                                "{} {} completed task successfully",
1003                                self.agent_type,
1004                                style("(SUCCESS)").green().bold()
1005                            )
1006                        );
1007                    }
1008                }
1009
1010                // Improved loop termination logic
1011                // Continue if: we had tool calls, task is not completed, and we haven't exceeded max turns
1012                let should_continue = had_tool_call || (!has_completed && turn < 9);
1013
1014                if !should_continue {
1015                    if has_completed {
1016                        println!(
1017                            "{} {}",
1018                            agent_prefix,
1019                            format!(
1020                                "{} {} finished - task completed",
1021                                self.agent_type,
1022                                style("(SUCCESS)").green().bold()
1023                            )
1024                        );
1025                    } else if turn >= 9 {
1026                        println!(
1027                            "{} {}",
1028                            agent_prefix,
1029                            format!(
1030                                "{} {} finished - maximum turns reached",
1031                                self.agent_type,
1032                                style("(TIME)").yellow().bold()
1033                            )
1034                        );
1035                    } else {
1036                        println!(
1037                            "{} {}",
1038                            agent_prefix,
1039                            format!(
1040                                "{} {} finished - no more actions needed",
1041                                self.agent_type,
1042                                style("(FINISH)").blue().bold()
1043                            )
1044                        );
1045                    }
1046                    break;
1047                }
1048            } else {
1049                // Empty response - check if we should continue or if task is actually complete
1050                if has_completed {
1051                    println!(
1052                        "{} {}",
1053                        agent_prefix,
1054                        format!(
1055                            "{} {} finished - task was completed earlier",
1056                            self.agent_type,
1057                            style("(SUCCESS)").green().bold()
1058                        )
1059                    );
1060                    break;
1061                } else if turn >= 9 {
1062                    println!(
1063                        "{} {}",
1064                        agent_prefix,
1065                        format!(
1066                            "{} {} finished - maximum turns reached with empty response",
1067                            self.agent_type,
1068                            style("(TIME)").yellow().bold()
1069                        )
1070                    );
1071                    break;
1072                } else {
1073                    // Empty response but task not complete - this might indicate an issue
1074                    println!(
1075                        "{} {}",
1076                        agent_prefix,
1077                        format!(
1078                            "{} {} received empty response, continuing...",
1079                            self.agent_type,
1080                            style("(EMPTY)").yellow()
1081                        )
1082                    );
1083                    // Don't break here, let the loop continue to give the agent another chance
1084                }
1085            }
1086        }
1087
1088        // Agent execution completed
1089        println!("{} Done", agent_prefix);
1090
1091        // Generate meaningful summary based on agent actions
1092        let summary = self.generate_task_summary(
1093            &modified_files,
1094            &executed_commands,
1095            &warnings,
1096            &conversation,
1097        );
1098
1099        // Return task results
1100        Ok(TaskResults {
1101            created_contexts,
1102            modified_files,
1103            executed_commands,
1104            summary,
1105            warnings,
1106        })
1107    }
1108
1109    /// Build system instruction for agent based on task and contexts
1110    fn build_system_instruction(&self, task: &Task, contexts: &[ContextItem]) -> Result<String> {
1111        let mut instruction = self.system_prompt.clone();
1112
1113        // Add task-specific information
1114        instruction.push_str(&format!("\n\nTask: {}\n{}", task.title, task.description));
1115
1116        // Add context information if any
1117        if !contexts.is_empty() {
1118            instruction.push_str("\n\nRelevant Context:");
1119            for ctx in contexts {
1120                instruction.push_str(&format!("\n[{}] {}", ctx.id, ctx.content));
1121            }
1122        }
1123
1124        Ok(instruction)
1125    }
1126
1127    /// Build available tools for this agent type
1128    fn build_agent_tools(&self) -> Result<Vec<Tool>> {
1129        // Build function declarations based on available tools
1130        let declarations = build_function_declarations();
1131
1132        // Filter tools based on agent type and permissions
1133        let allowed_tools: Vec<Tool> = declarations
1134            .into_iter()
1135            .filter(|decl| self.is_tool_allowed(&decl.name))
1136            .map(|decl| Tool {
1137                function_declarations: vec![decl],
1138            })
1139            .collect();
1140
1141        Ok(allowed_tools)
1142    }
1143
1144    /// Check if a tool is allowed for this agent
1145    fn is_tool_allowed(&self, tool_name: &str) -> bool {
1146        if let Ok(policy_manager) = self.tool_registry.policy_manager() {
1147            match policy_manager.get_policy(tool_name) {
1148                crate::tool_policy::ToolPolicy::Allow | crate::tool_policy::ToolPolicy::Prompt => {
1149                    true
1150                }
1151                crate::tool_policy::ToolPolicy::Deny => false,
1152            }
1153        } else {
1154            true
1155        }
1156    }
1157
1158    /// Execute a tool by name with given arguments
1159    async fn execute_tool(&self, tool_name: &str, args: &Value) -> Result<Value> {
1160        // Enforce per-agent shell policies for RUN_TERMINAL_CMD/BASH
1161        let is_shell = tool_name == tools::RUN_TERMINAL_CMD || tool_name == tools::BASH;
1162        if is_shell {
1163            let cfg = ConfigManager::load()
1164                .or_else(|_| ConfigManager::load_from_workspace("."))
1165                .or_else(|_| ConfigManager::load_from_file("vtcode.toml"))
1166                .map(|cm| cm.config().clone())
1167                .unwrap_or_default();
1168
1169            let cmd_text = if let Some(cmd_val) = args.get("command") {
1170                if cmd_val.is_array() {
1171                    cmd_val
1172                        .as_array()
1173                        .unwrap()
1174                        .iter()
1175                        .filter_map(|v| v.as_str())
1176                        .collect::<Vec<_>>()
1177                        .join(" ")
1178                } else {
1179                    cmd_val.as_str().unwrap_or("").to_string()
1180                }
1181            } else {
1182                String::new()
1183            };
1184
1185            let agent_prefix = format!(
1186                "VTCODE_{}_COMMANDS_",
1187                self.agent_type.to_string().to_uppercase()
1188            );
1189
1190            let mut deny_regex = cfg.commands.deny_regex.clone();
1191            if let Ok(extra) = std::env::var(format!("{}DENY_REGEX", agent_prefix)) {
1192                deny_regex.extend(extra.split(',').map(|s| s.trim().to_string()));
1193            }
1194            for pat in &deny_regex {
1195                if regex::Regex::new(pat)
1196                    .ok()
1197                    .map(|re| re.is_match(&cmd_text))
1198                    .unwrap_or(false)
1199                {
1200                    return Err(anyhow!("Shell command denied by regex: {}", pat));
1201                }
1202            }
1203
1204            let mut deny_glob = cfg.commands.deny_glob.clone();
1205            if let Ok(extra) = std::env::var(format!("{}DENY_GLOB", agent_prefix)) {
1206                deny_glob.extend(extra.split(',').map(|s| s.trim().to_string()));
1207            }
1208            for pat in &deny_glob {
1209                let re = format!("^{}$", regex::escape(pat).replace(r"\*", ".*"));
1210                if regex::Regex::new(&re)
1211                    .ok()
1212                    .map(|re| re.is_match(&cmd_text))
1213                    .unwrap_or(false)
1214                {
1215                    return Err(anyhow!("Shell command denied by glob: {}", pat));
1216                }
1217            }
1218            info!(target = "policy", agent = ?self.agent_type, tool = tool_name, cmd = %cmd_text, "shell_policy_checked");
1219        }
1220        // Clone the tool registry for this execution
1221        let mut registry = self.tool_registry.clone();
1222
1223        // Initialize async components
1224        registry.initialize_async().await?;
1225
1226        // Try with simple adaptive retry (up to 2 retries)
1227        let mut delay = std::time::Duration::from_millis(200);
1228        for attempt in 0..3 {
1229            match registry.execute_tool(tool_name, args.clone()).await {
1230                Ok(result) => return Ok(result),
1231                Err(_e) if attempt < 2 => {
1232                    tokio::time::sleep(delay).await;
1233                    delay = delay.saturating_mul(2);
1234                    continue;
1235                }
1236                Err(e) => {
1237                    return Err(anyhow!(
1238                        "Tool '{}' not found or failed to execute: {}",
1239                        tool_name,
1240                        e
1241                    ));
1242                }
1243            }
1244        }
1245        unreachable!()
1246    }
1247
1248    /// Generate a meaningful summary of the task execution
1249    fn generate_task_summary(
1250        &self,
1251        modified_files: &[String],
1252        executed_commands: &[String],
1253        warnings: &[String],
1254        conversation: &[Content],
1255    ) -> String {
1256        let mut summary = vec![];
1257
1258        // Add task title and agent type
1259        summary.push(format!(
1260            "Task: {}",
1261            conversation
1262                .get(0)
1263                .and_then(|c| c.parts.get(0))
1264                .and_then(|p| p.as_text())
1265                .unwrap_or(&"".to_string())
1266        ));
1267        summary.push(format!("Agent Type: {:?}", self.agent_type));
1268
1269        // Add executed commands
1270        if !executed_commands.is_empty() {
1271            summary.push("Executed Commands:".to_string());
1272            for command in executed_commands {
1273                summary.push(format!(" - {}", command));
1274            }
1275        }
1276
1277        // Add modified files
1278        if !modified_files.is_empty() {
1279            summary.push("Modified Files:".to_string());
1280            for file in modified_files {
1281                summary.push(format!(" - {}", file));
1282            }
1283        }
1284
1285        // Add warnings if any
1286        if !warnings.is_empty() {
1287            summary.push("Warnings:".to_string());
1288            for warning in warnings {
1289                summary.push(format!(" - {}", warning));
1290            }
1291        }
1292
1293        // Add final status
1294        let final_status = if conversation.last().map_or(false, |c| {
1295            c.role == "model"
1296                && c.parts.iter().any(|p| {
1297                    p.as_text().map_or(false, |t| {
1298                        t.contains("completed") || t.contains("done") || t.contains("finished")
1299                    })
1300                })
1301        }) {
1302            "Task completed successfully".to_string()
1303        } else {
1304            "Task did not complete as expected".to_string()
1305        };
1306        summary.push(final_status);
1307
1308        // Join all parts with new lines
1309        summary.join("\n")
1310    }
1311}
1312
1313/// Parse tool code in the format: function_name(arg1=value1, arg2=value2)
1314fn parse_tool_code(tool_code: &str) -> Option<(String, String)> {
1315    // Remove any markdown code blocks
1316    let code = tool_code.trim();
1317    let code = if code.starts_with("```") && code.ends_with("```") {
1318        code.trim_start_matches("```")
1319            .trim_end_matches("```")
1320            .trim()
1321    } else {
1322        code
1323    };
1324
1325    // Try to match function call pattern: name(args)
1326    if let Some(open_paren) = code.find('(') {
1327        if let Some(close_paren) = code.rfind(')') {
1328            let func_name = code[..open_paren].trim().to_string();
1329            let args_str = &code[open_paren + 1..close_paren];
1330
1331            // Convert Python-style arguments to JSON
1332            let json_args = convert_python_args_to_json(args_str)?;
1333            return Some((func_name, json_args));
1334        }
1335    }
1336
1337    None
1338}
1339
1340/// Convert Python-style function arguments to JSON
1341fn convert_python_args_to_json(args_str: &str) -> Option<String> {
1342    if args_str.trim().is_empty() {
1343        return Some("{}".to_string());
1344    }
1345
1346    let mut json_parts = Vec::new();
1347
1348    for arg in args_str.split(',').map(|s| s.trim()) {
1349        if arg.is_empty() {
1350            continue;
1351        }
1352
1353        // Handle key=value format
1354        if let Some(eq_pos) = arg.find('=') {
1355            let key = arg[..eq_pos].trim().trim_matches('"').trim_matches('\'');
1356            let value = arg[eq_pos + 1..].trim();
1357
1358            // Convert value to JSON format
1359            let json_value = if value.starts_with('"') && value.ends_with('"') {
1360                value.to_string()
1361            } else if value.starts_with('\'') && value.ends_with('\'') {
1362                format!("\"{}\"", value.trim_matches('\''))
1363            } else if value == "True" || value == "true" {
1364                "true".to_string()
1365            } else if value == "False" || value == "false" {
1366                "false".to_string()
1367            } else if value == "None" || value == "null" {
1368                "null".to_string()
1369            } else if let Ok(num) = value.parse::<f64>() {
1370                num.to_string()
1371            } else {
1372                // Assume it's a string that needs quotes
1373                format!("\"{}\"", value)
1374            };
1375
1376            json_parts.push(format!("\"{}\": {}", key, json_value));
1377        } else {
1378            // Handle positional arguments (not supported well, but try)
1379            return None;
1380        }
1381    }
1382
1383    Some(format!("{{{}}}", json_parts.join(", ")))
1384}
1385
1386/// Task specification consumed by the benchmark/autonomous runner.
1387#[derive(Debug, Clone, Serialize, Deserialize)]
1388pub struct Task {
1389    /// Stable identifier for reporting.
1390    pub id: String,
1391    /// Human-readable task title displayed in progress messages.
1392    pub title: String,
1393    /// High-level description of the task objective.
1394    pub description: String,
1395    /// Optional explicit instructions appended to the conversation.
1396    #[serde(default, skip_serializing_if = "Option::is_none")]
1397    pub instructions: Option<String>,
1398}
1399
1400impl Task {
1401    /// Construct a task with the provided metadata.
1402    pub fn new(id: String, title: String, description: String) -> Self {
1403        Self {
1404            id,
1405            title,
1406            description,
1407            instructions: None,
1408        }
1409    }
1410}
1411
1412/// Context entry supplied alongside the benchmark task.
1413#[derive(Debug, Clone, Serialize, Deserialize)]
1414pub struct ContextItem {
1415    /// Identifier used when referencing the context in prompts.
1416    pub id: String,
1417    /// Raw textual content exposed to the agent.
1418    pub content: String,
1419}
1420
1421/// Aggregated results returned by the autonomous agent runner.
1422#[derive(Debug, Clone, Serialize, Deserialize)]
1423pub struct TaskResults {
1424    /// Identifiers of any contexts created during execution.
1425    #[serde(default)]
1426    pub created_contexts: Vec<String>,
1427    /// File paths modified during the task.
1428    #[serde(default)]
1429    pub modified_files: Vec<String>,
1430    /// Terminal commands executed while solving the task.
1431    #[serde(default)]
1432    pub executed_commands: Vec<String>,
1433    /// Natural-language summary of the run assembled by the agent.
1434    pub summary: String,
1435    /// Collected warnings emitted while processing the task.
1436    #[serde(default)]
1437    pub warnings: Vec<String>,
1438}