Code Agent Dissection (11): Harness Design Part 1 — Control Flow

Examining MyCodeAgent's control flow from a harness engineering design perspective: why a single main loop rather than multiple nested loops, the engineering value of immutable state machines, the completion gate's feedback loop design, and the completeness of termination paths. This is the first article of Part 4 Harness Engineering, focused on 'why it was designed this way' rather than 'what it is.'

·8 min read·AI Engineering

What Is a Harness

The previous ten articles introduced the agent's individual capabilities: LLM calls, tool system, context compression, persistence and recovery. Part 4 takes a different angle — not asking 'how does this feature work,' but asking 'how is this agent framework designed, and what engineering choices were made.'

The word 'harness' comes from horse racing — it refers to the apparatus fitted on a horse to control its direction. In the agent context, harness refers to the framework's own engineering structure: how control flow is organized, how state is managed, how side effects are constrained, and how errors are recovered.

This article focuses on control flow.


Design Choice 1: Single Main Loop

MyCodeAgent has only one RuntimeRunner, and all agent behavior happens within a single _react_loop() loop.

Compare with another common design — nested loops:

# Common nested loop design
while not done:
    plan = planner.plan(goal)           # outer: planning loop
    for step in plan.steps:
        result = executor.execute(step) # inner: execution loop
        if result.needs_replan:
            break

MyCodeAgent's choice:

# Single loop
for step in range(1, max_steps + 1):
    model_view = build_model_view(history)
    response = llm.invoke(model_view)
    if tool_calls:
        observations = execute_tools(tool_calls)
        append_to_history(observations)
        continue
    if completion_gate.pass(response):
        return response
    inject_feedback(gate.blocking_reason)

Engineering value of the single loop:

  1. State is singular: LoopState is the only runtime state; no 'planning state' and 'execution state' to synchronize. Debugging requires looking at only one state object.

  2. Control flow is observable: Each loop iteration corresponds to one step; all events in a step share the same step number. Filtering by step in the trace file reconstructs exactly what happened at any moment.

  3. No implicit state machine: In nested loop designs, 'which loop we're in, how far we've gone' is typically tracked by flag variables — difficult to trace when bugs occur. The single loop's progress is just the step counter: direct and obvious.

The trade-off: a single loop means both planning and execution are done by the model within the same loop; you can't apply different strategies during a 'planning phase' (like enforcing planning-first). This project chooses to give planning authority entirely to the model; the loop only provides the execution framework.


Design Choice 2: Immutable State Machine

LoopState is a frozen=True dataclass:

@dataclass(frozen=True)
class LoopState:
    step: int
    tool_choice: str
    completion_block_count: int
    model_recovery_counts: dict[str, int]
    last_error: str | None
    transition: Transition | None  # reason for the most recent transition
    # ...
 
    def next(self, reason: TransitionReason, **changes) -> "LoopState":
        return replace(self, transition=Transition(reason), **changes)

Each state change produces a new object; the original remains unchanged.

Why immutable?

The problem with mutable state: any code can silently modify state, making it hard to pinpoint which step caused a bug.

# Mutable state problem
state.step = 2
state.last_error = "timeout"  # which branch did this change happen in?

Immutable state makes each change an explicit operation:

# Immutable state: each modification is a new object creation; reason recorded in transition field
state = state.next(TransitionReason.TOOLS_EXECUTED, step=step+1)
#                   ↑ why it changed: tools were executed

TransitionReason enum is key — it encodes 'why we arrived at this step' as a queryable enum value rather than relying on code comments or call stack inference. In the trace file, every state_transition event has a reason; you can filter by reason to find 'all model empty-response retries' or 'all completion gate blocks.'

Additional benefit of immutability: Every LoopState object is a snapshot of a point in time. If you need to set a breakpoint at some step or replay it, just rebuild that moment's LoopState from the transcript — no need to rerun the entire loop.


Design Choice 3: Completion Gate's Feedback Loop

Most agent frameworks' exit logic is: 'if the model returned no tool calls, exit.' MyCodeAgent adds one more layer:

Model has no tool_calls

Completion gate verdict (not a direct exit)
    ├─ PASS      → return final_text
    ├─ UNVERIFIED → return final_text (with marker)
    └─ FAIL      → inject feedback message → continue (keep looping)

Feedback messages injected on FAIL are structured:

# runtime/completion.py — _build_blocking_feedback()
lines = ["<system-reminder>Completion blocked by runtime gate.</system-reminder>"]
if "incomplete_todos" in reasons:
    lines.append("Incomplete todos remain: " + "; ".join(incomplete_todos))
if "missing_verification_evidence:tests" in reasons:
    lines.append("Missing verification evidence for tests. Run the required verification tool.")

Wrapping with <system-reminder> tags is intentional: the system prompt includes the instruction 'upon encountering a system-reminder, you must execute it.' This makes the model more likely to actually perform what the feedback requires (run tests, clear todos) rather than ignoring it.

Engineering value of the feedback loop:

Without a completion gate, the model might say 'I'm done' while the todo list still has incomplete items, or the user requested running tests but the model forgot to execute them. The completion gate provides a verification mechanism at the harness level that is independent of the model — it doesn't trust the model's self-declaration, and uses objective rules to check (are there incomplete todos? is there evidence of test execution?).

Injecting feedback rather than terminating directly is another design choice: error termination makes the user re-enter input; injected feedback gives the agent a chance to self-correct, providing better user experience, and in most cases the model can indeed complete the remaining work based on the feedback.

The upper limit is completion_gate_retry_limit (default 2 retries) to prevent infinite feedback injection from bloating the context.


Design Choice 4: Completeness of Termination Paths

All possible exit points of the loop:

Normal exits:
  Completion gate PASS              → return final_text
  Completion gate UNVERIFIED        → return final_text (with completed_unverified marker)
 
Abnormal exits (all have corresponding TerminalReason):
  Exceeded max_steps                → TerminalReason.MAX_STEPS
  Cumulative tokens exceeded budget → TerminalReason.TOKEN_BUDGET
  Model call exception unrecoverable → TerminalReason.MODEL_ERROR
  Empty response retries exhausted  → TerminalReason.EMPTY_RESPONSE_FAILED
  Completion gate feedback retries exhausted → TerminalReason.COMPLETION_GATE_BLOCKED

Each abnormal exit writes a terminal event to the transcript with a clear reason. This ensures every run in the transcript has a terminal event; ResumeLoader can use the terminal event to determine 'was this run completed normally or terminated abnormally,' and decide whether to re-run after recovery.

Why enumerate all termination reasons?

If there were only 'completed' and 'errored' terminations, debugging would be hard — why did the agent stop? The exhaustive TerminalReason enum gives each termination a clear explanation. Combined with the trace file, you can directly query 'what caused this agent to stop.'


Responsibility Separation in the Double Loop

Article 2 introduced the double loop structure; from a harness design perspective, it solves a specific problem: separating step consumption from error retries.

outer for step in range(max_steps):   ← controls "how many steps taken"
    inner while True:                  ← controls "how many retries this step"
        try: llm.invoke()
        except PROMPT_TOO_LONG: compact → continue   # inner continue, step unchanged
        if empty_response: inject_hint → continue    # inner continue, step unchanged
        break                                         # normal response, exit inner loop
    # outer continues, step + 1

If error retries were in the outer loop, each retry would consume a step. With max_steps=50, the agent might only run 30 real ReAct iterations due to error retries. The double loop isolates retries in the inner loop; error recovery doesn't use up step quota.


Summary

Design ChoiceApproachEngineering Value
Loop structureSingle main loopState is singular, control flow observable, no implicit state machine
State managementImmutable + TransitionReasonEach change is explicit, reason is queryable, supports snapshot replay
Completion conditionCompletion gate three-states + feedback injectionModel-independent verification, auto-correction instead of error termination
Termination pathsExhaustive TerminalReasonEvery termination has a clear reason, facilitates debugging and recovery decisions
Error retriesIsolated in inner loopRetries don't consume step quota; step count truly reflects progress

About the Source Code

All analysis in this series is based on the open-source project MyCodeAgent.

The source code includes inline comments aligned with the series' explanation order at key locations — you can follow along while reading, or clone it, run it, modify it, and extend it to build your own agent.

git clone https://github.com/chendongqi/MyCodeAgent
cd MyCodeAgent
cp .env.example .env   # fill in your LLM API key
uv sync
uv run python main.py

Check out PrimeSkills — a curated marketplace of AI agents and skills that have been validated in real-world, enterprise-grade workflows. No fluff, just what actually works.

Find more useful knowledge and interesting products on my Homepage