Vectrum
Sign In
170 lines · 9.8 KB
# Subagents

The agent can start other agents. `spawn_agent(preset, task)` returns
immediately with a short id — `a1`, `a2`, … — and that agent runs in the
background on its own goroutine while the main agent carries on with whatever
it was doing. `agent_wait(id)` blocks until that one finishes, for the times
the parent genuinely cannot proceed without the answer.

A subagent is a whole agent, not a subroutine: its own conversation, its own
context window, its own model, its own turn loop. The parent never sees its
turns — only the report it ends with. That is the point. Work that would have
filled the parent's window with file listings fills the child's instead, and
comes back a paragraph long.

Three presets, compiled in rather than configurable, because a preset the model
can invent is a blast radius the model can invent:

| Preset | For | Turn budget |
| --- | --- | --- |
| `explore` | finding things, and reporting paths and line numbers | 60 |
| `plan` | working out how a change should be made, without making it | 80 |
| `general` | a self-contained piece of work, edits and tests included | 200 |

The session itself runs uncapped; these are the one place a budget stays, and
they are the reason why. A subagent works alone, several run at once, and the
model that spawned it is not checking on it — so each one stops eventually, at
a number far past what the job takes. Hitting it means the agent is stuck, not
that it was busy, and the pane is the faster way out: `ctrl+b`, then `k`. A run
that did ask for `-max-turns` keeps it as a ceiling over these, because a child
free to sail past the cap would be a hole in exactly the guarantee the flag is
bought for.

All three get the full tool set — read, glob, grep, edit, write, bash, skill —
and none of them gets `spawn_agent`, so subagents cannot spawn subagents.

**A preset pins no model and no effort level.** Both are arguments to
`spawn_agent`, because what to spend is a judgement about the job and the agent
holding the task is the one making it: `model` picks what runs it — a small
fast model for mechanical search, a strong one for work that has to be right —
and `effort` picks how hard it thinks, `low` through `max`. Omit either and it
inherits the session's. A pinned id here would be a guess about someone else's
endpoint as well as about the job: point Codey at a gateway that serves
neither, and every spawn of that preset fails before its first turn.

The models it may choose between are in its own system prompt: Codey asks
the endpoint what it serves at startup — `GET /v1/models`, the same route the
picker uses — and lists them newest first, with the rule that the newest is the
one to reach for unless you asked for another or it has already failed this
session. An endpoint that does not answer that route simply gets no listing,
and the agent is told that naming no model runs the subagent on whatever the
session is using. The list is read once, because it sits in the cached prefix
and a prompt that changes mid-session throws the cache away. Subagents do not
get this paragraph — they have no such choice to make.

`output_config.effort` is a per-model capability, not a universal parameter.
Haiku 4.5 rejects a request carrying it, and `xhigh` did not exist before Opus
4.7 — so a request for a level its model does not take goes out without it,
rather than failing with a 400 that reads like a bug in Codey. Where that
happens to a subagent the answer says so outright (`claude-haiku-4-5 takes no
effort level, so "low" was not applied`) instead of letting the parent believe
it got what it asked for, and the effort pill in the status line reads `not on
Haiku` rather than claiming a setting that is not in force.

A model this build has never heard of — a gateway serving something else, a
family released after it — is treated the same way: the level is left off. The
two mistakes are not symmetric. Sending effort to a model that lacks it is a
400 that ends the run before any work happens; omitting it costs the level and
says so in the pill. Gateway decoration is seen past first, so
`anthropic.claude-opus-5`, `openrouter/anthropic/claude-opus-5` and
`claude-opus-5@20260401` are all recognised as the model they name — for
pricing and for the thinking parameter too.

Every spawn goes through the ordinary approval box, which shows the preset, the
model and the task. It is not rememberable per task — the task text differs
every time, so "always this exact one" would never match twice — but "always
allow this tool" still applies, for anyone who has decided to trust spawning as
such. A subagent's own tool calls go through the same policy, and the box says
which agent is asking.

Four run at once; further spawns queue. Two agents editing one file is not a
special case: `edit_file` already refuses a file whose mtime moved since that
agent read it, so the second one is told to read it again rather than
clobbering the first.

A report reaches the parent exactly once, whichever way it gets there. Both
paths — `agent_wait` returning it as a tool result, and the front end putting
it into the conversation as a message — claim it from the supervisor first, and
only one claim wins. Without that, an agent that finished before its
`agent_wait` call was even emitted was handed over twice: once as the result,
once as a message a turn later, which is the context cost the whole feature
exists to avoid. A wait that loses the claim answers with a line saying where
to find the report instead of repeating it.

**The report comes back on its own.** When a subagent finishes, its report is
appended to the main conversation as a user message and the main agent starts a
turn on it — you do not have to type anything. It is held back only while a
turn is already running, while an approval is on screen, while a `!` or `$`
command is running, or while you have text in the prompt box: a turn starting
under your fingers would blur the input and eat what you were typing. It goes
up the moment that clears. A failed or killed agent is reported the same way,
so the parent is never left waiting on a result that is not coming.

## The agents sidebar

`ctrl+b` opens a 30-column pane on the right and gives it the keyboard; the
transcript reflows into what is left rather than being covered by it.

Open and focused are separate things. The pane answers every key while it has
focus — that is what lets `k` be a kill rather than a letter — so **click the
prompt to type again**, and the pane stays where it is. Click a row to hand the
keys back. `ctrl+b` always moves towards the pane: it opens a closed one,
focuses an open one, and only closes a pane that already has the keys. Which
one is live is visible in two places: the heading carries the accent and the
prompt frame goes dim, or the other way round.

```
⏺ Two of those are independent, so I will farm  │  Agents                 2 ⏵
  them out.                                     │
                                                │  ❯ ✳ a1  plan         $0.23
⏺ SpawnAgent(preset=explore, task=…)            │   Opus · 2m04s · 6 turns
  ⎿ started a2 (explore) on claude-haiku-4-5, s…│   ● a2  explore
                                                │   Haiku · waiting · 1 turn
                                                │
                                                │ ↑↓ move · enter open
 ✢ Deliberating… (8s · turn 5 · esc to interrup…│ p pause · k kill · esc back
╭──────────────────────────────────────────────╮│
│ › ask for a change, /help, ! for a shell     ││
╰──────────────────────────────────────────────╯│
 joni   ✳ Opus 5   ⏵ 2 agents                   │
 ⏵ approvals on (shift+tab to cycle) · ctrl+b a…│
```

Each agent is two lines: the status glyph, the id and the preset, with the cost
at the right, and under it the model, how long it has been going and how many
turns it has taken. The model is the family alone — thirty columns will not
hold `claude-haiku-4-5` as well as the numbers — and the full id is in the
header of the thread, one keystroke away.

| Key | Does |
| --- | --- |
| `↑`/`↓` | move |
| `enter` | open that agent's thread in the main pane |
| `esc` | back to your own thread; again to close the pane |
| `p` | pause or resume |
| `k` | stop it |
| click the prompt | type again, leaving the pane open |

The statuses are queued, running, waiting — blocked on an approval of yours —
paused, done, failed and killed. Pause is checked between turns: the model call
and the tool calls in flight finish, and it holds before the next request, so
the conversation is never left with a `tool_use` whose `tool_result` never
came. Kill cancels immediately, and what the agent already did to the tree
stays done — this ends an agent, it does not undo one.

Mouse reporting is on, so clicking the agents pill in the status line toggles
the pane and clicking a row selects it. `/mouse` turns reporting off and on
again, because while it is on the terminal's own text selection is not.

A child's usage is priced at the child's model and rolled into the session
total, so a haiku agent spawned from an opus session is not billed at opus
rates. Every metrics event a child writes carries its agent id, so
`.harness/events.jsonl` can be split per agent afterwards.

Subagent transcripts last as long as the session does: `-resume` brings back
your conversation, not theirs.

Subagents belong to the TUI: their reports come back as messages the front end
puts into the conversation, and the line-based REPL has nowhere to show four
agents at once. `-p` and `-no-tui` runs are not offered the tools at all, and
`-no-agents` turns them off for a TUI run too.