Skip to content

4.1 添加一个工具

本章概览:按官方 cookbook 分步写出第一个面向模型的工具:最小形态、execute() 的约定规则、如何接入执行策略与后台任务,以及工具的 UI 渲染意图设计。

最小形态

工具就是注册到 ctx.tools 的一个 defineTool 定义:

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; exec carries immutable identity + signal
      return readFile(args.path, { encoding: 'utf8', signal: exec.signal })
    },
  }))
}

要点:注册基于副作用(dispose 插件 fiber 即注销);schema 自动流入系统提示词组装args 从 schema 推导出类型。

出处:docs/cookbook/adding-a-tool.mddeepseek-harness-src/docs/cookbook/adding-a-tool.md

execute() 的规则

  1. 参数已为你校验defineToolexecute 前按统一 ParameterSchemaSpec 校验模型生成的 arguments(类型、必填键、字面量、联合分支、嵌套值)。schema DSL 无法表达的约束(非空字符串、正数、跨字段)仍需你手动检查。
  2. 注册借用你的只读定义:注册后不要修改 schema 或替换回调;热替换 = dispose 副作用 + 注册替代品。
  3. 执行身份受保护:注册表在递归遍历中把 arguments 物化为分离的无损 JSON、在策略开始前冻结,并分配不透明的 exec.tokenargs 视为只读输入。
  4. 声明并返回一个规范 JSON 值execute 只返回推导出的值;注册表快照 → 校验 → 冻结 → output.render(args, value)工具主体不返回内容块
  5. 抛异常或返回无效值 = isError:基础设施故障抛异常;成功的领域结果(即使是不理想的状态,如进程非零退出)写入规范值。
  6. 遵守 exec.signal:信号触发时取消进行中的工作。
  7. 异步通知用 exec.agentagent.inject({ content, source: { kind: 'plugin', plugin: '<name>' } }) 追加持久化上下文——这不是唤醒,空闲 agent 保持空闲。

长时间运行的工作

通过 producer 配置控制 run_in_background,然后用 ctx.jobs.start({ kind, label, owner: exec.agent, run }) 注册后台任务:

  • 发布 id 后使用任务自有的取消信号,而不是 exec.signal——之后取消外层调用只停止等待,不终止已发布的工作(生命周期归 job_kill、owner dispose 与服务 teardown 所有);
  • 成功的后台分支返回类型化规范句柄 { kind: 'background', jobId }
  • 预中止的调用属于失败(此时没有任务,id 无法满足成功输出 schema)。

出处:docs/cookbook/adding-a-tool.mdgeneric-long-running-tool-runtime Agent Note

执行策略与观测:不要把策略内建到工具里

想做的事用哪个扩展点
允许/拒绝/询问策略tools/pre-execute(可重排的门)
最终单调拒绝ctx.tools.guard()(后续监听器无法撤销)
截止时间/重试/指标tools/execute(around 包裹)
替换展示内容/返回值、blocktools/post-execute
只读观察tools/result

出处:docs/cookbook/adding-a-tool.mdpackages/core/tools/README.md

Code Mode 自动触达你的工具

在 Code Mode 下,每个可见工具都可通过 await tools.<name>(args) 调用,无需额外集成:ToolArgsMap / ToolOutputMap 从同一组 schema 派生精确类型,调用重新进入正常执行流水线,成功解析为策略处理后的最终规范 JSON 值,失败以真正的 ToolCallError reject(程序只能检查 nametoolNamemessage)。

出处:docs/cookbook/adding-a-tool.md

UI 渲染意图:工具设计的一部分

output.render 返回模型可见内容;UI 卡片是独立的关注点,通过纯展示投影 + 可选的 presentCall/presentResult 声明:

  • presentCall(args) → PENDING 卡片:generic(默认)、terminal(调用即 shell 命令)、diff(创建/修改文件);
  • presentResult(args, { content, isError, meta? }) → 完成卡片:genericterminaldiff(已应用 hunk,常由 output.presentationMeta 派生)、search(grep/glob 匹配)、web(搜索/抓取检索)。

硬性规则:

  • 纯函数:这些方法在实时流式输出与日志回放时都会运行——不做 I/O、不读会话状态、不用时钟/随机数;
  • UI 格式不进模型结果```console 围栏、diff、相对化路径都不应为服务 UI 而进入规范值或 Native 内容;
  • 展示绝不能导致回放崩溃:格式错误或旧日志参数使包装器返回 undefined(通用回退)而非抛异常。

出处:docs/cookbook/adding-a-tool.mdtool-render-intent-union Agent Note

参考实现

  • packages/shell/tool-bash:生产级三包示例(terminal 卡片);
  • packages/fs/tool-fs:generic/diff 卡片;
  • packages/fs/tool-fs-search:search 卡片(grep/glob)。

验证

面向模型或 UI 的变更必须提供仓库测试策略规定的组装覆盖(见 4.5 不变式、测试与文档规范)。

参考资料

本文档为学习用途的原创讲解,基于 MIT 许可的开源项目 deepseek-ai/deepseek-harness 编写;所有引用均注明出处。