Expand description
§tool-loop-guard
Detect when an LLM agent gets stuck calling the same tool with the same args.
When an LLM agent gets confused, the most common failure mode is calling the same tool with the same args over and over. A circuit breaker won’t catch this (no errors), a deadline won’t catch it (single calls are fast), but the wall clock keeps ticking and the bill keeps growing.
LoopGuard watches a sliding window of recent tool calls and returns
LoopDetectedError the moment a (tool_name, args) pair repeats more
than threshold times within the last window calls.
use serde_json::json;
use tool_loop_guard::{LoopGuard, LoopDetectedError};
let mut guard = LoopGuard::with_capacity(8, 3);
let args = json!({"q": "anthropic prompt cache"});
guard.record("search", &args).unwrap();
guard.record("search", &args).unwrap();
match guard.record("search", &args) {
Ok(()) => unreachable!(),
Err(LoopDetectedError { tool_name, count, window, .. }) => {
assert_eq!(tool_name, "search");
assert_eq!(count, 3);
assert_eq!(window, 8);
}
}The default key is (tool_name, canonical_json(args)). For more exotic
matching (for example, ignore a request_id field in args) pass a custom
key_fn via LoopGuard::with_key_fn.
§Why not just serde_json::to_string?
serde_json::to_string does not sort object keys, so
{"a":1,"b":2} and {"b":2,"a":1} would hash differently. This crate
re-walks the serde_json::Value and emits a stable canonical form
(sorted object keys, compact separators) so semantically identical
argument blobs collide as expected. That matches Python’s
json.dumps(args, sort_keys=True, separators=(",", ":")).
Structs§
- Loop
Detected Error - Raised when the same
(tool_name, args)pair fires too often. - Loop
Guard - Sliding-window detector for repeated tool calls.
Enums§
- Config
Error - Errors from invalid
LoopGuardconstruction.
Functions§
- default_
key_ fn - Compute the canonical default key for a
(tool_name, args)pair.