intermediatearchitecturenested-looparchitecturestate-sharingsubtasks

Nested Loop Architecture

Learn how to build loops within loops in AI agent systems, including sequential, parallel, and conditional nesting patterns, state sharing between nested loops, and practical implementation examples.

Overview

The nested loop architecture extends the Single Loop Architecture by embedding loops within loops. The outer loop manages the high-level workflow -- planning subtasks, delegating work, and evaluating the aggregate -- while inner loops handle individual subtasks with their own execute-evaluate-iterate cycles. Each subtask converges independently before its result feeds into the outer loop's evaluation.

This pattern is visible across every production agent framework. CrewAI (github.com/crewAIInc/crewAI) orchestrates nested loops through its hierarchical task delegation: a manager agent runs the outer loop, while each crew member runs an inner loop with its own context and tools. LangGraph (github.com/langchain-ai/langgraph) builds nested loops as sub-graphs within a parent graph, where each sub-graph has its own state, nodes, and edges. OpenHands (github.com/All-Hands-AI/OpenHands) nests an inner execution loop (edit code, run tests, observe output) inside an outer task loop (receive task, plan steps, verify result).

The nesting depth is deliberate. Each level adds observability overhead and token cost, so the architecture demands clear interfaces between levels and per-loop iteration limits to prevent any single subtask from consuming all resources.

Concept: Loops Within Loops

In a nested loop system, each level operates with its own executor, evaluator, and feedback channel. The outer loop delegates subtasks to inner loops, collects their results, and evaluates the aggregate. This creates a tree of loops where each node has its own convergence criteria.

Outer Loop (Workflow Manager)
    |
    +-- Inner Loop 1 (Subtask: Research)
    |       execute --> evaluate --> feedback (repeat until quality met)
    |
    +-- Inner Loop 2 (Subtask: Draft)
    |       execute --> evaluate --> feedback (repeat until quality met)
    |
    +-- Inner Loop 3 (Subtask: Review)
            execute --> evaluate --> feedback (repeat until quality met)
    |
    Outer: aggregate results --> evaluate --> feedback (repeat if needed)

Nesting Patterns

Sequential Nesting

Inner loops execute one after another, with each inner loop receiving context from the previous one. This is the most common pattern and maps directly to CrewAI's sequential process mode, where crew members complete their tasks in order and pass outputs downstream.

class SequentialNestedLoop:
    def __init__(self, inner_loops: list[SingleLoop]):
        self.inner_loops = inner_loops

    def run(self, task: Task) -> Result:
        context = task.initial_context
        for loop in self.inner_loops:
            subtask = Task(
                description=loop.name,
                input_data=context,
                threshold=task.subtask_threshold
            )
            result = loop.run(subtask)
            context = {"previous_output": result.output, "context": context}
        return Result(task_id=task.id, output=context)

Parallel Nesting

Inner loops execute simultaneously, and results are merged afterward. This pattern maximizes throughput for independent subtasks and is what Cursor 2.0 achieves with up to 8 parallel agents, and what MetaGPT (github.com/geekan/MetaGPT, 45K+ stars) implements through its multi-role parallel execution.

class ParallelNestedLoop:
    def __init__(self, inner_loops: dict[str, SingleLoop], merger):
        self.inner_loops = inner_loops
        self.merger = merger

    def run(self, task: Task) -> Result:
        subtasks = self._split_task(task)

        with ThreadPoolExecutor() as executor:
            futures = {
                name: executor.submit(loop.run, subtask)
                for name, (loop, subtask) in zip(self.inner_loops.keys(), subtasks)
            }

            results = {
                name: future.result()
                for name, future in futures.items()
            }

        merged = self.merger.merge(results)
        return Result(task_id=task.id, output=merged)

Real-world example: MetaGPT runs parallel inner loops for different software engineering roles -- Product Manager writes requirements, Architect designs the system, Project Manager breaks down tasks, and Engineers write code. Each role runs its own inner loop with specialized prompts and tools. The outer loop collects all role outputs and runs a QA agent to verify consistency.

Conditional Nesting

Inner loops are activated only when specific conditions are met. This prevents unnecessary work and token spend on subtasks that do not apply to the current task.

class ConditionalNestedLoop:
    def __init__(self, conditions: dict[str, Callable], loops: dict[str, SingleLoop]):
        self.conditions = conditions
        self.loops = loops

    def run(self, task: Task) -> Result:
        results = {}
        for name, condition in self.conditions.items():
            if condition(task):
                subtask = self._create_subtask(task, name)
                results[name] = self.loops[name].run(subtask)
            else:
                results[name] = self._default_result(name, task)
        return Result(task_id=task.id, output=results)

Real-world example: SWE-Agent (github.com/princeton-nlp/SWE-Agent, 15K+ stars) uses conditional nesting in its research-oriented approach. Given a GitHub issue, it conditionally activates different inner loops: a search loop if the issue references unfamiliar code, an edit loop if the fix location is known, and a test loop if the issue includes reproduction steps. Each conditional activation saves tokens by skipping irrelevant subtasks.

OpenHands (github.com/All-Hands-AI/OpenHands) implements a similar conditional pattern. The outer loop classifies the task and activates specific inner execution loops based on the classification: a CodingAgent loop for code modifications, a BrowsingAgent loop for web research, and a DataAnalysisAgent loop for notebook-based tasks.

State Sharing Between Nested Loops

There are three primary approaches:

ApproachDescriptionTrade-off
Top-down contextOuter loop passes context to inner loopsSimple, but inner loops cannot influence outer state
Bottom-up resultsInner loops return results to outer loopClean separation, but limited cross-loop awareness
Shared state storeAll loops read/write to a shared storeMaximum flexibility, but requires conflict management

Top-Down Context: The CLAUDE.md Pattern

Claude Code (github.com/anthropics/claude-code) uses top-down context passing through CLAUDE.md files. The outer loop defines project-level conventions, coding standards, and quality gates. Each inner loop reads these constraints at the start of its iteration. Inner loops cannot modify the outer context, which keeps the architecture predictable.

# CLAUDE.md for a nested-loop project

## Outer Loop Rules
- Process tasks in priority order from tasks.json
- Max 3 outer iterations before accepting best-effort
- Budget cap: 100K tokens per task batch

## Inner Loop Rules (inherited by all subtask loops)
- All tests must pass before marking a subtask complete
- Run `mypy src/` and `pytest tests/ -v` after each edit
- Max 5 inner iterations per subtask

Shared State Implementation

For more complex scenarios where loops need bidirectional communication, a shared state store with scoped access prevents conflicts:

class NestedLoopState:
    def __init__(self):
        self._store: dict[str, Any] = {}
        self._lock = threading.Lock()

    def set(self, key: str, value: Any, scope: str = "global"):
        with self._lock:
            full_key = f"{scope}:{key}"
            self._store[full_key] = {
                "value": value,
                "timestamp": time.time(),
                "scope": scope
            }

    def get(self, key: str, scope: str = "global") -> Any:
        with self._lock:
            return self._store.get(f"{scope}:{key}")

    def get_context(self, loop_id: str) -> dict:
        with self._lock:
            return {
                k.split(":", 1)[1]: v["value"]
                for k, v in self._store.items()
                if v["scope"] in ("global", loop_id)
            }

This scoped approach implements the InfoQ compression and retention principles: global state holds only essential constraints (compressed), while each loop retains its own working state. Cline (github.com/cline/cline) uses a similar pattern through its MCP protocol support, where shared context is managed through a server that both the outer and inner agent loops connect to.

Complete Nested Loop Example

Putting the patterns together, here is a complete nested loop implementation with state sharing, per-loop iteration limits, and divergence detection:

class NestedLoop:
    def __init__(self, outer_loop: SingleLoop, inner_loop_factory, state: NestedLoopState):
        self.outer_loop = outer_loop
        self.inner_loop_factory = inner_loop_factory
        self.state = state

    def run(self, task: Task) -> Result:
        for iteration in range(task.max_iterations):
            plan = self._plan_subtasks(task)
            subtask_results = {}

            for subtask_def in plan:
                inner_loop = self.inner_loop_factory.create(
                    subtask=subtask_def,
                    shared_state=self.state
                )
                result = inner_loop.run(subtask_def)
                subtask_results[subtask_def.name] = result.output

            aggregated = self._aggregate(subtask_results)
            feedback = self.outer_loop.evaluator.evaluate(task, aggregated)

            if feedback.passed:
                return Result(task_id=task.id, output=aggregated)

        return Result(task_id=task.id, output=aggregated, status="best_effort")

Configuration Parameters for Nested Loops

Every level in the nesting hierarchy needs its own configuration. The outer loop and each inner loop should have independent limits:

ParameterOuter LoopInner LoopRationale
max_iterations3-55-10Outer loop should converge quickly; inner loops get more attempts
token_budgetProportional to task countProportional to subtask complexityBudget splits across active loops
time_limit30-60 min total5-10 min per subtaskProportional timeouts prevent resource starvation
quality_threshold0.85-0.950.8-0.9Outer loop enforces higher standards on the aggregate

Real-World Implementations

CrewAI: Hierarchical Agent Orchestration

CrewAI (github.com/crewAIInc/crewAI) implements nested loops through its manager-worker pattern:

from crewai import Agent, Task, Crew, Process

researcher = Agent(role="Researcher", goal="Gather information", verbose=True)
writer = Agent(role="Writer", goal="Write documentation", verbose=True)

research_task = Task(
    description="Research the API endpoints and their parameters",
    agent=researcher
)
writing_task = Task(
    description="Write API documentation based on research findings",
    agent=writer
)

# Sequential nesting: research loop runs first, then writing loop
crew = Crew(
    agents=[researcher, writer],
    tasks=[research_task, writing_task],
    process=Process.sequential,
    max_iter=10  # Per-task inner loop limit
)
result = crew.kickoff()

Each task in CrewAI runs its own inner loop where the assigned agent iterates until the task is complete. The Crew manages the outer loop, delegating tasks and collecting results.

OpenHands: Multi-Stage Autonomous Engineering

OpenHands (github.com/All-Hands-AI/OpenHands) nests loops for end-to-end software engineering:

  • Outer loop: Receives the task, plans a multi-step approach, executes each step through inner loops, and verifies the final result
  • Inner loops: Each step (edit code, run tests, browse docs) runs its own execution loop with independent iteration limits

The conditional nesting pattern in OpenHands is particularly effective. A task like "fix the authentication bug in the login handler" triggers an edit inner loop (modify code), a test inner loop (verify the fix), and only conditionally a browsing inner loop (if the developer's research phase identifies unclear API semantics).

When to Use Nested Loops

Use nested loops when:

  • A task naturally decomposes into sequential or parallel subtasks, each requiring its own quality control cycle
  • Subtask quality directly determines the quality of the aggregate result (a weak research phase produces a weak final draft)
  • You need independent convergence at different granularity levels
  • The subtasks are complex enough to benefit from multiple iterations, not trivially executed in one shot

Use simpler architectures when:

  • A single loop can handle the entire task (most coding tasks in Aider or Claude Code fit this pattern)
  • Subtasks are so simple that a single iteration suffices -- the inner loop overhead is not justified
  • The task is exploratory and the decomposition is not known in advance

Move to Multi-Level Task Loop when nesting goes deeper than two levels and cross-level communication becomes critical. Move to Multi-Agent Loop when different subtasks genuinely require different agent expertise rather than just different iteration budgets.

Best Practices

  1. Isolate inner loop state by default; share only what is necessary -- use top-down context passing (like CLAUDE.md) for constraints and bottom-up results for outputs. Reserve shared state stores for cases where bidirectional communication is genuinely required.

  2. Define clear interfaces between outer and inner loops -- each inner loop should receive a well-scoped task description and acceptance criteria, and return a structured result. This is what makes CrewAI and LangGraph composable.

  3. Set per-loop iteration limits independently -- the outer loop should have a tight limit (3-5 iterations), while inner loops get more room (5-10). This prevents any single subtask from consuming all resources.

  4. Propagate timeouts proportionally -- if the outer loop has a 30-minute budget and there are 5 subtasks, each inner loop should have a 5-minute timeout, not 30 minutes.

  5. Use structured logging with full hierarchy -- every log entry should include the loop path (e.g., [outer:iteration-2] [inner:research:iteration-3]). This enables debugging by tracing exactly which loop and iteration produced a failure.

  6. Design the evaluator first -- as with single loops, the evaluator determines when the loop stops. In nested architectures, design both the inner evaluator (does this subtask meet its criteria?) and the outer evaluator (do the aggregated results meet the overall goal?) before implementing executors.