# 工具契约与注册表

> Tool.define 的契约与包装、ToolRegistry 的内置清单与过滤规则、每轮如何把工具交给模型，以及截断、无效调用和工作区边界。

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

现役（V1）工具都在 `packages/opencode/src/tool/` 下。一个工具从定义到被模型调用，要依次经过契约、注册表、逐轮组装和请求准备四层。

## 契约：Def 与 Context

`Tool.define(id, init)` 接收一个 Effect，初始化后产出 `description`（多半来自同目录的 `.txt`）、`parameters`（Effect Schema）、可选的 `jsonSchema`、`execute` 与 `formatValidationError`。执行时拿到的上下文是：

```ts
export type Context<M extends Metadata = Metadata> = {
  sessionID: SessionID
  messageID: MessageID
  agent: string
  abort: AbortSignal
  callID?: string
  extra?: { [key: string]: unknown }
  messages: SessionV1.WithParts[]
  metadata(input: { title?: string; metadata?: M }): Effect.Effect<void>
  ask(input: Omit<PermissionV1.Request, "id" | "sessionID" | "tool">): Effect.Effect<void>
}
```

（`packages/opencode/src/tool/tool.ts:36`）

两个回调最关键：`metadata()` 把标题和元数据写回正在运行的工具部件，TUI 据此实时刷新，Shell 的滚动输出就靠它；`ask()` 发起权限询问，工具只填 `permission`、`patterns`、`always`、`metadata`，会话与调用定位由上下文补齐，详见[权限系统](https://daiw.org/manual/opencode/permission)。

`extra` 是个杂物袋：会话层放进 `model`、`bypassAgentCheck` 和给 task 用的 `promptOps`；用户附加文件时，会话层直接调用 read 并带上 `bypassCwdCheck`。返回值只有四项：`title`、`metadata`、`output`（给模型的文本）与可选的 `attachments`。

## wrap：白送的校验与截断

`define` 返回的并不是原始 `execute`，而是包了一层（删减后）：

```ts
const decode = Schema.decodeUnknownEffect(toolInfo.parameters)
const execute = toolInfo.execute
toolInfo.execute = (args, ctx) => {
  // ...
  return Effect.gen(function* () {
    const decoded = yield* decode(args).pipe(
      Effect.mapError(
        (error) =>
          new InvalidArgumentsError({
            tool: id,
            detail: toolInfo.formatValidationError ? toolInfo.formatValidationError(error) : String(error),
          }),
      ),
    )
    const result = yield* execute(decoded as Schema.Schema.Type<Parameters>, ctx)
    if (result.metadata.truncated !== undefined) {
      return result
    }
    const agent = yield* agents.get(ctx.agent)
    const truncated = yield* truncate.output(result.output, {}, agent)
    // ...
  }).pipe(Effect.orDie, Effect.withSpan("Tool.execute", { attributes: attrs }))
}
```

（`packages/opencode/src/tool/tool.ts:111`）

一是参数解码失败变成 `InvalidArgumentsError`，它的 `message` 是写给模型的“请按 schema 重写输入”；二是工具没表态（`metadata.truncated` 未设置）时，输出统一过一遍截断。read、bash 这类自带分页或截断的工具会显式设置该字段，避免被截两次。最后的 `Effect.orDie` 把失败变成异常交给 AI SDK，由[流处理器](https://daiw.org/manual/opencode/processor)记为工具部件的 `error` 状态。

## 截断：超长输出落盘

`truncate.ts` 默认在 2000 行或 50 KB 处截断，配置 `tool_output.max_lines` / `max_bytes` 可覆盖。超限时全文写入数据目录下的 `tool-output`，返回预览与提示；若当前 agent 能用 task，提示会建议把大文件外包给子代理（节选自 `packages/opencode/src/tool/truncate.ts:130`）：

```text
Use the Task tool to have explore agent process this file with Grep and Read (with offset/limit). Do NOT read the full file yourself - delegate to save context.
```

落盘文件保留 7 天，每小时清理一次。所有 agent 的 `external_directory` 都默认放行这个目录（`packages/opencode/src/agent/agent.ts:296`），模型事后用 read、grep 翻看不会触发询问。

## 注册表：内置清单与自定义来源

| 工具 ID | 注册条件 |
| --- | --- |
| `bash`、`read`、`glob`、`grep`、`edit`、`write`、`task`、`webfetch`、`todowrite`、`websearch`、`skill`、`apply_patch` | 总是注册 |
| `invalid` | 总是注册，但不开放给模型（见下文） |
| `question` | `OPENCODE_CLIENT` 为 app、cli、desktop（默认 cli），或设置 `OPENCODE_ENABLE_QUESTION_TOOL` |
| `execute` | `OPENCODE_EXPERIMENTAL_CODE_MODE` |
| `lsp` | `OPENCODE_EXPERIMENTAL_LSP_TOOL` |
| `plan_exit` | `OPENCODE_EXPERIMENTAL_PLAN_MODE` 且客户端为 cli |

后三个开关也会被总开关 `OPENCODE_EXPERIMENTAL` 一并打开。自定义工具有两个来源：配置目录下的 `{tool,tools}/*.{js,ts}`（默认导出以文件名为 ID，具名导出为 `文件名_导出名`），以及插件的 `tool` 字段。插件工具对外仍用 Zod 描述参数，注册表在边界上做兼容：

> Plugin tools still expose Zod args publicly; keep that compatibility boxed at the registry boundary and give the LLM the original JSON Schema.

（`packages/opencode/src/tool/registry.ts:126`）

插件 API 见[插件系统](https://daiw.org/manual/opencode/plugins)。

## 按模型过滤

`registry.tools()` 每轮根据模型再筛一遍：

```ts
const usePatch =
  input.modelID.includes("gpt-") && !input.modelID.includes("oss") && !input.modelID.includes("gpt-4")
if (tool.id === ApplyPatchTool.id) return usePatch
if (tool.id === EditTool.id || tool.id === WriteTool.id) return !usePatch
```

（`packages/opencode/src/tool/registry.ts:297`）

GPT 系列（gpt-4 与 oss 除外）拿 `apply_patch`，其余模型拿 `edit` + `write`，两套编辑工具互斥。`websearch` 只在使用 opencode 自家提供方或开启 Exa、Parallel 时出现；`execute` 在没有可见 MCP 工具时隐藏。之后每个工具还要过插件钩子 `tool.definition`，允许改写说明和参数；task 的说明末尾会追加当前 agent 可调用的子代理清单。

## 每轮组装：SessionTools.resolve

`packages/opencode/src/session/tools.ts` 把 `Tool.Def` 逐个包成 AI SDK 的 `tool()`：参数先经 `ToolJsonSchema.fromTool` 转成 JSON Schema 并规整（去掉可选字段的 `null` 分支、内联 `$ref`、给整数补安全范围等），再经 `ProviderTransform.schema` 按提供方修补（见[提供方适配层](https://daiw.org/manual/opencode/provider-transform)）；`execute` 前后触发 `tool.execute.before` / `tool.execute.after` 插件钩子，并给附件分配部件 ID。

MCP 工具也在这里并入（详见 [MCP 集成](https://daiw.org/manual/opencode/mcp)）：

- 键名是 `服务器名_工具名`（非法字符换成下划线），权限名就是这个键，模式固定为 `*`；
- 任一服务器支持 resources 时，额外注入 `list_mcp_resources`、`list_mcp_resource_templates`、`read_mcp_resource`，按 `read` 权限询问；
- 开启 Code Mode 时在这之前直接返回，MCP 工具改由 `execute` 调度（见[联网工具与 Code Mode](https://daiw.org/manual/opencode/web-and-codemode)）。

真正发请求前，`packages/opencode/src/session/llm/request.ts:208` 的 `resolveTools` 还会剔除被权限整体拒绝的工具，并把工具表按名字排序。

## 无效调用：invalid 工具

模型会拼错工具名，也会给出通不过校验的参数。AI SDK 此时回调 `experimental_repairToolCall`（删减后）：

```ts
async experimental_repairToolCall(failed) {
  const lower = failed.toolCall.toolName.toLowerCase()
  if (lower !== failed.toolCall.toolName && prepared.tools[lower]) {
    // ...
  }
  return {
    ...failed.toolCall,
    input: JSON.stringify({
      tool: failed.toolCall.toolName,
      error: failed.error.message,
    }),
    toolName: "invalid",
  }
},
```

（`packages/opencode/src/session/llm.ts:296`）

只是大小写不对就改成小写的工具名，其他情况一律改道到 `invalid` 工具，它只回一句 `The arguments provided to the tool are invalid: ...`。`invalid` 不在 `activeTools` 里，模型看不到它；坏调用由此变成一条普通的工具结果，循环照常继续。

## external_directory：工作区边界

`external-directory.ts` 是文件类工具共用的边界检查：目标在实例目录或 git worktree 内就放行（非 git 项目不看 worktree）；否则以 `目录/*` 为模式询问 `external_directory` 权限，文件取父目录。read、write、edit、apply_patch、glob、grep、lsp 都调用它，Shell 则自己解析命令找路径（见 [Shell 工具](https://daiw.org/manual/opencode/shell-tool)）。

V2 内核把这套契约重做成带输入输出编解码器的 `Tool.make`，截断也收归注册表统一处理，见 [V2 工具与权限](https://daiw.org/manual/opencode/tools-v2)。

下一篇：[文件工具：读、写、改](https://daiw.org/manual/opencode/file-tools)——read 的分页与嗅探、edit 的九级匹配级联，以及 apply_patch 的补丁格式。
