Skip to content

Route C: 一个工具的全生命周期 —— 以 BashTool 为线索串联 Claude Code 全架构

本文档用一条「线」串起整个 Claude Code 的架构:追踪 BashTool 从出生到死亡的全生命周期

当用户输入 “请帮我查看 git log” 时,Claude Code 内部发生了什么?一个 Bash 工具调用是如何从无到有、被定义、注册、发送给 API、被 API 选择、通过权限检查、执行命令、返回结果、最终驱动下一轮对话的?

沿着这条链路,你会依次理解 Claude Code 的 6 大核心系统

┌─────────────────────────────────────────────────────────────────┐
│ │
│ ① Tool 定义 → ② 注册 → ③ 发给 API → ④ API 选择工具 │
│ │
│ ↓ │
│ │
│ ⑤ 权限检查 → ⑥ Hook 拦截 → ⑦ 执行 → ⑧ 结果处理 │
│ │
│ ↓ │
│ │
│ ⑨ 消息组装 → ⑩ 状态转换 → ⑪ 继续/终止循环 │
│ │
└─────────────────────────────────────────────────────────────────┘

  1. 第一站:Tool 接口 —— 一个工具长什么样
  2. 第二站:BashTool 实现 —— 具体工具怎么写
  3. 第三站:工具注册 —— 工具池怎么组装
  4. 第四站:Schema 转换 —— 工具如何变成 API 参数
  5. 第五站:Agent Loop —— 核心循环如何运转
  6. 第六站:权限系统 —— 自主性与安全性的平衡
  7. 第七站:Hook 系统 —— 可扩展的拦截点
  8. 第八站:工具执行管线 —— 从调度到执行
  9. 第九站:流式工具执行器 —— 边收边执行的并发引擎
  10. 第十站:结果处理与状态转换 —— 循环的闭合
  11. 架构洞察:10 个值得借鉴的 Agent 工程模式

关键文件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() 方法 不是静态字符串,而是每个工具自己生成的系统提示。这意味着每个工具可以教模型怎么正确使用自己

关键文件src/tools/BashTool/BashTool.tsx

BashTool 是 Claude Code 中最核心、最复杂的工具之一。看它如何实现 Tool 接口,可以理解工具开发的范式。

// 输入 —— 用户(模型)能传什么参数
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(), // 大输出文件路径
// ... 更多字段
})
// 只有只读命令才标记为可并行
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)
}
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 可以实时流式显示命令输出

关键文件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 —— 最小可用工具集

关键文件src/utils/api.tstoolToAPISchema()

工具注册完毕后,需要转换为 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
}
}
{
"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 会分散模型注意力,延迟加载让模型先关注核心工具

关键文件src/query.tsqueryLoop()

这是 Claude Code 的心脏。所有交互最终都由这个循环驱动。

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
}
}

循环不是简单的「有工具就继续」,而是有明确的 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 事件流给 UI
  • maxOutputTokensRecoveryCount 允许最多 3 次恢复尝试
  • Token 预算检查在工具执行之后,确保模型有机会使用工具

洞察

  • Agent Loop 是生成器,不是 Promise。这让 UI 可以实时渲染模型的流式输出、工具的执行进度,而不是等全部完成后才显示。这是 Agent 系统中 UX 的关键
  • 状态机 vs while loop:显式的状态转换比隐式的 while 条件更安全 —— 每个 Continue 都有明确的 reason,方便调试和遥测
  • 流式工具执行 是 Claude Code 的杀手特性之一:模型还在生成后续工具调用时,前面的工具已经在执行了。这极大降低了用户感知的延迟

关键文件src/utils/permissions/permissions.ts

API 返回了 tool_use: Bash { command: "rm -rf /" } —— 怎么办?权限系统是 Agent 安全性的最后一道防线。

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 等),支持审计和遥测

关键文件src/utils/hooks/hooksConfigManager.ts, src/services/tools/toolHooks.ts

权限检查通过后,在实际执行前还有一层:Hook 拦截

API 返回 tool_use
输入校验(Zod schema)
┌─ PreToolUse Hooks ──────────────────┐
│ ├─ 可以修改输入 │
│ ├─ 可以做权限决策(allow/deny/ask) │
│ ├─ 可以阻止工具执行 │
│ └─ 可以注入额外上下文 │
└─────────────────────────────────────┘
权限检查
工具执行 (tool.call())
┌─ PostToolUse Hooks ─────────────────┐
│ ├─ 可以处理输出 │
│ ├─ 可以修改 MCP 工具输出 │
│ ├─ 可以阻止后续对话继续 │
│ └─ 可以注入额外上下文 │
└─────────────────────────────────────┘
结果返回
工具生命周期: PreToolUse, PostToolUse, PostToolUseFailure
权限决策: PermissionDenied, PermissionRequest
用户输入: UserPromptSubmit, Notification
会话生命周期: SessionStart, SessionEnd, Stop, StopFailure
压缩: PreCompact, PostCompact
子 Agent: SubagentStart, SubagentStop, TeammateIdle
任务: TaskCreated, TaskCompleted
MCP: Elicitation, ElicitationResult
配置: ConfigChange, InstructionsLoaded, CwdChanged, FileChanged
版本控制: WorktreeCreate, WorktreeRemove
基础设施: Setup
{
"hooks": [
{
"event": "PreToolUse",
"matcher": "Bash",
"config": {
"type": "command",
"command": "check-bash-safety.sh"
}
}
]
}
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 可以提前做出权限决策,跳过后续的交互式询问

关键文件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 让后续工具知道目录变了,而不是用全局状态

关键文件src/services/tools/StreamingToolExecutor.ts

这是 Claude Code 性能优化的精华所在。

传统做法:等 API 返回完所有工具调用 → 依次执行 → 返回结果

Claude Code 做法:API 还在流式返回时,已经开始执行前面的工具了

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] → 独占执行(前面有非并发安全的)
时间轴 →
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 命令通常没有意义

关键文件src/query.ts(回到起点)

工具执行完毕,结果如何驱动下一轮循环?

// 工具执行完成后的消息组装
const nextMessages = [
...previousMessages, // 之前的对话历史
...assistantMessages, // 本轮模型的回复(含 tool_use 块)
...toolResults, // 所有工具的执行结果
...attachmentMessages, // 附加消息(文件变更、记忆提取等)
]
// 工具结果作为 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' vs stop_reason: 'tool_use'
  • 大结果外置解决了上下文窗口膨胀的问题。一个 git log 可能输出 100KB,全放进消息会迅速耗尽上下文窗口。截断 + 磁盘持久化是务实的工程方案
  • 附加消息(attachmentMessages) 是一个巧妙的设计 —— 文件变更通知、记忆提取、任务更新等「元信息」通过附加消息注入对话,让模型知道环境发生了什么变化

从 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 系统
6Prompt Cache 稳定性工具排序、Schema 缓存、字节级前缀匹配高频调用 LLM API 的系统
7大结果外置超阈值存磁盘,消息只存摘要任何工具输出可能很大的 Agent
8编译时特性门控feature() 裁剪功能集一套代码出多个产品的场景
9显式状态机Continue/Terminal 状态转换替代 while(flag) 的 Agent Loop
10Context Modifier工具执行改变后续工具的上下文有状态变更的工具链
┌──────────────────── 编译时 ────────────────────┐
│ │
│ 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 → 结果处理 → 大结果外置 │
│ ↓ │
│ 消息组装 → 状态转换判断 → 继续/终止循环 │
│ │
└────────────────────────────────────────────────┘

文档主题核心价值
AgentTool 深度解读多 Agent 协作系统Fork 语义、Prompt Cache 共享、权限冒泡、Worktree 隔离、Swarm 编排
MCPTool 深度解读动态工具发现与外部协议5 种传输协议、OAuth 认证、Elicitation 交互、连接故障恢复
FileEditTool 深度解读安全文件编辑模型先读后改、唯一性约束、引号保全、路径权限、10 级错误码
主题关键文件与 BashTool 的关系
对话压缩services/compact/工具结果过多时如何压缩上下文
Terminal UIink/, screens/REPL.tsxBashTool 的渲染方法如何被 UI 消费
会话恢复utils/sessionStorage.ts工具结果如何序列化/反序列化