
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 和协议;“可恢复”也不只是重新载入聊天文本,而是从持久事件重建模型当时看见的内容、领域状态和未完成工作的真实不确定性。
阅读全文时可以持续追问三个问题:
- 这个行为由哪个插件贡献,它依赖哪个 Service、Registry、Event 或 UI Slot?
- 这项内容是否会被模型看到;如果会,它怎样从 Session Log 重建?
- 它创建了什么进程、Worker、Watcher、Socket 或外部副作用;owner 是谁,dispose 怎样达到 quiescence?
证据范围
本报告按约定排除了单元测试、测试支持、fixtures、snapshots、benchmarks 与开发配置,依据生产源码、运行时 Cordis 配置、英文 README、当前有效 Agent Notes 和架构文档整理。
本文引用的架构插图均为生成式白板风格 PNG,不使用 Mermaid 或 SVG。
文中“代码事实”来自仓库当前源码和 implemented Notes;“据此可推断”“架构解读”属于综合判断;与外部 Agent Runtime 的比较属于比较性分析,不是仓库自身声明。项目仍处于 pre-release,包名和持久格式没有首个正式版本后的兼容承诺。
六层总图:后续所有章节的索引

这张图给出机制的主要归属,不是严格的包依赖 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 插件树

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.mux 与 events.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:产品不是复制出来的,而是装配出来的

第二层回答两个不同尺度的问题:这个进程是什么产品?这个 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 为根,然后依次应用:
- Profile 声明的有序 Bundle patches;
- Profile 目录中的用户 patch;
$DSH_HOMEpatch;- 命令行
--patch; - 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 插件不是一种业务类型

“插件”只统一四件事:加载、依赖、作用域、生命周期。它不意味着所有插件都实现相同业务接口,也不存在一个全局 PluginKind = 'ui' | 'llm' | 'fs' 让消费端分支判断。
一次典型激活过程是:
- 配置树声明插件实例;
- Loader 创建对应 Fiber;
inject检查依赖 Service 是否可用;- 条件满足后调用插件初始化函数或 Service 构造器;
- 插件显式向某个 Service、Registry、Event 或 UI Slot 注册贡献;
- 注册返回 disposer,并归属于当前 effect/scope;
- 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.shell、ctx.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-step、tools/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-local、fs-e2b 等具体 Provider,也不通过 entry id 找实现。Policy、UI 与 Protocol Adapter 是正交角色:Policy 约束调用,UI 投影结果,Protocol Adapter 转换进程或网络协议。
Service 可以通过三种方式扩展:
- 替换实现:新 Provider 实现同一 Definition,例如
bash-sandbox提供ctx.shell;同一 Scope layer 不应悄悄保留竞争实现。 - 注册贡献:Service 暴露
register(),插件追加具名 Tool、Provider、Projection 或 Command,例如ctx.tools.register(bashTool)。 - 增加正交接口:职责、生命周期或调用语义不同时,新增 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 的定位

这三类扩展共享 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。当前 DSHmcp-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:为什么它更像持久化执行内核

图中的 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:输入与准入
followup、steer 或 inject 进入 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/header 和 request/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 分工如下:
- AgentLoop 先按模型顺序 append durable
tool/call; - Prepare:复制并冻结参数,运行
tools/pre-execute与 monotonic guards; - Dispatch + normalize:
tools/executewrapper、ToolDefinition.execute、输入/输出 schema、render 与 presentation metadata;Approval 可由具体 Consumer 在 body 内、产生副作用前请求; - Finalize/Finish:
tools/post-execute、finalizeContent、materialize、livetools/result; - AgentLoop scheduler:按模型原始 tool-call 顺序 append durable
tool/result。
工具 body 可以并发,但持久结果不能乱序,否则 Provider transcript 中 tool call/result 配对会被破坏。Call card 目前有 generic | terminal | diff,locations 是其中字段;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 Code、Codex 与 OpenCode 都提供各自的 Skill、MCP、Agent、Hook 或 Plugin 扩展面。因此比较重点不是“谁有插件”,而是这些扩展是否与产品装配、作用域、持久事件、Host/Browser 和资源收敛共享同一套运行机制。
DSH 源码能够证明的辨识度在内部组织:
- Cordis effect 生命周期贯穿 Host、Browser、Service、Tool、Provider 与 UI contribution;
- Capability 明确拆成 Definition / Provider / Consumer,执行世界可以成组替换;
- 所有 model-visible 输入必须能由 Session Log 语义重建;
- Profile/Bundle/Patch 与 per-Agent Preset 把产品和 Agent composition 数据化;
- Raw Log / Model Surface / Projection 分离,恢复时保留未知工具副作用;
- 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 或 executeCommand。lsp-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/asked 与 approval/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 事实的三种读法与明确不确定性

第六层是“可恢复”的基础。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 不产生通用
ignorableevent,统一 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-local 与 subprocess-local 实施文件约束和进程树管理。

生产装配
以下是 packages/bundle/base/cordis.patch.yml 展开后的非 Windows 活跃行节选。源码位于 insert rows 中,bash-sandbox 与 tool-bash 还带 Windows disabled 条件;后台路径同时依赖已装配的 jobs 与 tool-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.shell、ctx.subprocess、ctx.sandbox、ctx.sandboxPolicy、ctx.jobs 和 ctx.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/call 与 tool/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 与可信 dshEnv,run() 不在执行深处继续猜默认值。下面只保留默认化主线:
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 的硬依赖是 tools、shell、systemPrompt、shellEnv。它显式向两个 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' }) 的真实主线是:
- AgentLoop 解析模型调用并 append durable
tool/call; - Tool Runtime 查找当前 Agent 可见的
bash,复制并冻结参数; tools/pre-execute、monotonic guards 与tools/executewrappers 决定能否进入 body;defineTool校验输入 Schema,tool-bash再校验 Bash 专属字段,解析 workspace、环境与 Sandbox Policy;- 只有显式 widening 才在 Consumer body 内请求 Approval;
ctx.shell.resolve()产生完整 Spec;SandboxBashExecutor包装 argv,LocalBashExecutor调用ctx.subprocess.spawn();- Subprocess 收集有限尾部输出,必要时写 spill,并在 Abort 时终止进程树;
- Consumer 返回 canonical output,Tool Runtime 校验输出 Schema、render、执行 post/finalize 并发送 live
tools/result; - AgentLoop 按模型原始顺序 append durable
tool/result。
非零退出与前台 timeout 都是正常 Tool 结果,文本中附 exit/timeout marker;基础设施失败、调用取消或无效输出才进入结构化错误。Tool body 可以并发,Session result 仍按模型顺序提交,避免 Provider transcript 中 call/result 错配。
后台调用与所有权转移
run_in_background: true 在 tool-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_output、job_list、job_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 才能提权”是模型行为指令,不是内核搜索历史后证明的不变量。
案例结论:替换能力,不制造第二个世界
这套源码链证明了六点:
- Loader entry id 负责装配,业务代码通过 Service 与 Registry 连接;
- Consumer 定义模型协议,Provider 定义执行方式,Policy 定义允许范围;
bash-sandbox可以继承bash-local的机制,但 Consumer 不依赖具体类;- Tool Runtime 负责策略、校验与规范化,AgentLoop 负责 durable ordering;
- Registration、live resource 与 durable fact 拥有不同 Owner;
- 替换 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 仍读本地目录,会让模型同时看见两个互相矛盾的文件系统。
结论:六层怎样合成“可组合、可恢复”
从上到下重新串起来:
- Surfaces 把同一内核投影成 CLI、Web、SDK、ACP;
- Composition 用 Profile/Bundle/Patch 选择整个产品,用 Preset/Scope 选择单个 Agent;
- Microkernel 用 Service、Registry、Event、Effect 管理插件的依赖、贡献与 quiescent teardown;
- Agent Runtime 用稳定 Loop 推进 Turn/Step,把策略留给插件,把关键事实交给 Session;
- Capabilities 用 Definition/Provider/Consumer 替换本机、Sandbox、E2B 与第三方世界;
- 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 |
一个插件改动前的检查表
- 它贡献 Service、Provider、Consumer、Policy、UI 还是协议?
inject是否只声明真正必需的 Service?- 注册是否通过 effect,registry 是否返回 disposer?
- 进程、Worker、Watcher、Socket、Job 或 Activation 由谁持有?
- Dispose 是否发 cancel 后继续等待 quiescence?
- Model-visible 输入是否进入 durable event 或内容寻址引用?
- 配置错误能否在 load 或最早可解析点 fail loud?
- 跨 wire/process/worker/file/durable 边界是否做运行时校验?
- Consumer 是否只依赖 Definition,而没有 import 具体 Provider?
- 新能力是否真的需要独立 Service/包,还是一个 effect-scoped contribution 就够?
关键源码索引
- Boot/Composition:
apps/cli/src/profile-boot.ts、packages/boot/app-boot/src/index.ts、packages/bundle/base/cordis.patch.yml - Agent Runtime:
packages/core/agent-loop/src/agent.ts、packages/core/tools/src/index.ts、packages/core/session/src/index.ts - Persistence/Projection:
packages/session/session-persistence/src/index.ts、packages/session/session-projection/src/index.ts - Shell/FS/Sandbox:
packages/shell/shell/src/index.ts、packages/subprocess/subprocess/src/index.ts、packages/fs/fs/src/index.ts、packages/sandbox/sandbox/src/index.ts - Web/Client:
packages/client/web/src/boot.tsx、packages/client/modules/src/index.ts、packages/client/runtime/src/client/index.ts、packages/client/ui-slots/src/index.ts - Typert/API:
packages/typert/generator/src/analyzer.ts、packages/api/gateway/src/index.ts、packages/api/gateway/src/client/index.ts - Subagent/Workflow:
packages/subagent/subagent/src/continuation.ts、packages/workflow/workflow-worker-thread/src/host.ts
最终掌握这套架构,可以归结为三个检查:行为由哪个插件/Service 提供;模型看到的内容怎样重建;资源与副作用由谁拥有并收敛。