{"record":{"id":"d54fd67aead8632c","repo":"alibaba/spring-ai-alibaba","slug":"cannot-find-assistantmessage-before-toolresponseme","errorCode":null,"errorMessage":"Cannot find AssistantMessage before ToolResponseMessage","messagePattern":"Cannot find AssistantMessage before ToolResponseMessage","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":414,"sourceCode":"\t\tupdatedState.putAll(stateCollector.mergeAll());\n\n\t\tif (enableActingLog) {\n\t\t\tlogger.info(\"[ThreadId {}] Agent {} parallel tool execution completed. {} tools, {} failures.\",\n\t\t\t\t\tconfig.threadId().orElse(THREAD_ID_DEFAULT), agentName, toolCalls.size(), failures.size());\n\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","sourceCodeStart":396,"sourceCodeEnd":432,"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#L396-L432","documentation":"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.","triggerScenarios":"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.","commonSituations":"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.","solutions":["Always keep the originating AssistantMessage (with the tool calls) in the messages list before the ToolResponseMessage","When resuming from an interrupt, restore the full message history rather than just the new response","Check state-saving/compaction config so the last two messages are never truncated"],"exampleFix":"// before\nmessages = List.of(toolResponse); // size 1 -> throws\n// after\nmessages = List.of(assistantMessageWithToolCalls, toolResponse);","handlingStrategy":"validation","validationCode":"boolean partialResponseContextValid(List<Message> msgs) { return msgs.size() >= 2 && msgs.get(msgs.size()-1) instanceof ToolResponseMessage; }","typeGuard":"boolean hasAssistantBeforeToolResponse(List<Message> msgs) { return msgs.size() >= 2 && msgs.get(msgs.size()-2) instanceof AssistantMessage; }","tryCatchPattern":"try { resume(state); } catch (IllegalStateException e) { reloadFullHistoryFromCheckpoint(); resume(state); }","preventionTips":["Persist the full message history across interrupts/resumes","Never reconstruct state with only the ToolResponseMessage","Exclude at least the last AssistantMessage from compaction"],"tags":["java","state","tools","message-history"],"backgroundTag":"invalid-state-transition","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"}