diff --git a/learn/LEARN.md b/learn/LEARN.md new file mode 100644 index 000000000..189bfc9fd --- /dev/null +++ b/learn/LEARN.md @@ -0,0 +1,152 @@ +# Claude Code 源码学习路线 + +> 基于反编译版 Claude Code CLI (v2.1.888) 的源码学习跟踪 +> +> 各阶段详细笔记见同目录下的 `phase-*.md` 文件 + +## 第一阶段:启动流程(入口链路) ✅ + +详细笔记:[phase-1-startup-flow.md](phase-1-startup-flow.md) + +理解程序从命令行启动到用户看到交互界面的完整路径。 + +- [x] `src/entrypoints/cli.tsx` — 真正入口,polyfill 注入 + 快速路径分发 + - [x] 全局 polyfill:`feature()` 永远返回 false、`MACRO` 全局对象、`BUILD_*` 常量 + - [x] 快速路径设计:按开销从低到高检查,能早返回就早返回 + - [x] 动态 import 模式:`await import()` 延迟加载,减少启动时间 + - [x] 最终出口:`import("../main.jsx")` → `cliMain()` +- [x] `src/main.tsx` — Commander.js CLI 定义,重型初始化(4683 行) + - [x] 三段式结构:辅助函数(1-584) → main()(585-856) → run()(884-4683) + - [x] side-effect import:profileCheckpoint、startMdmRawRead、startKeychainPrefetch 并行预加载 + - [x] preAction 钩子:MDM 等待、init()、迁移、远程设置 + - [x] Commander 参数定义:40+ CLI 选项 + - [x] action handler(2800 行):参数解析 → 服务初始化 → showSetupScreens → launchRepl() + - [x] --print 分支走 print.ts;交互分支走 launchRepl()(7 个场景分支) + - [x] 子命令注册:mcp/auth/plugin/doctor/update/install 等 +- [x] `src/replLauncher.tsx` — 桥梁(22 行),组合 `` + `` 渲染到终端 +- [x] `src/screens/REPL.tsx` — 交互式 REPL 界面(5009 行) + - [x] Props:commands、tools、messages、systemPrompt、thinkingConfig 等 + - [x] 50+ 状态:messages、inputValue、screen、streamingText、queryGuard 等 + - [x] 核心数据流:onSubmit → handlePromptSubmit → onQuery → onQueryImpl → query() → onQueryEvent + - [x] QueryGuard 并发控制:idle → running → idle,防止重复查询 + - [x] 渲染:Transcript 模式(只读历史)/ Prompt 模式(Messages + PermissionRequest + PromptInput) + +**数据流**:`bun run dev` → `package.json scripts.dev` → `bun run src/entrypoints/cli.tsx` → 快速路径检查 → `main.tsx:main()` → `launchRepl()` → `` + +--- + +## 第二阶段:核心对话循环 ✅ + +详细笔记:[phase-2-conversation-loop.md](phase-2-conversation-loop.md) + +理解用户发一句话后,如何变成 API 请求、如何处理流式响应和工具调用。 + +- [x] `src/query.ts` — 核心查询循环(1732 行) + - [x] `query()` AsyncGenerator 入口,委托给 `queryLoop()` + - [x] `queryLoop()` — while(true) 主循环,State 对象管理迭代状态 + - [x] 消息预处理(autocompact、compact boundary) + - [x] `deps.callModel()` → 流式 API 调用 + - [x] StreamingToolExecutor — API 流式返回时并行执行工具 + - [x] 工具调用循环(tool use → 执行 → result → continue) + - [x] 错误恢复(prompt-too-long、max_output_tokens 升级+多轮恢复) + - [x] 模型降级(FallbackTriggeredError → 切换 fallbackModel) + - [x] Withheld 消息模式(暂扣可恢复错误) +- [x] `src/QueryEngine.ts` — 高层编排器(1320 行) + - [x] QueryEngine 类 — 一个 conversation 一个实例 + - [x] `submitMessage()` — 处理用户输入 → 调用 `query()` → 消费事件流 + - [x] SDK/print 模式专用(REPL 直接调用 query()) + - [x] 会话持久化(recordTranscript) + - [x] Usage 跟踪、权限拒绝记录 + - [x] `ask()` 便捷包装函数 +- [x] `src/services/api/claude.ts` — API 客户端(3420 行) + - [x] `queryModelWithStreaming` / `queryModelWithoutStreaming` — 两个公开入口 + - [x] `queryModel()` — 核心私有函数(2400 行) + - [x] 请求参数组装(system prompt、betas、tools、cache control) + - [x] Anthropic SDK 流式调用(`anthropic.beta.messages.stream()`) + - [x] `BetaRawMessageStreamEvent` 事件处理(message_start/content_block_*/message_delta/stop) + - [x] withRetry 重试策略(429/500/529 + 模型降级) + - [x] Prompt Caching 策略(ephemeral/1h TTL/global scope) + - [x] 多 provider 支持(Anthropic / Bedrock / Vertex / Azure) + +**数据流**:REPL.onSubmit → handlePromptSubmit → onQuery → onQueryImpl → `query()` AsyncGenerator → `queryLoop()` while(true) → `deps.callModel()` → `claude.ts queryModel()` → `anthropic.beta.messages.stream()` → 流式事件 → 收集 tool_use → 执行工具 → 结果追加到 messages → continue → 无工具调用时 return + +--- + +## 第三阶段:工具系统 + +理解 Claude 如何定义、注册、调用工具。先读框架,再挑具体工具。 + +- [ ] `src/Tool.ts` — Tool 接口定义 + - [ ] `Tool` 类型结构(name、description、inputSchema、call) + - [ ] `findToolByName`、`toolMatchesName` 工具函数 +- [ ] `src/tools.ts` — 工具注册表 + - [ ] 工具列表组装逻辑 + - [ ] 条件加载(feature flag、USER_TYPE) +- [ ] 具体工具实现(挑选 2-3 个深入阅读): + - [ ] `src/tools/BashTool/` — 执行 shell 命令,最常用的工具 + - [ ] `src/tools/FileReadTool/` — 读取文件,简单直观,适合理解工具模式 + - [ ] `src/tools/FileEditTool/` — 编辑文件,理解 diff/patch 机制 + - [ ] `src/tools/AgentTool/` — 子 Agent 机制,较复杂但核心 + +--- + +## 第四阶段:上下文与系统提示 + +理解 Claude 如何"知道"项目信息、用户偏好等上下文。 + +- [ ] `src/context.ts` — 系统/用户上下文构建 + - [ ] git 状态注入 + - [ ] CLAUDE.md 内容加载 + - [ ] 内存文件(memory)注入 + - [ ] 日期、平台等环境信息 +- [ ] `src/utils/claudemd.ts` — CLAUDE.md 发现与加载 + - [ ] 项目层级搜索逻辑 + - [ ] 多级 CLAUDE.md 合并 + +--- + +## 第五阶段:UI 层(按兴趣选读) + +理解终端 UI 的渲染机制(React/Ink)。 + +- [ ] `src/components/App.tsx` — 根组件,Provider 注入 +- [ ] `src/state/AppState.tsx` — 全局状态类型与 Context +- [ ] `src/components/permissions/` — 工具权限审批 UI +- [ ] `src/components/messages/` — 消息渲染组件 + +--- + +## 第六阶段:外围系统(按需探索) + +- [ ] `src/services/mcp/` — MCP 协议(Model Context Protocol) +- [ ] `src/skills/` — 技能系统(/commit 等斜杠命令) +- [ ] `src/commands/` — CLI 子命令 +- [ ] `src/tasks/` — 后台任务系统 +- [ ] `src/utils/model/providers.ts` — 多 provider 选择逻辑 + +--- + +## 学习笔记 + +### 关键设计模式 + +| 模式 | 位置 | 说明 | +|------|------|------| +| 快速路径 | cli.tsx | 按开销从低到高逐级检查,减少不必要的模块加载 | +| 动态 import | cli.tsx / main.tsx | `await import()` 延迟加载,优化启动时间 | +| feature flag | 全局 | `feature()` 永远返回 false,所有内部功能禁用 | +| React/Ink | UI 层 | 用 React 组件模型渲染终端 UI | +| 工具循环 | query.ts | AI 返回工具调用 → 执行 → 结果回传 → 继续,直到无工具调用 | +| AsyncGenerator 链 | query.ts → claude.ts | `yield*` 透传事件流,形成管道 | +| State 对象 | query.ts queryLoop | 循环间通过不可变 State + transition 字段传递状态 | +| StreamingToolExecutor | query.ts | API 流式返回时并行执行工具 | +| Withheld 消息 | query.ts | 暂扣可恢复错误,恢复成功则吞掉 | +| withRetry | claude.ts | 429/500/529 自动重试 + 模型降级 | +| Prompt Caching | claude.ts | 缓存系统提示和历史消息,减少 token 消耗 | + +### 需要忽略的内容 + +- `_c()` 调用 — React Compiler 反编译产物 +- `feature('...')` 后面的代码块 — 全部是死代码 +- tsc 类型错误 — 反编译导致,不影响 Bun 运行 +- `packages/@ant/` — stub 包,无实际实现 \ No newline at end of file diff --git a/learn/phase-1-qa.md b/learn/phase-1-qa.md new file mode 100644 index 000000000..71d692e73 --- /dev/null +++ b/learn/phase-1-qa.md @@ -0,0 +1,273 @@ +# 第一阶段 Q&A + +## Q1:cli.tsx 的快速路径分发具体在做什么? + +**核心思想**:根据用户输入的命令参数,尽早决定走哪条路,避免加载不需要的代码。cli.tsx 充当一个轻量级路由器,把简单请求就地处理,只有真正需要完整 CLI 时才加载 main.tsx。 + +### 场景对比 + +#### 场景 1:`claude --version`(命中快速路径) + +``` +cli.tsx main() 开始执行 + ├── args = ["--version"] + ├── 命中第 64 行: args[0] === "--version" ✅ + ├── console.log("2.1.888 (Claude Code)") + └── return ← 立即退出,零 import,~10ms +``` + +#### 场景 2:`claude --claude-in-chrome-mcp`(命中中间路径) + +``` +cli.tsx main() 开始执行 + ├── 第 64 行: --version? ❌ + ├── 第 75 行: 加载 profileCheckpoint(仅此一个 import) + ├── 第 81 行: feature("DUMP_SYSTEM_PROMPT") → false ❌ + ├── 第 95 行: --claude-in-chrome-mcp? ✅ 命中 + ├── await import("../utils/claudeInChrome/mcpServer.js") ← 只加载这一个模块 + └── return ← 没有加载 main.tsx 的 200+ import +``` + +#### 场景 3:`claude`(无参数,最常见,全部未命中) + +``` +cli.tsx main() 开始执行 + ├── --version? ❌ + ├── profileCheckpoint 加载 + ├── feature(DUMP)? ❌ (feature=false) + ├── --chrome-mcp? ❌ + ├── --chrome-native? ❌ + ├── feature(CHICAGO)? ❌ (feature=false) + ├── feature(DAEMON)? ❌ (feature=false) + ├── feature(BRIDGE)? ❌ (feature=false) + ├── ... 所有快速路径逐一检查,全部未命中 + │ + ├── 走到第 310 行 ← 最终出口 + ├── await import("../main.jsx") ← 加载完整 CLI(200+ import,~135ms) + └── await cliMain() ← 进入 main.tsx 重型初始化 +``` + +### 性能对比 + +| 方式 | `claude --version` 耗时 | +|------|------------------------| +| 无快速路径(全部走 main.tsx) | ~200ms(加载 200+ import → 初始化 Commander → 解析参数 → 打印) | +| 有快速路径(cli.tsx 拦截) | ~10ms(读 args → 打印 → 退出) | + +### feature() 的加速作用 + +大量快速路径被 `feature()` 守护: + +```ts +if (feature("DAEMON") && args[0] === "daemon") { ... } +``` + +`feature()` 返回 false → `&&` 短路求值 → 连 `args[0]` 都不检查,直接跳过。在反编译版本中这些路径等于不存在,进一步加速了"全部没命中 → 走默认路径"的过程。 + +--- + +## Q2:main.tsx 中不同命令的具体执行流程是怎样的? + +所有命令都会经过 main() → run(),但在 run() 内部根据 Commander 路由到不同分支。 + +### 场景 1:`claude`(无参数 — 启动交互 REPL) + +最常见的场景,走完整条主命令路径: + +``` +main() (第 585 行) + ├── 信号处理注册(SIGINT、exit) + ├── feature flag 路径全部跳过 + ├── isNonInteractive = false(有 TTY,没有 -p) + ├── clientType = 'cli' + └── await run() + │ + ▼ + run() (第 884 行) + ├── Commander 初始化 + preAction 钩子 + 主命令选项注册 + ├── isPrintMode = false → 注册所有子命令 + └── program.parseAsync(process.argv) + │ Commander 匹配到主命令,先执行 preAction + ▼ + preAction (第 907 行) + ├── await ensureMdmSettingsLoaded() ← 等 side-effect import 的子进程完成 + ├── await ensureKeychainPrefetchCompleted() ← 等 keychain 预读完成 + ├── await init() ← 遥测、配置、信任 + ├── initSinks() ← 分析日志 + ├── runMigrations() ← 数据迁移 + └── loadRemoteManagedSettings() / loadPolicyLimits() ← 非阻塞 + │ 然后执行 action handler + ▼ + action(undefined, options) (第 1007 行) ← prompt = undefined + ├── [参数解析] permissionMode, model, thinkingConfig... + ├── [工具加载] tools = getTools(toolPermissionContext) + ├── [并行初始化] + │ ├── setup() ← worktree、CWD + │ ├── getCommands() ← 加载斜杠命令 + │ └── getAgentDefinitionsWithOverrides() ← 加载 agent 定义 + ├── [MCP 连接] 连接配置的 MCP 服务器 + ├── [构建初始状态] initialState = { tools, mcp, permissions, ... } + │ + ├── [UI 初始化](交互模式专属) + │ ├── createRoot() ← 创建 Ink 渲染根节点 + │ └── showSetupScreens() ← 信任对话框 / OAuth / 引导 + │ + ├── [后续初始化] LSP、插件版本、session 注册 + │ + └── 默认分支 (第 3760 行) ← 没有 --continue/--resume/--print + └── await launchRepl(root, { + initialState + }, { + ...sessionConfig, + initialMessages: undefined ← 全新对话,无历史消息 + }, renderAndRun) + │ + ▼ + REPL.tsx 渲染,用户看到空白对话界面 +``` + +### 场景 2:`echo "explain this" | claude -p`(管道/非交互模式) + +``` +main() → + ├── isNonInteractive = true(-p 标志 + stdin 不是 TTY) + ├── clientType = 'sdk-cli' + └── run() + │ + ▼ + run() + ├── Commander 初始化 + preAction + 主命令选项 + ├── isPrintMode = true + │ → ★ 跳过所有子命令注册(节省 ~65ms) + └── program.parseAsync() ← 直接解析,Commander 路由到主命令 action + │ + ▼ + preAction → init、迁移等(同场景 1) + │ + ▼ + action("", { print: true, ... }) + ├── inputPrompt = await getInputPrompt("") + │ ├── stdin.isTTY = false → 从 stdin 读数据 + │ ├── 等待最多 3s 读入: "explain this" + │ └── 返回 "explain this" + ├── tools = getTools() + ├── setup() + getCommands()(并行) + │ + ├── isNonInteractiveSession = true → 走 --print 分支(第 2584 行) + │ ├── applyConfigEnvironmentVariables() ← -p 模式信任隐含 + │ ├── 构建 headlessInitialState(无 UI) + │ ├── headlessStore = createStore(headlessInitialState) + │ │ + │ ├── await import('src/cli/print.js') + │ └── runHeadless(inputPrompt, ...) ★ 不走 REPL + │ ├── 发送 API 请求 + │ ├── 流式输出到 stdout + │ └── 完成后 process.exit() + │ + └── ← 不走 createRoot()、showSetupScreens()、launchRepl() +``` + +**关键差异**: +- 检测到 `-p` 后跳过子命令注册(节省 ~65ms) +- 不创建 Ink UI,不调用 `showSetupScreens()` +- 从 stdin 读取输入(`getInputPrompt` 第 857 行) +- 走 `print.js` 路径直接执行查询输出到 stdout + +### 场景 3:`claude -c`(继续最近对话) + +``` +... main() → run() → preAction → action(前半部分同场景 1) + │ + ▼ + action(undefined, { continue: true, ... }) + ├── [参数解析 + 工具加载 + 并行初始化 + UI 初始化](同场景 1) + │ + ├── options.continue = true → 命中第 3101 行 + │ ├── clearSessionCaches() ← 清除过期缓存 + │ ├── result = await loadConversationForResume() + │ │ └── 从 ~/.claude/projects// 读最近的会话 JSONL + │ │ + │ ├── result 为 null? → exitWithError("No conversation found") + │ │ + │ ├── loaded = await processResumedConversation(result) + │ │ ├── 解析 JSONL → messages[] + │ │ ├── 恢复文件历史快照 + │ │ └── 重建 initialState + │ │ + │ └── await launchRepl(root, { + │ initialState: loaded.initialState + │ }, { + │ ...sessionConfig, + │ initialMessages: loaded.messages, ★ 带上历史消息 + │ initialFileHistorySnapshots: loaded.fileHistorySnapshots, + │ initialAgentName: loaded.agentName + │ }, renderAndRun) + │ │ + │ ▼ + │ REPL.tsx 渲染,显示历史对话,用户继续聊天 + │ + └── ← 其他分支不执行 +``` + +**关键差异**:`initialMessages` 有值(历史消息),REPL 启动时会渲染之前的对话内容。 + +### 场景 4:`claude mcp list`(子命令) + +``` +main() → run() + │ + ▼ + run() + ├── Commander 初始化 + preAction 钩子 + ├── 注册主命令 .action(...) + ├── isPrintMode = false → 注册所有子命令 + │ ├── program.command('mcp') (第 3894 行) + │ │ ├── mcp.command('serve').action(...) + │ │ ├── mcp.command('add').action(...) + │ │ ├── mcp.command('list').action(async () => { ★ + │ │ │ const { mcpListHandler } = await import('./cli/handlers/mcp.js'); + │ │ │ await mcpListHandler(); + │ │ │ }) + │ │ └── ... + │ ├── program.command('auth') + │ ├── program.command('doctor') + │ └── ... + │ + └── program.parseAsync(["node", "claude", "mcp", "list"]) + │ Commander 匹配到 mcp → list + ▼ + preAction (第 907 行) ← 子命令也触发 preAction + ├── await init() + ├── initSinks() + ├── runMigrations() + └── ... + │ + ▼ 执行子命令自己的 action(不走主命令 action) + mcp list action + ├── await import('./cli/handlers/mcp.js') + └── await mcpListHandler() + ├── 读取 MCP 配置(user/project/local 三级) + ├── 连接每个服务器做健康检查 + ├── 格式化输出到终端 + └── 退出 + + ← 主命令的 action handler 完全不执行 + ← 没有 REPL、没有 Ink UI、没有 showSetupScreens +``` + +**关键差异**: +- Commander 路由到子命令,**主命令 action 完全跳过** +- `preAction` 仍然执行(基础初始化所有命令都需要) +- 子命令有自己独立的轻量 action + +### 四种场景对比 + +| | `claude` | `claude -p` | `claude -c` | `claude mcp list` | +|---|---------|------------|------------|-------------------| +| preAction | 执行 | 执行 | 执行 | 执行 | +| 主命令 action | 执行 | 执行 | 执行 | **跳过** | +| 子命令注册 | 注册 | **跳过** | 注册 | 注册 | +| showSetupScreens | 执行 | **跳过** | 执行 | **跳过** | +| createRoot (Ink) | 执行 | **跳过** | 执行 | **跳过** | +| 加载历史消息 | 否 | 否 | **是** | 否 | +| 最终出口 | launchRepl | print.js | launchRepl | 子命令 action | \ No newline at end of file diff --git a/learn/phase-1-startup-flow.md b/learn/phase-1-startup-flow.md new file mode 100644 index 000000000..17e3c5595 --- /dev/null +++ b/learn/phase-1-startup-flow.md @@ -0,0 +1,597 @@ +# 第一阶段:启动流程详解 + +> 从 `bun run dev` 到用户看到交互界面的完整路径 + +## 启动链路总览 + +``` +bun run dev + → package.json scripts.dev: "bun run src/entrypoints/cli.tsx" + → cli.tsx: polyfill 注入 + 快速路径检查 + → import("../main.jsx") → cliMain() + → main.tsx: main() → run() + → Commander 参数解析 → preAction 钩子 + → action handler: 服务初始化 → showSetupScreens + → launchRepl() + → replLauncher.tsx: + → REPL.tsx: 渲染交互界面,等待用户输入 +``` + +--- + +## 1. cli.tsx(321 行)— 入口与快速路径分发 + +**文件路径**: `src/entrypoints/cli.tsx` + +### 1.1 全局 Polyfill(第 1-53 行) + +模块加载时立即执行的 side-effect,在 `main()` 之前运行。 + +#### feature() 桩函数(第 3 行) + +```ts +const feature = (_name: string) => false; +``` + +原版 Claude Code 构建时,Bun bundler 通过 `bun:bundle` 提供 `feature()` 函数,用于**编译时 feature flag**(类似 C 的 `#ifdef`)。反编译版没有构建流程,所以直接定义为永远返回 `false`。 + +**效果**:所有 Anthropic 内部功能分支全部禁用,包括: +- `COORDINATOR_MODE` — 协调器模式 +- `KAIROS` — 助手模式 +- `DAEMON` — 后台守护进程 +- `BRIDGE_MODE` — 远程控制 +- `SSH_REMOTE` — SSH 远程 +- `BG_SESSIONS` — 后台会话 +- ... 等 20+ 个 flag + +#### MACRO 全局对象(第 4-14 行) + +```ts +globalThis.MACRO = { + VERSION: "2.1.888", + BUILD_TIME: new Date().toISOString(), + FEEDBACK_CHANNEL: "", + ISSUES_EXPLAINER: "", + NATIVE_PACKAGE_URL: "", + PACKAGE_URL: "", + VERSION_CHANGELOG: "", +}; +``` + +原版构建时 Bun 会把这些值内联到代码里。这里模拟注入,让后续代码读 `MACRO.VERSION` 时能拿到值。 + +#### 构建常量(第 16-18 行) + +```ts +BUILD_TARGET = "external"; // 标记为"外部"构建(非 Anthropic 内部) +BUILD_ENV = "production"; // 生产环境 +INTERFACE_TYPE = "stdio"; // 标准输入输出模式 +``` + +这三个全局变量在代码各处被读取,用来区分运行环境。`"external"` 意味着很多 `("external" as string) === 'ant'` 的检查会返回 false。 + +#### 环境修补(第 22-33 行) + +- 禁用 corepack 自动 pin(防止污染 package.json) +- 远程模式下设置 Node.js 堆内存上限 8GB + +#### ABLATION_BASELINE(第 40-53 行) + +```ts +if (feature("ABLATION_BASELINE") && ...) { ... } +``` + +`feature()` 返回 false,**永远不执行**。Anthropic 内部 A/B 测试代码。 + +### 1.2 main() 函数(第 60-317 行) + +设计模式:**分层快速路径(fast path cascading)**——按开销从低到高逐级检查,命中即返回。 + +#### 快速路径列表 + +| 优先级 | 行号 | 检查条件 | 功能 | 开销 | 可执行 | +|--------|------|---------|------|------|--------| +| 1 | 64-72 | `--version` / `-v` | 打印版本号退出 | **零 import** | 是 | +| 2 | 81-94 | `feature("DUMP_SYSTEM_PROMPT")` | 导出系统提示 | - | 否(flag) | +| 3 | 95-99 | `--claude-in-chrome-mcp` | Chrome MCP 服务 | 动态 import | 是 | +| 4 | 101-105 | `--chrome-native-host` | Chrome Native Host | 动态 import | 是 | +| 5 | 108-116 | `feature("CHICAGO_MCP")` | Computer Use MCP | - | 否(flag) | +| 6 | 123-127 | `feature("DAEMON")` | Daemon Worker | - | 否(flag) | +| 7 | 133-178 | `feature("BRIDGE_MODE")` | 远程控制 | - | 否(flag) | +| 8 | 181-190 | `feature("DAEMON")` | Daemon 主进程 | - | 否(flag) | +| 9 | 195-225 | `feature("BG_SESSIONS")` | ps/logs/attach/kill | - | 否(flag) | +| 10 | 228-240 | `feature("TEMPLATES")` | 模板任务 | - | 否(flag) | +| 11 | 244-253 | `feature("BYOC_ENVIRONMENT_RUNNER")` | BYOC 运行器 | - | 否(flag) | +| 12 | 258-264 | `feature("SELF_HOSTED_RUNNER")` | 自托管运行器 | - | 否(flag) | +| 13 | 267-293 | `--tmux` + `--worktree` | tmux worktree | 动态 import | 是 | + +#### 参数修正(第 296-307 行) + +```ts +// --update/--upgrade → 重写为 update 子命令 +if (args[0] === "--update") process.argv = [..., "update"]; +// --bare → 设置简单模式环境变量 +if (args.includes("--bare")) process.env.CLAUDE_CODE_SIMPLE = "1"; +``` + +#### 最终出口(第 310-316 行) + +```ts +const { startCapturingEarlyInput } = await import("../utils/earlyInput.js"); +startCapturingEarlyInput(); // 捕获用户提前输入的内容 +const { main: cliMain } = await import("../main.jsx"); +await cliMain(); // 进入 main.tsx 重型初始化 +``` + +所有快速路径都没命中时(99% 的情况),才走到这里。 + +### 1.3 启动(第 320 行) + +```ts +void main(); +``` + +`void` 表示不关心 Promise 返回值。 + +### 1.4 关键设计思想 + +- **快速路径**:`--version` 零开销返回,不加载任何模块 +- **动态 import**:`await import()` 替代静态 import,每条路径只加载自己需要的模块 +- **feature flag 过滤**:`feature()` 返回 false 使大量内部功能成为死代码 + +--- + +## 2. main.tsx(4683 行)— 重型初始化与 Commander CLI + +**文件路径**: `src/main.tsx` + +整个项目最大的单文件,但结构清晰:**辅助函数 → main() → run()**。 + +### 2.1 Import 区(第 1-215 行) + +200+ 行 import,加载几乎所有子系统。关键的是前三个 **side-effect import**(import 即执行): + +```ts +// 第 9 行:记录时间戳 +profileCheckpoint('main_tsx_entry'); + +// 第 16 行:启动 MDM 子进程读取(macOS plutil) +startMdmRawRead(); + +// 第 20 行:启动 keychain 预读取(OAuth token、API key) +startKeychainPrefetch(); +``` + +这三个在 import 阶段就**并行启动子进程**,和后续 ~135ms 的模块加载同时进行——**用并行隐藏延迟**。 + +### 2.2 辅助函数(第 216-584 行) + +| 函数 | 行号 | 作用 | +|------|------|------| +| `logManagedSettings()` | 216 | 记录企业托管设置到分析日志 | +| `isBeingDebugged()` | 232 | 检测调试模式,**外部构建下直接 exit(1)**(第 266 行) | +| `logSessionTelemetry()` | 279 | Session 遥测(技能、插件) | +| `getCertEnvVarTelemetry()` | 291 | SSL 证书环境变量收集 | +| `runMigrations()` | 326 | 数据迁移(模型重命名、设置格式升级等) | +| `prefetchSystemContextIfSafe()` | 360 | 信任关系建立后安全预取系统上下文 | +| `startDeferredPrefetches()` | 388 | REPL 首次渲染后的延迟预取 | +| `eagerLoadSettings()` | 502 | 在 init() 之前提前加载 `--settings` 参数 | +| `initializeEntrypoint()` | 517 | 根据运行模式设置 `CLAUDE_CODE_ENTRYPOINT` | + +还有 `_pendingConnect`、`_pendingSSH`、`_pendingAssistantChat` 三个状态变量(第 542-583 行),用于暂存子命令参数。 + +### 2.3 main() 函数(第 585-856 行) + +`main()` 本身不长,做完环境检测后调用 `run()`: + +``` +main() +├── 安全设置(NoDefaultCurrentDirectoryInExePath) +├── 信号处理(SIGINT → exit, exit → 恢复光标) +├── feature flag 保护的特殊路径(全部跳过) +├── 检测 -p/--print / --init-only → 判断是否交互模式 +├── clientType 判断(cli / sdk-typescript / remote / github-action 等) +├── eagerLoadSettings() +└── await run() ← 进入真正的逻辑 +``` + +### 2.4 run() 函数(第 884-4683 行) + +占 3800 行,是整个文件的核心。 + +#### Commander 初始化 + preAction 钩子(第 884-967 行) + +```ts +const program = new CommanderCommand() + .configureHelp(createSortedHelpConfig()) + .enablePositionalOptions(); +``` + +**preAction 钩子**(所有命令执行前都会运行): + +``` +preAction +├── await ensureMdmSettingsLoaded() ← 等 MDM 子进程完成 +├── await ensureKeychainPrefetchCompleted() ← 等 keychain 预读完成 +├── await init() ← 一次性初始化 +├── initSinks() ← 分析日志接收器 +├── runMigrations() ← 数据迁移 +├── loadRemoteManagedSettings() ← 企业远程设置(非阻塞) +└── loadPolicyLimits() ← 策略限制(非阻塞) +``` + +#### 主命令 Option 定义(第 968-1006 行) + +定义了 40+ CLI 参数,关键的包括: + +| 参数 | 作用 | +|------|------| +| `-p, --print` | 非交互模式,输出后退出 | +| `--model ` | 指定模型(如 sonnet、opus) | +| `--permission-mode ` | 权限模式 | +| `-c, --continue` | 继续最近对话 | +| `-r, --resume` | 恢复指定对话 | +| `--mcp-config` | MCP 服务器配置文件 | +| `--allowedTools` | 允许的工具列表 | +| `--system-prompt` | 自定义系统提示 | +| `--dangerously-skip-permissions` | 跳过所有权限检查 | +| `--output-format` | 输出格式(text/json/stream-json) | +| `--effort ` | 推理努力级别(low/medium/high/max) | +| `--bare` | 最小模式 | + +#### action 处理器(第 1006-3808 行) + +主命令的执行逻辑,内部按阶段和场景分支: + +``` +action(async (prompt, options) => { + │ + ├── [1007-1600] 参数解析与预处理 + │ ├── --bare 模式 + │ ├── 解析 model / permission-mode / thinking / effort + │ ├── 解析 MCP 配置、工具列表、系统提示 + │ └── 初始化工具权限上下文 + │ + ├── [1600-2220] 服务初始化 + │ ├── MCP 客户端连接 + │ ├── 插件加载 + 技能初始化 + │ ├── 工具列表组装 + │ └── 初始 AppState 构建 + │ + ├── [2220-2315] UI 初始化(交互模式) + │ ├── createRoot() — 创建 Ink 渲染根节点 + │ ├── showSetupScreens() — 信任对话框、OAuth 登录、引导 + │ └── 登录后刷新各种服务 + │ + ├── [2315-2582] 后续初始化 + │ ├── LSP 管理器、插件版本管理 + │ ├── session 注册、遥测日志 + │ └── 遥测上报 + │ + ├── [2584-3050] --print 非交互模式分支 + │ ├── 构建 headless AppState + store + │ └── 交给 print.ts 执行 + │ + └── [3050-3808] 交互模式:启动 REPL(7 个分支) + ├── --continue → 加载最近对话 → launchRepl() + ├── DIRECT_CONNECT → ❌ flag 关闭 + ├── SSH_REMOTE → ❌ flag 关闭 + ├── KAIROS assistant → ❌ flag 关闭 + ├── --resume → 恢复指定对话 → launchRepl() + ├── --resume 无 ID → 显示对话选择器 + └── 默认(无参数) → launchRepl() ★最常走的路径 +}) +``` + +#### 子命令注册(第 3808-4683 行) + +| 子命令 | 行号 | 作用 | +|--------|------|------| +| `claude mcp` | 3892 | MCP 服务器管理(serve/add/remove/list/get) | +| `claude server` | 3960 | Session 服务器(❌ flag 关闭) | +| `claude auth` | 4098 | 认证管理(login/logout/status/token) | +| `claude plugin` | 4148 | 插件管理(install/uninstall/list/update) | +| `claude setup-token` | 4267 | 设置长期认证 token | +| `claude agents` | 4278 | 列出已配置的 agents | +| `claude doctor` | 4346 | 健康检查 | +| `claude update` | 4362 | 检查更新 | +| `claude install` | 4394 | 安装原生构建 | +| `claude log` | 4411 | 查看对话日志(内部) | +| `claude completion` | 4491 | Shell 自动补全 | + +最后执行解析: + +```ts +await program.parseAsync(process.argv); +``` + +### 2.5 main.tsx 学习建议 + +- **不要通读**。记住三段结构:辅助函数 → main() → run() +- `feature()` 返回 false 的分支全部跳过,可忽略 50%+ 代码 +- `("external" as string) === 'ant'` 的分支也跳过(内部构建专用) +- 需要深入某功能时,通过搜索定位对应代码段 + +--- + +## 3. replLauncher.tsx(22 行)— 胶水层 + +**文件路径**: `src/replLauncher.tsx` + +极其简单,就做一件事: + +```tsx +export async function launchRepl(root, appProps, replProps, renderAndRun) { + const { App } = await import('./components/App.js'); + const { REPL } = await import('./screens/REPL.js'); + await renderAndRun(root, ); +} +``` + +- `App` — 全局 Provider(AppState、Stats、FpsMetrics) +- `REPL` — 交互界面组件 +- `renderAndRun` — 把 React 元素渲染到 Ink 终端 + +动态 import 保持了按需加载的策略。 + +--- + +## 4. REPL.tsx(5009 行)— 交互界面 + +**文件路径**: `src/screens/REPL.tsx` + +项目第二大文件,是用户直接交互的界面。一个巨型 React 函数组件。 + +### 4.1 文件结构 + +``` +REPL.tsx (5009 行) +├── [1-310] Import 区(150+ import) +├── [312-525] 辅助组件 +│ ├── median() — 数学工具函数 +│ ├── TranscriptModeFooter — 转录模式底栏 +│ ├── TranscriptSearchBar — 转录搜索栏 +│ └── AnimatedTerminalTitle — 终端标题动画 +├── [527-571] Props 类型定义 +└── [573-5009] REPL() 组件主体 + ├── [600-900] 状态声明(50+ 个 useState/useRef/useAppState) + ├── [900-2750] 副作用与回调(useEffect/useCallback) + ├── [2750-2860] onQueryImpl — 核心:执行 API 查询 + ├── [2860-3030] onQuery — 查询守卫与并发控制 + ├── [3030-3145] 查询相关辅助回调 + ├── [3146-3550] onSubmit — 用户提交处理 + ├── [3550-4395] 更多副作用与状态管理 + └── [4396-5009] JSX 渲染 +``` + +### 4.2 Props + +从 main.tsx 通过 launchRepl() 传入: + +| Prop | 类型 | 含义 | +|------|------|------| +| `commands` | `Command[]` | 可用的斜杠命令 | +| `debug` | `boolean` | 调试模式 | +| `initialTools` | `Tool[]` | 初始工具集 | +| `initialMessages` | `MessageType[]` | 初始消息(恢复对话时有值) | +| `pendingHookMessages` | `Promise<...>` | 延迟加载的 hook 消息 | +| `mcpClients` | `MCPServerConnection[]` | MCP 服务器连接 | +| `systemPrompt` | `string` | 自定义系统提示 | +| `appendSystemPrompt` | `string` | 追加系统提示 | +| `onBeforeQuery` | `fn` | 查询前回调,返回 false 可阻止查询 | +| `onTurnComplete` | `fn` | 轮次完成回调 | +| `mainThreadAgentDefinition` | `AgentDefinition` | 主线程 Agent 定义 | +| `thinkingConfig` | `ThinkingConfig` | 思考模式配置 | +| `disabled` | `boolean` | 禁用输入 | + +### 4.3 状态管理 + +分三层: + +**全局 AppState(通过 useAppState 选择器读取):** + +```ts +const toolPermissionContext = useAppState(s => s.toolPermissionContext); +const verbose = useAppState(s => s.verbose); +const mcp = useAppState(s => s.mcp); +const plugins = useAppState(s => s.plugins); +const agentDefinitions = useAppState(s => s.agentDefinitions); +``` + +**本地状态(useState):** + +```ts +const [messages, setMessages] = useState(initialMessages ?? []); +const [inputValue, setInputValue] = useState(''); +const [screen, setScreen] = useState('prompt'); +const [streamingText, setStreamingText] = useState(null); +const [streamingToolUses, setStreamingToolUses] = useState([]); +// ... 50+ 个状态 +``` + +**关键 Ref:** + +```ts +const queryGuard = useRef(new QueryGuard()).current; // 查询并发控制 +const messagesRef = useRef(messages); // 消息的同步引用(避免闭包问题) +const abortController = ...; // 取消请求控制器 +const responseLengthRef = useRef(0); // 响应长度追踪 +``` + +### 4.4 核心数据流:用户输入 → API 调用 + +``` +用户按回车 + │ + ▼ +onSubmit (第 3146 行) + ├── 斜杠命令?→ immediate command 直接执行 或 handlePromptSubmit 路由 + ├── 空输入?→ 忽略 + ├── 空闲检测 → 可能弹出"是否开始新对话"对话框 + ├── 加入历史记录 + │ + ▼ +handlePromptSubmit (外部函数,src/utils/handlePromptSubmit.ts) + ├── 斜杠命令 → 路由到对应 Command handler + ├── 普通文本 → 构建 UserMessage,调用 onQuery() + │ + ▼ +onQuery (第 2860 行) — 并发守卫层 + ├── queryGuard.tryStart() → 已有查询?排队等待 + ├── setMessages([...old, ...newMessages]) — 追加用户消息 + ├── onQueryImpl() + │ + ▼ +onQueryImpl (第 2750 行) — 真正执行 API 调用 + │ + ├── 1. 并行加载上下文: + │ await Promise.all([ + │ getSystemPrompt(), // 构建系统提示 + │ getUserContext(), // 用户上下文 + │ getSystemContext(), // 系统上下文(git、平台等) + │ ]) + │ + ├── 2. buildEffectiveSystemPrompt() — 合成最终系统提示 + │ + ├── 3. for await (const event of query({...})) ★核心★ + │ │ 调用 src/query.ts 的 query() AsyncGenerator + │ │ 流式产出事件 + │ │ + │ └── onQueryEvent(event) — 处理每个流式事件 + │ ├── 更新 streamingText(打字机效果) + │ ├── 更新 messages(工具调用结果) + │ └── 更新 inProgressToolUseIDs + │ + └── 4. 收尾:resetLoadingState()、onTurnComplete() +``` + +**核心代码(第 2797-2807 行)**: + +```ts +for await (const event of query({ + messages: messagesIncludingNewMessages, + systemPrompt, + userContext, + systemContext, + canUseTool, + toolUseContext, + querySource: getQuerySourceForREPL() +})) { + onQueryEvent(event); +} +``` + +`query()` 来自 `src/query.ts`,是第二阶段要学的核心函数。 + +### 4.5 QueryGuard 并发控制 + +防止同时发起多个 API 请求的状态机: + +``` +idle ──tryStart()──▶ running ──end()──▶ idle + │ + └── tryStart() 返回 null(已在运行) + → 新消息排入队列 +``` + +- `tryStart()` — 原子操作,检查并转换 idle→running,返回 generation 号 +- `end(generation)` — 检查 generation 匹配后转换 running→idle +- 防止 cancel+resubmit 竞态条件 + +### 4.6 JSX 渲染 + +两个互斥的渲染分支: + +#### Transcript 模式(第 4396-4493 行) + +按 `v` 键切换,只读浏览对话历史,支持搜索: + +```tsx + + + + + + } + bottom={} + /> + +``` + +#### Prompt 模式(第 4552-5009 行) + +主交互界面,从上到下: + +```tsx + + // 终端 tab 标题 + // 全局快捷键 + // 命令快捷键 + // 滚动快捷键 + // Ctrl+C 取消 + // MCP 连接管理 + } // 权限审批覆盖层 + scrollable={ // 可滚动区域 + <> + // ★ 对话消息渲染 + // 用户输入占位 + {toolJSX} // 工具 UI + // 加载动画 + + } + bottom={ // 固定底部 + <> + {/* 各种对话框 */} + + + + + + + {/* ★ 用户输入框 */} + + + } + /> + + +``` + +### 4.7 REPL.tsx 学习建议 + +- 核心只有一条线:`onSubmit → onQuery → query() → onQueryEvent → 更新消息` +- 其余 4000+ 行是 UI 细节:快捷键、对话框、动画、边界情况处理 +- `feature('...')` 保护的 JSX 全部跳过 +- `("external" as string) === 'ant'` 的分支也跳过 + +--- + +## 关键设计模式总结 + +| 模式 | 位置 | 说明 | +|------|------|------| +| 快速路径 | cli.tsx | 按开销从低到高逐级检查,零开销处理简单请求 | +| 动态 import | cli.tsx / main.tsx | `await import()` 延迟加载,每条路径只加载需要的模块 | +| Side-effect import | main.tsx 顶部 | import 阶段就并行启动子进程,用并行隐藏延迟 | +| feature flag | 全局 | `feature()` 永远返回 false,编译时消除死代码 | +| preAction 钩子 | main.tsx run() | Commander.js 命令执行前统一初始化 | +| QueryGuard | REPL.tsx | 状态机防止并发 API 请求,带 generation 计数防竞态 | +| React/Ink | UI 层 | 用 React 组件模型渲染终端 UI,支持全屏和虚拟滚动 | + +## 需要忽略的代码模式 + +| 模式 | 来源 | 说明 | +|------|------|------| +| `_c(N)` 调用 | React Compiler | 反编译产生的 memoization 样板代码 | +| `feature('FLAG')` 后面的代码 | Bun bundler | 全部是死代码,在当前版本不会执行 | +| `("external" as string) === 'ant'` | 构建目标检查 | 永远为 false(external !== ant) | +| tsc 类型错误 | 反编译 | `unknown`/`never`/`{}` 类型,不影响 Bun 运行 | +| `packages/@ant/` | stub 包 | 空实现,仅满足 import 依赖 | \ No newline at end of file diff --git a/learn/phase-2-conversation-loop.md b/learn/phase-2-conversation-loop.md new file mode 100644 index 000000000..fc99227c2 --- /dev/null +++ b/learn/phase-2-conversation-loop.md @@ -0,0 +1,774 @@ +# 第二阶段:核心对话循环详解 + +> 用户发一句话后,如何变成 API 请求、如何处理流式响应和工具调用 + +## 对话循环总览 + +``` +用户输入 "帮我读取 README.md" + │ + ▼ +REPL.tsx: onSubmit → onQuery → onQueryImpl + │ + ├── 1. 并行加载上下文: + │ getSystemPrompt() + getUserContext() + getSystemContext() + │ + ├── 2. buildEffectiveSystemPrompt() — 合成最终系统提示 + │ + ├── 3. for await (const event of query({...})) ★ 核心循环 + │ │ + │ │ query.ts: queryLoop() + │ │ ├── while (true) { + │ │ │ ├── autocompact / microcompact 处理 + │ │ │ ├── deps.callModel() → claude.ts 流式 API 调用 + │ │ │ │ └── for await (message of stream) { yield message } + │ │ │ │ + │ │ │ ├── 收集 assistant 消息中的 tool_use 块 + │ │ │ │ + │ │ │ ├── needsFollowUp? + │ │ │ │ ├── true → 执行工具 → 收集结果 → state = next → continue + │ │ │ │ └── false → 检查错误恢复 → return { reason: 'completed' } + │ │ │ } + │ │ + │ └── onQueryEvent(event) — 更新 UI 状态 + │ + └── 4. 收尾: resetLoadingState(), onTurnComplete() +``` + +### 两条数据路径 + +| 路径 | 调用方 | 说明 | +|------|--------|------| +| **交互式(REPL)** | REPL.tsx → `query()` | 直接调用 `query()` AsyncGenerator | +| **非交互式(SDK/print)** | print.ts → `QueryEngine.submitMessage()` → `query()` | 通过 QueryEngine 包装,增加了会话持久化、usage 跟踪等 | + +--- + +## 1. query.ts(1732 行)— 核心查询循环 + +**文件路径**: `src/query.ts` + +### 1.1 文件结构 + +``` +query.ts (1732 行) +├── [0-120] Import 区 + feature flag 条件模块加载 +├── [122-148] yieldMissingToolResultBlocks() — 为未配对的 tool_use 生成错误 tool_result +├── [150-178] 常量与辅助函数 (MAX_OUTPUT_TOKENS_RECOVERY_LIMIT, isWithheldMaxOutputTokens) +├── [180-198] QueryParams 类型定义 +├── [200-216] State 类型 — 循环迭代间的可变状态 +├── [218-238] query() — 导出的 AsyncGenerator,委托给 queryLoop() +├── [240-1732] queryLoop() — 核心 while(true) 循环 +│ ├── [241-306] 初始化 State + 内存预取 +│ ├── [307-448] 循环开头:解构 state、消息预处理(snip/microcompact/context collapse) +│ ├── [449-578] 系统提示构建(第449行) + autocompact(第453行) + StreamingToolExecutor 初始化(第562行) +│ ├── [650-866] ★ deps.callModel()(第659行) + 流式响应处理 + tool_use 收集 +│ ├── [896-956] 错误处理(FallbackTriggeredError、通用错误) +│ ├── [1002-1054] 中断处理(abortController.signal.aborted) +│ ├── [1065-1360] 无 followUp 时的终止/恢复逻辑 +│ │ ├── prompt-too-long 恢复 +│ │ ├── max_output_tokens 恢复(升级 + 多轮) +│ │ ├── stop hooks 执行 +│ │ └── return { reason: 'completed' } +│ └── [1360-1732] 有 followUp 时的工具执行 + 下一轮准备 +│ ├── 工具执行(streaming 或 sequential) +│ ├── attachment 注入(排队命令、内存预取、技能发现) +│ ├── maxTurns 检查 +│ └── state = next → continue +``` + +### 1.2 入口:query() 函数(第 219 行) + +```ts +export async function* query(params: QueryParams): + AsyncGenerator { + const consumedCommandUuids: string[] = [] + const terminal = yield* queryLoop(params, consumedCommandUuids) + // 通知所有消费的排队命令已完成 + for (const uuid of consumedCommandUuids) { + notifyCommandLifecycle(uuid, 'completed') + } + return terminal +} +``` + +`query()` 本身很薄,只做两件事: +1. 委托给 `queryLoop()` 执行实际逻辑 +2. 在正常返回后通知排队命令的生命周期 + +### 1.3 QueryParams(第 181 行) + +```ts +type QueryParams = { + messages: Message[] // 当前对话消息 + systemPrompt: SystemPrompt // 系统提示 + userContext: { [k: string]: string } // 用户上下文(CLAUDE.md 等) + systemContext: { [k: string]: string } // 系统上下文(git 状态等) + canUseTool: CanUseToolFn // 工具权限检查函数 + toolUseContext: ToolUseContext // 工具执行上下文 + fallbackModel?: string // 备用模型 + querySource: QuerySource // 查询来源标识 + maxTurns?: number // 最大轮次限制 + taskBudget?: { total: number } // 令牌预算 +} +``` + +### 1.4 State — 循环迭代间的可变状态(第 204 行) + +```ts +type State = { + messages: Message[] // 累积的消息列表 + toolUseContext: ToolUseContext // 工具执行上下文 + autoCompactTracking: ... // 自动压缩跟踪 + maxOutputTokensRecoveryCount: number // 输出令牌恢复尝试次数 + hasAttemptedReactiveCompact: boolean // 是否已尝试响应式压缩 + maxOutputTokensOverride: number | undefined // 输出令牌覆盖 + pendingToolUseSummary: Promise<...> // 待处理的工具使用摘要 + stopHookActive: boolean | undefined // stop hook 是否活跃 + turnCount: number // 当前轮次 + transition: Continue | undefined // 上一次迭代为何 continue +} +``` + +**设计关键**:每次 `continue` 时通过 `state = { ... }` 一次性更新所有状态,而不是分散的 9 个赋值。`transition` 字段记录了为什么要继续循环(便于调试和测试)。 + +### 1.5 queryLoop() 核心流程(第 241 行) + +`while (true)` 循环(第 307 行)的每次迭代代表一次 API 调用。循环直到: +- 模型不需要工具调用 → `return { reason: 'completed' }` +- 被用户中断 → `return { reason: 'aborted_*' }` +- 达到最大轮次 → `return { reason: 'max_turns' }` +- 遇到不可恢复的错误 → `return { reason: 'model_error' }` + +#### 步骤 1:消息预处理 + +``` +每次迭代开头: + ├── 解构 state → messages, toolUseContext, tracking, ... + ├── getMessagesAfterCompactBoundary() — 只保留压缩边界后的消息 + ├── snip 处理(feature flag,跳过) + ├── microcompact 处理(feature flag,跳过) + └── autocompact 检查 — 消息过长时自动压缩 +``` + +#### 步骤 2:系统提示构建(第 449 行) + +```ts +const fullSystemPrompt = asSystemPrompt( + appendSystemContext(systemPrompt, systemContext), +) +``` + +将系统上下文(git 状态、日期等)追加到系统提示。注意:用户上下文(CLAUDE.md 等)不在这里注入,而是在 `deps.callModel()` 调用时通过 `prependUserContext(messagesForQuery, userContext)` 注入到消息数组的最前面(第 660 行)。 + +#### 步骤 3:Autocompact(第 454-543 行) + +当消息历史过长时自动压缩: + +``` +autocompact 流程: + ├── 检查 token 数量是否超过阈值 + ├── 超过 → 调用 compact API(用 Haiku 总结历史) + │ ├── yield compactBoundaryMessage ← 标记压缩边界 + │ └── 更新 messages 为压缩后的版本 + └── 未超过 → 继续 +``` + +#### 步骤 4:调用 API(第 559-708 行)— 核心 + +StreamingToolExecutor 在第 562 行初始化,API 调用在第 659 行开始: + +```ts +// 第 562 行:初始化流式工具执行器 +let streamingToolExecutor = useStreamingToolExecution + ? new StreamingToolExecutor( + toolUseContext.options.tools, canUseTool, toolUseContext, + ) + : null + +// 第 659 行:调用 API +for await (const message of deps.callModel({ + messages: prependUserContext(messagesForQuery, userContext), // ← 用户上下文注入到消息最前面 + systemPrompt: fullSystemPrompt, + thinkingConfig: toolUseContext.options.thinkingConfig, + tools: toolUseContext.options.tools, + signal: toolUseContext.abortController.signal, + options: { model: currentModel, querySource, fallbackModel, ... } +})) { + // 处理每条流式消息(第 708-866 行) +} +``` + +`deps.callModel()` 最终调用 `claude.ts` 的 `queryModelWithStreaming()`。 + +#### 步骤 5:流式响应处理(第 708-866 行) + +处理逻辑在 `for await` 循环体内(第 708 行的 `})` 之后到第 866 行): + +``` +for await (const message of stream): + ├── message.type === 'assistant'? + │ ├── 记录到 assistantMessages[] + │ ├── 提取 tool_use 块 → toolUseBlocks[] + │ ├── needsFollowUp = true(如果有 tool_use) + │ └── streamingToolExecutor.addTool() ← 流式工具并行执行 + │ + ├── withheld? (prompt-too-long / max_output_tokens) + │ └── 暂扣不 yield,等后面恢复逻辑处理 + │ + └── yield message ← 正常 yield 给上层(REPL/QueryEngine) +``` + +**StreamingToolExecutor**:在 API 流式返回的同时就开始执行工具(如读文件),不等流结束。通过 `addTool()` 添加待执行工具,`getCompletedResults()` 获取已完成的结果。 + +#### 步骤 6A:无 followUp — 终止/恢复(第 1065-1360 行) + +当模型没有请求工具调用时(`needsFollowUp === false`): + +``` +无 followUp: + ├── prompt-too-long 恢复? + │ ├── context collapse drain(feature flag,跳过) + │ ├── reactive compact → 压缩消息重试 + │ └── 都失败 → yield 错误 + return + │ + ├── max_output_tokens 恢复? + │ ├── 第一次 → 升级到 64k token 限制,continue + │ ├── 后续 → 注入恢复消息("继续,别道歉"),continue + │ └── 超过 3 次 → yield 错误 + return + │ + ├── stop hooks 执行 + │ ├── preventContinuation? → return + │ └── blockingErrors? → 将错误加入消息,continue + │ + └── return { reason: 'completed' } ★ 正常结束 +``` + +**恢复消息内容(第 1229 行)**: +``` +"Output token limit hit. Resume directly — no apology, no recap of what +you were doing. Pick up mid-thought if that is where the cut happened. +Break remaining work into smaller pieces." +``` + +#### 步骤 6B:有 followUp — 工具执行 + 下一轮(第 1363-1731 行) + +当模型请求了工具调用时(`needsFollowUp === true`): + +``` +有 followUp: + ├── 工具执行(两种模式) + │ ├── streamingToolExecutor? → getRemainingResults()(流式已启动) + │ └── 否 → runTools()(传统顺序执行) + │ + ├── for await (const update of toolUpdates): + │ ├── yield update.message ← 工具结果消息 + │ └── toolResults.push(...) ← 收集工具结果 + │ + ├── 中断检查(abortController.signal.aborted) + │ └── return { reason: 'aborted_tools' } + │ + ├── attachment 注入 + │ ├── 排队命令(其他线程提交的消息) + │ ├── 内存预取(相关记忆文件) + │ └── 技能发现预取 + │ + ├── maxTurns 检查 + │ └── 超过 → yield max_turns_reached + return + │ + └── state = { messages: [...old, ...assistant, ...toolResults], turnCount: +1 } + → continue ★ 回到循环顶部,发起下一次 API 调用 +``` + +### 1.6 错误处理与模型降级(第 897-956 行) + +``` +API 调用出错: + ├── FallbackTriggeredError(529 过载)? + │ ├── 切换到 fallbackModel + │ ├── 清空本轮 assistant/tool 消息 + │ ├── yield 系统消息 "Switched to X due to high demand for Y" + │ └── continue(重试整个请求) + │ + └── 其他错误 + ├── ImageSizeError/ImageResizeError → yield 友好错误 + return + ├── yieldMissingToolResultBlocks() — 补全未配对的 tool_result + └── yield API 错误消息 + return +``` + +### 1.7 关键设计思想 + +| 设计 | 说明 | +|------|------| +| **AsyncGenerator 模式** | `query()` 是 `async function*`,通过 `yield` 逐条产出事件,调用者用 `for await` 消费 | +| **while(true) + state 对象** | 每次 `continue` 构建新 State 对象,避免分散的状态修改 | +| **transition 字段** | 记录为什么要 continue(`next_turn`、`max_output_tokens_recovery`、`reactive_compact_retry`...),便于调试 | +| **StreamingToolExecutor** | API 流式返回时就并行执行工具,不等流结束 | +| **Withheld 消息** | 可恢复错误先暂扣,恢复成功则不 yield 错误,失败才 yield | + +--- + +## 2. QueryEngine.ts(1320 行)— 高层编排器 + +**文件路径**: `src/QueryEngine.ts` + +### 2.1 定位 + +QueryEngine 是 `query()` 的**上层包装**,主要用于: +- **print 模式**(`claude -p`):通过 `ask()` → `QueryEngine.submitMessage()` +- **SDK 模式**:外部程序通过 SDK 调用 +- **REPL 不用它**:REPL 直接调用 `query()` + +### 2.2 文件结构 + +``` +QueryEngine.ts (1320 行) +├── [0-130] Import 区 + feature flag 条件模块 +├── [131-174] QueryEngineConfig 类型定义 +├── [185-1202] QueryEngine 类 +│ ├── [185-208] 成员变量 + constructor +│ ├── [210-1181] submitMessage() — 核心方法(~970 行) +│ │ ├── [210-400] 参数解析 + processUserInputContext 构建 +│ │ ├── [400-465] 用户输入处理 + 会话持久化 +│ │ ├── [465-660] 斜杠命令处理 + 无需查询的快速返回 +│ │ ├── [660-690] 文件历史快照 +│ │ ├── [679-1074] ★ for await (const message of query({...})) — 消费 query() +│ │ └── [1074-1181] 结果提取 + yield result +│ ├── [1183-1202] interrupt() / getMessages() / setModel() 辅助方法 +├── [1210-1320] ask() — 便捷包装函数 +``` + +### 2.3 QueryEngineConfig + +```ts +type QueryEngineConfig = { + cwd: string // 工作目录 + tools: Tools // 工具列表 + commands: Command[] // 斜杠命令 + mcpClients: MCPServerConnection[] // MCP 服务器连接 + agents: AgentDefinition[] // Agent 定义 + canUseTool: CanUseToolFn // 权限检查 + getAppState / setAppState // 全局状态存取 + initialMessages?: Message[] // 初始消息(恢复对话) + readFileCache: FileStateCache // 文件读取缓存 + customSystemPrompt?: string // 自定义系统提示 + thinkingConfig?: ThinkingConfig // 思考模式配置 + maxTurns?: number // 最大轮次 + maxBudgetUsd?: number // USD 预算上限 + jsonSchema?: Record<...> // 结构化输出 schema + // ... 更多配置 +} +``` + +### 2.4 submitMessage() 核心流程 + +``` +submitMessage(prompt) + │ + ├── 1. 参数准备 + │ ├── 解构 config 获取 tools, commands, model, ... + │ ├── 构建 wrappedCanUseTool(包装权限检查,跟踪拒绝) + │ ├── fetchSystemPromptParts() — 获取系统提示各部分 + │ └── 构建 processUserInputContext + │ + ├── 2. 用户输入处理 + │ ├── processUserInput(prompt) — 解析斜杠命令 / 普通文本 + │ ├── mutableMessages.push(...messagesFromUserInput) + │ └── recordTranscript(messages) — 持久化到 JSONL + │ + ├── 3. yield buildSystemInitMessage() — SDK 初始化消息 + │ + ├── 4. shouldQuery === false?(斜杠命令的本地执行结果) + │ ├── yield 命令输出 + │ ├── yield { type: 'result', subtype: 'success' } + │ └── return + │ + ├── 5. ★ for await (const message of query({...})) + │ │ 消费 query() 产出的每条消息 + │ │ + │ ├── message.type === 'assistant' + │ │ ├── mutableMessages.push(msg) + │ │ ├── recordTranscript() ← fire-and-forget + │ │ ├── yield* normalizeMessage(msg) — 转换为 SDK 格式 + │ │ └── 捕获 stop_reason + │ │ + │ ├── message.type === 'user'(工具结果) + │ │ ├── mutableMessages.push(msg) + │ │ ├── turnCount++ + │ │ └── yield* normalizeMessage(msg) + │ │ + │ ├── message.type === 'stream_event' + │ │ ├── 跟踪 usage(message_start/delta/stop) + │ │ └── includePartialMessages? → yield 流事件 + │ │ + │ ├── message.type === 'system' + │ │ ├── compact_boundary → GC 旧消息 + yield 给 SDK + │ │ └── api_error → yield 重试信息 + │ │ + │ └── maxBudgetUsd 检查 → 超预算则 yield error + return + │ + └── 6. yield { type: 'result', subtype: 'success', result: textResult } +``` + +### 2.5 ask() 便捷函数(第 1211 行) + +```ts +export async function* ask({ prompt, tools, ... }) { + const engine = new QueryEngine({ ... }) + try { + yield* engine.submitMessage(prompt) + } finally { + setReadFileCache(engine.getReadFileState()) + } +} +``` + +`ask()` 是 `QueryEngine` 的一次性包装,创建 engine → 提交消息 → 清理。用于 `print.ts` 的 `--print` 模式。 + +### 2.6 QueryEngine vs REPL 直接调用 query() + +| 特性 | QueryEngine (SDK/print) | REPL 直接调用 query() | +|------|------------------------|---------------------| +| 会话持久化 | 自动 recordTranscript | 由 useLogMessages 处理 | +| Usage 跟踪 | 内部 totalUsage 累积 | 由外层 cost-tracker 处理 | +| 权限拒绝跟踪 | 记录 permissionDenials[] | 直接 UI 交互 | +| 结果格式 | yield SDKMessage 格式 | 原始 Message 格式 | +| 消息 GC | compact_boundary 后释放旧消息 | UI 需要保留完整历史 | + +--- + +## 3. claude.ts(3420 行)— API 客户端 + +**文件路径**: `src/services/api/claude.ts` + +### 3.1 文件结构 + +``` +claude.ts (3420 行) +├── [0-260] Import 区(大量 SDK 类型、工具函数) +├── [272-331] getExtraBodyParams() — 构建额外请求体参数 +├── [333-502] 缓存相关(getPromptCachingEnabled, getCacheControl, should1hCacheTTL, configureEffortParams, configureTaskBudgetParams) +├── [504-587] verifyApiKey() — API 密钥验证 +├── [589-675] 消息转换(userMessageToMessageParam, assistantMessageToMessageParam) +├── [677-708] Options 类型定义 +├── [710-781] queryModelWithoutStreaming / queryModelWithStreaming — 公开的两个入口 +├── [783-813] 辅助函数(shouldDeferLspTool, getNonstreamingFallbackTimeoutMs) +├── [819-918] executeNonStreamingRequest() — 非流式请求辅助 +├── [920-999] 更多辅助函数(getPreviousRequestIdFromMessages, stripExcessMediaItems) +├── [1018-3420] ★ queryModel() — 核心私有函数(2400 行) +│ ├── [1018-1370] 前置检查 + 工具 schema 构建 + 消息归一化 + 系统提示组装 +│ ├── [1539-1730] paramsFromContext() — 构建 API 请求参数 +│ ├── [1777-2100] withRetry + 流式 API 调用(anthropic.beta.messages.create + stream) +│ ├── [1941-2300] 流式事件处理(for await of stream) +│ └── [2300-3420] 非流式降级 + 日志、分析、清理 +``` + +### 3.2 两个公开入口 + +```ts +// 入口 1:流式(主要路径) +export async function* queryModelWithStreaming({ + messages, systemPrompt, thinkingConfig, tools, signal, options +}) { + yield* withStreamingVCR(messages, async function* () { + yield* queryModel(messages, systemPrompt, thinkingConfig, tools, signal, options) + }) +} + +// 入口 2:非流式(compact 等内部用途) +export async function queryModelWithoutStreaming({ + messages, systemPrompt, thinkingConfig, tools, signal, options +}) { + let assistantMessage + for await (const message of ...) { + if (message.type === 'assistant') assistantMessage = message + } + return assistantMessage +} +``` + +两者都委托给内部的 `queryModel()`。`withStreamingVCR` 是一个 VCR(录像/回放)包装器,用于调试。 + +### 3.3 Options 类型(第 677 行) + +```ts +type Options = { + getToolPermissionContext: () => Promise + model: string // 模型名称 + toolChoice?: BetaToolChoiceTool // 强制使用特定工具 + isNonInteractiveSession: boolean // 是否非交互模式 + fallbackModel?: string // 备用模型 + querySource: QuerySource // 查询来源 + agents: AgentDefinition[] // Agent 定义 + enablePromptCaching?: boolean // 启用提示缓存 + effortValue?: EffortValue // 推理努力级别 + mcpTools: Tools // MCP 工具 + fastMode?: boolean // 快速模式 + taskBudget?: { total: number; remaining?: number } // 令牌预算 +} +``` + +### 3.4 queryModel() 核心流程(第 1018 行) + +这是整个 API 调用的核心,2400 行。关键步骤: + +#### 阶段 1:前置准备(1018-1400 行) + +``` +queryModel() + ├── off-switch 检查(Opus 过载时的全局关闭开关) + ├── beta headers 组装(getMergedBetas) + │ ├── 基础 betas + │ ├── advisor beta(如果启用) + │ ├── tool search beta(如果启用) + │ ├── cache scope beta + │ └── effort / task budget betas + │ + ├── 工具过滤 + │ ├── tool search 启用 → 只包含已发现的 deferred tools + │ └── tool search 未启用 → 过滤掉 ToolSearchTool + │ + ├── toolToAPISchema() — 每个工具转为 API 格式 + │ + ├── normalizeMessagesForAPI() — 消息转换为 API 格式 + │ ├── UserMessage → { role: 'user', content: ... } + │ ├── AssistantMessage → { role: 'assistant', content: ... } + │ └── 跳过 system/attachment/progress 等内部消息类型 + │ + └── 系统提示最终组装 + ├── getAttributionHeader(fingerprint) + ├── getCLISyspromptPrefix() + ├── ...systemPrompt + └── advisor 指令(如果启用) +``` + +#### 阶段 2:构建请求参数 — paramsFromContext()(第 1539-1730 行) + +```ts +const paramsFromContext = (retryContext: RetryContext) => { + // ... 动态 beta headers、effort、task budget 配置 ... + + // 思考模式配置(adaptive 或 enabled + budget) + let thinking = undefined + if (hasThinking && modelSupportsThinking(options.model)) { + if (modelSupportsAdaptiveThinking(options.model)) { + thinking = { type: 'adaptive' } + } else { + thinking = { type: 'enabled', budget_tokens: thinkingBudget } + } + } + + return { + model: normalizeModelStringForAPI(options.model), + messages: addCacheBreakpoints(messagesForAPI, ...), // 带缓存标记的消息 + system, // 系统提示块(已构建好) + tools: allTools, // 工具 schema + tool_choice: options.toolChoice, + max_tokens: maxOutputTokens, + thinking, + ...(temperature !== undefined && { temperature }), + ...(useBetas && { betas: betasParams }), + metadata: getAPIMetadata(), + ...extraBodyParams, + ...(speed !== undefined && { speed }), // 快速模式 + } +} +``` + +#### 阶段 3:流式 API 调用(第 1779-1858 行) + +```ts +// 使用 withRetry 包装,自动处理重试 +const generator = withRetry( + () => getAnthropicClient({ maxRetries: 0, model, source: querySource }), + async (anthropic, attempt, context) => { + const params = paramsFromContext(context) + + // ★ 核心 API 调用(第 1823 行) + // 使用 .create() + stream: true(而非 .stream()) + // 避免 BetaMessageStream 的 O(n²) partial JSON 解析开销 + const result = await anthropic.beta.messages + .create( + { ...params, stream: true }, + { signal, ...(clientRequestId && { headers: { ... } }) }, + ) + .withResponse() + + return result.data // Stream + }, + { model, fallbackModel, thinkingConfig, signal, querySource } +) + +// 消费 withRetry 的系统错误消息(重试通知等) +let e +do { + e = await generator.next() + if (!('controller' in e.value)) yield e.value // yield API 错误消息 +} while (!e.done) +stream = e.value // 获取最终的 Stream 对象 + +// 处理流式事件(第 1941 行) +for await (const part of stream) { + switch (part.type) { + case 'message_start': // 记录 request_id、usage + case 'content_block_start': // 新的内容块开始(text/thinking/tool_use) + case 'content_block_delta': // 增量内容 → yield stream_event 给 UI + case 'content_block_stop': // 内容块完成 → yield AssistantMessage + case 'message_delta': // stop_reason、usage 更新 + case 'message_stop': // 整条消息完成 + } +} +``` + +#### 阶段 4:withRetry 重试策略 + +``` +withRetry 逻辑: + ├── 429 (Rate Limit) → 等待 Retry-After 后重试 + ├── 529 (Overloaded) → 切换到 fallbackModel,throw FallbackTriggeredError + ├── 500 (Server Error) → 指数退避重试 + ├── 408 (Timeout) → 重试 + ├── 其他错误 → 不重试,直接抛出 + └── 最大重试次数: 根据模型和错误类型动态计算 +``` + +#### 阶段 5:非流式降级 + +当流式请求中途失败时,可能降级为非流式请求: + +``` +流式失败(部分响应已收到): + ├── 已接收的内容 → yield 给上层 + ├── 剩余部分 → 降级为非流式请求(anthropic.beta.messages.create) + └── 非流式结果 → 转换格式 yield +``` + +### 3.5 消息转换函数 + +```ts +// UserMessage → API 格式 +userMessageToMessageParam(message, addCache, enablePromptCaching, querySource) + → { role: 'user', content: [...] } + // addCache=true 时最后一个 content block 添加 cache_control + +// AssistantMessage → API 格式 +assistantMessageToMessageParam(message, addCache, enablePromptCaching, querySource) + → { role: 'assistant', content: [...] } + // thinking/redacted_thinking 块不加 cache_control +``` + +### 3.6 Prompt Caching 策略 + +``` +缓存策略: + ├── cache_control: { type: 'ephemeral' } — 默认,5 分钟 TTL + ├── cache_control: { type: 'ephemeral', ttl: '1h' } — 订阅用户/Ant,1 小时 + ├── cache_control: { ..., scope: 'global' } — 跨会话共享(无 MCP 工具时) + └── 禁用条件: + ├── DISABLE_PROMPT_CACHING 环境变量 + ├── DISABLE_PROMPT_CACHING_HAIKU(仅 Haiku) + └── DISABLE_PROMPT_CACHING_SONNET(仅 Sonnet) +``` + +### 3.7 多 Provider 支持 + +`getAnthropicClient()` 根据配置返回不同的 SDK 客户端: + +| Provider | 入口 | 说明 | +|----------|------|------| +| Anthropic | 直接 API | 默认,`api.anthropic.com` | +| AWS Bedrock | 通过 Bedrock | 使用 `@anthropic-ai/bedrock-sdk` | +| Google Vertex | 通过 Vertex | 使用 `@anthropic-ai/vertex-sdk` | +| Azure | 通过 Azure | 类似 Bedrock 的包装 | + +Provider 选择逻辑在 `src/utils/model/providers.ts` 的 `getAPIProvider()` 中。 + +--- + +## 完整数据流:一次工具调用的生命周期 + +以用户输入 "读取 README.md" 为例: + +``` +1. REPL.tsx: 用户按回车 + onSubmit("读取 README.md") + └── handlePromptSubmit() + └── onQuery([userMessage]) + +2. REPL.tsx: onQueryImpl() + ├── getSystemPrompt() + getUserContext() + getSystemContext() + └── for await (event of query({messages, systemPrompt, ...})) + +3. query.ts: queryLoop() — 第 1 次迭代 + ├── messagesForQuery = [...messages] // 包含用户消息 + ├── deps.callModel({...}) + │ └── claude.ts: queryModel() + │ ├── 构建 API 参数 + │ └── anthropic.beta.messages.create({ ...params, stream: true }) + │ + ├── API 流式返回: + │ content_block_start: { type: 'tool_use', name: 'Read', id: 'toolu_123' } + │ content_block_delta: { input: '{"file_path": "/path/to/README.md"}' } + │ content_block_stop + │ message_delta: { stop_reason: 'tool_use' } + │ + ├── 收集: toolUseBlocks = [{ name: 'Read', id: 'toolu_123', input: {...} }] + ├── needsFollowUp = true + │ + ├── 工具执行: + │ streamingToolExecutor.getRemainingResults() + │ └── Read 工具执行 → 返回文件内容 + │ yield toolResultMessage ← 包含文件内容 + │ + └── state = { messages: [...old, assistantMsg, toolResultMsg], turnCount: 2 } + → continue + +4. query.ts: queryLoop() — 第 2 次迭代 + ├── messagesForQuery 现在包含: + │ [userMsg, assistantMsg(tool_use), userMsg(tool_result)] + │ + ├── deps.callModel({...}) ← 再次调用 API + │ + ├── API 返回: + │ content_block_start: { type: 'text' } + │ content_block_delta: { text: "README.md 的内容是..." } + │ content_block_stop + │ message_delta: { stop_reason: 'end_turn' } + │ + ├── toolUseBlocks = [] ← 没有工具调用 + ├── needsFollowUp = false + │ + └── return { reason: 'completed' } ★ 循环结束 + +5. REPL.tsx: onQueryEvent(event) + ├── 更新 streamingText(打字机效果) + ├── 更新 messages 数组 + └── 重新渲染 UI +``` + +--- + +## 关键设计模式总结 + +| 模式 | 位置 | 说明 | +|------|------|------| +| AsyncGenerator 链式传递 | query.ts → claude.ts | `yield*` 将底层事件透传给上层,形成事件流管道 | +| while(true) + State 对象 | query.ts queryLoop | 循环迭代间通过不可变 State 传递,transition 字段记录原因 | +| StreamingToolExecutor | query.ts | API 流式返回时并行执行工具,不等流结束 | +| Withheld 消息 | query.ts | 可恢复错误先暂扣不 yield,恢复成功则吞掉错误 | +| withRetry 重试 | claude.ts | 429/500/529 自动重试,529 触发模型降级 | +| Prompt Caching | claude.ts | 缓存系统提示和历史消息,减少 API token 消耗 | +| 非流式降级 | claude.ts | 流式请求中途失败时降级为非流式完成剩余部分 | +| QueryEngine 包装 | QueryEngine.ts | 为 SDK/print 提供会话管理、持久化、usage 跟踪 | + +## 需要忽略的代码 + +| 模式 | 说明 | +|------|------| +| `feature('REACTIVE_COMPACT')` / `feature('CONTEXT_COLLAPSE')` 等 | 所有 feature flag 保护的代码 — 全部是死代码 | +| `feature('CACHED_MICROCOMPACT')` | 缓存微压缩 — 死代码 | +| `feature('HISTORY_SNIP')` / `snipModule` | 历史截断 — 死代码 | +| `feature('TOKEN_BUDGET')` / `budgetTracker` | 令牌预算 — 死代码 | +| `feature('BG_SESSIONS')` / `taskSummaryModule` | 后台会话 — 死代码 | +| `process.env.USER_TYPE === 'ant'` | Anthropic 内部专用代码 | +| VCR (withStreamingVCR/withVCR) | 调试录像/回放包装器,不影响正常流程 | \ No newline at end of file diff --git a/learn/phase-2-qa.md b/learn/phase-2-qa.md new file mode 100644 index 000000000..4e2e3a3df --- /dev/null +++ b/learn/phase-2-qa.md @@ -0,0 +1,372 @@ +# 第二阶段 Q&A + +## Q1:query.ts 的流式消息处理具体是怎样的? + +**核心问题**:`deps.callModel()` yield 出的每一条消息,在 `queryLoop()` 的 `for await` 循环体(L659-866)中具体经历了什么处理? + +### 场景 + +用户说:**"帮我看看 package.json 的内容"** + +模型回复:一段文字 "我来读取文件。" + 一个 Read 工具调用。 + +### callModel yield 的完整消息序列 + +claude.ts 的 `queryModel()` 会 yield 两种类型的消息: + +| 类型标记 | 含义 | 产出时机 | +|---------|------|---------| +| `stream_event` | 原始 SSE 事件包装 | 每个 SSE 事件都产出一条 | +| `assistant` | 完整的 AssistantMessage | 仅在 `content_block_stop` 时产出 | + +本例中 callModel 依次 yield **共 13 条消息**: + +``` +#1 { type: 'stream_event', event: { type: 'message_start', ... }, ttftMs: 342 } +#2 { type: 'stream_event', event: { type: 'content_block_start', index: 0, content_block: { type: 'text' } } } +#3 { type: 'stream_event', event: { type: 'content_block_delta', index: 0, delta: { type: 'text_delta', text: '我来' } } } +#4 { type: 'stream_event', event: { type: 'content_block_delta', index: 0, delta: { type: 'text_delta', text: '读取文件。' } } } +#5 { type: 'stream_event', event: { type: 'content_block_stop', index: 0 } } +#6 { type: 'assistant', uuid: 'uuid-1', message: { content: [{ type: 'text', text: '我来读取文件。' }], stop_reason: null } } +#7 { type: 'stream_event', event: { type: 'content_block_start', index: 1, content_block: { type: 'tool_use', id: 'toolu_001', name: 'Read' } } } +#8 { type: 'stream_event', event: { type: 'content_block_delta', index: 1, delta: { type: 'input_json_delta', partial_json: '{"file_path":' } } } +#9 { type: 'stream_event', event: { type: 'content_block_delta', index: 1, delta: { type: 'input_json_delta', partial_json: '"/path/package.json"}' } } } +#10 { type: 'stream_event', event: { type: 'content_block_stop', index: 1 } } +#11 { type: 'assistant', uuid: 'uuid-2', message: { content: [{ type: 'tool_use', id: 'toolu_001', name: 'Read', input: { file_path: '/path/package.json' } }], stop_reason: null } } +#12 { type: 'stream_event', event: { type: 'message_delta', delta: { stop_reason: 'tool_use' }, usage: { output_tokens: 87 } } } +#13 { type: 'stream_event', event: { type: 'message_stop' } } +``` + +注意 `#6` 和 `#11` 是 **assistant 类型**(content_block_stop 时由 claude.ts 组装),其余全是 **stream_event 类型**。 + +### 循环体结构 + +循环体在 L708-866,结构如下: + +``` +for await (const message of deps.callModel({...})) { // L659 + // A. 降级检查 (L712) + // B. backfill (L747-789) + // C. withheld 检查 (L801-824) + // D. yield (L825-827) + // E. assistant 收集 + addTool (L828-848) + // F. getCompletedResults (L850-865) +} +``` + +### 逐条走循环体 + +#### #1 stream_event (message_start) + +``` +A. L712: streamingFallbackOccured = false → 跳过 + +B. L748: message.type === 'assistant'? + → 'stream_event' !== 'assistant' → 跳过整个 backfill 块 + +C. L801-824: withheld 检查 + → 不是 assistant 类型,各项检查均为 false → withheld = false + +D. L825: yield message ✅ → 透传给 REPL(REPL 记录 ttftMs) + +E. L828: message.type === 'assistant'? → 否 → 跳过 + +F. L850-854: streamingToolExecutor.getCompletedResults() + → tools 数组为空 → 无结果 +``` + +**净效果**:`yield` 透传。 + +--- + +#### #2 stream_event (content_block_start, type: text) + +``` +A-C. 同 #1 +D. yield message ✅ → REPL 设置 spinner 为 "Responding..." +E-F. 同 #1 +``` + +**净效果**:`yield` 透传。 + +--- + +#### #3 stream_event (text_delta: "我来") + +``` +A-C. 同 #1 +D. yield message ✅ → REPL 追加 streamingText += "我来"(打字机效果) +E-F. 同 #1 +``` + +**净效果**:`yield` 透传。 + +--- + +#### #4 stream_event (text_delta: "读取文件。") + +``` +同 #3 +D. yield message ✅ → REPL streamingText += "读取文件。" +``` + +**净效果**:`yield` 透传。 + +--- + +#### #5 stream_event (content_block_stop, index:0) + +``` +同 #2 +D. yield message ✅ → REPL 无特殊操作(真正的 AssistantMessage 在下一条 #6) +``` + +**净效果**:`yield` 透传。 + +--- + +#### #6 assistant (text block 完整消息) ★ + +第一条 `type: 'assistant'` 的消息,走**完全不同的路径**: + +``` +A. L712: streamingFallbackOccured = false → 跳过 + +B. L748: message.type === 'assistant'? → ✅ 进入 backfill + L750: contentArr = [{ type: 'text', text: '我来读取文件。' }] + L752: for i=0: block.type === 'text' + L754: block.type === 'tool_use'? → 否 → 跳过 + L783: clonedContent 为 undefined → yieldMessage = message(原样不变) + +C. L801: let withheld = false + L802: feature('CONTEXT_COLLAPSE') → false → 跳过 + L813: reactiveCompact?.isWithheldPromptTooLong(message) → 否 → false + L822: isWithheldMaxOutputTokens(message) + → message.message.stop_reason === null → false + → withheld = false + +D. L825: yield message ✅ → REPL 清除 streamingText,添加完整 text 消息到列表 + +E. L828: message.type === 'assistant'? → ✅ + L830: assistantMessages.push(message) + → assistantMessages = [uuid-1(text)] + + L832-834: msgToolUseBlocks = content.filter(type === 'tool_use') + → [](这是 text block,没有 tool_use) + + L835: length > 0? → 否 → 不设 needsFollowUp + L844: msgToolUseBlocks 为空 → 不调用 addTool + +F. L854: getCompletedResults() → 空 +``` + +**净效果**:`yield` 消息 + `assistantMessages` 增加一条。`needsFollowUp` 仍为 `false`。 + +--- + +#### #7 stream_event (content_block_start, tool_use: Read) + +``` +A-C. 同 stream_event 通用路径 +D. yield message ✅ → REPL 设置 spinner 为 "tool-input",添加 streamingToolUse +E. 不是 assistant → 跳过 +F. getCompletedResults() → 空 +``` + +--- + +#### #8 stream_event (input_json_delta: '{"file_path":') + +``` +D. yield message ✅ → REPL 追加工具输入 JSON 碎片 +F. getCompletedResults() → 空 +``` + +--- + +#### #9 stream_event (input_json_delta: '"/path/package.json"}') + +``` +D. yield message ✅ +F. getCompletedResults() → 空 +``` + +--- + +#### #10 stream_event (content_block_stop, index:1) + +``` +D. yield message ✅ +F. getCompletedResults() → 空 +``` + +--- + +#### #11 assistant (tool_use block 完整消息) ★★ + +这条是**最关键的**——触发工具执行: + +``` +A. L712: streamingFallbackOccured = false → 跳过 + +B. L748: message.type === 'assistant'? → ✅ 进入 backfill + L750: contentArr = [{ type: 'tool_use', id: 'toolu_001', name: 'Read', + input: { file_path: '/path/package.json' } }] + L752: for i=0: + L754: block.type === 'tool_use'? → ✅ + L756: typeof block.input === 'object' && !== null? → ✅ + L759: tool = findToolByName(tools, 'Read') → Read 工具定义 + L763: tool.backfillObservableInput 存在? → 假设存在 + L764-766: inputCopy = { file_path: '/path/package.json' } + tool.backfillObservableInput(inputCopy) + → 可能添加 absolutePath 字段 + L773-776: addedFields? → 假设有新增字段 + clonedContent = [...contentArr] + clonedContent[0] = { ...block, input: inputCopy } + L783-788: yieldMessage = { + ...message, // uuid, type, timestamp 不变 + message: { + ...message.message, // stop_reason, usage 不变 + content: clonedContent // ★ 替换为带 absolutePath 的副本 + } + } + // ★ 原始 message 保持不变(回传 API 保证缓存一致) + +C. L801-824: withheld 检查 → 全部 false → withheld = false + +D. L825: yield yieldMessage ✅ + → yield 的是克隆版(带 backfill 字段),给 REPL 和 SDK 用 + → 原始 message 下面存进 assistantMessages,回传 API 保证缓存一致 + +E. L828: message.type === 'assistant'? → ✅ + L830: assistantMessages.push(message) // ★ push 原始 message,不是 yieldMessage + → assistantMessages = [uuid-1(text), uuid-2(tool_use)] + + L832-834: msgToolUseBlocks = content.filter(type === 'tool_use') + → [{ type: 'tool_use', id: 'toolu_001', name: 'Read', input: {...} }] + + L835: length > 0? → ✅ + L836: toolUseBlocks.push(...msgToolUseBlocks) + → toolUseBlocks = [Read_block] + L837: needsFollowUp = true // ★★★ 决定 while(true) 不会终止 + + L840-842: streamingToolExecutor 存在 ✓ && !aborted ✓ + L844-846: for (const toolBlock of msgToolUseBlocks): + streamingToolExecutor.addTool(Read_block, uuid-2消息) + // ★★★ 工具开始执行! + // → StreamingToolExecutor 内部: + // isConcurrencySafe = true(Read 是安全的) + // queued → processQueue() → canExecuteTool() → true + // → executeTool() → runToolUse() → 后台异步读文件 + +F. L850-854: getCompletedResults() + → Read 刚开始执行,status = 'executing' → 无完成结果 +``` + +**净效果**: +- `yield` 克隆消息(带 backfill 字段) +- `assistantMessages` push 原始消息 +- `needsFollowUp = true` +- **Read 工具在后台异步开始执行** + +--- + +#### #12 stream_event (message_delta, stop_reason: 'tool_use') + +``` +A-C. 同 stream_event 通用路径 +D. yield message ✅ + +E. 不是 assistant → 跳过 + +F. L854: getCompletedResults() + → ★ 此时 Read 可能已经完成了!(读文件通常 <1ms) + → 如果完成: status = 'completed', results 有值 + L428(StreamingToolExecutor): tool.status = 'yielded' + L431-432: yield { message: UserMsg(tool_result) } + → 回到 query.ts: + L855: result.message 存在 + L856: yield result.message ✅ → REPL 显示工具结果 + L857-862: toolResults.push(normalizeMessagesForAPI([result.message])...) + → toolResults = [Read 的 tool_result] +``` + +**净效果**:`yield` stream_event + **可能 yield 工具结果**(如果工具已完成)。 + +--- + +#### #13 stream_event (message_stop) + +``` +D. yield message ✅ +F. getCompletedResults() + → 如果 Read 在 #12 已被收割 → 空 + → 如果 Read 此时才完成 → yield 工具结果(同 #12 的 F 逻辑) +``` + +--- + +### for await 循环退出后 + +``` +L1018: aborted? → false → 跳过 + +L1065: if (!needsFollowUp) + → needsFollowUp = true → 不进入 → 跳过终止逻辑 + +L1383: toolUpdates = streamingToolExecutor.getRemainingResults() + → 如果 Read 已在 #12/#13 被收割 → 立即返回空 + → 如果 Read 还没完成 → 阻塞等待 → 完成后 yield 结果 + +L1387-1404: for await (const update of toolUpdates) { + yield update.message → REPL 显示 + toolResults.push(...) → 收集 + } + +L1718-1730: 构建 next State: + state = { + messages: [ + ...messagesForQuery, // [UserMessage("帮我看看...")] + ...assistantMessages, // [AssistantMsg(text), AssistantMsg(tool_use)] + ...toolResults, // [UserMsg(tool_result)] + ], + turnCount: 1, + transition: { reason: 'next_turn' }, + } + → continue → while(true) 第 2 次迭代 → 带着工具结果再次调 API +``` + +### 循环体判定树总结 + +``` +for await (const message of deps.callModel(...)) { + │ + ├─ message.type === 'stream_event'? + │ │ + │ └─ YES → 几乎零操作 + │ ├─ yield message(透传给 REPL 做实时 UI) + │ └─ getCompletedResults()(顺便检查有没有完成的工具) + │ + └─ message.type === 'assistant'? + │ + ├─ B. backfill: 有 tool_use + backfillObservableInput? + │ ├─ YES → 克隆消息,yield 克隆版(原始消息保留给 API) + │ └─ NO → yield 原始消息 + │ + ├─ C. withheld: prompt_too_long / max_output_tokens? + │ ├─ YES → 不 yield(暂扣,等后面恢复逻辑处理) + │ └─ NO → yield + │ + ├─ E. assistantMessages.push(原始 message) + │ + ├─ E. 有 tool_use block? + │ ├─ YES → toolUseBlocks.push() + │ │ + needsFollowUp = true + │ │ + streamingToolExecutor.addTool() → ★ 立即开始执行工具 + │ └─ NO → 什么都不做 + │ + └─ F. getCompletedResults() → 收割已完成的工具结果 +} +``` + +**一句话总结**:stream_event 透传不处理;assistant 消息才是"真正的货"——收集起来、判断要不要暂扣、有工具就立即开始执行、顺便收割已完成的工具结果。 \ No newline at end of file