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 respondedView on GitHub (pinned to f82da0b50f)
Solutions
- Fix the state so a ToolResponseMessage is always preceded by the AssistantMessage containing the matching ToolCall.
- Do not manually trim messages to fewer than the assistant-call + tool-response pair when resuming checkpoints.
- Check the saver/checkpoint implementation for message truncation (e.g. max-message limits cutting the pair in half).
- 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
- Never hand-edit or truncate persisted message state.
- Verify checkpoint savers don't trim assistant tool-call messages.
- Validate message-tail invariants before resuming from any checkpoint.
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
- Thread '%s' not found or already released
- Cannot append to non-list value for key: {item.targetKey()}
- 会话 {} 不存在,创建新会话
- Failed to execute agent tool '%s' (parentThreadId=%s, input=
AI-assisted analysis of alibaba/spring-ai-alibaba@f82da0b50f (2026-09-09).
Data as JSON: /api/errors/edb460eadc5ad4c3.
Report an issue: GitHub.