0%

跟着OpenCode学智能体设计和开发2:工具系统

工具注册:内置工具与可扩展性

工具注册表是 OpenCode 系统中管理所有可用工具的核心组件,为内置工具和自定义工具提供了统一的接口。这种架构使 agents 能够通过一致的 API 访问多样化的功能集,同时通过插件和基于配置的工具定义支持可扩展性。

工具注册表架构

工具注册表采用集中式类插件架构,其中工具通过元数据、功能和权限要求进行注册。注册表维护两类工具:随 OpenCode 附带的内置工具,以及可由用户或第三方插件添加的自定义工具。

内置工具静态注册在 all() 函数中,包括文件操作、搜索功能、bash 执行和 web 交互等核心功能。自定义工具在运行时初始化期间从配置目录和插件中动态发现。

注册表根据使用的 AI provider 执行过滤——某些工具如 codesearch 和 websearch 仅适用于 OpenCode provider 或通过标志显式启用时。此外,工具在提供给特定 agents 之前会根据 agent 权限进行过滤。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
async function all(): Promise<Tool.Info[]> {
const custom = await state().then((x) => x.custom)
const config = await Config.get()

return [
InvalidTool,
...(Flag.OPENCODE_CLIENT === "cli" ? [QuestionTool] : []),
BashTool,
ReadTool,
GlobTool,
GrepTool,
EditTool,
WriteTool,
TaskTool,
WebFetchTool,
TodoWriteTool,
TodoReadTool,
WebSearchTool,
CodeSearchTool,
SkillTool,
...(Flag.OPENCODE_EXPERIMENTAL_LSP_TOOL ? [LspTool] : []),
...(config.experimental?.batch_tool === true ? [BatchTool] : []),
...custom,
]
}

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

export async function tools(providerID: string, agent?: Agent.Info) {
const tools = await all()
const result = await Promise.all(
tools
.filter((t) => {
// Enable websearch/codesearch for zen users OR via enable flag
if (t.id === "codesearch" || t.id === "websearch") {
return providerID === "opencode" || Flag.OPENCODE_ENABLE_EXA
}
return true
})
.map(async (t) => {
using _ = log.time(t.id)
return {
id: t.id,
...(await t.init({ agent })),
}
}),
)
return result
}

工具接口结构

注册表中的每个工具都实现由 Tool.Info 类型定义的一致接口。该接口确保在整个系统中对工具进行统一处理,并实现适当的验证、执行和输出管理。

Tool.define() 工厂函数使用 Zod schemas 和输出截断自动验证包装工具实现。当执行工具时,注册表根据工具的 schema 验证输入参数,执行工具逻辑,并在结果超过配置限制时应用输出截断。

来源:packages/opencode/src/tool/tool.ts, packages/opencode/src/tool/registry.ts

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
export namespace Tool {
interface Metadata {
[key: string]: any
}

export interface InitContext {
agent?: Agent.Info
}

export type Context<M extends Metadata = Metadata> = {
sessionID: string
messageID: string
agent: string
abort: AbortSignal
callID?: string
extra?: { [key: string]: any }
metadata(input: { title?: string; metadata?: M }): void
ask(input: Omit<PermissionNext.Request, "id" | "sessionID" | "tool">): Promise<void>
}
export interface Info<Parameters extends z.ZodType = z.ZodType, M extends Metadata = Metadata> {
id: string
init: (ctx?: InitContext) => Promise<{
description: string
parameters: Parameters
execute(
args: z.infer<Parameters>,
ctx: Context,
): Promise<{
title: string
metadata: M
output: string
attachments?: MessageV2.FilePart[]
}>
formatValidationError?(error: z.ZodError): string
}>
}

export type InferParameters<T extends Info> = T extends Info<infer P> ? z.infer<P> : never
export type InferMetadata<T extends Info> = T extends Info<any, infer M> ? M : never

export function define<Parameters extends z.ZodType, Result extends Metadata>(
id: string,
init: Info<Parameters, Result>["init"] | Awaited<ReturnType<Info<Parameters, Result>["init"]>>,
): Info<Parameters, Result> {
return {
id,
init: async (initCtx) => {
const toolInfo = init instanceof Function ? await init(initCtx) : init
const execute = toolInfo.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}.\nPlease rewrite the input so it satisfies the expected schema.`,
{ cause: error },
)
}
const result = await execute(args, ctx)
// skip truncation for tools that handle it themselves
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
},
}
}
}

内置工具概述

OpenCode 附带了一套按功能类别组织的全面内置工具。这些工具涵盖了 AI agents 在处理代码库时需要执行的最常见操作。

类别 工具 用途
文件操作 read, write, edit, multiedit, patch, batch 使用各种编辑策略读取、创建、修改文件
搜索与导航 grep, glob, codesearch, ls 搜索文件内容、按模式查找文件、列出目录
执行 bash, task 执行 shell 命令和长时间运行的任务
Web webfetch, websearch 获取网页和搜索互联网
LSP 集成 lsp 查询语言服务器以获取代码智能
任务管理 todo (read/write) 管理任务列表以跟踪工作
技能 skill 执行专门的 agent 工作流
系统 question, invalid 交互式提示词和错误处理

文件操作工具

Read Tool:读取文件内容,支持可选的分页功能。支持 offset(基于 0 的行号)和 limit 参数来读取大文件的特定范围。如果目标文件不存在,则提供相似文件名的建议。

来源:packages/opencode/src/tool/read.ts

它的description在read.txt中,即:

1
2
3
4
5
6
7
8
9
10
11
12
Reads a file from the local filesystem. You can access any file directly by using this tool.
Assume this tool is able to read all files on the machine. If the User provides a path to a file assume that path is valid. It is okay to read a file that does not exist; an error will be returned.

Usage:
- The filePath parameter must be an absolute path, not a relative path
- By default, it reads up to 2000 lines starting from the beginning of the file
- You can optionally specify a line offset and limit (especially handy for long files), but it's recommended to read the whole file by not providing these parameters
- Any lines longer than 2000 characters will be truncated
- Results are returned using cat -n format, with line numbers starting at 1
- You have the capability to call multiple tools in a single response. It is always better to speculatively read multiple files as a batch that are potentially useful.
- If you read a file that exists but has empty contents you will receive a system reminder warning in place of file contents.
- You can read image files using this tool.

这是告诉大模型该工具的用途,让大模型可以通过该描述来调用它,实际的用法在read.ts中。

Edit Tool:使用基于差异的编辑在文件中执行字符串替换。支持 replaceAll 标志以替换所有出现的字符串模式。包括安全检查以防止意外修改,并生成统一差异以供验证。

来源:packages/opencode/src/tool/edit.txt

1
2
3
4
5
6
7
8
9
10
Performs exact string replacements in files. 

Usage:
- You must use your `Read` tool at least once in the conversation before editing. This tool will error if you attempt an edit without reading the file.
- When editing text from Read tool output, ensure you preserve the exact indentation (tabs/spaces) as it appears AFTER the line number prefix. The line number prefix format is: spaces + line number + tab. Everything after that tab is the actual file content to match. Never include any part of the line number prefix in the oldString or newString.
- ALWAYS prefer editing existing files in the codebase. NEVER write new files unless explicitly required.
- Only use emojis if the user explicitly requests it. Avoid adding emojis to files unless asked.
- The edit will FAIL if `oldString` is not found in the file with an error "oldString not found in content".
- The edit will FAIL if `oldString` is found multiple times in the file with an error "oldString found multiple times and requires more code context to uniquely identify the intended match". Either provide a larger string with more surrounding context to make it unique or use `replaceAll` to change every instance of `oldString`.
- Use `replaceAll` for replacing and renaming strings across the file. This parameter is useful if you want to rename a variable for instance.

Write Tool:创建新文件或完全覆盖现有文件。用于从头创建新文件或替换整个文件内容时使用。

MultiEdit Tool:在单个工具调用中应用多个编辑操作,对于需要在多个位置进行协调更改的复杂重构任务很有用。

Patch Tool:将统一差异补丁应用于文件,支持标准补丁格式以从外部来源导入更改。

Batch Tool:用于批量文件操作的实验性工具,通过 config.experimental.batch_tool 标志启用。

搜索和导航工具

Grep Tool:使用正则表达式模式搜索文件内容,支持文件包含过滤器。底层使用 ripgrep 在大型代码库上进行快速、递归的搜索操作。

来源:packages/opencode/src/tool/grep.ts

Glob Tool:查找匹配 glob 模式的文件。支持标准 glob 语法和可选路径规范以在特定目录中搜索。

来源:packages/opencode/src/tool/glob.ts

CodeSearch Tool:提供语义代码搜索功能,仅适用于 OpenCode provider 或通过 OPENCODE_ENABLE_EXA 标志启用时可用。

来源:packages/opencode/src/tool/registry.ts

Ls Tool:列出目录内容,支持可选过滤,提供了一种探索目录结构的轻量级方式。

执行工具

Bash Tool:在持久的 shell 会话中执行 shell 命令,具有全面的安全措施。支持可选超时(默认 2 分钟)和 workdir 参数以在特定目录中执行命令。包括有关避免反模式的广泛指导,例如使用 cd 命令而不是 workdir 参数。

来源:packages/opencode/src/tool/bash.ts, packages/opencode/src/tool/bash.txt

Bash 工具包括复杂的 Git 安全协议,可防止强制推送、跳过挂钩或在没有明确用户同意的情况下修改已推送的提交等破坏性操作。它还提供了使用 gh CLI 工具创建 pull requests 的指导,包括正确的格式和分支管理。

Task Tool:管理可以跨越多个工具调用的长时间运行的任务,适用于需要在多个 agent 步骤之间保持持久状态的操作。

Web 工具

WebFetch Tool:从 web URL 获取内容,使 agents 能够访问在线文档、API 参考和其他 web 资源。

WebSearch Tool:使用 Exa 执行 web 搜索,仅适用于 OpenCode provider 或通过 OPENCODE_ENABLE_EXA 标志启用时可用。对于研究解决方案、查找库或从互联网收集信息很有用。

来源:packages/opencode/src/tool/registry.ts

LSP 集成

Lsp Tool:为代码智能功能提供语言服务器协议 (Language Server Protocol) 服务器的接口。这是一个通过 OPENCODE_EXPERIMENTAL_LSP_TOOL 标志启用的实验性工具,允许 agents 查询符号、定义、引用和其他代码结构信息。

来源:packages/opencode/src/tool/registry.ts

任务管理

TodoWrite Tool / TodoRead Tool:管理任务列表以跟踪跨 agent 会话的工作。支持创建、读取和更新具有结构化元数据的任务,适用于规划和跟踪多步骤操作。

技能

Skill Tool:执行专门的 agent 工作流或”技能”,将复杂的多步骤操作打包为可重用的单元。技能根据 agent 权限进行过滤,确保 agents 仅访问适合其权限级别的技能。

来源:packages/opencode/src/tool/skill.ts

Agent调用工具的原理

Agent 通过一个标准化的工具定义和 LLM 的函数调用能力来决定调用哪个工具。整个过程不是 Agent “思考”出来的,而是 LLM 基于工具描述自动推断的。

工作原理如下:

工具定义 - 每个工具都有完整的描述和参数定义。例如
read 工具L18-L22 定义了:

1
2
3
id: "read"(唯一标识)
description: 工具功能的详细说明
parameters: 使用 Zod schema 定义的参数结构(filePath, offset, limit 等)

工具注册与初始化 -
ToolRegistry.tools()L120-L140 收集所有可用工具并返回它们的完整信息:

1
2
3
4
return {
id: t.id,
...(await t.init({ agent })), // 包含 description 和 parameters
}

传递给 LLM - 在
LLM.stream()L135-L170 中,这些工具被传递给 AI SDK 的 streamText 函数:

1
2
3
4
5
6
const tools = await resolveTools(input)

return streamText({
tools, // 所有可用工具的完整定义
// ...其他参数
})

LLM 自动决策 - LLM 会:

  1. 读取每个工具的 description 理解其功能
  2. 查看每个工具的 parameters 了解如何调用
  3. 根据用户请求,自动选择最匹配的工具
  4. 生成符合该工具参数 schema 的调用请求

执行与反馈 - 工具调用会返回结果给 LLM,LLM 基于结果继续对话或决定是否需要调用其他工具

关键点: Agent 不需要知道何时调用哪个工具,这个决策完全由 LLM 基于工具的描述和参数定义自动完成。系统通过标准化的工具格式(OpenAI 函数调用兼容)实现了这种能力。

自定义工具和可扩展性

工具注册表通过两种主要机制支持可扩展性:基于配置的自定义工具和插件系统集成。

基于配置的自定义工具

自定义工具可以在配置目录中定义为 JavaScript 或 TypeScript 文件。注册表扫描 Config.directories() 返回的所有目录中匹配 tool/*.{js,ts} 的文件,允许用户在不修改 OpenCode 代码库的情况下添加自定义工具。

每个工具文件导出一个具有以下结构的 ToolDefinition 对象:

  • description:描述工具功能的字符串
  • args:定义工具参数的 Zod schema
  • execute:实现工具逻辑的异步函数
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
export const state = Instance.state(async () => {
const custom = [] as Tool.Info[]
const glob = new Bun.Glob("tool/*.{js,ts}")

for (const dir of await Config.directories()) {
for await (const match of glob.scan({
cwd: dir,
absolute: true,
followSymlinks: true,
dot: true,
})) {
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))
}
}
}

注册表自动使用输出截断和验证包装这些定义,通过 fromPlugin() 函数将其转换为内部 Tool.Info 格式。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
function fromPlugin(id: string, def: ToolDefinition): Tool.Info {
return {
id,
init: async (initCtx) => ({
parameters: z.object(def.args),
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, packages/opencode/src/tool/registry.ts

插件系统集成

插件可以通过在其插件定义中导出工具来扩展 OpenCode。注册表加载所有可用插件并提取其工具导出,将它们与内置工具和基于配置的自定义工具一起注册。插件系统使用相同的 ToolDefinition 类型,确保所有可扩展机制之间的一致性。

来源:packages/opencode/src/tool/registry.ts

1
2
3
4
5
6
7
8
9
  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 }
})

动态工具过滤

工具注册表根据多个因素应用动态过滤,以确保仅将适当的工具提供给特定的 agents 和 providers:

1、基于 Provider 的过滤:某些工具如 codesearch 和 websearch 仅限于 OpenCode provider 或需要通过 OPENCODE_ENABLE_EXA 标志显式启用。这可以防止在不可用时使用昂贵或特定于 provider 的工具。

2、基于标志的过滤:实验性和功能标志控制工具可用性:

LspTool:通过 OPENCODE_EXPERIMENTAL_LSP_TOOL 标志启用
BatchTool:通过 config.experimental.batch_tool 设置启用

3、 客户端特定过滤:QuestionTool 仅在 CLI 模式下运行时包含 (Flag.OPENCODE_CLIENT === “cli”),因为它在其他上下文中不适用。

4、基于权限的过滤:工具根据 agent 权限进行过滤,虽然这主要通过 ctx.ask() 权限请求机制在单个工具级别强制执行,而不是在注册表级别。

来源:packages/opencode/src/tool/registry.ts

工具执行上下文

每次工具执行都会收到一个包含有关执行环境信息的上下文对象:

1
2
3
4
5
6
7
8
9
10
{
sessionID: string, // 唯一会话标识符
messageID: string, // 触发工具调用的消息
agent: string, // Agent 标识符
abort: AbortSignal, // 取消执行的信号
callID?: string, // 唯一工具调用标识符
extra?: object, // 附加执行元数据
metadata(), // 更新工具元数据的函数
ask() // 请求权限的函数
}

metadata() 函数允许工具将元数据附加到其结果以进行 UI 显示和跟踪。ask() 函数实现权限系统,要求工具在访问敏感资源(如文件)或执行命令之前请求权限。

来源:packages/opencode/src/tool/tool.ts

输出管理和截断

工具注册表自动应用输出截断,以防止过多的输出淹没上下文窗口。截断系统支持行数 (MAX_LINES) 和字节大小 (MAX_BYTES) 的可配置限制。

当输出超过限制时,截断系统:

将完整输出写入文件
返回限制内的截断版本
包含指示截断状态和输出文件路径的元数据
允许 agents 使用带有 offset/limit 参数的 Read 工具读取特定部分

某些工具通过设置 metadata.truncated 来处理自己的截断,这会向注册表发出信号以跳过自动截断。这对于需要自定义截断策略或希望对输出格式进行更多控制的工具很有用。

来源:packages/opencode/src/tool/tool.ts, packages/opencode/src/tool/registry.ts

工具注册 API

注册表提供了 register() 函数,用于在运行时动态添加工具。该函数替换具有相同 ID 的现有工具,或者如果不存在则将其添加到注册表。此 API 对于需要在初始注册表初始化后注册工具的插件和扩展很有用。

来源:packages/opencode/src/tool/registry.ts

文件操作:编辑、读取、写入及多编辑工具

文件操作工具构成了 Agent 与本地文件系统交互的基础设施。这些工具——Read、Write、Edit 和 Multi-edit——提供了受控的、具有权限感知的文件访问能力,并具备复杂的 diff 生成、冲突检测和 LSP 集成功能以实现实时代码智能。该架构通过强制写入前读取、并发写入序列化以及灵活的文本匹配策略来处理常见的格式差异,从而强调了安全性。

工具架构概述

文件操作工具遵循基于 Tool.define() 框架的统一架构模式。每个工具都实现了一致的接口,包括 Zod schema 验证、参数化执行上下文,以及与权限系统、LSP 服务和事件总线的集成。该架构通过多层验证和并发操作处理优先保障安全性。

Read 工具

Read 工具提供了对文件内容的受控访问,支持可配置的分页、二进制文件检测以及对媒体文件的特殊处理。它作为 Agent 在修改之前检查代码和配置文件的基础访问点。

参数与配置

Parameter Type Required Description Default
filePath string Yes 要读取的文件的绝对路径 -
offset number No 开始读取的行号(从 0 开始) 0
limit number No 要读取的行数 2000

执行流程

Read 工具在返回文件内容之前会执行全面的验证和处理流程。当找不到文件时,它会基于目录条目提供模糊匹配的智能建议,以帮助 Agent 纠正路径错误。对于二进制文件,工具会检测其类型并返回相应的错误,而图像和 PDF 文件则会转换为 base64 编码的附件以提供预览功能。

来源:read.ts

安全性与限制

该工具实施了多重安全约束以防止资源耗尽并确保稳定运行:

  • 字节限制:每次读取操作最多 50KB
  • 行长度限制:超过 2000 个字符的行将被截断并添加 “…” 后缀
  • 默认行数限制:每次读取请求 2000 行
  • 二进制检测:自动识别机制防止尝试将二进制文件作为文本读取

这些限制确保了大文件不会淹没 LLM 的上下文窗口,同时仍为典型的源文件提供全面的访问。当内容被截断时,元数据会包含一个 truncated 标志以指示部分交付。

来源:read.ts

Write 工具

Write 工具支持完整的文件替换,并包含防止意外数据丢失和跟踪修改时间戳的安全机制。它与权限系统集成,确保在执行破坏性操作前获得用户批准。

参数与约束

Parameter Type Required Description
content string Yes 要写入文件的完整内容
filePath string Yes 要写入的文件的绝对路径

关键要求:覆盖现有文件之前必须先读取该文件。FileTime 系统会跟踪读取时间戳,如果文件尚未被读取,或者自上次读取以来文件已被外部修改,则拒绝写入操作。

来源:write.ts

诊断集成

写入成功后,工具会触发 LSP 诊断,以识别由更改引入的任何错误。诊断报告包括:

  • 文件级错误:每个修改的文件最多 20 个错误
  • 项目级错误:来自最多 5 个相关文件的错误
  • 美化输出:带有文件位置的格式化错误消息

这种即时反馈循环允许 Agent 在继续后续操作之前检测并纠正问题,从而在整个编辑过程中保持代码质量。

来源:write.ts

事件发布

Write 工具通过 Bus 系统发布 File.Event.Edited 事件,使其他系统组件能够对文件更改做出反应。这种事件驱动架构支持实时重载、自动化测试触发器和连接客户端的 UI 更新等功能。

来源:write.ts

Edit 工具

Edit 工具实现了复杂的查找和替换功能,支持多种文本匹配策略。与简单的字符串替换不同,它采用级联的替换器算法,逐步规范化文本以处理常见的格式差异,例如空白字符变化、缩进差异和转义序列。

参数规范

Parameter Type Required Description
filePath string Yes 要修改的文件的绝对路径
oldString string Yes 要替换的文本(必须与上下文完全匹配)
newString string Yes 替换文本
replaceAll boolean No 如果为 true,则替换所有出现的位置;如果为 false,则仅替换第一个匹配项

来源:edit.ts

替换器策略级联

Edit 工具采用复杂的回退机制,包含九种不同的替换器算法,按顺序应用,直到找到匹配项为止:

替换器算法:

  • SimpleReplacer:直接字符串匹配
  1. LineTrimmedReplacer:忽略每行前导/尾随空白字符
  2. BlockAnchorReplacer:使用 Levenshtein 距离的相似度评分对 3 行以上的代码块进行模糊匹配
  3. WhitespaceNormalizedReplacer:将多个空白字符折叠为单个空格
  4. IndentationFlexibleReplacer:移除搜索内容和内容的最小缩进
  5. EscapeNormalizedReplacer:反转义特殊字符(\n, \t, \ 等)
  6. TrimmedBoundaryReplacer:在匹配前修剪搜索字符串
  7. ContextAwareReplacer:使用周围行对 3 行以上的代码块进行模糊匹配
  8. MultiOccurrenceReplacer:查找所有出现位置以进行 replaceAll 操作

这种级联方法显著提高了 Agent 尝试匹配具有轻微格式差异的文本时的编辑成功率,这在 LLM 生成的代码与原始代码的空白或缩进略有不同时是一种常见情况。

来源:edit.ts

并发写入安全

Edit 工具使用 FileTime.withLock() 来序列化对同一文件的并发写入操作。此锁定机制确保多个 Agent 或工具无法同时修改同一文件,从而防止竞争条件和数据损坏。锁定系统将 Promise 链接起来,使每个写入操作等待前一个操作完成后再继续。

来源:edit.ts

Diff 生成与修剪

每次编辑后,工具使用 diff 库中的 createTwoFilesPatch() 函数生成统一 diff。trimDiff() 函数随后通过从所有更改行中移除最小公共缩进来规范化 diff 输出中的缩进,从而为用户生成更清晰、更易读的 diff 显示。

来源:edit.ts

Multi-edit 工具

Multi-edit 工具扩展了 Edit 功能,支持在单个原子事务中执行多个查找和替换操作。这对于需要对同一文件进行多个相关更改的重构任务特别有用。

参数结构

Parameter Type Required Description
filePath string Yes 要修改的文件的绝对路径
edits array Yes 编辑操作对象的数组
edits[].oldString string Yes 每个操作中要替换的文本
edits[].newString string Yes 每个操作的替换文本
edits[].replaceAll boolean No 每个操作是否替换所有出现位置

来源:multiedit.ts

执行语义

Multi-edit 工具按顺序运行,这对操作规划有重要影响:

  • 顺序应用:编辑操作按数组中指定的顺序应用
  • 累积状态:每个编辑操作都在所有先前编辑的结果上进行
  • 原子回滚:如果任何编辑失败,则不会对文件应用任何更改
  • 继承 Edit 工具行为:所有编辑操作都使用相同的替换器级联和安全检查

这种执行模型要求在编辑可能影响后续编辑尝试匹配的文本时进行仔细规划。Agent 必须确保早期的编辑不会使后续编辑的搜索字符串失效。

来源:multiedit.ts

使用 Multi-edit 创建文件时,请在第一个编辑操作中使用空的 oldString,并将完整的文件内容作为 newString。随后的编辑操作可以修改新创建的文件,从而允许在单个原子操作中同时进行创建和修改。

权限系统集成

所有文件操作工具都通过 ctx.ask() 与权限系统集成,在执行破坏性操作之前提示用户批准。权限请求包括:

  • 权限类型:Read 工具为 “read”,Write/Edit/Multi-edit 工具为 “edit”
  • 文件模式:受影响的文件路径
  • 元数据:附加上下文,包括编辑操作的 diff 预览
  • 始终允许模式:绕过批准的配置模式(通常为 “*”)

权限系统支持每会话的批准缓存,允许用户授予对项目中文件的全面访问权限,而无需为每次操作重复确认。

来源:permission/next.ts

FileTime 与并发管理

FileTime 系统通过读取跟踪和写入序列化为文件操作提供核心安全机制:

核心功能:

  1. read(sessionID, file):记录读取文件时的时间戳
  2. assert(sessionID, file):验证文件已被读取且未被外部修改
  3. withLock(filepath, fn):使用 Promise 链接序列化写入操作

该系统防止了三种关键故障模式:未先读取文件的意外覆盖、并发写入导致的竞争条件以及静默覆盖外部更改。

来源:file/time.ts

错误处理与恢复

文件操作工具实现了全面的错误处理,提供具体、可操作的错误消息:

Error Scenario Error Message Resolution
File not found “File not found: {filepath}\n\nDid you mean one of these?” 检查路径,使用建议
Binary file read “Cannot read binary file: {filepath}” 使用不同的方法
Write without read “You must read the file before overwriting it” 先使用 Read 工具
File modified externally “File has been modified since it was last read” 重新读取文件
oldString not found “oldString not found in content” 提供带有上下文的精确匹配
Multiple matches “oldString found multiple times and requires more code context” 提供更多周围行或使用 replaceAll

这些具体的错误消息指导 Agent 采取正确的恢复策略,从而提高自动化编辑工作流程的可靠性。

来源:read.ts, edit.ts

LSP 集成

所有写入和编辑操作都会触发 Language Server Protocol 集成以实现实时代码智能:

  1. 文件触碰:LSP.touchFile() 通知 LSP 文件更改
  2. 诊断检索:LSP.diagnostics() 从语言服务器收集错误
  3. 错误报告:错误被格式化并包含在工具输出中

这种集成确保语法错误、类型不匹配和其他问题立即暴露给 Agent,从而在继续执行其他操作之前快速进行更正。诊断限制(每个文件 20 个,5 个项目文件)在全面性和响应时间之间取得了平衡。

来源:write.ts, edit.ts

工具注册与发现

文件操作工具与其他 Agent 功能一起注册在中央工具注册表中。注册表支持:

  • 内置工具:核心文件、搜索和执行工具
  • 插件工具:从配置目录动态加载
  • 自定义工具:用户定义的扩展

注册表使用优先级系统,其中 InvalidTool 首先出现以处理格式错误的工具请求,随后是按功能分组的操作工具。这种架构在为 Agent 保持稳定接口的同时实现了可扩展性。

来源:registry.ts

最佳实践

何时使用每种工具

Scenario Recommended Tool Rationale
Inspecting file contents ReadTool 非破坏性,支持分页
Creating new files WriteTool 直接内容写入
Replacing existing file WriteTool 完整替换并验证
Small text changes EditTool 精确的带上下文查找和替换
Multiple related edits MultiEditTool 原子批处理操作
Refactoring across file EditTool with replaceAll 全局字符串替换

错误预防策略

  1. 始终先读后写:FileTime 系统强制执行此操作,但 Agent 应在编辑之前显式读取文件
  2. 提供足够的上下文:在 oldString 中包含周围的行以唯一标识匹配项
  3. 使用 Multi-edit 保证原子性:当多个更改必须同时成功或失败时
  4. 检查诊断:在写入/编辑操作后查看 LSP 输出以发现引入的错误
  5. 处理二进制文件:为图像、PDF 和其他二进制格式使用适当的工具

性能考虑

  1. 大文件:使用 offset 和 limit 参数读取特定部分
  2. 多次编辑:优先使用 Multi-edit 而不是顺序的 Edit 调用以减少锁争用
  3. 诊断:在处理会产生大量错误的文件时,请注意限制(每个文件 20 个)

搜索与导航:Grep、Glob 及代码搜索工具

本页面介绍了 OpenCode 的搜索和导航工具,这些工具使 agents 能够高效地探索代码库、查找特定代码模式以及检索外部文档。这些工具构成了跨本地和远程资源进行代码理解和上下文收集的基础。

工具架构概览

搜索工具构建在统一的架构之上,集成了权限管理、执行处理和结果格式化。每个工具都遵循 Tool.define() 模式,提供一致的参数验证、权限检查和输出格式化。

Grep 工具:基于正则表达式的内容搜索

GrepTool 提供跨任意规模代码库的快速、基于正则表达式的内容搜索。它利用 ripgrep 的性能,同时增加了 OpenCode 特有的功能,如权限控制、修改时间排序和结果截断。

参数

参数 类型 描述 示例
pattern string 在文件内容中搜索的正则表达式模式 “function\s+\w+”
path string 搜索目录(默认为工作目录) “src/utils”
include string 文件模式过滤器 .ts” 或 “.{ts,tsx}”

实现细节

grep 工具使用特定标志执行 ripgrep 以确保结构化输出:-nH 用于显示行号和文件名,—hidden 和 —follow 用于搜索隐藏文件和跟踪符号链接,—field-match-separator=| 用于分隔输出字段以便解析 packages/opencode/src/tool/grep.ts#L31-L32。

结果按文件修改时间排序(最新的在前),限制为 100 个匹配项,超过 2000 个字符的行文本会被截断 packages/opencode/src/tool/grep.ts#L10, packages/opencode/src/tool/grep.ts#L89-L96。

权限集成

在执行之前,工具会请求搜索模式的权限,允许安全策略控制可搜索的内容 packages/opencode/src/tool/grep.ts#L20-L27。权限检查包括有关搜索范围的元数据,用于审计目的。

使用场景

当你需要查找包含特定模式或代码构造的文件时,grep 工具是理想选择。对于匹配计数或详细分析,请直接使用带有 rg 的 bash 工具。对于需要多轮搜索的开放式探索,Task 工具更为合适 packages/opencode/src/tool/grep.txt#L5-L9。

Glob 工具:文件模式匹配

GlobTool 使用 glob 模式提供快速文件发现,基于 ripgrep 的 —files 模式,可跨大型代码库进行高效的模式匹配。

参数

参数 类型 描述 示例
pattern string 用于匹配文件的 glob 模式 /*.js” 或 “src//*.ts”
path string 搜索目录(省略时使用默认值) “packages/“

实现细节

glob 工具使用 ripgrep 的文件列表功能,通过 —files 标志按 glob 模式过滤并排除 .git 目录 packages/opencode/src/file/ripgrep.ts#L208-L223。结果限制为 100 个文件,并按修改时间排序 packages/opencode/src/tool/glob.ts#L35-L42。

路径解析处理绝对路径和相对路径,将它们规范化为相对于实例目录的路径 packages/opencode/src/tool/glob.ts#L27-L30。该工具还会验证外部目录以防止未授权访问 packages/opencode/src/tool/glob.ts#L31。

使用场景

glob 工具最适合通过名称模式查找文件,例如定位所有 TypeScript 文件、配置文件或特定目录中的文件。该工具支持批量调用,使得在单个请求中执行多个 glob 搜索变得高效 packages/opencode/src/tool/glob.txt#L6-L7。

CodeSearch 工具:外部上下文检索

CodeSearchTool 与 Exa Code API 集成,用于检索库、SDK 和框架的外部文档、示例和 API 参考。

参数

参数 类型 范围 描述 示例
query string - 针对API、库或SDK的搜索查询 “React useState hook examples”
tokensNum number 1000-50000 返回的 token 数量 5000

实现细节

该工具向位于 https://mcp.exa.ai/mcp 的 Exa Code API 发送 JSON-RPC 2.0 请求,使用 get_code_context_exa 方法 packages/opencode/src/tool/codesearch.ts#L6-L9, packages/opencode/src/tool/codesearch.ts#L58-L66。请求在 30 秒后超时,以防止挂起 packages/opencode/src/tool/codesearch.ts#L68。

API 返回服务器发送事件(SSE)响应,工具会解析这些响应,从第一个可用数据行中提取代码上下文 packages/opencode/src/tool/codesearch.ts#L79-L89。

Token 数量指导

1000-3000 tokens:针对特定函数或模式的聚焦查询
3000-8000 tokens:大多数查询的默认平衡上下文
8000-50000 tokens:全面的文档和大量示例 packages/opencode/src/tool/codesearch.txt#L7-L10

使用场景

codesearch 工具专为任何需要外部上下文的编程相关任务而设计,包括 API 文档、框架模式、库使用示例和最佳实践 packages/opencode/src/tool/codesearch.txt#L3-L5。

Ripgrep 基础设施

所有本地搜索工具都依赖 Ripgrep 模块,该模块管理跨平台的 ripgrep 二进制文件生命周期。该架构包括自动下载、特定平台安装和错误处理。

平台支持

ripgrep 模块支持五种平台组合,从 GitHub releases 下载 14.1.1 版本 packages/opencode/src/file/ripgrep.ts#L91-L100:

平台 架构 操作系统 包格式
aarch64-apple-darwin ARM64 macOS tar.gz
aarch64-unknown-linux-gnu ARM64 Linux tar.gz
x86_64-apple-darwin x64 macOS tar.gz
x86_64-unknown-linux-musl x64 Linux tar.gz
x86_64-pc-windows-msvc x64 Windows zip

搜索 API

Ripgrep.search() 函数提供结构化的 JSON 输出,包含详细的匹配信息,包括文件路径、行号、文本内容、绝对偏移量和子匹配位置 packages/opencode/src/file/ripgrep.ts#L370-L408。

结果模式定义了四种事件类型:

  • Begin:文件搜索开始通知
  • Match:带有行上下文和子匹配的单个匹配项
  • End:带有统计信息的文件搜索完成
  • Summary:总体搜索指标,包括经过时间、执行的搜索次数、搜索的字节数和匹配计数 packages/opencode/src/file/ripgrep.ts#L14-L90

工具注册与发现

搜索工具在 ToolRegistry 中注册,该注册表管理内置工具、来自配置目录的自定义工具和插件工具。注册表与标志和配置集成,以有条件地启用实验性功能 packages/opencode/src/tool/registry.ts#L70-L98。

CodeSearchTool 根据提供商(opencode)或 OPENCODE_ENABLE_EXA 标志有条件地启用,允许在部署场景中灵活配置 packages/opencode/src/tool/registry.ts#L106-L110。

权限模型

所有搜索工具都与权限系统集成,在访问文件系统或外部资源之前需要明确批准。权限模型使用通配符模式匹配来定义访问规则 packages/opencode/src/permission/next.ts#L1-L270。

权限操作包括:

  • Allow(允许):继续操作,无需干预
  • Ask(询问):请求用户批准,可选择记住以备将来使用
  • Deny(拒绝):立即阻止访问

搜索工具对比

工具 范围 数据源 用例 输出格式
grep 本地代码库 ripgrep 正则内容搜索 文件路径、行号、匹配文本
glob 本地代码库 ripgrep —files 文件模式发现 按修改时间排序的文件路径
codesearch 外部网络 Exa API 文档和示例 代码片段和文档文本

在探索代码库时,在单个请求中批量处理多个 glob 和 grep 调用——agent 可以并行化这些操作以加快结果返回速度 packages/opencode/src/tool/glob.txt#L7。

错误处理和边缘情况

搜索工具处理各种边缘情况:

  • 无效的正则表达式模式:Ripgrep 通过退出代码报告错误,工具将其转换为描述性错误消息 packages/opencode/src/tool/grep.ts#L59-L65
  • 不存在的目录:外部目录验证器会抛出带有正确错误代码的 ENOENT 错误 packages/opencode/src/file/ripgrep.ts#L226-L233
  • API 超时:代码搜索请求在 30 秒后中止,并显示明确的超时错误 packages/opencode/src/tool/codesearch.ts#L68-L70
  • 结果截断:grep 和 glob 都提供元数据,指示结果何时超过限制,建议使用更具体的查询 packages/opencode/src/tool/grep.ts#L101-L104

Bash 集成与命令执行

OpenCode 中的 Bash 集成系统提供安全、基于权限控制的命令执行能力,使 Agent 能够通过 Shell 命令与系统交互,同时保持严格的安全控制。该系统结合了复杂的命令分析、细粒度的权限管理和实时输出流,提供了一个强大的终端执行环境。

架构概述

Bash 集成作为一个多层系统运行,连接 AI Agent 请求与实际的 Shell 命令执行。系统的核心使用基于 tree-sitter 的命令解析在执行前分析命令,对命令模式和目录访问应用权限检查,通过适当的清理管理进程生命周期,并将输出实时流式传输回 Agent。

该架构展示了清晰的关注点分离:解析用于安全,权限检查用于安全,进程管理用于可靠性,输出处理用于可见性。每个组件都可独立测试,并遵循 OpenCode 更广泛的纵深防御安全理念。

Shell 检测和配置

在执行任何命令之前,系统会为操作环境确定合适的 Shell。Shell.acceptable() 和 Shell.preferred() 中的 Shell 检测逻辑提供了跨不同平台的智能回退机制。

来源: shell.ts

平台特定的 Shell 解析

平台 首选 Shell 回退行为
安装了 Git Bash 的 Windows 从检测到的 Git 安装路径获取 Git Bash 通过 COMSPEC 环境变量获取 cmd.exe
未安装 Git Bash 的 Windows cmd.exe 内置系统 Shell
macOS 用户的 SHELL 环境变量 /bin/zsh
Linux 用户的 SHELL 环境变量 /bin/bash → /bin/sh

系统维护不兼容 Shell(fish, nu)的黑名单,并自动回退到可接受的替代方案。在 Windows 上,它通过检查 Git 可执行文件路径并相对于它构建 bash.exe 路径来智能发现 Git Bash。

配置选项

多个标志控制 Bash 工具行为,可在系统级别配置:

  • OPENCODE_EXPERIMENTAL_BASH_DEFAULT_TIMEOUT_MS: 自定义超时时长(默认: 120,000ms / 2 分钟)
  • OPENCODE_GIT_BASH_PATH: Windows 上显式指定 Git Bash 路径(覆盖自动检测)

来源: bash.ts

命令分析和权限系统

Bash 集成最复杂的方面是其命令分析管道,它使用 tree-sitter-bash 解析在执行前理解命令结构。这实现了细粒度的权限控制,可以区分不同的命令类型及其潜在影响。

命令解析策略

命令使用带有 Bash 语法的 web-tree-sitter 进行解析,这产生一个抽象语法树(AST),可以分析命令模式、参数结构和潜在的安全问题。解析器被延迟初始化以提高启动性能。

来源: bash.ts

权限类别

系统区分两种主要的权限类型:

  1. bash Permission: 控制 Agent 可以执行哪些命令模式
  2. external_directory Permission: 控制对项目根目录之外目录的访问

在分析命令时,系统从 AST 中提取所有命令节点并对它们进行分类:

来源: bash.ts

命令模式匹配

BashArity 系统通过与常见 CLI 工具及其命令结构的综合字典匹配,来识别人类可理解的命令前缀。这使得权限系统能够理解 npm run devnpm run build 应该被分别跟踪,而 npm install package-anpm install package-b 共享相同的权限模式。

来源: arity.ts

命令前缀 Arity 示例命令
git 2 git checkout main, git log
git checkout 2 与 git 相同
npm run 3 npm run dev, npm run build
docker compose 3 docker compose up, docker compose down
kubectl rollout 3 kubectl rollout restart deploy

Arity 系统遵循最长匹配前缀优先的策略,确保特定的子命令(如 npm run)与其父命令具有不同的权限模式。

权限系统生成特定模式(精确命令匹配)和通配符模式(基于前缀的匹配)。特定模式触发初始权限请求,而通配符模式为类似的未来命令启用“始终允许”功能。这种双重模式方法在安全性和用户便利性之间取得了平衡。

执行模型和进程管理

一旦获得权限,Bash 工具使用 Node 的 spawn 函数生成子进程,并精心配置选项以确保安全性和可观察性。

来源: bash.ts

进程配置

参数 目的 实现
shell Shell 解释器 通过 Shell.acceptable() 检测
cwd 工作目录 项目目录或指定的 workdir
env 环境变量 父进程环境
stdio 流配置 [“ignore”, “pipe”, “pipe”] 仅用于 stdout/stderr
detached 进程组创建 在非 Windows 上启用以便正确清理

Detached 模式在类 Unix 系统上尤为重要,它支持基于进程组的清理,当父进程被终止时,可以终止整个命令树(包括子进程)。

来源: bash.ts

输出流和元数据

命令输出通过双路径机制实时流式传输:

  1. 累积输出: 所有 stdout/stderr 被累积用于最终返回值
  2. 元数据更新: 通过元数据接口发送给 Agent 的渐进式更新

这种方法提供了命令进度的即时可见性,同时确保完整输出可用于后续分析。元数据被截断为 30,000 个字符,以防止压垮 Agent 的上下文窗口。

来源: bash.ts

超时和终止

Bash 工具实现了强大的超时和终止机制:

超时机制使用一个触发进程终止的 setTimeout,而中止信号连接到提供的 AbortSignal。进程终止使用 Shell.killTree(),它实现了特定于平台的清理逻辑:

  • 类 Unix: 向进程组发送 SIGTERM,等待 200ms,如有必要再发送 SIGKILL
  • Windows: 使用 taskkill /pid <pid> /f /t 强制终止进程树

来源: bash.ts, shell.ts

200ms 的终止超时是经过精心选择的平衡:它在大多数情况下为优雅关闭提供足够的时间,同时仍确保在进程不响应 SIGTERM 时及时清理。此超时可通过 timeout 参数按命令配置。

工具参数和用法

Bash 工具接受几个控制命令执行行为的参数:

来源: bash.ts

参数 类型 必需 描述
command string 要执行的 Shell 命令
timeout number 超时时间(毫秒,默认: 120,000)
workdir string 工作目录(默认: 项目目录)
description string 命令的简短描述(5-10 个词)

工具使用的最佳实践

系统在其工具描述中提供了广泛的指导,强调了几个关键模式:

  1. 目录管理: 使用 workdir 参数而不是 cd 命令。这避免了权限歧义并提供更好的跟踪。

    • Good: workdir="/foo/bar" command="pytest tests"
    • Bad: command="cd /foo/bar && pytest tests"
  2. 命令链接: 对依赖命令使用 &&,对独立命令使用 ;,对真正独立的操作使用并行的工具调用。

    • Sequential (dependent): command="mkdir build && cd build && cmake .."
    • Sequential (independent): command="npm install; npm run build"
    • Parallel: Separate Bash tool calls in a single message
  3. 引用: 始终用双引号引用包含空格的路径。

来源: bash.txt

交互式终端会话

除了供 Agent 使用的一次性 Bash 工具外,OpenCode 还通过其 PTY(伪终端)系统提供持久的交互式终端会话。这使用户能够维护具有完整终端仿真的长生命周期的 Shell 会话。

PTY 会话管理

PTY 系统管理终端会话的生命周期状态:

每个会话为断开连接的客户端维护自己的缓冲区,并支持多个并发的 WebSocket 订阅者。缓冲区限制为 2MB 以防止过多的内存使用。

来源: pty/index.ts

会话能力

功能 描述
缓冲区管理 2MB 限制,64KB 分块传输
多订阅者 多个 WebSocket 客户端可以连接到同一个 PTY
进程组 会话终止时正确的进程树清理
终端仿真 xterm-256color 支持丰富的终端功能
动态调整大小 运行时终端大小调整

前端集成

Web 应用程序提供了一个终端组件,通过 WebSockets 连接到 PTY 会话,支持具有主题、序列化和调整大小处理的完整终端仿真。

来源: terminal.tsx

安全考虑

Bash 集成实现了多层安全性,以防止滥用和保护系统资源:

权限执行

  • 基于模式的授权: 命令必须匹配批准的模式才能执行
  • 目录边界: 访问外部目录需要单独的权限授予
  • 始终允许机制: 用户可以预先批准命令前缀以方便使用,而不影响安全性

资源保护

  • 超时限制: 命令在配置的超时后自动终止
  • 输出截断: 大输出被截断以防止内存耗尽
  • 进程清理: 正确的进程树终止防止僵尸进程

平台特定的保护

系统考虑了进程管理中的平台差异:

  • Windows: 使用 taskkill 进行进程树终止
  • 类 Unix: 使用进程组和 SIGTERM/SIGKILL 升级
  • 路径规范化: 在 Windows 上将 Git Bash Unix 风格路径转换为 Windows 格式

来源: bash.ts

测试和验证

Bash 集成包括跨多个场景的全面测试覆盖:

来源: bash.test.ts

测试类别 示例
基本执行 简单的 echo 命令,退出代码验证
权限请求 单个和多个命令的模式匹配
目录检测 cd ../ 和 /tmp workdir 的外部目录权限
项目边界 项目目录内操作不需要外部权限
自动批准 类似命令的“始终”模式

测试策略验证了快乐路径(具有适当权限的成功执行)和边缘情况(目录遍历、命令链接、权限边界)。

与工具系统集成

Bash 工具与其他内置工具一起在中央工具注册表中注册,可供所有支持工具使用的 Agent 使用。

来源: registry.ts

工具注册表位置

Bash 工具出现在工具列表的早期,反映了其基本重要性。它始终启用(不像 LSP 或 Batch 等实验性工具),表明其作为核心能力而不是可选功能的角色。

工具定义结构

1
2
3
4
5
6
{
id: "bash",
description: Tool description with dynamic parameters,
parameters: Zod schema for validation,
execute: Async function implementing command lifecycle
}

此结构遵循标准工具契约,在 Agent 框架内实现一致的处理。

高级功能

命令分析实现

基于 tree-sitter 的命令分析通过遍历 AST 和识别特定节点类型来提取文件操作并构建权限请求:

1
2
3
4
5
6
for (const node of tree.rootNode.descendantsOfType("command")) {
// Extract command components
// Check if file operation (cd, rm, cp, mv, etc.)
// Resolve file paths and check boundaries
// Generate permission patterns
}

来源: bash.ts

错误处理

系统通过结果元数据提供详细的错误信息:

  • Timeout: 超过超时时的清晰指示
  • Aborted: 用户发起的取消通知
  • Exit Codes: 元数据中可用的进程退出代码
  • Truncated Output: 为元数据截断输出时的指示器

来源: bash.ts

Git 集成

工具描述包括 Git 操作的广泛指导,包括:

  • 基于仓库历史记录起草提交消息
  • 破坏性操作的安全协议
  • Pre-commit hook 处理
  • 使用 gh CLI 创建 Pull Request 的工作流

这种专门的指导使 Agent 能够有效地参与版本控制工作流,同时遵守仓库约定。

来源: bash.txt

结论

Bash 集成系统代表了一种安全地将 AI Agent 与 Shell 命令执行连接起来的复杂方法。通过结合基于 AST 的命令分析、细粒度的权限控制、强大的进程管理和实时输出流,它为 Agent 通过终端操作与系统交互提供了安全的基础。

系统的架构反映了 OpenCode 的核心原则:通过解析确保安全,通过权限确保安全,通过流输出和智能默认值确保可用性。当你探索更高级的工具使用时,请考虑查看 权限系统 以深入了解权限如何在平台上工作,或 文件操作 以了解何时应优先使用 Bash 命令而不是专门的文件工具。

对于希望扩展系统的开发者,开发自定义工具 文档提供了实现遵循相同安全模型的额外命令执行工具的模式。

开发自定义工具:工具定义与实现

自定义工具通过添加针对特定工作流、领域知识或集成需求的专业能力,扩展了 OpenCode Agent 系统。工具是模块化、经过验证的函数,Agent 在其问题解决过程中可以调用这些函数,从而提供对系统资源和外部服务的受控访问。

工具架构概述

工具系统采用基于插件的架构,其中工具通过中央注册表被发现、初始化并提供给 Agent。每个工具定义其输入架构、描述和执行逻辑,使 Agent 能够理解何时以及如何有效地使用它。

工具与多个系统组件集成,包括用于安全检查的权限系统、用于代码智能的 LSP、用于事件通信的总线以及用于上下文管理的会话系统。这种集成确保工具在当前 Agent 和会话上下文的安全边界内运行。

来源:tool.ts,registry.ts

工具定义结构

核心工具定义使用 TypeScript 泛型和 Zod 架构进行类型安全的参数验证。Tool.define() 函数创建工具定义,包含自动参数解析、验证错误格式化和输出截断功能。

工具接口

Tool.Info 接口定义了所有工具的契约:

属性 类型 目的
id string 工具的唯一标识符
init function 返回工具元数据的异步初始化
description string 面向 Agent 的工具用途描述
parameters ZodType 用于输入验证的 Zod 架构
execute function 返回结果的核心执行逻辑
formatValidationError function? 用于无效输入的自定义错误格式化

执行上下文

工具接收一个丰富的上下文对象,提供对会话状态和系统功能的访问:

1
2
3
4
5
6
7
8
9
10
type Context<M extends Metadata = Metadata> = {
sessionID: string // 当前会话标识符
messageID: string // 当前消息标识符
agent: string // 调用工具的 Agent
abort: AbortSignal // 取消信号
callID?: string // 唯一调用标识符
extra?: { [key: string]: any } // 附加元数据
metadata: (input: { title?: string; metadata?: M }) => void // 更新工具元数据
ask: (input: PermissionRequest) => Promise<void> // 请求权限
}

metadata() 函数允许工具在执行期间更新其结果元数据,而 ask() 方法则支持在敏感操作之前进行权限检查。

来源:tool.ts

创建自定义工具

基本工具定义

最简单的工具定义遵循以下模式:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
import { Tool } from "./tool"
import z from "zod"

export const MyCustomTool = Tool.define("my_custom_tool", {
description: "对用户数据执行专门操作",
parameters: z.object({
input: z.string().describe("主要输入数据"),
options: z.record(z.string()).optional().describe("可选配置"),
}),
async execute(params, ctx) {
// 工具逻辑在这里
const result = processInput(params.input, params.options)

return {
title: "自定义操作已完成",
output: JSON.stringify(result),
metadata: {
processedAt: new Date().toISOString(),
inputLength: params.input.length
}
}
}
})

Tool.define() 包装器自动处理参数验证和错误格式化。当 Agent 使用无效参数调用工具时,系统会提供描述验证失败的清晰错误消息。

来源:tool.ts

工具结果结构

工具必须返回特定的结果结构:

字段 类型 必需 描述
title string 操作的可读摘要
output string 主要输出内容(返回给 Agent)
metadata Metadata 关于结果的结构化元数据
attachments FilePart[] 用于丰富结果的可选文件附件

元数据字段是可扩展的,并且当输出超过限制时会自动包含截断信息。工具可以添加自定义元数据字段以进行跟踪、调试或提供额外上下文。

来源:tool.ts

高级工具功能

异步初始化

工具可以通过向 Tool.define() 提供函数而不是对象来执行异步初始化工作:

1
2
3
4
5
6
7
8
9
10
11
12
export const AsyncTool = Tool.define("async_tool", async (initCtx) => {
// 根据 Agent 或环境执行异步设置
const capabilities = await detectCapabilities(initCtx?.agent)

return {
description: `具有以下功能的工具: ${capabilities.join(", ")}`,
parameters: z.object({ /* schema */ }),
async execute(params, ctx) {
// 执行逻辑
}
}
})

InitContext 提供可选的 Agent 信息,使工具能够根据使用它们的 Agent 自定义其行为。这对于具有权限感知的工具或特定于 Agent 的优化非常有用。

来源:skill.ts

权限请求

工具可以使用 ask() 方法在敏感操作之前请求权限:

1
2
3
4
5
6
7
8
9
10
async execute(params, ctx) {
await ctx.ask({
permission: "read",
patterns: ["/etc/passwd"],
always: ["*"],
metadata: {}
})

// 继续执行敏感操作
}

权限系统支持基于模式的访问控制,允许工具指定它们需要访问的资源。系统在允许执行之前,会根据 Agent 的权限配置评估这些请求。

来源:read.ts

输出截断

大型输出会通过可配置的限制自动截断。工具可以通过元数据控制截断行为:

1
2
3
4
5
6
7
8
9
10
return {
title: "结果",
output: largeContent,
metadata: {
truncated: undefined // 让系统处理截断
// 或者
truncated: true, // 工具处理了自己的截断
outputPath: "/path/to/full/output" // 可选引用
}
}

当超过限制时,截断系统会将完整输出保留在文件中,允许 Agent 使用偏移量/限制参数读取特定部分。这确保 Agent 可以访问大型结果而不会淹没上下文窗口。

来源:bash.ts,truncation.ts

工具注册方法

方法 1:项目目录工具

将工具文件放在项目内的 tool/ 子目录中:

1
2
3
4
5
6
project/
├── tool/
│ ├── my_tool.ts
│ └── another_tool.ts
├── src/
└── package.json

每个文件应导出工具作为命名导出:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
// tool/my_tool.ts
import { Tool } from "@opencode-ai/tool"
import z from "zod"

export const myTool = Tool.define("my_tool", {
description: "一个自定义项目工具",
parameters: z.object({ /* ... */ }),
async execute(params, ctx) {
// 实现
}
})

export default {
myTool
}

注册表使用匹配配置目录中 tool/*.{js,ts} 文件的 glob 模式自动发现这些工具。工具在初始化时加载,并可供所有 Agent 使用。

来源:registry.ts

方法 2:插件系统

创建一个导出工具定义的 npm 包:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
// my-plugin/src/index.ts
import type { PluginInput, Hooks } from "@opencode-ai/plugin"

export default function myPlugin(input: PluginInput): Hooks {
return {
async tool(def, ctx) {
return {
my_plugin_tool: {
description: "由 my-plugin 提供的工具",
args: {
input: { type: "string" }
},
async execute(args, ctx) {
return "工具执行结果"
}
}
}
}
}
}

通过配置安装插件:

1
2
3
4
5
6
// opencode.config.ts
export default {
plugin: [
"my-plugin@latest"
]
}

插件系统提供对项目上下文、服务器客户端和执行环境的访问。插件可以导出多个工具,并与身份验证和事件处理等其他钩子点集成。

来源:plugin/index.ts,registry.ts

方法 3:编程式注册

直接使用 ToolRegistry.register() 函数注册工具:

1
2
3
4
5
6
7
8
9
10
11
12
import { ToolRegistry } from "@opencode-ai/tool"
import { Tool } from "@opencode-ai/tool"

const customTool = Tool.define("custom", {
description: "以编程方式注册的工具",
parameters: z.object({ /* ... */ }),
async execute(params, ctx) {
// 实现
}
})

await ToolRegistry.register(customTool)

此方法适用于需要有条件地注册或基于运行时配置注册的工具。具有相同 ID 的现有工具将被替换,从而允许工具覆盖。

来源:registry.ts

内置工具示例

文件读取工具

ReadTool 演示了具有权限检查和二进制文件处理的文件访问:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
export const ReadTool = Tool.define("read", {
description: DESCRIPTION,
parameters: z.object({
filePath: z.string().describe("要读取的文件路径"),
offset: z.coerce.number().describe("开始读取的行号(从0开始)").optional(),
limit: z.coerce.number().describe("要读取的行数(默认为 2000)").optional(),
}),
async execute(params, ctx) {
// 路径解析和验证
let filepath = params.filePath
if (!path.isAbsolute(filepath)) {
filepath = path.join(process.cwd(), filepath)
}

// 权限请求
await ctx.ask({
permission: "read",
patterns: [filepath],
always: ["*"],
metadata: {}
})

// 带有二进制检测的文件读取
const file = Bun.file(filepath)
if (!(await file.exists())) {
throw new Error(`File not found: ${filepath}`)
}

// 内容读取和格式化
// ...
}
})

ReadTool 包含复杂的错误处理,为拼写错误提供文件未找到建议,并支持图像/PDF 预览附件。

来源:read.ts

Bash 执行工具

BashTool 演示了命令解析、权限推断和安全协议:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
export const BashTool = Tool.define("bash", async () => {
const shell = Shell.acceptable()

return {
description: DESCRIPTION,
parameters: z.object({
command: z.string().describe("要执行的命令"),
timeout: z.number().describe("可选超时(毫秒)").optional(),
workdir: z.string().describe("运行命令的工作目录").optional(),
description: z.string().describe("对该命令作用的清晰简明描述(5-10个词)").optional(),
}),
async execute(params, ctx) {
// 使用 tree-sitter 解析命令
const tree = await parser().then((p) => p.parse(params.command))

// 从命令结构推断权限
const directories = new Set<string>()
const patterns = new Set<string>()
for (const node of tree.rootNode.descendantsOfType("command")) {
// 分析命令参数以获取文件/目录引用
}

// 请求推断的权限
await ctx.ask({
permission: "bash",
patterns: Array.from(patterns),
always: ["*"],
metadata: {
directories: Array.from(directories)
}
})

// 执行并处理超时
// ...
}
}
})

BashTool 使用 tree-sitter 解析根据命令结构智能推断所需权限,从而实现细粒度的安全性,而无需为每个命令手动指定权限。

来源:bash.ts

最佳实践

参数设计

使用具有清晰描述性的 Zod 架构:

实践 示例 基本原理
使用特定类型 z.coerce.number() 自动类型转换
提供描述 .describe(“文件的绝对路径”) Agent 理解
标记可选字段 .optional() 灵活调用
验证约束 .max(1000) 早期错误检测

错误处理

提供上下文丰富的错误消息:

1
2
3
4
5
6
7
8
9
10
11
12
13
try {
const result = await riskyOperation()
return { title: "成功", output: result, metadata: {} }
} catch (error) {
if (error instanceof NotFoundError) {
const suggestions = await findAlternatives(error.resource)
throw new Error(
`资源未找到: ${error.resource}\n\n` +
`你的意思是这些中的一个吗?\n${suggestions.join("\n")}`
)
}
throw error
}

性能考虑

  1. 使用取消信号:对于长时间运行的操作,检查 ctx.abort
  2. 最小化输出:对于大型结果使用 limit 和 offset
  3. 批量操作:尽可能组合多个操作
  4. 缓存结果:在元数据中存储昂贵的计算以供重用

来源:read.ts

在处理之前始终验证外部输入。使用 Zod 的内置验证进行参数验证,并添加对文件路径、URL 或其他外部资源的额外验证,以防止安全漏洞和注入攻击。

完整示例:天气工具

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
import { Tool } from "./tool"
import z from "zod"

interface WeatherMetadata {
location: string
timestamp: string
source: string
}

export const WeatherTool = Tool.define(
"weather",
async (initCtx) => {
// 可以在这里加载 API 密钥或配置
const apiKey = process.env.WEATHER_API_KEY

return {
description: "获取给定位置的当前天气信息",
parameters: z.object({
location: z.string().describe("城市名称或坐标(纬度,经度)"),
units: z.enum(["celsius", "fahrenheit"]).optional().describe("温度单位"),
}),
formatValidationError(error: z.ZodError) {
const issues = error.issues.map(i => `- ${i.path.join(".")}: ${i.message}`)
return `天气工具参数无效:\n${issues.join("\n")}`
},
async execute(
params: z.infer<typeof this.parameters>,
ctx: Tool.Context<WeatherMetadata>
) {
// 请求外部 API 调用的权限
await ctx.ask({
permission: "network",
patterns: [`api.weather.com/*`],
always: [],
metadata: {}
})

// 获取天气数据
const response = await fetch(
`https://api.weather.com/current?location=${encodeURIComponent(params.location)}&units=${params.units || 'celsius'}`,
{
headers: { 'Authorization': `Bearer ${process.env.WEATHER_API_KEY}` },
signal: ctx.abort
}
)

if (!response.ok) {
throw new Error(`天气 API 错误: ${response.status} ${response.statusText}`)
}

const data = await response.json()

// 使用操作详细信息更新元数据
ctx.metadata({
title: `${params.location} 的天气`,
metadata: {
location: params.location,
timestamp: new Date().toISOString(),
source: "天气 API"
}
})

return {
title: `${params.location} 的当前天气`,
output: JSON.stringify(data, null, 2),
metadata: {
location: params.location,
timestamp: new Date().toISOString(),
source: "天气 API"
}
}
}
}
}
)

这个完整的示例演示了:

  • 带环境配置的异步初始化
  • 带枚举约束的参数验证
  • 自定义错误格式化
  • 外部网络访问的权限请求
  • 取消信号支持
  • 丰富的元数据跟踪
  • 外部 API 集成

工具生命周期

工具生命周期从定义开始,经过注册、被 Agent 发现、参数验证、权限检查、执行,最后到输出处理(包括必要时进行截断)。每个阶段都提供了自定义行为和错误处理的钩子。

来源:registry.ts,tool.ts