Curriculum/Multi-Agent Systems
A Write Barrier After Map
Map workers are read-only (or write only to their own prefix). Money and email happen once, after reduce, in the parent apply step, with an idempotency key.
Keep a write barrier: map workers read (or write only to artifacts/{child_id}/...). The parent’s apply step is the only place money or email happens. If you skip this, you have invented distributed side effects with a cute name.
Retries duplicate children. Two children that both “helpfully” refund the same customer is the two-writers incident with extra parallelism. Idempotency keys on apply save you. The Tools track already taught those keys — here the parent owns them. Children never see refund or email on their allow-list.
The swarm lesson forbade write tools on workers as the main risk. This lesson is the positive design: a barrier in the dispatcher, and a parent apply that is safe to retry.
Two sides of the barrier
Child. Legal: score, extract, maybe read-one-item. Illegal: refund, email, edit on a shared path, apply. Dispatcher returns write_barrier without mutating the world. Same copy-on-write habit as planner-cannot-patch-src.
Child-prefix writes: a scratch file under that child id can be ok if ACL hides it from siblings and it is not money. Do not put customer ledger files in a prefix and call it scratch.
Parent apply. After reduce. Reads the table. If missing/dropped too high, stop. If ok, call refund or email once with key job_id:customer_id (or job_id:item_id). Retry of apply with the same key returns dup: True and does not move money again.
| Caller | refund | score |
|---|---|---|
| Child | write_barrier | ok |
| Parent, new key | ok, first time | not a write |
| Parent, same key again | ok, dup | — |
Children map. Only the parent writes money or mail. Same key does not pay twice.
Write barrier after mapThe sequential billing subgraph still wraps this: HITL then apply. A swarm of scorers might sit before HITL (“rank these 200,” then a human sees the page, then apply). Children still cannot apply while the human is thinking.
Agreement among children is not apply either. Two children that both “think” the customer should be refunded still return labels. The parent reduce may vote refund_candidate. HITL may agree. Apply then runs once with a key. “Two children agreed” in the quiz is a trap: chorus is not a ledger, same as the grounded judge.
Walkthrough: child refund blocked; parent once
Child score on prefix c1: ok.
Child refund: write_barrier. World unchanged.
Parent apply refund with key job9:cust1: ok, dup false. Seen set now holds the key.
Parent apply same key: ok, dup true. No second refund.
Wrong parent call score as apply: not_a_write. Apply is for the write names only. Do not mix.
Run to execute this in your browser. Nothing is sent to a server.
What printed: child score ok. Child refund error write_barrier. First parent apply ok with dup: False. Second parent apply ok with dup: True. Child refund is blocked. Parent apply runs once; the retry is dup.
The seen set is a toy of the Tools idempotency store. Production uses a real key-value with TTL and the same semantics: first writer wins. Two parents (a bug) still collide on the key. Combine with one-writer-per-record in the next part so a loyalty agent cannot credit while billing refunds.
Pricing and enqueue
Do not let a child “queue an apply” either. Queuing a write is a write. Children return labels. Parent decide + HITL + apply.
Retries of the parent job must reuse the same apply key. If a crash restarts the job with a new key, you double-refund with perfect barriers and perfect child isolation. The key is derived from job_id plus record id, not from a UUID minted at apply time. Tools already taught this. Swarm parents forget it because “this is just reduce.” It is not just reduce. It is the only door to money.
Price the swarm in the job record before enqueue, then enforce the cap in the worker launcher. A write barrier plus an idempotency key is how you sleep. If launch refused because N was 50, you never needed the barrier for those 50 refunds — they did not start. Both controls matter: refuse over-budget fan-out, and barrier whatever did start.
Swarms of editors on one file cannot be saved by a barrier unless you redefine the swarm as “propose hunks to prefixes, parent merges.” If you cannot merge, it is not a swarm. Sequential one writer. The two-writers lesson will lock the file anyway; do not use a swarm to dodge the lock.
Friday ticket: two children, one customer
Billing fans out 12 invoice lines to scorers. Two lines belong to the same customer. Both scorers would like to be helpful and call refund. The dispatcher returns write_barrier twice. The world does not move. Reduce sees two refund_candidate labels for cust1. HITL agrees once. Apply runs with key job9:cust1. A worker crash retries apply. The store already has the key. dup: True. One refund. That is the whole point of the page.
If the parent minted a fresh UUID at apply time, the retry would look like a new refund. The barrier would still hold for children, and you would still double-pay. The key is job_id plus the record id, not a random token. Write that in the runbook next to the diagram.
| Failure | What you see | Fix |
|---|---|---|
| Child refund on allow-list | Two money events on one ticket | Remove write names from child tools |
| Apply key is a UUID | Dup after crash still pays | Derive key from job + customer |
| Child enqueues apply | Queue fills with writes | Queueing a write is a write; return a label |
| Parent apply before reduce | Refund with no score table | Apply reads reduce output or stops |
| Prefix “scratch” is the ledger | Children edit shared money files | Prefix is not a ledger |
What goes wrong
- Putting
refundon the child allow-list “just for this swarm.” There is no just. The dispatcher is the policy. - Treating
dup: Trueas an error and retrying with a new key. Dup is success. Log it. Do not invent a second door. - Logging only the thought “I would refund” and not the apply result. On-call needs the key and
dup. - A second parent (loyalty) that also apply-refunds. The barrier is per dispatcher, not a law of physics. One writer per record is the next part.
- Skipping HITL because “the children agreed.” Agreement is a label. Money is apply.
How agents use this
Stamp barrier=map on child spans and apply_key on parent writes. Alert if a child span ever has a write tool name, even if denied — prompt drift. Alert if apply runs without a reduce row for that id.
Fixture: 3 children, 2 try refund, 1 scores; after reduce, one parent refund with a key; replay apply; assert one money event. Keep that fixture when you change frameworks.
Swarms of editors on one file cannot be saved by a barrier unless you redefine the swarm as “propose hunks to prefixes, parent merges.” If you cannot merge, it is not a swarm. Sequential one writer.
The failure-modes part next names ping-pong, two writers, and the priced 20-versus-50 launch cap. You already have the habits: typed teams, boring orchestration, debate with a grounded judge, map-reduce with a barrier. The rest is how those teams still fail when a control is missing.
Check your understanding