alibaba/spring-ai-alibaba · error · IllegalArgumentException

checkpoints.numRetained must be a number or numeric string,

Error message

checkpoints.numRetained must be a number or numeric string, got: <value class name>

What it means

BaseCheckpointSaver.checkpointsNumRetained reads the checkpoints.numRetained value from RunnableConfig metadata. The value must be a Number or a numeric String; anything else (Boolean, Map, List, null object of other type) throws IllegalArgumentException naming the offending value's class.

Source

Thrown at spring-ai-alibaba-graph-core/src/main/java/com/alibaba/cloud/ai/graph/checkpoint/BaseCheckpointSaver.java:43

import static java.util.Optional.ofNullable;

public interface BaseCheckpointSaver {
	String THREAD_ID_DEFAULT = "$default";
	String CHECKPOINTS_NUM_RETAINED = "checkpoints.numRetained";

	default Optional<Checkpoint> getLast(LinkedList<Checkpoint> checkpoints, RunnableConfig config) {
		return (checkpoints.isEmpty()) ? Optional.empty() : ofNullable(checkpoints.peek());
	}

	default Optional<Integer> checkpointsNumRetained(RunnableConfig config) {
		return config.metadata(CHECKPOINTS_NUM_RETAINED).map(value -> {
			if (value instanceof Number number) {
				return number.intValue();
			}
			if (value instanceof String text) {
				return Integer.parseInt(text);
			}
			throw new IllegalArgumentException(
					"checkpoints.numRetained must be a number or numeric string, got: " + value.getClass().getName());
		}).filter(value -> value > 0);
	}

	default void retainLatestCheckpoints(LinkedList<Checkpoint> checkpoints, RunnableConfig config) {
		checkpointsNumRetained(config).ifPresent(numRetained -> {
			while (checkpoints.size() > numRetained) {
				checkpoints.removeLast();
			}
		});
	}

	Collection<Checkpoint> list(RunnableConfig config);

	Optional<Checkpoint> get(RunnableConfig config);

	RunnableConfig put(RunnableConfig config, Checkpoint checkpoint) throws Exception;

View on GitHub (pinned to f82da0b50f)

Solutions

  1. Set checkpoints.numRetained to an int/Integer in RunnableConfig metadata
  2. If loaded from JSON/YAML, ensure the value is an unquoted number and unwrap container nodes
  3. Parse/coerce the value yourself before putting it into the config
  4. Wrap retainLatestCheckpoints calls in try-catch and fall back to a default retention count

Example fix

// before
config.meta("checkpoints.numRetained", "true");
// after
config.meta("checkpoints.numRetained", 10);
Defensive patterns

Strategy: validation

Validate before calling

Object v = config.metadata().get("checkpoints.numRetained");
if (!(v instanceof Number) && !(v instanceof String s && s.matches("\\d+"))) throw new IllegalArgumentException("bad checkpoints.numRetained: " + v);

Type guard

Integer asInt(Object v) { if (v instanceof Number n) return n.intValue(); if (v instanceof String s) { try { return Integer.parseInt(s); } catch (NumberFormatException e) { return null; } } return null; }

Try / catch

try { saver.retainLatestCheckpoints(checkpoints, config); } catch (IllegalArgumentException e) { /* use default retention */ }

Prevention

When it happens

Trigger: Passing a RunnableConfig whose metadata key checkpoints.numRetained is set to a non-numeric type, e.g. a Boolean, JSONObject, or nested Map.

Common situations: Loading config from JSON/YAML where the value is parsed as a non-numeric node (e.g. quoted incorrectly or wrapped in an object), or programmatically putting the wrong type into config metadata.

Related errors


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