alibaba/spring-ai-alibaba · error · IllegalStateException
Message before ToolResponseMessage is not an…
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.
Solutions
- Keep the AssistantMessage with tool calls directly before every ToolResponseMessage
- Fix message pruning/compaction so an AssistantMessage always precedes a ToolResponseMessage
- Sanitize history: merge consecutive ToolResponseMessages under one AssistantMessage
- 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
- Never interleave UserMessage between AssistantMessage and its ToolResponseMessage
- Merge consecutive tool-response rounds under their issuing AssistantMessage
- Audit compaction logic to preserve AssistantMessage+ToolResponseMessage pairs
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
- Cannot find AssistantMessage before ToolResponseMessage
- Last message is neither an AssistantMessage nor a…
- Cannot merge incompatible types
- Either 'instruction' or 'includeContents' must be set for…
- 数据集版本数据量为0或不存在
AI-assisted analysis of alibaba/spring-ai-alibaba@f82da0b50f (2026-09-09).
Data as JSON: /api/errors/475167c264186d0f.
Report an issue: GitHub.
Appendix: 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)