English lecture · source-traced

DeepSeek Harness source analysis: when “everything is a plugin” is pushed until there is no kernel

**Subject**: [deepseek-ai/deepseek-harness](https://github.com/deepseek-ai/deepseek-harness) (product name **dsh**) **Baseline**: `v0.1.0-rc.5` @ commit **`47f9438`** (`Merge pull request #2519 from deepseek-harness/feat/npm-public`, 2026-08-13) **License**: MIT (`LICENSE`) · third-party disclosures in `THIRD_PARTY_NOTICES.md` **Code scale**: **497 489 lines of TypeScript** (2 319 files) + **66 633 lines of TSX** (25

EN
lecture
source
file:line
no
embellishment
中文
sibling

Subject: deepseek-ai/deepseek-harness (product name dsh) Baseline: v0.1.0-rc.5 @ commit 47f9438 (Merge pull request #2519 from deepseek-harness/feat/npm-public, 2026-08-13) License: MIT (LICENSE) · third-party disclosures in THIRD_PARTY_NOTICES.md Code 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: 73 ctx service keys · 47 model-visible tools · 28 verify-* 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 tagged file:line so you can check the source. Local baseline path: 参考项目/deepseek-harness @ 47f9438 Companion tutorial: Build a pluginized agent harness from scratch (20 steps to a minidsh)


Part 1

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


Part IOrigin: why DeepSeek built its own harness
Part 2

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:

  1. Everything is a plugin — not “plugins are supported,” but “only plugins.”
  2. The substrate is Cordis, an independent plugin framework with a paper, vendored into the repository.
  3. 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.


Part 3

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.


Part 4

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 residual any must 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 + client 39 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 carrying node-addon is historical naming only. On Linux, sandbox-local tries bwrap first then falls back to Landlock; darwin uses Seatbelt/sandbox-exec; win32 uses a windows-acl restricted token (PLATFORM_CHAINS in sandbox-local/src/index.ts)
  • Protocol surface: ACP (Agent Client Protocol), JSON-RPC SDK (TS + Python), Typert RPC gateway

Part IIConstitution: five ideas and three layers of order
Part 5

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

Part 6

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.


Part 7

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): Spec step in the owning implementation, never a hidden ?? default inside run()(the dsh-shell request/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.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.

“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-llm is both Definition and Consumer).

Part 8

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 @mode tag 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.waterfall is around-middleware. A listener receives (...args, next). Call next() to delegate the possibly wrapped result to the next service; return without next() to short-circuit.

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.


Part IIISpine: loop, log, tools
Part 9

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.


Part 10

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.


Part 11

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 raw assistant/chunk events 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 on session/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 SessionEventMap and 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 SessionEventMap member is required-on-read by default — builds that do not know its type refuse the log unless the event carries the envelope's ignorable: true; only structural format changes bump SESSION_FORMAT_VERSION.

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)

Part 12

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 extensible tools/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 of args.

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 globs packages/*/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.


Part 13

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:

  1. Only two levels (global / a single scope), no multi-level nesting
  2. No downward inheritance — a sub-agent does not automatically receive the parent agent’s scoped registrations
  3. 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.


Part IVContext economics and task governance
Part 14

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.spillStore backend; 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 structured LSP_UNAVAILABLE error 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.


Part 15

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

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 absolute at, and bounded fixed-rate every_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_write is session-owned state; UIs render the latest todo/write event as a checklist. allowParallelInProgress is required with no default, so the catalog states its choice: true, whose description invites several in_progress items. A deployment choosing false receives the same tool with a description asking for exactly one active task.

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 Config fields changeable from cordis.yml; a DEFAULT_* 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.


Part 16

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_fork bound to the fork backend. Each instance's description, run_in_background parameter, and system-prompt policy follow its own backgroundMode and enableRunInBackground, so the two shipped schemas are not identical: subagent is continuable and defaults omitted calls to background with automatic settlement delivery, while subagent_fork stays one-shot and defaults them to foreground.

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).


Part 17

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/message replacement catalogs via agent.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.


Part VExecution world and remote surface
Part 18

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) through ctx.subprocess as ordinary foreground calls (never background jobs) — no host rg install 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. Under code it is the registry's only wire contribution; the other visible capabilities are declared in a generated SDK section in the loaded runtime's language, and a program calls them through bindings scheduled under the native concurrency contract (submission-ordered starts and policy; concurrency-safe bodies overlap up to maxParallelSubCalls) that re-enter the complete guarded tool pipeline and link each nested execution to this outer result.

Unpacked:

  1. Under Code Mode, run_code is the registry’s only wire contribution — the model sees only one tool
  2. Other capabilities appear as a generated SDK section (in the runtime’s language, e.g. Python/JS)
  3. The program calls them through bindings, re-entering the complete guarded tool pipeline
  4. Each nested execution is linked to the outer result
  5. 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.”


Part 19

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.sandbox backend; 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.


Part 20

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 registry api/ 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.


Part 21

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-agent owns 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.


Part VIFull roster of 241 packages and the collaboration graph
Part 22

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

Part 23

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:

  1. core/tools at 13 743 lines is the second-largest single package — tool registry + five-stage pipeline + presentation modes; complexity concentrates here
  2. The session group of 13 packages splits “the log” into core + persistence + projection + title + telemetry + reference resolution, because their evolution cadences differ
  3. session-query 4 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
Part 24

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

23.2 Domain IV guide: the shared pattern of governance plugins

The 39 governance packages share a pattern I summarize as “sink determinism into the tool, leave judgment to the model”:

Package Deterministic part sunk into the tool Judgment left to the model
goal round counting, CAS check, blocked lower bound, human root authority whether the goal is actually achieved
plan mode state machine, step-boundary recording, reject calls outside the mode plan content
todo session state, parallelism policy task decomposition
schedule time semantics, bounded frequency, persistence barrier when something should be timed
jobs job ids, ownership, polling, termination what should go to the background
guard loop-hygiene thresholds, tool timeout —
interaction permission presets, one-shot approval semantics —
subagent lifecycle, background delivery, settlement what to delegate to whom
workflow workflow engine, worker threads script content

This division of labor is a different implementation of the same idea as this series’ MiMo Code (independent Goal referee), grok-build (Goal Mode five-piece set), Reasonix (Delivery Profile evidence sign-off), DeepTutor (mastery gates sunk into tools). dsh’s distinctive feature is that each of these is an independent plugin; the loop knows nothing about them.

skill · skills (4 packages · 6,255 lines)

Package Role What it does (source @module JSDoc) Wiring Lines
skill-filesystem Prov Local filesystem skill provider. This package is one implementation of the ctx.skills provider registry. It discovers directory-bundle and flat Markdown skills from project, custom, and user roots, parses YAML frontmatter, and loads bodies through ctx.fs when a filesystem service is present. injects skills 2,401
skill Def Agent skill provider registry. This package owns the Service Definition role of the skill capability seam. Concrete providers such as @deepseek-ai/dsh-skill-filesystem decide where skills come from; this service only merges provider catalogs, resolves the winning skill for a name, and exposes the … owns skills 2,172
tool-skill Tool Durable session skill catalog and model-facing skill loader tool. injects agents, skills, tools · tool skill 1,549
skill-badge Prov Bundled dsh-badge skill provider. injects skills 133

subagent · sub-agent seam (11 packages · 24,555 lines)

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

workflow · workflow engine (4 packages · 7,734 lines)

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

goal · same-session goals (4 packages · 5,812 lines)

Package Role What it does (source @module JSDoc) Wiring Lines
goal Def Same-session goal domain: event-sourced state, compare-and-set mutations, and process-local continuation activation. owns goals · injects agents 2,480
goal-round-driver Prov Same-session goal-round driver over public agent, session, and goal services. injects agents, goals, sessions 1,761
tool-goal Tool Model-facing get_goal, create_goal, and update_goal tools over the persisted same-session goal domain. injects agents, goals, systemPrompt, tools · tools create_goal, get_goal, update_goal 1,134
command-goal Prov Human-facing /goal command over the persisted same-session goal domain. injects commands, goals 437

plan · plan mode (1 package · 2,036 lines)

Package Role What it does (source @module JSDoc) Wiring Lines
plan-mode Def Plan mode is logged per-agent collaboration state: while active, a deployment-owned guidance section is included in each model request, and exit_plan_mode presents the completed plan for user review, while the /plan off command lets a user leave directly. … owns planMode · injects systemPrompt, tools 2,036

todo · todos (1 package · 1,012 lines)

Package Role What it does (source @module JSDoc) Wiring Lines
tool-todo Tool Model-facing whole-list replacement. Each call appends a todo/write snapshot to the calling agent's session; replay is last-write-wins, and UIs render from session events. A non-agent caller has no owning list and is rejected. Named exports preserve loader injection metadata. injects tools · tool todo_write 1,012

schedule · timing (1 package · 4,314 lines)

Package Role What it does (source @module JSDoc) Wiring Lines
schedule Tool Agent-scoped durable one-shot and fixed-rate reminders over the session event log. injects agents, sessionPersistence, sessions, tools · tools schedule_create, schedule_delete, schedule_list 4,314

jobs · background jobs (3 packages · 3,712 lines)

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

guard · loop hygiene (2 packages · 1,027 lines)

Package Role What it does (source @module JSDoc) Wiring Lines
repeat-tool-reminder Prov Advisory per-agent repeat-call detector. It enriches post-execute decisions with logged model context without vetoing or rewriting calls. Configuration and chain semantics live in the package README; rationale lives in the repeat-tool-reminder Agent Note. — 669
tool-call-timeout-policy Tool Cooperative tool-call timeout enforcer. A tool declares timeoutMs and promises to honor exec.signal; this wrapper arms that deadline and maps its own expiry to TOOL_TIMEOUT without racing or abandoning the tool promise. … injects tools 358

interaction · approval / permission / commands / ask-the-human (5 packages · 4,225 lines)

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

hooks · external hook-protocol bridges (3 packages · 4,968 lines)

Package Role What it does (source @module JSDoc) Wiring Lines
hooks-claude-code Prov Bridge for unmodified Claude Code command hooks on harness interception extension points. It supports SessionStart, prompt/tool pre/post, Stop, and subagent start/stop. … injects shell 1,837
hook-protocol Prov Shared, non-plugin hook protocol library: matching, command execution and decoding, restrictive outcome merging, durable event helpers, and detached run quiescence. Claude Code and Codex bridges own their distinct payloads, environment rules, matcher mode, and typed extension-point mappings. — 1,702
hooks-codex Prov Bridge for unmodified Codex command hooks on harness interception points. It supports five points (SessionStart, prompt/tool pre/post, Stop), regex-only matchers, snake_case payloads without a trailing newline, no hook environment or command substitution, and no pre-tool approval or rewrite path; on … injects shell 1,429
Part 25

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:

  1. dsh-base is a 117-line package with no runtime API — its entire substance is a cordis.patch.yml. The JSDoc’s own words: “this module carries no runtime API.” Configuration-is-architecture, literally.
  2. typert 11 packages, 15 404 lines is the largest investment in this domain; see Chapter 19.
  3. The host group, 8 packages, 22 412 lines provides host capabilities (keys such as dshHomePath launchEnvironment cmdlineArgs directoryPicker appExit) — 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) > /.env (read-only fallback) > $D … — 1,454
credentials Def Service Definition for the credential-reference capability seam (ctx.credentials). Settings and composition files carry references to secrets — environment-variable names — while providers own the actual values and their storage. … 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
Part 26

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, llm cross-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

Part VIIEngineering constitution: the strongest set this series has seen
Part 27

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/src needs a real pwsh: without one its executor suites self-skip and vitest.config.ts exempts the file so pwsh-less hosts stay green, while CI runners ship pwsh and enforce the full bar.

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 bin runs built lib/bin.js under plain node, 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).


Part 28

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.


Part 29

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:

(refactor is deliberately absent — it overlaps simplification, 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, or shape, ask whether a more exact term names the subject: write response fields, JSON validation, or ESM exports instead of response shape, validation boundary, or module shape.

“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.


Part 30

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.dynamicCordisRunner from @deepseek-ai/dsh-cordis-host-runner, which owns the definition registry and the vm sandbox; a composition missing it never activates the tools. A running package may register ADDITIONAL model-visible tools until it is stopped, undefined, or DSH restarts; a full changed request header logs those tool-set changes.

Five safety designs:

  1. Not in any shipped tree — a deliberate opt-in, because dynamic package code reaches the real runtime
  2. Needs ctx.dynamicCordisRunner (from an independent host-runner package), which holds the definition registry and the vm sandbox
  3. If the composition is missing this service, the tools never activate
  4. A running package may register additional model-visible tools — until it is stopped / undefined / DSH restarts
  5. 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.


Part VIIITaste and bounds
Part 31

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

Part 32

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

  1. dsh-invariants is 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.”
  2. Monotonic guards — using the type (no allow return value) to guarantee that a safety policy cannot be mathematically relaxed by a later plugin.
  3. 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.

Part 33

Chapter 32 · Honest bounds

  1. It is a developer preview. The README warns in capitals “THERE WILL BE COMPATIBILITY-BREAKING CHANGES”; SESSION_FORMAT_VERSION stays at 0 and explicitly makes no compatibility promise; AGENTS.md has a dedicated “pre-release stance” section and wrote its own deletion condition. Using it in production now requires an upgrade budget.
  2. 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.
  3. 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.
  4. Indirection makes debugging harder. To know who ctx.shell is you look at assembly, not code. That is why the whole navigation toolkit of --dump-config, cordis_inspect_*, module-graph.md exists — the existence of these tools is itself evidence of the cost.
  5. 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.
  6. 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.
  7. inject extraction 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.
  8. BENCHMARK.md is almost empty. The whole text only says “follow the Python SDK guide, run the jsonrpc-agent minimal variant, use an independent workspace and session id.” This repository’s quality confidence comes from gates and snapshots, not from a public scoreboard.
  9. 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.
  10. 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.

Part 34

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


Part 35

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%
Part 36

Appendix B · Three most distinctive designs (for catalog cards)

  1. No privileged kernel: the model adapter, the tool registry, the session log, and even the agent loop itself are plugins; 73 ctx service keys are replaceable from configuration; every row --dump-config prints can be swapped by a patch of your own.
  2. “May mutate” and “may only watch” are separated at the type level: three waterfalls may rewrite; tools/result is emit + Readonly + Object.freeze + a runtime invariant; the safety boundary additionally uses monotonic guards (the return type has no allow; it cannot be mathematically relaxed by a later plugin).
  3. 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-invariants depended on by 218/241 packages, 686 dual-axis-classified Agent Notes (even “do not add an index” has a note).
Part 37

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.

↑