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
348                    .provider_client
349                    .supports_reasoning_effort(&self.model)
350                {
351                    self.reasoning_effort
352                } else {
353                    None
354                },
355            };
356
357            // Use provider-specific client for OpenAI/Anthropic (and generic support for others)
358            // Prepare for provider-specific vs Gemini handling
359            #[allow(unused_assignments)]
360            let mut response_opt: Option<crate::llm::types::LLMResponse> = None;
361            let provider_kind = self
362                .model
363                .parse::<ModelId>()
364                .map(|m| m.provider())
365                .unwrap_or(ModelProvider::Gemini);
366
367            if matches!(
368                provider_kind,
369                ModelProvider::OpenAI | ModelProvider::Anthropic | ModelProvider::DeepSeek
370            ) {
371                let resp = self
372                    .provider_client
373                    .generate(request.clone())
374                    .await
375                    .map_err(|e| {
376                        println!(
377                            "{} {} Failed",
378                            agent_prefix,
379                            style("(ERROR)").red().bold().on_black()
380                        );
381                        anyhow!(
382                            "Agent {} execution failed at turn {}: {}",
383                            self.agent_type,
384                            turn,
385                            e
386                        )
387                    })?;
388
389                // Update progress for successful response
390                println!(
391                    "{} {}",
392                    agent_prefix,
393                    format!(
394                        "{} {} received response, processing...",
395                        self.agent_type,
396                        style("(RECV)").green().bold()
397                    )
398                );
399
400                let mut had_tool_call = false;
401
402                if let Some(tool_calls) = resp.tool_calls.as_ref() {
403                    if !tool_calls.is_empty() {
404                        had_tool_call = true;
405                        for call in tool_calls {
406                            let name = call.function.name.as_str();
407                            println!(
408                                "{} [{}] Calling tool: {}",
409                                style("[TOOL_CALL]").blue().bold(),
410                                self.agent_type,
411                                name
412                            );
413                            let args = call
414                                .parsed_arguments()
415                                .unwrap_or_else(|_| serde_json::json!({}));
416                            match self.execute_tool(name, &args).await {
417                                Ok(result) => {
418                                    println!(
419                                        "{} {}",
420                                        agent_prefix,
421                                        format!(
422                                            "{} {} tool executed successfully",
423                                            style("(OK)").green(),
424                                            name
425                                        )
426                                    );
427                                    let tool_result = serde_json::to_string(&result)?;
428                                    conversation.push(Content {
429                                        role: "user".to_string(),
430                                        parts: vec![Part::Text {
431                                            text: format!("Tool {} result: {}", name, tool_result),
432                                        }],
433                                    });
434                                    executed_commands.push(name.to_string());
435                                    if name == tools::WRITE_FILE {
436                                        if let Some(filepath) =
437                                            args.get("path").and_then(|p| p.as_str())
438                                        {
439                                            modified_files.push(filepath.to_string());
440                                        }
441                                    }
442                                }
443                                Err(e) => {
444                                    println!(
445                                        "{} {}",
446                                        agent_prefix,
447                                        format!(
448                                            "{} {} tool failed: {}",
449                                            style("(ERR)").red(),
450                                            name,
451                                            e
452                                        )
453                                    );
454                                    warnings.push(format!("Tool {} failed: {}", name, e));
455                                    conversation.push(Content {
456                                        role: "user".to_string(),
457                                        parts: vec![Part::Text {
458                                            text: format!("Tool {} failed: {}", name, e),
459                                        }],
460                                    });
461                                }
462                            }
463                        }
464                    }
465                }
466
467                // If no tool calls, treat as regular content
468                let response_text = resp.content.clone().unwrap_or_default();
469                if !had_tool_call {
470                    if !response_text.trim().is_empty() {
471                        Self::print_compact_response(self.agent_type, &response_text);
472                        conversation.push(Content {
473                            role: "model".to_string(),
474                            parts: vec![Part::Text {
475                                text: response_text.clone(),
476                            }],
477                        });
478                    }
479                }
480
481                // Completion detection
482                if !has_completed {
483                    let response_lower = response_text.to_lowercase();
484                    let completion_indicators = [
485                        "task completed",
486                        "task done",
487                        "finished",
488                        "complete",
489                        "summary",
490                        "i have successfully",
491                        "i've completed",
492                        "i have finished",
493                        "task accomplished",
494                        "mission accomplished",
495                        "objective achieved",
496                        "work is done",
497                        "all done",
498                        "completed successfully",
499                        "task execution complete",
500                        "operation finished",
501                    ];
502                    let is_completed = completion_indicators
503                        .iter()
504                        .any(|&indicator| response_lower.contains(indicator));
505                    let has_explicit_completion = response_lower.contains("the task is complete")
506                        || response_lower.contains("task has been completed")
507                        || response_lower.contains("i am done")
508                        || response_lower.contains("that's all")
509                        || response_lower.contains("no more actions needed");
510                    if is_completed || has_explicit_completion {
511                        has_completed = true;
512                        println!(
513                            "{} {}",
514                            agent_prefix,
515                            format!(
516                                "{} {} completed task successfully",
517                                self.agent_type,
518                                style("(SUCCESS)").green().bold()
519                            )
520                        );
521                    }
522                }
523
524                let should_continue = had_tool_call || (!has_completed && turn < 9);
525                if !should_continue {
526                    if has_completed {
527                        println!(
528                            "{} {}",
529                            agent_prefix,
530                            format!(
531                                "{} {} finished - task completed",
532                                self.agent_type,
533                                style("(SUCCESS)").green().bold()
534                            )
535                        );
536                    } else if turn >= 9 {
537                        println!(
538                            "{} {}",
539                            agent_prefix,
540                            format!(
541                                "{} {} finished - maximum turns reached",
542                                self.agent_type,
543                                style("(TIME)").yellow().bold()
544                            )
545                        );
546                    } else {
547                        println!(
548                            "{} {}",
549                            agent_prefix,
550                            format!(
551                                "{} {} finished",
552                                self.agent_type,
553                                style("(FINISH)").blue().bold()
554                            )
555                        );
556                    }
557                    break;
558                }
559
560                // Continue loop for tool results
561                continue;
562            } else {
563                // Gemini path (existing flow)
564                let response = self
565                    .client
566                    .generate(&serde_json::to_string(&request)?)
567                    .await
568                    .map_err(|e| {
569                        println!(
570                            "{} {} Failed",
571                            agent_prefix,
572                            style("(ERROR)").red().bold().on_black()
573                        );
574                        anyhow!(
575                            "Agent {} execution failed at turn {}: {}",
576                            self.agent_type,
577                            turn,
578                            e
579                        )
580                    })?;
581                response_opt = Some(response);
582            }
583
584            // For Gemini path: use original response handling
585            let response = response_opt.expect("response should be set for Gemini path");
586
587            // Update progress for successful response
588            println!(
589                "{} {}",
590                agent_prefix,
591                format!(
592                    "{} {} received response, processing...",
593                    self.agent_type,
594                    style("(RECV)").green().bold()
595                )
596            );
597
598            // Use response content directly
599            if !response.content.is_empty() {
600                // Try to parse the response as JSON to check for tool calls
601                let mut had_tool_call = false;
602
603                // Try to parse as a tool call response
604                if let Ok(tool_call_response) = serde_json::from_str::<Value>(&response.content) {
605                    // Check for standard tool_calls format
606                    if let Some(tool_calls) = tool_call_response
607                        .get("tool_calls")
608                        .and_then(|tc| tc.as_array())
609                    {
610                        had_tool_call = true;
611
612                        // Process each tool call
613                        for tool_call in tool_calls {
614                            if let Some(function) = tool_call.get("function") {
615                                if let (Some(name), Some(arguments)) = (
616                                    function.get("name").and_then(|n| n.as_str()),
617                                    function.get("arguments"),
618                                ) {
619                                    println!(
620                                        "{} [{}] Calling tool: {}",
621                                        style("[TOOL_CALL]").blue().bold(),
622                                        self.agent_type,
623                                        name
624                                    );
625
626                                    // Execute the tool
627                                    match self.execute_tool(name, &arguments.clone()).await {
628                                        Ok(result) => {
629                                            println!(
630                                                "{} {}",
631                                                agent_prefix,
632                                                format!(
633                                                    "{} {} tool executed successfully",
634                                                    style("(OK)").green(),
635                                                    name
636                                                )
637                                            );
638
639                                            // Add tool result to conversation
640                                            let tool_result = serde_json::to_string(&result)?;
641                                            conversation.push(Content {
642                                                role: "user".to_string(), // Gemini API only accepts "user" and "model"
643                                                parts: vec![Part::Text {
644                                                    text: format!(
645                                                        "Tool {} result: {}",
646                                                        name, tool_result
647                                                    ),
648                                                }],
649                                            });
650
651                                            // Track what the agent did
652                                            executed_commands.push(name.to_string());
653
654                                            // Special handling for certain tools
655                                            if name == tools::WRITE_FILE {
656                                                if let Some(filepath) =
657                                                    arguments.get("path").and_then(|p| p.as_str())
658                                                {
659                                                    modified_files.push(filepath.to_string());
660                                                }
661                                            }
662                                        }
663                                        Err(e) => {
664                                            println!(
665                                                "{} {}",
666                                                agent_prefix,
667                                                format!(
668                                                    "{} {} tool failed: {}",
669                                                    style("(ERR)").red(),
670                                                    name,
671                                                    e
672                                                )
673                                            );
674                                            warnings.push(format!("Tool {} failed: {}", name, e));
675                                            conversation.push(Content {
676                                                role: "user".to_string(), // Gemini API only accepts "user" and "model"
677                                                parts: vec![Part::Text {
678                                                    text: format!("Tool {} failed: {}", name, e),
679                                                }],
680                                            });
681                                        }
682                                    }
683                                }
684                            }
685                        }
686                    }
687                    // Check for Gemini functionCall format
688                    else if let Some(function_call) = tool_call_response.get("functionCall") {
689                        had_tool_call = true;
690
691                        if let (Some(name), Some(args)) = (
692                            function_call.get("name").and_then(|n| n.as_str()),
693                            function_call.get("args"),
694                        ) {
695                            println!(
696                                "{} [{}] Calling tool: {}",
697                                style("[TOOL_CALL]").blue().bold(),
698                                self.agent_type,
699                                name
700                            );
701
702                            // Execute the tool
703                            match self.execute_tool(name, args).await {
704                                Ok(result) => {
705                                    println!(
706                                        "{} {}",
707                                        agent_prefix,
708                                        format!(
709                                            "{} {} tool executed successfully",
710                                            style("(OK)").green(),
711                                            name
712                                        )
713                                    );
714
715                                    // Add tool result to conversation
716                                    let tool_result = serde_json::to_string(&result)?;
717                                    conversation.push(Content {
718                                        role: "user".to_string(), // Gemini API only accepts "user" and "model"
719                                        parts: vec![Part::Text {
720                                            text: format!("Tool {} result: {}", name, tool_result),
721                                        }],
722                                    });
723
724                                    // Track what the agent did
725                                    executed_commands.push(name.to_string());
726
727                                    // Special handling for certain tools
728                                    if name == tools::WRITE_FILE {
729                                        if let Some(filepath) =
730                                            args.get("path").and_then(|p| p.as_str())
731                                        {
732                                            modified_files.push(filepath.to_string());
733                                        }
734                                    }
735                                }
736                                Err(e) => {
737                                    println!(
738                                        "{} {}",
739                                        agent_prefix,
740                                        format!(
741                                            "{} {} tool failed: {}",
742                                            style("(ERR)").red().bold(),
743                                            name,
744                                            e
745                                        )
746                                    );
747                                    warnings.push(format!("Tool {} failed: {}", name, e));
748                                    conversation.push(Content {
749                                        role: "user".to_string(), // Gemini API only accepts "user" and "model"
750                                        parts: vec![Part::Text {
751                                            text: format!("Tool {} failed: {}", name, e),
752                                        }],
753                                    });
754                                }
755                            }
756                        }
757                    }
758                    // Check for tool_code format (what agents are actually producing)
759                    else if let Some(tool_code) = tool_call_response
760                        .get("tool_code")
761                        .and_then(|tc| tc.as_str())
762                    {
763                        had_tool_call = true;
764
765                        println!(
766                            "{} [{}] Executing tool code: {}",
767                            style("[TOOL_EXEC]").cyan().bold().on_black(),
768                            self.agent_type,
769                            tool_code
770                        );
771
772                        // Try to parse the tool_code as a function call
773                        // This is a simplified parser for the format: function_name(args)
774                        if let Some((func_name, args_str)) = parse_tool_code(tool_code) {
775                            println!(
776                                "{} [{}] Parsed tool: {} with args: {}",
777                                style("[TOOL_PARSE]").yellow().bold().on_black(),
778                                self.agent_type,
779                                func_name,
780                                args_str
781                            );
782
783                            // Parse arguments as JSON
784                            match serde_json::from_str::<Value>(&args_str) {
785                                Ok(arguments) => {
786                                    // Execute the tool
787                                    match self.execute_tool(&func_name, &arguments).await {
788                                        Ok(result) => {
789                                            println!(
790                                                "{} {}",
791                                                agent_prefix,
792                                                format!(
793                                                    "{} {} tool executed successfully",
794                                                    style("(OK)").green(),
795                                                    func_name
796                                                )
797                                            );
798
799                                            // Add tool result to conversation
800                                            let tool_result = serde_json::to_string(&result)?;
801                                            conversation.push(Content {
802                                                role: "user".to_string(), // Gemini API only accepts "user" and "model"
803                                                parts: vec![Part::Text {
804                                                    text: format!(
805                                                        "Tool {} result: {}",
806                                                        func_name, tool_result
807                                                    ),
808                                                }],
809                                            });
810
811                                            // Track what the agent did
812                                            executed_commands.push(func_name.to_string());
813
814                                            // Special handling for certain tools
815                                            if func_name == tools::WRITE_FILE {
816                                                if let Some(filepath) =
817                                                    arguments.get("path").and_then(|p| p.as_str())
818                                                {
819                                                    modified_files.push(filepath.to_string());
820                                                }
821                                            }
822                                        }
823                                        Err(e) => {
824                                            println!(
825                                                "{} {}",
826                                                agent_prefix,
827                                                format!(
828                                                    "{} {} tool failed: {}",
829                                                    style("(ERROR)").red().bold(),
830                                                    func_name,
831                                                    e
832                                                )
833                                            );
834                                            warnings
835                                                .push(format!("Tool {} failed: {}", func_name, e));
836                                            conversation.push(Content {
837                                                role: "user".to_string(), // Gemini API only accepts "user" and "model"
838                                                parts: vec![Part::Text {
839                                                    text: format!(
840                                                        "Tool {} failed: {}",
841                                                        func_name, e
842                                                    ),
843                                                }],
844                                            });
845                                        }
846                                    }
847                                }
848                                Err(e) => {
849                                    let error_msg = format!(
850                                        "Failed to parse tool arguments '{}': {}",
851                                        args_str, e
852                                    );
853                                    warnings.push(error_msg.clone());
854                                    conversation.push(Content {
855                                        role: "user".to_string(), // Gemini API only accepts "user" and "model"
856                                        parts: vec![Part::Text { text: error_msg }],
857                                    });
858                                }
859                            }
860                        } else {
861                            let error_msg = format!("Failed to parse tool code: {}", tool_code);
862                            warnings.push(error_msg.clone());
863                            conversation.push(Content {
864                                role: "user".to_string(), // Gemini API only accepts "user" and "model"
865                                parts: vec![Part::Text { text: error_msg }],
866                            });
867                        }
868                    }
869                    // Check for tool_name format (alternative format)
870                    else if let Some(tool_name) = tool_call_response
871                        .get("tool_name")
872                        .and_then(|tn| tn.as_str())
873                    {
874                        had_tool_call = true;
875
876                        println!(
877                            "{} [{}] Calling tool: {}",
878                            style("[TOOL_CALL]").blue().bold().on_black(),
879                            self.agent_type,
880                            tool_name
881                        );
882
883                        if let Some(parameters) = tool_call_response.get("parameters") {
884                            // Execute the tool
885                            match self.execute_tool(tool_name, parameters).await {
886                                Ok(result) => {
887                                    println!(
888                                        "{} {}",
889                                        agent_prefix,
890                                        format!(
891                                            "{} {} tool executed successfully",
892                                            style("(SUCCESS)").green().bold(),
893                                            tool_name
894                                        )
895                                    );
896
897                                    // Add tool result to conversation
898                                    let tool_result = serde_json::to_string(&result)?;
899                                    conversation.push(Content {
900                                        role: "user".to_string(), // Gemini API only accepts "user" and "model"
901                                        parts: vec![Part::Text {
902                                            text: format!(
903                                                "Tool {} result: {}",
904                                                tool_name, tool_result
905                                            ),
906                                        }],
907                                    });
908
909                                    // Track what the agent did
910                                    executed_commands.push(tool_name.to_string());
911
912                                    // Special handling for certain tools
913                                    if tool_name == tools::WRITE_FILE {
914                                        if let Some(filepath) =
915                                            parameters.get("path").and_then(|p| p.as_str())
916                                        {
917                                            modified_files.push(filepath.to_string());
918                                        }
919                                    }
920                                }
921                                Err(e) => {
922                                    println!(
923                                        "{} {}",
924                                        agent_prefix,
925                                        format!(
926                                            "{} {} tool failed: {}",
927                                            style("(ERROR)").red().bold(),
928                                            tool_name,
929                                            e
930                                        )
931                                    );
932                                    warnings.push(format!("Tool {} failed: {}", tool_name, e));
933                                    conversation.push(Content {
934                                        role: "user".to_string(), // Gemini API only accepts "user" and "model"
935                                        parts: vec![Part::Text {
936                                            text: format!("Tool {} failed: {}", tool_name, e),
937                                        }],
938                                    });
939                                }
940                            }
941                        }
942                    } else {
943                        // Regular content response
944                        Self::print_compact_response(self.agent_type, response.content.trim());
945                        conversation.push(Content {
946                            role: "model".to_string(),
947                            parts: vec![Part::Text {
948                                text: response.content.clone(),
949                            }],
950                        });
951                    }
952                } else {
953                    // Regular text response
954                    Self::print_compact_response(self.agent_type, response.content.trim());
955                    conversation.push(Content {
956                        role: "model".to_string(),
957                        parts: vec![Part::Text {
958                            text: response.content.clone(),
959                        }],
960                    });
961                }
962
963                // Check for task completion indicators in the response
964                if !has_completed {
965                    let response_lower = response.content.to_lowercase();
966
967                    // More comprehensive completion detection
968                    let completion_indicators = [
969                        "task completed",
970                        "task done",
971                        "finished",
972                        "complete",
973                        "summary",
974                        "i have successfully",
975                        "i've completed",
976                        "i have finished",
977                        "task accomplished",
978                        "mission accomplished",
979                        "objective achieved",
980                        "work is done",
981                        "all done",
982                        "completed successfully",
983                        "task execution complete",
984                        "operation finished",
985                    ];
986
987                    // Check if any completion indicator is present
988                    let is_completed = completion_indicators
989                        .iter()
990                        .any(|&indicator| response_lower.contains(indicator));
991
992                    // Also check for explicit completion statements
993                    let has_explicit_completion = response_lower.contains("the task is complete")
994                        || response_lower.contains("task has been completed")
995                        || response_lower.contains("i am done")
996                        || response_lower.contains("that's all")
997                        || response_lower.contains("no more actions needed");
998
999                    if is_completed || has_explicit_completion {
1000                        has_completed = true;
1001                        println!(
1002                            "{} {}",
1003                            agent_prefix,
1004                            format!(
1005                                "{} {} completed task successfully",
1006                                self.agent_type,
1007                                style("(SUCCESS)").green().bold()
1008                            )
1009                        );
1010                    }
1011                }
1012
1013                // Improved loop termination logic
1014                // Continue if: we had tool calls, task is not completed, and we haven't exceeded max turns
1015                let should_continue = had_tool_call || (!has_completed && turn < 9);
1016
1017                if !should_continue {
1018                    if has_completed {
1019                        println!(
1020                            "{} {}",
1021                            agent_prefix,
1022                            format!(
1023                                "{} {} finished - task completed",
1024                                self.agent_type,
1025                                style("(SUCCESS)").green().bold()
1026                            )
1027                        );
1028                    } else if turn >= 9 {
1029                        println!(
1030                            "{} {}",
1031                            agent_prefix,
1032                            format!(
1033                                "{} {} finished - maximum turns reached",
1034                                self.agent_type,
1035                                style("(TIME)").yellow().bold()
1036                            )
1037                        );
1038                    } else {
1039                        println!(
1040                            "{} {}",
1041                            agent_prefix,
1042                            format!(
1043                                "{} {} finished - no more actions needed",
1044                                self.agent_type,
1045                                style("(FINISH)").blue().bold()
1046                            )
1047                        );
1048                    }
1049                    break;
1050                }
1051            } else {
1052                // Empty response - check if we should continue or if task is actually complete
1053                if has_completed {
1054                    println!(
1055                        "{} {}",
1056                        agent_prefix,
1057                        format!(
1058                            "{} {} finished - task was completed earlier",
1059                            self.agent_type,
1060                            style("(SUCCESS)").green().bold()
1061                        )
1062                    );
1063                    break;
1064                } else if turn >= 9 {
1065                    println!(
1066                        "{} {}",
1067                        agent_prefix,
1068                        format!(
1069                            "{} {} finished - maximum turns reached with empty response",
1070                            self.agent_type,
1071                            style("(TIME)").yellow().bold()
1072                        )
1073                    );
1074                    break;
1075                } else {
1076                    // Empty response but task not complete - this might indicate an issue
1077                    println!(
1078                        "{} {}",
1079                        agent_prefix,
1080                        format!(
1081                            "{} {} received empty response, continuing...",
1082                            self.agent_type,
1083                            style("(EMPTY)").yellow()
1084                        )
1085                    );
1086                    // Don't break here, let the loop continue to give the agent another chance
1087                }
1088            }
1089        }
1090
1091        // Agent execution completed
1092        println!("{} Done", agent_prefix);
1093
1094        // Generate meaningful summary based on agent actions
1095        let summary = self.generate_task_summary(
1096            &modified_files,
1097            &executed_commands,
1098            &warnings,
1099            &conversation,
1100        );
1101
1102        // Return task results
1103        Ok(TaskResults {
1104            created_contexts,
1105            modified_files,
1106            executed_commands,
1107            summary,
1108            warnings,
1109        })
1110    }
1111
1112    /// Build system instruction for agent based on task and contexts
1113    fn build_system_instruction(&self, task: &Task, contexts: &[ContextItem]) -> Result<String> {
1114        let mut instruction = self.system_prompt.clone();
1115
1116        // Add task-specific information
1117        instruction.push_str(&format!("\n\nTask: {}\n{}", task.title, task.description));
1118
1119        // Add context information if any
1120        if !contexts.is_empty() {
1121            instruction.push_str("\n\nRelevant Context:");
1122            for ctx in contexts {
1123                instruction.push_str(&format!("\n[{}] {}", ctx.id, ctx.content));
1124            }
1125        }
1126
1127        Ok(instruction)
1128    }
1129
1130    /// Build available tools for this agent type
1131    fn build_agent_tools(&self) -> Result<Vec<Tool>> {
1132        // Build function declarations based on available tools
1133        let declarations = build_function_declarations();
1134
1135        // Filter tools based on agent type and permissions
1136        let allowed_tools: Vec<Tool> = declarations
1137            .into_iter()
1138            .filter(|decl| self.is_tool_allowed(&decl.name))
1139            .map(|decl| Tool {
1140                function_declarations: vec![decl],
1141            })
1142            .collect();
1143
1144        Ok(allowed_tools)
1145    }
1146
1147    /// Check if a tool is allowed for this agent
1148    fn is_tool_allowed(&self, tool_name: &str) -> bool {
1149        if let Ok(policy_manager) = self.tool_registry.policy_manager() {
1150            match policy_manager.get_policy(tool_name) {
1151                crate::tool_policy::ToolPolicy::Allow | crate::tool_policy::ToolPolicy::Prompt => {
1152                    true
1153                }
1154                crate::tool_policy::ToolPolicy::Deny => false,
1155            }
1156        } else {
1157            true
1158        }
1159    }
1160
1161    /// Execute a tool by name with given arguments
1162    async fn execute_tool(&self, tool_name: &str, args: &Value) -> Result<Value> {
1163        // Enforce per-agent shell policies for RUN_TERMINAL_CMD/BASH
1164        let is_shell = tool_name == tools::RUN_TERMINAL_CMD || tool_name == tools::BASH;
1165        if is_shell {
1166            let cfg = ConfigManager::load()
1167                .or_else(|_| ConfigManager::load_from_workspace("."))
1168                .or_else(|_| ConfigManager::load_from_file("vtcode.toml"))
1169                .map(|cm| cm.config().clone())
1170                .unwrap_or_default();
1171
1172            let cmd_text = if let Some(cmd_val) = args.get("command") {
1173                if cmd_val.is_array() {
1174                    cmd_val
1175                        .as_array()
1176                        .unwrap()
1177                        .iter()
1178                        .filter_map(|v| v.as_str())
1179                        .collect::<Vec<_>>()
1180                        .join(" ")
1181                } else {
1182                    cmd_val.as_str().unwrap_or("").to_string()
1183                }
1184            } else {
1185                String::new()
1186            };
1187
1188            let agent_prefix = format!(
1189                "VTCODE_{}_COMMANDS_",
1190                self.agent_type.to_string().to_uppercase()
1191            );
1192
1193            let mut deny_regex = cfg.commands.deny_regex.clone();
1194            if let Ok(extra) = std::env::var(format!("{}DENY_REGEX", agent_prefix)) {
1195                deny_regex.extend(extra.split(',').map(|s| s.trim().to_string()));
1196            }
1197            for pat in &deny_regex {
1198                if regex::Regex::new(pat)
1199                    .ok()
1200                    .map(|re| re.is_match(&cmd_text))
1201                    .unwrap_or(false)
1202                {
1203                    return Err(anyhow!("Shell command denied by regex: {}", pat));
1204                }
1205            }
1206
1207            let mut deny_glob = cfg.commands.deny_glob.clone();
1208            if let Ok(extra) = std::env::var(format!("{}DENY_GLOB", agent_prefix)) {
1209                deny_glob.extend(extra.split(',').map(|s| s.trim().to_string()));
1210            }
1211            for pat in &deny_glob {
1212                let re = format!("^{}$", regex::escape(pat).replace(r"\*", ".*"));
1213                if regex::Regex::new(&re)
1214                    .ok()
1215                    .map(|re| re.is_match(&cmd_text))
1216                    .unwrap_or(false)
1217                {
1218                    return Err(anyhow!("Shell command denied by glob: {}", pat));
1219                }
1220            }
1221            info!(target = "policy", agent = ?self.agent_type, tool = tool_name, cmd = %cmd_text, "shell_policy_checked");
1222        }
1223        // Clone the tool registry for this execution
1224        let mut registry = self.tool_registry.clone();
1225
1226        // Initialize async components
1227        registry.initialize_async().await?;
1228
1229        // Try with simple adaptive retry (up to 2 retries)
1230        let mut delay = std::time::Duration::from_millis(200);
1231        for attempt in 0..3 {
1232            match registry.execute_tool(tool_name, args.clone()).await {
1233                Ok(result) => return Ok(result),
1234                Err(_e) if attempt < 2 => {
1235                    tokio::time::sleep(delay).await;
1236                    delay = delay.saturating_mul(2);
1237                    continue;
1238                }
1239                Err(e) => {
1240                    return Err(anyhow!(
1241                        "Tool '{}' not found or failed to execute: {}",
1242                        tool_name,
1243                        e
1244                    ));
1245                }
1246            }
1247        }
1248        unreachable!()
1249    }
1250
1251    /// Generate a meaningful summary of the task execution
1252    fn generate_task_summary(
1253        &self,
1254        modified_files: &[String],
1255        executed_commands: &[String],
1256        warnings: &[String],
1257        conversation: &[Content],
1258    ) -> String {
1259        let mut summary = vec![];
1260
1261        // Add task title and agent type
1262        summary.push(format!(
1263            "Task: {}",
1264            conversation
1265                .get(0)
1266                .and_then(|c| c.parts.get(0))
1267                .and_then(|p| p.as_text())
1268                .unwrap_or(&"".to_string())
1269        ));
1270        summary.push(format!("Agent Type: {:?}", self.agent_type));
1271
1272        // Add executed commands
1273        if !executed_commands.is_empty() {
1274            summary.push("Executed Commands:".to_string());
1275            for command in executed_commands {
1276                summary.push(format!(" - {}", command));
1277            }
1278        }
1279
1280        // Add modified files
1281        if !modified_files.is_empty() {
1282            summary.push("Modified Files:".to_string());
1283            for file in modified_files {
1284                summary.push(format!(" - {}", file));
1285            }
1286        }
1287
1288        // Add warnings if any
1289        if !warnings.is_empty() {
1290            summary.push("Warnings:".to_string());
1291            for warning in warnings {
1292                summary.push(format!(" - {}", warning));
1293            }
1294        }
1295
1296        // Add final status
1297        let final_status = if conversation.last().map_or(false, |c| {
1298            c.role == "model"
1299                && c.parts.iter().any(|p| {
1300                    p.as_text().map_or(false, |t| {
1301                        t.contains("completed") || t.contains("done") || t.contains("finished")
1302                    })
1303                })
1304        }) {
1305            "Task completed successfully".to_string()
1306        } else {
1307            "Task did not complete as expected".to_string()
1308        };
1309        summary.push(final_status);
1310
1311        // Join all parts with new lines
1312        summary.join("\n")
1313    }
1314}
1315
1316/// Parse tool code in the format: function_name(arg1=value1, arg2=value2)
1317fn parse_tool_code(tool_code: &str) -> Option<(String, String)> {
1318    // Remove any markdown code blocks
1319    let code = tool_code.trim();
1320    let code = if code.starts_with("```") && code.ends_with("```") {
1321        code.trim_start_matches("```")
1322            .trim_end_matches("```")
1323            .trim()
1324    } else {
1325        code
1326    };
1327
1328    // Try to match function call pattern: name(args)
1329    if let Some(open_paren) = code.find('(') {
1330        if let Some(close_paren) = code.rfind(')') {
1331            let func_name = code[..open_paren].trim().to_string();
1332            let args_str = &code[open_paren + 1..close_paren];
1333
1334            // Convert Python-style arguments to JSON
1335            let json_args = convert_python_args_to_json(args_str)?;
1336            return Some((func_name, json_args));
1337        }
1338    }
1339
1340    None
1341}
1342
1343/// Convert Python-style function arguments to JSON
1344fn convert_python_args_to_json(args_str: &str) -> Option<String> {
1345    if args_str.trim().is_empty() {
1346        return Some("{}".to_string());
1347    }
1348
1349    let mut json_parts = Vec::new();
1350
1351    for arg in args_str.split(',').map(|s| s.trim()) {
1352        if arg.is_empty() {
1353            continue;
1354        }
1355
1356        // Handle key=value format
1357        if let Some(eq_pos) = arg.find('=') {
1358            let key = arg[..eq_pos].trim().trim_matches('"').trim_matches('\'');
1359            let value = arg[eq_pos + 1..].trim();
1360
1361            // Convert value to JSON format
1362            let json_value = if value.starts_with('"') && value.ends_with('"') {
1363                value.to_string()
1364            } else if value.starts_with('\'') && value.ends_with('\'') {
1365                format!("\"{}\"", value.trim_matches('\''))
1366            } else if value == "True" || value == "true" {
1367                "true".to_string()
1368            } else if value == "False" || value == "false" {
1369                "false".to_string()
1370            } else if value == "None" || value == "null" {
1371                "null".to_string()
1372            } else if let Ok(num) = value.parse::<f64>() {
1373                num.to_string()
1374            } else {
1375                // Assume it's a string that needs quotes
1376                format!("\"{}\"", value)
1377            };
1378
1379            json_parts.push(format!("\"{}\": {}", key, json_value));
1380        } else {
1381            // Handle positional arguments (not supported well, but try)
1382            return None;
1383        }
1384    }
1385
1386    Some(format!("{{{}}}", json_parts.join(", ")))
1387}
1388
1389/// Task specification consumed by the benchmark/autonomous runner.
1390#[derive(Debug, Clone, Serialize, Deserialize)]
1391pub struct Task {
1392    /// Stable identifier for reporting.
1393    pub id: String,
1394    /// Human-readable task title displayed in progress messages.
1395    pub title: String,
1396    /// High-level description of the task objective.
1397    pub description: String,
1398    /// Optional explicit instructions appended to the conversation.
1399    #[serde(default, skip_serializing_if = "Option::is_none")]
1400    pub instructions: Option<String>,
1401}
1402
1403impl Task {
1404    /// Construct a task with the provided metadata.
1405    pub fn new(id: String, title: String, description: String) -> Self {
1406        Self {
1407            id,
1408            title,
1409            description,
1410            instructions: None,
1411        }
1412    }
1413}
1414
1415/// Context entry supplied alongside the benchmark task.
1416#[derive(Debug, Clone, Serialize, Deserialize)]
1417pub struct ContextItem {
1418    /// Identifier used when referencing the context in prompts.
1419    pub id: String,
1420    /// Raw textual content exposed to the agent.
1421    pub content: String,
1422}
1423
1424/// Aggregated results returned by the autonomous agent runner.
1425#[derive(Debug, Clone, Serialize, Deserialize)]
1426pub struct TaskResults {
1427    /// Identifiers of any contexts created during execution.
1428    #[serde(default)]
1429    pub created_contexts: Vec<String>,
1430    /// File paths modified during the task.
1431    #[serde(default)]
1432    pub modified_files: Vec<String>,
1433    /// Terminal commands executed while solving the task.
1434    #[serde(default)]
1435    pub executed_commands: Vec<String>,
1436    /// Natural-language summary of the run assembled by the agent.
1437    pub summary: String,
1438    /// Collected warnings emitted while processing the task.
1439    #[serde(default)]
1440    pub warnings: Vec<String>,
1441}