alibaba/spring-ai-alibaba · error · RuntimeException
Tool call failed after ${maxAttempts} attempts
Error message
Tool call failed after ${maxAttempts} attempts What it means
When all maxAttempts attempts fail and OnFailureBehavior is RAISE, ToolRetryInterceptor throws RuntimeException("Tool call failed after N attempts", lastException), preserving the final underlying exception as the cause. This signals retry exhaustion rather than a single transient failure.
Source
Thrown at spring-ai-alibaba-agent-framework/src/main/java/com/alibaba/cloud/ai/graph/agent/interceptor/toolretry/ToolRetryInterceptor.java:127
// Calculate delay
long delay = calculateDelay(attempt);
log.warn("Tool '{}' failed (attempt {}/{}), retrying in {}ms: {}",
toolName, attempt + 1, maxAttempts, delay, e.getMessage());
try {
Thread.sleep(delay);
}
catch (InterruptedException ie) {
Thread.currentThread().interrupt();
throw new RuntimeException("Retry interrupted", ie);
}
}
}
// All retries exhausted
if (onFailure == OnFailureBehavior.RAISE) {
throw new RuntimeException("Tool call failed after " + maxAttempts + " attempts", lastException);
}
else {
// Return error message as tool response
String errorMessage = errorFormatter != null
? errorFormatter.apply(lastException)
: "Tool call failed after " + maxAttempts + " attempts: " + lastException.getMessage();
log.error("Tool '{}' failed after {} attempts: {}", toolName, maxAttempts, lastException.getMessage());
return ToolCallResponse.of(request.getToolCallId(), request.getToolName(), errorMessage);
}
}
private long calculateDelay(int retryNumber) {
long delay = (long) (initialDelayMs * Math.pow(backoffFactor, retryNumber));
delay = Math.min(delay, maxDelayMs);
if (jitter) {
// Add random jitter ±25%View on GitHub (pinned to f82da0b50f)
Solutions
- Inspect the cause (lastException) for the root failure and fix it
- Increase maxAttempts or backoff delay for slow-recovering dependencies
- Narrow retryOn to retry only transient exceptions
- Switch onFailure to return the error message as a tool response instead of raising
Example fix
// before ToolRetryInterceptor.builder().maxAttempts(2).build(); // after ToolRetryInterceptor.builder().maxAttempts(5).backoffMultiplier(2.0).build();
Defensive patterns
Strategy: try-catch
Validate before calling
// pre-check the dependency is reachable before expensive retry loops // and size attempts for expected recovery time ToolRetryInterceptor.builder().maxAttempts(5).backoffMultiplier(2.0).retryOn(IOException.class::isInstance).build();
Try / catch
try { return interceptor.interceptToolCall(request, handler); } catch (RuntimeException e) { log.error("Tool {} failed after retries", request.getName(), e.getCause() != null ? e.getCause() : e); return ToolCallResponse.of("Tool temporarily unavailable"); } Prevention
- Always inspect the cause chain for the root error
- Tune maxAttempts/backoff to the dependency's recovery profile
- Retry only transient exceptions via retryOn
- Use OnFailureBehavior returning error messages for non-critical tools
When it happens
Trigger: A tool call throws (or returns non-success status) on every one of the maxAttempts attempts with retryOn matching each failure, and onFailure behavior is RAISE.
Common situations: Downstream service permanently down; invalid tool arguments failing deterministically; maxAttempts too low for a flaky dependency; retryOn predicate matching exceptions that will never succeed.
Understand the failure class
Background: "API request failed": what wrapped HTTP errors from external APIs mean and how to find the real cause — this error's family across 29 libraries.
Related errors
- ${response.getResult()}
- Tool '{}' failed (attempt {}/{}), retrying in {}ms: {}
- failed to create index
- Retry interrupted
- Model call failed (non-retryable exception)
AI-assisted analysis of alibaba/spring-ai-alibaba@f82da0b50f (2026-09-09).
Data as JSON: /api/errors/346bec415c41d435.
Report an issue: GitHub.