mod matcher;
use serde::{Deserialize, Serialize};
use std::collections::HashSet;
use uuid::Uuid;
pub use matcher::SignatureMatcher;
#[derive(Debug, Clone, Copy, Serialize, Deserialize)]
pub struct SignatureConfig {
pub tool_weight: f32,
pub error_weight: f32,
pub structure_weight: f32,
pub min_overlap_threshold: f32,
pub normalize_scores: bool,
}
impl Default for SignatureConfig {
fn default() -> Self {
Self {
tool_weight: 0.4,
error_weight: 0.3,
structure_weight: 0.3,
min_overlap_threshold: 0.2,
normalize_scores: true,
}
}
}
impl SignatureConfig {
#[must_use]
pub fn tool_focused() -> Self {
Self {
tool_weight: 0.6,
error_weight: 0.2,
structure_weight: 0.2,
min_overlap_threshold: 0.3,
normalize_scores: true,
}
}
#[must_use]
pub fn error_focused() -> Self {
Self {
tool_weight: 0.2,
error_weight: 0.6,
structure_weight: 0.2,
min_overlap_threshold: 0.2,
normalize_scores: true,
}
}
#[must_use]
pub fn balanced() -> Self {
Self {
tool_weight: 0.33,
error_weight: 0.33,
structure_weight: 0.34,
min_overlap_threshold: 0.25,
normalize_scores: true,
}
}
}
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct ExecutionSignature {
pub episode_id: Uuid,
pub tools: HashSet<String>,
pub error_types: HashSet<String>,
pub step_pattern: StepPattern,
pub total_steps: usize,
pub successful_steps: usize,
pub failed_steps: usize,
pub avg_latency_ms: u64,
}
impl ExecutionSignature {
#[must_use]
pub fn new(episode_id: Uuid) -> Self {
Self {
episode_id,
tools: HashSet::new(),
error_types: HashSet::new(),
step_pattern: StepPattern::default(),
total_steps: 0,
successful_steps: 0,
failed_steps: 0,
avg_latency_ms: 0,
}
}
pub fn add_tool(&mut self, tool: &str) {
self.tools.insert(tool.to_lowercase());
}
pub fn add_error(&mut self, error_type: &str) {
self.error_types.insert(normalize_error_type(error_type));
}
pub fn record_step(&mut self, success: bool) {
self.total_steps += 1;
if success {
self.successful_steps += 1;
self.step_pattern.success_count += 1;
} else {
self.failed_steps += 1;
self.step_pattern.failure_count += 1;
}
}
pub fn set_avg_latency(&mut self, latency_ms: u64) {
self.avg_latency_ms = latency_ms;
}
#[must_use]
pub fn success_rate(&self) -> f32 {
if self.total_steps == 0 {
return 0.0;
}
self.successful_steps as f32 / self.total_steps as f32
}
#[must_use]
pub fn tool_count(&self) -> usize {
self.tools.len()
}
#[must_use]
pub fn error_count(&self) -> usize {
self.error_types.len()
}
#[must_use]
pub fn has_tools(&self) -> bool {
!self.tools.is_empty()
}
#[must_use]
pub fn has_errors(&self) -> bool {
!self.error_types.is_empty()
}
}
#[derive(Debug, Clone, Default, Serialize, Deserialize)]
pub struct StepPattern {
pub success_count: usize,
pub failure_count: usize,
pub final_success: bool,
pub pattern_code: String,
}
impl StepPattern {
#[must_use]
pub fn from_outcomes(outcomes: &[bool]) -> Self {
let success_count = outcomes.iter().filter(|&s| *s).count();
let failure_count = outcomes.len() - success_count;
let final_success = outcomes.last().copied().unwrap_or(false);
let pattern_code = outcomes
.iter()
.map(|s| if *s { 'S' } else { 'F' })
.collect::<String>();
Self {
success_count,
failure_count,
final_success,
pattern_code,
}
}
#[must_use]
pub fn similarity(&self, other: &StepPattern) -> f32 {
if self.pattern_code.is_empty() || other.pattern_code.is_empty() {
return 0.0;
}
let len_ratio = if self.pattern_code.len() > other.pattern_code.len() {
other.pattern_code.len() as f32 / self.pattern_code.len() as f32
} else {
self.pattern_code.len() as f32 / other.pattern_code.len() as f32
};
let self_success_ratio =
self.success_count as f32 / (self.success_count + self.failure_count).max(1) as f32;
let other_success_ratio =
other.success_count as f32 / (other.success_count + other.failure_count).max(1) as f32;
let ratio_similarity = 1.0 - (self_success_ratio - other_success_ratio).abs();
let final_match = if self.final_success == other.final_success {
1.0
} else {
0.0
};
len_ratio * 0.3 + ratio_similarity * 0.5 + final_match * 0.2
}
}
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct QuerySignature {
pub expected_tools: HashSet<String>,
pub relevant_errors: HashSet<String>,
pub expected_pattern: Option<StepPattern>,
}
impl QuerySignature {
#[must_use]
pub fn new() -> Self {
Self {
expected_tools: HashSet::new(),
relevant_errors: HashSet::new(),
expected_pattern: None,
}
}
#[must_use]
pub fn from_query_text(query: &str) -> Self {
let mut sig = Self::new();
let common_tools = [
"read",
"write",
"bash",
"grep",
"edit",
"glob",
"lsp",
"webfetch",
"websearch",
"skill",
"agent",
"cargo",
"rustc",
"git",
"npm",
"pytest",
];
for tool in common_tools {
if query.to_lowercase().contains(tool) {
sig.expected_tools.insert(tool.to_string());
}
}
let error_patterns = [
"timeout",
"error",
"failure",
"panic",
"exception",
" deadlock",
"race",
"null",
"missing",
"invalid",
];
for pattern in error_patterns {
if query.to_lowercase().contains(pattern) {
sig.relevant_errors.insert(normalize_error_type(pattern));
}
}
sig
}
pub fn add_tool(&mut self, tool: &str) {
self.expected_tools.insert(tool.to_lowercase());
}
pub fn add_error(&mut self, error: &str) {
self.relevant_errors.insert(normalize_error_type(error));
}
pub fn set_pattern(&mut self, pattern: StepPattern) {
self.expected_pattern = Some(pattern);
}
#[must_use]
pub fn has_tool_expectations(&self) -> bool {
!self.expected_tools.is_empty()
}
#[must_use]
pub fn has_error_expectations(&self) -> bool {
!self.relevant_errors.is_empty()
}
}
impl Default for QuerySignature {
fn default() -> Self {
Self::new()
}
}
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct SignatureMatch {
pub episode_id: Uuid,
pub score: f32,
pub tool_score: f32,
pub error_score: f32,
pub structure_score: f32,
pub contributing_components: Vec<String>,
}
impl SignatureMatch {
#[must_use]
pub fn is_strong_match(&self) -> bool {
self.score >= 0.5
}
#[must_use]
pub fn is_weak_match(&self) -> bool {
self.score >= 0.2
}
}
#[cfg(test)]
pub(crate) fn normalize_error_type(error: &str) -> String {
let lower = error.to_lowercase();
let canonical = match lower.as_str() {
"timeout" | "timed out" | "time out" => "timeout",
"panic" | "panicked" => "panic",
"deadlock" | "dead lock" => "deadlock",
"race" | "race condition" => "race_condition",
"null" | "null pointer" | "nullptr" => "null_pointer",
"missing" | "not found" | "does not exist" => "missing",
"invalid" | "invalid input" | "invalid argument" => "invalid",
e => e.trim(),
};
canonical.to_string()
}
#[cfg(not(test))]
fn normalize_error_type(error: &str) -> String {
let lower = error.to_lowercase();
let canonical = match lower.as_str() {
"timeout" | "timed out" | "time out" => "timeout",
"panic" | "panicked" => "panic",
"deadlock" | "dead lock" => "deadlock",
"race" | "race condition" => "race_condition",
"null" | "null pointer" | "nullptr" => "null_pointer",
"missing" | "not found" | "does not exist" => "missing",
"invalid" | "invalid input" | "invalid argument" => "invalid",
e => e.trim(),
};
canonical.to_string()
}
#[cfg(test)]
mod tests;