我支持内部尽可能开放,外部有意识地限制。平台应该有自己的判断,而不是把所有可能性原样交给插件作者和用户。
“一切皆插件”作为内部架构原则,我非常认同。通用引擎最怕的就是不断被具体产品的需求侵蚀:今天加一个特例,明天再补一层判断,最后虽然还叫通用引擎,实际上已经只能服务当前这个产品。其他人想做不同的东西,只能 fork,然后各自维护一套。
所以 DSH 希望把 Agent Loop、Session、Tool Runtime、LLM、Persistence、Sandbox 甚至 UI 都变成可以重新组合的部分,这个方向是有价值的。Harness 还处在快速变化期,现在也没有哪套实现已经被证明是最终答案。底层如果太早写死,后面想换一种上下文组织方式、Session 模型或者 Agent Loop,都会很难。
但我不认为,由此就能推出“所有内部逻辑都应该成为第三方扩展点”。
把 Pi、VS Code 和 DSH 放在一起看,区别其实不在于谁有插件冲突。Pi Extension 一样可以注册同名 Tool、修改 Prompt、拦截同一个事件;VS Code 里也一直有格式化器、快捷键和语言服务互相影响的问题。只要多个扩展可以同时运行,命名、顺序、优先级和兼容性就一定会出现。
真正的区别,是插件可以改到哪一层,以及平台有没有提前规定这些变化怎么组合。
VS Code 的做法是,核心仍然由平台控制,第三方主要通过 Contribution Points 和稳定接口增加能力。某个扩展点允许多个 Provider 时,宿主决定结果怎么合并;某个能力只能有一个实现时,就让用户选择;名字必须唯一的地方,就在注册时处理。它当然不能消灭冲突,但它尽量把冲突限制在已经定义好的范围里。
Pi 更开放一些。Extension 可以注册 Tool 和 Command、修改上下文、监听或拦截 Agent 的运行过程,甚至通过同名注册覆盖内置 Tool。但它仍然有一个相对固定的 coding-agent core。Agent Loop、Session Runtime 等部分主要由宿主持有,所以 Extension 大多还是围绕同一个内核运行。
DSH 再往下走了一层。它不是在固定内核周围增加能力,而是让插件共同组装内核本身。Session、Agent Loop、Tool Runtime、Persistence、Sandbox 都可能来自不同插件。
这种设计的自由度确实更高,但需要处理的问题也不再只是“两个 Tool 重名”。
DSH 实际上同时存在几套身份:npm/module instance、Cordis service 或 Symbol、loader entry、Tool/Command/Route/Prompt 的名字,以及 host/preset/agent/session 的 Scope。同一个插件在配置里看起来只装了一次,不代表运行时一定只有一个 module instance;module instance 相同,也不代表 registry、loader 和 Scope 对它的理解一致。再加上 reload 和 generation 切换,问题还会沿着时间继续展开。
这不是纯理论问题。至少在 rc.6 的社区运行时报告里,已经出现过双实例导致 Symbol 分裂、重复 loader entry、process-global registry 冲突、late provider 工具重名,以及旧 generation 没有完全 dispose 等情况。这里需要强调,这些是社区报告,不等于维护者已经确认了同一个根因;但它们足以说明,DSH 要处理的是一整套运行时身份和生命周期问题,不只是普通的插件重名。
Cordis 能很好地处理 activation、依赖等待、effect disposal 和 Scope visibility,但这些机制本身并不会决定:重复注册应该 reject、replace、merge 还是 shadow;谁拥有一个名字;两个实现谁优先;安装失败以后怎么回滚;旧 generation 什么时候才算真正退出。
这些选择最后还是要由平台来做。
Effect 也是一样。它能把依赖和副作用表达得更清楚,让资源释放、重试、并发和 TestClock 变得更容易测试。这是很大的价值,我完全认同。
但能够静态知道一段代码访问了文件、网络或者数据库,并不等于系统已经知道两个插件发生冲突时应该听谁的。知道“发生了什么”和决定“应该怎么办”,是两件事情。
外部操作尤其明显。消息已经发送、文件已经被读取、支付已经完成、货已经发出,这些事情不会因为插件 dispose 就自动消失。框架可以让它们更容易追踪,也可以强迫开发者把补偿逻辑写出来,但具体是重试、补偿、对账还是人工处理,仍然是产品选择。
所以“不 fork 主干”当然是个很好的目标,但它不会让不同需求之间的分歧消失。以前这些分歧存在于代码分支里,插件化以后,它们会存在于 Profile、Bundle、Scope、插件版本和加载顺序的不同组合里。
这通常仍然比长期维护多个 fork 更好,但维护成本只是换了位置。以前处理 merge 和升级,现在处理依赖、身份、生命周期、状态兼容和组合测试。
AI Coding 可以降低写 Adapter、迁移接口和补兼容层的成本,却不会自动决定两个插件冲突时哪一种行为才符合产品意图。代码越来越容易生成以后,平台自己的判断反而会变得更重要。
这也是我为什么更支持“内部开放,外部限制”。
平台不应该只是一个中性的插件容器。它首先应该知道自己要做成什么产品,然后决定哪些变化可以交给第三方,哪些行为必须保持一致。开放一个稳定接口,不只是让别人能够调用它,也意味着平台准备长期维护它的语义、升级方式和失败处理。
如果平台对什么都不做判断,默认所有逻辑都能被第三方改写,那么冲突出现以后,用户只能自己判断应该装哪个插件、谁覆盖谁、哪个版本能一起工作。这个结果不是更开放,而是平台把集成工作转交给了用户。
我的选择会很明确:
内部实现可以尽量模块化,Agent Loop、Session、Persistence、Sandbox 都应该有替换空间。这样核心团队自己做实验、切换实现或者服务不同产品时,不需要不断侵入底层代码。
但面向普通第三方时,应该只开放平台已经想清楚的部分。Tool、模型 Provider、Command、Prompt、上下文来源和 UI Contribution,可以较早形成稳定接口;Agent Loop、Session、Sandbox、Persistence 仍然可以允许替换,但先放在 experimental 或 proposed 层,不急着承诺跨版本兼容。
真正需要完全控制 Runtime 的团队,可以进入更底层的 distribution 层,自己固定插件版本,并测试 install、boot、reload、dispose 和 resume。到了这一层,他做的已经不只是写一个插件,而是在维护自己的发行版,自然也应该承担对应的集成责任。
所以我不反对“一切皆插件”。我反对的是把内部可替换性直接等同于外部无限扩展。
内部越开放,平台越有空间继续演化;外部越克制,第三方越容易知道哪些东西真的可以依赖。这两件事并不矛盾。
一个好的平台不只是提供可能性,也要敢于做选择。什么应该允许扩展,什么应该保持统一,什么现在还没想清楚、只能作为实验接口存在——这些判断本身,就是平台应该提供的价值。