27. DeepSeek Harness 源码全景与知识体系渗透
27. DeepSeek Harness 源码全景与知识体系渗透
本章不重复前 20 个知识维度的通用原理,而是回答“DeepSeek Harness 怎样把这些原理做成一个可组装、可恢复、可对外发布的产品”。阅读时不按包名孤立记忆,而要始终追三条链:组装链决定当前运行了哪些插件,执行链决定一次模型提议如何变成受控副作用,事实链决定界面、恢复、检索和遥测如何共用同一份可证明状态。
阅读本章前:先建立“一切皆插件”的心智模型
可以把 DeepSeek Harness 想成一台由配置装机的计算机:Profile 选择产品形态,Bundle 给出基础配件,Patch 做部署级替换,三者生成实际加载的 Cordis 插件树;Service 是能力插槽,Event 是消息与拦截总线,Effect 管理热插拔和资源回收,Scope 则让 Root、Preset、Agent 与 Child Agent 获得分层能力视图。图中的 Session、Agent Loop、LLM、工具、安全、Web 和遥测不是“核心之外的可选外挂”,它们本身都由插件贡献。
因此,“仓库里存在某个包”不等于“当前产品拥有该能力”。确认一项能力必须依次回答:它的 Definition 在哪里、哪个 Provider 实施真实副作用、哪个 Consumer 提供模型或用户入口、当前 Profile/Bundle 是否真正把它组装进插件树。插件产生的 Turn、Message 与 Tool 事实最终写入 Session Event Log,再被恢复、投影、查询、界面和遥测复用。
27.1 证据基线、成熟度与阅读规则
| 项目 | 本章基线 |
|---|---|
| 本地路径 | /deepseek-harness |
| Git 提交 | 47f943859bef60e4160492346772ded9b24f765a |
| 包版本 | 0.1.0-rc.5 |
| 运行时 | Node `^22.19.0 |
| 产品状态 | 开发者预览,尚未承诺磁盘格式和公共 API 长期兼容 |
| 主要事实来源 | packages/*/*/src、当前 cordis.patch.yml、生成目录和中文架构文档 |
本章将“存在包”、“存在可加载插件”、“当前 Bundle 已组装”和“默认配置真正启用”分开。例如 session-query-sqlite 已在 base bundle 挂载,但默认 openAt: never,因此精确读取与血缘追踪可用,全文搜索仍是显式选配;session-telemetry-otel 已挂载,但默认 mode 为 DISABLED。
建议每验证一项能力都保留四份证据:Service Definition 的类型与事件契约、Provider 的真实副作用、Consumer 的模型或用户入口、Bundle 中的实际组装行。少任何一份,都不应声称产品闭环已成立。
27.2 底层编程模型:Cordis 中的 Service、Event、Effect 与 Scope
DeepSeek Harness 的“一切皆插件”不是口号,而是四个组合原语:
- Service 为
ctx.llm、ctx.tools、ctx.sessions、ctx.fs、ctx.sandbox等能力提供唯一或注册表式入口,消费者依赖定义而不是具体提供方。 - Event 提供
emit、parallel、serial、waterfall四种分发语义;waterfall 监听器必须调用next()才会委派到下一层。 - Effect 将注册和 disposer 绑定,插件卸载、HMR 或 Scope 结束时撤销工具、提示段、监听器、连接和缓存。
- Scope 把全局、Preset 和 Agent 层的贡献叠加为查找链,同名贡献按近端优先;子 Agent 的工具限制、Persona 和策略只对子 Scope 可见。
图表加载中…
实战阅读时,先在 docs/capability-seams.zh.md 查 ctx 键的角色,再进入相应包的 src/index.ts,最后搜索 ctx.<service>、ctx.on('<event>') 和 Bundle id。这比从文件名猜测调用关系更可靠。
27.3 组装链:Profile、Bundle、Patch 与启动入口
一个运行中的 dsh 由空条目列表按顺序叠加:Profile 宣告的多个 Bundle → Profile 自己的 cordis.patch.yml → Harness home 级 patch → 命令行 --patch overlay。Patch 通过 id 定位行,替换整个 config 而非深层合并,因此后层必须重述所有应保留字段。
| 组装层 | 当前主要职责 |
|---|---|
bundle/base | LLM、Session、Agent Loop、Tool Runtime、JSONL 持久化、文件/Shell/Skill/Web Search、Plan/Goal/Todo、Subagent/Workflow、Approval/Sandbox、Telemetry |
bundle/web-app | Host WebServer、API Gateway、Web transport、Storage/Workspace、Projection Cache、Browser Client plugin roster |
bundle/headless | 一次性任务解析、Agent 创建、结果输出,不挂载 HTTP/Browser |
| Profile/Home/User overlay | 选模型、增删插件、替换 Provider、更改策略和产品默认 |
packages/boot/app-boot/src/profile.ts 是 Profile 发现和 Bundle 解析的核心路径,dsh --profile web --dump-config 则是检查最终插件树的一手证据。对一项“为什么我的 Tool/Provider 没有生效”问题,应先看 dump-config 和服务注入,再看具体执行代码。
27.4 核心主干:Session、System Prompt、Tools、Agent 与 Agent Loop
| 主干 | ctx 键 | 所有的核心事实 |
|---|---|---|
core/session | ctx.sessions | 仅追加 Session、Event seq、Surface、deriveMessages() |
core/system-prompt | ctx.systemPrompt | 有序 section、runtime context、工具 schema 组装 |
core/tools | ctx.tools | 工具注册、Scope 限制、策略流水线、Code Mode、呈现意图 |
core/agent | ctx.agents | Agent 工厂、活跃句柄、Inbox、agent/* 事件 |
core/agent-loop | ctx.agentLoop | 默认 React Loop、Turn/Step、模型请求、工具调度、终止原因 |
它们之间的关系是“主干最小化、行为从扩展点注入”。Agent Loop 没有直接导入 Skill、Compaction、Approval、Subagent、Plan 或 Telemetry;这些包通过 agent/pre-step、agent/request-error、agent/turn-stopping、tools/*、session/event 或 System Prompt 贡献接入。新功能若可以通过已有事件或 seam 表达,就不应修改 agent-loop。
27.5 一次 Turn 的完整时序
图表加载中…
这条时序中最容易忽略的是:agent/pre-step 可以拒绝或改写已领取消息,但首次领取被拒绝或改为空仍会产生一个持久 Turn 边界;Tool Result 先作为 Session Event 保存,再进入下一 Step;Turn 的最终原因从 completed/max-tokens/blocked/aborted/error 中明确选择,不用“驱动器停了”代替业务终态。
27.6 工具执行:从模型 ToolCall 到受控副作用
图表加载中…
并发调度保留两种顺序:Provider 可并发运行,但策略、结果持久化和模型下一次看到的 Context 按原 ToolCall 顺序提交。每个待启动调用都重新读取当前注册表与 concurrency mode,因此在前一个工具运行期间卸载插件或改变策略,尚未启动的后续调用会看到新状态。
Code Mode 通过 run_code 让模型写程序调用多个工具,但 sub-call 带 parent execution token 重新进入同一 Pipeline,仍受 Tool Restriction、Timeout、Spill 和每个能力的安全策略约束。“组合多调用”不等于“获得绕过治理的内部 API”。
27.7 能力 seam:Definition、Provider、Consumer 的完整闭环
| 能力 | Service Definition | Provider 例子 | Consumer 例子 | 替换时保持的契约 |
|---|---|---|---|---|
| LLM | llm/llm | llm-deepseek、llm-pi-ai、llm-replay | agent-loop、compaction-basic | Message/Chunk/Error 词汇、路由与取消 |
| Filesystem | fs/fs | fs-local、fs-sandbox、fs-e2b | tool-fs | 读写编辑语义、观测事件、工作区限制 |
| Shell | shell/shell | bash-local、bash-sandbox、pwsh-* | tool-bash、tool-pwsh、Hooks | 请求/规格分离、输出、超时与取消 |
| Sandbox | sandbox/sandbox | sandbox-local | bash-sandbox、terminal-bash | 精确 argv 包装、enforcement 说明、拒绝分类 |
| Skill | skill/skill | skill-filesystem、skill-badge | tool-skill、Host 显式调用 | 目录、优先级、调用权限、按需正文 |
| Web | web/web | DeepSeek/Exa/Perplexity Search、HTTP Fetch | tool-web | 稳定 Tool Schema、Provider 选择、超时和结果 |
| Subagent | subagent/subagent | spawn/fork-in-process、ACP、Codex、Claude Code、DSH SDK | tool-subagent* | 调用身份、血缘、终止原因、续跑语义 |
| Persistence | session-persistence | JSONL、SQLite | Agent Loop、Hooks、Session Query | 连续 seq、durability、恢复与 revision |
| Telemetry | session-telemetry | OTel | 记录后端/反馈流程 | Ledger/Ops 记录、脱敏点、shutdown drain |
判断 seam 是否完整的方法是反向替换 Provider:把 fs-local 换成 fs-e2b 时,tool-fs 和 Agent Loop 不应改变;把 JSONL 换成 SQLite 时,Session Event 词汇与 Resume 语义不应改变。若替换必须改 Consumer 的分支或请求类型,说明 seam 泄漏了提供方细节。
27.8 Context、Skill、MCP 与 Compaction 怎样进入主链
图表加载中…
Skill 和 MCP 的共同点是“不把外部能力绕过 Tool Runtime”:Skill 正文通过 ToolResult 或显式 Message Source 进入模型,MCP Tool 被转换为普通 ToolDefinition 后进入统一策略和 Session Event 链。区别在于 Skill 提供可执行指导文本,MCP 提供跨进程/网络动作入口;前者的主要风险是指令信任和版本,后者还要处理连接代际、名称空间、协议输入和远端失败。
27.9 Plan、Goal、Todo、Jobs、Workflow 与 Subagent 的责任边界
| 概念 | 状态所有者 | 持久化 | 解决的问题 |
|---|---|---|---|
| Plan Mode | ctx.planMode | plan/mode + Projection | 当前是否只许探索和提交计划 |
| Goal | ctx.goals | goal/change 折叠 | 跨 Turn 的用户目标、revision 和续跑边界 |
| Todo | tool-todo/Session | todo/write | 当前 Agent 可见的执行清单 |
| Jobs | ctx.jobs | 运行中任务 + Session 完成通知 | 统一管理后台 Shell、Terminal 和 Subagent |
| Workflow | ctx.workflowEngine | 工作流与子 Session 事件 | 用 worker-thread 执行可控脚本化 Agent 调度 |
| Subagent | ctx.subagents | 子 Session header/log + descriptor/projection | 把任务委派给可替换的子 Agent Provider |
深度限制使用父 Session header 中的持久 delegationDepth 作为单调下界,恢复后不会被当作根 Agent。Spawn 子 Agent 从空白历史开始,Fork 子 Agent 以父日志的稳定边界为 seed;两者都写 parentSession、Preset 和权限覆盖。Continuable 子 Agent 可在冷恢复后继续,其每次激活的终止原因只从该 epoch 的日志后缀推导,避免把上一次回答当成本次产物。
27.10 Session 事实、持久化、表层与投影
图表加载中…
三个一致性边界必须分开:
Session.append()返回,只证明事件已进入当前进程的 canonical log。session/event已发布,只证明持久化和投影消费者已获得提交后通知。session/flush完成,才证明当前所有 durability listener 已达到静默点。Fork、对外导出、跨进程 Hook 和关键调度交付应在这个边界后声称可恢复。
冷恢复的修复也是追加语义:最后一条物理记录若被撕裂可丢弃,但已完整持久的开放 Turn 不能删除;系统为已记录但无结果的 ToolCall 追加 outcome-unknown/not-started 结果,再补 Step/Turn 终止,使模型回放仍满足工具配对不变量。
27.11 Approval、Permission Preset 与 Sandbox 的安全链
图表加载中…
Permission Preset 只是用户可理解的联动层:read-only 与 workspace-write 默认配 approval: ask,danger-full-access 默认配 approval: never。Approval 决定是否允许本次动作,Sandbox 决定执行进程或文件提供方实际能访问什么;即使 Approval allow,也不应自动取消 Sandbox。
当前 local sandbox 主要管理文件效果边界,其 full | partial 是对已声明承诺的表达程度,不是“对所有 OS 风险都完全隔离”。安全评审还应单独检查环境变量、网络、进程树、设备、硬链/符号链接和远程 Provider 的信任边界。
27.12 Web、API Gateway、SDK 与 ACP 的进程外边界
Web 产品不是一个绕过主干的独立 Runtime。Host 侧 api-gateway 将 Typert 生成的 Remote 描述符与实时 Cordis Service 绑定,host/apiproxy 实现 Agent、Session、Settings、Workspace、Approval、Question、Skill、Goal 和 Job 等边界,client-connection 在 Browser 侧处理 RPC 与下行 frame。界面插件通过 client modules 和 UI slots 组装,不把所有功能写进一个巨型 App 组件。
| 边界 | 用途 | 核心证据 |
|---|---|---|
| Web Host/Client | 人类对话、设置、审批、进度和历史 | bundle/web-app/cordis.patch.yml、host/apiproxy、client/* |
| JSON-RPC SDK | 进程外 TypeScript 客户端和 Server | packages/sdk/{protocol,server,client} |
| ACP | 自动化导向 Agent Client Protocol Server | packages/acp/acp |
| Hooks | Claude Code/Codex 线协议桥接 | packages/hooks/{hook-protocol,hooks-*} |
| Typert | 从 TypeScript 类型图生成/加载运行时 RPC 描述符 | packages/typert/*、packages/api/gateway |
这些边界都需要把类型可信的同进程值与需要运行时校验的 Wire/File/Queue 输入区分。例如 Browser 上传图像会验证 canonical base64、数量、总字节和内容类型,保存为内容寻址 Attachment 后 Session 只记持久引用;Settings 对客户端返回脱敏描述符,Credential 只允许写入引用和在真正调用时解析值。
27.13 可观测、运行时不变量与测试金字塔
DeepSeek Harness 把“不变量是运行时产品能力”与“测试是作者证据”分开。packages/runtime-diagnostics/invariants 让每个包注册自己所有的关系检查,例如组装中必须有唯一 Provider、模型可见输入必须可从日志重建、配套 Consumer 与 Service 必须同时存在。它们检查真实事件流或可变数据,不通过“某方法存在”证明关系正确。
| 验证层 | 命令/基础设施 | 能证明什么 |
|---|---|---|
| Unit | pnpm run test / Vitest | 局部类型、状态机、失败与 disposer 语义 |
| Coverage gate | pnpm run test:coverage | packages/*/*/src 每文件覆盖约束,不等于产品组装正确 |
| Snapshot | pnpm run test:snapshot | 无密钥回放与真实可运行示例的用户/模型可见输出 |
| E2E | pnpm run test:e2e | 有密钥时的真实 Provider 行为;无密钥自跳过不是通过证据 |
| Static/build | typecheck、lint、build、hygiene | 类型、静态规则、发布产物和消费者兼容性 |
| Docs | doc-sync、website:build | 生成目录新鲜度、双语配对、链接与站点构建 |
| Platform sandbox | CI matrix/有针对性的真实后端测试 | 操作系统实际拒绝越界动作,而非只验证 Policy 编译 |
Telemetry 记录与 Session Log 共用 event type/seq 可实现回放对齐,但 Telemetry 是可失败、可脱敏、可关闭的外发副本,不是恢复的事实源。反过来,Session Log 可以重建业务状态,但不能代替 OTel 处理批量、重试、传输和跨实例查询。
27.14 二十个知识维度的 DeepSeek Harness 必读路径
| 维度 | 第一站 | 第二站 | 完成阅读后必须能回答 |
|---|---|---|---|
| Harness | docs/architecture.zh.md | bundle/base/cordis.patch.yml | 抽象架构怎样变成实际插件树? |
| Runtime | packages/core/agent-loop/src/agent.ts | packages/core/agent/src/{inbox,dispatch}.ts | 输入、取消、唤醒与终态归谁? |
| Multi-Agent | packages/subagent/README.zh.md | packages/subagent/subagent/src/{child-agent,lifecycle}.ts | 血缘、深度、权限与续跑怎样持久? |
| Agent Loop | docs/agent-lifecycle.zh.md | packages/core/agent-loop/src/{agent,tool-calls}.ts | 一个 Turn 为什么包含多个 Step? |
| Skill | packages/skill/skill/src/index.ts | packages/skill/tool-skill/src/index.ts | 目录、优先级、正文和调用权限如何分开? |
| MCP | packages/mcp/mcp-client/src/index.ts | packages/mcp/mcp-client/src/{connection,tools}.ts | 如何在掉线和目录更新时避免半代工具? |
| 长期记忆 | packages/session-query/README.md | packages/session-query/*/src | 当前实现到哪一层,真正记忆抽取还缺什么? |
| 短期记忆 | packages/core/session/src/surface.ts | packages/session/session-projection/src/index.ts | 事实、模型表层和查询状态如何分层? |
| 压缩 | packages/compaction/compaction-basic/src/index.ts | packages/compaction/compaction-basic/src/{region,summarizer}.ts、packages/compaction/compaction/src/tool-pairing.ts | 什么证明压缩已持久进展? |
| 窗口 | packages/llm/token-meter/src/* | packages/llm/llm/src/types.ts | 预算为什么要绑定实际 provider/model 请求? |
| Prompt | packages/core/system-prompt/src/index.ts | packages/preset/persona、packages/plan/plan-mode、packages/context/* | 段落的顺序、Scope 与卸载怎样保证? |
| Context | packages/context/*/src | packages/core/agent-loop/src/runtime-context.ts | 每项模型可见输入能否从 Session 日志重建? |
| UX | packages/bundle/web-app/cordis.patch.yml | packages/host/apiproxy、packages/client/ui-* | UI 怎样只从事件和投影构建状态? |
| Tool Use | packages/core/tools/src/index.ts | packages/core/agent-loop/src/tool-calls.ts | 并发执行与确定提交怎样同时成立? |
| Memory | packages/core/session、packages/session/session-projection | packages/session-query、packages/attachment、packages/spill | 不同存储类型的写入权和失效语义是什么? |
| Planning | packages/plan/plan-mode | packages/goal/*、packages/todo/*、packages/workflow/* | Plan、Goal、Todo、Workflow 为什么不能合并? |
| Session | packages/core/session | packages/session/session-persistence、packages/session/session-projection-cache | append、feed、flush 三个边界各证明什么? |
| 可观测 | packages/session/session-telemetry | packages/session/session-telemetry-otel | Ledger 与 Ops 如何区分,上传何时发生? |
| 权限 | packages/interaction/user-approval | packages/interaction/permission-presets、packages/subagent/subagent/src/child-agent.ts | 为什么没有回答方必须拒绝,子 Agent 为什么不能升级? |
| 沙箱 | packages/sandbox/sandbox-policy | packages/sandbox/sandbox-local、packages/fs/fs-sandbox、packages/shell/*-sandbox | 同一 cwd/mode 如何同时限制文件与进程? |
27.15 四条源码实战追踪
27.15.1 追一次文件编辑
packages/bundle/base/cordis.patch.yml 中 tool-fs + fs-observation-policy + fs-sandbox → packages/fs/tool-fs/src/edit.ts 构造编辑请求 → ctx.fs 提供方 → 观测策略验证是否先读 → sandboxPolicy.resolve() 固定 cwd/mode → Provider 写入 → Tool Runtime 投影 diff → tool/result 按原调用顺序持久。需要回答:“用户审批”、“必须先读”和“OS/Provider 路径限制”分别在哪一层强制?
27.15.2 追一次 MCP 工具目录更新
mcp-client.apply() 保留 serverName → Connection Supervisor 建立 transport/client 代际 → syncTools() 分页获取全量新定义 → 名称规范化与 schema 支持性检查 → 撤销旧代并注册新代 → 冲突时撤销已注册的新工具 → 调用时使用 raw name、timeout 和 AbortSignal。需要回答:拉取失败为什么保留旧代,注册冲突为什么收敛为零工具?
27.15.3 追一次崩溃后恢复
session/event 进入 write-behind → session/flush 等待 durability → 进程在开放 Step 中终止 → JSONL/SQLite load() 读取连续前缀 → 丢弃物理 torn tail,保留完整事件 → Repair 对未完 ToolCall 写 outcome-unknown/not-started 并关闭 Step/Turn → prepare() 创建未发布 Session → Agent Factory 恢复 Preset、路由、Plan、Goal、Sandbox/Approval 等日志折叠状态。需要回答:为什么不能删除已完整写入的开放 Turn?
27.15.4 追一次子 Agent 委派
tool-subagent 选择 provider/mode → resolveChildDepth() 以持久父深度为下界 → 捕获父 Session 的明确 Sandbox override 和 approval: never → 创建子 Session header(parent/origin/depth/seed/preset)→ 组合父 Preset、子 Persona 与 Tool Filter → 写入 delegation policy events → Provider 启动子 Agent → subagent/start/end 观测 epoch → 从子日志后缀推导终止原因 → 前台结果或后台通知进入父 Inbox。需要回答:子 Agent 为什么可继承工作区 Sandbox mode,却不能继承父级交互式升级能力?
27.16 与 Claude Code、Codex 的对照结论
| 观察点 | Claude Code | Codex | DeepSeek Harness |
|---|---|---|---|
| 主要扩展方式 | Tool/Hook/Skill/MCP 和产品内部状态 | Rust trait/crate、Tool Handler、Protocol Event | Cordis Service/Event/Effect/Scope,所有产品部件均为插件 |
| 模型历史与恢复事实 | Transcript 与恢复投影 | History 与 Rollout 明确分离 | Session Surface 与 append-only Session Log 明确分离 |
| 能力替换 | 多为工具或服务层扩展 | Trait/Handler/Provider 边界 | 强调 Definition/Provider/Consumer 三角 seam |
| Multi-Agent | AgentTool、Task/Mailbox、Worktree | 内建 Agent Control 与 Thread | 多 Provider seam,支持本地、ACP、Codex、Claude Code 和 DSH SDK 子 Agent |
| 持久化 | Session JSONL 与顺序写队列 | Rollout + Resume/Fork | JSONL/SQLite Provider + flush barrier + Projection Cache |
| 安全 | Permission Decision + 外部 sandbox runtime | PermissionProfile + 多平台 sandbox | Approval Event + Sandbox Policy + 文件/Shell Provider 组合 |
| 长期记忆 | 存在产品实现 | 存在后台抽取与合并 | 当前主要是 Session Query 检索基础,未见同等自动抽取闭环 |
对照的意义不是评判“谁更先进”,而是识别同一不变量的不同实现形式。例如三者都需要将 ToolCall 与 ToolResult 稳定配对,但 Claude Code 主要从 ToolUse/Transcript 观察,Codex 从 Response Item/Rollout 观察,DeepSeek Harness 从 tool/call/tool/result Session Event 与恢复修复观察。工程分析中应先回答这个不变量,再用三套源码证明你理解不同取舍。
27.17 完整理解的自测与工程检验题
- 画出
profile → bundle → patch → Cordis tree链路,解释为什么只看packages/不能确认功能已启用。 - 从
followup()开始,一直追到turn/end,标出每个持久事件与实时 waterfall。 - 说明为什么工具可以并发执行,但 ToolResult 必须按模型原顺序提交。
- 解释
agent.inject()与直接Session.append('user/message')的语义差异,以及注入何时真正变成模型可见事实。 - 用 Definition/Provider/Consumer 三角检查 Filesystem 或 Subagent seam,指出替换 Provider 时哪些契约不能变。
- 对比 Skill Catalog 和 MCP Tool Catalog:两者的缓存、版本、信任与失效边界有何不同?
- 说明 Session Log、Surface、Projection、Projection Cache 和 Session Query 五者的事实所有权。
- 构造“ToolCall 已持久,副作用可能已发生,ToolResult 尚未写入”崩溃,解释为什么恢复不能直接重放工具。
- 解释 Approval allow 为什么不代表关闭 Sandbox,以及没有 Approval answerer 时为什么必须 fail closed。
- 为 Linux/macOS/Windows 各设计一个真实 Sandbox conformance 用例,区分“Policy 返回拒绝”与“OS 确实阻断”证据。
- 说明 Plan Mode、Goal、Todo、Jobs、Workflow 和 Subagent 为什么各自需要独立状态与事件。
- 指出 DeepSeek Harness 当前长期记忆闭环的缺口,设计不修改 Agent Loop 的 Memory seam。
能独立完成这 12 题,并对每个回答给出“定义 → 组装 → 正常路径 → 失败路径 → 持久化/强制边界”五段证据,才算完成对 DeepSeek Harness 的渗透式理解;只能罗列包名、Tool 名或 UI 功能,仍停留在功能清单层。