源码解析 · 插件化 harness · v0.1.0-rc.5 @ 47f9438

一切皆插件
连循环也是

DeepSeek 官方 harness 把「可替换」贯彻到了没有特权内核:模型适配器、工具注册表、会话日志、乃至 agent 循环本身都是插件。241 个包、73 个服务键、47 个工具,靠 Cordis 的服务仓库 + 可逆 effect 连成一棵可打补丁的树。本文含 241 包逐个名册与协作图谱,全文标注 文件:行号。

241
workspace 包
73
ctx 服务键
218
包依赖 invariants
686
篇设计决策文档

分析对象deepseek-ai/deepseek-harness(产品名 dsh
基线v0.1.0-rc.5 @ commit 47f9438Merge pull request #2519 from deepseek-harness/feat/npm-public,2026-08-13)
许可:MIT(LICENSE)· 第三方依赖披露在 THIRD_PARTY_NOTICES.md
代码规模497 489 行 TypeScript(2 319 文件)+ 66 633 行 TSX(259 文件)+ 4 286 行 Python;241 个 workspace 包
服务与工具面73 个 ctx 服务键 · 47 个模型可见工具 · 28 个 verify-* 门禁 · 686 篇设计决策文档
产品一句话:DeepSeek 官方开源的 agent harness,架构是「everything is a plugin」——模型适配器、工具注册表、会话日志、乃至 agent 循环本身都是插件,全部可从配置替换。
读者对象:读过本系列任意一份编程 Agent 分析的工程师与架构师。
叙事方式:五段式「动机 → 约束 → 被否方案 → 选择 → 代价」。全文事实标注 文件:行号,可回源码核对。
本地基线路径参考项目/deepseek-harness @ 47f9438
配套教程从零构建插件化 Agent Harness(20 步造一个 minidsh)


Part 1

目录

Part I · 起点:DeepSeek 为什么要自己造 harness
1. 痛点与洞察:模型公司缺的不是模型,是承载模型的骨架
2. 动机证据:README / AGENTS.md / 论文里的原话
3. 宏观技术栈:241 个包的数字形状与依赖方向

Part II · 宪法:五个观念与三层秩序
4. 开发者思维导图:从「一切皆插件」推出全部设计
5. Cordis 五观念:空间可替换 × 时间可撤销
6. capability seam:三角色缺一不成缝
7. 三个事件域与四种 dispatch:把「能不能改」写进类型

Part III · 主轴:循环、日志、工具
8. turn 与 step:一个循环,两级边界
9. inbox 与 agent/turn-stopping:不改循环让循环继续
10. 「模型可见 ⟺ 已记录」:把宪法变成运行时断言
11. 工具流水线:三道 waterfall + 单调守卫 + 冻结结果
12. 系统提示与 scope:每步重装配、逐 agent 隔离

Part IV · 上下文经济学与任务治理
13. 四个服务键管一件事:compaction / spill / pruner / tokenMeter
14. goal / plan / todo / schedule / jobs:把「别自由心证」做成插件
15. subagent 11 包:从子进程到「别家产品的一轮」
16. skill / hooks / mcp:兼容别人的生态

Part V · 执行世界与远程面
17. 一个执行世界:换两个 provider 搬走五种能力
18. 沙箱:四平台 confine(argv) 与「隔离强度可查询」
19. typert:类型图驱动的 RPC,与 11 个包的野心
20. 四种前端一套行为:CLI / Web / ACP / SDK

Part VI · 241 个包全名册与协作图谱
21. 名册读法与六大功能域
22. 域一 · 主轴(30 包)与域二 · 上下文经济(11 包)
23. 域三 · 执行世界(40 包)与域四 · 任务治理(39 包)
24. 域五 · 装配与宿主(51 包)与域六 · 前端与支撑(70 包)
25. 协作图谱:入度、装配链路、一次 turn 的插件参与表

Part VII · 工程制度:本系列见过的最强一套
26. 逐文件 100% 覆盖率:把覆盖率当删码工具
27. 28 个 verify-*:把口头约定焊成机器门禁
28. 686 篇 Agent Notes:设计决策考古学的制度化
29. 动态 Cordis:agent 挂载自己的插件

Part VIII · 品味与边界
30. 十六项决策五段式复盘
31. 横向对比:harness 型 vs 循环型
32. 诚实边界
33. 源码导览索引与本地复现


Part I起点:DeepSeek 为什么要自己造 harness
Part 2

第 1 章 痛点与洞察:模型公司缺的不是模型,是承载模型的骨架

1.1 先回答「为什么」

一家模型公司开源一个 agent 框架,动机和一家应用公司完全不同。应用公司要的是产品;模型公司要的是让模型的能力被完整释放的载体,并且这个载体得能被外界改造成任何形状。

DeepSeek Harness 的 README 只有 55 行,但第一句话就把架构立场说完了(README.md:7):

It uses an architecture where everything is a plugin, and is powered by Cordis, whose design is described in A Programming Paradigm for Spatiotemporal Composability.

三个信息量极大的点:

  1. 一切皆插件——不是"支持插件",是"只有插件"。
  2. 底座是 Cordis,一个独立的、有论文的插件框架,被 vendored 进仓库。
  3. 论文标题里的 spatiotemporal composability(时空可组合性)——落到工程上就是:空间上服务可按键替换,时间上注册可逆序撤销。

1.2 它要解决的具体痛点

本系列分析过的 20 多个 agent 项目,绝大多数是这个形状:一个主循环,加一堆围着主循环长出来的功能。这个形状有一个共同的病:

每一个新需求都要回来改主循环。

权限确认插在工具执行前、上下文压缩插在 while 里、换模型厂商改适配函数、支持第二个前端复制一份循环。三个月后主循环变成一千行泥球,而且任何外部贡献者都无法在不改它的前提下扩展

dsh 的洞察是:先造一个没有特权内核的运行时,再把循环本身做成插件。

docs/architecture.md:13 那句话是整篇架构文档的胆:

There is no privileged core to patch: you extend dsh by mounting a plugin beside the others, and registrations are effects that unwind when their plugin unloads.

「没有特权内核可打补丁」——你扩展 dsh 的方式是在其它插件旁边挂一个新的,而不是往中心塞代码。

1.3 三个可验证的工程主张

主张 怎么验证 结果
循环本身是插件 agent-loop 是不是一个包? 是。packages/core/agent-loop,通过 ctx.agentLoop 注册(src/index.ts),可被配置替换
配置就是架构 能不能打印实际启动的树并替换任意一行? 能。dsh --profile web --dump-config;文档承诺 "Any row it prints can be replaced by a patch of your own"(architecture.md:39
模型看到的一切可重建 有没有运行时断言? 有。ctx.invariants 服务 + 218 个包的 ./invariant 伴生插件(docs/subsystems/invariants.md

1.4 与本系列前作的关系

mindmap
  root((Agent 架构的三种形态))
    循环型
      Claude Code
      Codex
      Cline
      特征:主循环是中心
功能围着它长 宿主型 Open Design 特征:不写循环
把别人的 CLI 当引擎 harness 型 DeepSeek Harness 特征:没有中心
循环也是插件 Reasonix 部分接近
但仍是单循环

同一家公司的两个开源 agent 恰好走了两条路:Reasonix 是"单静态二进制 + 缓存优先 + 传输无关控制器",追求省钱与可移植;Harness 是"241 包 monorepo + 一切皆插件",追求可替换与可扩展。这个对照本身就很有意思——第 31 章会展开。


Part 3

第 2 章 动机证据:README / AGENTS.md / 论文里的原话

2.1 证据表

证据 出处 它证明了什么
「everything is a plugin」 README.md:7 架构立场是第一句话,不是特性列表里的一条
「powered by Cordis … Spatiotemporal Composability」 README.md:7 底座有独立论文,不是自研玩具
「There is no privileged core to patch」 docs/architecture.md:13 没有中心可以塞代码
「Every part of the product is a plugin, including the model adapter, the tool registry, the session log, and the agent loop itself docs/architecture.md:11 循环也是插件,写在架构文档正文
Plugins, not loop changes: changing agent-loop requires updating docs/architecture.md」 AGENTS.md 改循环的门槛被抬到"要改架构文档"
Model-visible ⟺ logged AGENTS.md 可观测性是宪法级约束
Registrations are effects AGENTS.md 可撤销性是宪法级约束
「A capability seam comprises Service Definition / Service Provider / Consumer roles. It is complete, never one role」 AGENTS.md 能力的最小单位是三角色,不是一个类
Trust TypeScript at typed same-process boundaries. Do not add runtime validation … solely for values the static interface requires」 AGENTS.md 反过度防御——很少见的规则
「per-file 100% on packages/*/*/src AGENTS.md / docs/testing.md:10 覆盖率门禁是逐文件满格
We are DeepSeek — do not ration real-API tests. docs/testing.md:19 测试策略带着公司身份的自觉
「Developer preview … THERE WILL BE COMPATIBILITY-BREAKING CHANGES README.md:13 大写警告,且宪法有"pre-release stance"专章

2.2 一条罕见的规则:拒绝过度防御

大多数工程规范教你"多做校验"。dsh 反过来(AGENTS.md):

Trust TypeScript at typed same-process boundaries. Do not add runtime validation, fallback behavior, or hostile-input tests solely for values the static interface requires; validate at parser/config, queued, model/tool JSON, durable/file, worker, process, and wire boundaries.

它列出了七个必须校验的边界(解析器/配置、队列、模型与工具 JSON、持久化/文件、worker、进程、线缆),并明确说其它地方不要加。

理由是逐文件 100% 覆盖率的连锁反应:你加一个不可能触发的防御分支,就得为它写一个不可能的测试,或者写一条豁免注释。所以"过度防御"在这套制度下有直接成本。

这是一条我在本系列里没见过第二家写下来的规则。它说明这个仓库的规范不是抄来的最佳实践清单,而是从自己的门禁体系里长出来的。

2.3 「pre-release stance」:把技术债策略写进宪法

AGENTS.md 有一节标题叫 "Pre-release stance: foundation over blast radius",第一句是:

Remove this section at the first tagged release. With no external consumers, prefer the correct foundation over compatibility shims: rename or repackage freely and update every reference together. Backends reject old on-disk formats.

它甚至给自己写了删除条件——第一个正式 tag 时删掉这一节

这种"临时规则带自毁指令"的写法,比"我们以后会重构"的口头承诺有效得多。配套的还有 SESSION_FORMAT_VERSION 保持 0明确不做兼容承诺


Part 4

第 3 章 宏观技术栈:241 个包的数字形状与依赖方向

3.1 顶层形状

deepseek-harness/
├── vendor/          # 9 包 · 6 584 行 —— pinned 的 Cordis 源码副本(manifest 带上游 SHA)
├── packages/        # 219 包 —— @deepseek-ai/dsh-<name>,按 group/pkg 两级组织
│   ├── core/        #   8 包 · 40 854 行 —— session · system-prompt · tools · agent · agent-loop · scope
│   ├── client/      #  39 包 · 138 163 行 —— Web 客户端模块(全仓最大组)
│   ├── subagent/    #  11 包 · 24 555 行 —— 子智能体缝 + 多种 provider
│   ├── host/        #   8 包 · 22 513 行 —— 宿主能力
│   ├── session/     #  13 包 · 21 520 行 —— 日志持久化/投影/标题/遥测
│   ├── llm/         #   5 包 · 20 502 行 —— 模型缝 + DeepSeek providers
│   ├── extensions/  #   4 包 · 20 305 行
│   ├── typert/      #  11 包 · 15 404 行 —— 类型图生成器/加载器/运行时注册表
│   ├── test-support/#   6 包 · 13 391 行 —— 测试基础设施(自成一等公民)
│   ├── fs/ shell/ sandbox/ terminal/ lsp/ … 执行世界 40 包 · 62 000+ 行
│   └── goal/ plan/ todo/ schedule/ jobs/ guard/ … 治理 39 包
├── apps/            #   2 包 · 23 928 行 —— cli(拥有 dsh bin)+ web
├── native/          #   4 包 · 343 行 —— Landlock 原生启动器(按平台分包)
├── python/          #   4 286 行 —— Python SDK + bundled runtime
├── examples/        # 可运行的 cordis.yml 叶子
├── docs/            # 40+ 子系统文档,全部双语,其中 8 177 行是生成的
├── scripts/         # 124 个 .ts —— 生成器 + 28 个 verify-* 门禁
├── .agents/         # 686 篇 Agent Notes + 11 个 skill
└── AGENTS.md        # 架构宪法(CLAUDE.md 是它的符号链接)

3.2 微观技术栈:一次请求穿过的层

宏观说完,看微观。一次模型请求实际穿过的技术层:

flowchart TB
    subgraph L1["① 装配层(启动期,YAML → 插件树)"]
        A1["profile(web/headless)"] --> A2["bundle 有序叠层"]
        A2 --> A3["cordis.patch.yml × 3 级 + --patch"]
        A3 --> A4["Loader:!!js 表达式插值"]
    end
    subgraph L2["② 框架层(Cordis)"]
        B1["Context:服务仓库"] --> B2["inject:不动点解析加载顺序"]
        B2 --> B3["Typed Events:emit/waterfall/parallel/serial"]
        B3 --> B4["effect:可逆注册 + 逆序拆卸"]
    end
    subgraph L3["③ 主轴层(core 8 包)"]
        C1["ctx.sessions:append-only 日志"]
        C2["ctx.systemPrompt:有序段 + 工具 schema"]
        C3["ctx.tools:注册表 + 五段流水线"]
        C4["ctx.agents / ctx.agentLoop:驱动"]
        C5["ctx.scope:逐 agent 注册隔离"]
    end
    subgraph L4["④ 能力层(seam × N,每个三角色)"]
        D1["ctx.llm"] & D2["ctx.fs / subprocess"] & D3["ctx.shell / terminals / lsp"]
        D4["ctx.sandbox"] & D5["ctx.subagents"] & D6["ctx.compaction / spillStore"]
    end
    subgraph L5["⑤ 前端层(一套行为四种壳)"]
        E1["apps/cli"] & E2["apps/web + client 39 包"] & E3["ACP 自动化"] & E4["JSON-RPC SDK(TS/Python)"]
    end
    L1 --> L2 --> L3 --> L4
    L3 --> L5
    L4 -.被 L3 注入.-> L3

五层的分工非常干净:装配层决定有什么,框架层决定怎么连,主轴层决定怎么跑,能力层决定能做什么,前端层决定谁在看。

3.3 依赖入度:真正的重心在哪

我对 241 个包做了一次全量普查(脚本见第 33 章),统计 package.json 的包间依赖与 inject 声明。入度最高的包:

被依赖最多的包 次数 说明
dsh-invariants 218 241 个包里 90% 都依赖它——因为每个包都必须发布一个 ./invariant 伴生插件
schemastery 110 vendored 配置校验(Config 字段的运行时 schema)
dsh-session 84 会话日志是真相,谁都要读
dsh-llm 82 消息/流式词汇表
dsh-agent 61 Agent 接口与 inbox
dsh-tools 47 工具注册表
dsh-brand 30 branded id(宪法要求跨边界 id 不许是裸 string
dsh-system-prompt 30 提示词注册表
dsh-timeout 23 超时策略

dsh-invariants 218 次入度是整份分析里最有信息量的一个数字。它意味着"运行时不变量"不是某个团队的可选实践,而是包的出生证明的一部分。第 27 章会讲这套制度怎么运作。

服务侧的入度(inject 声明):

被 inject 最多的服务 次数
tools 31
systemPrompt 20
sessions 17
agents 13
subagents 12
llm 9
subprocess 8

tools 31 次、systemPrompt 20 次说明这两个是最大的汇聚点——半个仓库都在往工具表和提示词里贡献东西。这正是它们必须是注册表而不是常量的原因。

3.4 技术栈一句话

  • 语言:TypeScript(ESM everywhere,strict: true + noImplicitAny,每个残留 any 必须解释为什么无法收窄)
  • 包管理:pnpm 11.7 workspaces,node ^22.19 || >=24
  • 框架:vendored Cordis(插件/服务/事件/effect)
  • 构建:tsc 出 lib/types + tsdown 打运行时 bundle;源码面与产物面严格不混
  • 测试:vitest × 6 层(单元 / 逐文件 100% 覆盖 / 真实 API e2e / keyless 快照 / 浏览器快照 / 门禁集合)
  • 前端apps/web + client 39 包(138 163 行,含 UI slots / primitives / locale / connection 等分层)
  • 原生:Landlock 启动器是 C11 静态可执行文件(native/landlock-run/packages/entry/src/main.c),不是 N-API addon;npm 名带 node-addon 只是历史命名。Linux 上 sandbox-local 先试 bwrap 再 fallback Landlock;darwin 用 Seatbelt/sandbox-exec;win32 用 windows-acl 受限 token(sandbox-local/src/index.tsPLATFORM_CHAINS
  • 协议面:ACP(Agent Client Protocol)、JSON-RPC SDK(TS + Python)、Typert RPC 网关

Part II宪法:五个观念与三层秩序
Part 5

第 4 章 开发者思维导图:从「一切皆插件」推出全部设计

整个仓库的设计可以从一句话推导出来。这张图是全文的骨架:

mindmap
  root((everything
is a plugin)) 没有特权内核 Context 是服务仓库 73 个 ctx 服务键 循环也是插件
改它要改架构文档 空间可替换 capability seam 三角色 Definition 只声明接口 Provider 可换 Consumer 不知道底下是谁 换两个 provider
搬走五种能力 时间可撤销 registrations are effects 每个 register 返回 disposer 逆序拆卸 HMR + 逐会话装配 配置即架构 profile 叠 bundle 三级 patch + CLI overlay dump-config 每行可换 一个 117 行的包
实质是一个 YAML 真相唯一 append-only 会话日志 deriveMessages 投影 模型可见 ⟺ 已记录 218 包的 invariant 伴生 能改与只能看分开 waterfall 可改写 emit + Readonly + 冻结 单调守卫只能拒 dispatch mode 是公共契约 纪律机械化 逐文件 100% 覆盖 28 个 verify 门禁 686 篇 Agent Notes 8177 行生成文档

七个分支,每一个都是从"没有中心"这个约束倒推出来的必要条件:

  • 没有中心 → 使用者不能 import 实现 → 需要服务仓库
  • 服务可替换 → 需要接口与实现分离 → 需要 seam 三角色
  • 插件可来去 → 注册必须可撤销 → 需要 effect
  • 装配决定行为 → 装配必须可检查可覆盖 → 需要分层 patch + dump-config
  • 没有中心持有状态 → 状态必须在一个共享真相里 → 需要 append-only 日志
  • 任何人都能挂 listener → 安全边界不能靠 listener → 需要单调守卫
  • 规则太多且协作者含 AI → 口头约定必然腐化 → 需要机器门禁

Part 6

第 5 章 Cordis 五观念:空间可替换 × 时间可撤销

5.1 五句话

docs/cordis-primer.md:9-13 用五句话定义了整个框架:

# 观念 原文关键句
1 插件是实现 Service 的对象 "a function with optional inject and apply(ctx) fields, or a Service subclass whose lifecycle Cordis mounts into the current context"
2 context 是服务仓库 "other plugins find services via key instead of importing a concrete implementation"
3 inject 声明服务依赖 "load order is expressed through service requirements rather than manual boot sequencing"
4 Typed Events 通信 "declare event names through TypeScript declaration merging, then dispatch them as emit, waterfall, parallel, or serial"
5 注册是可逆 effect "installed through ctx.effect() or ctx.on() so reload and teardown unwind them predictably"

第 3 条是整个架构的转折点。"加载顺序通过服务需求表达,而不是手工启动排序"——这一句消灭了 agent 框架里最恶心的一类复杂度:启动顺序表。

5.2 服务键怎么声明:declaration merging

// packages/shell/shell/src/index.ts:40-44
declare module '@deepseek-ai/cordis' {
  interface Context {
    shell: ShellExecutor
  }
}

这一招同时拿到两件通常互斥的东西:

  • 全局类型可见:任何包里写 ctx.shell.run(...) 都有完整类型提示
  • 实现运行时注入:没有 import 任何具体实现

glossary 对 Service Definition 有一条严格约束(docs/glossary.md:9):它必须是 Cordis 的 Service(抽象类如 ShellExecutor,或具体注册表如 WebRuntime),"never a TypeScript interface"

为什么不能是 interface?因为 interface 只有类型没有运行时存在,而 seam 需要一个能被 providectx 上、能持有生命周期、能被 HMR 拆掉的运行时实体

5.3 服务重名怎么办:fail loud

packages/shell/shell/src/index.ts:16-20 的注释解释了 shell 设置命名空间为什么归 Definition 而不归任何一个 executor:

a host composes exactly one provider of ctx.shell (the win32 layer swaps the POSIX rows for the pwsh ones, and mounting both fails loud on a duplicate service registration), so the providers share one namespace without ever registering it twice, and a settings document carried between platforms keeps resolving on both.

三个收益一次拿到:Windows 层通过替换配置行而不是加分支来切 PowerShell;同时挂两个 provider 会立刻炸而不是静默取一个;跨平台带过来的设置文档在两边都能解析。

5.4 四种 dispatch 与 waterfall 语义

见第 7 章。

5.5 代价

代价 具体表现
学习曲线陡 读懂一次请求要同时理解服务仓库、inject 解析、四种 dispatch、effect 生命周期
间接层多 想知道 ctx.shell 到底是谁,得看装配配置而不是看代码
调试需要新工具 所以才有 --dump-configcordis_inspect_* 工具、docs/module-graph.md
vendored 依赖要维护 vendor/README.md 有 manifest + 同步流程 + 已记录的本地修改,每次同步要重跑 test 和 build

第 4 条值得注意:dsh 把 Cordis vendored 进仓库而不是当 npm 依赖。AGENTS.md 的 vendoring policy 规定:pinned 源码副本、manifest 带上游 SHA、通过同步流程更新、本地修改要重新应用或退役、同步后必须重跑 test && build

这是一个务实的选择:框架和产品在同一个演进节奏上,还没到能锁版本的阶段。


Part 7

第 6 章 capability seam:三角色缺一不成缝

6.1 动机与约束

动机:让"执行 bash"这类能力可以整体替换(本机 / 沙箱 / 远程 / PowerShell),而模型看到的工具契约保持不变

约束:模型看到的 schema 一旦变化,提示词缓存失效、模型行为漂移、快照测试全红。所以替换必须发生在模型看不见的层

6.2 被否方案

被否 为什么否
工具内部 if/else 选实现 每加一种实现改一次工具;工具变成实现的目录
用 TypeScript interface 当接口 没有运行时实体,无法 provide、无法持有生命周期、无法 HMR
一个包同时是接口+实现+工具 换实现要 fork 整个包,连工具一起 fork

6.3 选择:三角色

docs/architecture.md:100

A seam is a swappable capability with three roles: a Service Definition declaring the interface, a Service Provider implementing it, and a Consumer using it, commonly a model-facing tool. A package may combine roles, but one role alone is not a seam; adding a capability means designing all three.

AGENTS.md 把它升级成硬规则:

A capability seam comprises Service Definition / Service Provider / Consumer roles. It is complete, never one role; split only when roles evolve independently.

shell 是官方指定的范本(docs/glossary.md:9):

flowchart LR
    subgraph D["① Definition · dsh-shell(476 行)"]
        DD["abstract class ShellExecutor
declare ctx.shell
词汇:ExecRequest / ExecSpec / Process / RunResult
+ SHELL_SETTINGS_NAMESPACE"] end subgraph P["② Providers(四个)"] P1["bash-local"]; P2["bash-sandbox"]; P3["pwsh-local"]; P4["pwsh-sandbox"] end subgraph C["③ Consumers(三个模型可见工具)"] C1["tool-bash → bash"]; C2["tool-pwsh → pwsh"]; C3["tool-bash-persistent → bash(PTY)"] end DD -.实现.- P DD -.注入.- C C --> M["模型只看到工具名 + JSON Schema"] P --> W["ctx.subprocess / ctx.terminals"]

6.4 一个精妙细节:request/spec 分裂

AGENTS.md 指定 shell 为"显式优于隐式"的模板:

Explicit > implicit at package boundaries: defaulting is an explicit resolve(request): Spec step in the owning implementation, never a hidden ?? default inside run()(the dsh-shell request/spec split is the template).

ShellExecRequest(字段可选)与 ShellExecSpec(字段齐全)是两个类型。默认值解析是一个显式的、可被记录和断言的步骤,而不是藏在 run() 里的 ??

不这么做的代价很具体:调用方不知道实际超时是多少、日志里查不到、换 provider 后默默变了、测试断言不了。

6.5 Definition 的边界感

packages/shell/shell/src/index.ts:1-5 的模块 JSDoc:

Service Definition for the ctx.shell capability seam, covering foreground commands and background process handles. Job ids, ownership, polling, and notices belong to @deepseek-ai/dsh-jobs, keeping executors independent of sessions.

"让执行器与会话无关"——后台作业的 id、归属、轮询、通知全部归 ctx.jobs。所以同一个执行器能同时服务聊天、自动化协议、子 agent 三种场景。

这种"把不属于我的东西推出去"的边界感,在 241 个包的模块 JSDoc 里反复出现。它是这套架构能长这么大还没塌的原因。

6.6 代价

  • 一个能力至少三个包。仓库包数因此膨胀到 241。
  • 导航成本:想改"bash 超时"要先判断该改 Definition(契约)、Provider(实现)还是 Consumer(schema)。
  • 过度拆分的风险:宪法明写 "split only when roles evolve independently"(角色独立演进时才拆),并允许一个包持有多个角色(dsh-llm 同时是 Definition 和 Consumer)。

Part 8

第 7 章 三个事件域与四种 dispatch:把「能不能改」写进类型

7.1 三个事件域

docs/architecture.md:55 说得很直白:"Events are the extension points, and picking the right domain is the first decision in most changes."

语义 什么时候用
Session events 追加到日志的持久事实,通过 session/event 广播 这个事实必须活过重载
Agent eventsagent/* 携带活体 Agent:inbox / step / status / request / validation / continuation 观察或拦截在飞的工作
Capability eventsfs/*, tools/*, telemetry/* 把策略与适配器挂到 seam 上 加策略而不 import 循环

三域的完整生产者/消费者表在 docs/event-producer-consumer.md(生成的)。

7.2 四种 dispatch 模式

docs/cordis-primer.md:19-24

模式 await? 顺序 有返回值? 用途
emit 注册序 观察
waterfall 注册序 改写、拦截、策略
parallel 并行 扇出
serial 注册序 依次表态

关键的一句(:26):

The dispatch mode is part of the event's public contract. New harness events document it with an @mode tag so the generated catalog can check declarations against dispatch sites.

dispatch 模式是公共契约,用 @mode 标签写在 JSDoc 里,生成的目录会检查声明与实际 dispatch 点是否一致

7.3 技术闪光点:把「能改」和「只能看」用类型分开

看工具注册表的四个事件(packages/core/tools/src/index.ts:150,161,173,195):

/** @mode waterfall */ 'tools/pre-execute'(exec, next): Promise<PreToolDecision>
/** @mode waterfall */ 'tools/execute'(exec, next): Promise<…>
/** @mode waterfall */ 'tools/post-execute'(exec, result, next): Promise<PostToolDecision>
/** @mode emit      */ 'tools/result'(exec: Readonly<ToolExecution>, result: Readonly<ToolExecutionResult>): undefined

前三个是 waterfall(能改写),最后一个是 emit + 两个参数都 Readonly<>只能观察)。

而且它不止靠类型——还有运行时不变量(packages/core/tools/src/invariant.ts:23-25):

if (!Object.isFrozen(exec)) fail('tools/result execution must be frozen before publication')
if (…) fail('tools/result outcome and content must be frozen before publication')

结果一旦定稿就冻结,任何观察者都改不动,而且违反会被运行时抓住。

这是我在本系列里见过的对"事件系统"最讲究的处理。多数项目的 EventEmitter 里,"某个 listener 偷偷改了已定稿的结果"是一类极难调试的 bug;dsh 用 dispatch 模式 + Readonly + Object.freeze + 运行时断言四重设防把它变成不可能。

7.4 waterfall 是 around 中间件

docs/cordis-primer.md:30

ctx.waterfall is around-middleware. A listener receives (...args, next). Call next() to delegate the possibly wrapped result to the next service; return without next() to short-circuit.

配合 AGENTS.md 的大写规则:

Waterfall listeners MUST call next() to delegate; returning without it short-circuits the chain.

短路是设计的一部分:34):

For single-decision events, short-circuiting is the design. A policy listener can return without next() when it owns the decision, while a listener that only annotates or observes must delegate.

这条规则的危险性在于违反它不报错:一个只想记日志的 listener 忘了 next(),下游所有策略静默失效,而日志一切正常。所以它被顶到 AGENTS.md 并用大写 MUST。


Part III主轴:循环、日志、工具
Part 9

第 8 章 turn 与 step:一个循环,两级边界

8.1 定义

docs/architecture.md:65

A step is one model request plus the tools it calls. A turn is zero or more steps: it opens before its first input is claimed and closes once nothing is owed.

两个刻意的措辞:

  • "zero or more steps"——零 step 的 turn 合法(见 8.4)
  • "once nothing is owed"——结束条件是"不欠了",而不是"模型说完了"

8.2 完整流水线

架构文档给了一段伪代码(:67-82),我把它对照源码画成时序图:

sequenceDiagram
    participant IN as inbox
    participant D as ReactLoopAgent
    participant SP as ctx.systemPrompt
    participant L as 会话日志
    participant M as ctx.llm
    participant T as ctx.tools

    D->>L: turn/start
    loop 每个 step
        D->>IN: claim(target, turn)
        D->>SP: assemble(assembleContextFor(agent, signal))
        D->>D: agent/pre-step ⟨waterfall⟩ → reject | enter(messages)
        Note over D: reject 或首个 enter 被改空
→ 关掉零 step 的 turn(日志留痕) D->>L: step/start D->>L: user/message ×N D->>L: deriveMessages() D->>D: agent/request ⟨waterfall⟩ → provider/model/参数 D->>M: prepareCall → stream(request) M-->>L: assistant/chunk ×N(每片记 seq) D->>L: assistant/message(sourceEventSeqs 回指 chunk) alt finish = error/aborted D->>D: agent/request-error ⟨waterfall⟩ → retry? else max-tokens D->>D: 返回 max-tokens(sticky) else 无 tool-call D->>D: completed else 有 tool-call D->>T: executeToolCalls(三道 waterfall) T-->>IN: 附加上下文 → next-step 队列 end D->>L: step/end D->>D: agent/turn-stopping ⟨serial⟩ Note over D,IN: 再查 inbox.nextStep:非空则继续 end D->>L: turn/end(结构化 reason)

源码位置:外层 while (await this.turn()) {}packages/core/agent-loop/src/agent.ts:212turn():246step():332buildRequest():407

8.3 四个技术闪光点

max-tokens 是 sticky 的agent.ts:285-290):

max-tokens is sticky: once any step hits the ceiling, later steps that complete normally must not downgrade the turn outcome.

一个 turn 里第 3 步撞了输出上限、第 4 步正常结束——turn 的结论必须是"撞了上限"。因为下游(UI 提示、重试策略、评测)要据此判断结果是否可信。降级就等于撒谎。

② 每个 chunk 都进日志,且消息回指其来源agent.ts:349,381-390):

chunkSeqs.push(this.session.append('assistant/chunk', { turn, step, chunk }).seq)
// …
this.session.append('assistant/message', { turn, step, message, usage },
  { surfaceOp: 'append', sourceEventSeqs: chunkSeqs })

sourceEventSeqs 让任何一条最终消息可以回溯到拼成它的原始流式分片。调"为什么这条回答被截断"这类问题时这是救命的。

③ 请求错误是可重试的 waterfall,而不是 try/catchagent.ts:354-371):

const action = await this.dispatch.waterfall('agent/request-error',
  { turn, step, provider, failure, retryPolicy, signal }, () => Promise.resolve(undefined))
if (action?.kind !== 'retry') throw new LlmError(…)
continue    // 重试整个请求(包括重新 buildRequest)

重试策略是插件,不是循环里的常量。而且重试会重新走 buildRequest——所以重试时可以换 provider/模型。

④ 失败是结构化的agent.ts:307-314):

turnEnds = { kind: 'error', error: error instanceof LlmError
  ? error.failure                                     // LlmError 保留它的事实
  : { message: errorChain(error), code: 'UNKNOWN' } }  // 其它一切 flatten

turn/endfinally 里必发(:316-322)。永远不要让日志里出现半个 turn——否则重放逻辑要处理不完整状态,那是无底洞。

8.4 零 step 的 turn 为什么必须存在

// agent.ts:271-277
if (turnEnds && decision.messages.length === 0) break
// A removed waking message or an enter decision rewritten to empty
// still owns the initial turn boundary, but it spends no model call.
if (phase.step === 0 && decision.messages.length === 0) {
  turnEnds = { kind: 'completed' }
  return false
}

用户按了发送,但某个插件把消息拦空了。这时仍然开一个 turn 并正常关掉。architecture.md 解释(:88):

a rejected or empty first claim still closes a durable turn that spent no step, so the log records the attempt.

不这么做的后果:UI 上出现"我明明发了消息但什么都没发生",而日志里查不到任何痕迹。"用户发起过一次尝试"本身是需要被记录的事实。

8.5 一个制度痕迹:/* v8 ignore next */

agent.ts:333

/* v8 ignore next -- private callers establish the running phase before executing a step */
if (this.phase.kind !== 'running') throw new Error(`agent "${this.id}": step outside running phase`)

这是逐文件 100% 覆盖率门禁留下的痕迹:不可达的防御分支必须显式豁免并写清理由。而 scripts/coverage-exempt.ts + coverage-exempt.spec.ts 又在守着这些豁免本身。见第 26 章。


Part 10

第 9 章 inbox 与 `agent/turn-stopping`:不改循环让循环继续

9.1 一个 inbox,两个投递口

docs/architecture.md:86

Input reaches the driver through one inbox. Some messages wake it immediately; injected context waits in the inbox until another message does.

四种输入来源,两种唤醒语义(packages/core/agent-loop/src/agent.ts:122-132):

API / 来源 唤醒? 投递口
followup / 用户消息 立刻 next-turn
工具产生的附加上下文 不需要(当前 turn 内) next-step
steer 立刻 next-step
inject 不唤醒 next-step(等当前或下一次已唤醒的 step 边界领取)

injectsteer 进同一条 next-step 队列,差别只在 wake。注入不主动开 turn:idle 时只进 inbox;running 时在「已 claim 的 batch 之后」的 step 边界才会被吃到。否则每注入一句「当前分支是 main」就空开一轮。

Inbox 不是纯内存队列。先 session.append('agent/inbox/spliced', …),再改 live 投影(packages/core/agent/src/inbox.ts:157-193)。cancel / resume / fork 都能从日志重建 pending work。

循环里 target 的切换(agent.ts:261,300):

let target: InboxTarget = 'next-turn'   // turn 的第一个 step 领 turn 队列
// …一个 step 结束后…
target = 'next-step'                    // 后续 step 领 step 队列

9.2 工具怎么往 inbox 塞东西

agent.ts:395-398

const { concluded } = await executeToolCalls(
  this.loopCtx, turn, step, toolCalls, signal,
  context => this.inbox.splice('next-step', this.inbox.nextStep.length, 0, [context]),
)
return concluded ? { kind: 'completed' } : null

两个细节:

  • 第六个参数是投递回调——这是 tools/post-execute 的 "add context" 决策落地处。流水线文档描述为 "Active-batch additionalContexts FIFO → injected user/message after recorded tool results"。
  • concluded ? completed : null——"还欠不欠一次请求"由工具批次自己报告,不是循环猜的。

9.3 技术闪光点:agent/turn-stopping 的"公告—异议"模式

循环要停之前的两次检查(agent.ts:295-299):

if (turnEnds && this.inbox.nextStep.length === 0) {
  await this.dispatch.serial('agent/turn-stopping', { turn, signal })   // 发公告
  signal.throwIfAborted()
}
if (turnEnds && this.inbox.nextStep.length === 0) break                 // 再查一次

发完 serial 事件再查一次 inbox。因为 listener 完全可以在这一刻往 inbox 里塞新东西。

这是"不改循环让循环继续"的标准答案:给循环一个"我要停了"的公告时刻,任何插件都能在这一刻提出异议。

agent/turn-stoppingserial 且没有 next()architecture.md:84)——所有 listener 都会被依次 await,谁都不能短路掉别人的异议权。Listener 的标准异议方式是 steer(往 next-step 塞消息并 wake),然后二次检查 inbox 非空,turn 继续。这是「不改循环让循环继续」在同一个 turn 内的答案。

同 session 的 goal 续跑不是这条钩子goal-round-driveragent/status 进入 idle、goal 仍 armed,再 agent.followup下一个 turn(见第 14 章)。把 goal 续跑写进 turn-stopping 是错的:那会把「这一轮还欠一步」和「目标还没做完、另开一轮」混成一件事。

9.4 wake latch:拆卸不等模型轮次

agent.ts:164-193wakeDriver

if (this.phase.kind !== 'idle') {
  // Maintenance and aborted drivers cannot deliver the wake: latch it for
  // replay at convergence. Live drivers claim queued work themselves;
  // disposal never latches, so teardown waits on no model turn.
  const reason = this.phase.abort.signal.reason as AgentCancelCause | undefined
  if (reason?.kind !== 'disposed' && (this.phase.kind === 'maintenance' || wakeAfterAbort)) {
    this.phase.wakeRequested = true
  }
  return
}

Phase 是三态:idle / maintenance / runningagent.ts:38-46)。maintenance 阶段是别家少见的设计——驱动在做维护工作(如压缩、标题生成)时不接受唤醒投递,而是 latch 住等收敛。

而 disposal 从不 latchso teardown waits on no model turn(拆卸不等任何模型轮次)。这类"拆卸不能被业务逻辑拖住"的细节,是长期跑在生产里的 harness 和 demo 的分水岭。


Part 11

第 10 章 「模型可见 ⟺ 已记录」:把宪法变成运行时断言

10.1 动机

第 0 步那种 const messages = [...] 的方案里,模型输入活在内存。于是断线重连、会话分叉、审计"模型到底看到了什么"、UI 重放流式过程、评测复现——全都做不到。

10.2 选择:日志是真相,消息是投影

docs/architecture.md:94

The session log is the source of the context the model sees. deriveMessages() projects model history from it, and raw assistant/chunk events preserve replay and UI fidelity. Fork, resume, transcripts, telemetry, and persistence all derive from this stream.

方向很关键:不是"消息数组顺便记日志",而是"日志是真相,消息数组是投影"。

packages/core/session 的模块 JSDoc:

Event-sourced session service: append-only session log, in-memory store, and the derived LLM message history. Persistence is a plugin concern (subscribe to session/event, drain on session/flush).

持久化是插件关心的事——会话服务本身不碰磁盘。所以 session 组有 13 个包:日志核心 + JSONL/zstd 持久化 + 投影 + 标题生成 + 遥测 + 引用解析…

10.3 技术闪光点:不变量制度化

docs/architecture.md:96AGENTS.md

Model-visible means logged. Anything that reaches a model request must be reconstructable from the log, and a runtime invariant asserts it. This is why a new model-visible input requires a new session event: extend SessionEventMap and render from the log.

具体断言挂在 llm/stream 入口(packages/core/agent-loop/src/invariant.ts:21-52):对 agent-loop 发出的请求,把 GenerateOptions 冻结后,要求 session.deriveMessages()options.messages 的 JSON 全等,否则 fail(log-reconstruction desync)。agent/request waterfall 不能改 messages;要改历史只能 append session 事件。不这么做:插件可以 silently 改发出去的数组,live 与 replay 分叉且无断言。

不变量不是一个全局 assert 函数,而是逐包所有权的注册服务docs/subsystems/invariants.md):

// packages/runtime-diagnostics/invariants/src/index.ts:94
register(packageName: string, installer: InvariantInstaller): () => void

制度要点:

机制 细节
每包一个伴生插件 每个 workspace 包发布 ./invariant,用自己的完整 npm 包名注册
违规可归属 InvariantError,稳定 code: 'INVARIANT' + packageName,消息前缀 invariant violated by "<package>": …——注册表不 import 任何产品包
子 fiber 隔离 installer 在专属 Cordis 子 fiber 里跑,installer.inject 声明该 fiber 能访问哪些服务
名字预留 即使过滤器关闭了检查,包名仍被预留——两个插件永不可能静默抢同一个名字
原子失败 installer 失败则子 fiber 被 dispose 且预留被释放,两者原子
配置 fail loud 正则白/黑名单,黑名单胜过白名单;空串/带空格/重复/无效正则在服务启动时抛错而不是跳过

最讲究的是它规定了检查可以断言什么AGENTS.md):

Runtime invariants assert owned relationships. Check authoritative event streams or mutable data, not service or method presence, plugin metadata or effects, or fixed pure examples. Without a plausible relationship, an explained empty companion is correct.

不许写"检查 ctx.shell 是否存在"这种假装在检查的断言。没有可断言的真实关系时,正确做法是导出空 installer,并用以 No runtime invariant: 开头的注释解释为什么

而且这条也被机器守着:pnpm run verify-package-invariants 拒绝「生成的占位标记」「没解释的空 installer」「非空 installer 却忽略 reporter」「注册名写错」「导出/发布/依赖/bundle 接线不全」。

这就是 dsh-invariants 被 218 个包依赖的原因。 本系列 21 个项目里没有第二家这么做。

10.4 会话日志版本机制的一个精妙设计

AGENTS.md

A SessionEventMap member is required-on-read by default — builds that do not know its type refuse the log unless the event carries the envelope's ignorable: true; only structural format changes bump SESSION_FORMAT_VERSION.

默认严格:旧版本程序读到不认识的事件类型就拒绝整个日志,而不是跳过它继续。因为跳过意味着"模型可见的历史缺了一块",而那会产生静默错误的重放。

想让某个事件可被旧版本忽略,必须显式在信封上标 ignorable: true默认安全,例外显式。

10.5 代价

  • 任何"给模型加点上下文"的需求都要先扩 SessionEventMap + 加渲染,比 push 一条消息麻烦得多
  • 日志体积大(每个 chunk 都记)——所以有 JSONL + zstd 持久化和 packed-row 布局
  • 218 个包的 invariant 伴生是真实的维护负担(虽然大部分是带解释的空 installer)

Part 12

第 11 章 工具流水线:三道 waterfall + 单调守卫 + 冻结结果

11.1 动机

工具执行是 agent 里最需要插拔的地方:权限、审批、沙箱、超时、重试、指标、结果改写、UI 渲染、文件读写策略、hook 桥接…全塞进 runTool() 就是千行泥球。

docs/tool-execution-pipeline.md 的第一句话点明设计目标:

This graph shows where policy, hooks, sandboxing, filesystem guards, result rewriting, final-outcome observation, and UI rendering run without changing the loop.

11.2 完整流水线

packages/core/tools 的模块 JSDoc 概括为五段:

Tool registry, model presentation modes, and pre/guard/around/post/result execution pipeline.

flowchart TD
    A["assistant 消息含 tool-call"] --> B["session: tool/call
(执行前先记)"] B --> C["UI: presentCall(args)
待处理卡片"] B --> D["tools/pre-execute ⟨waterfall⟩
hooks · permission · sandbox"] D -->|allow| E["注册的单调守卫
deny 或弃权 · 身份受保护"] D -->|deny| X["跳过工具体"] D -->|ask| F["ctx.approval 一次性询问
缺失或无法回答 = deny"] F -->|allowed-once| E F -->|拒绝/取消/不可用| X E -->|allow| G["tools/execute ⟨waterfall · around⟩
timeout · retry · metrics"] E -->|deny| X G --> H["ToolDefinition.execute() 本体"] H --> I["fs/write-intent · fs/edit-intent 门
(仅 tool-fs 变更)"] H --> J["工具自有 session 事件
todo/write · fs/observed · hook/* · tool/code-dispatch"] H --> G G --> K["tools/post-execute ⟨waterfall⟩
accept · block · replace · add context"] X --> K K --> L["注册表外层规范化
流水线/快照抛出 → isError"] L --> M["finalizeContent
最后的 content-only 不变量"] M --> N["tools/result ⟨emit · 冻结⟩
权威结果,只能观察"] N --> O["session: tool/result
单一模型可见结果"] O --> P["additionalContexts FIFO
→ inbox.next-step"]

11.3 技术闪光点一:单调守卫

流水线里有一处看起来多余的设计:既然有 tools/pre-execute waterfall,为什么还要额外一层守卫?

因为 waterfall 有致命的灵活性:后注册的 listener 能覆盖前面的决定

// 一个能通过 code review 的"善意"插件
ctx.on('tools/pre-execute', async (exec, next) => {
  const d = await next()
  if (d.kind === 'deny' && isProbablySafe(exec)) return { kind: 'allow' }  // 提权了
  return d
})

于是 dsh 在可扩展的 waterfall 之后再跑一层守卫。流水线文档的描述:

Registered monotonic guards — deny or abstain; identity protected

只有两个选项:拒,或者不表态。没有"允许"这个返回值,所以它在数学上不可能放宽任何东西。源码 JSDoc(packages/core/tools/src/index.ts:704,1101):

A monotonic execution guard evaluated after every tools/pre-execute
Register a monotonic guard after the extensible tools/pre-execute

"after the extensible" 这个措辞很精确:可扩展的部分先跑,不可放宽的部分后跑。顺序本身就是安全设计。

waterfall 单调守卫
能改写别人的决定 不能
顺序敏感 是(后者赢) 否(任一 deny 即终局)
新插件能不小心提权 不可能
适合 策略协作、上下文补充 安全边界

11.4 技术闪光点二:流水线顺序本身是运行时契约

packages/core/tools/src/invariant.ts:94-115

if (eventName === 'tools/pre-execute') {
  if (stages.has(exec)) fail('tools/pre-execute repeated for one execution')
}
if (…) { if (stages.get(exec) !== 'pre') fail('tools/execute must follow tools/pre-execute') }
if (eventName === 'tools/post-execute') {
  if (…) fail('tools/post-execute must follow tools/pre-execute or tools/execute')
}

阶段顺序不是靠代码结构隐含保证,而是被运行时不变量显式断言。 一个写错的插件如果绕过了阶段顺序,会立刻炸并归属到它的包名。

11.5 技术闪光点三:UI 渲染意图是设计的一部分

AGENTS.md

A tool's UI render intent is part of its design, decided up front (generic/terminal/diff, locations); presentation methods are pure functions of args.

工具在定义时就要说清"我该被渲染成普通卡片、终端输出、还是 diff"。而且渲染函数必须是 args纯函数——所以待处理卡片能在执行之前就画出来(流水线图里 presentCall(args)tools/pre-execute 之前)。

11.6 47 个工具的目录是 boot 出来的

docs/tool-catalog.md(1 873 行)文件头注释:

Unlike the cordis catalog (a pure source-AST pass), this generator BOOTS each tool plugin on a real context and reads ctx.tools.schemas(), because a tool schema is not statically knowable (runtime-spread enums, concatenated descriptions, config-driven names, raw-JSON-Schema MCP tools). A completeness guard globs packages/*/tool-* and fails if any package is missing from the generator's boot manifest, so a new tool cannot be silently undocumented.

工具 schema 不是静态可知的(运行时展开的枚举、拼接的描述、配置驱动的名字、MCP 的原始 JSON-Schema),所以生成器真的启动每个工具插件去读 schema。而且有完整性守卫:新工具不可能被静默地漏掉文档

47 个模型可见工具(我的普查结果):

ask_user_question · bash · pwsh · console · edit · read · read_image · write · str_replace_editor
glob · grep · lsp · web_fetch · web_search · skill · todo_write · ralph · workflow
create_goal · get_goal · update_goal · schedule_create · schedule_delete · schedule_list
job_kill · job_list · job_output · terminal_open/read/send/close/list/signal
subagent · subagent_fork · send_message · interrupt_agent · list_agents · report
session_search · session_trace · session_event_read/search/trace
cordis_define/undefine/run/stop/inspect_list/inspect_query/inspect_self

最后那一行是第 29 章的主角:agent 可以定义、运行、检查自己的 Cordis 插件


Part 13

第 12 章 系统提示与 scope:每步重装配、逐 agent 隔离

12.1 systemPrompt 是注册表,不是模板

packages/core/system-prompt(1 582 行)的模块 JSDoc:

Registry for ordered system sections, dynamic context, tool schemas, and prompt variables.

四类贡献。被 30 个包依赖、inject 入度 20——半个仓库都要往提示词里塞东西,这正是它必须是注册表的原因。

装配发生在 每个 stepagent.ts:230):

const assembly = await this.loopCtx.systemPrompt.assemble(assembleContextFor(this, signal))

不是每个 turn 一次。因为工具集可能在 turn 中途变化:子 agent 起来了、计划模式退出了、cordis_run 挂载了新工具(第 29 章)。

assembleContextFor(this, signal) 把"哪个 agent 在问"和取消信号一起传进去——装配是 per-agent 的

12.2 scope:两级、扁平、不继承

packages/core/scope(1 215 行)的模块 JSDoc:

Scoped-context primitive: mint a Cordis context that tags registrations with an opaque identity and build routing-only event carriers for that identity.

glossary 的定义更严格(docs/glossary.md:13):

scope — the unit of per-agent registration: a contribution (tool, prompt section, variable, restriction, listener) is either global (visible to every agent) or scoped (owned by exactly one scope key). Two levels, flat: scoped registrations do not inherit down to subagents; subtree behavior is expressed with lineage data, never scope structure.

三个刻意的限制:

  1. 只有两级(全局 / 单个 scope),没有多级嵌套
  2. 不向下继承——子 agent 不自动获得父 agent 的 scoped 注册
  3. 需要子树行为时用 lineage 数据表达,绝不用 scope 结构

为什么这么克制?因为"作用域继承"是权限系统里最容易出错的地方。一旦允许继承,就要回答"子 agent 能不能覆盖父的限制""孙子怎么办""fork 之后呢"。两级扁平把这些问题全部消灭。

一个实例(docs/tool-catalog.mddsh-tool-subagent-report):

Registered per continuable in-process child rather than globally, so this schema is visible only inside such a child and survives its global toolFilter.

report 工具只在可续的进程内子 agent 里可见,而且能穿过全局工具过滤器——因为它是子 agent 向父汇报的唯一通道,不能被过滤掉。


Part IV上下文经济学与任务治理
Part 14

第 13 章 四个服务键管一件事:compaction / spill / pruner / tokenMeter

上下文管理在多数项目里是一个函数。dsh 拆成了四个独立的服务键,因为它们解决的是四个不同的问题。

13.1 问题分解

服务键 问题
ctx.compaction 对话太长:20 轮后历史超窗口 packages/compaction(4 包 · 8 032 行)
ctx.spillStore 单次输出太大:grep 匹配 3 万行 packages/spill(3 包 · 1 473 行)
ctx.toolResultPruner 工具结果需要修剪:保留结构、删冗余 在 core/tools 侧
ctx.tokenMeter 要知道花了多少:计量与预算 docs/subsystems/token-meter.md

只做第一个(压缩历史)的项目,遇到第二种情况会直接崩窗口。只做第二个(截断输出)的项目,模型永远看不到被截掉的部分。

13.2 spill:落盘 + 定位符 + 取回说明

packages/spill 的 Definition JSDoc(我的普查抓到的原文):

Service Definition for the spill storage capability seam (ctx.spillStore): an abstract service defining WHAT a spill backend does — persist a tool's oversized text and return a model-facing locator plus retrieval guidance.

三个词是关键:persist(落盘)+ locator(面向模型的定位符)+ retrieval guidance(怎么取回的说明)

不是简单截断,而是完整内容存起来、给模型一条"我把完整结果存在这里,你可以这样读"的消息。

真实用法(docs/tool-catalog.mddsh-tool-fs-search):

Capped results save the complete formatted list through the optional ctx.spillStore backend; returned locators are follow-up-readable/searchable when the backend exposes local paths in co-located deployments.

溢出的内容不是坟墓——定位符指向的东西还能被 readgrep 继续处理。

13.3 compaction 的难点不是摘要,是"算不算模型可见"

按第 10 章的宪法:压缩结果模型可见,所以它必须是一个 session event,不能是内存里的变量。

这带来三个收益:压缩可审计(看得到压了哪段、摘要是什么)、可重放(重放日志得到同样的模型输入)、可撤销(删掉那个事件就恢复原状)。

代价是压缩不能"就地改数组",实现复杂度更高。

13.4 ctx.spillStore 是 optional 的

注意上面那句 "the optional ctx.spillStore backend"。工具在没有 spill 后端时仍然工作(只是截断),有后端时自动升级为"截断 + 定位符"。

这是 seam 设计的一个次级收益:能力缺失是一种合法的部署形态,而不是启动失败。对比之下 ctx.lsp 的处理是(docs/tool-catalog.md):

Requires a registered provider (e.g. @deepseek-ai/dsh-lsp-stdio) at runtime; without one, a query returns the structured LSP_UNAVAILABLE error rather than changing the schema.

没有 provider 时返回结构化错误,而不是改变 schema。 因为改 schema 会让提示词缓存失效、让模型行为漂移。模型看到的契约必须与部署细节解耦。


Part 15

第 14 章 goal / plan / todo / schedule / jobs:把「别自由心证」做成插件

14.1 goal:event-sourced + CAS + 人类根权限 + 有界

packages/goal(4 包 · 5 786 行 · ctx.goals)的模块 JSDoc:

Same-session goal domain: event-sourced state, compare-and-set mutations, and process-local continuation activation.

工具目录里的部署说明(docs/tool-catalog.mddsh-tool-goal)信息量极大:

create_goal, get_goal, update_goalcreate, edit, pause, and resume require direct-human root authority; complete and blocked also accept the exact current goal round. The default blocked lower bound is three admitted rounds.

三个设计逐个拆:

① 创建/编辑/暂停/恢复需要"直接人类根权限"。 模型不能给自己派目标,也不能悄悄改目标。否则"把测试跑绿"会被改成"把测试注释掉"。

complete / blocked 要带上确切的当前轮次。 这是 compare-and-set:模型说"我完成了第 5 轮的目标",若实际已是第 6 轮,声明失效。防的是过期声明。

blocked 有下界:至少 3 个已准入轮次。 模型不能第一轮就说"我卡住了",必须真的试过三轮。

续跑不走 agent/turn-stoppinggoal-round-driverpackages/goal/goal-round-driver/src/index.ts:137-205)在 agent idle、goal active 且 armed、inbox 无竞争工作时,渲染 <goal_round>agent.followup。Resume 后 durable phase 仍在,但 activation 是进程内态、默认 disarmed,避免冷启动自动续跑。循环代码一行未改:driver 只消费公开的 agents / goals / sessions

14.2 plan:把计划模式做成被日志记录的状态

AGENTS.md 的仓库布局里,plan 的描述是 "plan mode as logged state"

对比 Claude Code 的 plan mode(一个会话级布尔开关 + 提示词变化),dsh 的做法是 ctx.planMode 服务 + 日志事件。工具目录里 dsh-plan-mode 的说明:

exit_plan_mode stays in the model-facing schema while planning is inactive so transitions add no tool-catalog churn on top of the plan-policy change. Its execute path rejects calls outside plan mode; in plan mode it presents the plan over the user-questions seam (approve / keep planning with feedback), and approval logs plan mode inactive at the step boundary.

三个讲究之处:

  • 工具在非计划模式下也留在 schema 里——因为进出计划模式不该改变模型看到的工具表(缓存友好 + 快照稳定)
  • 执行路径拒绝模式外的调用——schema 稳定不等于行为放宽
  • 批准后在 step 边界记录"计划模式失效"——状态变更有明确的日志时刻

14.3 schedule:版本化的定时能力

docs/tool-catalog.mddsh-schedule

Registered only inside live root Agent scopes created after the opt-in Schedule plugin loads. Version 1 accepts after_seconds, explicit absolute at, and bounded fixed-rate every_seconds, and discloses session-local delivery; management reads and mutations require the shared Session persistence barrier.

"Version 1"这个措辞值得注意——它把"这个工具的能力集"当作一个有版本的契约,而不是随意增长的参数表。every_seconds有界的(不能设成每毫秒一次)。而且明确披露投递是 session-local 的(不跨会话)。

14.4 jobs:kind-agnostic 的后台作业控制器

docs/tool-catalog.mddsh-tool-jobs

The kind-agnostic background-job controller: background bash commands, PTY sends, and subagents are read, listed, and killed through the same three tools. Loading the plugin attaches the controller that arms producers' ctx.jobs.start().

三个工具(job_list / job_output / job_kill)管所有种类的后台工作。 后台 bash、PTY 发送、子 agent 全部走同一套。

这解决了一个真实的模型体验问题:如果每种后台工作有自己的一套管理工具,模型要记 9 个工具名而不是 3 个,而且经常用错。

而且 ctx.jobs 的设计让执行器与会话解耦(回看第 6.5 节 shell Definition 的 JSDoc)——作业 id、归属、轮询、通知归 jobs,执行器只管执行。

14.5 todo:allowParallelInProgress 是必填无默认

docs/tool-catalog.mddsh-tool-todo

todo_write is session-owned state; UIs render the latest todo/write event as a checklist. allowParallelInProgress is required with no default, so the catalog states its choice: true, whose description invites several in_progress items. A deployment choosing false receives the same tool with a description asking for exactly one active task.

必填无默认是刻意的。因为"能不能同时有多个进行中任务"是一个产品语义选择,不该有隐含默认。而且这个选择会改变工具描述文本——同一个工具,两种教模型的方式。

这正是 AGENTS.md 那条 "No hardcoded tunables in plugins" 的落地:

deployment-varying choices are validated Config fields changeable from cordis.yml; a DEFAULT_* constant or test hook is not configurability.

14.6 guard:循环卫生

packages/guard 只有两个子包,没有统一入口包:repeat-tool-reminder(同一 tool+canonical args 连续 hammer 时注入提醒,从不 veto)和 timeout-policy(在 tools/execute 包一层 deadline,工具声明了 timeoutMs 却不 honor signal 时给出结构化 TOOL_TIMEOUT)。AGENTS.md 写 "loop-hygiene + tool-timeout plugins"。max-steps / token budget 不在这个组里。


Part 16

第 15 章 subagent 11 包:从子进程到「别家产品的一轮」

packages/subagent 是除 client 和 core 外最大的组:11 包 · 24 466 行

15.1 provider 的变化范围有多大

docs/architecture.md:102

Subagent providers vary just as widely behind one interface, from a fresh child agent to a delegated turn in another product.

一个接口后面,从"新起一个子 agent"到"委派给另一个产品的一轮对话"。普查里能看到 subagent-codexsubagent-claude-code 这样的包名——它把 Codex 和 Claude Code 当作 subagent provider

这与本系列 Open Design(把别人的 CLI 当引擎)、DeepTutor(把编程 CLI 当可会诊的知识库)是同一个思路的第三种形态:dsh 把它们当作 subagent seam 的 provider,因此它们自动获得了 jobs 管理、session 记录、工具流水线的全部基础设施。

15.2 三层工具设计

工具目录里 subagent 相关有四个包,分工很清楚:

工具 设计要点
tool-subagent subagent / subagent_fork 注册名是 load-time configtoolName),所以同一个包被加载多次,每个 backend 一个工具名
tool-subagent-control send_message interrupt_agent list_agents 全局注册一次的控制工具,管所有可续的后台子 agent
tool-subagent-report report per-child 注册,只在子 agent 内可见,且穿过全局 toolFilter

tool-subagent 那条部署说明特别能体现"配置即架构":

The shipped compositions load this package once per subagent backend, so the model additionally sees subagent_fork bound to the fork backend. Each instance's description, run_in_background parameter, and system-prompt policy follow its own backgroundMode and enableRunInBackground, so the two shipped schemas are not identical: subagent is continuable and defaults omitted calls to background with automatic settlement delivery, while subagent_fork stays one-shot and defaults them to foreground.

同一个包加载两次,产出两个语义不同的工具(一个可续+默认后台,一个一次性+默认前台)。这是插件化架构才能做到的事——在 main() 型架构里你得写两个工具类。

15.3 ralph:一个有意思的工作流工具

docs/tool-catalog.mddsh-tool-ralph

A fixed foreground workflow starts one fresh structured child per round; the model selects only the immutable objective and an optional round cap.

每轮起一个全新的结构化子 agent,模型只能选"不可变的目标"和轮次上限。模型不能改目标,也不能改流程——这是"把确定性骨架沉进工具"的又一个实例(对照 DeepTutor 第 14 章的同类设计)。


Part 17

第 16 章 skill / hooks / mcp:兼容别人的生态

16.1 hooks:桥接 Claude Code 与 Codex 的 hook 协议

AGENTS.md 的仓库布局里:

hooks/ Claude Code/Codex hook bridges + wire-protocol library

packages/hooks(3 包 · 4 933 行)实现了别家产品的 hook 线缆协议。这意味着为 Claude Code 写的 hook 脚本可以在 dsh 里跑。

这是一个很有战略意味的决定:降低迁移成本。用户已有的 hook 投资不作废。而且它被做成了独立的 wire-protocol 库,而不是硬编码在工具流水线里。

在流水线图里 hooks 挂在 tools/pre-execute("hooks · permission · sandbox")并产生自己的 session 事件(hook/invoked / hook/result)——hook 的调用与结果也是日志可见的

16.2 skill:注册表 + 本地实现 + catalog/loader 工具

packages/skill(4 包 · 6 242 行):provider 注册表 + 本地实现 + catalog/loader 工具。

工具目录里 dsh-tool-skill 的一行很值得注意:

skill | ctx.tools, ctx.agents, ctx.skills | tool/call, tool/result, user/message replacement catalogs via agent.inject()

技能目录是通过 agent.inject() 替换 user/message 目录来更新的——回看第 9 章,inject 不唤醒循环,所以技能目录的刷新不会触发额外的模型调用,而是等下次有真实消息时一起进去。这是 inbox 双语义设计的一个漂亮应用。

16.3 mcp:一个包 3 322 行

packages/mcp 只有 1 个包。MCP 工具的 schema 是原始 JSON-Schema(见 tool-catalog.md 生成器那段解释),所以它们无法被静态分析,必须 boot 后读取。


Part V执行世界与远程面
Part 18

第 17 章 一个执行世界:换两个 provider 搬走五种能力

17.1 架构最强的卖点

docs/architecture.md:102

Seams are why one provider swap changes the whole product. Filesystem and subprocess providers share one execution world, so pointing them at a remote sandbox moves Bash, PTY, and LSP with them, with no provider forks.

flowchart TB
    subgraph prim["两个原语 seam"]
        FS["ctx.fs"]:::p
        SP["ctx.subprocess"]:::p
    end
    subgraph built["建在原语之上(26 包 · 42 885 行)"]
        SH["ctx.shell → bash / pwsh"]
        TM["ctx.terminals → 持久 PTY"]
        LSP["ctx.lsp → 语言服务"]
        SEARCH["glob / grep → 打包的 ripgrep"]
        CR["ctx.codeRuntime → 代码模式"]
    end
    FS --> built
    SP --> built
    subgraph swap["换 provider = 换整个世界"]
        L["本机:node:fs + child_process"]
        E["远程:dsh-e2b(沙箱 FS/subprocess 适配器)"]
    end
    L -.任选其一.-> prim
    E -.任选其一.-> prim
    classDef p fill:#e8f0fe,stroke:#4285f4

17.2 论断的代码依据

关键在依赖方向:没有任何上层能力直接 import node:child_process。我在普查里验证了这点——subprocess 被 8 个包 inject。

最能说明问题的是搜索工具(docs/tool-catalog.mddsh-tool-fs-search):

glob and grep are unconditional discovery tools that spawn the packaged ripgrep binary (@vscode/ripgrep) through ctx.subprocess as ordinary foreground calls (never background jobs) — no host rg install and no shell layer.

"no shell layer":搜索不经过 shell。所以搜索能力不依赖"机器上有没有 bash",能在远程沙箱、Windows、精简容器里一致工作。

17.3 e2b 是这个论断的实证

packages/e2b(3 包 · 6 630 行),AGENTS.md 的定位:

e2b/ E2B POC: sandbox + FS/subprocess adapters

一个 POC 用 6 630 行证明架构承诺:换两个 provider,五种能力跟着搬家。

执行世界各组体量(普查数据):

行数 角色
fs 7 13 044 文件系统缝 + 策略(含 read-before-write 策略插件)
shell 9 10 405 bash/pwsh 缝 + 4 provider + 3 consumer
sandbox 4 9 040 沙箱缝 + 策略
e2b 3 6 630 远程实证
terminal 3 5 721 持久 PTY
lsp 3 5 496 语言服务
subprocess 2 4 256 进程树
code-runtime 2 3 963 代码模式运行时
合计 33 58 555 全部建在两个原语 seam 上

17.4 code-runtime:Code Mode

docs/tool-catalog.mdrun_code 那条是全文最长的部署说明,值得完整理解:

Owned by the tool registry as a reserved transport outside filterable capability layers under mode: code / mode: both. Under code it is the registry's only wire contribution; the other visible capabilities are declared in a generated SDK section in the loaded runtime's language, and a program calls them through bindings scheduled under the native concurrency contract (submission-ordered starts and policy; concurrency-safe bodies overlap up to maxParallelSubCalls) that re-enter the complete guarded tool pipeline and link each nested execution to this outer result.

拆开看:

  1. Code Mode 下 run_code 是注册表唯一的线缆贡献——模型只看到一个工具
  2. 其它能力以生成的 SDK 段出现(用运行时的语言,比如 Python/JS)
  3. 程序通过 bindings 调用它们,重新进入完整的受守卫工具流水线
  4. 每个嵌套执行链接到外层结果
  5. 并发有原生契约:提交序启动、并发安全体最多重叠 maxParallelSubCalls

这是"工具调用"到"写程序调用工具"的形态跃迁,而且没有绕过任何安全检查——嵌套调用重新走一遍守卫流水线。本系列里 Claude Code 的 code execution、Reasonix 的 use_capability 都做过类似探索,dsh 的版本在"嵌套执行也受完整流水线约束"这点上最彻底。


Part 19

第 18 章 沙箱:四平台 `confine(argv)` 与「隔离强度可查询」

18.1 沙箱不执行,只包装 argv

docs/architecture.md 的扩展点表:

Confine spawned processes | use a ctx.sandbox backend; consumers wrap argv before spawning

这个设计让沙箱能与任何 provider 组合:本机 bash、远程 subprocess、LSP 子进程,谁都能在 spawn 前包一层。如果沙箱自己负责执行,就会和执行器争夺同一个职责。

18.2 平台链不是「只有 Landlock」

sandbox-localPLATFORM_CHAINSpackages/sandbox/sandbox-local/src/index.ts:159-165):

平台 机制
Linux bwrap 优先,失败再 Landlock
darwin Seatbelt / sandbox-exec,唯一候选、不 probe
win32 ACL restricted-tokendsh-sandbox-windows-acl),enforcement 报 partial

Landlock 启动器在 native/landlock-run:本体是 C11 main.c(self-restrict-then-exec,规则集经 execve 继承;失败 exit 125、不 exec 目标)。npm 包名带 node-addon,但不是 N-API addon,而是预编译静态二进制 + TS 包装(grantArgs--ro/--rw)。macOS/Windows 没有 Landlock 移植。

pnpm-workspace.yaml

The Landlock launcher is developed with its harness consumers but keeps its native build and publication scripts under native/landlock-run.

18.3 两个独立的服务键

普查里 sandboxsandboxPolicy两个服务键。"能力"和"策略"分开——所以策略可以按部署替换而不换沙箱实现。

在工具侧能看到策略的痕迹(packages/shell/tool-bash/src/index.ts:23-25):

import { ESCALATION_TARGETS, approveEscalation, canonicalPath, validateEscalationArgs } from '@deepseek-ai/dsh-sandbox'
import type { SandboxPolicyService } from '@deepseek-ai/dsh-sandbox-policy'

ESCALATION_TARGETS / approveEscalation / validateEscalationArgs——有一套受控的提权路径,而不是"要么全禁要么全开"。

18.4 一篇值得所有沙箱作者读的事后复盘

docs/postmortem/0004-landlock-partial-notice-misclassified-child-failures.md

标题就是全部教训:Landlock 的"部分生效"通知曾把子进程自己的失败错误分类

当沙箱只能部分生效(老内核不支持某些 rule),你发出的"部分生效"提示如果把子进程自己的失败也算进去,用户会以为是沙箱问题而去关掉沙箱——一个诊断信息的分类错误,导致用户主动降低安全等级

dsh 一共只有 4 篇 postmortem,其中一篇给了沙箱的错误分类。这个选择本身说明了他们认为什么问题值得写下来。


Part 20

第 19 章 typert:类型图驱动的 RPC,与 11 个包的野心

19.1 它是什么

AGENTS.md

typert/ type graph generator, loader, and runtime registry
api/ Remote BFF assembly and Typert RPC gateway

packages/typert11 个包 · 15 404 行——比 goal、plan、todo、schedule、jobs、guard 加起来还大。这不是一个辅助工具,这是一套基础设施。

19.2 它解决什么问题

Web 前端(client 39 包 · 138 163 行)要调用后端的服务。传统做法:手写 REST/RPC 接口 + 手写类型定义 + 手动保持同步。

typert 的做法:从 TypeScript 类型生成类型图,加载器和运行时注册表据此提供端到端类型安全的 RPC

配套文档 docs/api-gateway.mddocs/subsystems/typert.md。普查里能看到 dsh-api-remotes 被 23 个包依赖——前端各模块通过它访问后端。

19.3 为什么这算"野心"

因为它意味着 dsh 不打算只做一个 CLI。api/ 的定位是 "Remote BFF assembly"(Backend For Frontend)——这是面向多客户端、多部署形态的架构。

AGENTS.md 的编译器面规则里还有一句相关的:

Keep compiler faces explicit. Each package uses one aggregate except api/remotes; repo-wide programs seed a face config, never the root solution.

api/remotes 是全仓唯一被允许有多个编译聚合的包——因为它要同时面向 host 面和 client 面。例外被显式记录,而不是默默存在。


Part 21

第 20 章 四种前端一套行为:CLI / Web / ACP / SDK

20.1 四种壳

前端 定位
CLI apps/cli(拥有 dsh bin) 交互终端 + --profile headless 一次性任务
Web apps/web + client 39 包(138 163 行) 浏览器应用,默认 127.0.0.1:3080
ACP packages/acp(1 669 行) automation-only Agent Client Protocol 服务器
SDK packages/sdk(3 包)+ python/sdk JSON-RPC 协议 + server + TS client + Python SDK

AGENTS.md 特意标注 ACP 是 "automation-only"——它不是给人用的交互界面,而是给程序用的自动化接口。

20.2 Web Client 的扩展点

docs/architecture.md 扩展点表里有一行:

Add a Web Client Chat node | register a ConversationNodeDefinition + keyed renderer

前端也是插件化的。会话里的每种节点(消息、工具卡、diff、终端输出)都是一个 ConversationNodeDefinition + 对应的 keyed renderer。

client 组 39 个包的分层(普查里的依赖入度):dsh-client-ui-slots 35 次、dsh-client-runtime 34 次、dsh-client-ui-primitives 29 次、dsh-client-locale 27 次、dsh-client-connection 19 次。UI slots 是前端的最大汇聚点,对应后端的 ctx.tools

20.3 一套行为怎么保证

快照测试矩阵docs/testing.md):

  • ACP 场景:examples/<name>/tests/snapshots/,boot 真实自动化服务器示例、重放录制会话、diff 规范化的 JSON-RPC 加上重新持久化的日志
  • headless 后端场景:examples/headless-agent 拥有内部标准事件 JSONL 快照
  • CLI 交互旅程:apps/cli/tests/snapshots/,JSONL 驱动
  • Web 浏览器旅程:apps/web/tests/snapshots/,Chromium 重放(Linux PR 必过门禁

而且有一条很讲究的规定:

One ACP scenario (text-turn) pins full system-prompt/tool-schema content; other fixtures tokenize it so an edit churns one line.

只有一个场景钉住完整的系统提示与工具 schema 内容,其它场景把它 token 化——所以改一句提示词只会让一个快照变一行,而不是几十个快照全红。

这是快照测试规模化的关键技巧,值得单独抄走。


Part VI241 个包全名册与协作图谱
Part 22

第 21 章 名册读法与六大功能域

21.1 这份名册怎么来的

我写了一个普查脚本(scripts/dsh-plugin-census.py),对 241 个包逐个提取:

字段 提取方式
包名 package.jsonname
角色 由包名前缀 + 是否声明 ctx 键 + 是否注册工具推断
拥有的 ctx 源码里 declare module '@deepseek-ai/cordis' { interface Context { … } }
注入的服务 export const inject = [...]static inject = [...]
模型可见工具 defineTool({ name: '…' })
行数 该包下所有 .ts/.tsx(排除 .d.ts
它做什么 源码顶部 @module JSDoc 的第一段原文(翻译)

最后一列是这份名册最有价值的部分:它不是我总结的,是包作者自己写的一句话职责。241 个包里 227 个有这样的 JSDoc(缺失的 14 个是 fixture、平台二进制包和 vendored Cordis 插件)。

这也侧面证明了 verify-export-jsdoc 这条门禁的效果——这个仓库的模块级文档覆盖率高到可以直接当名册用

21.2 角色分布

角色 数量 说明
Provider/插件 97 实现某个 seam,或纯策略/胶水插件
Service Definition 60 拥有至少一个 ctx
Web 客户端模块 39 client
Consumer(工具) 24 注册模型可见工具
支撑库 13 util / test-support
Bundle 3 分发层
其它 5 fixture 等

60 个 Service Definition 拥有 73 个服务键(少数包拥有多个键,如 agent-loop 同时拥有 agentLoopconfiguredAgentIdentities)。

21.3 六大功能域

52 个包组按功能重排成六域,后面三章逐域展开:

flowchart TB
    subgraph D1["域一 · 主轴(30 包 · 95 200 行)"]
        core["core 8"]; llm["llm 5"]; sess["session 13"]; sq["session-query 4"]
    end
    subgraph D2["域二 · 上下文经济(11 包 · 19 828 行)"]
        ctxg["context 4"]; comp["compaction 4"]; spill["spill 3"]
    end
    subgraph D3["域三 · 执行世界(40 包 · 64 743 行)"]
        fs["fs 7"]; sh["shell 9"]; sub["subprocess 2"]; term["terminal 3"]
        cr["code-runtime 2"]; lsp["lsp 3"]; sb["sandbox 4"]; e2b["e2b 3"]; web["web 6"]; mcp["mcp 1"]
    end
    subgraph D4["域四 · 任务治理(39 包 · 65 646 行)"]
        skill["skill 4"]; sa["subagent 11"]; wf["workflow 4"]; goal["goal 4"]
        plan["plan 1"]; todo["todo 1"]; sch["schedule 1"]; jobs["jobs 3"]
        guard["guard 2"]; inter["interaction 5"]; hooks["hooks 3"]
    end
    subgraph D5["域五 · 装配与宿主(51 包 · 71 000 行)"]
        preset["preset 2"]; bundle["bundle 3"]; boot["boot 2"]; host["host 8"]
        api["api 2"]; typert["typert 11"]; sdk["sdk 3"]; acp["acp 1"]
        misc["settings/credentials/identity/storage/
workspace/attachment/feedback/
runtime-diagnostics/extensions 19"] end subgraph D6["域六 · 前端与支撑(70 包 · 187 500 行)"] client["client 39"]; apps["apps 2"]; ex["examples 3"] ts["test-support 6"]; util["util 7"]; vendor["vendor 9"]; native["native 4"] end D2 & D3 & D4 -.注入/被注入.-> D1 D5 -.装配.-> D1 D6 -.消费.-> D5

Part 23

第 22 章 域一 · 主轴(30 包)与域二 · 上下文经济(11 包)

22.1 域一导读:五个键撑起整个产品

主轴 30 个包提供的核心服务键只有七个:sessions systemPrompt tools agents agentLoop llm + scope(库,无键)。但它们是所有其它插件的挂载面:

flowchart LR
    AL["agentLoop
驱动"] -->|注入| A["agents"] AL -->|注入| S["sessions"] AL -->|注入| SP["systemPrompt"] AL -->|注入| T["tools"] AL -->|注入| L["llm"] T -->|注入| SP S -.session/event.-> P["session 组 12 包
持久化/投影/标题/遥测"] S -.日志.-> SQ["session-query 4 包
5 个只读检索工具"] L -.adapter seam.-> PR["llm providers"]

agent-loop 注入五个服务agents llm sessions systemPrompt tools,普查数据)——这五个就是"跑一轮对话"的全部依赖。而它自己被配置挂载,可替换。

域一的三个观察:

  1. core/tools 13 743 行是单包第二大——工具注册表 + 五段流水线 + 呈现模式,复杂度集中在这里
  2. session 组 13 包把"日志"拆成核心 + 持久化 + 投影 + 标题 + 遥测 + 引用解析,因为它们的演进节奏不同
  3. session-query 4 包 12 283 行提供 5 个只读工具让模型检索自己的历史——agent 能查自己的会话日志,这是别家少见的能力

core · 产品 API 主轴(8 包 · 40,854 行)

角色 它做什么(源码 @module JSDoc) 接线 行数
tools Def Tool registry, model presentation modes, and pre/guard/around/post/result execution pipeline. 拥有 tools · 注入 systemPrompt 13,743
agent-loop Def Concrete agent-loop plugin: creates scoped ReactLoopAgents, publishes them through the agent/session registries, and owns their ordered teardown. 拥有 agentLoop, configuredAgentIdentities · 注入 agents, llm, sessions, systemPrompt, tools 12,207
session Def Event-sourced session service: append-only session log, in-memory store, and the derived LLM message history. Persistence is a plugin concern (subscribe to session/event, drain on session/flush). 拥有 sessions 8,401
agent Def Agent service: live registry, factory delegation, and process-local initiator scope. Concrete creation and driving belong to the loop. 拥有 agent, agents 3,206
system-prompt Def Registry for ordered system sections, dynamic context, tool schemas, and prompt variables. 拥有 systemPrompt 1,582
scope Prov Scoped-context primitive: mint a Cordis context that tags registrations with an opaque identity and build routing-only event carriers for that identity. 1,215
agent-default-model Def Default model selection for an Agent without a session-specific selection. 拥有 agentDefaultModel 264
agent-tool-presentation Prov Agent-plane presentation selector: the row an agent preset carries to say which form of its tools the model sees. The tool registry itself stays on the host plane — the agent loop's scheduler, the API proxy's presenters, and every tool plugin are all its consumers, so it cannot move into a preset. … 注入 tools 236

llm · 模型接入缝(5 包 · 20,502 行)

角色 它做什么(源码 @module JSDoc) 接线 行数
llm-pi-ai Prov Generic pi-ai-backed LLM adapter plugin. One plugin instance owns a dict of provider routes; a route naming an installed pi-ai provider inherits that provider's endpoint, protocol, and model catalog as defaults, and a route pi-ai does not ship is declared outright. … 注入 llm 6,972
llm Def LLM service: adapter registry with a waterfall-interceptable streaming call API. Exports the LlmRuntime default, the abstract LlmAdapter for provider backends, and BlockAssembler for chunk assembly. 拥有 llm 5,015
llm-deepseek Prov Register a {@link DeepSeekAdapter} for the deepseek-official provider route on ctx.llm, with connection facts resolved per request instead of frozen at load: the plugin layers its cordis.yml entry config under the optional llm-deepseek user-settings section (ctx.settings) and resolves the … 注入 llm 3,705
token-meter Def Single replay-aware token-meter service for request and surface pressure. 拥有 tokenMeter 2,477
llm-retry Prov Provider-routed model-request retry policy on the agent loop's request recovery extension point. Each scheduled retry is durable before its cancellable wait. 注入 agents 2,333

session · 持久会话日志(13 包 · 21,520 行)

角色 它做什么(源码 @module JSDoc) 接线 行数
session-persistence Def Durable session-persistence Service Definition (ctx.sessionPersistence). Backends store {@link SessionEvent}s as the event-sourced log and carry non-replayable {@link SessionHeader} metadata separately. 拥有 sessionPersistence 6,657
session-persistence-jsonl Prov JSONL durable session-persistence backend. It stores a header and contiguous events in one append-only file per session, and delegates orchestration to {@link PersistenceCoordinator}. Its side-effect-free locator returns the absolute per-session log target before materialization. 注入 sessions 4,570
session-title Def Log-backed session title service, deterministic fallback, and provider contract. 拥有 sessionTitle · 注入 sessions 2,339
session-persistence-sqlite Prov SQLite durable session-persistence backend. It maps each session header and event to rows, and delegates write-path orchestration to {@link PersistenceCoordinator}. It has no independent per-session artifact, so its locator returns undefined. 注入 sessions 1,667
session-telemetry Def SessionTelemetryBackend Service Definition for the DeepSeek Harness. This package owns the CAPTURE side of session-event reporting — which records exist (the chunk projection), what they carry (the logical record), when they are captured (adoption, the per-append firehose, lifecycle forwarding), liv … 拥有 sessionTelemetry 1,215
session-telemetry-otel Prov OpenTelemetry Service Provider for the DeepSeek Harness telemetry capability. Composes the OTel JS SDK as-is — a LoggerProvider with a BatchLogRecordProcessor and an OTLP/HTTP log exporter — and maps each record handed over by the capture coordinator onto logger.emit(). … 注入 sessions 1,004
session-projection Def Service Definition and drive registry for the session-projection capability seam: the merge-extensible SessionProjectionMap type table, the ProjectionDefinition state-driven computation unit contract, and the ctx.sessionProjections registry that DRIVES every registered unit forward eagerly ove … 拥有 sessionProjections 852
session-projection-cache Def Persisted projection cache (ctx.sessionProjectionCache): durable checkpoints of every registered projection unit's state, one record per session on the domain data form (session_projcache domain — the shipped json backend lands it beside workspace.json). … 拥有 sessionProjectionCache · 注入 sessionPersistence, sessionProjections, sessions, storageDomain 796
session-title-llm Prov Shared route, framing, timeout, assembly, and validation policy for model-backed session-title providers. 692
session-stats Prov Function plugin registering the sessionStats projection unit: whole-log turn/step counts and LLM/tool/first-token/decode wall times served through the session-projection seam (registry snapshot, change feed, and every projection carrier), so clients render full-session figures that paging and comp … 注入 sessionProjections 688
session-checkpoint-policy Prov Semantic durability checkpoints for model requests, top-level tool dispatch, and completed agent steps. 注入 llm, sessionPersistence, sessions, tools 559
session-title-first-prompt-llm Prov First-human-message model provider for ctx.sessionTitle. 注入 llm, sessionTitle, sessions 339
session-title-all-prompts-llm Prov All-human-messages model provider for ctx.sessionTitle. 注入 llm, sessionTitle, sessions 142

session-query · 日志检索(4 包 · 12,323 行)

角色 它做什么(源码 @module JSDoc) 接线 行数
session-query-sqlite Def Concrete session-query service with SQLite FTS5 over the live-preferred corpus. 拥有 launcherSessionQueryPath 3,936
session-query Def Service Definition for combined session-history reads, traces, filters, and full-text search. 拥有 sessionQuery · 注入 sessions 3,770
tool-session-query Tool Model-facing, workspace-authorized session-history search and read tools. 注入 sessionQuery, systemPrompt, tools · 工具 session_event_read, session_event_search, session_event_trace, session_search, session_trace 3,758
session-log-export Prov Web Session-log download command over the host endpoint owned by ApiProxy. 注入 commands 859

22.2 域二导读:四个键管上下文

见第 13 章。这一域只有 11 包 19 828 行,但它决定了长会话能不能活下来。

context 组(4 包 · 10 251 行)是"请求上下文插件"——决定什么东西以什么顺序进入一次请求。它与 systemPrompt 的分工是:systemPrompt系统提示段context运行时上下文投影(回看第 8 章 runtimeContext.project())。

context · 请求上下文装配(4 包 · 10,280 行)

角色 它做什么(源码 @module JSDoc) 接线 行数
agent-instructions Prov Workspace instruction loader for AGENTS.md-compatible files. Baseline instructions enter durable context before the first request; successful fs tool touches project nested, changed, and removed instructions into the inbox. … 6,643
time-context Prov Opt-in request clock context. Eligible steps add durable, source-attributed time readings to the request history. 注入 agents 1,549
session-reference Def Cross-session snapshot preparation. Hosts adapt mentions into structured references; this service owns exact reads, projection, budgets, and durable context. 拥有 sessionReferenceResolver · 注入 sessionQuery 1,395
tmux-context Prov Opt-in request-preparation tmux-location context. Eligible step attempts append durable, source-attributed context naming the tmux session, window, and pane this agent process runs in, plus the window's pane-tree layout. … 注入 agents 693

compaction · 上下文压缩(4 包 · 8,063 行)

角色 它做什么(源码 @module JSDoc) 接线 行数
compaction-basic Prov Basic replay-aware compaction backend. 注入 llm, sessions, tokenMeter 4,958
compaction Def Compaction Service Definition (ctx.compaction): providers decide when to compact and replace a history range with one summary node by subclassing {@link CompactionEngine}. … 拥有 compaction 1,800
compaction-tool-result-pruner Def Replay-safe, model-free tool-result pruning service. 拥有 toolResultPruner · 注入 tokenMeter 681
command-compact Prov Human-facing /compact command over the backend-independent compaction seam. 注入 commands, compaction 624

spill · 超量输出溢出(3 包 · 1,485 行)

角色 它做什么(源码 @module JSDoc) 接线 行数
spill-policy Prov The spill-policy PLUGIN: a tools/post-execute result transformer that keeps oversized plain-text tool results out of the model's context. When a final result's UTF-8 size exceeds maxInlineBytes, it saves the FULL text to a session-scoped spill artifact (ctx.spillStore) and replaces the model-f … 注入 tools 896
spill-local Prov LocalSpillStore: the host-filesystem implementation of the @deepseek-ai/dsh-spill storage seam. Persists a tool's oversized text to a private, session-scoped file (see ./store.ts for the traversal-safe naming and exclusive owner-only write) and returns a path locator plus local read/grep retri … 364
spill Def Service Definition for the spill storage capability seam (ctx.spillStore): an abstract service defining WHAT a spill backend does — persist a tool's oversized text and return a model-facing locator plus retrieval guidance — without saying HOW. … 拥有 spillStore 225
Part 24

第 23 章 域三 · 执行世界(40 包)与域四 · 任务治理(39 包)

23.1 域三导读:两个原语 + 八种能力

见第 17 章。这一域最重要的读法是看依赖方向fssubprocess 是原语,shell / terminal / lsp / code-runtime / 搜索工具都建在它们之上,sandbox 横切(包装 argv),e2b 是远程实证。

web 组(6 包 · 5 985 行)是另一个独立 seam:ctx.web 后面是可换的搜索/抓取 provider,模型只看到 web_search / web_fetch 两个稳定工具。

fs · 文件系统缝与策略(7 包 · 13,088 行)

角色 它做什么(源码 @module JSDoc) 接线 行数
tool-fs Tool Model-facing read, read_image, write, and edit tools over ctx.fs. This package owns schemas, validation, read windows, formatting, and observation events, never a concrete provider. An optional event policy supplies mutation guards; without one the tools use unconditional provider calls. 注入 fs, systemPrompt, tools · 工具 edit, read, read_image, write 3,905
tool-fs-search Tool The model-facing filesystem discovery tool suite (glob, grep) over the packaged ripgrep binary (@vscode/ripgrep). This single plugin registers both tools; the binary ships inside the npm dependency, so no system rg install and no shell layer is involved. … 注入 subprocess, systemPrompt, tools · 工具 glob, grep 3,247
fs-local Prov Host-filesystem implementation of ctx.fs. Realpath-derived target identity makes aliases share stale guards, and writes through a symlink update its target without replacing the link. 3,073
tool-str-replace-editor Tool Model-facing str_replace_editor over the Harness filesystem seam. 注入 fs, tools · 工具 str_replace_editor 1,135
fs Def Filesystem Service Definition for one execution world. Backends own stable target identity, process paths and file URIs, containment, text reads, decoding, binary rejection, and atomic mutations. … 拥有 fs 747
fs-sandbox Prov SandboxedFileSystem: the sandbox-enforcing implementation of the @deepseek-ai/dsh-fs Service Definition. It extends LocalFileSystem so all text-storage mechanics — resolve, stat, read/stream, list, the atomic write and the read-match-write edit critical section — are the local implementation's … 注入 sandboxPolicy 552
fs-observation-policy Prov Event-only filesystem observation policy; it registers no service. A weak owner/target map records every authoritative presence/absence observation, single-slot intent listeners derive guards from that state, and the provider performs the atomic freshness/no-clobber check. … 429

shell · bash/pwsh 执行缝(9 包 · 10,453 行)

角色 它做什么(源码 @module JSDoc) 接线 行数
tool-bash Tool Model-facing Consumer of the ctx.shell capability seam. Background calls register process handles with ctx.jobs; their work uses job cancellation rather than the tool-call signal after an id is returned. … 注入 shell, shellEnv, systemPrompt, tools · 工具 bash 2,081
tool-pwsh Tool Model-facing PowerShell Consumer of the ctx.shell capability seam. Intended for Windows compositions where a PowerShell executor (e.g. @deepseek-ai/dsh-pwsh-local) backs ctx.shell; the tool contract is PowerShell-dialect: native C:\... paths and $env:NAME variables. … 注入 shell, shellEnv, systemPrompt, tools · 工具 pwsh 1,900
bash-sandbox Prov Sandbox-consuming bash executor. It wraps the exact local bash argv through ctx.sandbox, inherits local process mechanics, and reports the selected mode, enforcement, and denial facts. … 1,594
tool-bash-persistent Tool Model-facing persistent bash tool over the owner-scoped PTY seam. 注入 terminals, tools · 工具 bash 1,213
pwsh-local Prov Local PowerShell Service Provider for the bash capability seam. Each command runs as pwsh -NoLogo -NoProfile -NonInteractive -Command <command> in a managed process spawned through ctx.subprocess; the executor owns command defaulting, deadlines and cause classification, the model-friendly termin … 注入 subprocess 1,083
bash-local Prov Local Service Provider for the bash capability seam over the subprocess capability seam. Public commands run as bash -c in a managed process group spawned through ctx.subprocess; subclasses may reuse the same mechanics with an explicit argv. … 注入 subprocess 831
pwsh-sandbox Prov Sandbox-consuming PowerShell executor — the pwsh twin of @deepseek-ai/dsh-bash-sandbox. It wraps the exact local pwsh argv through ctx.sandbox (which on Windows resolves to the ACL restricted-token runner chain), inherits local process mechanics, and reports the selected mode, enforcement, and d … 787
shell-env Def Tool-independent shell environment plugin: owns the ctx.shellEnv registry of trusted, per-execution DSH_* variables consumed by the model-facing shell tools (dsh-tool-bash, dsh-tool-pwsh). … 拥有 shellEnv 488
shell Def Service Definition for the ctx.shell capability seam, covering foreground commands and background process handles. Job ids, ownership, polling, and notices belong to @deepseek-ai/dsh-jobs, keeping executors independent of sessions. 拥有 shell 476

subprocess · 进程树(2 包 · 4,272 行)

角色 它做什么(源码 @module JSDoc) 接线 行数
subprocess-local Prov Local Service Provider for the subprocess capability seam. Each spawn is a detached process tree with the spec's per-stream stdio dispositions. Normal disposal terminates and joins live trees; Node's synchronous exit phase force-stops any trees the service still owns. … 3,740
subprocess Def Service Definition for the subprocess capability seam (ctx.subprocess): execution-world executable lookup, fully specified managed process trees with raw or collected stdio, and one terminal-process primitive. … 拥有 subprocess 532

terminal · 持久 PTY(3 包 · 5,741 行)

角色 它做什么(源码 @module JSDoc) 接线 行数
terminal-bash Prov Persistent shell PTY backend over the subprocess terminal primitive, shared sandbox policy, bounded output, and provider-owned session cleanup. 注入 sandboxPolicy, subprocess, terminals 3,141
tool-terminal Tool Six model-facing persistent terminal tools. Owner identity comes from the exact tool execution Agent; generic ctx.jobs owns background ids and collection. 注入 systemPrompt, terminals, tools · 工具 terminal_close, terminal_list, terminal_open, terminal_read, terminal_send, terminal_signal 1,310
terminal Def Owner-scoped persistent PTY registry. Backends own terminal mechanics while this service owns ids, publication, authorization, and awaited cleanup. 拥有 terminals 1,290

code-runtime · 代码模式运行时(2 包 · 3,982 行)

角色 它做什么(源码 @module JSDoc) 接线 行数
code-runtime-worker-thread Prov Worker-thread code runtime: a fresh worker runs each host-type-stripped TypeScript program and bridges bindings over its message port. This is containment, not a security boundary: model code has bash-equivalent trust despite an empty environment, a heap cap, measured event-loop busy-time and wall-t … 3,538
code-runtime Def Service Definition for the code-execution capability seam that runs one model-written program against host async bindings. Runtimes know nothing about tools or sessions; consumers own those concerns. 拥有 codeRuntime 444

lsp · 语言服务(3 包 · 5,528 行)

角色 它做什么(源码 @module JSDoc) 接线 行数
lsp-stdio Prov Generic stdio language-server backend for ctx.lsp. One plugin instance configures a named table of server commands and registers one isolated provider for each entry. … 注入 fs, lsp, subprocess 3,972
tool-lsp Tool Model-facing lsp tool over ctx.lsp. One read-only tool with four operations (goToDefinition/findReferences/goToImplementation/hover); it converts one-based UTF-16 cursor coordinates to the seam's zero-based positions, requires the session workspace with no fallback, caps and renders resu … 注入 lsp, systemPrompt, tools · 工具 lsp 1,025
lsp Def Service Definition for the LSP capability seam (ctx.lsp): a language-server provider registry and per-query, order-independent selection over normalized goToDefinition/findReferences/goToImplementation/ hover queries. … 拥有 lsp 531

sandbox · 沙箱与策略(4 包 · 9,089 行)

角色 它做什么(源码 @module JSDoc) 接线 行数
sandbox-windows-acl Prov Windows ACL write-restriction sandbox backend for the DeepSeek Harness sandbox seam. Mirrors the mechanism of github.com/huoyaoyuan/ windows-acl-restrict-poc @ 10e4dfb (the fixed revision): a WRITE_RESTRICTED token whose restricting SIDs include distinct workspace and temp write SIDs that this sandb … 5,889
sandbox-local Prov Local sandbox backend. It selects the platform runner chain (Linux bwrap then Landlock; macOS Seatbelt; Windows the ACL restricted-token runner), functionally probes competing candidates once, and reports each wrap's enforcement and stderr classification facts. … 1,975
sandbox Def Service Definition for the same-world process-confinement capability seam: wrap exact subprocess argv under a host-path file policy. Containers, microVMs, and remote execution replace the surrounding capability seam instead; this service shares the host kernel and filesystem. 拥有 sandbox 644
sandbox-policy Def The sandbox POLICY home (ctx.sandboxPolicy): the single owner of the deployment's sandbox fallbacks plus per-session resolution: the file-effect {@link SandboxMode}, the workspace-write root, and the override kit (the sandbox/mode event, its fold, and its write path, from ./session-mode.ts). … 拥有 sandboxPolicy 581

e2b · 远程沙箱 POC(3 包 · 6,646 行)

角色 它做什么(源码 @module JSDoc) 接线 行数
subprocess-e2b Prov E2B Service Provider for the subprocess capability seam. Each handle starts through the shared sandbox and retains command output/status paths in that remote world. 注入 e2b 4,578
fs-e2b Prov E2B provider for the filesystem capability seam. Paths, contents, and atomic staging files remain inside the shared remote sandbox. 注入 e2b 1,421
e2b Def Shared ownership of one E2B sandbox. Capability adapters await the same SDK handle, so filesystem and process operations inhabit one remote Linux world. 拥有 e2b 647

web · 搜索与抓取(6 包 · 6,010 行)

角色 它做什么(源码 @module JSDoc) 接线 行数
tool-web Tool Model-facing web_search and web_fetch tools over ctx.web. This package owns schemas, validation, prompt guidance, limits, and presentation, never concrete providers. … 注入 systemPrompt, tools, web · 工具 web_fetch, web_search 1,960
web-search-deepseek Prov Register a DeepSeek-backed provider in ctx.web. It calls the Anthropic-compatible Messages API with native web_search_20250305. The provider reuses DEEPSEEK_API_KEY but not DEEPSEEK_BASE_URL, because search and chat-completions use different bases. 注入 web 1,399
web-fetch-http Prov @deepseek-ai/dsh-web-fetch-http: registers an anonymous public HTTP(S) WebFetchProvider with ctx.web. A function/namespace plugin (NOT a default-export service): it registers INTO the seam's fetch registry, like the search providers register into the search registry. 注入 web 910
web-search-exa Prov @deepseek-ai/dsh-web-search-exa: registers an Exa-backed WebSearchProvider with ctx.web. A function/namespace plugin (NOT a default-export service): a search provider does not own the ctx.web key — it registers INTO the seam's provider registry, exactly as @deepseek-ai/dsh-llm-deepseek reg … 注入 web 593
web Def Service Definition for the web access capability seam (ctx.web): registries and provider-selecting execution for search and fetch. Duplicate ids are rejected. … 拥有 web 580
web-search-perplexity Prov @deepseek-ai/dsh-web-search-perplexity: registers a Perplexity-backed WebSearchProvider with ctx.web. A function/namespace plugin (NOT a default-export service): it registers INTO the seam's provider registry, like @deepseek-ai/dsh-llm-deepseek registers an adapter into ctx.llm. 注入 web 568

mcp · MCP 桥(1 包 · 3,333 行)

角色 它做什么(源码 @module JSDoc) 接线 行数
mcp-client Prov MCP client bridge plugin: connects to an external MCP server and registers its tools on ctx.tools under server-qualified public names (mcp__<serverName>__<rawName>). Each plugin instance connects to one MCP server; load multiple instances in cordis.yml for multiple servers. … 注入 tools 3,333

23.2 域四导读:治理插件的共同模式

39 个治理包有一个共同模式,我总结为"确定性沉进工具,判断留给模型"

沉进工具的确定性部分 留给模型的判断
goal 轮次计数、CAS 校验、blocked 下界、人类根权限 目标是否真的达成
plan 模式状态机、step 边界记录、模式外调用拒绝 计划内容
todo 会话状态、并行策略 任务分解
schedule 时间语义、有界频率、持久化屏障 什么时候该定时
jobs 作业 id、归属、轮询、终止 什么该放后台
guard 循环卫生阈值、工具超时 ——
interaction 权限档位、审批一次性语义 ——
subagent 生命周期、后台投递、settlement 委派什么给谁
workflow 工作流引擎、worker 线程 脚本内容

这套分工与本系列 MiMo Code(Goal 独立裁判)、grok-build(Goal Mode 五件套)、Reasonix(Delivery Profile 证据签收)、DeepTutor(掌握度门禁沉进工具)是同一个思想的不同实现。dsh 的特色是每一条都是独立插件,循环对它们一无所知。

skill · 技能(4 包 · 6,255 行)

角色 它做什么(源码 @module JSDoc) 接线 行数
skill-filesystem Prov Local filesystem skill provider. This package is one implementation of the ctx.skills provider registry. It discovers directory-bundle and flat Markdown skills from project, custom, and user roots, parses YAML frontmatter, and loads bodies through ctx.fs when a filesystem service is present. 注入 skills 2,401
skill Def Agent skill provider registry. This package owns the Service Definition role of the skill capability seam. Concrete providers such as @deepseek-ai/dsh-skill-filesystem decide where skills come from; this service only merges provider catalogs, resolves the winning skill for a name, and exposes the … 拥有 skills 2,172
tool-skill Tool Durable session skill catalog and model-facing skill loader tool. 注入 agents, skills, tools · 工具 skill 1,549
skill-badge Prov Bundled dsh-badge skill provider. 注入 skills 133

subagent · 子智能体缝(11 包 · 24,555 行)

角色 它做什么(源码 @module JSDoc) 接线 行数
subagent Def Service Definition for the subagent capability seam (ctx.subagents): a named-provider registry plus a capability-validating asynchronous start API. Providers establish a child before returning its run, so fulfillment is the single publication and ownership-transfer boundary. … 拥有 subagents 9,703
subagent-codex Prov Fixed Codex one-shot subagent provider. Every accepted run starts a fresh official codex app-server --stdio process in the delegating Session's workspace and publishes only after an ephemeral thread exists. 注入 subagents, subprocess 2,783
subagent-claude-code Prov Fixed Claude Code one-shot subagent provider. Every accepted run invokes the official Agent SDK in the delegating Session's workspace and places the SDK-spawned real CLI under the shared subprocess owner. 注入 subagents, subprocess 2,226
tool-subagent Tool Model-facing delegation through one configured ctx.subagents provider. Provider lifecycle controls tool registration and context-sensitive schema wording. Foreground calls always dispose the run after collection. … 注入 subagents, systemPrompt, tools 1,984
subagent-in-process-driver Prov Shared driver for in-process ONE-SHOT subagent providers. The agent factory's creation transaction owns unpublished setup and rollback; after publication the returned AgentHandle is the one quiescent lifecycle owner held by the provider's caller. … 1,940
subagent-acp Prov Out-of-process ACP subagent backend. Each child has its own process, session, model, and tools, so it shares no Cordis context and advertises no parent-enforced start capabilities; the ONE thing it reads off request.parent is the session's workspace cwd (see {@link resolveCwd}). … 注入 subagents, subprocess 1,920
tool-subagent-control Tool The globally named send_message and interrupt_agent tools: thin model-facing adapters over ctx.subagents.followup() and ctx.subagents.interrupt(). … 注入 subagents, tools · 工具 interrupt_agent, send_message 1,117
subagent-dsh-sdk Prov Out-of-process SDK subagent backend. Each child is a complete DeepSeek Harness runtime in its own process — own cordis.yml-decided composition, session, model route, and tools — driven over stdio JSON-RPC through the TypeScript SDK client, so it shares no Cordis context and advertises no parent-en … 注入 subagents 969
tool-subagent-report Tool The child-scoped report tool and its usage guidance, installed into every continuable in-process child's unpublished context. Roots, one-shot children, remote providers, and agentless executions never see the registration. 注入 subagents, systemPrompt, tools · 工具 report 734
subagent-spawn-in-process Prov The in-process SPAWN subagent backend: registers a {@link SubagentProvider} on ctx.subagents that runs each child as a fresh child {@link Agent} on the same cordis context (its own session, own system prompt, zero parent context). … 注入 subagents 692
subagent-fork-in-process Prov The in-process FORK subagent backend: registers a {@link SubagentProvider} on ctx.subagents that runs each child as a child {@link Agent} SEEDED with a prefix of the parent's session log — so the child inherits the parent's conversation context instead of starting fresh. … 注入 subagents 487

workflow · 工作流引擎(4 包 · 7,734 行)

角色 它做什么(源码 @module JSDoc) 接线 行数
workflow-worker-thread Prov Worker-thread workflow engine. Each run executes its model-written script in an escapable vm context on a fresh worker and bridges agent() calls to host subagents. … 注入 subagents 4,566
tool-workflow Tool The model-facing workflow tool: run a JavaScript orchestration script that fans out subagents, and return the script's final value. It owns the model-facing schema and run lifecycle; script parsing, execution, caps, and cancellation live behind ctx.workflowEngine (@deepseek-ai/dsh-workflow), s … 注入 systemPrompt, tools, workflowEngine 1,217
tool-ralph Tool Model-facing foreground Ralph loop over the workflow and subagent seams. A fixed script starts one fresh structured-output child per round, carrying only the immutable objective and the previous bounded handoff between them. 注入 subagents, systemPrompt, tools, workflowEngine · 工具 ralph 1,189
workflow Def Service Definition for the workflow capability seam. Service Providers execute orchestration scripts; observe-only lifecycle events never expose run control. 拥有 workflowEngine 762

goal · 同会话目标(4 包 · 5,812 行)

角色 它做什么(源码 @module JSDoc) 接线 行数
goal Def Same-session goal domain: event-sourced state, compare-and-set mutations, and process-local continuation activation. 拥有 goals · 注入 agents 2,480
goal-round-driver Prov Same-session goal-round driver over public agent, session, and goal services. 注入 agents, goals, sessions 1,761
tool-goal Tool Model-facing get_goal, create_goal, and update_goal tools over the persisted same-session goal domain. 注入 agents, goals, systemPrompt, tools · 工具 create_goal, get_goal, update_goal 1,134
command-goal Prov Human-facing /goal command over the persisted same-session goal domain. 注入 commands, goals 437

plan · 计划模式(1 包 · 2,036 行)

角色 它做什么(源码 @module JSDoc) 接线 行数
plan-mode Def Plan mode is logged per-agent collaboration state: while active, a deployment-owned guidance section is included in each model request, and exit_plan_mode presents the completed plan for user review, while the /plan off command lets a user leave directly. … 拥有 planMode · 注入 systemPrompt, tools 2,036

todo · 待办(1 包 · 1,012 行)

角色 它做什么(源码 @module JSDoc) 接线 行数
tool-todo Tool Model-facing whole-list replacement. Each call appends a todo/write snapshot to the calling agent's session; replay is last-write-wins, and UIs render from session events. A non-agent caller has no owning list and is rejected. Named exports preserve loader injection metadata. 注入 tools · 工具 todo_write 1,012

schedule · 定时(1 包 · 4,314 行)

角色 它做什么(源码 @module JSDoc) 接线 行数
schedule Tool Agent-scoped durable one-shot and fixed-rate reminders over the session event log. 注入 agents, sessionPersistence, sessions, tools · 工具 schedule_create, schedule_delete, schedule_list 4,314

jobs · 后台作业(3 包 · 3,712 行)

角色 它做什么(源码 @module JSDoc) 接线 行数
jobs-local Prov Process-local provider for the background-job capability seam (ctx.jobs). It keeps every record in memory and hands out fresh snapshots, never live state. Registrations outlive producer and controller fibers. … 1,774
tool-jobs Tool Model-facing job_output, job_list, and job_kill tools over ctx.jobs. Loading the plugin attaches the controller required by producers. It also delivers unreported completions to the owning agent: injected into a busy owner's next step, or opening a turn on an idle one under the default wake … | 注入jobs,systemPrompt,tools· 工具job_kill,job_list,job_output` 1,325
jobs Def The background-job Service Definition (ctx.jobs). It owns the contract for job ids, session-scoped access, lifecycle state, completion listeners, and owner cleanup while producers retain their execution resources. The process-local registry lives in @deepseek-ai/dsh-jobs-local. 拥有 jobs 613

guard · 循环卫生(2 包 · 1,027 行)

角色 它做什么(源码 @module JSDoc) 接线 行数
repeat-tool-reminder Prov Advisory per-agent repeat-call detector. It enriches post-execute decisions with logged model context without vetoing or rewriting calls. Configuration and chain semantics live in the package README; rationale lives in the repeat-tool-reminder Agent Note. 669
tool-call-timeout-policy Tool Cooperative tool-call timeout enforcer. A tool declares timeoutMs and promises to honor exec.signal; this wrapper arms that deadline and maps its own expiry to TOOL_TIMEOUT without racing or abandoning the tool promise. … 注入 tools 358

interaction · 审批/权限/命令/问人(5 包 · 4,225 行)

角色 它做什么(源码 @module JSDoc) 接线 行数
commands Def Plugin-owned human-command registry shared by interactive UI adapters. 拥有 commands 1,143
user-approval Def Service Definition for the approval capability seam, covering requests, cancellation, audit, and per-session policy. Missing answerers fail closed; grants apply only to the requested action. 拥有 approval 1,143
permission-presets Def User-facing permission presets over the independent sandbox-mode and approval-policy knobs. A switch records the selected preset, then writes changed knobs through their canonical setters. Execution, prompt narration, and replay keep reading their knob folds. … 拥有 permissionPresets · 注入 approval, sessions, shell 1,020
user-questions Def Service Definition for the user-questions capability seam (ctx.userQuestions): a UI-backed service for pausing an agent tool call until the human answers a question. The model- facing tool lives in @deepseek-ai/dsh-tool-ask-user; UI packages provide the single active provider. 拥有 userQuestions 465
tool-ask-user Tool Model-facing Consumer of the ctx.userQuestions capability seam. The tool pauses until a UI provider returns a human answer, then feeds that answer back into the agent loop as an ordinary tool result. 注入 tools, userQuestions · 工具 ask_user_question 454

hooks · 外部 hook 协议桥(3 包 · 4,968 行)

角色 它做什么(源码 @module JSDoc) 接线 行数
hooks-claude-code Prov Bridge for unmodified Claude Code command hooks on harness interception extension points. It supports SessionStart, prompt/tool pre/post, Stop, and subagent start/stop. … 注入 shell 1,837
hook-protocol Prov Shared, non-plugin hook protocol library: matching, command execution and decoding, restrictive outcome merging, durable event helpers, and detached run quiescence. Claude Code and Codex bridges own their distinct payloads, environment rules, matcher mode, and typed extension-point mappings. 1,702
hooks-codex Prov Bridge for unmodified Codex command hooks on harness interception points. It supports five points (SessionStart, prompt/tool pre/post, Stop), regex-only matchers, snake_case payloads without a trailing newline, no hook environment or command substitution, and no pre-tool approval or rewrite path; on … 注入 shell 1,429
Part 25

第 24 章 域五 · 装配与宿主(51 包)与域六 · 前端与支撑(70 包)

24.1 域五导读:从 YAML 到运行中的树

装配链路(第 19 步教程里画过):

flowchart LR
    PJ["package.json 的 dsh 字段
dsh.profile / dsh.bundle"] --> B["bundle 有序叠层"] B --> P1["profile 的 cordis.patch.yml"] P1 --> P2["home 级 cordis.patch.yml"] P2 --> P3["--patch CLI 覆盖层"] P3 --> LD["Loader:!!js 表达式插值
config(注入激活后)+ disabled(每次 mount)"] LD --> TREE["插件树"] TREE --> PRESET["preset:逐会话重装配
service 行需要 isolate realm"]

三个观察:

  1. dsh-base 是一个 117 行、没有运行时 API 的包——它的全部实质是一个 cordis.patch.yml。JSDoc 原话:"this module carries no runtime API"。配置即架构的字面体现。
  2. typert 11 包 15 404 行是这一域最大的投入,见第 19 章。
  3. host 组 8 包 22 412 行提供宿主能力(dshHomePath launchEnvironment cmdlineArgs directoryPicker appExit 等键)——把"进程/机器"这一层也做成了服务。

preset · 逐会话能力装配(2 包 · 3,814 行)

角色 它做什么(源码 @module JSDoc) 接线 行数
agent-presets Def Agent presets: each session composes its model-facing plugin set from one preset cordis.yml, mounted ONCE per preset under a standing scope and joined by every agent that names it. … 拥有 agentPresets · 注入 loader 3,570
persona Prov A per-agent persona as a composable row. dsh-system-prompt owns the global persona as its own config, and registers that section unconditionally — so this row is scope-only. … 注入 systemPrompt 244

bundle · 分发层(3 包 · 1,424 行)

角色 它做什么(源码 @module JSDoc) 接线 行数
web-app Bundle plus the bundle patch (cordis.patch.yml, declared by the dsh.bundle.patch manifest field). The plugin owns the browser-surface glue: it resolves the built frontend dist (workspace knowledge of this bundle, never user config), mounts the frontend-static fallback owner over it, registers the har … 注入 webServer 707
headless Bundle rides over dsh-base without Host, HTTP, or browser plugins; this runner creates one Agent through the core registry, drives the task to quiescence, flushes its Session, prints the final assistant text, and exits. 注入 agentDefaultModel, agents, sessions 600
base Bundle package's substance is cordis.patch.yml, declared by the dsh.bundle.patch manifest field and resolved by the profile composer through that field; this module carries no runtime API. 117

boot · 启动胶水(2 包 · 4,094 行)

角色 它做什么(源码 @module JSDoc) 接线 行数
app-boot Def Shared boot glue for the app bins (dsh, dsh-acp-demo): load the gitignored .env, install the fail-loud Loader guards, resolve the config path (snapshot-aware), load the optional user patch layers from the Harness home (~/.dsh), expose its path resolver to config expressions, and drive the Co … 拥有 dshHomePath 3,657
cmdline Def it boots. The launcher parses only its own flags (--profile, --patch, the config dumps) and hands everything after them to the tree verbatim through the {@link CmdlineArgs} service, so an app owns its flag family, its --help text, and its parse errors instead of the launcher knowing them. … 拥有 appExit, cmdlineArgs 437

host · 宿主能力(8 包 · 22,513 行)

角色 它做什么(源码 @module JSDoc) 接线 行数
host-apiproxy Def the ApiProxy contract (api/: types + zod schemas, browser-safe), the fetch carrier pair (fetch/: toFetchHandler on the host side, AbstractApiClient + platform subclasses on the client side), and the host-side implementation (api-proxy.ts: createApiProxy + the ApiProxyService gateway plugin providing … 拥有 apiProxy · 注入 agentDefaultModel, agents, attachments, directoryPicker, llm +6 18,430
host-directory-picker-native Prov Native backend of the directory-picker seam: registers ctx.directoryPicker with the native capability, opening one native OS chooser on the host display per pick (macOS osascript, Linux Zenity with a KDialog fallback; Windows opens the modern IFileOpenDialog in a spawned child process — a ko … 1,634
host-directory-picker-browse Prov Browse backend of the directory-picker seam: registers ctx.directoryPicker with the browse capability — one-level directory listing and child-directory creation over the host filesystem via Node's stdlib (which already carries the per-OS adaptation). … 599
host-directory-picker-auto Prov Adaptive chooser of the directory-picker seam: resolves the host's situation once at boot (bind host, SSH launch, display session, Linux chooser binary) and mounts the matching interaction — native or browse — as real Loader entries in the in-memory root tree. … 注入 loader, webServer 580
host-webserver Def server plus the webServer service (HTTP and upgrade route registries, index transform taps, and the single fallback seat for everything no route claims). Knows no harness concepts and serves no files; the composing application's frontend plugin owns dist serving through the fallback hook. … 拥有 webServer 554
host-frontend-static Prov fallback seat: serves the built frontend directory with the semantics the Web shell locked at step1 — traversal outside the dist root is 403, any miss falls back to index.html with HTTP 200 (SPA routing), unknown extensions ship as octet-stream, non-GET/HEAD is 405. … 注入 webServer 280
host-plugin-inventory Prov Read-only projection of the current Cordis Loader plugin entries. 注入 loader 230
host-directory-picker Def Service Definition for the ctx.directoryPicker capability seam: how the web-GUI host lets an operator select a workspace directory. Backends differ in interaction shape, not just mechanism, so the service exposes a discriminated capability instead of one method set: a native backend opens one OS … 拥有 directoryPicker 206

api · 远程 BFF 与 RPC 网关(2 包 · 4,431 行)

角色 它做什么(源码 @module JSDoc) 接线 行数
api-gateway Prov Live Typert Remote dispatch over Cordis Services and registered providers. Transport, request correlation, and response envelopes belong to Connection. 注入 typert 3,586
api-remotes Prov Host BFF entry and Loader shell for the Remote contribution assembly. 845

typert · 类型图运行时(11 包 · 15,404 行)

角色 它做什么(源码 @module JSDoc) 接线 行数
typert-generator Prov Public API of the Typert analyzer, compiler-independent model, and model-driven artifact emitters. Build wiring lives in the ./tsdown subpath. 10,745
typert-registry Prov Host entry for the shared Typert runtime registry. 1,504
typert-loader Prov Typert Loader integration: automatic registration for mounted plugin packages. When a loader entry mounts, this plugin resolves the entry's package.json; a package exporting ./typert has its host face imported and its TYPERT manifest registered into ctx.typert, and the registration is withdraw … 注入 loader, typert 1,167
typert-protocol Prov Remote decorators and explicit Gateway bindings backed only by private module state. Strict reflection remains a Typert compiler responsibility. 1,104
@fixture/workspace 其它 364
@fixture/host Def 拥有 aliased, defaultOnly, demo, ignoredExternal, ignoredInline, ignoredPrimitive 305
@fixture/remote-workspace 其它 78
@fixture/remote Prov 55
@fixture/client Def 拥有 clientBridge 40
@fixture/domain Prov Host-only live Agent object. 23
@fixture/write Def Service whose public annotations are intentionally absent. 拥有 writable 19

sdk · JSON-RPC 协议与客户端(3 包 · 4,492 行)

角色 它做什么(源码 @module JSDoc) 接线 行数
sdk-client Prov TypeScript client SDK for the DeepSeek Harness runtime: spawn the dsh-jsonrpc-agent runtime as a subprocess and drive agent turns over stdio JSON-RPC. DeepSeekHarness is the high-level run API; HarnessClient is the lower-level protocol client. … 1,953
sdk-jsonrpc-server Prov SDK-facing JSON-RPC plugin over stdio. An external cordis.yml decides whether to load it; see the single-executable Agent Note and package README. Stdout is reserved for protocol frames, so the tree must not load a stdout logger. … 注入 agents 1,787
sdk-protocol Prov Shared wire protocol for the DeepSeek Harness SDK runtime: the newline-delimited JSON-RPC stdio transport plus the named request, result, and notification types both wire ends speak. … 752

acp · Agent Client Protocol(1 包 · 1,669 行)

角色 它做什么(源码 @module JSDoc) 接线 行数
acp Prov Automation-only Agent Client Protocol server over JSON-RPC stdio. The bridge exposes fresh harness sessions to trusted programmatic clients. It carries prompt text, committed assistant text, cancellation, and one-shot permission decisions; presentation and human-interaction features stay with the ha … 注入 agents 1,669

settings · 用户设置(2 包 · 3,913 行)

角色 它做什么(源码 @module JSDoc) 接线 行数
settings Def Service Definition for the user-settings capability seam (ctx.settings). Providers store one raw document of per-namespace sections; plugins register a namespace schema and read the resolved value, which layers schema defaults, the registrant's composition base, and the user document section, in … 拥有 settings 2,472
settings-file Prov File-backed settings provider. One YAML or JSON document under the user's harness home carries every namespace section; external edits hot-publish through the seam, and every write re-reads the document under a cross-process writer lock before patching it as a comment-preserving leaf-level diff. 1,441

credentials · 凭据引用(2 包 · 1,831 行)

角色 它做什么(源码 @module JSDoc) 接线 行数
credentials-local Prov File-backed credentials provider over $DSH_HOME/.credentials.yaml, layered against the environment by how much each layer is trusted: ```text inherited process environment (read-only, wins) > $DSH_HOME/.credentials.yaml (provider-managed, writable) > /.env (read-only fallback) > $D … 1,454
credentials Def Service Definition for the credential-reference capability seam (ctx.credentials). Settings and composition files carry references to secrets — environment-variable names — while providers own the actual values and their storage. … 拥有 credentials 377

identity · 匿名身份(1 包 · 252 行)

角色 它做什么(源码 @module JSDoc) 接线 行数
anonymous-user-id Prov Per-harness-home anonymous user id shared by telemetry and feedback. The id is a random UUID persisted as a bare line in .anonymous-user-id inside the harness home resolved by {@link resolveDshHome} ($DSH_HOME > ~/.dsh), and never derived from the hostname, network address, git remote, or any … 252

storage · 存储域(4 包 · 3,428 行)

角色 它做什么(源码 @module JSDoc) 接线 行数
storage-domain Def Domain data form (ctx.storage.domain): schema-validated, change-emitting KV domains over storage backends. The single implementation of the domain layer — consumers depend on this package and never touch backends directly. … 拥有 storageDomain · 注入 storage 1,475
storage-sqlite Prov SQLite storage backend for the storage hub: one database file hosts every routed unit, document-per-row (key TEXT / value TEXT JSON). Registers as backend sqlite; the disposer unregisters first, then closes the medium. 注入 storage 766
storage-json Prov JSON storage backend: one human-readable file per unit under a configured root, published by atomic whole-file rewrite. Registers as backend json on the storage hub. 注入 storage 658
storage Def Storage hub (ctx.storage): a named backend registry plus mounted data-form facilities. The hub itself performs no IO — backends own media, data forms (the domain layer first) own semantics. 拥有 storage 529

workspace · 工作区注册(1 包 · 2,155 行)

角色 它做什么(源码 @module JSDoc) 接线 行数
workspace Def Workspace entity registry (ctx.workspaceRegistry): durable workspace records, stable registry order, and header-validated session membership over the domain data form. 拥有 workspaceRegistry · 注入 sessionPersistence, storageDomain 2,155

attachment · 附件(2 包 · 921 行)

角色 它做什么(源码 @module JSDoc) 接线 行数
attachment-local Prov Local durable attachment backend rooted below DSH_HOME. @module @deepseek-ai/dsh-attachment-local 745
attachment Def Durable attachment storage seam (ctx.attachments). @module @deepseek-ai/dsh-attachment 拥有 attachments 176

feedback · 消息反馈(2 包 · 2,167 行)

角色 它做什么(源码 @module JSDoc) 接线 行数
message-feedback Def Durable, lifecycle-bound feedback for finalized assistant messages. 拥有 messageFeedback · 注入 sessionPersistence, sessions, storageDomain 1,663
command-feedback Prov Session feedback event plus the human-facing /feedback producer. Recording appends one authoritative log-only event and does not start model work. The append is eager but unflushed, so acknowledgement reports that the entry is logged, not that it reached disk. 注入 commands 504

runtime-diagnostics · 运行时不变量(1 包 · 543 行)

角色 它做什么(源码 @module JSDoc) 接线 行数
invariants Def Configurable registry for package-owned runtime invariant contributions. Every workspace package registers checks from a ./invariant companion; ordinary package entrypoints stay independent of diagnostics. 拥有 invariants 543

extensions · 扩展装配(4 包 · 20,305 行)

角色 它做什么(源码 @module JSDoc) 接线 行数
cordis-client-runner Prov Dynamic-package runner plugin, node half. Pure browser-side capability: the empty apply exists so the row appears in the host cordis.yml / Loader, while the browser half ships through exports["./client"], discovered from the package.json dshClient declaration. 6,988
tool-cordis Tool Model-facing Cordis runtime/package inspection, define, run, stop, and remove tools. 注入 cordisInspect, dynamicCordisRunner, systemPrompt, tools · 工具 cordis_define, cordis_inspect_list, cordis_inspect_query, cordis_inspect_self, cordis_run, cordis_stop +1 6,280
cordis-host-runner Tool Dynamic Cordis Plugin service: immutable package definitions, one active run per Plugin, human-approved Client activation, and Host/Client invocation. 拥有 dynamicCordisRunner · 注入 tools · 工具 console 4,975
client-ui-cordis Prov Cordis dynamic-plugin card, node half. Pure UI plugin: the empty apply exists so the plugin appears in the host cordis.yml / Loader; the browser half ships via exports["./client"], discovered through the package.json dshClient declaration. 2,062

24.2 域六导读:前端 39 包与"测试基础设施是一等公民"

client 39 包 · 138 163 行是全仓最大的组,占 TS/TSX 总量的 1/4。它自己也是分层的:ui-slots(35 次入度)→ ui-primitives(29)→ runtime(34)→ connection(19)→ locale(27)。

test-support 6 包 · 13 344 行值得单独说:测试基础设施被做成了发布级的 workspace 包(如 dsh-acp-snapshot 是一个"suite factory")。多数项目的测试工具是散落在 tests/helpers/ 里的脚本;dsh 把它们当产品对待。

vendor 9 包是 pinned 的 Cordis 源码(含 cordis-plugin-loader / -hmr / -include / -group),native 4 包是 Landlock 启动器按平台分包。

client · Web 客户端模块(39 包 · 138,163 行)

角色 它做什么(源码 @module JSDoc) 接线 行数
client-ui-conversation Client Host registration for browser conversation preferences. 21,302
client-runtime Client Host loader entry for the browser runtime exported from ./client and ./loader. 16,755
client-ui-primitives Client Cordis-free React primitives styled only through --dsw-* tokens. 12,011
client-ui-trajectory Client Host loader entry for the browser-only trajectory plugin. 11,400
client-connection Client Host HTTP bridge for browser-client RPC. 注入 webServer 8,036
client-ui-settings-models Client Host loader entry for the browser implementation exported from ./client. 7,463
client-ui-tool Client Host loader entry for the browser-only Tool UI plugin. 6,335
client-ui-workspace Client Workspace picker plugin, node half. Pure UI plugin: the empty apply exists so the plugin appears in the host cordis.yml / Loader (load and lifecycle follow the host; the browser half ships via exports["./client"], discovered through the package.json dsh.client declaration). 5,828
client-ui-agent-preset Client Agent-preset surface plugin, node half. The empty apply exists so the plugin appears in the host cordis.yml / Loader; the browser half ships the General-settings row through exports["./client"], discovered from the package.json dsh.client declaration. 4,688
client-ui-directory-picker-browse Client Directory-picker browsing surface, node half. Pure UI plugin: the empty apply exists so the plugin appears in the host cordis.yml / Loader; the browser half ships via exports["./client"], discovered through the package.json dsh.client declaration. … 3,213
client-ui-commands Client Command UI plugin, node half. Pure UI plugin: the empty apply exists so the plugin appears in the host cordis.yml / Loader; the browser half ships via exports["./client"], discovered through the package.json dsh.client declaration. … 3,120
client-web-react Client React bindings for the framework-neutral slot and snapshot contracts. 3,096
client-ui-settings-plugins Client Plugins settings surface, node half. The empty apply exists so the plugin appears in the host cordis.yml / Loader; the browser half owns the section and its configurable tab through exports["./client"], discovered from the package.json dsh.client declaration. … 2,843
client-ui-input-trigger Client Slash trigger plugin, node half. Pure UI plugin: the empty apply exists so the plugin appears in the host cordis.yml / Loader; the browser half ships via exports["./client"], discovered through the package.json dsh.client declaration. 2,835
client-ui-slots Client Slot registry pure core. Owners declare slot contracts by merging into {@link SlotMap}; one register call contributes a component AND (optionally) declares child slots, a store seat, and the registrant's business face. Zero runtime dependencies (React types only). … 2,322
client-ui-theme Client Host registration for the browser theme preference and pre-plugin palette. 2,054
client-ui-message-feedback Client Message feedback surface plugin, node half. Pure UI plugin: the empty apply exists so the plugin appears in the host cordis.yml / Loader; the browser half ships via exports["./client"], discovered through the package.json dsh.client declaration. 1,914
client-ui-subagent Client Subagent reference plugin, node half. Pure UI plugin: the empty apply exists so the plugin appears in the host cordis.yml / Loader; the browser half ships via exports["./client"], discovered through the package.json dsh.client declaration. 1,759
client-ui-settings-general Client Host loader entry for the browser implementation exported from ./client. 1,688
client-ui-layout Client Host loader entry for the browser-only layout plugin. 1,560
client-ui-model-selection Client Model selection plugin, node half. Pure UI plugin: the empty apply exists so the plugin appears in the host cordis.yml / Loader; the browser half ships via exports["./client"], discovered through the package.json dsh.client declaration. 1,455
client-modules Client Node half of the client module system (dsh.client dual-face package): scans the host Loader's entries for packages declaring dsh.client, composes the window.__DSH_BOOT__ entry graph (wire single source: {@link WebBootEntry} in ./client/manifest.ts), serves /plugins/<id>/client.js and its s … 拥有 clientModules · 注入 loader, webServer 1,440
client-ui-user-questions Client Web question plugin, node half. Deliberately empty. Mounting ask_user_question here put it in the tools registry's GLOBAL layer, so every agent saw it no matter which preset composed it — a two-tool benchmark preset actually presented three, and a locally authored bash-only preset presented two. … 1,390
client-locale Client Host registration for the browser locale preference. 1,290
client-ui-permission-presets Client Permission surfaces plugin, node half. The empty apply exists so the plugin appears in the host cordis.yml / Loader; the browser half ships the new-session Settings row and current-session command picker through exports["./client"], discovered from the package.json dsh.client declaration. 1,206
client-ui-workflow-run Client Durable workflow-run UI plugin, node half. 1,176
client-ui-goal Client Goal surface plugin, node half. Pure UI plugin: the empty apply exists so the plugin appears in the host cordis.yml / Loader; the browser half ships via exports["./client"], discovered through the package.json dsh.client declaration. 1,093
client-ui-deliverables Client Deliverables plugin, node half. Registers the response-format guidance that lets the browser half recognize final-response file references. The browser half ships via exports["./client"], discovered through the package.json dsh.client declaration. 注入 systemPrompt 1,029
client-ui-attachment Client Pure React attachment atoms (zero cordis): the composer draft-image rail, the chat-history image gallery, the original-image lightbox, and the full-page drop overlay. Owners resolve every string through their own locale namespace and pass it down; nothing here reads application state. 986
client-web Client Web shell library entry. The shell's product is {@link AppWebEntry} — apps/web's vite entry runs it against #root; everything else (AppRoot gate, app-shell assembly entry, module-table staticModules, platform constants) is internal to the boot chain. … 979
client-ui-skill Client Skill reference plugin, node half. Pure UI plugin: the empty apply exists so the plugin appears in the host cordis.yml / Loader; the browser half ships via exports["./client"], discovered through the package.json dsh.client declaration. 974
client-ui-settings Client Host loader entry for the browser implementation exported from ./client. 961
client-ui-sidebar Client Host loader entry for the browser-only sidebar plugin. 957
client-ui-jobs Client Background-job list plugin, node half. Pure UI plugin: the empty apply exists so the plugin appears in the host cordis.yml / Loader; the browser half ships via exports["./client"], discovered through the package.json dshClient declaration. 663
client-hmr Client HMR plugin, node half: the host end of the dev reload chain. One interval stat-polls every graph row's client bundle (polling by design: network mounts deliver no inotify events), reports content changes through clientModuleHost.rebuilt(id), and serves the /plugins/events SSE channel broadcastin … 注入 clientModules, webServer 660
client-ui-settings-plugin-inventory Client Host loader entry for the inventory-tab browser implementation exported from ./client. 563
client-ui-plan Client Plan control plugin, node half. Pure UI plugin: the empty apply exists so the plugin appears in the host cordis.yml / Loader; the browser half ships via exports["./client"], discovered through the package.json dsh.client declaration. … 409
client-ui-directory-picker-native Client Native directory-picker surface, node half. Pure UI plugin: the empty apply exists so the plugin appears in the host cordis.yml / Loader; the browser half ships via exports["./client"], discovered through the package.json dsh.client declaration. … 391
client-schema-form Client Schema/draft model layer for settings editors: rehydrate the wire's serialized schemastery envelope, resolve nodes by settings path, validate drafts, and edit them immutably by path. Editors render their own controls (the Models page hand-writes its layout) on top of these helpers. 319

apps · 产品装配(CLI / Web)(2 包 · 23,928 行)

角色 它做什么(源码 @module JSDoc) 接线 行数
web-frontend Prov Web application entry: thin bootstrap over the shell library. Everything — loader holding, module-table seeding, AppRoot gate, plugin assembly — lives in @deepseek-ai/dsh-client-web; this file only finds the mount point. 20,359
dsh Prov dsh plugin --profile <name> <args...> — profile plugin management as a thin pnpm forwarder: initialize the profile on first use, run pnpm <args...> in the profile directory, then reconcile the dsh.profile.bundles layer list against the installed state (a dependency resolving to a package that … 3,569

examples · 可运行示例装配(3 包 · 2,876 行)

角色 它做什么(源码 @module JSDoc) 接线 行数
agent-spine-demo Prov Default executor-less, UI-less agent spine. It bundles the common services, background-job registry and controls, optional persisted goals, concrete loop, local skill and agent-instructions providers, and model-facing shell/skill consumers; deployments still choose the LLM adapter, bash executor, an … 1,854
acp-demo Prov The ACP automation server app: the default agent spine ({@link @deepseek-ai/dsh-agent-spine-demo}), JSONL session persistence, and the {@link @deepseek-ai/dsh-acp} bridge. The app owns those plugins through one ordered lifecycle so ACP sessions quiesce before persistence detaches. … 877
sdk-jsonrpc-demo Prov Bin-only app package: its generic and packaged entries discover an external cordis.yml and own process exit. This module exports no composition plugin; the config chooses whether to load the {@link @deepseek-ai/dsh-sdk-jsonrpc-server} serving plugin. 145

test-support · 测试基础设施(6 包 · 13,391 行)

角色 它做什么(源码 @module JSDoc) 接线 行数
acp-snapshot Lib ACP snapshot suite kit — the shared machinery behind the keyless snapshot tier (pnpm run test:snapshot). Four layers, composable per example: the shared subprocess/client launcher ({@link launchAcpTestAgent}), the scripted scenario harness ({@link runScenario}), the pure expected-output normalizer … 6,628
client-test-runtime Lib jsdom slot test runtime: a real small runtime — Cordis Context, the runtime SlotRegistry, and the web-react renderer — assembled around test-owned session/workspace doubles, so feature specs exercise declaration, registration, scope, store, inject, rendering, updates, and disposal without hand-b … 2,264
llm-replay Lib Keyless snapshot-test LLM replay. It derives one model-call script per recorded session from assistant/chunk events and explicitly marked local compaction calls, then binds fresh live sessions to parent/child scripts by first-call order. … 注入 llm 2,085
llm-mock-server Lib Scriptable OpenAI-compatible HTTP/SSE server for transport, protocol, and semantic-empty LLM recovery tests. Each accepted chat-completions request consumes one behavior; the server never retries or interprets harness policy. 1,557
loader-smoke Lib Shared subprocess harness for keyless example smokes that boot a real cordis.yml through an app bin and Cordis Loader. It also owns the mode-aware launch resolver every example subprocess harness shares ({@link resolveExampleLaunch}): booting an example bin from TypeScript source under tsx (the … 758
agent-loop-testkit Lib Shared mounting for the services required before tests load the concrete agent loop. The caller retains ownership of the context, loop, adapters, optional plugins, and teardown. 99

util · 零依赖工具库(7 包 · 2,225 行)

角色 它做什么(源码 @module JSDoc) 接线 行数
output-retention Lib A dependency-light retention library: bounded model-facing output for tools that must cap how much context they return. A caller feeds items or text chunks into a bounded object, then gets the retained content plus exact omission metadata ({@link RetainedItems} / {@link RetainedText}). … 852
timeout Lib Shared timeout arithmetic, signal fusion, and classification. The library only notifies through abort signals; each capability still owns the mechanism that stops its work and translates timeout reasons into public outcomes. 514
atomic-write Lib Zero-dependency atomic file replacement and writer coordination. writeFileAtomic writes a random-suffix sibling with exclusive create and the caller's permission bits, then renames it over the target, so readers observe either the old or the new complete content and a replaced file ends up with ex … 232
launch-environment Lib Immutable launch-time environment snapshot that records which layer supplied each value. Harness consumers resolve through it instead of a flattened process.env; launchers may still materialize accepted values for config expressions and third-party libraries. 拥有 launchEnvironment 226
home-paths Lib Shared filesystem path helpers for DeepSeek Harness user data. 221
native-command Lib Shared no-shell execFile runner for host-native OS integrations (the native directory chooser, the open-with-default-application hand-off): utf8 stdio capture, abort propagation, Windows console hide. A library, not a plugin — no ctx, no state, no events. 121
brand Lib The Branded<B> nominal-typing primitive — a type-only utility (no runtime code, no harness-package dependency) shared by every package that owns a cross-boundary id. … 59

vendor · vendored Cordis(9 包 · 6,584 行)

角色 它做什么(源码 @module JSDoc) 接线 行数
cordis Prov Core context type and root context implementation. 2,702
cordis-plugin-loader Def 拥有 entry, loader, startTime 1,161
schemastery Prov 921
cordis-plugin-hmr Def 拥有 hmr · 注入 loader, timer 614
cosmokit Prov Array set and normalization helpers. 483
cordis-plugin-include Prov 注入 loader 378
cordis-plugin-logger-console Prov Re-export shared console exporter config and base implementation. 173
cordis-plugin-timer Def 拥有 timer 148
cordis-plugin-group Prov 4

native · Landlock 原生启动器(4 包 · 343 行)

角色 它做什么(源码 @module JSDoc) 接线 行数
node-addon-landlock-run-workspace 其它 215
node-addon-landlock-run Prov The JavaScript API over the prebuilt landlock-run launcher: resolve the binary for this host, build its grant argv, and run its functional probe. This module owns the launcher's CLI contract (docs/cli-contract.md) so consumers never parse launcher output or spell launcher flags themselves — the … 128
node-addon-landlock-run-linux-arm64 其它 0
node-addon-landlock-run-linux-x64 其它 0
Part 26

第 25 章 协作图谱:入度、装配链路、一次 turn 的插件参与表

25.1 依赖入度分层

普查数据画出的依赖层级(箭头 = 被依赖):

flowchart BT
    INV["dsh-invariants ← 218 包
(制度层:每包必有 invariant 伴生)"] SCH["schemastery ← 110 包
(配置校验)"] BR["dsh-brand ← 30 包
(branded id)"] TO["dsh-timeout ← 23 包"] SESS["dsh-session ← 84 包"] LLM["dsh-llm ← 82 包"] AG["dsh-agent ← 61 包"] TOOLS["dsh-tools ← 47 包"] SP["dsh-system-prompt ← 30 包"] CLIENT["dsh-client-ui-slots ← 35 包
dsh-client-runtime ← 34"] API["dsh-api-remotes ← 23 包"] INV --- SCH --- BR --- TO SESS --> AG --> TOOLS LLM --> AG TOOLS --> SP API --> CLIENT

三层结构很清楚:

  • 制度层invariants / schemastery / brand / timeout):横切所有包,与业务无关
  • 主轴层sessionagenttoolssystem-promptllm 横切):业务核心
  • 前端层api-remotesclient-*):独立的依赖社区

25.2 一次 turn 的插件参与表

这是"它们如何互相配合"最直接的答案——同一次 turn 里,各插件在哪个时刻接入:

时刻 事件/钩子 谁在这里工作
turn 打开 turn/start(session 事件) session 持久化、遥测、UI
领取输入 inbox.claim 用户消息、工具附加上下文、jobs 完成通知、skill 目录注入
装配提示 systemPrompt.assemble 20 个包贡献段落:身份、工具 schema、cwd、skills 清单、todo、plan 约束、subagent 专属段
决定模型看什么 agent/pre-step ⟨waterfall⟩ plan-mode 拦截、context 投影、权限预检
定请求参数 agent/request ⟨waterfall⟩ 模型路由、reasoning effort、maxTokens、provider 特定参数
流式 llm/stream ⟨waterfall⟩ llm provider、token 计量、CoT 处理
每个分片 assistant/chunk(session 事件) UI 实时渲染、持久化
请求失败 agent/request-error ⟨waterfall⟩ 重试策略、provider 降级
工具调用前 tool/call(session 事件)+ presentCall UI 待处理卡片
工具准入 tools/pre-execute ⟨waterfall⟩ hooks 桥、permission、sandbox 策略、fs 观察策略
安全终局 单调守卫 不可放宽的拒绝
需要人 ctx.approval interaction 组的审批 UI
工具执行 tools/execute ⟨waterfall around⟩ timeout、retry、metrics
工具本体 ToolDefinition.execute 24 个 Consumer 包之一
变更意图 fs/write-intent / fs/edit-intent fs 策略插件
结果后处理 tools/post-execute ⟨waterfall⟩ spill(超量落盘)、pruner(修剪)、结果改写、附加上下文
结果定稿 tools/result ⟨emit·冻结⟩ 遥测、UI 完成卡片、invariant 检查
step 关闭 step/end(session 事件) 统计
停机公告 agent/turn-stopping ⟨serial⟩ 同 turn 内异议(典型:steer);不是 goal 跨 turn 续跑
idle 之后 agent/statusfollowup goal-round-driver 在 idle + armed 时开下一轮
turn 关闭 turn/end(session 事件) 标题生成、持久化 flush、遥测汇总

这张表就是插件化架构的价值证明:19 个时刻,每个时刻都有独立插件接入,而 agent.ts 的 496 行里没有一行是为"审批""沙箱""溢出""目标""标题"写的。

25.3 三条典型装配链路

链路一:bash 工具的完整依赖闭包

tool-bash(Consumer)
  → ctx.tools(core/tools)        注册与流水线
  → ctx.shell(shell Definition)  接口
     → bash-local | bash-sandbox(Provider)
        → ctx.subprocess           起进程
        → ctx.sandbox              包 argv(sandbox 组)
        → ctx.sandboxPolicy        策略
  → ctx.systemPrompt               贡献工具 schema + 用法段
  → ctx.shellEnv                   托管 DSH_* 环境
  → ctx.jobs(call time)          run_in_background
  → ctx.approval(间接)           写操作审批

一个工具,七个服务键,三层 provider。而它的源码只有 2 081 行,因为每一层都不需要它关心。

链路二:一条"给模型加上下文"的合法路径

某插件 → agent.inject(message)
  → inbox(不唤醒)
  → 下次有唤醒消息时一起 claim
  → append user/message(session 事件)
  → deriveMessages() 投影进模型历史
  → invariant 断言:模型可见 ⟺ 已记录 ✓

链路三:换到远程执行世界

patch 掉两行配置:
  fs-local     → e2b-fs
  subprocess-local → e2b-subprocess
自动跟着搬家:bash / pwsh / 持久 PTY / lsp / glob / grep / code-runtime
零 provider fork

Part VII工程制度:本系列见过的最强一套
Part 27

第 26 章 逐文件 100% 覆盖率:把覆盖率当删码工具

26.1 六层测试

docs/testing.md(49 行,全是干货):

命令 关键要求
单元 pnpm run test 每个注册表都要有 HMR 安全测试(dispose 贡献 fiber,断言清理干净);偏好边界、错误路径、事件顺序、并发竞态、契约回归的永久测试
覆盖率门禁 pnpm run test:coverage packages/*/*/src 逐文件 100%
真实 API e2e pnpm run test:e2e 带 key 打真实 provider;各套件按自己的 key 自跳过
keyless 快照 pnpm run test:snapshot boot 真实示例、重放录制会话、diff 规范化 JSON-RPC + 重新持久化的日志
浏览器快照 pnpm run test:web Chromium 重放,Linux PR 必过;CI 强制只读 DSH_SNAPSHOT=replay
门禁集合 doc-sync / hygiene / duplication 见第 27 章

26.2 技术闪光点:覆盖率的正确解读

docs/testing.md:10

An uncovered line is often dead code the gate is correctly flagging for deletion, not a missing test to bolt on. Line coverage is necessary, never sufficient — it proves lines ran, not that the feature works as shipped.

"未覆盖的行往往是死代码,门禁正确地在提示你删掉它"——把覆盖率当删代码的工具,而不是写测试的 KPI。

这解释了第 8 章看到的 /* v8 ignore next -- … */:不可达的防御分支必须显式豁免 + 写清理由,而 scripts/coverage-exempt.ts + coverage-exempt.spec.ts 守着这些豁免本身。

也解释了第 2.2 节那条"不要过度防御"的规则——在 100% 覆盖率制度下,每个多余的防御分支都有直接成本

26.3 一个诚实的例外

docs/testing.md:10

Per-file 100% on packages/shell/pwsh-local/src needs a real pwsh: without one its executor suites self-skip and vitest.config.ts exempts the file so pwsh-less hosts stay green, while CI runners ship pwsh and enforce the full bar.

没有 PowerShell 的机器上豁免该文件保持绿,CI 上装了 pwsh 强制满格。例外被写进文档并给出理由,而不是默默降低标准。

26.4 三条我认为最该抄的测试规矩

① 验证世界,不验证自述:29):

An e2e assertion re-runs the command or re-reads the file externally; a keyword probe on the agent's own output lets a cheating agent pass. Assert untouched files are byte-identical.

"对 agent 自己的输出做关键词探测会让作弊的 agent 通过"——这句话应该刻在每个 agent 项目的测试文件顶部。

② 守卫必须被证明有效:34):

A guard only guards if the regression actually fails it. … add an explicit assertion, and prove it: introduce the regression, watch red, revert.

先引入回归、看它变红、再回滚——这是我在本系列里见过的最高测试标准。

③ 测真实入口路径:35):

"Real entry path" means the published artifact: a package bin runs built lib/bin.js under plain node, exposing failures tsx masks (settle races, module resolution, swallowed load failures).

用 tsx 跑源码会掩盖三类真实故障。关键路径必须用发布产物测。

26.5 with-key 政策:一句带公司身份的话

docs/testing.md:19

The with-key policy: inference is cheap hereWe are DeepSeek — do not ration real-API tests. A no-key test proves plumbing; only a with-key run proves the agent works against a real model. … Self-skip keeps secretless CI and keyless contributors unblocked; it is not a cost signal.

"我们就是 DeepSeek,别省真实 API 测试"——这是全仓最有个性的一句话。而且它明确说:自跳过是为了让无密钥的贡献者不被阻塞,不是成本信号

配套的还有"最高价值是 smoke test"的判断:

Highest-value are smoke tests that boot the real example, send one prompt, and check the world — they catch the "green unit tests, broken product" class that mocks cannot(postmortem 0001).


Part 28

第 27 章 28 个 `verify-*`:把口头约定焊成机器门禁

27.1 宪法要求

AGENTS.md

Wire mechanically checkable invariants into an executed top-level gate and prove each changed acceptance path rejects an invalid case. Use narrow, justified exceptions instead of disabling a rule globally.

两个要求:机械可检查的不变量必须接到会被执行的顶层门禁;而且要证明每条被改的验收路径会拒绝非法输入

27.2 门禁清单

scripts/ 下 124 个 .ts,其中 28 个 verify-*

门禁 它把什么变成了机器可检查的
verify-export-jsdoc 每个导出有 JSDoc;函数类导出有 @param/@returns
verify-cordis-config cordis.yml 裸插件必须在 resolver manifest 的 dependencies
verify-package-invariants 每包有 ./invariant 伴生;空 installer 必须以 No runtime invariant: 解释
verify-built-package-invariants 产物侧的同一条
verify-type-equiv 文档里的 ts type-equiv 代码块必须与源码类型等价
verify-mermaid 图必须能渲染
verify-md-links / verify-doc-refs 链接与引用不许烂
verify-md-wrap markdown 换行纪律(一段一物理行)
verify-doc-budgets 文档字数预算(超了要改 manifest 的上限)
verify-doc-site-fragments 文档站片段
verify-package-readme-limitations 每个包 README 必须写清"局限"
verify-package-readme-model-experience 必须写清"模型体验"
verify-agent-note-classification / -format 设计文档的分类与格式
verify-archived-agent-notes 归档笔记的冻结规则
verify-translation-pairing / -prompt 双语文档逐段配对
verify-config-source-ownership 配置源的所有权
verify-client-domain-graph 前端域图
verify-runtime-closure 运行时依赖闭包
verify-node-next-types NodeNext 消费者类型
verify-package-paths 包路径
verify-dsh-package-licenses 许可证
verify-public-repository-links 公开仓库链接
verify-skill-invocation-metadata skill 调用元数据
verify-vendored-links vendored 链接
verify-md-* 其余文档纪律

另有 pnpm run hygiene(knip 死代码 + publint 发布规范 + workspace 约束 + NodeNext 消费者检查)与 pnpm run duplication(jscpd 跨文件 TypeScript 克隆检测)。

27.3 三条本系列没见过第二家做的

verify-package-readme-limitations:强制每个包的 README 写清局限。这防的是"过度宣传"——一个包的文档只说能做什么,不说不能做什么,使用者就会在错误的场景里用它。

verify-package-readme-model-experience:强制写清模型体验。要求作者站在模型的视角描述自己的包:模型会看到什么、会怎么误用、schema 里的措辞会导致什么行为。这是"为模型写文档"的制度化。

verify-type-equiv:文档里的类型代码块必须与源码类型等价。所以 docs/subsystems/*.md 里那些 ts type-equiv 围栏块不是手抄的,是被机器校验过与源码一致的。

27.4 门禁自己也被守卫

scripts/ 里能看到成对出现的文件:coverage-exempt.ts + coverage-exempt.spec.tsrun-gates.ts + run-gates.spec.tsgen-doc-graphs.ts + gen-doc-graphs.spec.tstranslation-pairing.ts + .spec.ts……

守卫也要被守卫。 一个坏掉的门禁比没有门禁更危险,因为它给人虚假的安全感。

27.5 8 177 行生成的文档

docs/ 里这些是生成的(由 scripts/gen-*.ts 产出、doc-sync 验证新鲜度):

文档 行数 生成方式
config-catalog.md 3 151 所有插件的 Config 字段
tool-catalog.md 1 873 boot 每个工具插件读 ctx.tools.schemas()
module-graph.md 1 638 模块图
persistence-catalog.md 944 持久化目录
capability-seams.md 471 能力图
event-producer-consumer.md 76 事件生产者/消费者
graph-atlas.md 24 图索引

再加上每个子系统页面里的 "Cordis API" 区块(由 gen-cordis-catalog.ts 生成,在两种语言侧字节相同)。

结论:这个仓库的文档不是"写"出来的,是从源码生成 + 机器校验新鲜度。这也是为什么我能用一个普查脚本就抽出 241 个包的准确职责——因为源码里的 JSDoc 是被门禁强制的


Part 29

第 28 章 686 篇 Agent Notes:设计决策考古学的制度化

28.1 数字

.agents/notes/ 下的英文主文件(每篇还有 .zh.md + .i18n.yaml 三件套):

生命周期 篇数 含义
implemented/ 507 决策已发货,且与实际发货保持同步
archived/ 143 已完成且理由不再指导未来工作,冻结
proposed/ 25 实现前被评审的提案
rejected/ 11 被考虑后拒绝,只在其理由能防止一个诱人的错误时保留
合计 686

28.2 它是什么

.agents/notes/README.md

An Agent Note records a decision or proposal that affects this codebase — the why and what we gave up, the parts code and docs can't carry.

"为什么"和"我们放弃了什么"——这正是本系列分析方法论(动机 → 约束 → 被否方案 → 选择 → 代价)想从代码里反推的东西。dsh 在源头就写下来了。

28.3 双轴路径编码

{lifecycle}/{class}/yyyy-mm-dd-topic-title.md
  • lifecycle(顶层目录)= 状态,笔记随状态在目录间移动
  • class(嵌套目录)= 决策的种类,来自封闭集合(由 scripts/agent-note-tree.ts 定义,分类门禁拒绝其它文件夹):
class 覆盖什么
feature 新的用户或模型可见能力
bug-fix 修缺陷或补 postmortem 暴露的漏洞
simplification 删代码/行为/表面积而不加能力
architecture 关于发货源码的结构决策
process 代码周边的工具/策略/流程(门禁、包管理器、vendoring)
testing 测试基础设施与策略

refactor 故意缺席,README 给了理由:

(refactor is deliberately absent — it overlaps simplification, whose discriminator, "does observable behavior change?", already covers it.)

用一个判别标准(可观察行为是否改变)消灭一个模糊分类。这是分类学上的好品味。

28.4 四条很讲究的规则

① implemented 必须与实际发货同步:

is kept current with what actually shipped: when the code later moves a file, renames a package, or changes a key/default, the Agent Note is updated in the same change to match (facts only — paths, names, structure — not the decision itself).

只更新事实(路径、名字、结构),不改决策本身。所以你读一篇 2026-06 的笔记,里面的路径是当前的,但决策是当时的。

② archived 是冻结的:

Archived notes are frozen: never edit or treat them as current authority.

归档时只允许:移动三件套、保留 Status: implemented、在状态下方插入同样的 Archived: YYYY-MM-DD 行(两种语言都插)、重录 sidecar、修复或删除入链。这是唯一被允许的内容改动。

③ 交叉引用必须是相对 markdown 链接:

Cross-references between Agent Notes use relative markdown links … — never bare prose or numbers — so they are mechanically checkable and survive moves between folders.

不许写"见那篇关于 X 的笔记",必须写链接。这样 verify-doc-refs 能检查,而且笔记在生命周期目录间移动后链接仍可修复。

④ 不要集中索引:

Do not add a centralized INDEX.md; the [no-index Agent Note] owns the rationale.

连"不要加索引"这个决定本身都有一篇 Agent Note。 这是制度自指的极致。

28.5 与 AGENTS.md 的配合

AGENTS.md

Non-trivial changes MUST include an Agent Note in the same PR; only mechanical/local edits are exempt.

verify-agent-note-classification / verify-agent-note-format / verify-archived-agent-notes 三条门禁守着这套制度。

28.6 11 个给 AI 协作者的 skill

.agents/skills/

skill 作用
dsh-prose-standard 文字标准(决定"contract/boundary/shape"这类词怎么用)
dsh-pre-push-checks 推送前跑哪些检查
dsh-code-review 代码评审
dsh-doc-standards / dsh-doc-site-sync 文档标准与站点同步
dsh-translate-docs 双语翻译(只有用户显式调用才能跑
dsh-archive-agent-notes 归档笔记的校准过工作流(README 明说:用这个而不是靠字数、年龄或配额)
dsh-find-simplifications 找可简化处
dsh-merging-stacked-prs 栈式 PR 合并
dsh-trim-cot-leakage 修剪 CoT 泄漏
record-browser-gif 录制浏览器 GIF

28.7 一条罕见的散文纪律

AGENTS.md 的 "Type safety and documentation" 一节:

Comments and docs state complete contracts and context, not reasoning transcripts. Use direct, concrete terms. Do not use metaphors. Before writing contract, boundary, or shape, ask whether a more exact term names the subject: write response fields, JSON validation, or ESM exports instead of response shape, validation boundary, or module shape.

"不要用隐喻" + "写 contract/boundary/shape 之前先问有没有更精确的词"——它甚至给出了三组替换示例,并规定 contract 只留给"前置条件、后置条件、不变量、兼容承诺"这类调用方真正依赖的义务

还有:

Do not narrate control flow or tests, preserve review history, or restate code. Keep behavior, failure, timing, ownership, and safe-use facts; link the rationale.

不要叙述控制流、不要保存评审历史、不要复述代码。 保留行为、失败、时序、所有权、安全使用这五类事实,理由用链接。

这套散文纪律加上 verify-doc-budgets(字数预算),解释了为什么这个仓库 40 多份子系统文档读起来密度极高却不冗长。


Part 30

第 29 章 动态 Cordis:agent 挂载自己的插件

29.1 它是什么

AGENTS.md 仓库布局仍写着 self-modification/仓库里没有这个目录。 vitest.config.ts 的 coverage exclude 也还列着 packages/self-modification/*/src/**。实际代码在 packages/extensions/

职责
dsh-cordis-host-runner ctx.dynamicCordisRunner:define / run / stop / undefine
dsh-tool-cordis 暴露给模型的 7 个工具
dsh-cordis-client-runner / dsh-ui-cordis Client 半与 UI

对应的工具(普查抓到 7 个):

cordis_define · cordis_undefine · cordis_run · cordis_stop
cordis_inspect_list · cordis_inspect_query · cordis_inspect_self

package.json 里有一条 demo 脚本:pnpm run demo:cordis —— "the agent modifies its own runtime"

29.2 安全边界

docs/tool-catalog.mddsh-tool-cordis 那条部署说明很谨慎:

Not in any shipped tree (a deliberate opt-in — dynamic package code reaches the real runtime, see .agents/notes/implemented/feature/2026-07-08-self-referential-cordis-toolset.md). The toolset injects ctx.dynamicCordisRunner from @deepseek-ai/dsh-cordis-host-runner, which owns the definition registry and the vm sandbox; a composition missing it never activates the tools. A running package may register ADDITIONAL model-visible tools until it is stopped, undefined, or DSH restarts; a full changed request header logs those tool-set changes.

五个安全设计:

  1. 不在任何发货树里——刻意的 opt-in,因为动态包代码会触达真实运行时
  2. 需要 ctx.dynamicCordisRunner(来自独立的 host-runner 包),它持有定义注册表与 vm 沙箱
  3. 组合里缺这个服务,工具永不激活
  4. 运行中的包可以注册额外的模型可见工具——直到它被 stop / undefine / DSH 重启
  5. 工具集变化会被完整的 changed request header 记录到日志——回到第 10 章的宪法:工具表是模型可见的,所以它的变化必须已记录

29.3 为什么这是"一切皆插件"的逻辑终点

如果一切都是插件,而 agent 能写代码,那 agent 就能写插件。而如果注册是可逆的 effect,那 agent 挂载的插件也能被干净地卸载。

这个能力不是加上去的,是架构推导出来的。 它同时也是对架构的压力测试:如果动态挂载一个插件会破坏什么,说明那个东西不该是特权状态。

本系列里最接近的是 DeepTutor 的 subagent(把编程 CLI 当 KB)和 Open Design(把 CLI 当引擎),但那两个是"调用外部 agent"。dsh 这个是agent 修改自己的运行时装配——量级不同。


Part VIII品味与边界
Part 31

第 30 章 十六项决策五段式复盘

# 决策 动机 被否方案 选择 代价
1 一切皆插件、无特权内核 每个新需求都改主循环 主循环 + 钩子 Cordis 服务仓库;循环也是插件 学习曲线陡;间接层多
2 vendored Cordis 框架与产品同节奏演进 npm 依赖锁版本 pinned 源码副本 + manifest + 同步流程 同步要重跑 test/build,本地修改要重新应用
3 inject 表达加载顺序 手工启动顺序表不可维护 显式 boot 序列 不动点解析 + 未激活即报错 排查"为什么没加载"需要新工具
4 四种 dispatch 模式 一个 EventEmitter 塞不进四种语义 统一 emit @mode 写进公共契约 + 生成目录校验 作者要判断该用哪种
5 waterfall MUST 调 next() 观察者短路会静默失效 靠约定 大写 MUST + 文档 + 测试 仍靠人守(无静态检查)
6 seam 三角色 换实现不该 fork 工具 interface + if/else Definition/Provider/Consumer,缺一不成缝 一个能力至少三包,仓库 241 包
7 request/spec 分裂 隐含默认无法记录与断言 ?? default in run() 显式 resolve(request): Spec 多一个类型与一次转换
8 append-only 日志为真相 内存 messages 丢一切 数组 + 顺便记日志 event sourcing + deriveMessages() 日志体积大;加上下文要扩事件表
9 模型可见 ⟺ 已记录 绕过日志会静默失真 靠 review 218 包的 invariant 伴生 + 归属化失败 真实维护负担
10 turn/step 两级边界 一级说不清"欠不欠" 单层 while step=一次请求+工具;turn=不欠为止 两个计数器与两个投递口
11 agent/turn-stopping 公告 同 turn 内续跑不该改循环 循环里加条件 serial 公告 + steer + 二次检查 inbox 停机多一次事件往返;跨 turn 的 goal 续跑另走 idle + followup
12 单调守卫 waterfall 能被提权 只用 waterfall 守卫只能 deny 或弃权 两层准入,作者要选对层
13 结果冻结 + emit 观察者偷改已定稿结果 统一 waterfall Readonly + Object.freeze + 不变量 后处理必须在 post-execute 完成
14 配置即架构(bundle/profile/patch) 多形态前端不能三份 main 环境变量 + if 有序叠层 + 整体替换 + --dump-config patch 是整体替换,写起来啰嗦
15 逐文件 100% 覆盖 死代码与假测试 总体百分比 逐文件满格 + 带理由豁免 需要豁免机制及其守卫
16 686 篇 Agent Notes 代码与文档承载不了"为什么" 靠 PR 描述 双轴路径 + 封闭分类 + 三条门禁 每个非平凡改动多一篇文档

Part 32

第 31 章 横向对比:harness 型 vs 循环型

31.1 与本系列主要项目对比

维度 DeepSeek Harness Reasonix(同公司) Claude Code Codex Open Design DeepTutor
架构形态 harness(无内核) 单二进制 + 传输无关控制器 循环型 循环型 宿主型(不写循环) 双轨循环 + 三层插件
主循环地位 一个可替换插件 核心,单循环 核心 核心 不写 核心(两条)
扩展方式 挂插件到 73 个服务键 MCP + use_capability 工具/权限/hooks 工具/沙箱策略 适配器即数据 三层插件
上下文真相 append-only 日志 + 投影 稳定前缀 + turn tail 会话 + 压缩 会话 剧本 UnifiedContext
能力替换 seam 三角色,换 provider 搬整个世界 provider 适配 有限 有限 换 CLI 换 RAG 引擎
权限模型 pre-execute + 单调守卫 + approval 应用层 权限规则 OS 三平台围栏 无(外围补) ToolMountFlags + 沙箱分级
沙箱 confine(argv):Linux bwrap→Landlock(C11 启动器);macOS Seatbelt;Win ACL 应用层 + OS 权限 Seatbelt/Landlock/Token 三后端两级
工程门禁 逐文件 100% + 28 verify + 686 notes 常规 常规 常规 常规 常规
包数 241 单二进制 219(Python)
许可 MIT MIT 各异 各异 视仓库 Apache-2.0

31.2 同一家公司的两条路

Reasonix:单静态二进制、缓存优先、传输无关的 control.Controller、soft→snip→summary 三阶压缩。目标是省钱 + 可移植

Harness:241 包 monorepo、一切皆插件、73 个服务键、六层测试。目标是可替换 + 可扩展 + 可长期维护

这不是矛盾,而是两个不同的产品假设:Reasonix 假设"用户要一个能塞进任何环境的终端 agent",Harness 假设"生态要一个能被改造成任何形状的骨架"。

31.3 harness 型的适用边界

harness 型赢在:多前端一套行为、第三方扩展、能力整体替换(本机↔远程)、长期可维护性、把纪律焊进门禁。

循环型赢在:上手快、调试直观(栈里能看到全过程)、小改动成本低、不需要维护装配层。

一个具体判据:你的产品要不要支持"用户替换掉执行环境"? 如果要(远程沙箱、企业内网、自定义运行时),harness 型的 seam 投入会回本。如果不要,那 241 个包的间接层就是纯税。

31.4 三个别家没有的组合

  1. dsh-invariants 被 218/241 个包依赖——运行时不变量是包的出生证明的一部分,而且规定了"不许写假装在检查的断言"。
  2. 单调守卫——用类型(没有 allow 返回值)保证安全策略在数学上不可能被后续插件放宽。
  3. 686 篇双轴分类的 Agent Notes + 3 条门禁——把"为什么这么设计、我们放弃了什么"制度化到有生命周期、有封闭分类、有归档冻结规则、连"不要加索引"都有一篇笔记。

Part 33

第 32 章 诚实边界

  1. 它是 developer preview。README 用大写警告 "THERE WILL BE COMPATIBILITY-BREAKING CHANGES",SESSION_FORMAT_VERSION 保持 0明确不做兼容承诺AGENTS.md 有专门的 "pre-release stance" 一节并给自己写了删除条件。现在拿它做生产要有升级预算。
  2. 体量巨大:497k 行 TS + 67k 行 TSX + 241 包。本分析抓宪法、主轴、协作图谱与制度,不是逐包深读——client 39 包 138k 行、typert 11 包 15k 行、extensions 4 包 20k 行都只做了名册级覆盖。
  3. 学习曲线是真实成本。读懂一次请求要同时装下服务仓库、inject 解析、四种 dispatch、effect 生命周期、seam 三角色、装配分层六件事。架构文档自己也说 "We recommend using an agent to explore the codebase"(docs/architecture.md:7)——它建议你用 agent 来读它
  4. 间接层让调试变难。想知道 ctx.shell 是谁得看装配而非代码。所以才需要 --dump-configcordis_inspect_*module-graph.md 这一整套导航工具——这些工具的存在本身就是代价的证据
  5. verify-* 门禁的维护成本。28 个门禁 + 各自的 .spec.ts + 124 个脚本,这套体系需要人持续喂养。对小团队是净负担。
  6. 名册的角色分类是我推断的。普查脚本按"包名前缀 + 是否声明 ctx 键 + 是否注册工具"分类,与仓库自己的 packages/README.md 分组可能有差异;职责描述那一列是源码 JSDoc 原文,可信度更高。
  7. inject 提取不完整。Cordis 声明依赖有多种写法(export const injectstatic inject、Service 构造),我的脚本覆盖前两种,所以 101 个包被识别为"有 inject"可能低估。包间依赖(229 个包有)那一列更可靠
  8. BENCHMARK.md 几乎是空的。全文只说"跟着 Python SDK 指南跑 jsonrpc-agent 最小变体,用独立 workspace 与 session id"。这个仓库的质量信心来自门禁与快照,不来自公开跑分。
  9. 没有验证过运行时行为。本分析是静态阅读 + 交叉核对文档与源码,没有真跑 pnpm dsh web(需要 API key 与完整构建)。时序图与流水线图是从源码与生成文档推导的。
  10. 行号锚定 47f9438。这是一个日更频率极高的仓库(HEAD 就是当天的 merge commit,PR 编号已到 #2519),行号会漂,回核请以符号名与文件名为准

Part 34

第 33 章 源码导览索引与本地复现

33.1 🔍 源码指路表

你想理解… 先读
架构宪法(给人和 AI 看的) AGENTS.md(根;CLAUDE.md 是它的符号链接)
架构全景 docs/architecture.md(129 行,必读)
Cordis 五观念 docs/cordis-primer.md(44 行,5 分钟)
术语的严格定义 docs/glossary.md(seam / scope / lineage…)
turn/step 循环 packages/core/agent-loop/src/agent.ts:212, 246, 332, 407
Agent 接口与 inbox packages/core/agent/
会话日志与投影 packages/core/session/ + packages/session/(13 包)
工具注册表与流水线 packages/core/tools/src/index.ts;图见 docs/tool-execution-pipeline.md
流水线顺序不变量 packages/core/tools/src/invariant.ts:94-115
系统提示装配 packages/core/system-prompt/
per-agent scope packages/core/scope/
seam 范本(三角色) packages/shell/(9 包:1 Definition + 4 Provider + 3 Consumer + 1 env)
一个执行世界 packages/fs/ + packages/subprocess/;实证 packages/e2b/
沙箱 packages/sandbox/ + native/landlock-run/
上下文经济 packages/compaction/ packages/spill/
目标续跑 packages/goal/
子智能体 packages/subagent/(11 包,含 codex / claude-code provider)
Code Mode packages/code-runtime/;说明见 docs/tool-catalog.mdrun_code
类型图 RPC packages/typert/(11 包)+ packages/api/docs/api-gateway.md
装配分层 packages/boot/ packages/bundle/ packages/preset/
不变量制度 docs/subsystems/invariants.md + packages/runtime-diagnostics/invariants/
测试宪法 docs/testing.md(49 行,全是干货)
门禁脚本 scripts/verify-*.ts(28 个)+ scripts/run-gates.ts
设计决策考古 .agents/notes/{proposed,implemented,archived,rejected}/(686 篇)
给 AI 的工作流 .agents/skills/(11 个 SKILL.md)
事故复盘 docs/postmortem/0001-0004
生成的目录 docs/{config,tool}-catalog.md module-graph.md capability-seams.md persistence-catalog.md
47 个工具的完整 schema docs/tool-catalog.md(1 873 行)

33.2 本地复现(只读分析)

cd 参考项目/deepseek-harness
git rev-parse --short HEAD          # 期望 47f9438
git log -1 --format='%s'            # Merge pull request #2519 …

# 复核本文的关键数字
find packages -name package.json -not -path '*/node_modules/*' -not -path '*/lib/*' | wc -l   # 219
find . -name '*.ts' -not -path '*/node_modules/*' -not -path './.git/*' | wc -l               # 2319
ls scripts/verify-*.ts | wc -l                                                                # 28
for s in proposed implemented archived rejected; do
  printf "%s: " "$s"; find ".agents/notes/$s" -name '*.md' -not -name '*.zh.md' | wc -l
done                                                                                          # 25/507/143/11

复现本文的 241 包名册(脚本在本仓库):

cd "claude code源码分析"
python3 scripts/dsh-plugin-census.py     # → scripts/dsh-plugin-census.json(241 包)
python3 scripts/dsh-roster-gen.py        # → scripts/dsh-roster.md(分组名册)

若要真的跑起来(非分析必需,需要 DEEPSEEK_API_KEY):

pnpm install                             # node ^22.19 || >=24
pnpm run build
pnpm dsh web                             # http://127.0.0.1:3080
pnpm dsh --profile web --dump-config     # 看实际装配的整棵树
pnpm dsh --profile headless "一句任务"
pnpm run demo:cordis                     # agent 修改自己的运行时

33.3 和本系列其它文档的衔接


Part 35

附录 A · 版本基线指纹

Repo https://github.com/deepseek-ai/deepseek-harness
版本 / Commit v0.1.0-rc.5package.json)/ 47f9438
许可 MIT
workspace 包总数 241packages/ 219 + vendor 9 + apps 2 + native 4 + 其它)
TypeScript 497 489 行 / 2 319 文件
TSX 66 633 行 / 259 文件
Python 4 286 行 / 19 文件
ctx 服务键 73
模型可见工具 47
Service Definition 包 60
Consumer(工具)包 24
Provider/插件包 97
最大组 client 39 包 · 138 163 行
最大单包 core/tools 13 743 行
依赖入度冠军 dsh-invariants218 包(90%)
verify-* 门禁 28scripts/ 共 124 个 .ts
Agent Notes 686(507 implemented / 143 archived / 25 proposed / 11 rejected)
生成的文档 8 177 行
postmortem 4 篇
.agents/skills 11
底座 vendored Cordis(9 包 · 6 584 行)
覆盖率门禁 packages/*/*/src 逐文件 100%
Part 36

附录 B · 三个最独特设计(给总目录卡片用)

  1. 没有特权内核:模型适配器、工具注册表、会话日志、乃至 agent 循环本身都是插件,73 个 ctx 服务键可从配置替换;--dump-config 打印的每一行都能被自己的 patch 换掉。
  2. 能改与只能看在类型层面分开:三道 waterfall 可改写,tools/resultemit + Readonly + Object.freeze + 运行时不变量;安全边界另用单调守卫(返回类型里没有 allow,数学上无法被后续插件放宽)。
  3. 纪律机械化到极致:逐文件 100% 覆盖率(把未覆盖行当死代码信号)、28 个 verify-* 门禁(含强制每包 README 写"局限"与"模型体验")、dsh-invariants 被 218/241 包依赖、686 篇双轴分类的 Agent Notes(连"不要加索引"都有一篇)。
Part 37

附录 C · 分析范围声明

本分析基于 47f9438 的静态阅读,覆盖:架构宪法(AGENTS.md 全文)、docs/ 全部纲领性文档、core/ 主轴源码逐行、工具流水线与不变量源码、241 包全量普查(脚本可复现)、测试与门禁体系、Agent Notes 制度。

不覆盖:client 39 包与 typert 11 包的内部实现、每个 provider 的具体实现、686 篇 Agent Notes 的逐篇内容、website/ VitePress 投影、Python SDK 内部、运行时实测(未跑 pnpm dsh)。extensions 组已核对 tool-cordis / cordis-host-runner 的职责与安全边界(第 29 章);其余 extension 细节仍不逐行展开。

AGENTS.md 布局里的 self-modification/vitest.config.ts 对该路径的 coverage exclude 是过时条目,以 packages/extensions/ 为准。

行号相对该基线。这是一个 PR 编号已到 #2519、HEAD 为当日 merge 的高频演进仓库,回核请以符号名与文件名为准