# 从插件树到 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。

![[01-bundle-profile-patch.png]]

插件树只解决了“本次启动有哪些组件”。它还没有回答插件进入运行期以后怎样找到依赖，以及某个实现被替换或卸载时，系统怎样避免留下失效的监听器、工具和服务实现。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)

![[02-capability-seam.png]]

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

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

动态系统最麻烦的地方通常发生在退出时，Linux 内核对 `rmmod` 如此谨慎，是因为模块代码可以卸载，指向它的回调却未必已经消失。在 DeepSeek Harness 中，插件注册了一项工具、一个 Provider 或一段事件监听，如果卸载只删除插件对象，这些登记仍可能留在运行环境里。Cordis 把这类需要随插件生命周期撤销的登记称为 **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 给不同组合规定同一套运行语义

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

官方架构用 **Turn** 和 **Step** 划分这段过程。Turn 是从一项输入开始，直到系统不再欠下待处理工作的一轮运行，它可以包含零个或多个 Step。Step 是一次模型请求，以及这次请求触发的工具调用。Turn 划定一轮任务的运行边界，Step 把每次模型请求及其工具执行标记成可定位的单位。

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

![[03-turn-step-session-log.png]]

一次任务开始运行以后，前面的概念会进入同一条执行链。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。构建者用它检查一次任务的输入、输出、调用和状态变化，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 会走向哪一种结果，可能要很久以后才能回答。


## 附录：本文术语表

下面的位置按照当前文章的小节和段落标记。一个术语第一次出现时如果只做了简要说明，后面才完成正式定义，两处位置都会列出。

### 总纲与运行系统

- **Harness**（开篇第 1 段）：模型外侧负责让 Agent 持续完成任务的运行系统。它组织模型收到的内容，连接工具和外部环境，并保存任务继续运行所需的状态。
    
- **Agent Runtime / Runtime**（首次出现于开篇第 2 段；正式定义见“Agent Loop 给不同组合规定同一套运行语义”第 1 段）：DeepSeek Harness 位于模型与外部环境之间的运行层。本文后续使用 Runtime 指代这层实际接收输入、请求模型、调度工具和保存结果的系统。
    
- **Everything is a Plugin**（开篇第 1 段；集中解释见第一节）：DeepSeek Harness 的基本设计原则。模型接入、工具执行、会话机制和 Agent Loop 都可以由插件提供，产品层不保留一组永远不可替换的特权组件。
    
- **Standard Mode**（“Everything is a Plugin 把 Agent 变成一棵插件树”第 3、5 段）：DeepSeek Harness 面向代码工作的默认组合，装配以后呈现为 Coding Agent。
    

### 插件树的启动装配

- **Cordis**（第一节第 1 段；第二节继续展开）：DeepSeek Harness 用来组织插件依赖、事件和生命周期的插件运行框架。
    
- **Plugin**（第一节第 1 段）：由 Cordis 挂载到运行上下文中的组件。插件可以提供 Service，也可以注册工具或事件监听器，其生效和退出由同一套生命周期管理。
    
- **Bundle**（第一节第 2 段）：配置与代码的分发单位。一个 Bundle 可以被多个 Profile 复用，但只有被某个 Profile 选择以后，才会进入本次启动组合。
    
- **Profile**（第一节第 3 段）：一套有名字的启动组合。它按顺序选择本次运行需要的 Bundle，也可以加入外部插件和上层 Patch。
    
- **Patch**（第一节第 4、5 段）：针对具体部署进行局部覆盖的配置层。它按配置行的 `id` 替换整行配置或插入新行，无须复制原有 Bundle。
    

### Cordis 的运行期关系

- **Cordis Context**（第二节第 1 段）：插件共享的 Service 容器，也是插件获得生命周期的运行环境。
    
- **Service**（第二节第 1、2 段）：插件通过 `ctx.<key>` 访问的命名能力。使用者依赖 Service 的公开约定，无须直接依赖实现这项能力的插件代码。
    
- **inject**（第二节第 2 段）：插件声明 Service 依赖的方式。依赖尚未就绪时，Cordis 不启动插件；依赖离开以后，相关组件也可以退出当前运行状态。
    
- **Capability Seam**（第二节第 3、4 段）：一项能力的完整替换边界。它把公开契约、能力实现和能力使用者放进同一条关系中，规定实现可以在哪里被换掉。
    
- **Service Definition**（第二节第 3 段）：Capability Seam 中公开的能力契约，规定 Provider 与 Consumer 共同遵守的接口和事件。
    
- **Provider**（第二节第 3 段）：按照 Service Definition 提供具体能力实现的组件。例如 `fs-local`、`fs-sandbox` 和 `fs-e2b` 都可以实现 `ctx.fs`。
    
- **Consumer**（第二节第 3 段）：通过 Service Definition 使用能力的组件。例如 `tool-fs` 只调用 `ctx.fs`，不直接绑定某个文件系统 Provider。
    
- **Typed Event**（第二节第 5 段；第三节第 6、7 段继续分类）：带有明确名称、类型和分发语义的事件接口，插件通过它交换状态或接入运行过程。
    
- **Reversible Effect**（第二节第 6、7 段）：需要随插件生命周期撤销的内部登记或资源。插件卸载或重载时，Cordis 调用对应的撤销动作，清除工具、监听器和 Provider 等内部状态；外部数据库或生产变更不在这一机制的回滚范围内。
    
- **Spatiotemporal Composability**（第二节第 8 段）：Cordis 对空间组合性与时间组合性的总称。前者处理组件在同一时刻怎样按照依赖建立关系，后者处理组件进入、替换和退出时，内部 Effect 怎样随生命周期清理。
    

### Agent 的运行过程

- **Agent Loop**（第三节第 1 段）：发生在 Runtime 内部的任务循环，从一次输入开始，经过模型请求和工具调用，直到这一轮任务结束。
    
- **Turn**（第三节第 2、3 段）：从一项输入开始，直到系统不再有待处理工作的一轮运行。一个 Turn 可以包含零个或多个 Step。
    
- **Step**（第三节第 2、3 段）：一次模型请求，以及这次请求触发的工具调用。同一次模型响应产生多个工具调用时，它们仍然属于同一个 Step。
    
- **Tool Execution Pipeline**（第三节第 4 段）：Runtime 解析模型生成的工具调用以后，用来完成策略检查、审批、Provider 调用和结果回写的执行管线。
    
- **Event**（第三节第 6、7 段）：DeepSeek Harness 在不同组件和运行阶段之间传递事实或控制信号的基本形式。本文随后按照用途将它分为三类。
    
- **Session Event**（第三节第 6、7 段）：写入会话并需要跨重载保存的持久事实，例如用户输入、模型消息、工具调用和工具结果。
    
- **Agent Event**（第三节第 6、7 段）：服务当前 Agent 运行的实时控制事件，例如输入到达、状态变化、请求和停止，不以长期保存为首要目的。
    
- **Capability Event**（第三节第 6、7 段）：围绕某项具体能力调用发生的扩展事件，例如在工具执行前接入审批或参数改写。
    

### 会话记录与运行路径

- **Session Log**（首次出现于开篇第 2 段；集中解释见“Session Log 保存模型看见过的运行事实”）：用只追加事件保存会话历史的记录，也是 DeepSeek Harness 构造会话状态和后续模型输入的事实来源。
    
- **Model-visible means logged**（第四节第 2 段）：凡是会进入模型请求的内容，都必须能够从 Session Log 中的事件重建。插件不能让影响模型输入的内容只存在于自己的临时内存中。
    
- **Trajectory**（第四节第 4 段）：本文对运行历史面向人的路径视图所用的名称。构建者可以按 Step 查看模型请求和工具调用；本文没有把它视为与 Session Log 并列的另一套状态模型。
    
- **AI Workflow Owner**（“每一项开放性都对应一条架构约束”第 3 段）：理解专业工作流程，并对 Agent 的上下文、执行边界、审批位置、异常接管和持续修改负责的人。
    

### 文中的配置与包名实例

- `**dsh-base**`**、**`**dsh-web-app**`**、**`**dsh-headless**`（第一节第 2、3 段）：本文用于解释 Bundle 的官方实例，分别提供基础能力、浏览器应用和一次性任务执行器。
    
- `**web**`**、**`**headless**`（第一节第 3 段）：本文用于解释 Profile 的两个官方模板，前者组合 `dsh-base` 与 `dsh-web-app`，后者组合 `dsh-base` 与 `dsh-headless`。
    
- `**ctx.fs**` **与** `**tool-fs**`（第二节第 3 段）：文件能力实例中的 Service Definition 与 Consumer。`tool-fs` 通过 `ctx.fs` 发出文件操作请求。
    
- `**fs-local**`**、**`**fs-sandbox**`**、**`**fs-e2b**`（第二节第 3 段）：`ctx.fs` 的三个 Provider 实例，分别把文件操作交给本机、受控沙箱或远程环境。