Retryable vs. deterministic errors: why your agent's stop condition needs both
A loop breaker that halts an agent after N identical failures sounds like a complete answer. It isn't, because it treats a rate limit and a malformed argument as the same event. They aren't — one resolves itself if you wait, the other resolves itself never.
Count them the same way and you get two failure modes at once: a run that hits a provider having a rough five minutes gets killed mid-recovery, while a run stuck sending the same broken payload gets to burn tokens for three more turns than it should have. The fix isn't a smarter threshold. It's recognizing that "the call failed" is not one signal — it's at least three.
The question that actually matters
Not "did this call fail," but "does retrying this call have any realistic chance of succeeding, or will it fail identically forever?" A 429 answers that question one way. A 422 answers it the other way. A generic "error" string with no further detail doesn't answer it at all — and that non-answer is itself useful information, not noise to be forced into a bucket.
Deterministic
halt at 2 in a row400 Bad Request, 422 validation error, malformed tool arguments, 401/403/404
The call fails because of what's in the request, not what's happening on the other end. Retrying with the exact same arguments produces the exact same failure, every time — there is no number of attempts that fixes a payload that was wrong to begin with. Two identical failures in a row is already enough evidence: the agent needs to change what it's sending, not send it again.
Unknown
halt at 3 in a rowAn error string with no clear shape — 'something went wrong', a stack trace with no status code, a tool result that just says 'error' with no further detail
Not every failure announces which bucket it belongs in. A generic error message from an unfamiliar tool could be a transient glitch or a hard failure — there's no reliable way to tell from the content alone. Forcing an ambiguous failure into either bucket is a guess; treating it as its own category, with a threshold between the other two, is honest about the uncertainty without being reckless about it.
Retryable
halt at 5 in a row408 timeout, 429 rate limit, 500/502/503/504, connection reset, "service temporarily unavailable"
The call failed because of a condition that changes on its own — a provider having a bad five minutes, a rate limit window that resets, a connection that drops once and works the next time. Backoff-and-retry is the textbook correct response to this category, not a symptom of a stuck agent. A stop condition that can't tell this apart from a genuine loop will kill runs that were one retry away from succeeding.
Why the thresholds aren't the same number
A deterministic failure repeating twice is already damning — nothing about a third identical attempt is going to teach the agent something the first two didn't. A retryable failure repeating five times is a different story: rate limits have windows, providers recover, connections drop and reconnect. Giving retryable failures more room before they count as "stuck" isn't leniency, it's matching the stop condition to how the failure actually behaves.
The unknown bucket sits in between on purpose. Treating every unclassifiable failure as automatically retryable means a genuinely broken call gets five free attempts instead of two. Treating it as automatically deterministic means a transient glitch with an unfamiliar error format gets killed after two attempts it might have recovered from on the third. Neither assumption is safe, so the middle threshold isn't a compromise — it's the correct response to genuine uncertainty.
What this looks like in Cognocient
The Failure Loop Breaker classifies every failed call this way before it ever counts toward a halt decision — a status code or error signature feedsclassify_failure(), and only a genuinely stuck pattern trips enforcement. A run legitimately backing off through a provider outage keeps running. A run sending the same malformed tool call twice in a row gets stopped before it burns a third turn on it. See the full Failure Loop Breaker reference for the enforcement modes, the exact thresholds, and how this interacts with budget and velocity limits.
If you're building your own stop condition rather than using ours, the takeaway travels on its own: don't count failures. Classify them first, and count separately.