1#![expect(
2 clippy::cast_possible_truncation,
3 reason = "Quality scores are normalized to the documented 0..=100 display range before narrowing."
4)]
5
6pub type CompactStr = compact_str::CompactString;
26
27pub mod tool_names {
34 pub const CODE_SEARCH: &str = "code_search";
36 pub const EXEC_COMMAND: &str = "exec_command";
38}
39
40pub const fn canonical_tool_name(name: &str) -> &str {
44 name
45}
46
47pub const ERROR_DETECTION_PATTERNS: &[&str] = &[
53 "error",
54 "failed",
55 "exception",
56 "permission denied",
57 "not found",
58 "no such file",
59 "cannot",
60 "could not",
61 "panic",
62 "crash",
63 "unhandled",
64 "fatal",
65 "timeout",
66 "connection refused",
67 "access denied",
68 "stack trace",
69 "traceback",
70 "abort",
71 "terminate",
72];
73
74pub const NETWORK_ERROR_PATTERNS: &[&str] = &["connection", "timeout", "network", "http", "ssl", "tls", "dns", "proxy"];
76
77pub const DEFAULT_VEC_CAPACITY: usize = 32;
79pub const DEFAULT_HASHMAP_CAPACITY: usize = 16;
80pub const DEFAULT_STRING_CAPACITY: usize = 256;
81
82pub const MAX_SEARCH_RESULTS: usize = 5;
84pub const MAX_LIST_ITEMS_SUMMARY: usize = 5;
85pub const OVERFLOW_INDICATOR_PREFIX: &str = "[+]";
86pub const OVERFLOW_INDICATOR_SUFFIX: &str = "more items]";
87
88pub const MAX_FILE_SIZE_FOR_PROCESSING: usize = 100 * 1024 * 1024; pub const MAX_CONTEXT_LINES: usize = 20;
91pub const MAX_OUTPUT_TOKENS: usize = 4000;
92
93pub fn empty_object_schema() -> Value {
96 serde_json::json!({"type": "object"})
97}
98
99use hashbrown::HashMap;
104use serde::{Deserialize, Serialize};
105use serde_json::Value;
106use std::fmt;
107use std::time::SystemTime;
108
109#[derive(Debug, Clone, Copy, Serialize, Deserialize, PartialEq, Eq)]
111pub enum ResultCompleteness {
112 Complete,
114 Partial,
116 Truncated,
118 Empty,
120}
121
122impl ResultCompleteness {
123 fn to_static_str(self) -> &'static str {
125 match self {
126 Self::Complete => "complete",
127 Self::Partial => "partial",
128 Self::Truncated => "truncated",
129 Self::Empty => "empty",
130 }
131 }
132}
133
134impl fmt::Display for ResultCompleteness {
135 fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
136 f.write_str(self.to_static_str())
137 }
138}
139
140#[derive(Debug, Clone, Serialize, Deserialize)]
142pub struct ResultMetadata {
143 #[serde(default = "default_confidence")]
145 pub confidence: f32,
146
147 #[serde(default = "default_relevance")]
149 pub relevance: f32,
150
151 pub completeness: ResultCompleteness,
153
154 #[serde(default)]
156 pub result_count: usize,
157
158 #[serde(default)]
160 pub false_positive_likelihood: f32,
161
162 #[serde(default)]
164 pub content_types: Vec<String>,
165
166 #[serde(default)]
168 pub tool_metrics: HashMap<String, Value>,
169}
170
171fn default_confidence() -> f32 {
172 0.5
173}
174
175fn default_relevance() -> f32 {
176 0.5
177}
178
179impl Default for ResultMetadata {
180 fn default() -> Self {
181 Self {
182 confidence: 0.5,
183 relevance: 0.5,
184 completeness: ResultCompleteness::Complete,
185 result_count: 0,
186 false_positive_likelihood: 0.1,
187 content_types: vec![],
188 tool_metrics: HashMap::new(),
189 }
190 }
191}
192
193impl ResultMetadata {
194 #[inline]
196 pub fn quality_score(&self) -> f32 {
197 let weighted = (self.confidence * 0.4) + (self.relevance * 0.4) + (self.false_positive_likelihood * -0.2);
198 weighted.clamp(0.0, 1.0)
199 }
200
201 #[inline]
203 pub fn success(confidence: f32, relevance: f32) -> Self {
204 Self {
205 confidence: confidence.clamp(0.0, 1.0),
206 relevance: relevance.clamp(0.0, 1.0),
207 completeness: ResultCompleteness::Complete,
208 result_count: 1,
209 false_positive_likelihood: 0.05,
210 ..Default::default()
211 }
212 }
213
214 #[inline]
216 pub fn empty() -> Self {
217 Self {
218 completeness: ResultCompleteness::Empty,
219 result_count: 0,
220 confidence: 1.0, ..Default::default()
222 }
223 }
224
225 pub fn error() -> Self {
227 Self {
228 confidence: 0.2,
229 completeness: ResultCompleteness::Empty,
230 ..Default::default()
231 }
232 }
233
234 pub fn merge(&mut self, other: &ResultMetadata) {
236 self.result_count += other.result_count;
237 self.confidence = (self.confidence + other.confidence) / 2.0;
238 self.relevance = (self.relevance + other.relevance) / 2.0;
239
240 for ct in &other.content_types {
242 if !self.content_types.contains(ct) {
243 self.content_types.push(ct.clone());
244 }
245 }
246
247 self.tool_metrics
249 .extend(other.tool_metrics.iter().map(|(k, v)| (k.clone(), v.clone())));
250 }
251}
252
253#[derive(Debug, Clone, Serialize, Deserialize)]
255pub struct EnhancedToolResult {
256 value: Value,
258
259 pub metadata: ResultMetadata,
261
262 timestamp: u64,
264
265 tool_name: CompactStr,
267
268 #[serde(default)]
270 from_cache: bool,
271}
272
273impl EnhancedToolResult {
274 pub fn new(value: Value, metadata: ResultMetadata, tool_name: impl Into<CompactStr>) -> Self {
275 Self {
276 value,
277 metadata,
278 timestamp: SystemTime::now()
279 .duration_since(SystemTime::UNIX_EPOCH)
280 .unwrap_or_default()
281 .as_secs(),
282 tool_name: tool_name.into(),
283 from_cache: false,
284 }
285 }
286
287 pub fn from_cache(value: Value, metadata: ResultMetadata, tool_name: impl Into<CompactStr>) -> Self {
288 Self {
289 value,
290 metadata,
291 timestamp: SystemTime::now()
292 .duration_since(SystemTime::UNIX_EPOCH)
293 .unwrap_or_default()
294 .as_secs(),
295 tool_name: tool_name.into(),
296 from_cache: true,
297 }
298 }
299
300 #[inline]
302 pub fn is_useful(&self) -> bool {
303 self.metadata.quality_score() > 0.3
304 }
305
306 #[inline]
308 pub fn is_high_quality(&self) -> bool {
309 self.metadata.quality_score() > 0.7
310 }
311
312 #[allow(
314 clippy::cast_sign_loss,
315 reason = "Intentional compatibility, platform, or test-only suppression."
316 )] pub fn to_summary(&self) -> String {
318 let quality = ((self.metadata.quality_score() * 100.0).round().max(0.0) as u32).min(100);
319 match self.metadata.completeness {
320 ResultCompleteness::Complete => {
321 format!("{} found {} results (confidence: {}%)", self.tool_name, self.metadata.result_count, quality)
322 }
323 ResultCompleteness::Partial => {
324 format!(
325 "{} found {} results (truncated, confidence: {}%)",
326 self.tool_name, self.metadata.result_count, quality
327 )
328 }
329 ResultCompleteness::Empty => {
330 format!("{} found no results", self.tool_name)
331 }
332 ResultCompleteness::Truncated => {
333 format!("{} found results (truncated due to size, confidence: {}%)", self.tool_name, quality)
334 }
335 }
336 }
337}
338
339pub trait ResultScorer {
341 fn score(&self, result: &Value) -> ResultMetadata;
343
344 fn tool_name(&self) -> &str;
346}