Agent Lifecycle

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:

  1. Format system message — assemble system prompt, optionally omitting CLAUDE.md/gitStatus per agent flags
  2. Call Anthropic API — stream the response with current message prefix
  3. Parse tool calls — extract tool_use blocks from the response
  4. Execute tools — route through permission check (canUseTool) then tool.call()
  5. Yield results — append tool_result content blocks as user messages
  6. Repeat — until the assistant produces a terminal message (no tool calls) or maxTurns is 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:

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

  1. 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.

  2. Drop gitStatus (runAgent.ts:404-410): Explore/Plan agents skip the parent's gitStatus since they'll run git status themselves if needed. Saves ~1-3 GTok/week.

  3. 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():

  1. Setup: Create progress tracker, optionally start background summarization
  2. Message loop: Stream messages, update progress, append to task if UI is holding it
  3. Completion: Extract final text, get worktree result, transition task status to completed, enqueue notification
  4. Error handling: AbortError (user kill) → extract partial result, enqueue "killed" notification. Other errors → transition to "failed"
  5. 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

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

Relations

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/