labs | 11 | orchestrate
lab 11 | ~12 min | masterclass

One conductor, a shared task list, parallel specialists, and a gate nothing ships without.

A single agent is a worker. Ten agents with a conductor and a gate is a team. A conductor is the one agent that hands out the work and combines the results. A gate is an automatic quality check that nothing gets past. The shape is always the same. Plan the whole batch of work up front. Start every worker at once (this is the fan-out). Wait for all of them to finish before moving on (this is the barrier). Run the gate over their results, where the gate is hostile on purpose and tries to reject anything weak. Then build the final answer from only the results that passed. You will build that exact loop in three short steps with Python's built-in async tools. One detail before you start, because it is the most honest thing in this masterclass: this deck was built this exact way. Ten writers in parallel, one shared task list, gated, while you were reading the earlier labs.

step 1

Plan the full batch, then fan out.

The conductor's first job is not to start workers. It is to list the work. Write out the whole set of sub-tasks before the first worker starts. The batch you forgot to plan becomes the second, duplicate batch you pay ten times over for later. Then one line starts them all at once. asyncio.gather (a built-in Python call that runs many tasks at the same time) launches every worker in parallel and comes back only after all of them finish. Each worker here is a small async function that stands in for one specialist. Save as fleet.py.

fleet.py
import asyncio, random

# Enumerate the FULL batch before the first spawn. This list is the plan.
SPECS = [
    {"id": 1, "topic": "auth"},
    {"id": 2, "topic": "rate-limits"},
    {"id": 3, "topic": "schema"},
    {"id": 4, "topic": "memory"},
    {"id": 5, "topic": "telemetry"},
]

async def worker(spec):
    """One specialist. Does its slice of work and returns a finding."""
    await asyncio.sleep(random.uniform(0.05, 0.2))  # stand-in for real work
    finding = {
        "id": spec["id"],
        "topic": spec["topic"],
        "claim": "reviewed " + spec["topic"],
        "evidence": spec["topic"] + "_server.py:1",  # the invariant the gate checks
    }
    if spec["id"] == 3:        # worker 3 forgets its evidence (we catch it next step)
        finding.pop("evidence")
    return finding

async def run_fleet():
    # asyncio.gather IS the fan-out: every worker starts at once, the call
    # returns only when ALL have returned. That return is the barrier.
    return await asyncio.gather(*[worker(spec) for spec in SPECS])

if __name__ == "__main__":
    results = asyncio.run(run_fleet())
    print("fanned out", len(results), "workers; all returned")
    for r in results:
        print(" ", r["id"], r["topic"], "->", r.get("evidence", "(no evidence)"))
python fleet.py (five workers run in parallel; worker 3 came back thin)
fanned out 5 workers; all returned
  1 auth -> auth_server.py:1
  2 rate-limits -> rate-limits_server.py:1
  3 schema -> (no evidence)
  4 memory -> memory_server.py:1
  5 telemetry -> telemetry_server.py:1

Five tasks listed up front, five workers started in one gather call, five findings back. The rule that pays for itself: the full batch sits in SPECS before any worker starts. Worker 3 came back without its evidence field. A careless setup would ship that broken result. We will not. That is step 2.

step 2

Barrier, then gate.

The gather in step 1 was the barrier: the code does not move past that line until every worker has finished. Now the conductor runs the gate over the combined results. The gate is hostile by design: its whole job is to doubt the workers and reject weak output. It is a pure function, meaning it just reads the results and returns an answer without changing anything outside itself. It fails any result that is missing a required field (an invariant, the thing that must always be true) and returns two lists: what passed and what got sent back. Nothing moves forward from the sent-back pile. Add this to fleet.py, above if __name__.

add to fleet.py
REQUIRED = ("id", "topic", "claim", "evidence")

def gate(results):
    """Pure function. Hard-fail any result missing a required invariant.
    Returns (passed, kicked_back). Nothing ships from kicked_back."""
    passed, kicked_back = [], []
    for r in results:
        missing = [k for k in REQUIRED if k not in r]
        if missing:
            r["_kicked"] = "missing " + ",".join(missing)
            kicked_back.append(r)
        else:
            passed.append(r)
    return passed, kicked_back

The rule here is simple, just "every field is present," but the shape is the whole point. The gate is separate from the workers and works against them. A worker's job is to produce. The gate's job is to doubt. Worker 3 dropped its evidence field, so it lands in kicked_back with a reason, and the four good findings move on.

the gate's verdict on the five findings
5 fanned out -> gate -> 9 passed, 1 kicked back -> revise
  result 3 missing evidence -> kicking back before synthesis

(Five here, ten in the real run. The math is the same. Every worker gets checked, and the one that fails the rule is caught at the barrier, not in the final result.)

step 3

Synthesize from what passed, and map it to the real thing.

The last move is the one a plain dispatcher skips. Build the final result (synthesize it: merge the pieces, drop duplicates, write it out) from only the results that cleared the gate. Then connect the three steps in main so the whole loop runs: start the workers, wait for all of them, gate, and build the final answer. Add both pieces to fleet.py (replace the step-1 if __name__ block with this one).

add to fleet.py + replace the main block
def synthesize(passed):
    """Merge + dedupe the gated findings into the final artifact."""
    seen, lines = set(), []
    for r in sorted(passed, key=lambda x: x["id"]):
        if r["topic"] in seen:
            continue
        seen.add(r["topic"])
        lines.append("- " + r["claim"] + " [" + r["evidence"] + "]")
    return "FLEET REPORT\n" + "\n".join(lines)

async def main():
    results = await run_fleet()                       # fan out + barrier
    print("fanned out", len(results), "workers; all returned")

    passed, kicked = gate(results)                    # adversarial gate
    print(len(passed), "passed,", len(kicked), "kicked back -> revise")
    for r in kicked:
        print("  gate: result", r["id"], r["_kicked"], "-> kicking back before synthesis")

    print()
    print(synthesize(passed))                          # ship only the gated

if __name__ == "__main__":
    asyncio.run(main())

# --- the same shape in Claude Code, 1:1 ------------------------------------
# SPECS                = the shared task list   -> TeamCreate + one TaskCreate per spec
# asyncio.gather(...)  = the fan-out            -> N parallel Agent() spawns, one batch
# gate(results)        = the barrier check      -> a gate agent that doubts every output
# the conductor (main) = reads every result     -> only the MAIN session can spawn agents
# caps                 = ~7 concurrent, 40/run  -> phase larger fleets; never exceed
python fleet.py (the whole loop: fan out, barrier, gate, synthesize)
fanned out 5 workers; all returned
4 passed, 1 kicked back -> revise
  gate: result 3 missing evidence -> kicking back before synthesis

FLEET REPORT
- reviewed auth [auth_server.py:1]
- reviewed rate-limits [rate-limits_server.py:1]
- reviewed memory [memory_server.py:1]
- reviewed telemetry [telemetry_server.py:1]

The report has four lines, not five. The schema worker's thin result never reached it. That comment block is the bridge to the real thing: gather is the fan-out, the gate is the barrier check, and the conductor is you (or the main Claude Code session) reading every output. Swap the stand-in async functions for real Agent() workers that share one TeamCreate task list, and you have run a real fleet.

a dispatcher collates. a producer defends.SBX SAOT / TeamCreate

The careless fleet runs many agents and just glues their output together, which means it ships the weakest piece. The real win is not the speed of running them at once. It is the gate: nothing moves forward until that hostile check passes, and the conductor reads every single output as if it is about to defend it to the client. A dispatcher collates. A producer doubts, sends weak work back, and only then builds the final answer. The two rules below were each paid for in real compute.

Rule one: only the main session can spawn agents. If you tell a sub-agent to act as a "lead" and start its own workers, it stops at its first move, because sub-agents do not have the tool that starts agents. Plan the batch and start it from the top. There is no conductor inside a conductor.

Rule two: one batch, one message. Start the whole fleet at once. The "let me just add the one I forgot" follow-up is a second batch, and it has cost 10x compute on exactly this mistake. List everything first, then start it all in one go.

the gate, mid-run result 7 has no evidence field, invariant violated -> kicking back before synthesis.

That one line is the difference between a fleet and a crowd. The crowd hands you ten answers and leaves you to find the broken one. The fleet catches it at the barrier and never lets it into the thing you ship.

hand this to your coding agent
Act as conductor. Split <task> into 5 independent sub-tasks, enumerate them all
up front, then run them as 5 parallel workers sharing one task list. After they
all return, run a gate that rejects any result missing <invariant> and kicks it
back for one revision. Only synthesize the final answer from results that passed
the gate, and show me what the gate caught.

Paste that into Claude Code with a real task and a real must-be-true rule. Watch it list the work before it starts any worker, run them in parallel, gate, and report what it sent back. That is the same loop you just built in fleet.py, now running on live agents instead of stand-in async functions.

checkpoint

You can turn one hard problem into ten parallel ones, wait for them all, check their quality at the gate, and build the answer from only what passed. The conductor lists the work before it starts, doubts every output, and ships nothing the gate did not clear. You are reading the output of exactly this pattern right now: ten writers, one task list, one gate, in parallel, while you read the labs before this one.