跳至内容

工具编写参考

面向模型的工具必须满足哪些约定,均以本文为准。如需按步骤构建第一个工具,请阅读构建工具packages/shell/tool-bash 是生产级的三包示例。

最小形态

ts
import { readFile } from 'node:fs/promises'
import type { Context } from '@deepseek-ai/cordis'
import { defineTool } from '@deepseek-ai/dsh-tools'

export const name = 'my-tool'
export const inject = ['tools']

export function apply(ctx: Context) {
  ctx.tools.register(defineTool({
    name: 'read_file',
    description: 'Read a file from disk.',          // what the model sees
    parameters: {
      path: { type: 'string', required: true, description: 'Absolute path' },
      limit: { type: 'number' },                     // optional by default
    },
    output: {
      schema: { type: 'string' },
      render: (_args, value) => [{ type: 'text', text: value }],
    },
    async execute(args, exec) {
      // args is TYPED from the schema: { path: string; limit?: number }
      // exec carries immutable identity + token; signal is the operational field
      return readFile(args.path, { encoding: 'utf8', signal: exec.signal })
    },
  }))
}

注册基于副作用:dispose(资源释放)插件 fiber 即注销该工具。schema 会自动流入系统提示词的组装过程。

execute() 约定的规则

  • 参数已为你校验。 defineToolexecute 运行前,会根据统一的 ParameterSchemaSpec 校验模型生成的 arguments(类型、必填键、字面量约束、恰好匹配一个分支的联合以及嵌套值——见运行时参数校验),因此 execute 内的 args 会匹配 InferArgs。显式对象节点必须声明 additionalProperties: true | false;隐式参数根对象保持开放。你仍需手动检查 schema DSL 无法表达的约束,例如非空字符串、正数或跨字段规则。直接注册的原始 JSON Schema 工具自行负责输入校验。
  • 注册借用你的只读定义。 类型化的同进程贡献不是序列化边界;注册后不要修改其 schema 或替换回调。schemas() 只物化显式的模型可见投影。如需热替换工具,请 dispose 其所属副作用并注册替代品;回调闭包内的可变状态仍是普通的插件状态。
  • 执行身份受保护。 注册表在一次递归遍历中将 arguments 物化为分离的无损 JSON,在策略开始前冻结该值,并分配一个不透明的 exec.tokencallIdnameargumentsagenttoken、必填且由调用方持有的 signal,以及可选的外层传输 parent token 在整个分发过程中保持不可变。parent 仅用于身份标识,不暴露活跃的外层执行。请将 args 视为只读输入。只有 around-dispatch 包装器会收到可变视图;它可以替换并恢复必填的 exec.signal 以施加截止时间,但不能移除该信号。
  • 声明并返回一个规范 JSON 值。 output.schema 使用 ValueSchemaSpec,根可以是对象、数组、标量或 null。execute 只返回推导出的值;注册表将其快照为无损 JSON,完成校验和冻结后,再传给 output.render(args, value)。工具主体不要返回内容块,也不要迫使调用方从自然语言中解析 id 和字段。
  • 抛出异常或返回无效值意味着 isError 注册表会捕获异常,并在观察者运行前收敛 schema、渲染器、元数据投影器和无损 JSON 失败。基础设施故障请抛异常。成功的领域结果即使表示不理想的状态,也应写入规范值;其 Native 渲染器可以解释该状态,例如进程以非零状态退出。
  • 遵守 exec.signal 信号触发时取消进行中的工作。
  • 使用 presentationMeta 投影持久化的卡片数据(可选)。 output.presentationMeta(args, value) 从同一个规范值派生可回放的 JSON。核心将其持久化在 tool/result 上并传给 presentResult,因此需要结果期事实的卡片——例如 writeedit 的已应用 hunk——无需持久化规范值也能在回放中重现。嵌套 Code 分发没有卡片,因此会跳过该投影器。
  • 使用 exec.agent 发送异步通知。 agent.inject({ content, source: { kind: 'plugin', plugin: '<name>' } }) 追加持久化上下文,下一次模型请求会看到它——这不是唤醒(空闲的 agent(智能体)保持空闲)。请防范已 dispose 的 agent(try/catch)。

长时间运行的工作

通过 producer 配置控制 run_in_background,然后使用 ctx.jobs.start({ kind, label, owner: exec.agent, run }) 注册任务。注册表会在进入 producer 主体前将已预先中止的调用判为失败;运行时会在 run() 启动工作前校验 owner 和任务控制器是否可用,随后提供 id、会话围栏、通用控制工具、通知和 owner cleanup。成功的后台分支会返回类型化的规范句柄,如 { kind: 'background', jobId };其 Native 渲染器可以保留 started background job bash-1 这类供人阅读的自然语言,但 Code Mode 绝不能通过解析该文本取得 id。

producer 提供同步的 cancel、在资源清理后 settle 且不 reject 的 done,以及可选的消费式 readOutput(负责有界输出的格式化)。预先中止的调用属于失败,因为此时没有任务,其 id 无法满足成功输出 schema。ctx.jobs.start() 发布 id 后,应使用任务自有的取消信号,而不是 exec.signal:之后取消外层调用只会停止等待本次调用,不会终止已经发布的工作;该生命周期归 job_kill、owner dispose 和服务 teardown 所有。前台工作仍与 exec.signal 耦合。流式 producer 的示例和完整约定见后台任务运行时 Agent Notedsh-tool-bash

执行策略与观测

尽量不要把部署策略内建到工具中。使用 tools/pre-execute 实现可扩展的允许/拒绝/询问策略(见权限门禁示例);使用 ctx.tools.guard() 设置最终的单调拒绝,后续监听器无法撤销;使用 tools/execute 为分发添加截止时间、重试或指标收集;使用 tools/post-execute 替换展示内容或返回值、阻止结果,或附加模型可见上下文;使用 tools/result 观测不可变的归一化结果而不改变它。替换内容不会阻止程序化访问 value;保密策略会屏蔽或替换该值。沙箱实现也可以在工具的执行器实现中运行;dsh-tools README 定义每个扩展点的输入、顺序、返回值和失败行为。

Code Mode 自动触达你的工具

Code Mode 中,每个可见的已注册工具都可通过 await tools.<name>(args) 调用,无需额外集成。生成的 ToolArgsMapToolOutputMap 会根据同一组 schema 分别派生精确的参数类型与规范返回类型,调用则重新进入正常的执行流水线。成功调用会解析为策略处理后的最终规范 JSON 值,而不是渲染后的 Native 内容。失败调用会以真正的 ToolCallError reject;程序只能检查其 nametoolName 和可供人阅读的 message,无法取得内部错误代码或失败联合。

请把 output.schema 设计为实用的程序化 API:直接返回句柄与字段;当标量、数组或 null 确实就是结果时,允许采用相应的根类型;将面向人类的解释放入 output.render。中间值只存在于执行期间,不会被持久化或按提示词上限截断,也不设字节上限,因此生产方如实声明的采集边界和进程内存仍然重要。只有外层 run_code 日志/结果会受到可配置输出上限和面向模型的 spill 流水线约束。

工具在 UI 中的渲染方式

工具的 output.render 返回模型可见的内容;其 UI 卡片 是另一项独立关注点,通过纯展示投影以及可选的 presentCallpresentResult 方法声明。请将这些内容与规范值一并设计。没有 UI 展示方法的工具会回退到通用卡片(标题 = 工具名,原始 args 作为输入)。

两个方法都返回一个 card 标签的渲染意图——选择与你的工具行为匹配的卡片类型:

  • presentCall(args) → 一个 ToolCallView(PENDING 卡片):
    • { card: 'generic', title, kind?, rawInput?, content?, locations? }——默认。设置 kind 获取图标(readsearch/…);设置 locations: [{ path, line? }] 标注工具涉及的文件,使有能力的编辑器跟随/跳转。
    • { card: 'terminal', title, description?, cwd? }——你的调用本身就是 shell 命令。title 是命令,description 渲染在终端卡片上方。(tool-bash。)
    • { card: 'diff', title, diffs, locations? }——你的调用创建或修改文件。diffs: [{ path, oldText, newText }](新文件时 oldText: null)渲染为内联 diff 卡片。(tool-fs writeedit。)
  • presentResult(args, { content, isError, meta? }) 返回完成后的卡片:
    • generic 提供可选的标题和内容。
    • terminal 提供原始输出和可选的退出元数据;各 UI 根据自身能力渲染对应视图或回退视图。
    • diff 提供已应用的 hunk,通常由 output.presentationMeta 派生并通过持久化的 result.meta 携带,使回放能重现它们。变更类工具保留 diff 结果,因为完成后的视图会替换 pending 卡片。
    • search 提供从持久化 result.meta 重建的发现型结果:按文件分组的匹配(shape: 'matches',grep)或扁平路径列表(shape: 'paths',glob),外加 truncatedtotal 使 UI 永不把被截断的结果当作完整结果呈现。该视图不携带结果文本(无 search 卡片的 UI 回退到原始结果内容),也没有 search 调用视图——发现型调用的 pending 状态保持为 generic 卡片,因为匹配只在 execute 之后才存在。(tool-fs-search 的 grepglob。)
    • web 提供已完成的 web 检索,以 kind: 'search' | 'fetch' 区分(结构化的搜索来源或抓取摘要),由 result.meta 派生;它不携带正文副本,因此不具备 web 能力的 UI 回退到原始结果内容。(tool-web web_searchweb_fetch。)

硬性规则(违反会出问题):

  • 纯函数。 这些方法在实时流式输出和会话日志回放时都会运行,因此必须是 args(加 result)的纯函数——不做 I/O、不读会话状态、不用时钟/随机数。diff 从 args 派生(write 使用 oldText: null,因为调用时的展示器没有文件先前内容);会话上下文由 UI 适配器而非工具提供。如果你发现自己想在 presentCall 内获取文件旧内容或工作目录,请停下:那属于持久结果元数据或适配器,不属于展示器。
  • UI 格式不进入模型结果。 围栏 ```console 块、diff、相对化路径均不应仅为服务 UI 而进入规范值或 Native 内容。output.render 负责模型可见的自然语言;presentationMeta 和卡片展示器负责可回放的 UI 状态。terminal 结果视图携带原始输出,由适配器按需添加回退格式。
  • defineTool 对展示路径做软校验。 格式错误或旧版日志中的参数会使包装器返回 undefined(通用回退)而非抛异常——展示绝不能导致回放崩溃。

中性词汇定义在 dsh-tools 中;工具绝不导入 UI 或传输类型。host/client 运行时将每个 card 映射到各自的视图。设计与原因见渲染意图联合体 Agent Notedsh-tool-fs(generic/diff)和 dsh-tool-bash(terminal)是参考实现。

验证

遵循仓库测试策略和所属包的测试文档。已交付且面向模型或 UI 的变更必须提供其中规定的组装覆盖。