🚶 Choose: breadth-first or depth-first traversal
Traverse (or “traversing”) and visiting the nodes of a graph by following its edges. There are two basic ways to do this, and choosing between them isn’t a technical detail: it’s a strategic decision you’ll repeat with agents.
🆕 New here? BFS and DFS
- BFS (breadth-first search, breadth-first search): visits all the neighbors of a node before going deeper. Imagine exploring a building one floor at a time.
- DFS (depth-first search, depth-first search): chooses a path and follows it to the end before returning and trying another. Imagine walking down a hallway to the last door before turning back.
What to look at: e o same a graph of both sides — only the order of the numbers changes. In breadth, 4 and 5 only appear after the entire second level. In depth, 3 and 4 appear before the right branch is even touched. Neither is “better”: breadth finds the closest one first, depth finds a fast response.
✓ Breadth works when
- ✓You want the closest / shortest (degrees of separation)
- ✓It’s better to scan many options superficially than to dive deep into one
- ✓With agents: “give me 8 different approaches, without going into depth”
✓ Depth works when
- ✓Any valid solution will do, and you want to get there quickly
- ✓The path only makes sense as a whole (you can’t evaluate it halfway through)
- ✓With agents: “pursue this hypothesis all the way before switching”
⚠️ The rule that prevents it from getting stuck
Mark which nodes have already been visited. Without that, any cycle turns your traversal into an infinite loop—the algorithm keeps going back and forth between the same two nodes forever. It’s bug number 1 for anyone who writes a traversal by hand.
Key concepts
One step at a time
Corridor to the end
Without marking it, the cycle gets stuck
Breadth or depth, with agents
🛣️ Find the cheap path (not the shortest one)
“Shortest path” almost always means lowest total weight, not fewer hops. Those are different things, and confusing them costs money. A path with 2 expensive edges can be worse than one with 5 cheap edges.
In the morning routine from the previous module, this became obvious: the shortcut had fewer in nodes and also cost less. But change the weight to “risk” and the picture flips — the shortcut may be the shortest path and the most dangerous at the same time. The weight you choose defines what is “best.”
Choose the weight before optimizing
Tokens? Latency? Risk? Confidence? Each choice produces a different optimal route. Optimizing without stating the weights is optimizing in the dark.
Weight can change during execution
An API’s latency rises, a model gets congested, prices change. An optimal route calculated once becomes a guess after a while.
More than one weight at the same time
When you want something cheap e reliable, there is no “the” best — only trade-offs. And then you’re already at the problem Track 2 calls “one metric is never enough.”
Key concepts
Count weight, not hops
Declare it in advance
Routes age
Turns into a trade-off, not an optimum
📋 Discover the topological order
Topological order is a valid execution order in a DAG: nothing appears before
anything it depends on. That's what the make calculates, which is what your CI calculates —
and what your agent graph orchestrator will calculate.
🎯 Two practical consequences
- •There isn't just one order. Several queues may be valid. That’s good: it means there’s flexibility—and that flexibility is exactly where parallelism can happen.
- •If you can't order them, there's a cycle. The algorithm gets stuck, and that’s a diagnosis, not a bug: it just showed you a circular dependency you hadn’t noticed.
🧪 Practical example: order your graph and detect cycles
Objective: given a dependency graph, get the execution order — and receive a clear error if there’s a cycle. Paste it into the browser console (F12) or run it in Node.
// grafo: no -> lista de quem DEPENDE dele (roda depois)
const dep = {
"pesquisar": ["escrever"],
"escrever": ["revisar"],
"coletar_dados": ["escrever"],
"revisar": []
};
function ordenar(g) {
const grau = {}, saida = [];
Object.keys(g).forEach(n => grau[n] = 0);
Object.values(g).flat().forEach(n => grau[n] = (grau[n] || 0) + 1);
let prontos = Object.keys(grau).filter(n => grau[n] === 0);
while (prontos.length) {
console.log("rodam em PARALELO agora:", prontos.join(", "));
const proximos = [];
for (const n of prontos) {
saida.push(n);
for (const viz of (g[n] || [])) if (--grau[viz] === 0) proximos.push(viz);
}
prontos = proximos;
}
if (saida.length !== Object.keys(grau).length)
throw new Error("CICLO detectado — nao existe ordem valida");
return saida;
}
console.log("ordem final:", ordenar(dep).join(" → "));
How to verify: the first line should say
rodam em PARALELO agora: pesquisar, coletar_dados — the two without dependencies.
Then escrever, then revisar.
Now force a cycle: add "revisar": ["pesquisar"] and run it again — it should trigger the CYCLE error.
If it does, you just implemented circular dependency detection.
Switch: the names by <as etapas do seu processo>. The lines that “RUN IN PARALLEL” are your allocation plan.
Key concepts
No one before your dependency
Several orderings work
Zero = ready to run
Diagnosis, not a bug
⚡ Count the Actual Parallelism
The previous script already printed the answer: each line “run in PARALLEL now” is a level of the graph, and the size of that line is how many workers fit at that point. This applies equally to people and agents. Launching 20 agents in a graph that only allows 3 nodes at a time doesn’t speed anything up—it just wastes resources.
What to look at: the three bands have different widths — 2, 3, and 1. The last level (“combine”) is a bottleneck: no matter how many agents you have, only one passes through there at a time. This pattern—branching into several, then narrowing to one—is the fan-out / fan-in that Track 4 will name.
Key concepts
Who’s ready at the same time
Worker cap
Width level 1
Splits, then joins
🌀 Escape the cycle: limits, progress, human oversight
A cycle isn't a defect — it's what lets you repeat work until it's good. The defect is a cycle no output. There are exactly three types of output, and every cycle you draw needs at least one of them written beside it.
✓ Legitimate outputs
- ✓Cap: “at most 5 rounds”—crude, but it always stops
- ✓Progress criterion: “if two consecutive rounds don’t improve, give up”
- ✓Human: “after N rounds, ask someone”
✗ False outputs
- ✗“It stops when it’s good”—who decides what’s good?
- ✗“The model knows when to stop”—it doesn’t, and the bill comes due
- ✗Mutual deadlock: A waits for B, B waits for A, and no one moves
💡 Practical tip
Use all three together, in layers: progress criteria as the output normal, a ceiling as a safety net, and a human as the final authority. The ceiling alone is wasteful (it runs 5 times even when 2 would do); the criterion alone is risky (if the measurement breaks, it never triggers).
Key concepts
Repeat until it works
Safety net
Normal output
Mutual waiting brings everything to a halt
🔍 Write it in JSON: the format the orchestrator reads
In practice, a graph is a dictionary: for each node, the list of neighbors. This is called adjacency list and handles 95% of cases. The trick for agents is simple: in addition to the neighbors, store the node data — model, tools, prompt, cost limit. That’s what turns an ordinary graph into an agentic graph agentic.
🧪 Practical example: the skeleton you'll reuse through the end of the course
Objective: have a file grafo.json that describes nodes (with agent config) and edges (with conditions). Save as grafo.json in your project folder.
{
"nos": {
"pesquisar": {
"papel": "Levanta fontes sobre <o tema> e resume em notas curtas",
"modelo": "<modelo rapido e barato>",
"ferramentas": ["busca_web", "ler_url"],
"teto_custo_usd": 0.50
},
"escrever": {
"papel": "Escreve o texto usando SO as notas recebidas",
"modelo": "<modelo bom de escrita>",
"ferramentas": [],
"teto_custo_usd": 1.00
},
"revisar": {
"papel": "Confere fatos e clareza. Responde aprovado: true|false",
"modelo": "<modelo forte de raciocinio>",
"ferramentas": ["busca_web"],
"contexto_limpo": true,
"teto_custo_usd": 1.00
}
},
"arestas": [
{ "de": "pesquisar", "para": "escrever", "quando": "sempre", "passa": "notas" },
{ "de": "escrever", "para": "revisar", "quando": "sempre", "passa": "rascunho" },
{ "de": "revisar", "para": "escrever", "quando": "aprovado == false", "passa": "criticas",
"max_voltas": 2 },
{ "de": "revisar", "para": "FIM", "quando": "aprovado == true" }
]
}
How to verify: four checks, all visual —
(1) the JSON is valid (paste it into any validator or run JSON.parse);
(2) there is at least a edge with quando different from "sempre" — otherwise you wrote a queue, not a graph;
(3) every back edge has max_voltas — and it’s the stopping condition from topic 5;
(4) there is a path to "FIM". If any of the four is missing, the graph has a real defect.
Switch: everything between < >. Save this file—the course's Track 5 will turn it into code you can run.
Quick check: Your topological sorting algorithm stalls and can't produce the sequence. What does that mean?
Key concepts
Node → neighbors
Model, tools, ceiling
Without it, it’s just a queue
JSON is enough to get started
📌 Module Summary
Next track:
Track 2 — Loop Engineering: the anatomy of the cycle, why the verifier is the bottleneck, and the four structural ways a loop can betray you.