conductor-oss/conductor · error · IllegalArgumentException

External tool data contains a cyclic structure

Error message

External tool data contains a cyclic structure

What it means

Thrown by ExternalDataLimits.validateStructure when an object already present in the per-validation IdentityHashMap is encountered again — i.e. the object graph is cyclic (a self-referential or mutually-referential structure). This guards against infinite recursion and unbounded serialization of tool data into workflow state. IllegalArgumentException. Note the validator removes each object from the seen-set in a finally block as it unwinds, so only true cycles (a node reachable from itself along the active path) trip this, not DAGs/diamonds.

Source

Thrown at ai/src/main/java/org/conductoross/conductor/ai/http/ExternalDataLimits.java:46

    /** Rejects object graphs that would make workflow state expensive or unsafe to process. */
    public static void validateStructure(Object value) {
        validateStructure(value, 0, new IdentityHashMap<>());
    }

    private static void validateStructure(
            Object value, int depth, IdentityHashMap<Object, Boolean> seen) {
        if (value == null
                || value instanceof String
                || value instanceof Number
                || value instanceof Boolean) {
            return;
        }
        if (depth > MAX_NESTING_DEPTH) {
            throw new IllegalArgumentException(
                    "External tool data exceeds the maximum nesting depth of " + MAX_NESTING_DEPTH);
        }
        if (seen.put(value, Boolean.TRUE) != null) {
            throw new IllegalArgumentException("External tool data contains a cyclic structure");
        }
        try {
            if (value instanceof Map<?, ?> map) {
                for (Map.Entry<?, ?> entry : map.entrySet()) {
                    validateStructure(entry.getKey(), depth + 1, seen);
                    validateStructure(entry.getValue(), depth + 1, seen);
                }
            } else if (value instanceof Collection<?> collection) {
                for (Object item : collection) {
                    validateStructure(item, depth + 1, seen);
                }
            } else if (value.getClass().isArray()) {
                for (int i = 0; i < Array.getLength(value); i++) {
                    validateStructure(Array.get(value, i), depth + 1, seen);
                }
            }
        } finally {
            seen.remove(value);

View on GitHub (pinned to cf7c3e4a8a)

Solutions

  1. Break the cycle before validation: convert the cyclic structure to a tree (JSON-like) by copying into plain Maps/Lists with no back-references, or null-out parent pointers.
  2. Serialize to JSON and re-parse into a fresh structure — JSON has no cycles, so the round-trip removes them.
  3. If the data is legitimately a graph, project it into an acyclic form (e.g. a flat node list + edge list) before handing it to the workflow.

Example fix

// before — parent references child which references parent
parent.setChild(child); child.setParent(parent); // cycle
// after — copy to DTOs without back-references
Map<String,Object> out = Map.of("name", parent.getName(),
    "children", parent.getChildren().stream().map(c -> Map.of("name", c.getName())).toList());
Defensive patterns

Strategy: validation

Validate before calling

// Remove cycles before validation: copy into plain JSON-shaped structures
// (Map/List with no back-references) or round-trip through JSON
Object acyclic = objectMapper.readTree(objectMapper.writeValueAsString(graph));
ExternalDataLimits.validateStructure(acyclic);

Try / catch

try {
    ExternalDataLimits.validateStructure(value);
} catch (IllegalArgumentException e) {
    // cyclic structure — break references at the source, then re-validate
    throw new IllegalArgumentException("Tool data has a cycle; convert to a tree first", e);
}

Prevention

When it happens

Trigger: An external tool result contains a cyclic object graph — e.g. a Map that contains itself as a value, or two objects referencing each other — passed to validateStructure. Identity-based (==) detection, so it fires on the exact same object instance recurring.

Common situations: Serializing a domain object that has bidirectional parent/child links into tool output without breaking cycles; reusing the same mutable Map/List instance in multiple places and letting it reference itself; a serializer that preserves references.

Related errors


AI-assisted analysis of conductor-oss/conductor@cf7c3e4a8a (2026-08-14). Data as JSON: /api/errors/7bdb72b2d6cb01b9. Report an issue: GitHub.