DeepSeek Harness 代码级分析:『一切皆插件』怎样落成可恢复 Agent 运行时
DeepSeek Harness 不是又一层“提示词 + 工具调用”包装。它更激进的选择,是把模型适配器、Agent 循环、工具、会话持久化、审批、沙箱、Web 界面,甚至运行时自我扩展都做成 Cordis 组件。官方把它概括为 Everything is a Plugin;真正值得分析的不是口号,而是当一切都可替换时,系统靠什么维持顺序、恢复、安全和可观测性。
本文固定在 2026 年 8 月 13 日主分支提交 47f9438,对应 dsh@0.1.0-rc.5。这是 Developer Preview,不应把当前接口当作稳定公共协议。
先给结论
DeepSeek Harness 最有价值的部分并非“支持很多模型或工具”,而是把 Agent 产品拆成三种可以独立替换、又有清晰生命周期的对象:
- Definition:某项能力的稳定语义,例如 session、tools、filesystem;
- Provider:能力的具体实现,例如 JSONL 持久化、worker code runtime;
- Consumer:只依赖 Definition,不直接绑死 Provider 的功能插件。
Cordis context 把三者在运行时连接起来;effect 记录插件安装的每个可逆动作,coeffect 声明插件启动前必须满足的依赖。于是“换一个实现”不再等于“重写 Agent loop”,而是卸载旧 Provider、恢复它的 effects,再装载新 Provider,让 consumer 按依赖变化重新进入生命周期。
但模块化没有消灭复杂度,只是重新分配了复杂度。该提交下 packages/ 内有 219 个 npm package;理解系统必须同时掌握插件树、能力 seam、session 事件契约和工具流水线。它更像一个 Agent 操作系统内核,而不是开箱即用的单体 SDK。
产品不是一棵依赖树,而是一组 profile
dsh-base 是所有产品 profile 的第一层,提供模型适配器、工具、持久化、沙箱/审批、设置、凭据和 telemetry;dsh-web-app 再加浏览器应用;dsh-headless 加一次性无服务 runner。用户最终启动的不是固定二进制能力表,而是若干层配置 patch 叠出的插件组合。
这个分层有两个现实收益。第一,同一个 loop 可以跑在 Web、headless 或 SDK host 内,不需要把 GUI 条件分支塞进核心。第二,安全策略不是散落在 bash、fs、MCP 各工具中的 if,而能挂在 capability event 与统一工具 pipeline 上。
Agent loop:事件日志在前,模型请求在后
packages/core/agent-loop/src/agent.ts 的主链路可归纳为:
inbox 取 user/followup/steer/inject
→ 写入 turn/start 与用户消息事件
→ 从 session log 派生模型可见 messages
→ 调用模型、流式记录 assistant/chunk
→ 解析工具调用
→ 执行工具并按模型顺序持久化结果
→ 有后续步骤则重建请求,否则 turn/end
这里最重要的不变量是:模型可见即已进入日志。Session log 是 source of truth,deriveMessages() 只是从事件流投影模型历史;fork、resume、transcript、telemetry、持久化与 UI 重放都读同一条流。它避免了常见的双状态问题:屏幕看见一套消息、重启恢复另一套、下一轮模型又吃第三套。
Steering 也不是偷偷修改当前 prompt。新的 steer/followup 进入 inbox,并以事件形式加入下一次安全重建点;工具执行结果同样先形成 durable event,再进入后续模型上下文。Agent 的“连续性”因此不是一条长期存活的 Promise,而是一段可重放、可检查的状态机历史。
为什么工具可并发,但结果仍确定
模型可能在一条响应中产生多个工具调用。tool-calls.ts 使用有界滚动池并行执行允许重叠的调用,同时识别 barrier;但提交到 session 的结果仍保持模型声明顺序。后面的工具即使先完成,也不会越过前面的 durable result。
这兼顾了吞吐与可重放性:真实开始/完成时间可交错,模型历史的因果次序却稳定。若 turn 被中止,运行时还会为没有正常结果的调用合成明确的 abort 结果,避免 session 留下“有 call、无 result”的破损配对。
工具不是函数表,而是策略流水线
Harness 的工具路径比 name → handler(args) 多得多。官方的 tool-execution-pipeline.md 描述了一条跨工具族的统一链路:
定义解析 / schema 校验
→ pre-policy waterfall
→ approval
→ monotonic guards
→ around-dispatch(超时等)
→ 工具执行
→ post-policy
→ lossless JSON snapshot
→ finalizeContent
→ tools/result 观察与持久化
“monotonic guard”意味着后面的插件可以进一步拒绝,却不能把前面已经拒绝的动作重新放行。审批先处理需要询问的分支,文件系统的“先读后改”则位于 tool-fs 下层,通过 fs/* 事件守卫具体目标版本。这比在 prompt 中提醒模型“改文件前先读”可靠,因为最终写入仍受版本与观测状态约束。
结果会先做无损 JSON 快照,再进入只允许改呈现内容的 finalize 阶段。这样 session、telemetry 与 UI 观察的是不可变规范结果,插件不能在持久化之后悄悄改变事实。
Code Mode 没有绕过安全管线
Code Mode 把许多工具暴露为 tools.<name>(args) 异步 binding,让模型生成一段程序来编排调用,减少多轮结构化输出的 token 和延迟。风险是“代码解释器成为万能后门”。Harness 的设计是让 run_code 内每个子调用重新进入完整工具 pipeline,继承父 token、审批与 guard,并分别记录 tool/code-dispatch。程序能并发编排,却不能因为套了一层代码就跳过策略。
当前实现仍有工程接缝:Code Mode 的选择存在临时 process-wide 环境变量路径;同一进程混跑不同配置时,这类全局状态应继续收敛为 session/profile scoped service。
Session 持久化:append-only 不等于“随便追加”
基础 profile 采用 JSONL session persistence。协调器不只是把事件 appendFile:它要处理 live session 的权威内存前缀、落盘队列、冷启动检查、破尾修复、HMR adoption 和仍在进行的 open turn。
对 live id,读取先快照内存日志并等待对应前缀 durable;若 turn 尚未闭合,就拒绝伪造“已中断”的结尾。对 cold id,才允许检查并修复 torn tail 或 interrupted turn。HMR 新 Provider 接管时会验证已存前缀与 live history 一致,再补齐内存领先的 suffix。
边界同样明确:存储格式仍是 v0,只兼容少数已知旧记录形状,并不承诺通用迁移。现在适合快速演进,不适合把日志格式当成第三方长期集成协议。
Cordis 如何兑现论文中的时空可组合性
配套论文《A Programming Paradigm for Spatiotemporal Composability》提出两种局部机制:
- revertible effect:组件每次改变 context 都给出 inverse,卸载时按 LIFO 恢复;
- reactive coeffect:组件声明所需能力,provider 出现、退出或替换时重新判定生命周期。
Harness 里的对应不是概念类比,而是直接使用同一运行时。工具注册、命令、模型路由、HTTP route、设置项和事件监听都绑定到插件 fiber 的 effect;插件卸载时注册自动消失。consumer 通过 inject 等依赖声明等待 capability provider,依赖尚未满足时不进入 Active。
这解释了为什么 Harness 敢让持久化、模型、tool、UI 都成为插件:可替换性不仅要求“有 interface”,还要求旧实现退出时把注册、listener、route、后台任务和状态观察者全部撤干净,并让依赖者在 provider 真正消失前完成 teardown。Cordis 给的正是这段生命周期协议。
不过论文能力与产品能力之间仍有距离。仓库有 HMR 基础设施和大量 HMR 安全测试,但 Web/headless 的共享产品组合中部分 HMR 目前被显式关闭;能可逆注册不代表每个最终 bundle 已经支持无缝在线换芯。
自修改:能动态长出工具,但不是安全容器
可选 tool-cordis 提供 cordis_define / inspect / run / stop / undefine。Agent 可以定义一段动态 Cordis package、启动它并注册新的模型可见工具;停止或 undefine 会触发生命周期恢复。这正是论文结尾所说的 self-evolving agent harness 原型。
必须同时看到三条限制:
- 动态 package 只存在于当前进程内,重启即消失,不会自动晋升为正式源码或持久插件;
- Node
vm只是隔离执行上下文,官方明确不把它当安全边界;动态代码应按 bash 同等级别授权; - 该工具集不在默认产品树中,需要显式启用。
因此合理闭环应是“运行时实验 → 记录行为与测试 → 生成持久源码 → 人工/策略审查 → 纳入 profile”,而不是让进程内临时代码无门槛变成永久供应链依赖。
安全模型:默认较稳健,但边界不能误读
基础配置把 telemetry 默认关闭,全文搜索默认不自动打开;工作区预设组合 workspace-write + ask,全权限预设则是显式的 danger-full-access。Web search 可启用,而通用 fetch 因 SSRF 风险默认不进入产品能力。这些默认值说明项目在“功能存在”与“默认开放”之间做了区分。
文件沙箱支持 read-only、workspace-write 和 danger-full-access,并尽量 fail closed;Linux 依赖 bwrap/Landlock,macOS 使用 Seatbelt,Windows 有 ACL 路径与明确的能力缺口。但它治理的是文件系统 effect,不是完整进程安全边界:网络、进程可见性、运行时对象和凭据还要由各自 Provider、审批与宿主隔离共同约束。把 workspace-write 理解成“整个 Agent 已被安全容器化”会高估保证。
这套架构真正难的地方
| 维度 | 强项 | 当前代价或风险 |
|---|---|---|
| 可替换性 | loop、host、model、tool、persistence 都经 seam 解耦 | 219 个 package 带来导航和版本协调成本 |
| 恢复 | effect-bound 注册与 LIFO dispose,HMR 测试覆盖广 | inverse 正确性最终仍由原子插件作者负责 |
| 一致性 | session log 单一事实源,结果按模型顺序提交 | v0 存储格式尚无通用迁移承诺 |
| 安全 | 审批、单调 guard、沙箱、先读后改统一进流水线 | 文件沙箱不覆盖网络/进程;Node vm 不是安全边界 |
| 自演化 | 动态 package 能注册新工具并可撤回 | 只在内存存在,持久晋升和治理闭环仍待产品化 |
| HMR | Cordis 原语天然适合替换,关键子系统有 adoption 测试 | 部分产品 bundle 暂时关闭共享 HMR |
最大的工程挑战会是“跨插件不变量”。插件越多,越不能靠每个 consumer 理解所有 provider。Harness 已经用 capability seam、durable event 和 invariant service 把一部分规则上收;下一步需要持续把临时全局开关、格式兼容、网络权限和动态插件晋升也变成可声明、可测试的正式 seam。
适合怎样采用
如果目标只是做一个固定三工具的聊天 Agent,直接采用这套 219-package 架构很可能过重。它适合的是另一类系统:多宿主、多模型、多策略、长会话、频繁增删工具,并且需要在不重启整个进程的情况下替换能力。
稳妥的落地顺序是:
- 先把 session log 和工具 pipeline 当作不可绕过的内核;
- 只为确实需要替换的边界建立 Definition/Provider/Consumer;
- 用 effect 绑定所有注册型资源,并为卸载、失败、HMR 写真实路径测试;
- 把 sandbox、approval、network 与 credential 分开建模,逐项验证;
- 最后才开启动态 Cordis 自修改,并要求实验产物经过持久化与审查闭环。
DeepSeek Harness 的意义,不是证明“插件越多越先进”,而是展示了 Agent 系统可以拥有一套比主循环更底层的组合内核:模型输出可以变化,工具和 Provider 可以变化,Host 也可以变化;只要事件事实、能力依赖、effect 恢复和策略管线保持稳定,系统就仍然可理解、可重放、可替换。
资料与快照
本文所有源码结论均以提交 47f943859bef60e4160492346772ded9b24f765a 为准;Developer Preview 后续很可能快速变化,版本号、默认 profile 和临时接缝应以新提交重新核对。