工具契约与注册
两段式的 ExecutableTool 契约、zod schema 与描述模板、从贡献点到工具表的注册链路,以及一次工具调用经过的执行管线。
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/ 为例,一个内置工具由这些部分组成:
| 组成 | 载体 |
|---|---|
| 输入 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 开头是这样的:
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()三个细节:
.describe()是写给模型看的,而且带替代方案指引:目录不支持,改用 Bash 的ls或 Glob。告诉模型「这个不行时该用什么」,能显著减少无效调用。line_offset仍是两个 schema 的 union:正数从头读,负数从尾读。v0.29 的负数下限绑在-MAX_LINES上;现在行数上限取消,负数不再设下限,长度改由字符预算约束(见文件工具)。- 描述里的数字来自插值:
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、goal | EnterPlanMode、ExitPlanMode;CreateGoal、GetGoal、UpdateGoal、SetGoalBudget |
features/ 其他 | TodoList、CronCreate、CronDelete、CronList、Skill、AgentSwarm、NotifyUser |
features/tower | 11 个 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提供规则匹配,详见文件工具与权限与审批。
下一篇:文件工具。