← Blog

A control plane for swarms of agents

Product·The alineo team
A terminal panel showing an alineod event stream fan out to three reviewer agents and gather into one editor.

The SDK makes one agent easy: load it, prompt it, fork a child from its sandbox. A swarm — a lead forking three workers, one of them paused mid-turn, an editor waiting on all three before it starts — is a different problem. Something has to track the tree, hold the gather step until its dependencies settle, and put every agent back together if the process running it dies halfway through. Building that yourself means writing a control plane before you've written the task.

alineod is that control plane, run once, driven over HTTP.

What it does

A gather is a spawn with a waitFor list — alineod holds it until every listed agent settles, then forks it from its parent and writes each dependency's result into its sandbox:

POST /runs/r_3f9a1c20/agents
content-type: application/json

{
  "parentAgentId": "a_7b2e44d1",
  "waitFor": ["a_c01d9e3a", "a_5e8f2b71", "a_91aa0c4d"],
  "spec": { "name": "editor", "cli": "pi" },
  "prompt": "Read /inputs.json and merge the three reports into one."
}

The request returns 202 immediately. alineod moves the child to spawning, waits for the three dependencies to reach a settled outcome, forks the editor from its parent's sandbox, and drops each dependency's result at /inputs/<agentId>.txt plus an /inputs.json manifest before the prompt ever runs.

The routes

That gather is one of thirteen routes. There's no SDK client for alineod on purpose — the interface is the wire protocol, generated from the same Zod schemas as specs/alineod/openapi.json and the event schema, so this table can't drift from what the daemon actually accepts:

MethodPathWhat it does
GET/healthLiveness check.
POST/runsCreate a run and its root agent.
GET/runs/:runIdThe run's spawn tree.
DELETE/runs/:runIdStop every live agent in the run.
GET/runs/:runId/eventsThe run's event stream (SSE).
POST/runs/:runId/agentsSpawn a child agent.
GET/agents/:agentIdOne agent's state and session stats.
POST/agents/:agentId/promptStart a new turn.
POST/agents/:agentId/steerRedirect the current turn.
POST/agents/:agentId/pauseFreeze the agent's container.
POST/agents/:agentId/resumeThaw a paused container.
POST/agents/:agentId/stopAbort and close the agent.
GET/agents/:agentId/resultGet, or long-poll for, the agent's result.

Every verb here is scoped to exactly one agent — there's no "steer every worker" or "pause this whole subtree" yet. More on that below.

The demo

The swarm-code-review cookbook runs this for real: one lead clones a repository, forks three reviewers — security, correctness, tests — pauses and resumes one mid-review, steers another onto a narrower brief, then gathers all three into one report through an editor.

An alineod run: a lead agent clones expressjs/cors, forks three reviewers, one is paused and resumed, one is steered mid-turn, then an editor gathers all three into a merged report.

How it works

Every agent is a live OpenSandbox container, not a child process of the daemon — so alineod itself can crash and restart without losing the swarm. On boot it replays its append-only ledger, then reconnects to every still-live agent with Alineo.reattach(), which rebinds to the harness bridge already running in the container instead of restarting it. An in-flight turn survives the daemon going away.

If reattach() doesn't get an answer — the bridge itself is gone, not just the daemon that was watching it — alineod falls back to Alineo.resume(), which restarts the bridge and does drop the in-flight turn. Only if that fails too does a live agent end as lost; a finished agent just keeps the outcome it already recorded.

How it fits

Every agent alineod runs — lead or child — is spawned from the exact same AgentSpec the SDK uses for a single agent, written to a spec file and handed to the SDK's own spawn(). The format doesn't change shape to become swarm-friendly. That means anything you've already set up per-agent keeps working without a second configuration: an AgentSpec.permissions gate pausing a risky tool call for a human still gates that call inside a forked worker three levels deep; a credential bound with approval: "hold" still holds the first request to that host until someone approves it, no matter where in the tree the agent sits. You don't secure "an agent" once and "a swarm" separately — it's the same spec either way.

The daemon installs the same way, too: alineo init starts it in Docker right alongside OpenSandbox, using whichever of your provider API keys are already in the shell. No new account, no new config format — just a new way to run agents you already know how to spec.

What it doesn't do

Results are inline text, not file references — a settled handle stores the agent's final message, addressed as fs://<agentId>/result.md, not an arbitrary path inside the sandbox. alineod is also one process: a control plane for one swarm on one host, not a distributed scheduler across machines. And it's software you run, not a hosted service — alineo init starts it in Docker alongside OpenSandbox, but you own the container.

Where this is going

Today, every control action reaches one agent at a time — steering, pausing, or stopping a five-agent review means up to five requests. That's fine at five; it won't be at twenty. The next round of work makes the swarm itself the unit of control: redirect or pause a whole branch of workers in one call, and undo a bad fan-out with a rollback instead of only being able to abort it.

Gather grows past "wait for everyone" the same way. waitFor today blocks until every listed dependency settles, which is the wrong shape the moment one worker is a straggler you don't want holding up the rest. The plan is to let a gather proceed once enough of its dependencies are in, or react the moment any one of them finishes, instead of always waiting on all of them.

And as swarms stop being single-player — a person driving one from a terminal, an agent forking its own children, another service polling the same run — alineod needs a real answer to "who's allowed to do what to a tree they didn't start." That's the least-finished part of the daemon today, and where the design work is headed next.

None of this changes the shape you already know: it's the same routes, the same AgentSpec, just more of the swarm addressable at once.

Try it