{"record":{"id":"1b068dc4c695437f","repo":"alibaba/spring-ai-alibaba","slug":"human-feedback-metadata-must-be-of-type-interrupti","errorCode":null,"errorMessage":"Human feedback metadata must be of type InterruptionMetadata.","messagePattern":"Human feedback metadata must be of type InterruptionMetadata\\.","errorType":"exception","errorClass":"IllegalArgumentException","httpStatus":null,"severity":"error","filePath":"spring-ai-alibaba-agent-framework/src/main/java/com/alibaba/cloud/ai/graph/agent/hook/hip/HumanInTheLoopHook.java","lineNumber":158,"sourceCode":"\t\telse {\n\t\t\tlog.warn(\"Last message is not an AssistantMessage, cannot process human feedback.\");\n\t\t}\n\n\t\treturn CompletableFuture.completedFuture(Map.of());\n\t}\n\n\t@Override\n\tpublic Optional<InterruptionMetadata> interrupt(String nodeId, OverAllState state, RunnableConfig config) {\n\t\tAssistantMessage lastMessage = getLastAssistantMessage(state);\n\n\t\tif (lastMessage == null || !lastMessage.hasToolCalls()) {\n\t\t\treturn Optional.empty();\n\t\t}\n\n\t\tOptional<Object> feedback = config.metadata(RunnableConfig.HUMAN_FEEDBACK_METADATA_KEY);\n\t\tif (feedback.isPresent()) {\n\t\t\tif (!(feedback.get() instanceof InterruptionMetadata)) {\n\t\t\t\tthrow new IllegalArgumentException(\"Human feedback metadata must be of type InterruptionMetadata.\");\n\t\t\t}\n\n\t\t\tif (!validateFeedback((InterruptionMetadata) feedback.get(), lastMessage.getToolCalls())) {\n\t\t\t\treturn buildInterruptionMetadata(state, lastMessage);\n\t\t\t}\n\t\t\treturn Optional.empty();\n\t\t}\n\n\t\t// 2. If last message is AssistantMessage\n\t\treturn buildInterruptionMetadata(state, lastMessage);\n\t}\n\n\tprivate static AssistantMessage getLastAssistantMessage(OverAllState state) {\n\t\tList<Message> messages = (List<Message>) state.value(\"messages\").orElse(List.of());\n\n\t\tAssistantMessage lastMessage = null;\n\t\tfor (int i = messages.size() - 1; i >= 0; i--) {\n\t\t\tMessage msg = messages.get(i);","sourceCodeStart":140,"sourceCodeEnd":176,"githubUrl":"https://github.com/alibaba/spring-ai-alibaba/blob/f82da0b50f35744c13968191be2b1cd2452ef550/spring-ai-alibaba-agent-framework/src/main/java/com/alibaba/cloud/ai/graph/agent/hook/hip/HumanInTheLoopHook.java#L140-L176","documentation":"HumanInTheLoopHook.interrupt() reads the human feedback payload from RunnableConfig under HUMAN_FEEDBACK_METADATA_KEY and requires it to be an InterruptionMetadata instance. If a resume carries feedback metadata of any other type, the hook cannot validate tool-call approvals and throws IllegalArgumentException instead of silently misinterpreting the data.","triggerScenarios":"Calling resume/continue on an interrupted graph while passing config metadata under key RunnableConfig.HUMAN_FEEDBACK_METADATA_KEY with a value that is not an InterruptionMetadata object (e.g. a raw Map, String, or custom feedback DTO).","commonSituations":"Building custom UI resumption code that stuffs a hand-rolled feedback object into metadata; upgrading library versions where the feedback contract changed to InterruptionMetadata; copying older example code that used a plain map of approved/rejected tool call ids.","solutions":["Construct an InterruptionMetadata (via its builder) carrying the approved/rejected tool call ids and put that object under HUMAN_FEEDBACK_METADATA_KEY instead of a Map or String.","Check the value type before resuming: if (cfg.metadata(HUMAN_FEEDBACK_METADATA_KEY).map(v -> v instanceof InterruptionMetadata).orElse(true)) proceed.","Review the HumanInTheLoopHook docs/examples for the current resume payload shape after upgrading versions."],"exampleFix":"// before\nconfig.metadata(HUMAN_FEEDBACK_METADATA_KEY, Map.of(\"approved\", List.of(\"call-1\")));\n// after\nInterruptionMetadata feedback = InterruptionMetadata.builder()\n    .approvedToolCallIds(List.of(\"call-1\"))\n    .build();\nconfig.metadata(HUMAN_FEEDBACK_METADATA_KEY, feedback);","handlingStrategy":"type-guard","validationCode":"Object fb = config.metadata(RunnableConfig.HUMAN_FEEDBACK_METADATA_KEY).orElse(null);\nif (fb != null && !(fb instanceof InterruptionMetadata)) {\n    throw new IllegalStateException(\"Feedback metadata must be InterruptionMetadata, got \" + fb.getClass());\n}","typeGuard":"boolean isValidFeedback(Object o) { return o == null || o instanceof InterruptionMetadata; }","tryCatchPattern":"try {\n    resume(config);\n} catch (IllegalArgumentException e) {\n    if (e.getMessage().contains(\"InterruptionMetadata\")) {\n        // rebuild feedback payload as InterruptionMetadata and retry\n    } else throw e;\n}","preventionTips":["Always build feedback via InterruptionMetadata.builder()","Centralize resume-config construction in one helper","Add a unit test asserting the metadata type placed on resume"],"tags":["java","human-in-the-loop","metadata","type-mismatch","resume"],"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"}