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
- Call mergeAll() exactly once, store and reuse the returned map.
- Create a new ToolStateCollector for each batch of tool calls.
- Wrap mergeAll() in an idempotent helper that caches the result.
- 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
- Call mergeAll exactly once and cache the result
- Never retry mergeAll; retry the tool round instead
- Use fresh collectors per batch
- Keep merge logic in one orchestration method
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
- Cannot create update map after mergeAll() has been called
- Shell session not initialized. Call initialize() before exec
- Default Scheduled Agent Manager is shut down
- Schedule already started
- ConfigAgentWatcher is already started
AI-assisted analysis of alibaba/spring-ai-alibaba@f82da0b50f (2026-09-09).
Data as JSON: /api/errors/b28cc5a2e422d3b3.
Report an issue: GitHub.