DeepSeek Harness 源码深度解析:一个"一切皆插件"的 Agent 运行时是如何搭建的
本文基于对
deepseek-ai/deepseek-harness仓库(开发者预览版,MIT 协议)源码与架构文档的逐层阅读写成,从架构与设计两条线拆解这个框架:它为什么长这样、每一层在源码里对应什么、以及这些设计决策背后的取舍。
一、先弄清定位:Harness 不是模型,是模型的"执行躯壳"
DeepSeek Harness(命令行名 dsh)是 DeepSeek 于 2026 年 8 月开源的 agent harness(智能体执行框架)[1][2]。官方用一个公式概括它的定位:
Model + Harness = Agent
模型负责"想"——推理、规划、生成;Harness 负责除此之外的一切——读写文件、执行命令、调用工具、管理上下文、审批权限、错误重试、结果闭环。README 对自身的定义只有一句话:“一切皆插件”(Everything is a Plugin),整个系统由 Cordis 插件框架驱动,其设计理念来自论文 A Programming Paradigm for Spatiotemporal Composability[3][4]。
这个定位直接决定了它的架构形态:既然 Harness 是"模型之外的所有工程",而"所有工程"又极其发散(文件系统、Shell、终端、LSP、网页检索、子代理、压缩、审批、持久化……),那么唯一可持续的组织方式就是把发散全部推给插件,让核心收敛到最小。
先给一组仓库的量化印象(基于 master 分支快照统计):
| 指标 | 规模 |
|---|---|
| workspace 包数量 | 约 219 个(packages/ 下 40+ 个功能组) |
| TypeScript 源码 | 约 2100 个文件、46 万行 |
| 测试文件 | 约 650 个 spec/test 文件 |
| 构建/包管理 | pnpm workspace + tsdown,Node ≥ 22.19 |
| vendor 的框架层 | Cordis 全家桶以源码方式内嵌,重命名至 @deepseek-ai scope |
46 万行代码里没有一行是"特权内核"——这是理解全文的钥匙。
二、全局架构:一棵由配置叠加出来的插件树
2.1 运行时 = 插件树
一个运行中的 dsh 进程,本质上是一棵 Cordis 插件树。这棵树的形状不是硬编码的,而是由若干配置层(layer)按序叠加出来的:
┌─────────────────────────────────────────────┐
│ --patch 命令行 overlay(最上层,最后应用) │
├─────────────────────────────────────────────┤
│ Harness home 级 cordis.patch.yml │
├─────────────────────────────────────────────┤
│ profile 自己的 cordis.patch.yml │
├─────────────────────────────────────────────┤
│ profile 声明的 bundles(按序) │
│ └─ dsh-base(每个 profile 的第一层) │
│ └─ dsh-web-app / dsh-headless … │
└─────────────────────────────────────────────┘
这里有两个核心概念,源码里都落实为 package.json 上的一个 dsh 字段:
- Profile(档案):存放在 Harness home(
$DSH_HOME或~/.dsh)下的一个具名目录,声明自己叠放哪些 bundle、保存用户安装的树外插件、以及用户自己的cordis.patch.yml。发行版自带web和headless两个模板。 - Bundle(组合包):一个 npm 包,通过
"dsh": { "bundle": { "patch": "./cordis.patch.yml" } }声明自己携带一份 patch 文件。它是"Cordis 配置项 + 挂载代码"的分发格式。
patch 的语义非常克制:按 id 定位某行,整体替换其 config,或插入新行——不合并、不寻址深层字段。这个"整条替换"的约束是刻意的:它保证任何一行配置最多只被"一个 bundle 层 + 用户层"写过,排查配置来源时不会出现三层叠加的鬼故事。
想看自己机器实际启动的树?一行命令:
dsh --profile web --dump-config
它打印出的每一个条目,都可以被你写的 patch 替换——包括 agent loop 本身。
2.2 组装机制的源码落点
这套组装逻辑集中在 packages/boot/app-boot(@deepseek-ai/dsh-app-boot)。它是所有应用入口(dsh CLI、ACP demo 等)共享的启动胶水,核心导出包括:
boot():创建 root context → 安装 Loader → 挂载 include 树 → 断言所有条目加载并激活成功;composeEntries():用 include 自己的applyEntryPatches算法在空条目列表上叠加各 patch 层——保证"dump 出来的配置"和"实际启动的配置"永不分叉;installFailLoud():把启动期或运行期的未处理拒绝统一变成一行带标签的 stderr +exit(1),并给持有终端的插件留出清理窗口(恢复 raw mode、bracketed paste 等),绝不在半成品状态上吊死。
dsh-base bundle 的 patch 文件(packages/bundle/base/cordis.patch.yml)是每个 profile 的地基:模型适配器、工具注册表、持久化、沙箱与审批策略、设置、凭据、遥测全部在这里以 - insert: 形式一次性插入。其注释里写明了一条重要的组织纪律——“行顺序不携带任何加载语义”,激活顺序完全由服务可用性驱动(详见下文的 inject)。
2.3 Agent preset:给单个会话换能力
profile/bundle 解决的是"进程级"组装,而 DSH 还支持会话级组装:agent preset。apps/cli/config/agent-presets/ 下自带四个 preset——standard(完整编码 agent)、code、minimal、cordis,每个 preset 就是一份 agent.cordis.yml 组合文件。
preset 文件的开头注释写透了其中的关键机制——isolate realm(隔离域):
preset 里的服务行必须放在带
isolaterealm 的 group 里。没有它,服务会发布到 root realm 成为进程全局——另一个 preset 发布同名服务就会冲突,host 侧的读取方还会把某个 preset 的实例错配给所有会话。
也就是说:同名服务在不同 realm 下可以有各自的实例,会话通过 scope 父链join到对应 preset 的 realm,于是"这个会话用全套工具、那个会话用极简工具"成为纯配置问题。这是 Cordis"时空可组合性"论文思想在产品里的直接投影。
三、基石:Cordis 插件框架
Cordis 是 DSH 以 vendor 方式内嵌的底层框架(上游 cordiverse/cordis,4.0.0-rc.7,本地有硬化修改)。整个 DSH 的插件哲学可以压缩成它的五个核心概念。
3.1 五个核心概念
1. 插件是实现 Service 的对象。 最简形态就是一个带 inject 和 apply(ctx) 的对象:
export const name = 'my-plugin'
export const inject = ['tools'] // 声明依赖:等 tools 就绪再启动
export function apply(ctx) {
ctx.tools.register(defineTool({ // 向共享上下文贡献一个工具
name: 'greet',
// ...
async execute(args) { return 'Hello, ' + args.name + '!' },
}))
}
2. 上下文(Context)是服务的容器。 一个服务占据一个稳定的 ctx.<key>——ctx.tools、ctx.llm、ctx.sessions、ctx.systemPrompt……其他插件通过 key 查找服务,而非 import 具体实现。这一条是"可替换性"的根源:消费方编译期只依赖接口,运行期才解析实现。
3. 用 inject 声明依赖,让加载顺序自己涌现。 插件声明需要什么服务,就等它就绪后再启动。整个系统没有任何手写的"启动编排脚本"——219 个包的加载顺序是依赖关系图的拓扑序,不是人肉维护的清单。
4. 类型化事件是插件间通信的主干道。 事件名通过 TypeScript 声明合并(declaration merging)注册,分发有且仅有四种模式,且每种模式是事件公开契约的一部分:
| 模式 | 是否 await | 语义 | 典型用途 |
|---|---|---|---|
emit | 否 | 监听器按序观察,无返回值 | 状态广播(agent/status) |
waterfall | 否 | 环绕中间件,可改写/短路 | 拦截(agent/pre-step、tools/pre-execute) |
parallel | 是 | 并行扇出 | 多方消费同一事实 |
serial | 是 | 按序执行,有返回值 | 终末检查点(agent/turn-stopping) |
5. 注册是可逆的副作用。 工具、提示词片段、适配器、监听器全部通过 ctx.effect() / ctx.on() 安装,插件卸载时自动撤销。这就是 README 那句"不存在需要打补丁的特权内核"的技术含义:扩展 dsh 的方式是把插件挂到其他插件旁边,而不是改别人的代码;热重载(vendor 了 cordis-plugin-hmr)和用户插件的装卸都建立在这个可逆性之上。
3.2 Waterfall:贯穿全系统的拦截原语
四种模式里最重要的是 waterfall,它是 DSH 所有"策略注入点"的统一形态。语义类似 Koa/洋葱模型:监听器收到 (...args, next),调用 next() 委托给下游并把下游结果包一层返回;不调 next() 直接返回则短路。
架构文档里有一条明确的设计约定:对于单决策事件,短路是设计意图——拥有决策权的策略监听器(比如审批插件)可以直接返回结果;而只做标注或观察的监听器必须委托。这条约定让"谁有最终决定权"在协议层面就是清晰的。
3.3 Vendor 策略:把框架层变成自己拥有的代码
值得单独说的是 vendor 目录的做法。DSH 没有通过 npm 依赖 Cordis,而是把 Cordis 及其基础库(cosmokit、schemastery、loader、include、group、timer、hmr、logger-console)以源码形式拷入 monorepo,并重命名到 @deepseek-ai scope。vendor/README.md 给出了理由:harness 要完全拥有自己的框架层——可审计、可打补丁、可锁定版本。
而且这份拷贝不是死快照,本地修改日志里记录了实质性的硬化,例如 cordis/src/fiber.ts 的生命周期加固:关闭了三个重入式销毁缺口——effect 的 owner 包装在 setup 体运行前注册、卸载开始后拒绝新建 effect、子 fiber 在 internal/plugin 发布前先拿到父级 disposer……这类修改说明 DeepSeek 团队是在逐行审计并接管框架的并发与销毁语义,而不是简单地"用"一个第三方框架。
四、核心主干:事件溯源的会话 + 可替换的 Agent Loop
packages/core/ 是产品 API 的脊柱,一个轮次恰好流经它的六个包:
agent-loop ──驱动──▶ session(仅追加日志,唯一真源)
│ ▲
├─▶ system-prompt(组装提示词 + 工具 schema)
├─▶ llm(流式获取模型响应)
└─▶ tools(分发工具调用,结果回写日志)
4.1 Session:一切皆为仅追加事件
packages/core/session 实现了一个事件溯源(Event Sourcing)模型:Session 是一份由类型化 SessionEvent 组成的仅追加日志,是 agent 完整交互历史的唯一真源。模型看到的历史从不单独存储,而是由 deriveMessages() 从日志投影出来;回放、fork、恢复、transcript、遥测、持久化,全部派生自同一条事件流。
事件词汇(SessionEventMap)的核心成员:
| 事件 | 含义 |
|---|---|
turn/start / turn/end | 轮次边界,turn/end 携带结构化的 TurnEndReason(completed / max-tokens / aborted / error / blocked) |
step/start / step/end | 步骤边界:一次模型请求 + 它引发的工具执行 |
user/message | 进入模型的用户消息(人类输入、inject() 注入的上下文、目标续跑,靠 source 区分) |
assistant/chunk | 原始流式 chunk——token 级回放保真度 |
assistant/message | 组装完成的助手消息,带 usage 和 sourceEventSeqs 精确指回它的 chunks |
tool/call / tool/result | 工具调用与结果(结果事件用 sourceEventSeqs 指回调用事件) |
request/header / request/context | 请求配置与上下文的锚点(initial / resume / change) |
SessionEventMap 可以通过声明合并扩展——compaction 插件就合并进了 compaction/start|summary|end 三个事件类型。这意味着事件词汇本身也是插件化的。
这套设计里有一条被运行时不变量强制执行的铁律,值得原文引用:
模型可见即已记录。 抵达模型请求的一切都必须能从日志重建,并由一项运行时不变量断言这一点。
packages/core/session/src/invariant.ts 与各包同名的 invariant 文件,配合 ctx.invariants 注册表,把这类架构级承诺变成了运行时会真实抛错的断言,而不是文档里的口号。新增一种模型可见输入的正确姿势只有一种:扩展 SessionEventMap 并从日志渲染——任何"绕过日志直接塞给模型"的写法都会被不变量当场抓获。
另外注意一个细节设计:surface(表面)与日志的分离。产生消息的事件带 surfaceOp(append / replace),compaction 摘要落地为一条 surfaceOp: { op: 'replace', start, end } 的 user/message,而 compaction/* 事件本身只进日志不进 surface。原始历史永久保留在日志里(可审计、可回放),模型看到的 surface 则是一个可替换的投影——上下文压缩因此不需要"删除"任何历史。
4.2 Agent Loop:一个三态机驱动的轮次/步骤循环
默认的循环实现是 packages/core/agent-loop 的 ReactLoopAgent(packages/core/agent-loop/src/agent.ts,全文不到 500 行,是整个系统最值得精读的single文件)。
对外接口(Agent,定义在 packages/core/agent)极简:
followup(input):排队到下一轮次并唤醒驱动器——正常用户输入走这里;steer(input):插队到下一步骤并唤醒——中途引导(用户趁 agent 干活时插话);inject(input):进入下一步骤的收件箱但不唤醒——插件注入上下文(文件变更通知、AGENTS.md、skill 内容、定时提醒)走这里,等下一条唤醒消息顺带捎进去;cancel(cause)/whenIdle()/runMaintenance(job)。
三种输入路由区分得如此细致,是因为它们对应三种不同的实时性语义:追问要打断当前节奏、steering 要在下一步生效、注入只需最终一致性。
内部状态机只有三个相位(Phase 类型):
type Phase =
| { kind: 'idle'; lastTurn: number }
| { kind: 'maintenance'; abort: AbortController; ... } // 维护作业(如压缩)独占期
| { kind: 'running'; abort: AbortController; turn: number; step: number; ... }
wakeDriver() 是唤醒的唯一入口,其中的 latch 逻辑处理了一个棘手的并发问题:唤醒信号到达时驱动器正在维护期或已中止怎么办?答案是锁存 wakeRequested,等驱动器收敛(回到 idle)时重放——且被 dispose 的驱动器不锁存,因为拆除永远不该等待一个模型轮次。
轮次主循环(turn() 方法)的骨架,与官方时序图一一对应:
turn/start 落盘
loop:
认领 inbox(首轮认 next-turn,之后认 next-step)
ctx.systemPrompt.assemble() 组装提示词
agent/pre-step waterfall ── 插件可改写消息或直接拒绝
拒绝 → 轮次以 blocked 关闭(不消耗模型调用,但日志留痕)
step/start 落盘,user/message 逐条落盘
step():
agent/request waterfall ── 插件可改写请求配置(provider/model/effort…)
ctx.llm.prepareCall() 解析适配器与精确模型默认值
request/header 落盘(变更时)
llm/stream → assistant/chunk* 逐块落盘 → assistant/message 落盘
失败 → agent/request-error waterfall 决定重试还是保留原错误
有工具调用 → executeToolCalls()(见 4.3)
step/end 落盘
自然停止且 inbox 空 → agent/turn-stopping serial 终末检查点(插件可续命)
turn/end 落盘(finally 中,携带结构化结束原因)
几个值得圈点的源码细节:
- max-tokens 是粘性的:一旦某一步撞上输出上限,后续正常完成的步骤不得把轮次结果"降级"回 completed——轮次结局是对用户体验的诚实记录。
- 每个失败都是结构化的:
LlmError保留全部事实,其他异常统一压成errorChain文本 +UNKNOWN码。turn/end的 reason 因此永远可机器消费。 - 被拒绝/被清空的输入也留痕:首次认领被拒或被改写为空,轮次照样以"零步骤"持久化关闭——日志记录这次尝试,审计无盲区。
- 错误恢复是瀑布式的:
agent/request-error让插件链决定重试策略;上下文溢出时 compaction 插件在此接管(先工具结果剪枝,再摘要,成功才开启全新重试轮次)。
4.3 工具调度器:屏障 + 有界滚动池
tool-calls.ts(约 290 行)实现了一步之内多个工具调用的调度,设计目标写在了文件头注释里:分派可以重叠,但策略、结果和结果上下文必须保持模型顺序(model-ordered)。
调度规则:
- 每个调用按
ctx.tools.executionMode()实时分类为exclusive或parallel; - exclusive 调用构成屏障(barrier)——必须独占执行,前后排空;
- parallel 调用进入有界滚动池(上限
maxParallelToolCalls),但启动前重新分类——因为注册表可能在运行中被插件改动,原本并行的调用可能已被改为独占; - 结果提交用
commitReady()推进一个只跨越连续已就绪槽位的指针,严格按模型顺序落盘tool/result; - abort 语义完整覆盖:停止补货、排空已启动调用、为未启动的调用补记合成错误结果(
TOOL_ABORTED_BEFORE_DISPATCH),保证回放永远有效;而调度器自身故障则排空已分派调用、抛出首个错误,绝不伪造工具结果。
这段代码是"并发性能"与"可回放性"两个目标正面相撞的地方,DSH 的选择很清楚:执行可以并行抢时间,但落到唯一真源日志里的顺序必须是模型当初发出的顺序。
4.4 工具执行管线:五段式受保护流水线
单个工具调用穿过 packages/core/tools 的流水线时,要过五道关卡(对应 docs/tool-execution-pipeline.md 的流程图):
tools/pre-execute (waterfall) ← 钩子、权限、沙箱策略
↓ allow / ask→审批 / deny
单调守卫(monotonic guards) ← 只能 deny 或弃权,不能翻案;身份受保护
↓
tools/execute (waterfall) ← 环绕分派:超时、重试、指标
↓
工具 execute() 本体 ← fs/* 事件闸门(先读后编辑)、工具自有日志事件
↓
tools/post-execute (waterfall) ← 接受、阻断、替换、追加上下文
↓
finalizeContent + tools/result ← 内容不变式最终检查 → 冻结的权威结果
两处设计尤其值得说:
- 审批是 fail-closed 的。
ctx.approval(packages/interaction/user-approval)的结果集是封闭的'allowed-once' | 'rejected' | 'cancelled' | 'unavailable',除非明确拿到一次性许可,其余一律拒绝;应答者缺失、不负责、抛异常,全部归入unavailable——而unavailable也是拒绝。会话级策略只有ask/never两档,never确定性拒绝且不分发(CI/无人值守场景的严格姿态)。 - 结果要经过无损快照。注册表会对候选结果做 JSON 无损快照,快照失败先规范化为
isError,再由冻结的finalizeContent做最后一道"仅内容"的同步检查,最后tools/result广播的是不可变的权威结果。钩子因此可以横跨所有工具家族工作,而工具自己完全不耦合任何策略服务。
还有一个大招藏在 tools 包里:Code Mode。它注册一个保留的 run_code 工具,把其余工具的 schema 渲染成一份 TypeScript/Python SDK 注入提示词(tools:sdk 段落),模型通过写程序来编排工具调用,子调用携带父级 token 走同一条管线并记录 tool/code-dispatch。等于把"工具组合"从模型的 N 次往返压缩成一次代码执行——这对长链路任务的可观测性和 token 开销都是数量级的改善。
五、设计模式:能力 Seam 三角色
如果说 Cordis 回答了"代码怎么组织",那么能力 seam 回答的是"产品能力怎么可替换"。架构文档给出定义:一个 seam 由三种角色构成——
| 角色 | 职责 | 例子 |
|---|---|---|
| Service Definition | 声明接口 | dsh-shell 声明 ctx.shell 执行器接口 |
| Service Provider | 实现接口 | 本地进程树后端、bwrap/Landlock/Seatbelt 沙箱后端、E2B 远程后端 |
| Consumer | 消费接口(通常是面向模型的工具) | tool-bash、tool-terminal、lsp 工具 |
“添加一项能力 = 把三者一并设计”。seam 是"换一个提供方就改变整个产品"的原因,文档给了一个漂亮的例证:文件系统提供方与进程提供方共享同一个执行世界——把它们指向远程沙箱,Bash、PTY、LSP 就一并搬过去了,不需要任何提供方专用的 fork。
看两个 seam 的具体形态:
Subagent seam(packages/subagent/)是"多提供方注册表"形态:ctx.subagents 按名称注册多个提供方,官方就带了六个——spawn-in-process(新建子进程 agent)、fork-in-process(fork 当前会话)、acp、codex、claude-code(把轮次委派给别家产品!)、dsh-sdk。提供方用静态能力描述符(SubagentCapabilities: outputSchema / depthLimit / toolFilter / persona)公布自己支持什么,请求了不支持的能力会在启动前被明确拒绝(UNSUPPORTED_CAPABILITY)——“大声失败,绝不静默降级”。
Sandbox seam(packages/sandbox/)则是"单提供方"形态:ctx.sandbox 只做一件事——把子进程 argv 包进文件效果策略。模式仅三种:read-only、workspace-write、danger-full-access(第三种直接绕过 seam 裸 spawn)。后端覆盖 Linux bwrap/Landlock、macOS Seatbelt、Windows ACL 受限令牌,并且后端要如实报告执行完整度(full / partial)——较旧的 Landlock ABI 管不住全部文件效果时,必须如实上报,让要求绝对保证的消费方自行拒绝。安全语义上的诚实,被做成了类型系统的一部分。
同样的三角色结构贯穿全库:LLM(ctx.llm + deepseek/pi-ai/replay 适配器)、持久化(ctx.sessionPersistence + JSONL/SQLite 后端)、压缩(ctx.compaction + basic 后端 + compact 命令)、设置、凭据、存储、技能、工作流、网页检索……数下来有近二十个 seam,全部同构。学会一个,就会了所有。
六、值得单独说的几个子系统
6.1 LLM seam 与双适配器策略
packages/llm/llm 定义对话与流式词汇:Message / ContentBlock(text / reasoning / image / tool-call / tool-result 五种块,且块类型 map 可声明合并扩展)/ StreamChunk / 适配器契约。循环通过 ctx.llm.prepareCall(config) 解析路由,适配器按 provider 名注册,重复注册直接抛 DUPLICATE_ADAPTER。
有意思的是官方带了两条 DeepSeek 通路:llm-deepseek(直连官方 API,fetch + SSE,路由名刻意叫 deepseek-official)和 llm-pi-ai(库实现,目录名 deepseek)——一份组合可以同时挂两条路径做对比。适配器配置里还能看到产品事实:默认模型目录是 V4-Flash / V4-Pro,默认上下文窗口 1,000,000 token,支持 reasoningEffort: off|low|high|max。
6.2 Compaction:以锁与事件表达的上下文压缩
压缩不在循环主干里,而是一个标准 seam。它向 SessionEventMap 合并三个仅日志事件:compaction/start(取锁)→ compaction/summary(记录摘要、被遮蔽的 seq 集、token 数、生成它的那次 ctx.llm.stream() 调用)→ compaction/end(放锁)。锁括住整个操作,中途崩溃表现为"有 start 无 end"的可检测遗留锁,而不是一个谎称完成的 end。对 surface 的唯一修改是一条 surfaceOp: replace 的 user/message——历史原样保留,模型视角完成替换。触发侧挂在 agent/pre-step(主动抗压)和 agent/request-error(规范的上下文溢出恢复)两个瀑布上。
6.3 Extensions:agent 修改自己的运行时
packages/extensions/ 是整个仓库里最大胆的一组包:它把 Cordis 运行时本身暴露成模型工具——cordis_inspect_*(查看已加载插件与服务 API)、cordis_define / cordis_run / cordis_stop / cordis_undefine(定义并运行模型自己写的动态插件,再撤销)。host 半边的动态包跑在 node:vm 沙箱里,浏览器半边由 client runner 求值为活插件。
这是"一切皆插件"的逻辑终点:既然系统的每一部分都是插件,那么 agent 当然也可以通过写插件来扩展它自己。Skill 还只是提示词工程,而这个是运行时的自我改造。
6.4 Web 架构:前后端也是插件
Web GUI 拆成 host(packages/host/)与 client(packages/client/)两半。host/webserver 是一个刻意"无知"的 node:http 插件——它只提供具名路由注册表(exact 表 → 最长前缀 → 唯一回退席位)和 index.html 转换钩子,不理解任何 harness 概念;所有功能路由(/api 桥接、插件 bundle、HMR 事件流)都由别的插件注册。前后端之间的 RPC 走自研的 Typert:构建期从源码类型生成类型图与产物,运行期经 ctx.typert 注册表和 API gateway 调用——前后端共享同一份类型真源。
进程外集成同样齐备:packages/sdk 提供 JSON-RPC 协议与 TS 客户端,packages/acp 提供 ACP(Agent Client Protocol)服务器接入编辑器生态,packages/python/ 下还有 Python SDK。所有入口都是"一个薄的自执行组合 + 共享 boot 胶水"。
七、工程文化:从仓库组织看设计品味
源码之外,这个仓库的"元工程"同样值得一写,因为它们本身就是设计哲学的一部分:
- Agent Notes 制度:
.agents/notes/implemented/下按日期归档每一份设计决策(能力 seam、subagent 后端、自指工具集……),子系统文档直接链回对应笔记。决策的"为什么"和代码的"是什么"被制度性地绑在一起。 - 文档即代码的校验体系:子系统文档里的类型签名用 ```````ts type-equiv ````代码块粘贴,
verify-type-equiv提取器会与源码逐名比对,签名漂移直接 CI 失败;中英文文档通过"双语配对"机制(verify-translation-pairing+ 专用 git merge driver)保证同步。 - 生成的目录:事件的生产方/消费方映射、工具目录、配置目录、持久化事件目录全部由脚本从源码生成(
gen-doc-graphs等),文档不可能说谎。 - 不变量即代码:几乎每个包都有
invariant.ts,架构级承诺(如"模型可见即已记录")是运行时会抛错的断言。 - Fail loud 无处不在:启动审计(
assertEntriesLoaded/Activated)、installFailLoud、能力不支持的UNSUPPORTED_CAPABILITY、审批的 fail-closed、沙箱的partial诚实上报——整个系统对"静默降级"有种近乎偏执的敌意。
八、评价:Agent 时代的"安卓",以及它的代价
把 DSH 放进坐标系里看会更清楚。对比 Claude Code / Codex 这类"精装单体"路线(Rust/TS 写的封闭核心 + 外围 MCP/hooks/skill 挂件),DSH 的选择是另一个极端:没有核心,只有协议。模型适配器是插件、工具是插件、持久化是插件、审批是插件,连 agent loop 这个"心脏"都只是 ctx.agentLoop 的一个可替换注册项。
这条路线的收益:
- 替换粒度无限细。换模型、换工具集、换执行环境(本地 → E2B 远程沙箱)、换循环本身,都不需要等官方发版,甚至不需要重启进程(HMR + 可逆注册)。
- 生态潜力大。插件就是 npm 包 + 一份
cordis.patch.yml,社区已有数千个deepseek-harnesstopic 的仓库在生长。 - 可审计性。事件溯源 + 不变量 + fail-loud,让它天然适合对"agent 到底干了什么"敏感的场景。
代价也同样真实:
- 抽象税。每个能力都要走"接口 → 注入 → 事件"的间接层,热路径上是一次次 waterfall 分发;46 万行代码里有相当比例在为"可替换性"本身付费。
- 认知门槛。要改行为,先得理解 Cordis 的分发模式、realm/scope、layer 叠加规则——"一切皆插件"的自由度是以学习曲线换的。
- 预览期的不稳定性。官方明言会有破坏兼容性的变更,插件 API 还在快速演化。
九、结语
读完 DeepSeek Harness 的源码,最强烈的感受是:这不是一个"带有插件系统的 agent 框架",而是一套插件协议恰好长成了 agent 框架的样子。它的几乎所有设计——事件溯源的会话日志、waterfall 拦截点、三角色 seam、可逆注册、fail-loud 哲学——都在服务同一个目标:让"模型之外的一切"成为可组合、可替换、可审计的零件。
如果 Model + Harness = Agent 这个公式成立,那么 DeepSeek 开源的其实是等号右边那一半的完整工程答案。它是否会成为"执行层标准"尚需时间验证,但就架构的彻底性和工程的纪律性而言,这份源码本身就是 2026 年 agent 工程领域最值得细读的文本之一。

294

被折叠的 条评论
为什么被折叠?



