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 | 文件 | 说明 |
|---|---|---|
invalid | invalid.ts | 错误处理(工具不存在) |
bash | bash.ts | Shell 命令执行 |
read | read.ts | 文件读取 |
glob | glob.ts | 文件模式匹配 |
grep | grep.ts | 内容搜索 |
edit | edit.ts | 文件编辑 |
write | write.ts | 文件写入 |
task | task.ts | Agent 通信 |
webfetch | webfetch.ts | Web 内容获取 |
todowrite | todo.ts | Todo 写入 |
todoread | todo.ts | Todo 读取 |
websearch | websearch.ts | Web 搜索 |
codesearch | codesearch.ts | 代码搜索 |
skill | skill.ts | 技能系统 |
apply_patch | apply_patch.ts | 补丁应用 |
条件性工具
| 工具 ID | 文件 | 条件 |
|---|---|---|
question | question.ts | 仅在 app, cli, desktop 客户端 |
lsp | lsp.ts | 仅在 OPENCODE_EXPERIMENTAL_LSP_TOOL 标志启用 |
batch | batch.ts | 仅在 config.experimental.batch_tool === true |
plan-enter | plan.ts | 仅在 OPENCODE_EXPERIMENTAL_PLAN_MODE && OPENCODE_CLIENT === "cli" |
plan-exit | plan.ts | 仅在 OPENCODE_EXPERIMENTAL_PLAN_MODE && OPENCODE_CLIENT === "cli" |
加载的工具
| 来源 | 位置 | 格式 |
|---|---|---|
| 自定义工具 | <project>/.opencode/tool.js | CommonJS 模块 |
| 自定义工具 | <project>/.opencode/tools.js | CommonJS 模块 |
| 插件工具 | 插件目录 | 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{...}`
}
工具初始化时机
工具在以下情况下初始化:
- 首次使用时:当 LLM 第一次调用工具时
- Agent 切换时:不同 Agent 可能需要不同的工具配置
- 配置变更时:工具列表可能因配置变更而重新加载
相关文档
- Agent - Agent 配置
- Permission - 权限系统
- Plugin - 插件系统
变更历史
| 版本 | 变更内容 | 日期 |
|---|---|---|
| v1 | 初始 Tool 系统 | - |
| v1.1 | 添加插件工具支持 | - |
| v1.2 | 添加自动输出截断 | - |
| v1.3 | 添加条件性工具过滤 | - |