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 string

View on GitHub (pinned to f82da0b50f)

Solutions

  1. Ensure the sub-agent's state factory registers a key strategy for 'messages' (appending list strategy) so all nodes write conversation state there.
  2. 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.
  3. If using persistence, verify the checkpointed state actually contains 'messages' before resume; clear stale checkpoints for the derived threadId.
  4. 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

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


AI-assisted analysis of alibaba/spring-ai-alibaba@f82da0b50f (2026-09-09). Data as JSON: /api/errors/dcaa735b216993fc. Report an issue: GitHub.