| name | dsh-tool-authoring |
| description | 写或改 dsh 里模型可调用的工具时使用——defineTool、parameters/output schema、execute 契约、exec.signal/exec.agent、后台任务(run_in_background + ctx.jobs)、UI 卡片(presentCall/presentResult/presentationMeta)、以及该用哪个执行策略钩子(tools/pre-execute、ctx.tools.guard、tools/execute、tools/post-execute、tools/result)。也用于排查工具没被模型调用、参数校验、isError 语义、Code Mode 兼容性。 |
写 dsh 工具
先运行 python3 tools/check-harness-drift.py。目标版本的语义真源是
vendor/deepseek-harness/docs/cookbook/adding-a-tool.zh.md(完整字段见
docs/subsystems/tools.zh.md),实际签名以目标插件安装的 .d.ts 为准。生产级实现可查
上游 packages/tool/tool-bash/;本地插件只在依赖版本相同且测试通过时作为参考。
最小形态
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): void {
ctx.tools.register(defineTool({
name: 'read_file',
description: 'Read a file from disk.',
parameters: {
path: { type: 'string', required: true, description: 'Absolute path' },
limit: { type: 'number' },
},
output: {
schema: { type: 'string' },
render: (_args, value) => [{ type: 'text', text: value }],
},
async execute(args, exec) {
return readFile(args.path, { encoding: 'utf8', signal: exec.signal })
},
}))
}
注册是副作用:dispose 插件 fiber 就注销工具。schema 自动流进系统提示词组装。
defineTool 是第一方工具用的类型化辅助函数;ctx.tools.register() 也直接吃原始
JSON Schema 的 ToolDefinition(MCP 来源的工具就走这条),但那种要自己负责
输入校验。
execute 契约
- 参数已经替你校验过。 类型、必填、字面量、联合(恰好匹配一个分支)、嵌套值
都在
execute 前校验完。schema DSL 表达不了的仍要手查:非空字符串、正数、
跨字段规则。显式对象节点必须声明 additionalProperties: true | false。
- 把
args 当只读。 注册表会把 arguments 物化成分离的无损 JSON 并在策略开始
前冻结,同时分配不透明的 exec.token。
- 返回规范 JSON 值,不返回内容块。
output.schema 用 ValueSchemaSpec,根可以
是对象/数组/标量/null。注册表快照 → 校验 → 冻结 → 才交给
output.render(args, value)。别让调用方从自然语言里解析 id 和字段。
- 抛异常或返回非法值 =
isError。 基础设施故障就抛。领域上的坏结果不算
错误——进程非零退出这类,要写进规范值,由 render 去解释。
- 遵守
exec.signal。 信号触发就取消。
exec.agent 用来发异步通知:
agent.inject({ content, source: { kind: 'plugin', plugin: '<name>' } }) 追加
持久上下文,下一次模型请求会看到。这不是唤醒——空闲的 agent 保持空闲。要
防 agent 已 dispose(try/catch)。
- 注册借用你的只读定义。 注册后不要改 schema 或换回调。要热替换工具就
dispose 它所在的 effect 再注册替代品。
描述怎么写
description 是模型判断工具用途的主要触发面。把用户真会说的话写进去;面向中英
用户的插件可以同时给出两种语言和明确触发词:
description:
'Save a manual snapshot of the current profile configuration '
+ 'into the configuration history. Trigger words: "保存快照", '
+ '"save a snapshot", "记下当前配置". Snapshots are never auto-cleaned.',
长时间运行的工作
用 producer 配置开 run_in_background,再
ctx.jobs.start({ kind, label, owner: exec.agent, run })。
- 预先中止的调用判失败(此时没有 job,id 满足不了成功输出 schema)。
- 成功的后台分支返回类型化句柄,如
{ kind: 'background', jobId }。渲染器可以
输出 started background job bash-1 这种人话,但 Code Mode 绝不能靠解析
这段文本拿 id。
ctx.jobs.start() 发布 id 之后,用 job 自己的取消信号,不要用
exec.signal:之后取消外层调用只停止等待,不终止已发布的工作。那段生命周期
归 job_kill、owner dispose 和服务 teardown。前台工作仍与 exec.signal 耦合。
别把部署策略写进工具
按需要的语义选钩子,别默认选第一个:
| 需求 | 用 |
|---|
| 可扩展的允许/拒绝/询问策略 | tools/pre-execute(waterfall,返回类型化决策) |
| 不变式要求的单调最终拒绝,后续监听器无法撤销 | ctx.tools.guard() |
| 包裹实际分派生命周期:超时/重试/指标 | tools/execute(只有它能替换 exec.signal) |
| 显式变换展示或返回值、阻止结果、附加模型可见上下文 | tools/post-execute |
| 只观察不可变最终结果(指标/审计/捕获) | tools/result |
| 过滤模型可见工具集(展示/查找/执行三者对齐) | ctx.tools.restrict() |
工具绝不 import UI 或传输类型。
Code Mode 会自动碰到你的工具
Code Mode 下每个可见工具都能 await tools.<name>(args),无需额外集成;生成的
ToolArgsMap / ToolOutputMap 从同一组 schema 派生。成功调用解析为策略处理后
的规范 JSON 值(不是渲染后的内容),失败以 ToolCallError reject(程序只能看
name、toolName、message)。
所以:把 output.schema 设计成好用的程序化 API——直接返回句柄和字段,人话
解释放 output.render。
UI 卡片
模型可见内容(output.render)和 UI 卡片是两件独立的事。卡片通过可选的
presentCall(args) / presentResult(args, result) 声明,选匹配行为的卡片类型:
{ card: 'generic', title, kind?, rawInput?, content?, locations? } —— 默认。
locations: [{ path, line? }] 让编辑器能跟随跳转。
{ card: 'terminal', title, description?, cwd? } —— 调用本身是 shell 命令。
{ card: 'diff', title, diffs, locations? } —— 创建/修改文件;新文件
oldText: null。
- 结果侧还有
search(shape: 'matches' | 'paths' + truncated/total)和
web(kind: 'search' | 'fetch')。
硬性规则(违反会出问题):
- 展示器必须是纯函数。 实时流和会话日志回放都会跑它——不做 I/O、不读会话
状态、不用时钟/随机数。想在
presentCall 里读文件旧内容或 cwd 就是信号:
那属于持久结果元数据或适配器,不属于展示器。
- UI 格式不许进模型结果。 ```console 围栏、diff、相对化路径都不该为了 UI
进规范值或 Native 内容。
- 展示路径是软校验:畸形或旧日志参数让包装器返回
undefined(回退到通用
卡片),而不是抛异常——展示绝不能让回放崩溃。
需要在回放里重现的结果期事实(write/edit 的已应用 hunk 之类),用
output.presentationMeta(args, value) 投影出可回放 JSON,核心会持久化在
tool/result 上。嵌套 Code 分派没有卡片,会跳过这个投影器。
自查