InterviewKata
AI engineering · Case study 12 · AI in the real world

Cognition: don't build multi-agents

The day before Anthropic published its multi-agent research system (C11), Walden Yan at Cognition, the company behind the Devin coding agent, published the opposite advice: don't build multi-agents. Read together, the two posts don't contradict each other. They describe different kinds of work. This case explains Cognition's two principles, the Flappy Bird example that made them famous, and how to tell which kind of work you have.

A 2-minute conversation: Sam asks, Priya explains
0:00 / 1:58

1. The situation

Splitting a task across agents is tempting: each subagent gets a smaller job and a fresh context window, and they can run at once. Frameworks made it easy to set up. Cognition, building a coding agent that works on long, tightly connected tasks, found that these setups were fragile.

Real case, an argument from experienceAll quotes are from "Don't build multi-agents" (Walden Yan, Cognition, 12 June 2025). It's a reasoned argument from building Devin, not a measured study; the demo's game decisions are illustrative.

2. The two principles

PrincipleWhat it means
"Share context, and share full agent traces, not just individual messages"A subagent given only its sub-task misses everything the main conversation established: the user's preferences, earlier decisions, what was tried.
"Actions carry implicit decisions, and conflicting decisions carry bad results"Every piece of work embeds choices nobody stated. Two agents working apart make different ones, and the parts don't fit.

The recommendation: a single-threaded, linear agent, where every action sees every earlier decision. For tasks too long for one context window, a separate model can "compress a history of actions & conversation into key details, events, and decisions"; Yan notes this is hard to get right.

3. How the split goes wrong

4. The split in code

The task needs decisions about art style, gravity and the flap. Two subagents given only the task text fall back on their own reasonable-looking defaults, and four decisions conflict with what the user meant. Sharing the full trace of earlier decisions fixes it; so does one agent with one context. The program was run in all three languages; each prints the output below it.

# Decisions made earlier in the main conversation: what "a Flappy Bird clone" meant to this user.
TRACE = {"art style": "8-bit pixel art", "gravity": "strong fall", "flap": "instant jump"}

# What each subagent assumes when nobody told it (its own reasonable-looking defaults).
DEFAULTS = {
    "background": {"art style": "Super Mario-style hills"},
    "bird": {"art style": "smooth vector art", "gravity": "gentle glide", "flap": "gradual lift"},
}
NEEDS = {"background": ["art style"], "bird": ["art style", "gravity", "flap"]}


def subagent(part, shared):
    """Builds one part. Every decision comes from what it was given, or else from its own defaults."""
    return {d: shared.get(d, DEFAULTS[part][d]) for d in NEEDS[part]}


def merge(parts):
    """The final step: compare every decision with what the user meant."""
    clashes = []
    for part, decisions in parts.items():
        for d, value in decisions.items():
            if value != TRACE[d]:
                clashes.append(f"{part} {d} = {value}")
    return clashes


setups = [
    ("two subagents, task text only", lambda part: subagent(part, {})),
    ("two subagents, full trace shared", lambda part: subagent(part, TRACE)),
    ("one agent, one context", lambda part: {d: TRACE[d] for d in NEEDS[part]}),
]
for name, build in setups:
    clashes = merge({part: build(part) for part in ("background", "bird")})
    print(f"{name}: {'fits together' if not clashes else str(len(clashes)) + ' conflicting decisions'}")
    for c in clashes:
        print("   ", c)
type Decisions = Record<string, string>;

// Decisions made earlier in the main conversation: what "a Flappy Bird clone" meant to this user.
const TRACE: Decisions = { "art style": "8-bit pixel art", gravity: "strong fall", flap: "instant jump" };

// What each subagent assumes when nobody told it (its own reasonable-looking defaults).
const DEFAULTS: Record<string, Decisions> = {
  background: { "art style": "Super Mario-style hills" },
  bird: { "art style": "smooth vector art", gravity: "gentle glide", flap: "gradual lift" },
};
const NEEDS: Record<string, string[]> = { background: ["art style"], bird: ["art style", "gravity", "flap"] };

// Builds one part. Every decision comes from what it was given, or else from its own defaults.
function subagent(part: string, shared: Decisions): Decisions {
  return Object.fromEntries(NEEDS[part].map((d) => [d, shared[d] ?? DEFAULTS[part][d]]));
}

// The final step: compare every decision with what the user meant.
function merge(parts: Record<string, Decisions>): string[] {
  const clashes: string[] = [];
  for (const [part, decisions] of Object.entries(parts)) {
    for (const [d, value] of Object.entries(decisions)) if (value !== TRACE[d]) clashes.push(`${part} ${d} = ${value}`);
  }
  return clashes;
}

const setups: [string, (part: string) => Decisions][] = [
  ["two subagents, task text only", (part) => subagent(part, {})],
  ["two subagents, full trace shared", (part) => subagent(part, TRACE)],
  ["one agent, one context", (part) => Object.fromEntries(NEEDS[part].map((d) => [d, TRACE[d]]))],
];
for (const [name, build] of setups) {
  const clashes = merge({ background: build("background"), bird: build("bird") });
  console.log(`${name}: ${clashes.length ? clashes.length + " conflicting decisions" : "fits together"}`);
  for (const c of clashes) console.log("   ", c);
}
import java.util.*;
import java.util.function.Function;

public class Main {
    // Decisions made earlier in the main conversation: what "a Flappy Bird clone" meant to this user.
    static final Map<String, String> TRACE = Map.of("art style", "8-bit pixel art", "gravity", "strong fall", "flap", "instant jump");

    // What each subagent assumes when nobody told it (its own reasonable-looking defaults).
    static final Map<String, Map<String, String>> DEFAULTS = Map.of(
            "background", Map.of("art style", "Super Mario-style hills"),
            "bird", Map.of("art style", "smooth vector art", "gravity", "gentle glide", "flap", "gradual lift"));
    static final Map<String, List<String>> NEEDS = Map.of("background", List.of("art style"), "bird", List.of("art style", "gravity", "flap"));

    // Builds one part. Every decision comes from what it was given, or else from its own defaults.
    static Map<String, String> subagent(String part, Map<String, String> shared) {
        Map<String, String> out = new LinkedHashMap<>();
        for (String d : NEEDS.get(part)) out.put(d, shared.getOrDefault(d, DEFAULTS.get(part).get(d)));
        return out;
    }

    // The final step: compare every decision with what the user meant.
    static List<String> merge(Map<String, Map<String, String>> parts) {
        List<String> clashes = new ArrayList<>();
        parts.forEach((part, decisions) -> decisions.forEach((d, value) -> { if (!value.equals(TRACE.get(d))) clashes.add(part + " " + d + " = " + value); }));
        return clashes;
    }

    public static void main(String[] args) {
        Map<String, Function<String, Map<String, String>>> setups = new LinkedHashMap<>();
        setups.put("two subagents, task text only", part -> subagent(part, Map.of()));
        setups.put("two subagents, full trace shared", part -> subagent(part, TRACE));
        setups.put("one agent, one context", part -> { Map<String, String> m = new LinkedHashMap<>(); for (String d : NEEDS.get(part)) m.put(d, TRACE.get(d)); return m; });
        setups.forEach((name, build) -> {
            Map<String, Map<String, String>> parts = new LinkedHashMap<>();
            parts.put("background", build.apply("background"));
            parts.put("bird", build.apply("bird"));
            List<String> clashes = merge(parts);
            System.out.println(name + ": " + (clashes.isEmpty() ? "fits together" : clashes.size() + " conflicting decisions"));
            for (String c : clashes) System.out.println("    " + c);
        });
    }
}
two subagents, task text only: 4 conflicting decisions
    background art style = Super Mario-style hills
    bird art style = smooth vector art
    bird gravity = gentle glide
    bird flap = gradual lift
two subagents, full trace shared: fits together
one agent, one context: fits together

5. Try it: spot the hidden decision

Each line is a piece of work split between two agents that don't share context. Is it safe to split, or will they make conflicting decisions?

    6. Two posts, one rule

    Anthropic (C11)Cognition (C12)
    WorkBroad research: many independent leadsCoding: one connected artefact
    PartsIndependent: one lead's finding doesn't change another'sCoupled: the bird must match the background
    ArchitectureLead with parallel subagentsOne agent, compression for long runs
    Cost of being wrongA duplicated searchParts that don't fit and must be rebuilt

    The rule that reconciles them: split only work whose parts don't share decisions, and when you do split, pass the decisions along. Read-only exploration (searching, reviewing, summarising) splits well; writing one connected thing usually doesn't.

    7. Lessons

    8. Your turn

    1. List hidden decisions (10 min). For a feature you're building, write five decisions nobody stated (naming, error handling, styling, data shapes).
    2. Try a split (15 min). Give two agents halves of a small coupled feature with only the task text. Count conflicts. Repeat with the decisions written down.
    3. Write the rule (5 min). One sentence for your team on what may be split across agents.
    Say it in the interview
    • "Why not split this across agents?" Because the parts share decisions. Cognition's point is that actions carry implicit decisions; agents that don't share context make conflicting ones. I'd use one agent with compaction, and use subagents only for independent reading tasks.

    9. Check yourself

    What are Cognition's two principles?

    Agents that don't share context make conflicting decisions.

    In the Flappy Bird example, why didn't the parts fit?

    The background looked like Super Mario; the bird didn't move like Flappy Bird's.

    Which task is safest to split across subagents?

    Independent reading tasks share no decisions.

    For a task too long for one context window, what does Cognition suggest?

    One thread of decisions, kept short enough to fit.

    10. Sources and further reading

    ResourceWhy
    Don't build multi-agents (Walden Yan, Cognition, June 2025)Every quote on this page.
    C11: Anthropic's multi-agent research systemThe case where splitting pays.
    Modules 07a and 05aArchitectures, and compression for long tasks.