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

TL;DR
- Plugin 让 Agent 的新能力不必等进核心版本,也能先从社区长出来。
- DSH Plugin 有何不同:它不只组合 Plugin,还允许 Agent 沿 Scope 父链继承能力。复用更方便,代价是开发模型更复杂,隐式依赖和同名遮蔽也更难排查。
- Plugin 适合开发者拆分能力;面向用户时,Preset 提供了选择一整套能力的更高层粒度。AgentPreset 组合 Agent 的能力与配置,PermissionPreset 组合一组执行策略。
- 一个好的 Plugin,既要能插,也要能被插。
过去一周,我围绕 DeepSeek Harness 做了四个插件:
- Martty:TUI插件,也是一个 ACP Client,可以连接 DSH 或其他 ACP Agent;同时开放主题、命令、面板和动态 Cordis Client Plugin 扩展点,既能被插入,也允许别人继续插入。
GitHub:https://github.com/openma-ai/Martty - DeepSeek Harness ACP:让 DSH 能够接入 Zed、TUI 等 ACP Client。
GitHub:https://github.com/openma-ai/deepseek-harness-acp - DSH Agent Plugins Bridge:把 Agent Plugins、Codex、Claude Code、Pi 插件转换成 DSH 能力。
GitHub:https://github.com/openma-ai/dsh-agents-plugins - DSH MCP Apps:在 DSH 已支持 MCP 的基础上补齐 MCP Apps,让 Tool 交付可交互界面。
GitHub:https://github.com/openma-ai/dsh-mcp-apps
回头看,这四项工作把同一套 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)
}这个模块只做两件事:
inject声明它需要acpServer;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 / EffectsProfile 决定当前 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 持有运行期资源。

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 或对象引用。
按代码实际运行的位置,可以把它们分成三类:
- Host Plugin 只运行在 Node 进程,提供 Agent、连接、存储、权限或其他后端 Service,不产生浏览器界面;
- Client Plugin 只在浏览器 Cordis tree 中运行,向 slot、主题或交互状态注册贡献。它的 package 仍需要一条 Host row 供 Modules Service 发现,但 Host entry 可以为空;
- 双向 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 能力的一种呈现方式,不掌握后端资源的生命周期。

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 把需要一起选择的决定归并成一套有名字、可复用的方案,并明确三件事:
- 一致性:整套组合表达一个明确意图;
- 协同边界:哪些选择需要一起变化,失败时各自如何处理;
- 生效时机:它何时确定,以及哪些后续状态依赖它。
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 的工具视图和历史语义。

两者只共享 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。每一层只占有自己的资源,并为下一层留下稳定的扩展入口。

从这些例证提炼 Plugin 最佳实践
这段开发过程留下了十条可以复用的实践:
- 依赖必须显式声明。 Plugin 通过
inject等待 Service,不扫描全局对象,也不猜测启动顺序。 - 用层次表达关系。
inject表达 Service 依赖,Fiber 父子树表达所有权,支持 Scope 的能力 registry 才沿父链继承;不要用 row 的排列顺序暗示它们。 - 克制使用继承。 Scope 父层只承载稳定的共享基座;可选和易变能力尽量显式组合,并让遮蔽关系与最终有效能力可检查。
- 安装与运行分层。 package 负责交付代码,Bundle patch 负责组合,Loader row 代表实例,Fiber 才拥有运行中的资源。
- 每项贡献都要可逆。 Event、timer、connection、slot 与 registry registration 都随 Fiber dispose,卸载不留下半活状态。
- 按所有权拆 Plugin。 Host 资源留在 Host,浏览器与终端界面留在各自 Client tree;一个 npm package 可以包含多个生命周期。
- 跨边界只投影能力。 Remote 或 ACP 传递明确的数据和操作,不同步 Context、Fiber 与整棵插件图。
- 动态代码仍走普通 Loader。 来源和授权时机可以变化,
inject、apply、错误隔离与清理规则保持一致。 - Preset 组合用户意图。 经验证的一组能力可以成套选择,同时要尊重 Agent composition 与运行权限不同的生效边界。
- 既要能插,也要能被插。 Plugin 通过公开扩展点进入别人的系统,也应在合适的位置继续开放 Service、slot 或协议,让下一项能力不必修改它的核心代码。
我们的四个组件都沿用 DeepSeek Harness 已经验证过的扩展点,并把它们带到协议、外部生态、Web 和终端:
| Harness 官方机制 | 我们的实践 | 保持不变的原则 |
|---|---|---|
| Profile、Bundle patch、Loader、Fiber | ACP 作为可嵌入 Host Plugin;Bridge 将外部能力编译成 rows | 安装、实例与生命周期分层 |
inject、Fiber parent、Scope parent | ACP 子 Plugin、Martty Client Plugin | 依赖、所有权与继承不靠数组顺序 |
| AgentPreset standing scope | ACP 投影 Preset roster;Martty 用 /agent 选择 | composition 仍由 Host 挂载 |
| Web 的 Host/Client 双 Cordis tree | MCP Apps 拆 Host/Web;Martty 建独立 Client tree | 两端不共享 Context 与 Fiber |
| Remote 与类型化 Client slot | AppBridge Remote;ACP Cordis 扩展;TuiNode slots | 跨边界只传明确能力和数据 |
| 动态 Host/Client Runner | Web 承接 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。两个方向都不要求修改对方的核心代码,依赖和生命周期也各自留在资源真正所属的一侧。
相关项目与规范
- Martty:https://github.com/openma-ai/Martty
- DeepSeek Harness ACP:https://github.com/openma-ai/deepseek-harness-acp
- DSH Agent Plugins Bridge:https://github.com/openma-ai/dsh-agents-plugins
- DSH MCP Apps:https://github.com/openma-ai/dsh-mcp-apps
- DeepSeek Harness:https://github.com/deepseek-ai/deepseek-harness
- Agent Plugins 标准:https://agent-plugins.org/