Tool 调用系统
a3647eb025c7 · 官方 Release
Agent 生成档案
Section titled “Agent 生成档案”- 章节 ID:
05-tool-calling - 章节摘要:从 read 能力出发,理解 Tool.define、registry 筛选、本轮绑定、参数校验、权限、hook、截断与 MCP 汇合。
- 教程版本:
v1.18.16 - 源码基线:
a3647eb025c7615159d417dcc49fc39fdaeba65b - 章节元数据:/versions/v1-18-16/data/chapters.json
- 源码映射:/versions/v1-18-16/data/source-map.json
主要源码路径
Section titled “主要源码路径”packages/opencode/src/tool/tool.tspackages/opencode/src/tool/registry.tspackages/opencode/src/session/tools.tspackages/plugin/src/tool.ts
本章以 OpenCode 源码版本
v1.18.16(提交a3647eb025c7)为证据基线。我们追踪一条典型源码路径:本轮 agent 获得read工具,模型提交参数后,OpenCode 怎样校验、申请权限、执行并返回结果?这条路径由源码结构证明,不代表模型一定会在某次真实会话中选择read。
0. 本章学习目标
Section titled “0. 本章学习目标”学完这一章,你应该能够:
- 画出
Tool.define -> ToolRegistry -> SessionTools.resolve -> model execute的位置图。 - 区分工具定义、registry 中的工具、本轮暴露给模型的工具与一次 tool call。
- 沿源码解释参数校验、schema 转换、权限、metadata、插件 hook 和输出截断的位置。
- 说明内置工具、项目工具、plugin tool 与 MCP tool 怎样汇入同一调用表。
- 判断模型选择工具、权限允许工具和工具成功执行为何是三件不同的事。
- 为自己的 mini agent 设计一个可扩展、可审计的工具边界。
1. 一句话讲明白
Section titled “1. 一句话讲明白”OpenCode 的 Tool 系统是能力注册表加安全执行适配器:registry 决定“这一轮有哪些能力”,SessionTools.resolve 决定“模型怎样调用它们”,具体工具只实现受 Schema 与 Context 约束的动作。
本章的中心问题是:
模型输出
{ tool: "read", args: ... }后,为什么不会直接变成一次不受控的函数调用?
因为模型只提出调用;OpenCode 仍要经过工具筛选、provider schema 转换、运行时参数解码、权限判断、执行 hook、输出截断和附件归属补全。核心合同见 packages/opencode/src/tool/tool.ts、packages/opencode/src/session/tools.ts。
v1.18.16 相比旧基线改变了什么
Section titled “v1.18.16 相比旧基线改变了什么”Tool.define的执行包装改用 Effect Schema 解码未知参数,并把验证失败格式化成可回传的 tool error,而不是信任模型 JSON。- 输出截断现在属于通用 Tool wrapper:工具返回超长文本时会记录
truncated与完整输出路径,具体工具不用各写一套。 SessionTools.resolve已直接纳入 MCP resource/list/read 工具及对应权限 pattern;registry tool 与 MCP capability 仍汇成同一张模型工具表。
统一参数解码与输出截断
packages/opencode/src/tool/tool.ts:102-171
Tool.define 把裸实现包装成稳定的安全执行合同。
102 truncate: Truncate.Interface,103 agents: Agent.Interface,104) {105 return () =>返回给上一层。106 Effect.gen(function* () {Effect 异步工作流。107 const toolInfo = typeof init === "function" ? { ...(yield* init()) } : { ...init }等待 Effect 结果。108 // Compile the parser closure once per tool init; `decodeUnknownEffect`109 // allocates a new closure per call, so hoisting avoids re-closing it for110 // every LLM tool invocation.111 const decode = Schema.decodeUnknownEffect(toolInfo.parameters)定义并校验数据形状。112 const execute = toolInfo.execute113 toolInfo.execute = (args, ctx) => {114 const attrs = {115 "tool.name": id,116 "session.id": ctx.sessionID,117 "message.id": ctx.messageID,118 ...(ctx.callID ? { "tool.call_id": ctx.callID } : {}),119 }120 return Effect.gen(function* () {Effect 异步工作流。121 const decoded = yield* decode(args).pipe(等待 Effect 结果。122 Effect.mapError(Effect 异步工作流。123 (error) =>124 new InvalidArgumentsError({125 tool: id,126 detail: toolInfo.formatValidationError ? toolInfo.formatValidationError(error) : String(error),127 }),128 ),129 )130 const result = yield* execute(decoded as Schema.Schema.Type<Parameters>, ctx)工具真正执行入口。131 if (result.metadata.truncated !== undefined) {按条件进入分支。132 return result返回给上一层。133 }134 const agent = yield* agents.get(ctx.agent)等待 Effect 结果。135 const truncated = yield* truncate.output(result.output, {}, agent)等待 Effect 结果。136 return {返回给上一层。137 ...result,138 output: truncated.content,139 metadata: {140 ...result.metadata,141 truncated: truncated.truncated,142 ...(truncated.truncated && { outputPath: truncated.outputPath }),143 },144 }145 }).pipe(Effect.orDie, Effect.withSpan("Tool.execute", { attributes: attrs }))Effect 异步工作流。146 }147 return toolInfo返回给上一层。148 })149}150151export function define<对外暴露模块成员。152 Parameters extends Schema.Decoder<unknown>,定义并校验数据形状。153 Result extends Metadata,154 R,155 ID extends string = string,156>(157 id: ID,158 init: Effect.Effect<Init<Parameters, Result>, never, R>,Effect 异步工作流。159): Effect.Effect<Info<Parameters, Result>, never, R | Truncate.Service | Agent.Service> & { id: ID } {Effect 异步工作流。160 return Object.assign(返回给上一层。161 Effect.gen(function* () {Effect 异步工作流。162 const resolved = yield* init等待 Effect 结果。163 const truncate = yield* Truncate.Service等待 Effect 结果。164 const agents = yield* Agent.Service等待 Effect 结果。165 return { id, init: wrap(id, resolved, truncate, agents) }返回给上一层。166 }),167 { id },168 )169}170171export function init<P extends Schema.Decoder<unknown>, M extends Metadata>(定义并校验数据形状。
本轮工具解析与权限上下文
packages/opencode/src/session/tools.ts:41-120
agent/session 权限在执行 adapter 内合并。
41export const resolve = Effect.fn("SessionTools.resolve")(function* (input: {Effect 异步工作流。42 agent: Agent.Info43 model: Provider.Model选择模型或 provider。44 session: Session.Info45 processor: Pick<SessionProcessor.Handle, "message" | "updateToolCall" | "completeToolCall">处理模型流事件。46 bypassAgentCheck: boolean47 messages: SessionV1.WithParts[]48 promptOps: TaskPromptOps49}) {50 const tools: Record<string, AITool> = {}51 const run = yield* EffectBridge.make()等待 Effect 结果。52 const plugin = yield* Plugin.Service调用插件扩展点。53 const permission = yield* Permission.Service等待 Effect 结果。54 const registry = yield* ToolRegistry.Service等待 Effect 结果。55 const mcp = yield* MCP.Service等待 Effect 结果。56 const truncate = yield* Truncate.Service等待 Effect 结果。57 const flags = yield* RuntimeFlags.Service等待 Effect 结果。5859 const context = (args: Record<string, unknown>, options: ToolExecutionOptions): Tool.Context => ({60 sessionID: input.session.id,61 abort: options.abortSignal!,62 messageID: input.processor.message.id,63 callID: options.toolCallId,64 extra: { model: input.model, bypassAgentCheck: input.bypassAgentCheck, promptOps: input.promptOps },选择模型或 provider。65 agent: input.agent.name,66 messages: input.messages,67 metadata: (val) =>68 input.processor.updateToolCall(options.toolCallId, (match) => {69 if (!["running", "pending"].includes(match.state.status)) return match按条件进入分支。70 return {返回给上一层。71 ...match,72 state: {73 title: val.title,74 metadata: val.metadata,75 status: "running",76 input: args,77 time: { start: Date.now() },78 },79 }80 }),81 ask: (req) =>82 permission83 .ask({84 ...req,85 sessionID: input.session.id,86 tool: { messageID: input.processor.message.id, callID: options.toolCallId },87 ruleset: Permission.merge(input.agent.permission, input.session.permission ?? []),88 })89 .pipe(Effect.orDie),Effect 异步工作流。90 })9192 for (const item of yield* registry.tools({等待 Effect 结果。93 modelID: ModelV2.ID.make(input.model.api.id),选择模型或 provider。94 providerID: input.model.providerID,选择模型或 provider。95 agent: input.agent,96 permission: input.session.permission,97 })) {98 const schema = ProviderTransform.schema(input.model, ToolJsonSchema.fromTool(item))定义并校验数据形状。99 tools[item.id] = tool({100 description: item.description,101 inputSchema: jsonSchema(schema),102 execute(args, options) {工具真正执行入口。103 return run.promise(返回给上一层。104 Effect.gen(function* () {Effect 异步工作流。105 const ctx = context(args, options)106 yield* plugin.trigger(调用插件扩展点。107 "tool.execute.before",108 { tool: item.id, sessionID: ctx.sessionID, callID: ctx.callID },109 { args },110 )111 const result = yield* item.execute(args, ctx)工具真正执行入口。112 const output = {113 ...result,114 attachments: result.attachments?.map((attachment) => ({115 ...attachment,116 id: PartID.ascending(),117 sessionID: ctx.sessionID,118 messageID: input.processor.message.id,119 })),120 }
2. 为什么工具不是“给模型一组函数”这么简单
Section titled “2. 为什么工具不是“给模型一组函数”这么简单”一个本地函数只需要参数和返回值;一个 coding-agent tool 还必须回答:
- 它怎样向模型描述自己?
- 模型给的 JSON 参数若不合法,谁拦住?
- 当前 agent/session 是否允许这次动作?
- 如何响应取消?
- 工具执行进度怎样让 UI 看见?
- 超长输出怎样避免挤爆上下文?
- 插件与 MCP 工具怎样接入而不改 agent loop?
所以 Tool 不是裸函数,而是一条能力边界。生活类比可以是工具房:目录告诉你有哪些工具,领用窗口核对身份与用途,真正的器械执行动作,结果再登记回工单。类比只用于看形状;真实标识符是 Tool.Def、ToolRegistry.Service 与 SessionTools.resolve。
3. 先画地图:从定义到一次执行
Section titled “3. 先画地图:从定义到一次执行”内置 Tool.define 项目 tool/*.ts plugin tool | | | +------------- ToolRegistry.State ------------------+ | 按 model / flags 筛选 | v SessionTools.resolve | +----------------+----------------+ | | registry tools MCP tools | | +------ schema + execute ----------+ | v Record<string, AITool> | v LLM runtime | model tool call | v Context -> validate -> ask -> execute | v result / attachments / error| 层 | 它回答的问题 | 它不负责什么 |
|---|---|---|
Tool.define / Tool.Def | 单个能力怎样描述、校验、执行 | 不决定本轮是否可见 |
ToolRegistry | 系统有哪些工具,当前 model/flag 下留下哪些 | 不绑定本次 session/call id |
SessionTools.resolve | 怎样绑定 session、permission、processor 与 AI SDK | 不决定模型选择哪一个 |
| 具体 tool | 怎样执行 read/edit/shell 等动作 | 不维护整个 agent loop |
| processor/loop | 怎样记录结果并决定下一轮 | 不属于本章的工具注册内核 |
通用 Agent 最少需要 definition、registry、runtime adapter 三层;OpenCode 又加入 Effect Schema、插件、MCP、agent 权限、输出文件化截断和 provider schema 兼容。
4. 最小机制:先看 16 行能力边界
Section titled “4. 最小机制:先看 16 行能力边界”1type ToolDef<A> = {定义数据结构约束。2 name: string3 description: string4 schema: RuntimeSchema<A>定义并校验数据形状。5 execute(args: A, ctx: ToolContext): Promise<ToolResult>工具真正执行入口。6}78function toolsForRound(registry: ToolDef<unknown>[], round: RoundContext) {定义一段可复用逻辑。9 return Object.fromEntries(registry.map((def) => [返回给上一层。10 def.name,11 llmTool({12 description: def.description,13 inputSchema: adaptSchema(def.schema, round.model),14 async execute(raw, call) {工具真正执行入口。15 const args = decode(def.schema, raw)16 const ctx = bindContext(round, call)17 return def.execute(args, ctx)工具真正执行入口。18 },19 }),20 ]))21}
这不是源码逐行翻译。OpenCode 的 wrapper 还负责 tracing 与截断,session adapter 还负责 permission、metadata、plugin hook 和附件 ID。
5. 读源码前,只分清四种对象
Section titled “5. 读源码前,只分清四种对象”5.1 Tool.Info:可延迟初始化的注册信息
Section titled “5.1 Tool.Info:可延迟初始化的注册信息”1export interface Info<P, M> {定义数据结构约束。2 id: string3 init: () => Effect.Effect<DefWithoutID<P, M>>Effect 异步工作流。4}
来源:packages/opencode/src/tool/tool.ts
内置工具模块通常先暴露这种 info;依赖齐备后,Tool.init(info) 才得到完整 Tool.Def。
5.2 Tool.Def:可执行的内部工具
Section titled “5.2 Tool.Def:可执行的内部工具”Tool.Def 包含 id、description、运行时 parameters、可选 jsonSchema、execute 与可选验证错误格式化函数。来源:packages/opencode/src/tool/tool.ts。
5.3 AI SDK Tool:发给模型 runtime 的适配形状
Section titled “5.3 AI SDK Tool:发给模型 runtime 的适配形状”SessionTools.resolve 把内部 Tool.Def 包装成 AI SDK 的 { description, inputSchema, execute }。它已经绑定本轮 session、assistant message、agent 和 permission。
5.4 tool call:模型提出的一次动作
Section titled “5.4 tool call:模型提出的一次动作”tool call 带 tool name、call id 与 args。它不是定义,也不是授权。一次定义可以被调用多次,每次都有不同 call id、abort signal 与权限判断。
6. 追一条典型源码旅程:read 怎样成为本轮能力
Section titled “6. 追一条典型源码旅程:read 怎样成为本轮能力”本章不进入 read.ts 的路径解析细节;只追它如何进入 registry、暴露给模型并执行统一 wrapper。文件读写的内部行为由下一章展开。
6.1 第一站:工具合同把执行环境显式化
Section titled “6.1 第一站:工具合同把执行环境显式化”Tool.Context 包含:
1{2 sessionID,3 messageID,4 agent,5 abort,6 callID?,7 extra?,8 messages,9 metadata(...),10 ask(...),11}
来源:packages/opencode/src/tool/tool.ts
ExecuteResult 统一返回 title、metadata、output 与可选 attachments。来源:packages/opencode/src/tool/tool.ts。
为什么 context 不使用全局变量?同一进程可能同时处理不同 session/call;显式 context 才能正确归属状态、权限和取消。
6.2 第二站:Tool.define 装上统一 wrapper
Section titled “6.2 第二站:Tool.define 装上统一 wrapper”Tool.define(id, init) 取得 Truncate 与 Agent service,返回带静态 id 的 Effect;之后 Tool.init 执行延迟 init 并补回 id。来源:packages/opencode/src/tool/tool.ts。
真正的公共护栏在 wrap:
- 预编译
Schema.decodeUnknownEffect(parameters); - 每次调用先 decode 模型传入的未知 JSON;
- 调具体
execute(decoded, ctx); - 若工具未自行声明截断信息,按 agent 规则统一截断;
- 写入 tracing span 属性。
来源:packages/opencode/src/tool/tool.ts。
这回答了第一个安全问题:模型参数是“不可信的 unknown”,不能因为 TypeScript 标了类型就跳过运行时校验。
6.3 第三站:registry 初始化内置工具
Section titled “6.3 第三站:registry 初始化内置工具”ToolRegistry.layer 先取得各工具的 Info,再用 Tool.init 并发初始化 invalid、shell、read、glob、grep、edit、write、task 等工具。来源:packages/opencode/src/tool/registry.ts、packages/opencode/src/tool/registry.ts。
最终 builtin 数组还受 runtime flags 影响,例如 question、background task status、experimental scout/LSP/plan。来源:packages/opencode/src/tool/registry.ts。
所以“OpenCode 有哪些内置工具”不是一个只由 import 列表决定的常量;客户端类型和实验开关会影响实际集合。
6.4 第四站:项目工具与 plugin tool 进入同一 registry
Section titled “6.4 第四站:项目工具与 plugin tool 进入同一 registry”registry 扫描配置目录中的 {tool,tools}/*.{js,ts},动态 import,并把合法 export 转为内部 Tool.Def;随后再收集 plugin.list() 中的 p.tool。来源:packages/opencode/src/tool/registry.ts。
plugin 公共 API 很轻:
1tool({2 description,3 args,4 async execute(args, context) { /* ... */ },工具真正执行入口。5})
来源:packages/plugin/src/tool.ts
Plugin ToolContext 还暴露 directory、worktree、abort、metadata 与 Promise 形式的 ask。来源:packages/plugin/src/tool.ts。
registry 的 fromPlugin 是兼容盒:把 Zod args 转为 JSON Schema,把 Promise context 与 Effect context 桥接,规范化字符串/对象结果,并为插件输出截断。来源:packages/opencode/src/tool/registry.ts。
6.5 第五站:registry 按本轮 model 与 flags 过滤
Section titled “6.5 第五站:registry 按本轮 model 与 flags 过滤”ToolRegistry.tools(...) 从所有 builtin/custom 工具中筛选:
- web search 是否可用由 provider 与 runtime flags 决定;
- 部分 GPT 模型使用 patch,其他模型保留 edit/write;
- task/skill description 会按当前 agent 动态补充可用 subagent/skill;
tool.definitionhook 可以修改 description、parameters/jsonSchema。
来源:packages/opencode/src/tool/registry.ts。
这意味着 registry 不是静态 Map<String, Tool>,而是一个按 model、agent 与运行标志求值的能力目录。
6.6 第六站:SessionTools.resolve 绑定本次调用环境
Section titled “6.6 第六站:SessionTools.resolve 绑定本次调用环境”Agent loop 把 agent、model、session、processor、messages 与 promptOps 交给 SessionTools.resolve。它为每个工具构造本轮 Tool.Context:
1const context = (args, options): Tool.Context => ({2 sessionID: input.session.id,3 messageID: input.processor.message.id,4 callID: options.toolCallId,5 abort: options.abortSignal!,6 agent: input.agent.name,7 messages: input.messages,8 metadata: /* 更新当前 tool call 状态 */,9 ask: /* 合并权限后询问 */,10})
节选自 packages/opencode/src/session/tools.ts。
metadata(...) 调 processor 的 updateToolCall,把 pending/running part 更新为 running,并记录 title、metadata、input 与开始时间。ask(...) 合并 agent permission 与 session permission,再交给 Permission service。来源同上。
6.7 第七站:Schema 先适配 provider,再交给模型
Section titled “6.7 第七站:Schema 先适配 provider,再交给模型”对 registry 中每个工具,代码先取得 JSON Schema,并调用 ProviderTransform.schema(input.model, ...),再包装为 AI SDK tool:
1const schema = ProviderTransform.schema(选择模型或 provider。2 input.model,3 ToolJsonSchema.fromTool(item),定义并校验数据形状。4)56tools[item.id] = tool({7 description: item.description,8 inputSchema: jsonSchema(schema),9 execute(args, options) { /* ... */ },工具真正执行入口。10})
来源:packages/opencode/src/session/tools.ts
为什么内部 parameters 已经能校验,还要转换 JSON Schema?两者面向不同边界:Effect Schema 保护本地执行,JSON Schema 告诉当前 provider/model 应怎样生成参数;某些 provider 对 JSON Schema 子集还有额外限制。
6.8 第八站:模型调用 read 后,执行仍经过 hook 与 wrapper
Section titled “6.8 第八站:模型调用 read 后,执行仍经过 hook 与 wrapper”AI SDK 调用该 execute 时,OpenCode:
- 用 args/options 创建本轮 context;
- 触发
tool.execute.before,允许插件修改 args; - 调用
item.execute(args, ctx); - 为返回附件补
PartID、sessionID 与 assistant messageID; - 触发
tool.execute.after; - 把 output 返回给 LLM runtime。
来源:packages/opencode/src/session/tools.ts。
item.execute 对内置工具已经被 Tool.wrap 包装,所以真实顺序可以重新组装为:
provider 生成 args -> before hook -> Schema decode -> 具体 read.execute -> output truncate -> 附件补归属 ID -> after hook -> 返回 runtime本章证据到“返回 runtime”为止。正常结果怎样经 LLM event 被 processor 持久化成 completed ToolPart,是上一章和 Agent 核心循环的职责;不要误称 SessionTools.resolve 自己完成了所有落库。
6.9 第九站:MCP 工具并行汇入同一个表
Section titled “6.9 第九站:MCP 工具并行汇入同一个表”SessionTools.resolve 还遍历 mcp.tools():
- 把 MCP input schema 转为 JSON Schema,并做 provider transform;
- 执行前统一
ctx.ask({ permission: key, patterns: ["*"] ... }); - 触发相同 before/after hooks;
- 把 MCP text/resource/image content 归一为 output 与 attachments;
- 用相同 truncate service 处理文本输出。
来源:packages/opencode/src/session/tools.ts。
MCP 工具没有先变成 registry Tool.Def,但在本轮最终都进入同一个 Record<string, AITool>。这是一种“汇合在 runtime adapter”而非“强迫所有来源共享定义类型”的设计。
7. 五个决定安全与可维护性的分支
Section titled “7. 五个决定安全与可维护性的分支”7.1 模型选择不等于权限允许
Section titled “7.1 模型选择不等于权限允许”模型只能看到暴露的工具并产生调用;具体工具在需要受保护动作时调用 ctx.ask(...)。ruleset 来自 agent 与 session permission 合并。来源:packages/opencode/src/session/tools.ts。
可见性和授权是两道门,不能用“没暴露某工具”替代所有执行时检查。
7.2 TypeScript 类型不等于运行时可信
Section titled “7.2 TypeScript 类型不等于运行时可信”模型输出来自进程外,编译期泛型无法保证 JSON 合法。Schema.decodeUnknownEffect 在工具 wrapper 中做真正的运行时解码。来源:packages/opencode/src/tool/tool.ts。
7.3 输出过长也属于工具边界
Section titled “7.3 输出过长也属于工具边界”内置工具在 Tool.wrap 统一截断;plugin adapter 和 MCP adapter 也分别调用 Truncate。截断 metadata 记录 truncated 与可选 outputPath。来源:packages/opencode/src/tool/tool.ts、packages/opencode/src/tool/registry.ts、packages/opencode/src/session/tools.ts。
这样做保护模型上下文,但调用者必须知道展示内容可能不是完整结果。
7.4 取消后仍需要尽量闭合工具状态
Section titled “7.4 取消后仍需要尽量闭合工具状态”context 把 AI SDK abortSignal 传给具体工具。若返回时 signal 已 aborted,adapter 会调用 processor 的 completeToolCall 作为收口路径。来源:packages/opencode/src/session/tools.ts、packages/opencode/src/session/tools.ts。
源码能证明这条 abort 分支;普通成功的最终状态仍由 LLM event/processor 主链处理。
7.5 Tool ID 是协议的一部分
Section titled “7.5 Tool ID 是协议的一部分”registry 用 id 作为 tools 对象 key,插件 default export 又会从文件名派生 namespace。来源:packages/opencode/src/tool/registry.ts、packages/opencode/src/session/tools.ts。改名不仅是重构函数名,也可能改变模型提示、权限规则与历史 tool call 的协议标识。
8. OpenCode 的选择:替代方案与代价
Section titled “8. OpenCode 的选择:替代方案与代价”| 设计问题 | OpenCode 的选择 | 可选做法 | 收益与代价 |
|---|---|---|---|
| 工具怎样组织 | definition -> registry -> per-session adapter | loop 内 switch(toolName) | 易扩展、职责清晰;调用链更长 |
| 参数怎样保护 | 编译期类型 + 运行时 Schema decode | 只信 TypeScript 类型 | 外部 JSON 更安全;schema 要维护 |
| 权限放哪里 | Context 中按调用询问 | 注册时一次性决定 | 能结合具体路径/命令;每个工具必须正确调用 ask |
| provider schema 差异放哪里 | 暴露本轮工具时 transform | 每个工具写 provider 分支 | 工具实现干净;adapter 更复杂 |
| 多种工具来源怎样汇合 | registry 与 MCP 在 SessionTools 汇合 | 强制所有来源使用同一 SDK | 保留各自协议;存在两套适配逻辑 |
表中选择由源码直接证明;收益与代价是设计解释。
9. TypeScript / Effect:只补四个阅读障碍
Section titled “9. TypeScript / Effect:只补四个阅读障碍”9.1 泛型 Schema 与参数类型
Section titled “9.1 泛型 Schema 与参数类型”Def<Parameters extends Schema.Decoder<unknown>> 让参数类型从 Schema 推导;Schema 同时提供运行时 decode。这比 Java 泛型多了一层运行时合同。
9.2 Object.assign(effect, { id })
Section titled “9.2 Object.assign(effect, { id })”Tool.define 返回一个 Effect,同时给它挂上静态 id 属性。这样注册代码可在初始化前引用工具 ID。它是函数/对象可组合的 TypeScript 写法,Java 通常会用显式类表示。
9.3 Record<string, AITool>
Section titled “9.3 Record<string, AITool>”它是普通对象形式的名字到工具映射。模型看到的是 key、description 和 schema;真正执行函数留在 runtime 本地。
9.4 Effect 与 Promise bridge
Section titled “9.4 Effect 与 Promise bridge”OpenCode 内部工具返回 Effect,AI SDK/plugin API 使用 Promise。EffectBridge.make() 在调用边界把 Effect 安全地运行成 Promise,同时保留当前服务上下文。来源:packages/opencode/src/session/tools.ts、packages/opencode/src/tool/registry.ts。
10. 可以带走的方法
Section titled “10. 可以带走的方法”方法一:把工具拆成“定义、发现、绑定、执行”
Section titled “方法一:把工具拆成“定义、发现、绑定、执行””定义只描述能力;registry 负责发现;每轮 adapter 绑定 session/permission;执行处理具体动作。
验证问题:同一个 read tool 能否被两个 session 同时调用而不串 messageID?
方法二:在最靠近副作用的位置授权
Section titled “方法二:在最靠近副作用的位置授权”工具可见性先缩小攻击面,ctx.ask 再根据具体参数做执行时授权。
验证问题:同一个 shell 工具能否允许安全命令、询问敏感命令,而不是只能全开或全关?
方法三:让失败与截断成为显式协议
Section titled “方法三:让失败与截断成为显式协议”参数错误要返回可理解信息,超长输出要标记 truncated,附件要补全归属 ID。
验证问题:模型和 UI 能否区分“完整输出”“被截断输出”和“工具失败”?
11. 费曼复述与练习阶梯
Section titled “11. 费曼复述与练习阶梯”11.1 60 秒复述
Section titled “11.1 60 秒复述”不要看上文,补全:
Tool.Def描述 ______;ToolRegistry决定 ______;SessionTools.resolve绑定 ______。模型给出的 args 先经过 ______,受保护动作通过 ______ 申请权限。结果过长时由 ______ 处理,最后回到 ______,而不是工具层自己决定 agent 是否继续。
如果你把 ToolRegistry 与 SessionTools 说成同一个“工具列表”,请回看 packages/opencode/src/tool/registry.ts 和 packages/opencode/src/session/tools.ts。
11.2 读源码练习
Section titled “11.2 读源码练习”- 入门:列出
Tool.Context与ExecuteResult的字段。 - 进阶:从 registry 的
read初始化追到 AI SDK tools 对象中的readkey。 - 辨析:解释内部 parameters、jsonSchema 与 provider-transformed inputSchema 的区别。
- 失败路径:画出非法参数、权限拒绝、执行异常、输出截断四条分支。
11.3 小实现
Section titled “11.3 小实现”实现一个 mini tool runtime:支持两个内置工具和一个 plugin tool;所有 raw args 必须运行时校验;每次调用绑定 sessionID/callID;副作用前询问 permission;输出超过上限时保存完整内容并返回摘要与路径。
12. 最后复盘:工具已经接通,接下来进入具体副作用
Section titled “12. 最后复盘:工具已经接通,接下来进入具体副作用”本章主链是:
Tool.define -> ToolRegistry 发现并筛选 -> SessionTools.resolve 绑定本轮上下文 -> provider schema + AI SDK execute -> validate / permission / hook / execute / truncate -> result 返回 LLM runtime你现在应该能回答中心问题:模型只提出工具调用;OpenCode runtime 才拥有定义、授权和执行能力,并把每次动作绑定到明确的 session/message/call。
下一章要把抽象能力落到最典型的副作用:read、edit、write 怎样处理路径、权限、diff、格式化与诊断?这就是“文件读写与代码修改”。