A team fails in ways one agent cannot
A multi-agent failure mode is a way a team of agents goes wrong that no single agent would. It matters because the fault is in the conversation between them, so each agent looks fine alone.
You find it by reading the transcript, the list of messages they sent.
By the end of this lesson
Note
I can find the failure in a transcript and name its kind.
Key words
| Word | What it means |
|---|---|
| Transcript | the messages agents sent, in order |
| Loop | the same message sent again and again |
| Deadlock | two agents each waiting for the other |
| Runaway cost | token use that grows past the budget |
| Passed on | a guess that is later repeated as a fact |
| Guard | a rule in code that stops a failure early |
The cells students fill in are left blank in this preview.
Four ways a team goes wrong
A loop repeats one message. A deadlock has two agents each waiting for the other to go first. A runaway cost grows, because each round reads the whole history so far.
The fourth is quieter. A worker guesses, the next agent repeats the guess, and by the third it is written as a fact.
Worked example: the sign and the guard
| Failure | Sign in a transcript | Guard |
|---|---|---|
| Loop | The same sender, receiver and text three times | Stop at the third repeat |
| Deadlock | Two messages that both say waiting for, swapped | A time limit and a tie-break |
| Passed on | A guess, then the same words as a fact | Carry the status with the claim |
| Runaway | Tokens per round that keep rising | A budget for the whole run |
The cells students fill in are left blank in this preview.
Check the sharpest sign first
A real transcript may show more than one sign. Decide an order and keep it. A deadlock and a loop both stop work, so name them before cost.
The module on building agents added a loop guard. This lesson is about recognising all four, so you know which guard to add.
A common mistake
Warning
Many people add a retry when a team stalls. A retry makes a loop longer and a deadlock no shorter. First name the failure, then pick the guard that fits it.
Write the model helper, part 1
docker exec -w /home/node/project agent sh -c "printf '%s\n' 'const base = process.env.AI_URL || \"http://ai:4000\";' '// The token counts of a reply, or null when the reply carries none.' 'function count(data, name) {' ' return data.usage && typeof data.usage[name] === \"number\" ? data.usage[name] : null;' '}' > model.js"Shown here, not run. Create a free account to press Run and type it into your own lab’s terminal.
Write the model helper, part 2
docker exec -w /home/node/project agent sh -c "printf '%s\n' 'async function ask(messages, sample, max) {' ' try {' ' const res = await fetch(base + \"/v1/chat/completions\", {' ' method: \"POST\",' ' headers: { \"content-type\": \"application/json\", authorization: \"Bearer lab\" },' ' body: JSON.stringify({ model: \"lab\", messages: messages, max_tokens: max || 100 }),' ' signal: AbortSignal.timeout(30000),' ' });' ' if (res.ok === false) throw new Error(\"status \" + res.status);' ' const data = await res.json();' ' const text = data.choices[0].message.content;' ' return { text, inTokens: count(data, \"prompt_tokens\"), outTokens: count(data, \"completion_tokens\"), recorded: false };' ' } catch (error) {' ' console.log(\"Recorded sample: no model answered (\" + error.message + \").\");' ' return { text: sample, inTokens: null, outTokens: null, recorded: true };' ' }' '}' 'module.exports = { ask };' >> model.js"Shown here, not run. Create a free account to press Run and type it into your own lab’s terminal.
Write the failure finder, part 1
docker exec -w /home/node/project agent sh -c "printf '%s\n' 'const { ask } = require(\"./model\");' 'function m(from, to, text, tokens, status) {' ' return { from: from, to: to, text: text, tokens: tokens, status: status };' '}' 'const T = {};' 'T.loop = [m(\"writer\", \"checker\", \"check the draft\", 300, \"none\"), m(\"checker\", \"writer\", \"add a source\", 200, \"none\"),' ' m(\"writer\", \"checker\", \"check the draft\", 300, \"none\"), m(\"checker\", \"writer\", \"add a source\", 200, \"none\"),' ' m(\"writer\", \"checker\", \"check the draft\", 300, \"none\")];' 'T.deadlock = [m(\"planner\", \"reviewer\", \"waiting for the review before I plan\", 150, \"none\"),' ' m(\"reviewer\", \"planner\", \"waiting for the plan before I review\", 150, \"none\")];' > fail.js"Shown here, not run. Create a free account to press Run and type it into your own lab’s terminal.
Write the failure finder, part 2
docker exec -w /home/node/project agent sh -c "printf '%s\n' 'T.passed = [m(\"reader\", \"planner\", \"Saturday visits rose 40 percent\", 200, \"guess\"),' ' m(\"planner\", \"writer\", \"Saturday visits rose 40 percent\", 200, \"fact\")];' 'T.grows = [m(\"a\", \"b\", \"round one\", 1000, \"none\"), m(\"b\", \"a\", \"round two\", 2000, \"none\"), m(\"a\", \"b\", \"round three\", 4000, \"none\"),' ' m(\"b\", \"a\", \"round four\", 8000, \"none\"), m(\"a\", \"b\", \"round five\", 16000, \"none\")];' 'T.fine = [m(\"planner\", \"reader\", \"find the Sunday demand\", 200, \"none\"), m(\"reader\", \"planner\", \"212 of 400\", 150, \"fact\")];' >> fail.js"Shown here, not run. Create a free account to press Run and type it into your own lab’s terminal.
Write the failure finder, part 3
docker exec -w /home/node/project agent sh -c "printf '%s\n' 'function some2(ms, test) {' ' return ms.some(function (x, i) { return ms.some(function (y, j) { return i < j && test(x, y); }); });' '}' 'function deadlock(ms) {' ' return some2(ms, function (x, y) {' ' return x.text.startsWith(\"waiting for\") && y.text.startsWith(\"waiting for\") && x.from === y.to && x.to === y.from;' ' });' '}' 'function loop(ms) {' ' return ms.some(function (x) {' ' return ms.filter(function (y) { return y.from === x.from && y.to === x.to && y.text === x.text; }).length >= 3;' ' });' '}' >> fail.js"Shown here, not run. Create a free account to press Run and type it into your own lab’s terminal.
Write the failure finder, part 4
docker exec -w /home/node/project agent sh -c "printf '%s\n' 'function passedOn(ms) {' ' return some2(ms, function (x, y) { return x.status === \"guess\" && y.status === \"fact\" && x.text === y.text; });' '}' 'function total(ms) { return ms.reduce(function (a, x) { return a + x.tokens; }, 0); }' 'function find(ms, budget) {' ' if (deadlock(ms)) return \"deadlock\";' ' if (loop(ms)) return \"loop\";' ' if (passedOn(ms)) return \"passed-on\";' ' return total(ms) > budget ? \"runaway\" : \"none\";' '}' >> fail.js"Shown here, not run. Create a free account to press Run and type it into your own lab’s terminal.
Write the failure finder, part 5
docker exec -w /home/node/project agent sh -c "printf '%s\n' 'async function main() {' ' for (const name of Object.keys(T)) {' ' console.log(name + \": \" + T[name].length + \" messages, \" + total(T[name]) + \" tokens, found \" + find(T[name], 20000));' ' }' ' const q = [{ role: \"user\", content: \"In one sentence: what simple rule stops two agents sending one message for ever?\" }];' ' const r = await ask(q, \"Count each message and stop at the third repeat.\", 40);' ' console.log(\"Model: \" + r.text.trim());' '}' 'main();' >> fail.js"Shown here, not run. Create a free account to press Run and type it into your own lab’s terminal.
Run the failure finder
docker exec -w /home/node/project agent node fail.jsShown here, not run. Create a free account to press Run and type it into your own lab’s terminal.
These are recorded transcripts
A real run would spend the lab's allowance on one broken team. So the five transcripts are recorded data, short and invented. The finder is real code.
The one live call asks for a rule. Its words differ each run.
One possible output
| Line | What it shows |
|---|---|
| loop: 5 messages, 1300 tokens, found loop | One message three times |
| deadlock: 2 messages, 300 tokens, found deadlock | Both wait for the other |
| passed: 2 messages, 400 tokens, found passed-on | A guess became a fact |
| grows: 5 messages, 31000 tokens, found runaway | Over the 20000 budget |
| fine: 2 messages, 350 tokens, found none | No sign of failure |
The cells students fill in are left blank in this preview.