Subject: deepseek-ai/deepseek-harness (product name dsh) Baseline:
v0.1.0-rc.5@ commit47f9438(Merge pull request #2519 from deepseek-harness/feat/npm-public, 2026-08-13) License: MIT (LICENSE) · third-party disclosures inTHIRD_PARTY_NOTICES.mdCode scale: 497 489 lines of TypeScript (2 319 files) + 66 633 lines of TSX (259 files) + 4 286 lines of Python; 241 workspace packages Service and tool surface: 73ctxservice keys · 47 model-visible tools · 28verify-*gates · 686 design-decision documents One-sentence product: DeepSeek’s official open-source agent harness. The architecture is “everything is a plugin” — the model adapter, the tool registry, the session log, and even the agent loop itself are plugins, all replaceable from configuration. Audience: engineers and architects who have read any coding-agent analysis in this series. Narrative: five-beat motivation → constraint → rejected alternatives → choice → cost. Every fact is taggedfile:lineso you can check the source. Local baseline path:参考项目/deepseek-harness @ 47f9438Companion tutorial: Build a pluginized agent harness from scratch (20 steps to a minidsh)
Contents
Part I · Origin: why DeepSeek built its own harness 1. Pain and insight: a model company is missing a skeleton, not a model 2. Evidence of motive: the original wording in README / AGENTS.md / the paper 3. Macro stack: the numeric shape of 241 packages and the direction of dependence
Part II · Constitution: five ideas and three layers of order 4. A developer mind map: every design follows from “everything is a plugin” 5. Cordis’s five ideas: spatially replaceable × temporally unwindable 6. The capability seam: three roles, or it is not a seam 7. Three event domains and four dispatch modes: “may mutate” is written into the type
Part III · Spine: loop, log, tools
8. Turn and step: one loop, two boundary levels
9. Inbox and agent/turn-stopping: keep the loop going without changing the loop
10. “Model-visible ⟺ logged”: turning the constitution into a runtime assertion
11. The tool pipeline: three waterfalls + monotonic guards + frozen results
12. System prompt and scope: reassemble every step, isolate per agent
Part IV · Context economics and task governance 13. Four service keys for one concern: compaction / spill / pruner / tokenMeter 14. goal / plan / todo / schedule / jobs: “no free-form judgment” as plugins 15. subagent, 11 packages: from a child process to “one turn of someone else’s product” 16. skill / hooks / mcp: living with other people’s ecosystems
Part V · Execution world and remote surface
17. One execution world: swap two providers, five capabilities move with them
18. Sandbox: four-platform confine(argv) and “isolation strength is queryable”
19. typert: type-graph-driven RPC, and the ambition of 11 packages
20. Four fronts, one behavior: CLI / Web / ACP / SDK
Part VI · Full roster of 241 packages and the collaboration graph 21. How to read the roster, and the six functional domains 22. Domain I · Spine (30 packages) and Domain II · Context economics (11 packages) 23. Domain III · Execution world (40 packages) and Domain IV · Task governance (39 packages) 24. Domain V · Assembly and host (51 packages) and Domain VI · Frontends and support (70 packages) 25. Collaboration graph: in-degree, assembly chains, plugin participation in one turn
Part VII · Engineering constitution: the strongest set this series has seen
26. Per-file 100% coverage: coverage as a deletion tool
27. 28 verify-* gates: welding spoken convention into machine gates
28. 686 Agent Notes: institutionalizing the archaeology of design decisions
29. Dynamic Cordis: the agent mounts its own plugins
Part VIII · Taste and bounds 30. Sixteen decisions, five-beat recap 31. Cross comparison: harness-shaped vs loop-shaped 32. Honest bounds 33. Source tour index and local reproduction
Chapter 1 · Pain and insight: a model company is missing a skeleton, not a model
1.1 Answer “why” first
When a model company open-sources an agent framework, the motive is not the same as an application company’s. An application company wants a product. A model company wants a carrier that can fully release the model’s capability, and that carrier has to be reshapeable by outsiders into any form.
DeepSeek Harness’s README is only 55 lines, but the first sentence already states the architectural stance (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.
Three points of extremely high information density:
- Everything is a plugin — not “plugins are supported,” but “only plugins.”
- The substrate is Cordis, an independent plugin framework with a paper, vendored into the repository.
- The paper title’s spatiotemporal composability — in engineering terms: spatially, services are replaceable by key; temporally, registrations unwind in reverse order.
1.2 The concrete pain it is solving
Of the 20-plus agent projects this series has analyzed, the overwhelming majority have this shape: one main loop, plus a pile of features that grew around the main loop. That shape has a shared disease:
Every new requirement comes back and patches the main loop.
Permission confirmation inserts itself before tool execution, context compaction inserts itself into the while, swapping model vendors edits the adapter function, supporting a second frontend copies a loop. Three months later the main loop is a thousand-line mud ball, and no external contributor can extend it without changing it.
dsh’s insight: first build a runtime with no privileged kernel, then make the loop itself a plugin.
The sentence at docs/architecture.md:13 is the spine of the entire architecture document:
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.
“There is no privileged kernel to patch” — you extend dsh by mounting a new plugin beside the others, not by stuffing code into the center.
1.3 Three verifiable engineering claims
| Claim | How to verify | Result |
|---|---|---|
| The loop itself is a plugin | Is agent-loop a package? |
Yes. packages/core/agent-loop, registered through ctx.agentLoop (src/index.ts), replaceable from configuration |
| Configuration is architecture | Can you print the tree that actually boots and replace any row? | Yes. dsh --profile web --dump-config; the docs promise “Any row it prints can be replaced by a patch of your own” (architecture.md:39) |
| Everything the model sees is reconstructable | Is there a runtime assertion? | Yes. ctx.invariants service + an ./invariant companion plugin in 218 packages (docs/subsystems/invariants.md) |
1.4 Relation to earlier work in this series
mindmap
root((Agent 架构的三种形态))
循环型
Claude Code
Codex
Cline
特征:主循环是中心
功能围着它长
宿主型
Open Design
特征:不写循环
把别人的 CLI 当引擎
harness 型
DeepSeek Harness
特征:没有中心
循环也是插件
Reasonix 部分接近
但仍是单循环
The same company’s two open-source agents happen to take two roads: Reasonix is “one static binary + cache-first + a transport-agnostic controller,” chasing cost and portability; Harness is “a 241-package monorepo + everything is a plugin,” chasing replaceability and extensibility. That contrast itself is interesting — Chapter 31 will expand it.
Chapter 2 · Evidence of motive: the original wording in README / AGENTS.md / the paper
2.1 Evidence table
| Evidence | Source | What it proves |
|---|---|---|
| “everything is a plugin” | README.md:7 |
The architectural stance is the first sentence, not a row in a feature list |
| “powered by Cordis … Spatiotemporal Composability” | README.md:7 |
The substrate has an independent paper; it is not a homemade toy |
| “There is no privileged core to patch” | docs/architecture.md:13 |
There is no center into which you can stuff code |
| “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 |
The loop is a plugin, written in the body of the architecture document |
“Plugins, not loop changes: changing agent-loop requires updating docs/architecture.md” |
AGENTS.md |
The bar for changing the loop is raised to “you have to change the architecture document” |
| “Model-visible ⟺ logged” | AGENTS.md |
Observability is a constitutional constraint |
| “Registrations are effects” | AGENTS.md |
Unwindability is a constitutional constraint |
| “A capability seam comprises Service Definition / Service Provider / Consumer roles. It is complete, never one role” | AGENTS.md |
The smallest unit of a capability is three roles, not a class |
| “Trust TypeScript at typed same-process boundaries. Do not add runtime validation … solely for values the static interface requires” | AGENTS.md |
Anti-over-defense — a rare rule |
“per-file 100% on packages/*/*/src” |
AGENTS.md / docs/testing.md:10 |
The coverage gate is per-file full marks |
| “We are DeepSeek — do not ration real-API tests.” | docs/testing.md:19 |
The test policy carries a company’s self-awareness |
| “Developer preview … THERE WILL BE COMPATIBILITY-BREAKING CHANGES” | README.md:13 |
A warning in capitals, and the constitution has a dedicated “pre-release stance” section |
2.2 A rare rule: refuse over-defense
Most engineering norms teach you to “validate more.” dsh does the opposite (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.
It lists seven boundaries that must be validated (parser/config, queue, model and tool JSON, durable/file, worker, process, wire) and explicitly says: do not add validation elsewhere.
The reason is a chain reaction of per-file 100% coverage: if you add a defensive branch that cannot fire, you have to write an impossible test for it, or write an exemption comment. So “over-defense” has a direct cost under this constitution.
This is a rule I have not seen a second house write down in this series. It shows that this repository’s norms are not a copied best-practice checklist; they grew out of its own gate system.
2.3 “Pre-release stance”: writing the technical-debt policy into the constitution
AGENTS.md has a section titled "Pre-release stance: foundation over blast radius". The first sentence is:
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.
It even wrote its own deletion condition — delete this section at the first official tag.
This style of “temporary rule with a self-destruct instruction” is far more effective than the verbal promise “we will refactor later.” Accompanying it: SESSION_FORMAT_VERSION stays at 0 and explicitly makes no compatibility promise.
Chapter 3 · Macro stack: the numeric shape of 241 packages and the direction of dependence
3.1 Top-level shape
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 Micro stack: the layers one request actually walks
After the macro, the micro. The technical layers a model request actually walks:
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
The five-layer division of labor is very clean: the assembly layer decides what exists, the framework layer decides how it is wired, the spine layer decides how it runs, the capability layer decides what it can do, the frontend layer decides who is watching.
3.3 In-degree: where the real center of gravity is
I ran a full census of the 241 packages (script in Chapter 33), counting inter-package dependencies in package.json and inject declarations. Packages with the highest in-degree:
| Most-depended-on package | Count | Note |
|---|---|---|
dsh-invariants |
218 | 90% of the 241 packages depend on it — because every package must publish an ./invariant companion plugin |
schemastery |
110 | vendored config validation (runtime schema for Config fields) |
dsh-session |
84 | the session log is the truth; everyone has to read it |
dsh-llm |
82 | message / streaming vocabulary |
dsh-agent |
61 | Agent interface and inbox |
dsh-tools |
47 | tool registry |
dsh-brand |
30 | branded id (the constitution forbids a bare string for a cross-boundary id) |
dsh-system-prompt |
30 | prompt registry |
dsh-timeout |
23 | timeout policy |
dsh-invariants at 218 in-degree is the single most informative number in this analysis. It means “runtime invariants” are not an optional practice of some team; they are part of a package’s birth certificate. Chapter 27 will cover how this constitution operates.
Service-side in-degree (inject declarations):
| Most-injected service | Count |
|---|---|
tools |
31 |
systemPrompt |
20 |
sessions |
17 |
agents |
13 |
subagents |
12 |
llm |
9 |
subprocess |
8 |
tools 31 times, systemPrompt 20 times means these two are the largest confluence points — half the repository is contributing into the tool table and the prompt. That is exactly why they must be registries, not constants.
3.4 The stack in one breath
- Language: TypeScript (ESM everywhere,
strict: true+noImplicitAny; every residualanymust explain why it cannot be narrowed) - Package management: pnpm 11.7 workspaces, node
^22.19 || >=24 - Framework: vendored Cordis (plugins / services / events / effects)
- Build: tsc emits lib/types + tsdown builds the runtime bundle; source face and artifact face are strictly unmixed
- Tests: vitest × 6 layers (unit / per-file 100% coverage / real-API e2e / keyless snapshot / browser snapshot / gate collection)
- Frontend:
apps/web+client39 packages (138 163 lines, including UI slots / primitives / locale / connection layering) - Native: the Landlock launcher is a C11 static executable (
native/landlock-run/packages/entry/src/main.c), not an N-API addon; the npm name carryingnode-addonis historical naming only. On Linux,sandbox-localtries bwrap first then falls back to Landlock; darwin uses Seatbelt/sandbox-exec; win32 uses a windows-acl restricted token (PLATFORM_CHAINSinsandbox-local/src/index.ts) - Protocol surface: ACP (Agent Client Protocol), JSON-RPC SDK (TS + Python), Typert RPC gateway
Chapter 4 · A developer mind map: every design follows from “everything is a plugin”
The entire repository’s design can be derived from one sentence. This diagram is the skeleton of the lecture:
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 行生成文档
Seven branches, each a necessary condition back-derived from the constraint “there is no center”:
- No center → consumers cannot import an implementation → you need a service warehouse
- Services must be replaceable → interface and implementation must separate → you need the seam’s three roles
- Plugins come and go → registration must be unwindable → you need effect
- Assembly decides behavior → assembly must be inspectable and overridable → you need layered patches + dump-config
- No center holds state → state must live in one shared truth → you need an append-only log
- Anyone can mount a listener → a safety boundary cannot rest on listeners → you need monotonic guards
- Too many rules, and collaborators include AIs → spoken convention will rot → you need machine gates
Chapter 5 · Cordis’s five ideas: spatially replaceable × temporally unwindable
5.1 Five sentences
docs/cordis-primer.md:9-13 defines the entire framework in five sentences:
| # | Idea | Key sentence from the original |
|---|---|---|
| 1 | A plugin is an object that implements a Service | "a function with optional inject and apply(ctx) fields, or a Service subclass whose lifecycle Cordis mounts into the current context" |
| 2 | context is a service warehouse | "other plugins find services via key instead of importing a concrete implementation" |
| 3 | Declare service dependencies with inject |
"load order is expressed through service requirements rather than manual boot sequencing" |
| 4 | Typed Events communication | "declare event names through TypeScript declaration merging, then dispatch them as emit, waterfall, parallel, or serial" |
| 5 | Registration is a reversible effect | "installed through ctx.effect() or ctx.on() so reload and teardown unwind them predictably" |
Item 3 is the turning point of the whole architecture. “Load order is expressed through service requirements, not manual boot sequencing” — that one sentence kills the nastiest class of complexity in agent frameworks: the boot-order table.
5.2 How a service key is declared: declaration merging
// packages/shell/shell/src/index.ts:40-44
declare module '@deepseek-ai/cordis' {
interface Context {
shell: ShellExecutor
}
}
This move gets two things that are usually mutually exclusive:
- Globally visible types: writing
ctx.shell.run(...)in any package has full type hints - Runtime-injected implementation: no import of any concrete implementation
The glossary has a strict constraint on Service Definition (docs/glossary.md:9): it must be a Cordis Service (an abstract class such as ShellExecutor, or a concrete registry such as WebRuntime), “never a TypeScript interface”.
Why not an interface? Because an interface is type-only with no runtime existence, and a seam needs a runtime entity that can be provided onto ctx, can hold a lifecycle, and can be torn down by HMR.
5.3 Duplicate service names: fail loud
The comment at packages/shell/shell/src/index.ts:16-20 explains why the shell settings namespace belongs to the Definition and not to any 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.
Three payoffs in one: the Windows layer switches to PowerShell by replacing config rows rather than adding a branch; mounting two providers at once explodes immediately rather than silently picking one; a settings document carried across platforms still resolves on both sides.
5.4 Four dispatch modes and waterfall semantics
See Chapter 7.
5.5 Cost
| Cost | Concrete manifestation |
|---|---|
| Steep learning curve | Understanding one request requires simultaneous grasp of the service warehouse, inject resolution, four dispatch modes, and the effect lifecycle |
| Many layers of indirection | To know who ctx.shell actually is, you look at assembly config, not at code |
| Debugging needs new tools | Hence --dump-config, the cordis_inspect_* tools, docs/module-graph.md |
| Vendored dependencies must be maintained | vendor/README.md has a manifest + sync procedure + recorded local patches; every sync must re-run test and build |
Item 4 is worth noting: dsh vendors Cordis into the repository rather than taking it as an npm dependency. The vendoring policy in AGENTS.md requires: a pinned source copy, a manifest carrying the upstream SHA, updates through the sync procedure, local patches must be re-applied or retired, and test && build must be re-run after sync.
This is a pragmatic choice: framework and product are on the same evolution cadence; they are not yet at a stage where a version can be locked.
Chapter 6 · The capability seam: three roles, or it is not a seam
6.1 Motive and constraint
Motive: let a capability such as “execute bash” be replaceable as a whole (host / sandbox / remote / PowerShell), while the tool contract the model sees stays unchanged.
Constraint: once the schema the model sees changes, the prompt cache dies, model behavior drifts, snapshot tests all go red. So replacement must happen on a layer the model cannot see.
6.2 Rejected alternatives
| Rejected | Why |
|---|---|
| if/else inside the tool to pick an implementation | Every new implementation edits the tool; the tool becomes a catalog of implementations |
A TypeScript interface as the interface |
No runtime entity; cannot provide, cannot hold a lifecycle, cannot HMR |
| One package that is interface + implementation + tool | Swapping the implementation forks the whole package, tool included |
6.3 Choice: three roles
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 upgrades this to a hard rule:
A capability seam comprises Service Definition / Service Provider / Consumer roles. It is complete, never one role; split only when roles evolve independently.
shell is the officially designated template (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 A refined detail: the request/spec split
AGENTS.md designates shell as the template for “explicit over implicit”:
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 (optional fields) and ShellExecSpec (complete fields) are two types. Default resolution is an explicit step that can be logged and asserted, not a ?? hidden inside run().
The cost of not doing this is concrete: the caller does not know the actual timeout, it cannot be found in the log, it silently changes after a provider swap, tests cannot assert it.
6.5 The Definition’s sense of boundary
The module JSDoc at packages/shell/shell/src/index.ts:1-5:
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.
“Keep executors independent of sessions” — background-job ids, ownership, polling, and notices all belong to ctx.jobs. So the same executor can serve chat, an automation protocol, and a sub-agent at once.
This sense of “push out what is not mine” appears repeatedly in the module JSDoc of 241 packages. It is why this architecture can grow this large without collapsing.
6.6 Cost
- A capability is at least three packages. The repository’s package count therefore inflates to 241.
- Navigation cost: to change “bash timeout” you first have to decide whether to change the Definition (contract), the Provider (implementation), or the Consumer (schema).
- Risk of over-splitting: the constitution writes “split only when roles evolve independently,” and allows one package to hold multiple roles (
dsh-llmis both Definition and Consumer).
Chapter 7 · Three event domains and four dispatch modes: “may mutate” is written into the type
7.1 Three event domains
docs/architecture.md:55 is blunt: "Events are the extension points, and picking the right domain is the first decision in most changes."
| Domain | Semantics | When to use |
|---|---|---|
| Session events | Durable facts appended to the log, broadcast through session/event |
This fact must survive reload |
Agent events (agent/*) |
Carry a live Agent: inbox / step / status / request / validation / continuation |
Observe or intercept in-flight work |
Capability events (fs/*, tools/*, telemetry/*) |
Hang policy and adapters onto a seam | Add policy without importing the loop |
The complete producer/consumer table for the three domains is in docs/event-producer-consumer.md (generated).
7.2 Four dispatch modes
docs/cordis-primer.md:19-24:
| Mode | await? | Order | Has a return value? | Use |
|---|---|---|---|---|
emit |
no | registration order | no | observe |
waterfall |
no | registration order | yes | rewrite, intercept, policy |
parallel |
yes | parallel | no | fan-out |
serial |
yes | registration order | yes | sequential statements |
The key sentence (: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.
The dispatch mode is a public contract, written in JSDoc with an @mode tag, and the generated catalog checks that declarations match actual dispatch sites.
7.3 Technical highlight: separating “may mutate” from “may only watch” with types
Look at the tool registry’s four events (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
The first three are waterfall (may rewrite); the last is emit + both parameters Readonly<> (may only observe).
And it does not rest on types alone — there is also a runtime invariant (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')
Once a result is finalized it is frozen; no observer can mutate it; a violation is caught at runtime.
This is the most meticulous treatment of an “event system” I have seen in this series. In most projects’ EventEmitters, “some listener secretly mutated a finalized result” is a class of extremely hard-to-debug bugs; dsh uses dispatch mode + Readonly + Object.freeze + a runtime assertion as four layers of defense to make it impossible.
7.4 waterfall is around-middleware
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.
Paired with the capitalized rule in AGENTS.md:
Waterfall listeners MUST call
next()to delegate; returning without it short-circuits the chain.
Short-circuit is part of the design (: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.
The danger of this rule is that violating it does not error: a listener that only wanted to log forgets next(), every downstream policy silently dies, and the logs look fine. That is why it is lifted into AGENTS.md with a capitalized MUST.
Chapter 8 · Turn and step: one loop, two boundary levels
8.1 Definitions
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.
Two deliberate phrasings:
- "zero or more steps" — a zero-step turn is legal (see 8.4)
- "once nothing is owed" — the close condition is “nothing is owed,” not “the model finished speaking”
8.2 The full pipeline
The architecture document gives a stretch of pseudocode (:67-82). I mapped it against the source into a sequence diagram:
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)
Source locations: the outer while (await this.turn()) {} is at packages/core/agent-loop/src/agent.ts:212, turn() at :246, step() at :332, buildRequest() at :407.
8.3 Four technical highlights
① max-tokens is 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.
If step 3 of a turn hits the output ceiling and step 4 finishes normally — the turn’s conclusion must still be “hit the ceiling.” Downstream (UI hints, retry policy, eval) uses that to judge whether the result is trustworthy. Downgrading it is lying.
② Every chunk enters the log, and the message points back at its sources (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 lets any final message be traced back to the raw streaming slices that composed it. When you are debugging “why was this answer truncated,” this is life-saving.
③ Request errors are a retryable waterfall, not 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)
Retry policy is a plugin, not a constant in the loop. And a retry re-walks buildRequest — so a retry can swap provider/model.
④ Failure is structured (agent.ts:307-314):
turnEnds = { kind: 'error', error: error instanceof LlmError
? error.failure // LlmError 保留它的事实
: { message: errorChain(error), code: 'UNKNOWN' } } // 其它一切 flatten
turn/end is always emitted in finally (:316-322). Never let a half-turn appear in the log — otherwise replay logic has to handle incomplete state, and that is a bottomless pit.
8.4 Why a zero-step turn must exist
// 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
}
The user pressed send, but some plugin emptied the message. A turn is still opened and closed normally. architecture.md explains (:88):
a rejected or empty first claim still closes a durable turn that spent no step, so the log records the attempt.
The consequence of not doing this: the UI shows “I clearly sent a message but nothing happened,” and the log has no trace. “The user initiated an attempt” is itself a fact that needs to be recorded.
8.5 A constitutional fingerprint: /* 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`)
This is a fingerprint left by the per-file 100% coverage gate: an unreachable defensive branch must be explicitly exempted and the reason written down. And scripts/coverage-exempt.ts + coverage-exempt.spec.ts in turn guard the exemptions themselves. See Chapter 26.
Chapter 9 · Inbox and `agent/turn-stopping`: keep the loop going without changing the loop
9.1 One inbox, two delivery slots
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.
Four input sources, two wake semantics (packages/core/agent-loop/src/agent.ts:122-132):
| API / source | Wake? | Delivery slot |
|---|---|---|
followup / user message |
immediately | next-turn |
| Additional context produced by a tool | not needed (inside the current turn) | next-step |
steer |
immediately | next-step |
inject |
does not wake | next-step (claimed at the current or next already-woken step boundary) |
inject and steer enter the same next-step queue; the difference is only wake. Injection does not open a turn on its own: when idle it only enters the inbox; when running it is eaten only at a step boundary after the already-claimed batch. Otherwise every injected “the current branch is main” would idle-open a round.
The inbox is not a pure in-memory queue. First session.append('agent/inbox/spliced', …), then mutate the live projection (packages/core/agent/src/inbox.ts:157-193). cancel / resume / fork can all reconstruct pending work from the log.
target switching inside the loop (agent.ts:261,300):
let target: InboxTarget = 'next-turn' // turn 的第一个 step 领 turn 队列
// …一个 step 结束后…
target = 'next-step' // 后续 step 领 step 队列
9.2 How tools stuff things into the 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
Two details:
- The sixth argument is a delivery callback — this is where the
tools/post-execute“add context” decision lands. The pipeline document describes it as “Active-batch additionalContexts FIFO → injected user/message after recorded tool results”. concluded ? completed : null— “whether another request is still owed” is reported by the tool batch itself, not guessed by the loop.
9.3 Technical highlight: the “announce–object” pattern of agent/turn-stopping
The two checks before the loop wants to stop (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 // 再查一次
After the serial event is sent, check the inbox again. Because a listener is fully allowed to stuff something new into the inbox at that moment.
This is the textbook answer to “keep the loop going without changing the loop”: give the loop an announcement moment of “I am about to stop,” and any plugin can object at that moment.
agent/turn-stopping is serial and has no next() (architecture.md:84) — every listener is awaited in turn; no one can short-circuit someone else’s right to object. The standard objection from a listener is steer (stuff a message into next-step and wake), then the second inbox check is non-empty, and the turn continues. This is the answer to “keep the loop going without changing the loop” inside the same turn.
Goal continuation in the same session is not this hook. goal-round-driver listens for agent/status entering idle with the goal still armed, then agent.followup opens the next turn (see Chapter 14). Writing goal continuation into turn-stopping is wrong: that would mash “this round still owes a step” and “the goal is not finished, open another round” into one thing.
9.4 Wake latch: teardown waits on no model turn
wakeDriver at agent.ts:164-193:
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 is three states: idle / maintenance / running (agent.ts:38-46). The maintenance phase is a design rarely seen elsewhere — while the driver is doing maintenance work (compaction, title generation) it does not accept wake delivery; it latches and waits for convergence.
And disposal never latches: so teardown waits on no model turn. This class of detail — “teardown must not be held hostage by business logic” — is the watershed between a harness that has run in production for a long time and a demo.
Chapter 10 · “Model-visible ⟺ logged”: turning the constitution into a runtime assertion
10.1 Motive
In the step-0 scheme of const messages = [...], the model’s input lives in memory. Then reconnect after disconnect, session fork, auditing “what did the model actually see,” UI replay of the streaming process, eval reproduction — none of it can be done.
10.2 Choice: the log is the truth, messages are a projection
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.
The direction is crucial: not “the message array happens to be logged,” but “the log is the truth, the message array is a projection.”
The module JSDoc of packages/core/session:
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).
Persistence is a plugin concern — the session service itself never touches disk. That is why the session group has 13 packages: log core + JSONL/zstd persistence + projection + title generation + telemetry + reference resolution…
10.3 Technical highlight: institutionalizing invariants
docs/architecture.md:96 and 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.
The concrete assertion hangs on the llm/stream entry (packages/core/agent-loop/src/invariant.ts:21-52): for requests emitted by agent-loop, freeze GenerateOptions, then require JSON equality between session.deriveMessages() and options.messages, otherwise fail (log-reconstruction desync). The agent/request waterfall cannot change messages; to change history you can only append a session event. Without this: a plugin can silently mutate the outgoing array, live and replay fork, and there is no assertion.
Invariants are not a global assert function; they are a per-package ownership registration service (docs/subsystems/invariants.md):
// packages/runtime-diagnostics/invariants/src/index.ts:94
register(packageName: string, installer: InvariantInstaller): () => void
Constitutional points:
| Mechanism | Detail |
|---|---|
| One companion plugin per package | Every workspace package publishes ./invariant and registers under its own full npm package name |
| Violations are attributable | Throws InvariantError, stable code: 'INVARIANT' + packageName, message prefix invariant violated by "<package>": … — the registry imports no product package |
| Sub-fiber isolation | The installer runs in a dedicated Cordis sub-fiber; installer.inject declares which services that fiber can access |
| Name reservation | Even if a filter turns the check off, the package name is still reserved — two plugins can never silently steal the same name |
| Atomic failure | If the installer fails, the sub-fiber is disposed and the reservation is released, atomically |
| Config fails loud | Regex allow/deny lists, deny beats allow; empty strings / whitespace / duplicates / invalid regex throw at service start rather than skip |
The most meticulous part is what it specifies a check may assert (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.
You may not write an assertion that pretends to check such as “does ctx.shell exist.” When there is no real relationship to assert, the correct move is to export an empty installer and explain why in a comment that starts with No runtime invariant:.
And this too is machine-guarded: pnpm run verify-package-invariants rejects “generated placeholder markers,” “unexplained empty installers,” “non-empty installers that ignore the reporter,” “wrong registration names,” “incomplete export/publish/dependency/bundle wiring.”
This is why dsh-invariants is depended on by 218 packages. None of the 21 projects in this series does this.
10.4 A refined design in the session-log versioning mechanism
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.
Strict by default: an old-version program that reads an unrecognized event type refuses the entire log, rather than skipping it and continuing. Because skipping means “a piece of model-visible history is missing,” and that produces silently wrong replay.
To let an event be ignorable by old versions, you must explicitly mark ignorable: true on the envelope. Safe by default, exceptions explicit.
10.5 Cost
- Any “add some context for the model” requirement must first extend
SessionEventMap+ add rendering, much more work than pushing a message - Log volume is large (every chunk is recorded) — hence JSONL + zstd persistence and a packed-row layout
- 218 packages’ invariant companions are a real maintenance burden (even though most are empty installers with an explanation)
Chapter 11 · The tool pipeline: three waterfalls + monotonic guards + frozen results
11.1 Motive
Tool execution is the place in an agent that most needs plug-and-play: permission, approval, sandbox, timeout, retry, metrics, result rewriting, UI rendering, filesystem read/write policy, hook bridging… stuffing all of that into runTool() is a thousand-line mud ball.
The first sentence of docs/tool-execution-pipeline.md states the design goal:
This graph shows where policy, hooks, sandboxing, filesystem guards, result rewriting, final-outcome observation, and UI rendering run without changing the loop.
11.2 The full pipeline
The module JSDoc of packages/core/tools summarizes it as five stages:
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 Technical highlight one: monotonic guards
There is a design in the pipeline that looks redundant: given the tools/pre-execute waterfall, why an extra guard layer?
Because waterfall has a fatal flexibility: a later-registered listener can override earlier decisions.
// 一个能通过 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
})
So dsh runs a guard layer after the extensible waterfall. The pipeline document’s description:
Registered monotonic guards — deny or abstain; identity protected
Only two options: deny, or abstain. There is no “allow” return value, so it is mathematically impossible to relax anything. Source 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…
The phrasing “after the extensible” is precise: the extensible part runs first, the non-relaxable part runs after. Order itself is the safety design.
| waterfall | monotonic guard | |
|---|---|---|
| Can rewrite someone else’s decision | yes | no |
| Order-sensitive | yes (later wins) | no (any deny is final) |
| A new plugin can accidentally privilege-escalate | yes | impossible |
| Fits | policy collaboration, context supplementation | safety boundary |
11.4 Technical highlight two: pipeline order itself is a runtime contract
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')
}
Stage order is not implied by code structure; it is explicitly asserted by a runtime invariant. A miswritten plugin that bypasses stage order explodes immediately and is attributed to its package name.
11.5 Technical highlight three: UI render intent is part of the design
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.
When a tool is defined it must already say “I should be rendered as an ordinary card, terminal output, or a diff.” And the render function must be a pure function of args — so a pending card can be drawn before execution (presentCall(args) in the pipeline diagram is before tools/pre-execute).
11.6 The catalog of 47 tools is booted, not statically scraped
The file-header comment of docs/tool-catalog.md (1 873 lines):
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.
A tool schema is not statically knowable (runtime-spread enums, concatenated descriptions, config-driven names, raw JSON-Schema from MCP), so the generator actually boots each tool plugin to read the schema. And there is a completeness guard: a new tool cannot be silently omitted from the docs.
The 47 model-visible tools (from my census):
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
That last line is the protagonist of Chapter 29: the agent can define, run, and inspect its own Cordis plugins.
Chapter 12 · System prompt and scope: reassemble every step, isolate per agent
12.1 systemPrompt is a registry, not a template
The module JSDoc of packages/core/system-prompt (1 582 lines):
Registry for ordered system sections, dynamic context, tool schemas, and prompt variables.
Four kinds of contribution. Depended on by 30 packages, inject in-degree 20 — half the repository has to stuff things into the prompt, which is exactly why it must be a registry.
Assembly happens on every step (agent.ts:230):
const assembly = await this.loopCtx.systemPrompt.assemble(assembleContextFor(this, signal))
Not once per turn. Because the tool set can change mid-turn: a sub-agent came up, plan mode exited, cordis_run mounted a new tool (Chapter 29).
assembleContextFor(this, signal) passes in both “which agent is asking” and the cancel signal — assembly is per-agent.
12.2 scope: two levels, flat, no inheritance
The module JSDoc of packages/core/scope (1 215 lines):
Scoped-context primitive: mint a Cordis context that tags registrations with an opaque identity and build routing-only event carriers for that identity.
The glossary’s definition is stricter (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.
Three deliberate restrictions:
- Only two levels (global / a single scope), no multi-level nesting
- No downward inheritance — a sub-agent does not automatically receive the parent agent’s scoped registrations
- When subtree behavior is needed, express it with lineage data, never with scope structure
Why this restraint? Because “scope inheritance” is the easiest place for a permission system to go wrong. Once inheritance is allowed, you have to answer “can a child override the parent’s restriction,” “what about a grandchild,” “what after a fork.” Two-level flatness kills all of those questions.
An instance (dsh-tool-subagent-report in docs/tool-catalog.md):
Registered per continuable in-process child rather than globally, so this schema is visible only inside such a child and survives its global
toolFilter.
The report tool is visible only inside a continuable in-process child, and can pass through the global tool filter — because it is the only channel a child has to report to the parent, and it must not be filtered out.
Chapter 13 · Four service keys for one concern: compaction / spill / pruner / tokenMeter
Context management is one function in most projects. dsh splits it into four independent service keys, because they solve four different problems.
13.1 Problem decomposition
| Service key | Problem | Packages |
|---|---|---|
ctx.compaction |
The conversation is too long: after 20 rounds history exceeds the window | packages/compaction (4 packages · 8 032 lines) |
ctx.spillStore |
A single output is too large: grep matched 30 000 lines | packages/spill (3 packages · 1 473 lines) |
ctx.toolResultPruner |
Tool results need pruning: keep structure, delete redundancy | on the core/tools side |
ctx.tokenMeter |
Need to know how much was spent: metering and budget | docs/subsystems/token-meter.md |
A project that only does the first (compress history) will blow the window on the second case. A project that only does the second (truncate output) will never let the model see the truncated part.
13.2 spill: persist + locator + retrieval guidance
The Definition JSDoc of packages/spill (original wording captured by the census):
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.
Three words are the key: persist + locator (model-facing) + retrieval guidance.
Not simple truncation: the full content is stored, and the model is given a message of “I stored the complete result here; you can read it this way.”
Real usage (dsh-tool-fs-search in docs/tool-catalog.md):
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.
Spilled content is not a grave — what the locator points at can still be processed by read and grep.
13.3 Compaction’s hard part is not the summary; it is “does this count as model-visible”
By Chapter 10’s constitution: a compaction result does count as model-visible, so it must be a session event, not a variable in memory.
That brings three payoffs: compaction is auditable (you can see which range was compacted and what the summary is), replayable (replaying the log yields the same model input), unwindable (delete that event and the original is restored).
The cost is that compaction cannot “mutate the array in place”; implementation complexity is higher.
13.4 ctx.spillStore is optional
Note the phrase above, “the optional ctx.spillStore backend.” Tools still work without a spill backend (they just truncate); with a backend they automatically upgrade to “truncate + locator.”
This is a secondary payoff of seam design: absence of a capability is a legal deployment shape, not a boot failure. Contrast ctx.lsp’s handling (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.
Without a provider, return a structured error, do not change the schema. Because changing the schema kills the prompt cache and drifts model behavior. The contract the model sees must be decoupled from deployment details.
Chapter 14 · goal / plan / todo / schedule / jobs: “no free-form judgment” as plugins
14.1 goal: event-sourced + CAS + human root authority + bounded
The module JSDoc of packages/goal (4 packages · 5 786 lines · ctx.goals):
Same-session goal domain: event-sourced state, compare-and-set mutations, and process-local continuation activation.
The deployment note in the tool catalog (dsh-tool-goal in docs/tool-catalog.md) is extremely dense:
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.
Three designs, unpacked one by one:
① Create / edit / pause / resume require “direct-human root authority.” The model cannot assign itself a goal, nor quietly change a goal. Otherwise “make the tests green” gets rewritten to “comment the tests out.”
② complete / blocked must carry the exact current round. This is compare-and-set: the model says “I completed the goal of round 5”; if it is actually round 6, the claim is invalid. This guards against stale claims.
③ blocked has a lower bound: at least 3 admitted rounds. The model cannot say “I am stuck” on round one; it must actually have tried three rounds.
Continuation does not go through agent/turn-stopping. goal-round-driver (packages/goal/goal-round-driver/src/index.ts:137-205) renders <goal_round> and agent.followup when the agent is idle, the goal is active and armed, and the inbox has no competing work. After resume the durable phase is still there, but activation is process-local state, disarmed by default, to avoid auto-continuation on a cold start. Not one line of loop code is changed: the driver only consumes the public agents / goals / sessions.
14.2 plan: making plan mode logged state
In AGENTS.md’s repository layout, plan is described as "plan mode as logged state".
Compared with Claude Code’s plan mode (a session-level boolean switch + prompt change), dsh’s approach is a ctx.planMode service + log events. The tool catalog’s note on 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.
Three points of care:
- The tool stays in the schema even outside plan mode — because entering and leaving plan mode should not change the tool table the model sees (cache-friendly + snapshot-stable)
- The execute path rejects calls outside the mode — schema stability is not behavioral relaxation
- After approval, “plan mode inactive” is logged at the step boundary — the state change has a definite log moment
14.3 schedule: a versioned timing capability
dsh-schedule in docs/tool-catalog.md:
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.
The phrasing “Version 1” is worth noticing — it treats “this tool’s capability set” as a versioned contract, not an arbitrarily growing parameter table. every_seconds is bounded (you cannot set it to once per millisecond). And it explicitly discloses that delivery is session-local (not cross-session).
14.4 jobs: a kind-agnostic background-job controller
dsh-tool-jobs in docs/tool-catalog.md:
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().
Three tools (job_list / job_output / job_kill) cover every kind of background work. Background bash, PTY sends, sub-agents all go through the same set.
This solves a real model-experience problem: if every kind of background work has its own management tools, the model has to remember 9 tool names instead of 3, and frequently picks the wrong one.
And ctx.jobs’s design decouples executors from sessions (look back at the shell Definition JSDoc in §6.5) — job ids, ownership, polling, notices belong to jobs; the executor only executes.
14.5 todo: allowParallelInProgress is required with no default
dsh-tool-todo in docs/tool-catalog.md:
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.
Required with no default is deliberate. Because “can several in-progress tasks exist at once” is a product-semantics choice, and should not have an implicit default. And that choice changes the tool description text — the same tool, two ways of teaching the model.
This is the landing of AGENTS.md’s “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: loop hygiene
packages/guard has only two sub-packages, no unified entry package: repeat-tool-reminder (injects a reminder when the same tool+canonical args are hammered consecutively, never vetoes) and timeout-policy (wraps a deadline on tools/execute; when a tool declared timeoutMs but does not honor the signal, it yields a structured TOOL_TIMEOUT). AGENTS.md writes “loop-hygiene + tool-timeout plugins”. max-steps / token budget are not in this group.
Chapter 15 · subagent, 11 packages: from a child process to “one turn of someone else’s product”
packages/subagent is the largest group besides client and core: 11 packages · 24 466 lines.
15.1 How wide the provider range is
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.
Behind one interface, from “start a new child agent” to “delegate to one round of conversation in another product.” The census shows package names such as subagent-codex and subagent-claude-code — it treats Codex and Claude Code as subagent providers.
This is a third shape of the same idea as this series’ Open Design (treat someone else’s CLI as the engine) and DeepTutor (treat a coding CLI as a consultable knowledge base): dsh treats them as providers of the subagent seam, so they automatically receive the entire infrastructure of jobs management, session recording, and the tool pipeline.
15.2 Three-layer tool design
The tool catalog has four subagent-related packages, with a clear division of labor:
| Package | Tools | Design point |
|---|---|---|
tool-subagent |
subagent / subagent_fork |
The registration name is load-time config (toolName), so the same package loaded multiple times yields one tool name per backend |
tool-subagent-control |
send_message interrupt_agent list_agents |
Control tools registered globally once, covering all continuable background children |
tool-subagent-report |
report |
Per-child registration, visible only inside the child, and passes through the global toolFilter |
The deployment note on tool-subagent particularly embodies “configuration is architecture”:
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.
The same package loaded twice produces two tools with different semantics (one continuable + default background, one one-shot + default foreground). This is something only a pluginized architecture can do — in a main()-shaped architecture you would have to write two tool classes.
15.3 ralph: an interesting workflow tool
dsh-tool-ralph in docs/tool-catalog.md:
A fixed foreground workflow starts one fresh structured child per round; the model selects only the immutable objective and an optional round cap.
Each round starts a brand-new structured child; the model can only choose the “immutable objective” and a round cap. The model cannot change the objective, nor the flow — another instance of “sink the deterministic skeleton into the tool” (compare the same class of design in DeepTutor Chapter 14).
Chapter 16 · skill / hooks / mcp: living with other people’s ecosystems
16.1 hooks: bridging Claude Code and Codex hook protocols
In AGENTS.md’s repository layout:
hooks/Claude Code/Codex hook bridges + wire-protocol library
packages/hooks (3 packages · 4 933 lines) implements other products’ hook wire protocols. That means a hook script written for Claude Code can run inside dsh.
This is a decision with strategic meaning: lower migration cost. The user’s existing hook investment is not wasted. And it is made into an independent wire-protocol library, not hardcoded into the tool pipeline.
In the pipeline diagram, hooks hang on tools/pre-execute (“hooks · permission · sandbox”) and produce their own session events (hook/invoked / hook/result) — hook invocation and results are also log-visible.
16.2 skill: registry + local implementation + catalog/loader tools
packages/skill (4 packages · 6 242 lines): provider registry + local implementation + catalog/loader tools.
One line in the tool catalog for dsh-tool-skill is worth noticing:
skill|ctx.tools,ctx.agents,ctx.skills|tool/call,tool/result,user/messagereplacement catalogs viaagent.inject()
The skill catalog is updated by replacing the user/message catalog via agent.inject() — looking back at Chapter 9, inject does not wake the loop, so refreshing the skill catalog does not trigger an extra model call; it waits for the next real message and goes in together. This is a clean application of the inbox’s dual semantics.
16.3 mcp: one package, 3 322 lines
packages/mcp has only 1 package. MCP tools’ schemas are raw JSON-Schema (see the generator explanation in tool-catalog.md), so they cannot be statically analyzed and must be read after boot.
Chapter 17 · One execution world: swap two providers, five capabilities move with them
17.1 The architecture’s strongest selling point
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 Code evidence for the claim
The key is the direction of dependence: no upper-layer capability directly imports node:child_process. I verified this in the census — subprocess is injected by 8 packages.
The search tools make the point most clearly (dsh-tool-fs-search in docs/tool-catalog.md):
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”: search does not go through the shell. So search capability does not depend on “whether the machine has bash”; it works consistently in a remote sandbox, on Windows, and in a stripped container.
17.3 e2b is the empirical proof of this claim
packages/e2b (3 packages · 6 630 lines), positioned in AGENTS.md:
e2b/E2B POC: sandbox + FS/subprocess adapters
A POC uses 6 630 lines to prove the architectural promise: swap two providers, five capabilities move with them.
Sizes of the execution-world groups (census data):
| Group | Packages | Lines | Role |
|---|---|---|---|
fs |
7 | 13 044 | filesystem seam + policy (including a read-before-write policy plugin) |
shell |
9 | 10 405 | bash/pwsh seam + 4 providers + 3 consumers |
sandbox |
4 | 9 040 | sandbox seam + policy |
e2b |
3 | 6 630 | remote empirical proof |
terminal |
3 | 5 721 | persistent PTY |
lsp |
3 | 5 496 | language services |
subprocess |
2 | 4 256 | process trees |
code-runtime |
2 | 3 963 | code-mode runtime |
| Total | 33 | 58 555 | all built on two primitive seams |
17.4 code-runtime: Code Mode
The run_code entry in docs/tool-catalog.md is the longest deployment note in the whole lecture; it is worth understanding in full:
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.
Unpacked:
- Under Code Mode,
run_codeis the registry’s only wire contribution — the model sees only one tool - Other capabilities appear as a generated SDK section (in the runtime’s language, e.g. Python/JS)
- The program calls them through bindings, re-entering the complete guarded tool pipeline
- Each nested execution is linked to the outer result
- Concurrency has a native contract: submission-ordered starts; concurrency-safe bodies overlap up to
maxParallelSubCalls
This is a morphological jump from “tool call” to “write a program that calls tools”, and it does not bypass any safety check — nested calls re-walk the guard pipeline. In this series Claude Code’s code execution and Reasonix’s use_capability have explored similar ground; dsh’s version is the most thorough on “nested execution is also constrained by the complete pipeline.”
Chapter 18 · Sandbox: four-platform `confine(argv)` and “isolation strength is queryable”
18.1 The sandbox does not execute; it only wraps argv
The extension-point table in docs/architecture.md:
Confine spawned processes | use a
ctx.sandboxbackend; consumers wrap argv before spawning
This design lets the sandbox compose with any provider: host bash, remote subprocess, LSP child process — anyone can wrap a layer before spawn. If the sandbox itself were responsible for execution, it would fight the executor for the same duty.
18.2 The platform chain is not “Landlock only”
PLATFORM_CHAINS of sandbox-local (packages/sandbox/sandbox-local/src/index.ts:159-165):
| Platform | Mechanism |
|---|---|
| Linux | bwrap first, Landlock on failure |
| darwin | Seatbelt / sandbox-exec, the only candidate, no probe |
| win32 | ACL restricted-token (dsh-sandbox-windows-acl), enforcement reports partial |
The Landlock launcher is in native/landlock-run: the body is C11 main.c (self-restrict-then-exec, the rule set inherited through execve; on failure exit 125, do not exec the target). The npm package name carries node-addon, but it is not an N-API addon; it is a prebuilt static binary + a TS wrapper (grantArgs → --ro/--rw). macOS/Windows have no Landlock port.
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 Two independent service keys
In the census, sandbox and sandboxPolicy are two service keys. “Capability” and “policy” are separated — so policy can be swapped per deployment without swapping the sandbox implementation.
On the tool side you can see traces of policy (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 — there is a controlled privilege-escalation path, not “all forbidden or all open.”
18.4 A postmortem every sandbox author should read
docs/postmortem/0004-landlock-partial-notice-misclassified-child-failures.md
The title is the entire lesson: Landlock’s “partial enforcement” notice once misclassified a child process’s own failures.
When the sandbox can only partially enforce (an old kernel does not support some rule), if the “partial enforcement” hint you emit also counts the child process’s own failure, the user will think it is a sandbox problem and turn the sandbox off — a classification error in a diagnostic message causes the user to voluntarily lower the security level.
dsh has only 4 postmortems in total, and one of them is given to sandbox misclassification. That choice itself shows what they think is worth writing down.
Chapter 19 · typert: type-graph-driven RPC, and the ambition of 11 packages
19.1 What it is
AGENTS.md:
typert/type graph generator, loader, and runtime registryapi/Remote BFF assembly and Typert RPC gateway
packages/typert has 11 packages · 15 404 lines — larger than goal, plan, todo, schedule, jobs, and guard combined. This is not a helper tool; this is a piece of infrastructure.
19.2 What problem it solves
The Web frontend (client 39 packages · 138 163 lines) needs to call backend services. The traditional approach: hand-write REST/RPC interfaces + hand-write type definitions + keep them in sync by hand.
typert’s approach: generate a type graph from TypeScript types; the loader and runtime registry then provide end-to-end type-safe RPC.
Companion docs: docs/api-gateway.md and docs/subsystems/typert.md. The census shows dsh-api-remotes depended on by 23 packages — frontend modules access the backend through it.
19.3 Why this counts as “ambition”
Because it means dsh does not intend to be only a CLI. api/’s positioning is “Remote BFF assembly” (Backend For Frontend) — this is an architecture aimed at multiple clients and multiple deployment shapes.
There is a related sentence in AGENTS.md’s compiler-face rules:
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 is the only package in the warehouse allowed to have multiple compiler aggregates — because it has to face both the host face and the client face. The exception is recorded explicitly, not silently present.
Chapter 20 · Four fronts, one behavior: CLI / Web / ACP / SDK
20.1 Four shells
| Frontend | Packages | Positioning |
|---|---|---|
| CLI | apps/cli (owns the dsh bin) |
interactive terminal + --profile headless one-shot tasks |
| Web | apps/web + client 39 packages (138 163 lines) |
browser app, default 127.0.0.1:3080 |
| ACP | packages/acp (1 669 lines) |
automation-only Agent Client Protocol server |
| SDK | packages/sdk (3 packages) + python/sdk |
JSON-RPC protocol + server + TS client + Python SDK |
AGENTS.md specifically marks ACP as "automation-only" — it is not an interactive UI for humans; it is an automation interface for programs.
20.2 Web Client extension points
One row in the extension-point table of docs/architecture.md:
Add a Web Client Chat node | register a
ConversationNodeDefinition+ keyed renderer
The frontend is pluginized too. Every kind of node in a conversation (message, tool card, diff, terminal output) is a ConversationNodeDefinition + a corresponding keyed renderer.
Layering of the client group’s 39 packages (in-degree in the census): dsh-client-ui-slots 35 times, dsh-client-runtime 34, dsh-client-ui-primitives 29, dsh-client-locale 27, dsh-client-connection 19. UI slots are the frontend’s largest confluence point, corresponding to the backend’s ctx.tools.
20.3 How one behavior is guaranteed
By a snapshot-test matrix (docs/testing.md):
- ACP scenarios:
examples/<name>/tests/snapshots/, boot a real automation-server example, replay a recorded session, diff normalized JSON-RPC plus the re-persisted log - headless backend scenarios:
examples/headless-agentowns internal standard-event JSONL snapshots - CLI interactive journeys:
apps/cli/tests/snapshots/, JSONL-driven - Web browser journeys:
apps/web/tests/snapshots/, Chromium replay (a required Linux PR gate)
And there is a very careful rule:
One ACP scenario (
text-turn) pins full system-prompt/tool-schema content; other fixtures tokenize it so an edit churns one line.
Only one scenario pins the full system-prompt and tool-schema content; other scenarios tokenize it — so changing one prompt sentence only turns one snapshot into one changed line, not dozens of snapshots all red.
This is the key trick for scaling snapshot tests; it is worth copying on its own.
Chapter 21 · How to read the roster, and the six functional domains
21.1 How this roster was produced
I wrote a census script (scripts/dsh-plugin-census.py) that extracts, for each of the 241 packages:
| Field | Extraction method |
|---|---|
| Package name | name in package.json |
| Role | inferred from package-name prefix + whether a ctx key is declared + whether a tool is registered |
Owned ctx keys |
declare module '@deepseek-ai/cordis' { interface Context { … } } in source |
| Injected services | export const inject = [...] and static inject = [...] |
| Model-visible tools | defineTool({ name: '…' }) |
| Lines | all .ts/.tsx under the package (excluding .d.ts) |
| What it does | the first paragraph of the source-top @module JSDoc (translated) |
The last column is the most valuable part of this roster: it is not my summary; it is the one-sentence duty the package author wrote. 227 of 241 packages have such a JSDoc (the missing 14 are fixtures, platform binary packages, and vendored Cordis plugins).
This also side-proves the effect of the verify-export-jsdoc gate — this repository’s module-level documentation coverage is high enough to be used as a roster directly.
21.2 Role distribution
| Role | Count | Note |
|---|---|---|
| Provider/plugin | 97 | implements some seam, or a pure policy/glue plugin |
| Service Definition | 60 | owns at least one ctx key |
| Web client module | 39 | client group |
| Consumer (tool) | 24 | registers a model-visible tool |
| Support library | 13 | util / test-support |
| Bundle | 3 | distribution layer |
| Other | 5 | fixtures etc. |
60 Service Definitions own 73 service keys (a few packages own multiple keys, e.g. agent-loop owns both agentLoop and configuredAgentIdentities).
21.3 Six functional domains
52 package groups rearranged by function into six domains; the next three chapters expand domain by domain:
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
Chapter 22 · Domain I · Spine (30 packages) and Domain II · Context economics (11 packages)
22.1 Domain I guide: five keys hold up the whole product
The 30 spine packages provide only seven core service keys: sessions systemPrompt tools agents agentLoop llm + scope (a library, no key). But they are the mount surface for every other plugin:
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 injects five services (agents llm sessions systemPrompt tools, census data) — those five are the complete dependency of “run one round of conversation.” And it itself is mounted by configuration, replaceable.
Three observations on Domain I:
core/toolsat 13 743 lines is the second-largest single package — tool registry + five-stage pipeline + presentation modes; complexity concentrates here- The
sessiongroup of 13 packages splits “the log” into core + persistence + projection + title + telemetry + reference resolution, because their evolution cadences differ session-query4 packages, 12 283 lines provide 5 read-only tools so the model can retrieve its own history — the agent can query its own session log, a capability rarely seen elsewhere
core · product-API spine (8 packages · 40,854 lines)
| Package | Role | What it does (source @module JSDoc) |
Wiring | Lines |
|---|---|---|---|---|
tools |
Def | Tool registry, model presentation modes, and pre/guard/around/post/result execution pipeline. | owns tools · injects 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. | owns agentLoop, configuredAgentIdentities · injects 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). |
owns sessions |
8,401 |
agent |
Def | Agent service: live registry, factory delegation, and process-local initiator scope. Concrete creation and driving belong to the loop. | owns agent, agents |
3,206 |
system-prompt |
Def | Registry for ordered system sections, dynamic context, tool schemas, and prompt variables. | owns 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. | owns 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. … | injects tools |
236 |
llm · model-access seam (5 packages · 20,502 lines)
| Package | Role | What it does (source @module JSDoc) |
Wiring | Lines |
|---|---|---|---|---|
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. … | injects 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. |
owns 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 … |
injects llm |
3,705 |
token-meter |
Def | Single replay-aware token-meter service for request and surface pressure. | owns 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. | injects agents |
2,333 |
session · durable session log (13 packages · 21,520 lines)
| Package | Role | What it does (source @module JSDoc) |
Wiring | Lines |
|---|---|---|---|---|
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. |
owns 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. | injects sessions |
4,570 |
session-title |
Def | Log-backed session title service, deterministic fallback, and provider contract. | owns sessionTitle · injects 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. |
injects 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 … | owns 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(). … |
injects 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 … |
owns 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). … |
owns sessionProjectionCache · injects 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 … |
injects sessionProjections |
688 |
session-checkpoint-policy |
Prov | Semantic durability checkpoints for model requests, top-level tool dispatch, and completed agent steps. | injects llm, sessionPersistence, sessions, tools |
559 |
session-title-first-prompt-llm |
Prov | First-human-message model provider for ctx.sessionTitle. |
injects llm, sessionTitle, sessions |
339 |
session-title-all-prompts-llm |
Prov | All-human-messages model provider for ctx.sessionTitle. |
injects llm, sessionTitle, sessions |
142 |
session-query · log retrieval (4 packages · 12,323 lines)
| Package | Role | What it does (source @module JSDoc) |
Wiring | Lines |
|---|---|---|---|---|
session-query-sqlite |
Def | Concrete session-query service with SQLite FTS5 over the live-preferred corpus. | owns launcherSessionQueryPath |
3,936 |
session-query |
Def | Service Definition for combined session-history reads, traces, filters, and full-text search. | owns sessionQuery · injects sessions |
3,770 |
tool-session-query |
Tool | Model-facing, workspace-authorized session-history search and read tools. | injects sessionQuery, systemPrompt, tools · 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. | injects commands |
859 |
22.2 Domain II guide: four keys manage context
See Chapter 13. This domain is only 11 packages and 19 828 lines, but it decides whether a long session can survive.
The context group (4 packages · 10 251 lines) is “request-context plugins” — they decide what enters a request, in what order. The division of labor with systemPrompt is: systemPrompt owns system-prompt sections, context owns runtime-context projections (look back at runtimeContext.project() in Chapter 8).
context · request-context assembly (4 packages · 10,280 lines)
| Package | Role | What it does (source @module JSDoc) |
Wiring | Lines |
|---|---|---|---|---|
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. | injects 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. | owns sessionReferenceResolver · injects 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. … | injects agents |
693 |
compaction · context compaction (4 packages · 8,063 lines)
| Package | Role | What it does (source @module JSDoc) |
Wiring | Lines |
|---|---|---|---|---|
compaction-basic |
Prov | Basic replay-aware compaction backend. | injects 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}. … |
owns compaction |
1,800 |
compaction-tool-result-pruner |
Def | Replay-safe, model-free tool-result pruning service. | owns toolResultPruner · injects tokenMeter |
681 |
command-compact |
Prov | Human-facing /compact command over the backend-independent compaction seam. |
injects commands, compaction |
624 |
spill · oversized-output spill (3 packages · 1,485 lines)
| Package | Role | What it does (source @module JSDoc) |
Wiring | Lines |
|---|---|---|---|---|
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 … |
injects 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. … |
owns spillStore |
225 |
Chapter 23 · Domain III · Execution world (40 packages) and Domain IV · Task governance (39 packages)
23.1 Domain III guide: two primitives + eight capabilities
See Chapter 17. The most important way to read this domain is the direction of dependence: fs and subprocess are primitives; shell / terminal / lsp / code-runtime / search tools are built on them; sandbox is cross-cutting (wraps argv); e2b is the remote empirical proof.
The web group (6 packages · 5 985 lines) is another independent seam: behind ctx.web are swappable search/fetch providers; the model sees only two stable tools, web_search / web_fetch.
fs · filesystem seam and policy (7 packages · 13,088 lines)
| Package | Role | What it does (source @module JSDoc) |
Wiring | Lines |
|---|---|---|---|---|
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. |
injects fs, systemPrompt, tools · 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. … |
injects subprocess, systemPrompt, tools · 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. |
injects fs, tools · tool 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. … | owns 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 … |
injects 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 execution seam (9 packages · 10,453 lines)
| Package | Role | What it does (source @module JSDoc) |
Wiring | Lines |
|---|---|---|---|---|
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. … |
injects shell, shellEnv, systemPrompt, tools · tool 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. … |
injects shell, shellEnv, systemPrompt, tools · tool 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. |
injects terminals, tools · tool 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 … |
injects 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. … |
injects 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). … |
owns 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. |
owns shell |
476 |
subprocess · process trees (2 packages · 4,272 lines)
| Package | Role | What it does (source @module JSDoc) |
Wiring | Lines |
|---|---|---|---|---|
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. … |
owns subprocess |
532 |
terminal · persistent PTY (3 packages · 5,741 lines)
| Package | Role | What it does (source @module JSDoc) |
Wiring | Lines |
|---|---|---|---|---|
terminal-bash |
Prov | Persistent shell PTY backend over the subprocess terminal primitive, shared sandbox policy, bounded output, and provider-owned session cleanup. | injects 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. |
injects systemPrompt, terminals, tools · 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. | owns terminals |
1,290 |
code-runtime · code-mode runtime (2 packages · 3,982 lines)
| Package | Role | What it does (source @module JSDoc) |
Wiring | Lines |
|---|---|---|---|---|
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. | owns codeRuntime |
444 |
lsp · language services (3 packages · 5,528 lines)
| Package | Role | What it does (source @module JSDoc) |
Wiring | Lines |
|---|---|---|---|---|
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. … |
injects 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 … |
injects lsp, systemPrompt, tools · tool 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. … |
owns lsp |
531 |
sandbox · sandbox and policy (4 packages · 9,089 lines)
| Package | Role | What it does (source @module JSDoc) |
Wiring | Lines |
|---|---|---|---|---|
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. | owns 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). … |
owns sandboxPolicy |
581 |
e2b · remote-sandbox POC (3 packages · 6,646 lines)
| Package | Role | What it does (source @module JSDoc) |
Wiring | Lines |
|---|---|---|---|---|
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. | injects 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. | injects 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. | owns e2b |
647 |
web · search and fetch (6 packages · 6,010 lines)
| Package | Role | What it does (source @module JSDoc) |
Wiring | Lines |
|---|---|---|---|---|
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. … |
injects systemPrompt, tools, web · tools 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. |
injects 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. |
injects 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 … |
injects 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. … |
owns 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. |
injects web |
568 |
mcp · MCP bridge (1 package · 3,333 lines)
| Package | Role | What it does (source @module JSDoc) |
Wiring | Lines |
|---|---|---|---|---|
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. … |
injects tools |
3,333 |
Chapter 24 · Domain V · Assembly and host (51 packages) and Domain VI · Frontends and support (70 packages)
24.1 Domain V guide: from YAML to a running tree
The assembly chain (drawn in tutorial step 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"]
Three observations:
dsh-baseis a 117-line package with no runtime API — its entire substance is acordis.patch.yml. The JSDoc’s own words: “this module carries no runtime API.” Configuration-is-architecture, literally.typert11 packages, 15 404 lines is the largest investment in this domain; see Chapter 19.- The
hostgroup, 8 packages, 22 412 lines provides host capabilities (keys such asdshHomePathlaunchEnvironmentcmdlineArgsdirectoryPickerappExit) — the “process/machine” layer is also made into services.
preset · per-session capability assembly (2 packages · 3,814 lines)
| Package | Role | What it does (source @module JSDoc) |
Wiring | Lines |
|---|---|---|---|---|
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. … |
owns agentPresets · injects 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. … |
injects systemPrompt |
244 |
bundle · distribution layer (3 packages · 1,424 lines)
| Package | Role | What it does (source @module JSDoc) |
Wiring | Lines |
|---|---|---|---|---|
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 … |
injects 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. | injects 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 · boot glue (2 packages · 4,094 lines)
| Package | Role | What it does (source @module JSDoc) |
Wiring | Lines |
|---|---|---|---|---|
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 … |
owns 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. … |
owns appExit, cmdlineArgs |
437 |
host · host capabilities (8 packages · 22,513 lines)
| Package | Role | What it does (source @module JSDoc) |
Wiring | Lines |
|---|---|---|---|---|
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 … | owns apiProxy · injects 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. … |
injects 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. … |
owns 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. … | injects webServer |
280 |
host-plugin-inventory |
Prov | Read-only projection of the current Cordis Loader plugin entries. | injects 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 … |
owns directoryPicker |
206 |
api · remote BFF and RPC gateway (2 packages · 4,431 lines)
| Package | Role | What it does (source @module JSDoc) |
Wiring | Lines |
|---|---|---|---|---|
api-gateway |
Prov | Live Typert Remote dispatch over Cordis Services and registered providers. Transport, request correlation, and response envelopes belong to Connection. | injects typert |
3,586 |
api-remotes |
Prov | Host BFF entry and Loader shell for the Remote contribution assembly. | — | 845 |
typert · type-graph runtime (11 packages · 15,404 lines)
| Package | Role | What it does (source @module JSDoc) |
Wiring | Lines |
|---|---|---|---|---|
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 … |
injects 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 |
other | — | — | 364 |
@fixture/host |
Def | — | owns aliased, defaultOnly, demo, ignoredExternal, ignoredInline, ignoredPrimitive |
305 |
@fixture/remote-workspace |
other | — | — | 78 |
@fixture/remote |
Prov | — | — | 55 |
@fixture/client |
Def | — | owns clientBridge |
40 |
@fixture/domain |
Prov | Host-only live Agent object. | — | 23 |
@fixture/write |
Def | Service whose public annotations are intentionally absent. | owns writable |
19 |
sdk · JSON-RPC protocol and clients (3 packages · 4,492 lines)
| Package | Role | What it does (source @module JSDoc) |
Wiring | Lines |
|---|---|---|---|---|
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. … |
injects 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 package · 1,669 lines)
| Package | Role | What it does (source @module JSDoc) |
Wiring | Lines |
|---|---|---|---|---|
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 … | injects agents |
1,669 |
settings · user settings (2 packages · 3,913 lines)
| Package | Role | What it does (source @module JSDoc) |
Wiring | Lines |
|---|---|---|---|---|
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 … |
owns 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 · credential references (2 packages · 1,831 lines)
| Package | Role | What it does (source @module JSDoc) |
Wiring | Lines |
|---|---|---|---|---|
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. … |
owns credentials |
377 |
identity · anonymous identity (1 package · 252 lines)
| Package | Role | What it does (source @module JSDoc) |
Wiring | Lines |
|---|---|---|---|---|
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 · storage domains (4 packages · 3,428 lines)
| Package | Role | What it does (source @module JSDoc) |
Wiring | Lines |
|---|---|---|---|---|
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. … |
owns storageDomain · injects 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. |
injects 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. |
injects 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. |
owns storage |
529 |
workspace · workspace registry (1 package · 2,155 lines)
| Package | Role | What it does (source @module JSDoc) |
Wiring | Lines |
|---|---|---|---|---|
workspace |
Def | Workspace entity registry (ctx.workspaceRegistry): durable workspace records, stable registry order, and header-validated session membership over the domain data form. |
owns workspaceRegistry · injects sessionPersistence, storageDomain |
2,155 |
attachment · attachments (2 packages · 921 lines)
| Package | Role | What it does (source @module JSDoc) |
Wiring | Lines |
|---|---|---|---|---|
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 |
owns attachments |
176 |
feedback · message feedback (2 packages · 2,167 lines)
| Package | Role | What it does (source @module JSDoc) |
Wiring | Lines |
|---|---|---|---|---|
message-feedback |
Def | Durable, lifecycle-bound feedback for finalized assistant messages. | owns messageFeedback · injects 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. |
injects commands |
504 |
runtime-diagnostics · runtime invariants (1 package · 543 lines)
| Package | Role | What it does (source @module JSDoc) |
Wiring | Lines |
|---|---|---|---|---|
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. |
owns invariants |
543 |
extensions · extension assembly (4 packages · 20,305 lines)
| Package | Role | What it does (source @module JSDoc) |
Wiring | Lines |
|---|---|---|---|---|
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. | injects cordisInspect, dynamicCordisRunner, systemPrompt, tools · 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. | owns dynamicCordisRunner · injects tools · tool 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 Domain VI guide: 39 frontend packages and “test infrastructure is a first-class citizen”
client 39 packages · 138 163 lines is the largest group in the warehouse, a quarter of the TS/TSX total. It is itself layered: ui-slots (in-degree 35) → ui-primitives (29) → runtime (34) → connection (19) → locale (27).
test-support 6 packages · 13 344 lines deserves a separate mention: test infrastructure is made into publish-grade workspace packages (e.g. dsh-acp-snapshot is a “suite factory”). Most projects’ test tools are scripts scattered in tests/helpers/; dsh treats them as product.
vendor 9 packages are pinned Cordis source (including cordis-plugin-loader / -hmr / -include / -group); native 4 packages are the Landlock launcher split by platform.
client · Web client modules (39 packages · 138,163 lines)
| Package | Role | What it does (source @module JSDoc) |
Wiring | Lines |
|---|---|---|---|---|
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. | injects 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 … |
owns clientModules · injects 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. | injects 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 … |
injects 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 · product assembly (CLI / Web) (2 packages · 23,928 lines)
| Package | Role | What it does (source @module JSDoc) |
Wiring | Lines |
|---|---|---|---|---|
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 · runnable example assemblies (3 packages · 2,876 lines)
| Package | Role | What it does (source @module JSDoc) |
Wiring | Lines |
|---|---|---|---|---|
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 · test infrastructure (6 packages · 13,391 lines)
| Package | Role | What it does (source @module JSDoc) |
Wiring | Lines |
|---|---|---|---|---|
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. … |
injects 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 · zero-dependency utility libraries (7 packages · 2,225 lines)
| Package | Role | What it does (source @module JSDoc) |
Wiring | Lines |
|---|---|---|---|---|
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. |
owns 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 packages · 6,584 lines)
| Package | Role | What it does (source @module JSDoc) |
Wiring | Lines |
|---|---|---|---|---|
cordis |
Prov | Core context type and root context implementation. | — | 2,702 |
cordis-plugin-loader |
Def | — | owns entry, loader, startTime |
1,161 |
schemastery |
Prov | — | — | 921 |
cordis-plugin-hmr |
Def | — | owns hmr · injects loader, timer |
614 |
cosmokit |
Prov | Array set and normalization helpers. | — | 483 |
cordis-plugin-include |
Prov | — | injects loader |
378 |
cordis-plugin-logger-console |
Prov | Re-export shared console exporter config and base implementation. | — | 173 |
cordis-plugin-timer |
Def | — | owns timer |
148 |
cordis-plugin-group |
Prov | — | — | 4 |
native · Landlock native launcher (4 packages · 343 lines)
| Package | Role | What it does (source @module JSDoc) |
Wiring | Lines |
|---|---|---|---|---|
node-addon-landlock-run-workspace |
other | — | — | 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 |
other | — | — | 0 |
node-addon-landlock-run-linux-x64 |
other | — | — | 0 |
Chapter 25 · Collaboration graph: in-degree, assembly chains, plugin participation in one turn
25.1 In-degree layering
The dependency layers drawn from census data (arrow = is depended on):
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
The three-layer structure is very clear:
- Constitution layer (
invariants/schemastery/brand/timeout): cross-cuts every package, independent of business - Spine layer (
session→agent→tools→system-prompt,llmcross-cutting): the business core - Frontend layer (
api-remotes→client-*): an independent dependency community
25.2 Plugin participation table for one turn
This is the most direct answer to “how they cooperate” — in the same turn, at which moment each plugin joins:
| Moment | Event / hook | Who works here |
|---|---|---|
| Turn opens | turn/start (session event) |
session persistence, telemetry, UI |
| Claim input | inbox.claim |
user messages, tool additional context, jobs completion notices, skill-catalog injection |
| Assemble prompt | systemPrompt.assemble |
20 packages contribute sections: identity, tool schemas, cwd, skills list, todo, plan constraints, subagent-specific sections |
| Decide what the model sees | agent/pre-step ⟨waterfall⟩ |
plan-mode intercept, context projection, permission pre-check |
| Fix request parameters | agent/request ⟨waterfall⟩ |
model routing, reasoning effort, maxTokens, provider-specific parameters |
| Streaming | llm/stream ⟨waterfall⟩ |
llm provider, token metering, CoT handling |
| Each slice | assistant/chunk (session event) |
UI live rendering, persistence |
| Request failure | agent/request-error ⟨waterfall⟩ |
retry policy, provider fallback |
| Before tool call | tool/call (session event) + presentCall |
UI pending card |
| Tool admission | tools/pre-execute ⟨waterfall⟩ |
hooks bridge, permission, sandbox policy, fs observation policy |
| Safety finality | monotonic guards | non-relaxable denial |
| Needs a human | ctx.approval |
interaction group’s approval UI |
| Tool execution | tools/execute ⟨waterfall around⟩ |
timeout, retry, metrics |
| Tool body | ToolDefinition.execute |
one of the 24 Consumer packages |
| Mutation intent | fs/write-intent / fs/edit-intent |
fs policy plugins |
| Result post-processing | tools/post-execute ⟨waterfall⟩ |
spill (oversized persist), pruner (trim), result rewrite, additional context |
| Result finalized | tools/result ⟨emit · frozen⟩ |
telemetry, UI completion card, invariant check |
| Step closes | step/end (session event) |
stats |
| Stop announcement | agent/turn-stopping ⟨serial⟩ |
same-turn objection (typical: steer); not goal’s cross-turn continuation |
| After idle | agent/status → followup |
goal-round-driver opens the next round when idle + armed |
| Turn closes | turn/end (session event) |
title generation, persistence flush, telemetry summary |
This table is the value proof of a pluginized architecture: 19 moments, each with an independent plugin joining, and of agent.ts’s 496 lines not one is written for “approval,” “sandbox,” “spill,” “goal,” or “title.”
25.3 Three typical assembly chains
Chain one: the complete dependency closure of the bash tool
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(间接) 写操作审批
One tool, seven service keys, three provider layers. And its source is only 2 081 lines, because it does not need to care about any of those layers.
Chain two: a legal path for “add context for the model”
某插件 → agent.inject(message)
→ inbox(不唤醒)
→ 下次有唤醒消息时一起 claim
→ append user/message(session 事件)
→ deriveMessages() 投影进模型历史
→ invariant 断言:模型可见 ⟺ 已记录 ✓
Chain three: switch to a remote execution world
patch 掉两行配置:
fs-local → e2b-fs
subprocess-local → e2b-subprocess
自动跟着搬家:bash / pwsh / 持久 PTY / lsp / glob / grep / code-runtime
零 provider fork
Chapter 26 · Per-file 100% coverage: coverage as a deletion tool
26.1 Six test layers
docs/testing.md (49 lines, all substance):
| Layer | Command | Key requirement |
|---|---|---|
| Unit | pnpm run test |
Every registry must have an HMR-safety test (dispose the contributing fiber, assert cleanup is clean); prefer permanent tests of boundaries, error paths, event order, concurrency races, contract regressions |
| Coverage gate | pnpm run test:coverage |
Per-file 100% on packages/*/*/src |
| Real-API e2e | pnpm run test:e2e |
Hit real providers with a key; each suite self-skips by its own key |
| Keyless snapshot | pnpm run test:snapshot |
Boot a real example, replay a recorded session, diff normalized JSON-RPC + the re-persisted log |
| Browser snapshot | pnpm run test:web |
Chromium replay, required on Linux PRs; CI forces read-only DSH_SNAPSHOT=replay |
| Gate collection | doc-sync / hygiene / duplication |
see Chapter 27 |
26.2 Technical highlight: the correct reading of coverage
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.
“An uncovered line is often dead code; the gate is correctly hinting that you should delete it” — treat coverage as a code-deletion tool, not a write-tests KPI.
This explains the /* v8 ignore next -- … */ seen in Chapter 8: an unreachable defensive branch must be explicitly exempted + the reason written down, and scripts/coverage-exempt.ts + coverage-exempt.spec.ts guard the exemptions themselves.
It also explains the “do not over-defend” rule in §2.2 — under a 100% coverage constitution, every surplus defensive branch has a direct cost.
26.3 An honest exception
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.
On a machine without PowerShell the file is exempted so the host stays green; CI ships pwsh and enforces full marks. The exception is written into the docs with a reason, not a silent lowering of the standard.
26.4 Three test rules I think most worth copying
① Verify the world, not the self-report (: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.
“A keyword probe on the agent’s own output lets a cheating agent pass” — that sentence should be carved at the top of every agent project’s test file.
② A guard must be proven to work (:34):
A guard only guards if the regression actually fails it. … add an explicit assertion, and prove it: introduce the regression, watch red, revert.
First introduce the regression, watch it go red, then revert — this is the highest testing standard I have seen in this series.
③ Test the real entry path (: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).
Running source under tsx will mask three classes of real failure. The critical path must be tested with the published artifact.
26.5 The with-key policy: a sentence that carries company identity
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.
“We are DeepSeek; do not ration real-API tests” — this is the most personality-bearing sentence in the warehouse. And it explicitly says: self-skip is so that keyless contributors are not blocked; it is not a cost signal.
Accompanying it is the judgment that “highest value is a 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).
Chapter 27 · 28 `verify-*` gates: welding spoken convention into machine gates
27.1 Constitutional requirement
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.
Two requirements: a mechanically checkable invariant must be wired into a top-level gate that will actually execute; and you must prove that every changed acceptance path rejects illegal input.
27.2 Gate list
124 .ts files under scripts/, of which 28 are verify-*:
| Gate | What it turns into a machine-checkable fact |
|---|---|
verify-export-jsdoc |
every export has JSDoc; function-class exports have @param/@returns |
verify-cordis-config |
a bare plugin in cordis.yml must be in the resolver manifest’s dependencies |
verify-package-invariants |
every package has an ./invariant companion; an empty installer must explain with No runtime invariant: |
verify-built-package-invariants |
the same on the artifact side |
verify-type-equiv |
ts type-equiv code blocks in docs must be type-equivalent to source |
verify-mermaid |
diagrams must render |
verify-md-links / verify-doc-refs |
links and refs must not rot |
verify-md-wrap |
markdown wrap discipline (one physical line per paragraph) |
verify-doc-budgets |
document word-count budgets (over the limit, raise the manifest’s cap) |
verify-doc-site-fragments |
doc-site fragments |
verify-package-readme-limitations |
every package README must state “limitations” |
verify-package-readme-model-experience |
must state “model experience” |
verify-agent-note-classification / -format |
classification and format of design docs |
verify-archived-agent-notes |
freeze rules for archived notes |
verify-translation-pairing / -prompt |
bilingual docs paired paragraph by paragraph |
verify-config-source-ownership |
ownership of config sources |
verify-client-domain-graph |
frontend domain graph |
verify-runtime-closure |
runtime dependency closure |
verify-node-next-types |
NodeNext consumer types |
verify-package-paths |
package paths |
verify-dsh-package-licenses |
licenses |
verify-public-repository-links |
public repository links |
verify-skill-invocation-metadata |
skill invocation metadata |
verify-vendored-links |
vendored links |
verify-md-* etc. |
remaining document discipline |
Also pnpm run hygiene (knip dead code + publint publish norms + workspace constraints + NodeNext consumer checks) and pnpm run duplication (jscpd cross-file TypeScript clone detection).
27.3 Three this series has not seen a second house do
① verify-package-readme-limitations: forces every package README to state limitations. This guards against “over-advertising” — if a package’s docs only say what it can do, not what it cannot, users will use it in the wrong scene.
② verify-package-readme-model-experience: forces a statement of model experience. It requires the author to describe their package from the model’s point of view: what the model will see, how it will misuse it, what behavior the schema wording will cause. This is the institutionalization of “write docs for the model.”
③ verify-type-equiv: type code blocks in docs must be equivalent to source types. So those ts type-equiv fenced blocks in docs/subsystems/*.md are not hand-copied; they are machine-checked against source.
27.4 The gates themselves are guarded
Under scripts/ you see files appearing in pairs: 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…
Guards must be guarded. A broken gate is more dangerous than no gate, because it gives a false sense of safety.
27.5 8 177 lines of generated documentation
These in docs/ are generated (produced by scripts/gen-*.ts, freshness verified by doc-sync):
| Document | Lines | Generation method |
|---|---|---|
config-catalog.md |
3 151 | Config fields of every plugin |
tool-catalog.md |
1 873 | boot each tool plugin and read ctx.tools.schemas() |
module-graph.md |
1 638 | module graph |
persistence-catalog.md |
944 | persistence catalog |
capability-seams.md |
471 | capability graph |
event-producer-consumer.md |
76 | event producers/consumers |
graph-atlas.md |
24 | graph index |
Plus the “Cordis API” blocks on every subsystem page (generated by gen-cordis-catalog.ts, byte-identical on both language sides).
Conclusion: this repository’s documentation is not “written”; it is generated from source + machine-checked for freshness. That is also why I could extract accurate duties for 241 packages with one census script — because JSDoc in source is forced by a gate.
Chapter 28 · 686 Agent Notes: institutionalizing the archaeology of design decisions
28.1 Numbers
English primary files under .agents/notes/ (each also has a .zh.md + .i18n.yaml three-piece set):
| Lifecycle | Count | Meaning |
|---|---|---|
implemented/ |
507 | the decision has shipped, and is kept in sync with what actually shipped |
archived/ |
143 | completed and the rationale no longer guides future work, frozen |
proposed/ |
25 | proposals reviewed before implementation |
rejected/ |
11 | considered then rejected, kept only when the rationale prevents a tempting mistake |
| Total | 686 |
28.2 What it is
.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.
“Why” and “what we gave up” — this is exactly what this series’ analysis methodology (motive → constraint → rejected alternatives → choice → cost) tries to reverse-engineer from code. dsh writes it down at the source.
28.3 Dual-axis path encoding
{lifecycle}/{class}/yyyy-mm-dd-topic-title.md
- lifecycle (top-level directory) = state; notes move between directories as state changes
- class (nested directory) = the kind of decision, from a closed set (defined by
scripts/agent-note-tree.ts; the classification gate rejects other folders):
| class | What it covers |
|---|---|
feature |
a new user- or model-visible capability |
bug-fix |
fix a defect or a hole a postmortem exposed |
simplification |
delete code/behavior/surface without adding capability |
architecture |
a structural decision about shipped source |
process |
tools/policy/process around the code (gates, package manager, vendoring) |
testing |
test infrastructure and policy |
refactor is deliberately absent; the README gives the reason:
(
refactoris deliberately absent — it overlapssimplification, whose discriminator, "does observable behavior change?", already covers it.)
Use one discriminator (does observable behavior change?) to kill a fuzzy category. This is good taxonomic taste.
28.4 Four careful rules
① implemented must stay in sync with what actually shipped:
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).
Only facts are updated (paths, names, structure), not the decision itself. So when you read a 2026-06 note, the paths in it are current, but the decision is of that time.
② archived is frozen:
Archived notes are frozen: never edit or treat them as current authority.
When archiving, the only allowed operations: move the three-piece set, keep Status: implemented, insert the same Archived: YYYY-MM-DD line below the status (in both languages), re-record the sidecar, fix or delete inbound links. This is the only allowed content change.
③ Cross-references must be relative markdown links:
Cross-references between Agent Notes use relative markdown links … — never bare prose or numbers — so they are mechanically checkable and survive moves between folders.
You may not write “see that note about X”; you must write a link. Then verify-doc-refs can check, and after a note moves between lifecycle directories the link can still be repaired.
④ Do not add a centralized index:
Do not add a centralized
INDEX.md; the [no-index Agent Note] owns the rationale.
Even the decision “do not add an index” itself has an Agent Note. This is the extreme of constitutional self-reference.
28.5 Cooperation with AGENTS.md
AGENTS.md:
Non-trivial changes MUST include an Agent Note in the same PR; only mechanical/local edits are exempt.
And verify-agent-note-classification / verify-agent-note-format / verify-archived-agent-notes three gates guard this constitution.
28.6 11 skills for AI collaborators
.agents/skills/:
| skill | Role |
|---|---|
dsh-prose-standard |
prose standard (decides how words such as “contract/boundary/shape” are used) |
dsh-pre-push-checks |
which checks to run before push |
dsh-code-review |
code review |
dsh-doc-standards / dsh-doc-site-sync |
document standards and site sync |
dsh-translate-docs |
bilingual translation (may run only when the user explicitly invokes it) |
dsh-archive-agent-notes |
a calibrated workflow for archiving notes (the README is explicit: use this rather than word count, age, or quota) |
dsh-find-simplifications |
find places that can be simplified |
dsh-merging-stacked-prs |
stacked-PR merging |
dsh-trim-cot-leakage |
trim CoT leakage |
record-browser-gif |
record a browser GIF |
28.7 A rare prose discipline
The “Type safety and documentation” section of AGENTS.md:
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.
“Do not use metaphors” + “before writing contract/boundary/shape, ask whether a more exact term names the subject” — it even gives three pairs of replacement examples, and stipulates that contract is reserved for obligations a caller actually depends on: preconditions, postconditions, invariants, compatibility promises.
Also:
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.
Do not narrate control flow, do not preserve review history, do not restate code. Keep five classes of fact — behavior, failure, timing, ownership, safe use — and link the rationale.
This prose discipline plus verify-doc-budgets (word-count budgets) explains why this repository’s 40-plus subsystem documents read at extremely high density without being verbose.
Chapter 29 · Dynamic Cordis: the agent mounts its own plugins
29.1 What it is
AGENTS.md’s repository layout still writes self-modification/. The repository has no such directory. vitest.config.ts’s coverage exclude still lists packages/self-modification/*/src/**. The actual code is in packages/extensions/:
| Package | Duty |
|---|---|
dsh-cordis-host-runner |
ctx.dynamicCordisRunner: define / run / stop / undefine |
dsh-tool-cordis |
7 tools exposed to the model |
dsh-cordis-client-runner / dsh-ui-cordis |
Client half and UI |
Corresponding tools (the census captured 7):
cordis_define · cordis_undefine · cordis_run · cordis_stop
cordis_inspect_list · cordis_inspect_query · cordis_inspect_self
There is a demo script in package.json: pnpm run demo:cordis — "the agent modifies its own runtime".
29.2 Safety boundary
The deployment note for dsh-tool-cordis in docs/tool-catalog.md is cautious:
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.
Five safety designs:
- Not in any shipped tree — a deliberate opt-in, because dynamic package code reaches the real runtime
- Needs
ctx.dynamicCordisRunner(from an independent host-runner package), which holds the definition registry and the vm sandbox - If the composition is missing this service, the tools never activate
- A running package may register additional model-visible tools — until it is stopped / undefined / DSH restarts
- Tool-set changes are recorded in the log as a full changed request header — back to Chapter 10’s constitution: the tool table is model-visible, so its changes must already be logged
29.3 Why this is the logical endpoint of “everything is a plugin”
If everything is a plugin, and the agent can write code, then the agent can write plugins. And if registration is a reversible effect, then a plugin the agent mounts can also be cleanly unloaded.
This capability is not bolted on; it is derived from the architecture. It is also a stress test of the architecture: if dynamically mounting a plugin would break something, that something should not be privileged state.
The closest in this series are DeepTutor’s subagent (treat a coding CLI as a KB) and Open Design (treat a CLI as the engine), but those two are “call an external agent.” This one in dsh is the agent modifying its own runtime assembly — a different order of magnitude.
Chapter 30 · Sixteen decisions, five-beat recap
| # | Decision | Motive | Rejected alternative | Choice | Cost |
|---|---|---|---|---|---|
| 1 | Everything is a plugin, no privileged kernel | Every new requirement patches the main loop | Main loop + hooks | Cordis service warehouse; the loop is also a plugin | Steep learning curve; many layers of indirection |
| 2 | Vendored Cordis | Framework and product evolve on the same cadence | npm dependency, lock a version | Pinned source copy + manifest + sync procedure | Sync must re-run test/build; local patches must be re-applied |
| 3 | inject expresses load order |
A manual boot-order table is unmaintainable | Explicit boot sequence | Fixed-point resolution + error if not activated | Diagnosing “why didn’t it load” needs new tools |
| 4 | Four dispatch modes | One EventEmitter cannot hold four semantics | Unified emit | @mode written into the public contract + generated catalog checks |
Authors must judge which to use |
| 5 | waterfall MUST call next() |
An observer short-circuit silently disables everything | Convention | Capitalized MUST + docs + tests | Still human-guarded (no static check) |
| 6 | Seam three roles | Swapping an implementation should not fork the tool | interface + if/else | Definition/Provider/Consumer; one role is not a seam | A capability is at least three packages; warehouse is 241 packages |
| 7 | request/spec split | Implicit defaults cannot be logged or asserted | ?? default in run() |
Explicit resolve(request): Spec |
One extra type and one conversion |
| 8 | Append-only log as truth | In-memory messages lose everything | Array + incidentally log | Event sourcing + deriveMessages() |
Log volume is large; adding context requires extending the event table |
| 9 | Model-visible ⟺ logged | Bypassing the log silently distorts | Rely on review | 218 packages’ invariant companions + attributable failure | Real maintenance burden |
| 10 | Turn/step two-level boundary | One level cannot say “is something owed” | Single-layer while | step = one request + tools; turn = until nothing is owed | Two counters and two delivery slots |
| 11 | agent/turn-stopping announcement |
Same-turn continuation should not change the loop | Add a condition in the loop | serial announcement + steer + second inbox check |
One extra event round-trip at stop; cross-turn goal continuation separately walks idle + followup |
| 12 | Monotonic guards | waterfall can be privilege-escalated | Waterfall only | Guards can only deny or abstain | Two admission layers; authors must pick the right one |
| 13 | Result freeze + emit | An observer secretly mutates a finalized result | Unified waterfall | Readonly + Object.freeze + invariant |
Post-processing must finish in post-execute |
| 14 | Configuration is architecture (bundle/profile/patch) | Multiple frontend shapes cannot have three mains |
Environment variables + if | Ordered overlay + whole-row replace + --dump-config |
A patch is a whole-row replace, verbose to write |
| 15 | Per-file 100% coverage | Dead code and fake tests | Overall percentage | Per-file full marks + reasoned exemptions | Needs an exemption mechanism and its guards |
| 16 | 686 Agent Notes | Code and docs cannot carry “why” | Rely on PR descriptions | Dual-axis path + closed classification + three gates | Every non-trivial change costs one more document |
Chapter 31 · Cross comparison: harness-shaped vs loop-shaped
31.1 Comparison with the main projects in this series
| Dimension | DeepSeek Harness | Reasonix (same company) | Claude Code | Codex | Open Design | DeepTutor |
|---|---|---|---|---|---|---|
| Architectural shape | harness (no kernel) | single binary + transport-agnostic controller | loop-shaped | loop-shaped | host-shaped (does not write a loop) | dual-track loop + three-layer plugins |
| Status of the main loop | a replaceable plugin | core, single loop | core | core | does not write one | core (two) |
| Extension method | mount plugins onto 73 service keys | MCP + use_capability |
tools / permissions / hooks | tools / sandbox policy | adapter as data | three-layer plugins |
| Context truth | append-only log + projection | stable prefix + turn tail | session + compaction | session | playbook | UnifiedContext |
| Capability replacement | seam three roles; swap a provider, the whole world moves | provider adapters | limited | limited | swap CLI | swap RAG engine |
| Permission model | pre-execute + monotonic guards + approval | application layer | permission rules | OS three-platform fences | none (patched at the periphery) | ToolMountFlags + sandbox grades |
| Sandbox | confine(argv): Linux bwrap→Landlock (C11 launcher); macOS Seatbelt; Win ACL |
application layer + OS | permissions | Seatbelt/Landlock/Token | — | three backends, two grades |
| Engineering gates | per-file 100% + 28 verify + 686 notes | conventional | conventional | conventional | conventional | conventional |
| Package count | 241 | single binary | — | — | — | 219 (Python) |
| License | MIT | MIT | various | various | depends on the repo | Apache-2.0 |
31.2 Two roads from the same company
Reasonix: one static binary, cache-first, a transport-agnostic control.Controller, three-stage compression soft→snip→summary. The goal is save money + portable.
Harness: 241-package monorepo, everything is a plugin, 73 service keys, six test layers. The goal is replaceable + extensible + long-term maintainable.
This is not a contradiction; it is two different product hypotheses: Reasonix assumes “the user wants a terminal agent that can be stuffed into any environment”; Harness assumes “the ecosystem wants a skeleton that can be reshaped into any form.”
31.3 Applicability bounds of the harness shape
The harness shape wins at: one behavior across multiple frontends, third-party extension, whole-capability replacement (host↔remote), long-term maintainability, welding discipline into gates.
The loop shape wins at: fast onboarding, intuitive debugging (you can see the whole process in the stack), low cost of a small change, no assembly layer to maintain.
A concrete criterion: does your product need to support “the user replaces the execution environment”? If yes (remote sandbox, enterprise intranet, custom runtime), the seam investment of the harness shape will pay back. If not, the 241 packages of indirection are a pure tax.
31.4 Three combinations nobody else has
dsh-invariantsis depended on by 218/241 packages — runtime invariants are part of a package’s birth certificate, and it specifies “you may not write an assertion that pretends to check.”- Monotonic guards — using the type (no
allowreturn value) to guarantee that a safety policy cannot be mathematically relaxed by a later plugin. - 686 dual-axis-classified Agent Notes + 3 gates — institutionalizing “why we designed it this way, what we gave up” to the point of having a lifecycle, a closed classification, archive-freeze rules, and even “do not add an index” having a note.
Chapter 32 · Honest bounds
- It is a developer preview. The README warns in capitals “THERE WILL BE COMPATIBILITY-BREAKING CHANGES”;
SESSION_FORMAT_VERSIONstays at0and explicitly makes no compatibility promise;AGENTS.mdhas a dedicated “pre-release stance” section and wrote its own deletion condition. Using it in production now requires an upgrade budget. - The volume is huge: 497k lines of TS + 67k lines of TSX + 241 packages. This analysis grabs the constitution, the spine, the collaboration graph, and the institutional system; it is not a per-package deep read — client 39 packages 138k lines, typert 11 packages 15k lines, extensions 4 packages 20k lines all received only roster-level coverage.
- The learning curve is a real cost. Understanding one request requires simultaneously loading the service warehouse, inject resolution, four dispatch modes, the effect lifecycle, the seam’s three roles, and assembly layering — six things. The architecture document itself says “We recommend using an agent to explore the codebase” (
docs/architecture.md:7) — it recommends you use an agent to read it. - Indirection makes debugging harder. To know who
ctx.shellis you look at assembly, not code. That is why the whole navigation toolkit of--dump-config,cordis_inspect_*,module-graph.mdexists — the existence of these tools is itself evidence of the cost. - Maintenance cost of the
verify-*gates. 28 gates + each one’s.spec.ts+ 124 scripts; this system needs people to keep feeding it. For a small team it is a net burden. - The roster’s role classification is my inference. The census script classifies by “package-name prefix + whether a ctx key is declared + whether a tool is registered,” which may differ from the repository’s own grouping in
packages/README.md; the duty-description column is source JSDoc original wording, higher credibility. injectextraction is incomplete. Cordis declares dependencies in several ways (export const inject,static inject, Service constructor); my script covers the first two, so “101 packages identified as having inject” may be an underestimate. The inter-package-dependency column (229 packages have it) is more reliable.- BENCHMARK.md is almost empty. The whole text only says “follow the Python SDK guide, run the
jsonrpc-agentminimal variant, use an independent workspace and session id.” This repository’s quality confidence comes from gates and snapshots, not from a public scoreboard. - Runtime behavior has not been verified. This analysis is static reading + cross-checking docs against source; I did not actually run
pnpm dsh web(needs an API key and a full build). Sequence diagrams and pipeline diagrams are derived from source and generated docs. - Line numbers are anchored at
47f9438. This is a repository with extremely high daily-update frequency (HEAD is that day’s merge commit; PR numbers have already reached #2519); line numbers will drift; when re-checking, take symbol names and file names as the authority.
Chapter 33 · Source tour index and local reproduction
33.1 🔍 Source pointer table
| You want to understand… | Read first |
|---|---|
| Architectural constitution (for humans and AIs) | AGENTS.md (root; CLAUDE.md is its symlink) |
| Architectural panorama | docs/architecture.md (129 lines, required reading) |
| Cordis’s five ideas | docs/cordis-primer.md (44 lines, 5 minutes) |
| Strict definitions of terms | docs/glossary.md (seam / scope / lineage…) |
| turn/step loop | packages/core/agent-loop/src/agent.ts:212, 246, 332, 407 |
| Agent interface and inbox | packages/core/agent/ |
| Session log and projection | packages/core/session/ + packages/session/ (13 packages) |
| Tool registry and pipeline | packages/core/tools/src/index.ts; diagram in docs/tool-execution-pipeline.md |
| Pipeline-order invariants | packages/core/tools/src/invariant.ts:94-115 |
| System-prompt assembly | packages/core/system-prompt/ |
| per-agent scope | packages/core/scope/ |
| Seam template (three roles) | packages/shell/ (9 packages: 1 Definition + 4 Provider + 3 Consumer + 1 env) |
| One execution world | packages/fs/ + packages/subprocess/; empirical proof packages/e2b/ |
| Sandbox | packages/sandbox/ + native/landlock-run/ |
| Context economics | packages/compaction/ packages/spill/ |
| Goal continuation | packages/goal/ |
| Sub-agents | packages/subagent/ (11 packages, including codex / claude-code providers) |
| Code Mode | packages/code-runtime/; notes in run_code of docs/tool-catalog.md |
| Type-graph RPC | packages/typert/ (11 packages) + packages/api/; docs/api-gateway.md |
| Assembly layering | packages/boot/ packages/bundle/ packages/preset/ |
| Invariant constitution | docs/subsystems/invariants.md + packages/runtime-diagnostics/invariants/ |
| Testing constitution | docs/testing.md (49 lines, all substance) |
| Gate scripts | scripts/verify-*.ts (28) + scripts/run-gates.ts |
| Design-decision archaeology | .agents/notes/{proposed,implemented,archived,rejected}/ (686 notes) |
| Workflows for AIs | .agents/skills/ (11 SKILL.md) |
| Incident postmortems | docs/postmortem/0001-0004 |
| Generated catalogs | docs/{config,tool}-catalog.md module-graph.md capability-seams.md persistence-catalog.md |
| Full schemas of the 47 tools | docs/tool-catalog.md (1 873 lines) |
33.2 Local reproduction (read-only analysis)
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
Reproduce this lecture’s 241-package roster (scripts in this repository):
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(分组名册)
If you actually want to run it (not required for analysis; needs 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 Handoff to other documents in this series
- Want to build one by hand → Build a pluginized agent harness from scratch (20 steps · minidsh)
- The same company’s other road → DeepSeek-Reasonix source analysis (single binary + cache-first)
- The extreme of not writing a loop → Open Design source analysis (treat someone else’s CLI as the engine)
- The classic of the loop shape → How Claude Code works (deep dive)
- Sandbox and permission contrast → openai-codex source analysis (three-platform OS fences)
- Three-layer plugins and the education axis → DeepTutor source analysis
- Engineering-technique cross-cut → Catalog of agent engineering patterns
Appendix A · Version-baseline fingerprint
| Item | Value |
|---|---|
| Repo | https://github.com/deepseek-ai/deepseek-harness |
| Version / Commit | v0.1.0-rc.5 (package.json) / 47f9438 |
| License | MIT |
| Total workspace packages | 241 (packages/ 219 + vendor 9 + apps 2 + native 4 + others) |
| TypeScript | 497 489 lines / 2 319 files |
| TSX | 66 633 lines / 259 files |
| Python | 4 286 lines / 19 files |
ctx service keys |
73 |
| Model-visible tools | 47 |
| Service Definition packages | 60 |
| Consumer (tool) packages | 24 |
| Provider/plugin packages | 97 |
| Largest group | client 39 packages · 138 163 lines |
| Largest single package | core/tools 13 743 lines |
| In-degree champion | dsh-invariants ← 218 packages (90%) |
verify-* gates |
28 (124 .ts files under scripts/ in total) |
| Agent Notes | 686 (507 implemented / 143 archived / 25 proposed / 11 rejected) |
| Generated documentation | 8 177 lines |
| postmortem | 4 notes |
.agents/skills |
11 |
| Substrate | vendored Cordis (9 packages · 6 584 lines) |
| Coverage gate | packages/*/*/src per-file 100% |
Appendix B · Three most distinctive designs (for catalog cards)
- No privileged kernel: the model adapter, the tool registry, the session log, and even the agent loop itself are plugins; 73
ctxservice keys are replaceable from configuration; every row--dump-configprints can be swapped by a patch of your own. - “May mutate” and “may only watch” are separated at the type level: three waterfalls may rewrite;
tools/resultisemit+Readonly+Object.freeze+ a runtime invariant; the safety boundary additionally uses monotonic guards (the return type has noallow; it cannot be mathematically relaxed by a later plugin). - Discipline mechanized to the extreme: per-file 100% coverage (treat uncovered lines as a dead-code signal), 28
verify-*gates (including forcing every package README to write “limitations” and “model experience”),dsh-invariantsdepended on by 218/241 packages, 686 dual-axis-classified Agent Notes (even “do not add an index” has a note).
Appendix C · Analysis-scope statement
This analysis is based on static reading of 47f9438, covering: the architectural constitution (full text of AGENTS.md), all programmatic documents under docs/, line-by-line of the core/ spine source, the tool pipeline and invariant source, a full census of 241 packages (scripts are reproducible), the test and gate system, the Agent Notes constitution.
Not covered: internal implementations of the client 39 packages and typert 11 packages, the concrete implementation of each provider, the per-note content of 686 Agent Notes, the website/ VitePress projection, the internals of the Python SDK, runtime measurement (did not run pnpm dsh). The extensions group’s duties and safety boundary for tool-cordis / cordis-host-runner have been checked (Chapter 29); remaining extension details are still not expanded line by line.
The self-modification/ in AGENTS.md’s layout and vitest.config.ts’s coverage exclude of that path are stale entries; take packages/extensions/ as the authority.
Line numbers are relative to that baseline. This is a high-frequency-evolution repository whose PR numbers have already reached #2519 and whose HEAD is that day’s merge; when re-checking, take symbol names and file names as the authority.