# 工具契约与注册

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

- 作者：David（道雾轩）
- 专栏：Kimi Code 源码解读（https://daiw.org/manual/kimi-code.md）
- 最后更新：2026-09-19
- 原文：https://daiw.org/manual/kimi-code/tool-contract
- 转载与引用：请注明出处并附原文链接（https://daiw.org/about/copyright）

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

## 一个工具由什么组成

契约的核心只有两层：

```ts
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`（供[调度](https://daiw.org/manual/kimi-code/agent-loop)判冲突）、审批规则 `approvalRule` / `matchesRule`（供[权限](https://daiw.org/manual/kimi-code/permission)匹配）和 UI 展示 `display`；参数有问题就直接返回错误结果，连执行体都不生成。

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

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

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

## schema 即文档

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

```ts
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` 上；现在行数上限取消，负数不再设下限，长度改由字符预算约束（见[文件工具](https://daiw.org/manual/kimi-code/file-tools)）。
3. **描述里的数字来自插值**：`read.md` 里写的是 `${DEFAULT_MAX_CHARS}`，`ReadTool` 的 `description` 是个 getter，每次按 `read` 配置段算出当前上限再渲染——改了配置，描述自动同步。

## 统一的输出预算

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

```ts
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`、`goal` | `EnterPlanMode`、`ExitPlanMode`；`CreateGoal`、`GetGoal`、`UpdateGoal`、`SetGoalBudget` |
| `features/` 其他 | `TodoList`、`CronCreate`、`CronDelete`、`CronList`、`Skill`、`AgentSwarm`、`NotifyUser` |
| `features/tower` | 11 个 `Tower*` 工具（实验功能，默认关闭） |

## 从贡献点到工具表

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

```ts
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 行）把一批调用走完四步：

```text
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` 提供规则匹配，详见[文件工具](https://daiw.org/manual/kimi-code/file-tools)与[权限与审批](https://daiw.org/manual/kimi-code/permission)。

下一篇：[文件工具](https://daiw.org/manual/kimi-code/file-tools)。
