2026-06-10 22:57:44 +08:00
# AGENTS.md
This is the monorepo for the DeepSeek Harness group. It currently hosts the code for **DeepSeek Code** , DeepSeek's coding agent product.
## Architecture
Document the architecture and rewrite AGENTS.md
docs/architecture.md: layering, service map, event taxonomy, the
session/turn/step lifecycle, Cordis waterfall semantics, an extension
cookbook, the plugin sanity checklist mapping every MVP feature to its
extension mechanism, and the deferred-work TODO list (sub-agents,
persistence backends, compaction, DeepSeek V4 adapter, parallel tool
execution, streaming-protocol review).
AGENTS.md: repo layout, commands, conventions (dsh-* naming, ESM,
effect-based registrations, declaration merging, waterfall semantics),
and the vendoring policy pointer.
2026-06-11 10:55:05 +08:00
This codebase is based on the **Cordis** framework, built microkernel-style: **everything is a plugin** . All necessary Cordis dependencies are copied into this monorepo as vendored source (under `vendor/` ) instead of being depended on via npm.
Read [docs/architecture.md ](docs/architecture.md ) before changing anything under `packages/` — it defines the service map, the event taxonomy, the session/turn/step lifecycle, and the plugin cookbook.
2026-06-10 23:12:38 +08:00
## Design Documents
- [Coding Harness MVP 需求分析 ](https://trtgsjkv6r.feishu.cn/wiki/ZwK6wfBE9i91V6kzMGYcgRGanxg ) — requirement analysis for the initial MVP.
- [微内核Harness实现思路 ](https://trtgsjkv6r.feishu.cn/wiki/VS9Lw1kQki6mDJk2UHocyuphnsc ) — discussion of the microkernel plugin-style architecture ("everything is a plugin").
Document the architecture and rewrite AGENTS.md
docs/architecture.md: layering, service map, event taxonomy, the
session/turn/step lifecycle, Cordis waterfall semantics, an extension
cookbook, the plugin sanity checklist mapping every MVP feature to its
extension mechanism, and the deferred-work TODO list (sub-agents,
persistence backends, compaction, DeepSeek V4 adapter, parallel tool
execution, streaming-protocol review).
AGENTS.md: repo layout, commands, conventions (dsh-* naming, ESM,
effect-based registrations, declaration merging, waterfall semantics),
and the vendoring policy pointer.
2026-06-11 10:55:05 +08:00
## Repository Layout
```
vendor/ Vendored Cordis framework source (original npm names, private).
See vendor/README.md for the manifest, local-modification log,
and the upstream sync procedure. Do NOT edit casually — every
divergence must be logged there.
packages/ Harness packages, all named @deepseek -ai/dsh-< name > :
2026-06-13 00:28:29 +08:00
llm/ abstract LLM service + content-block vocabulary
llm-deepseek/ DeepSeek API adapter (hand-rolled fetch/SSE)
llm-pi-ai/ DeepSeek adapter via @earendil -works/pi-ai (design twin)
Document the architecture and rewrite AGENTS.md
docs/architecture.md: layering, service map, event taxonomy, the
session/turn/step lifecycle, Cordis waterfall semantics, an extension
cookbook, the plugin sanity checklist mapping every MVP feature to its
extension mechanism, and the deferred-work TODO list (sub-agents,
persistence backends, compaction, DeepSeek V4 adapter, parallel tool
execution, streaming-protocol review).
AGENTS.md: repo layout, commands, conventions (dsh-* naming, ESM,
effect-based registrations, declaration merging, waterfall semantics),
and the vendoring policy pointer.
2026-06-11 10:55:05 +08:00
session/ event-sourced session log + in-memory store
system-prompt/ prompt-section + tool-schema assembly registry
tools/ tool registry + tools/execute waterfall
agent/ Agent interface, registry, agent/* event vocabulary
agent-loop/ THE concrete plugin: LoopAgent + the loop driver
Add bash execution: dsh-bash seam, dsh-bash-local impl, dsh-tool-bash tools
Three packages following the new capability-seam pattern (interface /
implementation / consumer, now documented in docs/architecture.md):
- dsh-bash: abstract BashExecutor service (ctx.bash) + vocabulary types.
- dsh-bash-local: local subprocesses — bash -c per call in a detached
process group, SIGTERM→SIGKILL group kills, tail-keep truncation with
full-stream spill files, model-friendly env, background task registry.
- dsh-tool-bash: the bash / bash_output / bash_kill tool schemas with
runtime arg validation and background completion notices via
agent.inject(). Non-zero exits are reported, not errored.
Design surveyed against the bash tools of Claude Code, OpenCode, Codex,
and pi (notes in the package READMEs). Permissions/sandbox stay TODO on
the tools/execute waterfall seam; stateful-shell alternatives recorded
in run.ts.
2026-06-12 23:28:44 +08:00
bash/ abstract bash executor seam (ctx.bash) — interface only
bash-local/ local-subprocess BashExecutor implementation
tool-bash/ model-facing bash/bash_output/bash_kill tool schemas
Document the architecture and rewrite AGENTS.md
docs/architecture.md: layering, service map, event taxonomy, the
session/turn/step lifecycle, Cordis waterfall semantics, an extension
cookbook, the plugin sanity checklist mapping every MVP feature to its
extension mechanism, and the deferred-work TODO list (sub-agents,
persistence backends, compaction, DeepSeek V4 adapter, parallel tool
execution, streaming-protocol review).
AGENTS.md: repo layout, commands, conventions (dsh-* naming, ESM,
effect-based registrations, declaration merging, waterfall semantics),
and the vendoring policy pointer.
2026-06-11 10:55:05 +08:00
examples/ Runnable demos (not workspaces). echo-agent = mock model + echo
tool + stdio UI + JSONL persistence, wired via cordis.yml.
Add examples/coding-agent and the docs cookbook
The first real agent wiring: DeepSeek V4 + the bash tool suite + stdio
chat + JSONL persistence, runnable via yarn demo:coding (reads the
gitignored repo-root .env through process.loadEnvFile).
- examples/coding-agent: cordis.yml wiring both real plugin families
(llm-deepseek with !!js env secrets; bash-local + tool-bash), a
bash-only coding system prompt, a max-steps-guard plugin (bounds
runaway turns via the agent/turn-continuation waterfall — abort()
from step-end is a no-op by then), and a stdio UI with dimmed
reasoning and exit-on-idle for piped stdin.
- e2e (yarn test:e2e, key-gated): full-loop.e2e.ts runs a real model
against the real bash tool; coding-task.e2e.ts is the swebench-style
smoke — the model fixes a buggy add.js in a temp dir and the test
re-runs node add.test.js itself rather than trusting the agent.
- docs/cookbook: adding-a-package (the verified checklist),
adding-a-tool (execute() contract, background pattern, seams),
adding-an-llm-adapter (protocol obligations, mock-server testing,
e2e policy). AGENTS.md layout/commands/secrets sections updated;
architecture.md points at both examples and the cookbook.
- vitest.e2e.config.ts: serialize test files + retry twice — parallel
e2e files trip the shared internal key's concurrency quota.
- fix: the !js YAML tag spelling in docs/JSDoc is actually !!js
(js-yaml resolves custom tags under tag:yaml.org,2002:js).
2026-06-13 00:47:32 +08:00
coding-agent = the real thing: DeepSeek V4 + bash tools
(yarn demo:coding, needs DEEPSEEK_API_KEY).
2026-06-11 15:27:07 +08:00
docs/ architecture.md — the design doc. adr/ — decision records (the
why behind vendoring, event-sourcing, the schema DSL, …).
rfc/ — proposals for substantial future work.
Add examples/coding-agent and the docs cookbook
The first real agent wiring: DeepSeek V4 + the bash tool suite + stdio
chat + JSONL persistence, runnable via yarn demo:coding (reads the
gitignored repo-root .env through process.loadEnvFile).
- examples/coding-agent: cordis.yml wiring both real plugin families
(llm-deepseek with !!js env secrets; bash-local + tool-bash), a
bash-only coding system prompt, a max-steps-guard plugin (bounds
runaway turns via the agent/turn-continuation waterfall — abort()
from step-end is a no-op by then), and a stdio UI with dimmed
reasoning and exit-on-idle for piped stdin.
- e2e (yarn test:e2e, key-gated): full-loop.e2e.ts runs a real model
against the real bash tool; coding-task.e2e.ts is the swebench-style
smoke — the model fixes a buggy add.js in a temp dir and the test
re-runs node add.test.js itself rather than trusting the agent.
- docs/cookbook: adding-a-package (the verified checklist),
adding-a-tool (execute() contract, background pattern, seams),
adding-an-llm-adapter (protocol obligations, mock-server testing,
e2e policy). AGENTS.md layout/commands/secrets sections updated;
architecture.md points at both examples and the cookbook.
- vitest.e2e.config.ts: serialize test files + retry twice — parallel
e2e files trip the shared internal key's concurrency quota.
- fix: the !js YAML tag spelling in docs/JSDoc is actually !!js
(js-yaml resolves custom tags under tag:yaml.org,2002:js).
2026-06-13 00:47:32 +08:00
cookbook/ — step-by-step guides: adding a package, a tool,
an LLM adapter.
Replace dumble with tsdown for JS bundling
dumble (0.2.x, ~530 dl/wk, single-maintainer) was a bus-factor risk as
the load-bearing bundler. tsdown (rolldown-based, ~2.5M dl/wk, actively
maintained) replaces it while output stays list-identical, verified by
snapshot diff: 17 JS bundles, externals preserved, schemastery dual
.mjs/.cjs and logger-console node+browser entries intact.
Root tsdown.config.ts uses workspace globs ['vendor/*', 'packages/*']
(explicit, so examples/* stays excluded); two per-package overrides in
vendor/ cover the special shapes and are logged in vendor/README.md as
ours (not upstream sync surface). scripts/build.ts (dumble
orchestration) is deleted; yarn build = tsc -b && tsdown. tsc -b keeps
owning declarations (dts: false, clean: false).
Rationale recorded in ADR 0008 (also covers the direct-esbuild and
pkgroll alternatives).
Gates: lint, typecheck, 134 tests, hygiene (knip/publint/constraints),
demo smoke all green.
2026-06-11 22:08:10 +08:00
scripts/ repo maintenance scripts (vendor-manifest guard, publint runner).
JS bundling is tsdown (root tsdown.config.ts + two per-package
overrides in vendor/).
Document the architecture and rewrite AGENTS.md
docs/architecture.md: layering, service map, event taxonomy, the
session/turn/step lifecycle, Cordis waterfall semantics, an extension
cookbook, the plugin sanity checklist mapping every MVP feature to its
extension mechanism, and the deferred-work TODO list (sub-agents,
persistence backends, compaction, DeepSeek V4 adapter, parallel tool
execution, streaming-protocol review).
AGENTS.md: repo layout, commands, conventions (dsh-* naming, ESM,
effect-based registrations, declaration merging, waterfall semantics),
and the vendoring policy pointer.
2026-06-11 10:55:05 +08:00
```
## Commands
```sh
yarn install # Yarn 4 workspaces (node-modules linker), node >= 24
yarn test # vitest run (packages/*/tests/**/*.spec.ts)
2026-06-13 00:28:29 +08:00
yarn test:e2e # real-API tests (packages|examples/*/tests/**/*.e2e.ts);
# self-skips without DEEPSEEK_API_KEY — see Secrets below
Document the architecture and rewrite AGENTS.md
docs/architecture.md: layering, service map, event taxonomy, the
session/turn/step lifecycle, Cordis waterfall semantics, an extension
cookbook, the plugin sanity checklist mapping every MVP feature to its
extension mechanism, and the deferred-work TODO list (sub-agents,
persistence backends, compaction, DeepSeek V4 adapter, parallel tool
execution, streaming-protocol review).
AGENTS.md: repo layout, commands, conventions (dsh-* naming, ESM,
effect-based registrations, declaration merging, waterfall semantics),
and the vendoring policy pointer.
2026-06-11 10:55:05 +08:00
yarn typecheck # tsc -b tsconfig.build.json (declarations only)
Replace dumble with tsdown for JS bundling
dumble (0.2.x, ~530 dl/wk, single-maintainer) was a bus-factor risk as
the load-bearing bundler. tsdown (rolldown-based, ~2.5M dl/wk, actively
maintained) replaces it while output stays list-identical, verified by
snapshot diff: 17 JS bundles, externals preserved, schemastery dual
.mjs/.cjs and logger-console node+browser entries intact.
Root tsdown.config.ts uses workspace globs ['vendor/*', 'packages/*']
(explicit, so examples/* stays excluded); two per-package overrides in
vendor/ cover the special shapes and are logged in vendor/README.md as
ours (not upstream sync surface). scripts/build.ts (dumble
orchestration) is deleted; yarn build = tsc -b && tsdown. tsc -b keeps
owning declarations (dts: false, clean: false).
Rationale recorded in ADR 0008 (also covers the direct-esbuild and
pkgroll alternatives).
Gates: lint, typecheck, 134 tests, hygiene (knip/publint/constraints),
demo smoke all green.
2026-06-11 22:08:10 +08:00
yarn build # typecheck + tsdown JS bundles into each package's lib/
Document the architecture and rewrite AGENTS.md
docs/architecture.md: layering, service map, event taxonomy, the
session/turn/step lifecycle, Cordis waterfall semantics, an extension
cookbook, the plugin sanity checklist mapping every MVP feature to its
extension mechanism, and the deferred-work TODO list (sub-agents,
persistence backends, compaction, DeepSeek V4 adapter, parallel tool
execution, streaming-protocol review).
AGENTS.md: repo layout, commands, conventions (dsh-* naming, ESM,
effect-based registrations, declaration merging, waterfall semantics),
and the vendoring policy pointer.
2026-06-11 10:55:05 +08:00
yarn demo # run examples/echo-agent (needs --expose-internals, the
# script passes it; type "echo hi" to see a tool call)
Add examples/coding-agent and the docs cookbook
The first real agent wiring: DeepSeek V4 + the bash tool suite + stdio
chat + JSONL persistence, runnable via yarn demo:coding (reads the
gitignored repo-root .env through process.loadEnvFile).
- examples/coding-agent: cordis.yml wiring both real plugin families
(llm-deepseek with !!js env secrets; bash-local + tool-bash), a
bash-only coding system prompt, a max-steps-guard plugin (bounds
runaway turns via the agent/turn-continuation waterfall — abort()
from step-end is a no-op by then), and a stdio UI with dimmed
reasoning and exit-on-idle for piped stdin.
- e2e (yarn test:e2e, key-gated): full-loop.e2e.ts runs a real model
against the real bash tool; coding-task.e2e.ts is the swebench-style
smoke — the model fixes a buggy add.js in a temp dir and the test
re-runs node add.test.js itself rather than trusting the agent.
- docs/cookbook: adding-a-package (the verified checklist),
adding-a-tool (execute() contract, background pattern, seams),
adding-an-llm-adapter (protocol obligations, mock-server testing,
e2e policy). AGENTS.md layout/commands/secrets sections updated;
architecture.md points at both examples and the cookbook.
- vitest.e2e.config.ts: serialize test files + retry twice — parallel
e2e files trip the shared internal key's concurrency quota.
- fix: the !js YAML tag spelling in docs/JSDoc is actually !!js
(js-yaml resolves custom tags under tag:yaml.org,2002:js).
2026-06-13 00:47:32 +08:00
yarn demo:coding # run examples/coding-agent — the real agent (needs
# DEEPSEEK_API_KEY; give it a coding task)
Document the architecture and rewrite AGENTS.md
docs/architecture.md: layering, service map, event taxonomy, the
session/turn/step lifecycle, Cordis waterfall semantics, an extension
cookbook, the plugin sanity checklist mapping every MVP feature to its
extension mechanism, and the deferred-work TODO list (sub-agents,
persistence backends, compaction, DeepSeek V4 adapter, parallel tool
execution, streaming-protocol review).
AGENTS.md: repo layout, commands, conventions (dsh-* naming, ESM,
effect-based registrations, declaration merging, waterfall semantics),
and the vendoring policy pointer.
2026-06-11 10:55:05 +08:00
```
2026-06-13 00:28:29 +08:00
## Secrets / .env
Real-API e2e tests (`yarn test:e2e` ) read `DEEPSEEK_API_KEY` (and optionally
`DEEPSEEK_BASE_URL` ) from the environment, or from a gitignored `.env` at the
repo root loaded via Node's native `process.loadEnvFile()` :
```
DEEPSEEK_API_KEY=sk-…
DEEPSEEK_BASE_URL=https://… # optional; defaults to the public API
```
cordis.yml configs reference env vars with the `!!js` tag:
`apiKey: !!js process.env.DEEPSEEK_API_KEY` . Never commit real credentials;
CI has no secrets and e2e suites must self-skip without them.
Document the architecture and rewrite AGENTS.md
docs/architecture.md: layering, service map, event taxonomy, the
session/turn/step lifecycle, Cordis waterfall semantics, an extension
cookbook, the plugin sanity checklist mapping every MVP feature to its
extension mechanism, and the deferred-work TODO list (sub-agents,
persistence backends, compaction, DeepSeek V4 adapter, parallel tool
execution, streaming-protocol review).
AGENTS.md: repo layout, commands, conventions (dsh-* naming, ESM,
effect-based registrations, declaration merging, waterfall semantics),
and the vendoring policy pointer.
2026-06-11 10:55:05 +08:00
Dev/test/demo run **unbuilt** via tsx + the `paths` map in the root
`tsconfig.json` (`vitest` resolves through `tsconfig.test.json` ). Building is
2026-06-11 23:10:43 +08:00
only needed for publishing/consumption outside the repo — with one exception:
`yarn lint` 's type-aware rules resolve vendor packages through their built
declarations (`tsconfig.typecheck.json` → `vendor/*/lib` ), so run
`yarn typecheck` once after a fresh clone (CI does the same) or lint reports
unresolved-type `no-unsafe-*` errors.
Document the architecture and rewrite AGENTS.md
docs/architecture.md: layering, service map, event taxonomy, the
session/turn/step lifecycle, Cordis waterfall semantics, an extension
cookbook, the plugin sanity checklist mapping every MVP feature to its
extension mechanism, and the deferred-work TODO list (sub-agents,
persistence backends, compaction, DeepSeek V4 adapter, parallel tool
execution, streaming-protocol review).
AGENTS.md: repo layout, commands, conventions (dsh-* naming, ESM,
effect-based registrations, declaration merging, waterfall semantics),
and the vendoring policy pointer.
2026-06-11 10:55:05 +08:00
## Conventions
- **Package naming**: every npm package in this repo is `@deepseek-ai/dsh-<name>`
(vendored packages keep their upstream names and are `private: true` ).
- **ESM everywhere** (`"type": "module"` ); imports between workspace packages
use package names, never relative paths across package boundaries.
In-package imports use explicit `.ts` extensions (allowImportingTsExtensions).
- **`cordis` is a peerDependency** (+ devDependency) of every harness package,
mirroring upstream convention.
- **Registrations are effects**: anything a plugin contributes (adapter, tool,
section, agent, event listener) goes through `ctx.effect()` / `ctx.on()` so
disposal and HMR work. If you write a registry, `register()` must return the
disposer.
- **Typed events via declaration merging**: services declare their events in
`declare module 'cordis' { interface Events { … } }` , and their ctx key in
`interface Context` . Extensible unions use the merge-extensible-map pattern
(see `ContentBlockMap` , `MessageSourceMap` ).
- **Waterfall semantics**: `ctx.waterfall` listeners receive `(...args, next)`
and MUST call `next()` to delegate; returning without it short-circuits.
This is the veto mechanism — use deliberately.
Add examples/coding-agent and the docs cookbook
The first real agent wiring: DeepSeek V4 + the bash tool suite + stdio
chat + JSONL persistence, runnable via yarn demo:coding (reads the
gitignored repo-root .env through process.loadEnvFile).
- examples/coding-agent: cordis.yml wiring both real plugin families
(llm-deepseek with !!js env secrets; bash-local + tool-bash), a
bash-only coding system prompt, a max-steps-guard plugin (bounds
runaway turns via the agent/turn-continuation waterfall — abort()
from step-end is a no-op by then), and a stdio UI with dimmed
reasoning and exit-on-idle for piped stdin.
- e2e (yarn test:e2e, key-gated): full-loop.e2e.ts runs a real model
against the real bash tool; coding-task.e2e.ts is the swebench-style
smoke — the model fixes a buggy add.js in a temp dir and the test
re-runs node add.test.js itself rather than trusting the agent.
- docs/cookbook: adding-a-package (the verified checklist),
adding-a-tool (execute() contract, background pattern, seams),
adding-an-llm-adapter (protocol obligations, mock-server testing,
e2e policy). AGENTS.md layout/commands/secrets sections updated;
architecture.md points at both examples and the cookbook.
- vitest.e2e.config.ts: serialize test files + retry twice — parallel
e2e files trip the shared internal key's concurrency quota.
- fix: the !js YAML tag spelling in docs/JSDoc is actually !!js
(js-yaml resolves custom tags under tag:yaml.org,2002:js).
2026-06-13 00:47:32 +08:00
- **Discriminated unions: match, don't chain**: branch on a tagged union
(`StreamChunk` , `FinishReason` , `SessionEvent` , …) with a `switch` on the
tag, not a chain of `if (x.kind === '…')` . The switch narrows each arm so
member-only fields (`finish.message` , `finish.code` ) are reachable in the
right case and a typo'd tag fails to compile. Prefer extracting a small
typed helper (`finishError(finish: FinishReason)` ) over inlining the
branches at the call site.
2026-06-11 15:21:25 +08:00
- **Switch exhaustiveness**: switches over CLOSED unions (e.g. `StreamChunk` )
end with `default: assertNever(value, 'context')` (from dsh-llm) so adding a
variant breaks compilation at every switch that must handle it. Switches
Add examples/coding-agent and the docs cookbook
The first real agent wiring: DeepSeek V4 + the bash tool suite + stdio
chat + JSONL persistence, runnable via yarn demo:coding (reads the
gitignored repo-root .env through process.loadEnvFile).
- examples/coding-agent: cordis.yml wiring both real plugin families
(llm-deepseek with !!js env secrets; bash-local + tool-bash), a
bash-only coding system prompt, a max-steps-guard plugin (bounds
runaway turns via the agent/turn-continuation waterfall — abort()
from step-end is a no-op by then), and a stdio UI with dimmed
reasoning and exit-on-idle for piped stdin.
- e2e (yarn test:e2e, key-gated): full-loop.e2e.ts runs a real model
against the real bash tool; coding-task.e2e.ts is the swebench-style
smoke — the model fixes a buggy add.js in a temp dir and the test
re-runs node add.test.js itself rather than trusting the agent.
- docs/cookbook: adding-a-package (the verified checklist),
adding-a-tool (execute() contract, background pattern, seams),
adding-an-llm-adapter (protocol obligations, mock-server testing,
e2e policy). AGENTS.md layout/commands/secrets sections updated;
architecture.md points at both examples and the cookbook.
- vitest.e2e.config.ts: serialize test files + retry twice — parallel
e2e files trip the shared internal key's concurrency quota.
- fix: the !js YAML tag spelling in docs/JSDoc is actually !!js
(js-yaml resolves custom tags under tag:yaml.org,2002:js).
2026-06-13 00:47:32 +08:00
over MERGE-EXTENSIBLE unions (`SessionEventMap` , `ContentBlockMap` ,
`FinishReason` , …) must NOT use assertNever — plugin-added variants are
valid unknown values; handle known cases and fall through `default` with a
comment (the lint rule `switch-exhaustiveness-check` makes the choice
explicit either way; a redundant disable directive is itself a lint error).
Document the architecture and rewrite AGENTS.md
docs/architecture.md: layering, service map, event taxonomy, the
session/turn/step lifecycle, Cordis waterfall semantics, an extension
cookbook, the plugin sanity checklist mapping every MVP feature to its
extension mechanism, and the deferred-work TODO list (sub-agents,
persistence backends, compaction, DeepSeek V4 adapter, parallel tool
execution, streaming-protocol review).
AGENTS.md: repo layout, commands, conventions (dsh-* naming, ESM,
effect-based registrations, declaration merging, waterfall semantics),
and the vendoring policy pointer.
2026-06-11 10:55:05 +08:00
- **Plugins, not loop changes**: new behavior goes into a plugin on the
documented extension seams (see the plugin sanity checklist in
docs/architecture.md). Changing `agent-loop` requires updating that doc.
Add bash execution: dsh-bash seam, dsh-bash-local impl, dsh-tool-bash tools
Three packages following the new capability-seam pattern (interface /
implementation / consumer, now documented in docs/architecture.md):
- dsh-bash: abstract BashExecutor service (ctx.bash) + vocabulary types.
- dsh-bash-local: local subprocesses — bash -c per call in a detached
process group, SIGTERM→SIGKILL group kills, tail-keep truncation with
full-stream spill files, model-friendly env, background task registry.
- dsh-tool-bash: the bash / bash_output / bash_kill tool schemas with
runtime arg validation and background completion notices via
agent.inject(). Non-zero exits are reported, not errored.
Design surveyed against the bash tools of Claude Code, OpenCode, Codex,
and pi (notes in the package READMEs). Permissions/sandbox stay TODO on
the tools/execute waterfall seam; stateful-shell alternatives recorded
in run.ts.
2026-06-12 23:28:44 +08:00
- **Capability seams are three packages**: when adding a swappable capability
(an execution backend, a provider integration, …), split it into
*interface* (abstract service + vocabulary types, e.g. `bash/` ),
*implementation* (a concrete subclass, e.g. `bash-local/` ), and
*consumer* (what the model/plugins see, e.g. `tool-bash/` ). Implementations
and consumers then evolve independently — a sandboxed executor replaces
`bash-local` without touching tool schemas. The LLM seam follows the same
shape (`llm/` is interface + consumer surface; adapters are implementations).
See docs/architecture.md § "Capability seams" for when NOT to split.
- **Explicit > implicit at package seams**: interface/vocabulary types spell
out every field a consumer must supply — no optional field that the
implementation silently fills with a hidden `?? default` . Put defaulting in
the owning implementation as an explicit step (a `resolve(request): Spec`
method that turns the optional-field request into the required-field spec),
not smuggled inside `run()` /`start()` . Example: `dsh-bash` splits
`BashExecRequest` (optional `workdir` /`timeoutMs` , model-facing) from
`BashExecSpec` (required, what `run` /`start` act on); the tool layer calls
`ctx.bash.resolve()` between them. The reader of a `BashExecSpec` never has
to wonder where the working directory came from.
2026-06-13 00:28:29 +08:00
- **An empty `catch` must name what it swallows and why nothing else can hit
it**: a bare `catch {}` hides bugs. When you deliberately ignore a throw, the
comment must (a) name the single expected failure, (b) say why ignoring it is
correct — usually because the useful state was already captured *before* the
`try` — and (c) make clear nothing else of consequence can reach the catch
(ideally the `try` wraps a single statement). Example: the error-body
`response.json()` parse in `dsh-llm-deepseek` 's adapter sets `code` + HTTP
`status` from the status line before the `try` , so a malformed provider body
can only cost a richer message, never the real error.
Add examples/coding-agent and the docs cookbook
The first real agent wiring: DeepSeek V4 + the bash tool suite + stdio
chat + JSONL persistence, runnable via yarn demo:coding (reads the
gitignored repo-root .env through process.loadEnvFile).
- examples/coding-agent: cordis.yml wiring both real plugin families
(llm-deepseek with !!js env secrets; bash-local + tool-bash), a
bash-only coding system prompt, a max-steps-guard plugin (bounds
runaway turns via the agent/turn-continuation waterfall — abort()
from step-end is a no-op by then), and a stdio UI with dimmed
reasoning and exit-on-idle for piped stdin.
- e2e (yarn test:e2e, key-gated): full-loop.e2e.ts runs a real model
against the real bash tool; coding-task.e2e.ts is the swebench-style
smoke — the model fixes a buggy add.js in a temp dir and the test
re-runs node add.test.js itself rather than trusting the agent.
- docs/cookbook: adding-a-package (the verified checklist),
adding-a-tool (execute() contract, background pattern, seams),
adding-an-llm-adapter (protocol obligations, mock-server testing,
e2e policy). AGENTS.md layout/commands/secrets sections updated;
architecture.md points at both examples and the cookbook.
- vitest.e2e.config.ts: serialize test files + retry twice — parallel
e2e files trip the shared internal key's concurrency quota.
- fix: the !js YAML tag spelling in docs/JSDoc is actually !!js
(js-yaml resolves custom tags under tag:yaml.org,2002:js).
2026-06-13 00:47:32 +08:00
- **Symmetry is usually more correct**: when two related values play parallel
roles (a test fixture and its expected output, a request shape and its
response shape, a buggy input and the test that checks the fix), give them
parallel form — both named consts, or both inline, not one each way. Asymmetry
is a smell that usually points at a missed extraction.
Document the architecture and rewrite AGENTS.md
docs/architecture.md: layering, service map, event taxonomy, the
session/turn/step lifecycle, Cordis waterfall semantics, an extension
cookbook, the plugin sanity checklist mapping every MVP feature to its
extension mechanism, and the deferred-work TODO list (sub-agents,
persistence backends, compaction, DeepSeek V4 adapter, parallel tool
execution, streaming-protocol review).
AGENTS.md: repo layout, commands, conventions (dsh-* naming, ESM,
effect-based registrations, declaration merging, waterfall semantics),
and the vendoring policy pointer.
2026-06-11 10:55:05 +08:00
- **Tests**: vitest, colocated under `packages/<name>/tests/*.spec.ts` . Every
registry needs an HMR-safety test (dispose the contributing fiber, assert
2026-06-11 12:18:52 +08:00
cleanup). **Excessive tests are welcome** — when in doubt, write the test;
err on the side of covering edge cases, error paths, event ordering, and
concurrency races even if they seem unlikely. Review findings get regression
tests (see `packages/agent-loop/tests/review-fixes.spec.ts` ).
Document the architecture and rewrite AGENTS.md
docs/architecture.md: layering, service map, event taxonomy, the
session/turn/step lifecycle, Cordis waterfall semantics, an extension
cookbook, the plugin sanity checklist mapping every MVP feature to its
extension mechanism, and the deferred-work TODO list (sub-agents,
persistence backends, compaction, DeepSeek V4 adapter, parallel tool
execution, streaming-protocol review).
AGENTS.md: repo layout, commands, conventions (dsh-* naming, ESM,
effect-based registrations, declaration merging, waterfall semantics),
and the vendoring policy pointer.
2026-06-11 10:55:05 +08:00
Add examples/coding-agent and the docs cookbook
The first real agent wiring: DeepSeek V4 + the bash tool suite + stdio
chat + JSONL persistence, runnable via yarn demo:coding (reads the
gitignored repo-root .env through process.loadEnvFile).
- examples/coding-agent: cordis.yml wiring both real plugin families
(llm-deepseek with !!js env secrets; bash-local + tool-bash), a
bash-only coding system prompt, a max-steps-guard plugin (bounds
runaway turns via the agent/turn-continuation waterfall — abort()
from step-end is a no-op by then), and a stdio UI with dimmed
reasoning and exit-on-idle for piped stdin.
- e2e (yarn test:e2e, key-gated): full-loop.e2e.ts runs a real model
against the real bash tool; coding-task.e2e.ts is the swebench-style
smoke — the model fixes a buggy add.js in a temp dir and the test
re-runs node add.test.js itself rather than trusting the agent.
- docs/cookbook: adding-a-package (the verified checklist),
adding-a-tool (execute() contract, background pattern, seams),
adding-an-llm-adapter (protocol obligations, mock-server testing,
e2e policy). AGENTS.md layout/commands/secrets sections updated;
architecture.md points at both examples and the cookbook.
- vitest.e2e.config.ts: serialize test files + retry twice — parallel
e2e files trip the shared internal key's concurrency quota.
- fix: the !js YAML tag spelling in docs/JSDoc is actually !!js
(js-yaml resolves custom tags under tag:yaml.org,2002:js).
2026-06-13 00:47:32 +08:00
## Defensive patterns (hard-won)
Each bullet is a bug class that bit us; the rule prevents the reoccurrence.
- **Report orthogonal outcomes independently.** A result can be several
things at once (a process can both time out AND exit 0 because it trapped
the signal). Don't nest the report of one flag inside the branch of
another. Surface each independent fact (`timedOut` , `signal` , `exitCode` )
on its own so a caller never reads a cut-short run as a clean success.
- **Honor cross-seam contracts on BOTH sides.** When an interface documents
two valid ways to signal something (e.g. an adapter may report a model
failure by THROWING from `stream()` *or* by ending the stream with a
`finish {kind:'error'|'aborted'}` chunk), the consumer must handle both —
not just the one the first implementation happened to use. A library-backed
adapter that can't throw mid-stream relies on the finish-chunk path; if the
loop only catches throws, a provider 401 becomes a normal completed turn.
Document the contract where the type is defined and exercise every branch
through the real consumer in tests.
- **Async state is not synchronous state.** `agent.send()` does not flip
status to `running` before it returns; a background task's completion races
turn boundaries; `reader.close()` fires for both EOF and disposal. Never
gate control flow on a status you only *just* requested. Drive lifecycle off
the events/promises that actually fire (`agent/status` , `task.done` ), and
when "done" needs a settle signal, observe the transition (saw `running`
THEN `idle` ) rather than counting actions you assume map 1:1 to turns —
the loop batches queued messages into one turn. But a settle-signal guard
cuts both ways: if the awaited transition can *never* occur (EOF with no
work submitted → no turn ever starts → never `running` ), it hangs forever.
Always handle the "nothing to wait for" branch explicitly alongside the
"wait for the work" branch.
- **Dispose must reach quiescence, not just request it.** A teardown that
issues kills/aborts but returns before the work stops leaves orphans. Make
cleanup `async` and `await` the children's exit (kill → await `done` ), and
close listener/notification registries *before* killing so late completions
stay silent. Tests must prove disposal *waited* (pid already gone right
after `await fiber.dispose()` ), not merely that the process eventually dies.
- **Contain callback exceptions at the boundary.** A user-supplied listener
(`onTaskDone` , event handlers) that throws must not reject the promise it
runs inside or starve the listeners after it. Wrap the dispatch loop in
try/catch and log; never let one bad subscriber break core lifecycle.
- **Never hand untrusted/model output the ambient environment or predictable
paths.** Spawned commands get a scrubbed env (drop `*KEY*` /`*SECRET*` /
`*TOKEN*` ) so the harness's own credentials can't leak into output, `env` ,
or spill files. Temp/spill files use a private (0700) dir, random names,
and exclusive owner-only (`'wx'` , `0o600` ) opens — predictable
world-readable paths invite symlink races and disclosure.
- **e2e tests own their resources.** Real-API/integration tests must create
the harness in the test and dispose it in `afterEach` (even on
failure/retry/timeout), so a flaky run doesn't leak processes or contexts.
Shared fixtures live in a plain `tests/harness.ts` module, NOT another
`*.e2e.ts` file — importing a spec file re-registers its `describe` and
duplicates real API calls. Verify the WORLD, not the agent's self-report:
re-run the command/check externally and assert files are byte-identical
where they should be unchanged (a keyword probe lets a cheating agent pass).
- **Tag spelling and EOF hygiene.** cordis.yml interpolates env via the
`!!js` tag (js-yaml resolves custom tags under `tag:yaml.org,2002:js` ), not
`!js` — keep code, comments, and docs consistent. Files end with exactly
one trailing newline; `git diff --check` (a pre-push gate) rejects new
blank lines at EOF.
Document the codebase thoroughly and tighten type safety
Docs: per-folder README.md for packages/ (family overview + one per
package: service, events, API, extension points, TODOs), examples/,
and examples/echo-agent/; folder-level AGENTS.md (+ CLAUDE.md
symlinks) for packages/ and vendor/; module-level doc comments in
every packages/*/src file; richer JSDoc on all exported API
(event side effects, disposal contracts, error behavior). Root
AGENTS.md gains a "Type Safety and Documentation" policy section:
the codebase aims to be very type-safe and well documented; type
gymnastics are acceptable in core packages when they improve
plugin-author DX; verbose docs are fine as long as they stay strictly
in sync with the code.
Type safety: removed the upstream-inherited "noImplicitAny": false
from tsconfig.base.json — packages/* now compile under full strict
mode; vendor/loader and vendor/include set it locally (vendor/cordis
already did). Eliminated every `: any` / `as any` from packages and
examples (catch clauses use unknown + a CodedError narrowing type;
event data access uses discriminated-union narrowing).
Typed tool schemas: new @deepseek-ai/dsh-tools schema DSL —
SchemaSpec with per-property `required: true` booleans, type-level
InferArgs<S>, a runtime SchemaSpec → JSON Schema converter, and
defineTool() so first-party tools get typed execute(args) with zero
casts (raw JSON Schema still accepted for MCP interop; chosen over
schemastery because it targets JSON Schema generation directly).
echo-tool and all test tools migrated; +7 tests.
2026-06-11 12:39:27 +08:00
## Type Safety and Documentation
This codebase aims to be **very type-safe and well documented** for
maintainability. Code that fails to compile under `strict: true` (with
`noImplicitAny` enabled for all `packages/*` source) is not acceptable. Every
`any` that remains must have a specific justification (a comment explaining why
a narrower type is infeasible).
In the **core** packages (`packages/llm` , `packages/tools` , `packages/agent` ,
`packages/agent-loop` , `packages/session` , `packages/system-prompt` ), **type
gymnastics are acceptable when they improve the DX of plugin authors** for
common plugin types. The `defineTool` typed schema DSL in `dsh-tools` is the
canonical example: the `SchemaSpec` to `InferArgs<S>` type-level mapping gives
tool authors zero-cast typed `execute` args, and the cost of the conditional
types stays inside the core package.
Verbose documentation is fine **as long as docs and code stay strictly in
2026-06-13 00:28:29 +08:00
sync**. Out-of-sync docs are worse than no docs. **When you change code, update
its docs in the SAME change** — grep the package README and the module/JSDoc
comments for the old behavior (config keys, defaults, error codes, wire field
names, event names) and fix every hit. CI has no doc-sync gate, so this is on
the author. Every module has a module-level doc comment explaining its role.
Every exported class, interface, type, function, and non-obvious method has a
JSDoc that explains semantics (not just the name) — contracts (what events fire
when), disposal behavior, error behavior, and extension intent. Internal
helpers get docs only where non-obvious. Prefer one-liners when one line
suffices.
**Editing these instructions**: `AGENTS.md` is the real file; `CLAUDE.md` is a
symlink to it (at the repo root and in `packages/` ). Always edit `AGENTS.md` —
never write through the `CLAUDE.md` symlink or replace it with a regular file.
Document the codebase thoroughly and tighten type safety
Docs: per-folder README.md for packages/ (family overview + one per
package: service, events, API, extension points, TODOs), examples/,
and examples/echo-agent/; folder-level AGENTS.md (+ CLAUDE.md
symlinks) for packages/ and vendor/; module-level doc comments in
every packages/*/src file; richer JSDoc on all exported API
(event side effects, disposal contracts, error behavior). Root
AGENTS.md gains a "Type Safety and Documentation" policy section:
the codebase aims to be very type-safe and well documented; type
gymnastics are acceptable in core packages when they improve
plugin-author DX; verbose docs are fine as long as they stay strictly
in sync with the code.
Type safety: removed the upstream-inherited "noImplicitAny": false
from tsconfig.base.json — packages/* now compile under full strict
mode; vendor/loader and vendor/include set it locally (vendor/cordis
already did). Eliminated every `: any` / `as any` from packages and
examples (catch clauses use unknown + a CodedError narrowing type;
event data access uses discriminated-union narrowing).
Typed tool schemas: new @deepseek-ai/dsh-tools schema DSL —
SchemaSpec with per-property `required: true` booleans, type-level
InferArgs<S>, a runtime SchemaSpec → JSON Schema converter, and
defineTool() so first-party tools get typed execute(args) with zero
casts (raw JSON Schema still accepted for MCP interop; chosen over
schemastery because it targets JSON Schema generation directly).
echo-tool and all test tools migrated; +7 tests.
2026-06-11 12:39:27 +08:00
Document the architecture and rewrite AGENTS.md
docs/architecture.md: layering, service map, event taxonomy, the
session/turn/step lifecycle, Cordis waterfall semantics, an extension
cookbook, the plugin sanity checklist mapping every MVP feature to its
extension mechanism, and the deferred-work TODO list (sub-agents,
persistence backends, compaction, DeepSeek V4 adapter, parallel tool
execution, streaming-protocol review).
AGENTS.md: repo layout, commands, conventions (dsh-* naming, ESM,
effect-based registrations, declaration merging, waterfall semantics),
and the vendoring policy pointer.
2026-06-11 10:55:05 +08:00
## Vendoring Policy
`vendor/` packages are pinned source copies (manifest with upstream commit
SHAs in [vendor/README.md ](vendor/README.md )). To update one, follow the sync
procedure there; re-apply (or retire) the logged local modifications and rerun
`yarn test && yarn build` .