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.
AgentTool Complete Call Flow
Section titled “AgentTool Complete Call Flow”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
Step 1: Agent Definition Resolution
Section titled “Step 1: Agent Definition Resolution”When AgentTool.call() receives a request, the first decision is fork vs. named agent:
// src/tools/AgentTool/AgentTool.tsx — agent selection logicconst 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:
subagent_typeprovided → look up fromactiveAgentslist (built-in, custom, or plugin)subagent_typeomitted + fork enabled → use syntheticFORK_AGENTdefinitionsubagent_typeomitted + fork disabled → fall back toGENERAL_PURPOSE_AGENT
Step 2: System Prompt Construction
Section titled “Step 2: System Prompt Construction”The system prompt path diverges between fork and normal agents:
// src/tools/AgentTool/runAgent.ts — system prompt resolutionconst 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.
Step 3: Model Selection
Section titled “Step 3: Model Selection”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:
- Explicit
modelparameter from the Agent tool call (e.g.,model: 'opus') - Agent definition’s
modelfield 'inherit'→ use parent’smainLoopModel- Default sub-agent model from configuration
Step 4: Tool Pool Assembly
Section titled “Step 4: Tool Pool Assembly”// src/tools/AgentTool/agentToolUtils.ts — resolveAgentToolsconst resolvedTools = useExactTools ? availableTools // Fork: identical tool set for cache hits : resolveAgentTools(agentDefinition, availableTools, isAsync).resolvedToolsThe tool resolution pipeline:
- Global filter (
filterToolsForAgent): removes tools blocked for all agents (ALL_AGENT_DISALLOWED_TOOLS), applies custom-agent restrictions, and async-agent allowed list - Disallowed tools: removes tools in the agent’s
disallowedToolslist - Allowed tools: if
toolsis defined (not['*']), intersect with the allowed list - MCP tools: always pass through (
mcp__*prefix)
Step 5: Nested Query Loop
Section titled “Step 5: Nested Query Loop”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 loopfor 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.
Sub-Agent Lifecycle
Section titled “Sub-Agent Lifecycle”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
Spawn Phase
Section titled “Spawn Phase”The parent agent issues an Agent() tool call. AgentTool.call() resolves the agent definition and decides between sync and async execution:
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 }}Init Phase
Section titled “Init Phase”Inside runAgent(), initialization follows a strict order:
- Model resolution —
getAgentModel()resolves the final model ID - System prompt —
getAgentSystemPrompt()or parent override - Tool pool —
resolveAgentTools()applies filtering - MCP servers —
initializeAgentMcpServers()starts agent-specific MCP servers, merges their tools - Skills — preloads skill commands from the agent’s
skillsfrontmatter - Hooks — registers agent-scoped session hooks from frontmatter
Context Injection
Section titled “Context Injection”The sub-agent gets its own ToolUseContext via createSubagentContext():
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
Cleanup Phase
Section titled “Cleanup Phase”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}Permission Bubbling Mechanism
Section titled “Permission Bubbling Mechanism”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.
Four Permission Modes
Section titled “Four Permission Modes”| Mode | Behavior | User Prompt? | Logging | Typical Use |
|---|---|---|---|---|
default | Normal permission checking | ✅ Yes | Full | Interactive sub-agents |
bypassPermissions | Skip all permission checks | ❌ No | Minimal | Trusted custom agents (e.g., code-reviewer) |
dontAsk | Auto-allow but still log | ❌ No | Full | Built-in agents like Claude Code Guide |
bubble | Surface prompts to parent’s terminal | ✅ Yes (parent terminal) | Full | Fork children |
How Permission Mode is Applied
Section titled “How Permission Mode is Applied”The permission mode is injected into the sub-agent’s AppState via a custom getAppState wrapper:
// src/tools/AgentTool/runAgent.ts — permission mode overrideconst 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
Section titled “The bubble Mode”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 definitionexport const FORK_AGENT = { // ... permissionMode: 'bubble', // Permission prompts surface to parent terminal tools: ['*'], // Full tool access (filtered at runtime)}Permission Mode Selection Guide
Section titled “Permission Mode Selection Guide”| Scenario | Recommended Mode | Reasoning |
|---|---|---|
| Explore/Plan (read-only) | default + disallowedTools | Restrict via tool list, not permission bypass |
| Trusted CI/CD agent | bypassPermissions | Non-interactive, pre-approved operations |
| Documentation lookup | dontAsk | Low risk, but maintain audit trail |
| Fork child with user nearby | bubble | User can approve in real-time via parent terminal |
| Background research | default + async | Auto-skip risky tools when user unavailable |
Four Agent Type Comparison
Section titled “Four Agent Type Comparison”Claude Code has four distinct patterns for spawning child agents. Understanding when to use each is critical:
| Aspect | Named Sub-Agent | Fork Child | Coordinator Worker | Team Member |
|---|---|---|---|---|
| Trigger | Agent(subagent_type='X') | Agent() (no type, fork enabled) | Coordinator spawns via Agent | TeamCreate + Agent(team_name) |
| Context | Fresh — agent’s own system prompt | Full parent history + parent system prompt | Fresh — worker system prompt | Fresh — teammate prompt |
| Tool Set | Agent definition’s tools/disallowedTools | Parent’s full tool pool (for cache) | ASYNC_AGENT_ALLOWED_TOOLS | Per-agent definition |
| Model | Agent definition or inherited | Always inherited (for cache) | Inherited or specified | Per-member config |
| Permission Mode | Agent definition (default: acceptEdits) | bubble (surface to parent) | Worker-scoped | Per-member (often plan) |
| Isolation | Optional worktree | Optional worktree | None (shared process) | Optional worktree per member |
| Lifecycle | Ephemeral — one query, return, cleanup | Ephemeral — same as named | Ephemeral per task, can be continued via SendMessage | Persistent — survives across tasks |
| Concurrency | Sequential or background | Always background (parallel) | Parallel by design | Parallel, independent processes |
| Anti-recursion | Depth limit via maxTurns | <fork-boilerplate> tag detection | Coordinator owns all dispatch | Team structure prevents loops |
| Use Case | Focused tasks (explore, plan, verify) | Parallel research with full context | Multi-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
AgentTool Internal Module Architecture
Section titled “AgentTool Internal Module Architecture”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
Module Responsibilities
Section titled “Module Responsibilities”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.
Key Data Flow
Section titled “Key Data Flow”// 1. Parent decides to spawn → AgentTool.call()Agent({ prompt: "Find all usages of X", subagent_type: "Explore" })
// 2. Resolve definition → loadAgentsDir.tsconst { activeAgents } = await getAgentDefinitionsWithOverrides(cwd)const selected = activeAgents.find(a => a.agentType === 'Explore')
// 3. Assemble tools → agentToolUtils.tsconst { resolvedTools } = resolveAgentTools(selected, availableTools, isAsync)
// 4. Execute → runAgent.tsfor await (const msg of runAgent({ agentDefinition: selected, promptMessages: [userMessage], toolUseContext, availableTools: resolvedTools, model: resolvedModel,})) { // 5. Messages flow back to parent}