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

  1. Inspect the cause (lastException) for the root failure and fix it
  2. Increase maxAttempts or backoff delay for slow-recovering dependencies
  3. Narrow retryOn to retry only transient exceptions
  4. 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

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


AI-assisted analysis of alibaba/spring-ai-alibaba@f82da0b50f (2026-09-09). Data as JSON: /api/errors/346bec415c41d435. Report an issue: GitHub.