conductor-oss/conductor · error · IllegalArgumentException
Circular skill reference detected: '${refName}' is already b
Error message
Circular skill reference detected: '${refName}' is already being normalized. Stack: ${stack} What it means
Thrown by SkillNormalizer during cross-skill reference wiring when a skill being normalized is already on the normalization stack — i.e., it references itself directly or transitively. The normalizer uses a ThreadLocal Set (normalizingStack) to track the current normalization chain and detect cycles. The error includes the full stack so the developer can trace the circular path.
Source
Thrown at agentspan/src/main/java/org/conductoross/conductor/ai/agentspan/runtime/normalizer/SkillNormalizer.java:253
log.debug(
"Skill '{}': created read_skill_file tool with {} resources + {} sections",
name,
resourceFiles.size(),
skillSections.size());
}
// Step 7: Build per-run workspace tools
if (!workspaceRoots.isEmpty()) {
addWorkspaceTools(tools, name, workspaceRoots);
}
// Step 8: Wire cross-skill references
Set<String> stack = normalizingStack.get();
for (Map.Entry<String, Object> entry : crossSkillRefs.entrySet()) {
String refName = entry.getKey();
if (stack.contains(refName)) {
throw new IllegalArgumentException(
"Circular skill reference detected: '"
+ refName
+ "' is already being normalized. Stack: "
+ stack);
}
stack.add(refName);
try {
Map<String, Object> refConfig = (Map<String, Object>) entry.getValue();
if (!workspaceRoots.isEmpty() && !refConfig.containsKey("workspace")) {
refConfig = new LinkedHashMap<>(refConfig);
refConfig.put("workspace", workspace);
}
AgentConfig refAgent = this.normalize(refConfig);
Map<String, Object> refToolConfig = new LinkedHashMap<>();
refToolConfig.put("agentConfig", refAgent);
// inputSchema must include "request" property — same as sub-agentView on GitHub (pinned to cf7c3e4a8a)
Solutions
- Break the circular reference: identify the cycle from the error's stack trace and remove one link.
- If skill A needs skill B's tools, make the dependency unidirectional (B should not reference A).
- Extract shared functionality into a third skill that both A and B reference without creating a cycle.
- For self-referencing skills, remove the skill's own name from its cross-skill references.
Example fix
// before: circular A→B→A // skill A references skill B // skill B references skill A // // after: extract shared into skill C // skill A references skill C (no cycle) // skill B references skill C (no cycle) // skill C has no cross-skill references
Defensive patterns
Strategy: validation
Validate before calling
// Pre-check for cycles in the skill reference graph before normalizing
boolean hasCycle(Map<String, Set<String>> graph, String start) {
Set<String> visited = new HashSet<>();
Set<String> stack = new HashSet<>();
return dfs(graph, start, visited, stack);
}
boolean dfs(Map<String, Set<String>> graph, String node,
Set<String> visited, Set<String> stack) {
if (stack.contains(node)) return true; // cycle
if (visited.contains(node)) return false;
visited.add(node);
stack.add(node);
for (String dep : graph.getOrDefault(node, Set.of())) {
if (dfs(graph, dep, visited, stack)) return true;
}
stack.remove(node);
return false;
} Type guard
static boolean isAcyclic(Map<String, Set<String>> skillGraph) {
for (String node : skillGraph.keySet()) {
if (hasCycle(skillGraph, node)) return false;
}
return true;
} Try / catch
try {
AgentConfig normalized = skillNormalizer.normalize(rawConfig);
} catch (IllegalArgumentException e) {
if (e.getMessage().contains("Circular skill reference")) {
// parse the stack from the message, find the cycle, break one link
}
throw e;
} Prevention
- Maintain a dependency graph of skills and check for cycles before normalization.
- Skills should form a DAG — if A depends on B, B must not depend on A.
- Log cross-skill references during skill construction to catch cycles early.
- Run a topological sort on skills to detect cycles at design time.
When it happens
Trigger: A skill (framework='skill') whose cross-skill references form a cycle: skill A references skill B which references skill A, or skill A references itself directly. The normalizer detects this when refName is already in the ThreadLocal normalizingStack during Step 8 (cross-skill reference wiring).
Common situations: Two skills that reference each other for composition (A includes B, B includes A), a skill that accidentally references itself (e.g., a 'utils' skill that lists itself as a dependency), or a longer transitive chain (A→B→C→A). Common when skills are auto-generated from a dependency graph that wasn't checked for cycles.
Related errors
- SWARM handoff type must be on_tool_result, on_text_mention,
- SWARM handoff target must name a swarm agent: ${handoff.getT
- on_tool_result requires toolName and resultContains
- on_text_mention requires text
- on_condition requires a nonblank taskName
AI-assisted analysis of conductor-oss/conductor@cf7c3e4a8a (2026-08-14).
Data as JSON: /api/errors/79d07ee35ab7303a.
Report an issue: GitHub.