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-agent

View on GitHub (pinned to cf7c3e4a8a)

Solutions

  1. Break the circular reference: identify the cycle from the error's stack trace and remove one link.
  2. If skill A needs skill B's tools, make the dependency unidirectional (B should not reference A).
  3. Extract shared functionality into a third skill that both A and B reference without creating a cycle.
  4. 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

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


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