Module 1 — The Execution Loop

Course: Master Course — Harness Engineering (12+ Hours) Module: 1 — The Execution Loop Duration: 90 minutes Level: Senior Engineer and above Prerequisites: Module 0 complete (the three-job definition, the ecosystem, the rubric)


Learning Objectives

After completing this module, you will be able to:

  1. Identify any of the five loop architectures in a harness's source and name its tradeoffs.
  2. Choose a loop architecture for a given use case — and defend the choice against the four alternatives.
  3. Design a complete stop-condition system (budget, end-turn, max-iter, error-threshold, human) and explain why "no stop condition" is a production defect.
  4. Choose among the four subagent patterns (agents-as-tools, handoffs, fork, worktree) based on the control/visibility tradeoff.
  5. Specify the per-turn observability payload and explain why a loop without it is un-debuggable.

1.1 — Loop Architectures

The heartbeat. Everything else plugs into it.

The five architectures

There are five loop architectures in production harnesses. The choice among them is the first and most consequential decision in the rubric (Module 1, "Execution Loop"), and it shapes everything downstream — tool design, context management, error handling, observability.

Architecture Core idea Examples
ReAct / TAO Thought → Action → Observation, repeated until stop Aider, Pi, most CLI harnesses
Plan-then-Execute Model produces a full plan first, then executes steps sequentially Claude Code (partially), OpenAI Agents SDK
Graph-based Nodes and edges define state transitions explicitly in code LangGraph
Dumb Loop Harness handles only stable transition logic; all reasoning delegated to the model Anthropic's philosophy, Pi
Conversation-driven Agents communicate via message passing; orchestration emerges from dialogue AutoGen

We will take each in turn, name the tradeoff, and show the code shape.

1.1.1 ReAct / TAO

ReAct (Yao et al., 2022, arXiv:2210.03629) is the foundational pattern. The model emits a Thought (reasoning about what to do next), an Action (a tool call), and receives an Observation (the tool's result). Repeat until the model produces a final answer with no tool call.

The minimal ReAct loop, in TypeScript:

async function reactLoop(task: string): Promise<string> {
  const messages: Message[] = [
    { role: "system", content: SYSTEM_PROMPT },
    { role: "user", content: task }
  ];

  while (true) {
    const response = await model.complete(messages);

    if (response.stopReason === "end_turn") {
      return response.content;  // Model says it's done
    }

    if (response.toolUse) {
      const result = await executeTool(response.toolUse);
      messages.push({ role: "assistant", content: response.content });
      messages.push({ role: "tool", content: result });
    }
  }
}

Gains: simple, debuggable, model-native. The model does what it is best at (reasoning step-by-step) and the harness does only what it must (executing tools and maintaining context).

Costs: errors compound across steps. If the model takes a wrong action at step 3, steps 4 through N inherit the error. No lookahead — the model cannot plan multiple steps ahead in a structured way. Module 7 (error handling) exists largely to mitigate this compounding.

Read this in real code: Tau (DD-21). The clearest ReAct loop in any open-source harness is Tau's tau_agent/loop.py — ~230 lines of fully-typed async Python. It's the teaching companion to this section. Three things make it worth reading alongside this module:

  1. The loop is stateless. It takes the messages list as an argument and appends to it. The caller (the harness) owns all state. This makes the loop trivially testable with a FakeProvider.
  2. Events are the sole output. The loop yields a typed AgentEvent union — never calls callbacks, never imports a UI library. A JSON event consumer is a structured log; a Textual consumer is a live UI.
  3. Tool execution is an isolation boundary. Each tool call runs inside try/except Exception. A tool failure becomes AgentToolResult(ok=False), never propagates. The loop cannot be crashed by a tool.

Clone github.com/huggingface/tau, open src/tau_agent/loop.py, and trace the flow: provider stream → event translation → tool dispatch → queue drain → repeat. That's the heartbeat, in readable code.

1.1.2 Plan-then-Execute

Plan-then-Execute separates planning from execution. The model first produces a complete plan (an ordered list of steps), then the harness executes the steps sequentially, often with per-step verification.

The 3.7× claim, precisely. LLMCompiler (Kim et al., 2024, arXiv:2312.04511) reports up to 3.7× latency speedup (ICML version); the conservative figure from the original arXiv preprint is 2.27×. Plan-then-Execute is approximately 3.7× faster than ReAct on complex tasks, primarily because planning can be parallelized and execution avoids the round-trip latency of interleaved reasoning. This is the number most often cited and most often mis-cited. The precise reading: the speedup holds on complex, decomposable tasks where the plan is correct. It inverts when the plan is wrong.

Gains: 3.7× faster on complex tasks with correct plans. Avoids the mid-task drift that plagues long ReAct loops. Enables parallel execution of independent plan steps (the LLMCompiler insight).

Costs: brittle if the early plan is wrong. Every subsequent step inherits the plan's error, with no mid-execution recovery. Requires plan validation — an extra pass to check the plan before executing, which adds latency and its own failure mode (what if the validator is wrong?).

The design question: how predictable is your task? Predictable tasks (CI/CD pipelines, fixed workflows) reward planning. Unpredictable tasks (debugging, exploration, security research) punish it — the plan will be wrong, and the wrongness compounds.

1.1.3 Graph-based (LangGraph)

LangGraph makes the state machine explicit. You define nodes (functions) and edges (transitions) in code; the harness executes the graph, managing state at each node boundary.

from langgraph.graph import StateGraph

graph = StateGraph(State)
graph.add_node("plan", plan_node)
graph.add_node("execute", execute_node)
graph.add_node("verify", verify_node)
graph.add_edge("plan", "execute")
graph.add_edge("execute", "verify")
graph.add_conditional_edges("verify", lambda s: "execute" if s.needs_work else "done")

Gains: explicit, testable state machine. You can see every possible transition in code. Easy to add or remove nodes. Checkpointing is native (Module 8). The interrupt() primitive enables human-in-the-loop at any node (Module 6).

Costs: more code to maintain. Harder to evolve as model capabilities grow — a rigid graph that encodes yesterday's best practice becomes tomorrow's constraint. The "fights model capability growth" problem. If the model gets smart enough to handle a transition the graph forces, the graph is now overhead.

LangGraph is the right choice when the process is the product (a regulated workflow, a multi-approval pipeline, a compliance-governed agent). It is the wrong choice when you want the model to figure out the process (open-ended research, creative tasks, exploration).

1.1.4 The Dumb Loop

The dumb loop is Anthropic's explicit philosophy, realized most purely in Pi. The harness handles only the stable transition logic — calling the model, parsing tool calls, executing tools, appending results. All reasoning is delegated to the model. No plan validation. No explicit state machine. No sophisticated control flow.

Gains: co-evolves with model improvements. When the underlying model upgrades, agent performance improves naturally because the harness does not constrain it. Thin code surface — Pi is ~1,200 lines. The Manus finding (Module 0.2 source): refactored five times in six months, each time getting thinner, performance improving each time.

Costs: debugging is harder. When something goes wrong, there is less structure to inspect. Reliability depends heavily on the model. A dumb loop with a weak model is unreliable; a dumb loop with a strong model is often state-of-the-art.

The dumb loop passes what we call the future-proof test: when the underlying model upgrades, does agent performance improve? If yes, your harness is not constraining the model. If no, your harness has become the constraint. Most over-engineered harnesses fail this test.

1.1.5 Conversation-driven (AutoGen)

AutoGen's GroupChat has multiple agents converse. A manager agent decides who speaks next. Orchestration emerges from the dialogue rather than from a pre-defined graph.

Gains: natural for multi-agent. Emergent collaboration — agents can help each other in ways the designer did not anticipate.

Costs: non-deterministic. Hard to audit — the execution trace is a conversation, not a graph. Can loop. The "who speaks next" decision is itself a failure mode (a bad manager agent derails the whole run).

Conversation-driven is the right choice for brainstorming and debate tasks (where emergent interaction is the point). It is the wrong choice for execution tasks (where determinism and auditability matter).

1.1.6 The choice

The architecture choice is the first rubric decision (Module 1, "Execution Loop"). State it as a decision, not a feature:

This harness uses [architecture] because [use case demands X], trading [cost] for [gain]. For a use case demanding [Y], we would choose [alternative] instead.

A senior engineer can produce that sentence with all five blanks filled, and defend each.


1.2 — Stop Conditions

When does the loop exit? This is safety logic, not an afterthought.

The five stop conditions

Every production loop must have at least one explicit stop condition. A loop with no stop condition is a production defect — it is the "infinite loop problem."

Condition What it catches Mechanism
Token budget exhaustion Runaway cost Cumulative token counter; halt at threshold
No-tool-call response Task completion Model emits end_turn / stop_reason: stop
Max iterations cap Stuck loops, runaway depth Hard counter; halt at N turns
Error threshold Cascading failure N consecutive errors → halt
Human interrupt Approval gate, ambiguity HITL checkpoint; loop pauses indefinitely

Token budget is the financial stop. Without it, a single runaway session can produce a five-figure bill. The budget must be cumulative across all calls in the session (Module 7), not a per-call max_tokens (which limits only one completion).

No-tool-call is the natural stop. The model says "I'm done" by emitting a response with no tool call. This is what we want — but it cannot be the only stop condition, because the model can fail to emit it (a hallucinated infinite task) or emit it prematurely (a false completion).

Max iterations is the safety net for stuck loops. Pi caps at a fixed number of turns. Claude Code uses a higher cap with compaction (Module 3) to extend effective context. The cap is the loop's hard ceiling.

Error threshold is the cascading-failure stop. N consecutive errors (typically 3–5) → halt. Without it, a misbehaving tool can trap the agent in an error-retry loop that consumes the entire budget. Module 7.2 builds a stuck-loop detector for exactly this.

Human interrupt is the approval stop. LangGraph's interrupt() serializes state and waits indefinitely for a human decision. This is the foundation of human-in-the-loop design (Module 6.2).

The infinite loop problem

What happens when none of these conditions trigger? The model keeps calling tools, each call costs money, and there is no natural exit. This is the infinite loop problem, and it is more common than it sounds.

Three failure modes produce it:

  1. The model hallucinates ongoing work. The task is done, but the model does not recognize it and keeps acting. Cured by max-iterations and by verification (Module 9).
  2. The tool keeps failing in a way the model keeps retrying. Cured by the error threshold and stuck-loop detection (Module 7.2).
  3. The context fills with noise and the model loses track of the original task. Cured by context management (Module 3) and the token budget.

A production loop has all five stop conditions. A toy loop has one or two. The difference between "works in a demo" and "works in production" is largely the stop conditions.


1.3 — Subagent Delegation from the Loop

When and how does the loop spawn additional agents?

The four patterns

When a task exceeds a single context window or requires parallel execution, the loop spawns subagents. There are four patterns, and they differ primarily in who keeps control and what returns to the parent.

Pattern Control Returns to parent Parallelism Examples
Agents-as-tools Parent stays in control Tool result (structured) Synchronous OpenAI Agents SDK
Handoffs Terminal — parent loses visibility Nothing (handoff is one-way) One-way OpenAI Agents SDK
Fork Isolated; no shared state Summary or nothing Independent Claude Code
Worktree Parallel; each in a git worktree Merged via git True parallel Claude Code

Agents-as-tools

The subagent is registered as a callable tool. The parent calls it like any other tool, waits synchronously, and gets back a structured result.

const subagents = {
  "research_agent": { schema: ResearchInputSchema, run: runResearchAgent },
  "code_review_agent": { schema: CodeReviewInputSchema, run: runCodeReviewAgent }
};

// In the parent loop:
if (response.toolUse.name === "research_agent") {
  const result = await subagents.research_agent.run(response.toolUse.input);
  // result returns to parent context, bounded to 1-2k tokens
}

Gains: parent stays in control. The result is structured and bounded (Module 3: subagent returns should be 1–2k tokens to avoid polluting parent context). Synchronous — easy to reason about.

Costs: no parallelism. The parent blocks while the subagent runs. Context can still pollute if the return bound is not enforced.

Handoffs

The parent explicitly transfers control to a specialized subagent. The handoff is terminal — the parent does not see what happens after.

# OpenAI Agents SDK
result = await Runner.run(specialist_agent, input=context)
# control returns to parent only if the specialist hands back

Gains: clean specialization. The specialist sees a fresh, focused context. No context bleed from the parent.

Costs: one-way. The parent loses visibility after the handoff. If the specialist fails, the parent may not know how or why. Harder to audit end-to-end.

Fork

An isolated copy of the parent agent runs independently. No shared state. Errors in the fork do not propagate to the parent.

Gains: isolation. The fork can attempt risky operations without endangering the parent. Useful for "try three approaches in parallel and pick the best."

Costs: coordination overhead. Duplicate state. The parent must decide how to reconcile multiple forks.

Worktree

Each parallel agent runs in a separate Git worktree. True parallelism with a defined merge step. Claude Code uses this for parallel development.

Gains: true parallelism. Output is merge-able via Git's native machinery.

Costs: Git conflicts. Requires a merge step. The merge step is itself a failure mode (conflicts the model must resolve).

oh-my-opencode's hierarchy — a case study

oh-my-opencode (Module 0.2's canonical meta-harness) implements a three-tier subagent hierarchy: Prometheus (planner) → Atlas (orchestrator) → Junior (workers). This is worth studying because it is the most developed production example of nested delegation.

The hierarchy is a hybrid: Prometheus→Atlas is agents-as-tools (Atlas returns a structured summary); Atlas→Junior is worktree (parallel execution with merge). Studying it teaches you that real systems mix patterns — and which mix is itself a design decision worth auditing.


1.4 — Loop Observability

What the loop should emit at every turn. Without it, debugging is divination.

The per-turn payload

A production loop emits a structured record at every turn. This is the substrate for Module 10 (observability) and the foundation of post-mortem debugging. The minimum payload:

Field Why
trace_id Correlates all events in a session
turn_number Which iteration of the loop
tool_name Which tool was called (or null for end_turn)
input_hash Hash of the tool input (for dedup, replay)
output_hash Hash of the tool output
latency_ms Time from call to result
token_delta Tokens consumed this turn (input + output)
stop_reason Why the turn ended (end_turn, max_iter, error, budget)

Why hashes, not full content? Full inputs and outputs are large (Module 3: tool outputs are 67.6% of context) and may contain secrets. Hashes let you dedup, detect stuck loops (same input + same output = no progress), and replay selectively. Full content can be logged to a separate, access-controlled store.

Pi vs Claude Code — what gets logged

Pi logs minimally — console output with tool names and turn markers. This is consistent with Pi's thin philosophy: observe enough to debug, no more.

Claude Code logs extensively — structured traces with full tool inputs/outputs, token accounting per turn, session diffs, and replay capability. This is consistent with Claude Code's thick philosophy: observability is a feature you need in enterprise, multi-tenant, auditable environments.

Neither is wrong. The observability payload is itself a design decision on the thickness spectrum. But there is a floor: a loop that emits nothing is un-debuggable. Pi's minimal logging is still above that floor; a loop with no logging is below it.

Read this in real code: Tau's event union (DD-21). Tau solves the observability problem by making the event stream the observability layer. Its events.py defines 14 frozen Pydantic models — AgentStart, TurnStart, MessageDelta, ToolExecutionStart/End, Retry, Error, etc. — as a closed union with extra="forbid". The same event stream drives print mode (a terminal transcript), Rich (live rendering), Textual (interactive TUI), JSON export (structured log), and HTML transcript rendering. You don't need a separate telemetry system — events ARE the trace. See src/tau_agent/events.py.

Building an observability wrapper

You can add observability to any loop without modifying the harness core, by wrapping the model-call and tool-execution functions:

function withObservability(loop: Loop): Loop {
  return {
    ...loop,
    callModel: async (messages) => {
      const start = Date.now();
      const response = await loop.callModel(messages);
      emit({
        trace_id: getCurrentTraceId(),
        turn_number: getTurnCount(),
        latency_ms: Date.now() - start,
        token_delta: response.usage.total_tokens,
        stop_reason: response.stopReason
      });
      return response;
    },
    executeTool: async (toolCall) => {
      const start = Date.now();
      const result = await loop.executeTool(toolCall);
      emit({
        trace_id: getCurrentTraceId(),
        turn_number: getTurnCount(),
        tool_name: toolCall.name,
        input_hash: hash(toolCall.input),
        output_hash: hash(result),
        latency_ms: Date.now() - start
      });
      return result;
    }
  };
}

This pattern — wrapping the loop to add observability without modifying it — is the same pattern you will use in Module 6 (adding permission gates) and Module 11 (adding security checks). The loop's interface (callModel, executeTool) is the extension point; everything else plugs in around it.


Anti-Patterns

The infinite retry loop

A loop that retries every error indefinitely. Cured by the error-threshold stop condition and a typed error taxonomy (Module 7).

The graph that fights the model

A LangGraph state machine so rigid that when the model could handle a transition naturally, the graph forces it through a suboptimal path. Cured by the dumb-loop philosophy: only encode transitions the model genuinely cannot handle.

The un-observable loop

A loop that emits no structured per-turn data. When it fails (and it will), you cannot reproduce the failure. Cured by the observability wrapper above — there is no excuse to ship a loop without it.

Subagents with unbounded returns

A subagent that returns its full context to the parent, polluting the parent's window and re-billing tokens. Cured by the 1–2k token bound on subagent returns (Module 3).


Key Terms

Term Definition
ReAct Thought → Action → Observation loop; the foundational pattern
Plan-then-Execute Plan fully first, then execute; 3.7× faster on complex tasks but brittle if plan is wrong
Dumb Loop Delegate all reasoning to the model; harness handles only transition logic
Stop condition The mechanism that exits the loop; a safety system, not an afterthought
Infinite loop problem The failure mode when no stop condition triggers
Agents-as-tools Subagent registered as a callable tool; parent stays in control; synchronous
Handoff Terminal transfer of control to a subagent; parent loses visibility
Future-proof test Does agent performance improve when the underlying model upgrades?
Per-turn payload The structured record (trace_id, tool, hashes, latency, tokens, stop_reason) emitted every turn

Lab Exercise

See 07-lab-spec.md. Implement the same simple task (read a file, summarize, write the summary) in three architectures — ReAct and Plan-then-Execute in n8n, Graph-based in TypeScript. Instrument the loop with the per-turn observability payload. The architectural difference, felt on the same task, is what makes the tradeoffs stick.


References

  1. Yao et al. (2022)ReAct: Synergizing Reasoning and Acting in Language Models. arXiv:2210.03629. The foundational ReAct paper.
  2. Kim et al. (2023)LLMCompiler: An LLM Compiler for Parallel Function Calling. Source of the 3.7× Plan-then-Execute speedup claim (arXiv:2312.04511).
  3. LangGraph documentation — the graph-based state machine reference.
  4. AutoGen (Microsoft) — the conversation-driven multi-agent reference.
  5. Anthropic (2025)Claude Code: Best Practices for Agentic Coding. The dumb-loop philosophy and the future-proof test.
  6. Pi source code — the ~1,200-line reference implementation of the dumb loop.
  7. oh-my-opencode source — the Prometheus → Atlas → Junior subagent hierarchy case study.
  8. Module 0.1 — the n8n ReAct-loop workflow (Diagram 6), the primary visual for this module.
# Module 1 — The Execution Loop

**Course**: Master Course — Harness Engineering (12+ Hours)
**Module**: 1 — The Execution Loop
**Duration**: 90 minutes
**Level**: Senior Engineer and above
**Prerequisites**: Module 0 complete (the three-job definition, the ecosystem, the rubric)

---

## Learning Objectives

After completing this module, you will be able to:

1. Identify any of the five loop architectures in a harness's source and name its tradeoffs.
2. Choose a loop architecture for a given use case — and defend the choice against the four alternatives.
3. Design a complete stop-condition system (budget, end-turn, max-iter, error-threshold, human) and explain why "no stop condition" is a production defect.
4. Choose among the four subagent patterns (agents-as-tools, handoffs, fork, worktree) based on the control/visibility tradeoff.
5. Specify the per-turn observability payload and explain why a loop without it is un-debuggable.

---

# 1.1 — Loop Architectures

*The heartbeat. Everything else plugs into it.*

## The five architectures

There are five loop architectures in production harnesses. The choice among them is the first and most consequential decision in the rubric (Module 1, "Execution Loop"), and it shapes everything downstream — tool design, context management, error handling, observability.

| Architecture | Core idea | Examples |
| --- | --- | --- |
| **ReAct / TAO** | Thought → Action → Observation, repeated until stop | Aider, Pi, most CLI harnesses |
| **Plan-then-Execute** | Model produces a full plan first, then executes steps sequentially | Claude Code (partially), OpenAI Agents SDK |
| **Graph-based** | Nodes and edges define state transitions explicitly in code | LangGraph |
| **Dumb Loop** | Harness handles only stable transition logic; all reasoning delegated to the model | Anthropic's philosophy, Pi |
| **Conversation-driven** | Agents communicate via message passing; orchestration emerges from dialogue | AutoGen |

We will take each in turn, name the tradeoff, and show the code shape.

### 1.1.1 ReAct / TAO

ReAct (Yao et al., 2022, arXiv:2210.03629) is the foundational pattern. The model emits a Thought (reasoning about what to do next), an Action (a tool call), and receives an Observation (the tool's result). Repeat until the model produces a final answer with no tool call.

The minimal ReAct loop, in TypeScript:

```typescript
async function reactLoop(task: string): Promise<string> {
  const messages: Message[] = [
    { role: "system", content: SYSTEM_PROMPT },
    { role: "user", content: task }
  ];

  while (true) {
    const response = await model.complete(messages);

    if (response.stopReason === "end_turn") {
      return response.content;  // Model says it's done
    }

    if (response.toolUse) {
      const result = await executeTool(response.toolUse);
      messages.push({ role: "assistant", content: response.content });
      messages.push({ role: "tool", content: result });
    }
  }
}
```

**Gains**: simple, debuggable, model-native. The model does what it is best at (reasoning step-by-step) and the harness does only what it must (executing tools and maintaining context).

**Costs**: errors compound across steps. If the model takes a wrong action at step 3, steps 4 through N inherit the error. No lookahead — the model cannot plan multiple steps ahead in a structured way. Module 7 (error handling) exists largely to mitigate this compounding.

> **Read this in real code: Tau (DD-21).** The clearest ReAct loop in any open-source harness is Tau's `tau_agent/loop.py` — ~230 lines of fully-typed async Python. It's the teaching companion to this section. Three things make it worth reading alongside this module:
>
> 1. **The loop is stateless.** It takes the `messages` list as an argument and appends to it. The caller (the harness) owns all state. This makes the loop trivially testable with a `FakeProvider`.
> 2. **Events are the sole output.** The loop yields a typed `AgentEvent` union — never calls callbacks, never imports a UI library. A JSON event consumer is a structured log; a Textual consumer is a live UI.
> 3. **Tool execution is an isolation boundary.** Each tool call runs inside `try/except Exception`. A tool failure becomes `AgentToolResult(ok=False)`, never propagates. The loop cannot be crashed by a tool.
>
> Clone `github.com/huggingface/tau`, open `src/tau_agent/loop.py`, and trace the flow: provider stream → event translation → tool dispatch → queue drain → repeat. That's the heartbeat, in readable code.

### 1.1.2 Plan-then-Execute

Plan-then-Execute separates planning from execution. The model first produces a complete plan (an ordered list of steps), then the harness executes the steps sequentially, often with per-step verification.

**The 3.7× claim, precisely.** LLMCompiler (Kim et al., 2024, arXiv:2312.04511) reports up to 3.7× latency speedup (ICML version); the conservative figure from the original arXiv preprint is 2.27×. Plan-then-Execute is approximately 3.7× faster than ReAct on complex tasks, primarily because planning can be parallelized and execution avoids the round-trip latency of interleaved reasoning. This is the number most often cited and most often mis-cited. The precise reading: the speedup holds on *complex, decomposable* tasks where the plan is correct. It inverts when the plan is wrong.

**Gains**: 3.7× faster on complex tasks with correct plans. Avoids the mid-task drift that plagues long ReAct loops. Enables parallel execution of independent plan steps (the LLMCompiler insight).

**Costs**: brittle if the early plan is wrong. Every subsequent step inherits the plan's error, with no mid-execution recovery. Requires plan validation — an extra pass to check the plan before executing, which adds latency and its own failure mode (what if the validator is wrong?).

The design question: *how predictable is your task?* Predictable tasks (CI/CD pipelines, fixed workflows) reward planning. Unpredictable tasks (debugging, exploration, security research) punish it — the plan will be wrong, and the wrongness compounds.

### 1.1.3 Graph-based (LangGraph)

LangGraph makes the state machine explicit. You define nodes (functions) and edges (transitions) in code; the harness executes the graph, managing state at each node boundary.

```python
from langgraph.graph import StateGraph

graph = StateGraph(State)
graph.add_node("plan", plan_node)
graph.add_node("execute", execute_node)
graph.add_node("verify", verify_node)
graph.add_edge("plan", "execute")
graph.add_edge("execute", "verify")
graph.add_conditional_edges("verify", lambda s: "execute" if s.needs_work else "done")
```

**Gains**: explicit, testable state machine. You can see every possible transition in code. Easy to add or remove nodes. Checkpointing is native (Module 8). The `interrupt()` primitive enables human-in-the-loop at any node (Module 6).

**Costs**: more code to maintain. Harder to evolve as model capabilities grow — a rigid graph that encodes yesterday's best practice becomes tomorrow's constraint. The "fights model capability growth" problem. If the model gets smart enough to handle a transition the graph forces, the graph is now overhead.

LangGraph is the right choice when the *process* is the product (a regulated workflow, a multi-approval pipeline, a compliance-governed agent). It is the wrong choice when you want the model to figure out the process (open-ended research, creative tasks, exploration).

### 1.1.4 The Dumb Loop

The dumb loop is Anthropic's explicit philosophy, realized most purely in Pi. The harness handles only the stable transition logic — calling the model, parsing tool calls, executing tools, appending results. *All reasoning is delegated to the model.* No plan validation. No explicit state machine. No sophisticated control flow.

**Gains**: co-evolves with model improvements. When the underlying model upgrades, agent performance improves naturally because the harness does not constrain it. Thin code surface — Pi is ~1,200 lines. The Manus finding (Module 0.2 source): refactored five times in six months, each time getting *thinner*, performance improving each time.

**Costs**: debugging is harder. When something goes wrong, there is less structure to inspect. Reliability depends heavily on the model. A dumb loop with a weak model is unreliable; a dumb loop with a strong model is often state-of-the-art.

The dumb loop passes what we call the **future-proof test**: when the underlying model upgrades, does agent performance improve? If yes, your harness is not constraining the model. If no, your harness has become the constraint. Most over-engineered harnesses fail this test.

### 1.1.5 Conversation-driven (AutoGen)

AutoGen's GroupChat has multiple agents converse. A manager agent decides who speaks next. Orchestration emerges from the dialogue rather than from a pre-defined graph.

**Gains**: natural for multi-agent. Emergent collaboration — agents can help each other in ways the designer did not anticipate.

**Costs**: non-deterministic. Hard to audit — the execution trace is a conversation, not a graph. Can loop. The "who speaks next" decision is itself a failure mode (a bad manager agent derails the whole run).

Conversation-driven is the right choice for *brainstorming* and *debate* tasks (where emergent interaction is the point). It is the wrong choice for *execution* tasks (where determinism and auditability matter).

### 1.1.6 The choice

The architecture choice is the first rubric decision (Module 1, "Execution Loop"). State it as a decision, not a feature:

> *This harness uses [architecture] because [use case demands X], trading [cost] for [gain]. For a use case demanding [Y], we would choose [alternative] instead.*

A senior engineer can produce that sentence with all five blanks filled, and defend each.

---

# 1.2 — Stop Conditions

*When does the loop exit? This is safety logic, not an afterthought.*

## The five stop conditions

Every production loop must have at least one explicit stop condition. A loop with no stop condition is a production defect — it is the "infinite loop problem."

| Condition | What it catches | Mechanism |
| --- | --- | --- |
| **Token budget exhaustion** | Runaway cost | Cumulative token counter; halt at threshold |
| **No-tool-call response** | Task completion | Model emits `end_turn` / `stop_reason: stop` |
| **Max iterations cap** | Stuck loops, runaway depth | Hard counter; halt at N turns |
| **Error threshold** | Cascading failure | N consecutive errors → halt |
| **Human interrupt** | Approval gate, ambiguity | HITL checkpoint; loop pauses indefinitely |

**Token budget** is the financial stop. Without it, a single runaway session can produce a five-figure bill. The budget must be *cumulative across all calls in the session* (Module 7), not a per-call `max_tokens` (which limits only one completion).

**No-tool-call** is the natural stop. The model says "I'm done" by emitting a response with no tool call. This is what we want — but it cannot be the *only* stop condition, because the model can fail to emit it (a hallucinated infinite task) or emit it prematurely (a false completion).

**Max iterations** is the safety net for stuck loops. Pi caps at a fixed number of turns. Claude Code uses a higher cap with compaction (Module 3) to extend effective context. The cap is the loop's hard ceiling.

**Error threshold** is the cascading-failure stop. N consecutive errors (typically 3–5) → halt. Without it, a misbehaving tool can trap the agent in an error-retry loop that consumes the entire budget. Module 7.2 builds a stuck-loop detector for exactly this.

**Human interrupt** is the approval stop. LangGraph's `interrupt()` serializes state and waits indefinitely for a human decision. This is the foundation of human-in-the-loop design (Module 6.2).

## The infinite loop problem

What happens when *none* of these conditions trigger? The model keeps calling tools, each call costs money, and there is no natural exit. This is the infinite loop problem, and it is more common than it sounds.

Three failure modes produce it:

1. **The model hallucinates ongoing work.** The task is done, but the model does not recognize it and keeps acting. Cured by max-iterations and by verification (Module 9).
2. **The tool keeps failing in a way the model keeps retrying.** Cured by the error threshold and stuck-loop detection (Module 7.2).
3. **The context fills with noise and the model loses track of the original task.** Cured by context management (Module 3) and the token budget.

A production loop has *all five* stop conditions. A toy loop has one or two. The difference between "works in a demo" and "works in production" is largely the stop conditions.

---

# 1.3 — Subagent Delegation from the Loop

*When and how does the loop spawn additional agents?*

## The four patterns

When a task exceeds a single context window or requires parallel execution, the loop spawns subagents. There are four patterns, and they differ primarily in **who keeps control** and **what returns to the parent**.

| Pattern | Control | Returns to parent | Parallelism | Examples |
| --- | --- | --- | --- | --- |
| **Agents-as-tools** | Parent stays in control | Tool result (structured) | Synchronous | OpenAI Agents SDK |
| **Handoffs** | Terminal — parent loses visibility | Nothing (handoff is one-way) | One-way | OpenAI Agents SDK |
| **Fork** | Isolated; no shared state | Summary or nothing | Independent | Claude Code |
| **Worktree** | Parallel; each in a git worktree | Merged via git | True parallel | Claude Code |

### Agents-as-tools

The subagent is registered as a callable tool. The parent calls it like any other tool, waits synchronously, and gets back a structured result.

```typescript
const subagents = {
  "research_agent": { schema: ResearchInputSchema, run: runResearchAgent },
  "code_review_agent": { schema: CodeReviewInputSchema, run: runCodeReviewAgent }
};

// In the parent loop:
if (response.toolUse.name === "research_agent") {
  const result = await subagents.research_agent.run(response.toolUse.input);
  // result returns to parent context, bounded to 1-2k tokens
}
```

**Gains**: parent stays in control. The result is structured and bounded (Module 3: subagent returns should be 1–2k tokens to avoid polluting parent context). Synchronous — easy to reason about.

**Costs**: no parallelism. The parent blocks while the subagent runs. Context can still pollute if the return bound is not enforced.

### Handoffs

The parent explicitly transfers control to a specialized subagent. The handoff is *terminal* — the parent does not see what happens after.

```python
# OpenAI Agents SDK
result = await Runner.run(specialist_agent, input=context)
# control returns to parent only if the specialist hands back
```

**Gains**: clean specialization. The specialist sees a fresh, focused context. No context bleed from the parent.

**Costs**: one-way. The parent loses visibility after the handoff. If the specialist fails, the parent may not know how or why. Harder to audit end-to-end.

### Fork

An isolated copy of the parent agent runs independently. No shared state. Errors in the fork do not propagate to the parent.

**Gains**: isolation. The fork can attempt risky operations without endangering the parent. Useful for "try three approaches in parallel and pick the best."

**Costs**: coordination overhead. Duplicate state. The parent must decide how to reconcile multiple forks.

### Worktree

Each parallel agent runs in a separate Git worktree. True parallelism with a defined merge step. Claude Code uses this for parallel development.

**Gains**: true parallelism. Output is merge-able via Git's native machinery.

**Costs**: Git conflicts. Requires a merge step. The merge step is itself a failure mode (conflicts the model must resolve).

### oh-my-opencode's hierarchy — a case study

oh-my-opencode (Module 0.2's canonical meta-harness) implements a three-tier subagent hierarchy: **Prometheus** (planner) → **Atlas** (orchestrator) → **Junior** (workers). This is worth studying because it is the most developed production example of nested delegation.

- Prometheus produces the plan and delegates to Atlas.
- Atlas decomposes the plan into tasks and delegates to Junior agents.
- Junior agents execute tasks in the worktree pattern, returning summaries to Atlas.
- Atlas aggregates and returns to Prometheus.

The hierarchy is a hybrid: Prometheus→Atlas is agents-as-tools (Atlas returns a structured summary); Atlas→Junior is worktree (parallel execution with merge). Studying it teaches you that real systems mix patterns — and *which* mix is itself a design decision worth auditing.

---

# 1.4 — Loop Observability

*What the loop should emit at every turn. Without it, debugging is divination.*

## The per-turn payload

A production loop emits a structured record at every turn. This is the substrate for Module 10 (observability) and the foundation of post-mortem debugging. The minimum payload:

| Field | Why |
| --- | --- |
| **trace_id** | Correlates all events in a session |
| **turn_number** | Which iteration of the loop |
| **tool_name** | Which tool was called (or `null` for end_turn) |
| **input_hash** | Hash of the tool input (for dedup, replay) |
| **output_hash** | Hash of the tool output |
| **latency_ms** | Time from call to result |
| **token_delta** | Tokens consumed this turn (input + output) |
| **stop_reason** | Why the turn ended (end_turn, max_iter, error, budget) |

**Why hashes, not full content?** Full inputs and outputs are large (Module 3: tool outputs are 67.6% of context) and may contain secrets. Hashes let you dedup, detect stuck loops (same input + same output = no progress), and replay selectively. Full content can be logged to a separate, access-controlled store.

## Pi vs Claude Code — what gets logged

Pi logs minimally — console output with tool names and turn markers. This is consistent with Pi's thin philosophy: observe enough to debug, no more.

Claude Code logs extensively — structured traces with full tool inputs/outputs, token accounting per turn, session diffs, and replay capability. This is consistent with Claude Code's thick philosophy: observability is a feature you need in enterprise, multi-tenant, auditable environments.

Neither is wrong. The observability payload is itself a design decision on the thickness spectrum. But there is a floor: a loop that emits *nothing* is un-debuggable. Pi's minimal logging is still above that floor; a loop with no logging is below it.

> **Read this in real code: Tau's event union (DD-21).** Tau solves the observability problem by making the event stream the observability layer. Its `events.py` defines 14 frozen Pydantic models — `AgentStart`, `TurnStart`, `MessageDelta`, `ToolExecutionStart/End`, `Retry`, `Error`, etc. — as a closed union with `extra="forbid"`. The same event stream drives print mode (a terminal transcript), Rich (live rendering), Textual (interactive TUI), JSON export (structured log), and HTML transcript rendering. You don't need a separate telemetry system — events ARE the trace. See `src/tau_agent/events.py`.

## Building an observability wrapper

You can add observability to any loop without modifying the harness core, by wrapping the model-call and tool-execution functions:

```typescript
function withObservability(loop: Loop): Loop {
  return {
    ...loop,
    callModel: async (messages) => {
      const start = Date.now();
      const response = await loop.callModel(messages);
      emit({
        trace_id: getCurrentTraceId(),
        turn_number: getTurnCount(),
        latency_ms: Date.now() - start,
        token_delta: response.usage.total_tokens,
        stop_reason: response.stopReason
      });
      return response;
    },
    executeTool: async (toolCall) => {
      const start = Date.now();
      const result = await loop.executeTool(toolCall);
      emit({
        trace_id: getCurrentTraceId(),
        turn_number: getTurnCount(),
        tool_name: toolCall.name,
        input_hash: hash(toolCall.input),
        output_hash: hash(result),
        latency_ms: Date.now() - start
      });
      return result;
    }
  };
}
```

This pattern — wrapping the loop to add observability without modifying it — is the same pattern you will use in Module 6 (adding permission gates) and Module 11 (adding security checks). The loop's interface (callModel, executeTool) is the extension point; everything else plugs in around it.

---

## Anti-Patterns

### The infinite retry loop

A loop that retries every error indefinitely. Cured by the error-threshold stop condition and a typed error taxonomy (Module 7).

### The graph that fights the model

A LangGraph state machine so rigid that when the model could handle a transition naturally, the graph forces it through a suboptimal path. Cured by the dumb-loop philosophy: only encode transitions the model genuinely cannot handle.

### The un-observable loop

A loop that emits no structured per-turn data. When it fails (and it will), you cannot reproduce the failure. Cured by the observability wrapper above — there is no excuse to ship a loop without it.

### Subagents with unbounded returns

A subagent that returns its full context to the parent, polluting the parent's window and re-billing tokens. Cured by the 1–2k token bound on subagent returns (Module 3).

---

## Key Terms

| Term | Definition |
| --- | --- |
| **ReAct** | Thought → Action → Observation loop; the foundational pattern |
| **Plan-then-Execute** | Plan fully first, then execute; 3.7× faster on complex tasks but brittle if plan is wrong |
| **Dumb Loop** | Delegate all reasoning to the model; harness handles only transition logic |
| **Stop condition** | The mechanism that exits the loop; a safety system, not an afterthought |
| **Infinite loop problem** | The failure mode when no stop condition triggers |
| **Agents-as-tools** | Subagent registered as a callable tool; parent stays in control; synchronous |
| **Handoff** | Terminal transfer of control to a subagent; parent loses visibility |
| **Future-proof test** | Does agent performance improve when the underlying model upgrades? |
| **Per-turn payload** | The structured record (trace_id, tool, hashes, latency, tokens, stop_reason) emitted every turn |

---

## Lab Exercise

See `07-lab-spec.md`. Implement the *same* simple task (read a file, summarize, write the summary) in three architectures — ReAct and Plan-then-Execute in n8n, Graph-based in TypeScript. Instrument the loop with the per-turn observability payload. The architectural difference, felt on the same task, is what makes the tradeoffs stick.

---

## References

1. **Yao et al. (2022)** — *ReAct: Synergizing Reasoning and Acting in Language Models*. arXiv:2210.03629. The foundational ReAct paper.
2. **Kim et al. (2023)** — *LLMCompiler: An LLM Compiler for Parallel Function Calling*. Source of the 3.7× Plan-then-Execute speedup claim (arXiv:2312.04511).
3. **LangGraph documentation** — the graph-based state machine reference.
4. **AutoGen (Microsoft)** — the conversation-driven multi-agent reference.
5. **Anthropic (2025)** — *Claude Code: Best Practices for Agentic Coding*. The dumb-loop philosophy and the future-proof test.
6. **Pi source code** — the ~1,200-line reference implementation of the dumb loop.
7. **oh-my-opencode source** — the Prometheus → Atlas → Junior subagent hierarchy case study.
8. **Module 0.1** — the n8n ReAct-loop workflow (Diagram 6), the primary visual for this module.