alibaba/spring-ai-alibaba · error · IllegalStateException

mergeAll() can only be called once

Error message

mergeAll() can only be called once

What it means

ToolStateCollector.mergeAll() is one-shot: it uses merged.compareAndSet(false, true) and throws IllegalStateException "mergeAll() can only be called once" on any second invocation. This prevents double-merging tool updates into agent state.

Source

Thrown at spring-ai-alibaba-agent-framework/src/main/java/com/alibaba/cloud/ai/graph/agent/tool/ToolStateCollector.java:131

	public void discardToolUpdateMap(int index) {
		toolUpdatesByIndex.remove(index);
	}

	/**
	 * Merges all tool updates in original index order (0, 1, 2, ...). This ensures
	 * deterministic results regardless of completion order.
	 *
	 * <p>
	 * <b>Note:</b> This method can only be called once. Subsequent calls will throw an
	 * {@link IllegalStateException}. This ensures that merging happens only after all
	 * tools have completed their execution.
	 * </p>
	 * @return the merged state updates
	 * @throws IllegalStateException if called more than once
	 */
	public Map<String, Object> mergeAll() {
		if (!merged.compareAndSet(false, true)) {
			throw new IllegalStateException("mergeAll() can only be called once");
		}

		Map<String, Object> result = new ConcurrentHashMap<>();

		for (int i = 0; i < totalTools; i++) {
			Map<String, Object> toolUpdate = toolUpdatesByIndex.get(i);
			if (toolUpdate == null || toolUpdate.isEmpty()) {
				continue;
			}

			for (Map.Entry<String, Object> entry : toolUpdate.entrySet()) {
				String key = entry.getKey();
				Object newValue = entry.getValue();
				Object existingValue = result.get(key);

				if (existingValue == null) {
					result.put(key, newValue);
				}

View on GitHub (pinned to f82da0b50f)

Solutions

  1. Call mergeAll() exactly once, store and reuse the returned map.
  2. Create a new ToolStateCollector for each batch of tool calls.
  3. Wrap mergeAll() in an idempotent helper that caches the result.
  4. Remove retry logic that re-invokes mergeAll; retry the tools instead on a fresh collector.

Example fix

// before
var r1 = collector.mergeAll();
var r2 = collector.mergeAll(); // throws
// after
var r1 = collector.mergeAll();
// reuse r1 instead of merging again
Defensive patterns

Strategy: validation

Validate before calling

// idempotent wrapper
Map<String,Object> merged; boolean done;
Map<String,Object> mergeOnce(ToolStateCollector c) { return done ? merged : (merged = c.mergeAll()); }

Try / catch

try { return collector.mergeAll(); } catch (IllegalStateException e) { return previouslyMerged; }

Prevention

When it happens

Trigger: Calling mergeAll() twice on the same collector instance — e.g. retry logic calling it again after a partial failure, or code paths that both finalize state and re-merge.

Common situations: Retry wrappers around tool execution, accidental double invocation in orchestration code, tests that call mergeAll per assertion.

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 alibaba/spring-ai-alibaba@f82da0b50f (2026-09-09). Data as JSON: /api/errors/b28cc5a2e422d3b3. Report an issue: GitHub.