Curriculum/Multi-Agent Systems
Hop Limits and Acyclic Graphs
A may call B, B may not call A. Same payload hash twice stops. Unbounded handoff is an infinite loop. Cap hops before you add a manager persona.
Peer handoff needs three brakes before you turn it on in production:
- Max hops for the whole job
- Allow-list of edges (A→B ok, B→A denied)
- Duplicate detection — same
(from, to, payload_hash)twice → stop
A may call B. B may not call A. The same payload twice stops.
Three brakes on handoffUnbounded handoff is an infinite loop with a chat UI. A third agent named Manager with more tools is not a control. It is another node that can join the bounce. The failure-modes part will detect ping-pong in depth. This lesson is the graph you install so ping-pong is illegal, not merely sad.
Sequential mode does not need peer edges. If you are still sequential, keep this lesson as the reason you do not “just enable handoff” in the framework’s default mesh. If you are already in handoff, install the brakes today.
Three brakes, in order
Max hops. A small integer on the job (4, 8, 12 — pick and measure). Each accepted edge increments. Hitting the cap is stop: max_hops with the path attached. It is not a suggestion to buy a larger model. Eval this with a fixture that would walk forever.
Edge allow-list. A set of tuples ("intake", "billing"), ("intake", "tech"). Not in the set means illegal_edge, even if hops remain. Acyclic for the default: if A may call B, B may not call A. Cycles are how “please review / please revise” lives. If you truly need a worker→planner replan, that is a named edge with a budget of one, or a return to a supervisor, not a free reverse.
Duplicates. The toy below uses a coarse key src + "->" + dst so you can see the idea without hashing. Production hashes the payload too (ping-pong lesson): intake→billing with the same brief twice is a stop even if you allowed the edge once. Identical briefs bouncing are a stop, not a personality. Pair with Agents tool-thrash: same idea, lifted to roles.
| Stop reason | When | Next action |
|---|---|---|
max_hops | Path length at cap | Handoff to human or fail the job |
illegal_edge | Tuple not in EDGES | Fix the graph; do not grant the edge to “be nice” |
duplicate | Same hop key (and later, same hash) seen | Treat as ping-pong |
done | Graph finished on a terminal node | Run apply / finish in code |
Put the graph in config. Tests can fire illegal edges with no tokens. On-call can read EDGES without decoding a prompt.
Walkthrough: intake may bill; tech may not return
Legal: hops ["billing"] from intake. Stop done. Path intake, billing.
Illegal cycle: hops ["tech", "intake"]. After intake→tech, tech→intake is not in EDGES. Stop illegal_edge.
Cap: hops ["billing", "tech"] with max_hops=2. Path already ["intake"]. First hop to billing makes length 2, which is the cap, so you never add a third name. The exact branch depends on when you check length; the toy checks before appending the next hop if len(path) >= max_hops. Intake plus one hop fills a cap of 2. Too many hops stop even on a legal name.
If you add ("tech", "intake") to EDGES to “let them clarify,” you have chosen a cycle. Then you must rely on hop cap and duplicate hash, and you should prefer a supervisor instead.
Run to execute this in your browser. Nothing is sent to a server.
What printed: intake→billing is stop: done. Tech→intake is illegal_edge with from: tech and to: intake. The third call stops with max_hops (path already at the cap after intake, or after billing, depending on length) — too many hops stop even on a legal name. Read the path field. That is what you log.
The duplicate branch is waiting for a hop list that repeats an edge. Try run(["billing", "billing"]) in your head: after the first billing, src is billing, which has no outgoing edge in EDGES, so you get illegal_edge before duplicate. That is fine. Defense in depth. Add a legal self-loop only if you like pain.
Handoff without these brakes is not “flexible”
Framework defaults often allow any node to call any node. That is a mesh. Meshes are for research demos. Products need EDGES. If routing is so content-dependent that you cannot list edges, you do not understand the product yet. Stay sequential or supervisor until you can list them.
A hop limit of 200 is not a limit. Pick a number you would be ashamed to exceed on a FAQ. High-stakes debate rounds are a separate cap (often one attack). Do not reuse the FAQ hop budget for debate.
Choosing the number is a product decision. Four hops is enough for intake → specialist → critic → stop. Twelve hops means you do not know the graph. Measure p95 hops on goldens before you raise anything. If p95 is already 11 of 12, you do not have slack — you have a loop waiting to happen. Cap first, then shorten the graph, then maybe add one numbered replan edge through a supervisor.
How agents use this
Put the graph in config. Tests can fire illegal edges with no tokens. Pair with Agents tool-thrash: identical briefs bouncing are a stop, not a personality.
Stamp stop on the job with the enum above. Alert on illegal_edge in production: that is a model trying to rewrite the graph. Alert on max_hops: that is a product bug or a poisoned loop. Do not “raise the cap” as the first fix.
When two agents keep handing the same ticket back and forth, the first control to add is this lesson, not a manager persona. The ping-pong lesson adds payload hashes on top. Together they are the loop story from the tax checklist.
Check your understanding