conductor-oss/conductor · error · IllegalArgumentException
Circular skill reference detected: {key}
Error message
Circular skill reference detected: {key} What it means
Thrown by rawConfigForDetail when hydrating cross-skill references would recurse into a skill already on the current resolution stack — i.e. skill A's crossSkillRefs reference B, and B's reference A (directly or transitively). A `Set<String>` of `name@version` keys is threaded through the recursion; a duplicate add returns false and aborts. This prevents infinite recursion / stack overflow when building a SkillDetail tree.
Source
Thrown at agentspan/src/main/java/org/conductoross/conductor/ai/agentspan/runtime/service/SkillRegistryService.java:690
private Map<String, Object> mergeMetadata(
Map<String, Object> packageMetadata, Map<String, Object> manifestMetadata) {
if (packageMetadata.isEmpty()) {
return manifestMetadata;
}
if (manifestMetadata.isEmpty()) {
return packageMetadata;
}
Map<String, Object> merged = new LinkedHashMap<>(packageMetadata);
merged.putAll(manifestMetadata);
return merged;
}
@SuppressWarnings("unchecked")
private Map<String, Object> rawConfigForDetail(SkillDetail detail, Set<String> stack) {
String key = detail.getName() + "@" + detail.getVersion();
if (!stack.add(key)) {
throw new IllegalArgumentException("Circular skill reference detected: " + key);
}
try {
Map<String, Object> rawConfig = deepCopy(detail.getRawConfig());
rawConfig.put(
"skillRef",
Map.of(
"name", detail.getName(),
"version", detail.getVersion(),
"checksum", detail.getChecksum()));
Map<String, Object> pinnedRefs = toMap(rawConfig.get("crossSkillRefs"));
if (!pinnedRefs.isEmpty()) {
rawConfig.put("crossSkillRefs", hydratePinnedCrossSkills(pinnedRefs, stack));
} else if (!Boolean.TRUE.equals(rawConfig.get("crossSkillRefsPinned"))) {
Object skillMd = rawConfig.get("skillMd");
if (skillMd instanceof String md) {
rawConfig.put("crossSkillRefs", resolveRegisteredCrossSkills(md, stack));
}
}View on GitHub (pinned to cf7c3e4a8a)
Solutions
- Inspect the `name@version` in the message — that is the skill re-entering its own resolution chain.
- Open each manifest's `crossSkillRefs` and break the cycle: make the dependency one-directional, or move shared logic into a third skill both reference.
- Re-publish the corrected skill(s); the registry does not auto-heal cycles.
Example fix
// before: skill-a references skill-b, skill-b references skill-a crossSkillRefs: skill-b: "1.0.0" # in skill-a manifest # in skill-b manifest crossSkillRefs: skill-a: "1.0.0" # REMOVE — forms cycle // after: only skill-a references skill-b; skill-b references a shared helper crossSkillRefs: skill-helpers: "1.0.0"
Defensive patterns
Strategy: validation
Validate before calling
// Detect cross-skill reference cycles before publishing.
boolean hasCycle(Map<String, Set<String>> graph, String start) {
Set<String> visited = new HashSet<>(), stack = new HashSet<>();
java.util.function.BiPredicate<String, String> dfs = null;
dfs = (node, from) -> {
if (stack.contains(node)) return true;
if (!visited.add(node)) return false;
stack.add(node);
for (String next : graph.getOrDefault(node, Set.of())) if (dfs.test(next, node)) return true;
stack.remove(node);
return false;
};
return dfs.test(start, start);
} Type guard
// Build a name@version -> set of referenced name@version map from manifests, then run hasCycle above before publish.
Try / catch
try {
return skillRegistry.getDetail(name, version);
} catch (IllegalArgumentException e) {
if (e.getMessage().contains("Circular skill reference")) {
return conflict(e.getMessage()); // 409, author must break the cycle
}
throw e;
} Prevention
- Keep cross-skill dependencies acyclic — factor shared logic into a leaf skill.
- Run a cycle check in CI over all published manifests.
- When bumping a skill, audit its crossSkillRefs for new cycles.
When it happens
Trigger: Two or more skill manifests declare pinned crossSkillRefs that form a cycle: A->B->A, or A->B->C->A. Calling any API that materializes a SkillDetail with hydrated refs (list-with-details, get detail) walks the graph and trips the guard.
Common situations: Author copy-pasted a manifest and forgot to update the ref list; two skills legitimately co-operate and were published referencing each other; a ref was bumped but the cycle was not noticed because individual publishes succeed.
Related errors
- Skill manifest name '{manifestName}' does not match package
- Skill {name} version {version} already exists with a differe
- File path is required
- Skill file is too large to preview: {cleanPath}
- Skill file not found: {cleanPath}
AI-assisted analysis of conductor-oss/conductor@cf7c3e4a8a (2026-08-14).
Data as JSON: /api/errors/a2ff79b848b5943e.
Report an issue: GitHub.