Skip to content
MCP Five

The CrewProject #431 / 51

Five agents, one human

The pause travels up, carrying origin, partial_results, and the frozen worker's whole mind.

This is the part that stops it being a toy, and it is where the real engineering in project #4 lives.

Part 1 Β· The assumption you didn't know you'd made

Project #3's approval model has an assumption buried in it so obviously true that it is invisible:

The agent that hit the gate is the agent you are talking to.

The loop stops, the request ends, your browser shows a card, you click, a new request restarts that same loop. One agent, one card, one resume. Every part of that design is correct and none of it survives delegation.

Because now the agent that hits the gate is:

  • two levels down in a tree
  • running inside a Promise.all
  • alongside two siblings that are still working
  • with nobody talking to it and no browser tab of its own

And the naive fix is worse than not delegating

If each worker asks separately, you get three cards at three different moments, out of order, each with no context. "Approve empty_jar {id:38}?" β€” from which worker? Based on what evidence? What about the other two?

Part 2 Β· So the pause travels up

The approval bubbling up
Worker 2 stops. The whole tree freezes β€” worker 1's finished report to partial_results, worker 2's entire mind to paused_children, the orchestrator's own state to messages. One card. The HTTP request ends. Minutes later a brand-new request rebuilds worker 2 and tells it yes β€” and worker 1 is never re-run.

Part 3 Β· Three details, and each one is a bug if you skip it

The detailWhat breaks without it
origin on every bubbled callThe card says "approve empty_jar {id:38}?" and you have no idea which of three workers wants it or what it found. You would be rubber-stamping decisions you cannot see β€” worse than not asking, because it looks like oversight.
partial_resultsClicking Approve re-runs the two workers that had already finished. You pay for them twice, they may answer differently the second time, and any side effect happens again.
paused_childrenThe stopped worker's entire conversation vanishes when the HTTP request ends, and there is nothing left to say yes to.

Take the second one seriously for a moment. Without partial_results, approval is not idempotent in the world β€” it re-executes work that already happened. For jar inspections that means paying twice. For anything with a side effect it means doing it twice.

Part 4 Β· The screen

That single approval standing in front of three agents' worth of work is the picture the whole project is aiming at:

terminal
══════════════════════════════════════════════════════════════════════════
⏸  ONE APPROVAL, 9 DESTRUCTIVE CALL(S)
   9 of them came from 3 sub-agent(s) that are frozen mid-task.
   Frozen sub-agents: "jars 1-20", "jars 21-40", "jars 41-60"
══════════════════════════════════════════════════════════════════════════

⏸  pantry__empty_jar  {"id":46,"reason":"Seal broken (lid gasket …"}
    asked by : sub-agent "jars 41-60"
    because  : Seal broken (lid gasket displaced) - not merely scuffed,
               no AUTHORIZED note present.
    host rule: empty_jar permanently destroys every cookie in that jar.

Nine calls. Three frozen agents. One decision.

Note the three lines under each call, because between them they are the whole argument for origin:

LineComes fromAnswers
asked byoriginwhich worker wants this
becausethe worker's own reasoning, carried upwhy it thinks so
host rulethe gate's reasonwhy I am being asked at all

Together they are enough to decide without going and reading three traces β€” which is the actual test of whether an approval UI works.

Part 5 Β· And it survives a partial no

Eight were approved and the ninth denied.

The worker that got the "no" read it as an ordinary tool result, adapted, and reported honestly β€” exactly as the deny branch intends. And the orchestrator surfaced it to the human rather than quietly dropping it:

One jar flagged but NOT emptied: Jar 59 β€” it does meet the tampering criteria, but the sub-agent's attempt to empty it was declined. This one still needs your attention.

Part 6 Β· Rewinding a tree came free

Nested replay was the stretch goal. It cost one extra select and one expandable row.

Not because it was over-estimated, but because of a decision in lib/run-driver.ts: a sub-agent gets a real row in runs, with a parent_run_id, and its own stream of trace_events under its own sequence numbers.

One nullable self-reference turns a flat list of runs into a tree. Everything that already worked on a run β€” status, stop reason, iteration count, token bill, replay β€” worked on sub-agents on day one.

A sub-agent is not a special kind of thing needing special handling. It is a run with a parent. So replaying one is GET /api/runs?id=<childRunId> β€” the same handler, recursing exactly the way the loop does.

When the new feature turns out to be an instance of the old one, you get the tooling for free.

What you now know

  • Project #3's gate assumed the agent that stopped is the one you are talking to, and delegation destroys that.
  • Three separate cards would hand the details back to you β€” worse than not delegating.
  • The pause bubbles up and freezes the whole tree: origin, partial_results, paused_children.
  • Without partial_results, approving re-executes finished work.
  • The card needs who asked, why they think so, and why you are being asked β€” enough to decide without reading traces.
  • Verify against the database, not the model's summary.
  • A sub-agent is a run with a parent, which is why nested replay was nearly free.