alibaba/spring-ai-alibaba · critical · RuntimeException

Less than 2 messages in state when last message is ToolRespo

Error message

Less than 2 messages in state when last message is ToolResponseMessage

What it means

In ReactAgent.makeModelToTools routing (set up by setupToolRouting), when the last message in state is a ToolResponseMessage the code expects the preceding AssistantMessage tool-call message to exist. If fewer than 2 messages are present it throws RuntimeException, as this indicates corrupted state that cannot occur in a valid ReAct loop.

Source

Thrown at spring-ai-alibaba-agent-framework/src/main/java/com/alibaba/cloud/ai/graph/agent/ReactAgent.java:805

			if (messages.isEmpty()) {
				logger.warn("No messages found in state when routing from model to tools");
				return endDestination;
			}
			Message lastMessage = messages.get(messages.size() - 1);

			// 1. Check the last message type
			if (lastMessage instanceof AssistantMessage assistantMessage) {
				// 2. If last message is AssistantMessage
				if (assistantMessage.hasToolCalls()) {
					return AGENT_TOOL_NAME;
				} else {
					return endDestination;
				}
			} else if (lastMessage instanceof ToolResponseMessage) {
				// 3. If last message is ToolResponseMessage
				if (messages.size() < 2) {
					// Should not happen in a valid ReAct loop, but as a safeguard.
					throw new RuntimeException("Less than 2 messages in state when last message is ToolResponseMessage");
				}

				Message secondLastMessage = messages.get(messages.size() - 2);
				if (secondLastMessage instanceof AssistantMessage) {
					AssistantMessage assistantMessage = (AssistantMessage) secondLastMessage;
					ToolResponseMessage toolResponseMessage = (ToolResponseMessage) lastMessage;

					if (assistantMessage.hasToolCalls()) {
						Set<String> requestedToolIds = assistantMessage.getToolCalls().stream()
								.map(AssistantMessage.ToolCall::id)
								.collect(java.util.stream.Collectors.toSet());

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

						if (executedToolIds.containsAll(requestedToolIds)) {
							return modelDestination; // All requested tools were executed or responded

View on GitHub (pinned to f82da0b50f)

Solutions

  1. Fix the state so a ToolResponseMessage is always preceded by the AssistantMessage containing the matching ToolCall.
  2. Do not manually trim messages to fewer than the assistant-call + tool-response pair when resuming checkpoints.
  3. Check the saver/checkpoint implementation for message truncation (e.g. max-message limits cutting the pair in half).
  4. Guard resume paths: validate the message tail before feeding state into the agent.

Example fix

// before
state.put("messages", List.of(toolResponse));
// after
state.put("messages", List.of(assistantWithToolCall, toolResponse));
Defensive patterns

Strategy: validation

Validate before calling

List<Message> msgs = (List<Message>) state.get("messages");
Message last = msgs.get(msgs.size() - 1);
if (last instanceof ToolResponseMessage && (msgs.size() < 2 || !(msgs.get(msgs.size() - 2) instanceof AssistantMessage)))
    throw new IllegalStateException("Invalid message tail: ToolResponseMessage without preceding AssistantMessage");

Type guard

static boolean validMessageTail(List<Message> m) {
    if (m == null || m.isEmpty()) return true;
    return !(m.get(m.size() - 1) instanceof ToolResponseMessage)
        || (m.size() >= 2 && m.get(m.size() - 2) instanceof AssistantMessage);
}

Try / catch

try { graph.resume(state); } catch (RuntimeException e) { if (e.getMessage().contains("Less than 2 messages")) { log.error("Corrupt checkpoint state: rebuild from last valid checkpoint"); } throw e; }

Prevention

When it happens

Trigger: Resuming a graph/agent from a persisted checkpoint where the message list was truncated or hand-crafted to end with a ToolResponseMessage without its AssistantMessage tool-call predecessor.

Common situations: Manually seeding conversation state (e.g. only a tool response for testing); checkpoint/serialization bugs dropping earlier messages; state surgery that removes the assistant tool-call message.

Understand the failure class

Background: "This is a bug, please report it": internal invariant violations, unreachable panics, and SNH errors explained — this error's family across 47 libraries.

Related errors


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