spring-projects/spring-ai · error · IllegalStateException

At most one ToolAdvisor is allowed in the advisor chain, but

Error message

At most one ToolAdvisor is allowed in the advisor chain, but found 

What it means

DefaultChatClient validates during build (validateSingleToolAdvisor) that the advisor chain contains at most one ToolAdvisor. Having two or more would apply tool-calling advice twice, so it fails fast with an IllegalStateException listing the duplicate advisors' names and order values.

Source

Thrown at spring-ai-client-chat/src/main/java/org/springframework/ai/chat/client/DefaultChatClient.java:1245

				return;
			}

			int configuredOrder = this.toolCallingAdvisorBuilder.getAdvisorOrder();

			boolean hasDownstreamMemoryAdvisor = this.advisors.stream()
				.anyMatch(a -> a instanceof MemoryAdvisor && a.getOrder() > configuredOrder);

			this.advisors.add(this.toolCallingAdvisorBuilder.copy()
				.conversationHistoryEnabled(!hasDownstreamMemoryAdvisor)
				.build());
		}

		private void validateSingleToolAdvisor() {
			List<Advisor> toolAdvisors = this.advisors.stream().filter(a -> a instanceof ToolAdvisor).toList();
			if (toolAdvisors.size() > 1) {
				String names = String.join(", ",
						toolAdvisors.stream().map(a -> a.getName() + " (order=" + a.getOrder() + ")").toList());
				throw new IllegalStateException("At most one ToolAdvisor is allowed in the advisor chain, but found "
						+ toolAdvisors.size() + ": [" + names + "]");
			}
		}

	}

}

View on GitHub (pinned to 98a7beda4f)

Solutions

  1. Search your advisor configuration and register only one ToolAdvisor per ChatClient.
  2. If both advisors are needed, merge their tool-calling configuration into a single ToolAdvisor.
  3. Inspect the exception message's [names (order=...)] list to locate the exact duplicate registrations.

Example fix

// before
.defaultAdvisors(ToolAdvisor.builder().build(), ToolAdvisor.builder().build())
// after
.defaultAdvisors(ToolAdvisor.builder().build())
Defensive patterns

Strategy: validation

Validate before calling

List<Advisor> toolAdvisors = advisorList.stream().filter(a -> a instanceof ToolAdvisor).toList();
if (toolAdvisors.size() > 1) throw new IllegalStateException("only one ToolAdvisor allowed, got: " + toolAdvisors);

Try / catch

try { chatClient = builder.build(); } catch (IllegalStateException e) { if (e.getMessage().contains("ToolAdvisor")) { /* dedupe advisor config */ } throw e; }

Prevention

When it happens

Trigger: Calling .advisors(...) / .defaultAdvisors(...) with two instances of ToolAdvisor (or two ToolCallingManager-style advisors) on the same ChatClient before build().

Common situations: Copy-pasted advisor configuration, merging default advisors with per-request advisors where both add a ToolAdvisor, or upgrading/migrating code where a ToolAdvisor was added in two config places.

Understand the failure class

Background: "Invalid state transition" errors: "status must be X, actually Y", "already rejected/charging/uninstalled", "cannot ... while running" — what they mean when a library rejects your call — this error's family across 31 libraries.

Related errors


AI-assisted analysis of spring-projects/spring-ai@98a7beda4f (2026-09-11). Data as JSON: /api/errors/a7ea3b0cdb77e159. Report an issue: GitHub.