Appearance
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.md(deepseek-harness-src/docs/cookbook/adding-a-tool.md)
execute() 的规则
- 参数已为你校验:
defineTool在execute前按统一ParameterSchemaSpec校验模型生成的arguments(类型、必填键、字面量、联合分支、嵌套值)。schema DSL 无法表达的约束(非空字符串、正数、跨字段)仍需你手动检查。 - 注册借用你的只读定义:注册后不要修改 schema 或替换回调;热替换 = dispose 副作用 + 注册替代品。
- 执行身份受保护:注册表在递归遍历中把
arguments物化为分离的无损 JSON、在策略开始前冻结,并分配不透明的exec.token。args视为只读输入。 - 声明并返回一个规范 JSON 值:
execute只返回推导出的值;注册表快照 → 校验 → 冻结 →output.render(args, value)。工具主体不返回内容块。 - 抛异常或返回无效值 =
isError:基础设施故障抛异常;成功的领域结果(即使是不理想的状态,如进程非零退出)写入规范值。 - 遵守
exec.signal:信号触发时取消进行中的工作。 - 异步通知用
exec.agent:agent.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.md与 generic-long-running-tool-runtime Agent Note
执行策略与观测:不要把策略内建到工具里
| 想做的事 | 用哪个扩展点 |
|---|---|
| 允许/拒绝/询问策略 | tools/pre-execute(可重排的门) |
| 最终单调拒绝 | ctx.tools.guard()(后续监听器无法撤销) |
| 截止时间/重试/指标 | tools/execute(around 包裹) |
| 替换展示内容/返回值、block | tools/post-execute |
| 只读观察 | tools/result |
出处:
docs/cookbook/adding-a-tool.md与packages/core/tools/README.md
Code Mode 自动触达你的工具
在 Code Mode 下,每个可见工具都可通过 await tools.<name>(args) 调用,无需额外集成:ToolArgsMap / ToolOutputMap 从同一组 schema 派生精确类型,调用重新进入正常执行流水线,成功解析为策略处理后的最终规范 JSON 值,失败以真正的 ToolCallError reject(程序只能检查 name、toolName、message)。
UI 渲染意图:工具设计的一部分
output.render 返回模型可见内容;UI 卡片是独立的关注点,通过纯展示投影 + 可选的 presentCall/presentResult 声明:
presentCall(args)→ PENDING 卡片:generic(默认)、terminal(调用即 shell 命令)、diff(创建/修改文件);presentResult(args, { content, isError, meta? })→ 完成卡片:generic、terminal、diff(已应用 hunk,常由output.presentationMeta派生)、search(grep/glob 匹配)、web(搜索/抓取检索)。
硬性规则:
- 纯函数:这些方法在实时流式输出与日志回放时都会运行——不做 I/O、不读会话状态、不用时钟/随机数;
- UI 格式不进模型结果:
```console围栏、diff、相对化路径都不应为服务 UI 而进入规范值或 Native 内容; - 展示绝不能导致回放崩溃:格式错误或旧日志参数使包装器返回
undefined(通用回退)而非抛异常。
出处:
docs/cookbook/adding-a-tool.md与 tool-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 不变式、测试与文档规范)。
参考资料
- docs/cookbook/adding-a-tool.md — 工具编写参考(官方)(
deepseek-harness-src/docs/cookbook/adding-a-tool.md) - docs/user/develop/basic/tool.md — 按步骤构建第一个工具(
deepseek-harness-src/docs/user/develop/basic/tool.md) - packages/core/tools/README.md — 工具注册表与流水线(
deepseek-harness-src/packages/core/tools/README.md) - docs/tool-catalog.md — 全量工具目录(生成)(
deepseek-harness-src/docs/tool-catalog.md)