Back to blog
Series · Day 27
Software Engineering in the AI Era
View all lessons →
The many hands problem

Day 27: The Many Hands Problem in Agent Pipelines

Three agents reported success. Production broke anyway. When someone asked who owned it, nobody in the room could answer. That's the many hands problem. When you chain agents into a pipeline, each stage can do its own job correctly and the system still fails, because every stage assumes someone else is checking. This lesson covers why that happens, why more tracing won't fix it, and the two structural fixes that do.

1. Start in the postmortem

Here's the shape of a postmortem I sat in at PhoenixDX. A three-agent pipeline (planner, coder, test agent) shipped a database migration that dropped a column the planner had marked as deprecated. A reporting service still read that column every night. The next morning the reports came out empty, and the support queue started filling up.

We pulled the run log. All three agents had reported success. The planner had produced a clean plan, the coder had written a migration that matched it line for line, and the test agent had run the suite and got green. Then someone asked the obvious question: "Who owned this?" Nobody answered. Nobody was dodging. There just wasn't an honest answer to give.

Hold on to that silence, because it's what this lesson is about. Fixing the bug was easy. The hard part was working out how a system with three successful parts and full logging ended up with nobody responsible for the result.

2. Rewind one handoff at a time

Walk the pipeline backwards. At each arrow, write down what the sending stage assumed the receiving stage would do. In our case none of those assumptions had been written down anywhere. The shape of the pipeline implied them.

  • ▹Planner → Coder. The planner saw a comment flagging the column as deprecated and planned its removal. Checking whether anything still queried it looked like an implementation detail, so that became the coder's problem.
  • ▹Coder → Test agent. The coder did exactly what the plan said. It had been asked to "write this migration", not to "decide whether this migration is safe". Catching breakage is what tests are for, so safety went to the test agent.
  • ▹Test agent → Deploy. The test agent ran the migration against the fixture database and the repo's suite, and everything passed. It validated exactly what it was given. But what it was given wasn't what was live: the reporting service sat in another repo and never appeared in the fixture.
  • ▹Nobody → Prod. The question "does anything in production still depend on this?" belonged to no stage, and every stage could reasonably believe it belonged to someone else.

No single stage was wrong on its own terms. In most bugs one component misbehaves. Here every component behaved correctly inside its own frame, and the failure lived in the gaps between the frames.

3. Observability gave us a perfect record of an unowned failure

Our tracing was good. Every agent call, prompt, tool invocation and output was captured, and we could replay the run span by span. That made the postmortem fast and precise about what happened. It told us nothing about who answers for it.

Teams adopting agents miss this all the time. They hear "agents are hard to debug", invest in tracing (LangSmith, Langfuse, OpenTelemetry spans around tool calls, and so on), and feel the accountability problem is handled. You do need tracing. It's your audit trail. But an audit trail with nobody accountable behind it is a nicely formatted account of what went wrong. A trace answers "what did each stage do?" Accountability answers "who was supposed to make sure the whole thing was right, and who had the authority to stop it?" Answering the first question doesn't answer the second.

4. Name the pattern: the problem of many hands

Philosophers and political theorists already have a name for this: the problem of many hands. Dennis Thompson coined the phrase in 1980, writing about public officials. When many people each contribute a small part to an outcome, it gets hard, sometimes impossible, to say who is responsible for it, even when the outcome is clearly bad. Later work, notably by Ibo van de Poel and colleagues, carried the idea into engineering and technology.

It comes up constantly in my PhD work on multi-agent systems at RMIT, as soon as agents get composed. Responsibility doesn't add up across agents. It thins out. Three agents that are each "partly responsible" don't make one fully responsible system. You get three slivers of responsibility, and each one can be honestly disowned.

If that's not your field, the closest everyday version is the bystander effect. Darley and Latané studied it experimentally in the late 1960s and explained it partly through diffusion of responsibility. When someone collapses on a busy street, each onlooker is less likely to help than if they were alone, because each assumes someone else will step in. Agent pipelines build the same dynamic into their structure:

  • ▹Every added stage is one more place to assume "someone else will check".
  • ▹Skipping a check locally feels safer with each extra agent, since there's always another stage downstream.
  • ▹The last stage, usually tests or deploy, gets treated as the safety net. It can only check what it's handed, and it can't see what was never passed to it.
  • ▹That's why adding a "reviewer agent" can make things worse: upstream stages now have a named stage to defer to.

5. Why this is worse than the human version

Earlier in this series we looked at code review and specs as human practices, and human teams diffuse responsibility in exactly the same way. They have one thing agent pipelines lack, though: social friction. An engineer about to drop a column usually feels a small itch. Wait, is anything still reading this? So they grep the other repo, ping someone on Slack, or mention it at standup. People double-check partly out of fear of being the one who broke prod, and partly because they know a colleague will ask about it on Monday.

Agents have none of that. An agent doesn't worry about Monday. Once it has finished the literal task it says "done" without hesitating, and it won't escalate an uneasy feeling because it doesn't have one. Unless the pipeline makes escalation a required output, the agent's default is to finish and hand off. The friction that used to catch these gaps in human teams is gone, so you have to rebuild it deliberately, in structure rather than in feelings.

6. Fix, part 1: an owner of last resort

The first fix is organisational, not technical. Every agent pipeline gets one named human who owns its outcome end to end, the same way the on-call engineer owns an incident. Put the name in the pipeline config, next to the stages, so it's versioned and everyone can see it.

yaml
pipeline: schema-migration
owner_of_last_resort:
  name: "<engineer name>"        # a person, not a team alias
  rotation: platform-oncall        # who covers when they're out
  authority:
    - halt_pipeline
    - reject_any_stage_output
    - require_human_approval_for: [destructive_ddl, data_backfill]
stages:
  - planner
  - coder
  - test_agent
  - deploy

What the owner of last resort does, and what they don't:

  • ▹They do NOT review every diff. If they had to, the pipeline wouldn't be worth running.
  • ▹They DO own the outcome. When something ships broken, the postmortem starts with their name, just as an incident review starts with whoever held the pager.
  • ▹They DO hold the right to halt. Any stage or any person can escalate to them, and they can stop the pipeline without asking anyone's permission.
  • ▹Closing gaps in the contracts (part 2) is their job too.
  • ▹It has to be one named person. "The platform team" owns nothing. A team alias is just many hands under another name.

7. Fix, part 2: handoff contracts

The second fix is technical. Every handoff gets an explicit contract with two halves: what this stage guarantees to the next one, and what it refuses to assume about the next one. The point is to make "the next stage will check" an illegal move. If a property matters, a named stage has to guarantee it, and the contract says which one.

Here's a small contract for the migration case. Look at where the dependency check went. Nobody owned it before. Now a specific stage owns it, and that stage has to produce evidence that the next stage verifies instead of trusting.

yaml
handoff: planner -> coder
planner_guarantees:
  - every column/table marked for removal has a usage scan attached
  - usage scan covers: all repos in org, BI/reporting queries, scheduled jobs
  - zero live readers found in last 30 days of query logs, OR plan is marked BLOCKED
planner_refuses_to_assume:
  - that the coder will check downstream usage
  - that a 'deprecated' comment means unused
evidence_required:
  usage_scan_artifact: required
  query_log_window_days: 30

handoff: coder -> test_agent
coder_guarantees:
  - destructive DDL is split: deprecate (rename/stop writes) before drop
  - migration is reversible, or flagged irreversible for owner approval
coder_refuses_to_assume:
  - that tests cover services outside this repo

handoff: test_agent -> deploy
test_agent_guarantees:
  - tests ran against a schema snapshot taken from prod, not only fixtures
  - explicitly lists what was NOT tested (out-of-repo consumers)
test_agent_refuses_to_assume:
  - that a green suite means no consumer breaks
on_contract_violation: halt_and_page_owner

Three things make this hold up in practice:

  • ▹Guarantees come with evidence. The planner doesn't just say "I checked". It attaches the usage scan, and the coder stage confirms the artifact exists and is fresh before starting. An agent saying it checked is not the same as an agent proving it.
  • ▹Writing down "refuses to assume" is what exposes the gap. Once the test agent declares that it doesn't cover out-of-repo consumers, either someone upstream takes that on, or the owner accepts the risk on the record.
  • ▹A violation halts the pipeline. A warning in a log is as good as no contract, because every downstream stage will assume someone else read it.

When to use this, and when not to: full contracts pay off on pipelines that touch shared state (schemas, infrastructure, auth, money, customer data) or that deploy with no human in the loop. If the pipeline drafts a PR that a human then reviews properly, a light contract is enough. The common mistake is a contract so vague ("code is correct") that it guarantees nothing anyone can check.

8. What changed at PhoenixDX

Three changes stuck. First, every agent pipeline config has an owner field, and CI won't run a pipeline without one. Second, postmortems open with the owner's name. It isn't about blame. It means "who owned this?" always has an answer, so the discussion can move straight to why their contracts didn't catch the problem. Third, each handoff writes a small contract-check record into the trace. Now the trace shows what each stage did and also what it promised, and that's where observability finally pulls its weight.

The trade-off, honestly: the pipelines are slower. Usage scans and prod-snapshot tests add real wall-clock time, and some migrations that used to flow straight through now wait for owner approval. Owners sometimes resent the pager, too. Being the named person for a pipeline you didn't write, built from agents you don't fully understand, is uncomfortable. We handle that with rotation, and by never questioning an owner's halt. Make someone accountable without giving them the power to stop the line and you've created a scapegoat, not an owner.

9. Monday checklist

Ask these three questions of any agent pipeline you run today:

  • ▹Who is the owner of last resort? You want a single named human, written in the config, with authority to halt. If the answer is a team name or "whoever triggered it", you don't have one.
  • ▹What does each handoff guarantee? For every arrow, can you point to a written guarantee backed by evidence, plus a list of what that stage refuses to assume? Any arrow where the honest answer is "the next stage will check" is where your next incident will come from.
  • ▹Which stage is allowed to say no? Find the stage that can halt the pipeline on a contract violation instead of just logging a warning. If no stage can refuse, every stage will pass the work along.

10. Looking ahead

So far this course has mostly been about capability: better specs, better review, better agents, better pipelines. This lesson covers a layer that capability doesn't give you. You can make every agent in the chain smarter and still have nobody accountable for the result. Over the next few days we'll build on this and look at how to design ownership, escalation and authority into agentic systems from the start, instead of bolting them on after your first silent postmortem.

Flashcards
Check yourself

Extend your knowledge

  • ▹Read Dennis F. Thompson, 'Moral Responsibility of Public Officials: The Problem of Many Hands' (American Political Science Review, 1980). This is where the term comes from.
  • ▹Read Ibo van de Poel, Lambèr Royakkers and Sjoerd Zwart, 'Moral Responsibility and the Problem of Many Hands' (Routledge, 2015), which applies the idea to engineering and technology.
  • ▹Look up Darley and Latané's bystander intervention studies (1968) for the psychology behind diffusion of responsibility.
  • ▹This week, pick one agent pipeline you run and write the handoff contract for its riskiest arrow, including an explicit 'refuses to assume' list. Then add an owner field to its config.
Test yourself on this lesson →

Discussion

Chat with Chi Cong (AI) about this article. Your conversation is private to you — you can publish a summary for others when you're done.

Ask me anything about “The many hands problem” — trade-offs, decisions, or the story behind it.