alibaba/spring-ai-alibaba · error · RuntimeException
Failed to execute agent tool '%s' (parentThreadId=%s, input=
Error message
Failed to execute agent tool '%s' (parentThreadId=%s, input=%s): sub-agent returned no messages
What it means
Thrown by AgentTool.executeAgent when the sub-agent's returned OverAllState has no 'messages' value at all (Optional.empty from state.value("messages", List.class)). The tool cannot extract any conversation output, so it throws 'sub-agent returned no messages'. This indicates the sub-agent ran (no exception) but its final state lacks the messages key entirely.
Source
Thrown at spring-ai-alibaba-agent-framework/src/main/java/com/alibaba/cloud/ai/graph/agent/AgentTool.java:250
catch (RuntimeException e) {
throw buildExecutionException(actualInput, parentConfigOpt.orElse(null),
"sub-agent invocation failed: " + e.getMessage(), e);
}
Optional<List> messages = resultState.flatMap(overAllState -> overAllState.value("messages", List.class));
if (messages.isPresent()) {
@SuppressWarnings("unchecked")
List<Message> messageList = (List<Message>) messages.get();
if (!messageList.isEmpty() && messageList.get(messageList.size() - 1) instanceof AssistantMessage assistantMessage) {
return assistantMessage;
}
throw buildExecutionException(actualInput, parentConfigOpt.orElse(null),
"sub-agent returned no assistant message. Last message type: "
+ (messageList.isEmpty() ? "<empty>" : messageList.get(messageList.size() - 1).getMessageType()),
null);
}
throw buildExecutionException(actualInput, parentConfigOpt.orElse(null),
"sub-agent returned no messages", null);
}
private RuntimeException buildExecutionException(String actualInput, RunnableConfig parentConfig, String detail,
Throwable cause) {
String threadId = parentConfig != null ? parentConfig.threadId().orElse("<no-thread-id>") : "<standalone>";
String message = String.format("Failed to execute agent tool '%s' (parentThreadId=%s, input=%s): %s",
agent.name(), threadId, actualInput, detail);
return cause == null ? new RuntimeException(message) : new RuntimeException(message, cause);
}
/**
* Extract the actual input value from the wrapped JSON structure.
* The input is expected to be in the format: {"input": "actual_value"}
* If the input is not a valid JSON object or doesn't contain "input" field,
* the original input string is returned as-is.
*
* @param input the wrapped input JSON stringView on GitHub (pinned to f82da0b50f)
Solutions
- Ensure the sub-agent's state factory registers a key strategy for 'messages' (appending list strategy) so all nodes write conversation state there.
- Have node handlers return Map.of("messages", List.of(assistantMessage)) instead of custom keys, or map the custom key to 'messages' in the final node.
- If using persistence, verify the checkpointed state actually contains 'messages' before resume; clear stale checkpoints for the derived threadId.
- Log the sub-agent's final OverAllState keys (state.value("messages")) standalone to confirm where output is being stored.
Example fix
// before: node writes a custom key
return Map.of("output", assistantMessage);
// after
return Map.of("messages", List.of(assistantMessage)); Defensive patterns
Strategy: validation
Validate before calling
OverAllState out = reactAgent.invoke(Map.of("messages", List.of(new UserMessage("ping")))).orElse(null);
if (out == null || out.value("messages", List.class).isEmpty()) {
throw new IllegalStateException("Sub-agent state does not expose 'messages'; fix key strategies before tool registration");
} Type guard
static boolean hasMessages(OverAllState state) {
return state != null && state.value("messages", List.class).isPresent();
} Try / catch
try {
AssistantMessage reply = agentToolExecutor.executeAgent(input, toolContext);
}
catch (RuntimeException e) {
if (e.getMessage() != null && e.getMessage().contains("returned no messages")) {
throw new IllegalStateException("Sub-agent " + agentName + " state missing 'messages' key — check key strategies");
}
throw e;
} Prevention
- Always use the standard 'messages' key with an append strategy in sub-agent state factories.
- Map custom output keys to 'messages' in the terminal node before exposing the agent as a tool.
- Smoke-test that the sub-agent's final state contains 'messages' during development.
- Purge stale checkpoints for derived threadIds when changing state schemas.
When it happens
Trigger: Sub-agent graph's output key strategy does not write to 'messages'; the state was reset/overwritten by cloneState/updateState without the messages key strategy; sub-agent graph ends at a node that emits a different state key (e.g. a custom output key); messages value is null rather than an empty list.
Common situations: Custom sub-agent using a different state key name (e.g. 'output') instead of the framework-standard 'messages'; keyStrategies misconfigured so merge drops messages; sub-agent built via builder without the default state factory; resumed/persisted state missing messages after checkpoint restore.
Understand the failure class
Background: "must not be empty", "cannot be empty" — required-field validation errors across open-source libraries — this error's family across 41 libraries.
Related errors
- Failed to execute agent tool '%s' (parentThreadId=%s, input=
- Failed to execute agent tool '%s' (parentThreadId=%s, input=
- TOOL_PARAMS_MISSING
- TOOL_EXECUTION_ERROR
- BUILD_TOOL_RESULT_ERROR
AI-assisted analysis of alibaba/spring-ai-alibaba@f82da0b50f (2026-09-09).
Data as JSON: /api/errors/dcaa735b216993fc.
Report an issue: GitHub.