Subagents
A subagent is a separate Session the parent spawns through a tool call. It runs its own loop with its own system prompt, its own message history, and a tool registry filtered down from the parent’s. The parent waits on the tool result while the child runs to completion; every event the child emits is wrapped in a subagent_event and forwarded into the parent’s stream.
#What is a subagent
Subagents are a different shape from skills and tools. A tool runs an in-process function and returns a string. A skill injects extra system-prompt instructions (and optionally a different model or tool allowlist) into the same conversation. A subagent spawns a brand-new agent loop: a fresh context window, a fresh message history, a fresh system prompt, and a tool registry that excludes Subagent itself. The parent only sees the child’s final assistant text as the tool result — the child’s intermediate messages never enter the parent’s context.
Use a subagent when you want to spend tokens without polluting the parent (deep research, log-grovelling, codebase walks), when you want to enforce a narrower tool surface for a sub-task (a read-only researcher, a write-only formatter), or when you want to run an independent persona with a different system prompt.
#Directory layout
Subagent definitions live as flat files directly under <configDir>/agents/ — one Markdown file per subagent, not one directory per subagent. configDir defaults to .skawld resolved against the current working directory and can be overridden via the Agent constructor. The loader walks the directory non-recursively, reads every file ending in .md (case-insensitive), and skips symlinks. Each file is YAML frontmatter followed by a Markdown body.
.skawld/
└── agents/
├── researcher.md
├── code-reviewer.md
└── planner.mdThe directory is read lazily on the first agent.session() call and the result is memoized for the lifetime of the Agent. If agents/ doesn’t exist the loader returns an empty list and no error. Files that fail to parse are dropped with a structured reason — missing-frontmatter, invalid-frontmatter, name-collision, or io-error — and the rest of the directory still loads.
---
name: researcher
description: Read-only research agent for codebase questions.
tools: [Read, Grep, Glob]
---
You are a research agent. You never modify files.
When asked a question, gather evidence by reading code, then
return a short summary with file:line citations.#Frontmatter reference
The loader consumes a strict, small schema. Unknown keys are silently ignored, which lets the same file double as a Claude Code .claude/agents/*.md agent (those carry extras like model, provider, mcpServers, hooks, color, permissionMode that skawld does not read).
| Field | Type | Required | Notes |
|---|---|---|---|
name | string | Optional | Agent identifier — what the model passes as subagent_type. Must match /^[a-z0-9][a-z0-9_-]*$/i. Defaults to the filename minus .md when omitted. Names are case-insensitively unique inside one agents/ directory; the first file wins on collision. |
description | string | Required | Non-empty string. Surfaced verbatim to the parent model in the Subagent tool’s catalog (- name: description). Treat it as a prompt for when the parent should pick this agent. |
tools | string[] | string | Optional | Tool allowlist. Accepts a YAML array of strings or a single comma-separated string. Omitting the field or passing ["*"] means “every tool the parent has, minus Subagent”. An empty list means “no tools”. Names that don’t resolve in the parent registry are silently dropped at spawn time. (...) permission patterns are stripped — Bash(npm:*) becomes Bash, since v1 has no per-argument permissions here. |
Only name, description, and tools
are read by the skawld loader. A model: key in your
frontmatter will be parsed by the YAML but ignored — the subagent always
runs on the parent agent’s provider and model. If you need a
different model for a sub-task today, do the work in a separately
constructed Agent.
#Body
Everything after the closing --- of the frontmatter is the Markdown body, used verbatim as the subagent’s userInstructions system-prompt block. The identity, environment, and tool-protocol blocks above it remain identical to the parent so the cache prefix lines up across spawns. There is no templating — the body is a string. Write it the way you’d write a system prompt: who the subagent is, what it has access to, what shape of output the parent expects back.
#The default subagent
Skawld always registers one built-in agent under the reserved type _default, regardless of what’s on disk. Disk agents cannot override it. It is intentionally not listed in the catalog the parent model sees — the model fires it implicitly by calling the Subagent tool with no subagent_type.
Its frontmatter is { name: "_default", description: "General-purpose subagent with full tool access.", tools: undefined } and its body is a short prompt instructing the child to complete one focused task and return a concise summary. UI display names are Agent #N per parent session (1-indexed); named agents keep their declared name as the display name.
Because the default agent inherits the parent’s full tool surface,
it’s the right choice for general-purpose delegation: offloading a
sub-task into a fresh context window without restricting what the child
can do. For anything narrower (read-only research, review-only diff
walks), define a named agent with a tools: allowlist.
#Invoking a subagent
Subagents are reached through a single built-in tool called Subagent (registered as exec-scope, marked parallelSafe). The parent model sees a catalog of disk agents in the tool’s description and produces a normal tool call with this input shape:
interface SubagentInput { /** Short 3-5 word label shown in the parent's UI. */ description: string; /** The full task for the child. The child has no parent history, * so the parent must inline every piece of context the child needs. */ prompt: string; /** Agent type name (e.g. "researcher"). Omit, leave empty, * or pass "general-purpose" to spawn the built-in _default agent. */ subagent_type?: string; }
Unknown subagent_type values fail loudly: the tool returns an is_error result listing available agents, so the parent model can correct itself on the next turn. An aborted or erroring child also returns is_error with whatever partial text the child produced. A successful run returns the joined text of the child’s last assistant message — including the text blocks of a multi-block message (e.g. thinking + text).
#Isolation
Subagents share the heavy infrastructure of the parent but run an independent conversation. The split:
| Component | Shared with parent | Notes |
|---|---|---|
| Provider + model | Yes | The child runs on the same Provider instance and model as the parent. There is no per-agent model override. |
| Permission engine | Yes | One PermissionEngine instance gates every tool call from every session under the Agent, parent and children alike. See Permissions. |
| Session store | Yes | The child gets its own record in the same store, tagged with { parentSessionId, subagentType, subagentRunId, displayName } meta so you can reconstruct a hierarchy later. |
| MCP connections | Yes | MCP servers are connected once per Agent. The child gets the same already-open clients; the runner bypasses re-connect. |
| Skills | Yes | Disk-loaded skills are shared. The child is registered in the agent’s session map for the duration of its run so Skill calls from within can resolve their session. |
| Tool registry | Filtered | The child gets a fresh ToolRegistry containing only the parent’s tools whose names appear in the frontmatter tools list (or all of them when the list is undefined / ["*"]). Subagent is always excluded — children cannot recurse. |
| System prompt | Identity + env shared, instructions replaced | The child reuses the parent’s identity, environment, and tool-protocol blocks (so cache prefixes line up) but the userInstructions block is the subagent’s Markdown body. |
| Message history | No | The child starts from an empty providerView. The parent’s prior turns are never visible. |
| Compaction state | No | Compaction is per-session. The child has its own context-window accounting and runs its own compaction policy if configured. |
| Run options | Partial | thinking and effort propagate from the parent’s current run to the child. temperature, images, and maxOutputTokens are intentionally not propagated. |
| Abort signal | Chained | The parent tool call’s AbortSignal is wired into the child two ways: passed straight into Session.run, and observed via an abort listener that calls child.abort(reason) mid-stream. When the parent aborts, the child stops at the next safe boundary. |
#Permissions for subagents
There is one permission engine per Agent. Every tool call the child makes flows through the same PermissionEngine.resolve() as the parent’s calls, including rules, mode, and canUseTool. The subagent does not get its own policy and cannot escalate.
The tools frontmatter list is a separate mechanism applied before the engine sees a call: tools missing from the list aren’t present in the child’s registry at all, so the model never sees them and the engine never gets a call for them. Treat the two as additive — frontmatter limits the surface area exposed to the child model, permissions gate what the child can actually do with whatever is in that surface. The exec-scope mode default (Bash, Skill, Subagent itself) still applies to the child, so an acceptEdits parent will still ask when the child tries to run shell. See Permissions for the resolution order.
#Subagent events
While a child runs, the runner wraps every event the child emits in a single envelope and pushes it into the parent’s event iterator. There is one event type on the wire:
interface SubagentEvent { type: "subagent_event"; /** The Session the consumer is iterating — always the top-level parent. */ parent_session_id: string; /** Unique per Subagent tool call. Use it to group events for one run. */ subagent_run_id: string; /** Agent type — "researcher" | "_default" | ... */ subagent_type: string; /** UI display name. "Researcher" for named agents, "Agent #N" for the default. */ display_name: string; /** The child Session's original event. Nested spawns nest: event.event.event... */ event: Event; }
The inner event is the full Event union the child would have produced as a standalone session — assistant, partial_assistant, tool_call_start, tool_call_end, permission_request, usage, compaction, skill_invoked, skill_completed, result, error, and so on. See Events for the full list. If a subagent spawns its own subagent the inner event will itself be a SubagentEvent, and a UI can render the tree by recursing on the type tag.
import { isSubagentEvent } from "@skawld/agent-sdk"; for await (const event of session.run(prompt)) { if (isSubagentEvent(event)) { // group activity by event.subagent_run_id, label with event.display_name renderChild(event.subagent_run_id, event.display_name, event.event); continue; } renderParent(event); }
subagent_run_id is generated per Subagent tool call (format sa_<8 hex>) and stays stable across every event for that run, so it’s the right key to collapse child activity into a row in your transcript UI.
#A worked example
A read-only code reviewer. It can read files, grep, and shell out to gh and git diff commands, but the frontmatter strips it of Write and Edit even if the parent agent has them.
---
name: code-reviewer
description: |
Walks a diff and returns a structured review. Read-only: cannot
write or edit files. Spawn me with the diff range or a PR number,
plus any context (style guide, hot files) — I have no parent history.
tools: [Read, Glob, Grep, Bash]
---
You are a code reviewer. You never modify files; your tools list excludes
Write and Edit. The parent has handed you a single review task.
Procedure:
1. Identify the diff to review. If the parent mentioned a PR number,
fetch the diff with `gh pr diff <n>`. Otherwise use `git diff` on
the range they specified.
2. Read the files touched by the diff for context — not just the hunks.
Skim sibling tests, callers of changed functions, and any types the
change moves.
3. Look for: correctness bugs, missing or weakened tests, unsafe error
handling, API surface changes without callers updated, and obvious
simplifications. Ignore stylistic preferences.
4. Return your review as a single markdown block with these sections:
- **Summary** (1-2 sentences)
- **Blocking** (numbered list, file:line each)
- **Non-blocking** (numbered list, file:line each)
- **Test gaps** (numbered list)
Be concise. The parent will relay your text verbatim, so don't pad with
preamble or apologies. If the diff is clean, say so in one line.Drop the file at .skawld/agents/code-reviewer.md next to your agent code, start a session, and the parent model will see code-reviewer in the Subagent tool’s catalog with its description on the next turn. No registration call, no rebuild — the loader picks it up on the first agent.session().