Skip to main content
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.
from praisonaiagents import Agent
from praisonaiagents.bots import BotLoopGuard, BotLoopPolicy

agent = Agent(
    name="Group Helper",
    instructions="Answer questions posted in the room.",
)

guard = BotLoopGuard(BotLoopPolicy(max_events_per_window=20, window_seconds=60))

# In your bot inbound handler, before dispatching to the agent:
if sender_is_bot and not guard.observe(
    self_bot_id="mybot",
    sender_bot_id=inbound_sender_id,
):
    return  # drop the turn — pair is over budget / in cooldown

agent.start(inbound_text)
Two bots ping-pong replies; the guard counts each exchange and, once a pair passes its budget, drops the turn and opens a cooldown.
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

1

Enable with defaults

Call observe() for each bot-authored inbound turn and drop the turn when it returns False.
from praisonaiagents.bots import BotLoopGuard

guard = BotLoopGuard()  # default policy: 20 exchanges / 60s window / 60s cooldown

if not guard.observe(self_bot_id="mybot", sender_bot_id="otherbot"):
    return  # drop the turn — loop budget hit
2

Tune the budget with BotLoopPolicy

Pass a BotLoopPolicy to change how quickly a loop trips and how long it stays quiet.
from praisonaiagents.bots import BotLoopGuard, BotLoopPolicy

guard = BotLoopGuard(BotLoopPolicy(
    max_events_per_window=10,
    window_seconds=30.0,
    cooldown_seconds=120.0,
))
3

Build the policy from YAML/kwargs

BotLoopPolicy.from_dict builds a policy from a config mapping and ignores unknown keys.
from praisonaiagents.bots import BotLoopGuard, BotLoopPolicy

policy = BotLoopPolicy.from_dict({
    "enabled": True,
    "max_events_per_window": 10,
    "window_seconds": 30.0,
    "cooldown_seconds": 120.0,
})
guard = BotLoopGuard(policy)

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.
BehaviourEffect
Direction independenceA↔B and B↔A share one budget
Sliding windowEvents older than window_seconds are evicted before the budget check
CooldownOnce exceeded, the pair is dropped for cooldown_seconds without recording
Distinct pairsA↔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.
FieldTypeDefaultDescription
enabledboolTrueWhen False, the guard is a transparent no-op (always allows).
max_events_per_windowint20Max bot-authored exchanges per pair before cooldown opens. Clamped to >= 1.
window_secondsfloat60.0Length of the sliding window.
cooldown_secondsfloat60.0How 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:
SymbolSignaturePurpose
ConstructorBotLoopGuard(policy: Optional[BotLoopPolicy] = None)Defaults to the enabled default policy; uses an internal lock for shared-state safety.
enabled@property -> boolWhether the guard actively suppresses loops.
observeobserve(*, self_bot_id: str, sender_bot_id: str, now: Optional[float] = None) -> boolRecords one exchange; returns True to allow or False to drop. Always True when disabled.
resetreset() -> NoneClears 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 call observe() only when the sender is a bot.
from praisonaiagents.bots import BotLoopGuard

guard = BotLoopGuard()

def on_inbound(msg, sender_is_bot):
    if sender_is_bot and not guard.observe(
        self_bot_id="mybot", sender_bot_id=msg.sender_id
    ):
        return  # loop budget hit — drop silently
    handle(msg)
Testing with an injected clock — pass explicit now= values for deterministic time-dependent tests.
from praisonaiagents.bots import BotLoopGuard, BotLoopPolicy

guard = BotLoopGuard(BotLoopPolicy(max_events_per_window=2, window_seconds=60.0))
assert guard.observe(self_bot_id="a", sender_bot_id="b", now=0.0)
assert guard.observe(self_bot_id="a", sender_bot_id="b", now=1.0)
assert not guard.observe(self_bot_id="a", sender_bot_id="b", now=2.0)
Resetting on gateway restart — call reset() after redeploy so leftover cooldowns from an earlier process don’t leak.
guard.reset()  # clear tracked pairs and cooldowns

When It Fires vs Doesn’t

Only bot-authored inbound turns reach the guard; humans and single-bot deployments never call observe().
Inbound senderBelow budgetAbove budget in cooldownAfter cooldown expires
HumanNot observed (always allowed)Not observedNot observed
Bot (different pair)AllowedAllowed (independent pair)Allowed
Bot (same pair)AllowedDroppedAllowed (fresh window)

Best Practices

Skip observe() when the sender is human — the guard is zero-cost on the common path.
Use a shorter window_seconds with a higher cooldown_seconds for chatty rooms — the window sizes urgency, the cooldown sizes recovery.
max_events_per_window=0 is clamped to 1 and hides intent. Set enabled=False to turn the guard off.
The deque/dict state is per-instance and per-pair, so a single shared BotLoopGuard tracks the whole gateway.
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.

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.