tinyhumansai/openhuman · error · TeamError
dependency cycle detected
Error message
dependency cycle detected
What it means
Thrown by agent_teams::ops::validate_dependencies when has_task_cycle — a Kahn's-algorithm topological sort over the existing tasks plus the candidate new task — cannot exhaust the graph, i.e. the combined dependency graph contains a cycle. It wraps TeamError::CyclicDependency and fires after self-dep and unknown-dep checks pass.
Source
Thrown at src/openhuman/agent/orchestration/agent_teams/ops.rs:416
existing: &[AgentTeamTask],
) -> Result<()> {
let known: HashSet<&str> = existing.iter().map(|t| t.id.as_str()).collect();
for dep in depends_on {
if dep == new_task_id {
return Err(anyhow!(TeamError::SelfDependency {
task_id: new_task_id.to_string(),
}));
}
if !known.contains(dep.as_str()) {
return Err(anyhow!(TeamError::UnknownDependency {
depends_on: dep.clone(),
}));
}
}
if has_task_cycle(new_task_id, depends_on, existing) {
return Err(anyhow!(TeamError::CyclicDependency));
}
Ok(())
}
/// Kahn's-algorithm cycle check over the task dependency graph (existing tasks
/// plus the candidate new task). Edge `dep -> task` means `task` depends on
/// `dep`. Edges pointing at unknown ids are ignored here (rejected separately).
fn has_task_cycle(
new_task_id: &str,
new_depends_on: &[String],
existing: &[AgentTeamTask],
) -> bool {
// Node set: every existing task id plus the new one.
let mut nodes: HashSet<&str> = existing.iter().map(|t| t.id.as_str()).collect();
nodes.insert(new_task_id);
let mut indegree: HashMap<&str, usize> = nodes.iter().map(|&n| (n, 0)).collect();View on GitHub (pinned to a221052e0d)
Solutions
- Map the existing team's task graph (list tasks + their deps) and remove the back-edge before assigning
- Break the loop by making one direction a separate follow-up task with no reverse dep
- If the existing graph itself is cyclic (data corruption), rebuild the team's tasks
Example fix
// before
// taskB already depends on taskA
agent_teams::ops::assign_task(&config, &team, "taskA-v2", member, &["taskB".into()])?; // closes cycle A->B->A
// after — cut the back-edge before adding the new node
let deps: Vec<String> = depends_on
.into_iter()
.filter(|d| !task_transitively_depends_on(&config, &team, d, &new_task_id))
.collect();
agent_teams::ops::assign_task(&config, &team, &new_task_id, member, &deps)?; Defensive patterns
Strategy: validation
Validate before calling
// Client-side Kahn check mirroring has_task_cycle: build edges dep -> task over
// existing tasks plus the candidate, then ensure a full topological order exists.
fn would_cycle(new_id: &str, new_deps: &[String], existing: &[AgentTeamTask]) -> bool {
let mut indeg: HashMap<&str, usize> = HashMap::new();
let mut edges: HashMap<&str, Vec<&str>> = HashMap::new();
let ids = existing.iter().map(|t| t.id.as_str()).chain([new_id]);
for id in ids { indeg.entry(id).or_insert(0); }
for t in existing { for d in &t.depends_on { edges.entry(d.as_str()).or_default().push(t.id.as_str()); *indeg.get_mut(t.id.as_str()).unwrap() += 1; } }
for d in new_deps { edges.entry(d.as_str()).or_default().push(new_id); *indeg.get_mut(new_id).unwrap() += 1; }
let mut q: Vec<&str> = indeg.iter().filter(|(_, &d)| d == 0).map(|(k, _)| *k).collect();
let mut seen = 0;
while let Some(n) = q.pop() { seen += 1; for &m in edges.get(n).into_iter().flatten() { if let Some(d) = indeg.get_mut(m) { *d -= 1; if *d == 0 { q.push(m); } } } }
seen != indeg.len()
} Try / catch
match agent_teams::ops::assign_task(&config, &team_id, &task_id, member, &depends_on) {
Ok(_) => Ok(()),
Err(e) if e.to_string().contains("dependency cycle") => {
// drop the back-edge and re-submit, or split the mutually-dependent work
assign_without_back_edge(&config, &team_id, &task_id, member, &depends_on)
}
Err(e) => Err(e),
} Prevention
- Model the task graph (DAG) in the caller and reject back-edges at authoring time
- If two tasks genuinely wait on each other, merge them or introduce an intermediate task
- Audit the stored task graph for pre-existing cycles when this error appears with surprising input
When it happens
Trigger: Assigning a task whose depends_on closes a loop: the new task depends on an existing task that (transitively) depends back on it, or the stored existing graph already contains a cycle that the new edges complete/expose. Requires the dep ids to all be known and non-self, otherwise the earlier checks fire instead.
Common situations: LLM-generated plans adding back-references; two tasks authored to wait on each other; a previously corrupted ledger where tasks were inserted with cyclic deps bypassing validation.
Related errors
- task {task_id} cannot depend on itself
- unknown dependency: {depends_on}
- agentTeamApi: ${label} must be a positive integer
- agentTeamApi.get: teamId is required
- agentTeamApi.listMessages: teamId is required
AI-assisted analysis of tinyhumansai/openhuman@a221052e0d (2026-08-16).
Data as JSON: /api/errors/240bdb16da62a322.
Report an issue: GitHub.