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 veyyon session that your session starts, hands one piece of work to, and collects a report from. The parent spawns it with the task tool; the subagent has its own context window, so bulk reading and long grinding work stay out of the conversation you are having.

Everything about them is configured in one place: the Subagents tab in /settings, backed by the subagent.* settings. /agents is the live picture of a run in progress: which agents are working right now and what they are saying to each other. It does not configure anything.

What you get out of the box

One agent type, the general-purpose worker, and delegation that the prompt requests:

subagent:
  delegation: preferred   # the default; the prompt requests that substantial work be delegated

Each subagent runs the model and effort set on its own page in the roster. An agent that names neither runs the profile’s default model at medium effort. Changing the model you are talking to moves that session and nothing else.

Veyyon also ships five specialists (scout, reviewer, designer, librarian, sonic), and they are disabled by default. During first-run setup, the Choose subagents step shows every available role with only task checked. Enable the specialists you want the model to start on its own. Each enabled type adds its description to future requests, so leave roles off when you do not use them.

How hard to push

subagent.delegation sets how hard this session is pushed to delegate:

ValueWhat happens
allowedThe tool is there; the model judges when it helps, and the prompt does not request it.
preferredThe default. The prompt instructs the model to fan substantial work out instead of doing it alone.
requiredThe same, plus a first-turn reminder that delegation is the default here.

The strength applies only when an enabled role matches the work. If task is enabled, it acts as the general-purpose fallback. If only specialists are enabled, work that matches none of their descriptions stays in the main session. With no enabled agent, the delegation preamble is omitted.

The separate subagent.enabled boolean (default on) is the kill switch: off removes the task tool and every delegation instruction from the prompt, so nothing can be spawned. A legacy delegation: off migrates to subagent.enabled: false.

The instructions follow the exact roles you enable. With only task offered, the prompt uses it as the general-purpose route. Enabling designer or reviewer adds those separate roles without changing what task means.

What counts as delegable work

subagent.delegation sets how strongly the model is pushed to delegate. The description of each enabled agent scopes the work that role covers. These are separate settings.

Veyyon preserves concrete roles. It does not infer a second role category from the tools an agent can call. For example, designer remains a designer and reviewer remains a reviewer. The model chooses the closest matching specialist for each independent slice:

# ~/.veyyon/subagents/accessibility-reviewer.md frontmatter
name: accessibility-reviewer
description: Reviews terminal interfaces for accessibility problems and reports findings
tools: read, search

Enable that role when you want it available:

$ veyyon config set subagent.agents.accessibility-reviewer.enabled true

When task is enabled, the model can use it for substantial work that does not fit a specialist. When task is disabled, the model keeps unmatched work inline. This prevents a specialist name from becoming a generic worker merely because no closer role is available.

An agent role is routing guidance, not a security boundary. Use the sandbox when you need to restrict filesystem or process access.

Choosing agents

subagent.agents holds one row per agent name. You choose initial permissions in the first-run Choose subagents step, then edit them through /settings → Subagents → Roster. The roster lists every discovered agent with its state, resolved model, and deciding setting. Enter opens one agent to set its state, model, and effort, or reset it to defaults.

To add an agent, put a markdown definition in ~/.veyyon/subagents/, or start from the shipped definitions by running veyyon agents unpack. The definition makes the role available. Enable its row before the model may start it. Writing a subagent covers the frontmatter fields, the system prompt and enabling the result.

That directory is read by every profile, and the file is the whole definition. Which profile may spawn the agent is a separate, per-profile answer: subagent.agents.<name>.enabled. Write the agent once, enable it where you want it.

A definition that lists a tool veyyon does not recognize is reported at startup rather than ignored. The tool grants nothing, and the guidance for it is left out of the agent’s system prompt, so a typo used to read as an agent that simply chose to do nothing.

A row has two states:

StateMeaning
EnabledListed in the task tool and choosable by the model. Only the bundled task worker defaults to enabled.
DisabledRefused even when named, with a message pointing at the setting. Specialists and user or project agents default to disabled.

The built-in flows still work with the specialists disabled because a command can grant its agent for the turn: /review requests agent: "reviewer" through a per-turn grant, and so can you (“use the scout agent to map the parser”).

Writing an agent file makes the role available but does not grant spawn permission. Enable the role during setup or in the Agents settings table.

Choosing models

Two scopes choose a subagent’s model and effort, and Subagents → Same Model for All Agents selects which one is in force. They are exclusive, not layered: the rows of the scope that is off are not drawn.

Off, the default, each agent decides. Open Subagents → Roster, press Enter on an agent, and set the model and the effort on that agent’s own page. The first of these that names a model wins:

  1. that agent’s lane, subagent.agents.<name>.model, and for a nested spawn the subagents level under it that governs that depth
  2. the agent definition’s own model: frontmatter, for an agent you wrote
  3. the profile’s default model role
subagent:
  agents:
    reviewer:
      enabled: true
      model: anthropic/claude-opus-4-5
      thinkingLevel: high

Effort resolves on the same three layers, ending at medium. An explicit :effort suffix on the resolved model pattern outranks all of them.

On, one pair decides for the whole roster, and the per-agent Model and Effort rows are hidden. Shared Model (subagent.model) and Shared Effort (subagent.thinkingLevel) sit under the switch; an unset chain runs every agent on the default model role:

subagent:
  sharedModel: true
  model: openai/gpt-5
  thinkingLevel: high

A lane keeps whatever it holds while the switch is on, and decides again the moment the switch goes off.

The default model role is the model the main assistant starts on, and it is the one keystroke path for the common case: /model writes it, and every agent with no model of its own follows it. A temporary pick, role cycling and plan mode move the live session model only, so an agent never changes model because of a keystroke aimed at the main assistant.

subagent.modelByDepth bound a chain to a spawn depth rather than to an agent and no longer applies. A config still holding it is reported once, naming the roster page that replaces it.

Fallback models

Every one of those places takes a list, not just one model:

subagent:
  agents:
    reviewer:
      model: anthropic/claude-opus-4-5,openai/gpt-5

The first entry is what that agent runs on. The rest are held in reserve: when a run errors on the model in use, that agent retries on the next entry rather than failing. The settings picker writes the value for you: open the model row, add a fallback, and press Enter on any entry to move it up the list.

A longer chain reads better as a list, and both spellings mean the same thing:

subagent:
  agents:
    reviewer:
      model:
        - anthropic/claude-opus-4-5
        - openai/gpt-5

Write it whichever way suits the file. compaction.model takes a chain the same two ways.

A chain only covers errors at run time. A model pattern that matches nothing is still a configuration mistake, so veyyon will not spawn the agent and states the setting, rather than quietly running it on the next entry: a typo must not silently downgrade the agent you spawn.

In the Subagents block above the composer, an agent that fell back is marked with before its model badge, so you can tell a deliberate model from a retried one at a glance.

Effort is chosen from a list: off, minimal through max, auto, or Inherit. Inherit on an agent’s own page means the default effort; on a nested page it means the page above it. auto requests that the provider choose. The same list appears in both places, so you cannot set a level that does not exist. If a hand-written config holds one that does not, veyyon reports the levels that work, rather than treating it as Inherit and leaving you with a setting that reads as configured and changes nothing.

A configured model that matches nothing available does not quietly fall through to the next layer. The spawn is rejected and the message states the setting to fix, because falling through is indistinguishable from your setting having no effect. Both agent surfaces show, for the selected agent, the pattern, the model it resolves to, and which of the three layers decided.

Watching a run

While a spawn is in flight, the Subagents block sits above the composer with one lane per agent. A lane reads left to right: a rail, the agent’s id, what it is doing, and the model it resolved to.

Subagents
 ▏ DockerSecretHarness    bash cargo test --workspace --all-targets         claude-opus-5 high
 ▏ SecretModeFlowUX       read modes/interactive-mode.ts                    claude-opus-5 high
 ▏ SecretModularityAudit  Audit secrets subsystem modularity, wiring, and…  claude-opus-5 med
 ▏ RateLimitedWorker      Retrying (2/5) in 38s · 429 rate limit exceeded   claude-opus-5 high

The id is painted in that agent’s own accent, the same hue the status line gives its name and the same one a delegated todo row uses to point back at it.

The middle column holds the most urgent fact the agent has. An agent asleep between provider attempts shows the recovery, its attempt count and the reason, counting down. An agent running a tool shows the tool and its argument. An agent waiting on the model has nothing to report, so it shows the work it was given instead, dimmed. Every lower rank is still true when a higher one is, and a lane that printed the description while the agent was asleep on a rate limit was byte-identical to one thinking.

Light travels down the rail while agents are working, and a lane is lit only while it has a tool in flight. The head crosses the whole block, so the cycle belongs to the block rather than the row, and arrives cold on a lane that is waiting or recovering. Where display.transitions is off, the block is still.

There is no elapsed clock and no context gauge. Total age ranks agents by seniority, which nothing acts on, and a parent decides nothing with a subagent’s remaining window. Whether a lane is stuck is answered by the recovery column. /agents carries the roster with the numbers.

A lane keeps its badge on its own row. Narrow the terminal and the model badge comes off first, then the columns shrink to what is left. Nothing wraps: the block draws no row it cannot fit, and draws nothing at all rather than overflow.

Eight lanes are drawn. Past that the block states how many more are running and points at /agents, which is the full roster. That row is the only place a count appears; the header is bare.

Limits and isolation

The remaining groups in the tab are operational: how many subagents run at once (subagent.maxConcurrency), how deeply they may nest (subagent.maxNestedSpawnDepth), per-run wall clock and request budgets, how long a finished subagent stays live before parking (subagent.idleTtlMs) and how long it stays listed after that (subagent.prune.*), and whether its edits land in an isolated copy of the tree first (subagent.isolation.*, see Safety).

Park and prune are two stages, and they do different things.

Park releases the live session, the process, its MCP clients and its memory. The roster row and the transcript stay, and messaging or opening the agent revives it. subagent.idleTtlMs (“Park After”) is the budget, five minutes by default for every model and provider. Set a positive millisecond value to override it, or 0 to keep idle agents live until exit.

Prune drops the roster row and with it the ability to wake the agent, so a long session does not accumulate every agent it ever spawned. It deletes nothing: the transcript stays readable at history://<name>.

Three settings in the Prune group control stage two. subagent.prune.enabled is on by default; turn it off to keep every parked subagent listed and revivable until you exit. subagent.prune.afterMs (“Prune After”) is how long a parked subagent keeps its row, counted from the moment it parked, and defaults to one hour. subagent.prune.waitingAfterMs (“Prune After While Waiting”) is the same budget for a subagent whose last message reported waiting on another agent, and defaults to two hours: it stopped on purpose to let a peer finish, so it is the agent you are most likely to message next. Set the two equal to treat both the same.

Turning pruning off does not turn parking off. Parking is what releases the session, and it happens either way; the prune switch only sets whether the parked row is eventually dropped. “Park After” sits in its own Park group for that reason, and the prune switch never hides it.

A subagent read back from a previous run is judged on the same budgets. Its age comes from when its transcript was last written, not from when this session found it, so resuming a session does not reset every old agent’s clock to zero.

When each budget starts counting

Both budgets count from the agent’s last transition, not from when it was spawned. An idle agent’s park budget starts when it went idle. A parked agent’s prune budget starts at the moment it parked. A revived agent starts its park budget again from the revival, so messaging a parked agent gives it a fresh five minutes rather than resuming a clock that was already half spent.

That is why a long-lived session does not prune everything at once: each agent’s deadline moves with its own activity.

Only idle and parked agents have a deadline

A running agent has no deadline at all, and neither does an aborted one. Nothing parks or prunes an agent that is mid-turn.

This matters when an agent looks stuck. A subagent waiting for you to answer an approval prompt is still mid-turn, so it stays running and no park or prune timer applies to it. If a finished agent is not being cleaned up, check its status first: the lifecycle only acts on idle and parked, so an agent stuck in running is a different problem and the prune settings will not affect it.

Turning it off

Set subagent.prune.afterMs to 0 and no parked agent is ever pruned. That also forces the waiting budget to 0, whatever subagent.prune.waitingAfterMs is.

That coupling is deliberate. If a zero parked budget still honoured a separate waiting budget, the only agents that were ever pruned would be the ones that stopped to wait on a peer, which are the agents you are most likely to message next. Zero means never prune, for both kinds.

If the session cannot be saved

Parking flushes the agent’s session to disk before releasing it. If that flush fails, the park is cancelled and the agent stays live with its timer re-armed. You keep a live agent rather than losing unsaved state, and the attempt repeats on the next expiry.

Nesting depth

subagent.maxNestedSpawnDepth is inclusive. The default is 0: the top-level session, at depth 0, may spawn direct subagents, but those children are leaves and cannot spawn more subagents. A value of 1 also lets direct children spawn, producing children at depth 2. Higher values extend the same rule, and -1 allows nesting without a depth limit.

An agent-specific value takes precedence over the blanket value:

subagent:
  maxNestedSpawnDepth: 0
  agents:
    reviewer:
      maxNestedSpawnDepth: 1

Here ordinary direct subagents remain leaves. A direct reviewer may spawn its own children because its effective limit is 1.

A subagent’s working directory is its own. If a subagent calls set_cwd, only that subagent moves: its tool paths resolve against the new directory and its system prompt is rebuilt for it, while your session and every other subagent stay where they were.

That matters because subagents run inside the same process you do. The main session also moves the process working directory when it re-roots, so that a command you run and a relative path you write agree with the project you have open. A subagent doing the same would move the ground under everyone else, and the symptom would be a command running in the wrong repository with nothing on screen to explain it.

The trade is that a subagent working elsewhere does not pick up that project’s settings, capabilities or plugins, because those are read once for the process. Give a subagent a task in another project only when the work is self-contained, and re-root your own session instead when you want that project’s configuration to apply.

The full key list is in the settings reference.