use std::collections::hash_map::DefaultHasher;
use std::hash::{Hash, Hasher};
use std::sync::{Arc, Mutex};
use std::time::{SystemTime, UNIX_EPOCH};
use serde::{Deserialize, Serialize};
#[derive(Debug, Clone, Serialize, Deserialize, PartialEq, Eq)]
pub struct ToolCallRecord {
pub name: String,
pub args_fingerprint: String,
pub timestamp_ms: u64,
}
impl ToolCallRecord {
fn new(name: impl Into<String>, args: &serde_json::Value) -> Self {
let name = name.into();
let args_fingerprint = fingerprint_json(args);
let timestamp_ms = SystemTime::now()
.duration_since(UNIX_EPOCH)
.map(|d| d.as_millis() as u64)
.unwrap_or(0);
Self {
name,
args_fingerprint,
timestamp_ms,
}
}
}
fn fingerprint_json(v: &serde_json::Value) -> String {
let mut h = DefaultHasher::new();
v.to_string().hash(&mut h);
format!("{:016x}", h.finish())
}
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct SequenceDiff {
pub expected: Vec<String>,
pub actual: Vec<String>,
pub edit_distance: usize,
pub similarity: f64,
}
impl SequenceDiff {
pub fn compute(expected: &[&str], actual: &[String]) -> Self {
let exp: Vec<String> = expected.iter().map(|s| s.to_string()).collect();
let ed = levenshtein(
expected,
actual
.iter()
.map(|s| s.as_str())
.collect::<Vec<_>>()
.as_slice(),
);
let max_len = exp.len().max(actual.len());
let similarity = if max_len == 0 {
1.0
} else {
1.0 - (ed as f64 / max_len as f64)
};
Self {
expected: exp,
actual: actual.to_vec(),
edit_distance: ed,
similarity,
}
}
pub fn is_exact_match(&self) -> bool {
self.edit_distance == 0
}
}
fn levenshtein(a: &[&str], b: &[&str]) -> usize {
let n = a.len();
let m = b.len();
let mut dp = vec![vec![0usize; m + 1]; n + 1];
for (i, row) in dp.iter_mut().enumerate().take(n + 1) {
row[0] = i;
}
for (j, val) in dp[0].iter_mut().enumerate().take(m + 1) {
*val = j;
}
for i in 1..=n {
for j in 1..=m {
dp[i][j] = if a[i - 1] == b[j - 1] {
dp[i - 1][j - 1]
} else {
1 + dp[i - 1][j].min(dp[i][j - 1]).min(dp[i - 1][j - 1])
};
}
}
dp[n][m]
}
#[derive(Debug, Clone, Default)]
pub struct ToolSequenceRecorder {
inner: Arc<Mutex<Vec<ToolCallRecord>>>,
}
impl ToolSequenceRecorder {
pub fn new() -> Self {
Self::default()
}
pub fn record(&self, name: impl Into<String>, args: &serde_json::Value) {
let record = ToolCallRecord::new(name, args);
self.inner
.lock()
.expect("recorder lock poisoned")
.push(record);
}
pub fn calls(&self) -> Vec<ToolCallRecord> {
self.inner.lock().expect("recorder lock poisoned").clone()
}
pub fn call_names(&self) -> Vec<String> {
self.inner
.lock()
.expect("recorder lock poisoned")
.iter()
.map(|r| r.name.clone())
.collect()
}
pub fn diff_against(&self, expected: &[&str]) -> SequenceDiff {
let actual = self.call_names();
SequenceDiff::compute(expected, &actual)
}
pub fn reset(&self) {
self.inner.lock().expect("recorder lock poisoned").clear();
}
pub fn len(&self) -> usize {
self.inner.lock().expect("recorder lock poisoned").len()
}
pub fn is_empty(&self) -> bool {
self.len() == 0
}
}
#[cfg(test)]
mod tests {
use super::*;
use serde_json::json;
#[test]
fn test_record_and_retrieve() {
let recorder = ToolSequenceRecorder::new();
recorder.record("read_file", &json!({"path": "a.rs"}));
recorder.record("write_file", &json!({"path": "b.rs"}));
let calls = recorder.calls();
assert_eq!(calls.len(), 2);
assert_eq!(calls[0].name, "read_file");
assert_eq!(calls[1].name, "write_file");
}
#[test]
fn test_call_names() {
let recorder = ToolSequenceRecorder::new();
recorder.record("bash", &json!({}));
recorder.record("read_file", &json!({}));
assert_eq!(recorder.call_names(), vec!["bash", "read_file"]);
}
#[test]
fn test_diff_exact_match() {
let recorder = ToolSequenceRecorder::new();
recorder.record("a", &json!({}));
recorder.record("b", &json!({}));
recorder.record("c", &json!({}));
let diff = recorder.diff_against(&["a", "b", "c"]);
assert!(diff.is_exact_match());
assert!((diff.similarity - 1.0).abs() < 1e-9);
}
#[test]
fn test_diff_partial_match() {
let recorder = ToolSequenceRecorder::new();
recorder.record("a", &json!({}));
recorder.record("x", &json!({})); recorder.record("c", &json!({}));
let diff = recorder.diff_against(&["a", "b", "c"]);
assert!(!diff.is_exact_match());
assert_eq!(diff.edit_distance, 1);
assert!(diff.similarity > 0.5);
}
#[test]
fn test_diff_empty_vs_expected() {
let recorder = ToolSequenceRecorder::new();
let diff = recorder.diff_against(&["a", "b"]);
assert_eq!(diff.edit_distance, 2);
assert_eq!(diff.similarity, 0.0);
}
#[test]
fn test_diff_both_empty() {
let recorder = ToolSequenceRecorder::new();
let diff = recorder.diff_against(&[]);
assert!(diff.is_exact_match());
assert!((diff.similarity - 1.0).abs() < 1e-9);
}
#[test]
fn test_reset_clears_calls() {
let recorder = ToolSequenceRecorder::new();
recorder.record("a", &json!({}));
recorder.reset();
assert!(recorder.is_empty());
}
#[test]
fn test_args_fingerprint_differs_for_different_args() {
let r1 = ToolCallRecord::new("tool", &json!({"a": 1}));
let r2 = ToolCallRecord::new("tool", &json!({"a": 2}));
assert_ne!(r1.args_fingerprint, r2.args_fingerprint);
}
#[test]
fn test_args_fingerprint_same_for_same_args() {
let r1 = ToolCallRecord::new("tool", &json!({"x": "hello"}));
let r2 = ToolCallRecord::new("tool", &json!({"x": "hello"}));
assert_eq!(r1.args_fingerprint, r2.args_fingerprint);
}
#[test]
fn test_levenshtein_identical() {
assert_eq!(levenshtein(&["a", "b", "c"], &["a", "b", "c"]), 0);
}
#[test]
fn test_levenshtein_single_substitution() {
assert_eq!(levenshtein(&["a", "b", "c"], &["a", "x", "c"]), 1);
}
#[test]
fn test_levenshtein_insert_delete() {
assert_eq!(levenshtein(&["a", "b"], &["a", "b", "c"]), 1);
assert_eq!(levenshtein(&["a", "b", "c"], &["a", "b"]), 1);
}
}