alibaba/spring-ai-alibaba · error · IllegalStateException

Cannot find AssistantMessage before ToolResponseMessage

Error message

Cannot find AssistantMessage before ToolResponseMessage

What it means

handlePartialToolResponses() expects the message history to end with [AssistantMessage(toolCalls), ToolResponseMessage(partial)] — at least two messages. When the list is shorter than 2, the AssistantMessage carrying the tool calls is missing and IllegalStateException is thrown.

Source

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

		updatedState.putAll(stateCollector.mergeAll());

		if (enableActingLog) {
			logger.info("[ThreadId {}] Agent {} parallel tool execution completed. {} tools, {} failures.",
					config.threadId().orElse(THREAD_ID_DEFAULT), agentName, toolCalls.size(), failures.size());
		}

		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();

View on GitHub (pinned to f82da0b50f)

Solutions

  1. Always keep the originating AssistantMessage (with the tool calls) in the messages list before the ToolResponseMessage
  2. When resuming from an interrupt, restore the full message history rather than just the new response
  3. Check state-saving/compaction config so the last two messages are never truncated

Example fix

// before
messages = List.of(toolResponse); // size 1 -> throws
// after
messages = List.of(assistantMessageWithToolCalls, toolResponse);
Defensive patterns

Strategy: validation

Validate before calling

boolean partialResponseContextValid(List<Message> msgs) { return msgs.size() >= 2 && msgs.get(msgs.size()-1) instanceof ToolResponseMessage; }

Type guard

boolean hasAssistantBeforeToolResponse(List<Message> msgs) { return msgs.size() >= 2 && msgs.get(msgs.size()-2) instanceof AssistantMessage; }

Try / catch

try { resume(state); } catch (IllegalStateException e) { reloadFullHistoryFromCheckpoint(); resume(state); }

Prevention

When it happens

Trigger: A ToolResponseMessage reaches the tool node as the only message (messages.size() < 2), e.g. resuming with only the tool response appended or constructing state with the response alone.

Common situations: Human-in-the-loop resume that appends only the ToolResponseMessage; custom state restoration dropping older messages; feeding the tool node directly with a tool response in tests.

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/d54fd67aead8632c. Report an issue: GitHub.