Weave Anthropic harness essay into s16 teaching spine

Ground the progressive chapter in why custom harnesses exist, the three
single-window failure modes, dynamic vs static, tasteful patterns, when
not to use workflows, and sharp neighbors (s06/s13/s15/s17).

Co-authored-by: Xinlu Lai <CrazyBoyM@users.noreply.github.com>
This commit is contained in:
Cursor Agent
2026-08-12 13:47:06 +00:00
parent e28bec6dd4
commit bab23aabf7
3 changed files with 191 additions and 53 deletions

View File

@@ -7,6 +7,8 @@ s01 → ... → s14 → [s15](../s15_integrated_harness/) → `s16` → [s17](..
> *"Chatting turn-by-turn is like texting the chef every ten seconds. A workflow is a recipe the kitchen can follow."*
>
> **Harness layer**: Orchestration — run a multi-agent script above the single-agent loop.
>
> Trust the model; engineer the harness. Workflows are harness engineering at the orchestration layer.
---
@@ -14,40 +16,52 @@ Imagine you are cooking with a friend over text. You send “chop the onions,”
That is ordinary model-as-orchestrator chatting. A **workflow** is the written recipe: the kitchen (runtime) follows it, helpers (subagents) do judgment, and intermediate bowls sit on the counter — not in the group chat.
## The problem
## Why a harness at all?
From s01 through s15, the model picks the next tool each round. That shines when the path depends on what you just discovered.
The default Claude Code harness is excellent at coding-shaped work: edit, run, read the error, try again — all in one loop.
Some jobs already know their shape:
Some jobs need a **custom harness on top**: deep research, security analysis, agent teams, large code review. You could hand-write that harness once in an SDK. Or — and this is the dynamic idea — Claude can **write a harness for this task on the fly**, run it, and optionally save the good ones.
- review many files on several dimensions
- research, then verify, then merge
- migrate N modules the same way
Course motto, one layer up: trust the model inside each step; engineer the structure around the steps.
If the model keeps “remembering” the plan inside `messages[]`, three things go wrong: context fills with orchestration noise, the plan drifts mid-run, and a crash means redoing finished work.
## The problem: one window, three ways to fail
You want parallelism, stable result shapes, and a way to resume. Chat history is a weak place to store all three.
From s01 through s15, the model plans and executes in the **same** context. Great when the next move depends on what you just found. Weak when the job is long, massively parallel, rigidly structured, or adversarial.
Claude Codes designers name three failure modes that show up in that single window. In plain language:
| Failure mode | What it feels like |
|--------------|--------------------|
| **Agentic laziness** | Stops halfway through a fifty-item review and says “done” after thirty-five |
| **Self-preferential bias** | Likes its own findings when asked to check itself — the fox grading the henhouse |
| **Goal drift** | The original “dont touch X” fades across many turns and compressions |
Chat history is also a weak place to store parallelism, stable result shapes, and resume. You need those for review-many-files, research-then-verify, migrate-N-modules — jobs whose **shape** is already known.
## The idea in one breath
**Move the plan into code.** Subagents still think. The script owns loops, fan-out, and merge. Intermediate results live in variables, not in the conversation.
**Move orchestration from intelligence to structure.**
Subagents still think — each in a clean context with a focused job. The **script** owns loops, fan-out, and merge. Intermediate results live in variables (and a journal), not in the conversation. Separate helpers + script-owned control flow is how you fight laziness, self-checking bias, and drift.
![Workflow Runtime Overview](images/workflow-runtime-overview.svg)
One `Workflow` tool call starts that scripted run. Lifecycle and progress events fire while it works; one tool result comes back with launch info, the result, and task state.
## Two doors into a workflow
## Two doors — and dynamic vs static
Claude Code is honest about how a workflow starts:
Claude Code opens two doors into the same kitchen:
| Door | What you pass | When |
|------|----------------|------|
| **Dynamic** | A JavaScript orchestration script (`script`, or later `scriptPath`) | The model writes a recipe for *this* task |
| **Saved** | `name` + `args` | A good recipe lives under e.g. `.claude/workflows/` and you rerun it |
Same kitchen either way. Dynamic is “write the recipe now.” Saved is “pull the card from the box.
Same kitchen. Dynamic is “write the recipe now.” Saved is “pull the card from the box” — the reusable residue of a good dynamic run.
**This lesson is a Python teaching runtime.** It shows the same ideas so you can read every line. Our demo registers a saved workflow by name; the concepts map 1:1 to Claude Codes script world. We do **not** claim “the model cannot submit executable code” — that was wrong for Claude Code. We simply skip embedding a full JS interpreter here.
There is also a cousin outside this lesson: **static** harnesses (Agent SDK / `claude -p` orchestrations you write ahead of time). Static ones must work for every edge case, so they stay generic. Dynamic ones are tailor-made for *this* task; save them when the cut fits well.
**This lesson is a Python teaching runtime.** Same ideas, every line readable. Our demo registers a saved workflow by name; concepts map 1:1 to Claude Codes script world. We do **not** claim “the model cannot submit executable code” — that was wrong for Claude Code. We simply skip embedding a full JS interpreter here.
```python
# Teaching adapter: saved door (name + args).
@@ -90,6 +104,18 @@ results = await ctx.pipeline(DIMENSIONS, audit, verify)
confirmed = [f for r in results if r for f in r["confirmed"]]
```
## Patterns with taste (not a laundry list)
Think of patterns as recipe styles. Our sample `review-changes` leans on three:
| Pattern | Plain meaning | In the sample |
|---------|---------------|---------------|
| **Fan-out-and-synthesize** | Split the work, give each piece a clean desk, then merge | Four dimensions audit in a `pipeline`, then one confirmed list |
| **Adversarial verification** | A second helper tries to knock the first ones work down | Each finding faces a verify agent before it counts |
| **Generate-and-filter** | Produce candidates, keep only what survives a test | Findings in → only `isReal` out |
Same toolbox, other styles you will meet later: **classify-and-act** (route by type), **tournament** (compete, then pick a winner), **loop-until-done** (keep going until nothing new appears). Use a pattern only when its cost earns a clearer or safer result.
## Make answers machine-readable
If a helper returns a poem, the next stage cannot reliably zip findings to verdicts. Pass a `schema`: the runtime asks for JSON, validates it, and retries **once**. Fail again and that call errors (see null-isolation below).
@@ -142,7 +168,7 @@ resume: A hit → B hit → C changed → D runs live (no silent hit on old D)
## Walk the sample: `review-changes`
Four review dimensions walk the same two-stage path:
Four review dimensions walk the same two-stage path — fan-out, then adversarial verify, then filter:
```text
correctness ── audit ── verify ──┐
@@ -151,8 +177,8 @@ performance ── audit ── verify ──┤
style ── audit ── verify ──┘
```
1. **Review** — each dimensions auditor returns structured findings.
2. **Verify** — each finding gets an adversarial checker (`parallel` inside the verify stage).
1. **Review** — each dimensions auditor returns structured findings (clean desks → less cross-contamination).
2. **Verify** — each finding gets an adversarial checker (`parallel` inside the verify stage) so the author is not also the judge.
3. Keep only findings marked real; sort by severity.
```python
@@ -177,6 +203,26 @@ s15 is still the host loop. s16 adds one tool: `Workflow`. The model (or you) as
The main loop does not become a workflow engine. It borrows one tool, the way it borrows `bash` or `task`.
## Neighbors: who holds the plan?
Workflows are not “more agents.” They change **who owns the topology**.
| Neighbor | Who holds the plan | Where intermediate results live | Best for |
|----------|--------------------|---------------------------------|----------|
| [s06 Subagent](../s06_subagent/) | Model, one-shot | Discarded except final summary | Isolate one dirty subtask |
| [s13 Agent Teams](../s13_agent_teams/) | Lead model turn-by-turn + mailbox | Shared tasks / messages | Long-running peers, human-like collaboration |
| [s15 Integrated Harness](../s15_integrated_harness/) | Model in one loop | Conversation `messages[]` | Cumulative coding agent |
| **s16 Workflow** | **Script** | **Script variables + journal** | Known / large structured fan-out + verify |
| [s17 Goal Loop](../s17_goal_loop/) | Evaluator at the stop boundary | Conversation as evidence | “Is the whole goal done?” |
Cheaper alternatives still win often: a skill or prompt as a soft plan, a short multi-agent chat, a hand-written static SDK orchestrator, or simply one bigger model turn. Reach for a workflow when structure must outlast a single context — not because panels sound impressive.
## When *not* to use a workflow
Workflows cost tokens and coordination. Most ordinary coding does **not** need a panel of five reviewers.
Ask: does this job really need more compute and a custom harness? If a normal s15 turn (or one s06 subagent) is enough, stop there. Restraint is part of the design thought — parallelism and specialization have to earn their keep.
## Try it
```bash
@@ -202,6 +248,6 @@ What to watch for:
**s16 = how a batch runs. s17 = whether the whole goal is done.**
[s17 Goal Loop](../s17_goal_loop/) asks an independent evaluator: should we stop, or take another turn?
[s17 Goal Loop](../s17_goal_loop/) asks an independent evaluator: should we stop, or take another turn? Pair them when a repeatable workflow also needs a hard completion check.
<!-- translation-sync: zh@v11, en@v11, ja@v11 -->
<!-- translation-sync: zh@v12, en@v12, ja@v12 -->