Easier to read with cream paper, a tint, more contrast or larger text? Choose your colours and text, once, for every page.

Free lesson

Multi-agent systems and hand-offs

After this lesson: I can split work between agents and say when one agent is enough.

60 min lesson6 commandsAI coding agent lab: use opencode to explain and fix code, and check its work

Two agents that kept passing a ticket

A multi-agent system splits work between agents that each have one role. They pass work along as hand-off messages. It matters because every hand-off costs a call and can lose context.

A billing agent sends a ticket to a stock agent. The stock agent sends it straight back. The ticket bounced all night.

By the end of this lesson

Note

I can split work between agents and say when one agent is enough.

Key words

WordWhat it meansExample
Rolethe one job an agent haspricer
Hand-offa message passing work to another agent
Routerthe part that picks which role gets a task
Shared recorda log every agent can read and add to
Loop guarda check that stops work bouncing for ever

The cells students fill in are left blank in this preview.

When one agent is enough

Start with one agent and one prompt. Add a second only when you can name the gain. It may be a shorter prompt, fewer tools in view, a different model, or a step a person must approve.

Each extra agent adds calls, delay and a place where context leaks. If one agent with three tools does the job, stop there.

Worked example: one agent or several

SituationChoiceReason
Look up a price and say itOne agentOne tool, one prompt
Price, stock and delivery rules, 20 toolsRolesEach role sees only its own tools
A refund over 100 poundsA person stepApproval is not an agent job

The cells students fill in are left blank in this preview.

Write the model helper, part 1

Command
docker exec -w /home/node/project agent sh -c "printf '%s\n' 'const base = process.env.AI_URL || \"http://ai:4000\";' '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 || 200 }),' '      signal: AbortSignal.timeout(30000),' '    });' '    if (res.ok === false) throw new Error(\"status \" + res.status);' '    return (await res.json()).choices[0].message.content;' > 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

Command
docker exec -w /home/node/project agent sh -c "printf '%s\n' '  } catch (error) {' '    console.log(\"Recorded sample: no model answered (\" + error.message + \").\");' '    const list = [].concat(sample);' '    const turn = messages.filter(function (m) { return m.role === \"assistant\"; }).length;' '    return list[Math.min(turn, list.length - 1)];' '  }' '}' '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 hand-off demo, part 1

Command
docker exec -w /home/node/project agent sh -c "printf '%s\n' 'const { ask } = require(\"./model\");' 'const PRICES = { pear: 45, bread: 120 };' 'const STOCK = { pear: 12, bread: 0 };' 'function look(table, q, unit) {' '  const key = Object.keys(table).find(function (w) { return q.indexOf(w) >= 0; });' '  return key === undefined ? \"nothing found\" : key + \": \" + table[key] + unit;' '}' 'const roles = {' '  pricer: function (q) { return look(PRICES, q, \"p\"); },' '  stocker: function (q) { return look(STOCK, q, \" in stock\"); },' '};' 'function route(q) {' '  if (q.indexOf(\"how much\") >= 0) return \"pricer\";' '  if (q.indexOf(\"in stock\") >= 0) return \"stocker\";' '  return null;' '}' > handoff1.js"

Shown here, not run. Create a free account to press Run and type it into your own lab’s terminal.

Write the hand-off demo, part 2

Command
docker exec -w /home/node/project agent sh -c "printf '%s\n' 'async function pick(q) {' '  const rule = route(q);' '  if (rule) return rule;' '  const text = \"Which role fits this task: pricer, stocker or none? Reply with one word. Task: \" + q;' '  const word = (await ask([{ role: \"user\", content: text }], \"none\", 10)).trim().toLowerCase();' '  return word === \"pricer\" || word === \"stocker\" ? word : \"none\";' '}' 'const trail = [];' 'function allowed(from, to) {' '  trail.push(from + \">\" + to);' '  return trail.filter(function (t) { return t === from + \">\" + to; }).length < 3;' '}' >> handoff1.js"

Shown here, not run. Create a free account to press Run and type it into your own lab’s terminal.

Write the hand-off demo, part 3

Command
docker exec -w /home/node/project agent sh -c "printf '%s\n' 'async function main() {' '  const record = [];' '  for (const q of [\"how much is a pear\", \"is bread in stock\", \"what is the weather\"]) {' '    const role = await pick(q);' '    const result = role === \"none\" ? \"no role fits, ask a person\" : roles[role](q);' '    record.push({ to: role, task: q, result: result });' '  }' '  record.forEach(function (r) { console.log(r.to + \" | \" + r.task + \" | \" + r.result); });' '  let who = \"pricer\";' '  for (let i = 0; i < 6; i++) {' '    const next = who === \"pricer\" ? \"stocker\" : \"pricer\";' '    if (allowed(who, next) === false) { console.log(\"Loop: \" + who + \" to \" + next + \" a third time, stopping\"); break; }' '    who = next;' '  }' '}' 'main();' >> handoff1.js"

Shown here, not run. Create a free account to press Run and type it into your own lab’s terminal.

Run it and read the shared record

Command
docker exec -w /home/node/project agent node handoff1.js

Shown here, not run. Create a free account to press Run and type it into your own lab’s terminal.

One possible output

LineWhat it may show
pricer | how much is a pearpear: 45p
stocker | is bread in stockbread: 0 in stock
The weather tasknone, or a role the model chose, with a weak result
Loopa line saying a pair was seen a third time

The cells students fill in are left blank in this preview.

A hand-off is a message with a shape

A hand-off message names who sends it, who gets it, the task, and the context the receiver needs. The receiver should not have to read the sender's whole history.

Agents write what they did into a shared record. Read it as the one place where the truth about the job lives.

Stop work bouncing between agents

Two agents can each decide the other should handle a task. Nothing is wrong inside either one, yet the work never ends.

Count every from-to pair. When one pair reaches a limit, stop and hand the job to a person. Keep the hand-off trail in the record so the loop is easy to see.

A common mistake

Warning

Many people think a router should always ask a model. A model call is slower, costs money and can name a role that does not exist. Use rules where they are exact, ask the model only for the rest, and check what it names.

About this lab and its source: AI coding agent lab: use opencode to explain and fix code, and check its work

Teaching a class? Set this module for your students with a teacher guide, marked assignments and a lab for every student.