- name
- subagent-background
- description
- Run subagents asynchronously with `run_in_background: true` and manage them with `subagent_send` (status / wait / steer / abort). Use when you want to fan out work, hide latency behind other steps, or keep the parent responsive while a long child runs. Pairs with `subagent-delegation`, which covers when and how to delegate at all.
# Subagent Background Lifecycle
`subagent({ run_in_background: true })` returns a handle immediately instead of blocking. The child keeps running in the
background - even across the parent turn boundary - and you retrieve its answer later with
`subagent_send({ to, action: "wait" })`. This skill is the lifecycle and the policy for when async is worth the extra
bookkeeping.
If you haven't decided whether to delegate at all, read `subagent-delegation` first.
## When background beats synchronous
Sync (`run_in_background: false`, the default) blocks the current assistant turn until the child finishes. The child
can't outlast the turn. That's fine for:
- A single quick exploration (<30s of child work).
- Anything where you literally cannot proceed without the result.
Go background when **any** of these is true:
- **Fan-out with work to do.** You want to spawn two or more children and use the parent's turn to make progress on
something else while they run.
- **Latency-hiding.** The child is going to take a while; meanwhile you could be drafting code, running tests, or
reading other files.
- **Cross-turn work.** The investigation is long enough that it would blow your turn budget or trigger a stall - let it
run across multiple parent turns.
- **Exploratory "maybe I'll use this".** You want a second opinion or an alternative plan in parallel; finish your own
attempt first, then compare.
- **Steerable tasks.** You expect to nudge the child mid-run (new constraint, narrower scope) via
`subagent_send({ to, text: "..." })`.
Stay sync when:
- The child answer is the _immediate next input_ to your reasoning.
- You have nothing else to do while it runs.
- The task is short enough that polling overhead costs more than it saves.
## Fan-out pattern
Multiple `subagent` calls in a **single assistant turn** run concurrently. This is the cheapest way to parallelize:
```text
Turn N (parent):
subagent({ agent: "explore", task: "find callers of A", run_in_background: true })
subagent({ agent: "explore", task: "find callers of B", run_in_background: true })
subagent({ agent: "explore", task: "find callers of C", run_in_background: true })
→ returns three handles immediately
Turn N (parent, same turn):
... do other work inline ...
Turn N+1 (parent):
subagent_send({ to: h1, action: "wait" })
subagent_send({ to: h2, action: "wait" })
subagent_send({ to: h3, action: "wait" })
→ merge results
```
If the tasks truly are independent and you have no inline work to do, you can also call them synchronously in the same
turn - the harness still runs them concurrently. Reach for `run_in_background` specifically when you want the parent
free to do something else, or when the children might outlast the turn.
## The handle
Every `subagent({ run_in_background: true })` call returns a short handle string (something like `c1`, `c2`, …).
**Record it the moment you get it.** Good places:
- `scratchpad` - "spawned `c1` to find callers of X; waiting."
- The `note` field on a `todo` review item - "parked pending `c1`".
Do NOT try to reconstruct handles from memory across compactions. If the scratchpad doesn't have it, list active
children with the `/agents running` surface or treat the work as lost and respawn.
## `subagent_send` actions
| Action | Blocking? | Purpose |
| ------------------- | --------- | --------------------------------------------------------------------- |
| `status` (default) | No | Cheap snapshot: running / finished / aborted, plus turn count. |
| `wait` | Yes | Block up to `timeoutMs` for the child to finish; return final answer. |
| `abort` | No | Cancel a running child. Final answer classified as `aborted`. |
| (no action, `text`) | No | Inject a user-role message into a running child to steer it. |
Rules the harness enforces:
- `text` is **not combinable** with `action: "abort"`. Pick one.
- `text` on a **finished** child is rejected - steer only while running.
- `wait` on a finished child returns the stored answer instantly; safe to call repeatedly.
- Only the parent can call `subagent_send`. Subagents never see this tool.
## Polling loop pattern
For a single long-running child:
```text
spawn → handle h
... do inline work ...
subagent_send({ to: h, action: "status" }) # cheap check
→ still running, do more inline work
subagent_send({ to: h, action: "wait", timeoutMs: 30000 })
→ final answer
```
For fan-out:
```text
spawn h1, h2, h3 in one turn
... do inline work ...
for h in [h1, h2, h3]:
subagent_send({ to: h, action: "wait" })
merge and act
```
Prefer `wait` with a generous `timeoutMs` over tight `status` polling - each `status` call burns a tool slot.
## Steering a running child
Use `subagent_send({ to, text })` to inject new guidance into a child that's still working. Good reasons:
- You discovered a constraint the child needs to honor ("ignore anything under external/").
- You realized the output shape you asked for was wrong ("return JSON, not prose").
- The child is about to waste turns on the wrong sub-problem ("stop reading search.ts; look at validate.ts instead").
The injected text appears to the child as a new user-role message. Write it the same way you'd write a clarifying reply
in chat - short, imperative, paste any paths literally.
Do NOT use steering as a replacement for a better initial `task`. If you're steering on every spawn, tighten the prompt
(see `subagent-delegation` → Writing the `task`).
## Abort criteria
Abort a background child when:
- The task is obsolete. The user changed direction, or the parent already solved it inline.
- The child is clearly stuck (repeated reads of the same file, no progress across multiple `status` checks, turn count
climbing with no output growth).
- You spawned the wrong agent type and steering won't fix it (e.g., you need write access but spawned `explore`).
`subagent_send({ to, action: "abort" })` is the clean exit. Don't just stop calling `wait` and hope it dies - background
children survive turn boundaries and keep consuming until they abort, finish, or time out.
## The "don't orphan children" rule
Every background handle you receive is a commitment. Before ending your assistant turn, for every live handle you
spawned:
- `wait` on it (if you need the answer), OR
- `status` it and note in `scratchpad` that you'll check next turn, OR
- `abort` it (if obsolete).
Silently moving on and never touching a handle again is the worst outcome:
- The child keeps burning tokens until it hits its own turn cap.
- Its final answer ends up in a tempfile the parent never reads.
- Future turns get cluttered with "running" entries in the injected status block.
A quick audit at the end of each turn - "any live handles I haven't resolved?" - catches this.
## Combining with `todo` and `scratchpad`
Background work and planning tools compose naturally:
- Move the corresponding todo to `review` with a note like "parked on `c1`" when you spawn.
- `complete` the todo only after `wait` returns and you've verified the answer.
- Keep handle → task mapping in `scratchpad` so you can re-attach after compaction:
`c1 = explore callers of searchHandler (src/api/search.ts:412)`.
## Anti-patterns
- **Don't spawn background children you have nothing to do with.** If you'll just `wait` immediately, spawn sync.
Background is for latency-hiding, not syntax sugar.
- **Don't tight-poll with `status`.** One `status` between substantial inline work is fine; a `status` every line of
reasoning is pure waste. Use `wait` with a timeout instead.
- **Don't steer on every turn.** If you're injecting text more than once or twice, the original `task` was wrong - abort
and respawn with a better prompt.
- **Don't forget handles exist across turns.** Unlike sync children, background children outlive the turn that spawned
them. Treat them like open file descriptors.
- **Don't race children against the user.** If the user is typing a follow-up, a background explorer can deliver its
answer mid-conversation and derail the thread. Abort or `wait` before pivoting direction.
- **Don't spawn a child per file.** If you're about to loop `for each file: subagent(...)`, write one `task` that
handles the batch, or fan out with a handful of children covering ranges - not dozens.
## Quick reference
| Goal | Move |
| ------------------------------------------ | ------------------------------------------------------------------------ |
| Spawn and keep working | `subagent({ run_in_background: true, ... })`, record handle |
| Cheap progress check | `subagent_send({ to, action: "status" })` |
| Block for the final answer | `subagent_send({ to, action: "wait", timeoutMs: 30000 })` |
| Push a new constraint into a running child | `subagent_send({ to, text: "also ignore external/" })` |
| Cancel a running child | `subagent_send({ to, action: "abort" })` |
| Fan out 3 independent explorations | 3× `subagent({ run_in_background: true })` in one turn, `wait` next turn |
| Never again for this handle | `abort` or `wait` - don't orphan |
عرض على GitHub