vtcode 0.99.1

A Rust-based terminal coding agent with modular architecture supporting multiple LLM providers
use anyhow::anyhow;
use serde_json::Value;
use vtcode_core::config::constants::tools;
use vtcode_core::tools::error_messages::agent_execution;
use vtcode_core::tools::registry::{ToolErrorType, ToolExecutionError};
use vtcode_core::tools::tool_intent;

use super::status::ToolExecutionStatus;

pub(super) fn parse_cached_output(cached_output: &str) -> serde_json::Result<Value> {
    serde_json::from_str(cached_output)
}

pub(super) fn is_loop_detection_status(status: &ToolExecutionStatus) -> bool {
    match status {
        ToolExecutionStatus::Success { output, .. } => output
            .get("loop_detected")
            .and_then(|value| value.as_bool())
            .unwrap_or(false),
        ToolExecutionStatus::Failure { error } => error.message.contains("LOOP DETECTION"),
        _ => false,
    }
}

pub(super) fn build_tool_status_message(tool_name: &str, args: &Value) -> String {
    if is_command_tool(tool_name, args) {
        let command = args
            .get("command")
            .and_then(|value| value.as_str())
            .unwrap_or(tool_name);
        format!("Running command: {}", command)
    } else {
        format!("Running tool: {}", tool_name)
    }
}

fn is_command_tool(tool_name: &str, args: &Value) -> bool {
    tool_name == tools::EXECUTE_CODE || tool_intent::is_command_run_tool_call(tool_name, args)
}

pub(crate) fn process_llm_tool_output(output: Value) -> ToolExecutionStatus {
    if let Some(loop_detected) = output.get("loop_detected").and_then(|v| v.as_bool())
        && loop_detected
    {
        let tool_name = output
            .get("tool")
            .and_then(|v| v.as_str())
            .unwrap_or("tool");
        let repeat_count = output
            .get("repeat_count")
            .and_then(|v| v.as_u64())
            .unwrap_or(0);
        let base_error_msg = output
            .get("error")
            .and_then(|e| e.get("message"))
            .and_then(|m| m.as_str())
            .unwrap_or("Tool blocked due to repeated identical invocations");

        let clear_error_msg = agent_execution::loop_detection_block_message(
            tool_name,
            repeat_count,
            Some(base_error_msg),
        );
        return ToolExecutionStatus::Failure {
            error: ToolExecutionError::policy_violation(tool_name.to_string(), clear_error_msg),
        };
    }

    if let Some(error) = ToolExecutionError::from_tool_output(&output) {
        return if matches!(error.error_type, ToolErrorType::Timeout) {
            ToolExecutionStatus::Timeout { error }
        } else {
            ToolExecutionStatus::Failure { error }
        };
    }

    if let Some(error_value) = output.get("error") {
        let error_msg = if let Some(message) = error_value.get("message").and_then(|m| m.as_str()) {
            message.to_string()
        } else if let Some(error_str) = error_value.as_str() {
            error_str.to_string()
        } else {
            "Unknown tool execution error".to_string()
        };
        return ToolExecutionStatus::Failure {
            error: ToolExecutionError::from_anyhow(
                "tool",
                &anyhow!(error_msg),
                0,
                false,
                false,
                Some("unified_runloop"),
            ),
        };
    }

    let exit_code = output
        .get("exit_code")
        .and_then(|value| value.as_i64())
        .unwrap_or(0);
    let command_success = exit_code == 0;
    let stdout = output
        .get("stdout")
        .or_else(|| {
            if is_command_like_output(&output) {
                output.get("output")
            } else {
                None
            }
        })
        .and_then(|value| value.as_str())
        .map(|s| s.trim())
        .filter(|s| !s.is_empty())
        .map(|s| s.to_string());
    let modified_files = output
        .get("modified_files")
        .and_then(|value| value.as_array())
        .map(|files| {
            files
                .iter()
                .filter_map(|entry| entry.as_str().map(|s| s.to_string()))
                .collect::<Vec<String>>()
        })
        .unwrap_or_default();
    ToolExecutionStatus::Success {
        output,
        stdout,
        modified_files,
        command_success,
    }
}

fn is_command_like_output(output: &Value) -> bool {
    output.get("command").is_some()
        || output.get("working_directory").is_some()
        || output.get("session_id").is_some()
        || output.get("process_id").is_some()
        || output.get("spool_path").is_some()
        || output.get("is_exited").is_some()
        || output.get("exit_code").is_some()
        || output
            .get("content_type")
            .and_then(Value::as_str)
            .is_some_and(|value| value == "exec_inspect")
}

#[cfg(test)]
mod tests {
    use serde_json::json;

    use super::*;

    #[test]
    fn loop_detection_status_detects_failure_marker() {
        let status = process_llm_tool_output(json!({
            "error": {
                "message": "Tool blocked after repeated invocations"
            },
            "loop_detected": true,
            "repeat_count": 3,
            "tool": "read_file"
        }));

        assert!(is_loop_detection_status(&status));
    }

    #[test]
    fn malformed_cached_json_is_rejected() {
        assert!(parse_cached_output("{not-valid-json").is_err());
    }

    #[test]
    fn falls_back_to_output_for_command_like_stdout() {
        let status = process_llm_tool_output(json!({
            "command": "ls -la",
            "output": "file-a\nfile-b\n",
            "exit_code": 0,
            "is_exited": true
        }));

        match status {
            ToolExecutionStatus::Success { stdout, .. } => {
                assert_eq!(stdout.as_deref(), Some("file-a\nfile-b"));
            }
            _ => panic!("expected success status"),
        }
    }

    #[test]
    fn falls_back_to_output_for_inspect_payload() {
        let status = process_llm_tool_output(json!({
            "output": "1: src/main.rs",
            "spool_path": ".vtcode/context/tool_outputs/run-1.txt",
            "content_type": "exec_inspect"
        }));

        match status {
            ToolExecutionStatus::Success { stdout, .. } => {
                assert_eq!(stdout.as_deref(), Some("1: src/main.rs"));
            }
            _ => panic!("expected success status"),
        }
    }
}