分析对象:deepseek-ai/deepseek-harness(产品名 dsh)
基线:v0.1.0-rc.5@ commit47f9438(Merge 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 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. 源码导览索引与本地复现
第 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.
三个信息量极大的点:
- 一切皆插件——不是"支持插件",是"只有插件"。
- 底座是 Cordis,一个独立的、有论文的插件框架,被 vendored 进仓库。
- 论文标题里的 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 章会展开。
第 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 且明确不做兼容承诺。
第 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+client39 包(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.ts的PLATFORM_CHAINS) - 协议面:ACP(Agent Client Protocol)、JSON-RPC SDK(TS + Python)、Typert RPC 网关
第 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 → 口头约定必然腐化 → 需要机器门禁
第 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 需要一个能被 provide 到 ctx 上、能持有生命周期、能被 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-config、cordis_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。
这是一个务实的选择:框架和产品在同一个演进节奏上,还没到能锁版本的阶段。
第 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): Specstep in the owning implementation, never a hidden?? defaultinsiderun()(thedsh-shellrequest/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.shellcapability 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)。
第 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 events(agent/*) |
携带活体 Agent:inbox / step / status / request / validation / continuation |
观察或拦截在飞的工作 |
Capability events(fs/*, 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
@modetag 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.waterfallis around-middleware. A listener receives(...args, next). Callnext()to delegate the possibly wrapped result to the next service; return withoutnext()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。
第 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:212,turn() 在 :246,step() 在 :332,buildRequest() 在 :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/catch(agent.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/end 在 finally 里必发(: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 章。
第 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 边界领取) |
inject 与 steer 进同一条 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-stopping 是 serial 且没有 next()(architecture.md:84)——所有 listener 都会被依次 await,谁都不能短路掉别人的异议权。Listener 的标准异议方式是 steer(往 next-step 塞消息并 wake),然后二次检查 inbox 非空,turn 继续。这是「不改循环让循环继续」在同一个 turn 内的答案。
同 session 的 goal 续跑不是这条钩子。goal-round-driver 听 agent/status 进入 idle、goal 仍 armed,再 agent.followup 开下一个 turn(见第 14 章)。把 goal 续跑写进 turn-stopping 是错的:那会把「这一轮还欠一步」和「目标还没做完、另开一轮」混成一件事。
9.4 wake latch:拆卸不等模型轮次
agent.ts:164-193 的 wakeDriver:
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 / running(agent.ts:38-46)。maintenance 阶段是别家少见的设计——驱动在做维护工作(如压缩、标题生成)时不接受唤醒投递,而是 latch 住等收敛。
而 disposal 从不 latch:so teardown waits on no model turn(拆卸不等任何模型轮次)。这类"拆卸不能被业务逻辑拖住"的细节,是长期跑在生产里的 harness 和 demo 的分水岭。
第 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 rawassistant/chunkevents 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 onsession/flush).
持久化是插件关心的事——会话服务本身不碰磁盘。所以 session 组有 13 个包:日志核心 + JSONL/zstd 持久化 + 投影 + 标题生成 + 遥测 + 引用解析…
10.3 技术闪光点:不变量制度化
docs/architecture.md:96 与 AGENTS.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
SessionEventMapand 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
SessionEventMapmember is required-on-read by default — builds that do not know its type refuse the log unless the event carries the envelope'signorable: true; only structural format changes bumpSESSION_FORMAT_VERSION.
默认严格:旧版本程序读到不认识的事件类型就拒绝整个日志,而不是跳过它继续。因为跳过意味着"模型可见的历史缺了一块",而那会产生静默错误的重放。
想让某个事件可被旧版本忽略,必须显式在信封上标 ignorable: true。默认安全,例外显式。
10.5 代价
- 任何"给模型加点上下文"的需求都要先扩
SessionEventMap+ 加渲染,比 push 一条消息麻烦得多 - 日志体积大(每个 chunk 都记)——所以有 JSONL + zstd 持久化和 packed-row 布局
- 218 个包的 invariant 伴生是真实的维护负担(虽然大部分是带解释的空 installer)
第 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 extensibletools/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 ofargs.
工具在定义时就要说清"我该被渲染成普通卡片、终端输出、还是 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 globspackages/*/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 插件。
第 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——半个仓库都要往提示词里塞东西,这正是它必须是注册表的原因。
装配发生在 每个 step(agent.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.
三个刻意的限制:
- 只有两级(全局 / 单个 scope),没有多级嵌套
- 不向下继承——子 agent 不自动获得父 agent 的 scoped 注册
- 需要子树行为时用 lineage 数据表达,绝不用 scope 结构
为什么这么克制?因为"作用域继承"是权限系统里最容易出错的地方。一旦允许继承,就要回答"子 agent 能不能覆盖父的限制""孙子怎么办""fork 之后呢"。两级扁平把这些问题全部消灭。
一个实例(docs/tool-catalog.md 的 dsh-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 向父汇报的唯一通道,不能被过滤掉。
第 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.md 的 dsh-tool-fs-search):
Capped results save the complete formatted list through the optional
ctx.spillStorebackend; returned locators are follow-up-readable/searchable when the backend exposes local paths in co-located deployments.
溢出的内容不是坟墓——定位符指向的东西还能被 read 和 grep 继续处理。
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 structuredLSP_UNAVAILABLEerror rather than changing the schema.
没有 provider 时返回结构化错误,而不是改变 schema。 因为改 schema 会让提示词缓存失效、让模型行为漂移。模型看到的契约必须与部署细节解耦。
第 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.md 的 dsh-tool-goal)信息量极大:
create_goal,get_goal,update_goal… create, 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-stopping。goal-round-driver(packages/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_modestays 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.md 的 dsh-schedule:
Registered only inside live root Agent scopes created after the opt-in Schedule plugin loads. Version 1 accepts
after_seconds, explicit absoluteat, and bounded fixed-rateevery_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.md 的 dsh-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.md 的 dsh-tool-todo:
todo_writeis session-owned state; UIs render the latesttodo/writeevent as a checklist.allowParallelInProgressis required with no default, so the catalog states its choice:true, whose description invites severalin_progressitems. A deployment choosingfalsereceives the same tool with a description asking for exactly one active task.
必填无默认是刻意的。因为"能不能同时有多个进行中任务"是一个产品语义选择,不该有隐含默认。而且这个选择会改变工具描述文本——同一个工具,两种教模型的方式。
这正是 AGENTS.md 那条 "No hardcoded tunables in plugins" 的落地:
deployment-varying choices are validated
Configfields changeable from cordis.yml; aDEFAULT_*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 不在这个组里。
第 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-codex、subagent-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 config(toolName),所以同一个包被加载多次,每个 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_forkbound to the fork backend. Each instance's description,run_in_backgroundparameter, and system-prompt policy follow its ownbackgroundModeandenableRunInBackground, so the two shipped schemas are not identical:subagentiscontinuableand defaults omitted calls to background with automatic settlement delivery, whilesubagent_forkstaysone-shotand defaults them to foreground.
同一个包加载两次,产出两个语义不同的工具(一个可续+默认后台,一个一次性+默认前台)。这是插件化架构才能做到的事——在 main() 型架构里你得写两个工具类。
15.3 ralph:一个有意思的工作流工具
docs/tool-catalog.md 的 dsh-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 章的同类设计)。
第 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/messagereplacement catalogs viaagent.inject()
技能目录是通过 agent.inject() 替换 user/message 目录来更新的——回看第 9 章,inject 不唤醒循环,所以技能目录的刷新不会触发额外的模型调用,而是等下次有真实消息时一起进去。这是 inbox 双语义设计的一个漂亮应用。
16.3 mcp:一个包 3 322 行
packages/mcp 只有 1 个包。MCP 工具的 schema 是原始 JSON-Schema(见 tool-catalog.md 生成器那段解释),所以它们无法被静态分析,必须 boot 后读取。
第 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.md 的 dsh-tool-fs-search):
glob and grep are unconditional discovery tools that spawn the packaged ripgrep binary (
@vscode/ripgrep) throughctx.subprocessas ordinary foreground calls (never background jobs) — no hostrginstall 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.md 里 run_code 那条是全文最长的部署说明,值得完整理解:
Owned by the tool registry as a reserved transport outside filterable capability layers under
mode: code/mode: both. Undercodeit 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 tomaxParallelSubCalls) that re-enter the complete guarded tool pipeline and link each nested execution to this outer result.
拆开看:
- Code Mode 下
run_code是注册表唯一的线缆贡献——模型只看到一个工具 - 其它能力以生成的 SDK 段出现(用运行时的语言,比如 Python/JS)
- 程序通过 bindings 调用它们,重新进入完整的受守卫工具流水线
- 每个嵌套执行链接到外层结果
- 并发有原生契约:提交序启动、并发安全体最多重叠
maxParallelSubCalls
这是"工具调用"到"写程序调用工具"的形态跃迁,而且没有绕过任何安全检查——嵌套调用重新走一遍守卫流水线。本系列里 Claude Code 的 code execution、Reasonix 的 use_capability 都做过类似探索,dsh 的版本在"嵌套执行也受完整流水线约束"这点上最彻底。
第 18 章 沙箱:四平台 `confine(argv)` 与「隔离强度可查询」
18.1 沙箱不执行,只包装 argv
docs/architecture.md 的扩展点表:
Confine spawned processes | use a
ctx.sandboxbackend; consumers wrap argv before spawning
这个设计让沙箱能与任何 provider 组合:本机 bash、远程 subprocess、LSP 子进程,谁都能在 spawn 前包一层。如果沙箱自己负责执行,就会和执行器争夺同一个职责。
18.2 平台链不是「只有 Landlock」
sandbox-local 的 PLATFORM_CHAINS(packages/sandbox/sandbox-local/src/index.ts:159-165):
| 平台 | 机制 |
|---|---|
| Linux | bwrap 优先,失败再 Landlock |
| darwin | Seatbelt / sandbox-exec,唯一候选、不 probe |
| win32 | ACL restricted-token(dsh-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 两个独立的服务键
普查里 sandbox 与 sandboxPolicy 是两个服务键。"能力"和"策略"分开——所以策略可以按部署替换而不换沙箱实现。
在工具侧能看到策略的痕迹(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,其中一篇给了沙箱的错误分类。这个选择本身说明了他们认为什么问题值得写下来。
第 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/typert 有 11 个包 · 15 404 行——比 goal、plan、todo、schedule、jobs、guard 加起来还大。这不是一个辅助工具,这是一套基础设施。
19.2 它解决什么问题
Web 前端(client 39 包 · 138 163 行)要调用后端的服务。传统做法:手写 REST/RPC 接口 + 手写类型定义 + 手动保持同步。
typert 的做法:从 TypeScript 类型生成类型图,加载器和运行时注册表据此提供端到端类型安全的 RPC。
配套文档 docs/api-gateway.md 与 docs/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 面。例外被显式记录,而不是默默存在。
第 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 化——所以改一句提示词只会让一个快照变一行,而不是几十个快照全红。
这是快照测试规模化的关键技巧,值得单独抄走。
第 21 章 名册读法与六大功能域
21.1 这份名册怎么来的
我写了一个普查脚本(scripts/dsh-plugin-census.py),对 241 个包逐个提取:
| 字段 | 提取方式 |
|---|---|
| 包名 | package.json 的 name |
| 角色 | 由包名前缀 + 是否声明 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 同时拥有 agentLoop 与 configuredAgentIdentities)。
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
第 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,普查数据)——这五个就是"跑一轮对话"的全部依赖。而它自己被配置挂载,可替换。
域一的三个观察:
core/tools13 743 行是单包第二大——工具注册表 + 五段流水线 + 呈现模式,复杂度集中在这里session组 13 包把"日志"拆成核心 + 持久化 + 投影 + 标题 + 遥测 + 引用解析,因为它们的演进节奏不同session-query4 包 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 |
第 23 章 域三 · 执行世界(40 包)与域四 · 任务治理(39 包)
23.1 域三导读:两个原语 + 八种能力
见第 17 章。这一域最重要的读法是看依赖方向:fs 与 subprocess 是原语,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 |
第 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"]
三个观察:
dsh-base是一个 117 行、没有运行时 API 的包——它的全部实质是一个cordis.patch.yml。JSDoc 原话:"this module carries no runtime API"。配置即架构的字面体现。typert11 包 15 404 行是这一域最大的投入,见第 19 章。host组 8 包 22 412 行提供宿主能力(dshHomePathlaunchEnvironmentcmdlineArgsdirectoryPickerappExit等键)——把"进程/机器"这一层也做成了服务。
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) > |
— | 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 |
第 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):横切所有包,与业务无关 - 主轴层(
session→agent→tools→system-prompt,llm横切):业务核心 - 前端层(
api-remotes→client-*):独立的依赖社区
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/status → followup |
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
第 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/srcneeds a realpwsh: without one its executor suites self-skip andvitest.config.tsexempts 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
binruns builtlib/bin.jsunder plainnode, 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 here — We 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).
第 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.ts、run-gates.ts + run-gates.spec.ts、gen-doc-graphs.ts + gen-doc-graphs.spec.ts、translation-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 是被门禁强制的。
第 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 给了理由:
(
refactoris deliberately absent — it overlapssimplification, 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, orshape, ask whether a more exact term names the subject: writeresponse fields,JSON validation, orESM exportsinstead ofresponse shape,validation boundary, ormodule 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 多份子系统文档读起来密度极高却不冗长。
第 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.md 里 dsh-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.dynamicCordisRunnerfrom@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.
五个安全设计:
- 不在任何发货树里——刻意的 opt-in,因为动态包代码会触达真实运行时
- 需要
ctx.dynamicCordisRunner(来自独立的 host-runner 包),它持有定义注册表与 vm 沙箱 - 组合里缺这个服务,工具永不激活
- 运行中的包可以注册额外的模型可见工具——直到它被 stop / undefine / DSH 重启
- 工具集变化会被完整的 changed request header 记录到日志——回到第 10 章的宪法:工具表是模型可见的,所以它的变化必须已记录
29.3 为什么这是"一切皆插件"的逻辑终点
如果一切都是插件,而 agent 能写代码,那 agent 就能写插件。而如果注册是可逆的 effect,那 agent 挂载的插件也能被干净地卸载。
这个能力不是加上去的,是架构推导出来的。 它同时也是对架构的压力测试:如果动态挂载一个插件会破坏什么,说明那个东西不该是特权状态。
本系列里最接近的是 DeepTutor 的 subagent(把编程 CLI 当 KB)和 Open Design(把 CLI 当引擎),但那两个是"调用外部 agent"。dsh 这个是agent 修改自己的运行时装配——量级不同。
第 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 描述 | 双轴路径 + 封闭分类 + 三条门禁 | 每个非平凡改动多一篇文档 |
第 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 三个别家没有的组合
dsh-invariants被 218/241 个包依赖——运行时不变量是包的出生证明的一部分,而且规定了"不许写假装在检查的断言"。- 单调守卫——用类型(没有
allow返回值)保证安全策略在数学上不可能被后续插件放宽。 - 686 篇双轴分类的 Agent Notes + 3 条门禁——把"为什么这么设计、我们放弃了什么"制度化到有生命周期、有封闭分类、有归档冻结规则、连"不要加索引"都有一篇笔记。
第 32 章 诚实边界
- 它是 developer preview。README 用大写警告 "THERE WILL BE COMPATIBILITY-BREAKING CHANGES",
SESSION_FORMAT_VERSION保持0且明确不做兼容承诺,AGENTS.md有专门的 "pre-release stance" 一节并给自己写了删除条件。现在拿它做生产要有升级预算。 - 体量巨大:497k 行 TS + 67k 行 TSX + 241 包。本分析抓宪法、主轴、协作图谱与制度,不是逐包深读——client 39 包 138k 行、typert 11 包 15k 行、extensions 4 包 20k 行都只做了名册级覆盖。
- 学习曲线是真实成本。读懂一次请求要同时装下服务仓库、inject 解析、四种 dispatch、effect 生命周期、seam 三角色、装配分层六件事。架构文档自己也说 "We recommend using an agent to explore the codebase"(
docs/architecture.md:7)——它建议你用 agent 来读它。 - 间接层让调试变难。想知道
ctx.shell是谁得看装配而非代码。所以才需要--dump-config、cordis_inspect_*、module-graph.md这一整套导航工具——这些工具的存在本身就是代价的证据。 verify-*门禁的维护成本。28 个门禁 + 各自的.spec.ts+ 124 个脚本,这套体系需要人持续喂养。对小团队是净负担。- 名册的角色分类是我推断的。普查脚本按"包名前缀 + 是否声明 ctx 键 + 是否注册工具"分类,与仓库自己的
packages/README.md分组可能有差异;职责描述那一列是源码 JSDoc 原文,可信度更高。 inject提取不完整。Cordis 声明依赖有多种写法(export const inject、static inject、Service 构造),我的脚本覆盖前两种,所以 101 个包被识别为"有 inject"可能低估。包间依赖(229 个包有)那一列更可靠。- BENCHMARK.md 几乎是空的。全文只说"跟着 Python SDK 指南跑
jsonrpc-agent最小变体,用独立 workspace 与 session id"。这个仓库的质量信心来自门禁与快照,不来自公开跑分。 - 没有验证过运行时行为。本分析是静态阅读 + 交叉核对文档与源码,没有真跑
pnpm dsh web(需要 API key 与完整构建)。时序图与流水线图是从源码与生成文档推导的。 - 行号锚定
47f9438。这是一个日更频率极高的仓库(HEAD 就是当天的 merge commit,PR 编号已到 #2519),行号会漂,回核请以符号名与文件名为准。
第 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.md 的 run_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 和本系列其它文档的衔接
- 想动手造一个 → 从零构建插件化 Agent Harness(20 步 · minidsh)
- 同公司另一条路 → DeepSeek-Reasonix 源码分析(单二进制 + 缓存优先)
- 不写循环的极端 → Open Design 源码分析(把别人的 CLI 当引擎)
- 循环型的经典 → Claude Code 工作原理科普(深入版)
- 沙箱与权限对照 → openai-codex 源码分析(三平台 OS 围栏)
- 三层插件与教育轴 → DeepTutor 源码分析
- 工程手法横向 → Agent 工程模式目录
附录 A · 版本基线指纹
| 项 | 值 |
|---|---|
| Repo | https://github.com/deepseek-ai/deepseek-harness |
| 版本 / Commit | v0.1.0-rc.5(package.json)/ 47f9438 |
| 许可 | MIT |
| workspace 包总数 | 241(packages/ 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-invariants ← 218 包(90%) |
verify-* 门禁 |
28(scripts/ 共 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% |
附录 B · 三个最独特设计(给总目录卡片用)
- 没有特权内核:模型适配器、工具注册表、会话日志、乃至 agent 循环本身都是插件,73 个
ctx服务键可从配置替换;--dump-config打印的每一行都能被自己的 patch 换掉。 - 能改与只能看在类型层面分开:三道 waterfall 可改写,
tools/result是emit+Readonly+Object.freeze+ 运行时不变量;安全边界另用单调守卫(返回类型里没有allow,数学上无法被后续插件放宽)。 - 纪律机械化到极致:逐文件 100% 覆盖率(把未覆盖行当死代码信号)、28 个
verify-*门禁(含强制每包 README 写"局限"与"模型体验")、dsh-invariants被 218/241 包依赖、686 篇双轴分类的 Agent Notes(连"不要加索引"都有一篇)。
附录 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 的高频演进仓库,回核请以符号名与文件名为准。