精读对象: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/message、assistant/message、tool/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.llm、ctx.tools),其他插件按 key 查找服务,绝不 import 具体实现 |
| inject 声明依赖 | 插件声明所需服务后,会等待服务就绪才启动——加载顺序由依赖表达,而非手动编排 |
| 类型化事件 | 服务通过声明合并注册事件名,以 4 种模式分发(见下) |
| 注册是可逆副作用 | 所有注册(提示词片段、工具 schema、监听器)都是副作用,插件卸载时自动撤销 |
2.2 事件分发模式:一鱼四吃
| 模式 | 语义 | 典型用途 |
|---|---|---|
emit |
监听器按注册顺序观察,不等待、无返回值 | 广播事实(如 session/event) |
waterfall |
环绕中间件:监听器收到 (...args, next),可包装、改写,不调 next 直接返回 = 短路 |
拦截/决策(如 agent/pre-step、tools/pre-execute) |
parallel |
所有监听器并行,await 全部 | 耐久检查点(如 session/flush) |
serial |
按序执行、有返回值 | 顺序决策(如 agent/turn-stopping) |
waterfall 是最聪明的:策略监听器拥有决策权时可以短路(比如”这条工具调用禁止执行”),而只做观察的监听器必须调 next() 委托下去。拦截、审批、限流、改写提示词全部可以用事件挂上去,不碰核心。
2.3 能力 seam:可替换能力的”三件套”
一个 seam 包含三种角色:
- Service Definition:声明接口,拥有自己的
ctx.<key>(如ShellExecutor抽象类) - Service Provider:实现该接口(可以有多个,可互换)
- 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(含 SessionEventMap、TurnEndReasonMap)都这么干。代价是约定:对这类联合类型做 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 认知层:直接吸收
-
轨迹复盘习惯:给 teammate 的每个投资决策、每篇博客内容生产,养成”过程可留痕”的习惯——关键节点(判断、工具结果、中途修改)记成结构化条目,事后可回放、可归因。WorkBuddy 的会话记忆 + 工作区日志已经是雏形,把它用得更狠。
-
“模型可见即已记录”是调试的底线:多 agent 协作(teammate 6 个 agent)出问题,最贵的是”不知道哪一步错了”。任何 AI 交互系统,先保证输入输出有完整留痕,再谈别的。
-
场景化运行模式:dsh 有 Standard / Code / Minimal / Creator 四模式,WorkBuddy 有 Craft / Plan / Ask——”按任务类型切换运行形态”已是行业共识,你现在的用法是对的。
5.2 架构层:可落到自己的代码
-
事件溯源代替”直接存消息数组”:如果你给《族谱》小程序或投资系统做 AI 会话持久化,参考 dsh 的模式——只追加事件日志(
user/assistant_chunk/assistant/tool_call/tool_result/header),历史由投影派生。收益:永不双写、可回放、可 fork、压缩只遮蔽不删除。 -
seam 三件套组织扩展:定义接口 / 提供方实现 / 消费方分离,消费方只依赖接口。teammate 的 6 个 agent 如果共享接口定义,换模型、换数据源都不动其他 agent。这是 dsh 全文最值得抄的组织模式。
-
崩溃恢复”不截断、合成结束标记”:长任务中断后恢复,别删尾部,合成一个
interrupted结束标记配平——数据永远不丢。 -
坏数据在源头拒绝:写日志前做序列化校验(拒绝不可 JSON 序列化的 payload),宁可在写入时报错,也不让坏事件进入真源。
-
waterfall 事件做策略:需要做”拦截/审批/改写”的地方,用”可短路的环绕中间件”而不是硬编码 if-else——策略与主流程解耦,还能按需叠加。
5.3 生态层:可抄的现成组件
- 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.md→docs/subsystems/persistence.zh.md - 插件系统:
docs/cordis-primer.zh.md→docs/capability-seams.zh.md→docs/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 规范范例)
发表回复