跳转到内容

Tool 调用系统

v1.18.16 源码提交 a3647eb025c7 · 官方 Release
状态已完成
难度中等
预计阅读50 分钟
  • packages/opencode/src/tool/tool.ts
  • packages/opencode/src/tool/registry.ts
  • packages/opencode/src/session/tools.ts
  • packages/plugin/src/tool.ts

本章以 OpenCode 源码版本 v1.18.16(提交 a3647eb025c7)为证据基线。我们追踪一条典型源码路径:本轮 agent 获得 read 工具,模型提交参数后,OpenCode 怎样校验、申请权限、执行并返回结果?这条路径由源码结构证明,不代表模型一定会在某次真实会话中选择 read

学完这一章,你应该能够:

  1. 画出 Tool.define -> ToolRegistry -> SessionTools.resolve -> model execute 的位置图。
  2. 区分工具定义、registry 中的工具、本轮暴露给模型的工具与一次 tool call。
  3. 沿源码解释参数校验、schema 转换、权限、metadata、插件 hook 和输出截断的位置。
  4. 说明内置工具、项目工具、plugin tool 与 MCP tool 怎样汇入同一调用表。
  5. 判断模型选择工具、权限允许工具和工具成功执行为何是三件不同的事。
  6. 为自己的 mini agent 设计一个可扩展、可审计的工具边界。

OpenCode 的 Tool 系统是能力注册表加安全执行适配器:registry 决定“这一轮有哪些能力”,SessionTools.resolve 决定“模型怎样调用它们”,具体工具只实现受 Schema 与 Context 约束的动作。

本章的中心问题是:

模型输出 { tool: "read", args: ... } 后,为什么不会直接变成一次不受控的函数调用?

因为模型只提出调用;OpenCode 仍要经过工具筛选、provider schema 转换、运行时参数解码、权限判断、执行 hook、输出截断和附件归属补全。核心合同见 packages/opencode/src/tool/tool.tspackages/opencode/src/session/tools.ts

  • 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.DefToolRegistry.ServiceSessionTools.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.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

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

  1. 预编译 Schema.decodeUnknownEffect(parameters)
  2. 每次调用先 decode 模型传入的未知 JSON;
  3. 调具体 execute(decoded, ctx)
  4. 若工具未自行声明截断信息,按 agent 规则统一截断;
  5. 写入 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.tspackages/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.definition hook 可以修改 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:

  1. 用 args/options 创建本轮 context;
  2. 触发 tool.execute.before,允许插件修改 args;
  3. 调用 item.execute(args, ctx)
  4. 为返回附件补 PartID、sessionID 与 assistant messageID;
  5. 触发 tool.execute.after
  6. 把 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. 五个决定安全与可维护性的分支”

模型只能看到暴露的工具并产生调用;具体工具在需要受保护动作时调用 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

内置工具在 Tool.wrap 统一截断;plugin adapter 和 MCP adapter 也分别调用 Truncate。截断 metadata 记录 truncated 与可选 outputPath。来源:packages/opencode/src/tool/tool.tspackages/opencode/src/tool/registry.tspackages/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.tspackages/opencode/src/session/tools.ts

源码能证明这条 abort 分支;普通成功的最终状态仍由 LLM event/processor 主链处理。

registry 用 id 作为 tools 对象 key,插件 default export 又会从文件名派生 namespace。来源:packages/opencode/src/tool/registry.tspackages/opencode/src/session/tools.ts。改名不仅是重构函数名,也可能改变模型提示、权限规则与历史 tool call 的协议标识。

8. OpenCode 的选择:替代方案与代价

Section titled “8. OpenCode 的选择:替代方案与代价”
设计问题OpenCode 的选择可选做法收益与代价
工具怎样组织definition -> registry -> per-session adapterloop 内 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:只补四个阅读障碍”

Def<Parameters extends Schema.Decoder<unknown>> 让参数类型从 Schema 推导;Schema 同时提供运行时 decode。这比 Java 泛型多了一层运行时合同。

Tool.define 返回一个 Effect,同时给它挂上静态 id 属性。这样注册代码可在初始化前引用工具 ID。它是函数/对象可组合的 TypeScript 写法,Java 通常会用显式类表示。

它是普通对象形式的名字到工具映射。模型看到的是 key、description 和 schema;真正执行函数留在 runtime 本地。

OpenCode 内部工具返回 Effect,AI SDK/plugin API 使用 Promise。EffectBridge.make() 在调用边界把 Effect 安全地运行成 Promise,同时保留当前服务上下文。来源:packages/opencode/src/session/tools.tspackages/opencode/src/tool/registry.ts

方法一:把工具拆成“定义、发现、绑定、执行”

Section titled “方法一:把工具拆成“定义、发现、绑定、执行””

定义只描述能力;registry 负责发现;每轮 adapter 绑定 session/permission;执行处理具体动作。

验证问题:同一个 read tool 能否被两个 session 同时调用而不串 messageID?

方法二:在最靠近副作用的位置授权

Section titled “方法二:在最靠近副作用的位置授权”

工具可见性先缩小攻击面,ctx.ask 再根据具体参数做执行时授权。

验证问题:同一个 shell 工具能否允许安全命令、询问敏感命令,而不是只能全开或全关?

方法三:让失败与截断成为显式协议

Section titled “方法三:让失败与截断成为显式协议”

参数错误要返回可理解信息,超长输出要标记 truncated,附件要补全归属 ID。

验证问题:模型和 UI 能否区分“完整输出”“被截断输出”和“工具失败”?

不要看上文,补全:

Tool.Def 描述 ______;ToolRegistry 决定 ______;SessionTools.resolve 绑定 ______。模型给出的 args 先经过 ______,受保护动作通过 ______ 申请权限。结果过长时由 ______ 处理,最后回到 ______,而不是工具层自己决定 agent 是否继续。

如果你把 ToolRegistry 与 SessionTools 说成同一个“工具列表”,请回看 packages/opencode/src/tool/registry.tspackages/opencode/src/session/tools.ts

  1. 入门:列出 Tool.ContextExecuteResult 的字段。
  2. 进阶:从 registry 的 read 初始化追到 AI SDK tools 对象中的 read key。
  3. 辨析:解释内部 parameters、jsonSchema 与 provider-transformed inputSchema 的区别。
  4. 失败路径:画出非法参数、权限拒绝、执行异常、输出截断四条分支。

实现一个 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。

下一章要把抽象能力落到最典型的副作用:readeditwrite 怎样处理路径、权限、diff、格式化与诊断?这就是“文件读写与代码修改”。