Claude Code设计与实现-第6章 工具类型系统设计

《Claude Code 设计与实现》完整目录

第6章 工具类型系统设计

在人工智能辅助编程的领域中,模型的能力上限往往不取决于其语言理解有多强,而取决于它能调用哪些工具、如何调用这些工具。Claude Code 的工具系统是整个产品的脊梁骨------它定义了模型与外部世界交互的全部边界。每一次文件读取、每一次命令执行、每一次代码编辑,在底层都是一次工具调用。

本章将深入 Claude Code 的工具类型系统,从核心类型定义出发,逐层剖析自描述工具模式、Zod 运行时校验、工具注册表、工具渲染机制以及并发安全标记。我们将看到一个精心设计的类型系统如何在保证灵活性的同时维持严格的类型安全,如何在支持 40 多个内建工具的同时保持架构的一致性。

:::tip 本章要点

  • 核心类型 Tool:理解 Claude Code 工具系统的类型基石,包含 30 多个字段和方法的完整工具契约
  • 自描述工具模式:为什么选择"工具即对象"而非类继承,以及这一决策带来的架构优势
  • Zod 运行时校验inputSchema 如何同时服务于类型推导和运行时验证,lazySchema 的延迟初始化策略
  • 工具注册表getAllBaseTools() 中条件注册的设计,特性标志如何控制 40 多个工具的可用性
  • 工具渲染体系:六种渲染方法如何协同工作,React/Ink 组件如何呈现工具的使用过程和结果
  • 并发安全标记isConcurrencySafe 如何实现工具级别的并发控制,分区编排的实现原理 :::

6.1 Tool 类型定义:工具系统的类型基石

Claude Code 的整个工具系统建立在一个核心类型之上:Tool。这个类型定义在 src/Tool.ts 中,是一个包含三个泛型参数的复合类型,它完整地描述了一个工具从输入验证到执行、从权限检查到 UI 渲染的全部行为契约。

6.1.1 Tool 类型的泛型签名

typescript 复制代码
// 源码文件:src/Tool.ts

export type Tool<
  Input extends AnyObject = AnyObject,
  Output = unknown,
  P extends ToolProgressData = ToolProgressData,
> = {
  // ... 30+ 个字段和方法
}

三个泛型参数各有分工:

  • Input extends AnyObject :工具输入的 Zod schema 类型。AnyObjectz.ZodType<{ [key: string]: unknown }> 的别名,确保所有工具的输入都是对象类型。
  • Output :工具执行结果的类型,默认为 unknown,由具体工具自行约束。
  • P extends ToolProgressData:进度报告的数据类型,允许不同工具上报不同格式的进度信息。

这种泛型设计的精妙之处在于:所有泛型参数都有合理的默认值。这意味着在需要处理"任意工具"的通用代码中(如工具编排器、权限系统),可以直接使用 Tool 而不必关心具体的泛型参数;而在具体工具的实现中,又能获得完整的类型推导。

6.1.2 核心标识字段

每个工具都通过一组标识字段来声明自己的身份:

typescript 复制代码
// 源码文件:src/Tool.ts

export type Tool<Input, Output, P> = {
  readonly name: string
  aliases?: string[]
  searchHint?: string
  // ...
}

name 是工具的唯一标识符,用于模型调用时的匹配。它被声明为 readonly,意味着一旦创建就不可修改------这是一个重要的不变量,因为工具名称贯穿了权限系统、日志追踪和 API 交互的方方面面。

aliases 是可选的别名数组,用于工具重命名时的向后兼容。当一个工具被重命名后,旧名称作为别名保留,确保历史对话中的工具调用不会失败。查找逻辑在 toolMatchesName 函数中实现:

typescript 复制代码
// 源码文件:src/Tool.ts

export function toolMatchesName(
  tool: { name: string; aliases?: string[] },
  name: string,
): boolean {
  return tool.name === name || (tool.aliases?.includes(name) ?? false)
}

searchHint 是为 ToolSearch 特性准备的关键词提示。当工具数量超过阈值时,部分工具会被"延迟加载"(deferred),模型需要通过 ToolSearch 工具来发现它们。searchHint 提供了 3-10 个关键词,帮助模型通过语义搜索找到所需工具。例如 BashTool 的 searchHint'execute shell commands',GlobTool 的是 'find files by name pattern or wildcard'

6.1.3 输入与输出 Schema

typescript 复制代码
// 源码文件:src/Tool.ts

export type Tool<Input, Output, P> = {
  readonly inputSchema: Input
  readonly inputJSONSchema?: ToolInputJSONSchema
  outputSchema?: z.ZodType<unknown>
  // ...
}

inputSchema 是基于 Zod 的输入定义,承担着双重职责:它既是运行时验证的基础,也是类型推导的来源。通过 z.infer<Input>,TypeScript 可以从 schema 自动推导出输入的类型,确保 callcheckPermissionsrenderToolUseMessage 等方法的参数类型与 schema 保持一致。

inputJSONSchema 是一个可选的 JSON Schema 表示。MCP(Model Context Protocol)工具会直接提供 JSON Schema 格式的输入定义,而不需要从 Zod 转换。这种双轨设计体现了对外部工具生态的兼容考量。

outputSchema 定义工具输出的结构。源码注释标注这个字段是 "Optional because TungstenTool doesn't define this",并计划在未来将其改为必选。

6.1.4 核心方法集

Tool 类型定义了一组覆盖工具全生命周期的方法。按功能可划分为以下几个层次:

执行层:

typescript 复制代码
// 源码文件:src/Tool.ts

call(
  args: z.infer<Input>,
  context: ToolUseContext,
  canUseTool: CanUseToolFn,
  parentMessage: AssistantMessage,
  onProgress?: ToolCallProgress<P>,
): Promise<ToolResult<Output>>

call 方法是工具执行的入口。它接收经过 Zod 验证的输入 args、执行上下文 context、权限校验函数 canUseTool、触发工具调用的助手消息 parentMessage,以及可选的进度回调 onProgress。返回值 ToolResult<Output> 不仅包含执行结果数据,还可以携带新消息和上下文修改器:

typescript 复制代码
// 源码文件:src/Tool.ts

export type ToolResult<T> = {
  data: T
  newMessages?: (UserMessage | AssistantMessage | AttachmentMessage | SystemMessage)[]
  contextModifier?: (context: ToolUseContext) => ToolUseContext
  mcpMeta?: {
    _meta?: Record<string, unknown>
    structuredContent?: Record<string, unknown>
  }
}

contextModifier 允许工具在执行完成后修改后续工具的执行上下文。这个设计非常关键------源码注释明确指出 "contextModifier is only honored for tools that aren't concurrency safe",这意味着只有串行执行的工具才能修改上下文,避免并发修改导致的竞态条件。

描述层:

typescript 复制代码
// 源码文件:src/Tool.ts

description(
  input: z.infer<Input>,
  options: {
    isNonInteractiveSession: boolean
    toolPermissionContext: ToolPermissionContext
    tools: Tools
  },
): Promise<string>

prompt(options: {
  getToolPermissionContext: () => Promise<ToolPermissionContext>
  tools: Tools
  agents: AgentDefinition[]
  allowedAgentTypes?: string[]
}): Promise<string>

descriptionprompt 方法共同构成了工具的自描述能力。prompt 方法生成发送给 Claude API 的工具描述文本,模型根据这些描述来决定何时以及如何调用工具。description 方法则提供面向上下文的简短描述。两者都是异步方法,因为描述内容可能依赖于运行时状态(如当前权限配置、可用工具列表等)。

验证层:

typescript 复制代码
// 源码文件:src/Tool.ts

validateInput?(
  input: z.infer<Input>,
  context: ToolUseContext,
): Promise<ValidationResult>

checkPermissions(
  input: z.infer<Input>,
  context: ToolUseContext,
): Promise<PermissionResult>

validateInput 是可选的业务逻辑验证,在 Zod schema 验证通过之后执行。例如 BashTool 会在此检测被阻止的 sleep 模式。checkPermissions 是必选的权限检查方法,决定工具是否需要用户确认。

属性层:

typescript 复制代码
// 源码文件:src/Tool.ts

isConcurrencySafe(input: z.infer<Input>): boolean
isEnabled(): boolean
isReadOnly(input: z.infer<Input>): boolean
isDestructive?(input: z.infer<Input>): boolean
interruptBehavior?(): 'cancel' | 'block'

这些布尔方法(或返回枚举的方法)描述了工具的静态属性。注意它们大多接收 input 参数------这意味着同一个工具在不同输入下可能有不同的属性。例如 BashTool 执行 ls 命令时是只读且并发安全的,而执行 rm 命令时则不是。这种输入敏感的属性系统是 Claude Code 工具编排的基础。

6.1.5 ToolUseContext:执行上下文

ToolUseContext 是传递给每个工具 call 方法的执行上下文,它是整个系统状态的一个横切面:

typescript 复制代码
// 源码文件:src/Tool.ts(简化)

export type ToolUseContext = {
  options: {
    commands: Command[]
    tools: Tools
    mcpClients: MCPServerConnection[]
    thinkingConfig: ThinkingConfig
    // ...
  }
  abortController: AbortController
  readFileState: FileStateCache
  getAppState(): AppState
  setAppState(f: (prev: AppState) => AppState): void
  messages: Message[]
  setToolJSX?: SetToolJSXFn
  updateFileHistoryState: (updater: (prev: FileHistoryState) => FileHistoryState) => void
  updateAttributionState: (updater: (prev: AttributionState) => AttributionState) => void
  // ... 40+ 其他字段
}

ToolUseContext 之所以如此庞大,是因为它承担了"依赖注入容器"的角色。不同的工具需要访问不同的系统服务:BashTool 需要 abortController 来支持命令中断,FileEditTool 需要 readFileState 来检测文件修改冲突,AgentTool 需要完整的 options 来配置子代理。将这些依赖统一在上下文对象中,避免了各工具自行管理依赖的复杂性。

下图展示了 Tool 类型核心字段与方法的完整架构,三大泛型参数贯穿整个工具契约:

flowchart TB subgraph Tool["Tool<Input, Output, P>"] direction TB subgraph Identity["标识字段"] name["name: string (readonly)"] aliases["aliases?: string[]"] searchHint["searchHint?: string"] end subgraph Schema["Schema 定义"] inputSchema["inputSchema: ZodType<Input>"] outputSchema["outputSchema?: ZodType<Output>"] lazySchema["lazySchema(() => ...)"] end subgraph Execution["执行方法"] call["call(input, context): Output"] validateInput["validateInput?(input): boolean"] checkPermissions["checkPermissions?(input): PermissionResult"] end subgraph Rendering["渲染方法"] renderUse["renderToolUseMessage()"] renderResult["renderToolResultMessage()"] renderProgress["renderToolUseProgressMessage()"] renderError["renderToolUseErrorMessage()"] renderRejected["renderToolUseRejectedMessage()"] renderGrouped["renderGroupedToolUse()"] end subgraph Concurrency["并发控制"] isConcurrencySafe["isConcurrencySafe(input): boolean"] isReadOnly["isReadOnly?(input): boolean"] end end subgraph Context["ToolUseContext (依赖注入)"] options["options: QueryOptions"] abortController["abortController: AbortController"] readFileState["readFileState: Map"] messages["messages: Message[]"] end Execution -->|"接收"| Context

6.2 自描述工具模式

6.2.1 "工具即对象"的设计抉择

Claude Code 的工具实现采用了一种独特的模式:每个工具不是一个类的实例,而是一个满足 Tool 类型的普通对象字面量,通过 buildTool 工厂函数构建。这个决策与许多框架中常见的类继承模式形成了鲜明对比。

typescript 复制代码
// 源码文件:src/tools/GlobTool/GlobTool.ts

export const GlobTool = buildTool({
  name: GLOB_TOOL_NAME,
  searchHint: 'find files by name pattern or wildcard',
  maxResultSizeChars: 100_000,
  async description() {
    return DESCRIPTION
  },
  get inputSchema(): InputSchema {
    return inputSchema()
  },
  isConcurrencySafe() {
    return true
  },
  isReadOnly() {
    return true
  },
  renderToolUseMessage,
  renderToolResultMessage,
  // ...
})

这种模式有几个深层的设计考量:

可组合性优于继承性。 不同工具之间的共性并非层次化的------GlobTool 和 GrepTool 共享渲染逻辑(GlobTool 直接复用了 GrepTool 的 renderToolResultMessage),但它们的执行逻辑完全不同。如果使用类继承,要么需要多重继承(TypeScript 不支持),要么需要复杂的 mixin 体系。对象字面量的方式允许自由地组合:你可以从其他模块导入任意方法并直接赋值。

树摇友好。 对象字面量比类实例更容易被打包工具优化。未使用的工具可以被 dead code elimination 完全移除。源码中大量使用的 feature()process.env 条件导入正是依赖这一特性。

类型推导更自然。 TypeScript 对对象字面量有"excess property checking"和更精确的类型收窄。buildTool 函数能够从传入的对象字面量中推导出具体的工具类型,而类继承模式下的类型推导往往需要更多样板代码。

6.2.2 buildTool 工厂函数

buildTool 是工具系统中的一个关键抽象层。它接收一个 ToolDef(部分定义),补充默认值,返回一个完整的 Tool

typescript 复制代码
// 源码文件:src/Tool.ts

const TOOL_DEFAULTS = {
  isEnabled: () => true,
  isConcurrencySafe: (_input?: unknown) => false,
  isReadOnly: (_input?: unknown) => false,
  isDestructive: (_input?: unknown) => false,
  checkPermissions: (
    input: { [key: string]: unknown },
    _ctx?: ToolUseContext,
  ): Promise<PermissionResult> =>
    Promise.resolve({ behavior: 'allow', updatedInput: input }),
  toAutoClassifierInput: (_input?: unknown) => '',
  userFacingName: (_input?: unknown) => '',
}

export function buildTool<D extends AnyToolDef>(def: D): BuiltTool<D> {
  return {
    ...TOOL_DEFAULTS,
    userFacingName: () => def.name,
    ...def,
  } as BuiltTool<D>
}

默认值的设计遵循"安全关闭"(fail-closed)原则:

  • isConcurrencySafe 默认为 false------假设工具不是并发安全的,需要串行执行
  • isReadOnly 默认为 false------假设工具会产生写操作
  • isDestructive 默认为 false------这是少数默认为"宽松"的字段
  • checkPermissions 默认为 allow------将权限决策委托给通用权限系统

BuiltTool<D> 类型通过条件类型精确地反映了运行时的展开行为:如果定义 D 提供了某个方法,使用 D 的类型;否则使用默认值的类型。这确保了 60 多个工具定义都能通过 0 错误的类型检查,源码注释中特别提到:"The type semantics are proven by the 0-error typecheck across all 60+ tools."

6.2.3 自描述如何服务于模型

工具的自描述能力不仅是代码组织的需要,更直接影响着模型的行为。当 Claude Code 向 API 发送请求时,每个工具会被转换为 API 所需的工具定义格式:

typescript 复制代码
// 源码文件:src/utils/api.ts(简化)

async function buildToolSchema(tool: Tool, options): Promise<BetaToolUnion> {
  let input_schema = (
    'inputJSONSchema' in tool && tool.inputJSONSchema
      ? tool.inputJSONSchema
      : zodToJsonSchema(tool.inputSchema)
  ) as Anthropic.Tool.InputSchema

  base = {
    name: tool.name,
    description: await tool.prompt({
      getToolPermissionContext: options.getToolPermissionContext,
      tools: options.tools,
      agents: options.agents,
    }),
    input_schema,
  }
  // ...
}

这里有一个值得关注的细节:工具的 prompt 方法(而非 description 方法)负责生成发送给 API 的描述文本。prompt 方法可以根据当前可用的工具列表、权限配置和代理定义来动态调整描述内容。例如 AgentTool 的 prompt 方法会根据可用的 MCP 服务器列表来调整其描述,告知模型可以创建哪些类型的子代理。

inputSchema 通过 zodToJsonSchema 函数转换为 JSON Schema 格式。Zod 的 .describe() 方法为每个字段添加的描述文本会被保留在 JSON Schema 中,成为模型理解工具参数的重要线索。例如 BashTool 的 command 参数描述为:

bash 复制代码
The command to execute. Must be a valid shell command.

description 参数则提供了更丰富的指导:

css 复制代码
Clear, concise description of what this command does in active voice...
For simple commands, keep it brief (5-10 words).
For commands that are harder to parse at a glance, add enough context.

这些描述文本经过精心设计,旨在引导模型生成高质量的工具调用参数。

6.3 Zod 运行时校验

6.3.1 inputSchema 的双重角色

Zod 在 Claude Code 工具系统中扮演着双重角色:它既是 TypeScript 类型的来源(编译时),也是输入验证的执行者(运行时)。这种"单一来源"的设计消除了类型定义和验证逻辑之间的不一致性。

以 GlobTool 为例:

typescript 复制代码
// 源码文件:src/tools/GlobTool/GlobTool.ts

const inputSchema = lazySchema(() =>
  z.strictObject({
    pattern: z.string().describe('The glob pattern to match files against'),
    path: z
      .string()
      .optional()
      .describe(
        'The directory to search in. If not specified, the current working '
        + 'directory will be used...',
      ),
  }),
)
type InputSchema = ReturnType<typeof inputSchema>

z.strictObject 的使用是一个重要的设计选择------它会拒绝包含未知属性的输入对象。这是对模型输出的一种约束:当模型生成了一个包含多余参数的工具调用时,strict 模式会将其作为错误捕获,而不是默默忽略。

6.3.2 lazySchema 的延迟初始化

工具 schema 的构建使用了一个名为 lazySchema 的延迟初始化工具:

typescript 复制代码
// 源码文件:src/utils/lazySchema.ts

export function lazySchema<T>(factory: () => T): () => T {
  let cached: T | undefined
  return () => (cached ??= factory())
}

这个看似简单的工具解决了一个重要的性能问题:Zod schema 的构建涉及大量对象创建和方法绑定。在 Claude Code 启动时,如果所有 40 多个工具同时构建自己的 schema,会导致明显的启动延迟。lazySchema 将 schema 构建推迟到首次访问时,并通过 nullish coalescing 赋值(??=)确保只构建一次。

工具中通过 getter 方式暴露延迟初始化的 schema:

typescript 复制代码
get inputSchema(): InputSchema {
  return inputSchema()
},

6.3.3 运行时校验流程

以下时序图展示了工具输入从模型产出到通过校验的完整流程,包括 Zod 类型校验和工具级业务校验两个阶段:

sequenceDiagram participant Model as Claude 模�� participant Pipeline as 工具执行管线 participant Zod as Zod Schema participant Tool as 工具 validateInput participant Error as 错误格式化 Model->>Pipeline: tool_use(name, input) Pipeline->>Zod: inputSchema.safeParse(input) alt 类型校验失败 Zod-->>Pipeline: { success: false, error } Pipeline->>Error: formatZodValidationError() Error-->>Pipeline: 结构化错误消息 Pipeline-->>Model: tool_result(is_error=true) Note over Model: 模型自纠错,重新调用 else 类型校验通过 Zod-->>Pipeline: { success: true, data } Pipeline->>Tool: validateInput(parsedInput) alt 业务校验失败 Tool-->>Pipeline: { result: false, message } Pipeline-->>Model: tool_result(is_error=true) else 业务校验通过 Tool-->>Pipeline: { result: true } Pipeline->>Pipeline: 进入权限检查阶段 end end

工具输入的运行时校验发生在 toolExecution.ts 中,是工具执行管线的第一个关卡:

typescript 复制代码
// 源码文件:src/services/tools/toolExecution.ts(简化)

// Validate input types with zod
// (surprisingly, the model is not great at generating valid input)
const parsedInput = tool.inputSchema.safeParse(input)
if (!parsedInput.success) {
  let errorContent = formatZodValidationError(tool.name, parsedInput.error)
  // ...返回错误消息给模型
  return [{
    message: createUserMessage({
      content: [{
        type: 'tool_result',
        content: `<tool_use_error>InputValidationError: ${errorContent}</tool_use_error>`,
        is_error: true,
        tool_use_id: toolUseID,
      }],
    }),
  }]
}

源码注释中那句 "surprisingly, the model is not great at generating valid input" 揭示了运行时校验存在的真实原因:即使是最先进的语言模型,在生成结构化 JSON 输入时也经常出错------遗漏必选参数、传入错误类型、添加多余字段。Zod 校验层将这些错误转化为对模型友好的错误消息,让模型有机会在下一轮对话中修正。

6.3.4 错误格式化

Zod 验证错误通过 formatZodValidationError 函数转化为结构化的错误消息:

typescript 复制代码
// 源码文件:src/utils/toolErrors.ts(简化)

export function formatZodValidationError(
  toolName: string,
  error: ZodError,
): string {
  const missingParams = error.issues
    .filter(err =>
      err.code === 'invalid_type' &&
      err.message.includes('received undefined'),
    )
    .map(err => formatValidationPath(err.path))

  const unexpectedParams = error.issues
    .filter(err => err.code === 'unrecognized_keys')
    .flatMap(err => err.keys)

  const typeMismatchParams = error.issues
    .filter(err =>
      err.code === 'invalid_type' &&
      !err.message.includes('received undefined'),
    )
    .map(err => ({
      param: formatValidationPath(err.path),
      expected: (err as { expected: string }).expected,
      received: err.message.match(/received (\w+)/)?.[1] ?? 'unknown',
    }))
  // ...构建人类可读的错误消息
}

错误被分为三类------缺失参数、多余参数、类型不匹配------每类生成不同格式的错误描述。这种精细化的错误报告帮助模型理解自己的错误并进行修正。例如,当模型遗漏了 BashTool 的 command 参数时,它会收到 "The required parameter command is missing" 这样的明确提示。

6.3.5 二级验证:validateInput

Zod 校验之后,工具还有机会执行业务逻辑层面的验证:

typescript 复制代码
// 源码文件:src/services/tools/toolExecution.ts

const isValidCall = await tool.validateInput?.(parsedInput.data, toolUseContext)
if (isValidCall?.result === false) {
  // 返回自定义的错误消息
}

这是一个两级验证架构:Zod 负责结构性校验(参数是否存在、类型是否正确),validateInput 负责语义性校验(值是否合理、是否违反业务规则)。例如 BashTool 的 validateInput 会检测长时间 sleep 命令并建议使用后台运行模式:

typescript 复制代码
// 源码文件:src/tools/BashTool/BashTool.tsx(简化)

async validateInput(input: BashToolInput): Promise<ValidationResult> {
  if (!input.run_in_background) {
    const sleepPattern = detectBlockedSleepPattern(input.command);
    if (sleepPattern !== null) {
      return {
        result: false,
        message: `Blocked: ${sleepPattern}. Run blocking commands in the
          background with run_in_background: true...`,
        errorCode: 10
      };
    }
  }
  return { result: true };
},

6.3.6 Schema 的条件构建

BashTool 展示了 schema 可以根据运行时条件动态构建的能力:

typescript 复制代码
// 源码文件:src/tools/BashTool/BashTool.tsx

const inputSchema = lazySchema(() =>
  isBackgroundTasksDisabled
    ? fullInputSchema().omit({ run_in_background: true, _simulatedSedEdit: true })
    : fullInputSchema().omit({ _simulatedSedEdit: true })
);

当后台任务被禁用时,run_in_background 参数会从 schema 中移除,模型根本不会知道这个参数的存在。_simulatedSedEdit 作为内部字段始终被隐藏,防止模型绕过权限检查。这种动态 schema 构建能力让工具的接口随环境自适应。

6.4 工具注册表

6.4.1 注册表架构

Claude Code 的工具注册表实现在 src/tools.ts 中。它不是一个静态数组,而是一个动态组装函数 getAllBaseTools(),根据环境变量和特性标志决定哪些工具可用。

scss 复制代码
+------------------------------------------------------------------+
|                    getAllBaseTools()                               |
|                                                                   |
|  +------------------+  +------------------+  +------------------+ |
|  |   核心工具       |  |   条件工具       |  |   实验性工具     | |
|  |                  |  |                  |  |                  | |
|  |  BashTool        |  |  REPLTool        |  |  OverflowTest    | |
|  |  FileReadTool    |  |  (USER_TYPE=ant) |  |  CtxInspectTool  | |
|  |  FileEditTool    |  |                  |  |  WebBrowserTool  | |
|  |  FileWriteTool   |  |  ConfigTool      |  |  SnipTool        | |
|  |  AgentTool       |  |  (USER_TYPE=ant) |  |  WorkflowTool    | |
|  |  WebSearchTool   |  |                  |  |  MonitorTool     | |
|  |  WebFetchTool    |  |  Task*Tool       |  |  ...             | |
|  |  TodoWriteTool   |  |  (TODO_V2)       |  |                  | |
|  |  SkillTool       |  |                  |  |                  | |
|  |  ...             |  |  Worktree*Tool   |  |                  | |
|  +------------------+  |  (WORKTREE_MODE) |  +------------------+ |
|                         |                  |                       |
|                         |  CronTools       |                       |
|                         |  (AGENT_TRIGGERS)|                       |
|                         +------------------+                       |
|                                                                   |
|                    v                                               |
|           filterToolsByDenyRules()                                 |
|                    v                                               |
|             getTools() / assembleToolPool()                        |
+------------------------------------------------------------------+

6.4.2 核心工具------始终可用

有一批工具是 Claude Code 的基本配置,在所有环境中都可用:

typescript 复制代码
// 源码文件:src/tools.ts(简化)

export function getAllBaseTools(): Tools {
  return [
    AgentTool,
    TaskOutputTool,
    BashTool,
    ...(hasEmbeddedSearchTools() ? [] : [GlobTool, GrepTool]),
    ExitPlanModeV2Tool,
    FileReadTool,
    FileEditTool,
    FileWriteTool,
    NotebookEditTool,
    WebFetchTool,
    TodoWriteTool,
    WebSearchTool,
    TaskStopTool,
    AskUserQuestionTool,
    SkillTool,
    EnterPlanModeTool,
    BriefTool,
    // ...
  ]
}

注意 GlobTool 和 GrepTool 的条件化:当 Anthropic 内部构建版本嵌入了快速搜索工具(bfs/ugrep)时,这两个工具会被省略,因为它们的功能已经通过 shell 别名在 BashTool 中获得了。这是一个典型的"能力去重"策略。

6.4.3 条件注册------特性标志驱动

大量工具的注册取决于特性标志(feature flags)。Claude Code 使用 Bun 的编译期 feature() 宏来实现死代码消除:

typescript 复制代码
// 源码文件:src/tools.ts

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

const cronTools = feature('AGENT_TRIGGERS')
  ? [
      require('./tools/ScheduleCronTool/CronCreateTool.js').CronCreateTool,
      require('./tools/ScheduleCronTool/CronDeleteTool.js').CronDeleteTool,
      require('./tools/ScheduleCronTool/CronListTool.js').CronListTool,
    ]
  : []

const WebBrowserTool = feature('WEB_BROWSER_TOOL')
  ? require('./tools/WebBrowserTool/WebBrowserTool.js').WebBrowserTool
  : null

feature() 在编译期被解析为布尔常量,使得条件分支中的"死"代码可以被打包工具完全移除。这意味着面向外部用户的构建不会包含任何内部工具的代码,既减小了包体积,也消除了安全隐患。

6.4.4 工具的分类体系

通过对 tools.ts 的注册逻辑和 constants/tools.ts 的禁止列表进行分析,可以将所有工具归纳为以下几个类别:

文件操作工具: FileReadTool、FileEditTool、FileWriteTool、NotebookEditTool。这是 Claude Code 作为编码助手的核心能力。

搜索工具: GlobTool、GrepTool。提供文件名模式匹配和内容搜索。

执行工具: BashTool、PowerShellTool。直接在用户环境中执行命令。

Agent/任务工具: AgentTool、TaskCreateTool、TaskGetTool、TaskUpdateTool、TaskListTool、TaskStopTool、TaskOutputTool。支持子代理创建和任务管理。

网络工具: WebFetchTool、WebSearchTool。提供网络资源获取和搜索引擎查询能力。

交互工具: AskUserQuestionTool、EnterPlanModeTool、ExitPlanModeV2Tool。支持与用户的结构化交互。

MCP 相关工具: ListMcpResourcesTool、ReadMcpResourceTool、ToolSearchTool。支持 MCP 资源发现和延迟工具加载。

协作工具: SendMessageTool、TeamCreateTool、TeamDeleteTool、ListPeersTool。支持多代理协作场景。

平台工具: ConfigTool、TungstenTool、REPLTool、SkillTool、BriefTool。提供配置管理、技能系统等平台级能力。

6.4.5 多层过滤管线

从注册到最终暴露给模型,工具经过多层过滤:

typescript 复制代码
// 源码文件:src/tools.ts

export const getTools = (permissionContext: ToolPermissionContext): Tools => {
  // 第一层:简单模式过滤
  if (isEnvTruthy(process.env.CLAUDE_CODE_SIMPLE)) {
    const simpleTools: Tool[] = [BashTool, FileReadTool, FileEditTool]
    return filterToolsByDenyRules(simpleTools, permissionContext)
  }

  // 第二层:移除特殊工具
  const specialTools = new Set([
    ListMcpResourcesTool.name,
    ReadMcpResourceTool.name,
    SYNTHETIC_OUTPUT_TOOL_NAME,
  ])
  const tools = getAllBaseTools().filter(tool => !specialTools.has(tool.name))

  // 第三层:权限否决规则过滤
  let allowedTools = filterToolsByDenyRules(tools, permissionContext)

  // 第四层:REPL 模式下隐藏原始工具
  if (isReplModeEnabled()) {
    const replEnabled = allowedTools.some(tool =>
      toolMatchesName(tool, REPL_TOOL_NAME)
    )
    if (replEnabled) {
      allowedTools = allowedTools.filter(
        tool => !REPL_ONLY_TOOLS.has(tool.name),
      )
    }
  }

  // 第五层:isEnabled() 检查
  const isEnabled = allowedTools.map(_ => _.isEnabled())
  return allowedTools.filter((_, i) => isEnabled[i])
}

最终,assembleToolPool 函数将内建工具与 MCP 工具合并,并按名称排序以保证 prompt cache 的稳定性:

typescript 复制代码
// 源码文件:src/tools.ts

export function assembleToolPool(
  permissionContext: ToolPermissionContext,
  mcpTools: Tools,
): Tools {
  const builtInTools = getTools(permissionContext)
  const allowedMcpTools = filterToolsByDenyRules(mcpTools, permissionContext)
  const byName = (a: Tool, b: Tool) => a.name.localeCompare(b.name)
  return uniqBy(
    [...builtInTools].sort(byName).concat(allowedMcpTools.sort(byName)),
    'name',
  )
}

排序策略的注释解释了其必要性:"The server's claude_code_system_cache_policy places a global cache breakpoint after the last prefix-matched built-in tool; a flat sort would interleave MCP tools into built-ins and invalidate all downstream cache keys whenever an MCP tool sorts between existing built-ins." 内建工具作为一个排序后的前缀组(contiguous prefix),MCP 工具作为另一个排序后的后缀组------这保证了 MCP 工具的增减不会影响内建工具部分的缓存命中率。

6.4.6 子代理的工具限制

不同的执行上下文对工具的可用性有着不同的约束。constants/tools.ts 定义了多个工具限制集合,用于不同场景下的工具过滤:

typescript 复制代码
// 源码文件:src/constants/tools.ts(简化)

export const ALL_AGENT_DISALLOWED_TOOLS = new Set([
  TASK_OUTPUT_TOOL_NAME,
  EXIT_PLAN_MODE_V2_TOOL_NAME,
  ENTER_PLAN_MODE_TOOL_NAME,
  ASK_USER_QUESTION_TOOL_NAME,
  TASK_STOP_TOOL_NAME,
  // ...
])

export const COORDINATOR_MODE_ALLOWED_TOOLS = new Set([
  AGENT_TOOL_NAME,
  TASK_STOP_TOOL_NAME,
  SEND_MESSAGE_TOOL_NAME,
  SYNTHETIC_OUTPUT_TOOL_NAME,
])

子代理禁止工具列表(ALL_AGENT_DISALLOWED_TOOLS) 阻止子代理调用可能导致递归或越权的工具。例如子代理不能进入/退出计划模式(这是主线程的抽象),不能直接向用户提问(这可能导致交互混乱),也不能停止任务(这需要访问主线程的任务状态)。

协调器模式允许工具列表(COORDINATOR_MODE_ALLOWED_TOOLS) 将协调器代理限制为只能使用代理管理工具------它只负责分配任务和收集结果,不直接操作文件或执行命令。这实现了一种关注点分离:协调器负责编排,工作代理负责执行。

异步代理允许工具列表(ASYNC_AGENT_ALLOWED_TOOLS) 为后台运行的异步代理划定了安全的工具边界。异步代理可以使用文件操作、搜索和编辑工具,但不能使用 MCP 工具(尚未实现安全的异步 MCP 调用),也不能创建子代理(防止递归)。

这种多层次的工具访问控制体现了一个重要的安全原则:最小权限原则。每个执行上下文只能访问它完成任务所必需的最小工具集。

6.5 工具渲染体系

6.5.1 渲染方法全景

Tool 类型定义了六种渲染方法,覆盖了工具生命周期中的每一个 UI 展示节点:

scss 复制代码
工具调用流程中的渲染节点:

  模型发出工具调用
       |
       v
  renderToolUseMessage()        -- 工具调用的参数展示
       |
       v
  renderToolUseProgressMessage() -- 执行过程中的进度展示(可选)
       |
       +---> 用户拒绝 ---> renderToolUseRejectedMessage()
       |
       v
  工具执行完成
       |
       +---> 成功 ---> renderToolResultMessage()
       |
       +---> 失败 ---> renderToolUseErrorMessage()
       |
       v
  renderGroupedToolUse()        -- 多个并行工具的聚合展示(可选)

6.5.2 renderToolUseMessage:调用参数展示

这是用户最先看到的渲染------当模型决定调用一个工具时,UI 需要立即展示工具的名称和参数。关键设计点在于:输入是 Partial<z.infer<Input>>,而非完整的 z.infer<Input>。源码注释解释了原因:

typescript 复制代码
// 源码文件:src/Tool.ts

/**
 * Render the tool use message. Note that `input` is partial because we render
 * the message as soon as possible, possibly before tool parameters have fully
 * streamed in.
 */
renderToolUseMessage(
  input: Partial<z.infer<Input>>,
  options: { theme: ThemeName; verbose: boolean; commands?: Command[] },
): React.ReactNode

Claude API 使用流式传输(streaming),工具参数是逐步到达的。为了给用户提供即时反馈,渲染在参数完全接收之前就已经开始。GlobTool 的实现是一个很好的示例:

typescript 复制代码
// 源码文件:src/tools/GlobTool/UI.tsx

export function renderToolUseMessage(
  { pattern, path }: Partial<{ pattern: string; path: string }>,
  { verbose }: { verbose: boolean },
): React.ReactNode {
  if (!pattern) {
    return null  // 参数还未到达,不渲染任何内容
  }
  if (!path) {
    return `pattern: "${pattern}"`
  }
  return `pattern: "${pattern}", path: "${verbose ? path : getDisplayPath(path)}"`
}

渲染逻辑优雅地处理了参数逐步到达的场景:先显示 pattern,path 到达后再更新显示。

6.5.3 renderToolResultMessage:结果展示

结果渲染方法接收工具的输出数据和进度消息:

typescript 复制代码
// 源码文件:src/Tool.ts

renderToolResultMessage?(
  content: Output,
  progressMessagesForMessage: ProgressMessage<P>[],
  options: {
    style?: 'condensed'
    theme: ThemeName
    tools: Tools
    verbose: boolean
    isTranscriptMode?: boolean
    isBriefOnly?: boolean
    input?: unknown
  },
): React.ReactNode

这个方法是可选的------源码注释说明:"When omitted, the tool result renders nothing (same as returning null). Omit for tools whose results are surfaced elsewhere." 例如 TodoWriteTool 的结果不通过工具渲染展示,而是更新专用的 todo 面板。

options 中的 style: 'condensed'verbose 标志控制着结果的详细程度。在非详细模式下,搜索工具只显示 "Found 3 files in 12ms" 这样的摘要;在详细模式下则展示完整的文件列表。isTranscriptMode 标志影响着 transcript 搜索的文本提取逻辑。

6.5.4 进度渲染

长时间运行的工具可以通过进度消息提供实时反馈:

typescript 复制代码
// 源码文件:src/Tool.ts

renderToolUseProgressMessage?(
  progressMessagesForMessage: ProgressMessage<P>[],
  options: {
    tools: Tools
    verbose: boolean
    terminalSize?: { columns: number; rows: number }
    inProgressToolCallCount?: number
    isTranscriptMode?: boolean
  },
): React.ReactNode

BashTool 的进度渲染会展示命令的实时输出------标准输出和标准错误流的内容随着命令执行逐步更新。AgentTool 的进度则展示子代理的思考过程和工具调用链。进度报告通过 onProgress 回调从 call 方法内部发出,类型由泛型参数 P 约束,确保每种工具的进度数据结构是类型安全的。

6.5.5 分组渲染

当多个相同类型的工具并行执行时,renderGroupedToolUse 提供了聚合展示能力:

typescript 复制代码
// 源码文件:src/Tool.ts

renderGroupedToolUse?(
  toolUses: Array<{
    param: ToolUseBlockParam
    isResolved: boolean
    isError: boolean
    isInProgress: boolean
    progressMessages: ProgressMessage<P>[]
    result?: {
      param: ToolResultBlockParam
      output: unknown
    }
  }>,
  options: {
    shouldAnimate: boolean
    tools: Tools
  },
): React.ReactNode | null

源码注释明确了其作用范围:"Renders multiple tool uses as a group (non-verbose mode only). In verbose mode, individual tool uses render at their original positions." 这为并行搜索操作(如同时搜索多个文件模式)提供了更紧凑的 UI 表示。

6.5.6 辅助 UI 方法

除了核心渲染方法外,Tool 类型还定义了一系列辅助 UI 方法:

  • userFacingName :返回工具在 UI 中的显示名称。注意它接收 Partial<z.infer<Input>>,因此可以根据输入内容动态调整名称。例如 BashTool 在沙盒模式下会显示为 "SandboxedBash",在检测到 sed 编辑命令时会显示为文件编辑的名称。

  • getToolUseSummary:返回工具调用的简短摘要,用于紧凑视图。

  • getActivityDescription:返回现在进行时的活动描述,用于加载动画(spinner)。例如 "Reading src/foo.ts"、"Running bun test"。

  • isResultTruncated:判断非详细模式的渲染是否被截断,控制点击展开的交互行为。

  • renderToolUseTag:在工具调用消息后面渲染可选的标签,用于显示超时时间、模型名称等元数据。

这些方法共同构成了一个完整的 UI 适配层,让每个工具能够控制自己在终端界面中的呈现方式。

6.5.7 渲染与逻辑的分离

值得注意的是,Claude Code 在代码组织上将渲染逻辑与业务逻辑严格分离。每个工具通常有两个文件:主文件(如 BashTool.tsx)负责执行逻辑和工具定义,UI.tsx 文件负责所有渲染方法的实现。工具定义中直接引用 UI 文件导出的函数:

typescript 复制代码
// 源码文件:src/tools/GlobTool/GlobTool.ts

import {
  renderToolResultMessage,
  renderToolUseErrorMessage,
  renderToolUseMessage,
  userFacingName,
} from './UI.js'

export const GlobTool = buildTool({
  // ...
  renderToolUseMessage,     // 直接引用 UI 模块的函数
  renderToolUseErrorMessage,
  renderToolResultMessage,
  userFacingName,
  // ...
})

这种分离带来了两个好处。第一,渲染逻辑可以独立测试,不需要模拟工具执行的完整上下文。第二,不同工具可以自由地复用彼此的渲染组件------GlobTool 直接复用了 GrepTool 的 renderToolResultMessage,因为两者的搜索结果展示格式相同。这种跨工具的渲染复用在类继承模式下很难优雅地实现。

此外,extractSearchText 方法为 transcript 搜索功能提供了文本索引。它返回渲染结果中实际可见的文本内容,确保搜索命中数与高亮数一致。源码注释特别提到了 "phantom" 问题------如果 extractSearchText 声称存在某些文本但渲染中实际不可见,就会导致搜索计数与高亮不匹配。为此,测试套件中专门有一个 "render-fidelity" 测试来检测这种不一致。

6.6 并发安全标记

下图展示了工具并发安全标记如何驱动编排层的分区决策,从工具调用到批次划分的完整决策链路:

flowchart LR subgraph Input["模型产出的工具调用"] T1["Grep(pattern)"] T2["Read(file_a)"] T3["Edit(file_b)"] T4["Glob(*.ts)"] T5["Bash(npm test)"] end subgraph Judge["isConcurrencySafe 判定"] T1 -->|"isReadOnly=true"| Safe["并发安全"] T2 -->|"isReadOnly=true"| Safe T4 -->|"isReadOnly=true"| Safe T3 -->|"写操作"| Unsafe["非并发安全"] T5 -->|"checkReadOnly=false"| Unsafe end subgraph Batches["批次分区结果"] Safe --> B1["Batch 1 并发"] Unsafe --> B2["Batch 2 串行"] Unsafe --> B3["Batch 3 串行"] end B1 -->|"runToolsConcurrently"| Exec["并发执行引擎"] B2 -->|"runToolsSerially"| Exec B3 -->|"runToolsSerially"| Exec

6.6.1 isConcurrencySafe 的设计意图

isConcurrencySafe 是工具类型系统中最具影响力的属性之一。它决定了一个工具调用能否与其他工具调用并行执行:

typescript 复制代码
// 源码文件:src/Tool.ts

isConcurrencySafe(input: z.infer<Input>): boolean

注意两个关键设计决策:

第一,它是一个方法而非静态属性,接收 input 参数。这使得同一工具在不同输入下可以有不同的并发性判定。BashTool 就是最典型的例子------ls 是并发安全的,npm install 不是:

typescript 复制代码
// 源码文件:src/tools/BashTool/BashTool.tsx

isConcurrencySafe(input) {
  return this.isReadOnly?.(input) ?? false;
},
isReadOnly(input) {
  const compoundCommandHasCd = commandHasAnyCd(input.command);
  const result = checkReadOnlyConstraints(input, compoundCommandHasCd);
  return result.behavior === 'allow';
},

BashTool 将并发安全性直接委托给只读性判定:如果命令是只读的,则并发安全;否则不安全。checkReadOnlyConstraints 维护了一个庞大的只读命令白名单(包括 git diffgrepfindcatdocker inspect 等),并对命令的标志和参数进行精细化分析。

第二,buildTool 的默认值是 false------即假设工具不是并发安全的。这遵循了安全设计中的"默认关闭"原则:忘记声明并发安全性只会导致性能损失(串行执行),而不会导致正确性问题。

6.6.2 并发安全工具清单

通过对源码的全面分析,标记为并发安全(isConcurrencySafe 返回 true)的工具包括:

始终并发安全的工具:

工具名称 理由
FileReadTool 纯读取操作
GlobTool 文件名模式匹配,纯读取
GrepTool 内容搜索,纯读取
WebSearchTool 外部 API 调用,无本地副作用
WebFetchTool 外部 API 调用,无本地副作用
AgentTool 代理内部独立管理自己的并发
ToolSearchTool 只是搜索工具元数据
ListMcpResourcesTool 只读查询 MCP 资源
ReadMcpResourceTool 只读读取 MCP 资源
LSPTool 只读查询语言服务器
ConfigTool 读取配置
AskUserQuestionTool 用户交互,不修改系统状态
EnterPlanModeTool 模式切换,原子操作
ExitPlanModeV2Tool 模式切换,原子操作
BriefTool 仅控制输出模式
TaskGetTool 只读查询
TaskListTool 只读查询
TaskCreateTool 创建独立任务
TaskUpdateTool 更新任务状态
TaskStopTool 停止任务

条件性并发安全的工具:

工具名称 条件
BashTool 仅当命令为只读时(lscatgit diff 等)
TaskOutputTool 委托给 isReadOnly

默认不并发安全的工具(使用 buildTool 默认值):

FileEditTool、FileWriteTool、NotebookEditTool、TodoWriteTool 等所有修改文件的工具都没有覆盖默认值,因此它们会被串行执行。

6.6.3 并发编排机制

isConcurrencySafe 标记在两个层面被使用。

分区编排(partitionToolCalls):toolOrchestration.ts 中,工具调用被分区为并发批次和串行批次:

typescript 复制代码
// 源码文件:src/services/tools/toolOrchestration.ts(简化)

function partitionToolCalls(
  toolUseMessages: ToolUseBlock[],
  toolUseContext: ToolUseContext,
): Batch[] {
  return toolUseMessages.reduce((acc: Batch[], toolUse) => {
    const tool = findToolByName(toolUseContext.options.tools, toolUse.name)
    const parsedInput = tool?.inputSchema.safeParse(toolUse.input)
    const isConcurrencySafe = parsedInput?.success
      ? (() => {
          try {
            return Boolean(tool?.isConcurrencySafe(parsedInput.data))
          } catch {
            return false
          }
        })()
      : false
    if (isConcurrencySafe && acc[acc.length - 1]?.isConcurrencySafe) {
      acc[acc.length - 1]!.blocks.push(toolUse)
    } else {
      acc.push({ isConcurrencySafe, blocks: [toolUse] })
    }
    return acc
  }, [])
}

分区算法将连续的并发安全工具合并为一个批次,遇到非并发安全的工具则创建新批次。产出的批次序列类似于 [并发批, 串行批, 并发批, ...],然后分别通过并发和串行策略执行。

流式执行器(StreamingToolExecutor): 在流式场景中,工具调用不是一次性到达的,而是随着模型输出逐个流入。StreamingToolExecutor 实现了更细粒度的并发控制:

typescript 复制代码
// 源码文件:src/services/tools/StreamingToolExecutor.ts(简化)

private canExecuteTool(isConcurrencySafe: boolean): boolean {
  const executingTools = this.tools.filter(t => t.status === 'executing')
  return (
    executingTools.length === 0 ||
    (isConcurrencySafe && executingTools.every(t => t.isConcurrencySafe))
  )
}

规则非常清晰:一个工具可以执行,当且仅当(1) 没有其他工具正在执行,或者 (2) 它是并发安全的,且所有正在执行的工具也都是并发安全的。这是一个经典的"读写锁"模型------多个读取者可以同时访问,但写入者需要独占访问。

6.6.4 contextModifier 与并发安全的关系

ToolResult 中的 contextModifier 字段与并发安全标记有着直接的关联:

typescript 复制代码
// 源码文件:src/Tool.ts

export type ToolResult<T> = {
  data: T
  // contextModifier is only honored for tools that aren't concurrency safe.
  contextModifier?: (context: ToolUseContext) => ToolUseContext
  // ...
}

源码注释明确指出 contextModifier 仅对非并发安全的工具生效。这是一个深思熟虑的设计:如果多个工具并发执行,它们对上下文的修改可能产生冲突。因此系统规定:只有串行执行的工具才能修改上下文,确保上下文修改的顺序性和确定性。

toolOrchestration.ts 的实现中,并发批次的 contextModifier 被收集到队列中,在整个批次完成后才按工具顺序依次应用:

typescript 复制代码
// 源码文件:src/services/tools/toolOrchestration.ts(简化)

if (isConcurrencySafe) {
  const queuedContextModifiers: Record<string, ((ctx: ToolUseContext) => ToolUseContext)[]> = {}
  for await (const update of runToolsConcurrently(blocks, ...)) {
    if (update.contextModifier) {
      const { toolUseID, modifyContext } = update.contextModifier
      queuedContextModifiers[toolUseID] ??= []
      queuedContextModifiers[toolUseID].push(modifyContext)
    }
    // ...yield results
  }
  // 批次完成后按序应用
  for (const block of blocks) {
    for (const modifier of queuedContextModifiers[block.id] ?? []) {
      currentContext = modifier(currentContext)
    }
  }
}

6.7 设计决策分析

6.7.1 为什么不用类继承

回顾整个工具类型系统,"工具即对象 + buildTool 工厂"的模式相比类继承有以下优势:

扁平化组合。 工具之间的代码复用是横向的而非纵向的。GlobTool 复用 GrepTool 的结果渲染,BashTool 复用 FileEditTool 的用户面向名称------这些跨工具的复用不遵循任何继承层次。对象展开(spread)和属性引用比方法覆盖(override)更直观、更安全。

零开销抽象。 每个工具都是编译期确定的纯对象。没有原型链查找,没有虚方法表,没有 super 调用。在一个每秒可能执行数十次工具查找的系统中,这种零开销很重要。

增量类型安全。 buildTool 函数让新工具可以只定义关心的字段,其余使用安全的默认值。而类继承方案中,要么需要抽象基类(增加复杂性),要么需要每个工具都实现所有方法(增加样板代码)。

6.7.2 类型安全的边界

Claude Code 的工具类型系统在追求类型安全的同时,也做出了务实的妥协。ToolUseContext 中的 40 多个字段意味着工具实现者需要理解大量上下文,但这些字段被组织为逻辑分组(options、状态访问器、回调函数等),并且大多是可选的。

Tool 类型中有些方法接收 Partial<z.infer<Input>>(如渲染方法),有些接收完整的 z.infer<Input>(如 callcheckPermissions)。这种区分反映了一个实际约束:在流式场景中,参数可能不完整,但执行和权限检查需要完整的输入。

6.7.3 maxResultSizeChars 与结果持久化

每个工具都必须声明 maxResultSizeChars 字段,定义工具结果在被持久化到磁盘之前的最大字符数:

typescript 复制代码
// 源码文件:src/Tool.ts

/**
 * Maximum size in characters for tool result before it gets persisted to disk.
 * When exceeded, the result is saved to a file and Claude receives a preview
 * with the file path instead of the full content.
 *
 * Set to Infinity for tools whose output must never be persisted (e.g. Read,
 * where persisting creates a circular Read->file->Read loop and the tool
 * already self-bounds via its own limits).
 */
maxResultSizeChars: number

这个字段的设计反映了对上下文窗口的精细管理意识。当工具输出超过阈值时,系统将完整结果持久化到临时文件,模型只收到一个预览摘要和文件路径。如果需要查看完整内容,模型可以使用 FileReadTool 读取该文件。不同工具有不同的阈值:BashTool 是 30,000 字符,GlobTool 和 GrepTool 是 100,000 字符,而 FileReadTool 设置为 Infinity------因为将读取结果持久化到文件再让模型读取该文件,会导致循环引用。

这种设计在保留信息完整性的同时,有效控制了单次 API 调用的 token 消耗,是工程务实主义的体现。

6.7.4 可扩展性的保证

这个类型系统的可扩展性体现在多个维度:

  • 新增工具 :只需创建一个满足 ToolDef 的对象,调用 buildTool,然后在 tools.ts 中注册即可。
  • 新增工具属性 :在 Tool 类型中添加可选字段不会破坏现有工具,buildTool 的默认值机制确保了向后兼容。
  • MCP 工具集成MCPTool 提供了一个基础模板对象,其中的 namedescriptioncalluserFacingName 等关键方法在 mcpClient.ts 中被逐一覆盖为 MCP 服务器提供的实际值。inputJSONSchema 字段允许 MCP 工具直接提供 JSON Schema 格式的输入定义,无需经过 Zod 转换。这种模板覆盖模式让任意 MCP 工具能以零代码变更接入系统。
  • 工具搜索/延迟加载shouldDeferalwaysLoadsearchHint 字段在不改变工具核心行为的前提下支持了大规模工具集的管理。

6.8 本章小结

本章深入分析了 Claude Code 工具类型系统的设计与实现。从 Tool 类型的 30 多个字段和方法出发,我们看到了一个精心设计的类型契约如何同时服务于模型调用、运行时验证、权限控制、UI 渲染和并发编排。

核心设计原则可归纳为:

  1. 自描述优于外部配置。 每个工具携带自己的完整描述------输入 schema、权限规则、渲染逻辑、并发属性。没有外部的 XML 配置文件或注册表元数据。

  2. 安全默认优于最大性能。 buildTool 的默认值遵循 fail-closed 原则:不并发、非只读、需权限检查。性能优化需要显式声明。

  3. 组合优于继承。 "工具即对象"模式支持灵活的代码复用,避免了类继承的刚性层次结构。

  4. 单一来源优于多处定义。 Zod schema 同时服务于类型推导、运行时验证和 API schema 生成,消除了定义不一致的可能性。

  5. 输入敏感优于静态属性。 isConcurrencySafeisReadOnlyisDestructive 等关键属性都依赖于具体输入,而非工具本身,实现了细粒度的行为控制。

这些原则共同构成了一个既灵活又严谨的工具类型系统,它是 Claude Code 能够可靠地编排 40 多个内建工具和任意数量 MCP 外部工具的基础。在下一章中,我们将深入工具编排层,看看这个类型系统如何在实际的工具执行流水线中发挥作用。

相关推荐
杨艺韬7 小时前
Claude Code设计与实现-第1章 为什么需要理解 Claude Code
agent
杨艺韬7 小时前
Claude Code设计与实现-前言
agent
杨艺韬7 小时前
Claude Code设计与实现-第2章 架构总览
agent
杨艺韬7 小时前
Claude Code设计与实现-第4章 Query 引擎:Agent 的心脏
agent
杨艺韬7 小时前
Claude Code设计与实现-第3章 CLI 启动与性能优化
agent
杨艺韬7 小时前
OpenClaw设计与实现-第12章 定时任务与自动化
agent
杨艺韬7 小时前
OpenClaw设计与实现-第14章 CLI 与交互界面
agent
杨艺韬7 小时前
OpenClaw设计与实现-第13章 安全与权限
agent
杨艺韬7 小时前
OpenClaw设计与实现-第8章 通道实现深度剖析
agent