spring-projects/spring-ai · error · IllegalStateException

Cannot set both delegate and toolObject

Error message

Cannot set both delegate and toolObject

What it means

build() enforces that delegate (a ToolCallback/ToolCallbackProvider delegate) and toolObject (an object with @Tool methods) are mutually exclusive sources for the provider. Setting both would create ambiguity about which source provides the tools, so the builder rejects it with an IllegalStateException.

Source

Thrown at spring-ai-model/src/main/java/org/springframework/ai/tool/augment/AugmentedToolCallbackProvider.java:154

			this.removeExtraArgumentsAfterProcessing = removeExtraArgumentsAfterProcessing;
			return this;
		}

		/**
		 * Builds the {@link AugmentedToolCallbackProvider} instance.
		 * @return the built instance
		 * @throws IllegalStateException if required fields are not set
		 */
		public AugmentedToolCallbackProvider<T> build() {
			if (this.argumentType == null) {
				throw new IllegalStateException("argumentType is required");
			}
			if (this.argumentConsumer == null) {
				throw new IllegalStateException("argumentConsumer is required");
			}

			if (this.delegate != null && this.toolObject != null) {
				throw new IllegalStateException("Cannot set both delegate and toolObject");
			}

			if (this.delegate == null && this.toolObject == null) {
				throw new IllegalStateException("Either delegate or toolObject must be set");
			}

			if (this.toolObject != null) {
				return new AugmentedToolCallbackProvider<>(this.toolObject, this.argumentType, this.argumentConsumer,
						this.removeExtraArgumentsAfterProcessing);
			}
			else if (this.delegate != null) { // Redundant if condition to please NullAway
				return new AugmentedToolCallbackProvider<>(this.delegate, this.argumentType, this.argumentConsumer,
						this.removeExtraArgumentsAfterProcessing);
			}
			else {
				throw new IllegalStateException();
			}
		}

View on GitHub (pinned to 98a7beda4f)

Solutions

  1. Remove either .delegate(...) or .toolObject(...) from the builder chain, keeping only the intended source.
  2. If you need to combine both kinds of tools, create two AugmentedToolCallbackProvider instances and merge their ToolCallbacks.
  3. Audit shared builder helper methods so they only set one source.

Example fix

// before
var provider = AugmentedToolCallbackProvider.builder()
    .delegate(wrappedCallbacks)
    .toolObject(new MyTools())
    .argumentType(Args.class)
    .argumentConsumer(a -> a)
    .build();
// after
var provider = AugmentedToolCallbackProvider.builder()
    .toolObject(new MyTools())
    .argumentType(Args.class)
    .argumentConsumer(a -> a)
    .build();
Defensive patterns

Strategy: validation

Validate before calling

boolean hasDelegate = delegate != null;
boolean hasToolObject = toolObject != null;
if (hasDelegate == hasToolObject && !hasDelegate) { /* neither */ }
if (hasDelegate && hasToolObject) {
    throw new IllegalArgumentException("Set only one of delegate or toolObject");
}

Try / catch

try { provider = builder.build(); } catch (IllegalStateException e) { throw new IllegalArgumentException("Invalid tool source configuration: " + e.getMessage(), e); }

Prevention

When it happens

Trigger: Calling builder().delegate(delegateProvider).toolObject(myToolObject)...build(); typically when migrating code from the toolObject style to the delegate style and forgetting to remove the old setter.

Common situations: Refactoring from @Tool-annotated objects to wrapping existing ToolCallbacks; copy-pasted builder chains where both setters appear; fluent chains reused across two provider configurations.

Related errors


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