← 返回AI教程
🌐 其他

DeepSeek Harness 可组合的插件运行时&一切皆插件

来源:掘金 · 发布于 2026-08-14 10:33:12
DeepSeek Harness 是 DeepSeek 官方出品的可组合插件运行时 Agent 框架。本文结合源码与官方教程,从零上手跑通从第一个插件到真实编码 Agent 的完整链路。

DeepSeek Harness 可组合的插件运行时&一切皆插件

Cosolar 2026-08-14 94 阅读29分钟

本文结合 deepseek-harness 仓库源码与官方 docs/cordis-tutorial/ 教程编写,并在最后一章与 Claude Code、Codex 以及 AgentScope(阿里) 做深度对比。读完你应能:理解 Harness 的设计哲学、跑通从“Hello 插件”到“真实编码 Agent”的完整链路、看懂 cordis.yml 组合与 HMR,并清楚它与其他主流框架的区别。

GitHub: deepseek-harness.github.io/deepseek-ha…

一、DeepSeek Harness 是什么

1.1 一句话定义

DeepSeek Harness 是 DeepSeek 官方出品的 Agent(智能体)运行框架。它不是一个“写死的助手”,而是一个可组合的插件运行时:你用一份 YAML 配置文件(cordis.yml)把“会话管理、系统提示词、工具、LLM 适配器、文件访问、子进程、沙箱、乃至 agent 主循环本身”等全部能力像搭积木一样拼起来。

它的底层是一个名为 Cordis 的微型插件框架(源码 vendored 在 vendor/cordis/)。Cordis 提供一个共享的 Context(上下文),每个能力都是一个挂载到 ctx 上的插件。

一句话定位:

Claude Code 是“产品”,DeepSeek Harness 是“框架”,AgentScope 是“开发库”。 前者给你一个开箱即用的编码助手;Harness 给你一套可以自由拼装、自托管、可嵌入自己产品的 Agent 引擎;而 AgentScope 给你一套以 Python 为主的“构建智能体应用的库”,侧重多智能体与工程化工具链。

1.2 多维度拆解:它到底是什么

要真正理解 Harness,需要从五个维度同时看它:

维度它是什么反例(它不是什么)
交付形态一套框架 + 一组官方插件 + 一个 CLI/ACP 入口一个封闭、开箱即用的产品
内核范式基于 Cordis 的插件运行时,一切皆插件一个硬编码逻辑的单体程序
组合方式声明式 cordis.yml 配置驱动、按 id 装配命令式代码里 new 实例
能力边界抽象服务(Service Definition)+ 可替换 Provider + 消费者把实现细节写死在调用方
适用范围可自托管、可嵌入自有产品、可长期会话仅终端交互的助手

1.3 设计思路:为什么这么设计(核心分析)

DeepSeek Harness 采取“一切皆插件”的设计思路。我们采用插件式开放架构来构建 Agent Harness:模型、工具、技能、会话、沙箱、存储、循环、调度、UI 等所有 Agent 能力均由插件组合而成,可自由替换、灵活重组。

Harness 的设计不是“为了可插拔而可插拔”,而是围绕几个明确的工程目标与约束做出的系统性取舍。下面逐条分析。

思路一:用“一切皆插件”消除硬编码的循环逻辑

绝大多数编码助手的 agent 主循环(call model → run tools → repeat)是写死在核心里的。Harness 的反直觉决定是:整个仓库只有 dsh-agent-loop 一个包包含具体循环逻辑(packages/core/agent-loop/README.md 明确如此),其余全是抽象服务或扩展点插件。

设计动机:

  • 循环只描述“驱动协议”,不掺带具体能力。hooks、sandbox、plan mode、retry、subagent、compaction 等行为全部通过监听 agent/*、tools/*、session/* 事件实现,而非改循环代码。
  • 这带来“行为在扩展点上、不在循环里”的硬约束——AGENTS.md 原话:“Plugins, not loop changes: new behavior goes on documented extension points; changing agent-loop requires updating docs/architecture.md.”
  • 底层原理:当循环成为唯一且稳定的驱动者,所有可变行为都被推到事件订阅侧,于是能力的增删=插件的挂载/卸载,与主循环彻底解耦。

思路二:能力分层的“三角色”模型(capability-seam)

每个能力被刻意拆成三个相互独立演化的角色(见 docs/glossary.md#capability-seam):

  • Service Definition(服务定义):拥有 ctx.<key> 与词汇表类型的 Cordis Service,是抽象类或具体注册表(如 ShellExecutor、WebRuntime),绝不是 TypeScript interface——因为它要作为真实服务被挂载。
  • Service Provider(服务提供者):一种或多种实现,如 dsh-shell-local / dsh-shell-pwsh。
  • Consumer(消费者):注入该服务、面向模型暴露工具的插件,如 dsh-tool-bash。

设计动机:

  • 角色独立演化:当只有 provider 需要换(本地→沙箱→E2B)时,定义和消费者代码一行都不用动。这把“变化”限制在最窄的边界内。
  • 以 shell 为例:dsh-shell(定义)→ dsh-shell-local / dsh-shell-pwsh(provider,按平台 disabled)+ dsh-bash-sandbox(沙箱 policy)。LLM 同理:dsh-llm(定义)→ dsh-llm-deepseek(原生)/ dsh-llm-pi-ai(多 provider 孪生)。
  • Swappable capability:seam 是“完整能力”,不是单个角色——文档特别强调“reserve the term for that meaning”,因为误把某一角色当能力,会导致消费者直接依赖实现而破坏可替换性。

思路三:注册即副作用(effect),把生命周期交给框架

AGENTS.md 铁律:“Registrations are effects: every contribution goes through ctx.effect() / ctx.on(); a registry's register() returns the disposer.”

设计动机:

  • 插件不持有自己资源的“拆除责任” 。任何注册(ctx.tools.register、ctx.on、子插件、服务实例)都附着在调用它的插件上,插件卸载时自动撤销。
  • ctx.plugin(child) 让一个插件把另一个插件挂为“子”,父子一起 dispose,递归卸载。
  • 底层原理:资源所有权 = 插件生命周期,而非手动 if 分支。这从机制上消灭了“忘记移除监听器/关闭定时器”这类资源泄漏——docs/defensive-patterns.md 把“Dispose must reach quiescence”列为头号缺陷类规则:拆除要异步 await 到真正静止,而不是只发一个 kill。

思路四:依赖注入是“持续跟踪”,而非一次性检查

消费方写 inject: ['tools'],Cordis 会让插件保持 PENDING 直到 ctx.tools 存在,且运行期若服务消失(provider 被卸载/热替换),依赖方随之卸载,服务恢复后再加载。

设计动机:

  • 配置可替换服务:卸载 dsh-shell-local、挂载另一个 shell provider,所有 inject: ['shell'] 的插件自动重启用新实现——这就是“框架级热替换”的物理基础。
  • 顺序无关:cordis.yml 里插件行序不影响正确性,只影响就绪先后。彻底移除某服务后,依赖方保持 PENDING,既不崩溃也不会半运行。
  • 底层原理:依赖图是运行时动态满足的,而非构建期静态绑定,因此组合(composition)本身是数据(YAML),不是代码。

思路五:“模型可见 ⟺ 已记录”的审计约束

AGENTS.md 硬约束:“Model-visible ⟺ logged: anything that reaches a model request must be reconstructable from the session log; a new model-visible input requires a session event.”

设计动机:

  • 任何送达模型的输入(工具结果、系统提示词切片、变量)都必须对应一条会话事件,使得会话日志即真相(source of truth) ——可以回放、审计、fork、resume。
  • 会话是一等公民:session(JSONL/SQLite 持久化、投影、血缘)、session-query(SQLite 全文检索)、compaction(压缩+工具结果裁剪)共同支撑长期记忆与合规。
  • 底层原理:把“可重建性”上升为架构不变量,而非依赖开发者自觉。这使得调试一个错误回答 = 重放那条会话事件流,而不是猜测模型当时“看到了什么”。

思路六:显式优于隐式,错则明报,绝不静默

AGENTS.md 多项规则:

  • “Misconfiguration fails loud at load when self-contained, otherwise at the earliest resolvable point; never silently skip a missing referent.”
  • “No hardcoded tunables in plugins: deployment-varying choices are validated Config fields changeable from cordis.yml.”
  • 跨边界不透明 id 用 Branded<B> 品牌类型,非裸 string;只在校验边界(config、模型/工具 JSON、文件、worker、进程、线)做运行时校验,同进程类型边界信任 TypeScript。

设计动机:

  • 可部署性来自“可变项都是可校验的 Config”,而非代码里 ?? default 的隐藏默认值(那是协议常量/安全不变量才固定的)。
  • 可诊断性来自“缺引用就明报”,避免“插件没反应却不知道为什么”(见教程第九章诊断器)。
  • 底层原理:把“部署差异”与“安全不变量”两类变化分离——前者进 Config 受 schema 校验,后者写死且不可被配置绕过。

思路七:安全是分层的,而非单点

docs/defensive-patterns.md 给出具体规则:

  • 生成命令拿到的是清洗过的环境(丢弃 *KEY*/*SECRET*/*TOKEN*/*PASSWORD*),防止 harness 凭据泄漏进输出或 spill 文件。
  • 临时/spill 文件用私有(0700)目录、随机名、独占 owner-only 打开('wx'、0o600),避免可预测路径导致的 symlink 竞争与泄露。
  • 沙箱是一等能力:sandbox(bwrap/Landlock/Seatbelt),执行与文件系统访问都可套沙箱 policy。

设计动机:把“执行不可信输出”当头等威胁,从环境、文件、进程三层同时设防,而非依赖“用户别跑奇怪命令”的约定。

1.3.1 一切皆插件设计

image.png

1.3.2 多种运行模式

针对不同的使用场景,DeepSeek Harness 提供四种模式,每种模式会默认加载不同的插件集合:

  • 标准模式:提供完整的工具组合;
  • PTC 模式:程序化工具调用(Programmatic Tool Calling),由模型生成的一段代码来组合多轮工具调用;
  • 极简模式:仅保留一个 shell 工具与一个文件编辑工具,用于最小环境下的模型基准测试;
  • 创造模式:可以检查当前运行时、在内存中试验 Cordis 插件,并据此组合和创作新的模式。

image.png

1.4 这七个思路如何收敛为一个系统

把以上七点串起来,Harness 的设计主线是:

用插件运行时(Cordis)承载一切能力,用“抽象服务 + 可替换 provider”隔离变化,用 effect 把生命周期交给框架,用注入的动态满足实现配置驱动的组合,用事件把行为推到扩展点,用会话日志作为可重建的真相,用显式校验与安全分层守住部署与执行边界。

它因此呈现出与 Claude Code / Codex(产品)、AgentScope(Python 开发库)截然不同的取向:前者关心“用户开箱即用”,后者关心“研究者快速搭多智能体”,而 Harness 关心的是 “平台/产品工程师如何可靠地自托管并长期演化一个 Agent 底座” 。这也是为什么它的工程纪律极严(100% 覆盖率门禁、type-equiv 文档同步、品牌类型、声明式 surface),因为底座的可靠性是上层一切的前提。

二、核心心智模型:一切皆插件

第一章回答了“为什么这么设计”(七条思路 + 设计动机);本章回答“它实际怎么跑起来”,并用一张整体架构图把这七条思路落到物理结构上。如果你跳过了第一章,只需记住一句话:Harness 里没有“写死的助手”,所有能力都是挂在共享 ctx 上的插件。

DeepSeek Harness 基于具有时空可组合性的 Cordis 插件系统构建。Cordis 元框架只负责插件的加载与卸载以及依赖关系,Agent Harness 的所有具体组件都是不同的 Cordis 插件。插件通过 Cordis 服务与事件彼此协作,并可以在配置层自由组合。

开发者无需改动 DeepSeek Harness 的源码本身,就能以插件的方式独立选择、替换或扩展其中的任一能力。这就是 DeepSeek Harness 最重要的设计原则:一切皆插件。

整个仓库遵循一条铁律(见 AGENTS.md):

Everything is a plugin. 一切皆是插件。

这意味着:

  • 工具是插件(dsh-tools)
  • 大语言模型适配器是插件(dsh-llm + DeepSeek provider)
  • 文件系统访问是插件(dsh-fs)
  • shell / 子进程 / 终端是插件(dsh-shell / dsh-subprocess / dsh-terminal)
  • 连 agent 主循环(agent-loop)本身都是可替换的插件(dsh-agent-loop)

所有插件共享同一个 ctx,通过三种机制协作:

机制关键字作用
依赖注入inject: ['tools']插件声明它依赖某服务,Cordis 在该服务就绪后再启动它
注册 / effectctx.effect() / ctx.on()插件贡献能力(注册工具、监听事件);卸载时自动撤销
事件ctx.on(event, cb) / ctx.waterfall()解耦的插件间通信

关键设计:注册是“副作用”(effect) 。每个贡献都通过 ctx.effect() 完成,插件卸载时贡献自动撤销。这是 Cordis 生命周期管理的核心,也是它区别于“手动管理全局单例”类框架(如很多 Python agent 库)的根本点。

image.png

2.1 整体架构图(基于源码)

下图依据 packages/bundle/base/cordis.patch.yml(base bundle 的 45+ 个 id 行)与 packages/core/agent-loop/README.md(agent-loop 注入的 5 个服务)绘制,反映真实组合关系,而非示意。

graph TB
    subgraph RUNTIME[&#34;Cordis 运行时 (vendor/cordis)&#34;]
        ROOT[&#34;根 Context\n(共享 ctx)&#34;]
        LOADER[&#34;Loader 插件\n读 cordis.yml / --profile 补丁层&#34;]
        HMR[&#34;@cordis-plugin-hmr\n文件变更热重载&#34;]
        TIMER[&#34;@cordis-plugin-timer&#34;]
    end

    subgraph CORE[&#34;核心脊梁 (packages/core + base bundle)&#34;]
        AGENTLOOP[&#34;dsh-agent-loop\n(ctx.agentLoop)\n唯一具体循环驱动&#34;]
        AGENT[&#34;dsh-agent\n(ctx.agents 工厂)&#34;]
        TOOLS[&#34;dsh-tools\n(ctx.tools 注册表)&#34;]
        LLM[&#34;dsh-llm\n(ctx.llm 抽象 + DeepSeek provider)&#34;]
        SYSPROMPT[&#34;dsh-system-prompt\n(ctx.systemPrompt)&#34;]
        SESSION[&#34;dsh-session\n(ctx.session 持久化/投影)&#34;]
    end

    subgraph DRIVE[&#34;agent-loop 注入的 5 个接口服务(来自 README)&#34;]
        AGENTLOOP -.注入.-> AGENT
        AGENTLOOP -.注入.-> SESSION
        AGENTLOOP -.注入.-> LLM
        AGENTLOOP -.注入.-> TOOLS
        AGENTLOOP -.注入.-> SYSPROMPT
    end

    subgraph EXEC[&#34;执行 / 沙箱能力 (Provider 插件)&#34;]
        SHELL[&#34;dsh-shell-local / dsh-shell-pwsh&#34;]
        SUBPROC[&#34;dsh-subprocess-local&#34;]
        SANDBOX[&#34;dsh-sandbox-local\n+ sandbox-policy&#34;]
        FS[&#34;dsh-tool-fs / fs-search\n(dsh-fs-sandbox)&#34;]
        TERMINAL[&#34;terminal / code-runtime&#34;]
    end

    subgraph MODEL[&#34;模型 / 检索能力&#34;]
        WEB[&#34;dsh-web (web_search)&#34;]
        DEEPSEEK[&#34;dsh-llm-deepseek\n(原生适配器)&#34;]
        PIAI[&#34;dsh-llm-pi-ai\n(多 provider 孪生)&#34;]
        RETRY[&#34;dsh-llm-retry&#34;]
    end

    subgraph ORCH[&#34;编排 / 子任务能力&#34;]
        SUBAGENT[&#34;dsh-subagent\n(spawn / fork provider)&#34;]
        WORKFLOW[&#34;dsh-workflow\n(worker-thread)&#34;]
        JOBS[&#34;dsh-jobs-local&#34;]
        TODO[&#34;dsh-tool-todo&#34;]
        GOAL[&#34;dsh-goal / command-goal&#34;]
    end

    subgraph SESS[&#34;会话 / 人机协作&#34;]
        PERSIST[&#34;session-persistence-jsonl&#34;]
        QUERY[&#34;session-query-sqlite\n(全文检索, 可选)&#34;]
        PROJ[&#34;session-projection&#34;]
        COMPACT[&#34;compaction-basic\n+ tool-result-pruner&#34;]
        APPROVAL[&#34;user-approval + permission-presets&#34;]
        INTERACT[&#34;interaction / commands / plan-mode&#34;]
    end

    subgraph EXT[&#34;扩展 / 互操作&#34;]
        SKILL[&#34;dsh-skill + tool-skill&#34;]
        HOOKS[&#34;hooks-claude-code / hooks-codex\n(Hook 互操作桥接)&#34;]
        ACP[&#34;dsh-acp\n(自动化协议服务器)&#34;]
        EXTENSIONS[&#34;extensions\n(agent 自修改插件)&#34;]
        BUNDLE[&#34;dsh-bundle\n(--profile 补丁层)&#34;]
    end

    %% 配置驱动加载
    LOADER -->|&#34;按 id 装配\n服务就绪即激活&#34;| CORE
    LOADER --> EXEC
    LOADER --> MODEL
    LOADER --> ORCH
    LOADER --> SESS
    LOADER --> EXT
    HMR --> LOADER

    %% 工具注册: 能力插件把工具注册进 ctx.tools
    SHELL --> TOOLS
    FS --> TOOLS
    WEB --> TOOLS
    SUBAGENT --> TOOLS
    WORKFLOW --> TOOLS
    TODO --> TOOLS
    SKILL --> TOOLS

    %% 模型链路
    DEEPSEEK --> LLM
    PIAI --> LLM
    RETRY --> LLM

    %% 会话链路
    PERSIST --> SESSION
    QUERY --> SESSION
    PROJ --> SESSION
    COMPACT --> SESSION

    %% 事件总线(解耦插件通信)
    EVENTS{{&#34;事件总线\nagent/* · tools/result · agent/request\napproval/request · session/event&#34;}}
    AGENTLOOP --> EVENTS
    TOOLS --> EVENTS
    SHELL --> EVENTS
    APPROVAL --> EVENTS
    SANDBOX --> EVENTS
    INTERACT --> EVENTS

    %% 入口
    ENTRY[&#34;CLI (pnpm dsh)\n/ ACP / JSON-RPC 入口&#34;]
    ENTRY --> ROOT
    BUNDLE --> LOADER

2.2 图中关系对照源码说明

1. “一切皆插件”的物理形态

  • 启动器 = node --import tsx ../../vendor/cordis/bin.js,它只创建 root Context 并挂载 Loader。
  • Loader 读取 cordis.yml 或 --profile 补丁层(dsh-bundle)。base bundle 在 packages/bundle/base/cordis.patch.yml 里用 45+ 个 id 行声明了全部默认插件,行序无关(激活由“服务可用性”驱动)。
  • 每一行就是一个插件;id(如 agent-loop、tools、llm-deepseek)是 stable 标识,后续补丁层按 id 覆盖它。

2. agent-loop 是唯一的“具体循环”

  • 据 dsh-agent-loop/README.md:整个 harness 只有这一个包包含具体循环逻辑,其他全是抽象服务或扩展点插件。
  • 它注入并依赖 5 个接口服务:agents、sessions、llm、tools、systemPrompt(图中虚线)。这 5 个都是 ctx 上的服务,具体 provider 可热替换。
  • “call model → run tools → repeat”之外的所有行为(hooks、sandbox、plan、retry、subagent、compaction)都通过监听 agent/*、tools/*、session/* 事件实现——这就是图中的事件总线。

3. 工具是“注册”而非“硬编码”

  • bash、fs、web、subagent、workflow、todo、skill 等执行/编排插件,通过 ctx.tools.register(...)(effect)把工具挂进 dsh-tools 注册表,再由 agent-loop 在 tools/result 等事件里消费。两插件互不知对方存在。

4. Provider 可替换三角色

  • 以 shell 为例:dsh-shell(定义)→ dsh-shell-local / dsh-shell-pwsh(provider,按平台 disabled)+ dsh-bash-sandbox(沙箱 policy)。LLM 同理:dsh-llm(定义)→ dsh-llm-deepseek(原生)/ dsh-llm-pi-ai(多 provider 孪生)。

5. 自修改与互操作

  • extensions 让 agent 运行时装载/卸载插件;hooks-claude-code/hooks-codex 桥接外部 Hook;acp 暴露自动化协议服务器。这些都在 base bundle 之外,按需叠加。

图中 dsh-agent-loop 的 5 条虚线注入、dsh-tools 的 7+ 条工具注册、--profile 补丁层装配,均直接来自 packages/bundle/base/cordis.patch.yml 与 packages/core/agent-loop/README.md 的源码事实。

三、环境准备(5 分钟)

3.1 你需要先知道的背景(新手必读)

本文示例用 TypeScript 写插件,但不需要你精通 TS。只要理解下面四点即可:

  • ESM 与 import:代码用 import { x } from 'pkg' 引入依赖;本文所有相对导入都带 .ts 后缀(如 './hello.ts'),这是 Cordis loader 的约定。
  • workspace 包名:@deepseek-ai/cordis、@deepseek-ai/dsh-tools 等是仓库内部的 npm 包名(pnpm workspace 解析),不是从网络下载的。import type { Context } from '@deepseek-ai/cordis' 就是从 Cordis 取类型。
  • cordis.yml 是 YAML 列表:每个 - name: ... 是一项插件;缩进用两个空格,不要混用 Tab。
  • ctx 是什么:贯穿全文的 ctx 是 Cordis 的共享上下文,所有插件通过它注册能力与监听事件。你可以把它当成“整个运行时的总接线板”。

3.2 环境前置条件

前置条件(详见 docs/development.md):

  • Node.js 22.19+ 或 24+(CI 覆盖 22.19 / 24 / 26)
  • pnpm(启用 Corepack:corepack enable),仓库锁定 pnpm@11.7.0
  • Git 2.26+
  • 可选:DeepSeek API Key(DEEPSEEK_API_KEY),仅真实跑模型时需要;本教程第三至第十章(含 HMR 与工具管线)完全无密钥可运行
git clone https://github.com/deepseek-ai/deepseek-harness.git
cd deepseek-harness
pnpm install
pnpm run typecheck   # 验证环境就绪

创建教程临时目录(tmp/ 已被 git 忽略,不会被提交):

mkdir -p tmp/cordis-tutorial
cd tmp/cordis-tutorial

后续所有示例都从这同一个目录运行:

node --import tsx ../../vendor/cordis/bin.js

这个单文件启动器会:① 创建根 Context;② 挂载 Loader 插件;③ 从当前目录读取 ./cordis.yml 并加载里面列出的每个插件。无需任何构建步骤(--import tsx 让 Node 直接跑 TS)。

四、动手:你的第一个插件

4.1 写插件

在 tmp/cordis-tutorial 下创建 hello.ts:

import type { Context } from '@deepseek-ai/cordis'

export const name = 'hello'

export function apply(ctx: Context) {
  console.log('hello from my first plugin')
}
  • 插件通过命名导出 apply 函数被 loader 挂载。
  • ctx 是 Cordis 上下文,插件通过它注册所有贡献。
  • name 是可选显示名,用于诊断信息。

4.2 组合应用

创建 cordis.yml:

- name: './hello.ts'

这是一个配置项列表。name 是模块指定符(相对路径或 npm 包名)。

4.3 运行

node --import tsx ../../vendor/cordis/bin.js

输出:

hello from my first plugin

4.4 三种插件形态

import { Service, type Context } from '@deepseek-ai/cordis'

// 1. 函数插件(最常用)
export function apply(ctx: Context) {}

// 2. 对象插件:带 apply 方法的对象
export const objectPlugin = { name: 'obj', apply(ctx: Context) {} }

// 3. 类插件:Service 子类(需要公开服务时用,见第六章)
export class MyService extends Service {
  constructor(ctx: Context) { super(ctx, 'myService') }
}

新手建议:在需要公开服务之前,一律使用函数形态。

4.5 容错行为(新手必知)

  • 若插件 apply 抛错 → 进程直接崩溃并报错(不会静默跳过)。
  • 若 cordis.yml 里的模块路径/包名拼错(解析失败)→ Cordis 只通过 logger 报告,不会崩溃。新插件“没反应”时,先检查拼写。

五、生命周期与 effect(资源自动回收)

Cordis 插件可能因修改配置、热重载、显式资源释放或所需服务消失而卸载。通过 Cordis API 建立的注册属于 effect,会在所属插件卸载时撤销;在这些 API 之外管理的资源必须包装在 ctx.effect() 中。

创建 lifecycle.ts:

import type { Context } from '@deepseek-ai/cordis'

export const name = 'lifecycle-demo'

function heartbeat(ctx: Context) {
  console.log('heartbeat plugin loading')
  ctx.effect(() => {
    const timer = setInterval(() => console.log('tick'), 200)
    return () => {
      clearInterval(timer)
      console.log('heartbeat cleaned up')
    }
  })
}

export function apply(ctx: Context) {
  const fiber = ctx.plugin(heartbeat)
  ctx.effect(() => {
    const timer = setTimeout(async () => {
      await fiber.dispose()
      console.log('disposed')
      process.exit(0)
    }, 700)
    return () => clearTimeout(timer)
  })
}

运行后输出:

heartbeat plugin loading
tick / tick / tick
heartbeat cleaned up
disposed

三点关键:

  • ctx.plugin(heartbeat) 把一个来自代码的函数挂载为插件,与 YAML loader 为每个配置项做的完全一致。调用返回 fiber——已加载插件实例的运行时句柄。
  • effect 主体在加载期间运行,返回的 disposer 在卸载期间运行。生命周期与插件一致的资源,你绝不需要手动调用 disposer。
  • fiber.dispose() 会等该插件所有清理(含异步 disposer)完成后才结束,并递归卸载它挂载的子插件。

Fiber 状态机

每个已加载插件实例都有 fiber,在以下状态间转换:

PENDING → LOADING → ACTIVE → UNLOADING → DISPOSED
                 ↘ FAILED
  • PENDING:已声明,但所需服务尚不可用(见第六章)。
  • LOADING / ACTIVE:apply 正在运行/已完成。
  • FAILED:apply 或配置校验抛异常。
  • UNLOADING / DISPOSED:disposer 正在运行/已拆除。

已经是 effect 的操作(你很少需要手写 ctx.effect())

  • ctx.on(event, listener):监听器随插件卸载自动移除。
  • ctx.plugin(child):子插件随父插件一同 dispose。
  • 服务注册、harness 注册表(如 ctx.tools.register(...))的返回 disposer 都附着在调用插件上,自动撤销。

顺序注意:disposer 按注册逆序启动,但多个异步 disposer 并发运行;若拆除必须按顺序,请把步骤放进同一个 disposer 内依次 awaits。

六、服务(Service):能力的注册与消费

服务是插件提供、其他插件通过 ctx 消费的具名能力。在 harness 中,ctx.tools、ctx.llm、ctx.agents 都是服务。消费方只指定 'tools' 这样的能力名,而不导入提供方——因此配置可以选择提供方,无需改动消费方代码。

6.1 提供服务

greeter.ts:

import { Service, type Context } from '@deepseek-ai/cordis'

declare module '@deepseek-ai/cordis' {
  interface Context {
    greeter: GreeterService
  }
}

export class GreeterService extends Service {
  constructor(ctx: Context) {
    super(ctx, 'greeter')
  }
  greet(who: string) {
    return `Hello, ${who}!`
  }
}

export const name = 'greeter'
export function apply(ctx: Context) {
  ctx.plugin(GreeterService)
}

两部分协同:

  • 运行时:super(ctx, 'greeter') 以名称 greeter 注册实例,ctx.greeter 随处可访问;注册属于 effect,卸载时移除。
  • 编译时:declare module 用 TS 声明合并把 greeter 加入 Context 接口,使消费方获得类型安全(无此声明运行时仍工作,但失去类型)。

6.2 消费服务(inject)

consumer.ts:

import type { Context } from '@deepseek-ai/cordis'

export const name = 'consumer'
export const inject = ['greeter']

export function apply(ctx: Context) {
  console.log(ctx.greeter.greet('world'))
}

inject 列出该插件需要的服务。Cordis 会让插件保持 PENDING 直到每项服务都存在,因此在 apply 内可保证 ctx.greeter 已就绪——加载顺序无关紧要。

- name: './greeter.ts'
- name: './consumer.ts'

输出 Hello, world!。交换两行顺序输出不变;若彻底移除 greeter.ts(或拼错包名),消费方会保持 PENDING / 启动失败——它既不崩溃、也不会在依赖缺失时半运行,而是明确停在等待状态(诊断器见第九章)。

术语区分:本例 export const name = 'consumer' 是插件显示名;inject: ['greeter'] 里的 greeter 是服务名(由 super(ctx, 'greeter') 注册)。二者命名空间不同——插件可任意取名,但注入必须精确匹配服务名,否则永远 PENDING。

6.3 inject 是持续跟踪,而非一次性检查

若运行期间所需服务消失(如提供方被卸载、热替换),每个依赖插件会随之卸载,服务恢复后再加载。结合 effect,这防止消费方保留对不可用服务的引用。也正是配置能替换服务的原因:卸载 dsh-shell-local、挂载另一个 shell 提供方,所有 inject: ['shell'] 的插件会重启并用新实现。

6.4 可选依赖

inject 是硬性依赖。缺失仍可工作时跳过 inject 并探测:

export function apply(ctx: Context) {
  const greeter = ctx.get('greeter')
  console.log(greeter?.greet('maybe') ?? 'no greeter available')
}

原则:扩展插件依赖 Service Definition(抽象服务) 而非具体 provider。这样 LLM 适配器、执行器等都能热替换、互不影响。服务名共用扁平命名空间,自有服务请加前缀(harness 已占用 tools、llm 等)。

七、事件系统:解耦通信与拦截

服务支持直接调用;事件让插件无需知道谁在监听就能广播。harness 用事件处理工具结果、模型请求、审批决定等交互。

7.1 声明、发出、监听

stats.ts(计数服务):

import { Service, type Context } from '@deepseek-ai/cordis'

declare module '@deepseek-ai/cordis' {
  interface Context { stats: StatsService }
  interface Events {
    'stats/report'(name: string, count: number): void
  }
}

export class StatsService extends Service {
  private counts = new Map<string, number>()
  constructor(ctx: Context) { super(ctx, 'stats') }
  bump(name: string) {
    const next = (this.counts.get(name) ?? 0) + 1
    this.counts.set(name, next)
    this.ctx.emit('stats/report', name, next)
  }
}

reporter.ts:

import type { Context } from '@deepseek-ai/cordis'
import type {} from './stats.ts'

export const name = 'reporter'
export const inject = ['stats']

export function apply(ctx: Context) {
  ctx.on('stats/report', (name, count) => {
    console.log(`[stats] ${name} -> ${count}`)
  })
  ctx.stats.bump('tool_call'); ctx.stats.bump('tool_call'); ctx.stats.bump('prompt')
}

import type {} from './stats.ts' 让 TS 看到声明合并(运行时无副作用)。输出:

[stats] tool_call -> 1
[stats] tool_call -> 2
[stats] prompt -> 1

ctx.on() 属于 effect,监听器随插件消失,绝不需手动 removeListener。

注意 declare module '@deepseek-ai/cordis' { interface Events { ... } } 这处声明合并:它把 'stats/report' 及其签名写入 Cordis 的全局事件表,于是 ctx.emit / ctx.on 在编译期就检查事件名与参数类型。漏写声明合并,事件仍是合法的 string 事件,但失去类型保护——这是基于 Cordis 开发时最常踩的坑。

7.2 五种分发模式

模式调用语义
emitctx.emit(name, ...)