Skip to content
My Blog
Go back

一切皆插件:读 DeepSeek Harness 的架构

DeepSeek 开源了一个 Agent Harness,代号 dsh。装起来一行命令:npx @deepseek-ai/dsh web,默认在 127.0.0.1:3080 起一个 Web UI。

README 里两句话值得先抄下来。第一句:「built on an everything-is-a-plugin architecture and powered by Cordis」。第二句:项目处于 developer preview,迭代很快,「THERE WILL BE COMPATIBILITY-BREAKING CHANGES」——全大写的免责声明。

我把源码读了一遍,这篇先讲整体架构。根包是 @deepseek-ai/dsh-root,版本 0.1.2-alpha.1,pnpm 11.7.0 的 workspace monorepo,要求 node ^22.19.0 || >=24.0.0,全仓库 ESM。

Cordis:不是依赖,是 vendored 进来的框架层

Cordis 是一个插件框架,上游在 cordiverse/cordis,设计思路有篇配套论文(arXiv:2608.25512,标题是「A Programming Paradigm for Spatiotemporal Composability」)。有意思的是它不是普通的 npm 依赖——源码被 vendored 进 vendor/ 目录,rescope 成 @deepseek-ai/cordis,pin 在上游 4.0.0-rc.7。

vendor/README.md 里写了原因:「so that the harness fully owns its framework layer (auditable, patchable, pinned)」。框架层要可审计、可打补丁、版本钉死,干脆整份源码拿进来。这个决定本身就是一种架构表态。

Cordis 的核心机制就四件事:

五种事件分发模式定义在 vendor/cordis/src/events.ts

export type DispatchMode = 'emit' | 'parallel' | 'serial' | 'bail' | 'waterfall'

emit 是发后即忘不等候;parallel 并发等候;serial 顺序等候;bail 同步执行、遇到第一个 bail 值就停;waterfall 用 next() 续延把监听器组合成链。最后这个在 dsh 里用得最关键,模型调用前的拦截管线全靠它,下一篇细讲。

Profile + Bundle:一份核心,叠出四种形态

dsh webdsh --profile headless "跑一下测试" 启动的是同一套核心的不同组合。Profile 目前有四个:webheadlesssdkacp,另有一个独立的 sdk-minimal

每个 profile 不是一个大配置,而是一摞 bundle 的 patch 层自底向上叠加。每个 bundle 包在 package.json 里声明自己的 patch 文件:

// packages/bundle/web-app/package.json
"dsh": {
  "bundle": { "patch": "./cordis.patch.yml" }
}

packages/bundle/README.md 的原话:「The web, headless, acp, and sdk profiles build on dsh-base, while sdk-minimal supplies its complete tree in one bundle.」也就是 dsh-base 是四个主 profile 共享的第一层,各自的形态 bundle(dsh-web-appdsh-headlessdsh-sdk-appdsh-acp-app)再往上叠,用户还能叠自己的层。想换掉某个插件,不用 fork,叠一层 patch 覆盖就行。

52 个 group,核心只有几个包

packages/ 是两级结构 packages/<group>/<pkg>/,数了一下有 52 个 group,包名统一 @deepseek-ai/dsh-*。group 大致按能力域切:llm、shell、fs、subprocess、terminal、lsp、web、subagent、workflow、webhook、sandbox、interaction……每个能力域基本都自带接口包和若干实现包,这个「能力接缝」模式值得单独写一篇。

产品的主干在 packages/core/,一共 8 个包:

ctx key职责
sessionctx.sessions只追加的 SessionEvent 日志,一切模型可见事实的唯一来源
system-promptctx.systemPrompt每步组装系统提示词和工具 schema
toolsctx.tools工具注册 + 带守卫的执行管线(策略、包装、结果检查)
agentctx.agentsAgent 句柄、活跃注册表、agent/* 事件词汇表
agent-loopctx.agentLoop唯一的具体循环实现,驱动 turn 和 step 生命周期
scope给注册划分 per-agent 作用域

packages/core/README.md 用一句话把 turn 的旅程讲完了:循环认领输入、在 session log 上开一个 turn、经 system-prompt 组装请求、流式接收模型响应、把工具调用派发给注册表,「append every model-visible fact back to the log」。这句话里藏着整个项目最重要的一条不变量,下一篇展开。

外部程序怎么接进来

除了人用的 web 和 headless,dsh 还有两个给程序的 profile:

Python SDK 的做法比较直接:python/ 下有两个包,一个是高层 turns API 加 JSON-RPC 客户端,另一个干脆把 dsh 可执行文件和 native sidecar 打进了 wheel——SDK 启动时直接拉起捆绑的 dsh --profile sdk 运行时,用户不用自己装 Node 环境。

反方向也留了口子:packages/mcp/ 的 mcp-client 可以把外部 MCP 服务器的工具桥接进来,挂在 mcp__github__create_issue 这类稳定名字下。只桥接工具(resources 和 prompts 不支持),默认不启用任何服务器,按需 opt-in。

几条值得抄走的明文规则

根目录的 AGENTS.md 把项目规约写成了给 AI 协作者的「常备命令」,里面有几条放在任何项目里都成立:

最后一条最符合 developer preview 的定位:这个阶段把地基修对,比假装稳定重要。

下一篇讲 dsh 怎么驱动一个 turn,以及「模型可见 = 已记录」这条不变量是怎么落地的。

本系列


Share this post:

Previous Post
第 04 课 · 一次 Turn 的完整旅程
Next Post
崩溃的 turn 要关闭,不是截断:dsh 的会话持久化