跳至内容

21 · 添加新包与工具

前置阅读20 · SDK 与 JSON-RPC 协议下一步22 · 添加 LLM 适配器与 Chat 节点

学习目标

  1. 掌握添加新 workspace package 的完整流程
  2. 理解 package.json 不变量和 tsconfig 配置
  3. 掌握工具开发的完整契约
  4. 知道工具的 UI 渲染意图设计
  5. 理解 Code Mode 自动集成

一、添加新包

1.1 目录结构

文件docs/cookbook/adding-a-package.md:10-21

packages/<group>/<pkg>/
  package.json     # 从 packages/core/tools 复制,调整 name/description/deps
  tsconfig.json    # extends ../../../tsconfig.base.json, rootDir src, outDir lib/types
  src/index.ts     # service default export 或 plugin (name/inject/apply/Config)
  README.md        # service API, events, extension points, design notes

1.2 选择包组

现有包组:corellmbashcompactsubagenttodosession-persistenceuiutilsupport

新包组允许,但必须是纯容器:无 package.json,无源文件,包仍在其下一级。

1.3 package.json 不变量

文件adding-a-package.md:25

pnpm run constraints / scripts/check-workspace-constraints.ts 强制:

字段要求
privatetrue
version匹配根 package.json
type"module"
main"lib/index.js"
types"lib/types/index.d.ts"
exports["."].types"./lib/types/index.d.ts"
exports["."].default"./lib/index.js"
@deepseek-ai/cordis同时在 peerDependencies 和 devDependencies(同范围)
@deepseek-ai/schemastery在 dependencies(runtime validator)
files恰好 lib/index.js, lib/invariant.js, lib/types/**/*.d.ts + 包特定产物

1.4 相对导入用 .ts 后缀

文件adding-a-package.md:27

typescript
// 源码中
export * from './types.ts'

编译器将 .ts 重写为 .js(emitted JS),声明中保留 .ts(NodeNext 消费者解析到 .d.ts)。

1.5 注册到根配置

文件变更
tsconfig.base.json现有包组无需编辑;新包组加 ./packages/<group>/*/src@deepseek-ai/dsh-* 通配符
tsconfig.host.jsontsconfig.client.json{ "path": "./packages/<group>/<pkg>" }references
knip.json仅当包有仓库发现未覆盖的入口时

1.6 自动覆盖(无需编辑)

  • package.json workspaces
  • scripts/publint-all.ts
  • tsdown.config.ts
  • .oxlintrc.json
  • scripts/check-workspace-constraints.ts

1.7 验证

sh
pnpm install
pnpm run doc-sync
pnpm run constraints && pnpm run typecheck && pnpm run lint
pnpm run build && pnpm run hygiene

二、包拓扑决策

2.1 能力缝三角色

文件adding-a-package.md:42-43

可替换能力:将 Service Definition / Service Provider / Consumer 角色分到独立包(当它们独立演化时)。单用途插件保持一个包。

2.2 命名规则

文件adding-a-package.md:47-71

命名稳定的当前职责,不命名第一个实现、可能的未来扩展或 Cordis 基类。

使用场景不使用场景
Controller接受命令/用户意图,改变一个域或展示状态执行任意工作、拥有 provider 舰队
Store拥有一个数据集,主要提供 CRUD/snapshot/订阅验证状态机、仲裁权限
Registry拥有动态命名注册集主要职责是调度/执行
Runtime运行实时工作,拥有调度/取消/生命周期只存储记录、返回目录
Executor运行一个显式请求或解析的规范拥有广泛应用生命周期
Provider提供一个能力定义的实现是能力定义本身

2.3 ctx key 规则

  • 单数 ctx key:一个 engine/runtime/policy/controller
  • 复数 ctx key:registry 或拥有多个命名成员的 service
  • 不复用一个 Cordis Context key 给不兼容的 host/client 声明

三、工具开发

3.1 最小形状

文件docs/cookbook/adding-a-tool.md:9-36

typescript
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.',
    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 })
    },
  }))
}

注册是基于 effect 的:dispose plugin fiber 即注销工具。

3.2 execute() 契约规则

文件adding-a-tool.md:42-50

规则说明
Args 已验证defineToolexecute 前验证 model-generated arguments
注册借用只读定义不修改 schema 或替换回调
执行身份受保护exec.tokencallIdnameargumentsagentsignal 不可变
返回一个规范 JSON 值output.schema 使用 ValueSchemaSpec
抛出或无效值 = isErrorregistry 捕获抛出并包含 schema/renderer 失败
遵守 exec.signal触发时取消 in-flight 工作
presentationMeta 可选投影持久化 card 数据

3.3 长运行工作

文件adding-a-tool.md:52-55

typescript
ctx.jobs.start({ kind, label, owner: exec.agent, run })
  • registry 在 producer body 前拒绝 pre-aborted 调用
  • runtime 验证所有权和 task-controller 可用性
  • 成功的后台分支返回 { kind: 'background', jobId }
  • producer 提供 cancel、非拒绝 done、可选 readOutput

3.4 执行策略与观察

文件adding-a-tool.md:58-59

扩展点用途
tools/pre-execute可扩展 allow/deny/ask 策略
ctx.tools.guard()最终单调 deny(后续 listener 无法撤销)
tools/execute包装 dispatch 加 deadline/retry/metrics
tools/post-execute替换展示内容或返回值
tools/result观察不可变规范化结果

四、Code Mode 自动集成

4.1 免费获得

文件adding-a-tool.md:62-65

Code Mode 中,每个可见注册工具可用 await tools.<name>(args),无需额外集成。

4.2 类型推导

typescript
// 生成的 ToolArgsMap 和 ToolOutputMap 从同一 schema 推导精确类型
const result: string = await tools.read_file({ path: '/tmp/test.txt' })

4.3 设计 output.schema

  • 返回 handle 和字段直接
  • 允许 scalar/array/null root
  • 人类解释放在 output.render
  • 中间值是 execution-local,不持久化

五、UI 渲染意图

5.1 两种方法

文件adding-a-tool.md:68-82

方法返回用途
presentCall(args)ToolCallView(PENDING card)调用时的卡片
presentResult(args, { content, isError, meta? })完成卡片结果时的卡片

5.2 Card 类型

Card用途示例
generic默认title + rawInput + content + locations
terminalshell 命令tool-bash
diff文件创建/修改tool-fs write/edit
search发现结果tool-fs-search grep/glob
webweb 检索tool-web web_search/web_fetch

5.3 硬规则

文件adding-a-tool.md:84-90

规则说明
纯度在 live streaming 和 session-log REPLAY 上运行,必须是 args(+ result)的纯函数
UI-only 格式化不进 model resultoutput.render 拥有 model-facing prose;presentationMeta + card presenters 拥有 replayable UI state
defineTool 软验证展示路径malformed 参数返回 undefined(generic fallback)而非抛出

5.4 locations 字段

typescript
presentCall(args) → { card: 'generic', locations: [{ path, line? }] }

设置 locations 让有能力的编辑器跟随/跳转到工具触及的文件。


六、完整工具示例

6.1 带终端卡片的 shell 工具

typescript
import { exec } from 'node:child_process'
import { defineTool } from '@deepseek-ai/dsh-tools'

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

export function apply(ctx: Context) {
  ctx.tools.register(defineTool({
    name: 'run_command',
    description: 'Run a shell command.',
    parameters: {
      command: { type: 'string', required: true, description: 'Shell command' },
      cwd: { type: 'string', description: 'Working directory' },
    },
    output: {
      schema: { 
        type: 'object',
        properties: {
          stdout: { type: 'string' },
          stderr: { type: 'string' },
          exitCode: { type: 'number' },
        },
      },
      render: (_args, value) => [{ type: 'text', text: value.stdout }],
    },
    presentCall(args) {
      return { card: 'terminal', title: args.command, cwd: args.cwd }
    },
    presentResult(_args, { content, isError }) {
      return { card: 'terminal', title: 'completed', output: content[0]?.text ?? '', isError }
    },
    async execute(args, exec) {
      return new Promise((resolve) => {
        const proc = exec(args.command, { cwd: args.cwd, signal: exec.signal })
        let stdout = '', stderr = ''
        proc.stdout?.on('data', (d) => { stdout += d })
        proc.stderr?.on('data', (d) => { stderr += d })
        proc.on('close', (exitCode) => resolve({ stdout, stderr, exitCode: exitCode ?? -1 }))
      })
    },
  }))
}

6.2 带 diff 卡片的文件写入工具

typescript
ctx.tools.register(defineTool({
  name: 'write_file',
  description: 'Write content to a file.',
  parameters: {
    path: { type: 'string', required: true },
    content: { type: 'string', required: true },
  },
  output: {
    schema: { type: 'object', properties: { written: { type: 'boolean' } } },
    render: () => [{ type: 'text', text: 'File written.' }],
  },
  presentCall(args) {
    return { 
      card: 'diff', 
      title: `write ${args.path}`,
      diffs: [{ path: args.path, oldText: null, newText: args.content }],
      locations: [{ path: args.path }],
    }
  },
  async execute(args, exec) {
    await writeFile(args.path, args.content, { signal: exec.signal })
    return { written: true }
  },
}))

七、包 README

7.1 必需结构

文件adding-a-package.md:77-103

markdown
## Model Experience

### Request context and condition

#### What the model sees

[数据相关字段或生成的 catalog 链接]

##### Verbatim text for this field, when needed

\`\`\`markdown
[稳定系统提示文本,逐字复制]
\`\`\`

#### Token effect

[Fixed/conditional/retained/replaced/capped/zero-direct]

#### KV Cache effect

[Append-only/prefix-stable/replacing/independent]

## Known Limitations and Deferred Work

- **Consumer-visible gap** — 确切缺失操作、后果、维护者约束

7.2 无 context effect 的包

使用 SENTENCE_MODEL_EXPERIENCE 中的 None, asIndirectly, through 句式,或加入 NO_MODEL_EXPERIENCE_SECTION


实战练习

  1. 创建新包:按 1.1-1.7 步骤创建 packages/util/my-utils/ 包,包含一个简单的字符串处理 service。

  2. 创建工具:按 3.1 最小形状创建一个 list_files 工具,列出目录内容,使用 generic card。

  3. 设计 UI 卡片:为 list_files 工具添加 presentCallpresentResult,设置 locations

  4. 理解纯度约束:说明为什么 presentCallpresentResult 必须是纯函数,在 replay 时会发生什么。


关键源码定位

内容位置
添加包 cookbookdocs/cookbook/adding-a-package.md
添加工具 cookbookdocs/cookbook/adding-a-tool.md
扩展 cookbookdocs/cookbook/extension-cookbook.md
package.json 不变量docs/cookbook/adding-a-package.md:25
命名规则表docs/cookbook/adding-a-package.md:51-71
工具最小形状docs/cookbook/adding-a-tool.md:9-36
execute() 契约docs/cookbook/adding-a-tool.md:42-50
长运行工作docs/cookbook/adding-a-tool.md:52-55
执行策略扩展点docs/cookbook/adding-a-tool.md:58-59
Code Mode 集成docs/cookbook/adding-a-tool.md:62-65
UI 渲染意图docs/cookbook/adding-a-tool.md:68-82
Card 类型docs/cookbook/adding-a-tool.md:73-82
硬规则docs/cookbook/adding-a-tool.md:84-90
Model Experience 格式docs/cookbook/adding-a-package.md:77-103
defineToolpackages/core/tools/src/index.ts
ToolRuntimepackages/core/tools/src/index.ts
check-workspace-constraintsscripts/check-workspace-constraints.ts

下一步

本文理解了添加新包与工具的完整流程。下一篇 22 · 添加 LLM 适配器与 Chat 节点 将讲解 LLM 适配器和 Conversation Node 开发。

基于 DeepSeek Harness 源码整理的渐进式学习文档