Skip to content

feat(composition): allow self-referential sub-workflows with depth tracking #103

@PolyphonyRequiem

Description

@PolyphonyRequiem

Provenance: AI-drafted (GitHub Copilot CLI / Claude Opus 4.6), human-reviewed and approved, AI-submitted.

Problem

The engine currently rejects any workflow that references its own file as a sub-workflow:

# src/conductor/engine/workflow.py
if current_path is not None and sub_path == current_path:
    raise ExecutionError(
        f"Circular sub-workflow reference: agent '{agent.name}' "
        f"references its own workflow file '{agent.workflow}'.",
        suggestion="A workflow cannot reference itself as a sub-workflow.",
    )

This prevents recursive workflow patterns where the same logic applies at different levels of a hierarchy (e.g., planning at epic/issue/task level, or any divide-and-conquer pattern).

The depth-tracking infrastructure already exists (MAX_SUBWORKFLOW_DEPTH=10, _subworkflow_depth counter). The circular reference check is overly conservative — depth tracking already prevents infinite recursion.

Proposed solution

Remove the file-path equality check. Rely on MAX_SUBWORKFLOW_DEPTH for safety (already enforced). The depth counter already increments on each _execute_subworkflow call and raises ExecutionError at the limit.

Optional enhancement: Add a max_depth field to type: workflow agents so authors can set tighter bounds per-agent:

agents:
  - name: plan_children
    type: workflow
    workflow: ./self.yaml
    max_depth: 3                    # override global MAX_SUBWORKFLOW_DEPTH
    input_mapping:
      work_item_id: "{{ item.id }}"
    routes:
      - to: aggregate

What about mutual recursion (A → B → A)?
The current check only catches direct self-reference. Mutual recursion is already handled by the depth limit. Removing the self-reference check makes the behavior consistent: all circular patterns (direct and mutual) are bounded by depth, not by static analysis.

Termination guarantee:

  • MAX_SUBWORKFLOW_DEPTH=10 is the hard ceiling
  • Per-agent max_depth provides author control
  • max_iterations on each sub-workflow provides a second safety net
  • These compound: a 3-level recursive workflow with 200 iterations each can do at most 600 agent executions total

Example: fractal planning

With all three issues in this series implemented:

# plan.yaml — recursive planner
workflow:
  name: fractal-planner
  input:
    work_item_id: { type: number }
    depth: { type: number, default: 0 }
  entry_point: planner

agents:
  - name: planner
    model: claude-opus-4.6-1m
    prompt: |
      Read work item #{{ workflow.input.work_item_id }}.
      If it's an Epic, break it into Issues.
      If it's an Issue, break it into Tasks.
      If it's a Task, output the implementation steps.
    output:
      children: { type: array }
      is_leaf: { type: boolean }
    routes:
      - to: plan_children
        when: "{{ not output.is_leaf }}"
      - to: $end

  - name: plan_children
    type: for_each                      # Issue #102
    source: planner.output.children
    as: child
    max_concurrent: 1
    agent:
      type: workflow
      workflow: ./plan.yaml             # Issue #103 (this): self-reference
      input_mapping:                    # Issue #101
        work_item_id: "{{ child.id }}"
        depth: "{{ workflow.input.depth + 1 }}"
    routes:
      - to: $end

output:
  children: "{{ planner.output.children }}"

Implementation

The change is minimal — remove the 5-line path equality check in _execute_subworkflow(). The existing MAX_SUBWORKFLOW_DEPTH enforcement (already present 10 lines above the check being removed) provides the safety guarantee.

Optionally, add max_depth: int | None = None to AgentDef for per-agent control, enforced alongside the global limit.

Dependency

Relationship to other requests

This is Issue 3 of 3 in a series enabling recursive workflow composition:

Metadata

Metadata

Assignees

No one assigned

    Labels

    No labels
    No labels

    Type

    No type

    Projects

    No projects

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions