rig3.5.3

subagents

Work off the turn, not off the machine.

rig has three ways to put work beside your turn: a delegate for a bounded subtask, the scheduler for work that owns a clock, and the swarm for draining a board. They are not three runtimes. They are three doors onto one worker path, one store, and one priority queue.

That is the whole design argument: a worker is just another rig request. Where the model runs — a local child with two places, or a hosted endpoint that queues for you — changes nothing above it.

the three

Delegation, schedule, swarm

axisdelegateschedulerswarm
the jobone bounded subtask, nowa job that fires on a clockdrain a board, continuously
who starts ita tool call in the turnthe crontab (or a one-shot at)swarm start N
the shapea headless rig -p run, or one deterministic bash line as a command workera one-shot rig -p in the job's own cwd, fired by rig run-job <key>N workers claiming tasks, plus reviewers taking verdicts
the boundtimeoutMs is the kill bound for a command worker; a model worker has no clockthe job's timeout and budget, per firethe swarm's own context — no stall kill; budget= caps the spend
how it returnsa WorkerDone message at priority 70: in hand at the turn's boundarya recorded run in the scheduler store; the dashboard has the auditthe board: the worker completes, a reviewer accepts or rejects
the recorda run in the scheduler store, a resumable transcript, a logthe event log is the spine; the rows a projectionthe todo board is the state; notes carry findings
the jailthe same runner as a scheduled firebubblewrap, landlock, or off by namethe same jail, the same allow-list narrowing

A delegated batch can take two rows of its own in the TUI — the count and elapsed over the most recent call of any of its workers. The swarm band carries the live counts per role. Both are display, not state:

delegate #1 returned · exit 0 · 41s · session 7d41d9c
214 tests pass; two flaky under -race, noted on t2

/swarm · start 2
swarm: added 2 agents (role worker · model local)
w1 claimed t1 · w2 waits for t1
workers 2 · +3 ✓5 ✕1 · w2 t388 12s
reviewer 1 · ⧗1 ✓1 ✕0

what they share

One worker path, three callers

Every worker — delegated, scheduled, or swarmed — is spawned through the same path: a one-shot rig -p, the row's model, the operator's interpreter, the sandbox profile, the socket proxy, the recorded run. The differences are who owns the trigger and who reads the result.

the prompt

rig -p - takes the prompt from stdin, capped at the read tool's 1 MiB ceiling — which is why a worker's prompt is never an argv.

the menu

A worker gets the doing set — bash, read, write, edit, view, python, web, rem. The session's verbs (todo prune/accept/reject/move, scheduler remove, plugin delete) leave its menu and refuse at its gate. Only a reviewer holds verdict.

the return

core.WorkerDone carries the message, exit, duration, session and log. The frontend drains it at the prompt as the head of the next user message: returns first, then what you typed. A drain with no live turn starts one.

the ownership

A worker belongs to the session, not the turn that spawned it. The interrupt gesture with no turn live — esc on an empty prompt, the dashboard's stop — stops every running worker.

the caps

maxWorkers caps all workers at once across delegate and swarm; a delegate past the cap hands back queued and starts when one returns. workerWindow caps every worker's window at N tokens, scaling reserve and maxTokens by the same ratio; the session keeps its full window.

backend-agnostic

The model is a row, not a scheduler detail

Nothing in the worker path names a backend. A worker is a request, and the row says where the request goes:

hosted row

remote: true or a named provider. A worker's request is just a second request: the endpoint queues it, rig counts the cost, and a 429 or 5xx retries with bounded backoff. The room's residency read is skipped entirely.

local row

The row's baseUrl is the endpoint and its child half knows how to start it. A worker claims the model's key in the room like every other client; what is loaded is read live from the port, and a model that is not resident refuses by name, naming the holder.

The scheduler never asks which one it is. It fires rig -p; the row answers. The swarm never asks either: each worker is a one-shot run, and the board is the shared state. Swapping a local row for a hosted one changes one file, not one worker.

one slot, many workers

Places, not slots, and one counted key

Here is the part most fleets get wrong: a model is one counted key, and the row says how many places it serves at once — two with two sequences, three for an -np 3 child. The holder set is the guarded state. There is deliberately no key per slot: the child serves N interchangeable sequences, and per-place keys would invent identity the server does not have.

the claim

An instance claims the key, and when the claim is final it sends its own request to the child's port. Its completion is the release — the stream's terminal event mints the next version on the chain.

the entry

A strictly higher-priority claim may enter while the places are full: one holder beyond the places, never two, and only strictly above every holder. Entry, never eviction — nothing kills a request mid-flight.

the continuation

Within one priority, the claim naming the conversation already in the model goes first. A dropped reply costs a full prompt re-read, and a slot's conversation state is the one thing the engine keeps.

the cap

maxWorkers is the delegate's hand-off cap on rig's own workers. It is not the model's places: one is how much work you start, the other is what the server can hold.

So "multiple workers on one slot" is not a special mode. It is N claims on one key, ordered by priority and arrival, released by their own completions, with the continuation rule keeping each conversation's cache warm. On a hosted row the same N requests simply queue at the endpoint instead. Either way the worker code is identical, because the worker never sees the slot.

the priority queue

The scheduler is the queue

One consumer runs the session; work arrives as closures ordered by priority first, arrival second. The fleet costs no extra machinery because the queue orders by priority, not by source: your input, a worker's return, a tool completion and a reviewer's verdict are all just producers.

 90  input      your line, at the boundary
 70  delegate   a worker's return — ahead of the turn in flight
 60  write      an effect, inline, in call order
 50  read       overlap, eight at a time
 50  stream · tool
 30  fleet      swarm notices and claims, in the gaps of a turn
 10  review     the reviewer's bite, only when nothing else is queued
the return

A delegate's return posts at 70, above the turn's own events. The work you asked for is in hand at the boundary of the turn in flight, however many turns queue behind it.

the chatter

The fleet's other deliveries post at 30, below the turn. A busy session delays them and loses nothing: the stores hold the facts. Starvation is the documented property here, not a bug to chase.

the bite

The reviewer's batch posts at 10 and starts only when nothing else is queued. Labelling gets the idle time; nothing waits on the labels.

the wakeup

No timer. A step that waits on the world spawns and posts its completion back: the event is the wakeup. That is why a fleet of workers adds producers, not threads you have to babysit.

the board

The swarm is a board with workers

swarm start <count> [role=worker|reviewer] [model=<id>] [budget=<dollars>] puts drain workers and reviewers on the same loop, jail and return path as you. The count rides start and is capped at 16; a budget stops the controller's claims at the cap with a notice, and the spend is summed from the recorded run costs.

Each worker claims the next unblocked task, spawns a one-shot run through the delegate path, and submits for review. A reviewer takes the worker's verdict and calls accept or reject. The board's two links are the whole gate:

requires

I wait for that task. claim takes the next task nothing waits for; a blocked claim refuses, naming the blocker.

blocks

That task waits for me. The DAG is validated at create, so a cycle never lands.

the gate

Who completes decides: a solo session lands done; a worker submits for review; the reviewer's verdict settles it. A dead claim is released through the store's reap door.

the notes

A task carries notes in order with their session and time — the worker's findings go there and in rem, not into tasks it creates.

The dashboard shows the same board, and its stop button is the same interrupt gesture: it stops every running worker and lets their returns name the interrupt as their exit.

when to use which

Pick by who owns the clock

delegate

Your turn needs the result, but not before it ends. A test sweep, a broad read, one bounded sub-question. One line costs the turn; the return heads the next one.

scheduler

The clock owns it. A health check every two hours, a nightly training run, a job that must outlive the session and be auditable afterwards.

swarm

The board owns it. Many independent tasks of the same shape, with a review gate and a budget — a backlog you want drained while you review the results.

none

Everything else. A worker is not free: it is another request against the same model. The smallest correct thing is often the turn you are already in.