
摘要:从调试、事件契约、生命周期和 Session 协议出发,讨论 dsh 这套设计的适用边界,以及其中可以独立复用的设计方法。
本文基线
@deepseek-ai/dsh0.1.2-rc.1(npm latest),对应仓库标签dsh-v0.1.2-rc.1,commita66e470204。事件与服务统计来自仓库生成的docs/event-producer-consumer.md和docs/capability-seams.md,可通过pnpm run gen-doc-graphs重新生成核对。
这个拆解 dsh 系列从一个问题“Agent 运行时的扩展点应该开放到哪一层”开始。

七篇文章拆解下来,dsh 给出的答案越发清晰:配置中可以替换作为插件存在的模型适配器、工具注册表、会话日志和 Agent Loop;Cordis 负责插件挂载、卸载和注册项回收;主循环则通过事件开放 Turn、Step 和 Inbox 中的关键执行节点;模型上下文由会话日志中的事件按规则重建;能力 seam 则按 Definition / Provider / Consumer 拆成 29 个 seam。
虽然这些机制解决了扩展、替换和运行期组装的问题,但也给系统增加了新的复杂度。作为系列的最后一篇文章,本文将回到一个实际的问题:什么样的 Agent 运行时需要这套设计,其中哪些方法可以单独拿出来使用。
在讨论适用边界之前,先看这套设计需要承担哪些额外成本。
前面拆 Agent Loop 时讲过,dsh 会通过 agent/pre-step、agent/request、agent/turn-stopping 等事件,让插件介入主循环中的关键执行节点。
当多个插件都能介入同一个执行节点后,同一个行为可能会受到多层插件的共同影响。以 agent/pre-step 为例,按 0.1.2-rc.1 统计,共有 15 个包监听这个事件:
compaction-basic plan-mode
agent-instructions session-checkpoint-policy
session-reference tool-skill
time-context subagent-in-process-driver
tmux-context tool-subagent
tool-cordis hooks-claude-code
goal-round-driver hooks-codex
repeat-tool-reminder但是监听器数量多了,一旦有问题出现,排查范围就会从单个调用点扩展到整条监听器链路。
在 waterfall 模式下,这种排查还会多一层判断。因为监听器可以逐层处理,也可以在某一层提前结束后续传递。因此,如果 agent/pre-step 处理后的消息不符合预期,除了确认哪一层修改了结果,还要判断插件是否完成注册、触发条件是否满足,以及后续传递是否在前面的某一层结束。
第三篇「Profile 与 Bundle 如何装配运行时」中介绍过的 --dump-config,在这里可以用来确认运行时的插件组合。源码中的 import 关系能够看到模块之间的静态依赖,但当前加载了哪些插件、这些插件以什么顺序参与处理,还需要结合展开后的配置来判断。
除了调试,agent/pre-step 这类事件还要明确插件应该以什么方式参与执行。除了waterfall,当前 Harness 还会使用 emit、serial 和 parallel 这三种派发模式,不同模式会规定监听器的执行顺序,以及是否允许修改结果、结束后续处理或等待异步任务完成。这些规则一旦开放给插件,就会成为事件契约的一部分。
当事件开放给插件使用后,派发方式也会成为接口语义的一部分。dsh 会为 Harness 事件声明 @mode,再通过生成脚本核对声明与实际派发方式。
例如,一个原本只负责通知的事件,后续如果需要让插件修改输入,就要把派发模式从 emit 改成 waterfall。这时,相关监听器的函数签名和调用方式也要一起调整。
第二篇拆 Cordis 时讲过,插件注册的事件监听、服务和工具可以跟随插件上下文一起回收,文件 watcher、timer、外部连接等资源也可以通过 ctx.effect() 纳入同一套生命周期管理。
这套机制主要应对运行时中的动态变化。插件卸载、重载、Provider 替换或服务重新绑定之后,原有插件占用的资源也需要同步释放,否则旧监听、连接或状态可能继续影响后续 Turn。
因此,生命周期管理的复杂度主要集中在卸载和重组阶段。插件如果只在启动时完成装配,运行过程中保持稳定,这部分机制可以简化很多;当系统支持能力在运行期卸载、替换和重新绑定时,资源清理就需要成为持续维护的一部分。
第六篇「Session 的事件溯源与状态重建」讲过,dsh 的模型上下文由 Session Log 中的事件按规则生成。每增加一种需要进入模型上下文的内容,都需要补充对应的事件和投影规则。随着这类事件逐渐增加,Session Log 除了记录执行过程,也开始承担一部分协议职责。
这套协议还要额外处理格式兼容。如果只是新增一种普通事件,可以继续使用现有的 Session 事件格式;只有涉及底层结构变化时,才需要提升 SESSION_FORMAT_VERSION。
写入 Session Log 的事件还需要满足序列化要求,相关内容必须能够被保存,并在后续恢复时重新读取。
因此,如果模型上下文需要从日志重新生成,那么每增加一种要进入模型上下文的内容,都要同时处理对应事件、生成规则、序列化要求和格式兼容。这样一来,模型上下文的重建、压缩和恢复也可以围绕同一份 Session Log 进行。
第七篇「能力接缝:底层实现替换的影响边界」中拆过 dsh 的 29 个能力 seam,以及 Definition / Provider / Consumer 的分工。这样一来,底层 Provider 可以替换,上层 Consumer 不需要绑定到某个具体实现。
这种拆分带来了替换空间,也增加了理解一个功能时需要经过的层级。
按当前的基线统计,dsh 仓库内有 240 个公开发布的 @deepseek-ai 包。这个数字本身不能说明设计好坏,但能反映 dsh 的拆分粒度。当我们定位一个功能时,可能需要先找到对应的 seam,再确认它的 Definition、Consumer 和当前加载的 Provider,再结合事件与配置,才能还原它在运行时中的实际关系。
因此,capability-seams.md 和 event-producer-consumer.md 这类生成文档,也承担了运行时索引的作用。模块拆得越细,单看代码目录越难还原完整关系,也越需要额外的文档把能力、事件和实际运行组合串起来。
回头看前面的这几类成本,它们分别来自四个目标:让插件介入执行过程、支持运行期动态重组、让模型上下文可以从日志重建,以及让底层能力可以替换。
然而,这些需求并不会同时出现在所有 Agent 运行时中。
其中,插件契约和生命周期成本与两个条件关系最密切:扩展由谁提供,以及运行过程中是否需要动态重组。
把两个维度放在一起,可以得到四种运行时形态:

外部扩展 + 动态重组。当扩展来自独立作者,同时运行过程中还需要卸载、替换和重新绑定时,dsh / Cordis 这套机制能够覆盖较完整的需求。
事件契约用于开放关键执行节点,Definition / Provider / Consumer 划分能力替换边界,Cordis 上下文则管理插件生命周期中的资源创建与回收。
dsh 还提供了一个更特殊的场景:dsh-tool-cordis 会把 Cordis 能力交给 Agent,让模型可以检查当前运行时、生成插件代码,并将插件挂进现有插件树。
这些插件可能注册 Tool Schema、监听事件、占用 Service Key,也可能创建额外资源。只要运行时允许这类变化,插件退出时能否完整清理相关状态,就会影响后续 Turn。配置热更新、插件卸载、Provider 替换和服务重新绑定,也面临同一类生命周期问题。
外部扩展 + 静态加载。如果插件来自独立作者,但只在启动阶段完成加载,稳定的事件契约、能力 seam 和配置入口仍然重要,因为扩展作者不能依赖主程序的内部实现。
这类场景不需要承担完整的运行期生命周期管理,插件卸载、重新绑定和资源回收等机制都可以简化。系统重点维护插件 API、能力边界和配置兼容性即可。
因此,第三方插件生态和动态生命周期可以分开设计。支持外部扩展,并不代表运行时还需要持续发生插件重组。
内部扩展 + 动态重组。如果扩展都来自内部团队,Consumer 和 Provider 处在同一套研发体系中,接口变化可以随版本一起调整,对公开契约的要求也会低一些。
但只要运行时仍需要处理能力卸载、Provider 替换和重新绑定,生命周期管理就还有必要。这类场景可以保留 effect、资源回收和 Provider 生命周期,同时缩小需要长期保持稳定的扩展接口范围。
内部扩展 + 静态形态。如果系统由单一团队维护,运行形态固定,代码同步发布,运行期间也没有卸载和重新组装需求,完整插件运行时能解决的问题就会少很多。
这类场景下,依赖注入、显式 registry、函数组合或配置组装,基本可以覆盖能力替换需求。控制流也可以集中在 Agent Loop 内部,定位问题时沿固定路径排查即可。
第三篇讲过的 Profile 和 Bundle,也适合这类启动阶段的组装需求。web、headless、desktop 等运行形态可以通过配置选择不同的能力组合;如果运行开始后组件关系保持稳定,就不需要再引入完整的运行期卸载和生命周期机制。
当然,随着团队数量、产品形态、独立发布节奏和测试替换需求增加,稳定 seam 和配置组装的作用也会逐渐上升。
从这四种形态来看,dsh / Cordis 的完整设计主要适合这样一类场景:扩展来源较独立,同时运行时还需要持续发生能力变化。
如果需求只集中在启动阶段的能力组合,或者内部系统中的实现替换,其中不少机制都可以单独采用。
dsh 没有单独维护一份模型消息历史。执行过程会先写入追加式的 Session Log,再按照规则从日志中筛选并组织相关事件,生成模型所需的上下文。
这样一来,模型上下文和执行记录会共用同一份基础数据。一旦需要重新构建模型输入时,就可以从 Session Log 重新生成,不必再维护一套与执行记录同步变化的消息数组。
压缩也可以沿用这套方式。历史事件继续保留,压缩结果作为新的事件写入 Session,后续模型请求再基于新的记录生成上下文。
崩溃恢复同样可以利用 Turn、Step、Tool Call 等事件判断执行停在什么位置,再恢复对应的运行状态和模型上下文。这里恢复的是记录和状态,之前发生过的工具操作不会再次执行。
这套设计与插件机制没有绑定。即使 Agent Loop 集中在一个模块中,只要模型上下文能够从 Session Log 重建,就可以减少消息历史与执行记录分别维护带来的同步问题。两份状态分别维护时,漏写、顺序变化或压缩处理都可能造成内容不一致。
第七篇拆过 dsh 的能力 seam,以及 Definition / Provider / Consumer 三个角色。这套关系并不依赖 Cordis,也可以通过依赖注入容器、registry,甚至显式函数参数来实现。
其中有几条约束值得保留。
ctx.lsp 只暴露四个只读操作,没有把语言服务器的全部能力都放进接口。接口范围越克制,不同 Provider 需要对齐的行为就越少。
subagent-acp 暴露过一个问题:方法签名可以保持一致,但 Consumer 还可能依赖契约没有明确描述的时序行为。切换 Provider 后,这类隐含假设就可能变成兼容问题。
因此,一个能力 seam 除了定义方法和数据结构,还需要覆盖 Consumer 实际依赖的行为语义。
E2B 的替换结果也体现了这一点:底层 Provider 替换后,大部分 Consumer 仍能沿原有 Definition 工作;出现问题的地方,集中在契约没有覆盖的行为语义上。
0.1.2-rc.1 的事件目录共有 69 行,其中 65 行是带派发模式的具名 Harness 事件,另外 4 行属于内部事件分类。
65 个具名事件按派发模式分布如下:
emit 49 通知型
waterfall 14 可改写、可结束后续传递
serial 1 按序等待
parallel 1 并行等待相比事件总数,更值得关注的是:哪些位置允许插件改变后续执行。
14 个 waterfall 分布在主循环、模型调用、系统提示词、工具执行、文件修改、人工介入和遥测等位置:
主循环 agent/pre-step
agent/request
agent/request-error
模型 llm/stream
提示词 system-prompt/assemble
工具 tools/pre-execute
tools/execute
tools/post-execute
tools/ptc-dispatch-log
文件 fs/write-intent
fs/edit-intent
人工 approval/request
user-questions/request
遥测 session-telemetry/record这些位置有一个共同点:都处在可能影响后续执行结果的关键节点。
因此,设计 Agent 扩展接口时,可以先沿运行过程找出真正需要开放的决策位置,再决定插件在每个位置可以参与到什么程度。
有些事件只需要通知插件,有些允许修改输入或返回值,有些允许结束后续处理,还有一些负责等待异步任务完成。派发模式把这些差异固定在事件契约中。
agent/turn-stopping 和 session/flush 可以说明这种差异。agent/turn-stopping 使用 serial。监听器执行完成后,主循环会重新检查 Inbox,再根据队列状态决定当前 Turn 是否继续。session/flush 使用 parallel。这里需要等待相关监听器完成持久化工作,再结束这次 flush。
两个事件都涉及等待,但对应的运行语义不同。
所以,判断扩展能力时,事件数量只是表面信息。更重要的是哪些运行节点被开放,以及插件在这些位置可以做什么。
八篇文章发布期间,dsh 的基线从 0.1.0-rc.7 走到 0.1.2-rc.1,中间跨过 0.1.1 和 0.1.2 两轮版本。项目仍处于 developer preview,API、配置和部分实现还会继续变化。
因此,这个系列最终留下的,是一组关于 Agent 运行时扩展边界的设计取舍。
八篇拆下来,dsh 划出了几条清晰的边界:模型上下文从哪里生成,可替换能力通过什么契约连接,插件可以在哪些执行节点介入。
对应到具体设计,可以归纳成三点:模型上下文从可重建的日志生成;可替换能力明确区分 Definition、Provider 和 Consumer;扩展接口围绕运行时的决策位置设计,并明确插件在每个位置可以参与到什么程度。
至于 Cordis、动态重组和完整的插件生命周期要采用到什么程度,则取决于扩展来源、运行形态,以及系统实际需要开放的范围。
以下路径以 dsh-v0.1.2-rc.1 为基线:
docs/event-producer-consumer.md (生成文件,pnpm run gen-doc-graphs)
docs/capability-seams.md (同上)
docs/cordis-primer.md
docs/architecture.md
packages/core/session/src/types.ts (SESSION_FORMAT_VERSION 的 bump 规则)
packages/core/agent-loop
packages/core/agent
packages/core/tools
packages/extensions/tool-cordis
packages/e2b/subprocess-e2b/README.md (Known Limitations)系列全部八篇的基线版本、commit 和验证命令都写在各自文首,相关数据可以通过 npm view @deepseek-ai/dsh 和仓库里的生成脚本复现。
原创声明:本文系作者授权腾讯云开发者社区发表,未经许可,不得转载。
如有侵权,请联系 cloudcommunity@tencent.com 删除。