Skip to content

DeepSeek Harness 如何管理插件

DeepSeek Harness 把自己的架构概括为 Everything is a Plugin

插件包括 Agent Loop、模型适配器、工具、会话、沙箱、权限和界面能力,Cordis 负责管理它们的作用域、依赖和生命周期

模型调用与工具执行组成的循环只有几步,一个长期运行的 Agent 还要回答另一组问题:工具怎样临时加入,模型服务被替换时哪些组件需要重载,插件退出后怎样清掉已经注册的事件、Prompt 和定时任务,依赖消失时又怎样避免组件访问失效服务

Cordis 论文把这些问题归为动态组合,其中时间组合关心修改能否完整撤销,空间组合关心依赖变化后组件能否自动断开并重新连接

DeepSeek Harness 把 Cordis 用于 Agent 系统,使模型、工具和运行策略可以在同一个进程内重新组合

本文以 2026 年 8 月 14 日的开发者预览版为准,仓库版本为 0.1.0-rc.5,检视提交为 47f9438官方架构文档明确提醒接口可能发生不兼容变化

1. Agent Loop 的职责边界

带工具的 Agent Loop 如下:

带工具的 Agent Loop|760

DeepSeek Harness 的 Agent Loop 实现沿用这条主线,每一步开始前,插件通过 preStep 事件补充 Prompt、工具和上下文,模型返回的数据以流式事件写入 Session,工具结果也写回 Session,循环再决定继续或结束

Loop 只负责调用模型、执行工具和推进循环,模型选择、上下文压缩、工具注册、权限检查、持久化、子 Agent 与 UI 都在它的职责之外,agent-loop 包的说明也按这条边界组织代码

普通依赖注入容器可以完成初始连接,运行时变化还包括几种情况:

  • 文件系统插件已经注册了工具和事件,现在要卸载
  • Shell Provider 被替换,依赖它的工具还在执行
  • 某个 Session 需要更严格的权限,同名服务要换成受限实现
  • Agent 临时生成了一个插件,任务结束后必须清理干净

这些情况要求运行时同时记录组件何时可用、改过哪些状态、谁依赖它,以及退出应该按什么顺序发生

术语:Fiber

Cordis 为每个插件实例建立一个 Fiber,它不是 JavaScript 线程或协程,而是这个实例的生命周期档案与状态机,其中保存当前状态、依赖绑定、清理动作和正在进行的迁移 Pending 表示等待依赖,Loading 表示正在加载,Active 表示稳定可用,Unloading 表示正在清理,Failed 表示加载失败,Disposed 表示已永久移除

术语:Provider 与 Consumer

Provider 是某项服务的提供方,例如 Shell Provider 提供 shell,Consumer 是声明并使用这项服务的插件,Cordis 根据服务键和当前作用域记录两者之间的依赖关系

术语:Context

Cordis 的 Context 是运行时作用域,决定一个插件能看见哪些 Provider、它登记的可撤销修改归谁所有,以及局部覆盖在哪里生效,它不同于 LLM 的 context window,后者指模型一次请求能够读取的 token 范围

术语:epoch 与 inertia

epoch 在这里不是时间戳,而是当前候选 Provider 身份组成的摘要,inertia 是正在执行的加载或卸载 Promise,它让同一个 Fiber 不会同时跑两条生命周期迁移

实现中需要把它们拆成五类运行时事实,混在一个 dispose() 回调里很难处理:

运行时事实Cordis 中的载体它回答的问题缺失后的典型错误
当前可用性Context 中的服务表,以及 Fiber 的候选依赖 _store此刻从这个作用域解析 shell 会得到哪个 Provider组件在依赖缺失时仍然启动
已提交绑定Fiber 的 store,论文中称 committed当前这一轮插件代码究竟是用哪一组 Provider 启动的清理阶段误用刚出现的新 Provider
修改记录Fiber 的 _disposables这次启动已经注册了什么,应当以什么顺序撤销工具项、监听器和定时器泄漏
迁移进度Fiber 的 stateinertia当前正在加载还是卸载,期间发生的新变化应当接到哪里加载和卸载并发,产生半激活实例
依赖目标_runner.epoch候选 Provider 身份是否已经变化同名 Provider 被替换后 Consumer 没有重连

把 Shell Provider 记为 A₁,把依赖 Shell 的 Bash Tool 插件记为 B,一次替换可以分成下面几步

  1. A₁ 进入 Active,服务表中的 shell 指向 A₁
  2. B 解析依赖,候选 _store.shell 指向 A₁,目标摘要 epoch 记录 A₁ 这个 Provider
  3. B 开始加载时,把候选依赖复制到已提交的 store,随后注册工具和监听器,清理函数进入 _disposables
  4. A₁ 被撤下时,Cordis 先从公共服务表删除它,新的解析立即失败,再通知 B 刷新依赖目标
  5. B 的新目标变成缺失,状态进入 Unloading,但它的 store.shell 仍暂时指向 A₁,因此清理代码还能完成最后一次资源释放
  6. B 的清理栈倒序执行完毕,已提交绑定被清空,A₁ 才删除自己提交视图中的 shell 条目并释放底层资源
  7. 如果替代 Provider A₂ 已经出现,B 再以 A₂ 形成新的 epoch,完成一次全新的加载

下面把七个动作压缩成五个观察时刻,纵向比较目标、已提交依赖、生命周期状态和进行中的迁移:

Provider 快速替换时 Fiber 保存的四类状态|760

这段时序里有两个不同的事实,一个是现在想连接谁,另一个是当前实例当初用谁启动,前者可以在异步卸载期间继续变化,后者必须保持到清理结束

普通依赖注入通常只保存第一个事实,Cordis 同时保存两者,插件替换因此成为一次受控的旧实例退场和新实例入场

2. 一项工具怎样从配置走到模型请求

前一节说明了运行时为什么要保存生命周期状态,现在沿着仓库里的 bash 工具走一遍,从配置中的插件条目开始,一直走到模型下一次能够调用它

术语:Bundle

Bundle 是一组预先打包的 Cordis 配置条目,DeepSeek Harness 的基础 Bundle 提供模型适配器、工具、持久化、沙箱与权限等默认插件,后续 Bundle 和用户配置可以按条目 ID 覆盖它,Bundle 描述想挂载哪些插件,本身不是运行中的插件

从 Bundle 配置到模型看见 bash 工具|760

图中有三层事实,最左边的 Bundle 是配置期望,中间的 Fiber 和服务表是 Cordis 的当前运行状态,最右边的工具 schema 与 Prompt 段才是下一次模型请求能够看到的能力

基础 Bundle 的 cordis.patch.yml同时列出 bash-sandboxshell-envtool-bash 等条目,文件中的先后位置只方便人阅读,不规定插件的激活顺序

Cordis 读到这些条目后先为插件实例创建 Fiber,tool-bash 此时可以处于 Pending,它的依赖声明 inject 明确要求 toolsshellsystemPromptshellEnv 四项服务

bash-sandbox 加载成功后成为 shell 的 Provider,其他三个 Provider 也进入 Active 后,服务表才满足 tool-bash 的全部依赖,Cordis 随后把它从 Pending 推进到 Loading

tool-bash 的插件主体这时才运行,它向工具注册表加入 bash 的工具说明,也向 Prompt 注册表加入对应的使用规则,这两项修改都带有清理方法并记入 Fiber,Cordis 把这种可撤销修改称为 Effect

Agent Loop 在下一次模型请求前读取当前工具注册表和 Prompt 注册表,模型由此看到 bash,图中的路径也就在请求边界完成

配置文件中仍保留 tool-bash,但 shell 因故障退出时,期望集合没有变化,实际激活集合会收缩,tool-bash 回到 Pending,它注册的工具和 Prompt 也随卸载撤回

这正是配置顺序与激活顺序需要分开的原因,Bundle 只回答想装什么,服务表回答现在有什么,Fiber 状态回答哪些插件已经可以运行

放回完整 Harness,模型、Prompt、工具、会话和底层资源之间的关系如下:

DeepSeek Harness 中的插件关系|760

Cordis 不认识 LLM、Tool 或 Session,它只处理组件、依赖、作用域和副作用,Agent 领域的含义由 DeepSeek Harness 插件提供

Everything is a Plugin 的工程含义落在连接面上,每个组件需要声明自己提供什么、依赖什么,以及退出时怎样撤回注册

3. 插件卸载的两个问题

继续沿用第 2 节的例子,A 是提供 shellbash-sandbox,B 是依赖它的 tool-bash,B 激活后注册 bash 工具及配套提示词

加载可以按 A、B 的顺序执行,卸载 A 时直接调用它的 dispose 会留下两个问题:

  1. B 仍认为 Shell 工具可用,下一次调用可能访问已经关闭的进程服务
  2. B 注册到工具表、事件总线和 Prompt 注册表里的内容可能继续存在

给每个插件加一个清理函数只能处理局部回收,插件作者仍要记住所有注册动作,并手工维护跨插件的退出顺序

如果异步初始化只完成了一部分,清理函数还需要知道哪些步骤已经生效

常见故障可以按发生位置进一步区分:

故障发生过程需要的约束
注册泄漏插件从工具表退出,但事件监听仍留在总线上所有共享修改都登记为 Effect
释放后使用Provider 先关闭连接,Consumer 的卸载回调随后还要使用它Provider 等待依赖者停稳后释放
半初始化插件注册了工具,下一步加载 Prompt 时抛错每完成一步就登记局部逆操作
过期异步结果旧版插件的异步初始化晚于新版插件返回在异步边界重新检查目标版本
交叉重载依赖连续变化两次,两个 reload 同时写同一注册表用一个进行中的迁移 Promise 串行化

局部清理与依赖排序解决的是不同层面

B 知道怎样删除自己注册的 bash,却不知道 A 何时可以关闭底层进程;A 知道怎样关闭进程,却不知道还有多少 Consumer 正在执行退出逻辑

Cordis 让 B 持有自己的逆操作,让运行时持有 A → B 的依赖边,退出协议才能同时具备动作和顺序

Cordis 把问题拆成两个维度:

维度要回答的问题Cordis 的机制
时间组合这个组件对外部状态做过哪些修改,怎样撤销Effect 与逆操作
空间组合当前依赖由谁提供,依赖变化后谁要退出或重载Coeffect、服务表与 Fiber

术语:Effect 与 Coeffect

Effect 记录插件向共享状态做出的修改及其撤销方法,Coeffect 记录插件成立所需的外部条件,在 Cordis 中主要表现为服务依赖,前者回答改了什么,后者回答依赖什么

时间组合负责回收修改,空间组合负责安排依赖顺序

缺少前者,旧组件会留下事件监听和工具项,缺少后者,Provider 可能在 Consumer 完成清理之前释放资源

4. Effect 与逆操作

论文第 9 页先把 Effect 表示成返回新状态和逆操作的函数:

Cordis 论文第 9 页的 Effect 函数形式|760

便于阅读的伪类型是 effect: Γ → (Γ', undo),其中 Γ 表示执行前的状态,Γ' 表示执行后的状态,undo 负责撤销这次变化

论文要求的关系可以写成 undo(Γ') ≃ Γ,符号 表示可观察等价

这里约束的是这一次 Effect 到达的状态,并不要求 undo 对任意输入都是全局逆函数

例如 addListener(handler) 的逆操作是删除同一个 handler,它只需要对刚才注册出的监听关系成立,不能拿来撤销另一个插件注册的同名函数

插件执行注册动作时,必须同时交回对应的清理方法

下面是简化后的示意代码:

ts
ctx.effect(() => {
  toolRegistry.add("search_logs", handler)

  return () => {
    toolRegistry.remove("search_logs")
  }
})

术语:disposer

disposer 是清理回调,注册动作成功后把它交给 Fiber,插件退出时再由 Fiber 调用,例如删除工具、取消监听或关闭资源

一个插件通常会产生多项 Effect,例如提供服务、监听事件、增加 Prompt 片段和启动定时器,Cordis 收集每项 Effect 的撤销方法,退出从后注册项开始发起:

text
加载:E1 → E2 → E3
卸载:E3⁻¹ → E2⁻¹ → E1⁻¹

术语:LIFO

LIFO 是 Last In, First Out,即后进先出,最后登记的清理动作最先开始,适合资源逐层建立后再逐层退出

后注册的状态可能引用先注册的状态,如果先撤销底层资源,后续清理就会访问失效对象

它与函数调用栈采用相同的退出顺序,用来保持资源之间的嵌套关系

假设三个动作依次为创建临时目录、启动使用该目录的进程、注册指向该进程的工具,正确退出顺序只能是删除工具、停止进程、删除目录

正序清理会先删目录,进程停止回调便可能找不到工作路径,工具也会在一段时间内继续指向已经损坏的执行环境

Fiber.effect() 中,同一个 Effect 产生的 disposer 进入局部数组,清理时通过 reverse() 取出,遇到异步 disposer 后沿 Promise 链顺序等待

Fiber 顶层的多个 Effect 也先经过 DisposableList.clear() 逆序取出,_unload() 随后用 Promise.all 启动并等待它们,因而严格的串行 LIFO 保证落在单个 Effect 内部,多个独立顶层 Effect 的异步清理可以重叠

如果两个清理动作必须严格先后执行,应把它们放进同一个 Effect 返回的 disposer 链,或显式建成 Provider 与 Consumer 的依赖关系,不能只依赖两个顶层 ctx.effect() 的登记次序

如果 Effect 返回 Promise,Cordis 等待其解析后再接收 disposer;如果返回同步或异步迭代器,每次 yield 都可以交回一项已经完成步骤的 disposer

迭代式初始化适合分段取得资源:

ts
ctx.effect(async function* () {
  const workspace = await openWorkspace()
  yield () => workspace.close()

  const watcher = await startWatcher(workspace)
  yield () => watcher.stop()

  const index = await buildIndex(workspace)
  yield () => index.dispose()
})

buildIndex 抛错,运行时已有两个可撤销节点,于是先停止 watcher,再关闭 workspace,未完成的 index 没有虚构出的清理动作

依赖目标也可能在 await startWatcher() 期间变化,Fiber 会在迭代边界比较 epoch,发现当前执行已经过期便停止继续推进,随后走卸载路径

因此异步 Effect 的安全性来自两个配合:已经完成的步骤立即留下逆操作,尚未开始的步骤在目标失效后不再继续

Cordis 的 Fiber 源码还处理异步 Effect 和迭代式初始化,插件每完成一步就交出这一阶段的清理方法,后续步骤失败时,运行时只回滚已经完成的部分

已经发出的异步操作会运行到可处理的边界,Cordis 再根据当前依赖目标决定是否卸载

Effect 提供结构化清理协议,但不验证撤销方法是否正确,插件注册工具后如果返回空函数,运行时仍会留下脏状态

它也无法撤回已经越过进程边界的事实,网络请求可以取消等待,却未必能取消服务端已经提交的写入,邮件和支付更需要幂等键、确认阶段或补偿事务

论文中的恢复结论以逆操作正确为前提

5. Coeffect 与依赖变化

Effect 描述组件改变了什么,Coeffect 描述组件需要什么,每个插件声明一组依赖,运行时维护一张服务表,所有依赖键都能解析到 Provider 时,插件才进入 Active 状态

设当前作用域可见的服务映射为 Σ,插件依赖声明为 dΣ ⊧ d 表示 d 中每个必需键都能在这条 Context 链上解析到活跃 Provider

满足关系只决定能否启动,组件启动后还要保存一次解析结果,因为 Σ 可以继续变化

以仓库中的 tool-bash 为例:

组件声明
Shell Provider提供 shell 服务
Tool Runtime提供 tools 服务
System Prompt Registry提供 systemPrompt 服务
Bash Tool 插件依赖 toolsshellsystemPromptshellEnv,再以 Effect 注册 bash 工具和提示词段

只有四项依赖都能在当前 Context 解析时,Bash Tool 插件才执行主体,工具 schema 与提示词段在同一次 Fiber 激活中登记

Shell Provider 消失时,Bash Tool 的依赖目标变成缺失,它登记的工具与提示词在同一次卸载中撤回

论文第 20 页用满足关系 定义了三种通知结果:

Cordis 论文第 20 页的 Coeffect 状态分类|760

对应到运行时:

  • Activating:缺失的依赖变得可用,组件可以加载
  • Deactivating:已经绑定的依赖失效,组件需要卸载
  • Neutral:服务表发生变化,但不影响该组件当前绑定

插件不必轮询服务,也不用在每个回调里检查依赖是否存在

依赖解析发生时,Cordis 记录组件实际绑定的 Provider,后续变化与这份已提交的依赖视图比较,再决定是否触发生命周期切换

实现中的通知链条位于 reflect.tsfiber.ts

  1. ctx.provide("shell", value) 把 Provider 记录到当前隔离域的服务表
  2. notify() 遍历存活 Fiber,只检查声明过 shell 且位于相容隔离域的组件
  3. _checkImpl("shell") 重新解析 Provider,把候选结果写入 Fiber 的 _store
  4. _refresh() 把所有依赖 Provider 的 Fiber UID 连接成 epoch
  5. _setEpoch() 比较新旧目标,决定保持、卸载或重新加载

epoch 比较的是 Provider 身份组合,例如依赖 shellfs 的 Fiber 可能得到 :41:73,缺少任何必需依赖时则得到 __INACTIVE__

只要目标摘要相同,通知就是 Neutral;摘要从 __INACTIVE__ 变为 Provider 组合时进入加载;摘要从一组 Provider 变为另一组,即使依赖仍全部满足,也要先卸载旧实例再加载新实例

provide() 自身也通过当前 Fiber 的 Effect 注册,所以 Provider 插件退出时,服务表删除、Consumer 通知和最终引用释放都属于同一条可等待的清理链

这里的遍历并未预先维护一张固定拓扑表,依赖边来自 Fiber 的声明与当前隔离域,Provider 变化时按条件筛选受影响的 Fiber

这种做法适合插件集合经常变化的运行时,代价是服务变化需要扫描相关 Fiber,规模、通知频率和监控数据会影响实际开销

Context 允许同一个服务键在不同作用域中解析到不同实现,例如主 Agent 使用完整文件系统,子 Agent 的 Context 则把 fs 解析成只允许访问工作目录的 Provider

调用代码仍然请求 fs,具体实现和隔离规则由当前 Context 决定

Context 还可以携带拦截信息,服务绑定保持不变,权限、租户或审计策略在调用时生效

隔离与拦截需要分开看

隔离改变解析结果,例如子 Agent 的 fs 键指向另一个 Provider,因此会改变 epoch 并触发生命周期切换

拦截保留原 Provider,在访问路径上附加策略,例如记录调用或缩小参数范围,它未必要求 Consumer 重载

如果权限变化需要更换底层资源所有权,就应建模成 Provider 替换;如果只是对同一服务增加调用检查,拦截更合适

6. Fiber 生命周期

Cordis 为每个插件实例创建一个 Fiber,Fiber 保存插件的 Context、依赖声明、已绑定的 Provider、Effect 清理栈和当前生命周期状态

论文使用四个概念字段解释算法,TypeScript 实现为了减少重复存储采用了稍有不同的表示:

论文概念TypeScript 字段内容
target_runner.epoch_store当前解析得到的候选 Provider 组合,以及压缩后的身份摘要
committedstore当前插件实例启动时承诺使用的 Provider 集合
dispose_disposables当前实例已经积累的逆操作
inertiainertia正在进行的 reload 或 unload Promise
生命周期statePending、Loading、Active、Unloading、Failed 或 Disposed

这个映射解释了为什么源码里找不到一个名为 target 的完整对象,目标的成员保存在 _store,目标是否变化则由 epoch 判断

Fiber 如何响应依赖变化|760

_refresh()_setEpoch() 组成状态机入口

_refresh() 逐项检查注入声明,任一必需依赖缺失就生成 __INACTIVE__,全部存在则把 Provider UID 编入摘要

_setEpoch() 先写入最新目标,再查看当前是否已有 inertia

没有迁移在进行时,从缺失变为满足会调用 _reload(),其余目标变化会调用 _unload()

已有迁移在进行时,它不会并行启动第二条迁移,最新 epoch 已经被记录,当前 Promise 到达边界后再决定下一步

回到第 1 节的替换图,若 t2 之后 A₂ 又消失,t3 观察到的目标仍是缺失,B 直接停在 Pending,不会先加载一个已经过期的 Provider

inertia 因此没有冻结外部世界,它只保证同一 Fiber 每次最多有一条生命周期迁移在执行,期间的变化通过最新 epoch 合并到下一个决策

加载开始时,_reload() 先把 _store 复制到 store,再执行插件主体,这一步给整个激活期建立稳定依赖视图

插件主体成功且 epoch 未变化,Fiber 进入 Active;主体抛错时,错误被记录,目标被置为缺失,已登记 Effect 被回滚,Fiber 进入 Failed

实现不会在服务表下一次抖动时悄悄清掉这项初始化错误,显式 update() 会清除 _error 并重启 Fiber,这让配置或代码错误保持可见,避免无界自动重试

主体执行期间若目标已经变化,刚完成的实例不会对外宣告为稳定 Active,它会接着进入卸载

卸载开始后,_unload() 倒序清空 _disposables,然后清除 store,目标仍可满足时紧接一次 reload,目标缺失时回到 Pending

Provider 退出时必须先处理依赖顺序,仍以 A 提供服务、B 消费服务为例:

Provider 退出时的依赖卸载顺序|760

A 先对新的消费者不可见,但资源暂时保留,现有消费者卸载完成后再释放

这段等待用来避免 use-after-free,B 清理自己的工具或事件时仍可访问 A

provide() 的 disposer把这项原则落实成三段式撤回:

  1. 从公共 Provider 表删除 A,阻止新的 Fiber 绑定它
  2. 通知全部受影响 Consumer,并等待它们各自的 fiber.await()
  3. Consumer 停稳后,删除 A 在旧提交视图中的最后可见项,让 A 完成资源释放

第一步建立进入屏障,第三步建立退出屏障,中间阶段允许旧消费者只为清理目的继续访问 A

如果一开始就从所有视图抹掉 A,B 的 disposer 可能在关闭子进程、注销远端句柄或刷新缓存时拿不到 Provider

如果等所有消费者自行发现以后才停止新解析,另一条并发路径又可能在撤回期间建立新绑定

两段可见性配合等待屏障,才构成安全撤回

如果新的 Provider 已经出现,B 会在卸载后用新的依赖重新加载

重连包含一次完整的生命周期切换,旧实例先退出,新实例再按一致的依赖视图加载

生命周期状态适合观测,不适合单独推断资源是否已经释放

例如 Unloading 表示清理已开始,store 仍可能保留旧 Provider,只有等待 inertia 结束后,调用方才能把 Fiber 视为停稳

管理界面若只显示状态名而不显示目标 epoch、已提交 Provider 和进行中的迁移,就难以解释一个组件为何反复 Pending 或为什么长期停在 Unloading

7. Context 的作用域

传统依赖注入容器通常把 Context 当作对象表,Cordis 的 Context 还记录作用域、Effect 和依赖关系

Context 形成一棵可继承的运行时作用域树,子节点能看到父节点公开的 Provider,同时可以通过隔离域把某个服务键重新映射到局部实现

Context 把生命周期所有权落到作用域树上,对象查找只是表层用途

在某个 Session Context 中注册的工具,其 Effect 归这个 Context 对应的 Fiber 所有,Session 结束时可以沿这条所有权边回收,其他 Session 的同名注册不受影响

子 Context 可以继承父级能力,也可以增加局部 Provider 或拦截规则,在 Agent 系统中常用于下面几类边界:

场景Context 解决的边界
多 Session每个会话拥有独立的事件、Prompt 和运行状态
子 Agent继承部分能力,同时替换文件系统、模型或权限 Provider
沙箱同名 Shell 或 FS 服务在受限作用域中指向代理实现
临时插件只在当前任务的 Context 中注册工具和界面,不污染其他会话
测试用局部 Provider 替换外部服务,结束后随 Context 一起回收

以子 Agent 为例,父 Context 可以提供模型路由、只读仓库视图和日志服务,子 Context 再提供自己的 Session 与受限 Shell

子 Agent 的工具只声明 shell,解析时得到子级 Provider,父 Agent 同一个工具实例若位于父 Context,则得到父级 Provider

两者的业务代码可以相同,Fiber 的作用域和依赖摘要不同,退出时也各自清理局部注册

局部覆盖还需要处理恢复,子级 Shell 退出后,是否回退到父级 Shell 由隔离规则决定,若解析结果从子级 Provider 变为父级 Provider,Consumer 会收到一次 Provider 身份变化并重载

Cordis 要求共享状态经过 Context,插件如果绕开 Context 修改全局变量,或者直接挂接无法追踪的监听器,运行时就无法恢复或隔离这些状态

因此边界设计先于 API 设计,需要问清一个能力属于全局、工作区、会话还是单个 Agent,再决定在哪个 Context 提供它

把所有服务放在根 Context 虽然容易接线,却会让会话级卸载失去所有权依据,也使权限替换退化成全局开关

8. Cordis 与 Harness 的对应关系

DeepSeek Harness 把 Agent 领域的对象映射到 Cordis 的运行时概念:

Cordis 概念Harness 中的对象运行时作用
Service / ProviderLLM、文件系统、Shell、Session Store、沙箱允许实现按作用域替换
Effect工具、事件、Prompt 片段、UI 插槽的注册插件退出时成组回收
Coeffect插件声明的服务依赖依赖就绪后激活,失效后退出
Fiber一个插件实例保存依赖绑定与清理栈
Context全局、工作区、会话或子 Agent 作用域控制可见能力和局部覆盖
Loader / HMR配置加载与热更新把期望配置收敛为实际插件集合

Agent Loop 每一轮都从当前运行时取得模型、Prompt 和工具

源码中的一次执行分成 Turn、Step 和 Request 三层边界

边界何时开始何时结束主要记录
TurnAgent 从 Inbox 领取一批下一轮输入本轮完成、阻塞、中止或失败turn/startturn/end
Step准备一次模型调用及其工具结果模型结束,或工具结果要求下一步step/startstep/end
Request系统提示、工具 schema、消息历史和模型参数冻结流结束或进入重试request/header、流分片、assistant/message

每个 Step 开始前,preStep() 先领取 Inbox 消息,再调用 System Prompt 组件组装有序段落、变量和工具 schema,随后把动态运行时上下文投影成带来源的 user 消息

插件还能通过 agent/pre-step waterfall 接受、拒绝或改写这次输入,只有 Enter 决策才会产生 step/start

术语:waterfall

waterfall 是顺序执行的中间件链,前一个处理器可以把结果交给后一个处理器继续修改,最后得到一个确定的组装结果

buildRequest() 在请求边界重新解析模型 Provider 的精确配置,并把最终系统提示、工具 schema、模型与参数写成规范请求头

插件在两个 Step 之间变化,可以影响下一次组装;已经开始流式返回的 Request 使用冻结快照,不会在半个响应中改换工具表或模型参数

模型输出的每个 chunk 先写入 Session,流结束后再组装出一条 assistant/message,若包含 Tool Call,调度器执行工具并把结果排入下一 Step 的上下文

因而 Cordis 管理的是请求边界之间的能力集合,Agent Loop 提供能力变化可以安全生效的离散边界

Session 日志采用追加式事件流,模型可见的历史、界面展示、恢复与持久化都从同一份事件推导

需要区分原始事件日志和模型可见 surface

术语:surface

surface 是 Session 日志中面向模型的消息投影,它决定下一次请求会带上哪些消息,不等于原始事件日志,也不是浏览器 UI

原始日志保存轮次边界、步骤边界、流分片、调用结果与恢复事实,deriveMessages() 只投影 user/messageassistant/messagetool/result 等 surface 条目

压缩插件可以追加一条 replace 操作,让旧消息不再出现在未来模型输入中,旧事件仍保留在日志里,审计和恢复不会因为上下文压缩而丢掉来源

原始日志记录实际发生过什么,surface 决定下一次模型能看到什么

插件如果要影响下一轮模型输入,必须把结果写入 Session 或参与 Prompt 组装,单纯修改内存对象不会进入模型上下文

例如一个权限插件在内存中把当前状态改成只读,如果它只拦截工具执行,模型仍可能继续看到写入工具的旧 schema,只是调用时被拒绝

若希望模型从下一步开始不再提出写入操作,插件还要在 Prompt 或工具组装阶段隐藏相关 schema

执行权限与模型可见能力属于两个控制面,Harness 允许它们协作,但两者不能互相替代

工具执行有独立的调度规则,允许并发的调用可以在上限内同时运行,标为 exclusive 的调用形成屏障,前面的调用结束后它才开始,后面的调用也要等它完成

模型发出的一组 Tool Call 因而不会无条件并发,结果仍按原始顺序写回 Session

executeToolCalls()把调度和提交分开处理

并行工具可以在一个有界 rolling pool 中重叠执行,完成结果先放入对应槽位,只有从队头开始连续就绪时才按模型原始顺序提交

假设模型依次调用 read_aread_bwrite_c,前两个可并行,read_b 先结束也要等 read_a 的槽位就绪后再写入 Session,write_c 若为 exclusive,则等待并行池排空后单独执行

每个尚未启动的调用都会在派发前重新查询当前 Tool Registry 的执行模式,因此前一个工具若动态修改后续工具的注册或策略,未启动调用采用新规则

中止发生时,调度器停止补充新调用,等待已经启动的调用排空,并为跳过的调用记录合成错误结果,回放时仍能保持 Tool Call 与 Tool Result 配对

内部调度器故障则保留已经写入的 tool/call 事实,不伪造一个并未得到的业务结果,恢复过程据此区分未开始和结果未知

这里仍有一个边界,运行中工具能否被强制停止取决于工具实现是否响应 AbortSignal,Cordis 的 Fiber 卸载不会自动撤销一个已经提交到外部系统的写操作

模型适配器、工具、压缩策略和持久化插件都能更换,Agent Loop 无需包含它们的实现分支

服务合同规定组件怎样连接,生命周期规定组件怎样进入和退出,Agent Loop 只消费当前可用的能力

System Prompt 也采用注册表组合,Harness 身份、部署 persona、工具指导、动态 Context 和工具 schema 由不同插件贡献

组装时先合并全局与 Agent 作用域条目,局部条目可以遮蔽全局条目,再按稳定规则排序并执行 system-prompt/assemble waterfall

工具 schema 的确定顺序不仅影响可读性,也影响 KV Cache 前缀,运行集合未变时保持次序稳定,插件新增、移除或重新排序时,从第一个变化 token 开始失去缓存复用

术语:KV Cache

KV Cache 是模型对已计算请求前缀保存的注意力中间结果,下一次请求前缀相同时可以复用,Prompt 或工具 schema 越早发生变化,可复用的前缀就越短

插件卸载后,它登记的 Prompt 段和工具 Provider 随 Fiber 一起撤销,下一 Step 的请求头会记录这次前缀变化

Cordis 的 HMR 实现会保存旧模块,再尝试导入并加载新版本,新版本失败时可以退回原插件

术语:HMR

HMR 是 Hot Module Replacement,即进程不退出时替换已经加载的代码模块,Cordis 还要负责清理旧模块留下的工具、事件和服务注册

热更新仍依赖 Effect 清理和依赖排序,否则回退只能恢复代码引用,不能恢复运行时状态

一次可靠热更新包含两个层次,Loader 管理模块版本与失败回退,Cordis 管理旧版本已经留下的运行时状态

若新版模块导入成功但插件主体在第三个 Effect 抛错,Fiber 先回滚新版已经完成的注册,Loader 再恢复旧模块,旧版插件重新加载

只有模块回退而没有 Effect 回滚,注册表会同时保留新旧条目;只有 Effect 回滚而没有模块回退,运行时则停在没有可用实现的状态

9. 临时插件

DeepSeek Harness 提供可选的 tool-cordis 扩展,启用后,模型可以检查当前运行时、定义插件版本、运行、更新、停止或删除插件

当前源码把 Harness 的插件面拆成七个模型工具,检查占三个,生命周期操作占四个:

工具改变的状态是否执行代码
cordis_inspect_list列出 Host 与 Client 的只读检查 Provider 及查询 schema
cordis_inspect_query按 Provider 声明的 schema 查询服务、事件、Builtin 或 UI Slot
cordis_inspect_self查看当前 Session 所有的动态 Plugin、Package、运行状态与诊断
cordis_define新建 Plugin 的首个不可变 Package,或给已有 Plugin 追加 Package否,只校验参数与语法
cordis_run运行指定 Package,或把当前 Plugin 切换到另一 Package
cordis_stop等待 host Fiber 完全停稳,并从页面撤回 browser 半只执行清理
cordis_undefine必要时先 stop,再删除 Plugin、全部 Package、授权和版本指针只执行清理

定义和运行分成两步很重要,define 产生一个可审查的候选 Package 和会话卡片,用户或 Agent 再明确触发 run

Plugin ID 表示一条稳定身份,Package ID 表示这条身份下的一份不可变 host 或 client 源码版本,修改代码不会覆盖旧 Package,而是给同一个 Plugin 追加新 Package

runner 还分别记录 currentPackageIdnextPackageId 和每次尝试的 pluginRunId

currentPackageId 只在 host 与 client 两半全部成功后提交,nextPackageId 表示正在尝试的目标,pluginRunId 区分同一个 Package 的不同运行尝试

这与 Fiber 的候选依赖和已提交依赖采用同一种思路,目标先记录,成功后再提交,诊断时可以分清当前稳定版本和正在启动的版本

run 模式用于首次激活、重启当前版本或回滚,update 模式用于切换到不同 Package

更新会先撤回旧 Run,再启动目标 Package,目标失败时 currentPackageId 仍指向最后成功版本,但旧 Run 不会自动复活,Agent 需要修复目标后重试,或显式用 run 回滚

同一个动态包再次 run 会重新投递当前版本,刷新后的浏览器页面因此可以重新取得 client 半,stop 只停止实例而保留定义、授权和版本指针,之后还能再次运行,undefine 才删除整条 Plugin 身份

排查一批结构固定的日志时,Agent 可以临时定义一个 search_project_logs 工具,把目录限制、解析逻辑和结果格式放进插件

插件加载后,工具出现在当前 Context 的注册表中,任务完成后插件停止,工具、事件和 Prompt 片段随 Effect 清理栈一起移除

动态包的模型可见性也遵循 Step 边界

cordis_run 自身的结果先写入 Session,包注册的新工具和 Prompt 在下一个组装阶段进入请求,新 schema 不会倒流进刚刚产生 cordis_run 调用的那次请求

停止时,runner 等待 Fiber 停稳后才返回工具结果,因此后续 Step 不应再看到已撤下的贡献

运行的完成语义更复杂,纯 host Package 可以在工具调用内完成激活,带 client 半的 Package 可能先返回 startingawaiting-approval,最后结果通过运行状态和 steering context 回到后续 Step

因此 cordis_run 返回成功只表示请求已被接受时,Agent 还要检查 currentPackageId 与 Run 状态,不能把异步启动误报成完整激活

tool-cordis 完成的是进程内运行时重配置,模型权重不发生变化,临时能力也不会跨重启保留:

  • 动态插件默认存在于内存,进程结束后消失
  • 这项机制本身不修改基础 Bundle 和磁盘配置
  • VM 执行环境不是恶意代码的安全边界,权限仍要由外部沙箱和 Provider 收口
  • Cordis 只能撤销登记过的 Effect,已经发出的网络请求、邮件或扣款不能物理回滚

README 还给出更窄的所有权边界,动态包在共享 DSH 进程中运行,可以跨同一会话的后续轮次保持活跃,控制操作以定义它的会话为界,进程重启后定义与实例都消失

host façade 只暴露白名单能力,其中包括 ctx.effect()ctx.on()ctx.provide() 与受守卫的 tools.register(),动态代码仍要把自定义资源释放写进 Effect

白名单缩小了误用框架内部机制的范围,却没有把 VM 变成恶意代码隔离层,host realm helper 仍可能触达 Node 能力,启用它应采用与授予 Bash 相近的信任标准

外部动作需要业务层另行设计,可以先暂存操作,确认后再提交,已经提交的动作则使用补偿流程,例如退款、撤回或追加反向记录

一个 undo 回调无法让外部世界回到原样

10. 形式化保证与前提

Cordis 论文的形式化结论针对运行时状态,不涉及模型能力提升,主要覆盖六项性质:

性质含义
Preservation每一步生命周期转换后,运行时仍满足类型与状态约束
Recovery exactness正确的逆操作可以恢复 Effect 之前的可观察状态
OrderingProvider 的资源会等依赖它的 Consumer 退出后再释放
Resolution coherence活跃组件看到的 Provider 与已提交依赖视图一致
Progress在有限、无环且异步操作可完成的条件下,系统会进入静止状态
Confluence动态加载和卸载稳定后,结果等价于只把最终活跃组件按依赖顺序从头加载一次

这些性质之间有先后关系

Recovery exactness 先要求每个局部 Effect 能撤销,Ordering 再保证逆操作调用时依赖资源仍存活,Resolution coherence 保证插件运行期间看到的是已提交 Provider,Progress 和 Confluence 才能讨论整个动态系统最终是否停稳以及停稳在哪里

逐项换成运行时问题:

  • Preservation 约束每次迁移不能产生 Active 但缺少已提交依赖的 Fiber
  • Recovery exactness 约束一次工具注册加一次正确撤销,观察上等价于从未注册
  • Ordering 约束 Shell Provider 的最后释放晚于 Bash Tool 插件的清理
  • Resolution coherence 约束 B 在一次激活期内不能初始化时使用 A₁,清理时突然解析到 A₂
  • Progress 约束依赖变化停止后,有限个 Fiber 的异步迁移最终结束
  • Confluence 约束系统最终状态由稳定的插件集合决定,不由中间经历过多少次可撤销装卸决定

Confluence 只约束运行时的收敛结果,不同的动态变化历史稳定后,可以得到同一个可观察状态

例如初始为空,历史一依次加载 A、加载 B、卸载 A,历史二只加载 B,如果 B 最终不依赖 A,而且 A 的所有 Effect 都被正确撤销,两条历史的稳定状态应当可观察等价

若 A 曾向外部发送邮件,即使运行时注册表已经恢复,外部收件箱仍留下差异,这项动作超出可撤销状态域,不能用 Confluence 为它背书

模型生成的答案,以及运行时外部已经发生的副作用,都不在这项保证内

这些结论依赖一组严格前提:

  • 共享可变状态通过 Context 表达
  • 每个 Effect 都提供正确的逆操作
  • 相互独立的 Effect 及其逆操作可以交换顺序
  • 依赖图无环,Fiber 数量有限
  • 异步步骤最终完成,迭代过程有界
  • 讨论的是可观察等价,不要求内存中的每个对象身份完全相同

前提可以按责任方归类:

前提主要责任方如何被破坏
Effect 具有正确逆操作插件作者注册监听后返回空 disposer
独立 Effect 可交换插件协议设计者两个插件暗中修改同一个无版本全局槽位
依赖声明完整插件作者与类型约定代码读取 ctx.shell 却没有声明注入
依赖图满足论文限制Bundle 与运行时A 等 B,B 又等 A
异步过程最终有界Provider 与工具作者初始化 Promise 永不完成或忽略中止
可变状态位于受管状态域Harness 架构插件直接修改模块级单例且不登记 Effect

形式化系统能验证的是在这些假设成立时状态机如何组合,无法从一个任意 JavaScript 回调中自动证明它真的删除了所有监听器

工程上仍需要故障注入测试,例如在每个异步初始化点抛错,检查注册表是否恢复;替换 Provider 时延迟 Consumer disposer,检查旧资源是否提前释放;连续改变依赖目标,检查最终只留下最新 Provider 的实例

循环依赖、错误的 undo、无法结束的异步任务和未登记的全局状态都会破坏结论

恶意插件需要进程、容器或操作系统级隔离,生命周期管理不承担代码安全

论文目前是持续修订的预印本,案例来自 Koishi 插件社区,论文描述的是 Cordis v4,而 Koishi 当时仍使用 v3

第 67 页明确限定了案例能够支持的结论:

Cordis 论文第 67 页对案例证据的限定|620

这个案例能够说明单一插件社区中的可行性,DeepSeek Harness 的性能开销、故障率和开发效率还没有受控数据

11. 适用范围与工程代价

DeepSeek Harness 的 Agent Loop 采用常见的模型调用和工具执行流程

Cordis 管理能力进入、退出与重新连接的过程,工具注册成为可回收的 Effect,服务依赖成为可观察的 Coeffect,插件实例由 Fiber 管理,Context 则限定每个会话和子 Agent 能看到什么

Cordis 适合长期运行并频繁更换能力的 Agent 宿主

可以用四个问题判断是否需要这类运行时:

问题回答为是时的含义
进程是否长于单个任务或会话不能依赖进程退出清理全部状态
Provider 是否会在运行中替换需要区分候选依赖与已提交依赖
是否存在会话、子 Agent 或沙箱等多层作用域需要局部覆盖与独立生命周期所有权
插件是否会同时注册工具、Prompt、事件和资源需要统一 Effect 栈避免跨注册表泄漏

插件较少、进程随任务结束的应用,从中得到的收益有限,多会话、子 Agent、动态工具、模型切换和热更新越多,明确的生命周期协议越有价值

相应的复杂度落到插件作者一侧,依赖要声明准确,逆操作要完整,外部动作还要有补偿流程

作者需要为每一种取得资源的动作设计释放语义,还要明确异步取消后结果算未发生、结果未知还是已经提交

运行时则需要暴露至少四类观测信息:Fiber 当前状态、目标 epoch、已提交 Provider、尚未完成的 inertia

只有 Active 或 Pending 两个标签不足以排障,一个长期 Unloading 的 Fiber 可能卡在 Consumer 清理,也可能卡在外部 Provider 的关闭 Promise,二者的处理方式不同

运行时需要处理异步边界、版本冲突和可观测性,生产可靠性与性能开销仍缺少部署数据

对 DeepSeek Harness 做生产评估时,可以测量 Provider 替换的停稳时间、单次服务通知扫描的 Fiber 数量、失败初始化后的残留注册、工具中止后的未知外部结果,以及 Prompt 或工具变化造成的 KV Cache 失效率

这些指标分别对应生命周期延迟、运行时开销、恢复正确性、外部副作用风险和模型调用成本,比单独测量插件加载耗时更接近 Harness 的真实代价

参考资料

最后更新于: