Martty

架构与实践

《我为DeepSeek Harness补齐了这四个重要的基础Agent组件--DSH Plugin最佳实践》

DeepSeek Harness 四个 Agent 组件与 DSH Profile 的关系

TL;DR

  • Plugin 让 Agent 的新能力不必等进核心版本,也能先从社区长出来。
  • DSH Plugin 有何不同:它不只组合 Plugin,还允许 Agent 沿 Scope 父链继承能力。复用更方便,代价是开发模型更复杂,隐式依赖和同名遮蔽也更难排查。
  • Plugin 适合开发者拆分能力;面向用户时,Preset 提供了选择一整套能力的更高层粒度。AgentPreset 组合 Agent 的能力与配置,PermissionPreset 组合一组执行策略。
  • 一个好的 Plugin,既要能插,也要能被插。

过去一周,我围绕 DeepSeek Harness 做了四个插件:

回头看,这四项工作把同一套 Plugin 机制推到了四个不同的边界。DeepSeek Harness ACP 把 DSH 的 Session、Tool 和权限翻译成 ACP 消息,让 Zed、Martty 等 Client 可以直接连接 DSH;DSH Agent Plugins Bridge 把其他 Agent 生态的插件转换成 DSH 能管理的能力;DSH MCP Apps 在现有 MCP 支持之上补齐应用层;Martty 则让TUI插件本身也成为一个可以继续扩展的组件。

这些边界看似不同,落到 Harness 里却反复遇到同一组问题:Plugin 如何声明依赖,package 如何进入 Profile,Loader 怎样创建 Fiber,Web 为什么拥有独立 Client tree,动态代码如何服从同一套生命周期,以及 Preset 怎样把一组 Plugin 组合成可选择的 Agent。社区可以沿用这些官方抽象补充新的 Agent 能力,无须把每项能力都写进 Harness 核心。

我们先从一个最小 Plugin 开始,顺着加载过程走到外部生态、Web、动态 Plugin 和 Preset,最后再回看这四项工作留下了哪些可复用的实践。

从最基础的 Plugin 开始

DeepSeek Harness ACP 的 stdio transport 几乎就是一个最小的 DSH Plugin:

export const inject = ['acpServer']

export function apply(ctx) {
  const stream = nodeAcpStream(process.stdin, process.stdout)
  return ctx.acpServer.connect(stream)
}

这个模块只做两件事:

  1. inject 声明它需要 acpServer;
  2. apply(ctx) 获得满足依赖的 Context,把 stdio stream 接到 Service 上。

这几行代码已经包含“插入”的基本原理。Plugin 不寻找全局 ACP Server,也不创建第二套 Harness。它等待 Host 提供 Service,再把一个 transport 接上去。连接产生的运行期资源由 Fiber 持有,上层退出时再按各自的所有权回收。事件、定时器或注册表贡献通常用 ctx.effect() 绑定清理函数,同样服从这条生命周期。

要让 DSH 找到这个模块,package 还要声明自己的 Bundle patch:

{
  "dsh": { "bundle": { "patch": "./cordis.patch.yml" } }
}

patch 再把模块写成 Loader row:

- insert:
    - id: acp-bridge
      name: '@openma/deepseek-harness-acp/stdio'

TypeScript 模块定义 Plugin 的形状,package.json 告诉 DSH 去哪里找组合声明,Bundle patch 决定它插在 profile 的什么位置。这三部分构成一个最小、可安装的 DSH Plugin。

所以,一个合格的 Plugin 要满足四个条件:

  • 依赖可声明:需要什么 Service,由 inject 说明,形成明确的依赖图;
  • 能力可组合:通过 Context 注册贡献,Host 无须增加专用分支;
  • 实例可隔离:每个配置实例由独立 Fiber 持有;
  • 生命周期可逆:停用后,连接、事件和界面贡献完整撤销。

Host 无须理解 Plugin 的具体业务,也能用同一种方式装载、管理和卸载它。

它是怎样从安装走到运行的

DSH 的加载链路可以压缩成五层:

Profile
  └─ 有序 Bundle patch layers
       └─ Loader rows
            └─ Cordis Fibers
                 └─ Services / Events / Effects

Profile 决定当前 DSH 实例组合哪些能力。dsh plugin --profile acp add ... 会让 pnpm 把 package 安装进 profile,同时把 Bundle 名称追加到 package.json 的 dsh.profile.bundles 列表。

Bundle patch 是声明式组合层。Patch 的优先级来自这份 bundles 列表,并非 pnpm 的依赖图或 lockfile 顺序。DSH 依次应用各 Bundle 的 patch,再应用 profile、home 和命令行 overlay;后面的层可以按 id 覆盖前面的 Loader row。--dump-config 展示最终合成结果。

Loader row 对应一个可管理的 Plugin 实例。它在配置数组里的位置也不负责表达运行依赖:Loader 可以并发挂载各 row,Plugin 通过 inject 等待所需 Service。Loader 先为 row 创建 Fiber,等依赖满足后再调用 apply。Plugin 注册的 Service、事件和 effect 都归该 Fiber 所有。禁用 row 时,Loader dispose Fiber,各项贡献沿原路径退出。

这就把“npm 包已经安装”和“能力正在运行”分开了。package 提供代码,Bundle 决定插入位置,Loader 创建实例,Fiber 持有运行期资源。

DSH Plugin 从 package 安装到 Fiber 运行及 dispose 回收的流程

DeepSeek Harness ACP 的演进让这条分界逐渐清楚。最初它更像一个自带启动逻辑的独立适配器:找到 Harness、创建 Session、占用 stdio,然后把两边消息互译。独立运行没有问题,一旦希望它进入 Zed、Martty 或任意现有 profile,自行启动第二套 Host 就会复制凭证、Session、Tool 和持久化状态。

后来的改动逐步收回了启动器里的专用分支:ACP Host 被暴露成可嵌入 Plugin,stdio、TUI Client bridge、用户提问等部分又拆成独立子 Plugin。Host 提供 agents、commands、approval 等 Service,ACP 只做协议投影;某个 Client 离开时,只回收这条连接带来的资源,不影响 Harness 中的其他 Session。这段开发经历给了我们第一个判断标准:能运行的集成还不够,好的集成必须能进入别人的 composition,并把自己拥有的资源完整交还。

组合与继承:DSH Plugin 有何不同

Codex、CC、Pi 和 Agent Plugins 都把自己的扩展称为插件,却没有共享同一个运行时含义。它们各自规定 package 从哪里发现、manifest 可以声明什么、代码在哪个扩展点执行。DSH Plugin 最终进入 Cordis composition:Bundle patch 把 Loader row 插入当前 Profile,Plugin 再通过 Service 向系统贡献能力。

Agent Plugins 标准在这些生态之上提供了一套可移植的 package 规范:用 plugin.json 描述插件,以 skills/ 和 mcp.json 承载标准能力。它主要解决“一个插件包如何被不同 Agent 识别”,DSH 继续回答“包里的能力进入运行时后怎样相互依赖、由谁持有”。

多数 Agent 插件系统先从 package 的内容出发:把 Skill、MCP、Hook 等能力放进同一个包,安装时再把这些内容组合进 Agent。DSH 的运行时还要处理两种关系:inject 决定 Plugin 等待哪个 Service,Fiber 父子树决定资源由谁拥有、跟谁一起退出。对于 Tool、Skill、prompt section 等支持 Scope 的 registry,Agent 还可以沿 agent → preset → global 的父链查找能力。普通 Service 不走这条 Scope 父链,仍由 Context、realm 与 inject 解析。

这套能力继承与编程语言里的继承几乎同构。公共 Tool、Skill 和 Prompt 可以复用,代价也是熟悉的:隐式依赖、父层耦合与同名遮蔽。排查一个 Agent 的有效能力时,单看当前 composition 不一定够,还要沿 Scope 父链向上看。组合适合可选、易变的能力,继承更适合稳定基座。

inject 也只是一种偏弱的依赖声明:它写明所需 Service 的名字,却不固定由哪个 Plugin 提供,也不在声明里约束 provider 的版本与作用域。依赖图要到实际 composition 中才闭合。这样做让实现可以替换、Plugin 可以自由组合;相应地,缺失依赖、提供者冲突和版本不兼容也更晚暴露。

用户可以扩展的位置更多了:协议、Tool、Agent composition、后端连接、Web slot 和 Client Surface 都能由社区补充,而且可以分别启停、局部失败。代价落在开发与治理上:作者要理解 Bundle、Loader、Service、Fiber;生态越开放,插件在清理、错误处理、UI、权限与升级上的质量也越容易良莠不齐。

外部插件因此不能只做 manifest 改名。Skill 要进入 Skill Service,MCP Server 要成为 MCP Client row,Hook 和 Agent 也要找到各自的注册入口。

Harness 的核心提供稳定的 Context、Service、Loader 与 Fiber,新的格式和能力可以在社区 Plugin 中生长。这像是 Bitter Lesson 在插件系统里的一层工程映射:与其由核心团队不断穷举能力,不如先提供能承接未知扩展的通用机制。

如果你还想享受到传统的 Harness 插件生态,Agent Plugins Bridge 负责这层转换。它以 Agent Plugins 1.0 作为可移植核心规范,再为 Codex、Claude Code 和 Pi 增加兼容 adapter:Discovery 只读识别,用户明确 Import 后才复制到当前 Profile,Activation 再把各项能力变成 Loader row。

Web Plugin:Host、Client 与双向 Plugin

官方 Web 运行着两棵彼此独立的 Cordis tree:一棵在 Node Host 中,拥有 Agent、Session 和后端 Service;另一棵在浏览器中,拥有 React、页面 slot、主题和交互状态。Web 中看到的设置页、Tool 卡片、主题和其他 UI contribution,都来自浏览器这棵树上的 Client Plugin。两棵树不共享 Context、Fiber 或对象引用。

按代码实际运行的位置,可以把它们分成三类:

  1. Host Plugin 只运行在 Node 进程,提供 Agent、连接、存储、权限或其他后端 Service,不产生浏览器界面;
  2. Client Plugin 只在浏览器 Cordis tree 中运行,向 slot、主题或交互状态注册贡献。它的 package 仍需要一条 Host row 供 Modules Service 发现,但 Host entry 可以为空;
  3. 双向 Plugin 在同一个安装单元中同时交付 Host half 与 Client half。Host half 持有数据和资源,Client half 负责呈现,双方通过 Remote 交换明确的数据、调用与事件。

三类 Plugin 背后都有依赖和所有权层次。Host tree 和 Client tree 各自拥有 inject 形成的依赖关系与 Fiber 形成的所有权关系;双向 Plugin 用 Remote 连接两套层次,不会把它们合并成一棵树。

双向 Plugin 包含两个 Plugin 实例。Package 的根 entry 由 profile Loader 挂到 Host;manifest 中的 dsh.client 声明浏览器依赖,./client 导出 Client Plugin。Host 的 Modules Service 扫描已启用的 row、生成浏览器模块图,页面 Loader 再为 Client half 创建独立 Fiber。“双向”描述的是一个安装单元同时覆盖两端,而且数据可以往返,不代表一个 Fiber 或 Context 横跨两个进程。

同一个 package 名不代表同一个 Plugin 实例。Host row 与浏览器 row 拥有不同 Context 和不同 Fiber。页面刷新只重建 Client tree,Host 中的连接、任务或监听可以继续运行;反过来,Host Plugin 卸载也不会越过进程边界直接销毁已经打开页面里的 Client Fiber,页面要在刷新或重新组合后才得到新的模块图。Web UI 只是 Host 能力的一种呈现方式,不掌握后端资源的生命周期。

双向 Web Plugin 在 Node Host 与浏览器中产生两个独立实例

DSH MCP Apps 就是一个双向 Plugin。我们开始开发时所基于的 DSH 版本,已经通过 dsh-mcp-client 提供 MCP 连接、Tool 发现与执行等基础能力,还没有支持 MCP Apps。Apps 需要从 Tool definition 的 _meta 找到 ui:// Resource,再读取界面内容、建立 AppBridge,并安全运行 Server 提供的 HTML。

这个安装包同时交付 Host 与 Client 两端的 Bundle。Host 复用现有 MCP connection,Client 接管对应的 Tool result slot,把 App 放进 iframe sandbox;用户操作再经 Remote 回到 Host。AppBridge 与 sandbox 构成专门的 App 呈现路径,两端本身仍由普通 Loader 管理,也不会复制第二条 MCP connection。TUI 仍可使用同一批 MCP Tool 和 Resource,只是不执行浏览器 HTML。

Creator 模式下的动态 Cordis Plugin:自进化的

动态插件解决的是另一件事:代码不必预先存在于 profile,Agent 可以在自己的运行过程中不断给自己添加插件。

具体来说,cordis_define tool 用于验证并登记代码,每次修改都会留下一个新的不可变版本;cordis_run 负责装载。停止会撤销正在运行的实例,删除则连同定义和全部版本一起清理。

动态 Plugin 仍使用静态 Plugin 的装载规则。Web 的 cordis-client-runner 会把运行时代码转换成普通 Loader entry,所以它仍然要经过 inject 等待、apply 激活和 disposer 清理。运行后的 Plugin 作为 Runner 下的一棵子 Fiber 加入原有所有权层,也通过 Service 加入原有依赖层。它只改变代码的来源和加入 Loader 的时机,依赖与所有权关系没有另起一套。

define 与 run 分开后,定义阶段只登记版本,运行阶段才跨越信任边界、产生 Fiber 与副作用;调试失败时可以追加版本再切换。官方实现也刻意把定义留在 Host 进程内存中:页面刷新只重建 Client tree,已保存的定义仍然存在;Host 进程重启后,它们不会自动恢复。若要长期使用,应当把验证过的结果保存成普通 package,让安装、版本与卸载重新回到静态 Plugin 流程。

动态 Plugin 也能运行在 Web 之外。只有 Host half 的 Plugin 可以在组成了 Host Runner 的 headless 或 ACP 环境中运行;带 Client half 的 Plugin 则需要某个 Surface 提供对应 Runner。官方 Web 用 React 与页面 slot 呈现它,Martty 用 TuiNode 与终端 Service 呈现它。

Preset:决定 Agent 带着哪套能力进入运行时

前面说到,Plugin package 可以组合多种能力,DSH 又让这些能力形成依赖与继承。但运行环境已经装入什么,不等于每个 Agent 都要得到什么。Preset 把组合带到 Agent 这一层:它把一组相互配合的选择归并成有名字的方案,再交给具体的 Agent。

动态 Plugin 的 Host Runner 可以一直存在于运行环境中,但普通 Agent 未必能看到控制它的 Tool。官方 cordis AgentPreset 在完整 composition 中加入 tool-cordis 和配套 Skill,让 Agent 能够检查 Cordis API、定义和运行 Plugin,并根据装载错误继续修改。

官方 Web 上的 Cordis 动态插件因此由三部分共同完成:静态的 cordis-client-runner 负责装载,静态的 ui-cordis 负责审批与管理界面,cordis AgentPreset 让 Agent 成为定义者。Plugin runtime 提供能力,AgentPreset 决定哪一种 Agent 可以使用它。 换回 standard 后,Host 中的基础设施仍然存在,只是当前 Agent 不再拥有那组创作工具。

系统同时暴露 prompt、Tool、Skill、模型、sandbox、approval 等自由度后,任意组合会迅速制造出许多未经验证的状态。Preset 把需要一起选择的决定归并成一套有名字、可复用的方案,并明确三件事:

  1. 一致性:整套组合表达一个明确意图;
  2. 协同边界:哪些选择需要一起变化,失败时各自如何处理;
  3. 生效时机:它何时确定,以及哪些后续状态依赖它。

AgentPreset 对组合做得更深:它把一份 composition 变成可继承的 standing scope;Agent 选择 Preset,相当于选择自己的能力父层。PermissionPreset 只组合两个策略值,没有建立新的 Scope 继承关系。两者名称相似,前者建立能力层次,后者提供操作上的快捷选择。

AgentPreset 直接把 Preset 实现为一层 Scope。官方实现中,每个 Preset 是一个带 agent.cordis.yml 的目录,里面仍是一组普通 Loader rows。Roster 将这份 composition 挂成常驻 scope,Agent 创建时把自己的 scope 接到它下面;对于 Tool、Skill 和 prompt section 等支持 Scope 的 registry,查找顺序于是形成 agent → preset → global。不同 Preset 会从 prompt、Tool、Skill、模型与 compaction 等能力中取舍,并不要求齐全;例如 minimal 只保留少数基础工具,并不带 Skill 与 compaction。Plugin 实例可以共享,按 Session/Agent 分键的状态仍彼此隔离。

编程语言里的“脆弱基类”问题,在这里会变成“脆弱 Preset”。Agent 的有效能力来自整条父链,单看 Session 自己看不全;修改父 Preset 会影响之后加入它的 Agent,局部同名 Tool 又可能遮蔽父层版本。官方因此让运行中的 Agent 固定在已经挂载的 composition generation 上:Preset 文件后来变化,新会话可以使用新版本,旧会话仍保留创建时的解释环境。这条时间边界让能力复用不至于破坏旧会话的可重现性。

AgentPreset 因此比“配置模板”更重。它保存的是一张能力拓扑:模型首轮看到哪些 Tool schema、哪些 prompt 段落进入前缀、Skill 从哪里发现、压缩策略怎样参与长期历史。选择一旦进入对话,这张拓扑就成了历史的解释环境。官方 recompose() 因此只服务空白 Agent;它会先确保新 composition 已经挂载,再移动 Agent 的 scope,失败时保留旧绑定。已有输出的 Session 被锁住,因为旧历史可能含有新 Preset 无法解释或执行的 Tool call。

PermissionPreset 则是一张策略表。一个名字对应 sandbox/mode 与 approval/policy 两个独立调节项,例如 workspace-write + ask 或 danger-full-access + never。切换时,服务记录用户选了哪个组合,再按顺序更新真正被执行器和审批器消费的值;这条路径没有 AgentPreset recompose 那样的事务回退。如果用户分别修改两个调节项,最终没有命中任何具名组合,界面会显示派生的 custom。它约束下一次 Tool 如何执行,不改写 Agent 的工具视图和历史语义。

AgentPreset 的能力 Scope 与 PermissionPreset 的执行策略对比

两者只共享 Preset 这个思路:把容易失配的一组决定归并成一个用户意图,并规定它何时生效;Preset 这个名字本身并不保证事务语义。AgentPreset 回答“这个 Agent 是谁、会做什么”,PermissionPreset 回答“它接下来能做到什么程度、什么时候要问人”。

不仅要插,还要被插:Martty 的例证

Martty 的架构直接受到官方 Web 双树模型的启发。我们保留 DSH 的 Host Cordis tree,另外启动一棵 Node Client tree,用 ACP 连接两端;Rust 只拥有 TTY、输入和绘制,不再复制一套 Agent runtime。于是,Web 的 React renderer 可以换成终端的语义 renderer,Plugin 的依赖、Fiber 和生命周期仍然成立。

这并非一开始就设计完整。早期终端程序直接拥有配色、状态栏、计划视图和 Session 状态,功能增长很快,但每多一种界面能力,Rust 主循环就多认识一种业务。沿着后续提交回看,Plan 与统计先被移成 Client Plugin,欢迎页被拆成 slot contribution,接着有了 UI Preset、主题包和持久化的本地 Plugin 生命周期。Rust 逐渐退回 painter:接收结构化 TuiNode,处理布局、键盘与终端兼容,不理解某块内容来自 Plan、Bridge 还是模型生成的动态 Plugin。

这里沿用了 Web 的边界,也保留了进程隔离。Host tree 与 Client tree 不共享 Loader graph、ctx.* 或 Fiber。标准 ACP 负责 Session、命令和配置;双方协商 _dsh/cordis 能力后,再传递运行请求和序列化后的 Surface 更新。协议里即使带有用于关联两侧实例的 ID,也不表示两端共享同一个 Plugin。我们没有尝试把整棵 Cordis tree 跨进程同步,因为依赖和生命周期只有留在各自拥有资源的进程里才清楚。

Martty 的 Client Plugin 通过 Service 管理 TuiNode。主题、slot、command 和 overlay 都是 Service;第三方 Plugin 通过 inject 加入依赖层,通过注册返回的 disposer 加入所有权层。Rust painter 位于这套层次的末端,只消费最终投影,不成为所有界面插件共同依赖的全局对象。

Martty 本身先作为 surface plugin 插入 DSH:

dsh plugin --profile martty add martty@latest

这个 Bundle 把 ACP 与 Creator overlay 挂到 Base Host tree,并建立 Client 与 painter 的启动关系。Harness 无须为 Martty 修改源码;移除这个 Bundle 或 package,就能撤下整套 TUI surface。

Martty 还保留独立 ACP Client 的身份。同一份 Client Plugin 既可以作为独立 Client tree 的根,spawn dsh-acp 或连接其他 ACP Agent;也可以作为某棵已有 Client tree 中的一条 row,消费调用方给出的 stream。安装到 DSH profile 是第一层“被插入”;作为 row 进入别人管理的 Client composition 后,依赖和回收也交给调用方管理。

同时,Martty 也允许其他 Plugin 插入自己:

拿构建监控举例:Web Plugin 向 slot 注册 React component;Martty Plugin 向 chrome.right 注册一组 TuiNode,由 Rust painter 绘制终端右栏。功能相同,表现层各自适配,Plugin 只描述自己贡献什么。

两端都限制 Plugin 能直接接触的资源。终端 Plugin 看不到 raw mode、文件描述符、ratatui widget 和绝对坐标;Web Plugin 同样不直接拿 Host 的 Session 对象。插件得到的是语义插槽和服务目录,宿主保留物理渲染与资源所有权。这样的 API 看起来少,却让插件能够随 Fiber 一起完整卸载,也让第三方贡献不必绑定 Martty 内部实现。

Martty 的 default 与 deepseek 欢迎页也都是 UI Plugin。它们通过 tuiPresets.register(...) 组合 welcome.hero 和 welcome.info,自动进入 /ui 选择器,并沿用统一的切换、持久化和卸载流程。这里组合的是 Client UI,与 Host 中的 AgentPreset 分属两棵树。这是 Preset 在 Client 侧的用法:用户选择一套完整的界面,底层仍由多个独立 slot contribution 组成。

至此,“好的插件应该能被插”才有了完整含义。Martty 能插进 DSH,也能作为 ACP Client 插进其他 Agent;主题、命令、面板和动态 Cordis Client Plugin 又能插进 Martty。每一层只占有自己的资源,并为下一层留下稳定的扩展入口。

Martty 作为 ACP Client 插入 DSH,并允许第三方 Client Plugin 插入自己

从这些例证提炼 Plugin 最佳实践

这段开发过程留下了十条可以复用的实践:

  1. 依赖必须显式声明。 Plugin 通过 inject 等待 Service,不扫描全局对象,也不猜测启动顺序。
  2. 用层次表达关系。 inject 表达 Service 依赖,Fiber 父子树表达所有权,支持 Scope 的能力 registry 才沿父链继承;不要用 row 的排列顺序暗示它们。
  3. 克制使用继承。 Scope 父层只承载稳定的共享基座;可选和易变能力尽量显式组合,并让遮蔽关系与最终有效能力可检查。
  4. 安装与运行分层。 package 负责交付代码,Bundle patch 负责组合,Loader row 代表实例,Fiber 才拥有运行中的资源。
  5. 每项贡献都要可逆。 Event、timer、connection、slot 与 registry registration 都随 Fiber dispose,卸载不留下半活状态。
  6. 按所有权拆 Plugin。 Host 资源留在 Host,浏览器与终端界面留在各自 Client tree;一个 npm package 可以包含多个生命周期。
  7. 跨边界只投影能力。 Remote 或 ACP 传递明确的数据和操作,不同步 Context、Fiber 与整棵插件图。
  8. 动态代码仍走普通 Loader。 来源和授权时机可以变化,inject、apply、错误隔离与清理规则保持一致。
  9. Preset 组合用户意图。 经验证的一组能力可以成套选择,同时要尊重 Agent composition 与运行权限不同的生效边界。
  10. 既要能插,也要能被插。 Plugin 通过公开扩展点进入别人的系统,也应在合适的位置继续开放 Service、slot 或协议,让下一项能力不必修改它的核心代码。

我们的四个组件都沿用 DeepSeek Harness 已经验证过的扩展点,并把它们带到协议、外部生态、Web 和终端:

Harness 官方机制我们的实践保持不变的原则
Profile、Bundle patch、Loader、FiberACP 作为可嵌入 Host Plugin;Bridge 将外部能力编译成 rows安装、实例与生命周期分层
inject、Fiber parent、Scope parentACP 子 Plugin、Martty Client Plugin依赖、所有权与继承不靠数组顺序
AgentPreset standing scopeACP 投影 Preset roster;Martty 用 /agent 选择composition 仍由 Host 挂载
Web 的 Host/Client 双 Cordis treeMCP Apps 拆 Host/Web;Martty 建独立 Client tree两端不共享 Context 与 Fiber
Remote 与类型化 Client slotAppBridge Remote;ACP Cordis 扩展;TuiNode slots跨边界只传明确能力和数据
动态 Host/Client RunnerWeb 承接 React half;Martty 承接终端 code.client临时代码仍服从 Loader 生命周期

这些映射都保留了原系统的边界。Web React 组件没有被硬搬进终端,ACP 也没有变成另一套 Harness;Bridge 不模拟四个完整 Agent runtime,MCP Apps 不复制第二条 MCP connection。每次扩展都先寻找原系统已经拥有的 Service,再贡献最小的新能力。

回看这一周的开发,四个项目最终各自接住了一个清晰的扩展点:ACP 接协议,Bridge 接外部插件,MCP Apps 接交互式 Tool,Martty 接终端。它们沿用 Loader、Service 与 Fiber,也没有要求 Harness 核心提前认识每一种新能力。对社区来说,这大概是插件机制最理想的状态:装上以后像系统原生能力,移除时又能干净退出;下一种 Agent 能力出现时,继续增加一个 Plugin 就够了,无须 fork Harness。

前九条让 Plugin 可以安全地进入现有系统,第十条决定它会不会成为生态的新起点。只消费别人扩展点的 Plugin 最终会停在叶节点;允许下一层能力继续进入,它才会把一次集成变成可以继续生长的生态。

如果需要一个现成范例,可以拿 Martty 对照。它既能作为 DeepSeek Harness Plugin 安装进 DSH,也能作为独立 ACP Client 连接其他 ACP Agent;主题、命令、面板和动态 Cordis Client Plugin 又可以进入 Martty。两个方向都不要求修改对方的核心代码,依赖和生命周期也各自留在资源真正所属的一侧。

相关项目与规范