A subagent is an agent dispatched as a tool call from another agent. Both engines support arbitrary nesting (capped by depth guard). The contract to the frontend is identical — both emit subagentStart / subagentComplete SidecarEvents with parentToolUseId set — but the codepath that gets there is engine-specific.

The nesting model

Native Task / Agent tool

Claude SDK ships with Agent/Task as a built-in tool. When the parent LLM calls it:
  1. SDK spawns a sub-query with isolated context.
  2. parentToolUseId is set automatically to the tool_use_id of the spawning Task call.
  3. Events stream through the same normalization layer with the parent id attached.

D9 body filter

When an agent is dispatched as a subagent, the body sent to the model is AGENTS.md only. SOUL.md (persona) and MEMORY.md (seed memory) are dropped because they pollute when an agent runs as another’s helper. The filter is enforced via getAgentDefForSubagent (src-tauri/sidecar/agents/loader.mjs).
D9 sub-rule (added 2026-05-05): Any tool whose return value flows back into another model’s context (e.g. agent_info) MUST also use getAgentDefForSubagent, not getAgentDef. Otherwise the discovery result leaks SOUL+MEMORY into the downstream subagent. Verified leak path was fixed at commit d2a26640. ✓ VERIFIED. See agents-as-first-class-entities.md D9 invariant.

Three nesting modes (Pi only)

spawn_subagent accepts three dispatch shapes:
ModeBehavior
singleOne subagent, one result. Most common.
chainSequential — output of subagent N feeds into prompt of subagent N+1.
parallelAll subagents dispatched concurrently; results collected after all complete.
Claude SDK has no native equivalent for chain / parallel — those compositions must be expressed by the parent LLM itself (multiple Task calls).

Subagent vs spawn_subagent — the same contract

Both produce the same SidecarEvent shape:
// SidecarEvent
type SubagentStart = {
  type: 'subagentStart';
  subagentId: string;
  parentToolUseId: string;   // ← set by both engines
  agent: string;             // e.g. "scout" or "research-analyst"
  prompt: string;
  rootConversationId: string;
}

type SubagentComplete = {
  type: 'subagentComplete';
  subagentId: string;
  parentToolUseId: string;
  result: string;
  isError: boolean;
}
The frontend’s ActivityStreamV3 and SubAgentPanel components consume these the same way regardless of engine.

Safety gaps in subagent dispatch

GAP-A8: SubagentJsonlWriter rotation policy unverified. The JSONL files at <vault>/.orion/subagent-events/<rootConversationId>/events.jsonl could grow unbounded if rotation isn’t wired. Audit confirmed the writer exists but did not verify rotation. See Trust → GAP-A8.
GAP-A3: parentToolUseId asymmetry. Claude SDK sets it automatically; Pi sets it manually inside pi-subagent-runner.mjs. Equivalent in practice, but if the Pi code regresses, subagent events orphan in the frontend (rendered at top level, not nested). Defensible against regression tests.

Key files

Pi’s spawn_subagent implementation. Depth guard _depthMap, model fallback loop, parentToolUseId propagation, JSONL writer wire-up.
resolveAgentLayered({builtinDir, vaultDir}) resolves agents from bundle + vault overrides. getAgentDefForSubagent is the D9-filtered variant — drops SOUL+MEMORY.
Boot reconcile + auth-change reconcile. DB index of agents (derived index, D7 invariant — not state-of-record).
Programmatic agents map for Claude SDK query({agents}).

Next

MCP, Permissions & Sandbox

Subagents inherit tool sets from their parent runtime — which means inheriting the parent’s permission posture. Read this next.