146 篇 RFC 译文按 v4 基线(#348)重出:v4 模板+术语表、金标 few-shot、三段协议、切换行后处理;全量机械核对零异常(一处 task id 术语违规已修)。三篇超长 RFC(code-mode 已入,web-seam/ agent-scope/sandbox/cds-core 仍在长文档通道产出)随后补。
7.4 KiB
RFC:将示例应用提取为独立包
English | 中文
Status: implemented
问题
示例目录本应是精简的——只包含演示的可变接线,而非演示的基础设施。在此次变更之前,它是臃肿的。每个示例都携带一份手写的 start.ts 启动引导、一段基础设施前导(timer,以及 stdio 演示所需的 logger + hmr(热模块替换))、三个共享 YAML 片段的嵌套引用(base.yml / base-core.yml / acp-agent/acp-tail.yml),还有各示例自身的 agent-loop/persistence/system-prompt 配置。真正的应用——每个 agent(智能体)都需要的服务主干——散落在叶子配置和那些 include 中。
叶子配置还拥有一个耦合的前门。ACP(Agent Client Protocol)要求 stdout 纯净,并通过 session/new 创建 agent;stdio 则需要一个控制台 logger 和一个预创建的 main。防止错误组合的唯一屏障是文档中的文字警告,而三个 start.ts 文件重复着 Loader 引导和生命周期代码。
决策
每个示例现在主要是对一个应用包(package)的调用,沿着既有的接口 / 实现 / 消费方 seam 拆分接线:应用包拥有组合,叶子 cordis.yml 只拥有可替换的选择(哪个 LLM(大语言模型)适配器、哪个 bash 执行器、模型、提示词、持久化根目录)。
@deepseek-ai/dsh-agent-spine-demo(packages/examples/agent-spine-demo)组合了不含 provider、不含执行器、不含 UI 的主干,并转发 agent loop(智能体循环)的 agent 列表配置。它对具体 loop 的依赖是有意为之,因为该包组合的是主干而非扩展主干;替换 loop 意味着提供另一个 bundle。@deepseek-ai/dsh-stdio-demo(packages/examples/stdio-demo)和@deepseek-ai/dsh-acp-demo(packages/examples/acp-demo)各自内置了前门。Stdio 包含ui-stdio、控制台 logger 和main;ACP 包含 bridge 和 JSONL 持久化,但不含 stdout logger 或预创建的 agent。叶子可以添加插件,但安全的组合现在是默认产物。start.ts已移除。 每个应用包暴露一个bin(dsh-stdio-demo/dsh-acp-demo);demo:*脚本调用它(例如dsh-stdio-demo ./cordis.yml)。Loader 引导尾部、.env加载和快速失败守卫位于共享的@deepseek-ai/dsh-app-boot包(在逐文件覆盖率门禁下有单元测试——见共享应用 bin 的启动胶水);每个 bin 是一个精简的自执行组合,基于这些辅助函数加上其应用特有的生命周期逻辑(ACP bin:快照模式选择与 stdin-dispose)。bin.ts文件本身仍被排除在覆盖率之外(自执行 CLI(命令行界面)入口,与旧的start.ts性质相同),由 keyless 的 Loader 路径测试驱动。- 每个叶子
cordis.yml精简为后端 + 配置:LLM 适配器(带 apiKey/models 的llm-deepseek,或llm-replay)、bash 执行器(bash-local)、stdio 演示的hmr(见下方修正),以及一个承载应用配置的 app 条目(模型、系统提示词、持久化根目录——以应用包自身的Config形式暴露,由它将各值路由到应用接线的目标位置:stdio 路由到预创建的 agent,acp 路由到 bridge 插件)。 - echo-agent 折叠到
dsh-stdio-demo上,将 LLM 后端替换为本地的mock-llm,并在叶子层添加本地的echo-tool(加上bash-local,由主干的tool-bash注入)——这是「替换后端、保留应用」的干净示范。mock-llm.ts/echo-tool.ts作为示例本地的教学插件保留。 base.yml、base-core.yml和acp-agent/acp-tail.yml已退役——它们共享的主干现在位于dsh-agent-spine-demo中。
bash-local 和 LLM 适配器仍然是叶子选择:bundle 提供 tool-bash(消费方 schema),叶子选择执行器实现,因此沙箱执行器或回放适配器无需触碰应用即可替换。
实现修正:hmr 保留为叶子条目
提案最初将 hmr 列入 stdio 应用内置的前门集群。对照代码验证后发现,将 hmr 内置到 dsh-stdio-demo 包中会在两个方面与 Cordis 冲突,因此改为作为叶子 cordis.yml 条目交付:
@cordisjs/plugin-hmr是一个仅限 Loader、仅限子进程的开发插件——其构造函数在没有node --expose-internals和活跃的loader服务时会抛出异常,因此只能在真实的demo:*/bin 子进程中运行,不能在进程内的单元/覆盖率测试层运行。- 进程内测试层(vitest)甚至无法导入 vendor 的
hmr模块(其 class-decorator@Inject形式在 Vite 的 transform 下会失败),因此一个apply静态导入了它的包永远无法满足其主函数的逐文件 100% 覆盖率门禁。
关键在于,hmr 不是像控制台 logger 那样的 stdout 纯净隐患:ACP 配置中误加 hmr 不会破坏 JSON-RPC 帧,因此将它留在叶子层不会损失耦合论证所关注的安全性。logger(真正的耦合点)保持内置:stdio 应用包含它,ACP 应用省略它。
曾考虑的替代方案
为什么不继续用共享 YAML include 来管理接线?
旧的 base*.yml/acp-tail.yml include 已经去重了配置,但 YAML include 无法封装前门耦合——它只能在注释中描述,并信任每个叶子遵守。它也无法拥有 bin,因此启动胶水一直在三个 start.ts 文件中重复。包将「ACP 应用绝不向 stdout 输出日志」从文字警告变成了产物的属性:叶子中不存在可以写错的 logger 条目。
验证
- 示例目录只包含配置、README 和测试:
start.ts、基础设施前导和共享 YAML include 已移除。 demo:echo、demo:repl和demo:acp调用应用包的 bin。- 每个新包都有 README 和逐文件 100% 覆盖率;每个应用包还有一个 keyless 的真实 Loader 路径 bin 冒烟测试,用于捕获事后分析 0001 中描述的导出形状故障。
- ACP 回放 transcript(文本记录)保持不变,因为插件集合和加载顺序未改变。
后果
- 裸插件树的教学性。 echo-agent 内联的
cordis.yml曾一次展示所有插件;主干现在隐藏在 bundle 之后,查看完整树意味着打开dsh-agent-spine-demo。应用包的 README 承担了这份教学职责。 - 多了一层间接。「这个演示加载了什么?」从扫描单个 YAML 变成了阅读一个包。
相关
- 取代 Make the shared example base providerless:一旦主干移入
dsh-agent-spine-demo且base*.yml文件被删除,将base.yml重命名为无 provider 核心便不再有意义。 - 基于 capability-seams 的接口/实现/消费方拆分——后端和展示层保持为叶子选择;主干是共享 bundle。
- 与 Reorganize packages into a modular hierarchy 互补:新的 app/core 包按该层级结构归入既有分组(
core放可复用的主干 bundle,ui放应用特有的前门)。