90 minutes · 4 sub-sections: Loop Architectures · Stop Conditions · Subagent Delegation · Observability
Prerequisite: Module 0 complete
Module 0.1 defined the loop as one of the harness's three jobs. This module goes inside it.
Five architectures. Five ways out. Four subagent patterns. One per-turn payload. That's the module.
Thought → Action → Observation. The foundational pattern most harnesses implement.
Plan all steps upfront, then execute. 3.6× faster; brittle if plan is wrong.
LangGraph. Explicit nodes/edges. Testable; rigid.
Delegate everything to model. Co-evolves. Anthropic's bet, Pi.
AutoGen GroupChat. Emergent; non-deterministic.
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.
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.
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.
But: brittle if the early plan is wrong. Every subsequent step inherits the plan's error. No mid-execution recovery.
| ReAct | Plan-then-Execute | |
|---|---|---|
| When model decides | every turn | once, upfront |
| Recovery from wrong step | yes | no |
| Speed | slower | ~3.6× faster |
| Best for | unpredictable (debug, explore) | predictable (CI/CD, fixed workflows) |
Neither universally better. Task predictability decides.
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 = 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.
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.
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.
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.
| Condition | Catches | Mechanism |
|---|---|---|
| Token budget | runaway cost | cumulative counter; halt at threshold |
| No-tool-call | task completion | model emits end_turn / stop_reason: stop |
| Max iterations | 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; 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.
Three failure modes produce it:
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.
| Pattern | Control | Returns to parent | Parallel |
|---|---|---|---|
| Agents-as-tools | parent keeps | structured result (1–2k tokens) | no (sync) |
| Handoffs | terminal — parent loses | nothing (one-way) | one-way |
| Fork | isolated; no shared state | summary or nothing | independent |
| Worktree | parallel; git worktrees | merged via git | true parallel |
The key decision: does the parent keep visibility after spawning? Agents-as-tools and worktree keep it; handoffs and fork lose it.
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.
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.
A production loop emits this structured record at every turn. Memorize it.
| Field | Why |
|---|---|
trace_id | correlate all events in a session |
turn_number | iteration; ordering + caps |
tool_name | which tool (or null for end_turn) |
input_hash | dedup, replay, stuck-loop detection |
output_hash | same input + same output = no progress |
latency_ms | model vs tool latency |
token_delta | cost attribution; budget enforcement |
stop_reason | why 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 (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.
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.
Next: Module 2 — Tool Design & the Tool Contract. Where most security vulnerabilities live.