---
url: /wave-agent/specs/core/tool-input-validation.md
description: 内置工具调用前按自己声明的 JSON Schema 校验参数，参数错误作为可恢复的工具结果返回给模型
---

# 功能规格说明：内置工具的参数校验

**创建日期**：2026-10-19

## 用户场景与测试 *（必填）*

### 用户故事：内置工具调用前先校验参数（优先级：P1）

作为模型，我希望参数写错时立刻被告知**哪个参数错了、错在哪**，以便下一轮自己改对；作为用户，我希望参数错误不要变成静默的错事——少传 `content` 就把 `undefined` 当内容写进文件，参数名写错就拿 `undefined` 去查任务，最后报出来的是一句与真正原因无关的下游怪象。

**为什么是这个优先级**：参数错误是模型最高频的一类失误，而 wave 的每个内置工具都把自己要的参数声明在 `config.function.parameters`（JSON Schema）里，这份声明**只用来给模型看**，运行时从不校验。错误的后果因此不是"报错"而是"下沉"：工具内部某个字段成了 `undefined`，要么做错事（写空内容、写到错误路径），要么在很远的地方报一句读不出原因的错误（`Task #undefined not found`）。补齐这一层的成本远低于它挡住的问题。

**参照物**：Claude Code 在工具执行前做四层检查（JSON 解析 → 工具自己的 `coerceInput` 兼容层 → zod schema 校验 → 工具自己的语义校验），任何一层失败都**不抛异常**，而是返回一条带 `is_error` 的工具结果，模型下一轮可自行修正。本规格对齐它的**第三层与文案**，不抄前两层。

**独立测试**：调用 `TaskUpdate` 只传 `{ "id": "2", "status": "completed" }`，确认返回的错误点明「缺少 `taskId`」且任务未被修改；补齐 `taskId` 后再调一次，确认正常执行。

**验收场景**：

1. **假设** 一个必填参数没有出现在调用参数里，**当** 该工具被调用时，**则** 返回失败的工具结果，内容为 ``The required parameter `taskId` is missing``，且工具实现**没有**被执行。
2. **假设** 调用参数里带了声明之外的多余键，**当** 该工具被调用时，**则** 返回 ``An unexpected parameter `id` was provided``——参数名写错正是靠这一条被点出来。
3. **假设** 参数值类型与声明不符（如声明 `number` 却传 `"20"`），**当** 该工具被调用时，**则** 返回 ``The parameter `offset` type is expected as `number` but provided as `string` ``。
4. **假设** 参数值不在声明的 `enum` 内，**当** 该工具被调用时，**则** 返回 ``The parameter `mode` is invalid: expected one of …``。
5. **假设** 一个可选参数传了 `null` 而声明只允许 `string`，**当** 该工具被调用时，**则** 与类型不符同处理（`null` 不算"缺失"）；声明允许 `null` 时正常通过。
6. **假设** 调用参数完全合法（包括可选参数缺省、以及声明为自由键值对的对象参数，如 `AskUserQuestion` 的 `metadata`），**当** 该工具被调用时，**则** 行为与本规格之前完全一致。
7. **假设** 一次参数校验失败，**当** 它返回给模型时，**则** 它只是一条**可恢复**的工具结果：对话继续、模型下一轮可以重试，不抛异常中断回合。
8. **假设** 被调用的是 MCP 工具，**当** 参数不完整时，**则** 不由 wave 校验（参数契约属于 MCP 服务端，wave 只透传），行为与本规格之前一致。
9. **假设** 被调用的是 `buildTool()` / `customTools` 注册的自定义工具，**当** 参数不完整时，**则** 不由 wave 校验——其 schema 由 SDK 用户定义，行为保持不变。
10. **假设** 一次调用参数有误，**当** 校验失败时，**则** 不触发 PreToolUse hook、也不进入权限判定——参数错的调用不该弹权限框、也不该在 hook 里留下痕迹。

**口径与边界**：

* **真相源是工具自己声明的 `config.function.parameters`**，不新增第二份 schema：不引入 zod 再导出 JSON Schema 的双写形态（CC 用 zod 是因为它的 schema 本来就以 zod 定义；wave 反过来）。
* **支持的子集**：`type`、`required`、`enum`、未知键、嵌套 object 的 `properties`、数组 `items` 的逐元素类型。`oneOf` / `anyOf` / `$ref` / 数值区间等不解释——解释不了的声明不拦截，避免把"没实现的 constraint"读成"校验通过"以外的含义。
* **未知键默认报错**（对齐 CC 的 zod strict 语义）：schema 显式声明 `additionalProperties: true` 时放开；声明为**对象 schema**（如 `metadata` 的 `{type: "string"}`）时按其校验值，不按"未知键"处理。
* **文案对齐 Claude Code**：三句固定形态（缺失 / 多余 / 类型不符）+ 一句 `enum` 与其它取值错误，汇总成 `` `${toolName} failed due to the following issue(s):` `` 加逐行明细；一次调用里的问题全部列出，不只报第一个。
* **不做 `coerceInput` 那类兼容层**：先把真实错误暴露出来，等有了校验失败的数据再决定兼容什么，避免"为兼容而兼容"。
* **校验失败不产生权限决策、不进 hook**：它发生在工具执行之前，不是一次被拒绝的权限请求。

## 边界情况

* **声明之外但被工具读取的参数**：本规格上线前已逐个核对过，内置工具读取的每个参数都在自己的 `parameters` 里声明过（`agentTool` 的 `availableSubagents` 属于 prompt 入参，不来自模型）。新增工具若读了未声明的参数，会被这一层挡住——这是有意的：要么补声明，要么改读法。
* **工具自己注入的内部字段**：只在校验前不增删参数；工具内部的临时字段由工具自己处理。
* **校验与 `additionalProperties` 缺省**：缺省视为"只认声明过的键"（严格），与 CC 一致；如果将来某个工具的合法调用因此被误伤，正确修法是给该工具的 schema 补上对应属性，而不是放宽全局。
