Five stop-condition guards. Prevent infinite loops and runaway API costs in production.
# This runs until YOUR money runs out
while not agent.is_done():
agent.step() # no guard = no ceilingThe ReAct loop is elegant in a notebook. In production, it's a ticking clock. "Done" is defined by the LLM — and a confused model loops forever.
| Failure Mode | What Happens | Real Cost |
|---|---|---|
| Hard loop | Same action, forever | $10–$500+ per stuck task |
| Semantic loop | Different words, same dead end | Silent budget burn |
| Retry storm | Broken tool retried 80× | 80× wasted API calls |
| Scope creep | Unbounded search expands forever | Hours of compute, no output |
flowchart LR
A[Agent Step] --> B[MaxStepsGuard]
B --> C[CostCeilingGuard]
C --> D[LoopDetectionGuard]
D --> E[TimeoutGuard]
E --> F[ProgressGuard]
F --> G{Any fired?}
G -->|no| H[Continue]
G -->|yes| I[STOP + reason\n+ final answer]
| Guard | Stops | Key Config |
|---|---|---|
MaxStepsGuard |
Hard step ceiling | max_steps=50 |
CostCeilingGuard |
Token spend ceiling | max_cost_usd=1.00 |
LoopDetectionGuard |
Repeated action-observation pairs | window=5, min_repeats=2 |
TimeoutGuard |
Wall-clock time limit | max_seconds=120 |
ProgressGuard |
Stalled / non-improving agent | stall_threshold=3 |
git clone https://github.com/darshjme/sentinel
cd sentinel && pip install -e .from react_guards import GuardedReActAgent, StepOutput
from react_guards.guards import (
MaxStepsGuard, CostCeilingGuard, LoopDetectionGuard,
TimeoutGuard, ProgressGuard, AgentState,
)
def my_agent_step(task: str, state: AgentState) -> StepOutput:
response = call_your_llm(task, state)
return StepOutput(
action=response.action,
observation=response.observation,
is_done=response.finished,
final_answer=response.answer,
input_tokens=response.usage.input,
output_tokens=response.usage.output,
)
agent = GuardedReActAgent(
agent_fn=my_agent_step,
guards=[
MaxStepsGuard(max_steps=50),
CostCeilingGuard(max_cost_usd=1.00),
LoopDetectionGuard(window=5),
TimeoutGuard(max_seconds=120),
ProgressGuard(stall_threshold=3),
],
)
result = agent.run("Research the latest advances in fusion energy")
print(f"Stopped by: {result.stopped_by}") # "agent_done" or guard name
print(f"Steps: {result.steps_taken} | Cost: ${result.total_cost_usd:.4f}")sequenceDiagram
participant A as Agent
participant L as LoopDetectionGuard
participant R as Runner
A->>R: Step 1: search("fusion energy")
R->>L: check(action, observation)
L-->>R: continue
A->>R: Step 2: search("fusion energy")
R->>L: check(action, observation)
L-->>R: continue (1st repeat)
A->>R: Step 3: search("fusion energy")
R->>L: check(action, observation)
L-->>R: STOP — repeated 2x in window=5
R-->>A: GuardTriggered: LoopDetectionGuard
- Zero dependencies — pure Python stdlib. Drops into any stack.
- Composable — use one guard or all five. Order doesn't matter.
- Stateless between runs —
reset()called automatically on eachagent.run(). - Protocol-based — implement
should_stop / reason / resetto build custom guards. - Fail-safe — guards never raise; they return
bool.
Guard overhead: < 1ms per step. The bottleneck is always your LLM call.
verdict · sentinel · herald · engram · arsenal
| Repo | Purpose |
|---|---|
| verdict | Score your agents |
| sentinel | ← you are here |
| herald | Semantic task router |
| engram | Agent memory |
| arsenal | The full pipeline |
MIT © Darshankumar Joshi · Built as part of the Arsenal toolkit.