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)
After completing this module, you will be able to:
The heartbeat. Everything else plugs into it.
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.
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:
- The loop is stateless. It takes the
messageslist as an argument and appends to it. The caller (the harness) owns all state. This makes the loop trivially testable with aFakeProvider.- Events are the sole output. The loop yields a typed
AgentEventunion — never calls callbacks, never imports a UI library. A JSON event consumer is a structured log; a Textual consumer is a live UI.- Tool execution is an isolation boundary. Each tool call runs inside
try/except Exception. A tool failure becomesAgentToolResult(ok=False), never propagates. The loop cannot be crashed by a tool.Clone
github.com/huggingface/tau, opensrc/tau_agent/loop.py, and trace the flow: provider stream → event translation → tool dispatch → queue drain → repeat. That's the heartbeat, in readable code.
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.
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).
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.
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).
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.
When does the loop exit? This is safety logic, not an afterthought.
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).
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:
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.
When and how does the loop spawn additional agents?
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 |
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.
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.
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.
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 (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.
What the loop should emit at every turn. Without it, debugging is divination.
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 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.pydefines 14 frozen Pydantic models —AgentStart,TurnStart,MessageDelta,ToolExecutionStart/End,Retry,Error, etc. — as a closed union withextra="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. Seesrc/tau_agent/events.py.
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.
A loop that retries every error indefinitely. Cured by the error-threshold stop condition and a typed error taxonomy (Module 7).
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.
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.
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).
| 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 |
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.
# 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.