DeepSeek Harness 精读:一个「一切皆插件」的 Agent 运行时

精读对象:github.com/deepseek-ai/deepseek-harness(dsh),2026-08-26 克隆自 master 分支
版本:0.1.x 开发者预览(rc 阶段)· MIT 许可
精读重点:轨迹日志(事件溯源)插件系统(Cordis / seam) 两套核心设计


0. 一句话速览

dsh 不是又一个 AI 编程工具,而是一个”Agent 外壳”的插件平台。 它的核心命题是:Agent = 模型 + Harness——模型只负责推理,Harness 负责把模型接进文件、命令、工具、会话、权限与界面。而它赌的架构是:Harness 本身没有特权内核,一切能力都是可插拔的插件,包括模型适配器、工具、沙箱、会话日志,甚至 Agent 循环本身。

两套最值得读的设计:

设计 一句话概括 杀手锏
轨迹日志 会话 = 一条仅追加的事件日志,是唯一真源 “模型可见即已记录”——模型看到的一切都能从日志重建
插件系统 Cordis 微内核只管装配,能力按 seam(缝)三件套组织 换掉一个提供方就能改变整个产品,不用改源码

1. 轨迹日志:事件溯源式会话(精读重点 #1)

1.1 设计哲学:模型可见即已记录

这是 dsh 的第一条铁律,由运行时不变量强制断言:

抵达模型请求的一切都必须能从日志重建。 新增任何一项模型可见输入,就必须新增一个会话事件类型。

也就是说:日志不是”聊天记录的副本”,而是唯一真源。LLM 的消息历史是从日志派生出来的(deriveMessages()),从不单独存储。回放 = 用同一组事件重新派生一遍。

会话事件溯源数据流

派生的好处:历史永远不会和日志不一致——不存在”存了一份消息数组、又改了日志”的双写问题。所有下游消费方(UI 转录、遥测、会话标题、搜索索引)都从同一份事件流派生,各取所需。

1.2 事件词汇表:日志里有什么

SessionEventMap 定义了事件类型,插件可以通过 TypeScript 声明合并扩展(见 §2.4)。核心事件如下:

事件 作用 说明
turn/start / turn/end 轮次边界 轮次 = 一次排空已接纳输入的过程;turn/end 带结束原因
step/start / step/end 步骤边界 步骤 = 一次模型请求 + 它触发的工具执行
user/message 用户输入 普通提示词、注入上下文(agent.inject())、目标续行共用一个类型,靠 source 区分
assistant/chunk 原始流分片 token 级回放保真——UI 能逐字重现输出过程
assistant/message 组装后的模型回复 携带 usage(token 记账随消息走);中断的回复标记 interrupted
tool/call 工具调用 保留模型产出的原始 JSON 参数(未解析),callId 与结果配对
tool/result 工具结果 可选携带 error 和工具私有 meta(如文件 diff 卡片)
todo/write 待办全量快照 整表替换、last-write-wins,故意不做成带 id 的条目
request/header 请求完整信封 调用配置 + 系统提示词 + 工具 schema 全量快照,每次变化记一份
request/context 路由元数据 提供方/模型/上下文窗口,容量变化时记录
session/end-seed 种子边界 标记”哪些事件是恢复/fork 继承的、哪些是本进程新写的”

几个细节非常见功力:

  • seq 必须连续seq = log.length),因此不能从规范日志里过滤分片——连 assistant/chunk 都必须无损保存。
  • 所有 data 必须是可无损 JSON 序列化的Session.append() 在写入前做一次递归校验(BigInt、函数、循环引用、Map/Set 一律拒绝),坏事件在源头就失败,绝不让错误进入日志。这对”日志即真源”是底线保障。
  • 热路径不阻塞 I/O:追加是同步进内存,持久化插件异步批量落盘。

1.3 Surface 机制:如何从日志”长”出模型历史

不是所有事件都投影成消息。只有 3 种”产生消息”的事件(user/messageassistant/messagetool/result)携带 surfaceOp,声明自己如何加入有序的派生 surface:

  • append:普通尾部追加。
  • replace遮蔽从 start 到 end 的一段旧节点,在原位插入新事件。

replace压缩(compaction) 的落点:上下文太长时,压缩插件把一段旧历史折叠成一条摘要,用 replace 遮蔽旧节点——日志本身一个字不删,只是”模型视野”里那一段被摘要替代了。这就是”日志永远可审计、只被遮蔽、不被删除”。

每个 surface 事件还携带 sourceEventSeqs(引用了哪些早期事件),比如 assistant/message 会引用构成它的 assistant/chunk 序号。压缩时被遮蔽的节点也必须在 sourceEventSeqs 里列全——血缘可追溯

1.4 持久化与崩溃恢复

持久化本身是一个 seam(见 §2.3):ctx.sessions 只做内存事件存储,持久化由插件订阅 session/event 完成。

  • flush 检查点session/flush 是显式耐久屏障;日常写入走异步批量缓冲(固定时间窗,窗口内事件不重置截止时间)。
  • 两个可互换后端:JSONL(逐会话追加日志,默认 Zstandard 压缩 + checksum,原子写入)和 SQLite(schema 17,同一分片块内字段完全匹配的 delta 段做有界物理存储)。
  • 崩溃恢复的优雅之处:重新加载时发现一个”开了 turn/start 却没有 turn/end“的轮次,不截断日志(长任务里一个轮次可能很大,删了就是丢数据),而是合成一个 turn/end { kind: 'interrupted' } 把它配平——这是唯一一个不由循环发出的结束原因。
  • 格式拒绝:遇到版本更新的日志、或未知的必需事件类型(没有 ignorable 标记),后端拒绝加载而不是静默跳过——因为”跳过一条可能改变整体解读的事件”比”拒绝”危险得多。新格式明确提示”由更新的 harness 写入,请升级”。
  • fork / resume:可以从任意稳定轮次间位置 fork;恢复时元数据里存了 agentPreset——因为 preset 决定工具和提示词,恢复成不同的组合等于让模型回放一段它无法再行动的历史

1.5 为什么说这是杀手锏

对跑 Agent 的人来说,最贵的成本是”它到底为什么搞砸了“。dsh 的答案:日志里什么都有,且保证能重建。调试一个跑了 50 步的 Agent 任务,可以回放、定位到出错的那一步、看当时模型看到的完整上下文、连原始流分片都在。这同时是审计、基准测试(同一任务换模型对比)、以及多 Agent 协作排障的基础设施。


2. 插件系统:Cordis 微内核 + 能力 seam(精读重点 #2)

2.1 五个核心概念(Cordis 入门)

概念 含义
插件 实现 Service 的对象(函数 + apply(ctx) 或 Service 子类),生命周期由 Cordis 管理
上下文 服务的容器。服务占据稳定的 ctx.<key>(如 ctx.llmctx.tools),其他插件按 key 查找服务,绝不 import 具体实现
inject 声明依赖 插件声明所需服务后,会等待服务就绪才启动——加载顺序由依赖表达,而非手动编排
类型化事件 服务通过声明合并注册事件名,以 4 种模式分发(见下)
注册是可逆副作用 所有注册(提示词片段、工具 schema、监听器)都是副作用,插件卸载时自动撤销

2.2 事件分发模式:一鱼四吃

模式 语义 典型用途
emit 监听器按注册顺序观察,不等待、无返回值 广播事实(如 session/event
waterfall 环绕中间件:监听器收到 (...args, next),可包装、改写,不调 next 直接返回 = 短路 拦截/决策(如 agent/pre-steptools/pre-execute
parallel 所有监听器并行,await 全部 耐久检查点(如 session/flush
serial 按序执行、有返回值 顺序决策(如 agent/turn-stopping

waterfall 是最聪明的:策略监听器拥有决策权时可以短路(比如”这条工具调用禁止执行”),而只做观察的监听器必须调 next() 委托下去。拦截、审批、限流、改写提示词全部可以用事件挂上去,不碰核心。

2.3 能力 seam:可替换能力的”三件套”

一个 seam 包含三种角色:

  1. Service Definition:声明接口,拥有自己的 ctx.<key>(如 ShellExecutor 抽象类)
  2. Service Provider:实现该接口(可以有多个,可互换)
  3. Consumer:消费该服务的插件(通常是面向模型的工具)

规范范例——shell 能力:

ctx.shell(Service Definition)
├── dsh-bash-local       (Provider:本地 bash)
├── dsh-bash-sandbox     (Provider:沙箱 bash)
├── dsh-pwsh-local       (Provider:本地 PowerShell)
└── dsh-tool-bash        (Consumer:面向模型的 bash 工具)

替换一个提供方就能改变整个产品:把 bash-local 换成 bash-sandbox,所有消费 ctx.shell 的消费方(bash 工具、钩子桥接)自动获得沙箱保护,消费方零改动。文件系统、子进程、沙箱、搜索、凭据、存储、技能……整个平台都是按这个模式组织的。

seam 生态(部分代表性服务):

ctx 键 类型 可换提供方 说明
ctx.llm seam deepseek / pi-ai / replay 模型适配器注册表,agent loop 提供方无关地调流式服务
ctx.sessionPersistence seam jsonl / sqlite 会话持久化,应用组合时选后端
ctx.shell seam bash-local / bash-sandbox / pwsh-local Bash 执行器
ctx.fs seam fs-local / fs-sandbox / fs-e2b 文件系统
ctx.subagents seam 6 个提供方 从进程内 fork,到委派给 Claude Code / Codex 进程
ctx.web seam exa / perplexity / deepseek / http-fetch 搜索与抓取
ctx.skills seam badge / filesystem 技能注册表——tool-skill 渲染会话前缀目录并加载 skill 正文
ctx.approval seam acp 一次性权限决策
ctx.credentials seam credentials-local 凭据
ctx.storage seam json / sqlite 非会话存储
ctx.sessions core 仅追加事件日志(真源)
ctx.tools core 工具注册表 + 带把关的执行流水线
ctx.agents / ctx.agentLoop core Agent 注册表 + 唯一具体循环驱动
ctx.invariants core 配套不变量注册表(运行时自检)

2.4 无特权内核的两个关键机制

① Map → derived-union 扩展模式。 几乎所有可扩展的联合类型都遵循同一模式:接口以判别标签为 key(ThingMap),联合类型由 keyof 派生。插件通过 TypeScript 声明合并添加变体——不需要修改拥有该类型的包

declare module '@deepseek-ai/dsh-llm' {
  interface ContentBlockMap {
    'c': { kind: 'c'; /* … */ }
  }
}

六个规范 map(含 SessionEventMapTurnEndReasonMap)都这么干。代价是约定:对这类联合类型做 switch 时禁止 assertNever——插件添加的变体是合法的未知值,default 分支必须放行。

② 注册即副作用,卸载即撤销。 扩展 dsh 的方式就是”把插件挂载到其他插件旁边”,不存在需要打补丁的特权内核。每个注册要么从 ctx.effect() 返回一个 disposer,要么用 Cordis 辅助方法自动处理——reload 和 teardown 时按预期撤销。连 Agent 循环本身都是可换的。

2.5 装配:profile 与组合包

运行中的 dsh 是一棵插件树,由启动时按序叠加的各层组成:

  • profile(web / headless):具名组装,列出叠放的组合包,保存用户的 cordis.patch.yml
  • 组合包(bundle):配置项 + 挂载代码的分发格式;dsh-base 是第一层(模型/工具/持久化/沙箱/审批/设置/凭据/遥测),dsh-web-app 加浏览器应用。
  • patch 覆盖:任何条目都可以被上层 patch 替换——dsh --profile web --dump-config 打印真实配置树,打印出的任何条目都能被你的 patch 替换

这就是”配置层可换任意能力”的落点:换模型端点、换执行工作流、换 UI,都不碰源码,只改 YAML。


3. Agent 循环:轮次 / 步骤 / scope / inbox

一个轮次按同一条循环流经 6 个核心包:

认领输入 → 开轮次(turn/start) → 组装提示词+工具schema → 从日志派生历史
→ 模型流式请求(llm/stream) → 工具执行(tools/*) → 每个模型可见事实追加回日志 → 关轮次

值得抄的设计点:

  • scope(作用域):按 agent 划分注册单位,只有”全局”和”恰好属于一个 agent”两层。shadowing 机制——带作用域的工具/提示词片段在该 scope 内替换同名全局项,这是”按 agent 定制 persona 和工具集”的机制。过滤掉的工具既不出现在提示词里、也拒绝执行,与不存在的工具无法区分(安全特性)。
  • inbox(收件箱):agent 拥有两条有序待处理列表——next-turn(普通续问)和 next-step(steering 中途引导);inject() 注入上下文不唤醒驱动,留到下一个步骤边界。
  • 拦截决策agent/pre-step 是请求派生前唯一的串行监听器链,可改写甚至拒绝进入步骤的消息(拒绝仍会记录一个空轮次——日志记录这次尝试本身);agent/request-error 监听器可以返回 { kind: 'retry' } 拥有失败恢复。
  • 取消cancel(cause) 带类型化原因(user / parent / hook / disposed),持久日志保留粗粒度 aborted谁请求的取消不塞进终态结果(那是另一类持久事件的职责)。

4. 与 WorkBuddy 对照:同行不同路

能力 dsh 的答案 WorkBuddy 的答案
扩展机制 Cordis 插件树,配置层可换任意能力 Skill 技能 + Expert 专家 + Connector 连接器(MCP)
会话记忆 事件溯源日志,模型可见即已记录,可回放 三层记忆(云端画像 / 用户级 / 工作区日志)+ 会话历史检索
子 Agent ctx.subagents 6 个提供方(进程内 / Claude Code / Codex) 子 Agent + Team 多智能体协作
技能系统 ctx.skills + tool-skill 从文件系统加载技能目录 ~/.workbuddy/skills/ 技能目录(同构!)
权限 ctx.approval + permission-presets 预设表 权限系统 + 沙箱
斜杠命令 ctx.commands(不经过模型轮次) / 命令体系
后台任务 ctx.jobs 后台任务 / 自动化
计划模式 ctx.planMode Plan 模式
沙箱 ctx.sandbox / ctx.fs / ctx.shell 三 seam Bash 沙箱
形态 开源 MIT,npx @deepseek-ai/dsh web 一行起 商业产品,桌面 IDE 开箱即用

最有趣的发现:dsh 的 ctx.skills 与 WorkBuddy 的 skill 目录是同一种东西——都是”技能提供方注册表 + 从文件系统加载技能正文 + 渲染进会话前缀”。你在 WorkBuddy 里积累 skill 的路线,被 dsh 印证为行业共识。

而 WorkBuddy 目前没有的是 dsh 的”事件溯源级完整轨迹”:WorkBuddy 有记忆沉淀,但不是”每次工具调用、每个原始 chunk 都可回放”的完整日志。


5. 可借鉴清单(结合你的项目)

5.1 认知层:直接吸收

  1. 轨迹复盘习惯:给 teammate 的每个投资决策、每篇博客内容生产,养成”过程可留痕”的习惯——关键节点(判断、工具结果、中途修改)记成结构化条目,事后可回放、可归因。WorkBuddy 的会话记忆 + 工作区日志已经是雏形,把它用得更狠。

  2. “模型可见即已记录”是调试的底线:多 agent 协作(teammate 6 个 agent)出问题,最贵的是”不知道哪一步错了”。任何 AI 交互系统,先保证输入输出有完整留痕,再谈别的

  3. 场景化运行模式:dsh 有 Standard / Code / Minimal / Creator 四模式,WorkBuddy 有 Craft / Plan / Ask——”按任务类型切换运行形态”已是行业共识,你现在的用法是对的。

5.2 架构层:可落到自己的代码

  1. 事件溯源代替”直接存消息数组”:如果你给《族谱》小程序或投资系统做 AI 会话持久化,参考 dsh 的模式——只追加事件日志(user / assistant_chunk / assistant / tool_call / tool_result / header),历史由投影派生。收益:永不双写、可回放、可 fork、压缩只遮蔽不删除。

  2. seam 三件套组织扩展:定义接口 / 提供方实现 / 消费方分离,消费方只依赖接口。teammate 的 6 个 agent 如果共享接口定义,换模型、换数据源都不动其他 agent。这是 dsh 全文最值得抄的组织模式。

  3. 崩溃恢复”不截断、合成结束标记”:长任务中断后恢复,别删尾部,合成一个 interrupted 结束标记配平——数据永远不丢。

  4. 坏数据在源头拒绝:写日志前做序列化校验(拒绝不可 JSON 序列化的 payload),宁可在写入时报错,也不让坏事件进入真源。

  5. waterfall 事件做策略:需要做”拦截/审批/改写”的地方,用”可短路的环绕中间件”而不是硬编码 if-else——策略与主流程解耦,还能按需叠加。

5.3 生态层:可抄的现成组件

  1. dsh 的技能、子 agent 多后端、session-query(会话全文搜索)、compaction(上下文压缩)都是独立包,概念可以直接移植到你自己的架构里,不用抄代码。

6. 风险与观望点

风险 说明
0.1.x 开发者预览 8/13 发布,API 与磁盘格式都在快速演进,SESSION_FORMAT_VERSION 明确不做迁移——旧日志可能打不开
Star 数有水分 一周 17 万 star 明显含大量”先收藏再说”,真实工程采纳率未验证
生态起步期 dsh-plugin npm 话题刚形成,成熟插件少;Cordis 是 vendor 进来的第三方内核,上游风险要看 cordiverse/cordis 的维护
定位窄 它只是 harness(外壳),不提供模型、不做训练、不做 RAG 中间件——别指望开箱即用的”完整 Agent 应用”
对普通用户不友好 是给开发者/平台构建者的积木,日常用户仍应待在 WorkBuddy 这类成品里

结论: 值得读源码和文档(尤其是 docs/subsystems/docs/cookbook/),作为”下一代 Agent 平台”的免费教材;但别急着上生产,等它到 1.0 或生态稳定再考虑作为自建底座。


7. 延伸阅读

  • 架构总览:docs/architecture.zh.md(含轮次流程图)
  • 轨迹日志:docs/subsystems/session.zh.mddocs/subsystems/persistence.zh.md
  • 插件系统:docs/cordis-primer.zh.mddocs/capability-seams.zh.mddocs/event-producer-consumer.zh.md
  • Agent 循环:docs/subsystems/core.zh.md
  • 术语表:docs/glossary.zh.md(seam / scope / turn / step 的规范定义)
  • 扩展实操:docs/cookbook/extension-cookbook.zh.md(加工具 / 加 LLM 适配器 / 加 Chat 节点分步指南)
  • 本地源码:本仓库已克隆至 dsh-src/packages/core/session 是轨迹日志实现,packages/shell 是 seam 规范范例)

已发布

分类

来自

评论

发表回复

您的邮箱地址不会被公开。 必填项已用 * 标注