vtcode-core 0.136.4

Core library for VT Code - a Rust-based terminal coding agent
//! Tool result metadata and quality scoring
//!
//! Provides metadata about tool result quality, confidence, and usefulness.
//! This allows the agent to make informed decisions about result reliability
//! and prioritize high-quality results in context windows.
//!
//! Core types (`ResultCompleteness`, `ResultMetadata`, `EnhancedToolResult`, `ResultScorer`)
//! are defined in `vtcode-commons::tool_types` and re-exported here for backward compatibility.
//! This module retains the scorer implementations that depend on tool_intent.

// Re-export core types from vtcode-commons::tool_types for backward compatibility
pub use vtcode_commons::tool_types::{
    CompactStr, EnhancedToolResult, ResultCompleteness, ResultMetadata, ResultScorer,
};

use crate::config::constants::tools;
use crate::tools::tool_intent;
use hashbrown::HashMap;
use serde_json::Value;

/// Scorer for bounded code search results.
pub struct CodeSearchScorer;

impl ResultScorer for CodeSearchScorer {
    fn score(&self, result: &Value) -> ResultMetadata {
        let mut metadata = ResultMetadata::default();
        metadata.content_types.push("code".to_string());

        match result {
            Value::Object(map) => {
                if let Some(results) = map.get("results")
                    && let Some(entries) = results.as_array()
                {
                    metadata.result_count = entries.len();

                    metadata.confidence = if entries.len() > 5 {
                        0.85
                    } else if !entries.is_empty() {
                        0.80
                    } else {
                        1.0
                    };

                    metadata.relevance = 0.75;
                    metadata.completeness = if map.get("truncated").and_then(Value::as_bool).unwrap_or(false) {
                        ResultCompleteness::Partial
                    } else {
                        ResultCompleteness::Complete
                    };
                    metadata.false_positive_likelihood = 0.05;
                }
            }
            Value::Array(arr) => {
                metadata.result_count = arr.len();
                metadata.confidence = if arr.is_empty() { 1.0 } else { 0.80 };
                metadata.relevance = 0.75;
            }
            _ => {
                metadata = ResultMetadata::error();
            }
        }

        metadata
    }

    fn tool_name(&self) -> &str {
        tools::CODE_SEARCH
    }
}

/// Scorer for file finding results
pub struct FindScorer;

impl ResultScorer for FindScorer {
    fn score(&self, result: &Value) -> ResultMetadata {
        let mut metadata = ResultMetadata::default();
        metadata.content_types.push("filesystem".to_string());

        match result {
            Value::Object(map) => {
                if let Some(files) = map.get("files")
                    && let Some(file_arr) = files.as_array()
                {
                    metadata.result_count = file_arr.len();
                    metadata.confidence = if file_arr.is_empty() {
                        1.0 // High confidence in "no files"
                    } else {
                        0.90 // Very high confidence in file paths
                    };
                    metadata.relevance = 0.80;
                    metadata.completeness = ResultCompleteness::Complete;
                }
            }
            Value::Array(arr) => {
                metadata.result_count = arr.len();
                metadata.confidence = 0.90;
                metadata.relevance = 0.80;
            }
            _ => {
                metadata = ResultMetadata::error();
            }
        }

        metadata
    }

    fn tool_name(&self) -> &str {
        "find"
    }
}

/// Scorer for shell command results
pub struct ShellScorer;

impl ResultScorer for ShellScorer {
    fn score(&self, result: &Value) -> ResultMetadata {
        let mut metadata = ResultMetadata::default();

        match result {
            Value::Object(map) => {
                // Check for exit code
                let exit_code = map.get("exit_code").and_then(|v| v.as_i64()).unwrap_or(-1);

                // Success means high confidence
                if exit_code == 0 {
                    metadata.confidence = 0.85;
                } else {
                    metadata.confidence = 0.20;
                    metadata.completeness = ResultCompleteness::Empty;
                }

                if let Some(output) = map.get("output").or_else(|| map.get("stdout"))
                    && let Some(s) = output.as_str()
                {
                    metadata.result_count = s.lines().count();
                    metadata.relevance = 0.70;
                }
            }
            _ => {
                metadata = ResultMetadata::error();
            }
        }

        metadata
    }

    fn tool_name(&self) -> &str {
        tools::UNIFIED_EXEC
    }
}

/// Registry for result scorers
pub struct ScorerRegistry {
    scorers: HashMap<CompactStr, Box<dyn ResultScorer>>,
}

impl ScorerRegistry {
    pub fn new() -> Self {
        let mut scorers: HashMap<CompactStr, Box<dyn ResultScorer>> = HashMap::new();
        scorers.insert(CompactStr::from(tools::CODE_SEARCH), Box::new(CodeSearchScorer) as Box<dyn ResultScorer>);
        scorers.insert(CompactStr::from("find"), Box::new(FindScorer) as Box<dyn ResultScorer>);
        scorers.insert(CompactStr::from(tools::UNIFIED_EXEC), Box::new(ShellScorer) as Box<dyn ResultScorer>);

        Self { scorers }
    }

    /// Register a custom scorer
    pub fn register(&mut self, scorer: Box<dyn ResultScorer>) {
        self.scorers.insert(CompactStr::from(scorer.tool_name()), scorer);
    }

    /// Score a tool result
    pub fn score(&self, tool_name: &str, result: &Value) -> ResultMetadata {
        let canonical_tool_name = tool_intent::canonical_command_session_tool_name(tool_name).unwrap_or(tool_name);
        if let Some(scorer) = self.scorers.get(canonical_tool_name) {
            scorer.score(result)
        } else {
            // Default scoring for unknown tools
            match result {
                Value::Null => ResultMetadata::empty(),
                Value::Object(_) => ResultMetadata::success(0.6, 0.6),
                Value::Array(arr) => {
                    let mut meta = ResultMetadata::success(0.6, 0.6);
                    meta.result_count = arr.len();
                    meta
                }
                _ => ResultMetadata::success(0.5, 0.5),
            }
        }
    }
}

impl Default for ScorerRegistry {
    fn default() -> Self {
        Self::new()
    }
}

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

    #[test]
    fn test_result_completeness() {
        assert_eq!(ResultCompleteness::Complete.to_string(), "complete");
        assert_eq!(ResultCompleteness::Partial.to_string(), "partial");
        assert_eq!(ResultCompleteness::Empty.to_string(), "empty");
    }

    #[test]
    fn test_quality_score() {
        let meta = ResultMetadata {
            confidence: 0.8,
            relevance: 0.8,
            false_positive_likelihood: 0.1,
            ..Default::default()
        };

        let score = meta.quality_score();
        assert!(score > 0.6 && score < 0.8);
    }

    #[test]
    fn test_enhanced_result_is_useful() {
        let result = EnhancedToolResult::new(
            json!({"results": [], "returned": 0, "truncated": false}),
            ResultMetadata::success(0.8, 0.8),
            tools::CODE_SEARCH.to_string(),
        );

        assert!(result.is_useful());
        assert!(!result.is_high_quality());
    }

    #[test]
    fn test_code_search_scorer() {
        let scorer = CodeSearchScorer;
        let result = json!({
            "results": ["line1", "line2", "line3"],
            "returned": 3,
            "truncated": false
        });

        let meta = scorer.score(&result);
        assert_eq!(meta.result_count, 3);
        assert!(meta.confidence > 0.7);
        assert_eq!(meta.completeness, ResultCompleteness::Complete);
    }

    #[test]
    fn test_scorer_registry() {
        let registry = ScorerRegistry::new();
        let result = json!({"files": ["a.txt", "b.txt"]});

        let meta = registry.score("find", &result);
        assert_eq!(meta.result_count, 2);
    }

    #[test]
    fn test_command_session_aliases_share_shell_scorer() {
        let registry = ScorerRegistry::new();
        let result = json!({"exit_code": 0, "stdout": "line1\nline2"});

        let unified_meta = registry.score(tools::UNIFIED_EXEC, &result);
        let legacy_meta = registry.score(tools::RUN_PTY_CMD, &result);

        assert_eq!(legacy_meta.result_count, unified_meta.result_count);
        assert!((legacy_meta.confidence - unified_meta.confidence).abs() < f32::EPSILON);
        assert!((legacy_meta.relevance - unified_meta.relevance).abs() < f32::EPSILON);
    }
}