LoopGuard
Protect AI agents from infinite loops. One decorator, zero dependencies.
The Problem
AI agents get stuck in loops. They call the same function with the same arguments over and over, burning tokens and failing silently. This happens in production more than you'd think (50% of n8n users report loops).
The Solution
from loopguard import loopguard
@loopguard(max_repeats=3, window=60)
def agent_action(query: str) -> str:
return llm.complete(query)
# Third call with same query within 60s raises LoopDetectedError
Installation
pip install loopguard
Features
- Thread-safe - Safe for multi-threaded applications
- Memory-safe - Auto-cleans old signatures, no memory leaks
- Zero dependencies - Only Python stdlib
- Async support - Works with async/await
- Non-blocking handlers - Custom handlers don't block other calls
- Type hints - Full typing support with
py.typed - Sub-second precision - Float windows like
window=0.5for rate limiting - Clock-immune - Uses monotonic time, immune to system clock changes
Usage
Basic
from loopguard import loopguard, LoopDetectedError
@loopguard(max_repeats=3, window=60)
def search(query: str) -> str:
return search_api.search(query)
try:
for _ in range(10):
search("same query") # Raises on 4th call
except LoopDetectedError as e:
print(f"Loop stopped: {e}")
Custom Handler
@loopguard(max_repeats=3, on_loop=lambda f, a, k: "Loop detected, stopping")
def agent_step(state: dict) -> str:
return llm.complete(state["query"])
Check Call Count
@loopguard(max_repeats=5, window=60)
def my_func(x):
return x
my_func(10)
my_func(10)
print(my_func.get_count((10,))) # 2
Reset History
@loopguard(max_repeats=2, window=60)
def my_func(x):
return x
my_func(5)
my_func(5)
my_func.reset() # Clear all history
my_func(5) # Works again
Async Support
from loopguard import async_loopguard
@async_loopguard(max_repeats=3, window=60)
async def async_agent_action(query: str) -> str:
return await llm.complete(query)
Async with Async Handler
async def my_handler(func, args, kwargs):
await log_loop_event()
return "fallback response"
@async_loopguard(max_repeats=3, on_loop=my_handler)
async def agent_action(query: str) -> str:
return await llm.complete(query)
With LangChain
from langchain.tools import tool
from loopguard import loopguard
@tool
@loopguard(max_repeats=3, window=120)
def search_tool(query: str) -> str:
"""Search the web."""
return search_api.search(query)
With CrewAI
from crewai import Agent, Task
from loopguard import loopguard
@loopguard(max_repeats=5, window=300)
def execute_task(task: Task) -> str:
return agent.execute(task)
Sub-second Rate Limiting
# Allow max 5 calls per 500ms
@loopguard(max_repeats=5, window=0.5)
def rate_limited_api(query: str) -> str:
return api.call(query)
API
loopguard(max_repeats=3, window=60, on_loop=None)
Decorator for sync functions. Thread-safe.
max_repeats: Max calls with identical args within window (default: 3)window: Time window in seconds, can be float for sub-second precision (default: 60)on_loop: Optional callback(func, args, kwargs) -> Any. If provided, return value is used instead of raising.
Uses monotonic time internally, so immune to system clock adjustments.
Attached methods:
func.reset()- Clear all call historyfunc.get_count(args_tuple, kwargs_dict=None)- Get current count for specific arguments (e.g.,func.get_count((5,)))func.would_trigger(args_tuple, kwargs_dict=None)- Check if next call would trigger loop detectionfunc.get_signatures()- Get list of tracked signature hashes (for debugging)
Supports both @loopguard and @loopguard() syntax.
async_loopguard(max_repeats=3, window=60, on_loop=None)
Same as above, for async functions. Coroutine-safe.
The on_loop callback can be sync or async - both are handled correctly.
LoopDetectedError
Raised when loop detected (unless on_loop provided).
Attributes:
func_name: Name of the looping functioncount: Number of repeated calls that triggered detectionwindow: Time window in seconds
How It Works
- Hash function arguments to create a signature (SHA-256, truncated)
- Track call timestamps per signature (thread-safe)
- Clean entries outside the time window
- If calls with same signature exceed
max_repeats, trigger loop handler - Periodically clean old signatures to prevent memory growth
Thread Safety
Both loopguard and async_loopguard are safe for concurrent use:
import threading
@loopguard(max_repeats=100, window=60)
def my_func(x):
return x
# Safe to call from multiple threads
threads = [threading.Thread(target=lambda: my_func(1)) for _ in range(10)]
License
MIT
Metadata
Release files for loopguard 0.2.0
For a detailed explanation of source distributions (sdists) and built distributions (wheels), please see the package formats documentation.
Source distribution (sdist)
| File | Size | Uploaded | Provenance | |
|---|---|---|---|---|
| loopguard-0.2.0.tar.gz | 9.3 kB | Without provenance | Details |
Built distribution (wheel)
| File | Interpreter | ABI | Platform | Reset |
|---|---|---|---|---|
| loopguard-0.2.0-py3-none-any.whl | Python 3 | none | any | Details |
Total release size: 17.3 kB
Release files / loopguard-0.2.0.tar.gz
| Download URL | loopguard-0.2.0.tar.gz |
|---|---|
| Size | 9.3 kB |
| Tags | Source |
|
SHA-256 checksum How to use checksums |
656b9f480aa77d9e754ad7f4077a3fdd39392e239446d4701de0e6d31b751cc7
|
|
BLAKE2b-256 checksum How to use checksums |
d81b312a4c8f43838db352c06f068ef3ab206edf55159d57d20f60245e04d20b
|
| Upload date | |
|
Uploaded using Trusted Publishing? What is trusted publishing? |
No |
| Uploaded via |
twine/6.2.0 CPython/3.12.3
|
Release files / loopguard-0.2.0-py3-none-any.whl
| Download URL | loopguard-0.2.0-py3-none-any.whl |
|---|---|
| Size | 8.0 kB |
| Tags | Python 3 |
|
SHA-256 checksum How to use checksums |
d8220747d06f833bc9d37a9c724e79ff00d99064fe07d168d5b684b2b2b289be
|
|
BLAKE2b-256 checksum How to use checksums |
650b1eb0792a184f9759681bcf5add4cc6fc402db7797ba03fc938c0955bf167
|
| Upload date | |
|
Uploaded using Trusted Publishing? What is trusted publishing? |
No |
| Uploaded via |
twine/6.2.0 CPython/3.12.3
|
Security for loopguard 0.2.0
Provenance
Provenance describes where a file came from. On PyPI, provenance is shared via attestations, which provide a verifiable record of the build or publishing details. View details, limitations and caveats.
No provenance found
None of the files in this release were uploaded with provenance metadata.
A note on package security
PyPI is a repository for distributing packages, not a service for auditing them. While third-party security researchers and our staff do their best to detect and remove malicious packages, PyPI cannot guarantee the quality, security or suitability of any given package.
To learn more about protecting yourself from malicious actors, visit our package security documentation.