vtcode_core/tools/
result_metadata.rs1pub use vtcode_commons::tool_types::{
13 CompactStr, EnhancedToolResult, ResultCompleteness, ResultMetadata, ResultScorer,
14};
15
16use crate::config::constants::tools;
17use crate::tools::tool_intent;
18use hashbrown::HashMap;
19use serde_json::Value;
20
21pub struct CodeSearchScorer;
23
24impl ResultScorer for CodeSearchScorer {
25 fn score(&self, result: &Value) -> ResultMetadata {
26 let mut metadata = ResultMetadata::default();
27 metadata.content_types.push("code".to_string());
28
29 match result {
30 Value::Object(map) => {
31 if let Some(results) = map.get("results")
32 && let Some(entries) = results.as_array()
33 {
34 metadata.result_count = entries.len();
35
36 metadata.confidence = if entries.len() > 5 {
37 0.85
38 } else if !entries.is_empty() {
39 0.80
40 } else {
41 1.0
42 };
43
44 metadata.relevance = 0.75;
45 metadata.completeness = if map.get("truncated").and_then(Value::as_bool).unwrap_or(false) {
46 ResultCompleteness::Partial
47 } else {
48 ResultCompleteness::Complete
49 };
50 metadata.false_positive_likelihood = 0.05;
51 }
52 }
53 Value::Array(arr) => {
54 metadata.result_count = arr.len();
55 metadata.confidence = if arr.is_empty() { 1.0 } else { 0.80 };
56 metadata.relevance = 0.75;
57 }
58 _ => {
59 metadata = ResultMetadata::error();
60 }
61 }
62
63 metadata
64 }
65
66 fn tool_name(&self) -> &str {
67 tools::CODE_SEARCH
68 }
69}
70
71pub struct FindScorer;
73
74impl ResultScorer for FindScorer {
75 fn score(&self, result: &Value) -> ResultMetadata {
76 let mut metadata = ResultMetadata::default();
77 metadata.content_types.push("filesystem".to_string());
78
79 match result {
80 Value::Object(map) => {
81 if let Some(files) = map.get("files")
82 && let Some(file_arr) = files.as_array()
83 {
84 metadata.result_count = file_arr.len();
85 metadata.confidence = if file_arr.is_empty() {
86 1.0 } else {
88 0.90 };
90 metadata.relevance = 0.80;
91 metadata.completeness = ResultCompleteness::Complete;
92 }
93 }
94 Value::Array(arr) => {
95 metadata.result_count = arr.len();
96 metadata.confidence = 0.90;
97 metadata.relevance = 0.80;
98 }
99 _ => {
100 metadata = ResultMetadata::error();
101 }
102 }
103
104 metadata
105 }
106
107 fn tool_name(&self) -> &str {
108 "find"
109 }
110}
111
112pub struct ShellScorer;
114
115impl ResultScorer for ShellScorer {
116 fn score(&self, result: &Value) -> ResultMetadata {
117 let mut metadata = ResultMetadata::default();
118
119 match result {
120 Value::Object(map) => {
121 let exit_code = map.get("exit_code").and_then(|v| v.as_i64()).unwrap_or(-1);
123
124 if exit_code == 0 {
126 metadata.confidence = 0.85;
127 } else {
128 metadata.confidence = 0.20;
129 metadata.completeness = ResultCompleteness::Empty;
130 }
131
132 if let Some(output) = map.get("output").or_else(|| map.get("stdout"))
133 && let Some(s) = output.as_str()
134 {
135 metadata.result_count = s.lines().count();
136 metadata.relevance = 0.70;
137 }
138 }
139 _ => {
140 metadata = ResultMetadata::error();
141 }
142 }
143
144 metadata
145 }
146
147 fn tool_name(&self) -> &str {
148 tools::UNIFIED_EXEC
149 }
150}
151
152pub struct ScorerRegistry {
154 scorers: HashMap<CompactStr, Box<dyn ResultScorer>>,
155}
156
157impl ScorerRegistry {
158 pub fn new() -> Self {
159 let mut scorers: HashMap<CompactStr, Box<dyn ResultScorer>> = HashMap::new();
160 scorers.insert(CompactStr::from(tools::CODE_SEARCH), Box::new(CodeSearchScorer) as Box<dyn ResultScorer>);
161 scorers.insert(CompactStr::from("find"), Box::new(FindScorer) as Box<dyn ResultScorer>);
162 scorers.insert(CompactStr::from(tools::UNIFIED_EXEC), Box::new(ShellScorer) as Box<dyn ResultScorer>);
163
164 Self { scorers }
165 }
166
167 pub fn register(&mut self, scorer: Box<dyn ResultScorer>) {
169 self.scorers.insert(CompactStr::from(scorer.tool_name()), scorer);
170 }
171
172 pub fn score(&self, tool_name: &str, result: &Value) -> ResultMetadata {
174 let canonical_tool_name = tool_intent::canonical_command_session_tool_name(tool_name).unwrap_or(tool_name);
175 if let Some(scorer) = self.scorers.get(canonical_tool_name) {
176 scorer.score(result)
177 } else {
178 match result {
180 Value::Null => ResultMetadata::empty(),
181 Value::Object(_) => ResultMetadata::success(0.6, 0.6),
182 Value::Array(arr) => {
183 let mut meta = ResultMetadata::success(0.6, 0.6);
184 meta.result_count = arr.len();
185 meta
186 }
187 _ => ResultMetadata::success(0.5, 0.5),
188 }
189 }
190 }
191}
192
193impl Default for ScorerRegistry {
194 fn default() -> Self {
195 Self::new()
196 }
197}
198
199#[cfg(test)]
200mod tests {
201 use super::*;
202 use serde_json::json;
203
204 #[test]
205 fn test_result_completeness() {
206 assert_eq!(ResultCompleteness::Complete.to_string(), "complete");
207 assert_eq!(ResultCompleteness::Partial.to_string(), "partial");
208 assert_eq!(ResultCompleteness::Empty.to_string(), "empty");
209 }
210
211 #[test]
212 fn test_quality_score() {
213 let meta = ResultMetadata {
214 confidence: 0.8,
215 relevance: 0.8,
216 false_positive_likelihood: 0.1,
217 ..Default::default()
218 };
219
220 let score = meta.quality_score();
221 assert!(score > 0.6 && score < 0.8);
222 }
223
224 #[test]
225 fn test_enhanced_result_is_useful() {
226 let result = EnhancedToolResult::new(
227 json!({"results": [], "returned": 0, "truncated": false}),
228 ResultMetadata::success(0.8, 0.8),
229 tools::CODE_SEARCH.to_string(),
230 );
231
232 assert!(result.is_useful());
233 assert!(!result.is_high_quality());
234 }
235
236 #[test]
237 fn test_code_search_scorer() {
238 let scorer = CodeSearchScorer;
239 let result = json!({
240 "results": ["line1", "line2", "line3"],
241 "returned": 3,
242 "truncated": false
243 });
244
245 let meta = scorer.score(&result);
246 assert_eq!(meta.result_count, 3);
247 assert!(meta.confidence > 0.7);
248 assert_eq!(meta.completeness, ResultCompleteness::Complete);
249 }
250
251 #[test]
252 fn test_scorer_registry() {
253 let registry = ScorerRegistry::new();
254 let result = json!({"files": ["a.txt", "b.txt"]});
255
256 let meta = registry.score("find", &result);
257 assert_eq!(meta.result_count, 2);
258 }
259
260 #[test]
261 fn test_command_session_aliases_share_shell_scorer() {
262 let registry = ScorerRegistry::new();
263 let result = json!({"exit_code": 0, "stdout": "line1\nline2"});
264
265 let unified_meta = registry.score(tools::UNIFIED_EXEC, &result);
266 let legacy_meta = registry.score(tools::RUN_PTY_CMD, &result);
267
268 assert_eq!(legacy_meta.result_count, unified_meta.result_count);
269 assert!((legacy_meta.confidence - unified_meta.confidence).abs() < f32::EPSILON);
270 assert!((legacy_meta.relevance - unified_meta.relevance).abs() < f32::EPSILON);
271 }
272}