OpenCode 工具系统
OpenCode 工具系统
概览
本文档详细介绍 OpenCode 的工具系统,包括工具注册、执行流程、上下文管理和多平台适配。
核心概念
| 概念 | 说明 |
|---|---|
| 工具注册 | 所有工具在 src/tool/registry.ts 中注册 |
| 工具执行 | 由 AI SDK 自动调用,通过 tool-call 事件触发 |
| 执行上下文 | 包含 sessionID、messageID、agent、abort 等信息 |
文档结构
- 工具注册与使用:工具注册机制和使用时机
- 工具执行:执行上下文、流程和代码链路
- 工具详解:以 WebSearch 为例详解工具实现
1. 工具注册
位置: src/tool/registry.ts
async function all(): Promise<Tool.Info[]> {
return [
InvalidTool,
QuestionTool,
BashTool,
ReadTool,
GlobTool,
GrepTool,
EditTool,
WriteTool,
TaskTool,
WebFetchTool,
TodoWriteTool,
WebSearchTool,
CodeSearchTool,
SkillTool,
ApplyPatchTool,
// ...更多工具
...custom,
]
}
2. 工具使用时机
工具在 LLM.stream() 调用中被传递给模型,模型在响应中可以请求调用工具:
- 模型返回
tool-call事件 SessionProcessor捕获事件- 执行工具
tool.execute(args, ctx) - 返回结果给模型
- 模型继续生成响应
3. 工具执行上下文
位置: src/tool/tool.ts
export type Context = {
sessionID: string
messageID: string
agent: string
abort: AbortSignal
callID?: string
extra?: { [key: string]: any }
messages: MessageV2.WithParts[]
metadata(input: { title?: string; metadata?: M }): void
ask(input: Omit<PermissionNext.Request, "id" | "sessionID" | "tool">): Promise<void>
}
4. 工具执行流程
触发条件:当模型返回 tool-call 事件时,AI SDK 会自动调用工具的 execute 函数。
代码位置:
| 阶段 | 位置 | 作用 |
|---|---|---|
| 事件处理 | src/session/processor.ts:134-179 |
处理 tool-call 事件,更新状态为 "running" |
| 工具执行 | src/session/prompt.ts:806-839 |
定义工具的 execute 函数 |
| 结果处理 | src/session/processor.ts:180-202 |
处理 tool-result 事件,更新状态为 "completed" |
完整流程图:
模型返回 tool-call 事件
↓
processor.ts 检测到 tool-call
↓
更新 part 状态为 "running"
↓
AI SDK 自动调用 tools[name].execute()
↓
工具执行完成,返回结果
↓
AI SDK 发出 tool-result 事件
↓
processor.ts 更新 part 状态为 "completed"
↓
下一轮循环,结果作为消息发送给模型
工具执行代码(src/session/prompt.ts:806-839):
tools[item.id] = tool({
id: item.id,
description: item.description,
inputSchema: jsonSchema(schema),
async execute(args, options) {
const ctx = context(args, options)
// 执行前触发插件
await Plugin.trigger("tool.execute.before", { tool: item.id, ... }, { args })
// 执行工具
const result = await item.execute(args, ctx)
// 处理附件
const output = {
...result,
attachments: result.attachments?.map((attachment) => ({
...attachment,
id: Identifier.ascending("part"),
sessionID: ctx.sessionID,
messageID: input.processor.message.id,
})),
}
// 执行后触发插件
await Plugin.trigger("tool.execute.after", { tool: item.id, ... }, output)
return output
}
})
5. WebSearch 工具详解
位置:src/tool/websearch.ts
5.1 触发条件
当模型调用 websearch 工具时(即模型返回 tool-call 且 toolName === "websearch")。
5.2 工具定义
export const WebSearchTool = Tool.define("websearch", async () => {
return {
description: "...",
parameters: z.object({
query: z.string().describe("Websearch query"),
numResults: z.number().optional().describe("Number of results (default: 8)"),
livecrawl: z.enum(["fallback", "preferred"]).optional(),
type: z.enum(["auto", "fast", "deep"]).optional(),
contextMaxCharacters: z.number().optional(),
}),
async execute(params, ctx) {
// 1. 权限检查
await ctx.ask({ permission: "websearch", patterns: [params.query] })
// 2. 调用 Exa API
const response = await fetch("https://mcp.exa.ai/mcp", {
method: "POST",
body: JSON.stringify({
jsonrpc: "2.0",
method: "tools/call",
params: {
name: "web_search_exa",
arguments: { query: params.query, type: "auto", numResults: 8 }
}
})
})
// 3. 解析 SSE 响应
const data = JSON.parse(line.substring(6))
return {
output: data.result.content[0].text, // 搜索结果文本
title: `Web search: ${params.query}`,
metadata: {},
}
}
}
})
5.3 返回结果结构
interface ToolResult {
output: string // 搜索结果文本(已格式化)
title: string // 标题:"Web search: xxx"
metadata: object // 元数据
attachments?: [] // 可选附件
}
output 字段内容:Exa API 返回的搜索结果,是文本格式(非 JSON)。
示例输出:
Search results for "TypeScript tutorial":
1. TypeScript: The starting point for learning TypeScript
https://www.typescriptlang.org/docs/
TypeScript is a strongly typed programming language...
2. TypeScript Tutorial - W3Schools
https://www.w3schools.com/typescript/
Learn TypeScript with examples...
5.4 结果处理流程
1. 结果存储(src/session/processor.ts:180-202):
case "tool-result": {
await Session.updatePart({
state: {
status: "completed",
input: value.input, // 工具输入参数
output: value.output.output, // 工具输出结果(搜索文本)
metadata: value.output.metadata,
title: value.output.title,
attachments: value.output.attachments,
time: { start, end }
}
})
}
2. 数据加工(src/session/prompt.ts:820-828):
const result = await item.execute(args, ctx)
const output = {
...result,
// 处理附件,添加 ID
attachments: result.attachments?.map((attachment) => ({
...attachment,
id: Identifier.ascending("part"),
sessionID: ctx.sessionID,
messageID: input.processor.message.id,
})),
}
3. 传递给模型(src/session/message-v2.ts):
// 转换后的消息格式
{
role: "tool",
content: [{
type: "tool-result",
toolCallId: "xxx",
toolName: "websearch",
result: "搜索结果文本..." // ← WebSearch 的 output
}]
}
4. 完整流程图:
模型调用 websearch 工具
↓
AI SDK 调用 WebSearchTool.execute()
↓
调用 Exa API 获取搜索结果
↓
返回 { output: "搜索结果文本", title: "...", metadata: {} }
↓
AI SDK 发出 tool-result 事件
↓
processor.ts 存储结果到 part.state.output
↓
loop 继续循环
↓
MessageV2.toModelMessages() 转换消息
↓
工具结果作为 tool-result 消息发送给模型
↓
模型看到搜索结果,生成最终回复
5.5 WebSearch vs CodeSearch 对比
OpenCode 提供了两个搜索工具:websearch 和 codesearch。两者都调用 Exa AI 的 MCP 服务,但用途和实现有所不同。
5.5.1 核心区别
| 特性 | websearch |
codesearch |
|---|---|---|
| MCP 工具名 | web_search_exa |
get_code_context_exa |
| 用途 | 通用网页搜索 | 编程/代码相关搜索 |
| 内容类型 | 新闻、文章、通用信息 | 代码示例、文档、API 参考 |
5.5.2 MCP 调用特征
两者都通过 MCP (Model Context Protocol) 协议调用 Exa AI 服务,代码中体现为:
1. API 端点
// 两者使用相同的 MCP 端点
const API_CONFIG = {
BASE_URL: "https://mcp.exa.ai", // ← mcp 子域名
ENDPOINTS: {
SEARCH: "/mcp", // ← /mcp 路径
},
}
2. JSON-RPC 2.0 请求格式
MCP 协议基于 JSON-RPC 2.0:
// websearch 请求
const searchRequest = {
jsonrpc: "2.0", // ← JSON-RPC 版本
id: 1,
method: "tools/call", // ← MCP 方法:调用工具
params: {
name: "web_search_exa", // ← MCP 工具名称
arguments: { query: "...", numResults: 8, ... },
},
}
// codesearch 请求
const codeRequest = {
jsonrpc: "2.0", // ← JSON-RPC 版本
id: 1,
method: "tools/call", // ← MCP 方法:调用工具
params: {
name: "get_code_context_exa", // ← MCP 工具名称
arguments: { query: "...", tokensNum: 5000, ... },
},
}
3. SSE (Server-Sent Events) 响应格式
// 两者都使用相同的 SSE 响应解析
const lines = responseText.split("\n")
for (const line of lines) {
if (line.startsWith("data: ")) { // ← SSE 格式
const data = JSON.parse(line.substring(6))
// ...
}
}
5.5.3 参数设计差异
| 维度 | websearch | codesearch | 设计意图 |
|---|---|---|---|
| 结果控制 | numResults (结果数量,默认 8) |
tokensNum (token 数量,默认 5000) |
codesearch 返回整合后的上下文,适合 LLM 消费 |
| 搜索模式 | type: auto/fast/deep |
无 | websearch 需要平衡速度/深度 |
| 实时抓取 | livecrawl: fallback/preferred |
无 | codesearch 依赖预索引的代码/文档库 |
| 超时时间 | 25 秒 | 30 秒 | codesearch 可能需要更多处理时间 |
5.5.4 参数范围
websearch 参数:
parameters: z.object({
query: z.string(),
numResults: z.number().optional().default(8),
livecrawl: z.enum(["fallback", "preferred"]).optional(),
type: z.enum(["auto", "fast", "deep"]).optional(),
contextMaxCharacters: z.number().optional(),
})
codesearch 参数:
parameters: z.object({
query: z.string(),
tokensNum: z.number().min(1000).max(50000).default(5000),
})
5.5.5 使用场景对比
websearch - 通用搜索:
"AI news 2026"
"React 19 release date"
"Super Bowl 2026 results"
"TypeScript tutorial"
codesearch - 代码/编程搜索:
"React useState hook examples"
"Python pandas dataframe filtering"
"Express.js middleware"
"Next.js partial prerendering configuration"
5.5.6 返回内容形态(推测)
基于参数设计,可以推断两者的返回形态:
websearch 返回(多个独立搜索结果):
1. [标题] [URL]
摘要内容...
2. [标题] [URL]
摘要内容...
3. ...
codesearch 返回(整合后的代码上下文):
// React useState example
const [count, setCount] = useState(0);
// Documentation excerpt
useState is a React Hook that lets you add state...
// API reference
useState<T>(initialState: T): [T, Dispatch<SetStateAction<T>>]
5.5.7 架构图
┌─────────────────────────────────────────────────────────────────────────────┐
│ OpenCode 搜索工具架构 │
├─────────────────────────────────────────────────────────────────────────────┤
│ │
│ OpenCode Client │
│ │ │
│ ├── websearch.ts │
│ │ └── Tool.define("websearch", ...) │
│ │ │
│ └── codesearch.ts │
│ └── Tool.define("codesearch", ...) │
│ │
│ │ │
│ ▼ │
│ ┌─────────────────────────────────────────────────────────────────────┐ │
│ │ MCP 协议 (JSON-RPC 2.0) │ │
│ │ │ │
│ │ POST https://mcp.exa.ai/mcp │ │
│ │ { │ │
│ │ "jsonrpc": "2.0", │ │
│ │ "method": "tools/call", │ │
│ │ "params": { "name": "web_search_exa" | "get_code_context_exa" } │ │
│ │ } │ │
│ └─────────────────────────────────────────────────────────────────────┘ │
│ │ │
│ ▼ │
│ ┌─────────────────────────────────────────────────────────────────────┐ │
│ │ Exa AI MCP Server │ │
│ │ │ │
│ │ web_search_exa: │ │
│ │ └── 通用网页索引 → 返回多个搜索结果 │ │
│ │ │ │
│ │ get_code_context_exa: │ │
│ │ └── 代码/文档索引 → 返回整合的代码上下文 │ │
│ │ │ │
│ │ 响应: SSE (Server-Sent Events) │ │
│ └─────────────────────────────────────────────────────────────────────┘ │
│ │
└─────────────────────────────────────────────────────────────────────────────┘
5.5.8 关键代码位置
| 工具 | 文件位置 | MCP 工具名 |
|---|---|---|
| websearch | src/tool/websearch.ts |
web_search_exa |
| codesearch | src/tool/codesearch.ts |
get_code_context_exa |
| 描述文件 | src/tool/websearch.txt |
- |
| 描述文件 | src/tool/codesearch.txt |
- |
5.6 WebFetch vs WebSearch 使用场景
OpenCode 提供了两个网络相关工具:webfetch 和 websearch。理解两者的区别有助于正确选择工具。
5.6.1 核心区别
| 特性 | webfetch |
websearch |
|---|---|---|
| 输入 | 具体的 URL | 搜索关键词 |
| 用途 | 获取特定网页内容 | 搜索互联网信息 |
| 返回 | 单个页面的完整内容 | 多个搜索结果列表 |
| 适用场景 | 已知网址,需要详细阅读 | 未知来源,需要查找信息 |
5.6.2 WebFetch 使用场景
当你已经知道具体的 URL,需要读取该页面的内容时使用:
- 用户提供了具体网址,想读取内容
- 需要查看某个文档页面的详细信息
- 需要获取某个 API 文档的具体内容
- 读取已知 URL 的文章、博客等
示例:
用户: 帮我读取 https://docs.anthropic.com 的内容
→ 使用 WebFetch
5.6.3 WebSearch 使用场景
当你需要查找信息但不知道具体 URL,或者需要获取最新信息时使用:
- 查找某个技术问题的解决方案
- 搜索最新的库/框架文档
- 了解某个主题的相关信息
- 查找某个错误的原因
- 获取最新的事件/新闻
示例:
用户: React 19 有什么新特性?
→ 使用 WebSearch(因为需要搜索最新信息)
5.6.4 快速判断表
| 情况 | 使用工具 |
|---|---|
| 用户给了具体 URL | WebFetch |
| 需要最新信息/不知道去哪找 | WebSearch |
| 查找多个来源 | WebSearch |
| 深入阅读某个特定页面 | WebFetch |
5.6.5 组合使用
有时需要结合使用:先用 WebSearch 找到相关链接,再用 WebFetch 深入阅读。
用户: 帮我了解一下 Next.js 15 的新特性
流程:
1. WebSearch("Next.js 15 new features") → 获取多个搜索结果
2. 从结果中识别官方文档链接
3. WebFetch("https://nextjs.org/blog/next-15") → 获取详细信息
4. 综合信息回复用户
6. 工具调用完整代码链路
本节从代码层面完整追踪一个工具调用从注册到执行到返回结果的全过程。
6.1 代码链路总览
┌─────────────────────────────────────────────────────────────────────────────┐
│ 工具调用完整链路 │
├─────────────────────────────────────────────────────────────────────────────┤
│ │
│ 1. 工具注册 │
│ Tool.define("websearch", ...) [src/tool/websearch.ts:40-150] │
│ ToolRegistry.tools() 返回所有工具 [src/tool/registry.ts] │
│ │
│ 2. 工具解析 │
│ resolveTools() → 构建 AI SDK Tool 对象 [src/session/prompt.ts:750-842] │
│ │
│ 3. 传递给 LLM │
│ LLM.stream({ tools }) [src/session/llm.ts:46-260] │
│ streamText({ tools }) [src/session/llm.ts:176-260] │
│ │
│ 4. 模型响应工具调用 │
│ fullStream → "tool-call" 事件 [AI SDK] │
│ │
│ 5. 事件处理 │
│ processor.process() 处理 tool-call [src/session/processor.ts:134-179]│
│ 更新 part 状态为 "running" │
│ │
│ 6. 工具执行 │
│ AI SDK 调用 execute() [src/session/prompt.ts:806-839] │
│ item.execute(args, ctx) [src/tool/websearch.ts:65-148] │
│ │
│ 7. 结果处理 │
│ fullStream → "tool-result" 事件 [AI SDK] │
│ 更新 part 状态为 "completed" [src/session/processor.ts:180-202]│
│ │
│ 8. 结果发送给模型 │
│ toModelMessages() 转换 [src/session/message-v2.ts] │
│ 下一轮循环作为 tool-result 消息发送 │
│ │
└─────────────────────────────────────────────────────────────────────────────┘
6.2 第一步:工具定义和注册
工具定义(src/tool/tool.ts:48-88):
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
// 包装 execute:添加参数校验和输出截断
toolInfo.execute = async (args, ctx) => {
try {
toolInfo.parameters.parse(args) // Zod 校验
} 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)
// 自动截断过长输出
const truncated = await Truncate.output(result.output, {}, initCtx?.agent)
return {
...result,
output: truncated.content,
metadata: { ...result.metadata, truncated: truncated.truncated },
}
}
return toolInfo
},
}
}
具体工具示例(src/tool/websearch.ts:40-150):
export const WebSearchTool = Tool.define("websearch", async () => {
return {
get description() {
return DESCRIPTION.replace("{{year}}", new Date().getFullYear().toString())
},
parameters: z.object({
query: z.string().describe("Websearch query"),
numResults: z.number().optional(),
// ...
}),
async execute(params, ctx) {
// 1. 权限请求
await ctx.ask({
permission: "websearch",
patterns: [params.query],
always: ["*"],
metadata: { query: params.query, ... },
})
// 2. 构建请求
const searchRequest: McpSearchRequest = {
jsonrpc: "2.0",
id: 1,
method: "tools/call",
params: {
name: "web_search_exa",
arguments: { query: params.query, type: params.type || "auto", ... },
},
}
// 3. 调用 Exa API
const response = await fetch(`${API_CONFIG.BASE_URL}${API_CONFIG.ENDPOINTS.SEARCH}`, {
method: "POST",
headers: { accept: "application/json, text/event-stream", "content-type": "application/json" },
body: JSON.stringify(searchRequest),
signal,
})
// 4. 解析 SSE 响应
const lines = responseText.split("\n")
for (const line of lines) {
if (line.startsWith("data: ")) {
const data: McpSearchResponse = JSON.parse(line.substring(6))
if (data.result && data.result.content && data.result.content.length > 0) {
return {
output: data.result.content[0].text, // 搜索结果
title: `Web search: ${params.query}`,
metadata: {},
}
}
}
}
return { output: "No search results found.", title: `Web search: ${params.query}`, metadata: {} }
},
}
})
6.3 第二步:resolveTools 构建 AI SDK 工具对象
位置:src/session/prompt.ts:750-842
export async function resolveTools(input: {
agent: Agent.Info
model: Provider.Model
session: Session.Info
tools?: Record<string, boolean>
processor: SessionProcessor.Info
bypassAgentCheck: boolean
messages: MessageV2.WithParts[]
}) {
const tools: Record<string, AITool> = {}
// 创建工具执行上下文
const context = (args: any, options: ToolCallOptions): Tool.Context => ({
sessionID: input.session.id,
abort: options.abortSignal!,
messageID: input.processor.message.id,
callID: options.toolCallId,
extra: { model: input.model, bypassAgentCheck: input.bypassAgentCheck },
agent: input.agent.name,
messages: input.messages,
// 元数据更新回调
metadata: async (val: { title?: string; metadata?: any }) => {
const match = input.processor.partFromToolCall(options.toolCallId)
if (match && match.state.status === "running") {
await Session.updatePart({
...match,
state: { title: val.title, metadata: val.metadata, status: "running", input: args, time: { start: Date.now() } },
})
}
},
// 权限请求
async ask(req) {
await PermissionNext.ask({
...req,
sessionID: input.session.id,
tool: { messageID: input.processor.message.id, callID: options.toolCallId },
ruleset: PermissionNext.merge(input.agent.permission, input.session.permission ?? []),
})
},
})
// 遍历所有已注册工具
for (const item of await ToolRegistry.tools({ modelID: input.model.api.id, providerID: input.model.providerID }, input.agent)) {
// 适配 JSON Schema
const schema = ProviderTransform.schema(input.model, z.toJSONSchema(item.parameters))
// 构建 AI SDK 工具对象
tools[item.id] = tool({
id: item.id as any,
description: item.description,
inputSchema: jsonSchema(schema as any),
// ★ 关键:execute 函数
async execute(args, options) {
const ctx = context(args, options)
// 执行前触发插件
await Plugin.trigger("tool.execute.before", { tool: item.id, sessionID: ctx.sessionID, callID: ctx.callID }, { args })
// ★ 调用工具的实际执行函数
const result = await item.execute(args, ctx)
// 处理附件
const output = {
...result,
attachments: result.attachments?.map((attachment) => ({
...attachment,
id: Identifier.ascending("part"),
sessionID: ctx.sessionID,
messageID: input.processor.message.id,
})),
}
// 执行后触发插件
await Plugin.trigger("tool.execute.after", { tool: item.id, sessionID: ctx.sessionID, callID: ctx.callID, args }, output)
return output
},
})
}
// 处理 MCP 工具(类似逻辑)
for (const [key, item] of Object.entries(await MCP.tools())) {
// ... 类似的包装逻辑
}
return tools
}
resolveTools 的作用:
- 从 ToolRegistry 获取所有注册的工具
- 将每个工具包装成 AI SDK 的
Tool格式 - 注入执行上下文(sessionID、abort、权限等)
- 添加插件钩子(before/after)
6.4 第三步:传递给 LLM.stream
位置:src/session/prompt.ts:666-717
// 在 loop() 中调用
const streamInput: LLM.StreamInput = {
user: lastUser,
sessionID,
model,
agent,
system,
abort,
messages: MessageV2.toModelMessages(msgs, model),
tools: await resolveTools({
agent,
model,
session,
processor,
bypassAgentCheck: false,
messages: msgs,
}),
}
const result = await processor.process(streamInput)
LLM.stream 内部(src/session/llm.ts:46-260):
export async function stream(input: StreamInput) {
// ...
const tools = await resolveTools(input) // 这里实际上已经在 prompt.ts 中调用过了
return streamText({
// ...
activeTools: Object.keys(tools).filter((x) => x !== "invalid"),
tools, // ← 传递给 AI SDK
toolChoice: input.toolChoice,
// ...
})
}
注意:resolveTools 在 prompt.ts 中被调用,生成的 tools 对象通过 StreamInput 传递给 LLM.stream,然后传给 streamText。
6.5 第四步:AI SDK streamText 处理
streamText 是 Vercel AI SDK 的核心函数,它:
- 将 tools 定义转换为各提供商的格式
- 发送请求给模型 API
- 解析流式响应
- 当模型返回工具调用时,自动调用工具的 execute 函数
- 产生
tool-call、tool-result等事件
AI SDK 内部流程(伪代码):
// AI SDK 内部逻辑(简化)
async function* streamText({ tools, messages, ... }) {
// 1. 发送请求给模型
const response = await model.doStream({ messages, tools: transformTools(tools), ... })
// 2. 解析流式响应
for await (const chunk of response.stream) {
if (chunk.type === "tool_use") {
// 模型请求调用工具
const toolCall = chunk.toolCall
// 3. 产生 tool-call 事件(给 consumer 处理)
yield { type: "tool-call", toolCallId: toolCall.id, toolName: toolCall.name, input: toolCall.input }
// 4. 自动执行工具
const tool = tools[toolCall.name]
if (tool && tool.execute) {
const result = await tool.execute(toolCall.input, { toolCallId: toolCall.id, ... })
// 5. 产生 tool-result 事件
yield { type: "tool-result", toolCallId: toolCall.id, output: result }
}
}
}
}
6.6 第五步:processor.process 处理工具事件
位置:src/session/processor.ts:55-347
async process(streamInput: LLM.StreamInput) {
const stream = await LLM.stream(streamInput)
for await (const value of stream.fullStream) {
input.abort.throwIfAborted()
switch (value.type) {
// ... 其他事件处理
case "tool-call": {
const match = toolcalls[value.toolCallId]
if (match) {
// 更新 part 状态为 "running"
const part = await Session.updatePart({
...match,
tool: value.toolName,
state: {
status: "running",
input: value.input,
time: { start: Date.now() },
},
metadata: value.providerMetadata,
})
toolcalls[value.toolCallId] = part as MessageV2.ToolPart
// 检测死循环(连续 3 次相同工具调用)
const parts = await MessageV2.parts(input.assistantMessage.id)
const lastThree = parts.slice(-3)
if (lastThree.length === 3 && lastThree.every(
(p) => p.type === "tool" && p.tool === value.toolName && JSON.stringify(p.state.input) === JSON.stringify(value.input)
)) {
await PermissionNext.ask({ permission: "doom_loop", ... })
}
}
break
}
case "tool-result": {
const match = toolcalls[value.toolCallId]
if (match && match.state.status === "running") {
// 更新 part 状态为 "completed"
await Session.updatePart({
...match,
state: {
status: "completed",
input: value.input ?? match.state.input,
output: value.output.output, // ← 工具输出
metadata: value.output.metadata,
title: value.output.title,
time: { start: match.state.time.start, end: Date.now() },
attachments: value.output.attachments,
},
})
delete toolcalls[value.toolCallId]
}
break
}
case "tool-error": {
const match = toolcalls[value.toolCallId]
if (match && match.state.status === "running") {
await Session.updatePart({
...match,
state: {
status: "error",
input: value.input ?? match.state.input,
error: (value.error as any).toString(),
time: { start: match.state.time.start, end: Date.now() },
},
})
// 如果是权限拒绝或问题拒绝,设置 blocked 标记
if (value.error instanceof PermissionNext.RejectedError || value.error instanceof Question.RejectedError) {
blocked = shouldBreak
}
delete toolcalls[value.toolCallId]
}
break
}
}
}
// 返回处理结果
if (needsCompaction) return "compact"
if (blocked) return "stop"
if (input.assistantMessage.error) return "stop"
return "continue"
}
6.7 第六步:结果发送给模型
当 processor.process() 返回 "continue" 时,loop() 继续循环:
位置:src/session/prompt.ts:718-726
// processor.process() 返回 "continue"
if (result === "continue") {
continue // 继续循环
}
// 下一轮循环
const msgs = await MessageV2.filterCompacted(MessageV2.stream({ sessionID }))
消息转换(src/session/message-v2.ts:606-670):
// 转换助手消息中的工具调用
for (const part of msg.parts) {
if (part.type === "tool") {
if (part.state.status === "completed") {
// 已完成的工具调用 → 输出结果
assistantMessage.parts.push({
type: ("tool-" + part.tool),
state: "output-available",
input: part.state.input,
output: part.state.output, // ← WebSearch 的搜索结果
})
}
}
}
// 转换为模型消息格式
function toModelMessage(assistantMessage) {
return {
role: "assistant",
content: [
...assistantMessage.parts.map((part) => {
if (part.type.startsWith("tool-")) {
return {
type: "tool-call",
toolCallId: part.callID,
toolName: part.tool,
input: part.input,
}
}
// ...
}),
],
}
}
// 工具结果作为 tool 消息
function toToolResultMessage(part) {
return {
role: "tool",
content: [{
type: "tool-result",
toolCallId: part.callID,
toolName: part.tool,
result: part.state.output, // ← 搜索结果文本
}],
}
}
6.8 完整代码调用时序图
┌──────────────────────────────────────────────────────────────────────────────────┐
│ 工具调用时序图 │
├──────────────────────────────────────────────────────────────────────────────────┤
│ │
│ loop() [prompt.ts] │
│ │ │
│ ├── resolveTools() [prompt.ts:750-842] │
│ │ ├── ToolRegistry.tools() [registry.ts] │
│ │ │ └── 返回 WebSearchTool, ReadTool, ... │
│ │ │ │
│ │ └── 构建 tools 对象: │
│ │ tools["websearch"] = tool({ │
│ │ description: "...", │
│ │ inputSchema: jsonSchema(...), │
│ │ execute: async (args, options) => { │
│ │ ctx = context(args, options) │
│ │ await Plugin.trigger("tool.execute.before", ...) │
│ │ result = await WebSearchTool.execute(args, ctx) │
│ │ await Plugin.trigger("tool.execute.after", ...) │
│ │ return result │
│ │ } │
│ │ }) │
│ │ │
│ ├── processor.process(streamInput) [processor.ts:45-417] │
│ │ │ │
│ │ └── LLM.stream(streamInput) [llm.ts:46-260] │
│ │ │ │
│ │ └── streamText({ tools, messages, ... }) [AI SDK] │
│ │ │ │
│ │ ├── [模型返回 tool-call] │
│ │ │ │
│ │ ├── AI SDK 自动调用 tools["websearch"].execute(args) │
│ │ │ │ │
│ │ │ └── WebSearchTool.execute(args, ctx) [websearch.ts] │
│ │ │ ├── ctx.ask() 权限检查 │
│ │ │ ├── fetch("https://mcp.exa.ai/mcp") │
│ │ │ └── return { output: "搜索结果", ... } │
│ │ │ │
│ │ └── [AI SDK 发出 tool-result 事件] │
│ │ │
│ ├── processor.process() 处理事件 [processor.ts] │
│ │ │ │
│ │ ├── case "tool-call": │
│ │ │ └── Session.updatePart({ state: { status: "running" } }) │
│ │ │ │
│ │ └── case "tool-result": │
│ │ └── Session.updatePart({ │
│ │ state: { │
│ │ status: "completed", │
│ │ output: "搜索结果文本...", │
│ │ } │
│ │ }) │
│ │ │
│ └── return "continue" → loop() 继续循环 │
│ │
│ 下一轮 loop() 循环 │
│ │ │
│ ├── msgs = MessageV2.stream() → 读取包含工具结果的最新消息 │
│ │ │
│ ├── MessageV2.toModelMessages() → 转换为模型格式 │
│ │ └── 包含 tool-result 消息:{ role: "tool", content: [...] } │
│ │ │
│ └── LLM.stream({ messages: [..., tool-result] }) → 发送给模型 │
│ └── 模型看到搜索结果,生成最终回复 │
│ │
└──────────────────────────────────────────────────────────────────────────────────┘
6.9 关键代码位置总结
| 阶段 | 文件 | 行号 | 作用 |
|---|---|---|---|
| 工具定义 | src/tool/tool.ts |
48-88 | Tool.define() 包装函数 |
| 工具实现 | src/tool/websearch.ts |
40-150 | WebSearch 具体实现 |
| 工具注册 | src/tool/registry.ts |
- | ToolRegistry.tools() 返回所有工具 |
| 工具解析 | src/session/prompt.ts |
750-842 | resolveTools() 构建 AI SDK 工具对象 |
| 传递工具 | src/session/llm.ts |
176-260 | streamText({ tools }) |
| 事件处理 | src/session/processor.ts |
134-202 | tool-call/tool-result 事件处理 |
| 消息转换 | src/session/message-v2.ts |
606-670 | toModelMessages() 包含工具结果 |
| 循环控制 | src/session/prompt.ts |
718-726 | continue 继续循环 |
7. 安全防护:doom_loop 死循环检测
7.1 什么是 doom_loop
doom_loop 是 OpenCode 的一个死循环检测防护机制,用于防止 AI 陷入无限重复调用同一个工具的死循环。
定义:当相同的工具调用以相同的输入重复 3 次时触发
7.2 触发场景
当 AI 遇到问题但没有正确处理时,可能会陷入重复尝试的死循环,例如:
- 反复尝试读取一个不存在的文件
- 重复执行一个失败的命令
- 持续调用一个没有响应的 API
- 使用相同参数多次调用同一个工具
7.3 代码实现
位置:src/session/processor.ts(在 tool-call 事件处理中)
case "tool-call": {
// ... 更新 part 状态 ...
// 检测死循环(连续 3 次相同工具调用)
const parts = await MessageV2.parts(input.assistantMessage.id)
const lastThree = parts.slice(-3)
if (lastThree.length === 3 && lastThree.every(
(p) => p.type === "tool" &&
p.tool === value.toolName &&
JSON.stringify(p.state.input) === JSON.stringify(value.input)
)) {
await PermissionNext.ask({ permission: "doom_loop", ... })
}
}
7.4 检测逻辑
1. 获取当前消息中最近 3 个工具调用 parts
↓
2. 检查是否都是同一个工具(tool name 相同)
↓
3. 检查输入参数是否完全相同(JSON 序列化后比较)
↓
4. 如果 3 个都相同 → 触发 doom_loop 权限检查
↓
5. 暂停执行,询问用户是否继续
7.5 默认行为
| 权限 | 默认值 | 说明 |
|---|---|---|
| 大部分权限 | "allow" |
自动批准 |
doom_loop |
"ask" |
询问用户 |
external_directory |
"ask" |
询问用户 |
7.6 用户选项
当 doom_loop 触发时,UI 会提供三个选项:
| 选项 | 说明 |
|---|---|
once |
仅批准此请求,下次仍会询问 |
always |
批准后续相同请求(当前会话有效) |
reject |
拒绝请求,停止执行 |
7.7 配置示例
在 opencode.json 中配置:
{
"$schema": "https://opencode.ai/config.json",
"permission": {
"doom_loop": "ask"
}
}
可选值:
"allow"— 自动允许,不询问"ask"— 询问用户(默认)"deny"— 直接拒绝
7.8 流程图
工具调用 #1(相同工具、相同参数)
↓
工具调用 #2(相同工具、相同参数)
↓
工具调用 #3(相同工具、相同参数)
↓
┌─────────────────────────────────┐
│ doom_loop 检测触发 │
│ │
│ 比较: tool name + input JSON │
│ │
│ 3次完全相同 → 触发权限检查 │
└─────────────────────────────────┘
↓
┌─────────────────────────────────┐
│ 询问用户 │
│ • once - 仅此一次 │
│ • always - 始终允许 │
│ • reject - 拒绝 │
└─────────────────────────────────┘

浙公网安备 33010602011771号