{"record":{"id":"7bdb72b2d6cb01b9","repo":"conductor-oss/conductor","slug":"external-tool-data-contains-a-cyclic-structure","errorCode":null,"errorMessage":"External tool data contains a cyclic structure","messagePattern":"External tool data contains a cyclic structure","errorType":"validation","errorClass":"IllegalArgumentException","httpStatus":null,"severity":"error","filePath":"ai/src/main/java/org/conductoross/conductor/ai/http/ExternalDataLimits.java","lineNumber":46,"sourceCode":"    /** Rejects object graphs that would make workflow state expensive or unsafe to process. */\n    public static void validateStructure(Object value) {\n        validateStructure(value, 0, new IdentityHashMap<>());\n    }\n\n    private static void validateStructure(\n            Object value, int depth, IdentityHashMap<Object, Boolean> seen) {\n        if (value == null\n                || value instanceof String\n                || value instanceof Number\n                || value instanceof Boolean) {\n            return;\n        }\n        if (depth > MAX_NESTING_DEPTH) {\n            throw new IllegalArgumentException(\n                    \"External tool data exceeds the maximum nesting depth of \" + MAX_NESTING_DEPTH);\n        }\n        if (seen.put(value, Boolean.TRUE) != null) {\n            throw new IllegalArgumentException(\"External tool data contains a cyclic structure\");\n        }\n        try {\n            if (value instanceof Map<?, ?> map) {\n                for (Map.Entry<?, ?> entry : map.entrySet()) {\n                    validateStructure(entry.getKey(), depth + 1, seen);\n                    validateStructure(entry.getValue(), depth + 1, seen);\n                }\n            } else if (value instanceof Collection<?> collection) {\n                for (Object item : collection) {\n                    validateStructure(item, depth + 1, seen);\n                }\n            } else if (value.getClass().isArray()) {\n                for (int i = 0; i < Array.getLength(value); i++) {\n                    validateStructure(Array.get(value, i), depth + 1, seen);\n                }\n            }\n        } finally {\n            seen.remove(value);","sourceCodeStart":28,"sourceCodeEnd":64,"githubUrl":"https://github.com/conductor-oss/conductor/blob/cf7c3e4a8adfb158be778ab1ec525323c363cd3a/ai/src/main/java/org/conductoross/conductor/ai/http/ExternalDataLimits.java#L28-L64","documentation":"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.","triggerScenarios":"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.","commonSituations":"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.","solutions":["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.","Serialize to JSON and re-parse into a fresh structure — JSON has no cycles, so the round-trip removes them.","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."],"exampleFix":"// before — parent references child which references parent\nparent.setChild(child); child.setParent(parent); // cycle\n// after — copy to DTOs without back-references\nMap<String,Object> out = Map.of(\"name\", parent.getName(),\n    \"children\", parent.getChildren().stream().map(c -> Map.of(\"name\", c.getName())).toList());","handlingStrategy":"validation","validationCode":"// Remove cycles before validation: copy into plain JSON-shaped structures\n// (Map/List with no back-references) or round-trip through JSON\nObject acyclic = objectMapper.readTree(objectMapper.writeValueAsString(graph));\nExternalDataLimits.validateStructure(acyclic);","typeGuard":null,"tryCatchPattern":"try {\n    ExternalDataLimits.validateStructure(value);\n} catch (IllegalArgumentException e) {\n    // cyclic structure — break references at the source, then re-validate\n    throw new IllegalArgumentException(\"Tool data has a cycle; convert to a tree first\", e);\n}","preventionTips":["Break parent/child back-references when serializing domain objects to tool output.","Round-trip through JSON to guarantee an acyclic tree before validation.","For graph data, emit a flat node/edge list instead of nested references."],"tags":["validation","data-limits","external-data","illegalargument","cycle","workflow-state"],"backgroundTag":null,"analyzedSha":"cf7c3e4a8adfb158be778ab1ec525323c363cd3a","analyzedAt":"2026-08-14T03:33:19.897Z","schemaVersion":2},"datasetVersion":"2026-08-14T05:17:29.042Z"}