agentafk
Guides

Subagents & Delegation

How to delegate work to subagents: the agent tool, background mode, /bgsub, the compose DAG, and when to delegate vs. stay inline.

Agent AFK can fork child sessions — subagents — that run isolated tasks in parallel or sequence. This guide covers the mechanics: the agent tool, background mode, the compose DAG, and the decision rules for when to delegate vs. stay inline.

How subagents work

Subagents are child sessions that run independently. Each inherits permissions from the parent and runs the same tool surface. What it does not inherit is context — every subagent starts with zero prior conversation.

Subagents are non-interactive

A forked subagent has no operator surface of its own — it returns findings to its parent, which owns the human relationship. So subagents run non-interactive by default:

  • The ask_question tool is stripped from a subagent's toolset entirely — it cannot block on the operator. It must proceed on a stated assumption or return a Blocked / Asking result to the parent.
  • A path-approval prompt (file access outside the granted roots) is auto-denied rather than surfaced — pre-authorize the path in the parent or run in bypass mode.
  • MCP elicitations are auto-declined.

The parent session decides what to do with a subagent's Blocked/Asking result — including re-dispatching with wider grants or asking you directly.

Always include in a subagent brief:

  • Objective
  • Relevant file paths and constraints
  • Expected deliverable (format, length)
  • What not to do and when to stop

A subagent brief with missing context produces unreliable output. Synthesize what the subagent needs; don't assume it can infer from the parent conversation.

The agent tool

The primary dispatch mechanism is the built-in agent tool. Pass a prompt describing the work:

agent({
  prompt: "Read the retry logic in src/http/client.ts and explain the backoff strategy in ≤150 words. Cite file:line for each claim.",
  model: "haiku",          // optional — right-size the model
  max_turns: 5             // optional — bound the budget
})

The tool forks a child session, runs it to completion, and returns the compressed result to the parent. The parent's context window receives the final message only — not the child's internal reasoning or tool call log.

Model selection

You can dispatch subagents on cheaper models when the task doesn't need full capability:

ModelBest for
haikuFast lookups, simple summarisation, file reads
sonnetGeneral investigation, multi-file analysis
opusComplex reasoning, long-horizon planning

Foreground vs. background

By default agent is foreground: the parent waits for the child to finish before continuing.

Pass mode: "background" to fire-and-forget. The tool returns a jobId immediately. The parent can keep working and join the result later:

// Dispatch background job
agent({ prompt: "...", mode: "background" })
// → returns { jobId: "abc123" }

// Later, in the REPL:
/bgsub:join abc123

Use background mode for long investigations where the result isn't needed in the current turn — for example, running a security audit while the main session continues implementing a feature.

/bgsub — background job management

In the REPL, background subagent jobs are surfaced through the /bgsub namespace:

/bgsub:join <jobId>    # wait for and return the result

The status bar at the bottom of the REPL shows running background task counts.

Source: src/cli/background-status-bar.ts, src/cli/commands/bg.ts.

afk bg — CLI background job inspection

From the terminal, inspect persisted background job logs with the afk bg command:

afk bg list              # list all jobs (most recent first, --max <n> to limit)
afk bg tail <jobId>      # stream live events (--from-start to replay first)
afk bg replay <jobId>    # replay all events, then exit

Jobs are persisted to ~/.afk/state/bg/ even after the parent REPL exits. Use afk bg to check status, review logs, or re-stream a job's output from disk. Complements /bgsub:join when you need access outside an active REPL session.

The compose tool — DAG orchestration

For structured multi-agent workflows, the compose tool dispatches up to 20 subagent nodes with explicit dependency edges. Independent nodes run in parallel; dependent nodes wait.

compose({
  nodes: [
    { id: "security", prompt: "Audit src/api for authz and injection bugs. List each finding with file:line." },
    { id: "coverage", prompt: "Find untested branches in src/api. List each with file:line." },
    { id: "report",   prompt: "Combine the security and coverage findings into one prioritised punch-list." }
  ],
  edges: [
    { from: "security", to: "report" },
    { from: "coverage", to: "report" }
  ]
})

Dependencies are declared as edges ({ from, to } — "from must finish before to starts"), not as a field on the node. Here security and coverage have no incoming edges, so they run in parallel; report waits for both. If any node fails and fail-fast is enabled (the default), downstream nodes are cancelled rather than running with incomplete inputs.

Limits and failure behavior:

  • Maximum 20 nodes per compose call
  • Cycles are rejected at dispatch time
  • Fail-fast cancels all downstream nodes on first failure — the parent sees which nodes ran and which were skipped

Source: src/agent/tools/compose-executor.ts, src/agent/dag.ts.

How cancellation works

When you cancel a session, all running subagents stop immediately. The rules:

  • Parent abort cascades down — aborting the parent cancels all running children.
  • Child abort notifies up but does not auto-abort the parent — a failed subagent surfaces an error to the parent; the parent decides whether to continue.
  • Abort beats hook decisions — if an abort signal fires while a hook is deciding, abort wins.

This means you can cancel a daemon session or a REPL turn and all its child work stops immediately. A single broken subagent in a compose DAG does not bring down the orchestrator unless it's in a dependency chain.

When to delegate vs. stay inline

Delegate when the work would:

  • Read or grep more than 3 files inline
  • Verify a claim independently from the chain that produced it
  • Investigate a failing test or unexplained behavior
  • Run two or more independent investigations that could happen in parallel
  • Consume more main-session context than the subagent's compressed answer would

Stay inline when:

  • The task is a single-file edit or localized fix visible in fewer than 2 reads
  • The answer is conversational or requires no file access
  • The user explicitly asked for a direct tool call
  • Dispatch overhead exceeds the work itself

Source: AGENTS.md (Delegation section).

Right-sizing the budget

Subagents should have explicit budgets. Before dispatching:

  • Set max_turns on focused investigations (max_turns: 5 for a quick lookup)
  • Choose the cheapest sufficient model (haiku for reads, sonnet for multi-file analysis)
  • For wide fan-outs, dispatch in bounded waves rather than all at once — a burst of parallel children can hit provider rate limits and cascade into timeouts

Subagent output

Subagents return their final assistant message verbatim. Structure the brief to ask for compressed output:

"...Return: answer (1–2 sentences), evidence with file:line citations, confidence (high/medium/low), and any unresolved questions."

For typed output, the SubagentManager accepts optional Zod schemas — the subagent's output is validated before it reaches the parent. Malformed output surfaces as an error rather than silently passing through.

Verifying subagent output

High-stakes subagent findings should not be trusted blindly. Use the /shadow-verify skill to dispatch parallel adversarial verifiers that re-derive 2–3 key claims from scratch using tool calls. Any disagreement is flagged before you act on the result.

See the Verification guide for the full workflow.

Child-to-parent progress events

By default a running background subagent is opaque to its parent — the parent only learns the result when the child finishes. Opt into progress events to get live updates at each tool-call boundary while the child is still running.

Enabling emit_progress

Pass progress_events: true when dispatching:

agent({
  prompt: "Run the full test suite and summarise failures...",
  mode: "background",
  progress_events: true
})
// → returns { jobId: "abc123" }

This grants the child an extra emit_progress tool not available on non-opted-in subagents.

The emit_progress tool (child-side)

When progress_events: true is set, the child calls:

emit_progress({
  message: "Running unit tests — 142/500 complete, 3 failures so far.",
  phase: "testing",        // optional label for the current stage
  metadata: { passed: 142, failed: 3 }  // optional structured payload
})
// → { delivered: true }

Fields:

  • message (required) — free-text description of current status. Capped at 2048 bytes; truncated at the UTF-8 byte boundary if exceeded.
  • phase (optional) — short label for the current stage (e.g. "scanning", "testing", "summarising").
  • metadata (optional) — arbitrary key/value object for structured data.

Delivery to the parent

Each event is delivered to the parent's next user turn as a <child-progress> XML envelope:

<child-progress subagentId="sub_abc123" phase="testing" timestamp="2026-09-17T12:00:00.000Z">
  Running unit tests — 142/500 complete, 3 failures so far.
</child-progress>

Events are buffered in a ring per handle; the oldest are evicted on overflow rather than blocking the child. If the parent's abort signal has already fired, emit_progress returns { delivered: false, reason: "parent_aborted" } and is otherwise a no-op.

This is the reverse direction of send_message_to_agent (parent → child). Together they form a bidirectional steering channel.

Source: src/agent/tools/subagent/emit-progress.ts, src/agent/subagent/fork-progress-events.ts.

Steering a running background child

The parent can inject a message into a running background subagent at its next tool-call boundary using send_message_to_agent. This lets you redirect, correct, or update context in a child without cancelling and re-dispatching it.

send_message_to_agent({
  jobId: "abc123",
  message: "The spec changed — focus only on the auth module, skip everything else."
})
// → "Steering message queued for background job abc123."

The message arrives as a synthetic user turn before the child's next tool round. The child sees it the same way it would see any other instruction and can adjust its plan mid-execution.

Constraints

  • Background jobs only — foreground subagents are steered by the human via the interrupt picker (Ctrl+C → Steer), not through this tool.
  • Model-created jobs only — jobs that were user-backgrounded (the operator pressed Ctrl+B) are user-owned and the tool refuses them with a clear error.
  • Same-session only — a parent session cannot steer a job belonging to a different concurrent session.
  • Anthropic provider only — steering requires the provider's inter-round hook (setBeforeNextRound). OpenAI-compatible providers do not wire this hook; the tool returns a descriptive error rather than silently dropping the message.
  • Terminal jobs — if the child has already completed or failed, the tool returns a non-error acknowledgement (the job is done; no message is queued).

Source: src/agent/tools/subagent/send-message.ts, src/agent/tools/schemas.background-steer.ts.

Workspace subscriptions

Sibling agents in a compose DAG share a workspace via workspace_publish and workspace_query. Polling with workspace_query works, but a child that calls workspace_subscribe receives matching entries pushed to it at each tool-call boundary without repeated queries.

Subscribing

workspace_subscribe({
  subject: "auth",          // optional — substring filter on entry subject
  type: "finding"           // optional — restrict to one entry type
})
// → { subscriptionId: "ws_abc", active: true }

Both filters are optional. Omitting them subscribes to all entries published by any sibling in the same session.

Delivery

Entries matching the subscription are accumulated in a ring buffer and delivered at the next tool-call boundary as a <workspace-delivery> envelope:

<workspace-delivery subscriptionId="ws_abc">
  { "id": 7, "type": "finding", "subject": "auth refresh", "content": "..." }
</workspace-delivery>

No polling needed — the child just reads normal tool-result context in its next round. If the ring fills before the child drains it, the oldest entry is evicted (dropped count is traced).

Availability

workspace_subscribe is only available on forked children — the tool is not registered on top-level REPL, one-shot chat, Telegram, or daemon surfaces. The workspace_query poll is always available.

The subscription is cancelled automatically when the child's handle is torn down.

Source: src/agent/workspace/workspace-tools.ts, src/agent/subagent/workspace-subscription-wiring.ts.

Subagent result sidecar spill

When a foreground subagent's final message exceeds the configured byte threshold, AFK does not silently truncate it. Instead, the full output is spilled to a sidecar file and the parent receives a head+tail slice with a pointer to the full text:

[Subagent output was 98304 bytes — full result at
~/.afk/state/sessions/<sessionId>/subagent-handoffs/<subagentId>.txt.
Use read_file to retrieve it.]

The parent can then call read_file on that path to retrieve the complete output.

Threshold

The default cap is 32 KB (AFK_SUBAGENT_RESULT_CAP_BYTES=32768). Tune it with:

AFK_SUBAGENT_RESULT_CAP_BYTES=65536   # raise to 64 KB
AFK_SUBAGENT_RESULT_CAP_BYTES=0       # disable the cap entirely

Setting it to 0 disables the cap — the full result is returned to the parent verbatim, which can blow up the parent's context window if the child produces large output.

Spill layout

Sidecar files are written under the parent session's state directory:

~/.afk/state/sessions/<sessionId>/subagent-handoffs/<subagentId>.txt

Both sessionId and subagentId are path-sanitized before the file is written. Spill is best-effort — if the write fails (disk full, permissions), the parent still receives the truncated view, but without the file pointer.

Source: src/agent/tools/subagent/foreground-promotion.result-cap.ts.

On this page