RetryPolicy.max_attempts is documented as the total attempt count including the first, aag/error_recovery/policies.py:9:
max_attempts: int = 3 # Total attempts (including the first)
But the block-level retry loop iterates range(max_attempts + 1), aag/error_recovery/error_manager.py:75:
for attempt in range(max_attempts+1):
try:
result = await operation(error_history)
if attempt > 1:
logger.info("✅ %s succeeded on attempt %d/%d", name, attempt, max_attempts)
return result
except Exception as e:
...
if attempt < max_attempts:
logger.warning("⚠️ %s failed on attempt %d/%d: %s", name, attempt, max_attempts, err["error"])
else:
logger.error("❌ %s failed after %d attempts", name, max_attempts)
With max_attempts = 3, range(3 + 1) yields attempt = 0, 1, 2, 3 — the operation runs four times, not three.
Failure scenario. Every hard failure in an operation wrapped by run() (LLM-driven code generation, parameter extraction, dependency resolution) performs one more attempt than configured. Since each attempt is typically an LLM call plus an exec, this is a real cost and latency regression on exactly the slow, expensive path. The error log "failed after %d attempts" also prints max_attempts (3) while 4 calls actually happened, so the logs disagree with reality.
Two smaller correctness slips in the same block:
if attempt > 1 means a success on the second attempt (attempt == 1) never logs the "succeeded on attempt" line.
- The final failure branch logs
max_attempts rather than attempt + 1, undercounting.
Fix. Use for attempt in range(max_attempts): and gate the success log on attempt >= 1 (or attempt > 0). Then the "including the first" contract in policies.py holds.
RetryPolicy.max_attemptsis documented as the total attempt count including the first,aag/error_recovery/policies.py:9:But the block-level retry loop iterates
range(max_attempts + 1),aag/error_recovery/error_manager.py:75:With
max_attempts = 3,range(3 + 1)yieldsattempt = 0, 1, 2, 3— the operation runs four times, not three.Failure scenario. Every hard failure in an operation wrapped by
run()(LLM-driven code generation, parameter extraction, dependency resolution) performs one more attempt than configured. Since each attempt is typically an LLM call plus anexec, this is a real cost and latency regression on exactly the slow, expensive path. The error log"failed after %d attempts"also printsmax_attempts(3) while 4 calls actually happened, so the logs disagree with reality.Two smaller correctness slips in the same block:
if attempt > 1means a success on the second attempt (attempt == 1) never logs the "succeeded on attempt" line.max_attemptsrather thanattempt + 1, undercounting.Fix. Use
for attempt in range(max_attempts):and gate the success log onattempt >= 1(orattempt > 0). Then the "including the first" contract inpolicies.pyholds.