# 从插件树到 Session Log：DeepSeek Harness 的复杂概念怎样连成一套系统

上一篇谈到，DeepSeek Harness 的复杂并不完全来自 Developer Preview 阶段的粗糙。它把 Harness 的许多内部选择开放给构建者，目标是让构建者控制 Agent 怎样被组织和运行。这篇继续向架构内部走一步：当 **Everything is a Plugin** 成为整个系统的前提，DeepSeek Harness 用什么办法避免插件化变成一团混乱？

DeepSeek Harness 是一套 Agent Runtime。模型在其中生成文本或结构化调用，Runtime 负责组织模型收到的内容、调度工具并保存任务状态。Session Log用只追加的事件记录一次会话。按照官方架构文档，负责接入模型的适配器、工具系统、会话机制和任务循环都由插件提供，产品层没有一组永远不可替换的特权组件。[DeepSeek Harness Architecture](https://github.com/deepseek-ai/deepseek-harness/blob/master/docs/architecture.md)

开放到这个程度，系统首先要确定一次启动加载哪些插件。选出的插件进入运行期以后，又要取得依赖、替换实现并清理自己留下的状态。当工具、上下文和任务循环都能被插件改变，已经发生的过程也需要成为可以重建的记录。这些问题会依次带出后面的术语，沿着装配、运行和记录的顺序连成一条设计链。

## Everything is a Plugin 把 Agent 变成一棵插件树

DeepSeek Harness 底层使用 **Cordis** 组织插件，Cordis 是负责依赖、事件和生命周期的插件运行框架。这里的 **Plugin** 是由 Cordis 挂载到运行上下文中的组件，它可以提供服务，也可以注册工具或事件监听器，生效和退出都受同一套生命周期管理。插件的范围远大于通常意义上的工具扩展，就连默认任务循环与会话日志自己也是一类插件，所以构建者不仅可以改变 Agent 会做什么，也可以改变它怎样运行以及怎样保存状态。

**Bundle** 是配置与代码的分发单位。它把一组 Cordis 配置行连同这些配置行要挂载的代码打包起来，供不同的启动组合复用。官方的 `dsh-base` 是每个 Profile 都会加载的基础层，模型接入、工具和持久化等通用能力从这里进入系统，沙箱与审批也包含在内。`dsh-web-app` 增加浏览器应用，`dsh-headless` 增加不启动服务器的一次性任务执行器。它们描述的是可以叠加的能力层。

**Profile** 是一套有名字的启动组合。它按顺序列出本次运行需要的 Bundle，也可以加入不属于这些 Bundle 的外部插件。官方提供了 `web` 和 `headless` 两个 Profile 模板：`web` 在 `dsh-base` 上叠加 `dsh-web-app`，`headless` 则在 `dsh-base` 上叠加 `dsh-headless`。前者提供浏览器界面，后者执行一个任务、输出结果以后退出。Standard Mode 的默认组合面向代码工作，装配出来的是 Coding Agent。

**Patch** 处理一套组合在具体部署中的局部差异。加载顺序依次是 Profile 列出的 Bundle、Profile 自带的 Patch、用户目录 Patch 和命令行 `--patch`，后一层可以覆盖前一层。Patch 以配置行的 `id` 为目标，覆盖时替换该行的整份配置，也可以插入新行。例如，一个团队沿用 `web` Profile，却要改变其中的沙箱设置，就可以覆盖对应配置行，无须复制和修改 `dsh-base` 或 `dsh-web-app`。

Bundle 只有进入 Profile，才成为本次运行采用的一层；Patch 作用在已经选定的层之上。一个 Bundle 可以被多个 Profile 复用，同一个 Profile 也能在不同部署中保留少量配置差异。解析完成以后，这些选择形成一棵有挂载关系的插件树。Standard Mode 是其中一种已有装配结果；换一套 Profile，DeepSeek Harness 可以呈现为另一种 Agent。

插件树只解决了“本次启动有哪些组件”。它还没有回答插件进入运行期以后怎样找到依赖，以及某个实现被替换或卸载时，系统怎样避免留下失效的监听器、工具和服务实现。DeepSeek Harness 把这部分运行期关系交给 Cordis 管理。

## Cordis 让插件在运行时建立关系，也能退出关系

**Cordis Context** 是插件共享的服务容器，也是插件获得生命周期的运行环境。插件由 Cordis 启动后，不直接依赖其他插件的具体代码，而是通过 Context 中约定好的Service名称(`ctx.<key>`)调用所需能力。Service背后的实现可以替换，使用这项Service的插件不必随之修改。[Cordis Primer](https://github.com/deepseek-ai/deepseek-harness/blob/master/docs/cordis-primer.zh.md)
插件通过 `inject` 声明自己需要哪些 Service。比如一个插件需要会话服务，它先在 `inject` 中写明这项依赖；会话 Service 尚未就绪时，该插件不会启动。依赖离开以后，Cordis 也能让相关组件退出当前运行状态。Cordis 所说的空间组合性就发生在这里。组件声明自己需要什么，运行系统根据当下存在的依赖建立关系，无需每个插件自行猜测加载顺序。

仅有稳定的 Service 名称，还不足以保证某项能力可以替换。DeepSeek Harness 用 **Capability Seam** 划定一项能力可以替换的范围。以文件操作为例，`fs` 包通过 `ctx.fs` 定义统一的文件操作接口，面向模型提供文件工具的 `tool-fs` 只通过这份接口发出请求。具体操作可以由 `fs-local` 在本机完成，也可以交给 `fs-sandbox` 或 `fs-e2b`。这些实现都是 **Provider**，`tool-fs` 则是 **Consumer**。只要 Provider 遵守 `ctx.fs` 的接口与事件约定，更换文件系统实现就不需要修改 `tool-fs`。**Service Definition 规定双方共同遵守什么，Provider 决定能力怎样实现，Consumer 负责使用这项能力；三者围出的替换范围就是 Capability Seam。**[Capability Seams](https://github.com/deepseek-ai/deepseek-harness/blob/master/docs/capability-seams.zh.md) 

Profile 完成启动时的组合选择以后，Capability Seam 随后确定一个实现要在什么接口边界内被换掉。这种抽象确实让我又想起了OpenStack的阴影，尤其是厂商给OpenStack Neutron写适配代码的时候。Provider 的替换成本取决于接口是否完整，也取决于 Consumer 有没有绕过接口依赖内部细节，这些问题要到代码走读和案例中检验；概念本身先把“可以换”落实成了接口、实现与使用者之间的关系。

插件之间还需要通信和拦截运行过程。**Typed Event** 是带有明确名称、类型和分发语义的事件接口。它给扩展行为规定可识别的入口，插件可以通过事件交换状态，也可以在指定流程节点接入策略。到 Agent Loop 部分，同一套事件机制还会承担另一项工作：把需要长期保存的事实与只服务当前运行的控制信号分开。

动态系统最麻烦的地方通常发生在退出时，Linux 内核对 `rmmod` 如此谨慎，是因为模块代码可以卸载，指向它的回调却未必已经消失。在DeepSeek Harness 中，插件注册了一项工具、一个 Provider 或一段事件监听，如果卸载只删除插件对象，这些登记仍可能留在运行环境里。Cordis 把这类受 Runtime 管理的注册称为 **Reversible Effect**。注册发生时会留下对应的撤销动作，插件被卸载或重载时，Cordis 调用这些动作，清除该插件在当前作用域中登记的内部状态。[Cordis Lifecycle and Effects](https://github.com/deepseek-ai/deepseek-harness/blob/master/docs/cordis-tutorial/02-lifecycle-and-effects.md)

Reversible Effect 的作用范围是 Cordis 管理的内部登记，包括工具、监听器和 Provider。已经提交的数据库变更与生产环境中的其他操作位于这个范围之外，需要事务、补偿动作或人工处置。插件系统能否重载和替换，很大程度上取决于旧组件退出后会不会继续污染新的运行状态，所以内部 Effect 的撤销依然很重要。

Cordis 用 **Spatiotemporal Composability** 概括这套设计。空间组合性管理组件在同一时刻怎样按照依赖建立关系，时间组合性处理组件进入、替换和退出以后，内部 Effect 怎样随生命周期撤销。Spatiotemporal Composability 这个术语本身看起来很科幻，大概是我所看到被攻击”自嗨“最多的概念，关于它是否德不配位，我会阅读论文之后专门写一篇理解。

Profile 和 Bundle 让插件树能够被装配出来，Cordis 则让这棵树在运行中保持关系。Agent 接到任务以后，还需要一套所有组合都能遵守的工作节奏，否则每种 Profile 都可能发明自己的模型调用、工具执行和结束条件。

## Agent Loop 给不同组合规定同一套运行语义

**Agent Loop** 很好理解，它是驱动 Agent 从一次输入走向模型请求、工具调用、下一步或结束的运行循环。它本身也可以由插件提供，但只要接入 DeepSeek Harness，就要使用 Runtime 能够识别的任务边界和事件协议。

前文所说的 **Runtime**，是 DeepSeek Harness 位于模型与外部环境之间的运行层。它接收用户输入，组织上下文并请求模型；模型返回工具调用以后，Runtime 负责调度执行，并把结果保存到会话中。**Agent Loop** 是发生在 Runtime 内部的任务循环，从一次输入开始，经过模型请求与工具调用，直到这一轮任务结束。Agent Loop 本身也可以由插件提供，但不同实现都要遵守 DeepSeek Harness 统一规定的任务边界和事件协议，Runtime 才能用同一种方式调度并记录这些运行过程。

官方架构用 **Turn** 和 **Step** 划分这段过程。Turn 是从一项输入开始，直到系统不再欠下待处理工作的一轮运行，它可以包含零个或多个 Step。Step 是一次模型请求，以及这次请求触发的工具调用。Turn 回答一轮任务何时结束，Step 给模型调用和随后发生的工具执行建立可定位的单位。

举一个最简单的例子。用户问“README 第一行写了什么？”，从这条输入进入系统，到 Agent 给出答案，是一个 Turn。第一个 Step 中，Runtime 请求模型，模型返回读取文件的工具调用，文件内容写回会话；第二个 Step 中，Runtime 带着工具结果请求模型，模型给出最终答案，本轮不再有待处理工作，Turn 在这里结束。一个 Step 可以包含多个工具调用，划分 Step 的依据是模型请求次数：Runtime 使用工具结果再次请求模型，就进入了下一个 Step。

把前面的概念放进一次运行，整套关系就会清楚许多。Profile 先选出 Bundle 与 Patch，Cordis 把相应插件挂载到 Context，插件通过 Service 取得依赖，并注册工具或 Provider。用户输入到达 Agent 后，Agent Loop 开启一个 Turn；进入 Step 时，Runtime 从会话历史组装模型需要的消息和工具定义。模型返回普通文本或结构化工具调用，Runtime 解析调用并送入工具执行管线。管线在执行前接入策略检查，规则要求审批时会暂停执行并等待批准，随后由对应 Provider 完成调用，工具结果进入会话。[Tool Execution Pipeline](https://github.com/deepseek-ai/deepseek-harness/blob/master/docs/tool-execution-pipeline.zh.md)

模型生成调用，Runtime 解析和调度，Provider 执行工具，人的批准只发生在规则要求的节点。把这些动作分开，才能看清 Harness 承担的工程工作。Agent Loop 根据结果判断当前 Turn 是否仍有待处理工作；需要模型继续处理工具结果时，它开启下一个 Step，没有待处理工作时，本轮结束。

这一过程中会经过几类 **Event**。**Session Event** 是写入会话、需要跨重载保存的持久事实，例如输入、模型消息、工具调用和工具结果。**Agent Event** 服务正在运行的 Agent，处理输入到达、状态变化、请求或停止等实时控制，不以长期保存为首要目的。**Capability Event** 则把策略和适配器接到某项具体能力的调用过程中，例如工具执行前的审批与改写。

前一节的 Typed Event 主要解释插件怎样通信；到了 Agent Loop，它增加了一层更严格的区分。影响会话历史的内容要成为 Session Event，当前运行中的协调交给 Agent Event，能力调用周围的扩展由 Capability Event 接入。插件仍然可以改变运行方式，但它必须选择相应的事件域，不能把持久事实和临时控制混成同一种消息。

## Session Log 保存模型看见过的运行事实

用只追加的事件记录会话，是 Session Log 的第一层作用。DeepSeek Harness 同时把它作为会话状态的事实来源，模型消息历史由这条事件流派生，分叉、恢复、转录、遥测和持久化也可以围绕同一历史建立。[Session README](https://github.com/deepseek-ai/deepseek-harness/blob/master/packages/core/session/README.md)

官方架构文档给出了一句直接的约束：**Model-visible means logged**。任何会进入模型请求的内容，都要能够从 Session Log 中的事件重建。一个插件可以给模型增加提示词片段、工具结果或其他上下文，但这些内容一旦影响模型下一步看到的输入，就不能只存在于某个插件的临时内存里。新的模型请求要从已有事件派生，生成的消息、工具调用与结果继续写回同一条事件流。

Session Log 因而同时处在两种关系里。面向构建者，它保留一次任务中可检查的输入、输出、调用和状态变化；面向 Runtime，它又参与下一次模型请求的上下文构造。这个观测窗口本身就是运行语义的一部分。插件可以扩展模型上下文，Session Log 要求这些扩展留下可以重建的来源，Everything is a Plugin 由此获得一条状态约束。

本文把 **Trajectory** 限定为运行历史面向人的路径视图，构建者可以按照步骤查看一次任务经历了哪些模型请求和工具调用。当前官方架构文档没有把 Trajectory 定义为与 Session Log 并列的独立状态模型，所以本文只把 Session Log 视为事件事实来源，Trajectory 用于描述人怎样阅读这条运行路径。两者在产品界面中的精确对应关系，留到使用篇根据届时版本核对。

这套可见性有两条明确边界。Session Log 保存 Harness 能够观察的模型输入与输出、工具调用和状态变化，不包含模型没有交给 Runtime 的私有思维过程。它能够重建某一步的模型输入，却不承诺确定性重放；模型服务、外部数据和工具环境发生变化以后，同一组历史事件可能得到不同结果。它让人能够确认模型当时收到过哪些输入、Runtime 执行过哪些动作，但开放世界不会因此变成一段可以无损倒带的录像。

## 每一项开放性都对应一条架构约束

沿着一次任务走完以后，DeepSeek Harness 的概念已经连接成了一套运行结构。Everything is a Plugin 扩大了可替换范围，Profile、Bundle 与 Patch 先把选择变成一棵可重复装配的插件树。Cordis 通过依赖、Capability Seam 和生命周期控制这棵树的运行关系；Agent Loop 用 Turn、Step 与事件域规定任务怎样推进；Session Log 把模型可见内容收回到一条能够重建的事件历史中。

这套设计最吸引我的地方，是它没有把开放性当成一句口号。实现能够替换，是因为 Capability Seam 先划出了能力接口；组件动态挂载以后，Cordis 会继续跟踪依赖变化，并在退出时清理内部状态。插件进入 Agent Loop，要遵守 Turn、Step 与事件协议。任何扩展一旦改变模型收到的上下文，Session Log 就要求它留下能够重建的来源。前一个设计带来的自由，会在后一个概念中遇到约束。DeepSeek Harness 的复杂主要生长在这些配对关系里。

对 AI Workflow Owner 来说，这些抽象对应的是可以修改和检查的控制位置。他可以用 Profile 选择一套 Agent 组合，通过 Capability Seam 接入本专业流程需要的数据和执行环境，并从 Session Log 检查模型收到的上下文、工具调用与结果。最终使用者可能只看到一个经过封装的 Agent，构建者却能在运行系统中找到修改和检查的位置。DeepSeek Harness 面向的首先是负责设计和维护工作流的人，这一点与上一篇的判断保持一致。

我目前愿意给这套概念设计很高的评价，因为插件的自由从启动装配一直延伸到运行历史，每一层又受到下一层的约束。当然我也仍然同时会抱着审慎的态度去进一步研究和观察这些美好设想是否能够建立起完整和充满活力的生态体系。如果以编程语言为例，我认为DeepSeek Harness很像是Python或者Ruby，在十几年前看来这两个编程语言都许入了崇高的愿景，而最后因为难以事先评价的设计抽象决策形成大相径庭的结果，那么DeepSeek Harness会是怎么样就非常有意思了。