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:
breakMyCodeAgent'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:
-
State is singular:
LoopStateis the only runtime state; no 'planning state' and 'execution state' to synchronize. Debugging requires looking at only one state object. -
Control flow is observable: Each loop iteration corresponds to one step; all events in a step share the same
stepnumber. Filtering by step in the trace file reconstructs exactly what happened at any moment. -
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
stepcounter: 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 executedTransitionReason 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_BLOCKEDEach 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 + 1If 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 Choice | Approach | Engineering Value |
|---|---|---|
| Loop structure | Single main loop | State is singular, control flow observable, no implicit state machine |
| State management | Immutable + TransitionReason | Each change is explicit, reason is queryable, supports snapshot replay |
| Completion condition | Completion gate three-states + feedback injection | Model-independent verification, auto-correction instead of error termination |
| Termination paths | Exhaustive TerminalReason | Every termination has a clear reason, facilitates debugging and recovery decisions |
| Error retries | Isolated in inner loop | Retries 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.pyCheck 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