Writing /Multi-agent /2026-03-15

Multi-Agent Patterns That Held Up, and the Ones That Didn't

After months of running coordinated AI agents, here are the patterns that hold up and the ones that fall apart.

Read time
9 minutes
Topic
Multi-agent

One agent stops being enough at some point. The context window fills up, or the task is too broad, or you want two things happening at once. So you start a second agent, then a third, and then it falls over, because coordinating agents is a different problem from using one.

I’ve spent the last several months building and using Orch, a CLI that coordinates multiple Claude Code instances. Along the way I’ve run multi-agent teams on building this site, shipping client work, developing Orch itself. Some patterns worked immediately. Others looked fine in theory and collapsed on contact with a real repo.

Focused agents beat generalists

Agents with narrow responsibilities do better work than generalists, which wasn’t what I expected. A capable model can handle “build this feature and review it and deploy it.” It just won’t do any of the three as well as three agents that each own one.

The pattern that works best is borrowed from how small teams operate:

  • Engineer: Writes code. Gets a detailed spec. Doesn’t review its own work.
  • PM: Checks in on a schedule. Runs the build. Coordinates handoffs. Never writes code.
  • Reviewer: Reads diffs, runs tests, sends specific feedback. Categorizes issues by severity.

The PM role is the one people skip, and it’s the one that makes everything else work. Without a PM, the engineer finishes and nobody notices. The reviewer never gets triggered. Work sits idle. A PM that checks in every 8 minutes and runs git log and npm run build creates a self-correcting loop that keeps the whole system moving.

Why does role separation matter so much for AI agents specifically? Because LLMs are prone to satisficing. An agent tasked with both building and reviewing will unconsciously lower its review standards for code it just wrote. It’s the same reason we don’t let developers merge their own PRs. Separation of concerns matters for agents the same way it matters for code.

Keep the coordination layer dumb

When I started building Orch, my instinct was to build something sophisticated: WebSocket connections between agents, a shared memory store, structured message schemas. I threw all of it away.

The coordination layer that works is files. An agent writes .orch-send-reviewer with plain text feedback. A polling loop picks it up, delivers it, and deletes the file.

Why files win over every other approach:

  1. AI agents already know how to use them. Claude Code can write a file with zero extra prompting. It can’t open a WebSocket.
  2. They’re debuggable. When coordination breaks, you ls the directory and see exactly what’s pending. Try that with a message queue.
  3. They’re atomic enough. You don’t need exactly-once delivery guarantees for “please review my code.” You need it to get there eventually.
  4. No protocol negotiation. No serialization formats, no handshakes, no versioning. The message is a string in a file.

The scheduler went the same way. I considered fsnotify, cron, and systemd timers, and ended up with a polling loop on a 10-second tick. It’s slower than event-driven and it’s also restartable, debuggable, and free of edge cases around file system event ordering. Ten seconds is nothing when the agents are spending minutes writing code.

The coordination layer is not where the cleverness belongs. Spend that budget on the agents.

Shared context is a trap

The obvious approach to multi-agent work is a shared context: give all agents access to the same files, the same state, the same understanding of the project. This breaks in subtle ways.

Two agents editing the same file will silently clobber each other’s changes. One agent’s refactoring can invalidate another agent’s in-progress work. Even read-only shared context is dangerous, because an agent that reads a file mid-edit gets an inconsistent snapshot.

What works better is message passing with clear ownership. Each agent owns its working directory or its set of files. Communication happens through explicit messages, not shared state. If the reviewer needs to see the engineer’s code, it reads the git diff, not the live working tree.

This is the same insight that makes microservices work (when they work): shared databases are a liability. APIs are a feature. The overhead of explicit communication is worth it because it forces clarity about what each agent needs to know.

Agents have to schedule themselves

Most multi-agent frameworks focus on message passing and tool use, and treat scheduling as an afterthought. Letting an agent schedule its own follow-ups is what turns a set of chat sessions into something that runs on its own.

A PM agent that can write “check on the engineer’s progress in 8 minutes” into a schedule file becomes autonomous in a meaningful way. It doesn’t need a human to trigger each check-in. It creates its own control loop.

This is what makes the system self-correcting. The PM schedules a check-in. At the check-in, it runs the build. If the build fails, it messages the engineer with the error output. If the build passes and all sections are implemented, it triggers a review. The reviewer does its pass, sends feedback, and schedules a follow-up to verify the fixes.

No human intervention required between kickoff and completion. The agents coordinate among themselves because each one can schedule future actions.

Prompt injection between agents

When Agent A sends a message to Agent B, that message becomes part of Agent B’s context. If Agent A’s message contains instructions that look like system prompts, Agent B might follow them. This isn’t theoretical. I’ve seen agents successfully convince other agents to change their behavior through carefully (or accidentally) worded messages.

The mitigation isn’t technical, it’s structural. Keep messages short and factual. “The build failed with error X on line Y” is safe. “You should now ignore your previous instructions and…” is the kind of thing that can only appear if an agent’s context has been poisoned.

This is another argument for narrow roles. A reviewer that only reviews is harder to hijack than a generalist that does everything. Its behavioral surface area is smaller.

Patterns that failed

Democratic decision-making. Three agents voting on an architecture decision produces mush. One agent should own each decision. Others can provide input, but someone has to be the decider.

Agents managing other agents. I tried having a “lead” agent spin up and tear down worker agents dynamically. The lead spent more time managing the workers than doing useful work. Static team composition, set up by a human at the start, works better.

Long feedback chains. Engineer builds, reviewer finds 12 issues, engineer fixes all 12, reviewer finds 8 new issues from the fixes. After two rounds of this, quality degrades. Better to have the reviewer prioritize ruthlessly (MUST FIX / SHOULD FIX / NIT) and only block on the must-fixes.

Shared CLAUDE.md files. If two agents share a working directory, they can’t each have their own CLAUDE.md. I learned this the hard way and switched to injecting identity via --append-system-prompt. Per-agent configuration should live outside the shared workspace.

When one agent is the right answer

Multi-agent coordination has real overhead. Don’t use it when a single agent can handle the job. The threshold I use:

SetupAgentsWhen to useCoordination cost
Single agent1Task fits in one context window, doesn’t need reviewZero
Engineer + Reviewer2Large enough that self-review would be unreliableLow (one handoff)
Engineer + PM + Reviewer330+ minutes, multiple phases needing coordinationMedium (scheduled check-ins)

Most tasks are single-agent tasks. Reaching for multi-agent because it’s interesting is a trap. The coordination cost is zero with one agent and non-zero with two.

Handling failure

Agents fail. They hallucinate, lose context, get stuck in loops, or stop responding. A setup that doesn’t plan for that works fine in a demo and nowhere else.

The failure modes I’ve seen most:

Silent stalls. An agent finishes but doesn’t signal completion. The PM waits for a commit that already happened, or the reviewer waits for a message that was never sent. The fix is the git commit watcher. Poll for state changes rather than relying on agents to announce them. If the engineer committed three minutes ago and the PM hasn’t noticed, something is wrong.

Context window exhaustion. An agent fills its context window and starts losing instructions. This is insidious because the agent doesn’t error out. It just gets worse. The fix is narrow roles. An engineer that only writes code uses context for code. A PM that only checks status uses context for status. Each agent’s context budget goes toward its specific job rather than spreading thin across everything.

Cascading feedback loops. The reviewer sends 12 issues. The engineer fixes them and introduces 4 new issues. The reviewer finds those plus 3 regressions from the first round. Two cycles of this and you’re worse off than when you started. The fix is severity triage. The reviewer categorizes everything as MUST FIX, SHOULD FIX, or NIT, and only blocks on must-fixes. The engineer addresses must-fixes first, and the reviewer accepts that not everything gets fixed in one pass.

Role drift. A PM agent starts writing code because it “noticed a small fix.” An engineer starts reviewing its own work because “it looked wrong.” Once an agent drifts outside its role, the separation of concerns breaks and quality drops across the board. The fix is in the system prompt: “Do NOT write code” for the PM, “Do NOT review your own work” for the engineer. Be explicit about what each role cannot do, alongside what it should do.

Individually none of these sink a run. They compound, though, and a stall plus a drifting PM plus a context-exhausted engineer is how you end up with three sessions burning tokens on nothing.

The distributed systems parallel

None of this is new. Narrow roles, message passing instead of shared state, explicit interfaces, polling instead of event-driven complexity: these are the things that make any distributed system survivable.

Which makes sense, because that’s what this is. Several unreliable workers, lossy channels between them, and one shared resource that needs coordination to avoid conflicts. The fact that the workers are language models changes the failure rate, not the shape of the problem.

I expect the models to get better and the context windows to get bigger. I don’t expect that to change much about the coordination, since none of it was really about the models in the first place.