affaan-m/ECC · error

agent profile inheritance cycle

Error message

agent profile inheritance cycle: {}

What it means

This error is raised while resolving an agent profile's inheritance chain: the profile being resolved already appears in the current chain, meaning A inherits from B which (directly or transitively) inherits back to A. The library detects the loop before it would recurse forever, and the message includes the full cycle path (e.g. `a -> b -> c -> a`) so the offending links are visible. It is a guard against malformed profile inheritance graphs.

Solutions

  1. Break the cycle: edit the config so the inheritance chain is acyclic — the message's path shows exactly which profiles form the loop.
  2. Remove any self-reference where a profile's `extends`/base field names itself.
  3. Rename the base profile referenced in the cycle to a genuinely independent root profile, or inline its values into the child.
  4. Add a startup config lint that runs resolution for every agent profile and fails fast with cycle detection before runtime use.

Example fix

# before (cyclic)
[agent_profiles.reviewer]
extends = "builder"

[agent_profiles.builder]
extends = "reviewer"   # cycle: reviewer -> builder -> reviewer

# after
[agent_profiles.reviewer]
extends = "builder"

[agent_profiles.builder]
# no extends; standalone base profile
Defensive patterns

Strategy: validation

Validate before calling

fn profile_graph_is_acyclic(profiles: &HashMap<String, AgentProfile>) -> bool {
    fn visit(n: &str, p: &HashMap<String, AgentProfile>, seen: &mut Vec<String>) -> bool {
        if seen.contains(&n.to_string()) { return false; }
        if let Some(pr) = p.get(n) {
            if let Some(base) = &pr.extends {
                seen.push(n.to_string());
                return visit(base, p, seen);
            }
        }
        true
    }
    profiles.keys().all(|n| visit(n, profiles, &mut vec![]))
}

Try / catch

match config.resolve_agent_profile(name, &mut chain) {
    Ok(p) => p,
    Err(e) if e.to_string().contains("inheritance cycle") => {
        eprintln!("Fix profile extends chain: {e}");
        default_profile()
    }
    Err(e) => return Err(e),
}

Prevention

When it happens

Trigger: Calling profile resolution (`resolve_agent_profile`-style API with a `chain` accumulator) on a name that is already present in `chain`, i.e. the `extends`/`base` references among agent profiles form a directed cycle.

Common situations: Hand-editing config where profile X was changed to extend profile Y and Y was simultaneously repointed to extend X; merging config branches where two profiles each adopted the other as a parent; typos where a profile extends itself (`extends = "itself"`); renaming a base profile so an old `extends` value now points back into the chain.

Understand the failure class

Background: "Invalid state transition" errors: "status must be X, actually Y", "already rejected/charging/uninstalled", "cannot ... while running" — what they mean when a library rejects your call — this error's family across 31 libraries.

Related errors


AI-assisted analysis of affaan-m/ECC@8321021c54 (2026-09-16). Data as JSON: /api/errors/3890510ffdb12957. Report an issue: GitHub.

Appendix: source

Thrown at ecc2/src/config/mod.rs:473

        }

        Ok(ResolvedOrchestrationTemplate {
            template_name: name.to_string(),
            description,
            project,
            task_group,
            steps,
        })
    }

    fn resolve_agent_profile_inner(
        &self,
        name: &str,
        chain: &mut Vec<String>,
    ) -> Result<ResolvedAgentProfile> {
        if chain.iter().any(|existing| existing == name) {
            chain.push(name.to_string());
            anyhow::bail!("agent profile inheritance cycle: {}", chain.join(" -> "));
        }

        let profile = self
            .agent_profiles
            .get(name)
            .ok_or_else(|| anyhow::anyhow!("Unknown agent profile: {name}"))?;

        chain.push(name.to_string());
        let mut resolved = if let Some(parent) = profile.inherits.as_deref() {
            self.resolve_agent_profile_inner(parent, chain)?
        } else {
            ResolvedAgentProfile::default()
        };
        chain.pop();

        resolved.apply(name, profile);
        Ok(resolved)
    }

View on GitHub (pinned to 8321021c54)