The Execution Loop

Module 1 · Master Course — Harness Engineering

90 minutes · 4 sub-sections: Loop Architectures · Stop Conditions · Subagent Delegation · Observability

Prerequisite: Module 0 complete

The heartbeat

Module 0.1 defined the loop as one of the harness's three jobs. This module goes inside it.

The loop is the agent. Without it you have a chatbot. The loop's architecture is the first and most consequential rubric decision — it shapes tool design, context management, error handling, observability.

Five architectures. Five ways out. Four subagent patterns. One per-turn payload. That's the module.

1.1 Loop Architectures

The five shapes

The five architectures

ReAct / TAO

Thought → Action → Observation. The foundational pattern most harnesses implement.

Plan-then-Execute

Plan all steps upfront, then execute. 3.6× faster; brittle if plan is wrong.

Graph-based

LangGraph. Explicit nodes/edges. Testable; rigid.

Dumb Loop

Delegate everything to model. Co-evolves. Anthropic's bet, Pi.

Conversation-driven

AutoGen GroupChat. Emergent; non-deterministic.

(hybrids)

Real systems mix patterns. oh-my-opencode: agents-as-tools + worktree.

The shape predicts the failure mode: cycles can loop; lines can't recover; graphs can fight the model.

ReAct — the reference pattern

while (true) {
  const r = await model.complete(messages);
  if (r.stopReason === "end_turn") return r.content;
  if (r.toolUse) {
    const out = await executeTool(r.toolUse);
    messages.push({role:"tool",content:out});
  }
  messages.push({role:"assistant",content:r.content});
}

~80 lines in Pi. The model's intelligence is not in this code — the loop is pure orchestration.

Gains: simple, debuggable, model-native

Costs: errors compound across steps; no lookahead

If the model takes a wrong action at step 3, steps 4–N inherit the error. Module 7 (error handling) exists largely to mitigate this.

ReAct as an n8n workflow

Per the visual stack: n8n first. One full traversal of this ring = one TURN.

   Trigger → Assemble Context → Loop(maxIter:8)
                                  │
                                  ▼
                              Call Model ◀── back-edge = TURN BOUNDARY
                                  │
                          ┌───────┴───────┐
                        done            tool_use
                          │                │
                          ▼                ▼
                       Output      Route → read/write/bash
                                         │
                                         ▼
                              Append Observation to History ──┐
                                                         (back to Loop) ┘

The model is one node of seven. Thought = model reasoning; Action = tool_use; Observation = appended result.

Plan-then-Execute — the comparison

3.6× faster on complex tasks (LLMCompiler). Planning parallelizes; execution avoids round-trip latency.

But: brittle if the early plan is wrong. Every subsequent step inherits the plan's error. No mid-execution recovery.

ReActPlan-then-Execute
When model decidesevery turnonce, upfront
Recovery from wrong stepyesno
Speedslower~3.6× faster
Best forunpredictable (debug, explore)predictable (CI/CD, fixed workflows)

Neither universally better. Task predictability decides.

The same task, two shapes (n8n)

ReAct

Context → Loop → Model → Stop?
                        ▲           │
                        │       tool│done→Output
                        │           ▼
                        └── Append Observation

A cycle. Can recover.

Plan-then-Execute

Plan all steps → Step 1 → Step 2 → Step 3 → Output
   (no back-edge)

A line. Cannot recover.

The structural difference between the two node graphs is the architectural lesson. The cycle can recover; the line is faster.

Graph-based (LangGraph)

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_conditional_edges("verify",
    lambda s: "execute" if s.needs_work else "done")

Gains: explicit, testable state machine. Native checkpointing (Module 8). interrupt() for HITL (Module 6).

Costs: more code; fights model capability growth. A rigid graph encoding yesterday's best practice becomes tomorrow's constraint.

Right when the process is the product (regulated workflow). Wrong when you want the model to figure out the process.

The Dumb Loop

Anthropic's explicit philosophy. Harness handles only stable transition logic. All reasoning delegated to the model.

Gains: co-evolves with model improvements. Thin code surface (Pi: ~1,200 LOC). The Manus finding: refactored 5× in 6 months, each time getting thinner, performance improving each time.

Costs: debugging is harder. Reliability depends heavily on the model.

The future-proof test: when the underlying model upgrades, does agent performance improve? If yes, your harness isn't constraining the model. If no, your harness has become the constraint.

Conversation-driven (AutoGen)

Multiple agents converse. A manager decides who speaks next. Orchestration emerges from dialogue.

Gains: natural for multi-agent; emergent collaboration.

Costs: non-deterministic; hard to audit; can loop. The "who speaks next" decision is itself a failure mode.

Right for brainstorming/debate. Wrong for execution where determinism matters.

The choice, stated as a decision

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 fill all five blanks for each of the five architectures. The rubric (Module 0.3) scores this as the Execution Loop decision.

1.2 Stop Conditions

Safety logic, not an afterthought

The five stop conditions

ConditionCatchesMechanism
Token budgetrunaway costcumulative counter; halt at threshold
No-tool-calltask completionmodel emits end_turn / stop_reason: stop
Max iterationsstuck loops, runaway depthhard counter; halt at N turns
Error thresholdcascading failureN consecutive errors → halt
Human interruptapproval gate, ambiguityHITL checkpoint; pause indefinitely

A production loop has all five. A toy loop has one or two. The difference between "demo" and "production" is largely this list.

The infinite loop problem

What happens when none of the stop conditions trigger? The model keeps calling tools, each costs money, no natural exit.

Three failure modes produce it:

  1. Model hallucinates ongoing work — task is done, model doesn't recognize it. Cured by max-iter + verification (Module 9).
  2. Tool keeps failing, model keeps retrying — cured by error threshold + stuck-loop detection (Module 7.2).
  3. Context fills with noise, model loses the task — cured by context management (Module 3) + token budget.

The token budget must be cumulative across the session, not a per-call max_tokens. Per-call limits one completion; session limits the whole run.

1.3 Subagent Delegation

Who keeps control?

The four subagent patterns

PatternControlReturns to parentParallel
Agents-as-toolsparent keepsstructured result (1–2k tokens)no (sync)
Handoffsterminal — parent losesnothing (one-way)one-way
Forkisolated; no shared statesummary or nothingindependent
Worktreeparallel; git worktreesmerged via gittrue parallel

The key decision: does the parent keep visibility after spawning? Agents-as-tools and worktree keep it; handoffs and fork lose it.

oh-my-opencode's hierarchy — case study

Prometheus (planner)
    │  agents-as-tools (Atlas returns structured summary)
    ▼
Atlas (orchestrator)
    │  worktree (Juniors execute in parallel, merge)
    ▼
Junior × N (workers) → merge → Atlas aggregates → Prometheus

Real systems mix patterns. Prometheus→Atlas is agents-as-tools; Atlas→Junior is worktree.

Which mix you choose is itself a design decision worth auditing. The rubric (Module 10, Subagent Orchestration) scores it.

Anti-pattern: unbounded subagent return

A subagent that returns its full context to the parent pollutes the parent's window and re-bills tokens.

Cure: bound subagent returns to 1–2k tokens (Module 3). Summary-only by default; full output available via JIT retrieval.

This is the same pattern as the orchestrator-context-trap in Fleet Module F02 — orchestrator accumulates context from every worker; at 4+ workers it overflows.

1.4 Loop Observability

Without it, debugging is divination

The per-turn payload

A production loop emits this structured record at every turn. Memorize it.

FieldWhy
trace_idcorrelate all events in a session
turn_numberiteration; ordering + caps
tool_namewhich tool (or null for end_turn)
input_hashdedup, replay, stuck-loop detection
output_hashsame input + same output = no progress
latency_msmodel vs tool latency
token_deltacost attribution; budget enforcement
stop_reasonwhy the turn ended

Hashes, not full content: tool outputs are 67.6% of context (Module 0.3) and may contain secrets. Full content → separate access-controlled store.

Pi vs Claude Code: what gets logged

Pi (thin)

console output: tool names + turn markers. Minimal. Enough to debug, no more.

Claude Code (thick)

structured traces, full tool I/O, token accounting per turn, session diffs, replay capability.

Neither is wrong. Observability is a thickness-spectrum decision. But there is a floor: a loop that emits nothing is un-debuggable. Pi is above the floor; no-logging is below it.

The observability wrapper

Add observability without modifying the harness core — wrap the model-call and tool-exec functions.

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

Same pattern you'll use in Module 6 (permission gates) and Module 11 (security checks). The loop's interface is the extension point.

Four anti-patterns

The infinite retry. Retries every error forever. Cure: error-threshold stop + typed taxonomy (Module 7).
The graph that fights the model. A rigid LangGraph forcing suboptimal transitions the model could handle. Cure: dumb-loop philosophy — only encode what the model can't.
The un-observable loop. No structured per-turn data. When it fails, you can't reproduce. Cure: the wrapper above.
Unbounded subagent return. Full context back to parent. Cure: 1–2k token bound (Module 3).

Takeaways

  • Five architectures, each a distinct bet. Task predictability decides.
  • Five stop conditions — a loop missing any is a production defect.
  • Four subagent patterns differ in who keeps control.
  • Per-turn observability is the floor, not a feature. 8 fields, every turn.
  • The future-proof test: does performance improve when the model upgrades?

Next: Module 2 — Tool Design & the Tool Contract. Where most security vulnerabilities live.