alibaba/spring-ai-alibaba · error · IllegalStateException

Message before ToolResponseMessage is not an AssistantMessag

Error message

Message before ToolResponseMessage is not an AssistantMessage

What it means

When handling partial tool responses, handlePartialToolResponses() requires the message immediately before the ToolResponseMessage to be the AssistantMessage that issued the tool calls. If the second-to-last message is any other type (UserMessage, ToolResponseMessage, etc.), it throws IllegalStateException.

Source

Thrown at spring-ai-alibaba-agent-framework/src/main/java/com/alibaba/cloud/ai/graph/agent/node/AgentToolNode.java:419

		}

		return updatedState;
	}

	/**
	 * Handle partial tool responses (ToolResponseMessage branch). Supports both parallel
	 * and sequential execution of remaining tools.
	 */
	private Map<String, Object> handlePartialToolResponses(ToolResponseMessage toolResponseMessage,
			List<Message> messages, OverAllState state, RunnableConfig config) {

		if (messages.size() < 2) {
			throw new IllegalStateException("Cannot find AssistantMessage before ToolResponseMessage");
		}

		Message secondLastMessage = messages.get(messages.size() - 2);
		if (!(secondLastMessage instanceof AssistantMessage assistantMessage)) {
			throw new IllegalStateException("Message before ToolResponseMessage is not an AssistantMessage");
		}

		List<ToolResponseMessage.ToolResponse> existingResponses = toolResponseMessage.getResponses();
		Set<String> executedToolIds = existingResponses.stream()
			.map(ToolResponseMessage.ToolResponse::id)
			.collect(Collectors.toSet());

		// Filter out tools that haven't been executed yet
		List<AssistantMessage.ToolCall> remainingToolCalls = assistantMessage.getToolCalls()
			.stream()
			.filter(tc -> !executedToolIds.contains(tc.id()))
			.toList();

		if (remainingToolCalls.isEmpty()) {
			// All tools have been executed - return empty map to avoid duplicate append
			// (toolResponseMessage is already in the messages list)
			return Map.of();
		}

View on GitHub (pinned to f82da0b50f)

Solutions

  1. Keep the AssistantMessage with tool calls directly before every ToolResponseMessage
  2. Fix message pruning/compaction so an AssistantMessage always precedes a ToolResponseMessage
  3. Sanitize history: merge consecutive ToolResponseMessages under one AssistantMessage
  4. Log messages.get(size-2).getClass() to see what is actually second-to-last

Example fix

// before
List.of(toolResponse1, toolResponse2); // ToolResponseMessage precedes ToolResponseMessage
// after
List.of(assistantWithCalls, toolResponse1, toolResponse2);
Defensive patterns

Strategy: validation

Validate before calling

boolean sequenceValid(List<Message> msgs) { int i = msgs.size()-1; return i > 0 && msgs.get(i) instanceof ToolResponseMessage && msgs.get(i-1) instanceof AssistantMessage; }

Type guard

Message requireAssistantBefore(List<Message> msgs) { Message m = msgs.get(msgs.size()-2); if (!(m instanceof AssistantMessage a)) throw new IllegalStateException("expected AssistantMessage, got " + m.getClass()); return a; }

Try / catch

try { toolNode.apply(state, config); } catch (IllegalStateException e) { sanitizeHistory(state); retryOnce(); }

Prevention

When it happens

Trigger: Message history where a ToolResponseMessage is preceded by another ToolResponseMessage, a UserMessage, or a SystemMessage — e.g. two consecutive tool-response rounds with the assistant reply removed, or hand-assembled message lists.

Common situations: Manual message-list manipulation that interleaves responses; multiple parallel tool rounds where only responses were kept; prompt-compaction that kept ToolResponseMessage but dropped the AssistantMessage before it.

Understand the failure class

Background: Type mismatch errors: IllegalArgumentException, TypeError and type guards across 150 open-source libraries — this error's family across 150 libraries.

Related errors


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