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;
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
}
}
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 } else {
0.90 };
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"
}
}
pub struct ShellScorer;
impl ResultScorer for ShellScorer {
fn score(&self, result: &Value) -> ResultMetadata {
let mut metadata = ResultMetadata::default();
match result {
Value::Object(map) => {
let exit_code = map.get("exit_code").and_then(|v| v.as_i64()).unwrap_or(-1);
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
}
}
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 }
}
pub fn register(&mut self, scorer: Box<dyn ResultScorer>) {
self.scorers.insert(CompactStr::from(scorer.tool_name()), scorer);
}
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 {
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);
}
}