Study guides / CCDV-F / Domain 3

Agents & Workflows · Lesson 2 of 5

3.2 - Single-Agent vs. Multi-Agent Orchestration

Recognise when a task genuinely needs multiple coordinated agents, and pick between the common orchestration shapes when it does.

Most tasks are better served by a single well-scoped agentic loop with the right tools than by multiple agents. Every additional agent is itself a full agentic loop - its own system prompt, its own conversation history, its own token spend re-deriving context the coordinator already has. Multi-agent orchestration earns that added cost and coordination overhead when a task has a concrete, nameable need for isolation (one subagent's context shouldn't pollute another's, or a risky exploratory step shouldn't consume the main agent's context budget) or parallelism (genuinely independent subtasks that don't depend on each other's output).

Coordinator-subagent vs. hub-and-spoke

Two shapes cover most real cases. A coordinator-subagent pattern has one agent plan and delegate to specialised subagents with different tools, different context, and different jobs - one might search the web, another might analyse a document, another might synthesise a report - then integrate their results. It fits heterogeneous subtasks. A hub-and-spoke pattern runs several similar subagents in parallel against different slices of the same uniform problem (e.g. each reviewing a different file for the same style rule) and merges their output at the end. It fits uniform, parallelisable subtasks where the win is throughput, not specialisation.

In both shapes, the coordinator is the hub: it owns decomposition, delegates, aggregates results, and handles errors. Subagents talk back to the coordinator, not to each other - that keeps the coordinator as the single place you can observe what's happening, apply consistent error handling, and control what information flows where.

The isolation principle: what subagents don't inherit

A subagent does not automatically inherit the coordinator's system prompt, its conversation history, or another subagent's results. It starts with only what the coordinator explicitly writes into its prompt or task description. Two separate invocations of the same kind of subagent share nothing either - there's no shared memory across calls unless the coordinator deliberately carries something forward. This is the same statelessness principle from Lesson 1.1, just applied one level up: if it isn't explicitly passed in, it doesn't exist for that subagent.

The practical consequence is that context-passing design is as important as picking the orchestration shape. If a synthesis subagent needs findings from a search subagent, the coordinator must pass those findings - in full, and with any attribution metadata (source, confidence, etc.) intact - because the synthesis subagent cannot "look them up" from anywhere else.

Exam trap: tracing failures to the wrong agent

When a multi-agent system produces incomplete or wrong output, the instinct is to blame whichever subagent produced the bad section. Check the coordinator first. If a coordinator decomposes "impact of AI on creative industries" into only visual-arts subtopics, no subagent can produce coverage of music or film - they were never asked. If a coordinator strips source metadata before passing findings to a synthesis subagent, that subagent cannot cite sources it was never given. In both cases the subagent executed correctly on what it received; the fault is upstream, in decomposition or context passing.

Parallel spawning and the cost you're trading for it

When subtasks are genuinely independent, invoke them in parallel rather than one at a time across sequential turns - waiting for subagent A to finish before starting independent subagent B adds latency for no benefit. But parallelism isn't free: running three subagents multiplies token spend roughly threefold, since each one re-derives whatever context the coordinator gives it from scratch. The parallelism win has to be worth that multiplied cost, which is exactly why single-agent stays the default for anything that doesn't have a clear independent-subtask structure.

A worked example: tracing a coverage gap

A multi-agent research system is tasked with covering "renewable energy technologies." The coordinator decomposes this into two subtopics - solar and wind - and spawns a subagent for each. Both subagents do excellent, well-sourced work on exactly what they were assigned. The final merged report is thorough on solar and wind and says nothing about geothermal, tidal, biomass, or nuclear fusion.

It's tempting to conclude the search subagents didn't look hard enough, or that more subagents are needed. Neither is the fix. The subagents researched precisely what they were told to research; no amount of additional search depth on solar and wind produces a section on geothermal. The coverage gap traces to one place - the coordinator's decomposition only named two of at least six major subtopics. The fix is a coordinator that enumerates the full breadth of a topic before delegating, not a smarter or more numerous set of subagents executing against the same narrow assignment. This is the general shape of most multi-agent failures worth knowing for the exam: incomplete scope traces to decomposition; incomplete attribution or detail within a covered subtopic traces to context passing; only a genuinely wrong subagent output traces to the subagent itself.

Key concept

Default to single-agent. Reach for multi-agent when you can name the specific isolation or parallelism benefit you're getting and it outweighs the coordination overhead and multiplied token cost - not because a task sounds complex enough to deserve it.

Exam traps

Practice question

A task requires reviewing 20 independent files for a specific style violation, with no dependency between files. Which orchestration approach fits best?

  • A A single agent processing all 20 files sequentially in one long context.

    This works but doesn't take advantage of the fact that the files are independent - it's slower than it needs to be and risks context growing unmanageably long.

  • B A coordinator agent that delegates each file to a differently-specialised subagent.

    The subtasks here are uniform (same check, different files), not heterogeneous - specialisation isn't the relevant need.

  • C A hub-and-spoke pattern with parallel subagents each reviewing a slice of the files, merged at the end. Correct

    Uniform, independent, parallelisable subtasks are exactly the case hub-and-spoke is suited for - it gets the parallelism win without needing per-subagent specialisation.

  • D No orchestration is needed since file review doesn't benefit from agentic loops at all.

    This is a legitimate agentic task (read, evaluate, report per file); the question is how to structure it, not whether to use an agent.

Build exercise: Build and compare single-agent, sequential, and parallel hub-and-spoke reviews

Intermediate · 40 minutes

You'll practice:

  1. Write a single agentic loop that reviews a list of 6 short code snippets (as one big prompt containing all 6) for one specific style rule, and note how the response quality and prompt size change as you imagine scaling this to 200 files.

    You need the baseline to compare against - the single-agent approach is legitimate at small scale, and seeing where it starts to strain is what motivates decomposing it.

    You should see: One API call returning a review of all 6 snippets, with the prompt string containing every snippet's text concatenated together.

    Hints
    1. What happens to a single prompt's size and the model's ability to track per-file findings as the file count grows?
    2. Concatenate the snippets with clear delimiters and ask for a per-file verdict list. Notice that the prompt is doing double duty as both instructions and the entire dataset - that doesn't scale.
    3. const prompt = snippets.map((s, i) => `--- File ${i}: ${s.name} ---\n${s.code}`).join("\n\n") + "\n\nFlag any use of var instead of const/let, per file.";
      const response = await client.messages.create({ model: "claude-sonnet-5", max_tokens: 1024, messages: [{ role: "user", content: prompt }] });
  2. Split the 6 snippets into 2 uniform "subagent" calls of 3 files each, each with its own independent messages.create() call and its own explicitly-passed context (just its 3 files and the style rule - nothing else).

    This is the isolation principle in code: each subagent call gets exactly what it needs written into its prompt, and nothing it doesn't, proving to yourself that no context is inherited automatically.

    You should see: Two separate API calls, each with a prompt containing only its assigned 3 files, each returning its own findings independently.

    Hints
    1. If these were run through a shared conversation, what would leak between them that shouldn't? How do you prevent it with separate calls?
    2. Write a reviewSlice(files, rule) function that builds a prompt from only the files passed in, and call it twice with different slices. Each call is a fresh messages array with no shared history.
    3. async function reviewSlice(files, rule) {
        const prompt = files.map(f => `--- ${f.name} ---\n${f.code}`).join("\n\n") + `\n\nRule: ${rule}`;
        const res = await client.messages.create({ model: "claude-sonnet-5", max_tokens: 512, messages: [{ role: "user", content: prompt }] });
        return res.content.find(b => b.type === "text")?.text;
      }
      const slice1 = files.slice(0, 3), slice2 = files.slice(3, 6);
  3. Run both slice reviews in parallel with Promise.all instead of sequentially with two awaits, and measure the wall-clock time difference against running them one after another.

    This is the parallelism win the exam expects you to recognise: independent subtasks invoked together finish in roughly the time of the slowest one, not the sum of both.

    You should see: The Promise.all version completing in noticeably less wall-clock time than two sequential awaits of the same two calls.

    Hints
    1. What JavaScript/TypeScript construct lets two independent async calls run concurrently instead of one blocking the other?
    2. Use Promise.all([reviewSlice(slice1, rule), reviewSlice(slice2, rule)]) instead of awaiting each call in sequence. Time both approaches with console.time/console.timeEnd to see the difference directly.
    3. console.time("parallel");
      const [r1, r2] = await Promise.all([reviewSlice(slice1, rule), reviewSlice(slice2, rule)]);
      console.timeEnd("parallel");
      
      console.time("sequential");
      const s1 = await reviewSlice(slice1, rule);
      const s2 = await reviewSlice(slice2, rule);
      console.timeEnd("sequential");
  4. Deliberately break isolation once on purpose - pass slice2 a review prompt that references "the issues found in the previous file" without actually including them - and observe that the subagent has nothing to work with, then fix it by passing the prior findings explicitly if a task genuinely needs continuity between slices.

    Seeing the isolation failure firsthand is the fastest way to internalise why context has to be explicit - a subagent literally cannot answer questions about information it was never given.

    You should see: The subagent either hallucinating an answer, asking what "the previous file" refers to, or producing an unusable response - followed by a corrected version that includes the actual prior findings and produces a sensible answer.

    Hints
    1. If a subagent's prompt refers to something it was never shown, what are its only two options?
    2. It can either say it doesn't have that information, or (worse) guess - neither is useful. Fix it by adding the actual prior findings as an explicit argument to the function and interpolating them into the prompt.
    3. // Broken: references context that was never passed
      const brokenPrompt = `Given the issues found in the previous file, review these:\n${slice2Text}`;
      
      // Fixed: pass it explicitly
      async function reviewSliceWithContext(files, rule, priorFindings) {
        const prompt = `Prior findings for context: ${priorFindings}\n\nNow review:\n` + files.map(f => f.code).join("\n\n") + `\n\nRule: ${rule}`;
        // ...
      }
  5. Write a small coordinator function that merges the two slice results into one combined report, and confirm the merged output covers all 6 files with no duplication or gaps.

    Aggregation is a coordinator responsibility distinct from delegation - the exam expects you to recognise that merging correctly (not just dispatching correctly) is part of the pattern.

    You should see: A single combined report listing findings for all 6 original files, sourced from the two independent slice results.

    Hints
    1. What's the simplest check that would catch a coordinator merge bug, like accidentally dropping one slice's results?
    2. Concatenate or structure the two results, then assert the merged output mentions all 6 original filenames - a quick sanity check for silent gaps in the merge.
    3. const merged = { findings: [r1, r2].join("\n\n") };
      const allNamed = files.every(f => merged.findings.includes(f.name));
      console.log(allNamed ? "All files covered" : "Coverage gap in merged report");

Sources