tool-loop-break 0.1.0

Detect repeated agent tool invocations to break runaway loops. Tracks recent (tool, args_hash) tuples and signals a loop when count exceeds a threshold. Zero deps.
Documentation
use tool_loop_break::LoopDetector;

#[test]
fn three_identical_trips_loop() {
    let mut d = LoopDetector::new(10, 3);
    assert!(!d.record("read", "abc"));
    assert!(!d.record("read", "abc"));
    assert!(d.record("read", "abc"));
}

#[test]
fn different_args_dont_trip() {
    let mut d = LoopDetector::new(10, 3);
    assert!(!d.record("read", "a"));
    assert!(!d.record("read", "b"));
    assert!(!d.record("read", "c"));
}

#[test]
fn different_tools_dont_collide() {
    let mut d = LoopDetector::new(10, 2);
    assert!(!d.record("read", "x"));
    assert!(!d.record("write", "x"));
    assert!(d.record("read", "x")); // 2nd "read x" trips
}

#[test]
fn window_eviction_clears_loop() {
    // window=3, threshold=2. After 3 records with different args, a 4th
    // record evicts the first.
    let mut d = LoopDetector::new(3, 2);
    d.record("t", "a");
    d.record("t", "b");
    d.record("t", "c");
    // "a" has been evicted; recording "a" again is fresh.
    assert!(!d.record("t", "a"));
}

#[test]
fn is_looping_flag() {
    let mut d = LoopDetector::new(10, 2);
    assert!(!d.is_looping());
    d.record("t", "x");
    d.record("t", "x");
    assert!(d.is_looping());
    d.reset();
    assert!(!d.is_looping());
}