BotLoopGuard breaks runaway bot-to-bot reply loops — two bots answering each other forever — by budgeting how many exchanges a pair may have inside a sliding window.
BotLoopGuard is a gateway-layer primitive: it decides whether to admit one bot’s reply to another bot. It differs from Loop Guard, which caps tool calls inside a single agent turn.Quick Start
Enable with defaults
Call
observe() for each bot-authored inbound turn and drop the turn when it returns False.Tune the budget with BotLoopPolicy
Pass a
BotLoopPolicy to change how quickly a loop trips and how long it stays quiet.How It Works
The guard tracks each bot pair in a sliding window; exchange #21 trips the budget and both bots go quiet for the cooldown.A → B and B → A collapse to the same pair key, so a ping-pong loop accrues one budget instead of two independent streams.
| Behaviour | Effect |
|---|---|
| Direction independence | A↔B and B↔A share one budget |
| Sliding window | Events older than window_seconds are evicted before the budget check |
| Cooldown | Once exceeded, the pair is dropped for cooldown_seconds without recording |
| Distinct pairs | A↔B at budget does not affect A↔C |
When to Enable
BotLoopGuard only matters when your gateway accepts bot-authored inbound messages and multiple bots can share a room.
Configuration Options
BotLoopPolicy declares the per-pair budget. Every field matches praisonaiagents/bots/silence.py.
| Field | Type | Default | Description |
|---|---|---|---|
enabled | bool | True | When False, the guard is a transparent no-op (always allows). |
max_events_per_window | int | 20 | Max bot-authored exchanges per pair before cooldown opens. Clamped to >= 1. |
window_seconds | float | 60.0 | Length of the sliding window. |
cooldown_seconds | float | 60.0 | How long a pair stays suppressed once the budget is exceeded. |
BotLoopPolicy.from_dict(data) builds a policy from a mapping; None returns the enabled default and unknown keys are ignored.
The runtime BotLoopGuard exposes this surface:
| Symbol | Signature | Purpose |
|---|---|---|
| Constructor | BotLoopGuard(policy: Optional[BotLoopPolicy] = None) | Defaults to the enabled default policy; uses an internal lock for shared-state safety. |
enabled | @property -> bool | Whether the guard actively suppresses loops. |
observe | observe(*, self_bot_id: str, sender_bot_id: str, now: Optional[float] = None) -> bool | Records one exchange; returns True to allow or False to drop. Always True when disabled. |
reset | reset() -> None | Clears all tracked pairs and cooldowns. |
BotLoopGuard ships in the Python SDK only. TypeScript and Rust ports do not exist yet — do not wire it into docs/js/ or docs/rust/.Common Patterns
Three patterns cover most gateway deployments. Group room with multiple assistants — use one guard per gateway and callobserve() only when the sender is a bot.
now= values for deterministic time-dependent tests.
reset() after redeploy so leftover cooldowns from an earlier process don’t leak.
When It Fires vs Doesn’t
Only bot-authored inbound turns reach the guard; humans and single-bot deployments never callobserve().
| Inbound sender | Below budget | Above budget in cooldown | After cooldown expires |
|---|---|---|---|
| Human | Not observed (always allowed) | Not observed | Not observed |
| Bot (different pair) | Allowed | Allowed (independent pair) | Allowed |
| Bot (same pair) | Allowed | Dropped | Allowed (fresh window) |
Best Practices
Size the window and cooldown separately
Size the window and cooldown separately
Use a shorter
window_seconds with a higher cooldown_seconds for chatty rooms — the window sizes urgency, the cooldown sizes recovery.Disable with enabled=False, not a zero budget
Disable with enabled=False, not a zero budget
max_events_per_window=0 is clamped to 1 and hides intent. Set enabled=False to turn the guard off.Combine with Intentional Silence
Combine with Intentional Silence
Pair the guard with
allow_silence and Intentional Silence for full ambient-channel safety — the agent chooses to stay quiet, and the guard stops runaway loops.Related
Intentional Silence
The sibling
NO_REPLY primitive in the same silence.py file.Messaging Bots
Where bot inbound flow originates.
Loop Guard
Per-turn tool-call guard — a different layer inside one agent.
Gateway
Channel configuration where the guard sits.

