{"record":{"id":"475167c264186d0f","repo":"alibaba/spring-ai-alibaba","slug":"message-before-toolresponsemessage-is-not-an-assis","errorCode":null,"errorMessage":"Message before ToolResponseMessage is not an AssistantMessage","messagePattern":"Message before ToolResponseMessage is not an AssistantMessage","errorType":"exception","errorClass":"IllegalStateException","httpStatus":null,"severity":"error","filePath":"spring-ai-alibaba-agent-framework/src/main/java/com/alibaba/cloud/ai/graph/agent/node/AgentToolNode.java","lineNumber":419,"sourceCode":"\t\t}\n\n\t\treturn updatedState;\n\t}\n\n\t/**\n\t * Handle partial tool responses (ToolResponseMessage branch). Supports both parallel\n\t * and sequential execution of remaining tools.\n\t */\n\tprivate Map<String, Object> handlePartialToolResponses(ToolResponseMessage toolResponseMessage,\n\t\t\tList<Message> messages, OverAllState state, RunnableConfig config) {\n\n\t\tif (messages.size() < 2) {\n\t\t\tthrow new IllegalStateException(\"Cannot find AssistantMessage before ToolResponseMessage\");\n\t\t}\n\n\t\tMessage secondLastMessage = messages.get(messages.size() - 2);\n\t\tif (!(secondLastMessage instanceof AssistantMessage assistantMessage)) {\n\t\t\tthrow new IllegalStateException(\"Message before ToolResponseMessage is not an AssistantMessage\");\n\t\t}\n\n\t\tList<ToolResponseMessage.ToolResponse> existingResponses = toolResponseMessage.getResponses();\n\t\tSet<String> executedToolIds = existingResponses.stream()\n\t\t\t.map(ToolResponseMessage.ToolResponse::id)\n\t\t\t.collect(Collectors.toSet());\n\n\t\t// Filter out tools that haven't been executed yet\n\t\tList<AssistantMessage.ToolCall> remainingToolCalls = assistantMessage.getToolCalls()\n\t\t\t.stream()\n\t\t\t.filter(tc -> !executedToolIds.contains(tc.id()))\n\t\t\t.toList();\n\n\t\tif (remainingToolCalls.isEmpty()) {\n\t\t\t// All tools have been executed - return empty map to avoid duplicate append\n\t\t\t// (toolResponseMessage is already in the messages list)\n\t\t\treturn Map.of();\n\t\t}","sourceCodeStart":401,"sourceCodeEnd":437,"githubUrl":"https://github.com/alibaba/spring-ai-alibaba/blob/f82da0b50f35744c13968191be2b1cd2452ef550/spring-ai-alibaba-agent-framework/src/main/java/com/alibaba/cloud/ai/graph/agent/node/AgentToolNode.java#L401-L437","documentation":"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.","triggerScenarios":"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.","commonSituations":"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.","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"],"exampleFix":"// before\nList.of(toolResponse1, toolResponse2); // ToolResponseMessage precedes ToolResponseMessage\n// after\nList.of(assistantWithCalls, toolResponse1, toolResponse2);","handlingStrategy":"validation","validationCode":"boolean sequenceValid(List<Message> msgs) { int i = msgs.size()-1; return i > 0 && msgs.get(i) instanceof ToolResponseMessage && msgs.get(i-1) instanceof AssistantMessage; }","typeGuard":"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; }","tryCatchPattern":"try { toolNode.apply(state, config); } catch (IllegalStateException e) { sanitizeHistory(state); retryOnce(); }","preventionTips":["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"],"tags":["java","state","tools","message-history"],"backgroundTag":"type-mismatch","analyzedSha":"f82da0b50f35744c13968191be2b1cd2452ef550","analyzedAt":"2026-09-09T15:32:42.421Z","contentChangedAt":"2026-09-09T15:32:42.421Z","schemaVersion":2},"datasetVersion":"2026-09-14T05:17:10.506Z"}