← Back to home@HuanLinOTO

dsh-plugin-toolsets

按需加载命名工具集:一条目录 + get_tools 元工具,把工具 schema 的上下文成本推迟到模型确认需要时 | On-demand toolset loading: a catalog plus a get_tools meta-tool that defers tool-schema cost until the model asks

Stars
0
Language
TypeScript
Created
Oct 18, 2026
Updated
Oct 18, 2026
GitHub repo

Introduction

dsh-plugin-toolsets card

dsh-plugin-toolsets

在 ctx 上提供统一的 toolset API:插件注册带"什么时候该用"说明的命名工具集,模型需要时调用全局 get_tools 工具按名把整组工具加载进自己所在 agent 的工具列表。

动机:DSH 插件注册的工具全部进入全局工具列表,每轮请求都要为所有工具的 schema 付出上下文成本。本插件把成本推迟到"模型确认需要"的那一刻——平时只有一条目录(各 toolset 的名称 + 用途摘要),模型调用 get_tools('name') 后整组工具下一轮生效。

工作机制

  • 插件通过 ctx.toolsets.register({ name, description, tools }) 注册 toolset(description 即"什么时候该用",模型可见,建议英文)。
  • 目录非空时,插件在全局注册唯一的 get_tools 元工具,目录嵌入其 description;目录变化(注册/注销)时热重建。
  • 模型调用 get_tools({ toolset: 'name' }) 时,插件把这组工具逐个注册进发起调用的 agent 自己的 scope layer(exec.agent.ctx):
    • 下一轮模型请求即可调用(工具列表按请求组装,会随请求头部记录入会话日志);
    • 仅对该 agent 可见;子 agent 需自行调用 get_tools;
    • 随 agent 销毁自动回收,无需善后;
    • 同一 agent 重复调用是幂等 no-op(返回当前工具清单)。
  • 注册时严格校验(fail loud):toolset 名唯一且匹配 ^[a-z][a-z0-9_-]*$;工具须为完整 defineTool 契约(output: { schema, render });跨 toolset 工具名全局唯一;工具名 get_tools 保留。
  • 激活时若工具名与该 agent 当前可见工具冲突,拒绝注册(遮蔽几乎必是配置错误)。
  • 未知的 toolset 名按参数错误处理(ToolArgsError,结果里列出全部已知名,模型下一轮自纠)。

消费方接入

其他插件声明 inject: ['toolsets'] 并在 peerDependencies 加上 @huanlin/dsh-plugin-toolsets:

import type { Context } from '@deepseek-ai/cordis'
import { defineTool } from '@deepseek-ai/dsh-tools'

const androidTap = defineTool({
  name: 'android_tap',
  description: 'Tap a point on the Android screen.',
  parameters: { x: { type: 'integer', required: true }, y: { type: 'integer', required: true } },
  output: { schema: { type: 'json' }, render: () => [] },
  async execute(args) { /* … */ },
})
// …其余工具…

export const name = 'my-android-plugin'
export const inject = ['toolsets']

export function apply(ctx: Context): void {
  ctx.toolsets.register({
    name: 'android',
    description: 'Operate a real Android phone over ADB — tap, swipe, screenshot, UI dump.',
    tools: [androidTap, /* … */],
  })
}

模型侧看到的效果(get_tools 描述节选):

Toolsets:
- android: Operate a real Android phone over ADB — tap, swipe, screenshot, UI dump. (7 tools)

开发(本仓库)

pnpm install        # 需要本机 dsh checkout 位于 D:/Projects/deepseek-harness/dsh
                    # (devDependencies 以绝对路径 link 到其中的 vendored/workspace 包)
pnpm test           # 单元测试(mock 宿主边界)+ 真实组合测试(真实 cordis + ToolRuntime + SystemPrompt,不 mock)
pnpm run typecheck  # tsc --noEmit(src/)
pnpm run build      # tsc + tsdown → lib/(单 bundle:index.js + index.d.ts)

无 prepare 脚本、lib/ 入库(预构建策略):@deepseek-ai/* private peer 无法在 pnpm git-install 的临时 prepare 目录解析,github: 安装直接使用仓库内 lib/。

运行

dsh plugin --profile <name> add "link:D:/Projects/deepseek-harness/dsh-plugin-toolsets"
# 或 github:huanlinoto/dsh-plugin-toolsets / @huanlin/dsh-plugin-toolsets(npm)

安装后重启 dsh web(人类操作)。注意:本插件自身不提供任何 toolset——装好后工具列表暂时无变化;装载任一 inject: ['toolsets'] 的消费插件后,get_tools 才会出现。

检查

pnpm test                                        # 30 个测试(27 单元 + 3 真实组合)
pnpm run build                                   # 重建 lib/
pnpm pack --dry-run                              # 清单只含 lib/ + cordis.patch.yml
dsh --profile <name> --dump-config               # 应出现 id: toolsets 的 insert 行

已知取舍

  • 工具前缀缓存:激活改变工具列表,会打断 provider 的工具前缀缓存——这是按需加载特性的固有代价(以一次缓存失效换取长期上下文节省)。
  • 下一轮生效:get_tools 返回后工具下一轮请求才可调用(工具列表按请求固定,所有延迟方案共性)。
  • 子 agent 不继承:激活绑定发起调用的 agent;子 agent 需自行调用 get_tools。
  • 激活态为内存态(WeakMap,不持久化):会话重启/agent 替换后激活消失,模型可重新 get_tools(幂等)。
  • 与 deferLoading 正交:dsh-llm 内建的 deferLoading(provider 层延迟声明 + developer 工具添加记录)与本品(注册表层按 agent 惰性物化)是不同机制,可组合但互不替代。
  • 保留名:工具名 get_tools 与 toolset 名空间(^[a-z][a-z0-9_-]*$)由本插件管理。

许可

AGPL-3.0