# DeepSeek Harness 源码解读：插件运行时、Agent Loop 与 Session Event

前面的文章先解释了 DeepSeek Harness 为什么主动选择复杂，接着梳理了插件树、Cordis、Agent Loop 与 Session Log，第三篇从安装开始完成了一次实际操作。经过这三层铺垫，许多术语已经反复出现。源码篇要处理的问题变得明确：这些概念在代码里各自对应什么，它们之间究竟靠什么连接？虽然现在通过AI来直接编写代码已经非常成熟，但我们仍然有必要理解一下基本的代码组成，即使也是通过AI的辅助。

DeepSeek Harness 的 monorepo 包含大量插件、Provider 与界面组件，逐个介绍很容易变成目录说明。本文从运行入口开始，抓住几组决定系统结构的代码：Profile 怎样生成插件树，Cordis 怎样管理插件的依赖、通信与退出，Agent Loop 怎样规定任务的执行语义，Session Log 怎样同时支撑模型上下文和人看到的对话、轨迹。

## 运行入口与插件树装配

`dsh web` 的启动代码先解析 `web` Profile。Profile 声明要加载哪些 Bundle，Bundle 带来 Cordis 配置与对应代码；Profile 自身、用户目录和命令行传入的 Patch 随后覆盖这些配置。这个过程在前文已经解释过，源码中最有信息量的细节是：Profile 的根配置从空数组开始。

```
const PROFILE_ROOT_CONFIG = `
[]
`

const bundlePatches = profile.layers.flatMap(layer => layer.patches)
const patches = [
  ...bundlePatches,
  ...profile.patches,
  ...homePatches,
  ...overlays,
]
```

代码位于 `[apps/cli/src/profile-boot.ts](https://github.com/deepseek-ai/deepseek-harness/blob/47f943859bef60e4160492346772ded9b24f765a/apps/cli/src/profile-boot.ts)`。这个空数组规定了启动装配的方向。

Patch 合成以后，`boot()` 创建根 `Context`，安装 Cordis Loader，再把合成结果交给 Include 插件挂载。配置树中的每一行由 Loader 解析成 `Entry`，`Entry` 根据 `name` 动态导入模块，最后调用 Registry：

```
plugin = await tree.import(entry.name)
fiber = ctx.registry.plugin(plugin, entry.config)
await fiber.await()
```

原始调用位于 `[vendor/loader/src/config/entry.ts](https://github.com/deepseek-ai/deepseek-harness/blob/47f943859bef60e4160492346772ded9b24f765a/vendor/loader/src/config/entry.ts)`。从 Profile 到 Bundle、Patch、Entry，再到 Registry 与 Fiber，配置中的插件声明终于变成运行中的插件实例。

这条链路给 **Everything is a Plugin** 提供了源码层的含义。模型接入、任务运行、状态保存和 Web 界面都能进入同一套装配协议，默认产品与后续扩展共享加载机制。仓库里的插件数量只能描述规模，统一装配才是这个口号的架构内容。

Loader 对配置变更的处理也体现了这种统一。若 Entry 只改变普通配置，现有 Fiber 可以接收更新；若模块名、依赖或分组发生变化，Loader 会处置旧 Fiber，再启动新插件。新插件启动失败时，代码尝试恢复旧插件。这里的回滚对象是插件装配状态，已经写入文件或提交到外部系统的操作不在处理范围内。

## Cordis 的运行时对象

配置树完成了“选择哪些插件”，Cordis 接手以后需要回答三个不同的问题：插件在哪个范围内查找能力，某段代码怎样被识别为插件，以及一次具体挂载由谁管理。源码分别使用 **Context、Registry 和 Fiber** 承担这三个职责。

`Context` 是带作用域的服务容器。它在运行时由 Proxy 包装，插件读取 `ctx.fs`、`ctx.sessions` 或 `ctx.agentLoop` 时，属性访问会进入 Service 解析逻辑。`extend()` 创建继承当前环境的子 Context，`isolate(name)` 让某项 Service 在子树中使用新的作用域标签。同一个进程因此可以在不同子树中装入同名 Service 的不同实现，无需把所有能力塞进全局单例。`[Context](https://github.com/deepseek-ai/deepseek-harness/blob/47f943859bef60e4160492346772ded9b24f765a/vendor/cordis/src/context.ts)`

`Registry` 负责把多种代码形态归一为插件。函数、构造器或带 `apply()` 的对象都会被解析成可执行入口，Registry 为插件保存 Runtime 记录，并在每次 `ctx.plugin()` 时创建一个 Fiber。`[RegistryService.plugin()](https://github.com/deepseek-ai/deepseek-harness/blob/47f943859bef60e4160492346772ded9b24f765a/vendor/cordis/src/registry.ts)`

`Fiber` 对应一次具体挂载。它拥有自己的子 Context，保存配置、依赖声明、当前依赖实现和生命周期状态，同时收集插件运行期间登记的 Effects。一个插件模块可以在不同 Context 中挂载多次，每次挂载都有独立 Fiber。修改或卸载针对的是某个运行实例，不必抹去整个插件定义。

```
Profile / Bundle / Patch
          │
        Loader
          │
       Registry ── 识别插件定义
          │
        Fiber ───── 管理一次挂载
          │
        Context ─── 解析作用域内的 Service
```

这三个对象把插件系统中经常混在一起的“定义、实例和环境”分开了。后面的依赖替换和卸载清理都以 Fiber 为所有者，以 Context 为解析边界；如果缺少这种区分，动态插件很快会退化成一组全局注册表和难以追踪的回调。

## 依赖解析、事件通信与生命周期

插件通过 `inject` 声明自己需要哪些 Service。Registry 创建 Fiber 时会解析这份声明，Fiber 随后为每个 Service 查找当前作用域内的实现。依赖尚未出现，Fiber 保持 `PENDING`；全部就绪以后，插件进入加载并成为 `ACTIVE`。

依赖检查贯穿插件的生命期。Fiber 的 `_refresh()` 会根据当前 Provider Fiber 的 uid 生成一个 epoch：

```
let epoch = ''
for (const name of Object.keys(this.inject)) {
  const impl = this._store[name]
  if (!impl) {
    epoch = INACTIVE
    break
  }
  epoch += ':' + impl.fiber.uid
}
this._setEpoch(epoch)
```

原始实现见 `[vendor/cordis/src/fiber.ts](https://github.com/deepseek-ai/deepseek-harness/blob/47f943859bef60e4160492346772ded9b24f765a/vendor/cordis/src/fiber.ts)`。epoch 同时表示依赖是否齐备，以及当前依赖来自哪些 Provider。某项 Service 消失时，Consumer 的 epoch 变成 inactive；Provider 换成另一个 Fiber，epoch 同样改变。Fiber 会退出旧状态，清理旧 Effects，然后在新的依赖集合上重新加载。

第二篇出现的 **Spatiotemporal Composability** 到这里有了具体结构。空间组合性对应某个时刻的 Service 解析：不同 Context 根据作用域和 `inject` 建立当前依赖关系。时间组合性处理这组关系发生变化后的状态迁移：旧插件怎样退出，内部登记怎样清理，新依赖到位以后怎样重新进入。这个术语很大，落到代码里却是相当具体的 epoch、状态机与 disposer。

插件之间主要有两种通信方式。Service 适合直接调用一项稳定能力，Consumer 通过 `ctx.<name>` 取得实现，`inject` 同时规定这项调用的依赖前提。Typed Event 适合通知或流程拦截，发送方只知道事件契约，不需要持有监听插件。Cordis 为事件定义了不同分发语义：`emit` 同步通知全部监听器，`parallel` 等待并行结果，`serial` 依次执行，`bail` 在得到有效返回时停止，`waterfall` 让监听器包裹后续流程。`[EventsService](https://github.com/deepseek-ai/deepseek-harness/blob/47f943859bef60e4160492346772ded9b24f765a/vendor/cordis/src/events.ts)`

这些通信关系都服从 Fiber 生命周期。`ctx.provide()` 注册 Service 时把实现归到当前 Fiber；`ctx.on()` 注册事件监听器时也通过 Fiber Effect 保存撤销函数。插件退出以后，Cordis 删除 Service 实现并通知依赖方，同时从事件表中移除监听器。Consumer 退出、Provider 替换和事件回调清理由同一套所有权关系驱动。

**Reversible Effect** 的核心代码很短。`Fiber.effect()` 执行注册逻辑，收集它返回的 disposer；Effect 被主动释放或 Fiber 卸载时，disposer 按逆序运行。重复释放会直接返回，异步清理也会被 Fiber 等待。`[Fiber.effect()](https://github.com/deepseek-ai/deepseek-harness/blob/47f943859bef60e4160492346772ded9b24f765a/vendor/cordis/src/fiber.ts)`

它是一套内部资源所有权协议。插件注册监听器、Provider 或其他运行资源时，需要同时说明怎样撤销，Fiber 负责把撤销动作绑定到插件的生命期。数据库提交、生产命令和外部文件位于 Cordis 控制范围之外，需要事务、补偿动作或人工处置。Reversible 描述的是插件内部 Effects 可以随 owner 退出，现实世界不会因此自动倒带。

## Capability Seam 的接口边界

生命周期解决“谁在运行”，Capability Seam 继续处理插件与具体实现的关系。DeepSeek Harness 把一项能力拆成 Service Definition、Provider 与 Consumer，文件系统是源码中最容易看清的例子。

```
模型侧 read / write / edit
             │
          tool-fs       Consumer
             │
           ctx.fs       Service Definition
             │
  fs-local / fs-sandbox / fs-e2b
                         Provider
```

`FileSystem` 基类通过 `super(ctx, 'fs')` 定义 `ctx.fs` 的公开能力；`fs-local` 实现本机文件操作；面向模型的 `tool-fs` 声明依赖 `fs`，负责工具 Schema、参数校验和结果呈现。`tool-fs` 的依赖边界止于文件系统契约。`[FileSystem](https://github.com/deepseek-ai/deepseek-harness/blob/47f943859bef60e4160492346772ded9b24f765a/packages/fs/fs/src/index.ts)` `[fs-local](https://github.com/deepseek-ai/deepseek-harness/blob/47f943859bef60e4160492346772ded9b24f765a/packages/fs/fs-local/src/index.ts)` `[tool-fs](https://github.com/deepseek-ai/deepseek-harness/blob/47f943859bef60e4160492346772ded9b24f765a/packages/fs/tool-fs/src/index.ts)`

当 Profile 把 `fs-local` 换成沙箱实现，模型看到的工具与 `tool-fs` 的主要调用逻辑可以保持不变。Context 根据作用域解析新的 `ctx.fs`，Fiber 发现 Provider 身份变化以后重新装载 Consumer。Capability Seam 规定替换边界，Context 完成实现解析，Fiber 处理替换发生时的状态迁移，三个机制在一次 Provider 替换中接到了一起。

这种解耦取决于契约本身。Provider 必须覆盖 Consumer 依赖的语义，Consumer 也不能绕过 `ctx.fs` 读取某个实现的内部细节。源码给出了清楚的边界，边界是否足够完整，要在不同 Provider 和实际工作负载中继续检查。

## Agent Loop 的执行状态机

插件树和能力关系就绪以后，Agent Loop 负责把输入推进为一段持续任务。`AgentLoop` 自身也是 Cordis Service，它依赖会话、模型和工具等核心能力。默认实现 `ReactLoopAgent` 维护 `idle`、`maintenance` 和 `running` 等 Phase，Inbox 保存下一轮或下一步需要处理的消息。`[AgentLoop](https://github.com/deepseek-ai/deepseek-harness/blob/47f943859bef60e4160492346772ded9b24f765a/packages/core/agent-loop/src/index.ts)` `[ReactLoopAgent](https://github.com/deepseek-ai/deepseek-harness/blob/47f943859bef60e4160492346772ded9b24f765a/packages/core/agent-loop/src/agent.ts)`

主循环从 `kick()` 进入 `turn()`。Turn 开始时写入边界事件，`preStep()` 从 Inbox 领取输入并组装 System Prompt 与 Runtime Context。条件满足以后，Agent Loop 开启 Step，发起模型请求并处理模型返回的工具调用。骨架可以压缩为：

```
session.append('turn/start', { turn })

while (hasWork) {
  session.append('step/start', { turn, step })
  await runModelAndTools()
  session.append('step/end', { turn, step })
}

session.append('turn/end', { turn, reason })
```

Agent Loop 主动写入这些边界，前端读取的是 Runtime 已经记录的运行事实。Turn 规定一轮输入何时完成；Step 以一次模型请求为中心，并包含该次响应触发的工具执行。同一模型响应可以提出多个工具调用，它们依然位于同一个 Step。

工具调度把执行并发与日志顺序分开。`executeToolCalls()` 可以让允许并行的调用同时 dispatch，提交 `tool/result` 时按照模型给出的调用顺序推进；每个结果通过 `sourceEventSeqs` 引用对应 `tool/call`。取消发生以后，尚未开始的调用会写入合成错误结果，避免日志只留下无法配对的调用。`[tool-calls.ts](https://github.com/deepseek-ai/deepseek-harness/blob/47f943859bef60e4160492346772ded9b24f765a/packages/core/agent-loop/src/tool-calls.ts)`

默认循环具有明确骨架，同时预留了插件入口。`agent/pre-step` 可以在模型请求前增加或拒绝输入，`agent/request-error` 可以参与请求失败处理，`agent/turn-stopping` 在 Turn 准备结束时允许其他组件提交待处理工作。这些入口使用 Typed Event 的 `waterfall` 或 `serial` 语义。插件改变上下文、错误策略与停止行为时，Agent Loop 的内部 Phase 不需要向每个扩展暴露可随意修改的引用。

这部分源码说明了 DeepSeek Harness 怎样同时追求可插拔和共同运行语义。Agent Loop 可以被替换，循环周围可以挂载策略；进入系统的实现需要按 Session Event 写出 Turn、Step、模型消息和工具结果。这层协议让不同 Profile 产生的 Agent 能够共享恢复、观察和客户端界面。

## Session Log 的事件溯源模型

Session 位于 Agent Loop 的主路径上。用户消息、模型输出、工具调用、工具结果和运行边界在发生时就进入 Session，下一次模型请求会读取这份历史。审计能力来自运行过程中已经形成的事件，无需在任务结束后补写一套记录。

`Session` 内部持有私有数组 `log`。公开的 `events` 是冻结快照，已经返回给调用方的数组不会随着后续 append 增长，事件内容在接受时也被深度冻结。`append()` 为新事件分配 `seq = log.length`，验证数据能否无损序列化为 JSON，检查 Surface 元数据，最后把事件提交到日志。`[Session.append()](https://github.com/deepseek-ai/deepseek-harness/blob/47f943859bef60e4160492346772ded9b24f765a/packages/core/session/src/index.ts)`

```
const event = deepFreeze({
  type,
  seq: this.log.length,
  time: Date.now(),
  data: snapshotJsonValue(data),
  ...surfaceMetadata,
})

surfaceManager.validateNext(event)
log.push(event)
emit('session/event', session, event)
```

这段顺序很重要。候选事件先通过结构与 Surface 校验，进入 `log` 以后 append 即告提交。`session/event` 的观察者发生错误，只会被记录和隔离，不会把已经接受的事实改写回去。持久化被设计成插件职责：后端订阅 `session/event` 缓冲新事件，在 `session/flush` 时完成耐久写入。第三篇下载到的 JSONL 就是这种持久化表示之一。

只追加日志会遇到一个现实问题：模型上下文需要压缩、替换或重组。如果永远按照原始事件顺序把全部内容送给模型，Session 很快会失去可用性。DeepSeek Harness 在 append-only log 之上增加了 **Surface**，专门维护模型当前可见的有序消息。

目前可以进入 Surface 的核心事件包括 `user/message`、`assistant/message` 和 `tool/result`。普通消息携带 `surfaceOp: 'append'`，加入可见历史尾部；压缩等操作可以写入新的 replacement 事件，用 `{ op: 'replace', start, end }` 遮蔽一段既有 Surface 节点。原始事件仍保留在日志，新事件还必须通过 `sourceEventSeqs` 引用被遮蔽的来源。`[surface.ts](https://github.com/deepseek-ai/deepseek-harness/blob/47f943859bef60e4160492346772ded9b24f765a/packages/core/session/src/surface.ts)` `[SurfaceOp](https://github.com/deepseek-ai/deepseek-harness/blob/47f943859bef60e4160492346772ded9b24f765a/packages/core/session/src/types.ts)`

```
Session Log： E0  E1  E2  E3  E4  E5   （只追加）
                        └──────┘
Surface：     E0  E1  E2  E3  E4
                      ↓ replace
             E0  E1  E5             （模型当前可见）
```

`Session.deriveMessages()` 遍历 Surface 节点，将它们投影为下一次请求的 Message。Turn/Step 边界、流式 chunk、工具调用记录与其他控制事件继续存在于 Session Log，却不会进入模型消息。Agent Loop 的 `step()` 在构造请求时直接调用 `this.session.deriveMessages()`，历史事实与模型状态由此接上。`[deriveMessages()](https://github.com/deepseek-ai/deepseek-harness/blob/47f943859bef60e4160492346772ded9b24f765a/packages/core/session/src/index.ts)` `[Agent.step()](https://github.com/deepseek-ai/deepseek-harness/blob/47f943859bef60e4160492346772ded9b24f765a/packages/core/agent-loop/src/agent.ts)`

这也是 **Model-visible means logged** 在源码中的具体约束。`preStep()` 生成的 Runtime Context 会先作为 `user/message` 写入 Session，模型输出和工具结果随后回到同一事件流，下一次请求只能从已有 Surface 派生。插件可以改变模型看到的上下文，但这项改变需要留下事件来源。append-only 保护发生过的事实，Surface 允许当前上下文演进，两者结合以后，记录与上下文管理不必互相牺牲。

## Session Event 的客户端投影

第三篇看到的 Chat 和 Trajectory 读取同一段连续 Session Event。`ConversationNodeAssembler` 根据已经注册的 Event Definition 建立节点，再把节点交给不同 View Builder 生成 Snapshot。`[ConversationNodeAssembler](https://github.com/deepseek-ai/deepseek-harness/blob/47f943859bef60e4160492346772ded9b24f765a/packages/client/runtime/src/client/sessions/conversation-assembler.ts)`

Chat 注册 `target: 'chat'` 的 `ChatSnapshotBuilder`，把消息与工具卡片组织成适合连续阅读的对话。Trajectory 注册 `target: 'trajectory'` 的 `TrajectorySnapshotBuilder`，从同一批事件中整理模型请求、工具执行和运行时间，再形成调用路径与详情面板。`[ChatSnapshotBuilder](https://github.com/deepseek-ai/deepseek-harness/blob/47f943859bef60e4160492346772ded9b24f765a/packages/client/ui-conversation/src/client/conversation-nodes/chat-snapshot-builder.ts)` `[TrajectorySnapshotBuilder](https://github.com/deepseek-ai/deepseek-harness/blob/47f943859bef60e4160492346772ded9b24f765a/packages/client/ui-trajectory/src/client/trajectory-snapshot-builder.ts)`

```
                       ┌─ deriveMessages() ─→ 模型请求
append-only Session Log├─ Chat Builder ─────→ 对话
                       ├─ Trajectory Builder → 轨迹
                       └─ Persistence Plugin → JSONL
```

不同投影承担不同阅读目的。模型 Surface 会让压缩后的 replacement 遮蔽旧节点，人类对话不应因为上下文压缩而突然丢失已经看过的交流，因此 `surface.ts` 专门区分 append-origin event 与 replacement event，并在注释中说明人类 transcript 应以原始 append 事件为材料。Trajectory 保留边界、调用和时间，帮助构建者检查 Runtime 的执行过程。

`ui-trajectory` 自己同样是客户端插件。它注册节点定义、Trajectory View 与对应界面槽位；注册动作受 Effect 管理，插件卸载时可以移除整套视图。`[ui-trajectory apply()](https://github.com/deepseek-ai/deepseek-harness/blob/47f943859bef60e4160492346772ded9b24f765a/packages/client/ui-trajectory/src/client/index.ts)` 这让可观测性继续服从 Everything is a Plugin：Session Event 规定事实来源，View 插件决定构建者怎样阅读这些事实。

仓库还提供了更通用的 `SessionProjectionRegistry`。投影插件通过 `init()`、`apply(state, event)` 与 `view(state)` 描述自己的折叠规则，Registry 统一订阅 `session/event`，把每个已提交事件交给所有注册单元，并向客户端发布当前快照。投影注册本身也是 Effect，插件退出以后，相应 key 与缓存状态随之移除。`[session-projection](https://github.com/deepseek-ai/deepseek-harness/blob/47f943859bef60e4160492346772ded9b24f765a/packages/session/session-projection/src/index.ts)`

这套设计为成本、审批、任务目标或专业流程状态留下了扩展位置。新的观察面可以从同一事件历史建立，不必另造一份会话事实，再费力维持二者同步。

## 设计约束与架构判断

沿着源码主路径看，DeepSeek Harness 的复杂来自开放位置与配套约束。每开放一个替换位置，代码都会增加一条约束来维持清楚的运行关系：

|开放位置|对应约束|
|---|---|
|Profile 可以重组运行结构|Loader 把配置行统一转换为 Registry 与 Fiber 管理的插件实例|
|Provider 可以动态替换|Service Definition 规定契约，Context 解析作用域，Fiber 跟踪 Provider epoch|
|插件可以进入和退出|Effect 把注册与 disposer 绑定到 owner Fiber|
|插件可以介入运行过程|Typed Event 规定事件名称与分发语义，Agent Loop 保留 Turn/Step 边界|
|模型上下文可以扩展和压缩|模型可见内容进入 Session Event，Surface replacement 保留来源关系|
|客户端可以增加观察方式|Chat、Trajectory 与其他 Projection 读取同一 Session Event|

我在第二篇中把这套关系概括为“前一个概念带来自由，后一个概念补上约束”。源码确认了这种设计方法，而且比概念文档展示得更紧密。Capability Seam 依赖 Context 的 Service 解析和 Fiber 的 epoch 迁移；Reversible Effect 贯穿 Service、Event 与客户端 View 的注册；Session Log 直接参与下一次模型请求，它承担的职责远大于给轨迹提供数据。

这套架构的风险也能从同一批代码中看见。Service 契约如果覆盖不足，Provider 替换会把行为差异泄漏给 Consumer；插件若绕过 Effect 持有资源，Fiber 退出以后仍会残留状态；绕过 Session Event 注入模型的内容，会让恢复与复盘缺少来源。复杂性在这里表现为一套开发纪律，代码框架只能让违规更容易暴露，无法替所有插件作者保持纪律。

从 AI Workflow Owner 的角度，这些机制给出了较清楚的修改位置。运行组合由 Profile 与插件树决定，专业能力落在 Service 与 Provider，流程介入点通过 Event 出现，模型请求沿 Agent Loop 推进，运行事实进入 Session，面向人的检查界面来自 Projection。工作流发生异常时，构建者可以先判断问题属于组合、实现、策略、运行边界还是投影，模型只是这条链路中的一个责任主体。

读完这条源码主线，我认为 DeepSeek Harness 的研发团队试图建立一套 Agent Runtime 的内部秩序：插件可以替换，但替换要有契约；组件可以动态变化，但退出要清理自己拥有的状态；上下文可以演进，但影响模型的内容要进入事件历史；界面可以自由组织信息，但事实来源保持统一。

这些机制能否支撑庞大的生态，当前源码无法给出答案，但是它目前看起来也不如想象中那么复杂。它已经清楚地表明，DeepSeek Harness 所追求的目标超出了“做一个能写代码的 Agent”。Coding Agent 是当前装配好的一种产品形态，下面这套插件运行时、执行协议和事件溯源结构，才是它希望带到更多 Agent 工作流中的基础。