DeepSeek Harness 源码深度解析:一个“一切皆插件“的 Agent 运行时是如何搭建的

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。发行版自带 webheadless 两个模板。
  • 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)、codeminimalcordis,每个 preset 就是一份 agent.cordis.yml 组合文件。

preset 文件的开头注释写透了其中的关键机制——isolate realm(隔离域)

preset 里的服务行必须放在带 isolate realm 的 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 的对象。 最简形态就是一个带 injectapply(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.toolsctx.llmctx.sessionsctx.systemPrompt……其他插件通过 key 查找服务,而非 import 具体实现。这一条是"可替换性"的根源:消费方编译期只依赖接口,运行期才解析实现。

3. 用 inject 声明依赖,让加载顺序自己涌现。 插件声明需要什么服务,就等它就绪后再启动。整个系统没有任何手写的"启动编排脚本"——219 个包的加载顺序是依赖关系图的拓扑序,不是人肉维护的清单。

4. 类型化事件是插件间通信的主干道。 事件名通过 TypeScript 声明合并(declaration merging)注册,分发有且仅有四种模式,且每种模式是事件公开契约的一部分:

模式是否 await语义典型用途
emit监听器按序观察,无返回值状态广播(agent/status
waterfall环绕中间件,可改写/短路拦截(agent/pre-steptools/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 scopevendor/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-loopReactLoopAgentpackages/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 中,携带结构化结束原因)

几个值得圈点的源码细节:

  1. max-tokens 是粘性的:一旦某一步撞上输出上限,后续正常完成的步骤不得把轮次结果"降级"回 completed——轮次结局是对用户体验的诚实记录。
  2. 每个失败都是结构化的LlmError 保留全部事实,其他异常统一压成 errorChain 文本 + UNKNOWN 码。turn/end 的 reason 因此永远可机器消费。
  3. 被拒绝/被清空的输入也留痕:首次认领被拒或被改写为空,轮次照样以"零步骤"持久化关闭——日志记录这次尝试,审计无盲区。
  4. 错误恢复是瀑布式的agent/request-error 让插件链决定重试策略;上下文溢出时 compaction 插件在此接管(先工具结果剪枝,再摘要,成功才开启全新重试轮次)。

4.3 工具调度器:屏障 + 有界滚动池

tool-calls.ts(约 290 行)实现了一步之内多个工具调用的调度,设计目标写在了文件头注释里:分派可以重叠,但策略、结果和结果上下文必须保持模型顺序(model-ordered)

调度规则:

  • 每个调用按 ctx.tools.executionMode() 实时分类为 exclusiveparallel
  • 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.approvalpackages/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-bashtool-terminallsp 工具

“添加一项能力 = 把三者一并设计”。seam 是"换一个提供方就改变整个产品"的原因,文档给了一个漂亮的例证:文件系统提供方与进程提供方共享同一个执行世界——把它们指向远程沙箱,Bash、PTY、LSP 就一并搬过去了,不需要任何提供方专用的 fork。

看两个 seam 的具体形态:

Subagent seampackages/subagent/)是"多提供方注册表"形态:ctx.subagents 按名称注册多个提供方,官方就带了六个——spawn-in-process(新建子进程 agent)、fork-in-process(fork 当前会话)、acpcodexclaude-code(把轮次委派给别家产品!)、dsh-sdk。提供方用静态能力描述符(SubagentCapabilities: outputSchema / depthLimit / toolFilter / persona)公布自己支持什么,请求了不支持的能力会在启动前被明确拒绝(UNSUPPORTED_CAPABILITY)——“大声失败,绝不静默降级”

Sandbox seampackages/sandbox/)则是"单提供方"形态:ctx.sandbox 只做一件事——把子进程 argv 包进文件效果策略。模式仅三种:read-onlyworkspace-writedanger-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: replaceuser/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 的一个可替换注册项。

这条路线的收益:

  1. 替换粒度无限细。换模型、换工具集、换执行环境(本地 → E2B 远程沙箱)、换循环本身,都不需要等官方发版,甚至不需要重启进程(HMR + 可逆注册)。
  2. 生态潜力大。插件就是 npm 包 + 一份 cordis.patch.yml,社区已有数千个 deepseek-harness topic 的仓库在生长。
  3. 可审计性。事件溯源 + 不变量 + fail-loud,让它天然适合对"agent 到底干了什么"敏感的场景。

代价也同样真实:

  1. 抽象税。每个能力都要走"接口 → 注入 → 事件"的间接层,热路径上是一次次 waterfall 分发;46 万行代码里有相当比例在为"可替换性"本身付费。
  2. 认知门槛。要改行为,先得理解 Cordis 的分发模式、realm/scope、layer 叠加规则——"一切皆插件"的自由度是以学习曲线换的。
  3. 预览期的不稳定性。官方明言会有破坏兼容性的变更,插件 API 还在快速演化。

九、结语

读完 DeepSeek Harness 的源码,最强烈的感受是:这不是一个"带有插件系统的 agent 框架",而是一套插件协议恰好长成了 agent 框架的样子。它的几乎所有设计——事件溯源的会话日志、waterfall 拦截点、三角色 seam、可逆注册、fail-loud 哲学——都在服务同一个目标:让"模型之外的一切"成为可组合、可替换、可审计的零件。

如果 Model + Harness = Agent 这个公式成立,那么 DeepSeek 开源的其实是等号右边那一半的完整工程答案。它是否会成为"执行层标准"尚需时间验证,但就架构的彻底性和工程的纪律性而言,这份源码本身就是 2026 年 agent 工程领域最值得细读的文本之一。

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包

打赏作者

AI砖家

各位大佬,可怜可怜小弟

¥1 ¥2 ¥4 ¥6 ¥10 ¥20
扫码支付:¥1
获取中
扫码支付

您的余额不足,请更换扫码支付或充值

打赏作者

实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值