DeepSeek Harness 插件微内核架构:可组合、可恢复的 Agent 运行系统

DeepSeek Harness 插件微内核架构:可组合、可恢复的 Agent 运行系统

从六层系统架构与 M0–M4 微内核编程栈出发,解析 DeepSeek Harness 如何通过 Cordis 插件、Capability Seam、Session Event Log 与完整 Bash 插件链实现可组合、可恢复的 Agent 运行系统。

DeepSeek Harness 插件微内核架构:可组合、可恢复的 Agent 运行系统

本文以系统六层为主轴:Surfaces → Composition → Microkernel → Agent Runtime → Capabilities → Data & Infra。L3 再用一张微内核编程栈纵剖图解释插件代码怎样贯穿装配、运行与持久化;两套视角不是两套 Runtime。

L0:先建立唯一的中心命题

DeepSeek Harness(下文简称 DSH)最准确的定位是:一个用插件组合产品、用事件日志保存事实、用能力接缝连接外部世界的 Agent 运行系统。

它的核心模型—工具循环与 Claude Code、Codex、OpenCode 并没有代际差距:读取上下文、调用模型、执行工具、把结果送回模型,直到 Turn 收敛。DSH 的辨识度不在新的推理算法,而在于把产品装配、服务依赖、插件生命周期、会话事实、执行后端、Web 客户端和远程协议统一进一套可组合、可恢复的工程体系。

可以先记住一个等式:

产品 = 表面入口 × 装配配方 × 插件微内核 × 持久化执行内核 × 可替换能力 × 数据与基础设施

这里的“可组合”不是指把几个工具放进数组,而是启动时能够用插件图决定整个产品有什么服务、Provider、策略、UI 和协议;“可恢复”也不只是重新载入聊天文本,而是从持久事件重建模型当时看见的内容、领域状态和未完成工作的真实不确定性。

阅读全文时可以持续追问三个问题:

  1. 这个行为由哪个插件贡献,它依赖哪个 Service、Registry、Event 或 UI Slot?
  2. 这项内容是否会被模型看到;如果会,它怎样从 Session Log 重建?
  3. 它创建了什么进程、Worker、Watcher、Socket 或外部副作用;owner 是谁,dispose 怎样达到 quiescence?

证据范围

本报告按约定排除了单元测试、测试支持、fixtures、snapshots、benchmarks 与开发配置,依据生产源码、运行时 Cordis 配置、英文 README、当前有效 Agent Notes 和架构文档整理。

本文引用的架构插图均为生成式白板风格 PNG,不使用 Mermaid 或 SVG。

文中“代码事实”来自仓库当前源码和 implemented Notes;“据此可推断”“架构解读”属于综合判断;与外部 Agent Runtime 的比较属于比较性分析,不是仓库自身声明。项目仍处于 pre-release,包名和持久格式没有首个正式版本后的兼容承诺。

六层总图:后续所有章节的索引

DeepSeek Harness 六层架构总览

这张图给出机制的主要归属,不是严格的包依赖 DAG,也不表示组件不能跨层协作。箭头表达主依赖方向,不表达唯一所有权。

层级 名称 核心问题 代表机制
L1 Surfaces 用户从哪里进入同一个运行系统? CLI、Web、Headless、SDK、ACP
L2 Composition 这次进程与这个 Agent 装什么? Profile、Bundle、Patch、Preset
L3 Microkernel 插件何时激活、依赖谁、归谁持有、如何卸载? Cordis Context、Loader、Service、Event、Effect、Scope
L4 Agent Runtime 一次智能行为如何推进,并保持可恢复? Agent、Session、Turn、Step、LLM Runtime、Tool Runtime
L5 Capabilities Agent 如何以可替换方式接触外部世界? Definition、Provider、Consumer、FS、Shell、Web、Subagent、Workflow
L6 Data & Infra 系统如何保存事实、派生读面、恢复和约束? Event Log、Persistence、Projection、Protocol、Sandbox、Provider infra

六层关系

Loop 只位于 L4。L1 的多产品入口、L2 的产品装配、L3 的插件生命周期、L5 的执行世界替换以及 L6 的事件事实与恢复都在 Loop 之外,却共享作用域、生命周期和可重建规则。因此 DSH 不能只用模型—工具循环解释。


L1 · Surfaces:同一内核怎样呈现为不同产品

第一层不决定 Agent 如何思考,而是定义人、脚本或外部客户端如何进入系统,以及看到什么语义。

1.1 五类入口不是五套 Runtime

Surface 适用场景 暴露的语义 有意不暴露的内容
CLI / Headless 本地交互、一次性任务、自动化 同一 Agent/Session/Tool 语义,headless 可绕过 Web/API 不要求浏览器和 Host carrier
Web 长会话、工作区、交互式审批与丰富工具卡片 Host + Browser 两棵插件树、Session Runtime、typed UI slots 不让 Browser 自行决定插件 roster
TypeScript / Python SDK Harness 原生集成 JSON-RPC、durable events、whole-agent status 不承诺每个 Prompt 独立 Future 结果
ACP 通用自动化互操作 fresh session、committed assistant text reasoning、完整工具轨迹、导航与恢复体验
Examples / JSON-RPC leaves 演示和嵌入 基于同一 Cordis 机制的最小 composition 不复制一套产品内核

可以把它类比成同一台发动机装进命令行工具、网页控制台和自动化接口:入口、仪表盘和传输方式不同,但 Agent、Session、Tool 与能力定义不是重复实现。

1.2 Web 是两棵 Cordis 插件树

Web Host 与 Browser 双运行时

Node Host 与 Browser 各自拥有 Cordis Context、Loader 和插件生命周期。Host 负责 Node 能力、HTTP/WS carrier、远程 API 和浏览器插件 roster;Browser 负责 React-free runtime objects、UI slots、conversation renderer 和浏览器侧 remote methods。

Host Loader 扫描当前插件图中声明的 dsh.client bundle,解析其 client entry,把 bundle hash 成启动图并通过 /plugins 提供。Browser shell 不硬编码产品插件清单,也不根据环境自行决定“该加载哪些 UI”。因此 Host 上安装、卸载或替换插件时,Browser 收到的是与 Host 当前 composition 对应的 roster。

这个设计的作用是消除双份配置:若 Host 与 Browser 分别维护插件表,很容易出现 Host 已卸载服务、Browser 仍显示按钮,或 Browser 已加载 renderer、Host 没有对应远程方法。

1.3 两阶段原子启动

Browser shell 先解析 Host 提供的 boot graph,并行预取 immediately factories;随后创建浏览器 Cordis/Loader、替换 importer、创建 entries 与 app shell,等待 Loader settle 且全部 Fiber ACTIVE,最后才把 loading/error 原子切换为完整 UI。

原子启动优先保证“界面与能力图一致”,代价也很明确:任一入口失败会让整个 UI 不可用,没有 progressive rendering;HMR 是插件粒度,旧 Fiber teardown 后再刷新,没有事务 rollback,插件局部状态可能丢失;lazy CJS factory 明确拒绝 require cycle。

1.4 Transport:上行、下行与 connected 定义

Web carrier 不是一条全能 WebSocket:普通 unary 调用用 HTTP upstream;Host 到 Browser 的事件分为 events.muxevents.host 两条 downlink-only WebSocket。只有两条流都打开并且 host.describe 成功,客户端才进入 connected 状态。

连接层按 generation 建 baseline 和 cleanup:mux 事件进入 Sessions,host 事件进入 Sessions/Workspaces,remote-event 进入 ctx.remote.$dispatch。这种分工让 unary RPC、Session 高流量事件和 Host 管理事件可以有不同的重连与投影语义。

1.5 React-free runtime objects 与 typed UI slots

Browser Runtime 中的 Session、Workspace、Projection Store 使用 getSnapshot()/subscribe(),不依赖 React;React 只在 web-react 绑定层通过 useSyncExternalStore 订阅。这使 runtime objects 可以被非 React 代码、远程事件和测试工具共同使用。

UI 也不是一个中央 switch(node.type):插件通过 typed slot registry 声明 layout seat、conversation node、tool card、trajectory、settings、theme、locale、workspace、preset、subagent 等贡献。一次注册可以同时提供 component、child slots、store seat 和业务注入;disposer 会递归撤销对应贡献。

以 Conversation 为例,插件通过 declaration merging 扩展 ChatNodeDataMap,向 conversation event registry 注册节点定义,再向 conversation.chat.node keyed slot 注册 renderer。新增节点类型不需要修改中央 renderer。

“Slot 是否预设”需要分成两个问题。Slot protocol 需要由拥有该界面区域的插件声明,包括稳定名称、Props、single | list | keyed | chain 语义、key 与排序规则;具体 contribution 则在运行时随插件装卸。list 汇集多个贡献,keyed 按节点或业务 key 选择 renderer,chain 按 priority 调用 selector 并取首个 non-null,single 对外暴露一个逻辑 winner、允许较低 priority 的 shadow candidate,同 priority 冲突。

UI 扩展层级 自由度 必须满足的条件
向现有 Slot 添加组件 使用已声明的名称、Props 与 contribution 语义
在自己的组件中声明 child slots 较高 父组件实际渲染 child slot,并拥有递归 disposer
改变已有 Slot 类型或任意 DOM 注入 不支持 需要修改或替换拥有该区域的 UI 插件
重组产品级 UI 高但属于 Composition Profile 更换 layout/conversation/settings 等插件;Browser boot shell 仍保留 roster、加载、settle 与 error 职责

因此新 Slot 仅被声明还不会出现在界面上,必须有组件实际渲染它。Typed Slot 提供的是结构化组合、类型和生命周期隔离,不是 DOM 注入或浏览器安全沙箱;错误或恶意 Client 插件仍可能抛错、阻塞或滥用它在同一页面已有的权限。

1.6 Typert 与显式 BFF:服务存在不等于允许上网

Typert 使用 TypeScript compiler 同时分析 Host/Client face,生成 type graph、descriptor、Zod codec、Host ./typert contribution、Client ./remote artifact 和 catalog。它把业务签名、wire codec、客户端类型与协议描述从同一源派生,减少四份手写定义漂移。

Host Gateway 每次调用解析 descriptor 与当前 Cordis Service,精确验证 named args、解析 lookup/context identity,并在方法声明支持取消时,把 carrier 外的 AbortSignal 作为最后一个参数注入。Client 挂载的是普通的 remote.namespace.method() 对象方法,不用 Proxy;卸载会 abort inflight 并移除方法与空 namespace。

Typert 并不会自动把所有 Service 暴露到网络。api/remotes 是显式 BFF,只有被选中的 Host face 和固定 allowlist event 才能穿过边界。这同时回答了一个常见疑问:“既然一切都是插件,某个插件注册了 Service,Browser 会不会自动拿到?”不会。Service 注册、remote descriptor 与 BFF 暴露是三件不同的事。

当前 Typert 主要是 unary;stream 仍走 Session carrier,source fallback 只能处理较简单签名,legacy API proxy 仍然是 Typert interceptor first + fallback,全面迁移尚未完成。Remote events 当前按 allowlist 原样转发,没有 redaction、projection 或断线 replay。

1.7 Web 信任边界

Host 严格校验 Host、Origin 与 Fetch-Metadata,特权管理面还限制 loopback。设置、凭据、llm.discoverModels、native directory/open,以及 preset 的 read/copy/openDocument/remove 属于更窄的管理面;preset list/select 和 session.create 时选择 preset 则刻意允许可信 LAN 客户端。

但这些检查只是 reachability/trust fence,不是用户认证、授权系统或 TLS。部署到不可信 LAN 时,不能把它当成完整 Web 安全模型。

L2 · Composition:产品不是复制出来的,而是装配出来的

启动、Profile 与插件组合

第二层回答两个不同尺度的问题:这个进程是什么产品?这个 Agent 看见哪些能力? 前者由 Profile/Bundle/Patch 决定,后者主要由 Preset 与 Agent Scope 决定。

2.1 Bundle、Profile、Patch、Composition Leaf

概念 决定什么 例子
Bundle 可安装、可复用的插件 patch 配方 base、web-app、headless
Profile 一次实际部署的目录与 bundle 选择 某个用户的 Web 或 headless 配置
Patch 对 Cordis entry 的明确替换 用户覆盖 provider、策略、UI 或参数
Composition leaf 直接从 cordis.yml 组装的运行入口 ACP、JSON-RPC、部分 examples
Preset 单个 Agent 的模型可见 composition tools、prompt sections、provider/model、persona

Bundle 更像发行方准备的菜单,Profile 是某家门店的后厨与营业配置,Preset 是某一桌客人的套餐。这个比喻的重点是尺度不同:Profile 改变整个进程,Preset 改变一个 Agent 的能力世界。

2.2 CLI Profile 的精确叠层顺序

CLI 以故意为空的 cordis.yml 为根,然后依次应用:

  1. Profile 声明的有序 Bundle patches;
  2. Profile 目录中的用户 patch;
  3. $DSH_HOME patch;
  4. 命令行 --patch
  5. CLI 注入的 shipped preset-root overlay。

DSH_TELEMETRY_DISABLED 的值非空,并且合成树中存在 telemetry row,启动器还会追加最终 disable patch。Base 本身默认 DSH_TELEMETRY_MODE || 'DISABLED',因此不能把它误写成“所有启动最后都无条件硬关闭 telemetry”。

每次启动重新创建空根,是为了避免 Cordis Loader writeback 把合成结果烘焙回源配置;clone patch 则避免 Loader 原地修改用户提供的数据。最终插件树因此可复现、可 dump,也更容易审计“某个能力从哪一层进入”。

2.3 为什么 Patch 不是 deep merge

Patch 按 Cordis entry id 替换整项配置,而不是递归深合并。深合并看似省字,但旧字段可能残留,新 Provider 继承到前一个 Provider 的无关参数,最终行为难以解释。整项替换要求配置更明确,却把“最终运行图是什么”放在“少写几行 YAML”之前。

这也解释了插件 id 的一个作用:entry id 是装配地址。Patch 用它找到树中的某个实例,Loader 用它跟踪 Fiber、HMR 与状态;它不承担业务类型判断。

2.4 Loader activation audit

Boot 创建 Cordis root/Loader,安装 include/group 与 !!js resolver,启动后审计 failed、pending、missing service、fiberless import 等状态。若配置在装配阶段就能确认缺依赖或冲突,系统倾向 fail loud,而不是静默跳过插件后留下半个产品。

产品表面因此是 composition 的结果:Web/headless 用 Profile Bundle 配方装配;ACP、JSON-RPC 与 examples 也可以是基于同一 Cordis 插件机制的独立 composition leaves。它们不是各自维护一套 Agent 内核。

2.5 Preset:每个 Agent 的 composition

Agent Preset 从 shipped/system 与 writable/user roots 发现。每个 Preset 目录包含 agent.cordis.yml,可以另带 preset.yml metadata。每个 generation 挂在独立的 standing preset scope/layer;Agent 创建或恢复时再通过父链加入对应 composition。

作用域读取链是 agent → preset → global:Agent-local contribution 可以覆盖或补充 Preset,Preset 又可以使用 global Service。兄弟 Agent 不会因为属于同一 Host 就互相看见私有工具或 prompt section。

一个空 Session 可以重组 Preset,非空 Session 拒绝,以免已有模型可见历史与新能力表面产生无法解释的混合。Mount 还会检查没有 agent scope、unresolved/pending/failed rows 与 Host Service 泄漏等错误。

Web preset authoring 使用 copy-only + 外部文件编辑,管理面提供 read/copy/openDocument/remove。Codex/Claude product subagent provider 由 Profile 显式安装,不属于生产 base 默认装配。

2.6 Parent/Child 不自动等于权限继承

父子 Agent 关系本身不产生隐式 Scope 或权限继承。当前 in-process child 会显式加入 parent 当前 standing Preset generation,从而继承其 tools 与 prompt sections,再叠加 child-local persona/toolFilter;sandbox 只复制 parent 的显式 override,approval 固定为 never

这种显式组合比“孩子天然继承父亲所有权限”更可审计,但也有一个窄限制:in-process cold resume 可能加入 live parent 的当前 standing composition,而 child header 记录的是历史 preset;两者在 preset 编辑窗口中可能不同。

L3 · Microkernel:插件、Service 与生命周期怎样协作

插件微内核编程栈纵剖图

系统六层回答各子系统位于哪里;上图的 M0–M4 回答插件代码如何穿过这些层。M 前缀专指微内核编程栈,避免与系统 L1–L6 混淆:

编程栈 含义 对应系统层
M0 Cordis Context、Scope、Loader/Fiber、Effect/Disposer L3 Microkernel
M1 Service、Registry、Typed Event、UI Slot L3 Microkernel
M2 Definition、Provider、Consumer、Policy/UI/Protocol 连接机制在 L3;Capability/Policy 主要落 L5,UI 落 L1,Protocol 落 L1/L6
M3 Agent Loop、LLM/Tool Runtime、Session Log L4 Agent Runtime + L6 Data
M4 Profile、Bundle/Patch、Preset、Host/Browser L1 Surfaces + L2 Composition

Cordis Context 不是普通全局对象:它同时提供服务容器、作用域视图、事件总线和生命周期树。Loader 根据配置创建 Fiber,在依赖满足时激活插件;插件初始化代码再显式把贡献注册到正确位置。

3.1 插件不是一种业务类型

Scoped ctx 的五个核心动作

“插件”只统一四件事:加载、依赖、作用域、生命周期。它不意味着所有插件都实现相同业务接口,也不存在一个全局 PluginKind = 'ui' | 'llm' | 'fs' 让消费端分支判断。

一次典型激活过程是:

  1. 配置树声明插件实例;
  2. Loader 创建对应 Fiber;
  3. inject 检查依赖 Service 是否可用;
  4. 条件满足后调用插件初始化函数或 Service 构造器;
  5. 插件显式向某个 Service、Registry、Event 或 UI Slot 注册贡献;
  6. 注册返回 disposer,并归属于当前 effect/scope;
  7. HMR、配置撤销或 owner dispose 时,Cordis 撤销贡献并等待相关资源收敛。

最准确的一句话是:

Loader 决定加载谁,依赖注入决定何时初始化,插件代码决定注册到哪里,effect 决定卸载时撤销什么。

所以“插件加载后会自动把自己分配到正确区域”只对了一半。框架不会根据包名或源码猜测它是 UI 还是 LLM Adapter;插件内部主动调用 ctx.tools.register()、某个 provider registry、ctx.on() 或 slot registration API。

ctx 理解插件的五个动作

动作 插件做什么 关键限制
Provide 向当前 Cordis scope 提供 Context Service 不是写入进程全局;Host、Browser、Preset、Agent 有不同 Context 树或读取层
Consume 通过 inject 取得并调用稳定 Service 消费 Service contract,不查找具体插件实例或 entry id
Contribute 通过 ctx.tools.register()、Provider Registry、Slot Registry 等注册贡献 调用的是经 ctx 暴露的 Registry API,不是直接调用另一个插件对象
Communicate 通过 typed ctx.on() / ctx.emit() 观察、协调或短路过程 Cordis runtime event 默认是进程内瞬时事件,不自动进入 Session Log
Own 用 effect、disposer、AbortSignal 和 holder 绑定资源 卸载必须撤销 contribution,并等待自己拥有的异步工作收敛

这五个动作比“Service 插件之上再挂一组 Hook”更准确。Service、Registry、Event 与 Slot 是并列的连接机制;Provider、Consumer、Policy、UI contribution 根据职责选择其中一种或多种。Context 也不是可任意写属性的全局对象,新增 Service 需要稳定 token、类型声明、作用域和 owner。

3.2 四种标识不能混用

标识 回答的问题 消费端是否通常使用
Package / plugin name Loader 要解析哪个模块? 只在装配层
Loader entry id 配置树中的哪个插件实例?Patch/HMR 撤销谁? 不用于业务类型判断
Service key / token Consumer 依赖哪个稳定服务? 是,例如 ctx.shellctx.fs
Contribution id Registry 中选择哪个具名能力? 是,例如工具名、LLM provider/model、slot key

Tool Runtime 不会写 if (plugin.id === 'tool-bash'),而是从 Tool Registry 取得名为 bash 的 Tool Definition;tool-bash 也不寻找 bash-local 插件实例,而是调用 ctx.shell。因此 entry id 固定与否不会决定类型安全。

3.3 四种连接机制与消费隔离

Service、Registry、Typed Event 与 UI Slot 是并列机制,不是一棵“Service 上继续挂 Hook”的继承树:

机制 适合解决的问题 例子
Service 一个稳定调用接口 ctx.fs.readText()ctx.shell.run()
Registry 多个具名贡献的注册、选择与撤销 Tools、LLM Provider、Subagent Provider
Typed Event 观察、顺序协作或策略短路 agent/pre-steptools/post-execute
UI Slot Browser 中的类型化组件贡献 Conversation node renderer

同一插件可以消费 Service、向 Registry 注册贡献、监听 Event,并为 Slot 提供组件。Registry 必须明确作用域、key、priority 或冲突规则;例如 Tools 在同一 layer 重名会直接失败。Waterfall listener 调用 next() 才委托下一插件,直接返回就是显式短路。

隔离来自多个相互补充的机制:

  • Context 中不同的 Service key;
  • 不同的 typed Registry 与 contribution schema;
  • TypeScript declaration merging 与静态签名;
  • Cordis inject 激活条件和 duplicate service 检查;
  • Agent/Preset/global 的 Scope 读取链;
  • wire、file、worker、process 等非信任边界上的专用 parser/codec;
  • 显式 BFF 与事件 allowlist。

同进程 typed boundary 信任 TypeScript,不为静态接口要求的值重复做 hostile-input 校验;模型 JSON、配置、队列、文件、Worker、子进程和网络则在各自真实边界验证。Durable event 当前依靠 JSON 可序列化、event envelope、Surface 校验、generated known vocabulary 与 owner-specific decoder 共同防守;仓库尚无覆盖全部 SessionEvent.data 的通用 runtime schema registry。

3.4 Service 扩展与 Capability 角色

可替换能力通常形成一个 Capability Seam:

角色 回答的问题 Shell 示例
Service Definition 能力是什么,调用双方必须遵守什么 ShellExecutor
Provider 能力如何实现 SandboxBashExecutor
Consumer 产品或模型怎样使用能力 tool-bash

Provider 与 Consumer 都依赖 Definition;Consumer 不应 import bash-localfs-e2b 等具体 Provider,也不通过 entry id 找实现。Policy、UI 与 Protocol Adapter 是正交角色:Policy 约束调用,UI 投影结果,Protocol Adapter 转换进程或网络协议。

Service 可以通过三种方式扩展:

  1. 替换实现:新 Provider 实现同一 Definition,例如 bash-sandbox 提供 ctx.shell;同一 Scope layer 不应悄悄保留竞争实现。
  2. 注册贡献:Service 暴露 register(),插件追加具名 Tool、Provider、Projection 或 Command,例如 ctx.tools.register(bashTool)
  3. 增加正交接口:职责、生命周期或调用语义不同时,新增 Service、Typed Event 或 Slot,而不是 monkey-patch 现有实例。

三种方式都需要稳定 token 与方法语义、明确 Scope、effect-scoped disposer、资源 owner 与 cancellation、冲突规则、结构化错误,以及在配置、模型 JSON、file/worker/process/wire 等真实边界上的相应校验。Durable contribution 还要遵守现有 envelope、known vocabulary 与 owner decoder;若贡献会被模型看到,必须能由 Session Log 重建。

3.5 Event extension points

Cordis 事件模式不是同义的回调数组:

模式 语义 典型用途
waterfall 按顺序委托;listener 必须调用 next() 才继续 pre-step、策略链、answerer
serial 依次执行所有 listener 需要稳定顺序的生命周期动作
parallel 并发通知并等待 相互独立的扩展工作
emit 观察性通知 UI、telemetry、live event

DSH 没有另造一套 Koa middleware 或通用状态机。新策略优先放到已记录的 Service/Event 接缝,只有改变 Agent Loop 的不变量时才修改 Loop。

ctx.emit()ctx.on() 连接的是当前进程和作用域中的插件生命周期,不等于 Session durable event。需要恢复、重放或进入模型请求的事实必须通过 session.append() 或对应领域 API 写入 Session Log;插件可以在 runtime event 后追加 durable event,但系统不会把所有 Cordis event 自动持久化。

3.6 Agent Scope 与原子 publication

每个 Agent 是 flat registration layer,不是复制一整棵 Service graph。普通 ctx 决定注册归属,operation subject 决定读取哪个作用域视图。创建过程遵循 private world → setup → commit → publication last:在工具、prompt、provider 尚未完整装好前,其他代码不能看到半个 Agent。

销毁过程是 abort/cancel → 等待 whenIdle() → dispose Agent scope → 从 Agent registry 与 Session registry 分离;持久化最终 drain 由 Session/Persistence 生命周期负责,而不是 AgentLoop 销毁函数直接调用 sessions.flush()。原则是“disposal 到 quiescence”,不只是发出 cancel 后不再等待。

同进程取消仍是合作式:不理会 signal 的插件可能无限延迟 teardown。必须硬终止的工作应放到 Worker 或进程边界,并由 owner 持有终止能力。

3.7 Skill、MCP 与 UI Slot 的定位

Skill、MCP 与 UI Slot 的架构位置

这三类扩展共享 Cordis 生命周期,却连接到不同位置:

  • Skill 是模型操作说明资源。Skill 插件向分层 Registry 注册带 list/load 的 provider/source,正文通常在命中后按需加载;真正副作用仍由已有 Tool 执行。加载 Skill 不等于安装或执行代码。
  • MCP 是外部 Tool Adapter。每个 MCP Server 对应一个 Client 插件实例;工具名通常呈现为 mcp__<server>__<tool>,非法字符或超长名称会使用规范化、截断与确定性 hash。贡献进入原生 Tool Runtime,继续经过校验、策略、结果规范化和 durable commit。完整 canonical content 只在执行期保真;Durable/Model Surface 保存文本投影,非文本块会退化为 placeholder。当前 DSH mcp-client 只桥接 Tools;这不是 MCP 协议本身的能力上限。
  • UI Slot 是 Browser UI 的类型化贡献协议。Owner 预先声明名称、Props、Scope 与 single | list | keyed | chain 语义,其他插件动态贡献组件;没有 Owner 暴露的 seat,就不能任意注入 DOM。

例如“安全查询数据库”Skill 可以要求先读取 Schema、查询带 LIMIT、写入前请求批准;MCP 提供真正的数据库 Tool;通用 Tool policy/Approval 可以约束调用,但 DSH 的文件 Sandbox 不会自动围住远端 MCP 副作用;UI Slot 只负责展示。这些角色不会因为都叫插件而被错误消费。

图中的 “MCP tools only” 描述当前 DSH mcp-client 的实现范围,不是 MCP 协议永久只能承载 Tools。

完整的生产 Bash 插件链放在六层说明之后,集中展示 Definition、Provider、Consumer、Policy、Jobs、Tool Runtime、Session commit 与 teardown,避免在本层重复源码细节。

3.8 核心 Service 域不是冻结枚举

Context Service 可由插件继续扩展,不存在永远固定的中央枚举;当前主要服务域可以按职责归纳:

服务域 代表 Service / Registry 作用
Agent spine Agent、AgentLoop、Session、Tools、SystemPrompt Turn/Step、模型与工具、Prompt、会话身份
Model LLM Runtime、provider/model registry 精确路由、stream、错误标准化
Execution Shell、Subprocess、Terminal、FS、LSP、CodeRuntime 命令、进程树、PTY、文件、语言服务、Worker 执行
External integrations Web、MCP、Skill search/fetch、可读写外部工具、运行时技能来源
Orchestration Subagent、Workflow、Jobs、Schedule 子 Agent、并行脚本、后台句柄、定时 follow-up
Composition Preset、Settings、Credentials、Workspace Agent composition、配置、secret reference、工作区
Interaction Approval、Question、Commands、Permission、Feedback 用户决策、命令、权限模式、sidecar 反馈
State/read models Persistence、Projection、Compaction、Goal、Plan、Todo 持久事实、派生状态、上下文压缩
Web/protocol Webserver、Client modules/connection、Typert、Remote Host/Browser 装配与远程调用

新增 Service 只有在承担独立职责、有清晰消费者和生命周期价值时才合理。并非每个包都要为了“万物皆插件”再制造一层 Service;Definition、Provider、Consumer 只有确实独立演进时才拆分。

L4 · Agent Runtime:为什么它更像持久化执行内核

Agent Turn 与 Step 生命周期

图中的 Checkpoint 与 Retry 都是可选插件分支:前者依赖 session-checkpoint-policy,后者依赖 recovery 插件;未装配对应插件时,这两步不属于裸 AgentLoop。

第四层才进入通常意义上的 Agent Runtime。DSH 只有一个具体 Agent Loop,但 Loop 刻意保持为稳定骨架;retry、checkpoint、compaction、approval、hooks、goal driver 等行为通过 Service/Event 插件接入。

4.1 先区分六个对象

对象 保存或负责什么 不是什么
Session append-only durable facts 与 header identity 当前正在运行的 Agent 实例
Agent 精确身份、Scope、inbox 与当前活动资源 持久聊天记录本身
Turn 首次 claim 前开启、包含 0..n 个 Step、没有欠工作时闭合 必然只有一次模型请求
Step 一次 accepted 模型交互单元 一个完整用户任务
LLM Runtime provider/model 选择、一次 stream attempt 规范化 隐式 retry/failover 控制器
Tool Runtime 工具准备、dispatch、结果规范化和 finalize durable result 排序的唯一 owner

可以记成:Session 说明发生过什么,Agent 说明现在由谁执行,Loop 决定下一步做什么,LLM/Tool Runtime 负责具体执行。

4.2 一个 Turn 的精确顺序

阶段 A:输入与准入

followupsteerinject 进入 inbox。Agent 先 append turn/start,再 claim 输入、装配 system prompt 与动态上下文,并运行 agent/pre-step。只有 accepted step 才 append step/start 和 user message。

这个顺序很重要:Turn 是一次真实活动边界;Step 则表示某次模型交互已经被准入。把 turn/start 放在 claim 之后会丢失被插件拒绝、改写或等待过的活动事实。

阶段 B:从 Session 派生模型请求

Agent 先调用 deriveMessages() 取得当前 Model Surface snapshot,然后解析 provider/model、system prompt、tool schemas 与配置,记录 request/headerrequest/context,最后构造本次 Provider 请求。request/context 当前精确包含 provider、model 和 contextWindow,不应泛化为“所有上下文”。

这种顺序与“model-visible ⇔ logged”共同保证:模型请求不是从一堆无记录内存变量临时拼成,而能通过事件与内容寻址附件重建。

阶段 C:一次 LLM attempt

LLM Runtime 精确选择 provider/model adapter。Adapter 每个 stream 只拥有一次 wire attempt;最终 adapter selection、同步 dispatch、iterator construction 与 iteration 失败会被规范化为 terminal finish,LLM middleware、adapter cleanup、nested call 或 downstream consumer 的编程错误仍然 throw。

Retry 不藏在 SDK 或 Adapter 中。可选 recovery 插件在同一个 open step 内记录新的 provider attempt,并受有界策略控制;不会静默切换 provider/model。这样每次失败、等待和重试都可见、可回放,但也不能消除重复计费或上游已经接收请求的不确定性。

阶段 D:工具准备、执行与 durable commit

Tool Runtime 与 AgentLoop 分工如下:

  1. AgentLoop 先按模型顺序 append durable tool/call
  2. Prepare:复制并冻结参数,运行 tools/pre-execute 与 monotonic guards;
  3. Dispatch + normalize:tools/execute wrapper、ToolDefinition.execute、输入/输出 schema、render 与 presentation metadata;Approval 可由具体 Consumer 在 body 内、产生副作用前请求;
  4. Finalize/Finish:tools/post-executefinalizeContent、materialize、live tools/result
  5. AgentLoop scheduler:按模型原始 tool-call 顺序 append durable tool/result

工具 body 可以并发,但持久结果不能乱序,否则 Provider transcript 中 tool call/result 配对会被破坏。Call card 目前有 generic | terminal | difflocations 是其中字段;Result card 另有 generic | terminal | diff | search | read | web

Code Mode 没有另开一条“特权工具通道”。Worker 中生成的 typed SDK 调用重新进入同一 Tool Runtime,因此继续经过 schema、policy、approval、审计和 durable result 流水线。Worker 本身仍只是 containment,不是安全边界。

阶段 E:继续下一 Step 或结束 Turn

Assistant 没有工具调用时可以完成;有工具调用时,Loop 依次提交结果,除非某个结果明确 concludesTurn,否则继续下一 Step。additionalContexts 会同步进入 next step,但不是“继续下一步”的必要条件。最后由 turn-stopping 扩展点参与收敛。

4.3 Checkpoint 是装配出来的策略

在装配 session-checkpoint-policy 的产品中,系统会在 LLM dispatch、顶层 Tool body 和 accepted pre-step 边界前建立 checkpoint,以降低“模型已看到/工具已执行但日志尚未 durable”的窗口。未装配该插件时不能把这些 checkpoint 当成 AgentLoop 的无条件保证。

这正是插件化的含义:Loop 提供可插入时机,部署决定是否安装更强 durability policy;文档必须区分核心语义与某个 Bundle 的默认 composition。

4.4 Goal、Plan、Todo、Compaction 为什么不是 Loop 私有字段

能力 Durable vocabulary 运行语义 有意不做什么
Goal goal/change durable goal state;process-local armed 驱动下一次维护 不是 DAG task engine
Plan plan/mode idle 时立即切换;running 时排到下一 accepted pre-step 模型退出需评审,但人类 /plan off 可直接关闭
Todo todo/write 当前 Turn 的模型可见 checklist,下一 turn/start 清空 不充当持久项目管理器
Compaction start/summary/end、prune、tool-result replacement Raw Log 保留,Model Surface 被摘要或裁剪 不删除真实事件

把这些能力写进 Loop 私有 mutable state 会形成第二事实源,也会迫使所有产品加载相同策略。通过事件与扩展点,它们可以按 Profile/Preset 装配,并从 Session 恢复。

4.5 核心问题:为什么不是“以 Graph 为中心”

先区分两种完全不同的图:

  • Cordis plugin graph 表达构件依赖、Service availability、Scope 与生命周期;
  • LangGraph/Workflow graph 表达业务控制流中的节点、边、条件和状态迁移。

DSH 的主组织轴是 durable Session Event Log + 一个具体 Agent Loop + 插件扩展点。交互式 Agent 的下一条路径经常由模型输出、工具结果、用户 follow-up、审批、后台任务和恢复状态共同决定;如果要求所有行为先变成一张业务 Graph,那么 Graph 最终会被迫同时承担 UI、Provider 生命周期、会话持久化、权限、Web carrier 和插件热卸载,失去单一职责。

DSH 当前交付的是可选的脚本式 workflowEngine:一 run 一 Worker 执行 fan-out,通过 Host RPC 创建真实 child Agent,并复用 Agent、Tool、Session 与 Subagent;它不接管全局 Runtime。通用 Graph Engine 或 LangGraph Adapter 尚未交付,但可以按相同架构作为编排能力插件接入,而不必成为全系统唯一组织轴。

截至 2026-08-14,LangGraph 官方文档仍把持久化、流式处理和人工介入建立在 graph runtime 上,Graph API 以 shared State、Nodes 与 Edges 为中心。这里的区别是中心抽象不同,不是 DSH 有持久化而 Graph 框架没有。

什么时候 Graph 更合适

  • 稳定、可预声明的业务步骤;
  • 明确分支、审批节点和补偿路径;
  • 强流程可视化与运营配置;
  • 每个节点都需要独立重试、SLA 或人工队列;
  • 主要目标是确定性工作流,而非开放式长会话。

什么时候 DSH 的持久化执行内核更合适

  • 长寿交互 Session 与恢复;
  • 工具、Prompt、Provider、UI 能按 Profile/Preset 热组合;
  • 用户随时 follow-up/steer/approve;
  • Subagent、Jobs、Schedule 等活动语义不同,不能强行装进统一 Task node;
  • 同一内核需要 CLI、Web、SDK、ACP 多种投影。

4.6 与 Claude Code、Codex、OpenCode:差异在系统组织

只看模型—工具循环,DSH 与产品化 coding agent runtime 没有代际差距。它不因架构更复杂就让模型更聪明,也不应被描述成“其他产品没有 Agent Runtime”。

截至 2026-08-14,Claude CodeCodexOpenCode 都提供各自的 Skill、MCP、Agent、Hook 或 Plugin 扩展面。因此比较重点不是“谁有插件”,而是这些扩展是否与产品装配、作用域、持久事件、Host/Browser 和资源收敛共享同一套运行机制。

DSH 源码能够证明的辨识度在内部组织:

  1. Cordis effect 生命周期贯穿 Host、Browser、Service、Tool、Provider 与 UI contribution;
  2. Capability 明确拆成 Definition / Provider / Consumer,执行世界可以成组替换;
  3. 所有 model-visible 输入必须能由 Session Log 语义重建;
  4. Profile/Bundle/Patch 与 per-Agent Preset 把产品和 Agent composition 数据化;
  5. Raw Log / Model Surface / Projection 分离,恢复时保留未知工具副作用;
  6. Browser 运行由 Host-authored roster 决定的独立插件树。

这些差异强调系统级一致性与可验证性,而不是 Loop 算法领先;代价是包更多、间接层更深、学习与运维成本更高。Claude Code、Codex、OpenCode 的具体功能和扩展 API 会变化,本节属于比较性架构判断,不是仓库源码事实。

L5 · Capabilities:用稳定接缝替换执行世界

第五层把 Agent Runtime 与外部世界隔开。每个成熟 Capability Seam 包含三个角色:Service Definition 定义 WHAT,Provider 决定 HOW,Consumer 决定 PRODUCT USE。

5.1 三个角色不是三步调用顺序

Provider 和 Consumer 都依赖 Definition;Consumer 通过稳定 Service 或 Registry 调用当前 composition 选择的实现。它们不一定按 Definition→Provider→Consumer 顺序激活,也不要求三个角色永远分成三个包。

只有角色确实独立演进时才拆分。例如 Shell 的本机/Pwsh/Sandbox Provider 会变化,模型工具也会独立变化,因此拆分有价值;某个内聚领域若只有单一实现和单一消费者,不应为形式整齐制造空包。

5.2 为什么要成组替换“执行世界”

Local world 的两个基础 Provider 是 fs-local + subprocess-local;E2B world 则需要共享生命周期 owner ctx.e2b,再挂 fs-e2b + subprocess-e2b。Agent、Session、LLM 和 Tool schema 仍留在 Host。

如果只把 FS 换成 E2B,而 Shell 仍在本机,文件工具与命令看到的 cwd、路径和进程就不属于同一台机器。成组替换保证 FS、Subprocess、LSP 等 Consumer 使用同一个世界,同时不必复制 Agent Loop。

E2B 也不是完整安全结论:sandbox state 是 ephemeral,没有 Host workspace sync;cwd 只是解析约定,不是 containment;网络沿用 base image policy;SDK 仍可能在 Host 内存累计完整 stdout/stderr;控制状态与模型进程同 UID;默认环境不适合放 secret。

5.3 Capability 全景

家族 Definition / 核心 Service Provider 示例 Consumer / 产品投影
LLM ctx.llm、provider/model registry DeepSeek 等 adapters AgentLoop request、model discovery UI
Shell ctx.shell bash-local、bash-sandbox、pwsh tool-bash、persistent bash
Subprocess ctx.subprocess subprocess-local、subprocess-e2b Shell、LSP、Terminal、MCP stdio
FS ctx.fs fs-local、fs-e2b、fs-sandbox wrapper read/write/edit/search tools
Terminal terminal registry/backend terminal-bash tool-terminal
LSP LSP registry/query service lsp-stdio tool-lsp
Web ctx.web search/fetch HTTP fetch、Exa、Perplexity、DeepSeek search tool-web
Code Runtime ctx.codeRuntime worker-thread Code Mode typed calls
Workflow ctx.workflowEngine worker-thread tool-workflow、Ralph
Jobs job registry jobs-local bash/terminal/subagent producer,tool-jobs controller
Schedule durable domain + timer projection local runtime create/list/delete tools
Skill scoped skill provider registry filesystem、badge catalog/loader tool
MCP external tool bridge stdio、streamable HTTP native Tool Registry contributions
Subagent named provider registry in-process、fork、ACP、DSH SDK、显式产品 adapters delegation tools、Workflow
Settings/Credentials namespace/document、reference resolver file/local/env/.env UI、Provider config、secret lookup
Interaction Approval、Question、Commands、Permission UI/waterfall answerer、policy plugins model tools、audit events
Workspace/Feedback workspace registry、sidecar feedback local persistence Web workspace、message/command feedback

下面只展开决定架构边界的能力,而不是逐包复述 API。

5.4 Shell 与 Subprocess:默认值和进程树归谁

凡存在请求默认化的 Capability,默认化由 owning implementation 的显式 resolve(request): Spec 承担;Shell 是模板。Subprocess 则直接要求 fully specified SubprocessSpawnSpec:argv、cwd、每个 stdio disposition、grace、signal、env 都必须明确,不经过 shell 解释。

Deadline 与 timedOut/aborted 原因分类属于 Shell 等 Consumer/上层 executor;Subprocess 只响应 caller-owned signal、管理 bounded stream/tail/spill 与进程树终止。terminate() 是唯一、幂等的终止动词,执行 graceful→forced escalation 并等待 whole-tree。

但本机进程清理不是数学意义的绝对保证:POSIX daemon reparent、child setsid、观察前脱离的 terminal child,或 Windows tree liveness fallback 都可能逃出 owner 视野。

5.5 FS:存储接缝不等于安全策略

FileSystem 提供 12 个 primitives;opaque branded target/version 将显示路径、真实 process path 与 file URI 分开。Text read 拒绝无效 UTF-8 和 NUL binary;readBytes 强制调用方给 max bound;mutation 支持版本 guard 与原子 publish;editText 把版本检查、字面匹配和 rewrite 放在同一 critical section,确保并发编辑只有一方成功。

Seam 有意不包含 delete/move/copy/watch;FS async signal 是 best effort,没有统一 IO deadline。Local Provider 的 config.cwd 只是相对路径默认值,不是 containment,绝对路径或 .. 可以越出。

Observation Policy 通过 fs/observed 与 write/edit intent waterfall 强制 read-before-edit 和 optimistic version guard。它的观察状态在 owner/session WeakMap 中,resume 后不保留;windowed read 可以授权未变化文件的 full-file overwrite,因为 freshness 不等于视图完整性;直接调用裸 ctx.fs read 不自动发 observation event。

fs-sandbox 是可信进程内 canonical-path mutation fence,只围写/edit,不围 read。workspace-write 允许 workspace 加平台 temp roots;紧邻 mutation 再 canonicalize 只能缩小 symlink TOCTOU,不能消灭 hostile same-process race。

5.6 LSP、Web 与外部知识

LSP 按规范化文件扩展名路由,Provider id/extensions 原子预留,冲突整体失败,避免注册顺序决定胜者。只开放四类只读语义查询,没有任意 JSON-RPC escape hatch 或 executeCommandlsp-stdio 通过 ctx.fs 读取文档、通过 ctx.subprocess 启动 Server,从而与当前执行世界一致。

Web 的 search/fetch 并列在同一 ctx.web。请求可显式指定 Provider;若恰好一个 usable Provider 才自动选择,绝不让注册顺序胜出。HTTP fetch 只允许 http(s)、禁止 URL credentials、只接受 same-origin redirects、限制 timeout/bytes/chars/content-type/charset,不携带 ambient cookies/credentials。

但当前 fetch 尚未阻止 private-network SSRF,Provider 文档明确把它称为 SSRF primitive;可达敏感内网的部署不得启用。它只接受文本,无 PDF/高级抽取;缺 Content-Type、binary 或不支持 charset 会拒绝。

5.7 Code Runtime 与 Workflow:Worker 是 containment

Code Runtime 每次 run 新建 Worker,Host 先 strip TypeScript types,再用 async body 支持 top-level await/return。Host 把 Worker 当 hostile peer:exact-shape message validation、own-property lookup、at-most-once call id、null-prototype namespace、lossless JSON、扁平 wire format;Worker 内 compute budget 捕 hot loop,Host wall budget 捕 stalled await。

但 Worker 可见 parentPort,程序启动的 OS 子进程可能活过 Worker termination。因此这是故障 containment,不是恶意代码安全边界。

Workflow 也是一 run 一 Worker,Worker 只发送 agent()/parallel()/pipeline()/phase()/log() RPC,真实 child Agent 由 Host 创建。start() 同步验证后返回 holder-owned run;一旦接受,result 用 stopReason 表达 failure/cancel,不 reject;engine unload 不取消已接受 run,caller 所有路径都必须 dispose。

父 Session 可记录 observe-only workflow/run/member events,Chat 能重放执行轨迹,但运行本身没有 journal/resume。Durable observation 不等于 durable execution。Worker 内 node:vm 也不是 security boundary。

5.8 Jobs 与 Schedule 为什么不统一成 Task Engine

Jobs 是 live registry:id 可预测,所以授权靠 exact Agent/Session identity;producer 持真实执行资源;stream read 只有一个消费 cursor。Producer cancel() 如果返回但 done 永不 settle,Registry 无法判断是慢停还是失约,会永久占 capacity 并拖住 teardown;这不是 durable/cross-process backend。

tool-jobs 控制 list/read/kill,并用 wakeup/quiet 策略通知 owner;默认连续主动唤醒有上限,但一次 wake 仍可能购买新的模型请求,存在成本与自激风险。

Schedule 直接以 Session 的 schedule/change durable events 为事实源,timer 是可销毁 live projection。每次管理读写前 flush,append 后再 barrier;失败返回 persistence_uncertain 并要求 re-list。它处理 RFC3339/local+IANA zone、DST gap/overlap、超长 timer、overdue 与 fixed-rate backlog。

但 Schedule 只安装到插件加载后新建的 live root Agent;已有 Agent 与 runtime children 不自动获得。Dispatch 表示 follow-up 已排队并记录,不表示模型成功或用户读到;窄崩溃窗口不保证 exactly-once。

Jobs、Schedule、Workflow、Subagent 都可能看似“后台任务”,但它们的持久性、owner、输出、时间与恢复语义不同,强行统一会掩盖差异。

5.9 Subagent:Provider capability,不是固定团队范式

Subagent Runtime 是 named Provider registry。Provider 可以在进程内 spawn/fork、通过 ACP、DSH SDK,或由 Profile 显式安装 Codex/Claude 等 product adapter。系统不预设“研究员、程序员、经理”这样的团队模型。

One-shot start() 的返回是 publication/ownership transfer:返回前失败由 Provider cleanup,返回后由 caller/manager dispose。Continuable child 则由 manager 保证一个 durable child Session 最多一个 process-local Activation,inbox 是唯一 turn queue;followup 是 FIFO future turn,不是对当前 turn steering。

Durable descriptor 记录 provider、mode、agentProvider、agentModel、persona、toolFilter;Preset 在 child Session header 的 meta.agentPreset。Manager 可据此 cold resume,但没有 durable offline mailbox、receipt 或 exactly-once settlement;parent gone 时 notice 也可能丢失。

5.10 Settings、Credentials 与 Interaction

Skill、MCP 与 UI Slot 的角色已在 L3.7 统一说明;能力层只关心它们最终仍进入原生 Registry、Tool Runtime 与 Browser Slot。

Settings 把 schema/base composition 与 provider document 分开,user layer 覆盖 base,invalid reload 保留 last-good;file Provider 原子写、保留 YAML comments 并串行 refresh。Credentials 的配置面只携带 reference,resolve 时从 process env、managed credentials YAML、project/user .env 取实际值,不缓存 resolved secret。

Managed .credentials.yaml 本身保存实际 secret。0600/0700 与 atomic write 只能限制其他 OS 用户,同 UID 模型工具仍可能主动读取;系统做到不自动把 secret 注入普通 subprocess、不告诉模型 resolved path,不等于同用户进程隔离。

Approval 是 one-shot allow/reject/cancel/unavailable;没有 answerer 或 answerer failure 时 fail closed。它只在 open Agent Turn 请求,使 approval/askedapproval/decided 落在同一 replay/commit 周期。Permission preset 协调 sandbox/approval 并 durable 记录切换;delegated child 不能直接问用户。

5.11 安全阶梯:每层只承诺自己能证明的保证

机制 能证明什么 不能证明什么
1 Tool schema、policy、approval 输入结构、部署策略、一次性用户同意 OS 隔离
2 FS observation policy read-before-edit、optimistic version 阻止恶意同进程代码
3 fs-sandbox path fence 可信进程内 mutation roots Kernel confinement、read restriction
4 Subprocess owner 显式 argv/env/stdio、bounded output、树终止 所有 daemon 永不逃逸
5 OS sandbox 文件访问机制与 enforcement report 统一网络/进程可见性;Windows 当前 fixed partial
6 E2B remote world 远端环境与 Host 分离 自动获得完整 secret/network/同步策略

请求 confined mode 时,若没有可用 Sandbox Provider 必须 fail closed;danger-full-access 则明确绕过 Provider,是不受限执行。Sandbox mode 只表达文件副作用 read-only | workspace-write | danger-full-access,不承诺网络和进程可见性。

环境 scrub 按变量名包含 KEY/PASSWORD/SECRET/TOKEN 或 DSH_* 的启发式规则剔除,denial signature 与 runner-failure classification 也包含启发式,因此可能 false positive/negative。安全设计的优点恰恰是承认这些边界,而不是用“sandbox”一个词覆盖所有风险。

L6 · Data & Infra:Session 事实的三种读法与明确不确定性

Session Event Log、Model Surface 与 Projections

第六层是“可恢复”的基础。DSH 不把聊天消息数组当作全部状态,而把 Session 设计为 append-only typed event log;模型消息、Conversation UI、Goal/Plan、统计、搜索和遥测都是从事件派生的不同读面。

6.1 Session 内可恢复事实为什么只有一个权威源

如果同一 Session 同时用 mutable messages、工具状态表、UI 通知和持久记录表示模型看见的内容,崩溃、重试或插件卸载会让它们漂移。DSH 让 Session Event Log 成为模型上下文与可恢复会话事实的权威源,其余 Session 读面从中 fold/derive。Settings、Credentials、附件字节、Profile/Preset 文件等仍有各自领域存储,不属于 Session Log。

Append 在内存中同步可见并触发 post-commit;PersistenceCoordinator 负责串行 write-behind 与 bounded batching;flush() 是明确 durability barrier。这个模型允许高频 chunk 不必每条都同步 fsync,但在 LLM dispatch、Tool body、用户确认等关键边界可由 checkpoint policy 强制 durable。

JSONL 与 SQLite 实现相同 SessionEvent 语义:JSONL 能识别和修复 torn tail,SQLite 用事务和 schema version;PersistenceCoordinator 只编排 append/flush/recovery,backend hooks 保留各自 bytes/rows 机制。两者都不能把 storage-specific row 变成第二事实源。

6.2 Session Log 的三种读法

Raw Event Log:发生过什么

它保存 user/assistant/tool、turn/step、request、retry、goal/plan/todo、approval、usage、subagent、schedule 等 durable vocabulary。Raw chunks、失败 attempt 与中断边界不会为了得到“漂亮对话”被删除。

Model Surface:模型现在看什么

Surface 只包含模型可见节点,例如 user、assistant、tool-result,并允许受约束 replacement。Compaction summary 可以替换早期 Surface 区间,model-free pruning 可以替换超大 tool-result content;原始 Raw Log 仍保留。

Projection / Read Model:人和查询怎样读取

Goal、Title、Stats、Plan、Todo 等可以注册为纯同步 projection unit,cache 只加速这些 unit;Conversation assembler 与 Session Query 是各自基于事件的独立读面,不应都笼统称为 SessionProjectionRegistry unit。

Projection cache、搜索索引和 UI Store 坏掉可以重建,不能反写覆盖 Raw Log。这一分离让“模型看到的简化历史”和“审计看到的完整事实”同时成立。

6.3 Model-visible ⇔ Logged

这是仓库最强的不变量之一:所有进入模型请求的内容必须可由 Session Log 重建。系统不仅记录聊天文本,还记录或引用:

  • provider 与 model route;
  • system prompt 与 tool schemas;
  • adapter defaults、配置与 contextWindow;
  • 动态 instructions、session references 和模型可见领域状态;
  • attachment 的不可变、内容寻址 durable reference。

大附件本体可以外置,避免日志膨胀,但 durable event 必须保存内容寻址引用。进程内一个“不会修改的对象引用”不满足要求,因为重启后无法重建。

这一设计提高 replay、fork、prefix cache 稳定性和审计能力,代价是任何新的模型可见特性都必须先设计 durable event vocabulary、decoder 与恢复语义,不能只改 Prompt builder 的内存逻辑。

6.4 崩溃恢复为什么保留“不知道”

恢复时若看到 Assistant 请求工具,却没有 durable call,系统可标记 TOOL_NOT_STARTED;若已有 durable call 但没有 result,则标记 TOOL_OUTCOME_UNKNOWN。后者可能已经产生文件、网络或进程副作用,系统不能因为日志里没有结果就自动重放。

这种恢复策略牺牲“看起来自动完成”的便利,换取副作用安全和诚实审计。调用者可以在 UI 中解释未知结果,或让用户/模型检查外部状态,而不是把不确定性伪装成失败后重试。

崩溃 Turn 也不截断原始事实;恢复逻辑可追加 synthetic closers 使逻辑结构闭合。这样中断、partial stream 和已发生的 Tool Call 仍可追溯。

6.5 严格版本与 Typed Events

Session format 使用单调整数版本,而不是 major/minor 兼容猜测。Reader 遇到更新版本会拒绝;只有 backend 暴露 per-session artifact 时,诊断才能同时给出 raw log path。未知 required event 默认不可忽略,表示当前 Runtime 不理解可能影响语义的新 vocabulary,并抛出 SessionFormatUnsupportedError

Envelope 协议预留了 ignorable: true,但当前 writer 不写该标志,Session.append() 也没有通用写入入口;仓库外插件新增 durable event 后,第一方 generated known vocabulary 会在 cold resume 时拒绝它,除非相应词汇也进入第一方 reader。这与 corruption 不同:未知事件可能是合法但来自更新 Runtime;坏 JSON、无效字段或断裂结构才是数据损坏。

当前 SESSION_FORMAT_VERSION 仍为 0,pre-release 不承诺旧格式兼容,v0→v1 upgrader chain 尚未交付。统一 runtime typed-event schema registry 仍是 proposed;现状是 TypeScript declaration merging、JSON/envelope/Surface 检查、generated known vocabulary 与 owner decoder 共同防守。

6.6 SessionPreparation、Compaction 与查询成本

SessionPreparation 会缓存 exact unpublished Session,让 history inspect 与随后 resume 可复用同一实例,并用 revision 检查拒绝 stale;这降低重复读取和解析,但引入 LRU/占用管理问题,且跨进程 writer 并非排他。

长日志投影成本由 bounded persistence batching、projection cache、compaction、model-free prune 等机制缓解。Compaction 仍可能丢失模型 Surface 中的细粒度 KV 语义,摘要质量会影响后续行为;它压缩的是模型上下文,不是权威历史。

6.7 SDK、ACP 与 Python Runtime 的数据语义

SDK Protocol 是 newline-delimited JSON-RPC 2.0 named maps。Server 以 Session id 创建/获取 Agent,prompt 立即返回 receipt/message id,随后推 durable events、whole-agent status 与 subagent lifecycle。

高层 SDK 把“receipt 到下一次 whole-agent idle”定义为 activity interval,结果取最后 committed root assistant text。这不是严格属于单个 Prompt 的 Promise:期间可能包含 steering、queued follow-up 或 descendant activity。

关闭顺序先 best-effort protocol shutdown 与 stdin EOF,再用平台原生 graceful/forced termination;TypeScript POSIX 是 SIGTERM→SIGKILL,Windows 直接 forced termination,Python 是 terminate()kill()

ACP 是 automation-only 的窄投影:fresh-session-only,没有 list/load/delete/resume;一个 connection 可并发持有多个 Session,每个 Session 一次一个 inflight Prompt;只输出 committed assistant text,不输出 reasoning、完整 tool/trace;没有单 Session close,connection-owned teardown 统一 dispose。

Python SDK 同步镜像 JSON-RPC。Runtime wheel 携带精确版本的单文件 Node executable(macOS helper)与默认 cordis.yml;SDK 显式注入 config,runtime 自身永远要求显式 config,没有隐藏 fallback;system-node 仅是显式开发模式。

6.8 Hooks、Native 与动态扩展

Hook Protocol 把 Claude Code/Codex 配置 matcher、subprocess 调用、output parse/merge 与 durable hook events 统一,再把厂商 dialect 映射到 agent/pre-step、tools pre/post、turn stop 等 Cordis extension points。它是兼容桥,不比原生插件拥有更高权限;updatedInput 当前解析但未应用,startup hook 也可能错过首个 request。

动态 Cordis Host/Client 扩展允许模型临时写插件:Host 在 VM 中只暴露声明的 injection/lifecycle verbs,Client closure 只暴露 React、console、styles、host 和 guarded facade。仓库明确把它定位为教学护栏,不是强安全边界,应按 shell 权限信任。

Native Landlock runner 在 Linux 上自限制后 exec,无法强制、路径打不开或 exec 失败都 fail closed 125;它协商 ABI 1–5,partial enforcement 由 Consumer 判断。非 Linux 不支持,旧 ABI 不涵盖新访问类型,目标程序本身也可能返回 125,因此调用者还需诊断文本。

6.9 跨层设计选择总表

设计选择 为什么这样设计 得到什么 付出什么
Cordis 微内核 复用 Service、Event、Scope、Effect/HMR 插件统一装卸与依赖管理 间接层和 Fiber 调试成本
Profile/Bundle/Patch 把部署差异数据化 多 Surface 共享机制 配置层次复杂,整项替换更冗长
Session Log 是可恢复会话事实的权威源 避免 mutable history/notification 双事实 Replay、fork、audit、恢复 durable vocabulary 与投影成本
Raw/Surface/Projection 分离 审计历史与模型上下文需求不同 原始事实不丢,模型可压缩 三种读面需要严格区分
Model-visible ⇔ Logged 模型输入及有效请求配置可从 Session Log 语义重建 prefix 稳定、可重构 新特性必须付持久语义成本
Scope publication last 避免半装配 Agent 被看见 创建/销毁可验证 生命周期算法更复杂
Definition/Provider/Consumer 环境实现与产品投影独立演进 Local/E2B/Sandbox 可替换 包和 wiring 增多
显式 Request→Spec 默认值和预算有唯一 owner 调用边界清晰、可审计 Consumer/Provider 需多一阶段
可选 checkpoint policy Durability 强度可按产品装配 关键副作用前可强制落盘 核心语义与默认 Bundle 要区分
Adapter single attempt + plugin retry 每次失败和费用都可见 无隐藏 retry/failover 恢复策略实现更显式
Typert + explicit BFF 类型、codec、文档同源,拒绝自动暴露 Host/Client 漂移减少 Build graph 更复杂,仍主要 unary
Typed UI slots UI 随插件装卸 无中央 renderer switch registry 查找与 HMR 状态代价
Named Subagent providers 多后端共存,不预设团队范式 in/out-process 统一 ownership 能力不完全同构,无 exactly-once
分层安全机制 每层只承诺能证明的保证 Fail-closed 且边界诚实 没有“一键全安全”的简单叙事
严格 Session 版本 静默语义损坏比拒绝更危险 新 vocabulary 不被误读 预发布升级便利性较差

6.10 当前系统级限制清单

这些限制是架构的一部分,不应被营销式总结隐藏:

  • Same-process cancellation 只能合作式收敛;不协作插件可拖延 quiescence。
  • Local subprocess 的 daemon/setsid/平台观察差异可能造成 orphan。
  • Continuable Subagent 没有 durable offline mailbox、receipt 或 exactly-once settlement。
  • Workflow execution 无 journal/resume;Terminal、Jobs 和多数 live handles 不跨进程恢复。
  • Schedule dispatch 不代表模型成功或用户收到;窄窗口可能重复,已有 Agent 不自动安装。
  • 旧 Preset generation 会为潜在 join 保留;长寿 Web Host 的 active Agent eviction 尚未完整解决。
  • Attachments、spill files 与部分缓存缺少完整 GC/配额闭环。
  • Typert 主要支持 unary;legacy migration 未完成;remote events 无 redaction/replay。
  • Atomic Browser boot 牺牲 progressive rendering;HMR 粗粒度、无 rollback、插件局部状态丢失。
  • SDK run 是 activity interval,不是严格 prompt causal result;ACP 是 fresh-session-only 的文本窄投影。
  • Session format 仍为 version 0,尚无 v0→v1 upgrader;未知 required event fail loud,当前 writer 不产生通用 ignorable event,统一 runtime event schema 尚未落地。
  • Compaction 可能损失细粒度模型上下文语义,Projection/Preparation 有缓存占用与跨进程 writer 限制。
  • fs-sandbox 不是 kernel boundary,Worker/VM 不是安全沙箱,Windows ACL 固定 partial。
  • Web Host 信任检查不等于 auth/TLS;Web fetch 是 SSRF primitive。
  • Managed credentials file 存实际 secret,同 UID 模型工具仍可能读取。
  • E2B 状态 ephemeral、无 Host workspace sync,stdout 内存与 secret/network policy 仍有适配限制。

完整实战:生产 Bash 插件链

本节把六层重新压回一条真实调用链。非 Windows 的生产 Base 使用 tool-bash 作为模型 Consumer,bash-sandbox 提供 ctx.shell,并复用 bash-local 的本地进程语义;底层由 sandbox-localsubprocess-local 实施文件约束和进程树管理。

Bash 插件完整调用与生命周期

生产装配

以下是 packages/bundle/base/cordis.patch.yml 展开后的非 Windows 活跃行节选。源码位于 insert rows 中,bash-sandboxtool-bash 还带 Windows disabled 条件;后台路径同时依赖已装配的 jobstool-jobs

- id: subprocess
  name: '@deepseek-ai/dsh-subprocess-local'
- id: sandbox
  name: '@deepseek-ai/dsh-sandbox-local'
- id: sandbox-policy
  name: '@deepseek-ai/dsh-sandbox-policy'
- id: bash-sandbox
  name: '@deepseek-ai/dsh-bash-sandbox'
- id: jobs
  name: '@deepseek-ai/dsh-jobs-local'
- id: tool-jobs
  name: '@deepseek-ai/dsh-tool-jobs'
- id: tool-bash
  name: '@deepseek-ai/dsh-tool-bash'

Entry id 只用于 Loader、Patch、Fiber 与 HMR。业务调用使用 ctx.shellctx.subprocessctx.sandboxctx.sandboxPolicyctx.jobsctx.tools 等稳定接口。

参与者与责任

角色 责任
shell/shell Definition 定义 Request、Spec、Result、Process Handle 与 ctx.shell
shell/bash-local 实现复用层 默认化、Bash argv、环境、输出预算、前后台进程语义
shell/bash-sandbox 当前 Shell Provider 包装 argv、报告 enforcement/denial、处理 Sandbox runner failure
shell/tool-bash Consumer 模型 Schema、Prompt、Approval、Jobs、Renderer 与 Presentation
sandbox/sandbox Definition 定义文件策略与 ctx.sandbox.confine()
sandbox/sandbox-local Provider 探测平台 Runner,生成受限 argv;不可用时 fail closed
subprocess/subprocess Definition 定义完整 Spawn Spec、Handle 与进程树终止义务
subprocess/subprocess-local Provider 创建进程、收集有界输出、终止进程树并等待 quiescence
sandbox/sandbox-policy Policy Service 解析部署默认、Session override 与本次获批模式
jobs/jobs-local Background Registry 接管后台 Handle、Owner、状态、输出游标与清理
jobs/tool-jobs Background Consumer 注册 job_output/list/kill 并投递完成通知
core/tools Registry/Runtime 可见性、pre/guard/execute/post、校验、render 与 normalize
core/agent-loop Scheduler 按模型顺序写入 durable tool/calltool/result

Definition:固定调用义务

ShellExecutor 以稳定 Service key shell 注册,并把 raw Request 与 fully specified Spec 分开:

export abstract class ShellExecutor extends Service {
  constructor(ctx: Context) {
    super(ctx, 'shell')
  }

  abstract resolve(request: ShellExecRequest): ShellExecSpec
  abstract run(spec: ShellExecSpec): Promise<ShellRunResult>
  abstract start(spec: ShellExecSpec): ShellProcess
}

这里固定的不是 Bash 实现,而是双方义务:非零退出、timeout 与 abort 返回结构化结果,基础设施失败才拒绝;start() 立即返回 Handle,后台进程没有 Bash timeout,Owner teardown 必须停止并等待仍运行进程。

Provider:本地机制与 Sandbox 包装

LocalBashExecutor 注入 subprocess。它在 resolve() 中集中补齐工作目录、timeout、输出上限、stdin、env 与可信 dshEnvrun() 不在执行深处继续猜默认值。下面只保留默认化主线:

resolve(request: ShellExecRequest): ShellExecSpec {
  const timeoutMs = clampTimeout(
    request.timeoutMs,
    this.config.timeoutMs,
    this.config.maxTimeoutMs,
    'bash-local: request.timeoutMs',
  )
  return {
    command: request.command,
    workdir: request.workdir ?? this.config.cwd ?? process.cwd(),
    timeoutMs,
    stdoutMaxBytes: request.stdoutMaxBytes ?? this.config.maxOutputBytes,
    sandboxPolicy: request.sandboxPolicy,
  }
}

生产 Shell Provider 是继承该机制的 SandboxBashExecutor

export class SandboxBashExecutor extends LocalBashExecutor {
  static override inject = ['subprocess', 'sandbox', 'sandboxPolicy']

  override resolve(request: ShellExecRequest): ShellExecSpec {
    return {
      ...super.resolve(request),
      sandboxPolicy: request.sandboxPolicy ?? this.ctx.sandboxPolicy.resolve(),
    }
  }
}

模式为 danger-full-access 时,它明确绕过文件 confinement;否则调用 ctx.sandbox.confine(['bash', '-c', command], policy),再把包装后的 argv 交给本地执行机制。Sandbox Runner 不可用或返回确定性 fatal 诊断时,前台调用抛出 SandboxUnavailableError,不会偷偷裸跑。

Consumer:把 Service 投影成模型 Tool

tool-bash 的硬依赖是 toolsshellsystemPromptshellEnv。它显式向两个 Registry 贡献 Prompt section 与 bash Tool;框架不会根据包名自动判断它属于工具层。

export function apply(ctx: Context, config: Config = {}): void {
  const defaultMode = ctx.shell.sandboxMode
  const sandboxPolicy = defaultMode === undefined
    ? undefined
    : ctx.get('sandboxPolicy')

  if (defaultMode !== undefined && sandboxPolicy === undefined) {
    throw new Error('mounted bash executor confines but sandboxPolicy is missing')
  }

  ctx.systemPrompt.section({ name: 'tool:bash', order: 105, text: '...' })
  ctx.tools.register(defineTool({ name: 'bash', /* schema, execute, render */ }))
}

核心执行仍只面向 ctx.shell

const result = await ctx.shell.run(ctx.shell.resolve({
  command: args.command,
  workdir,
  timeoutMs: args.timeoutMs,
  dshEnv,
  sandboxPolicy: policy,
  signal: exec.signal,
}))

tools/pre-execute 返回 ask 时,Tool Runtime 可以通过可选 Approval Service 解析后再进入 guards。Bash 的 sandbox_permissions 是另一条路径:tool-bash 在自己的 execute body 内调用 approveEscalation(),强制目标模式严格更宽并取得 allowed-once,再把完整 per-call policy 交给 Provider。

前台调用全过程

bash 没有声明 isConcurrencySafe,Tool Runtime 将它按 exclusive 调度;同一批其他工具不会与其 body 重叠。一次 bash({ command: 'git status', description: 'Show working tree status' }) 的真实主线是:

  1. AgentLoop 解析模型调用并 append durable tool/call
  2. Tool Runtime 查找当前 Agent 可见的 bash,复制并冻结参数;
  3. tools/pre-execute、monotonic guards 与 tools/execute wrappers 决定能否进入 body;
  4. defineTool 校验输入 Schema,tool-bash 再校验 Bash 专属字段,解析 workspace、环境与 Sandbox Policy;
  5. 只有显式 widening 才在 Consumer body 内请求 Approval;
  6. ctx.shell.resolve() 产生完整 Spec;
  7. SandboxBashExecutor 包装 argv,LocalBashExecutor 调用 ctx.subprocess.spawn()
  8. Subprocess 收集有限尾部输出,必要时写 spill,并在 Abort 时终止进程树;
  9. Consumer 返回 canonical output,Tool Runtime 校验输出 Schema、render、执行 post/finalize 并发送 live tools/result
  10. AgentLoop 按模型原始顺序 append durable tool/result

非零退出与前台 timeout 都是正常 Tool 结果,文本中附 exit/timeout marker;基础设施失败、调用取消或无效输出才进入结构化错误。Tool body 可以并发,Session result 仍按模型顺序提交,避免 Provider transcript 中 call/result 错配。

后台调用与所有权转移

run_in_background: truetool-bash 处分叉。Consumer 先完成 Jobs controller、owner、配置与 capacity preflight,再在 jobs.start() 的 starter 中调用 ctx.shell.start(ctx.shell.resolve(request));任何 preflight 失败都发生在 spawn 前。

jobs.start() 成功返回 jobId 后,原 Tool Call 结束,后台进程不再受本次 exec.signal 或 Bash timeout 控制。Jobs Registry 接管取消、等待与单消费输出游标;job_outputjob_listjob_kill 是新的 Tool Call。

Job 状态机、进程 Handle 与输出游标都只存在于进程内。Session 持久化最初的 Bash call/result、后续 Job 工具事件,以及完成通知进入 Agent inbox 后产生的事件;它不会把 Job 变成可跨进程恢复的任务。

tool-bash 卸载只撤销模型入口,不终止已移交 Job。Agent Scope、Jobs Service 与 Subprocess Service 分别拥有更长寿的资源,并在各自 teardown 中取消、等待。这里展示了所有权可以在 publication 时显式转移,而不是永远归创建对象的插件。

失败、取消与安全边界

情况 Owner 结果
参数或配置无效 Tool/Plugin activation 最早可判断处失败,不执行命令
tools/pre-execute 或 guard 拒绝 Tool Runtime body 不运行,仍形成 durable call/result 对
提权未获批准 Bash Consumer isError,保留 approval audit pair
非零退出 Shell Provider 正常 Tool 结果,附 exit marker
前台 timeout Shell Provider 正常 Tool 结果,附 timeout marker
Tool Call 取消 Tool Runtime + Consumer 未启动 body 跳过;已启动 body 必须真正收敛
Sandbox 后端不可用 Sandbox Bash Provider SandboxUnavailableError,fail closed
后台普通 spawn 异步失败 Shell Process Handle 当前映射为 killed,并通过 read path 报告失败
Tool 插件卸载 Tool Registry Effect 撤销 bash contribution,不追溯终止已移交 Job
Agent/Jobs/Subprocess 卸载 对应 Owner 取消并等待仍持有的 Job 或进程

同进程取消依赖合作。Subprocess 具有 graceful→forced 的树终止机制,但 daemon re-parent、setsid 或平台观察差异仍可能让后代逃逸。

Sandbox Mode 只描述文件副作用,不承诺统一的网络或进程可见性。workdir 只决定相对路径解析,不是 containment;danger-full-access 只绕过文件 confinement,环境清理、输出上限、进程所有权与 Session 记录仍存在。“先发生一次真实 denial 才能提权”是模型行为指令,不是内核搜索历史后证明的不变量。

案例结论:替换能力,不制造第二个世界

这套源码链证明了六点:

  1. Loader entry id 负责装配,业务代码通过 Service 与 Registry 连接;
  2. Consumer 定义模型协议,Provider 定义执行方式,Policy 定义允许范围;
  3. bash-sandbox 可以继承 bash-local 的机制,但 Consumer 不依赖具体类;
  4. Tool Runtime 负责策略、校验与规范化,AgentLoop 负责 durable ordering;
  5. Registration、live resource 与 durable fact 拥有不同 Owner;
  6. 替换 Provider 不应改变 Tool Schema,但相关 FS、Subprocess、LSP 必须属于同一执行世界。

最后一点解释了 E2B 的成组替换:裸本地实现复用层是 fs-local + subprocess-local,shipped Base 在其上使用 fs-sandbox + subprocess-local;E2B 则由 ctx.e2b owner 统一持有 fs-e2b + subprocess-e2b。只把 Bash 指向远端、FS/LSP 仍读本地目录,会让模型同时看见两个互相矛盾的文件系统。

结论:六层怎样合成“可组合、可恢复”

从上到下重新串起来:

  1. Surfaces 把同一内核投影成 CLI、Web、SDK、ACP;
  2. Composition 用 Profile/Bundle/Patch 选择整个产品,用 Preset/Scope 选择单个 Agent;
  3. Microkernel 用 Service、Registry、Event、Effect 管理插件的依赖、贡献与 quiescent teardown;
  4. Agent Runtime 用稳定 Loop 推进 Turn/Step,把策略留给插件,把关键事实交给 Session;
  5. Capabilities 用 Definition/Provider/Consumer 替换本机、Sandbox、E2B 与第三方世界;
  6. Data & Infra 用 append-only events、durability barrier、派生读面和严格恢复保存可解释的历史。

因此核心主题不是“有很多插件”,而是:

插件组合决定系统拥有什么能力;Session 事实决定系统能恢复到什么程度;Lifecycle ownership 决定系统能否安全卸载。

这三条一起成立,才构成“可组合、可恢复的插件化 Agent 运行系统”。

与其他框架的最终坐标

类型 中心抽象 DSH 与它的主要区别
LangGraph 类 业务 Graph / state machine DSH 不把业务 Graph 作为唯一组织轴;通用 Graph 可按插件能力接入,但当前未交付
Claude Code / Codex / OpenCode 产品化 agentic coding runtime Loop 能力高度重叠;DSH 更显式统一 composition、capability seam、Host/Browser plugin tree 与 model-visible/logged
OpenAI Agents SDK run、handoff、tools、guardrails、tracing DSH 额外把部署、Web、持久日志、执行世界和热生命周期纳入同一架构
AutoGen / CrewAI 角色、团队、对话协作 DSH 不预设团队范式,Subagent 是 named Provider capability
Semantic Kernel plugin/function/service DSH 更强调 Cordis effect scope、generated Typert、事件事实与双运行时产品装配

这个比较不是说 DSH 在所有场景更好。若目标只是快速搭一条 Python Chain、一个固定 Graph 或少数角色协作,DSH 的包数量和生命周期概念会显得过重;它更适合需要长寿 Session、多产品表面、可替换执行后端、热装卸插件与恢复审计的 Agent 产品。

二次开发:需求应该落在哪一层

需求 优先进入的层与扩展点 不要先做什么
新增 CLI/Web/自动化入口 L1 Surface + 既有 runtime/BFF 不复制 Agent 内核
新增产品形态 L2 Bundle/Profile/composition leaf 不 fork 一套 packages
新增 Service 或跨插件协议 L3 Definition + effect-scoped registry/event 不靠 entry id 分支
新增 Agent 策略 L4 已有 typed event/service extension point 不先修改 AgentLoop
新增模型工具 L5 Capability Consumer / Tool Definition 不把 OS 实现写进工具
新增本机/远端后端 L5 Capability Provider 不复制 Tool schema
新增模型可见上下文 L6 Session event + Surface 派生 不只改内存 Prompt
新增持久领域状态 L6 durable event + strict fold,或明确 domain storage 不让 UI state 成为事实源
新增 Web UI 区块 L1 Browser plugin + typed slot/node registry 不改中央 renderer switch
新增远程方法 L1/L6 Typert face + generated artifacts + explicit BFF 不自动暴露 Service
新增 Subagent 后端 L5 named Provider 不把厂商 API 写进 manager
加强安全 L5 先写 threat model,再选 policy/fence/process/OS/remote 不把所有限制统称 sandbox

一个插件改动前的检查表

  1. 它贡献 Service、Provider、Consumer、Policy、UI 还是协议?
  2. inject 是否只声明真正必需的 Service?
  3. 注册是否通过 effect,registry 是否返回 disposer?
  4. 进程、Worker、Watcher、Socket、Job 或 Activation 由谁持有?
  5. Dispose 是否发 cancel 后继续等待 quiescence?
  6. Model-visible 输入是否进入 durable event 或内容寻址引用?
  7. 配置错误能否在 load 或最早可解析点 fail loud?
  8. 跨 wire/process/worker/file/durable 边界是否做运行时校验?
  9. Consumer 是否只依赖 Definition,而没有 import 具体 Provider?
  10. 新能力是否真的需要独立 Service/包,还是一个 effect-scoped contribution 就够?

关键源码索引

最终掌握这套架构,可以归结为三个检查:行为由哪个插件/Service 提供;模型看到的内容怎样重建;资源与副作用由谁拥有并收敛。