Agent Lifecycle
- Entity ID:
ent-20260410-e3e9d1cfc16d - Type:
concept - Scope:
shared - Status:
active - Aliases: agent lifecycle, agent spawning, sub-agent execution
Description
The spawn-to-completion lifecycle of Claude Code agents — from how the parent decides to create a sub-agent, through isolated execution, to result delivery and cleanup. The lifecycle is designed around three concerns: prompt cache economics (reuse the parent's cached prefix), concurrent isolation (multiple agents in one process via AsyncLocalStorage), and minimal token overhead (drop CLAUDE.md and gitStatus for read-only agents).
The agent loop
The core message loop lives in src/query.ts (line 219). Each turn:
- Format system message — assemble system prompt, optionally omitting CLAUDE.md/gitStatus per agent flags
- Call Anthropic API — stream the response with current message prefix
- Parse tool calls — extract
tool_useblocks from the response - Execute tools — route through permission check (
canUseTool) thentool.call() - Yield results — append
tool_resultcontent blocks as user messages - Repeat — until the assistant produces a terminal message (no tool calls) or
maxTurnsis reached
The loop is an async generator that yields Message | StreamEvent | RequestStartEvent, allowing the caller to process events as they stream.
QueryEngine
src/QueryEngine.ts (line 184) wraps the query loop with session-level state:
- Maintains the full message history, usage tracking, and file cache
- Builds the system prompt by merging default + custom + memory mechanics
- Handles slash command parsing and attachment processing
- Injects coordinator capabilities when in coordinator mode
- Tracks permission denials across turns
submitMessage() (line 209) is the main entry: accepts a string or ContentBlockParam[] prompt and returns an AsyncGenerator<SDKMessage>.
How agents are spawned
AgentTool
src/tools/AgentTool/AgentTool.tsx (lines 239-765) is the primary spawn mechanism. Input parameters:
| Parameter | Purpose |
|---|---|
prompt |
Task description for the agent |
description |
3-5 word label |
subagent_type |
Agent type (Explore, Plan, general-purpose, etc.) |
model |
Optional model override (sonnet/opus/haiku) |
run_in_background |
Async spawn — returns immediately |
name |
Addressable name for SendMessage routing |
isolation |
worktree for filesystem isolation |
mode |
Permission mode override |
Agent resolution (lines 318-356): explicit subagent_type wins over defaults. A fork experiment gate can route untyped spawns to the Fork subagent path. Recursive fork spawning is guarded against.
Two execution paths
Synchronous (lines 765+): Agent runs inline within the parent's turn. Blocks parent completion. Registered as a foreground task that can be auto-backgrounded after 2 seconds.
Asynchronous (lines 686-764): Agent runs in a detached closure. Returns immediately with agentId and outputFile path. The parent continues working while the agent executes. Name registration enables SendMessage() routing.
Both paths wrap execution in runWithAgentContext() for AsyncLocalStorage isolation.
Agent execution
runAgent
src/tools/AgentTool/runAgent.ts (line 248) orchestrates the full agent lifecycle:
Setup phase (lines 700-730):
1. Create isolated ToolUseContext via createSubagentContext()
2. Register frontmatter hooks from agent definition
3. Preload skills from agent definition
4. Initialize agent-specific MCP servers
Query loop (lines 748-806):
for each message from query({...}):
record to sidechain transcript
yield message to caller
Cleanup (lines 816-859): - Clear MCP connections - Clear session hooks - Release prompt cache tracking - Release file state cache - Unregister perfetto tracing - Terminate agent's bash tasks
Token-saving optimizations
-
Omit CLAUDE.md (runAgent.ts:390-398): Read-only agents (Explore, Plan) drop CLAUDE.md rules. Gate:
tengu_slim_subagent_claudemd. Saves ~5-15 GTok/week across 34M+ spawns fleet-wide. -
Drop gitStatus (runAgent.ts:404-410): Explore/Plan agents skip the parent's gitStatus since they'll run
git statusthemselves if needed. Saves ~1-3 GTok/week. -
Cache-identical fork (AgentTool.tsx:622, runAgent.ts:500): Fork agents inherit the parent's exact system prompt and tool array, making the API prefix byte-identical. This means the child agent gets prompt cache hits from the parent's cached prefix — significant cost savings.
Fork path
Fork agents (runAgent.ts:495-512) take a special path:
- Inherit parent's system prompt verbatim
- Messages built via buildForkedMessages(): clone parent assistant + placeholders + directive
- useExactTools: true ensures identical tool registration
- Result: the child's API request shares the parent's prompt cache
Context isolation
AsyncLocalStorage
src/utils/agentContext.ts — when agents run in the background (ctrl+b or run_in_background: true), multiple agents execute concurrently in the same process. AsyncLocalStorage gives each async execution chain its own isolated context:
const agentContextStorage = new AsyncLocalStorage<AgentContext>()
export function runWithAgentContext<T>(context: AgentContext, fn: () => T): T {
return agentContextStorage.run(context, fn)
}
Each agent's context carries: agentId, parentSessionId, agentType, subagentName, invokingRequestId, and invocationKind (spawn vs resume).
Subagent context creation
src/utils/forkedAgent.ts (line 345) — createSubagentContext() creates an isolated ToolUseContext:
Isolated by default:
- readFileState — cloned from parent
- abortController — new linked controller (parent abort propagates down)
- getAppState — wrapped to set shouldAvoidPermissionPrompts
- setAppState — no-op (agents can't modify parent state)
- Fresh collections for memory triggers, skill names, tool decisions
Explicitly shared (opt-in via flags):
- shareSetAppState — for interactive agents that need to update parent state
- shareAbortController — for coordinated cancellation
- Root setAppStateForTasks always goes through — agents must be able to register/kill tasks
Async agent lifecycle
src/tools/AgentTool/agentToolUtils.ts (line 508) — runAsyncAgentLifecycle():
- Setup: Create progress tracker, optionally start background summarization
- Message loop: Stream messages, update progress, append to task if UI is holding it
- Completion: Extract final text, get worktree result, transition task status to
completed, enqueue notification - Error handling: AbortError (user kill) → extract partial result, enqueue "killed" notification. Other errors → transition to "failed"
- Cleanup (always): Release skills, dump prompts, unregister tracing
Task registration
src/tasks/LocalAgentTask/LocalAgentTask.tsx (line 466) — async agents are registered in AppState.tasks[agentId] with state including: status (running/completed/failed/killed), abort controller, progress, result, messages (streamed transcript), and eviction timing.
Completion notifications are XML-formatted and enqueued to the coordinator:
<task-notification>
<task-id>{agentId}</task-id>
<status>completed|failed|killed</status>
<result>{final text}</result>
<usage><total_tokens>N</total_tokens></usage>
</task-notification>
Coordinator mode
src/coordinator/coordinatorMode.ts — when active, the parent agent's system prompt is replaced with coordinator-specific guidance that documents worker orchestration patterns, the task-notification XML format, and parallel execution strategies. The coordinator manages multiple workers and synthesizes their results.
What depends on it
- Tool system — tools execute within agent context, respecting agent-specific permissions
- Auto-compact — each agent manages its own compaction state
- MCP — agents can have their own MCP server connections, cleaned up on exit
- Prompt cache — fork agents are specifically designed to share the parent's cache
- Session persistence — agent transcripts are recorded to sidechain files
Design trade-offs
| Decision | Trade-off |
|---|---|
| AsyncLocalStorage over separate processes | Cheaper (no process spawn), but agents share memory — a crash in one can affect others |
| Omitting CLAUDE.md for read-only agents | Saves tokens but means Explore/Plan agents don't follow project-specific rules |
| Fork path (cache-identical prefix) | Huge cache savings, but tightly couples child to parent's exact prompt structure |
| No-op setAppState for subagents | Clean isolation, but agents can't signal state changes to parent except through messages |
| XML task notifications | Human-readable and parseable, but verbose compared to structured JSON |
| Auto-background after 120s | Prevents sync agents from blocking the parent forever, but may surprise users |
Key claims
- omitClaudeMd optimization saves 5-15 GTok/week fleet-wide
- gitStatus drop for read-only agents saves 1-3 GTok/week
- Fork path achieves prompt cache hits by making child requests byte-identical to parent prefix
- AsyncLocalStorage isolates concurrent agents without process overhead
Relations
rel-20260410-9efe5889: claude-code --[implements]--> agent-lifecyclerel-lifecycle-query: QueryEngine --[runs]--> query looprel-lifecycle-compact: agent-lifecycle --[triggers]--> auto-compact (per agent)rel-lifecycle-cache: fork path --[optimizes]--> prompt cache economics
Sources
src-20260409-711253502f5d, source code at src/QueryEngine.ts, src/query.ts, src/tools/AgentTool/, src/utils/agentContext.ts, src/utils/forkedAgent.ts, src/coordinator/