diff --git a/docs/context/system-prompt.mdx b/docs/context/system-prompt.mdx
index 002c8be..bc6e3de 100644
--- a/docs/context/system-prompt.mdx
+++ b/docs/context/system-prompt.mdx
@@ -1,36 +1,225 @@
---
title: "System Prompt 动态组装 - AI 工作记忆构建"
-description: "深入解析 Claude Code 的 System Prompt 动态组装过程:如何将 CLAUDE.md、项目上下文、工具定义和用户偏好拼装为 AI 的工作记忆。"
-keywords: ["System Prompt", "系统提示词", "动态组装", "CLAUDE.md", "上下文构建"]
+description: "深入解析 Claude Code 的 System Prompt 动态组装过程:缓存策略、分界标记、Section 注册表、CLAUDE.md 多级合并,以及如何将零散上下文拼装为 API 可消费的缓存友好结构。"
+keywords: ["System Prompt", "系统提示词", "动态组装", "CLAUDE.md", "Prompt Cache", "缓存策略"]
---
-{/* 本章目标:解释 System Prompt 的组装过程和设计思想 */}
+## 从数组到 API 调用:System Prompt 的完整链路
-## 什么是 System Prompt
+System Prompt 在 Claude Code 中不是一段写死的文本,而是一个 **`string[]` 数组**(品牌类型 `SystemPrompt`,定义于 `src/utils/systemPromptType.ts:8`),经过组装、分块、缓存标记后发送给 API。
-每次调用 AI API 时,都需要发送一个 System Prompt——它是 AI 的"人设说明书",告诉 AI:
+### 三阶段管道
-- 你是谁(Claude Code,一个编程助手)
-- 你能做什么(可用工具列表)
-- 你在什么环境(操作系统、当前目录、git 状态)
-- 你需要遵守什么规则(安全规范、输出格式)
+```
+getSystemPrompt() → string[] (组装内容)
+ ↓
+buildEffectiveSystemPrompt() → SystemPrompt (选择优先级路径)
+ ↓
+buildSystemPromptBlocks() → TextBlockParam[] (分块 + cache_control 标记)
+```
-## 不是静态模板,而是动态组装
+1. **`getSystemPrompt()`**(`src/constants/prompts.ts:444`)—— 收集静态段 + 动态段,插入 `SYSTEM_PROMPT_DYNAMIC_BOUNDARY` 分界标记
+2. **`buildEffectiveSystemPrompt()`**(`src/utils/systemPrompt.ts:41`)—— 按 Override > Coordinator > Agent > Custom > Default 优先级选择
+3. **`buildSystemPromptBlocks()`**(`src/services/api/claude.ts:3214`)—— 调用 `splitSysPromptPrefix()` 分块,为每个块附加 `cache_control`
-Claude Code 的 System Prompt 不是一段写死的文本,而是根据当前环境**实时组装**的:
+## SystemPrompt 品牌类型
-
-
-
+```typescript
+// src/utils/systemPromptType.ts:8
+export type SystemPrompt = readonly string[] & {
+ readonly __brand: 'SystemPrompt'
+}
+export function asSystemPrompt(value: readonly string[]): SystemPrompt {
+ return value as SystemPrompt // 零开销类型断言
+}
+```
-| 组成部分 | 内容 | 来源 |
-|----------|------|------|
-| 基础人设 | 角色定义、行为准则 | 内置模板 |
-| 环境信息 | 操作系统、shell 类型、当前日期 | 运行时检测 |
-| Git 状态 | 当前分支、最近提交、工作区状态 | `git` 命令输出 |
-| 项目知识 | CLAUDE.md 文件内容 | 项目目录层级扫描 |
-| 记忆文件 | 用户偏好、项目约定 | 持久化记忆系统 |
-| 工具说明 | 每个可用工具的描述和参数 | 工具注册表 |
+品牌类型(branded type)防止普通 `string[]` 被意外传入 API 调用——只有通过 `asSystemPrompt()` 显式转换才能获得 `SystemPrompt` 类型。
+
+## getSystemPrompt():内容组装的全景
+
+`src/constants/prompts.ts:444` 是 System Prompt 的核心工厂函数,返回一个有序数组:
+
+| 阶段 | 内容 | 缓存策略 |
+|------|------|----------|
+| **静态区** | Intro Section、System Rules、Doing Tasks、Actions、Using Tools、Tone & Style、Output Efficiency | 可跨组织缓存(`scope: 'global'`) |
+| **BOUNDARY** | `SYSTEM_PROMPT_DYNAMIC_BOUNDARY = '__SYSTEM_PROMPT_DYNAMIC_BOUNDARY__'` | 分界标记(不发送给 API) |
+| **动态区** | Session Guidance、Memory、Model Override、Env Info、Language、Output Style、MCP Instructions、Scratchpad、FRC、Summarize Tool Results、Token Budget、Brief | 每次会话不同(`scope: 'org'` 或无缓存) |
+
+### 动态区的 Section 注册表
+
+动态区通过 `systemPromptSection()` / `DANGEROUS_uncachedSystemPromptSection()` 注册,这两个工厂函数定义于 `src/constants/systemPromptSections.ts`:
+
+```typescript
+// 缓存式 Section:计算一次,/clear 或 /compact 后才重新计算
+systemPromptSection('memory', () => loadMemoryPrompt())
+
+// 危险:每轮重新计算,会破坏 Prompt Cache
+DANGEROUS_uncachedSystemPromptSection(
+ 'mcp_instructions',
+ () => isMcpInstructionsDeltaEnabled() ? null : getMcpInstructionsSection(mcpClients),
+ 'MCP servers connect/disconnect between turns' // 必须给出破坏缓存的理由
+)
+```
+
+`resolveSystemPromptSections()` 在每轮查询时解析所有 Section,对于 `cacheBreak: false` 的 Section,优先使用 `getSystemPromptSectionCache()` 中的缓存值。只有 MCP 指令等真正动态的内容使用 `DANGEROUS_uncachedSystemPromptSection`。
+
+### `CLAUDE_CODE_SIMPLE` 快速路径
+
+当环境变量 `CLAUDE_CODE_SIMPLE` 为真时,整个 System Prompt 缩减为一行:
+
+```typescript
+`You are Claude Code, Anthropic's official CLI for Claude.\n\nCWD: ${getCwd()}\nDate: ${getSessionStartDate()}`
+```
+
+跳过所有 Section 注册、缓存分块、动态组装——用于最小化 token 消耗的测试场景。
+
+## buildEffectiveSystemPrompt():五级优先级
+
+`src/utils/systemPrompt.ts:41` 决定最终使用哪个 System Prompt:
+
+| 优先级 | 条件 | 行为 |
+|--------|------|------|
+| **0. Override** | `overrideSystemPrompt` 非空 | 完全替换,返回 `[override]` |
+| **1. Coordinator** | `COORDINATOR_MODE` feature + 环境变量 | 使用协调者专用提示词 |
+| **2. Agent** | `mainThreadAgentDefinition` 存在 | Proactive 模式:追加到默认提示词尾部;否则:替换默认提示词 |
+| **3. Custom** | `--system-prompt` 参数指定 | 替换默认提示词 |
+| **4. Default** | 无特殊条件 | 使用 `getSystemPrompt()` 完整输出 |
+
+`appendSystemPrompt` 始终追加到末尾(Override 除外)。
+
+## 缓存策略:分块、标记、命中
+
+这是 System Prompt 设计中最精密的部分。
+
+### Anthropic Prompt Cache 基础
+
+Anthropic API 的 Prompt Cache 允许跨请求复用相同的 System Prompt 前缀,按缓存命中量计费(远低于完整输入价格)。缓存键由内容的 Blake2b 哈希决定——任何字符变化都会导致缓存失效。
+
+### `splitSysPromptPrefix()`:三种分块模式
+
+`src/utils/api.ts:321` 是缓存策略的核心,根据条件选择三种分块模式:
+
+#### 模式 1:MCP 工具存在时(`skipGlobalCacheForSystemPrompt=true`)
+
+```
+[attribution header] → cacheScope: null (不缓存)
+[system prompt prefix] → cacheScope: 'org' (组织级缓存)
+[everything else] → cacheScope: 'org' (组织级缓存)
+```
+
+MCP 工具列表在会话中可能变化(连接/断开),破坏了跨组织缓存的基础,因此降级为组织级。
+
+#### 模式 2:Global Cache + Boundary 存在(1P 专用)
+
+```
+[attribution header] → cacheScope: null (不缓存)
+[system prompt prefix] → cacheScope: null (不缓存)
+[static content] → cacheScope: 'global' (全局缓存!跨组织共享)
+[dynamic content] → cacheScope: null (不缓存)
+```
+
+这是缓存效率最高的模式。`SYSTEM_PROMPT_DYNAMIC_BOUNDARY` 之前的静态内容(Intro、Rules、Tone & Style 等)对所有用户相同,可跨组织缓存。
+
+#### 模式 3:默认(3P 提供商 或 Boundary 缺失)
+
+```
+[attribution header] → cacheScope: null (不缓存)
+[system prompt prefix] → cacheScope: 'org' (组织级缓存)
+[everything else] → cacheScope: 'org' (组织级缓存)
+```
+
+### `getCacheControl()`:TTL 决策
+
+`src/services/api/claude.ts:359` 生成的 `cache_control` 对象:
+
+```typescript
+{
+ type: 'ephemeral',
+ ttl?: '1h', // 仅特定 querySource 符合条件时
+ scope?: 'global', // 仅静态区
+}
+```
+
+1 小时 TTL 的判定逻辑(`should1hCacheTTL()`,第 394 行):
+- **Bedrock 用户**:通过环境变量 `ENABLE_PROMPT_CACHING_1H_BEDROCK` 启用
+- **1P 用户**:通过 GrowthBook 配置的 `allowlist` 数组匹配 `querySource`,支持前缀通配符(如 `"repl_main_thread*"`)
+- **会话级锁定**:资格判定结果在 bootstrap state 中缓存,防止 GrowthBook 配置中途变化导致同一会话内 TTL 不一致
+
+### 缓存破坏:Session-Specific Guidance 的放置
+
+`getSessionSpecificGuidanceSection()`(`src/constants/prompts.ts:352`)的内容必须放在 `SYSTEM_PROMPT_DYNAMIC_BOUNDARY` **之后**。因为它包含:
+- 当前会话的 enabledTools 集合
+- `isForkSubagentEnabled()` 的运行时判定
+- `getIsNonInteractiveSession()` 的结果
+
+这些运行时 bit 如果放在静态区,会产生 2^N 种 Blake2b 哈希变体(N = 运行时条件数),完全破坏缓存命中率。源码注释明确警告:
+
+> Each conditional here is a runtime bit that would otherwise multiply the Blake2b prefix hash variants (2^N). See PR #24490, #24171 for the same bug class.
+
+### `CLAUDE_CODE_SIMPLE` 模式
+
+当设置了 `CLAUDE_CODE_SIMPLE` 环境变量时,整个系统提示词会大幅缩减:
+
+```typescript
+return [`You are Claude Code, Anthropic's official CLI for Claude.\n\nCWD: ${getCwd()}\nDate: ${getSessionStartDate()}`]
+```
+
+## 上下文注入:System Context 与 User Context
+
+System Prompt 数组本身不包含运行时上下文(git 状态、CLAUDE.md 内容)。上下文通过两个独立的管道注入:
+
+### System Context(`src/context.ts:116`)
+
+```typescript
+export const getSystemContext = memoize(async () => {
+ return {
+ gitStatus, // git 分支、状态、最近提交(截断至 MAX_STATUS_CHARS=2000)
+ cacheBreaker, // 仅 ant 用户的缓存破坏器
+ }
+})
+```
+
+- 使用 `lodash.memoize` 缓存——**整个会话期间只计算一次**
+- Git 状态快照包含 5 个并行 `git` 命令(branch、defaultBranch、status、log、userName)
+- `status` 超过 2000 字符时截断并附加提示使用 BashTool 获取更多信息
+- `systemPromptInjection` 变更时,通过 `getUserContext.cache.clear?.()` 清除所有上下文缓存
+
+### User Context(`src/context.ts:155`)
+
+```typescript
+export const getUserContext = memoize(async () => {
+ return {
+ claudeMd, // 合并后的 CLAUDE.md 内容
+ currentDate, // "Today's date is YYYY-MM-DD."
+ }
+})
+```
+
+- **CLAUDE.md 禁用条件**:`CLAUDE_CODE_DISABLE_CLAUDE_MDS` 环境变量,或 `--bare` 模式(除非通过 `--add-dir` 显式指定目录)
+- `--bare` 模式的语义是"跳过我没要求的东西"而非"忽略所有"
+
+### 注入位置
+
+在 `src/query.ts:449`:
+
+```typescript
+// System Context 追加到 System Prompt 尾部
+const fullSystemPrompt = asSystemPrompt(
+ appendSystemContext(systemPrompt, systemContext) // 简单拼接
+)
+```
+
+User Context 通过 `prependUserContext()`(`src/utils/api.ts:449`)注入为 `` 标签包裹的首条用户消息,放在所有对话消息之前。
+
+## Attribution Header:计费与安全
+
+每个 API 请求的 System Prompt 首块是 Attribution Header(`src/constants/system.ts:30`),包含:
+- **`cc_version`**:Claude Code 版本 + 指纹
+- **`cc_entrypoint`**:入口点标识(REPL / SDK / pipe 等)
+- **`cch=00000`**(NATIVE_CLIENT_ATTESTATION 启用时):Bun 原生 HTTP 层在发送前将零替换为计算出的哈希值,服务器验证此 token 确认请求来自真实 Claude Code 客户端
+
+Header 始终 `cacheScope: null`——它因版本和指纹不同而变化,不适合缓存。
## CLAUDE.md:项目级知识注入
@@ -49,10 +238,15 @@ Claude Code 的 System Prompt 不是一段写死的文本,而是根据当前
└── /project/src/CLAUDE.md ← 子目录(模块特定)
```
-## 缓存策略
+加载逻辑在 `src/utils/claudemd.ts` 中的 `getClaudeMds()` 和 `getMemoryFiles()` 实现——从 CWD 向上遍历目录树,合并所有匹配的 CLAUDE.md 文件内容。
-System Prompt 的 token 消耗不小(可能占总量的 30%+)。为了降低成本,系统使用了缓存机制:
+## 设计洞察:为什么是 `string[]` 而非单个 `string`
-- 不变的部分(基础人设、工具说明)可以跨请求复用
-- 变化的部分(git 状态、记忆文件)每次重新生成
-- 缓存节点的位置经过精心设计,最大化缓存命中率
+将 System Prompt 设计为数组而非单段文本,是为了 **缓存分块**:
+
+1. Anthropic Prompt Cache 以 **内容块**(TextBlock)为缓存单位
+2. 将 System Prompt 拆为多个块,可以让不变的部分(Intro、Rules)获得独立的缓存命中
+3. 如果是单个 `string`,任何一个字符变化(如日期更新)都会导致整个 System Prompt 的缓存失效
+4. `SYSTEM_PROMPT_DYNAMIC_BOUNDARY` 标记允许 `splitSysPromptPrefix()` 精确地将静态区标记为 `scope: 'global'`,动态区不标记或标记为 `scope: 'org'`
+
+这是 Claude Code 在 token 成本优化上的核心设计——一次典型的 System Prompt 约 20K+ tokens,通过缓存分块可以节省 30-50% 的输入 token 费用。
diff --git a/docs/conversation/the-loop.mdx b/docs/conversation/the-loop.mdx
index 744e024..9166ad2 100644
--- a/docs/conversation/the-loop.mdx
+++ b/docs/conversation/the-loop.mdx
@@ -1,69 +1,182 @@
---
title: "Agentic Loop:AI 自主循环的核心机制"
-description: "深入解析 Claude Code 的 Agentic Loop 机制——AI 如何通过思考-行动-观察的循环,自主决策工具调用链,直到任务完成。包含源码级流程分析。"
-keywords: ["Agentic Loop", "AI 循环", "工具调用", "自主决策", "ReAct 模式"]
+description: "深入解析 Claude Code 的 query() 异步生成器循环——从流式 API 调用、工具并行执行、上下文压缩、错误恢复到终止条件的完整状态机,基于 src/query.ts 的源码级分析。"
+keywords: ["Agentic Loop", "query loop", "tool_use", "状态机", "auto-compact", "streaming", "recovery"]
---
-{/* 本章目标:解释 Agentic Loop 这个核心机制 */}
+{/* 本章目标:基于 src/query.ts 揭示 Agentic Loop 的完整状态机 */}
## 什么是 Agentic Loop
传统聊天机器人:你问一句,它答一句。
Claude Code 不一样:你说一个需求,它可能连续执行十几步操作才给你最终结果。
-这背后的机制叫做 **Agentic Loop**(智能体循环):
+这背后的机制叫做 **Agentic Loop**(智能体循环),核心实现在 `src/query.ts` 的 `queryLoop()` 异步生成器函数(第 241 行)。它是一个 `while(true)` 无限循环,每次迭代代表一次"思考→行动→观察"周期。
-
-
- AI 分析当前上下文,决定下一步该做什么
-
-
- AI 发出工具调用请求(比如"读取这个文件"、"执行这条命令")
-
-
- 工具执行完毕,结果回传给 AI
-
-
- AI 根据观察结果决定:继续下一步操作,还是任务已完成、直接回答用户
-
-
+## 循环的完整结构
-## 一个真实的例子
+`queryLoop()` 的每次迭代(`src/query.ts:307` `while(true)`)包含以下阶段:
-> 用户:"帮我找到项目里所有未使用的导入语句,然后删掉它们"
+### 阶段 1:上下文预处理(Pre-Processing Pipeline)
-AI 的内部过程:
+在调用 API 之前,依次执行 5 个压缩/优化步骤:
-1. **思考**:我需要先了解项目结构 → **行动**:调用 Glob 工具扫描所有源文件
-2. **观察**:拿到文件列表 → **思考**:逐个检查 → **行动**:调用 Grep 搜索 import 语句
-3. **观察**:发现 3 个文件有未使用导入 → **行动**:调用 FileEdit 逐个删除
-4. **观察**:编辑成功 → **结束**:告诉用户"已清理 3 个文件中的 5 条未使用导入"
+```
+messagesForQuery(原始消息)
+ ↓ applyToolResultBudget() — 工具结果预算截断(按 maxResultSizeChars)
+ ↓ snipCompactIfNeeded() — 历史 Snip 压缩(HISTORY_SNIP feature)
+ ↓ microcompact() — 微压缩(工具结果摘要)
+ ↓ applyCollapsesIfNeeded() — 上下文折叠(CONTEXT_COLLAPSE feature)
+ ↓ autocompact() — 自动压缩(超出阈值时触发)
+messagesForQuery(处理后的消息)→ 发往 API
+```
-整个过程可能涉及 10+ 次工具调用,但用户只需要说一句话。
+每个步骤的输出是下一步的输入,形成串行管道。Snip 和 Microcompact 的释放 token 数会传递给 autocompact 的阈值计算(`snipTokensFreed`),避免重复压缩。
+
+### 阶段 2:流式 API 调用(Streaming Loop)
+
+`deps.callModel()` 发起流式请求(第 659 行),返回一个 AsyncGenerator。在流式过程中:
+
+- **AssistantMessage** 被收集到 `assistantMessages[]` 数组
+- **tool_use 块** 被提取到 `toolUseBlocks[]`,设置 `needsFollowUp = true`
+- **StreamingToolExecutor** 在流式过程中就开始并行执行工具(不等流结束)
+- 可恢复的错误(prompt-too-long、max-output-tokens)被**暂扣**(withheld),先尝试恢复
+
+流式回调中的关键守卫:
+- `backfillObservableInput()`(第 763 行)—— 为 tool_use 块回填可观察字段(如文件路径展开),但只在添加了新字段时才克隆消息,避免破坏 prompt cache 的字节一致性
+- 流式降级检测——如果 `streamingFallbackOccured`,已收集的消息被标记为 tombstone(第 717 行),清空后重试
+
+### 阶段 3:工具执行(Tool Execution)
+
+如果 `needsFollowUp` 为 true,循环不会终止,而是执行工具:
+
+```typescript
+// 两种工具执行器(互斥)
+const toolUpdates = streamingToolExecutor
+ ? streamingToolExecutor.getRemainingResults() // 流式:获取已完成的+等待中的
+ : runTools(toolUseBlocks, assistantMessages, canUseTool, toolUseContext)
+```
+
+工具结果通过 `normalizeMessagesForAPI()` 标准化后,与原始消息合并,进入**下一轮循环迭代**。
+
+### 阶段 4:终止或继续
+
+每次迭代结束时,根据条件决定 `return`(终止)或 `continue`(继续):
+
+## 7 种终止条件(源码级)
+
+| 终止原因 | 触发位置 | 机制 |
+|----------|---------|------|
+| **completed** | 第 1360 行 | AI 未发出 tool_use → `needsFollowUp = false` → 经过 stop hooks → 返回 |
+| **blocking_limit** | 第 646 行 | Token 计数超过硬限制(非 autocompact 模式)→ 生成 PTL 错误消息 → 返回 |
+| **aborted_streaming** | 第 1054 行 | `abortController.signal.aborted` → 为未完成的 tool_use 生成合成 tool_result → 返回 |
+| **model_error** | 第 999 行 | `callModel()` 抛出异常 → 生成错误消息 → 返回 |
+| **prompt_too_long** | 第 1178 行 | 413 错误且 reactive compact 无法恢复 → 暂扣的错误消息被释放 → 返回 |
+| **image_error** | 第 980/1178 行 | 图片尺寸/大小错误 → 直接返回 |
+| **stop_hook_prevented** | 第 1282 行 | Stop hook 返回 `preventContinuation: true` → 返回 |
+
+## 4 种继续条件(恢复路径)
+
+循环不仅是一个简单的"有 tool_use 就继续",它还包含多种恢复/重试路径:
+
+### 1. 正常工具循环
+`needsFollowUp = true` → 执行工具 → 新消息追加到 `messagesForQuery` → `continue`
+
+### 2. max_output_tokens 恢复(第 1191-1255 行)
+当 AI 输出被截断时(`apiError === 'max_output_tokens'`):
+- **首次**:尝试将 `maxOutputTokens` 从默认值提升到 `ESCALATED_MAX_TOKENS`(64K),无 meta 消息,静默重试
+- **后续**:注入恢复消息"Output token limit hit. Resume directly...",最多重试 `MAX_OUTPUT_TOKENS_RECOVERY_LIMIT = 3` 次
+- 恢复耗尽后,暂扣的错误消息被释放
+
+### 3. Prompt-Too-Long 恢复(第 1088-1186 行)
+当遇到 413 错误时,有两个恢复阶段:
+- **Context Collapse Drain**(第 1097 行):提交所有已暂存的折叠,释放空间后重试。如果上一轮已经是 collapse_drain_retry 则跳过
+- **Reactive Compact**(第 1123 行):触发即时压缩,生成摘要后重试。`hasAttemptedReactiveCompact` 防止无限循环
+
+### 4. Stop Hook 阻塞重试(第 1285-1308 行)
+Stop hook 可以注入阻塞错误消息,强制 AI 重新思考。新的消息(包含阻塞错误)被追加到对话中,`stopHookActive = true`,进入下一轮迭代。
+
+## 模型降级(Fallback)
+
+当主模型不可用时(`FallbackTriggeredError`,第 897 行):
+
+1. 已收集的 `assistantMessages` 被清空,tool_use 块收到合成 tool_result:"Model fallback triggered"
+2. 思维签名块被移除(`stripSignatureBlocks`)—— 因为思维签名与模型绑定,跨模型回放会 400
+3. 切换到 `fallbackModel`,更新 `toolUseContext.options.mainLoopModel`
+4. 生成系统消息:"Switched to {fallback} due to high demand for {original}"
+5. 重新发起流式请求
+
+## 状态机:State 对象
+
+每次迭代的状态通过 `State` 类型(第 204 行)传递:
+
+```typescript
+type State = {
+ messages: Message[] // 当前对话消息
+ toolUseContext: ToolUseContext // 工具上下文(含权限)
+ autoCompactTracking: AutoCompactTrackingState // 压缩跟踪
+ maxOutputTokensRecoveryCount: number // 输出截断恢复计数
+ hasAttemptedReactiveCompact: boolean // 是否已尝试即时压缩
+ maxOutputTokensOverride: number | undefined // 输出 token 上限覆盖
+ pendingToolUseSummary: Promise<...> | undefined // 异步工具摘要
+ stopHookActive: boolean | undefined // Stop hook 是否激活
+ turnCount: number // 轮次计数
+ transition: Continue | undefined // 上一次继续的原因
+}
+```
+
+每次 `continue` 都创建新的 State 对象(不可变更新),而非就地修改。`transition` 字段记录了为什么继续——让后续迭代能检测特定恢复路径(如 `collapse_drain_retry`)避免循环。
+
+## Token Budget(实验性)
+
+当 `TOKEN_BUDGET` feature 启用时(第 1311 行),循环在终止前会检查 token 消耗:
+
+- **continuation**:未达到预算但超过阈值 → 注入 nudge 消息,让 AI 加速收尾
+- **diminishing_returns**:检测到收益递减 → 提前终止
+- 预算数据来自 `createBudgetTracker()`,跨迭代累计
## 为什么不是"一次规划,批量执行"
-一个常见的替代方案是:AI 先生成一个完整的计划,然后一次性执行所有步骤。Claude Code 选择了逐步循环,原因是:
+源码揭示了为什么 Claude Code 选择逐步循环:
-- **每一步都能看到真实结果**:文件内容、命令输出、错误信息——这些只有执行后才知道
-- **动态调整**:如果第 3 步发现了意外情况,AI 可以立刻改变策略
-- **错误恢复**:某步失败了,AI 可以当场诊断和修复,不需要推倒重来
-- **用户可控**:用户可以在任何一步中断,AI 的循环不会失控
+- **每一步都产生真实信息**:`runTools()` 返回的 `toolResults` 是 API 不可能预知的——命令输出、文件内容、错误信息
+- **动态上下文管理**:每轮迭代前都重新评估压缩需求(autocompact → microcompact → snip),基于最新的 token 计数
+- **错误即时恢复**:工具失败不需要推倒重来——stop hook 可以注入阻塞错误让 AI 修正策略
+- **用户可控**:`abortController.signal` 在循环的多个检查点被检测(第 1018、1048、1488 行),用户按 ESC 可以优雅中断
+- **成本控制**:Token Budget 在每轮终止前检查,防止 AI 无效循环
-## 循环的终止条件
+## 一个完整的迭代示例
-Agentic Loop 不会无限运行,以下情况会让循环停止:
+> 用户:"帮我找到项目里所有未使用的导入语句,然后删掉它们"
-| 条件 | 说明 |
-|------|------|
-| AI 主动结束 | AI 判断任务完成,返回纯文本回答(不再调用工具) |
-| 用户中断 | 用户按 Ctrl+C 或 ESC 打断当前操作 |
-| Token 预算耗尽 | 单轮对话的 token 用量达到上限 |
-| 输出过长自动续写 | AI 回复被截断时,系统自动发起续写请求(有次数上限) |
-| 成本上限 | 累计 API 花费超过用户设定的预算 |
+```
+迭代 1: 思考→行动
+ 预处理: 无需压缩(上下文很短)
+ API 调用: 返回 tool_use(Glob, "**/*.ts")
+ 工具执行: 返回 42 个文件路径
+ → needsFollowUp = true, continue
+
+迭代 2: 思考→行动
+ 预处理: 42 个文件结果仍在预算内
+ API 调用: 返回 tool_use(Grep, "import.*from")
+ 工具执行: 在 15 个文件中找到 120 条 import
+ → needsFollowUp = true, continue
+
+迭代 3: 思考→行动(多轮)
+ 预处理: 120 条 Grep 结果触发 microcompact → 摘要化
+ API 调用: 返回 3 个 tool_use(FileEdit, ...)
+ 工具执行: 删除 5 条未使用导入
+ → needsFollowUp = true, continue
+
+迭代 4: 总结
+ API 调用: 返回纯文本"已清理 3 个文件中的 5 条未使用导入"
+ → needsFollowUp = false
+ → Stop hooks 通过
+ → return { reason: 'completed' }
+```
diff --git a/docs/extensibility/skills.mdx b/docs/extensibility/skills.mdx
index 7243475..9614ea2 100644
--- a/docs/extensibility/skills.mdx
+++ b/docs/extensibility/skills.mdx
@@ -1,61 +1,221 @@
---
-title: "Skills 技能系统 - 可复用的 AI 工作流封装"
-description: "详解 Claude Code Skills 系统:如何将常用工作流封装为可复用的技能包,Skills 与 Tools 的区别,以及技能定义和加载机制。"
-keywords: ["Skills", "技能系统", "工作流封装", "可复用技能", "AI 工作流"]
+title: "Skills 技能系统 - Prompt 即能力的架构哲学"
+description: "深入剖析 Claude Code Skills 系统的完整实现:从磁盘加载、Frontmatter 解析、预算感知描述截断、双模式执行(inline/fork)、权限白名单、条件激活、动态发现到远程技能加载,揭示一条完整的 Skill 生命周期链路。"
+keywords: ["Skills", "SkillTool", "技能加载", "Frontmatter", "whenToUse", "allowedTools", "fork执行", "动态发现"]
---
-{/* 本章目标:解释 Skills 系统的设计思想 */}
+{/* 本章目标:揭示 Skill 系统从文件到执行的全链路实现 */}
-## Tool vs Skill
+## Tool vs Skill:本质差异
| | Tool | Skill |
|---|---|---|
| 粒度 | 单个原子操作(读文件、执行命令) | 一套完整的工作流(代码审查、创建 PR) |
-| 触发方式 | AI 自主选择 | 用户主动调用(`/skill-name`)或 AI 根据场景推荐 |
-| 本质 | 执行逻辑 | 预制的 Prompt + 工具权限配置 |
+| 触发方式 | AI 自主选择 | 用户 `/skill-name` 或 AI 通过 `SkillTool` 自动匹配 |
+| 本质 | TypeScript 执行逻辑 | **Prompt + 权限配置**的声明式封装 |
+| 注册位置 | `src/tools.ts` → `getTools()` | `src/commands.ts` → `getCommands()` |
+| 执行器 | 各 Tool 的 `call()` 方法 | `SkillTool.call()` → 两条分支(inline / fork) |
-## Skill 的三个来源
+Skill 的核心洞见:**复杂任务的关键不在代码逻辑,而在 Prompt 质量**。一个代码审查 Skill 不需要审查引擎,只需告诉 AI "审查什么、按什么顺序、输出什么格式"——Skill 把这种"经验"封装为可复用的 Markdown。
-
-
- 编译进 CLI 的技能包。如 `/commit`、`/review`、`/debug`
-
-
- 项目 `.claude/skills/` 目录中的 Markdown 文件。团队共享
-
-
- 通过 MCP Server 提供的技能。动态发现
-
-
+## Skill 的五个来源与加载链路
-## 一个 Skill 包含什么
+### 1. 内置命令(Built-in Commands)
-每个 Skill 本质上是一个"AI 行为的预设":
+硬编码在 `src/commands.ts:258` 的 `COMMANDS` memoize 数组中,包含 70+ 条命令(`/commit`、`/review`、`/compact` 等)。这些是 TypeScript 模块而非 Markdown,但实现了相同的 `Command` 接口(`src/types/command.ts`)。
-| 组成部分 | 作用 |
-|----------|------|
-| **名称和描述** | 告诉 AI 和用户这个技能做什么 |
-| **whenToUse** | 什么场景下应该使用这个技能(AI 据此自动推荐) |
-| **Prompt 模板** | 注入给 AI 的详细指令——相当于"操作手册" |
-| **allowedTools** | 这个技能允许使用哪些工具(能力边界) |
-| **model** | 可选指定使用的模型 |
+### 2. Bundled Skills(编译时打包)
-## 设计精妙之处
+通过 `registerBundledSkill()`(`src/skills/bundledSkills.ts:53`)在模块初始化时注册。关键特性:
-Skill 的核心洞见是:**很多复杂任务的关键不在于代码逻辑,而在于 Prompt 的质量**。
+- **延迟文件提取**:如果 Skill 声明了 `files`(参考文件),首次调用时才解压到临时目录(`getBundledSkillExtractDir()`),使用 `O_NOFOLLOW | O_EXCL` 防止符号链接攻击(`safeWriteFile`,第 186 行)
+- **闭包级 memoize**:并发调用共享同一个 extraction promise,避免竞态写入
+- 来源标记为 `source: 'bundled'`,在 Prompt 预算中享有**不可截断**的特权
-一个好的代码审查,不是写了什么代码来审查,而是:
-- 告诉 AI 审查的标准是什么
-- 告诉 AI 按什么顺序审查
-- 告诉 AI 输出什么格式的报告
-- 限制 AI 只能用读取类工具(不要边审查边改代码)
+### 3. 磁盘 Skills(`.claude/skills/`)
-Skill 把这些"经验"封装起来,任何人都能一键调用。
+由 `loadSkillsFromSkillsDir()`(`src/skills/loadSkillsDir.ts:407`)加载,这是最重要的加载路径:
-## 技能发现
+```
+管理策略: $MANAGED_DIR/.claude/skills/ (policySettings)
+用户全局: ~/.claude/skills/ (userSettings)
+项目级: .claude/skills/ (projectSettings, 向上遍历至 home)
+附加目录: --add-dir 指定的路径下 .claude/skills/
+```
-当可用技能很多时,AI 可以通过 **SkillTool** 搜索匹配的技能:
+**加载协议**:只识别 `skill-name/SKILL.md` 目录格式,不再支持单文件 `.md`。加载流程:
-- 用户说"帮我做代码审查"
-- AI 搜索已注册的技能,发现 `code-review` 匹配
-- AI 调用该技能,按预设的流程执行
+1. `readdir` 扫描目录 → 仅保留 `isDirectory()` 或 `isSymbolicLink()` 的条目
+2. 在每个子目录中查找 `SKILL.md`,未找到则跳过
+3. `parseFrontmatter()` 解析 YAML 头部,提取 `whenToUse`、`allowedTools`、`context` 等字段
+4. `parseSkillFrontmatterFields()`(第 185 行)统一解析 17 个 frontmatter 字段
+5. `createSkillCommand()`(第 270 行)构造 `Command` 对象
+
+**去重机制**:使用 `realpath()` 解析符号链接获得规范路径(`getFileIdentity`,第 118 行),避免通过符号链接或重叠父目录导致的重复加载。
+
+### 4. MCP Skills(动态发现)
+
+通过 `registerMCPSkillBuilders()` 注册构建器,MCP Server 的 prompt 被 `mcpSkillBuilders.ts` 转换为 `Command` 对象。标记为 `loadedFrom: 'mcp'`。
+
+**安全边界**:MCP Skills 的 Prompt 内容**禁止执行内联 shell 命令**(`loadSkillsDir.ts:374` 的 `loadedFrom !== 'mcp'` 守卫),因为远程内容不可信。
+
+### 5. Legacy Commands(`/commands/` 目录)
+
+向后兼容的旧格式,由 `loadSkillsFromCommandsDir()`(第 566 行)加载。同时支持 `SKILL.md` 目录格式和单 `.md` 文件格式。
+
+## Frontmatter 字段全景
+
+一个 `SKILL.md` 的完整 frontmatter(`parseSkillFrontmatterFields`,第 185 行):
+
+```yaml
+---
+name: code-review # 显示名称(覆盖目录名)
+description: 系统性代码审查 # 描述(或从 Markdown 首段提取)
+when_to_use: "用户说审查代码、找 bug" # AI 自动匹配依据
+allowed-tools: # 工具白名单
+ - Read
+ - Grep
+ - Glob
+argument-hint: "" # 参数提示
+arguments: [path] # 声明式参数名(用于 $ARGUMENTS 替换)
+model: opus # 模型覆盖
+effort: high # 努力级别
+context: fork # 执行模式:inline(默认)| fork
+agent: code-reviewer # 指定 Agent 定义文件
+user-invocable: true # 用户是否可 /调用
+disable-model-invocation: false # 禁止 AI 自主调用
+version: "1.0" # 版本号
+paths: # 条件激活的文件路径模式
+ - "src/**/*.ts"
+hooks: # Hook 配置
+ PreToolUse:
+ - command: ["echo", "checking"]
+shell: ["bash"] # Shell 执行环境
+---
+```
+
+解析后有 17 个字段被提取,其中 `allowedTools`、`model`、`effort` 在执行时动态修改 `toolPermissionContext`。
+
+## 两条执行路径:Inline vs Fork
+
+SkillTool(`src/tools/SkillTool/SkillTool.ts:332`)在 `call()` 中根据 `command.context` 分流:
+
+### Inline 模式(默认)
+
+Skill 的 Prompt 内容被注入为 **UserMessage**,在主对话流中继续执行:
+
+1. `processPromptSlashCommand()` 处理参数替换(`$ARGUMENTS`)和 shell 命令展开(`` !`...` ``)
+2. `${CLAUDE_SKILL_DIR}` 被替换为 Skill 所在目录的绝对路径
+3. `${CLAUDE_SESSION_ID}` 被替换为当前会话 ID
+4. 返回 `newMessages`(注入到对话流)+ `contextModifier`(修改权限上下文)
+
+`contextModifier`(第 776 行)做了三件事:
+- **工具白名单注入**:将 `allowedTools` 合并到 `alwaysAllowRules.command`
+- **模型切换**:`resolveSkillModelOverride()` 处理模型覆盖,保留 `[1m]` 后缀以避免 200K 窗口截断
+- **努力级别覆盖**:修改 `effortValue`
+
+### Fork 模式(`context: fork`)
+
+Skill 在**独立子 Agent** 中执行(`executeForkedSkill`,第 122 行):
+
+1. `prepareForkedCommandContext()` 构建隔离的 Agent 定义和 Prompt
+2. `runAgent()` 启动子 Agent 循环,拥有独立的 token 预算
+3. 通过 `onProgress` 回调报告工具使用进度
+4. 结果通过 `extractResultText()` 提取,子 Agent 的全部消息在提取后被释放(`agentMessages.length = 0`)
+5. 最终通过 `clearInvokedSkillsForAgent()` 清理状态
+
+Fork 模式适用于需要强隔离的场景(如长时间运行的审查任务),避免污染主对话的上下文。
+
+## 权限模型:Safe Properties 白名单
+
+`checkPermissions()`(第 433 行)实现了一个四层权限检查:
+
+```
+1. Deny 规则匹配(支持精确匹配和 prefix:* 通配符)
+ ↓ 未命中
+2. 官方市场 Skill 自动放行(plugin + isOfficialMarketplaceName)
+ ↓ 未命中
+3. Allow 规则匹配
+ ↓ 未命中
+4. Safe Properties 白名单检查(skillHasOnlySafeProperties,第 911 行)
+ ↓ 有非安全属性
+5. Ask 用户确认(附带精确匹配和前缀匹配两条建议规则)
+```
+
+**Safe Properties**(`SAFE_SKILL_PROPERTIES`,第 876 行)是一个包含 28 个属性名的白名单。任何不在白名单中的**有意义的属性值**(排除 `undefined`、`null`、空数组、空对象)都会触发权限请求。这是**正向安全**设计——未来新增的属性默认需要权限。
+
+## Prompt 预算:1% 上下文窗口的截断策略
+
+Skill 列表注入 System Prompt 时有严格的字符预算(`prompt.ts`):
+
+- **预算计算**:`contextWindowTokens × 4 chars/token × 1%`(约 8000 字符)
+- **单条上限**:`MAX_LISTING_DESC_CHARS = 250` 字符(超出截断为 `…`)
+- **Bundled Skills 不可截断**:它们始终保留完整描述,预算不足时只截断非 bundled 的
+- **降级策略**:
+ 1. 尝试完整描述 → 超预算?
+ 2. Bundled 保留完整,非 bundled 均分剩余预算 → 每条描述低于 20 字符?
+ 3. 非 bundled 仅保留名称
+
+`formatCommandsWithinBudget()`(`prompt.ts:70`)实现了这个三级降级。
+
+## 动态发现与条件激活
+
+### 基于文件路径的动态发现
+
+`discoverSkillDirsForPaths()`(`loadSkillsDir.ts:861`)在文件操作时触发:
+
+1. 从被操作的文件路径开始,**向上遍历**至 CWD(不包含 CWD 本身)
+2. 在每层查找 `.claude/skills/` 目录
+3. 使用 `realpath` 去重,`git check-ignore` 过滤 gitignored 目录
+4. 按路径深度排序(**深层优先**),更接近文件的 Skill 优先级更高
+
+### 条件激活(paths frontmatter)
+
+带有 `paths` 模式的 Skill 在加载时不会立即可用,而是存入 `conditionalSkills` Map。当被操作的文件路径匹配某个 Skill 的 paths 模式时(使用 `ignore` 库做 gitignore 风格匹配),该 Skill 才被**激活**——从 `conditionalSkills` 移入 `dynamicSkills`。
+
+这意味着一个只在 `*.test.ts` 上激活的测试 Skill,平时完全不可见,只有当 AI 读取或编辑测试文件时才会出现。
+
+## 使用频率排名
+
+`recordSkillUsage()`(`skillUsageTracking.ts`)使用指数衰减算法计算 Skill 排名分数:
+
+```
+score = usageCount × max(0.5^(daysSinceUse / 7), 0.1)
+```
+
+- **7 天半衰期**:一周前的使用权重减半
+- **最低 0.1 保底**:避免老但高频使用的 Skill 完全沉底
+- **60 秒去抖**:同一 Skill 在 1 分钟内的多次调用只计一次,减少文件 I/O
+
+排名数据持久化在全局配置的 `skillUsage` 字段中。
+
+## 远程技能加载(Experimental)
+
+通过 `EXPERIMENTAL_SKILL_SEARCH` feature flag 控制,支持从远程(AKI/GCS/S3)加载 `_canonical_` 格式的 Skill:
+
+1. `validateInput()` 中 `stripCanonicalPrefix()` 拦截 canonical 名称
+2. `executeRemoteSkill()`(第 970 行)从远程 URL 加载 SKILL.md
+3. 支持 `gs://`、`https://`、`s3://` 等 URL 协议
+4. 内容经过 frontmatter 剥离、`${CLAUDE_SKILL_DIR}` 替换后直接注入
+5. 通过 `addInvokedSkill()` 注册到 compaction 保留状态,确保压缩后仍可恢复
+6. 远程 Skill 不经过 `processPromptSlashCommand`——无 `!command` 替换、无 `$ARGUMENTS` 展开
+
+## 完整生命周期总结
+
+```
+磁盘 SKILL.md
+ ↓ parseFrontmatter()
+ ↓ parseSkillFrontmatterFields() → 17 个字段
+ ↓ createSkillCommand() → Command 对象
+ ↓ 去重(realpath + seenFileIds)
+ ↓ 条件 Skill → conditionalSkills Map(等待路径匹配激活)
+ ↓ getSkillDirCommands() memoize 缓存
+ ↓ getAllCommands() 合并 local + MCP
+ ↓ formatCommandsWithinBudget() → 截断后的 Skill 列表注入 System Prompt
+ ↓ AI 选择匹配的 Skill
+ ↓ SkillTool.validateInput() → 名称校验 + 存在性检查
+ ↓ SkillTool.checkPermissions() → 四层权限检查
+ ↓ SkillTool.call() → inline 或 fork 执行
+ ↓ contextModifier() → 注入 allowedTools + model + effort
+ ↓ recordSkillUsage() → 更新使用频率排名
+```
diff --git a/docs/introduction/architecture-overview.mdx b/docs/introduction/architecture-overview.mdx
index 7cfbbbb..dfbf6a3 100644
--- a/docs/introduction/architecture-overview.mdx
+++ b/docs/introduction/architecture-overview.mdx
@@ -1,7 +1,7 @@
---
title: "架构全景 - Claude Code 五层架构详解"
-description: "从交互层到基础设施层,详解 Claude Code 的五层架构设计。涵盖 React/Ink UI、QueryEngine 编排、Agentic Loop 核心循环、Tool 系统和 API 层。"
-keywords: ["Claude Code 架构", "五层架构", "QueryEngine", "Agentic Loop", "系统设计"]
+description: "从交互层到基础设施层,详解 Claude Code 的五层架构设计。基于 src/main.tsx、src/QueryEngine.ts、src/query.ts、src/tools.ts、src/services/api/claude.ts 的源码级数据流分析。"
+keywords: ["Claude Code 架构", "五层架构", "QueryEngine", "Agentic Loop", "数据流"]
---
{/* 本章目标:一张图讲清楚整体架构,为后续章节建立坐标系 */}
@@ -14,53 +14,99 @@ Claude Code 从上到下分为五个层次,每一层职责清晰、边界分
-| 层次 | 职责 | 关键词 |
-|------|------|--------|
-| **交互层** | 终端 UI、用户输入、消息展示 | React/Ink、REPL |
-| **编排层** | 管理多轮对话、会话生命周期、成本追踪 | QueryEngine、会话持久化 |
-| **核心循环层** | 单轮对话:发请求 → 拿响应 → 执行工具 → 再发请求 | Agentic Loop |
-| **工具层** | AI 的"双手"——读写文件、执行命令、搜索代码 | Tool System、MCP |
-| **通信层** | 与 Claude API 的流式通信、多云 Provider 适配 | Streaming、Bedrock/Vertex |
+| 层次 | 职责 | 入口源码 | 关键词 |
+|------|------|---------|--------|
+| **交互层** | 终端 UI、用户输入、消息展示 | `src/screens/REPL.tsx` | React/Ink、PromptInput |
+| **编排层** | 多轮对话、会话持久化、成本追踪 | `src/QueryEngine.ts` | QueryEngine、transcript |
+| **核心循环层** | 单轮:发请求 → 拿响应 → 执行工具 → 循环 | `src/query.ts` | Agentic Loop、State |
+| **工具层** | AI 的"双手"——读写文件、执行命令 | `src/tools.ts` → `src/Tool.ts` | Tool 接口、MCP |
+| **通信层** | 与 Claude API 的流式通信 | `src/services/api/claude.ts` | Streaming、Provider |
-## 一条主数据流
+## 一条主数据流的源码追踪
-整个系统的运转可以浓缩为一条核心数据流:
+整个系统的运转可以浓缩为一条核心数据流,以下是每一步对应的源码路径:
-
-
- 用户在终端键入自然语言需求
-
-
- 系统自动拼接项目信息、git 状态、配置文件、记忆,形成完整的 System Prompt
-
-
- 将 System Prompt + 对话历史发送给 Claude API,流式接收响应
-
-
- 若 AI 返回工具调用请求 → 权限检查 → 执行工具 → 结果回传 → AI 继续思考 → 循环
-
-
- AI 不再调用工具,输出最终回答,等待用户下一条输入
-
-
+### 1. 用户输入 → REPL
+
+`src/screens/REPL.tsx` 是基于 React/Ink 的终端 UI 组件。用户输入经 `processUserInput()`(`src/utils/processUserInput/processUserInput.ts`)处理,支持斜杠命令、文件附件、图片等。
+
+### 2. QueryEngine 编排
+
+`src/QueryEngine.ts` 是 REPL 与 `query()` 之间的中间层,管理:
+- **会话状态**:消息数组、工具权限上下文(`ToolPermissionContext`)、文件历史快照
+- **成本追踪**:`accumulateUsage()` / `getTotalCost()` 累计 token 用量
+- **Transcript 持久化**:`recordTranscript()` 将对话序列化到磁盘,支持 `--resume`
+- **文件历史**:`fileHistoryMakeSnapshot()` 在修改前创建快照,支持 undo
+
+关键方法:`queryEngine.query()` 构造 `QueryParams`,调用 `query()` 异步生成器。
+
+### 3. Agentic Loop(`src/query.ts`)
+
+`query()` 是一个 `AsyncGenerator`,`while(true)` 循环的每次迭代包含:
+
+```
+① 上下文预处理管道:
+ applyToolResultBudget → snipCompact → microcompact → contextCollapse → autocompact
+
+② 流式 API 调用:
+ deps.callModel() → AsyncGenerator
+ 收集 assistantMessages[]、toolUseBlocks[]
+
+③ 工具执行:
+ StreamingToolExecutor(并行) 或 runTools(串行)
+ → toolResults[]
+
+④ 终止/继续判定:
+ needsFollowUp ? continue : return { reason }
+```
+
+完整的状态机通过 `State` 类型(`src/query.ts:204`)在迭代间传递,包含 10 个字段(messages、autoCompactTracking、maxOutputTokensRecoveryCount 等)。
+
+### 4. 工具层(`src/tools.ts` → `src/Tool.ts`)
+
+`getAllBaseTools()`(`src/tools.ts:191`)组装 50+ 工具列表,经过 `filterToolsByDenyRules()` 权限过滤后传给 API。
+
+每个工具实现 `Tool` 接口(`src/Tool.ts:362`),核心方法链:
+```
+validateInput() → canUseTool()(UI 层)→ checkPermissions() → call() → ToolResult
+```
+
+### 5. 通信层(`src/services/api/claude.ts`)
+
+API 客户端支持 4 种 Provider:
+- **Anthropic Direct**:默认
+- **AWS Bedrock**:`ANTHROPIC_BEDROCK_BASE_URL`
+- **Google Vertex**:`ANTHROPIC_VERTEX_PROJECT_ID`
+- **Azure**:通过自定义 base URL
+
+`deps.callModel()` 发起流式请求,返回 `BetaRawMessageStreamEvent` 事件流。支持 Prompt Cache(`cache_control`)、thinking blocks、multi-turn tool use。
## 四个核心设计原则
- 所有 API 通信都是流式的——用户看到 AI "逐字打出"回答,而不是等待完整响应。工具执行结果也实时反馈。这不仅提升体验,更是处理长时间任务的工程必需。
+ 所有 API 通信都是流式的——`deps.callModel()` 返回 AsyncGenerator,用户看到 AI "逐字打出"回答。StreamingToolExecutor 在流式过程中就开始并行执行工具,不等流结束。模型降级(Fallback)时,已收集的 assistantMessages 被标记为 tombstone 并清空,重试整个流式请求。
- AI 的每一项"动手能力"都被抽象为一个 Tool。想让 AI 能做新事情?注册一个新 Tool 就好。统一的接口让能力可插拔、可组合。
+ 每个工具是 `Tool` 结构化类型,通过 `buildTool()` 工厂创建。`getTools()` 在每次 API 调用时组装(非全局缓存),因为 `isEnabled()` 可能随运行时状态变化。MCP 工具通过 `mcpInfo` 字段标记来源,支持 server 级别的 blanket deny。
- AI 能操作真实环境是强大的,也是危险的。每个工具调用都经过权限系统的裁决——该放行的自动放行,该拦截的坚决拦截。
+ 每次工具调用经过 `validateInput() → checkPermissions()` 双重检查。权限规则从 5 个来源汇聚(session → project → user → managed → default),支持工具名、命令模式、路径前缀等匹配方式。Plan Mode 通过 `prepareContextForPlanMode()` 切换为只读模式,退出时自动恢复。
- AI 没有真正的记忆,但通过精心的 System Prompt 组装、对话压缩、持久化记忆文件,系统营造出"AI 理解你的项目"的效果。这是上下文工程的核心。
+ System Prompt 由 `fetchSystemPromptParts()` 动态组装,包含 CLAUDE.md、git 状态、日期、MCP 服务器列表。Auto-compact 在每轮迭代前评估 token 阈值,超出时触发压缩。压缩后的摘要通过 `buildPostCompactMessages()` 替换原始消息,`taskBudgetRemaining` 跨压缩边界累计。
+
+## 入口与引导
+
+| 入口 | 文件 | 说明 |
+|------|------|------|
+| CLI 启动 | `src/entrypoints/cli.tsx` | 注入 `feature()` polyfill(始终返回 false)、MACRO 全局变量 |
+| 命令定义 | `src/main.tsx` | Commander.js 解析参数,初始化 auth/analytics/policy |
+| 一次性初始化 | `src/entrypoints/init.ts` | 遥测配置、信任对话框 |
+| 管道模式 | `src/main.tsx` `-p` flag | `echo "say hello" \| bun run dev -p` |
diff --git a/docs/safety/permission-model.mdx b/docs/safety/permission-model.mdx
index ada9ed0..87f7f79 100644
--- a/docs/safety/permission-model.mdx
+++ b/docs/safety/permission-model.mdx
@@ -1,69 +1,170 @@
---
title: "权限模型 - Allow/Ask/Deny 三级权限体系"
-description: "详解 Claude Code 的三级权限模型:Allow 自动放行、Ask 确认对话框、Deny 直接拒绝。权限规则的层级来源与优先级机制。"
-keywords: ["权限模型", "Allow Ask Deny", "权限控制", "安全权限", "工具权限"]
+description: "详解 Claude Code 的三级权限模型实现:基于 src/utils/permissions/permissions.ts 的规则匹配引擎、五层规则来源优先级、工具名/命令/路径三维度匹配、Denial Tracking 死循环防护、权限模式切换机制。"
+keywords: ["权限模型", "Allow Ask Deny", "PermissionRule", "checkPermissions", "Denial Tracking", "权限规则"]
---
-{/* 本章目标:详解权限系统的设计 */}
+{/* 本章目标:基于源码揭示权限系统的完整实现 */}
## 三种权限行为
每一次工具调用,系统都会做出三种裁决之一:
-| 行为 | 含义 | 典型场景 |
-|------|------|---------|
-| **Allow** | 自动放行,用户无感知 | Read 工具读取项目内的文件 |
-| **Ask** | 弹出确认对话框,等待用户决定 | Bash 执行一条未知命令 |
-| **Deny** | 直接拒绝,AI 收到"权限被拒"的反馈 | 尝试执行被禁止的命令 |
+| 行为 | 含义 | 返回类型 | 典型场景 |
+|------|------|---------|---------|
+| **Allow** | 自动放行,用户无感知 | `{ behavior: 'allow', updatedInput, decisionReason }` | Read 读取项目内文件 |
+| **Ask** | 弹出确认对话框 | `{ behavior: 'ask', message, suggestions, metadata }` | Bash 执行未知命令 |
+| **Deny** | 直接拒绝 | `{ behavior: 'deny', message, decisionReason }` | 尝试执行被禁止的命令 |
-## 权限规则的层级
+这些行为由 `PermissionResult` 类型定义(`src/utils/permissions/PermissionResult.ts`)。
-规则可以来自多个来源,优先级从高到低:
+## 权限规则的五层来源
-
-
-
+规则从 5 个来源汇聚(`PERMISSION_RULE_SOURCES`,`permissions.ts:109`),优先级从高到低:
-
-
- 用户在当前对话中手动授权的("对这个工具始终允许")
-
-
- 项目目录中的 `.claude/settings.json`,团队共享
-
-
- `~/.claude/settings.json`,跨项目生效
-
-
- 企业管理员下发的策略,用户不可覆盖
-
-
- 系统内置的基线规则
-
-
+```
+1. session — 用户在当前对话中手动授权("Always allow")
+2. cliArg — 命令行 --allow/--deny 参数
+3. command — Skill 工具的 allowedTools 白名单
+4. projectSettings — .claude/settings.json(团队共享)
+5. userSettings — ~/.claude/settings.json(跨项目)
+6. policySettings — 企业管理员下发的策略(用户不可覆盖)
+```
-## 规则的匹配方式
+每个来源维护三个数组:`alwaysAllowRules[source]`、`alwaysAskRules[source]`、`alwaysDenyRules[source]`。
-权限规则不是简单的"允许/禁止某个工具",它支持丰富的匹配条件:
+规则数据结构为 `PermissionRule`:
+```typescript
+{
+ source: PermissionRuleSource // 来自哪个层级
+ ruleBehavior: 'allow' | 'ask' | 'deny'
+ ruleValue: {
+ toolName: string // 如 "Bash"、"mcp__server1"
+ ruleContent?: string // 如 "git *"、"src/**"
+ }
+}
+```
-- **工具名匹配**:`"tool": "Bash"` → 针对所有 Bash 命令
-- **命令模式匹配**:`"command": "git *"` → 只针对 git 开头的命令
-- **路径匹配**:`"path": "src/**"` → 只允许操作 src 目录下的文件
-- **组合条件**:以上条件可以组合使用
+## 规则匹配引擎
+
+### 三维度匹配
+
+`permissions.ts` 实现了三种匹配维度:
+
+**1. 工具名匹配**(`toolMatchesRule()`,第 238 行)
+
+匹配整个工具,仅当规则没有 `ruleContent`:
+```typescript
+// 精确匹配
+rule "Bash" → 匹配 BashTool
+rule "mcp__server1" → 匹配该 MCP Server 的所有工具(server 级别)
+rule "mcp__server1__*" → 通配符匹配(同上)
+```
+
+MCP 工具使用 `getToolNameForPermissionCheck()` 获取匹配名称,支持有前缀(`mcp__server__tool`)和无前缀模式。
+
+**2. 命令模式匹配**(BashTool 的 `checkPermissions()`)
+
+BashTool 通过 `preparePermissionMatcher()`(`Tool.ts:514`)解析命令模式:
+```json
+{"tool": "Bash", "ruleContent": "git *"} → 匹配 "git commit -m 'fix'"
+```
+
+命令通过 AST 解析(`readOnlyValidation.ts` 使用 tree-sitter bash),提取第一个子命令进行匹配。
+
+**3. 路径匹配**(文件工具的 `checkPermissions()`)
+
+Read/Edit/Write 工具通过 `getPath()` 提取文件路径,与 `ruleContent` 中的 glob 模式匹配:
+```json
+{"tool": "Edit", "ruleContent": "src/**"} → 匹配 "src/utils/foo.ts"
+```
+
+### 权限检查的完整流程
+
+每次工具调用的权限检查(`canUseTool()` → `checkPermissions()`)经过以下步骤:
+
+```
+1a. Blanket deny 检查
+ getDenyRuleForTool() → 工具名完全匹配 deny 规则?
+ ↓ 命中 → deny(工具在 getTools() 阶段就被过滤掉)
+
+1b. Blanket allow 检查
+ toolAlwaysAllowedRule() → 工具名完全匹配 allow 规则?
+ ↓ 命中 → allow
+
+2. 工具自身 checkPermissions()
+ 各工具有自定义逻辑:
+ - BashTool: readOnlyValidation → sandbox 判定 → AST 解析 → 模式匹配
+ - FileEditTool: 路径白名单检查
+ - SkillTool: safe properties 白名单 + 精确/前缀匹配
+ ↓ 返回 PermissionResult
+
+3. Hook 系统
+ executePermissionRequestHooks() → PreToolUse hook 可以 override
+ ↓ hook 返回 deny → deny
+ ↓ hook 返回 ask → 升级为 ask
+
+4. Ask 规则检查
+ getAskRules() → 命中 → ask
+
+5. 默认行为
+ 根据当前 permissionMode 决定默认行为
+ - 'default': 大部分工具 ask
+ - 'plan': 写操作 deny,读操作 allow
+ - 'bypass': 全部 allow
+```
## 权限模式
-系统提供几种预设的权限模式,适应不同信任级别:
+| 模式 | `PermissionMode` 值 | 适用场景 | 行为 |
+|------|---------------------|---------|------|
+| **Default** | `'default'` | 日常使用 | 敏感操作逐一确认 |
+| **Plan Mode** | `'plan'` | 探索阶段 | 只能读不能写(`isReadOnly()` 检查) |
+| **Auto** | `'auto'` | 信任 AI | 通过 transcript classifier 自动决策 |
+| **Bypass** | `'bypassPermissions'` | 完全信任 | 所有操作自动放行(需显式 `--dangerously-skip-permissions`) |
-| 模式 | 适用场景 |
-|------|---------|
-| **Default** | 日常使用,敏感操作逐一确认 |
-| **Plan Mode** | 探索阶段,AI 只能读不能写 |
-| **Bypass** | 完全信任 AI,所有操作自动放行(需显式开启) |
+Plan Mode 切换由 `EnterPlanModeTool.call()` 触发:
+```typescript
+// EnterPlanModeTool.ts:88
+context.setAppState(prev => ({
+ ...prev,
+ toolPermissionContext: applyPermissionUpdate(
+ prepareContextForPlanMode(prev.toolPermissionContext),
+ { type: 'setMode', mode: 'plan', destination: 'session' },
+ ),
+}))
+```
-## Denial Tracking
+退出时由 `ExitPlanModeV2Tool` 恢复为之前的模式。
-系统不仅记录"允许了什么",还追踪"拒绝了什么":
+## Denial Tracking:死循环防护
-- 如果 AI 被连续拒绝多次同一类操作,系统会调整策略
-- 这防止 AI 陷入"反复请求同一个被拒操作"的死循环
+`src/utils/permissions/denialTracking.ts` 实现了拒绝追踪机制:
+
+```typescript
+const DENIAL_LIMITS = {
+ maxDenialsPerTool: 3, // 同一工具连续拒绝上限
+ cooldownPeriodMs: 30_000, // 冷却期 30 秒
+}
+```
+
+当 AI 被连续拒绝同一类操作达到上限时:
+1. `recordDenial()` 记录拒绝,增加计数
+2. `shouldFallbackToPrompting()` 检测到连续拒绝,返回 true
+3. 系统向 AI 注入消息:"Your previous tool call was rejected..."
+4. AI 被迫改变策略,避免"反复请求同一个被拒操作"的死循环
+
+操作成功时调用 `recordSuccess()` 重置计数。
+
+## 规则的运行时更新
+
+权限规则可以在运行时动态更新(`applyPermissionUpdate()`,`PermissionUpdate.ts`):
+
+```typescript
+type PermissionUpdate =
+ | { type: 'addRule', behavior, rule, destination }
+ | { type: 'removeRule', behavior, rule, destination }
+ | { type: 'setMode', mode, destination }
+```
+
+当用户在 Ask 对话框中选择 "Always allow",系统调用 `persistPermissionUpdates()` 将规则写入对应层级的 settings 文件(project/user/managed),同时更新内存中的 `toolPermissionContext`。
diff --git a/docs/safety/plan-mode.mdx b/docs/safety/plan-mode.mdx
index 8e37506..bc8739f 100644
--- a/docs/safety/plan-mode.mdx
+++ b/docs/safety/plan-mode.mdx
@@ -1,10 +1,10 @@
---
title: "计划模式 - Plan Mode 先看后做的安全机制"
-description: "解析 Claude Code Plan Mode 设计:给 AI 一个只读探索阶段,先分析再执行,避免不可逆操作,让用户在 AI 行动前审查方案。"
-keywords: ["Plan Mode", "计划模式", "只读模式", "安全执行", "预览方案"]
+description: "基于源码解析 Claude Code Plan Mode 的完整实现:EnterPlanModeTool/ExitPlanModeV2Tool 的工具设计、权限上下文切换机制、Prompt-based 权限请求、计划文件持久化、Teammate 审批流程。"
+keywords: ["Plan Mode", "计划模式", "EnterPlanMode", "ExitPlanMode", "prepareContextForPlanMode", "allowedPrompts"]
---
-{/* 本章目标:解释 Plan Mode 的设计理念 */}
+{/* 本章目标:基于源码揭示 Plan Mode 的完整实现 */}
## 问题场景
@@ -12,50 +12,140 @@ keywords: ["Plan Mode", "计划模式", "只读模式", "安全执行", "预览
## Plan Mode 的解决方案
-计划模式给对话加了一个"只读阶段":
+计划模式给对话加了一个"只读阶段",通过两个工具实现闭环:
-
- AI 自主判断任务需要规划(或用户主动触发),进入计划模式
+
+ AI 自主判断(或用户触发)任务需要规划,调用 `EnterPlanModeTool`(`src/tools/EnterPlanModeTool/EnterPlanModeTool.ts:36`)。该工具需要**用户审批**(`checkPermissions` 返回 `ask`)。
-
- 在这个阶段,AI 只能使用读取和搜索类工具——不能编辑文件、不能执行命令
+
+ 权限模式切换为 `'plan'`,AI 只能使用 `isReadOnly()` 为 true 的工具(Read、Grep、Glob、Agent 等)。写操作被自动拒绝。
-
- AI 把理解和方案写入计划文件,提交给用户审阅
+
+ AI 完成探索后,调用 `ExitPlanModeV2Tool`(`src/tools/ExitPlanModeTool/ExitPlanModeV2Tool.ts:147`),将计划文件提交给用户审阅。这是第二个**需要用户审批**的节点。
-
- 用户阅读计划,提出修改意见或直接批准
-
-
- 计划被批准后,AI 恢复全部工具权限,按计划执行
+
+ 用户批准后,权限模式恢复为进入前的状态,AI 按计划执行。
## 权限的自动收窄与恢复
-计划模式的精妙之处在于**自动改变权限上下文**:
+### 进入:`prepareContextForPlanMode()`
-- 进入时:系统自动保存当前权限模式,切换为"只读"
-- 退出时:系统自动恢复之前的权限模式
+`EnterPlanModeTool.call()`(第 77 行)的核心逻辑:
-AI 和用户都不需要手动调整权限设置。
+```typescript
+// 1. 记录转换状态(保存之前的模式)
+handlePlanModeTransition(currentMode, 'plan')
+
+// 2. 切换权限上下文为 plan 模式
+context.setAppState(prev => ({
+ ...prev,
+ toolPermissionContext: applyPermissionUpdate(
+ prepareContextForPlanMode(prev.toolPermissionContext),
+ { type: 'setMode', mode: 'plan', destination: 'session' },
+ ),
+}))
+```
+
+`prepareContextForPlanMode()`(`src/utils/permissions/permissionSetup.ts`)做了什么:
+- 创建新的 `ToolPermissionContext`,`mode` 设为 `'plan'`
+- 在 plan 模式下,工具的 `isReadOnly()` 检查成为唯一准入条件
+- 如果用户的默认模式是 `'auto'`,还会激活 classifier 的副作用
+
+### 退出:权限恢复 + Prompt-based 权限
+
+`ExitPlanModeV2Tool` 的退出逻辑做了两件关键的事:
+
+**1. 恢复权限模式**
+
+通过 `handlePlanModeTransition()` 和 `applyPermissionUpdate()` 恢复到进入前的模式。
+
+**2. 注入 Prompt-based 权限**
+
+这是 Plan Mode 最精妙的设计——AI 可以在计划中声明它需要执行的命令类别:
+
+```typescript
+// ExitPlanModeV2Tool 的 inputSchema
+allowedPrompts: z.array(z.object({
+ tool: z.enum(['Bash']),
+ prompt: z.string().describe('Semantic description, e.g. "run tests"'),
+})).optional()
+```
+
+当 AI 提交计划时,如果声明了 `allowedPrompts: [{ tool: 'Bash', prompt: 'run tests' }]`,用户批准后,"run tests" 这类 Bash 命令会被自动放行——不再需要逐个确认。
+
+## 计划文件的持久化
+
+计划内容被写入磁盘文件(由 `getPlanFilePath()` 确定路径),这与简单的"AI 说一段话然后开始执行"有本质区别:
+
+1. `ExitPlanModeV2Tool` 的 `normalizeToolInput` 从磁盘读取计划内容,注入到 `input.plan` 和 `input.planFilePath`
+2. 计划文件是用户**可编辑**的——用户可以在审批前修改 AI 的方案
+3. `planWasEdited` 字段标记用户是否修改了计划,影响后续的 tool_result 回显
+4. `persistFileSnapshotIfRemote()` 在远程场景下保存文件快照
+
+## Teammate 场景下的计划审批
+
+在 Agent Swarms(`isAgentSwarmsEnabled()`)模式下,计划审批有额外的协作流程:
+
+```typescript
+// 如果是 Teammate 角色
+if (isTeammate()) {
+ // 发送计划到 Team Leader 的 mailbox 等待审批
+ const requestId = generateRequestId()
+ writeToMailbox(getTeamName(), {
+ type: 'plan_approval_request',
+ plan, requestId, ...
+ })
+ // 返回 awaitingLeaderApproval: true
+ // Team Leader 审批后通过 mailbox 通知 Teammate
+}
+```
+
+这意味着在蜂群模式下,计划可能不是由直接用户审批,而是由 Team Leader 审批。
## 什么时候该用计划模式
-| 场景 | 是否需要计划 |
-|------|-------------|
-| 修复一个明确的 typo | 不需要,直接修 |
-| 添加一个简单的函数 | 不需要 |
-| 重构一个大模块 | 需要——先搞清楚影响范围 |
-| 涉及多个文件的 feature | 需要——先统一方案 |
-| 不确定该怎么做 | 需要——让 AI 先调研 |
+`EnterPlanModeTool` 的 Prompt(`src/tools/EnterPlanModeTool/prompt.ts`)定义了两套触发标准——外部版本更积极(鼓励规划),内部版本更克制(仅在真正模糊时使用):
+
+| 场景 | 外部版本 | 内部版本 |
+|------|---------|---------|
+| 修复 typo | 跳过 | 跳过 |
+| 添加删除按钮 | **进入**(涉及多个文件) | **跳过**(路径明确) |
+| 重构认证系统 | **进入** | **进入**(高影响重构) |
+| "开始做 X" | — | **跳过**(直接开始) |
+| 架构决策(Redis vs 内存缓存) | **进入** | **进入**(真正模糊) |
## 计划模式 + 任务系统
计划模式通常与任务系统配合使用:
-1. 在计划模式中,AI 把实施步骤创建为任务列表
+1. 在计划模式中,AI 把实施步骤创建为任务列表(`TodoWrite`)
2. 用户审批计划(包含任务列表)
3. 退出计划模式后,AI 按任务列表逐项执行
4. 用户可以通过任务列表追踪进度
+
+## 完整生命周期
+
+```
+用户: "重构这个模块"
+ ↓
+AI 判断需要规划 → 调用 EnterPlanModeTool
+ ↓ 用户审批(Ask 对话框)
+handlePlanModeTransition(default, 'plan') // 保存 default
+prepareContextForPlanMode() // 创建只读上下文
+ ↓
+AI 使用 Read/Grep/Glob/Agent 探索代码库
+ ↓ (可能 10+ 轮只读工具调用)
+AI 形成方案 → 调用 ExitPlanModeV2Tool({
+ allowedPrompts: [
+ { tool: 'Bash', prompt: 'run tests' },
+ { tool: 'Bash', prompt: 'install dependencies' }
+ ]
+})
+ ↓ 用户审批计划(可编辑计划文件)
+恢复权限模式 → 注入 prompt-based 权限
+ ↓
+AI 使用全部工具执行计划,"run tests" 等命令自动放行
+```
diff --git a/docs/tools/what-are-tools.mdx b/docs/tools/what-are-tools.mdx
index 14f4e96..c6e4d00 100644
--- a/docs/tools/what-are-tools.mdx
+++ b/docs/tools/what-are-tools.mdx
@@ -1,10 +1,10 @@
---
title: "工具系统设计 - AI 如何从说到做"
-description: "深入理解 Claude Code 的 Tool 抽象设计。每个工具是标准化的能力单元,包含名称、描述、输入 Schema 和执行函数。了解工具注册与调用机制。"
-keywords: ["工具系统", "Tool 抽象", "AI 工具", "function calling", "工具调用"]
+description: "深入理解 Claude Code 的 Tool 抽象设计:从类型定义、注册机制、调用链路到渲染系统,揭示 50+ 内置工具如何通过统一的 Tool 接口协同工作。"
+keywords: ["工具系统", "Tool 抽象", "AI 工具", "function calling", "buildTool", "getTools"]
---
-{/* 本章目标:让读者理解 Tool 抽象的设计思想 */}
+{/* 本章目标:基于 src/Tool.ts 和 src/tools.ts 揭示工具系统的完整架构 */}
## AI 为什么需要工具
@@ -14,33 +14,161 @@ keywords: ["工具系统", "Tool 抽象", "AI 工具", "function calling", "工
工具是 AI 的双手。AI 说"我想读这个文件",工具系统替它真正去读;AI 说"我想执行这条命令",工具系统替它真正去跑。
-## 一个工具长什么样
+## Tool 类型:35 个字段的统一接口
-每个工具都是一个标准化的"能力单元",包含四个要素:
+所有工具都实现 `src/Tool.ts:362` 的 `Tool` 类型。这不是一个 class,而是一个包含 35+ 字段的**结构化类型**(structural typing),任何满足该接口的对象就是一个工具:
-| 要素 | 说明 | 示例(FileRead 工具) |
-|------|------|----------------------|
-| **名称** | 工具的唯一标识 | `Read` |
-| **描述** | 告诉 AI 这个工具能做什么(AI 据此决定是否使用) | "读取本地文件系统中的文件" |
-| **参数定义** | 工具接受什么输入 | `file_path`(必填)、`offset`、`limit` |
-| **执行逻辑** | 工具被调用时实际做什么 | 读取文件内容并返回 |
+### 核心四要素
-## AI 如何选择工具
+| 字段 | 类型 | 说明 |
+|------|------|------|
+| `name` | `string` | 唯一标识(如 `Read`、`Bash`、`Agent`) |
+| `description()` | `(input) => Promise` | **动态描述**——根据输入参数返回不同描述(如 `Execute skill: ${skill}`) |
+| `inputSchema` | `z.ZodType` | Zod schema,定义参数类型和校验规则 |
+| `call()` | `(args, context, canUseTool, parentMessage, onProgress?) => Promise>` | 执行函数 |
-AI 不是从下拉菜单里选工具——它是根据**工具描述**和**当前任务**自主决策的:
+### 注册与发现
-1. 系统把所有可用工具的名称、描述、参数告诉 AI
-2. AI 在思考过程中决定"我需要用某个工具"
-3. AI 生成一个结构化的工具调用请求(工具名 + 参数)
-4. 系统执行工具,将结果返回给 AI
+| 字段 | 说明 |
+|------|------|
+| `aliases` | 别名数组(向后兼容重命名) |
+| `searchHint` | 3-10 词的短语,供 ToolSearch 关键词匹配(如 `"jupyter"` for NotebookEdit) |
+| `shouldDefer` | 是否延迟加载(配合 ToolSearch 按需加载) |
+| `alwaysLoad` | 永不延迟加载(如 SkillTool 必须在 turn 1 可见) |
+| `isEnabled()` | 运行时开关(如 PowerShellTool 检查平台) |
-
-工具描述的质量直接影响 AI 的决策准确性。一段好的描述不仅说明"能做什么",还说明"什么时候该用、什么时候不该用"。
-
+### 安全与权限
-## 50+ 内置工具
+| 字段 | 说明 |
+|------|------|
+| `validateInput()` | 输入校验(在权限检查之前),返回 `ValidationResult` |
+| `checkPermissions()` | 权限检查(在校验之后),返回 `PermissionResult` |
+| `isReadOnly()` | 是否只读操作(影响权限模式) |
+| `isDestructive()` | 是否不可逆操作(删除、覆盖、发送) |
+| `isConcurrencySafe()` | 相同输入是否可以并行执行 |
+| `preparePermissionMatcher()` | 为 Hook 的 `if` 条件准备模式匹配器 |
+| `interruptBehavior()` | 用户中断时的行为:`'cancel'` 或 `'block'` |
-Claude Code 内置了覆盖软件开发全流程的工具集:
+### 输出与渲染
+
+| 字段 | 说明 |
+|------|------|
+| `maxResultSizeChars` | 结果字符上限(超出则持久化到磁盘,如 `100_000`) |
+| `mapToolResultToToolResultBlockParam()` | 将 Output 映射为 API 格式的 `ToolResultBlockParam` |
+| `renderToolResultMessage()` | React 组件渲染工具结果到终端 |
+| `renderToolUseMessage()` | React 组件渲染工具调用过程 |
+| `backfillObservableInput()` | 在不破坏 prompt cache 的前提下回填可观察字段 |
+
+### 上下文与 Prompt
+
+| 字段 | 说明 |
+|------|------|
+| `prompt()` | 返回该工具的详细使用说明,注入到 System Prompt |
+| `outputSchema` | 输出 Zod schema(用于类型安全的结果处理) |
+| `getPath()` | 提取操作的文件路径(用于权限匹配和 UI 显示) |
+
+## 工具注册:`getTools()` 的分层组装
+
+`src/tools.ts` 的 `getAllBaseTools()`(第 191 行)是工具注册的核心:
+
+```
+固定工具(始终可用):
+ AgentTool, BashTool, FileReadTool, FileEditTool, FileWriteTool,
+ NotebookEditTool, WebFetchTool, WebSearchTool, TodoWriteTool,
+ AskUserQuestionTool, SkillTool, EnterPlanModeTool, ExitPlanModeV2Tool,
+ TaskOutputTool, BriefTool, ListMcpResourcesTool, ReadMcpResourceTool
+
+条件工具(运行时检查):
+ ← hasEmbeddedSearchTools() ? [] : [GlobTool, GrepTool]
+ ← isTodoV2Enabled() ? V2 Tasks : []
+ ← isWorktreeModeEnabled() ? Worktree : []
+ ← isAgentSwarmsEnabled() ? Teams : []
+ ← isToolSearchEnabled() ? ToolSearch: []
+ ← isPowerShellToolEnabled() ? PowerShell: []
+
+Feature-flag 工具:
+ ← feature('COORDINATOR_MODE') ? [coordinatorMode tools]
+ ← feature('KAIROS') ? [SleepTool, SendUserFileTool, ...]
+ ← feature('WEB_BROWSER_TOOL') ? [WebBrowserTool]
+ ← feature('HISTORY_SNIP') ? [SnipTool]
+
+Ant-only 工具:
+ ← process.env.USER_TYPE === 'ant' ? [REPLTool, ConfigTool, TungstenTool]
+```
+
+`getTools()`(第 269 行)在 `getAllBaseTools()` 基础上应用权限过滤:
+
+```typescript
+export const getTools = (permissionContext): Tools => {
+ const base = getAllBaseTools()
+ // 过滤 blanket deny 规则命中的工具
+ return filterToolsByDenyRules(base, permissionContext)
+}
+```
+
+**关键设计**:工具列表在每次 API 调用时组装(而非全局缓存),因为 `isEnabled()` 的结果可能随运行时状态变化。
+
+## `buildTool()` 工厂函数
+
+大多数工具通过 `buildTool()` 创建(`src/Tool.ts:721`),它是一个类型安全的构造器:
+
+```typescript
+export const BashTool: Tool<...> = buildTool({
+ name: 'Bash',
+ inputSchema: lazySchema(() => z.object({command: z.string(), ...})),
+ // ...其他字段
+}) satisfies ToolDef
+```
+
+`satisfies ToolDef` 确保编译时类型检查,`lazySchema` 延迟 Zod schema 解析(避免循环依赖)。
+
+## 工具调用的完整链路
+
+从 AI 发出 `tool_use` 到结果回传,经过以下步骤:
+
+```
+1. API 返回 tool_use block(包含 name + input)
+ ↓
+2. StreamingToolExecutor.addTool() / runTools()
+ ↓
+3. findToolByName() 查找工具
+ ↓
+4. validateInput() — 输入校验
+ ↓ 失败 → 返回错误 tool_result
+5. canUseTool() — 权限 UI(Ask 模式下弹确认)
+ ↓ 拒绝 → 返回拒绝 tool_result
+6. checkPermissions() — 规则匹配
+ ↓
+7. call() — 执行实际操作
+ ↓ onProgress() 回调实时更新 UI
+8. 返回 ToolResult