dsh 拆解系列 Vol.01:没有特权内核的 Agent 运行时

简介: DeepSeek Harness(dsh)是DeepSeek开源的Agent运行时框架,秉持“一切皆插件”理念,将模型适配器、工具、会话、主循环等全部解耦为可配置、可替换、可卸载的插件,基于Cordis元框架实现时空可组合性。当前v0.1.0-rc.7为开发者预览版,MIT协议,强调工程可扩展性而非仅功能堆砌。

本文基于 @deepseek-ai/dsh 0.1.0-rc.7(npm latest 标签),文中的包名与依赖数据都可以用文末命令复现。dsh 目前处于 developer preview,官方明确预告后续会有破坏性变更,因此这个系列更关注具体 API 背后的工程设计。读到本文时,如果版本继续往前走,请以文首标注的版本为准。

这个系列拆什么

上周 DeepSeek 开源了自己的 Agent Harness——DeepSeek Harness,命令行入口是 dsh。相比常见的工具、Hook 扩展,dsh 把可替换的范围继续往运行时内部推进了。

接下来的 dsh 拆解系列,就围绕一个问题逐层展开:Agent 运行时的扩展点,到底应该开放到哪一层?

  • 01 没有特权内核的 Agent 运行时(本篇)

    • 先看扩展一个 Harness 常见的几种方式,它们分别能改到哪里,以及 dsh 为什么继续把扩展边界往内部推进。
  • 02 Cordis:dsh 插件架构的地基

    • 看如何在运行时安全挂载和卸载插件,以及如何声明、追踪和回滚依赖。
  • 03 Profile 与 Bundle:运行时的组装层

    • 一棵插件树如何被逐层组装出来,以及 web、headless、desktop 为什么可以只是不同的配置。
  • 04 Turn 与 Step:主循环的事件切面

    • 拆一次对话从输入到结束经过的事件流,以及主循环中哪些位置允许拦截、改写和短路。
  • 05 会话日志:上下文的唯一事实源

    • 看「模型可见即已记录」这条运行时约束,怎么撑起 fork、恢复、回放和遥测。
  • 06 能力接缝:一次 provider 替换的传导范围

    • 拆文件系统、子进程和执行环境之间的关系,看更换一个 provider 如何带动一组能力迁移。
  • 07 这套设计什么时候值得抄

    • 最后把账算清楚:这套架构增加了哪些复杂度,适合什么场景,以及哪些设计即使不用 Cordis 也值得带走。

第一篇先从最外层开始,看扩展一个 Agent Harness,究竟可以改到哪一层。

一分钟认识 dsh

只要你安装了 Node.js,就可以用下面这条命令启动 dsh:

npx @deepseek-ai/dsh web

执行命令后,它会启动 Web UI,并在终端返回一个默认服务地址 http://127.0.0.1:3080。项目采用 MIT 协议,模型路由、文件访问和持久化都放在本地。

README 对整个架构的概括很直接:everything is a plugin。dsh 底层使用 Cordis 作为插件框架,Cordis 背后的设计还写成了一篇论文——「A Programming Paradigm for Spatiotemporal Composability」。

只看这些描述,dsh 和过去一年出现的不少 Harness 看起来差别不大:同样开源、支持插件,也强调扩展性。真正的区别在实现层面——不同 Harness 虽然都在讲“可扩展”,但开放到哪一层、能改到什么程度,并不一样。

扩展一个 Harness 的 3 条既有路径

先来看几种常见的 Harness 扩展方式。它们在 Harness 的不同位置起作用:

第一种是外部工具接入。MCP 是当中比较典型的一种形式,Claude Code 支持 MCP Server,Codex 也支持 stdio、Streamable HTTP 等接入方式。搜索、数据库、浏览器、内部业务系统等能力,都可以通过协议接进 Harness。

这种扩展方式增加的是模型可调用的工具。由 Harness 自己决定工具如何接入和管理、什么时候把 Schema 放进上下文、一次工具调用结束后主循环要怎么继续。

第二种是生命周期 Hook。Claude Code 会在 PreToolUsePostToolUseSessionStartStop 等固定节点开放 Hook,其他 Harness 也有类似机制。开发者可以在这些节点加入检查、日志、安全策略等逻辑,但具体能够介入 Harness 运行过程中的哪些环节,取决于框架开放了哪些 Hook 节点。

第三种是 Plugin 打包与分发。开发者可以把命令、Subagent、MCP Server、Hook 等扩展能力组合成一个可安装的 Plugin,再分发给其他用户或团队使用。

主循环的天花板

Plugin 主要解决扩展能力的打包和分发,并不会因此继续打开 Harness 更内部的组件。等 MCP、Hook 这些扩展方式碰到边界,再想往里改,就得开始动源码了。

毕竟再往里,就是工具注册表、上下文装配、Agent Loop、模型适配器和会话日志这些运行时组件。

把这些部分留在框架内部其实有一定的合理性。主循环和状态结构相对固定,运行行为更容易保持一致,问题排查、安全策略和兼容性也更好控制。扩展点开得越深,框架需要长期维护的接口、生命周期和行为约定也会越多。

dsh 则把这条扩展边界进一步推进到运行时内部。模型适配器、工具注册表、会话日志,甚至 Agent Loop 本身都被做成了插件,可以通过配置自由地组合和替换。不同插件挂在同一棵运行时树上,它们产生的注册也跟着各自的生命周期一起管理

运行时配置树

前面讲的是 dsh 的架构设计,实际运行起来后,也能在最终配置中看到这些组件。执行如下命令:

npx @deepseek-ai/dsh --profile web --dump-config

--dump-config 会打印 Profile、Bundle 和 Patch 组合后的最终配置,不需要启动 Web 服务。

从输出里可以看到 dsh-llmdsh-sessiondsh-toolsdsh-agent-loop 等插件项,模型适配器、Session、工具注册表和 Agent Loop 都进入了同一份运行时配置。Web Profile 还会在 dsh-base 的基础上继续加入或修改一批配置,例如 dsh-web-app、Web Server、Storage 和客户端相关插件。

至于 dsh-base、Web Profile 和 Patch 最后是怎么叠成这份配置的,我们放到第三篇再展开。

依赖清单的插件分解

运行时配置能看到最后装进来了什么,npm 依赖清单则能在包层面看到这些能力是怎么被拆解的。

在本文测试的版本中,dsh 主包有 61 个直接依赖,其中 58 个来自 @deepseek-ai/*。继续展开 dsh-base,前两层依赖去重后可以看到 103 个 @deepseek-ai 包。

这些包覆盖了运行时的多个部分。工具注册表放在 dsh-tools,bash、文件读写、Web、Subagent、字符串替换编辑等工具继续拆成各自的 dsh-tool-* 包;模型侧由 dsh-llm 定义接缝,再接入 dsh-llm-deepseekdsh-llm-pi-ai 等实现,重试逻辑也单独放在 dsh-llm-retry 中。

其中 dsh-llm-pi-ai 的包描述称它为 dsh-llm-deepseekdesign-validation twin。它相当于第二套模型适配实现,用来验证 dsh-llm 这一层确实能够承载不同 Provider,这也让“模型适配器可以替换”有了一个现成的对照。

Session 也沿用了类似的拆法。dsh-session 负责主体能力,持久化、查询、Projection、Telemetry、Checkpoint 等继续拆开;会话标题也分成 dsh-session-title 和具体的标题生成 Provider。到了这一层,Session 已经不再对应某一种固定存储实现,围绕它的存储、查询和附加能力都有各自的组件边界。

独立的运行时策略

还有一些包不负责提供工具,只专门用来处理运行时策略。

例如 dsh-fs-observation-policy 管的是文件操作中的“先读后改”。它建立在 ctx.fs 之上,通过 fs/* 事件维护文件是否被观察过、写入时的版本校验等状态,同时不提供新的 Service API。

这样一条规则没有塞进 dsh-tool-fs,也没有写进某个文件系统 Provider。文件工具负责发起操作,文件系统负责执行,Policy 决定这次操作是否符合约束。

dsh-sandbox-policydsh-user-approvaldsh-permission-presets 也采用了类似的拆法。权限、审批和执行环境各自存在,后面替换其中一层时,不需要把整套工具实现一起改掉。

这种拆分也解释了为什么 dsh 会出现这么多小包:模型、工具、存储、策略和编排都被切出了独立边界,运行时再通过插件树把它们重新组合起来。

能自己装插件的 Agent

这一套插件机制还有一个更特殊的用途。

dsh-tool-cordis 提供了一组面向模型的 Cordis 工具,Agent 可以检查当前运行时,写一段插件代码,再把它挂到当前插件树上。插件开始工作以后,它可能注册新的 Tool Schema、事件监听器,也可能占用一个 ctx Service Key。

这时,插件能不能卸载干净就变得很重要。

如果插件离开以后,Tool Schema 还留在下一轮 Prompt 里,或者事件监听器还挂在 Agent Loop 上,下一轮 Turn 就会继续收到这段插件留下的影响。对一个允许 Agent 在运行期间修改自身能力的系统来说,插件的生命周期需要覆盖这些注册项的创建和回收。

Cordis 处理的正是这部分。插件产生的注册会跟随上下文生命周期管理,卸载时再执行对应的清理操作。ctx.effect()ctx.on() 怎么记录这些变化,依赖又怎么跟着插件一起挂载和撤销,就是下一篇要拆的内容。

插件化的成本

运行时拆得越细,调试方式也会跟着变化。面对上百个包,仅看源码里的 import 关系很难还原某一次运行到底加载了什么,真正的运行时结构还要结合 Bundle、Profile 和 Patch 来看。前面的 --dump-config,也因此成了排查配置时很实用的一条入口。

Patch 也会带来维护成本。dsh 按 id 定位配置项,替换时处理的是对应项的完整 config。上游修改配置结构之后,已有 Patch 可能也要跟着调整。

插件生命周期同样增加了约束。向运行时注册的监听器、Service 或其他资源,需要有对应的清理逻辑;如果多个资源之间还存在销毁顺序,相关处理也要一起纳入生命周期管理。这类问题在加载阶段未必出现,更多会在插件卸载时暴露。

事件系统还有自己的契约。不同事件采用不同的派发模式,插件依赖的不只是一组事件名称,也依赖它们的执行方式。派发语义变化时,对应插件也可能需要调整。

再加上 dsh 目前仍处于 developer preview,具体 API 和配置结构还会继续变化。所以这个系列会以当前版本做验证,同时把重点放在这些设计背后的边界划分和运行方式上。

适用边界

如果一套 Harness 只服务内部团队,产品形态固定,也没有运行时动态扩展的需求,把主循环拆成大量可替换组件未必划算。组件越多,需要维护的配置、生命周期和接口约定也会一起增加。

dsh 这套设计更贴近两类场景:一类是第三方需要持续向运行时加入能力,另一类是 Agent 本身会在运行过程中修改自己的能力。到了这里,“插件能加载”还不够,卸载后的状态也要能够恢复干净。

下一篇就从这个问题继续往下拆:Cordis 怎么记录插件给运行时带来的变化,又怎么在插件离开时把这些变化撤回来。

相关文章
人工智能 缓存 前端开发
9146 40
人工智能 JavaScript 开发工具
3765 9
开发工具 Swift git
1429 2
缓存 JavaScript Shell
1732 2
人工智能 JavaScript 测试技术
1307 0
Shell API 调度
949 3
人工智能 JavaScript 测试技术
523 4
人工智能 Java BI
610 0