ARTICLE · 987274
DeepSeek-Harness 源码深度解析
DeepSeek-Harness 源码深度解析
基于官方仓库 deepseek-ai/deepseek-harness的dsh-v0.1.2-alpha.2(commit0a53fb55,2026-08-30)源码阅读。项目处于 Developer Preview,文中接口和目录随版本演进可能发生破坏性变化。
1. 先给结论:它不是“一个 Agent”,而是可拼装的 Agent 运行时
DeepSeek Harness(命令名 dsh)把通常被写死在 Agent 框架中的部分——模型适配、提示词、工具、会话、沙箱、UI、调度和子 Agent——全部变成 Cordis 插件。它的核心不是一个巨大的 runAgent(),而是:以插件树组装运行时,以事件日志保存事实,以服务接口隔离可替换能力。
这带来三个直接结果:
同一 CLI 可由 profile 组成 Web、一次性 headless、SDK 和 ACP 服务; 换一个 LLM、文件系统或沙箱提供方,消费这些能力的工具无需修改; 会话日志是事实来源,恢复、fork、回放、UI 和遥测从同一事件流派生。
下面的图概括了这套分层:

源码规模也印证了它的工程定位:packages/ 下有 258 个 npm 包清单,全仓约 3054 个 TypeScript 源文件。这不是一个单体聊天程序,而是一套有明确扩展边界的 Agent 平台。
2. 代码库导航:先找“所有权”,再找实现
顶层目录的职责非常清晰:
deepseek-harness/ ├── apps/cli/ dsh 的唯一受支持 Node 应用入口 ├── packages/ 产品插件(258 个 workspace 包) │ ├── core/ session、prompt、tools、agent、agent-loop │ ├── bundle/ base/web/headless/sdk/acp 的组装层 │ ├── llm/ 模型抽象与 DeepSeek、pi-ai 等适配器 │ ├── fs/ shell/ 文件、命令、终端、子进程能力 │ ├── session/ 持久化、投影、查询、标题、遥测 │ ├── host/ Web 服务端 / BFF │ └── client/ 浏览器端状态、槽位和 UI 插件 ├── vendor/ 随仓固定的 Cordis 等上游源码 ├── python/ Python SDK 与随附运行时 ├── native/ Landlock 相关 Node addon 源码 ├── docs/ 架构、事件、流水线与生成目录 └── scripts/ 构建、质量门禁与文档/图谱生成器官方把每个能力放在 packages/<group>/<package>/,包名统一为 @deepseek-ai/dsh-*。阅读某一功能时,最有效的路线是:先看该 group 的 README 找到 Service Definition(接口)、Provider(实现)和 Consumer(通常是模型可调用工具),再进入 src/。这避免把某个本地实现误认为不可替换的“内核”。
3. Cordis:插件机制为什么能做到“可拆可换”
Cordis 源码位于 vendor/cordis/src。其 Context 是 Proxy 化的依赖容器:插件向 ctx 提供服务、注册类型化事件,并通过 ctx.effect() / ctx.on() 注册可撤销副作用。插件卸载时,注册项随 disposer 撤销。

关键设计点是 service scope。Context.isolate(name) 会创建某项服务的独立解析作用域;因此某个 Agent preset 可以拿到自己的工具或模型组合,而不污染全局运行时。Context.intercept() 则把服务配置叠加到子上下文。源码中的类型声明合并(例如向 Context 增加 sessions、tools)让插件 API 在编译期可见,而运行时依旧由装配决定。
这里有一个重要约束:注册本身必须是 effect,而不是直接修改全局单例。它保障了热加载、销毁和 profile 切换时不会残留“幽灵工具”或事件监听器。
4. Profile 与 Bundle:dsh 如何变成不同产品形态
CLI 入口在 apps/cli/src/bin.ts:解析参数后只走 profile、plugin 管理或 --dump-config 三条路径。真正的装配规则在 packages/boot/app-boot/src/profile.ts。
一个 profile 是 Harness home 中的具名目录,声明有序 bundle 列表和用户的 cordis.patch.yml;bundle 则是一个 npm 包,其 package.json 的 dsh.bundle.patch 指向配置 patch。最终配置遵循“后层覆盖前层”的顺序:

内置模板如下:
web | dsh-basedsh-web-app | ||
headless | dsh-basedsh-headless | ||
sdk | dsh-basedsh-sdk-app | ||
sdk-minimal | dsh-sdk-minimal | ||
acp | dsh-basedsh-acp-app |
dsh-base/cordis.patch.yml 是最值得读的“运行时物料清单”:它挂载 LLM、session、持久化、设置与凭据、沙箱与授权、filesystem、shell、tools、agent loop 等基础插件。配置行用稳定 id 定位,之后的 patch 替换该行完整 config 或插入新行。换言之,定制 Harness 的常规方式不是 fork 核心,而是写一个 bundle 或 patch。
5. Agent 主循环:一次用户输入如何走到下一轮
核心循环的入口是 packages/core/agent-loop/src/agent.ts 中的 ReactLoopAgent。它维护 inbox 和 idle / maintenance / running 三种 phase;followup()、steer()、inject() 都进入同一 inbox,只是目标边界和是否立即唤醒不同。
一次 turn 的主干如下:

两层概念必须区分:step 是“一次模型请求及其工具调用”;turn 是从领取输入到没有未完成工作的一段连续工作,可包含多个 step。agent/pre-step、agent/request、llm/stream 和工具流水线事件是 waterfall:监听器必须调用 next() 才会继续向下游委托。这是诸如上下文压缩、请求改写、权限策略和重试能够插入而不改 loop 的原因。
6. Session:事件溯源是整套系统的数据骨架
packages/core/session/src/index.ts 定义追加式 SessionEvent 日志及其内存 store。它不把“当前对话”当作可随意覆写的数组:请求历史由日志投影,持久化也是订阅 session/event 的插件责任。

这就是项目的硬约束“模型可见即已记录”:任何会进入模型请求的内容都必须能从日志重建。因此新增模型上下文不能只改 prompt 拼接,还必须扩展 SessionEventMap 并提供日志渲染/投影。它以一点记录成本换来了可审计性与可重放性。
默认持久化提供方是 dsh-session-persistence-jsonl:每个 session 一份仅追加 JSONL,默认采用带校验和的 Zstandard 帧;加载时能保留完整提交前缀并修复中断 turn。SQLite 是可替换后端。需要强调的是,session 记录的是逻辑事件;压缩、分块和文件布局只是 provider 的物理实现细节。
7. Tools:模型调用不是“直接执行函数”
packages/core/tools/src/index.ts 既定义 ToolDefinition,也提供 scoped registry 与执行管线。工具声明输入 JSON Schema、输出 schema、execute()、展示投影,以及可选超时与并发安全元数据;系统把原始结果规范化为可无损 JSON 表示的结果后才交给模型与 UI。

这个设计避免了权限、超时、审计和 UI 逻辑散落在每一个工具里:
pre-execute可 allow / deny / ask;没有可回答的 approval 时, ask变为拒绝;execute是 around hook,适合超时、重试和 metrics; post-execute可以过滤或补充结果; 最终 tools/result是不可变观察事件,UI 可按同一原始事件重放卡片。
文件编辑还会经过 fs/* 意图事件;shell、文件、web、subagent、workflow 都是各自能力 seam 的 Consumer。所谓 PTC(programmatic tool calling)模式则把直接可见工具压缩为 run_code,由模型生成程序再经同一工具管线调度子调用,而非绕过安全与日志。
8. 能力 seam:替换后端时,为什么上层不用改
项目把可替换功能设计为三角色:Service Definition、Service Provider、Consumer。一个“只有工具没有 provider”的功能不算完成的 seam。
ctx.llm | ||
ctx.fs | ||
ctx.sandbox | ||
这是“Everything is a plugin”的实际含义:不是所有代码都可随意动态加载,而是每个可演化能力都有接口、实现和消费方,依赖指向抽象定义而不是某个本地 provider。
9. Web、SDK 与 Python:同一日志,不同投影面
Web 模式的 host/client 分离很彻底:host/ 提供 API gateway、HTTP server 与静态资源;client/ 负责浏览器状态、slot 和各项 ui-* 插件。工具 UI 卡片不是工具随手写 DOM,而是由工具的 presentCall / presentResult 生成纯展示意图;因此实时流与 session 回放可以得到一致界面。
SDK 层通过 JSON-RPC 投影 agent loop;Python SDK 不复刻一套核心运行时,而是启动同版本打包的 dsh --profile sdk。这是一种很务实的取舍:协议对外稳定,Agent 语义只有一份实现,跨语言客户端只消费它。
10. 工程质量与安全:从“能跑”到“可维护”
根 package.json 展示出这是一套严谨的 monorepo:TypeScript strict、Vitest、构建双面(host/client)、生成的工具/配置/模块图谱、文档链接与 Mermaid 校验、依赖闭包检查、发布产物检查。特别值得注意的实践包括:
所有 Node 应用必须经 dsh+ profile 启动,避免不可控的旁路入口;受支持 profile 通过 base bundle 统一挂载 sandbox、权限、approval 与凭据; 凭据优先从环境读取,再回退到受控配置;不会把托管凭据直接 materialize 到进程环境; telemetry 默认采用反馈触发的上传模式; 预发布阶段明确不承诺旧磁盘格式兼容, SESSION_FORMAT_VERSION仍为 0。
因此部署者应把它当作开发者预览期的可组合基础设施,而不是宣称完全稳定的最终端用户产品。
11. 推荐源码阅读路线
1. README.zh.md 与 docs/architecture.zh.md:建立 vocabulary。<br/>2. vendor/cordis/src/context.ts、registry.ts、events.ts:理解服务、effect 与 waterfall。<br/>3. packages/boot/app-boot/src/profile.ts 与 packages/bundle/base/cordis.patch.yml:看一个运行时怎样装配。<br/>4. packages/core/agent-loop/src/agent.ts:理解 inbox、turn、step 与请求构造。<br/>5. packages/core/session/src/index.ts:理解事件日志与历史投影。<br/>6. packages/core/tools/src/index.ts 以及 docs/tool-execution-pipeline.zh.md:理解安全执行与结果规范化。<br/>7. 选择一个纵切能力(例如 fs/、subagent/ 或 web/),分别追踪 definition、provider 与 tool consumer。
12. 总结
DeepSeek Harness 的真正创新点不在某个工具或某个模型适配器,而在它把 Agent runtime 的可变部分系统化为三个稳定支点:Cordis 插件组合、事件溯源会话、Definition–Provider–Consumer 能力 seam。profile/bundle 解决“如何组装”,agent loop 解决“如何持续工作”,session log 解决“如何可重建”,tools pipeline 解决“如何安全地作用于外部世界”。
如果要扩展它,最佳问题不是“该改哪个核心文件”,而是:“这项能力属于哪个 seam?应该新增哪条可记录的事实?要在哪个事件点插入?”——这正是该仓库源码结构试图强制引导开发者形成的思维方式。
本文依据
官方源码: deepseek-harness/官方架构说明: docs/architecture.zh.md生命周期与工具执行图: docs/agent-lifecycle.zh.md、docs/tool-execution-pipeline.zh.md包总览: packages/README.md