Code Reader
首页
帮助
设计文档
首页
帮助
设计文档
  • Tool 系统实现机制

Tool 系统实现机制

OpenCode 的 Tool 系统提供了可扩展的工具注册、执行和权限管理框架。

概述

Tool 系统是 OpenCode 中 AI 助手(Agent)执行特定操作的接口。每个工具都有明确的参数定义、执行逻辑和返回格式。系统支持:

  • 内置工具的自动注册
  • 自定义工具的动态加载
  • 插件工具的集成
  • 基于权限和配置的动态过滤
  • 输出截断和错误处理

定义位置

  • packages/opencode/src/tool/tool.ts - Tool 核心接口定义
  • packages/opencode/src/tool/registry.ts - 工具注册表

核心接口

Tool.Info

Tool 的核心接口定义:

export interface Info<Parameters extends z.ZodType = z.ZodType, M extends Metadata = Metadata> {
  id: string // 工具唯一标识符
  init: (ctx?: InitContext) => Promise<{
    description: string // 工具描述(传递给 LLM)
    parameters: Parameters // Zod 参数 schema
    execute(
      args: z.infer<Parameters>, // 解析后的参数
      ctx: Context<Metadata>, // 执行上下文
    ): Promise<{
      title: string // 返回结果的标题
      metadata: M // 元数据
      output: string // 文本输出
      attachments?: MessageV2.FilePart[] // 附件文件
    }>
    formatValidationError?(error: z.ZodError): string // 自定义错误格式化
  }>
}

Tool.Context

工具执行的上下文接口:

export type Context<M extends Metadata = Metadata> = {
  sessionID: string // 当前会话 ID
  messageID: string // 当前消息 ID
  agent: string // 当前 Agent 名称
  abort: AbortSignal // 中止信号
  callID?: string // 工具调用 ID
  extra?: { [key: string]: any } // 额外上下文数据

  // 更新元数据
  metadata(input: { title?: string; metadata?: M }): void

  // 权限检查
  ask(input: Omit<PermissionNext.Request, "id" | "sessionID" | "tool">): Promise<void>
}

Tool.Metadata

工具元数据接口(可扩展):

export interface Metadata {
  [key: string]: any // 灵活的键值对
}

Tool.InitContext

工具初始化上下文:

export interface InitContext {
  agent?: Agent.Info // 初始化时提供的 Agent 信息
}

Tool.define

工具定义函数:

export function define<Parameters extends z.ZodType, Result extends Metadata>(
  id: string, // 工具 ID
  init: Info<Parameters, Result>["init"] | Awaited<ReturnType<Info<Parameters, Result>["init"]>>,
): Info<Parameters, Result>

Tool 生命周期

1. 定义阶段

export const MyTool = Tool.define("my-tool", async (initCtx) => ({
  description: "Tool description for LLM",
  parameters: z.object({
    param1: z.string().describe("Parameter description"),
  }),
  async execute(args, ctx) {
    // 工具执行逻辑
    return {
      title: "Result title",
      metadata: {},
      output: "Result output",
    }
  },
})

2. 初始化阶段

在工具首次使用时调用 init 函数:

// packages/opencode/src/tool/tool.ts:53-84
init: async (initCtx) => {
  const toolInfo = init instanceof Function ? await init(initCtx) : init
  const execute = toolInfo.execute

  // 包装 execute 函数以添加参数验证和输出截断
  toolInfo.execute = async (args, ctx) => {
    // 参数验证
    try {
      toolInfo.parameters.parse(args)
    } catch (error) {
      if (error instanceof z.ZodError && toolInfo.formatValidationError) {
        throw new Error(toolInfo.formatValidationError(error), { cause: error })
      }
      throw new Error(
        `The ${id} tool was called with invalid arguments: ${error}.`,
        { cause: error },
      )
    }

    // 执行工具
    const result = await execute(args, ctx)

    // 输出截断
    if (result.metadata.truncated !== undefined) {
      return result
    }
    const truncated = await Truncate.output(result.output, {}, initCtx?.agent)
    return {
      ...result,
      output: truncated.content,
      metadata: {
        ...result.metadata,
        truncated: truncated.truncated,
        ...(truncated.truncated && { outputPath: truncated.outputPath }),
      },
    }
  }

  return toolInfo
},

3. 执行阶段

当 LLM 调用工具时,执行包装后的 execute 函数:

// 步骤:
1. 参数验证(Zod schema parse)
2. 调用原始 execute 函数
3. 输出截断(如果工具未自己处理)
4. 返回结果

ToolRegistry 注册机制

工具加载流程

定义位置:packages/opencode/src/tool/registry.ts:34-61

export const state = Instance.state(async () => {
  const custom = [] as Tool.Info[]
  const glob = new Bun.Glob("{tool,tools}/*.{js,ts}")

  // 1. 加载自定义工具(tool.js, tools.js)
  for (const dir of await Config.directories()) {
    for await (const match of glob.scan({ cwd: dir, ... })) {
      const namespace = path.basename(match, path.extname(match))
      const mod = await import(match)
      for (const [id, def] of Object.entries<ToolDefinition>(mod)) {
        custom.push(fromPlugin(id === "default" ? namespace : `${namespace}_${id}`, def))
      }
    }
  }

  // 2. 加载插件工具
  const plugins = await Plugin.list()
  for (const plugin of plugins) {
    for (const [id, def] of Object.entries(plugin.tool ?? {})) {
      custom.push(fromPlugin(id, def))
    }
  }

  return { custom }
})

插件工具转换

定义位置:packages/opencode/src/tool/registry.ts:63-80

function fromPlugin(id: string, def: ToolDefinition): Tool.Info {
  return {
    id,
    init: async (initCtx) => ({
      parameters: z.object(def.args), // 将定义转换为 Zod schema
      description: def.description,
      execute: async (args, ctx) => {
        const result = await def.execute(args as any, ctx)

        // 应用输出截断
        const out = await Truncate.output(result, {}, initCtx?.agent)
        return {
          title: "",
          output: out.truncated ? out.content : result,
          metadata: {
            truncated: out.truncated,
            outputPath: out.truncated ? out.outputPath : undefined,
          },
        }
      },
    }),
  }
}

工具注册

定义位置:packages/opencode/src/tool/registry.ts:82-90

export async function register(tool: Tool.Info) {
  const { custom } = await state()
  const idx = custom.findIndex((t) => t.id === tool.id)

  if (idx >= 0) {
    custom.splice(idx, 1, tool) // 替换现有工具
    return
  }

  custom.push(tool) // 添加新工具
}

工具获取和过滤

获取所有工具

定义位置:packages/opencode/src/tool/registry.ts:92-118

export async function all(): Promise<Tool.Info[]> {
  const custom = await state().then((x) => x.custom)
  const config = await Config.get()

  return [
    InvalidTool, // 总是第一个(错误处理)

    // 基于客户端类型过滤
    ...(["app", "cli", "desktop"].includes(Flag.OPENCODE_CLIENT) ? [QuestionTool] : []),

    // 核心工具
    BashTool,
    ReadTool,
    GlobTool,
    GrepTool,
    EditTool,
    WriteTool,
    TaskTool,
    WebFetchTool,
    TodoWriteTool,
    TodoReadTool,
    WebSearchTool,
    CodeSearchTool,
    SkillTool,
    ApplyPatchTool,

    // 基于实验性标志过滤
    ...(Flag.OPENCODE_EXPERIMENTAL_LSP_TOOL ? [LspTool] : []),
    ...(config.experimental?.batch_tool === true ? [BatchTool] : []),
    ...(Flag.OPENCODE_EXPERIMENTAL_PLAN_MODE && Flag.OPENCODE_CLIENT === "cli" ? [PlanExitTool, PlanEnterTool] : []),

    // 自定义和插件工具
    ...custom,
  ]
}

获取工具 ID 列表

定义位置:packages/opencode/src/tool/registry.ts:120-122

export async function ids() {
  return all().then((x) => x.map((t) => t.id))
}

获取过滤后的工具

定义位置:packages/opencode/src/tool/registry.ts:124-58

export async function tools(model: { providerID: string; modelID: string }, agent?: Agent.Info) {
  const tools = await all()

  const result = await Promise.all(
    tools
      .filter((t) => {
        // 1. 过滤 websearch/codesearch(仅对特定提供商启用)
        if (t.id === "codesearch" || t.id === "websearch") {
          return model.providerID === "opencode" || Flag.OPENCODE_ENABLE_EXA
        }

        // 2. 过滤 patch/edit/write(基于模型)
        const usePatch =
          model.modelID.includes("gpt-") && !model.modelID.includes("oss") && !model.modelID.includes("gpt-4")
        if (t.id === "apply_patch") return usePatch
        if (t.id === "edit" || t.id === "write") return !usePatch

        return true
      })
      .map(async (t) => {
        // 3. 初始化每个工具(传递 Agent 上下文)
        using _ = log.time(t.id)
        return {
          id: t.id,
          ...(await t.init({ agent })),
        }
      }),
  )

  return result
}

权限系统

权限检查接口

定义位置:packages/opencode/src/tool/tool.ts:24

export type Context<M extends Metadata = Metadata> = {
  // ...

  // 权限检查方法
  ask(input: Omit<PermissionNext.Request, "id" | "sessionID" | "tool">): Promise<void>
}

权限检查示例

async execute(params, ctx) {
  // 检查权限
  await ctx.ask({
    permission: "read",
    patterns: ["*.ts", "*.js"],
    always: ["package.json"],
  })

  // 执行工具逻辑
  // ...
}

输出截断

自动截断

工具的输出会自动截断,除非工具自己处理:

定义位置:packages/opencode/src/tool/tool.ts:69-82

const result = await execute(args, ctx)

// 如果工具未设置 metadata.truncated,则自动截断
if (result.metadata.truncated !== undefined) {
  return result
}

const truncated = await Truncate.output(result.output, {}, initCtx?.agent)
return {
  ...result,
  output: truncated.content,
  metadata: {
    ...result.metadata,
    truncated: truncated.truncated,
    ...(truncated.truncated && { outputPath: truncated.outputPath }),
  },
}

手动控制截断

工具可以选择自己处理截断:

async execute(params, ctx) {
  // 工具自己处理截断
  const truncated = await Truncate.output(largeOutput, {}, agent)

  return {
    title: "Result",
    metadata: {
      truncated: truncated.truncated,  // 👈 标记为已处理
      outputPath: truncated.outputPath,
    },
    output: truncated.content,
  }
}

错误处理

参数验证错误

定义位置:packages/opencode/src/tool/tool.ts:56-67

try {
  toolInfo.parameters.parse(args)
} catch (error) {
  if (error instanceof z.ZodError && toolInfo.formatValidationError) {
    throw new Error(toolInfo.formatValidationError(error), { cause: error })
  }

  throw new Error(`The ${id} tool was called with invalid arguments: ${error}.`, { cause: error })
}

自定义错误格式化

export const MyTool = Tool.define("my-tool", async (initCtx) => ({
  description: "Tool description",
  parameters: z.object({
    /* ... */
  }),

  // 自定义错误格式化
  formatValidationError(error: z.ZodError): string {
    return `参数错误:${error.message}\n请检查:${error.errors.map((e) => e.message).join(", ")}`
  },

  async execute(args, ctx) {
    // ...
  },
}))

内置工具列表

以下工具在 ToolRegistry.all() 中注册:

核心工具

工具 ID文件说明
invalidinvalid.ts错误处理(工具不存在)
bashbash.tsShell 命令执行
readread.ts文件读取
globglob.ts文件模式匹配
grepgrep.ts内容搜索
editedit.ts文件编辑
writewrite.ts文件写入
tasktask.tsAgent 通信
webfetchwebfetch.tsWeb 内容获取
todowritetodo.tsTodo 写入
todoreadtodo.tsTodo 读取
websearchwebsearch.tsWeb 搜索
codesearchcodesearch.ts代码搜索
skillskill.ts技能系统
apply_patchapply_patch.ts补丁应用

条件性工具

工具 ID文件条件
questionquestion.ts仅在 app, cli, desktop 客户端
lsplsp.ts仅在 OPENCODE_EXPERIMENTAL_LSP_TOOL 标志启用
batchbatch.ts仅在 config.experimental.batch_tool === true
plan-enterplan.ts仅在 OPENCODE_EXPERIMENTAL_PLAN_MODE && OPENCODE_CLIENT === "cli"
plan-exitplan.ts仅在 OPENCODE_EXPERIMENTAL_PLAN_MODE && OPENCODE_CLIENT === "cli"

加载的工具

来源位置格式
自定义工具<project>/.opencode/tool.jsCommonJS 模块
自定义工具<project>/.opencode/tools.jsCommonJS 模块
插件工具插件目录CommonJS 模块

典型使用场景

场景 1:定义内置工具

// packages/opencode/src/tool/my-tool.ts
export const MyTool = Tool.define("my-tool", {
  description: "My custom tool",
  parameters: z.object({
    input: z.string(),
  }),
  async execute(args, ctx) {
    return {
      title: "Result",
      metadata: {},
      output: "Processed: " + args.input,
    }
  },
})

场景 2:注册自定义工具

创建文件:.opencode/tool.js

module.exports = {
  myCustomTool: {
    description: "Custom tool for specific task",
    args: {
      param1: {
        type: "string",
        description: "Parameter description",
      },
    },
    async execute(args, ctx) {
      // 执行逻辑
      return "Result"
    },
  },
}

场景 3:动态注册工具

import { ToolRegistry } from "@/tool/registry"
import { Tool } from "@/tool/tool"

const customTool: Tool.Info = {
  id: "custom-tool",
  init: async () => ({
    description: "Dynamic tool",
    parameters: z.object({
      /* ... */
    }),
    async execute(args, ctx) {
      return { title: "", metadata: {}, output: "" }
    },
  }),
}

await ToolRegistry.register(customTool)

场景 4:获取可用工具

const allTools = await ToolRegistry.all()
console.log(`Total tools: ${allTools.length}`)

const toolIds = await ToolRegistry.ids()
console.log(`Tool IDs: ${toolIds.join(", ")}`)

const filteredTools = await ToolRegistry.tools({ providerID: "anthropic", modelID: "claude-3-5-sonnet" }, agent)
console.log(`Available tools: ${filteredTools.length}`)

对象关系

最佳实践

1. 清晰的参数定义

使用 Zod schema 清晰定义参数:

parameters: z.object({
  path: z.string().describe("File path to read"),
  offset: z.number().optional().describe("Start line number"),
  limit: z.number().optional().describe("Number of lines to read"),
})

2. 合理的输出截断

  • 对于可能产生大量输出的工具,让系统自动截断
  • 对于需要精确控制输出的工具,手动处理截断
  • 总是在 metadata 中设置 truncated 标记

3. 适当的权限检查

在工具开始时检查所需权限:

async execute(params, ctx) {
  await ctx.ask({
    permission: "read",
    patterns: [params.filepath],
  })

  // 执行操作
}

4. 丰富的元数据

提供有用的元数据帮助 UI 和监控:

return {
  title: "File processed",
  metadata: {
    filepath: params.path,
    size: stats.size,
    lines: content.split("\n").length,
    truncated: false,
  },
  output: content,
}

5. 自定义错误消息

提供清晰的错误消息帮助 LLM 理解:

formatValidationError(error: z.ZodError): string {
  const issues = error.errors.map(e =>
    `${e.path.join('.')} - ${e.message}`
  ).join('\n')
  return `Invalid parameters:\n${issues}\n\nExample usage:\n{...}`
}

工具初始化时机

工具在以下情况下初始化:

  1. 首次使用时:当 LLM 第一次调用工具时
  2. Agent 切换时:不同 Agent 可能需要不同的工具配置
  3. 配置变更时:工具列表可能因配置变更而重新加载

相关文档

  • Agent - Agent 配置
  • Permission - 权限系统
  • Plugin - 插件系统

变更历史

版本变更内容日期
v1初始 Tool 系统-
v1.1添加插件工具支持-
v1.2添加自动输出截断-
v1.3添加条件性工具过滤-