What Does an Agent Orchestrator Do? Inside DevPilot's Graph
5 min read
An agent orchestrator decides what happens next: which unit of work runs, when to retry it, when to pause for a human, and when to stop. It does not do the work itself. In DevPilot the orchestrator is a graph; loading tasks, checking capacity, and dispatching remain the job of code the graph calls but never replaces.
The core split: orchestration vs. effects
An orchestrator is a control-flow layer. It decides sequencing and stopping points. It is not an execution layer — it does not load data, call an API, write a row, or spawn a process. Those are effects, and effects have their own owners. See the glossary definition of an orchestrator for the boundary against schedulers and dispatch queues, which is a different shape of problem.
DevPilot's codebase keeps this split explicit. This is the actual concern/owner map from the system, not a simplified version of it:
| Concern | Owner |
|---|---|
| Which wave runs next, when to refine, when to retry, when to stop | The graph |
| Human review as a first-class pause | The graph (interrupt()) |
| Loading a wave's tasks, checking fleet capacity, dispatching | WaveExecutionController / WaveDispatchCoordinator, unchanged |
| Session rows, orchestrator calls, rollback | dispatch-coordinator.ts, unchanged |
| Scoring, critical path, wave assignment | PlanRefinementService / plan-scorer, unchanged |
The left column changed when DevPilot introduced its graph. The right column did not. That is the point of separating orchestration from effects: you can replace how decisions get made without touching what those decisions act on.
What the graph actually decides
The graph decides four things, no more: which wave runs next, when to refine a plan rather than execute it, when to retry a failed step, and when to stop altogether.
"When to stop" sounds trivial until a run has stalled halfway through a wave plan and nobody knows whether to push forward or roll back. The graph makes that decision explicit rather than implicit in whatever script happened to be running at the time.
Human review is not bolted on afterwards. It is a first-class pause implemented with interrupt() — the graph halts, waits for a person, and resumes from the same point. There is no separate polling loop or side channel for approvals; it is a state the graph can be in, not an exception to it.
What stays untouched: the effects layer
Nothing about how work actually gets done changed.
WaveExecutionController and WaveDispatchCoordinator still load a wave's tasks, still check fleet capacity, and still dispatch. The capacity logic in particular is unchanged and documented on its own terms — see how many agents you can run at once.
dispatch-coordinator.ts still owns session rows, orchestrator calls, and rollback. When a shared session needs to roll back because a dispatched task failed, that rollback path is the same code it was before the graph existed.
PlanRefinementService and plan-scorer still own scoring, critical path calculation, and wave assignment — covered in how DevPilot scores a plan before running it and how critical path shapes agent plans. The graph asks these components for answers. It does not recompute them itself.
Why only the control flow got rewritten
The dispatch path was already proven before the graph existed. Two real Claude Code sessions ran through it end to end, and that run is documented in docs/SESSION-RUNNER.md. Not a mock, not a simulated dispatcher — two actual Claude Code sessions, dispatched and observed to completion.
Claude Code is, at the time of writing, the only agent DevPilot dispatches to. Rewriting the effects layer underneath a new orchestration framework would have meant re-verifying that path from nothing, for no behavioural gain. The graph needed better control flow, not a different way of running a session.
So WaveExecutionController survives, but its role split. It remains an effects library: the methods that load tasks, check capacity, and dispatch are still called directly, unchanged. Its orchestration methods — the ones that used to decide what happened next — are superseded by the graph and are no longer the code path in use.
The seam: ConductorPorts
Two systems that don't touch each other need a seam. src/lib/conductor.ts implements ConductorPorts, a set of interfaces the graph calls against real DevPilot code — not mocks, not a simulated dispatcher standing in for the real one.
This is where a graph decision turns into an effect. The graph decides a wave should run; ConductorPorts is the interface through which that decision reaches WaveDispatchCoordinator. The graph never calls the dispatcher directly, and the dispatcher has no idea a graph exists above it.
The design reasoning for this boundary is written down in docs/CONDUCTOR-AGENT.md, lines 19 to 47, if you want the seam's own justification rather than this summary of it.
So, what does an agent orchestrator do? A working definition
An orchestrator decides sequencing: which piece of work runs next, whether to retry it on failure, where to stop, and when to hand control to a person for review.
It does not load tasks, check capacity, dispatch a job, or touch session rows itself. Those are effects, owned by other code, reached through a seam — not folded into the orchestrator's own logic.
Is an agent an orchestrator? No. An agent executes a task, generally inside a session. An orchestrator decides which unit of work runs, in what order, and whether the result was good enough to move on — the distinction behind why DevPilot dispatches in waves rather than a queue.
Use one when the cost of a wrong sequencing decision — retrying the wrong thing, stopping too early, skipping a human check — outweighs the cost of the extra layer. Below that threshold, a script that dispatches tasks in order is simpler and probably correct. DevPilot's graph exists because the sequencing decisions in a multi-wave plan turned out to be more numerous, and more consequential, than that threshold suggests.