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

Free lesson

Orchestrator and worker agents

After this lesson: I can split a task into stand-alone briefs whose results I check before joining.

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

A worker only knows its brief

The orchestrator-worker pattern is a program where one agent splits a task into briefs, hands each to a worker and joins the results. It matters because a worker knows nothing but its brief.

A vague brief gets a confident answer to the wrong question. A brief that stands alone gets an answer you can check.

By the end of this lesson

Note

I can split a task into stand-alone briefs whose results I check before joining.

Key words

WordWhat it means
Briefeverything one worker is given: question, material and reply shape
Result shapethe fields a worker must send back
Materialthe text a worker may use for its answer
Validationchecking a result before it is used
Joinputting the accepted results together
Unknown sourcea source a worker cites but was never given

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

What goes in a brief

A worker cannot see the task, the other briefs or the chat. If it is not in the brief, it does not exist.

So a brief holds the question, the material the worker may use, the shape of the reply and a size limit. The material is the text itself, not a promise that the worker can look it up.

Worked example: one brief

FieldExampleWhy it is there
questionWhat would a Sunday cost in staff time?One question, not the whole task
materialThe staff cost sheet, as textThe worker can use nothing else
sourcess2So the answer can name where it came from
replyAn object with answer and sourcesSo the orchestrator can check it

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

Validate a result before you join it

A result is data from a part of the system you do not control. Check the shape first: is there an answer, is there a list of sources. Then check that every source is one the brief gave.

Join only what passes. The module on building agents passed a message between agents. Here you decide whether to believe what comes back.

A common mistake

Warning

Many people join every result and fix the mess afterwards. One bad result then sits in the final answer with the same weight as a good one. Reject at the door and keep the reason.

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\";' '// 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

Command
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 orchestrator, part 1

Command
docker exec -w /home/node/project agent sh -c "printf '%s\n' 'const { ask } = require(\"./model\");' 'const SOURCES = {' '  s1: \"Survey: 212 of 400 visitors asked for Sunday opening.\",' '  s2: \"Staff: six hours at 18 pounds an hour is 108 pounds each Sunday.\",' '  s3: \"Visits: Saturday had 310 visits last month, a weekday had 120.\",' '};' '// A brief carries everything its worker may use: the question and the text.' 'function brief(id, question, ids) {' '  const material = ids.map(function (i) { return SOURCES[i]; });' '  return { id: id, question: question, sources: ids, material: material, reply: \"an object with answer and sources\" };' '}' > team.js"

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

Write the orchestrator, part 2

Command
docker exec -w /home/node/project agent sh -c "printf '%s\n' '// Scripted workers. Each sees its own brief and nothing else.' 'const WORKERS = {' '  b1: function (b) { return { answer: \"212 of 400 visitors want it\", sources: [\"s1\"] }; },' '  b2: function (b) { return { answer: \"108 pounds each Sunday\", sources: [\"s2\"] }; },' '  b3: function (b) { return { answer: \"Weekends are busy\", sources: [\"s9\"] }; },' '};' 'function problems(b, r) {' '  const out = [];' '  const full = typeof r.answer === \"string\" && r.answer.trim().length > 0;' '  if (full === false) out.push(\"missing answer\");' '  if (Array.isArray(r.sources) === false || r.sources.length === 0) out.push(\"missing sources\");' '  else r.sources.forEach(function (s) { if (b.sources.includes(s) === false) out.push(\"unknown source \" + s); });' '  return out;' '}' >> team.js"

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

Write the orchestrator, part 3

Command
docker exec -w /home/node/project agent sh -c "printf '%s\n' 'async function main() {' '  const own = \"How busy is the library on a weekend day?\";' '  const q = [{ role: \"user\", content: \"Write one short research question about how busy a library is at weekends.\" }];' '  const text = (await ask(q, own, 30)).text.trim();' '  const briefs = [' '    brief(\"b1\", \"How many visitors want Sunday opening?\", [\"s1\"]),' '    brief(\"b2\", \"What would a Sunday cost in staff time?\", [\"s2\"]),' '    brief(\"b3\", text.endsWith(\"?\") ? text : own, [\"s3\"]),' '  ];' '  for (const b of briefs) {' '    const r = WORKERS[b.id](b);' '    const p = problems(b, r);' '    console.log(b.id + \" [\" + b.material.length + \" source] \" + b.question);' '    console.log(\"   \" + (p.length ? \"REJECTED: \" + p.join(\", \") : \"accepted: \" + r.answer));' '  }' '}' 'main();' >> team.js"

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

Run the orchestrator

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

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

The workers are scripted

The lab has one model, so a real team would be one model playing every part. The three workers here are plain functions with recorded answers. The one live call only words a research question.

A real system sends each brief to a model, and the check stays the same.

One possible output

LineWhat it shows
b1 [1 source] How many visitors want Sunday opening?A brief with its material
accepted: 212 of 400 visitors want itShape and sources both pass
accepted: 108 pounds each SundayThe second worker also passes
REJECTED: unknown source s9The third cited a source it was never given

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

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.