工具契约与注册

两段式的 ExecutableTool 契约、zod schema 与描述模板、从贡献点到工具表的注册链路,以及一次工具调用经过的执行管线。

作者 David更新于 第 12 篇(共 33 篇)

v0.29 的工具契约住在 v1 引擎 agent-core 里;v1 删除后,现役契约在 packages/agent-core-v2/src/tool/toolContract.ts(241 行),工具实现分布在 agent/tools/ 与 features/*/tools/ 下。

一个工具由什么组成

契约的核心只有两层:

export interface RunnableToolExecution {
  readonly isError?: false | undefined;
  readonly accesses?: ToolAccesses | undefined;
  readonly display?: ToolInputDisplay | undefined;
  readonly description?: string;
  readonly stopBatchAfterThis?: boolean | undefined;
  readonly approvalRule: string;
  readonly matchesRule?: ((ruleArgs: string) => boolean) | undefined;
  readonly execute: (ctx: ExecutableToolContext) => Promise<ExecutableToolResult>;
}

export type ToolExecution = RunnableToolExecution | ExecutableToolErrorResult;

export interface ExecutableTool<Input = unknown> extends Tool {
  resolveExecution(input: Input): ToolExecution | Promise<ToolExecution>;
}

Tool 提供 name、description、parameters;关键在 resolveExecution:先解析,后执行。解析阶段不碰任何文件,只把路径规范化,算出资源声明 accesses(供调度判冲突)、审批规则 approvalRule / matchesRule(供权限匹配)和 UI 展示 display;参数有问题就直接返回错误结果,连执行体都不生成。

以 agent/tools/os/read/ 为例,一个内置工具由这些部分组成:

组成载体
输入 schemaread.ts 的 zod ReadInputSchema,经 toInputJsonSchema() 转成 draft-7 JSON Schema
描述read.md,?raw 导入,renderPrompt() 插值——提示词独立成文件,便于打磨
实现readTool.ts 的 ReadTool 类,依赖全部由构造函数注入
注册文件末尾的 registerAgentToolService() 调用

toInputJsonSchema() 还会给没写 additionalProperties 的对象补上 false,多余参数在校验阶段就被挡住。

schema 即文档

read.ts 的输入 schema 开头是这样的:

const PositiveLineOffsetSchema = z.number().int().min(1);
const TailLineOffsetSchema = z.number().int().negative();

export const ReadInputSchema = z.object({
  path: z
    .string()
    .describe(
      'Path to a text file or a kimi-file:// attachment reference in the current session. Relative filesystem paths resolve against the working directory; a path outside the working directory must be absolute. Directories are not supported; use `ls` via Bash for a known directory, or Glob for pattern search.',
    ),
  line_offset: z
    .union([PositiveLineOffsetSchema, TailLineOffsetSchema])
    .optional()

三个细节:

  1. .describe() 是写给模型看的,而且带替代方案指引:目录不支持,改用 Bash 的 ls 或 Glob。告诉模型「这个不行时该用什么」,能显著减少无效调用。
  2. line_offset 仍是两个 schema 的 union:正数从头读,负数从尾读。v0.29 的负数下限绑在 -MAX_LINES 上;现在行数上限取消,负数不再设下限,长度改由字符预算约束(见文件工具)。
  3. 描述里的数字来自插值:read.md 里写的是 ${DEFAULT_MAX_CHARS},ReadTool 的 description 是个 getter,每次按 read 配置段算出当前上限再渲染——改了配置,描述自动同步。

统一的输出预算

v0.29 靠各工具自带的硬上限;v2 在执行器出口加了一道统一闸门:

export const DEFAULT_TOOL_RESULT_MAX_CHARS = 50_000;

export const DEFAULT_TOOL_RESULT_MAX_RETAINED_CHARS = 10_000_000;

结果定稿时,执行器调用 IAgentToolResultTruncationService.truncateForModel():文本超过 5 万字符就把全文落盘,结果换成一段指针——output_path、「用 Read 翻页或用 Grep 搜索」的提示,外加开头 4096、结尾 1024 字符的预览,并置 truncated: true。自己按预算分页的 Read 返回 spillExempt: true,跳过这一刀。

内置工具全清单

位置工具
agent/tools/os/Read、Write、Glob、Grep、Bash
agent/tools/Edit、ReadMediaFile、FetchURL、WebSearch、Agent、AskUserQuestion、select_tools
agent/tools/task/TaskList、TaskOutput、TaskStop、WaitFor
features/plan、goalEnterPlanMode、ExitPlanMode;CreateGoal、GetGoal、UpdateGoal、SetGoalBudget
features/ 其他TodoList、CronCreate、CronDelete、CronList、Skill、AgentSwarm、NotifyUser
features/tower11 个 Tower* 工具(实验功能,默认关闭)

从贡献点到工具表

注册写在每个工具文件的末尾:

registerAgentToolService(IEditTool, EditTool, {
  name: 'Edit',
  domain: 'edit',
  requiredRuntimeCapabilities: ['fs'],
});

它把工具类登记成 Agent 作用域的按需服务,同时记一条 AgentToolContribution;features/ 里的工具走 Feature.contributeTool(),进入同一个集合贡献点。App 作用域的 BuiltinToolAssemblyService 把记录注入集合,每个 Agent 作用域里的 AgentToolActivationService 再逐条过滤:运行时能力(fs / process)、工作区禁用、profile 的工具白名单与 disallowedTools、可选的 when(accessor) 谓词——全部通过才实例化并注册。运行时切换时会重新评估,能力不再满足的工具会被撤下。

终点 AgentToolRegistryService 只是一张按名字索引的 Map,登记时带上 source(builtin / user / mcp)和 disclosure(inline / deferred)。另外两个来源:

  • MCP:qualifyMcpToolName() 生成 mcp__<server>__<tool>,非法字符换成 _,超过 64 字符就截断并拼 8 位哈希。限定后仍撞名的,后来者不注册,记为 McpToolCollision(same_server / other_server)并告警。
  • 用户工具:宿主经 IAgentUserToolService.register() 注入只有名字、描述和 JSON Schema 的工具,执行时通过 interactions.request()(kind: 'user_tool')回到宿主去跑。

启用与禁用由 agent/toolPolicy/evaluate.ts 叠加判定:tools.enabled / tools.disabled 配置、profile、工作区与会话级禁用。注意 glob 只对 MCP 名字生效——mcp__github__* 可以,内置工具必须写全名,写成通配会被 findInactiveToolPatterns() 标为 wildcard-not-mcp。

一次调用的执行管线

AgentToolExecutorService.execute()(agent/toolExecutor/toolExecutorService.ts,993 行)把一批调用走完四步:

preflight  解析 JSON 参数 → 按名字找工具 → 工具策略守卫 → ajv 校验参数
prepare    resolveExecution() → onBeforeExecuteTool(权限、计划模式、hooks…)
           → onWillExecuteTool → 广播 ToolCallStarted
schedule   ToolScheduler 按 accesses 两两判冲突,不冲突的并发执行
finalize   onDidExecuteTool 钩子 → 结果归一化 → truncateForModel()

几个细节:

  • 参数校验失败,以 Invalid args for tool "Read": ... 的形式原样回给模型。
  • resolveExecution() 抛出的 PathSecurityError(敏感文件、工作区外的相对路径)直接变成工具错误,不进入审批。
  • 没声明 accesses 的工具按 ToolAccesses.all() 处理,与一切互斥。
  • 某个调用带 stopBatchAfterThis 时,同批后续调用直接以「Tool skipped because a previous tool call stopped the turn.」收尾。

select_tools:按需加载

工具很多(主要是 MCP)时,AgentToolSelectService 可以把 disclosure: 'deferred' 的工具挪出顶层 tools[],只在上下文里用 <tools_added> / <tools_removed> 公告名字,模型调用 select_tools 后再注入完整 schema。前提是实验开关 KIMI_CODE_EXPERIMENTAL_TOOL_SELECT(默认关)打开、模型能力声明了 dynamically_loaded_tools;哪些工具走这条路,由 MCP 服务器配置的 deferred: true 决定。

展示与路径策略

  • tool/toolInputDisplay.ts 的 ToolInputDisplay 是 13 种形态的联合(command、file_io、diff、search、url_fetch、plan_review、generic 等)。ToolCallStarted 事件和审批请求都带着它,前端据此渲染,而不是丢一段 JSON。
  • tool/path-access.ts 的 resolvePathAccessPath() 是文件类工具共用的路径入口,tool/rule-match.ts 提供规则匹配,详见文件工具与权限与审批。

下一篇:文件工具。

本页目录