
大家好,我是人月聊IT。
今天对昨天刚开源的DeepSeek Harness进行逆向工程,解析核心的技术架构和插件机制。从官网介绍大家也可以看到其核心的设计哲学就是一切都是插件,因此整个架构我们也可以将完全是围绕插件展开的,包括插件的注册安装卸载,同时包括插件本不是的组合编排,上层治理。
在多数 Agent 框架中,“插件”通常只是工具扩展的另一种说法:核心循环、模型调用、会话管理和安全策略由框架固定实现,开发者只能在外围增加若干函数。DeepSeek Harness 采用了更彻底的设计。它基于 vendored Cordis 构建,模型适配器、工具注册表、技能系统、会话日志、Agent Loop、沙箱、存储、调度、交互界面乃至启动入口本身都由插件提供。平台没有一个要求所有能力围绕其修改的特权内核,而是通过共享 Context、类型化事件和可逆 Effect,把能力装配成一棵可以替换、隔离和卸载的运行时插件树。
这种架构的意义不只是“方便扩展”。它把 Agent 系统中容易耦合的三类问题分开处理:配置层决定本次部署加载什么,Service 定义插件之间可依赖的稳定能力,Event 负责跨插件协作和策略拦截,Session Event Log 则记录模型实际看到和产生的事实。开发者可以在不修改 Agent Loop 的前提下替换模型 Provider、改变工具策略、挂载会话级能力、增加新的执行环境,或者为某个 Agent 组合完全不同的工具与人格。
本文基于当前仓库的源码、架构文档与现有产品能力,解析 DeepSeek Harness 的插件装配方式、核心运行机制,以及模型、工具、技能、会话、沙箱、安全、记忆、存储、循环和调度等能力如何通过插件形成统一生态。

DeepSeek Harness 的运行实例不是一组写死的模块,而是从空的 Cordis entry 列表开始,由 Profile、Bundle 和 Patch 逐层合成。
Profile 是一个具名部署组合,例如 Web 或 Headless。它声明需要叠加哪些 Bundle,并允许安装仓库外插件及维护自己的 cordis.patch.yml。Bundle 是可分发的配置层,既包含 Cordis 配置行,也指向这些配置行加载的插件代码。基础 Bundle 提供模型、工具、会话持久化、沙箱、审批、设置、凭据和遥测等通用能力;Web Bundle 增加浏览器应用与服务端入口;Headless Bundle 增加一次性任务运行器。
最终配置按照明确顺序叠加:Profile 所列 Bundle、Profile Patch、Harness Home Patch,以及命令行 --patch 覆盖层。Patch 可以按 id 替换一整行配置,也可以插入新的插件行。这意味着平台的可扩展性首先发生在配置层:部署者无需 fork 源码,即可选择 Provider、关闭某项能力、改变插件参数,或者插入自定义插件。
配置完成后,App Boot 与 Loader 解析模块、验证配置并挂载插件。插件可能是带 apply(ctx) 的函数或对象,也可能是继承 Cordis Service 的类。所有插件都进入同一套生命周期管理,但可以处在不同的 Context 与 Scope 中。进程级服务位于共享 Context;Agent Preset 中的插件挂载到 Agent 自己的 agent.ctx,从而实现同一进程内多个 Agent 使用不同的人格、提示词和工具集合。
可以把整个平台理解为四层结构:
这四层没有通过大量具体类互相引用。组合 Bundle 可以依赖具体实现,但普通扩展插件面向 Service Definition 编程,Provider 可以替换,Consumer 不需要感知实现来自本地进程、远端服务还是隔离沙箱。

Cordis Context 可以视为运行时能力目录。服务使用稳定的 ctx key 发布,例如 ctx.llm、ctx.tools、ctx.sessions、ctx.agents、ctx.sandbox 和 ctx.storage。消费插件通过这些 key 获取能力,而不是直接导入某个 Provider 的实现类。
一个插件如果依赖其他服务,可以声明 inject:
import type { Context } from '@deepseek-ai/cordis'
export const name = 'my-tool-plugin'
export const inject = ['tools', 'sessions']
export function apply(ctx: Context) {
// ctx.tools 与 ctx.sessions 在这里已经可用
}
inject 不只是类型提示。Cordis 会等待所需服务就绪后再激活插件,并在依赖失效时处理相应生命周期,因此加载顺序来自服务关系,而不是由开发者手工安排一串初始化调用。缺少必需依赖属于组合错误,应在能够确定时尽早失败,而不是让插件静默跳过功能。
Context 同时携带作用域。全局插件注册的模型 Provider 或存储后端可供整个进程使用;挂载在 agent.ctx 中的工具、提示词和事件监听只属于该 Agent,并在 Agent 销毁时一并回收。Scope 还支持继承和过滤,使全局能力可以被会话继承,同时允许 Agent 层增加自己的注册或限制继承工具。
当插件需要向其他插件提供可直接调用的能力时,通常使用 Cordis Service。Service Definition 声明接口、类型和事件;Provider 注册具体实现;Consumer 调用 Service 或把它包装为模型可见工具。DeepSeek Harness 将这一完整组合称为 capability seam。
以 Shell 为例,Service Definition 描述 Shell 请求与执行结果,Local Provider 通过 Subprocess 能力启动本地进程,其他 Provider 可以把请求送入远端或 E2B 环境,模型侧 Consumer 则注册 Bash 工具。Consumer 只依赖 ctx.shell,因此替换 Provider 后,调用工具的 Agent Loop 无需改变。
同样的三角色结构用于文件系统、终端、LSP、Web、Compaction、Subagent、Workflow、Sandbox、Settings、Credentials 和 Storage 等能力。一个能力只有接口没有实现,或者只有 Provider 没有消费入口,都不能形成完整的平台能力。三角色拆分使实现可以独立演进,也让部署者能够在保持模型侧语义稳定的前提下切换执行世界。
Service 适合直接调用,Event 适合观察、拦截和组合策略。DeepSeek Harness 使用 TypeScript declaration merging 扩展事件表,并明确每个事件的分发模式。
emit 用于同步通知;parallel 并行等待所有监听器;serial 按顺序执行;waterfall 则实现 around-middleware。Waterfall 监听器收到 next(),调用它表示把请求委托给后续监听器,可以在前后修改参数或结果;不调用 next() 就会短路后续链路。因此模型请求、工具执行和 Agent Step 都可以被插件包装,而不需要把策略写进核心循环。
当前关键扩展点包括:
agent/pre-step:决定本次 Step 接纳哪些消息,可改写或拒绝输入。agent/request:在模型调用前调整请求。llm/stream:包装模型流式响应。tools/pre-execute:执行前做允许、拒绝或询问决策。tools/execute:在工具主体外增加超时、追踪或执行环境包装。tools/post-execute:检查或替换工具结果,并附加后续上下文。agent/turn-stopping:在 Turn 关闭前按顺序完成收尾。这套机制把策略与能力实现分开。例如审批插件不需要侵入 Bash 工具,超时策略不需要改写所有工具,重复调用提醒也不需要修改 Agent Loop。插件监听已有事件即可成为控制链路的一部分。
在动态插件系统中,注册容易,正确卸载困难。事件监听器、工具定义、定时器、Provider 和文件监听如果没有统一所有权,热重载或 Agent 销毁后就会留下重复监听与后台任务。
Cordis 将“注册”视为 Effect。ctx.on()、注册表的 register() 以及 ctx.effect() 都返回或持有 disposer。插件卸载时,Cordis 按生命周期撤销这些 Effect。自定义资源也可以纳入同一机制:
export function apply(ctx: Context) {
ctx.effect(() => {
const timer = setInterval(() => {
// background work
}, 5000)
return () => clearInterval(timer)
})
}
对于异步资源,清理不仅要发出取消请求,还要等待子任务真正停止。Harness 的生命周期设计强调 quiescence:进程、Worker、子 Agent 或后台任务的 disposer 应当等待所拥有的工作退出,避免插件已经卸载但副作用仍在继续。
DeepSeek Harness 的 Agent Loop 是默认 Driver,但它仍然是插件,并通过 ctx.agents.setFactory() 注册到 Agent Registry。UI、Hook、SDK 和编排插件依赖公开的 Agent 接口与 ctx.agents,而不直接依赖具体 Loop。这使未来替换 Driver 成为正常的 Provider 替换,而不是重构整个平台。

一次 Turn 可以包含零个或多个 Step。Step 表示一次模型请求以及该请求产生的工具调用。Agent 的所有输入进入统一 Inbox,但具有不同语义:followup() 排入普通后续 Turn 并唤醒 Driver;steer() 尽量在最近的 Step 边界介入;inject() 增加下一次模型可见上下文,但不会单独唤醒空闲 Agent。
Turn 开始后,Driver 从 Inbox 领取下一步输入与一条排队消息,随后执行 agent/pre-step。插件可以改写输入或拒绝 Step。被拒绝的首次输入仍会形成可追踪的 Turn 边界,只是不会消耗一次模型请求。
进入 Step 后,System Prompt 服务收集插件注册的提示词 Section 与工具 Schema;Session 根据事件日志调用 deriveMessages() 投影模型历史;agent/request 允许插件调整最终请求;LLM Runtime 选择 Provider 和 Model,经过 llm/stream 返回标准化流式 Chunk。Assistant Chunk 和最终消息被追加到 Session Log,然后 Agent Loop 执行模型返回的 Tool Call。
工具结果提交后,如果工具仍要求下一次模型请求、Inbox 中出现 Step 级输入,或者插件附加了后续上下文,Loop 会继续下一个 Step。没有待处理工作时,Driver 触发 Turn stopping 扩展点,写入 turn/end 并回到 idle。
Session Event Log 是 Agent 运行的事实来源。turn/*、step/*、user/message、assistant/*、tool/call 和 tool/result 等事件按序追加。模型下一次请求的历史不是从某个可变 messages 数组直接读取,而是由日志投影得到。
这一设计建立了平台最重要的不变量:任何进入模型请求的内容都必须能够从 Session Log 重建。插件注入的上下文、工具追加的提醒、压缩生成的替代摘要、权限策略提示以及跨会话引用,都必须先形成对应的会话事实。这样,恢复、分叉、回放、Trajectory 展示、持久化和遥测能够共享同一条事件流,而不是各自维护一份容易分叉的状态。
Session Log 还是故障恢复的依据。如果中断发生在工具请求与结果之间,恢复逻辑能够区分“工具尚未记录为开始”和“工具已经开始但结果未知”,并给模型不同的恢复提示。对于可能产生外部副作用的操作,系统不会假设重试安全,而是要求根据工具语义检查外部状态。
System Prompt 不是一个静态大字符串。插件以带顺序和作用域的 Section 注册人格、工作区指令、时间、权限状态或其他上下文。每个 Step 组装时,服务根据 Agent Scope 选择有效 Section,并通过 system-prompt/assemble Waterfall 提供最后的组合扩展点。
工具 Schema 同样来自当前 Scope 的 Tool Registry。某个 Agent Preset 可以只暴露少量工具,另一个 Agent 可以继承完整能力集,也可以通过 Tool Restriction 对全局工具做 allow/deny 过滤。由于 Prompt 与 Tool Schema 每一步都从注册表解析,插件的挂载、卸载和会话级组合可以实时反映到后续请求中。

LLM 家族定义消息、Content Block、Tool Schema、Request、Stream Chunk 和 Adapter 接口,具体 Provider 在 ctx.llm 上注册路由。Agent Options 选择 Provider、Model 与最大输出 Token;默认模型配置插件可以为新 Agent 提供部署级默认值。
LLM Runtime 负责把不同 Provider 的返回统一为流式 Chunk,并规范化错误与终止结果。Provider 可以抛出异常,也可以产生 error 或 aborted finish,但对 Agent Loop 暴露的模型请求失败会收敛为终止 Chunk;中间件或消费方自身的缺陷仍然抛出。这样,Loop 不需要理解每个 SDK 的异常习惯,日志插件也能观察一致的流式协议。
增加模型 Provider 时,开发者实现 Adapter 并注册到 ctx.llm 即可。提示词、工具调用、会话持久化和 UI 不需要针对该 Provider 分叉。模型能力因此是可替换的 Service Provider,而不是 Agent Loop 内部的条件分支。
Tool Runtime 是模型能力进入外部世界的统一入口。工具定义同时包含模型可见 Schema、执行函数、规范化输出 Schema、结果渲染方法、可选超时、并发安全分类及 UI 呈现函数。注册表只把名称、描述和参数 Schema 投影到模型请求,执行回调、超时和内部策略元数据不会泄漏到模型侧。
一次调用先经过参数的 lossless JSON 物化与冻结,然后依次进入 tools/pre-execute、单调 Guard、tools/execute、工具主体、tools/post-execute、结果最终化和 tools/result。前置策略可以决定 allow、deny 或 ask;Guard 只能收紧权限,不能把已经拒绝的操作重新允许,因此策略顺序不会意外覆盖最终拒绝。执行 Wrapper 可以组合调用方取消信号与超时信号,但不能丢弃调用方的取消请求。
工具输出不是任意字符串。成功结果先验证为工具声明的规范 JSON 值,再渲染为模型 Content Block 和可回放 UI 元数据。参数错误、输出错误、超时和策略拒绝都进入统一失败结果。并发调度也由工具显式声明:只有 isConcurrencySafe() 返回 true 的调用才能与同组调用并行,其他调用作为 exclusive barrier 单独执行。
平台在此基础上提供文件读写、Bash、持久终端、LSP、Web 搜索与抓取、技能加载、子 Agent、工作流、任务列表、用户提问和计划等工具。Code Mode 还能让模型生成一段代码,通过受控桥接组合多次原生工具调用,同时保留子调用日志与父调用身份。
技能系统不是把所有说明一次性塞入 System Prompt。ctx.skills 维护 Provider 注册表,文件系统 Provider 从受信目录和用户目录发现 Skill,模型侧 skill 工具提供目录浏览与按需加载。技能可以来自本地文件系统,也可以由其他 Provider 提供,而 Tool Consumer 的模型交互保持一致。
这种按需加载降低了基础上下文体积,也让能力说明与运行代码独立分发。技能适合承载特定领域工作流、仓库规范、工具使用约束和可复用操作步骤;真正执行动作的权限仍由工具、沙箱和审批系统控制。换言之,技能告诉 Agent 如何完成任务,工具决定它能做什么,安全插件决定某次调用是否允许。
当前架构没有把“记忆”压缩成单一 Memory Service。Harness 的记忆能力由多组插件协同构成:Session Event Log 保存原始事实;Session Persistence 将日志写入 JSONL 或 SQLite;Session Projection 从完整事件流计算统计、权限等视图;Session Query 提供有界读取、谱系、事件关系、语义过滤和 SQLite 全文检索;Context 插件把工作区指令、时间信息、tmux 位置或其他会话引用注入下一次请求;Compaction 在 Token 压力下追加摘要或工具结果替代事件。
这种设计区分了“事实记录”“检索视图”和“模型上下文”。日志保留可恢复事实,Query 负责寻找相关内容,Context 决定本次请求应看到什么,Compaction 控制上下文预算。跨会话引用也不是直接共享可变消息数组,而是生成有界快照并作为新的、可记录的模型输入进入目标 Session。
Session 还支持 fork 与 resume。Fork 可以基于某个事件边界创建带谱系信息的新会话,子 Agent Provider 可以从父会话已完成历史启动新 Agent。由于 Prompt 历史来自事件投影,分叉和恢复使用与正常运行相同的重建逻辑。
安全不是一个布尔开关,而是多个插件共同实施的策略链。Sandbox Service 根据会话持久化的模式解析进程约束,Local Provider 在不同平台使用可用的隔离后端包装命令;Shell 和 Filesystem 等 Consumer 在执行时消费这些策略。远程隔离环境则通常替换整组 FS、Subprocess 和 Shell Provider,使它们指向同一个远端执行世界。
审批系统通过 ctx.approval 提供与 UI 通道无关的一次性决策。工具前置策略可以返回 ask,审批插件再向 Web、ACP 或其他交互适配器请求 allowed-once、rejected、cancelled 或 unavailable。缺少应答者或应答失败时按拒绝处理,授权只适用于当前请求。approval/asked 与 approval/decided 写入会话日志,模型只看到最终工具结果,不直接看到面向人的权限 UI。
Permission Preset 把 sandbox/mode 与 approval/policy 组合成用户可选择的预设,并在会话创建时固定为持久事实。后续修改默认值不会悄悄改变已经存在的会话。凭据插件则通过引用和环境 Provider 提供密钥,执行子进程时会清理包含 KEY、SECRET、TOKEN 或 PASSWORD 等名称的环境变量,避免 Harness 凭据通过命令输出或临时文件泄露。
此外,Guard 插件监听工具和 Agent 事件,实现重复工具调用提醒与每次调用的 Deadline。提醒以插件来源的上下文写入日志,超时通过 tools/execute Wrapper 组合取消信号。安全策略因此仍然遵守“模型可见即记录”和“注册可卸载”两项基础原则。
Harness 将 Session Persistence 与非会话 Storage 明确分开。前者只负责会话事件及其检查点,后者通过 ctx.storage 管理其他应用数据。Storage Provider 可以是 JSON 或 SQLite Backend,消费插件通过具名且类型化的 Data Form 访问数据,而不是直接绑定某个数据库实现。
storage-domain 在此之上提供经过验证的领域记录存储和变更事件。设置、工作区实体或其他插件数据可以拥有独立的命名空间、Schema 和生命周期。分离两类存储的价值在于:会话日志保持追加式、可重放语义;业务状态可以选择更适合的读写模型,不需要伪装成对话事件。
Subagent 家族允许多个具名 Provider 同时注册。Provider 可以启动全新的进程内子 Agent、基于父历史 fork、通过 ACP 或 SDK 启动外部 Harness,也可以连接 Codex 或 Claude Code。模型侧 Consumer 提供委派、列举、消息控制和子 Agent 回报工具。父 Agent 只面向统一的 ctx.subagents 协议,不需要知道子执行器属于哪个产品或进程。
Workflow 家族允许模型编写编排脚本,并由 Worker Thread Provider 执行。Worker Thread 隔离宿主事件循环,但不被当作安全边界;实际外部操作仍经工具与权限流水线。通用 Workflow Tool 和固定策略的 Ralph Tool 都是 Consumer,可以复用同一引擎。
Jobs 则承载通用后台工作及 job_* 控制工具。它与 Subagent、Workflow 组合后,可以让长任务在后台运行,再由 Agent 收集、停止或继续。所有权和取消仍遵循 Agent 与插件生命周期,避免后台任务脱离创建它的运行时。
Schedule 能力提供会话内提醒的创建、列举和删除工具。它没有另建可变 Schedule 数据库,而是把版本化调度事件写入原始 Session Log,通过 Fold 得到当前提醒状态。进程内 Owner 只在该 Session 存在活动的根 Agent 时维护计时器;冷会话重新变为活动状态后,会恢复已经到期的工作。
到期提醒通过 Agent 的普通 Follow-up 队列重新进入对话,因此沿用同一 Turn、日志、权限和模型调用机制。该能力不是外部通知系统,也不保证在进程完全离线时主动唤醒用户。它解决的是 Session 本地的持久后续工作,而不是分布式任务平台。
最小插件只需要导出 apply:
import type { Context } from '@deepseek-ai/cordis'
export const name = 'hello-plugin'
export function apply(ctx: Context) {
console.log('[hello-plugin] loaded')
}
把插件作为绝对路径插入一个 Patch,即可在不修改现有 Bundle 的情况下加载:
- insert:
- id: hello
name: '/absolute/path/to/my-plugin.ts'
然后通过 pnpm dsh web --patch ./my-plugin.patch.yml 启动。对实际能力插件而言,开发过程通常遵循以下路径。
第一步是选择正确的扩展位置。新增模型实现应注册 LLM Adapter;新增模型可调用能力应注册 Tool;拦截请求或执行策略应监听对应 Waterfall;需要跨插件直接调用的能力应设计 Service Definition、Provider 和 Consumer;需要模型长期看到的事实必须定义 Session Event,而不是只保存在进程内变量中。
第二步是明确作用域。进程级 Provider、注册表和跨会话设施留在主组合中;只属于一个 Agent 的工具、人格或提示词放入 Agent Preset,并挂载到 agent.ctx。如果一个 Preset 试图发布进程全局 Service,平台会在挂载时拒绝,以免多个 Session 发生服务冲突。
第三步是把生命周期纳入 Effect。工具注册、事件监听、Provider 注册和后台资源都必须有 disposer。插件卸载后不应继续响应事件,也不应留下子进程、Watcher 或 Worker。跨进程和并发能力还需要遵守取消、等待退出、先关闭通知再终止子任务等防御性规则。
第四步是保持模型输出可回放。工具的展示方法必须是参数与持久结果的纯函数;插件注入的上下文必须写入日志;新增用户可见行为需要通过真实可运行示例进入 Keyless Snapshot,而不是只写 Mock 单元测试。这样 Web UI、Headless、ACP 和 Session Replay 才能观察同一行为。
第五步是将可变部署选择放入经过验证的 Config。模型名称、超时、目录、Provider 路由和策略等部署参数不应散落为插件内部硬编码。Profile 与 Patch 可以覆盖 Config,从而在不同运行模式之间复用同一插件实现。
DeepSeek Harness 的插件系统解决的并不是“如何多注册几个工具”,而是如何让 Agent 平台在持续增加能力时仍保持清晰的所有权与替换关系。
对部署者而言,Profile、Bundle 和 Patch 把产品形态变成可审查的配置组合。同一套源码可以形成 Web Agent、一次性 Headless Runner、最小工具基准环境或具有特殊 Preset 的领域 Agent。模型、持久化、沙箱与交互方式可以独立替换。
对能力开发者而言,Service Definition 提供稳定依赖,Typed Event 提供协作点,Scope 提供会话隔离,Effect 提供生命周期回收。扩展代码不需要导入 Agent Loop 内部实现,也不需要复制一套请求和工具调度逻辑。
对平台维护者而言,追加式 Session Log 把模型上下文、回放和持久化统一到同一事实来源;工具流水线把验证、权限、超时、并发、结果规范化和 UI 呈现集中处理;能力三角色结构减少 Consumer 对具体 Provider 的耦合。安全、记忆和调度因此不是外围补丁,而是沿用相同插件协议的正式能力。
最关键的是,这套架构允许系统在扩展时保持“插件之外没有例外”。新增行为应挂到公开 Service 或 Event 上,新增执行环境应提供完整 Provider,新增模型可见输入应形成 Session Event,新增会话差异应通过 Scope 与 Preset 组合。只要这些约束保持成立,DeepSeek Harness 就能在模型、工具、技能、沙箱、存储和多 Agent 编排不断演进的同时,继续维持可替换、可追踪、可回放和可治理的运行基础。