Skip to content

Sub-Agent Orchestration

When the parent agent decides to delegate work, it invokes the Agent tool. This page dissects the complete call flow — from parameter parsing to nested execution loop to result extraction — and compares all four agent spawning patterns in Claude Code.

The Agent tool entry point is AgentTool.tsx, which dispatches to runAgent.ts for the actual execution. Here’s the full sequence:

sequenceDiagram
    participant P as Parent Agent
    participant AT as AgentTool.call()
    participant RA as runAgent()
    participant Q as query() loop
    participant API as Claude API

    P->>AT: Agent({ prompt, subagent_type, model, ... })
    AT->>AT: Resolve agent definition
    AT->>AT: Check fork path vs named agent
    AT->>AT: Assemble tool pool (resolveAgentTools)
    AT->>AT: Create worktree (if isolation: 'worktree')

    alt Async (run_in_background)
        AT->>AT: registerAsyncAgent()
        AT-->>P: Return task ID immediately
    end

    AT->>RA: runAgent({ agentDefinition, promptMessages, ... })
    RA->>RA: Resolve model (getAgentModel)
    RA->>RA: Build system prompt (getAgentSystemPrompt)
    RA->>RA: Initialize MCP servers
    RA->>RA: Preload skills
    RA->>RA: Register hooks
    RA->>RA: Create sub-agent ToolUseContext

    RA->>Q: query({ messages, systemPrompt, tools, ... })
    loop Nested agentic loop
        Q->>API: Send messages
        API-->>Q: Assistant response (text + tool_use)
        Q->>Q: Execute tool calls
        Q->>Q: Append tool results
    end
    Q-->>RA: Final message

    RA->>RA: Cleanup (MCP, hooks, caches, Perfetto)
    RA-->>AT: Yield messages
    AT-->>P: Return result text

When AgentTool.call() receives a request, the first decision is fork vs. named agent:

// src/tools/AgentTool/AgentTool.tsx — agent selection logic
const effectiveType = subagent_type ??
(isForkSubagentEnabled() ? undefined : GENERAL_PURPOSE_AGENT.agentType)
const isForkPath = effectiveType === undefined
if (isForkPath) {
if (isInForkChild(toolUseContext.messages)) {
throw new Error('Fork is not available inside a forked worker')
}
selectedAgent = FORK_AGENT
} else {
const agents = filterDeniedAgents(allAgents, ...)
const found = agents.find(a => a.agentType === effectiveType)
if (!found) throw new Error(...)
selectedAgent = found
}

Three outcomes:

  1. subagent_type provided → look up from activeAgents list (built-in, custom, or plugin)
  2. subagent_type omitted + fork enabled → use synthetic FORK_AGENT definition
  3. subagent_type omitted + fork disabled → fall back to GENERAL_PURPOSE_AGENT

The system prompt path diverges between fork and normal agents:

// src/tools/AgentTool/runAgent.ts — system prompt resolution
const agentSystemPrompt = override?.systemPrompt
? override.systemPrompt // Fork path: parent's rendered prompt
: asSystemPrompt(
await getAgentSystemPrompt( // Normal path: agent's own prompt
agentDefinition,
toolUseContext,
resolvedAgentModel,
additionalWorkingDirectories,
resolvedTools,
),
)

For fork agents, the parent’s already-rendered system prompt bytes are passed via override.systemPrompt. This is critical for prompt cache sharing — re-calling getSystemPrompt() could produce different bytes (e.g., due to GrowthBook state changes).

For normal agents, getAgentSystemPrompt() calls the agent definition’s getSystemPrompt() function. Built-in agents receive the full ToolUseContext for dynamic prompt generation; custom agents return a static markdown body, optionally appended with agent memory.

src/tools/AgentTool/runAgent.ts
const resolvedAgentModel = getAgentModel(
agentDefinition.model, // Agent-level preference ('inherit', 'haiku', specific ID)
toolUseContext.options.mainLoopModel, // Parent's model
model, // Caller override from Agent() params
permissionMode,
)

Resolution priority:

  1. Explicit model parameter from the Agent tool call (e.g., model: 'opus')
  2. Agent definition’s model field
  3. 'inherit' → use parent’s mainLoopModel
  4. Default sub-agent model from configuration
// src/tools/AgentTool/agentToolUtils.ts — resolveAgentTools
const resolvedTools = useExactTools
? availableTools // Fork: identical tool set for cache hits
: resolveAgentTools(agentDefinition, availableTools, isAsync).resolvedTools

The tool resolution pipeline:

  1. Global filter (filterToolsForAgent): removes tools blocked for all agents (ALL_AGENT_DISALLOWED_TOOLS), applies custom-agent restrictions, and async-agent allowed list
  2. Disallowed tools: removes tools in the agent’s disallowedTools list
  3. Allowed tools: if tools is defined (not ['*']), intersect with the allowed list
  4. MCP tools: always pass through (mcp__* prefix)

The core execution happens inside runAgent(), which calls query() — the same agentic loop used by the main agent:

// src/tools/AgentTool/runAgent.ts — the nested agentic loop
for await (const message of query({
messages: initialMessages,
systemPrompt: agentSystemPrompt,
userContext: resolvedUserContext,
systemContext: resolvedSystemContext,
canUseTool,
toolUseContext: agentToolUseContext,
querySource,
maxTurns: maxTurns ?? agentDefinition.maxTurns,
})) {
// Forward API metrics to parent
// Handle max_turns signal
// Yield messages back to caller
}

The sub-agent runs its own independent agentic loop — it can call tools, receive results, and make further API calls, just like the main agent. The maxTurns parameter bounds the loop depth.

flowchart TD
    S[Spawn] --> I[Init]
    I --> C[Context Injection]
    C --> L[Nested Query Loop]
    L --> R[Result Extraction]
    R --> CL[Cleanup]

    subgraph Init
        I1[Resolve model] --> I2[Build system prompt]
        I2 --> I3[Assemble tool pool]
        I3 --> I4[Initialize MCP servers]
        I4 --> I5[Preload skills]
        I5 --> I6[Register hooks]
    end

    subgraph Context Injection
        C1[Create sub-agent ToolUseContext] --> C2[Override permission mode]
        C2 --> C3[Set up agent-scoped AppState]
        C3 --> C4[Initialize ReadFileState cache]
    end

    subgraph Nested Query Loop
        L1[Send to API] --> L2[Receive response]
        L2 --> L3[Execute tool calls]
        L3 --> L4[Append results]
        L4 -->|more turns| L1
        L4 -->|done or max_turns| L5[Exit loop]
    end

    subgraph Cleanup
        CL1[Shutdown MCP servers] --> CL2[Unregister hooks]
        CL2 --> CL3[Clear prompt cache tracking]
        CL3 --> CL4[Release file state cache]
        CL4 --> CL5[Unregister Perfetto trace]
        CL5 --> CL6[Kill background bash tasks]
        CL6 --> CL7[Cleanup worktree if needed]
    end

The parent agent issues an Agent() tool call. AgentTool.call() resolves the agent definition and decides between sync and async execution:

src/tools/AgentTool/AgentTool.tsx
if (shouldRunAsync) {
const agentBackgroundTask = registerAsyncAgent({
agentId: asyncAgentId,
description,
prompt,
selectedAgent,
setAppState: rootSetAppState,
})
// Background execution — parent continues immediately
} else {
// Synchronous — parent blocks until sub-agent completes
for await (const message of runAgent(runAgentParams)) {
yield message
}
}

Inside runAgent(), initialization follows a strict order:

  1. Model resolutiongetAgentModel() resolves the final model ID
  2. System promptgetAgentSystemPrompt() or parent override
  3. Tool poolresolveAgentTools() applies filtering
  4. MCP serversinitializeAgentMcpServers() starts agent-specific MCP servers, merges their tools
  5. Skills — preloads skill commands from the agent’s skills frontmatter
  6. Hooks — registers agent-scoped session hooks from frontmatter

The sub-agent gets its own ToolUseContext via createSubagentContext():

src/tools/AgentTool/runAgent.ts
const agentToolUseContext = createSubagentContext(toolUseContext, {
options: agentOptions,
agentId,
agentType: agentDefinition.agentType,
messages: initialMessages,
readFileState: agentReadFileState, // Independent file cache
abortController: agentAbortController, // Independent abort
getAppState: agentGetAppState, // Permission mode override
shareSetAppState: !isAsync,
contentReplacementState,
})

Key isolation properties:

  • Independent AbortController — aborting the sub-agent doesn’t abort the parent
  • Separate ReadFileState — file read caching is scoped to the sub-agent
  • Overridden getAppState — permission mode, effort level, and allowed tools can differ

The finally block in runAgent() ensures deterministic resource release:

// src/tools/AgentTool/runAgent.ts — cleanup (finally block)
finally {
await mcpCleanup() // Shutdown agent-specific MCP servers
cleanupSessionHooks() // Unregister agent hooks
cleanupPromptCacheTracking() // Release cache tracking
releaseReadFileState(agentReadFileState) // Free file cache memory
unregisterPerfettoAgent(agentId) // Remove trace entry
killBackgroundBashTasks(agentId) // Stop lingering bash processes
}

Sub-agents can operate under different permission modes, controlling how tool execution is authorized. The mode is set via the agent definition’s permissionMode field.

ModeBehaviorUser Prompt?LoggingTypical Use
defaultNormal permission checking✅ YesFullInteractive sub-agents
bypassPermissionsSkip all permission checks❌ NoMinimalTrusted custom agents (e.g., code-reviewer)
dontAskAuto-allow but still log❌ NoFullBuilt-in agents like Claude Code Guide
bubbleSurface prompts to parent’s terminal✅ Yes (parent terminal)FullFork children

The permission mode is injected into the sub-agent’s AppState via a custom getAppState wrapper:

// src/tools/AgentTool/runAgent.ts — permission mode override
const agentGetAppState = () => {
const parentState = toolUseContext.getAppState()
return {
...parentState,
toolPermissionContext: {
...parentState.toolPermissionContext,
mode: agentDefinition.permissionMode ?? parentState.toolPermissionContext.mode,
},
shouldAvoidPermissionPrompts: isAsync, // Async agents can't show prompts
}
}

The bubble mode is unique to fork children. When a fork child needs permission to run a tool (e.g., Bash), the permission prompt is surfaced to the parent agent’s terminal rather than being shown in the child’s context. This works because fork children share the parent’s terminal session.

// src/tools/AgentTool/forkSubagent.ts — FORK_AGENT definition
export const FORK_AGENT = {
// ...
permissionMode: 'bubble', // Permission prompts surface to parent terminal
tools: ['*'], // Full tool access (filtered at runtime)
}
ScenarioRecommended ModeReasoning
Explore/Plan (read-only)default + disallowedToolsRestrict via tool list, not permission bypass
Trusted CI/CD agentbypassPermissionsNon-interactive, pre-approved operations
Documentation lookupdontAskLow risk, but maintain audit trail
Fork child with user nearbybubbleUser can approve in real-time via parent terminal
Background researchdefault + asyncAuto-skip risky tools when user unavailable

Claude Code has four distinct patterns for spawning child agents. Understanding when to use each is critical:

AspectNamed Sub-AgentFork ChildCoordinator WorkerTeam Member
TriggerAgent(subagent_type='X')Agent() (no type, fork enabled)Coordinator spawns via AgentTeamCreate + Agent(team_name)
ContextFresh — agent’s own system promptFull parent history + parent system promptFresh — worker system promptFresh — teammate prompt
Tool SetAgent definition’s tools/disallowedToolsParent’s full tool pool (for cache)ASYNC_AGENT_ALLOWED_TOOLSPer-agent definition
ModelAgent definition or inheritedAlways inherited (for cache)Inherited or specifiedPer-member config
Permission ModeAgent definition (default: acceptEdits)bubble (surface to parent)Worker-scopedPer-member (often plan)
IsolationOptional worktreeOptional worktreeNone (shared process)Optional worktree per member
LifecycleEphemeral — one query, return, cleanupEphemeral — same as namedEphemeral per task, can be continued via SendMessagePersistent — survives across tasks
ConcurrencySequential or backgroundAlways background (parallel)Parallel by designParallel, independent processes
Anti-recursionDepth limit via maxTurns<fork-boilerplate> tag detectionCoordinator owns all dispatchTeam structure prevents loops
Use CaseFocused tasks (explore, plan, verify)Parallel research with full contextMulti-phase workflows (research → implement → verify)Long-running collaborative projects
graph TD
    subgraph Named Sub-Agent
        P1[Parent] -->|"Agent(subagent_type='Explore')"| C1[Explore Agent]
        C1 -.->|result| P1
    end

    subgraph Fork Child
        P2[Parent] -->|"Agent(prompt='...')"| F1[Fork 1]
        P2 -->|"Agent(prompt='...')"| F2[Fork 2]
        F1 -.->|result| P2
        F2 -.->|result| P2
    end

    subgraph Coordinator Worker
        CO[Coordinator] -->|Agent| W1[Worker]
        CO -->|SendMessage| W1
        W1 -.->|task-notification| CO
    end

    subgraph Team Member
        L[Leader] -->|TeamCreate + Agent| T1[Member 1]
        L -->|Agent| T2[Member 2]
        T1 <-->|SendMessage| T2
        T1 -.->|idle notification| L
    end

The agent system is implemented across several tightly coupled modules in src/tools/AgentTool/:

flowchart TD
    AT["AgentTool.tsx<br/>(Tool definition + call entry)"]
    RA["runAgent.ts<br/>(Core execution engine)"]
    LA["loadAgentsDir.ts<br/>(Agent loading + parsing)"]
    BA["builtInAgents.ts<br/>(Built-in registry)"]
    FS["forkSubagent.ts<br/>(Fork path logic)"]
    PR["prompt.ts<br/>(Prompt generation)"]
    UT["agentToolUtils.ts<br/>(Tool resolution + filtering)"]
    CO["constants.ts<br/>(Names + tool sets)"]

    AT -->|"calls"| RA
    AT -->|"resolves agents"| LA
    AT -->|"checks fork"| FS
    AT -->|"assembles tools"| UT

    RA -->|"builds prompt"| PR
    RA -->|"initializes MCP"| MCP["MCP server init"]
    RA -->|"runs"| QL["query() loop"]

    LA -->|"loads"| BA
    LA -->|"parses"| MD["Markdown frontmatter"]
    LA -->|"loads"| PL["Plugin agents"]

    FS -->|"provides"| FA["FORK_AGENT definition"]
    FS -->|"builds"| FM["Forked messages"]

    BA -->|"registers"| BI["Built-in agents:<br/>General Purpose<br/>Explore, Plan<br/>Verification<br/>Claude Code Guide<br/>Statusline Setup"]

    UT -->|"uses"| CO

    PR -->|"formats"| AL["Agent list for parent prompt"]

    style AT fill:#f9f,stroke:#333,stroke-width:2px
    style RA fill:#bbf,stroke:#333,stroke-width:2px
    style LA fill:#bfb,stroke:#333
    style FS fill:#fbf,stroke:#333

AgentTool.tsx — The tool definition registered in Claude Code’s tool pool. Handles parameter validation (inputSchema), agent selection (fork vs. named), worktree creation, and the sync/async execution decision. This is the entry point for all sub-agent invocations.

runAgent.ts — The core execution engine. An async generator that initializes the sub-agent environment (MCP servers, skills, hooks), creates the sub-agent’s ToolUseContext, runs the nested query() loop, and handles cleanup in a finally block. All resource lifecycle management lives here.

loadAgentsDir.ts — Agent definition loading and resolution. Exports getAgentDefinitionsWithOverrides() (memoized) which merges built-in, plugin, and custom agents, applying the priority chain. Also contains parseAgentFromMarkdown() for parsing .md agent files with Zod-validated frontmatter.

builtInAgents.ts — Registry for the six built-in agents. Handles feature-flag gating (Explore/Plan, Verification, Claude Code Guide) and the coordinator mode switch that replaces the entire agent set.

forkSubagent.ts — Fork-specific logic: the FORK_AGENT synthetic definition, buildForkedMessages() for cache-optimal message construction, buildChildMessage() for the fork protocol, isInForkChild() anti-recursion guard, and buildWorktreeNotice() for worktree path translation.

prompt.ts — Generates the Agent tool’s description prompt shown to the parent agent. Formats the available agent list, fork usage examples, and usage guidelines. This is what the parent sees when deciding which sub-agent to spawn.

agentToolUtils.ts — Tool resolution and filtering. resolveAgentTools() applies the three-layer filter (global blacklist → agent blacklist → agent allowlist). filterToolsForAgent() handles async-agent restrictions and custom-agent sandboxing.

src/tools/AgentTool/AgentTool.tsx
// 1. Parent decides to spawn → AgentTool.call()
Agent({ prompt: "Find all usages of X", subagent_type: "Explore" })
// 2. Resolve definition → loadAgentsDir.ts
const { activeAgents } = await getAgentDefinitionsWithOverrides(cwd)
const selected = activeAgents.find(a => a.agentType === 'Explore')
// 3. Assemble tools → agentToolUtils.ts
const { resolvedTools } = resolveAgentTools(selected, availableTools, isAsync)
// 4. Execute → runAgent.ts
for await (const msg of runAgent({
agentDefinition: selected,
promptMessages: [userMessage],
toolUseContext,
availableTools: resolvedTools,
model: resolvedModel,
})) {
// 5. Messages flow back to parent
}