Claude Code 源码分析第 50 课 · 第 01
第 50 课

架构总览

把前面所有子系统重新拼回一张完整地图:从启动、状态、查询、工具、服务,到最终 UI 渲染的全链路视角。

01 概览

前面几十课分别拆开讲了启动、系统提示词、工具、MCP、技能、状态与 UI。这一课要做相反的事:把这些局部机制重新接回一张整体图,让你能从一次用户输入出发,顺着代码把整套系统跑通。

本课程涵盖的源文件
main.tsxsetup.tsQueryEngine.tsquery.tstools.tsTool.tsbootstrap/state.tsstate/AppStateStore.tsreplLauncher.tsxscreens/REPL.tsxservices/api/services/mcp/

你可以把 Claude Code 看成六层串接的系统:启动层决定会话如何建立,UI 层承接交互,状态层保存长期上下文,查询层驱动与模型的往返,工具层执行副作用,服务层负责 API / MCP / 压缩等外部连接。

Boot main.tsx, setup.ts, entrypoints/init.ts — 进程启动、设置、迁移、会话连接
用户界面外壳 replLauncher.tsx, screens/REPL.tsx, components/App.tsx — 墨迹渲染的交互式终端界面
State state/AppStateStore.ts, bootstrap/state.ts, state/store.ts — 不可变的 AppState + 全局单例状态
查询引擎 QueryEngine.ts, query.ts — 对话生命周期、系统提示组装、API 流循环
Tools tools.ts, Tool.ts, tools/*/ — 功能注册表:Bash、文件 I/O、代理、搜索、MCP、技能
Services services/api/, services/mcp/, services/compact/ — Anthropic API 客户端、MCP 连接、压缩
02 完整架构图

该 ASCII 图映射了每个主要组件及其与其他组件的关系。 自上而下地阅读:用户的击键向下流过每一层,直到到达 API,然后响应通过工具和 UI 向上冒泡。

╔════════════════════ ═════════════════════ ═════════════════════ ════════════════════╗ ║ 克劳德代码 — 完整架构 ║ ╚════════════════════ ═════════════════════ ═════════════════════ ════════════════════╝ ┌──────────────────────────────────────────────────────────┐ │ 进程入口 (entrypoints/cli.tsx) │ │ │ │ $ claude [flags] [prompt] │ │ │ │ │ ├─ 快速路径:--version、--daemon-worker、bridge → exit 早期 │ │ └─ 动态导入 main.tsx → main() │ └──────────────────────────────┬──────────────────────────────────────────┘ │ ┌────────────────────────────────▼──────────────────────────────────────────┐ │ MAIN.TSX — Commander CLI 接线 + 编排 │ │ │ │ 顶级副作用(导入完成前): │ │ profileCheckpoint('main_tsx_entry') │ │ startMdmRawRead() ← MDM 策略读取(并行子进程) │ │ startKeychainPrefetch() ← macOS 钥匙串读取(并行) │ │ │ │ init() ──────────────────────────────────────────────────────────► │ │ applySafeConfigEnvironmentVariables() applyExtraCACertsFromConfig() │ │ setupGracefulShutdown() EnsureMdmSettingsLoaded() │ │initializeRemoteManagedSettingsLoadingPromise() │ │initializePolicyLimitsLoadingPromise() │ │ preconnectAnthropicApi() ← 早期 TCP 连接预热 │ │ │ │ runMigrations() ── 架构升级,模型字符串迁移 (v11) │ │ Commander.parse() ── 解析:cwd、permissionMode、模型、会话标志 │ └──────────────────────────────┬──────────────────────────────────────────┘ │ ┌──────────────────────────────▼────────────────────────────────────────┐ │ SETUP.TS — 会话初始化 │ │ │ │ setCwd(cwd) │ │ captureHooksConfigSnapshot()initializeFileChangedWatcher() │ │ startUdsMessaging() captureTeammateModeSnapshot() [群] │ │ createWorktreeForSession() [--worktree 标志] │ │ createTmuxSessionForWorktree() [--tmux 标志] │ │ initSessionMemory() initContextCollapse() [功能门] │ │ lockCurrentVersion() │ │ getCommands() 预取 loadPluginHooks() processSessionStartHooks() │ └──────────────────────────┬──────────────────────────────────────────┘ │ ┌──────────────────────┴──────────────────────┐ │ 交互路径 │ 无头路径 (--print/-p) ▼ ▼ ┌───────────────────┐ ┌──────────────────┐ │ replLauncher.tsx │ │ print.ts / SDK │ │ launchRepl() │ │ runHeadless() │ │ import App.tsx │ │ QueryEngine │ │ import REPL.tsx │ │ .submitMessage() │ │ renderAndRun() │ └──────────┬──────────┘ └────────┬──────────┘ │ │ │ ┌────────▼──────────────────────────────────────────────────────────────────┐ │ INK TERMINAL UI (react + ink) │ │ │ │ Components/App.tsx ── AppState 提供者,上下文注入 │ │ Screens/REPL.tsx ── 主交互循环 │ │ ├── Components/PromptInput/ ← 用户在此处输入 │ │ ├── Components/messages/ ← 呈现的辅助消息 │ │ ├── Components/permissions/ ← 工具权限对话框 │ │ ├── Components/Tasks/ ← 任务列表面板 │ │ ├── Components/mcp/ ← MCP status │ │ └── Screens/Doctor.tsx ← /doctor 命令输出 │ │ │ │ state/AppStateStore.ts ── DeepImmutable │ │ { settings, mainLoopModel, toolPermissionContext, messages, │ │推测, mcpClients, agentDefinitions, fileHistory, ... } │ └────────┬──────────────────────────────────────────────────────────────────┘ │ 用户提交提示 ▼ ┌──────────────────────────────────────────────────────────────────────┐ │ 查询引擎 (QueryEngine.ts + query.ts) │ │ │ │ QueryEngine.submitMessage(prompt) ←── 每个会话一个实例 │ │ │ │ │ ├─ processUserInput() ── /slash 命令处理、消息准备 │ │ ├─ recordTranscript() ── 在 API 调用之前保留用户消息 │ │ ├─ fetchSystemPromptParts() ── 组装系统提示符: │ │ │ defaultSystemPrompt + userContext + systemContext │ │ │ + customSystemPrompt +appendSystemPrompt + memoryMechanics │ │ ├─ getSlashCommandToolSkills() loadAllPluginsCacheOnly() │ │ └─ yield buildSystemInitMessage() ── SDK init event │ │ │ │ query(params) ────────────────────────────────────────────────────► │ │ queryLoop() ── 异步生成器,每轮 API 迭代一次 │ │ │ │ │ ├─ buildQueryConfig() ── 快照 statsig / env / 会话状态 │ │ ├─calculateTokenWarningState() autoCompact 跟踪 │ │ ├─ deps.sendRequest() ── 调用 services/api/claude.ts │ │ │ └── 流:text_delta、tool_use、思考块 │ │ ├─executePostSamplingHooks() ── 用户定义 钩子在这里运行 │ │ ├─ StreamingToolExecutor ── 并行工具执行 │ │ │ └── runTools() ── 分派到各个工具处理程序 │ │ ├─ checkTokenBudget() ── 500k 预算延续逻辑 │ │ ├─ buildPostCompactMessages() ── 如果需要,上下文压缩 │ │ └─ 循环继续直到 stop_reason = end_turn 或 maxTurns │ └────────┬──────────────────────────────────────────────────────────────┘ │ 工具使用块 ▼ ┌────────────────────────────────────────────────────────────┐ │ 工具 SYSTEM (tools.ts → Tool.ts → tools/*/ToolName.ts) │ │ │ │ getAllBaseTools() ── 规范注册表(同步到 Statsig 缓存键) │ │ BashTool FileReadTool FileEditTool FileWriteTool │ │ GlobTool GrepTool WebFetchTool WebSearchTool │ │ AgentTool SkillTool TodoWriteTool LSPTool │ │ ListMcpResourcesTool ReadMcpResourceTool ToolSearchTool │ │ TaskCreateTool TaskUpdateTool TaskListTool TaskGetTool │ │ EnterPlanModeTool ExitPlanModeV2Tool EnterWorktreeTool ExitWorktreeTool │ │ ConfigTool AskUserQuestionTool TungstenTool BriefTool │ │ NotebookEditTool [+功能门控:SleepTool、MonitorTool、WorkflowTool] │ │ │ │ canUseTool() ── 权限门 (PermissionMode:默认/自动/旁路) │ │ getDenyRuleForTool() alwaysAllowRules ToolPermissionContext │ │ │ │ 每个工具实现: │ │ 名称、描述、inputSchema (Zod) │ │ isEnabled() → bool │ │ call(input, context) → AsyncGenerator │ │ renderToolResult() → React 组件(适用于 Ink UI) │ └────────┬────────────────────────────────────────────────────────────────┘ │ 特殊工具进一步委托 │ ├────────────────────────────────────────────────┐ │ AgentTool │ │ 生成具有受限工具集的子查询引擎 │ │ Swarm:TeamCreateTool、SendMessageTool、UDS 收件箱 │ │ │ ├────────────────────────────────────────────────┘ ├──────────────────────────────────────────────┐ │ MCP 工具(ListMcpResourcesTool、ReadMcpResourceTool)│ │ services/mcp/client.ts ── 连接到 MCP 服务器 │ │ MCPServerConnection → JSON-RPC over stdio / SSE │ └────────────────────────────────────────────────┘ │ ▼ ┌──────────────────────────────────────────────────────────────────────┐ │ ANTHROPIC API (services/api/claude.ts) │ │ │ │ sendRequest() ── 通过 @anthropic-ai/sdk 进行流式传输 SSE │ │ 请求包括:模型、系统、消息[]、工具[]、max_tokens │ │ betas:计算机使用、扩展思维、... │ │ task_budget(output_config.task_budget,如果已配置) │ │ │ │ 通过 query() 生成器返回的流事件: │ │ message_start → content_block_start → content_block_delta │ │ → │ content_block_stop → message_delta → message_stop │ │ │ │ withRetry() ── 处理 529 过载、529 配额、网络错误 │ │accumulateUsage() ── 跟踪输入/输出令牌以显示成本 │ └────────┬────────────────────────────────────────────────────┘ │ 响应流返回 ▼ ┌────────────────────────────────────────────────────────────────────┐ │ SESSION STORAGE (utils/sessionStorage.ts) │ │ │ │ recordTranscript() ── 将消息写入 ~/.claude/projects// │ │flushSessionStorage() ── 排出写入队列(JSONL 格式) │ │ cacheSessionTitle() ── 第一条消息成为会话标题 │ │ loadTranscriptFromFile() ── --resume / --Continue 路径 │ └──────┬──────────────────────────────────────────────────────┘ │ 产生消息 返回 UI ▼ ┌──────────────────────────────────────────────────────────────────────┐ │ INK RENDER (screens/REPL__.tsx → Components/) │ │ │ │ useLogMessages hook ── 将消息附加到 AppState │ │ AssistantMessage 组件 ── 渲染文本/思维/工具结果 │ │ ProgressMessage 组件 ── 流式进行中显示 │ │ PermissionRequest 组件 ── 阻止渲染,等待用户决定 │ │ ink.ts ── 瘦包装器:render()、unmount()、stdout write │ └──────────────────────────────────────────────────────────────────────┘ ═══════════════════ 横切系统 ═══════════════════════════════════ bootstrap/state.ts ── 全局 单例(非 React 状态):sessionId、cwd、originalCwd、projectRoot、totalCostUSD、modelUsage、isInteractive、mainLoopModelOverride、sdkBetas、钩子注册表、OTel 仪表、代币预算计数器 utils/hooks/ ── 生命周期钩子(settings.json):PreToolUse、PostToolUse、PreCompact、PostCompact、Notification、Stop、FileChanged、SessionStart services/analytics/ ── Statsig + GrowthBook 功能标志 logEvent() → statsig initializeGrowthBook() getFeatureValue_CACHED_MAY_BE_STALE() utils/permissions/ ── PermissionMode: 默认 | 汽车 | 绕过 canUseTool() → 允许 | 否认| Ask-user services/compact/ ── 上下文窗口填满时自动压缩 isAutoCompactEnabled() buildPostCompactMessages() snipCompact (HISTORY_SNIP) ReactorCompact (REACTIVE_COMPACT) memdir/ ── CLAUDE.md 内存注入 loadMemoryPrompt() 嵌套内存附件触发 Skills/ ── 用户自定义技能 .md 文件 getSlashCommandToolSkills() SkillTool.call() plugins/ ── 版本化插件注册表 loadAllPluginsCacheOnly() initBundledPlugins()
03 数据流:用户提示API响应

现在让我们跟踪单个用户消息 - 比如说 "refactor this function" — 贯穿整个堆栈。 每个编号的步骤都映射到源树中的实际代码。

sequenceDiagram participant U as User (terminal) participant REPL as REPL.tsx participant PUI as processUserInput() participant QE as QueryEngine participant Q as query() loop participant API as Anthropic API participant T as Tool handler participant SS as sessionStorage U->>REPL: types prompt, presses Enter REPL->>PUI: processUserInput({ input, mode:'prompt' }) PUI->>PUI: parse /slash commands
attach images, memory files
build UserMessage PUI-->>QE: { messages, shouldQuery, allowedTools } QE->>QE: fetchSystemPromptParts()
assemble system + user + system context QE->>SS: recordTranscript(messages) [BEFORE API call] QE->>Q: query({ messages, systemPrompt, canUseTool, ... }) Q->>API: POST /v1/messages (streaming SSE) API-->>Q: message_start API-->>Q: content_block_delta (text streaming) Q-->>REPL: yield AssistantMessage (partial) REPL->>U: renders streaming text API-->>Q: content_block (tool_use: Bash) Q->>Q: canUseTool() permission check Q->>T: BashTool.call({ command: "..." }) T-->>Q: yield BashProgress (live output) Q-->>REPL: yield ProgressMessage REPL->>U: renders tool progress T-->>Q: ToolResult { output: "..." } Q->>API: POST /v1/messages (tool_result appended) API-->>Q: next assistant turn API-->>Q: stop_reason: end_turn Q-->>QE: Terminal result QE->>SS: recordTranscript (final) QE-->>REPL: yield SDKResultMessage REPL->>U: final render complete
批判性的洞察力
成绩单已写好 before API 调用,而不是之后。 这是故意的:如果进程在请求过程中被终止,会话仍然可以恢复。 看 QueryEngine.ts:450 用于解释此设计决策的评论。
04 启动顺序深度回顾

启动顺序经过精心编排,以最大限度地缩短首次渲染时间。 三类工作尽早并行进行:

导入时并行

后台输入/输出

startMdmRawRead() — MDM 策略 plutil 子流程。
startKeychainPrefetch() — macOS 钥匙串读取。
两者都会在 135ms 模块评估完成之前触发。

在初始化()之后

网络预热

preconnectAnthropicApi() 在用户键入任何内容之前建立到 API 端点的 TCP 连接,因此第一个 API 请求不支付 TCP 握手成本。

第一次渲染后

延迟预取

startDeferredPrefetches() — 用户/git 上下文、提示、模型功能、文件计数、更改检测器。 跑步 after 绘制,这样就不会阻止提示。

setup.ts 优先级

会话接线

captureHooksConfigSnapshot() 必须追赶 setCwd() 但在任何查询之前。 钩子配置被读取一次并冻结,因此会话中文件修改无法注入新的钩子。

setup.ts 即发即忘

插件缓存

getCommands() and loadPluginHooks() 作为后台任务预取。 它们填充第一次查询时消耗的缓存,而不会阻塞渲染路径。

迁移 (main.tsx)

配置升级

runMigrations() checks migrationVersion against CURRENT_MIGRATION_VERSION=11 并仅运行所需的模型字符串/设置架构迁移。

为什么 main.tsx 静态导入所有内容但仍然感觉很快?

所有静态导入的约 135ms 模块评估成本与 startMdmRawRead() and startKeychainPrefetch() 在任何导入完成之前,子进程调用会在文件的最顶部触发。 当 JavaScript 完成评估模块图时,两个子进程调用都已分派到操作系统。

像 OpenTelemetry (~400KB) 和 gRPC (~700KB) 这样的重型模块是通过动态延迟加载的 import() inside init() 仅当实际需要遥测时,它们才不会触及关键路径。

React 和 Ink 也很懒: launchRepl() in replLauncher.tsx only import()s App.tsx and REPL.tsx 在通话时间。 在无头模式下(-p),这些根本不会被加载。

05 查询引擎内部结构

QueryEngine (在后来的重构中引入)提取什么是整体的 ask() 函数到拥有完整对话生命周期的类中。 每个对话会话存在一个实例。

说明发动机拥有

class QueryEngine {
  private config: QueryEngineConfig       // tools, commands, mcpClients, model, ...
  private mutableMessages: Message[]       // full conversation history (grows each turn)
  private abortController: AbortController  // shared with all tools in this session
  private permissionDenials: SDKPermissionDenial[]  // accumulated for SDK result
  private totalUsage: NonNullableUsage     // token counts across all turns
  private readFileState: FileStateCache   // snapshot of files read this session
  private discoveredSkillNames: Set<string> // skills seen in this turn (for telemetry)
  private loadedNestedMemoryPaths: Set<string> // CLAUDE.md files already injected
}

SubmitMessage() — 回合生命周期

每次致电 submitMessage() 是一个异步生成器 SDKMessage 事件。 每回合的顺序:

StepCodePurpose
1. 处理斜杠命令processUserInput()处理/commands,构建UserMessage数组,确定shouldQuery
2. 持久化用户消息recordTranscript(messages)在 API 之前写入磁盘,以便终止中请求可恢复
3. 组装系统提示fetchSystemPromptParts()组合默认 + 自定义 + 内存 + 协调器上下文
4.加载技能+插件getSlashCommandToolSkills()无头的仅缓存加载; 全面刷新互动
5.收益系统初始化buildSystemInitMessage()SDK 调用者接收工具、命令、代理的列表
6.进入查询循环query()流式API调用+工具执行直到end_turn
7. 产量结果SDKResultMessage最终成本、使用情况、permission_denials、stop_reason

query() 循环——迭代剖析

键不变量
查询循环是一个纯粹的异步生成器 - 它在流式传输时生成每条消息,并且仅在当前回合中的所有工具调用完成时才前进到下一次迭代。 这使得 REPL 能够在工具并行运行时显示实时流输出。
// query.ts — simplified loop skeleton
async function* queryLoop(params) {
  let state = { messages, turnCount: 1, autoCompactTracking, ... }
  const config = buildQueryConfig()   // snapshot env/statsig state

  while (true) {
    // 1. Optionally start skill/job prefetch (async, consumes settled results only)
    // 2. Send streaming API request via deps.sendRequest()
    for await (const event of streamEvents) {
      yield event  // passes text deltas directly to REPL
      if (event.type === 'tool_use') collectToolUse(event)
    }

    // 3. Check stop reason
    if (stopReason === 'end_turn') return 'success'

    // 4. Execute tools (StreamingToolExecutor — parallel where possible)
    for await (const result of runTools(toolUseBlocks, canUseTool, context)) {
      yield result
    }

    // 5. Token budget / compact checks → may compact and continue
    // 6. Append tool_results to messages, increment turnCount, loop
  }
}
06 工具系统架构

克劳德可以调用的每项能力都是 Tool。 工具系统故意扁平化——没有工具层次结构,只有注册表功能 getAllBaseTools() in tools.ts 返回权威列表。

工具接口(Tool.ts)

// Simplified Tool interface
interface Tool {
  name: string                                         // must be stable (used in Statsig cache key)
  description: string                                  // injected into system prompt
  inputSchema: ZodSchema                             // validation before call()
  isEnabled(): boolean                               // feature-gate / env check
  call(input, context: ToolUseContext):               // async generator
    AsyncGenerator<ToolProgressData, ToolResult>
  renderToolResult(result, context): React.ReactNode  // Ink UI rendering
}

ToolUseContext — 工具通向世界的窗口

每个工具调用都会收到一个 ToolUseContext 将工具可能需要的所有内容捆绑在一起,而不将其耦合到全局状态:

PropertyTypePurpose
messagesMessage[]完整的对话历史记录
mainLoopModelModelSetting当前子代理生成模型
toolsTools可用工具集(供AgentTool向下传递)
mcpClientsMCPServerConnection[]活动 MCP 连接
agentDefinitionsAgentDefinitionsResult自定义代理配置
abortControllerAbortController共享中止信号(Ctrl-C 传播)
readFileStateFileStateCache读取的文件快照(用于比较/撤消)
setAppStateSetter<AppState>工具可以改变 UI 状态(例如 TodoWriteTool)
handleElicitationElicitFnMCP URL 获取(OAuth 流)

功能门控工具

许多工具是有条件包含的 feature() 标志(Bun 捆绑时死代码消除)或环境变量。 这使工具列表对于 Anthropic 的提示缓存键保持确定性:

// tools.ts — feature gate pattern
const SleepTool =
  feature('PROACTIVE') || feature('KAIROS')
    ? require('./tools/SleepTool/SleepTool.js').SleepTool
    : null

// getAllBaseTools() filters nulls from the array
// NOTE: this list is synced to Statsig console for prompt cache invalidation
07 状态管理:两层模型

Claude Code 有一个两层状态模型。 了解使用哪一层来做什么对于理解代码库至关重要。

第一层——全局单例

bootstrap/state.ts

进程生命周期常量:sessionId、cwd、projectRoot、模型、身份验证令牌、遥测仪表、挂钩注册表。 明确不是 React 存储。 文件中的注释警告:“请勿在此处添加更多状态”。

第 2 层——反应状态

state/AppStateStore.ts

DeepImmutable<AppState> — UI 需要的一切:消息、mcpClients、权限上下文、推测状态、设置、任务列表、代理定义、文件历史记录。 通过不可更改地更新 setAppState(prev => ...).

关键设计原则: bootstrap/state.ts 是模块级单例(普通 JS 对象),而 AppState 是 React 上下文。 这种分离意味着查询引擎和工具可以在不导入 React 的情况下访问会话身份,而 UI 可以根据任何 AppState 更改进行反应式重新渲染。

// bootstrap/state.ts — the singleton shape (partial)
type State = {
  originalCwd: string
  projectRoot: string
  totalCostUSD: number
  totalAPIDuration: number
  cwd: string
  modelUsage: { [modelName: string]: ModelUsage }
  mainLoopModelOverride: ModelSetting | undefined
  isInteractive: boolean
  sessionId: SessionId
  sdkBetas: BetaMessageStreamParams['betas']
  hookRegistry: RegisteredHookMatcher[]
  meter: Meter | undefined
  tokenBudgetInfo: { remainingTokens: number; ... }
  // ... ~40 more fields, all process-lifetime
}

// state/AppStateStore.ts — the React state shape (partial)
type AppState = DeepImmutable<{
  settings: SettingsJson
  mainLoopModel: ModelSetting
  toolPermissionContext: ToolPermissionContext
  messages: Message[]
  mcpClients: MCPServerConnection[]
  agentDefinitions: AgentDefinitionsResult
  speculation: SpeculationState
  fileHistory: FileHistoryState
  plugins: LoadedPlugin[]
  tasks: TaskState | null
  // ... ~50 more fields
}>
08 会话管理

Claude Code 中的“会话”是具有唯一 UUID 的持久对话。 会话作为 JSONL 转录文件存储在 ~/.claude/projects/<cwd-hash>/<session-id>.jsonl.

会话生命周期

// Startup: generate or restore session ID
getSessionId()          // reads bootstrap/state.ts
registerSession()        // registers in concurrent sessions tracking
countConcurrentSessions() // used for display in status bar

// During conversation:
recordTranscript(messages)   // enqueues write (lazy 100ms JSONL flush)
flushSessionStorage()         // forced flush (EAGER_FLUSH env / cowork)
cacheSessionTitle()           // first user message → title for resume UI

// Resume path (--continue / --resume):
loadTranscriptFromFile()      // reads JSONL back into Message[]
processResumedConversation()  // validates + replays into initial messages
写队列设计
为了提高性能,转录写入是惰性的(100 毫秒耗尽计时器)。 这 recordTranscript() 对于助理消息,呼叫是即发即忘的,但对于用户消息则需要等待。 这是故意的——评论 QueryEngine.ts:727 解释说等待助理写入会阻塞流生成器,从而阻止 message_delta 处理中的事件。
09 MCP 如何插入

MCP(模型上下文协议)服务器连接为 MCPServerConnection 里面装着的物体 AppState.mcpClients。 它们在第一个查询之前初始化并传递到每个 ToolUseContext.

flowchart LR CFG["settings.json\nmcpServers config"] --> MGR["services/mcp/client.ts\ngetMcpToolsCommandsAndResources()"] MGR --> CONN["MCPServerConnection\nJSON-RPC over stdio/SSE"] CONN --> TOOLS["MCP tools added to\ngetTools() registry"] CONN --> CMDS["MCP slash commands\nadded to getCommands()"] CONN --> RES["MCP resources\nListMcpResourcesTool\nReadMcpResourceTool"] TOOLS --> QE["QueryEngine\ntoolUseContext.mcpClients"] CMDS --> QE RES --> QE

MCP 工具是 not in getAllBaseTools() - 它们在会话启动时与基本工具一起动态添加 getMcpToolsCommandsAndResources()。 这就是 MCP 工具名称可能与基本工具名称冲突的原因:重复数据删除发生在加载时。

10 许可门

每个工具调用都会经过 canUseTool() 执行前。 这个单一功能是所有权限决策的架构瓶颈。

// The permission gate — called by query.ts before runTools()
const wrappedCanUseTool: CanUseToolFn = async (
  tool, input, toolUseContext, assistantMessage, toolUseID, forceDecision
) => {
  const result = await canUseTool(tool, input, toolUseContext, ...)
  if (result.behavior !== 'allow') {
    // Track for SDK result reporting
    this.permissionDenials.push({ tool_name, tool_use_id, tool_input })
  }
  return result
}
权限模式Behavior配置通过
default向用户询问不在允许列表中的任何工具正常 CLI 启动
auto自动允许安全工具,阻止危险--permission-mode auto
bypass无需询问即可允许所有工具--dangerously-skip-permissions
始终允许规则每个工具的允许列表(来自设置 + 会话)用户在会话期间接受
11 上下文压缩

当对话增长到足以威胁模型的上下文窗口时,Claude Code 会触发自动压缩。 这对用户来说是透明的。

Trigger

代币门槛

calculateTokenWarningState() 将当前上下文标记计数与模型的上下文窗口进行比较。 在 ~80% 填充时,自动压缩触发器。

Process

buildPostCompactMessages()

将对话发送给 Claude,并附有摘要提示。 返回单个紧凑摘要消息以及任何保留的最近消息。

HISTORY_SNIP

剪断压实

功能门控替代方案: snipCompact.ts 产生一个 compact_boundary 系统消息。 SDK 路径在内存中截断; REPL 保留完整的回滚和按需项目。

预算跟踪

500k 延续

checkTokenBudget() 处理单个 API 响应超过 max_output_tokens 的情况。 它会自动继续显示“请继续”,直到响应完成。

12 Hooks:用户定义的生命周期事件

钩子允许用户在特定的生命周期点注入 shell 命令或回调。 它们配置在 settings.json 并在启动时捕获一次(不可变快照模式)。

挂钩式触发时可以拦截吗?
PreToolUse在任何工具执行之前是的 - 可以拒绝该工具
PostToolUse任何工具完成后No
PreCompact上下文压缩之前No
PostCompact压实完成后No
Stop当克劳德输出stop_reason=end_turn时是的——可以继续
Notification任何助理通知事件No
FileChanged观察磁盘上修改的文件No
SessionStart在新会话中第一次查询之前是 - 延迟第一个查询
安全不变性
captureHooksConfigSnapshot() 必须运行 after setCwd() and before 任何查询。 快照后,会话的钩子配置将被冻结。 这可以防止恶意项目修改 settings.json 会话中期注入以当前权限执行的钩子命令。
13 两种模式:交互式模式与无头模式

代码库有两个不同的共享运行时路径 QueryEngine 但它们的 UI 和启动行为有显着差异:

Aspect交互式(默认)无头(-p / --print)
UIInk/React 终端渲染仅 stdout 文本输出
信任对话框首次启动时显示跳过(隐式信任)
会议记录在 API 调用之前等待Fire-and-forget
反应进口满载从未进口过
插件预取设置期间的背景跳过(isBareMode())
延迟预取第一次渲染后运行完全跳过
查询引擎路径REPL → ask()print.tsQueryEngine.submitMessage()
入口点标签clisdk-cli
裸模式标志
isBareMode() 返回 true 时 --print/-p 是活跃的。 代码库广泛使用此标志来跳过所有仅限交互的工作。 这也是 SDK 调用者所依赖的标志来获得可预测的低延迟执行。
14 代理群和子代理

The AgentTool 启用递归执行:Claude 可以生成子代理,每个子代理都有自己的 QueryEngine 实例和受限工具集。

flowchart TD M["Main QueryEngine\n(full tool set)"] --> AT["AgentTool.call()"] AT --> SA1["Sub-agent 1\nQueryEngine\n(restricted tools)"] AT --> SA2["Sub-agent 2\nQueryEngine\n(restricted tools)"] SA1 --> API1["Anthropic API\n(separate stream)"] SA2 --> API2["Anthropic API\n(separate stream)"] SA1 --> BR["BashTool\nFileEditTool\netc."] SA2 --> BR2["BashTool\nFileEditTool\netc."] SA1 --> SM["SendMessageTool\n→ UDS inbox"] SA2 --> SM SM --> M

在集群模式下(ENABLE_AGENT_SWARMS=true),代理通过 Unix Domain Socket (UDS) 消息服务器进行通信,该服务器于 setup.ts。 每个代理注册 TeamCreateTool 并可以通过以下方式将消息发送回协调器 SendMessageTool.

15 主时间线:第一次击键到第一个令牌

一切都在一起 - 从冷启动到流响应的完整时间表:

t=0ms $ claude (cli.tsx main()) t=1ms profileCheckpoint('cli_entry') t=1ms startMdmRawRead() (MDM 子进程被触发) t=1ms startKeychainPrefetch() (钥匙串读取被触发) t=136ms profileCheckpoint('main_tsx_imports_loaded') (模块评估完成) t=136ms initializeWarningHandler() t=137ms eagerLoadSettings() (--设置标志解析) t=140ms Commander.parse() (argv → 选项结构) t=141ms init() (entrypoints/init.ts) t=142ms applySafeConfigEnvironmentVariables() t=143ms applyExtraCACertsFromConfig() t=144ms setupGracefulShutdown() t=145ms EnsureMdmSettingsLoaded()(等待 MDM 子进程) t=160ms preconnectAnthropicApi()(TCP 预热) t=161ms runMigrations()(如果迁移版本 < 11) t=163ms setup(cwd,permissionMode, ...) t=164ms setCwd(cwd) t=165ms captureHooksConfigSnapshot() (重要:在任何查询之前) t=166ms initializeFileChangedWatcher() t=168ms startUdsMessaging() [if !bareMode] t=170ms getCommands() 预取(后台) t=171ms loadPluginHooks() (后台) t=172ms initSessionMemory() t=173ms lockCurrentVersion()(后台) t=174ms logEvent('tengu_started') t=175ms prefetchApiKeyFromApiKeyHelperIfSafe() t=180ms showSetupScreens()(如果首次运行则信任对话框) t=182ms launchRepl() (replLauncher.tsx) t=183ms import App.tsx, REPL.tsx (lazy) t=190ms 第一次渲染 ────────────────────────────── 用户看到提示 t=191ms startDeferredPrefetches() (渲染后) t=191ms initUser(), getUserContext() (背景) t=192ms getSystemContext() (git status 等) t=193ms getRelevantTips() (背景) t=194ms refreshModelCapability() (背景) t=195ms settingsChangeDetector.initialize() (背景) [用户输入提示符并按 Enter] t+0ms processUserInput() (斜杠命令、UserMessage) t+2ms fetchSystemPromptParts()(系统提示组件) t+3ms recordTranscript(messages)(在 API 调用之前持续) t+5ms getSlashCommandToolSkills()(仅缓存) t+6ms 产生 buildSystemInitMessage() t+7ms query() → queryLoop() t+8ms buildQueryConfig()(statsig/env 快照) t+9ms deps.sendRequest() (Anthropic API SSE 流) t+50ms 第一个令牌到达 ──────────────────────────── 用户看到文本
16 随处可见的关键设计模式

1. 异步生成器线程

从 API 到 UI 的整个数据流是一个异步生成器链。 query() yields StreamEvents, QueryEngine.submitMessage() yields SDKMessages,并且 REPL 消耗它们。 这使得真正的流式传输无需回调或事件总线。

2. 死代码消除通过 feature()

Bun 的捆绑时间 feature('FLAG_NAME') 从编译的二进制文件中完全删除禁用的功能分支。 这意味着每个构建的工具列表都是确定的(对于 Anthropic 的提示缓存键很重要),并且禁用的功能会增加零运行时开销。

3. 缓存预热以应对延迟

关键路径(系统提示、工具、命令、模型功能)均在设置/启动期间并行预热。 当用户提交第一个提示时,几乎所有昂贵的 I/O 都已经完成。 模式:触发异步工作,丢弃承诺,并记录/缓存结果。

4. 不可变的AppState + 可变的引导/状态

React 状态是不可变的(DeepImmutable)以防止意外突变并启用 React 的变更检测。 但是会话级常量(cwd、sessionId、model)存在于一个普通的模块单例中,该单例故意不是 React 状态——这些值由查询引擎深处的非 React 代码访问。

5. 的 isBareMode() 快速路径

每一个昂贵的启动操作都由 if (!isBareMode())。 此单个标志(无头运行时为 true)会跳过 React、Ink、UDS 消息传递、插件预取、延迟预取和所有仅交互式设置。 无头执行几乎变成纯粹的计算。

6. 并行子流程投资

代码库不是顺序 I/O,而是尽早触发子进程和异步操作,并让它们与 JavaScript 执行并行运行。 startMdmRawRead() and startKeychainPrefetch() 两者都会在 135ms 模块图完成评估之前触发。 当使用其结果的代码运行时,它们通常已经完成。

顶点要点

  • 引导在设计上是并行的。 MDM 读取、钥匙串读取、TCP 预热和命令预取在需要消除顺序 I/O 成本之前全部启动。
  • QueryEngine 是对话所有者。 每个对话一个实例。 它在所有回合中保存消息历史记录、令牌使用情况、文件缓存和中止控制器。
  • 查询循环是一个纯异步生成器。 每条消息(文本增量、工具进度、工具结果)都会流经 yield 从API到UI。 没有回调,没有事件总线。
  • 工具是一个平面注册表。 getAllBaseTools() in tools.ts 是唯一的事实来源。 出于提示缓存目的,该列表在每个构建中都是稳定的。
  • 两个状态层服务于不同的主人。 bootstrap/state.ts (单例)用于查询引擎; AppStateStore.ts (反应)UI。
  • 权限是一个单一的瓶颈。 canUseTool() 在每个工具执行之前调用。 所有三种权限模式(默认、自动、旁路)都流经它。
  • 脚本是在 API 调用之前编写的。 这确保即使进程在请求过程中终止,会话也可以恢复。
  • 无头模式在架构上是独特的。 isBareMode() 删除 React、Ink、UDS、插件和所有推迟的工作。 SDK 调用者的开销几乎为零。
  • 功能门是捆绑时的,而不是运行时的。 feature('FLAG') 是在构建时由 Bun 消除的死代码。 二进制文件中确实不存在禁用的功能。
  • MCP 服务器是一流的对等服务器。 他们的工具、命令和资源与内置工具集成到相同的注册表中,并通过相同的注册表传递。 ToolUseContext.

知识检查

Q1. 从整体架构看,真正拥有一次对话生命周期的是哪一层?
正确! UI 只是展示壳,真正驱动一轮轮对话推进的是 QueryEngine。它管理消息、系统提示、工具调用与流式 API 循环。
Q2. 为什么说无头模式(bare / print / SDK 调用)在架构上是独立路径,而不只是“少渲染一个 UI”?
正确! 无头模式不是 UI 层面的裁剪,而是启动链上的系统性减法。很多只服务交互体验的步骤会整段被跳过。
Q3. 为什么 transcript 会在 API 请求发出之前就写入会话存储?
正确! 这是恢复能力设计,而不是性能优化。只要用户消息已经被接受,系统就希望它能在崩溃后被恢复出来。
Q4. bootstrap/state.ts 和 React 的 AppState 最大的架构差异是什么?
正确! 这是 Claude Code 里最重要的状态分层之一:一个服务查询引擎和底层模块,一个服务 UI 渲染,两者职责不同。
Q5. MCP 工具为什么能在体验上与内置工具并列?
正确! MCP 不是旁路系统,而是被包装成同一套工具机制的一部分。它们走的依旧是同样的工具注册、权限和执行模型。
0/5

课程完成

您已完成 Claude Code 源代码课程的全部 50 课。 现在,您对 Claude Code 的工作原理有了一个完整的心智模型 - 从第一次击键到最终呈现的标记。 这些知识是贡献、扩展或简单地深入理解有史以来最复杂的人工智能编码工具之一的基础。