Route C: 一个工具的全生命周期 —— 以 BashTool 为线索串联 Claude Code 全架构
本文档用一条「线」串起整个 Claude Code 的架构:追踪 BashTool 从出生到死亡的全生命周期。
当用户输入 “请帮我查看 git log” 时,Claude Code 内部发生了什么?一个 Bash 工具调用是如何从无到有、被定义、注册、发送给 API、被 API 选择、通过权限检查、执行命令、返回结果、最终驱动下一轮对话的?
沿着这条链路,你会依次理解 Claude Code 的 6 大核心系统:
┌─────────────────────────────────────────────────────────────────┐│ ││ ① Tool 定义 → ② 注册 → ③ 发给 API → ④ API 选择工具 ││ ││ ↓ ││ ││ ⑤ 权限检查 → ⑥ Hook 拦截 → ⑦ 执行 → ⑧ 结果处理 ││ ││ ↓ ││ ││ ⑨ 消息组装 → ⑩ 状态转换 → ⑪ 继续/终止循环 ││ │└─────────────────────────────────────────────────────────────────┘- 第一站:Tool 接口 —— 一个工具长什么样
- 第二站:BashTool 实现 —— 具体工具怎么写
- 第三站:工具注册 —— 工具池怎么组装
- 第四站:Schema 转换 —— 工具如何变成 API 参数
- 第五站:Agent Loop —— 核心循环如何运转
- 第六站:权限系统 —— 自主性与安全性的平衡
- 第七站:Hook 系统 —— 可扩展的拦截点
- 第八站:工具执行管线 —— 从调度到执行
- 第九站:流式工具执行器 —— 边收边执行的并发引擎
- 第十站:结果处理与状态转换 —— 循环的闭合
- 架构洞察:10 个值得借鉴的 Agent 工程模式
1. Tool 接口
Section titled “1. Tool 接口”关键文件:src/Tool.ts
这是整个 Tool 系统的「契约」。所有工具(无论是内置的 Bash/Read/Write,还是 MCP 动态发现的外部工具)都必须实现这个接口。
interface Tool<Input, Output> { // ═══ 身份标识 ═══ name: string // 工具名,如 "Bash" aliases?: string[] // 别名(向后兼容) searchHint?: string // ToolSearch 搜索提示
// ═══ Schema 定义 ═══ inputSchema: ZodSchema<Input> // Zod 输入校验 schema inputJSONSchema?: JSONSchema // JSON Schema(MCP 工具用) outputSchema?: ZodSchema<Output> // 输出 schema(可选)
// ═══ 核心执行 ═══ call(input, context, canUseTool, parentMessage, onProgress) : Promise<ToolResult<Output>> // 执行工具 description(input, options): string // 工具描述(动态) prompt(options): string // 系统提示词(告诉模型怎么用这个工具)
// ═══ 安全分类 ═══ isReadOnly(input): boolean // 只读操作? isDestructive(input): boolean // 不可逆操作?(删除、发送等) isConcurrencySafe(input): boolean // 可并行执行? checkPermissions(input, context) // 权限检查 : Promise<PermissionResult> validateInput(input, context) // 输入合法性校验 : Promise<ValidationResult>
// ═══ UI 渲染 ═══ renderToolUseMessage(...) // 渲染「正在调用工具」 renderToolResultMessage(...) // 渲染「工具执行结果」 renderToolUseProgressMessage(...) // 渲染「执行进度」 renderToolUseErrorMessage(...) // 渲染「执行错误」
// ═══ 高级特性 ═══ shouldDefer?: boolean // 延迟加载(ToolSearch) alwaysLoad?: boolean // 强制包含在初始提示中 maxResultSizeChars?: number // 结果大小阈值(超出存磁盘) strict?: boolean // 结构化输出模式 isMcp?: boolean // 是否为 MCP 工具 toAutoClassifierInput(input): string // 安全分类器的紧凑表示 preparePermissionMatcher(input) // 权限规则匹配器 : (pattern: string) => boolean}事实:
- 接口包含 30+ 个方法/属性,覆盖了工具生命周期的每一个环节
inputSchema用 Zod,inputJSONSchema用 JSON Schema —— 因为内置工具用 Zod 做运行时校验,MCP 工具只有 JSON Schema
洞察:
- 安全三元组
isReadOnly / isDestructive / isConcurrencySafe是并发控制和权限决策的基础。只读工具可以并行执行,破坏性工具需要额外审批 shouldDefer机制解决了「50+ 工具全部放进 system prompt 太占 token」的问题 —— 非关键工具延迟加载,通过 ToolSearch 工具按需发现prompt()方法 不是静态字符串,而是每个工具自己生成的系统提示。这意味着每个工具可以教模型怎么正确使用自己
2. BashTool 实现
Section titled “2. BashTool 实现”关键文件:src/tools/BashTool/BashTool.tsx
BashTool 是 Claude Code 中最核心、最复杂的工具之一。看它如何实现 Tool 接口,可以理解工具开发的范式。
输入/输出 Schema
Section titled “输入/输出 Schema”// 输入 —— 用户(模型)能传什么参数const inputSchema = z.strictObject({ command: z.string(), // 必填:要执行的命令 timeout: z.number().optional(), // 超时时间(ms) description: z.string().optional(), // 命令描述(给 UI 用) run_in_background: z.boolean().optional(), // 后台运行 dangerouslyDisableSandbox: z.boolean().optional(), // 绕过沙箱})
// 输出 —— 执行完返回什么const outputSchema = z.object({ stdout: z.string(), // 标准输出 stderr: z.string(), // 标准错误 interrupted: z.boolean(), // 是否被中断 isImage: z.boolean().optional(), // 输出是否为图片 backgroundTaskId: z.string().optional(), // 后台任务 ID persistedOutputPath: z.string().optional(), // 大输出文件路径 // ... 更多字段})安全分类的实现
Section titled “安全分类的实现”// 只有只读命令才标记为可并行isConcurrencySafe(input) { return this.isReadOnly(input) // 只读 = 可并发}
// 判断命令是否只读isReadOnly(input) { const hasCd = commandHasAnyCd(input.command) return checkReadOnlyConstraints(input, hasCd).behavior === 'allow'}
// 判断是否为搜索/读取命令(UI 会折叠显示)isSearchOrReadCommand(command) { // 解析命令,检查所有子命令是否都是搜索/读取/列表操作 return { isSearch, isRead, isList }}// 将 bash 命令解析为子命令,用于匹配权限规则preparePermissionMatcher(input) { const parsed = parseForSecurity(input.command) // 返回一个函数:判断命令是否匹配某个权限模式 // 例如规则 "Bash(git *)" 会匹配 "git status"、"git log" 等 return (pattern: string) => matchesSubcommands(parsed, pattern)}call() 执行核心
Section titled “call() 执行核心”async call(input, context, canUseTool, parentMessage, onProgress) { // 1. 检查是否为模拟的 sed 编辑(权限预览用) if (input._simulatedSedEdit) { return applySedEdit(input._simulatedSedEdit, context) }
// 2. 创建异步生成器执行 shell 命令 const generator = runShellCommand(input.command, { timeout: input.timeout, sandbox: shouldUseSandbox(input), background: input.run_in_background, })
// 3. 消费生成器,收集输出和进度 for await (const event of generator) { onProgress(event) // 实时报告进度给 UI }
// 4. 处理输出:截断、图片检测、大文件持久化 if (output.length > maxResultSizeChars) { // 持久化到磁盘,消息里只存路径 persistedOutputPath = await persistLargeOutput(output) }
return { data: { stdout, stderr, interrupted, ... } }}事实:
- BashTool 设置了
strict: true,启用结构化输出(模型严格按 schema 返回) maxResultSizeChars = 30_000,超出时结果存磁盘- 有专门的
_simulatedSedEdit内部字段用于权限预览
洞察:
- 输入校验用
z.strictObject(而非z.object),多余字段会报错。这是防御性设计 —— 防止模型传入未定义参数 parseForSecurity()是命令级安全分析,用 tree-sitter 做 AST 级别的命令解析,而不是简单的字符串匹配。这是工业级的做法- 进度报告通过异步生成器,不是回调轮询。这让 UI 可以实时流式显示命令输出
3. 工具注册
Section titled “3. 工具注册”关键文件:src/tools.ts
getAllBaseTools() → 返回所有内置工具的数组(BashTool 无条件包含) ↓getTools(permCtx) → 根据权限上下文过滤(deny 规则、isEnabled 检查) ↓assembleToolPool(permCtx, mcpTools) → 合并内置工具 + MCP 动态工具 ↓getMergedTools(permCtx, mcpTools) → 最终工具池,供 API 调用使用function getAllBaseTools(): Tools { return [ // ═══ 无条件注册(始终可用) ═══ BashTool, // Shell 执行 FileReadTool, // 文件读取 FileEditTool, // 文件编辑 FileWriteTool, // 文件写入 GlobTool, // 文件搜索 GrepTool, // 内容搜索 AgentTool, // 子 Agent 生成 WebFetchTool, // 网页获取 WebSearchTool, // 网页搜索 // ... 30+ 更多
// ═══ 条件注册(Feature Gates) ═══ ...(feature('KAIROS') ? [SleepTool] : []), // 后台 Agent ...(feature('COORDINATOR_MODE') ? [WorkerTool] : []), // 多服务器协调 ...(feature('AGENT_TRIGGERS') ? [CronTools] : []), // 定时任务 ]}function assembleToolPool(permissionContext, mcpTools) { const baseTools = getTools(permissionContext) // 内置工具(已过滤) const allTools = [...baseTools, ...mcpTools] // 合并 MCP 工具
// 按名称排序 —— 保证 prompt cache 稳定性! allTools.sort((a, b) => a.name.localeCompare(b.name))
// 去重(同名时内置工具优先) return deduplicateByName(allTools)}事实:
- 工具排序用
name.localeCompare()—— 这不是为了美观,而是为了保证 system prompt 中工具定义的顺序稳定 - BashTool 无条件注册,永远可用
- MCP 工具以
mcp__前缀命名,如mcp__github__create_issue
洞察:
- 排序保证 Prompt Cache 命中率。Claude API 的 prompt cache 是按字节匹配的,如果工具顺序每次不同,system prompt 变了,cache 就失效了。这是一个细微但重要的性能优化
- Feature Gates 用
feature()做编译时裁剪,不是运行时判断。同一份代码可以编译出不同功能集的版本(开源版 vs 内部版) - 「SIMPLE 模式」 只保留 BashTool + FileReadTool + FileEditTool —— 最小可用工具集
4. Schema 转换
Section titled “4. Schema 转换”关键文件:src/utils/api.ts → toolToAPISchema()
工具注册完毕后,需要转换为 Claude API 能理解的格式发送出去。
async function toolToAPISchema(tool, options): Promise<BetaToolUnion> { // 1. 检查缓存(避免重复转换) const cached = schemaCache.get(tool.name) if (cached) return cached
// 2. Schema 转换:Zod → JSON Schema const inputSchema = tool.inputJSONSchema ?? zodToJsonSchema(tool.inputSchema)
// 3. 生成工具描述(调用工具自己的 prompt() 方法) const description = await tool.prompt(options)
// 4. 结构化输出支持 const strict = feature('tengu_tool_pear') && tool.strict === true && modelSupportsStructuredOutputs(options.model)
// 5. 细粒度工具流式传输(FGTS) const eager_input_streaming = feature('tengu_fgts') && !isProxiedAPI()
// 6. 延迟加载标记 const defer_loading = options.deferLoading && tool.shouldDefer
return { name: tool.name, description, input_schema: inputSchema, strict, // 结构化输出 eager_input_streaming, // 流式工具输入 defer_loading, // 延迟加载 cache_control: options.cacheControl, // prompt cache }}BashTool 的 API Schema 长这样
Section titled “BashTool 的 API Schema 长这样”{ "name": "Bash", "description": "Executes a given bash command and returns its output.\n\nIMPORTANT: Avoid using this tool to run `find`, `grep`, `cat`...", "input_schema": { "type": "object", "properties": { "command": { "type": "string", "description": "The command to execute" }, "timeout": { "type": "number", "description": "Optional timeout in ms" }, "description": { "type": "string", "description": "Clear, concise description..." }, "run_in_background": { "type": "boolean", "description": "Set to true to run in background" } }, "required": ["command"], "additionalProperties": false }, "strict": true, "eager_input_streaming": true}事实:
- Schema 转换结果会被缓存(
schemaCache),因为在 Agent Loop 中每轮都需要发送工具定义 eager_input_streaming只对第一方 API 开启,代理 API(LiteLLM/Bedrock/Vertex)不支持
洞察:
eager_input_streaming(细粒度工具流) 是一个关键性能特性 —— 模型还在生成工具参数时,客户端就已经开始接收,不用等参数全部生成完strict模式 让模型保证输出严格符合 JSON Schema,减少解析错误defer_loading是「注意力经济学」—— 50+ 工具全放进 prompt 会分散模型注意力,延迟加载让模型先关注核心工具
5. Agent Loop
Section titled “5. Agent Loop”关键文件:src/query.ts → queryLoop()
这是 Claude Code 的心脏。所有交互最终都由这个循环驱动。
核心循环结构
Section titled “核心循环结构”async function* queryLoop(state: State): AsyncGenerator<StreamEvent> { while (true) { // ══════════ Phase 1: 调用 API ══════════ const stream = await callModel({ messages: state.messages, tools: toolSchemas, // 上一步转换好的工具 Schema system: systemPrompt, max_tokens: tokenBudget, })
// ══════════ Phase 2: 处理流式响应 ══════════ const assistantMessages = [] const toolUseBlocks = [] let needsFollowUp = false
for await (const event of stream) { yield event // 实时把事件传给 UI
if (event.type === 'tool_use') { toolUseBlocks.push(event) needsFollowUp = true // 有工具调用 → 需要继续循环
// 流式工具执行:不等 API 返回完就开始执行! streamingToolExecutor.addTool(event) } }
// ══════════ Phase 3: 没有工具调用 → 准备终止 ══════════ if (!needsFollowUp) { // 运行 Stop Hooks const stopResult = await handleStopHooks() if (stopResult.preventContinuation) { return { reason: 'stop_hook_prevented' } } // 检查 Token 预算 if (tokenBudgetExhausted) { return { reason: 'completed' } } return { reason: 'completed' } }
// ══════════ Phase 4: 有工具调用 → 执行工具 ══════════ const toolResults = [] for await (const update of runTools(toolUseBlocks)) { yield update.message toolResults.push(update.message) }
// ══════════ Phase 5: 检查是否超过最大轮次 ══════════ if (++state.turnCount > maxTurns) { return { reason: 'max_turns' } }
// ══════════ Phase 6: 组装消息,继续循环 ══════════ state.messages = [ ...state.messages, // 之前的消息 ...assistantMessages, // 模型的回复 ...toolResults, // 工具执行结果 ] state.transition = { reason: 'next_turn' } // → 回到 while(true) 顶部,带上新消息再次调用 API }}状态机的显式定义
Section titled “状态机的显式定义”循环不是简单的「有工具就继续」,而是有明确的 Continue/Terminal 状态转换:
Continue 状态(循环继续): ├─ next_turn → 正常工具执行后继续 ├─ reactive_compact_retry → 压缩后重试 ├─ max_output_tokens_escalate → 提升 token 限制后重试 ├─ stop_hook_blocking → Stop Hook 注入阻塞错误 └─ token_budget_continuation → Token 预算允许继续
Terminal 状态(循环终止): ├─ completed → 正常完成(模型不再调用工具) ├─ aborted_streaming → 用户中断 API 流 ├─ aborted_tools → 用户中断工具执行 ├─ model_error → API 错误(认证、限流等) ├─ prompt_too_long → 上下文超限(恢复失败后) ├─ max_turns → 超过最大轮次 ├─ stop_hook_prevented → Stop Hook 阻止继续 └─ hook_stopped → Hook 终止循环事实:
queryLoop是一个 AsyncGenerator —— 它不直接返回结果,而是yield事件流给 UImaxOutputTokensRecoveryCount允许最多 3 次恢复尝试- Token 预算检查在工具执行之后,确保模型有机会使用工具
洞察:
- Agent Loop 是生成器,不是 Promise。这让 UI 可以实时渲染模型的流式输出、工具的执行进度,而不是等全部完成后才显示。这是 Agent 系统中 UX 的关键
- 状态机 vs while loop:显式的状态转换比隐式的 while 条件更安全 —— 每个 Continue 都有明确的 reason,方便调试和遥测
- 流式工具执行 是 Claude Code 的杀手特性之一:模型还在生成后续工具调用时,前面的工具已经在执行了。这极大降低了用户感知的延迟
6. 权限系统
Section titled “6. 权限系统”关键文件:src/utils/permissions/permissions.ts
API 返回了 tool_use: Bash { command: "rm -rf /" } —— 怎么办?权限系统是 Agent 安全性的最后一道防线。
决策树(13 层检查)
Section titled “决策树(13 层检查)”hasPermissionsToUseTool(tool, input, context)│├─ Step 1: 检查 Deny 规则│ └─ 命中 deny 规则? → 拒绝(reason: 'rule')│├─ Step 2: 检查 Ask 规则│ └─ 命中 ask 规则? → 要求用户确认│├─ Step 3: 工具自身权限检查│ └─ tool.checkPermissions() 返回 deny? → 拒绝│ └─ BashTool 在这里做 6 层安全检查:│ ├─ 路径约束(是否访问了允许的目录)│ ├─ sed 约束(sed 编辑的安全性)│ ├─ 只读约束(是否为破坏性命令)│ ├─ 沙箱约束(是否需要沙箱)│ ├─ 安全分类(tree-sitter AST 分析)│ └─ 权限规则匹配│├─ Step 4: 用户交互要求│ └─ tool.requiresUserInteraction()? → 强制询问│├─ Step 5: 内容特定的 Ask 规则│ └─ 如 Bash(npm publish:*) 匹配? → 即使 bypass 模式也要问│├─ Step 6: 安全检查覆盖│ └─ 访问 .git/ .claude/ 等敏感路径? → bypass 模式也要问│├─ Step 7: 模式检查│ └─ bypassPermissions 模式? → 放行│├─ Step 8: Allow 规则检查│ └─ 命中 allow 规则? → 放行│├─ Step 9: 默认 → 要求用户确认(ask)│├─ Step 10: 重置拒绝计数(auto 模式)│├─ Step 11: dontAsk 模式转换│ └─ 'ask' → 'deny'(子 Agent 等不能弹对话框的场景)│├─ Step 12: Auto 模式分类器│ ├─ acceptEdits 快速路径 → 放行│ ├─ 安全工具白名单 → 放行│ └─ ML 分类器判断│ ├─ shouldBlock = false → 放行│ └─ shouldBlock = true → 拒绝│ └─ 连续拒绝过多 → 回退到交互式询问│└─ Step 13: Headless Agent 处理 ├─ PermissionRequest Hook 决策 → 放行/拒绝 └─ 无 Hook → 拒绝(后台 Agent 不能弹对话框)type PermissionRule = { source: 'userSettings' | 'projectSettings' | 'localSettings' | 'policySettings' | 'cliArg' | 'command' | 'session' ruleBehavior: 'allow' | 'deny' | 'ask' ruleValue: { toolName: string // "Bash", "mcp__github__*" ruleContent?: string // "git push", "npm publish"(细粒度匹配) }}规则来源优先级:cliArg > command > session > userSettings > projectSettings > localSettings > policySettings
事实:
- 权限检查是 13 层决策树,不是简单的 allow/deny
- 即使在
bypassPermissions模式下,安全检查(.git/ 目录访问等)仍然生效 - Auto 模式使用 ML 分类器 自动判断操作安全性
洞察:
- 「安全检查 > bypass」 是一个重要的设计原则 —— 没有任何模式可以绕过所有安全检查。这防止了 prompt injection 通过绕过权限来执行危险操作
- 连续拒绝回退:如果 ML 分类器连续拒绝多次,系统会自动回退到交互式询问 —— 这是「失败时安全降级」的典范
- 规则的细粒度匹配:不是简单的「允许 Bash」,而是可以精确到
Bash(git push:origin/main)这个级别。通过preparePermissionMatcher()实现命令级别的模式匹配 - 决策原因追踪:每个权限决策都附带
decisionReason(rule/classifier/hook/safetyCheck 等),支持审计和遥测
7. Hook 系统
Section titled “7. Hook 系统”关键文件:src/utils/hooks/hooksConfigManager.ts, src/services/tools/toolHooks.ts
权限检查通过后,在实际执行前还有一层:Hook 拦截。
Hook 在工具链路中的位置
Section titled “Hook 在工具链路中的位置”API 返回 tool_use ↓输入校验(Zod schema) ↓┌─ PreToolUse Hooks ──────────────────┐│ ├─ 可以修改输入 ││ ├─ 可以做权限决策(allow/deny/ask) ││ ├─ 可以阻止工具执行 ││ └─ 可以注入额外上下文 │└─────────────────────────────────────┘ ↓权限检查 ↓工具执行 (tool.call()) ↓┌─ PostToolUse Hooks ─────────────────┐│ ├─ 可以处理输出 ││ ├─ 可以修改 MCP 工具输出 ││ ├─ 可以阻止后续对话继续 ││ └─ 可以注入额外上下文 │└─────────────────────────────────────┘ ↓结果返回25 种 Hook 事件
Section titled “25 种 Hook 事件”工具生命周期: PreToolUse, PostToolUse, PostToolUseFailure权限决策: PermissionDenied, PermissionRequest用户输入: UserPromptSubmit, Notification会话生命周期: SessionStart, SessionEnd, Stop, StopFailure压缩: PreCompact, PostCompact子 Agent: SubagentStart, SubagentStop, TeammateIdle任务: TaskCreated, TaskCompletedMCP: Elicitation, ElicitationResult配置: ConfigChange, InstructionsLoaded, CwdChanged, FileChanged版本控制: WorktreeCreate, WorktreeRemove基础设施: SetupHook 配置示例
Section titled “Hook 配置示例”{ "hooks": [ { "event": "PreToolUse", "matcher": "Bash", "config": { "type": "command", "command": "check-bash-safety.sh" } } ]}Hook 退出码语义
Section titled “Hook 退出码语义”Exit 0: Hook 成功,使用 stdout 中的 JSON 输出Exit 2: Hook 阻塞操作 —— 停止工具执行,显示 stderr其他: 仅告警,不阻塞事实:
- Hook 支持三种类型:
command(Shell)、query(Claude API 调用)、http(外部 Webhook) - PreToolUse Hook 可以修改工具输入 —— 这让用户可以实现「自动参数注入」
洞察:
- Hook 是 Agent 系统可扩展性的关键。用户不需要修改源码就能改变 Agent 的行为 —— 这是 Agent 框架设计中的「开放-封闭原则」
- 阻塞能力(exit code 2) 让 Hook 成为了安全栏杆。例如,企业可以部署一个 Hook 阻止所有
git push --force操作 - Hook 在权限检查之前执行(PreToolUse),这意味着 Hook 可以提前做出权限决策,跳过后续的交互式询问
8. 工具执行管线
Section titled “8. 工具执行管线”关键文件:src/services/tools/toolExecution.ts
权限和 Hook 都通过后,终于到了实际执行。
runToolUse() —— 单个工具的完整执行流
Section titled “runToolUse() —— 单个工具的完整执行流”async function* runToolUse(toolUse, assistantMessage, canUseTool, context) { // ═══ 1. 查找工具 ═══ const tool = findToolByName(toolUse.name, context.tools) if (!tool) yield errorMessage("Tool not found")
// ═══ 2. 输入校验 ═══ const parsed = tool.inputSchema.safeParse(toolUse.input) if (!parsed.success) yield errorMessage("Invalid input")
const validation = await tool.validateInput(parsed.data, context) if (!validation.ok) yield errorMessage(validation.error)
// ═══ 3. PreToolUse Hooks ═══ for await (const hookResult of runPreToolUseHooks(tool, parsed.data)) { if (hookResult.type === 'hookPermissionResult') { hookPermission = hookResult.hookPermissionResult } if (hookResult.type === 'hookUpdatedInput') { parsedInput = hookResult.updatedInput // Hook 修改了输入! } if (hookResult.type === 'stop') return // Hook 阻止了执行 }
// ═══ 4. 权限检查 ═══ const { decision } = await resolveHookPermissionDecision( hookPermission, tool, parsedInput, context, canUseTool ) if (decision === 'deny') { yield deniedMessage(tool, parsedInput) return }
// ═══ 5. 执行工具 ═══ const result = await tool.call( parsedInput, context, canUseTool, assistantMessage, progress => onToolProgress(progress) // 进度回调 )
// ═══ 6. PostToolUse Hooks ═══ for await (const hookResult of runPostToolUseHooks(tool, result)) { if (hookResult.updatedMCPToolOutput) { result.data = hookResult.updatedMCPToolOutput // Hook 修改了输出! } }
// ═══ 7. 结果转换 ═══ const toolResultBlock = tool.mapToolResultToToolResultBlockParam(result.data)
// ═══ 8. 组装消息 ═══ yield { message: createUserMessage({ content: [{ type: 'tool_result', tool_use_id: toolUse.id, ...toolResultBlock }] }), contextModifier: result.contextModifier // 工具可能修改了上下文 }}事实:
runToolUse是一个 AsyncGenerator,不是 async function —— 它可以在执行过程中 yield 多个消息- 工具执行可以返回
contextModifier—— 用于修改后续工具的执行上下文
洞察:
- Generator 模式让执行管线可以在任何阶段产出消息(进度、错误、结果),UI 实时消费
contextModifier是一个精妙的设计 —— 例如cd命令改变了工作目录,通过 contextModifier 让后续工具知道目录变了,而不是用全局状态
9. 流式工具执行器
Section titled “9. 流式工具执行器”关键文件:src/services/tools/StreamingToolExecutor.ts
这是 Claude Code 性能优化的精华所在。
传统做法:等 API 返回完所有工具调用 → 依次执行 → 返回结果
Claude Code 做法:API 还在流式返回时,已经开始执行前面的工具了
并发控制模型
Section titled “并发控制模型”class StreamingToolExecutor { private canExecuteTool(isConcurrencySafe: boolean): boolean { const executing = this.tools.filter(t => t.status === 'executing')
// 规则 1: 没有工具在执行 → 任何工具都可以执行 if (executing.length === 0) return true
// 规则 2: 当前都是并发安全的,新工具也是并发安全的 → 可以并行 if (isConcurrencySafe && executing.every(t => t.isConcurrencySafe)) return true
// 规则 3: 有非并发安全工具在执行,或新工具不安全 → 等待 return false }}queued → executing → completed → yielded ↑ (错误也变为 completed)输入: [Read, Read, Bash, Read] ↓ 分区批次 1: [Read, Read] → 并发执行(都是 isReadOnly)批次 2: [Bash] → 独占执行(非并发安全)批次 3: [Read] → 独占执行(前面有非并发安全的)流式执行时序
Section titled “流式执行时序”时间轴 →API Stream: ──[Read₁]──[Read₂]──[Bash₁]──[text...]──[Read₃]──▎完成执行: ↓Read₁ ↓Read₂ ↓Read₃ ══════ ══════ ════ ↓Bash₁(等 Read 完成后开始) ═══════════════════════════════════UI 显示: Read₁✓ Read₂✓ Bash₁进度... Read₃✓ Bash₁✓事实:
- 最大并发数可通过
CLAUDE_CODE_MAX_TOOL_USE_CONCURRENCY环境变量配置,默认 10 - Bash 错误会通过
siblingAbortController级联取消其他 Bash 工具,但不影响其他类型的工具 contextModifier只能在非并发工具完成后应用(并发批次在批次结束后统一应用)
洞察:
- 并发安全标记是自下而上的:每个工具自己声明是否并发安全(
isConcurrencySafe()),执行器只做调度 - 顺序保证:即使并发执行,结果按原始顺序返回给 API —— 这对模型的上下文理解很重要
- Bash 错误级联 是一个实用的设计 —— 如果一个 Bash 命令失败了(如编译错误),继续执行其他 Bash 命令通常没有意义
10. 结果处理与状态转换
Section titled “10. 结果处理与状态转换”关键文件:src/query.ts(回到起点)
工具执行完毕,结果如何驱动下一轮循环?
// 工具执行完成后的消息组装const nextMessages = [ ...previousMessages, // 之前的对话历史 ...assistantMessages, // 本轮模型的回复(含 tool_use 块) ...toolResults, // 所有工具的执行结果 ...attachmentMessages, // 附加消息(文件变更、记忆提取等)]Tool Result 的 API 格式
Section titled “Tool Result 的 API 格式”// 工具结果作为 user 消息发回 API{ role: 'user', content: [ { type: 'tool_result', tool_use_id: 'toolu_abc123', // 对应 tool_use 块的 ID content: 'commit 7a3f2b1...', // 工具输出 is_error: false, // 是否为错误 } ]}工具结果组装完成 ↓检查中断信号 → 是 → Terminal: 'aborted_tools' ↓ 否检查 Hook 停止 → 是 → Terminal: 'hook_stopped' ↓ 否检查最大轮次 → 超过 → Terminal: 'max_turns' ↓ 未超过更新 State: - messages = nextMessages - turnCount++ - transition = { reason: 'next_turn' } ↓回到 while(true) 顶部 ↓再次调用 API(带上工具结果) ↓模型看到工具结果 → 决定是回复用户还是继续调用工具 ↓... 循环继续 ...// 工具输出超过 maxResultSizeChars(BashTool = 30,000 字符)if (output.length > tool.maxResultSizeChars) { // 持久化到磁盘 const path = `~/.claude/tool-results/${sessionId}/${toolUseId}.json` await writeFile(path, output)
// 消息中只保留摘要 + 路径 content = `[Output too large, saved to ${path}. First 1000 chars:]\n${output.slice(0, 1000)}`}事实:
- 工具结果作为
user消息(不是system消息)发回 API —— 这是 Claude API 的 tool_result 规范 - 大输出持久化到
~/.claude/tool-results/目录 contextModifier在这一步应用 —— 例如 cd 命令改变了工作目录
洞察:
- 循环的终止取决于模型,不是客户端。客户端只管执行工具和返回结果,「是否继续调用工具」由模型决定(
stop_reason: 'end_turn'vsstop_reason: 'tool_use') - 大结果外置解决了上下文窗口膨胀的问题。一个
git log可能输出 100KB,全放进消息会迅速耗尽上下文窗口。截断 + 磁盘持久化是务实的工程方案 - 附加消息(attachmentMessages) 是一个巧妙的设计 —— 文件变更通知、记忆提取、任务更新等「元信息」通过附加消息注入对话,让模型知道环境发生了什么变化
11. 架构洞察
Section titled “11. 架构洞察”从 BashTool 全链路中提炼的 10 个 Agent 工程模式
Section titled “从 BashTool 全链路中提炼的 10 个 Agent 工程模式”| # | 模式 | 在 Claude Code 中的体现 | 可借鉴的场景 |
|---|---|---|---|
| 1 | 接口标准化 | 30+ 属性的 Tool 接口,覆盖全生命周期 | 任何需要插件系统的 Agent 框架 |
| 2 | 流式管线 | AsyncGenerator 贯穿全链路,边收边执行边渲染 | 对延迟敏感的交互式 Agent |
| 3 | 分层权限决策 | 13 层检查 + ML 分类器 + 安全降级 | 任何需要人机协作的 Agent |
| 4 | 事件驱动扩展 | 25 种 Hook 事件 + 阻塞能力 | 需要用户自定义行为的 Agent 平台 |
| 5 | 并发安全标记 | isReadOnly/isDestructive/isConcurrencySafe 三元组 | 多工具并发的 Agent 系统 |
| 6 | Prompt Cache 稳定性 | 工具排序、Schema 缓存、字节级前缀匹配 | 高频调用 LLM API 的系统 |
| 7 | 大结果外置 | 超阈值存磁盘,消息只存摘要 | 任何工具输出可能很大的 Agent |
| 8 | 编译时特性门控 | feature() 裁剪功能集 | 一套代码出多个产品的场景 |
| 9 | 显式状态机 | Continue/Terminal 状态转换 | 替代 while(flag) 的 Agent Loop |
| 10 | Context Modifier | 工具执行改变后续工具的上下文 | 有状态变更的工具链 |
完整生命周期总结
Section titled “完整生命周期总结” ┌──────────────────── 编译时 ────────────────────┐ │ │ │ Tool 接口定义 → BashTool 实现 → 工具注册 │ │ (Tool.ts) (BashTool.tsx) (tools.ts) │ │ │ └────────────────────────────────────────────────┘ ↓ ┌──────────────────── 启动时 ────────────────────┐ │ │ │ 工具池组装 → Schema 转换 → 发送给 API │ │ (tools.ts) (api.ts) (query.ts) │ │ │ └────────────────────────────────────────────────┘ ↓ ┌──────────────────── 运行时(循环) ─────────────┐ │ │ │ API 返回 tool_use → 输入校验 → PreToolUse Hook│ │ ↓ │ │ 权限检查(13 层)→ 执行 tool.call() │ │ ↓ │ │ PostToolUse Hook → 结果处理 → 大结果外置 │ │ ↓ │ │ 消息组装 → 状态转换判断 → 继续/终止循环 │ │ │ └────────────────────────────────────────────────┘本目录深度解读文档
Section titled “本目录深度解读文档”| 文档 | 主题 | 核心价值 |
|---|---|---|
| AgentTool 深度解读 | 多 Agent 协作系统 | Fork 语义、Prompt Cache 共享、权限冒泡、Worktree 隔离、Swarm 编排 |
| MCPTool 深度解读 | 动态工具发现与外部协议 | 5 种传输协议、OAuth 认证、Elicitation 交互、连接故障恢复 |
| FileEditTool 深度解读 | 安全文件编辑模型 | 先读后改、唯一性约束、引号保全、路径权限、10 级错误码 |
其他相关模块
Section titled “其他相关模块”| 主题 | 关键文件 | 与 BashTool 的关系 |
|---|---|---|
| 对话压缩 | services/compact/ | 工具结果过多时如何压缩上下文 |
| Terminal UI | ink/, screens/REPL.tsx | BashTool 的渲染方法如何被 UI 消费 |
| 会话恢复 | utils/sessionStorage.ts | 工具结果如何序列化/反序列化 |