Keyboard shortcuts

Press or to navigate between chapters

Press S or / to search in the book

Press ? to show this help

Press Esc to hide this help

Subagents

A subagent is a second agent loop, launched by the agent itself, running against the same container and the same tools. It exists so a big task can be split up without every piece having to fit in one context under one preamble: the parent hands off a scoped job, the subagent works through it with a fresh context, and only its report comes back.

The agent reaches subagents through the outrig__ tools – OutRig’s own built-ins, which appear alongside MCP tools like fs__read_file and shell__exec and are called the same way. The outrig server name is reserved in config so nothing can shadow them.

Subagents are enabled by default. Set subagents = false on an [agents.<name>] block to leave the tools out entirely; see Reference -> Config.

What a subagent is

A subagent is a headless REPL. outrig run drives one agent loop from lines you type; a subagent is the same loop driven by prompts from the parent instead, keeping its own conversation history across them. That is the whole idea – the rest is bookkeeping.

What it inherits, and what it does not:

InheritedNot inherited
The container, and everything in itYour conversation and context
The MCP tools, over the same connectionsThe session preamble – the parent sets it
The tool limits and sampling settingsThe outrig__ launch tools past the limit
The model and provider, unless overriddenmax-tokens, when a model is named
/workspace, at the same paths

Because it borrows the parent’s MCP connections, launching one starts no container and connects no server. That keeps the MCP trust model invariant intact: a subagent can only reach tools the operator already granted the session.

The tools

outrig__subagent({"name": "audit-config", "prompt": "..."})   -> returns immediately
outrig__wait_results({"names": [...], "min_count": 1})        -> which ones have something
outrig__get_result({"name": "audit-config"})                  -> that one's findings
outrig__subagent_send({"name": "audit-config", "prompt": "..."})
outrig__subagent_release({"names": ["audit-config"]})

Inside a subagent there is exactly one:

outrig__set_result({"status": "result", "body": "..."})    // findings
outrig__set_result({"status": "error",  "body": "..."})    // could not finish

name is a short kebab-case handle the parent picks, and is how it refers to that subagent everywhere afterward.

Choosing the subagent’s model

outrig__subagent takes an optional model. Omit it and the subagent runs under the model the launching agent is running under, which is what every call above does.

outrig__subagent({"name": "grep-callers", "prompt": "...", "model": "fast"})

The value is a model name – a key under [models.<name>], like fast or smart – not a provider and not a wire identifier like gpt-4o-mini. The names configured for the running build are listed in the argument’s schema, so an agent never has to guess one.

This is for delegating mechanical work. Grepping a tree, summarizing logs, or checking whether a symbol is still used does not need the parent’s expensive reasoning model, and moving it to a cheap one keeps the parent’s context free for synthesis.

Sampling and the tool limits still come from the launching agent – temperature, tool-call-max, and tool-result-max are unaffected by the model named. max-tokens is not: an output-token ceiling belongs to the model, and a cheap model generally serves fewer output tokens than an expensive one, so a subagent takes the ceiling that applies to the model it actually runs on. That is [agents.<name>].max-tokens where the launching agent sets one, otherwise the named model’s [models.<name>].max-tokens, capped at the model’s published ceiling where outrig knows one – the same order config applies to any other turn.

A subagent’s model is fixed for its lifetime: outrig__subagent_send cannot re-point a live one, since its history was accumulated under the model it started on. Launch a second subagent instead.

Launching is not waiting

outrig__subagent returns as soon as the subagent starts. Two calls in a row give two subagents running at once – that is how fan-out works, and it does not depend on the model emitting parallel tool calls. The parent’s own tool calls stay sequential and ordered.

Fan-out has a ceiling. subagent-width-max (default 8) bounds how many live subagents one agent may hold, and a launch past it is refused rather than queued. Every subagent counts against the budget until it is released – including one that has finished and whose result the parent already collected – so at the limit the remedy is outrig__subagent_release, not a retry. The budget is per launching agent: a parent that is full does not stop its own subagents from launching theirs.

Reporting is explicit

A subagent reports by calling outrig__set_result, not by finishing with a nicely worded message. This matters more than it looks: a model’s last message is whatever it happened to close with, and “Done, let me know if you need anything else” is a poor thing to hand another agent. Publishing explicitly also makes failure honest – a subagent that runs out of tool calls never publishes, so the parent is told it stopped rather than handed a status string dressed up as an answer.

A subagent may call it more than once. The inbox keeps only the latest value, so a later call simply supersedes an earlier one, and calling it does not end the subagent’s round.

Both fields are required, which is deliberate. An earlier shape took {result} or {error} as two optional strings, and models called it as {} constantly – a schema where every field is optional permits the empty call, so the only rejection possible came from the runtime, after the subagent had already spent a tool call. required is the one constraint providers enforce and models reliably attend to, and an exclusive choice between two optional fields cannot use it. Folding the choice into a status enum makes both fields mandatory and the empty call unrepresentable.

When a report does not fit

body carries the whole report, so it is the one argument in the toolset large enough to run into the model’s output-token ceiling. A reply cut off part-way arrives as a set_result call with status present and body missing, because fields generate in schema order.

OutRig recognizes that shape rather than passing a bare “missing field” back: the subagent is told its report was probably truncated and asked to shorten it, which is something it can act on.

That ask is bounded, because the ceiling does not move between attempts. A subagent that could have fit the report in body does it when first asked; past that it regenerates the same oversized body and fails the same way, so retrying only spends model calls:

AttemptWhat the subagent is told
1stThe report was probably cut off – call again with a shorter body.
2ndStop chasing the report; send status: "error" with a one-sentence body.
3rdThe failure has been reported upward already – stop calling.
4th onThe same refusal, unchanged.

The third attempt is what makes this end. OutRig publishes the truncation as that round’s outcome on the subagent’s behalf, so a parent blocked in outrig__get_result wakes with the cause immediately instead of once the subagent’s whole tool-call budget has drained into one call that could not succeed.

Having given up, the round stays given up: it publishes and warns once, and every later truncated call gets the same refusal. That refusal is a tool failure, not a success, so identical repeats keep accumulating against the breaker below – which is what ends the round if the subagent will not take the hint.

The durable fix is a bigger ceiling. max-tokens is unset by default, which leaves the limit to the provider, and that default can be much lower than expected behind a gateway. The first truncated attempt prints one line to stderr naming the agent and the ceiling that was in effect for that subagent, because that is the part a human – not the model – has to act on. Set [agents.<name>].max-tokens explicitly if subagents produce long reports, or, for one launched under a named model, [models.<name>].max-tokens on the model it runs.

Repeating a failing call does not pay

The same shape shows up beyond set_result: a model that cannot act on a tool error tends to re-emit the identical call rather than try something else. Within a subagent round, OutRig counts consecutive failures of the same tool called with identical arguments. The second such failure gets a note appended to the tool result saying that repeating will not change the outcome; the fourth ends the round. Changing the arguments, or any call that succeeds, resets the count – a subagent taking the hint is making progress, not looping.

A round ended this way publishes nothing, so its parent is told the reason it stopped rather than the bare “stopped without calling outrig__set_result”. The same is true of a round that runs out of tool calls.

This applies to subagents only. Nothing about it is specific to reporting – any tool can be looped on – but the primary agent has someone sitting in front of it who can interrupt, while a subagent loops unattended inside its parent’s tool call.

Reading is edge-triggered

Each subagent’s inbox carries a version, and the parent keeps a read position against it. outrig__get_result blocks until there is something newer than what the parent last saw, returns it, and moves the read position past it. Reading twice with nothing new in between blocks rather than returning the same answer again.

outrig__wait_results blocks on the same condition across several subagents and reports names only. Results can be large, so a call that returned three of them at once is exactly the oversized tool result worth avoiding; the parent pulls each one with outrig__get_result and can stop once it has enough. Use min_count to react to whichever finishes first:

outrig__subagent({"name": "audit-config", "prompt": "..."})
outrig__subagent({"name": "audit-mcp",    "prompt": "..."})
outrig__subagent({"name": "audit-net",    "prompt": "..."})

outrig__wait_results({"names": ["audit-config", "audit-mcp", "audit-net"], "min_count": 1})
  -> ["audit-mcp"]

outrig__get_result({"name": "audit-mcp"})
  -> what it found

A subagent that is ready and left uncollected stays ready. Keep passing its name to outrig__wait_results and every call returns it immediately and never blocks for the others – drop names once they have been collected.

Subagents stay addressable

Finishing a round does not end a subagent. It goes idle with its history intact, and outrig__subagent_send reopens it – to follow up on a result, or to redirect one that is still working. A running subagent sees the message at its next step, so the parent never has to know whether it is busy. Idle subagents live until released or until the session ends.

outrig__subagent_release takes the whole list or none of it. If any name in the call is unknown – or named twice – nothing is released and every subagent in that call stays live, with its history and its unread results intact. Releasing is unrecoverable, so a bad list is better retried than half-applied.

Subagents can launch subagents, up to a depth limit

A subagent’s toolset is the session’s MCP tools plus outrig__set_result, and – while there is depth left – the same launch tools the primary has. The primary agent is the root at depth 1; a subagent it launches is at depth 2, one of theirs is at depth 3, and so on. An agent gets the launch tools only while its depth is under subagent-depth-max (default 3), so nesting stops on its own rather than running away.

Each launching agent has its own private view: it sees only the subagents it launched, names them in its own namespace, and collects their results independently. A mid-tree subagent both reports upward with outrig__set_result and collects its own children with outrig__get_result.

Set subagent-depth-max = 1 to switch subagents off entirely, or 2 to allow only the single layer the primary launches – see Reference -> Config. Releasing a subagent, or ending the session, tears down everything it launched with it.

What you see

Nothing reaches stdout except the primary agent’s reply, so outrig run > out.txt still captures only the model’s text. Subagent activity shows up two other ways:

  • On stderr, with each trace labeled by name. Concurrent subagents interleave; filter by name.
  • In <session_dir>/logs/subagent-<name>.log, beside the MCP servers’ stderr logs, holding that subagent’s prompts, replies, and published outcomes.

Ctrl-C behaves as it always has: it abandons whatever the parent was waiting on and returns you to the prompt. Subagents keep running and are still collectable on the next turn – the same way an abandoned shell__exec keeps running to completion inside the container. A second Ctrl-C ends the session, and everything shuts down with it.

The shared workspace

Every subagent writes to the same bind-mounted /workspace as the parent and its siblings. Two subagents told to edit the same file will fight over it, and OutRig does not stop them – keeping concurrent work disjoint is the parent’s job. Read-only analysis fans out safely; parallel edits want non-overlapping scopes.

See also