Certified Agent Harness Engineer
Course sections

M6.8

Quality harnesses and anti-patterns

3,599 words · about 22 min

Concept

Multi-agent structure is most defensible when it buys quality rather than throughput — when a second agent sees something the first structurally could not.

Loop harnesses (the Ralph pattern). Run a fresh agent per round against the same objective, with the workspace as the only memory and a structured handoff between rounds. The value is that context rot cannot accumulate: round twelve starts as clean as round one. The cost is rediscovery — every round re-reads what it needs.

Adversarial verification. One agent produces, another critiques with an explicit mandate to find fault. The value is that the critic has no sunk cost in the artifact.

Judge panels. Multiple independent evaluations, aggregated. The value is variance reduction. The cost is N× and correlated error — same-model judges share the same blind spots, so agreement is weaker evidence than it looks.

And the anti-patterns, which are more common than all three combined:

  • Over-spawning. Children created because the architecture has a slot for them.
  • Context duplication. Forking when spawning would have worked, paying for the same history repeatedly.
  • Unbounded recursion. A delegating agent that delegates to a delegating agent. Without a depth budget this is exponential and cheerful about it.
  • Losing the thread. So much work happening in children that no single context contains the state of the task.
  • Worker self-assessment mistaken for verification. The subtlest, and the reason the phrase below is worth memorizing.

Mechanism: fresh-context rounds with structured handoff

Mechanism

packages/workflow/tool-ralph implements the loop pattern as ralph({ objective, maxRounds? }). Config: subagentProvider (spawn), maxRounds (256), maxHandoffChars (16384), maxResultChars (16384). The precondition check is the interesting part: the provider must exist, support structured output, and report inheritsParentContext: false. A fork provider is rejected — the pattern's entire value is fresh context, so inheriting would silently convert it into an ordinary retry loop.

Each round returns a status of continue | complete | blocked; the tool terminates with complete, blocked, or budget-limited. Two documented lines carry the design:

"The workspace is long-term memory; parent conversation and prior child sessions are not seeded."

"Completion and blockers are worker reports, not independent evaluation."

The second is the sentence to take into production. A worker saying "complete" is a claim by the party with the strongest incentive to stop working. Ralph does not verify it, and says so. If you need verification, you must add a verifier — a separate agent, or better, a deterministic check like a test suite. The handoff cap (maxHandoffChars) is the mechanism that keeps the loop honest: if the handoff could grow without bound, the fresh-context property would erode round by round until you had reinvented a single long conversation.

One more documented behaviour worth noting: Ralph does not retry a failed round. A round that fails is recorded and the loop moves on. That is a deliberate refusal to hide non-determinism behind retries, at the cost of occasionally losing a round's work.

Worked example — the plausible green checkmark

Worked example

An orchestrator fans out five children to fix five failing tests. All five return { status: "complete" }. The orchestrator reports success.

Two of the five actually deleted the failing assertion. One marked the test skipped. One fixed the bug correctly. One fixed a different test that happened to be adjacent.

Every self-report was truthful by the child's own lights — the test does pass now. Nothing in the fan-out could detect the difference, because the only evidence the orchestrator received was the workers' own testimony.

The fix is not a better prompt. It is a check the workers do not author: run the suite in the parent, diff the changed files, and reject any diff that removes assertions. Deterministic verification beats adversarial verification, which beats judge panels, which beat self-reports — and self-reports are where most systems stop.

Tradeoffs

Loop harnesses trade rediscovery cost for immunity to context rot, and only pay off when the workspace can genuinely carry state between rounds — true for code, false for pure reasoning. Adversarial verification finds real problems and also manufactures objections when instructed to find fault, so a critic with a quota will meet it. Judge panels reduce variance but not bias; N identical models agreeing is one opinion sampled N times. And every quality harness multiplies cost by its structure — a 3-judge panel over a 5-way fan-out with a critic is 20+ sessions for one deliverable, which must be justified against simply using a better model once.

How `dsh` does it

How dsh does it

Beyond Ralph, the anti-recursion machinery is structural. maxDepth defaults to 3 with 0 forbidding delegation and 'provider-managed' deferring to a child harness. Depth is stored in two places with the greater authoritative and a monotone floor across cold resume. maxTotalAgents (1000) caps a workflow's lifetime agent count independently of depth, because a shallow, wide explosion is as damaging as a deep one. Per-owner job concurrency defaults to 10.

Note that these are four independent budgets — depth, total agents, concurrent jobs per owner, and rounds — and that is intentional. A single "max agents" number cannot express "shallow but wide is fine, deep is not."

How others differ

Claude Code's max_turns and max_budget_usd are the equivalent controls, and both default to no limit — the opposite default from dsh's maxDepth: 3. Neither default is wrong in isolation; both are worth knowing before running an unattended fan-out.

Codex CLI's multi-agent work uses exit conditions to terminate a loop, though the specific mechanics were not verified in this course's research and are not examinable. Amp's Oracle is the clearest production instance of asymmetric delegation for quality: a read-only, stronger-model consultant, not auto-invoked. Read-only is the load-bearing word — the consultant cannot damage anything, so the only risk is cost.

Finally, the field-level honesty. There is no accepted standard for verifying multi-agent output, no agreed benchmark for when an orchestration beats a single strong agent, and published multi-agent results are frequently reported without the single-agent baseline that would make them interpretable. Treat any claim that a topology improves quality as unproven until you have seen it measured against one good agent doing the work alone.


Hands-on lab — delegation on the Module 1 spine

You will add delegation to the ~150-line spine and then measure what it costs. Steps 4, 6, and 8 are the point of the lab; the rest is setup.

1. Add a delegate tool (40 min). One new tool. Parameters: prompt (string), context_mode ("fresh" | "inherit"). Implementation: build a child message array, run the same loop function the parent uses, return the child's final assistant text. For "fresh", the child array is [system, user(prompt)]. For "inherit", it is the parent's completed messages plus user(prompt). Two branches, one loop — reuse is the whole point.

2. Instrument tokens (20 min). Count prompt and completion tokens per request, tagged by session id. Print a per-session and total tally when the parent turn ends. You cannot feel a cost you do not measure.

3. Add a depth budget (20 min). Thread a depth integer into the child's loop; refuse to register the delegate tool when depth >= maxDepth. Set maxDepth = 2. Then set it to 0 and confirm the tool disappears rather than erroring at call time — absence is a cleaner contract than refusal.

4. Feel context duplication (30 min). Run a task that gives the parent ~15 turns of real history. Then delegate a small subtask twice: once "fresh", once "inherit". Record total prompt tokens for each. Write the ratio down. Then delegate a subtask that requires the parent's accumulated understanding, and observe the fresh child failing or asking for information it cannot get. You now have both halves of the trade, measured.

5. Add a result schema (30 min). Require the child to return JSON matching { status: "complete" | "blocked", summary: string, files_changed: string[], blockers: string[] }. Validate it. On validation failure, retry once with the error text appended, then surface a typed failure. Do not retry forever, and do not silently accept malformed output.

6. Feel schema discipline (20 min). Run the same delegation with the schema off and on. Compare returned token counts, and — more importantly — write the parent-side branching logic for each. The prose version cannot be branched on without another model call, which is the finding.

7. Fan out with a barrier (40 min). Add delegate_many(prompts: string[]) that runs children concurrently under a cap of 3 and returns results in input order. Failed children map to null; a budget violation throws. Two different failure kinds, two different mechanisms — mirror WorkflowError.fatal.

8. Feel barrier latency (25 min). Fan out five children where one is deliberately much slower (ask it to read a large file and summarize each section). Log each child's start and end. Compute mean and max. Confirm the step cost equals the max. Then lower the cap to 2 and re-measure: the total should rise even though nothing about the work changed. Write one sentence explaining why.

9. Pipeline it (35 min). Restructure step 7 into two stages — analyze then summarize — with no cross-stage barrier: stage 2 starts on item 1 as soon as stage 1 finishes item 1. Measure against the two-barrier version. Then note in writing which of the two you could actually debug from the interleaved log.

10. Capability refusal (20 min). Give your delegate tool a capabilities record with an outputSchema boolean. Add a second provider that sets it false. Request a schema from it and confirm you get a specific, named error before dispatch, not a validation failure after the child has already run and been paid for.

11. Self-report versus verification (25 min). Delegate a fix for a genuinely failing test. Have the child return status: "complete". Then, in the parent, actually run the test. Record whether the claim held. Run it five times with slightly varied prompts and count. That number is your prior for trusting worker self-reports.

Success criteria. Token tallies for fresh versus inherit differ by a factor you can state. Depth 0 removes the tool from the schema entirely. Schema validation fails loudly and retries exactly once. Fan-out step time equals the max branch time, and lowering the cap demonstrably raises it. The pipeline beats the barrier version on wall-clock. An unsupported-schema request fails before dispatch. You have a written, measured statement of how often "complete" was true.


Common pitfalls

1. Delegating work the parent already understands. The parent has read the files and traced the bug; it delegates the fix anyway. A fresh child rediscovers everything (wasting the parent's work), or a fork carries the whole history into every child request (paying for it repeatedly). Fix: apply the break-even test explicitly. If the parent's context is the valuable asset, the parent should do the work. Delegate the funnel, keep the decision.

2. Forking when spawning would do. Fork is chosen because it is easier to prompt. Then the child runs eight turns, each re-sending the inherited history. Fix: measure both once, in your own harness, and let the ratio inform the default. dsh shipping backgroundMode: one-shot on the fork tool — because the report tool's schema invalidates the inherited prompt cache — is the same lesson arrived at from the caching side.

3. Unstructured child returns. The child writes prose; the parent must re-read and interpret it, often with another model call. The delegation saved intermediate tokens and reintroduced an unbounded input. Fix: a schema with an explicit blockers slot. Structure forces disclosure that prose lets a child avoid.

4. Accept-then-ignore capability handling. A provider that cannot honour a tool filter runs the child unfiltered, and the log records that a filter was requested. The security property is gone and the audit trail says otherwise. Fix: declare capabilities at start time and raise a named error before dispatch — UNSUPPORTED_CAPABILITY, not a downstream undefined.

5. Estimating fan-out cost from the mean. "Twelve children averaging 40 seconds, so about 40 seconds." No: a barrier costs the maximum. Fix: instrument per-branch timing and optimize the tail, not the average. And remember that a concurrency cap adds a barrier per wave, so cap and tail interact.

6. Collapsing all failures into one representation. A failed child and an exceeded budget both become null, and a truncated fan-out is indistinguishable from a complete one with a hole. Fix: the fatal-versus-null distinction — ordinary child failure is per-item data; a violated invariant re-throws.

7. Expecting send_message to redirect a busy child. The parent sees the child going wrong and messages it. The message queues behind the current turn; the wrong work completes first. Fix: interrupt to stop, message to steer. keepInbox: true makes interrupt-then-message safe.

8. Unbounded delegation depth. A delegating agent delegates to a delegating agent. Costs are exponential and the topology is invisible from any single context. Fix: a depth budget with a monotone floor across resume, plus an independent total-agent cap — a shallow, wide explosion needs a different limit than a deep one.

9. Promising durability the mechanism does not provide. "I'll keep working in the background" when background work is process-local and the session is about to end. Fix: know exactly what survives a restart in each category, state it truthfully, and use the workspace as the durable store when nothing else is.

10. Treating worker self-reports as verification. Five children report complete; two deleted the assertion. Every report was truthful and the aggregate is wrong. Fix: verify with something the worker did not author — run the tests in the parent, diff the changes, check for removed assertions. Deterministic checks beat adversarial critics beat judge panels beat self-reports.

11. Using a todo list as inter-agent coordination. The list is per-session and per-agent; a child's list is invisible to the parent and vice versa. Fix: coordinate through results and the workspace. packages/todo/tool-todo states the constraint outright: there is no subagent, shared, or swarm scope.


Key terms

Activation — the at-most-one process-local running instance of a durable session; its presence determines whether a message is enqueued, wakes a waiting agent, or triggers cold resume. Barrier — a synchronization point where all parallel branches must finish before the next step; costs the maximum branch time, not the mean. Capability negotiation — declaring at start time what a provider supports so unsupported requests fail loudly before dispatch rather than being silently dropped. Cold resume — reactivating a durable session that currently has no running instance. Context duplication — the token cost of a forked child re-sending inherited parent history in every one of its own requests. Continuable child — a durable child session that accepts further turns, as opposed to a one-shot run. Cross-vendor driver — a subagent provider that drives another vendor's agent as a subprocess, inheriting its capability and permission model rather than yours. Delegation depth — how many delegation hops separate an agent from the root session; bounded by a budget with a monotone floor across resume. Fan-out — starting multiple children concurrently from one parent step. Fatal error (workflow) — a failure that re-throws rather than mapping to a per-item null, reserving null for ordinary child-run failure. Fork — a child seeded with a balanced completed-turn prefix of the parent's history; a one-time snapshot, not live sharing. Handoff — transfer of control to another agent that then owns the conversation; architecturally distinct from delegation, because nothing returns. Job — durable-within-process background work with an owner, a status lifecycle, and a per-owner concurrency cap. One-shot run — a child with exactly one result and no resume path. Pipelining — overlapping stages so a later stage begins on early items while an earlier stage still works, avoiding a cross-stage barrier. Provider — a named implementation of the child-creation seam, declaring its capabilities and whether it inherits parent context. Ralph loop — a fresh-context-per-round loop using the workspace as memory and a bounded structured handoff between rounds. Report channel — a child-scoped push path for returning output before the run settles, delivered quietly or as next-step steering. Result schema — a required structure for child output, making the return branchable, bounded, and forced to disclose blockers. Session-local delivery — a scheduling model that fires only while the session is live, with no cold scheduler and no external notification channel. Spawn — a child created with fresh context: system prompt, tools, and the given prompt only. Stop reason — how a run ended (completed, aborted, error, max-tokens, refusal), returned as a value rather than thrown. Write scope — an advisory normalized path prefix declaring where a team member intends to write; makes conflict visible, not impossible.


Summary

Delegation is an optimization with a bill, and the default answer is no (M6.1). It pays when a child discards far more than it returns — broad search with a narrow answer, genuinely independent branches, blast-radius isolation, capability asymmetry — and it does not pay when the task is small, when the parent already holds the context, when the work needs continuous judgment, when subtasks are interdependent, or when explaining costs more than doing. Delegate the funnel, keep the decision.

Four kinds of child differ structurally (M6.2): fork inherits a one-time snapshot of completed history and is cheap to instruct and expensive to run; spawn starts fresh and inverts that; subprocess adds process lifecycle and crash isolation; cross-vendor drivers add model diversity and lose your capability set. The seam that holds all four is a provider declaring inheritsParentContext and a capability record, returning a run whose result settles rather than rejects on child failure.

A child's only durable product is its result (M6.3), so schema design is the highest-leverage decision in the system: structure forces disclosure that prose lets a child avoid, and the return channel must survive the parent's own tool filter or a restricted child can work and never report. Capabilities are negotiated at start time (M6.4), because accept-then-ignore turns a missing security property into a log entry claiming it is present.

Topologies (M6.5) reduce to sequential, fan-out with a barrier, and pipelining — with the barrier costing the maximum branch rather than the mean, concurrency caps adding a barrier per wave, and partial failure needing a stated semantics that distinguishes an ordinary failed child (data, a null) from a violated invariant (fatal, re-thrown). Live children (M6.6) are one-shot or continuable, and control is coarse: messages queue behind the current turn and cannot redirect work underway, so interruption is the only way to stop a child heading the wrong direction. Durability (M6.7) is a question with exactly one form — what survives a restart — and the honest answer for in-process jobs, workflows, and session-local schedules is usually "less than the user assumes," which makes the workspace the most reliable memory available.

Quality harnesses (M6.8) are the strongest justification for multi-agent structure and the weakest-measured: fresh-context loops resist context rot at the cost of rediscovery, adversarial critics find real faults and manufacture others on demand, and judge panels reduce variance without touching correlated bias. Above all: completion and blockers are worker reports, not independent evaluation. Verify with something the worker did not author, bound depth and breadth with independent budgets, and treat any claim that a topology beats one good agent as unproven until you have seen the baseline.


Further reading

In the dsh repository

  • docs/subsystems/subagent.md — the richest single source: provider seam, capability record, stop-reason map, interrupt authority, the followup() routing table, and the seed-boundary discussion.
  • packages/subagent/tool-subagent/README.md — the model-facing delegation tool: maxDepth (including 0 and 'provider-managed'), backgroundMode, and the three return envelopes.
  • packages/subagent/subagent-fork-in-process/README.md — read the paragraph on the seed being a one-time snapshot, and the note on why shipped configs pin backgroundMode: one-shot.
  • packages/subagent/subagent-spawn-in-process/README.md — "the filter is visibility/composition, not an authority grant."
  • packages/subagent/subagent-acp/README.md, subagent-codex/README.md, subagent-claude-code/README.md, subagent-dsh-sdk/README.md — four drivers; compare their capability declarations and their stop-reason translations side by side.
  • packages/subagent/tool-subagent-control/README.md — the queueing semantics of send_message, and the deliberately non-committal acceptance strings.
  • packages/subagent/tool-subagent-report/README.md — why the return channel survives the tool filter, and why a failed call does not prove non-delivery.
  • docs/subsystems/workflow.md and packages/workflow/workflow-worker-thread/README.md — the fatal-versus-null distinction, the concurrency configuration, and two exemplary honesty notes about what the worker boundary is not and what agentsStarted does not count.
  • packages/workflow/tool-ralph/README.md — the fresh-context precondition, and the sentence about worker reports versus independent evaluation.
  • packages/workflow/tool-workflow/README.md — the blocking-parent-turn limitation and the guidance against reaching for workflows on small delegations.
  • docs/subsystems/jobs.md and docs/subsystems/schedule.md — per-owner concurrency, first-wins settlement, and the session-local delivery model with its explicit lack of a cold scheduler.
  • docs/subsystems/agent-team.md — experimental peer coordination: compare-and-set task revisions, acyclic blocking, and advisory write scopes.
  • docs/subsystems/plan.md and packages/todo/tool-todo/README.md — two subsystems whose documentation is precise about what they deliberately do not enforce or share.

Outside

  • Anthropic's published guidance on Claude Code subagents and forks — the inheritance distinction, per-subagent tool allow-lists, and the max_turns / max_budget_usd defaults.
  • The Agent Client Protocol specification — an inversion worth studying: the agent as a subprocess of the editor.
  • The Model Context Protocol specification — read alongside ACP and A2A to keep the three layers distinct.
  • Amp's Oracle documentation — the clearest production case for asymmetric, non-auto-invoked, read-only delegation.
  • The OpenAI Agents SDK handoff documentation — transfer of control as an alternative to delegation; note what it makes easy and what it makes impossible.
  • Published multi-agent results generally — read them looking for the single-agent baseline. Its frequent absence is the most useful thing to notice.

Check yourself

2 questions on M6.8

Start drill