React Loop Guard
LLMs aren't perfect. They go silent, give half-answers, or spin in circles. The Guard makes the agent recover on its own — instead of looping forever or calling it done too early.
What is a React Loop
ReAct (Reasoning + Acting) is the core working pattern of an agent: the LLM reasons → calls a tool → observes the result → reasons again, looping until the task is done. This "think-act" cycle is the React Loop.
The React Loop Guard protects this cycle — when the LLM produces abnormal output mid-loop, the Guard detects and handles it automatically, preventing infinite loops or premature termination.
The Problem
Agent loops depend on the LLM producing valid responses turn after turn. In practice, LLMs sometimes:
- Spin — output internal reasoning but never call a tool or give an answer, like thinking forever without acting
- Go silent — return completely empty, saying nothing at all
- Quit early — call a bunch of tools, then wrap up with a one-liner that doesn't actually answer the question
Without a Guard, these either burn tokens in infinite loops or silently fail — you think it's done, but it isn't.
We hit these problems again and again across real-world vertical agent deployments. The Guard is the result — not a single hard-coded rule, but a layered, configurable, trait-based defense system refined through production use.
Four Lines of Defense
The Guard automatically detects anomalies after every LLM response. No manual checks needed:
| Anomaly | What happened | What the Guard does |
|---|---|---|
| reasoning-only | LLM keeps thinking, never acts | Injects a nudge to decide; stops after 3 strikes |
| empty response | LLM says nothing at all | Injects a nudge to retry; stops after 3 strikes |
| text-only | LLM gives a plain text answer | Ends immediately, trusts the model |
| text-only after tools | Short answer after calling tools | LLM judge steps in — is the task actually done? |
The first three are basic protection. The fourth is the key — the judge mechanism.
Judge: Using LLM to Supervise LLM
When the agent returns plain text after calling tools, the Guard doesn't just trust it. It calls another LLM as a referee:
- Judge says done → loop ends
- Judge says not done → reason is injected, agent keeps working
This is fully transparent to the user, with zero extra configuration. The judge itself is carefully optimized — short response detection, long response skip, large input fallback — balancing safety with token efficiency.
Configuration
use agent_works::guard::{DefaultGuard, DefaultGuardConfig};
// Works out of the box (recommended)
let guard = DefaultGuard::with_llm_client(
DefaultGuardConfig::default(),
llm_client,
);
builder = builder.guard(guard);
| Parameter | Default | Tuning tip |
|---|---|---|
use_llm_judge |
true |
Turning off saves tokens but risks premature completion |
judge_skip_threshold |
256 |
Skip judge for long responses. Higher = stricter, lower = saves more |
judge_fail_open |
false |
false = safer, extra round on failure; true = trust model if judge fails |
judge_timeout_secs |
10 |
Judge timeout ceiling |
detect_short_response |
true |
Long question + short answer = likely incomplete, auto-nudge |
reasoning_only_max_strikes |
3 |
How many spins before giving up |
empty_response_max_strikes |
3 |
How many silences before giving up |
Fault Tolerance: Fail-Closed vs Fail-Open
The judge defaults to fail-closed: when the judge times out or errors, the model is not trusted and the agent is forced to continue.
This is a lesson from production — for long-running tasks, the cost of finishing early far outweighs the cost of one extra round. Better to confirm than to miss something.
For simpler tasks or unstable judges, switch to fail-open:
Disabling the Judge
Don't pass an LLM client to disable the judge entirely. Good for latency-sensitive scenarios where you'd rather risk premature completion than add overhead:
Extending
The Guard is built on a trait, supporting full custom implementations or decorator-style enhancement. See the agent-base design docs for details.