# Conflicts: # docs/architecture.md # docs/config-catalog.md # docs/module-graph.md # docs/rfc/INDEX.md # examples/acp-agent/tests/snapshots/text-turn/session.jsonl # packages/core/agent-core/src/index.ts # packages/core/tools/tests/gen-tool-catalog.spec.ts # packages/support/acp-snapshot/src/suite.ts # packages/ui/acp-agent/src/index.ts
13 KiB
AGENTS.md
The DeepSeek Harness group monorepo, hosting DeepSeek Harness SDK — a plugin-based SDK for building agent harnesses on the vendored Cordis framework, microkernel-style: everything is a plugin. Read docs/architecture.md before changing packages/; the documentation standard is docs/AGENTS.md.
Pre-release stance: foundation over blast radius
Applies only while the harness is unreleased — remove this section at the first tagged release. With no external consumers, optimize for the correct foundation, not a small diff: move files, rename public symbols, repackage plugins, and update every reference in the same change. No backward-compat shims, deprecation aliases, or re-export stubs. On-disk formats need no migrations — a backend REJECTS anything not at the current version. Two sanctioned version stances: monotonic bump-and-reject (the SQLite backend's SCHEMA_VERSION), and a pinned 0 that absorbs all shape churn (SESSION_FORMAT_VERSION in dsh-session, documented "no compatibility implied"). Real version policy begins at the first release.
Repository layout
vendor/ Vendored Cordis source — manifest + sync procedure in vendor/README.md
packages/ Harness packages at packages/<group>/<pkg>/, all named @deepseek-ai/dsh-<pkg>
core/ product API spine: session, system-prompt, tools, agent, agent-loop, agent-core (the bundle)
llm/ LLM seam + the DeepSeek adapters (hand-rolled + pi-ai design twin)
bash/ bash executor seam + local impl + model-facing bash tools
fs/ filesystem seam + local impl + policy gate + read/write/edit tools
skill/ skill provider registry + local impl + catalog/loader tool
web/ web seam + search/fetch providers + model-facing web tools
compact/ compaction seam + basic backend
subagent/ subagent seam + spawn/fork/ACP backends + delegation tool
workflow/ workflow seam + worker-thread engine + the workflow tool
todo/ the todo_write tool
guard/ loop-hygiene plugins
cordis/ self-referential toolset: the agent inspects/mounts plugins in its own runtime
hooks/ Claude Code / Codex hook bridges + shared wire-protocol library
session-persistence/ persistence seam + JSONL/SQLite backends
ui/ ACP bridge, app-boot glue, stdio/ACP app bins, user-interaction seam, ask-user tool
support/ dev/test infrastructure packages
util/ zero-dependency utilities
examples/ Runnable demos: thin cordis.yml leaves over the app packages (see examples/AGENTS.md)
docs/ architecture, generated catalogs, RFCs, postmortems, cookbook (see docs/AGENTS.md)
scripts/ repo gates and generators
Per-package map: the group READMEs, indexed from packages/README.md.
Commands
pnpm install # pnpm workspaces, node ^22.19 || >=24
pnpm run test # vitest unit tests
pnpm run test:coverage # THE gating test run: per-file 100% coverage on packages/*/*/src
pnpm run test:e2e # real-API tests; self-skip without DEEPSEEK_API_KEY
pnpm run test:snapshot # keyless ACP replay vs goldens; filter: -t <name>
pnpm run test:snapshot:record # re-record goldens (needs key)
pnpm run typecheck
pnpm run lint
pnpm run build # tsc emits lib/types, tsdown bundles runtime
pnpm run hygiene # knip + publint + workspace constraints + NodeNext consumer check
pnpm run doc-sync # all documentation gates; see the doc-sync script in package.json
pnpm run demo:echo # mock-model REPL, no key needed
pnpm run demo:repl # real REPL coding agent (needs DEEPSEEK_API_KEY)
pnpm run demo:cordis # self-referential demo: the agent modifies its own runtime (needs key)
pnpm run demo:acp # ACP server agent (needs DEEPSEEK_API_KEY)
Run the CI gates locally before marking a PR ready
During implementation, run the narrowest affected checks; run this full CI-equivalent sequence only when complete and before marking a PR ready. From a fresh clone/worktree, pnpm run build first because publint and NodeNext validate built lib/:
set -euo pipefail
pnpm run typecheck
pnpm run lint
pnpm run test:coverage
pnpm run test:snapshot
pnpm run doc-sync
pnpm run verify-module-graph
pnpm run build
pnpm run hygiene
out=$(printf 'echo ci smoke\n' | pnpm run demo:echo 2>&1)
printf '%s\n' "$out" | grep -q '\[tool call\] echo({"text":"ci smoke"})'
printf '%s\n' "$out" | grep -q '\[tool result\] ECHO: CI SMOKE'
test -n "$(find .sessions -path '.sessions/cwd-*/main-session-*.jsonl' -type f -print -quit)"
rm -rf .sessions
pnpm exec vitest run --config vitest.e2e.config.ts packages/ui/stdio-agent/tests/built-bin.e2e.ts packages/ui/acp-agent/tests/built-bin.e2e.ts packages/workflow/workflow-workerthread/tests/built-worker.e2e.ts packages/code-runtime/code-runtime-worker/tests/built-lib.e2e.ts
test:coverage, not test, is the gating run (why); a sign-off counts only for commands actually run.
Secrets / .env
Real-API tests and demos read DEEPSEEK_API_KEY (and optional DEEPSEEK_BASE_URL) from the environment or a gitignored root .env loaded via process.loadEnvFile(). cordis.yml references env vars with the !!js tag (never !js). Never commit credentials. CI has no secrets, so e2e suites self-skip without a key — a CI accommodation, not a cost signal; the with-key policy is in docs/testing.md.
Conventions
- Every npm package is
@deepseek-ai/dsh-<name>; vendored packages keep upstream names and areprivate: true.cordisis a peerDependency (+ dev) of every harness package. - ESM everywhere (
"type": "module"). Cross-package imports use package names, never relative paths; in-package relative imports use explicit.tsextensions. Dev/test/demo run unbuilt via tsx + the root tsconfigpathsmap; builds are for outside consumers only. - Registrations are effects: every contribution goes through
ctx.effect()/ctx.on(); a registry'sregister()returns the disposer. - Typed events via declaration merging; extensible unions use the merge-extensible-map pattern (
ContentBlockMap,SessionEventMap, …). Every new event's JSDoc carries an@modetag and a@paramper payload parameter (this/trailingnextexempt); every public service-class method documents each parameter and non-void return (@param/@returns) — the catalog generator hard-errors otherwise; mode semantics are in the generated events catalog header. - Discriminated unions:
switchon the tag, not if-chains. Closed unions end withdefault: assertNever(...); merge-extensible unions must NOT — handle known cases and fall throughdefaultwith a comment. - Waterfall listeners MUST call
next()to delegate; returning without it is the veto (semantics). - Model-visible ⟺ logged: anything that reaches a model request must be reconstructable from the session log; a new model-visible input requires a session event.
- Plugins, not loop changes: new behavior goes on the documented extension seams; changing
agent-looprequires updating docs/architecture.md. - Capability seams are three packages — interface / implementation / consumer; don't split preemptively.
- Explicit > implicit at package seams: defaulting is an explicit
resolve(request): Specstep in the owning implementation, never a hidden?? defaultinsiderun()(thedsh-bashrequest/spec split is the template). - No hardcoded tunables in plugins: anything two deployments could want different — timeouts, caps, model names, base URLs — is a defaulted, validated
Configfield, not a literal; aDEFAULT_*constant or test-only seam is not configurability. The test: changeable fromcordis.yml, no code edit. Protocol/wire constants, external-spec values, security invariants stay hardcoded. - Misconfiguration fails loud: a config value referencing something that does not exist (a
toolOrdertool name, a plugin path) throws — at load when the check is self-contained, else at the earliest moment the referent exists (fortoolOrder, every prompt assembly) — never a silent skip. - Opaque cross-boundary ids are branded (
Branded<B>fromdsh-brand), never barestring. - An empty
catchnames what it swallows and why nothing else can reach it; keep thetryto one statement. - Symmetry is usually more correct: parallel values get parallel form; asymmetry smells of a missed extraction.
- Tests document behavior, not golden truth: a green test pins what the code DOES, not what it SHOULD do. Before preserving a behavior solely for its test, ask whether it is load-bearing; an artifact changes together with its test, with the why in the PR.
- RFCs are proposals, not golden truth: validate its premise against current code before implementing; friction is evidence of over-reach — amend on the way to
implemented/. - Testing policy — docs/testing.md. Transcript/UX changes need snapshots or a PR note. Snapshot fixtures must replay on macOS/Linux; avoid GNU/BSD-only commands (e.g.
sed -i); fix fixtures, not normalizers. - A tool's ACP render intent is part of its design, decided up front (
generic/terminal/diff,locations); presentation methods are pure functions ofargs(cookbook). - A new capability seam, lifecycle shape, or transcript surface names its coverage at every tier (unit, e2e, snapshot) at plan time and verifies the harness can express it — a gap is scheduled work, not a mid-build surprise.
- Merge PRs with merge commits (
gh pr merge --merge), never squash/rebase. Never rewrite a pushed branch; update a child by merging its parent down. A review fix lands on the PR that introduced the issue, as a separate commit, then merges down (stacked-review guide). - TODO markers:
FIXME/TODO/XXXby urgency (semantics). - Files end with exactly one trailing newline;
git diff --check(pre-push) gates it.
Defensive patterns
docs/defensive-patterns.md carries the hard-won bug-class rules: report orthogonal outcomes independently; honor cross-seam contracts on both sides; async state is not synchronous state; dispose must reach quiescence; contain callback exceptions; never hand untrusted output the ambient environment or predictable paths. Read it before lifecycle, concurrency, subprocess, or teardown work.
Type safety and documentation
Everything compiles under strict: true with noImplicitAny; every remaining any carries a comment saying why a narrower type is infeasible. Every module has a module-level doc comment; every export (and non-obvious method) has a JSDoc explaining semantics — contracts, disposal, errors — not the name restated; internal helpers only where non-obvious; one-liners when one line suffices. The export half is mechanical: verify-export-jsdoc (in doc-sync) requires description prose on every package export plus @param/@returns (and an annotated return) on function-like ones. Heritage-declared members, plugin-protocol slots, and constructors are exempt — their docs' one home is the seam declaration, the framework protocol, and the class doc respectively. Lean toward the stricter lint rule and the extra mechanical gate: encode invariants in checks (verify-* scripts), preferring a narrow justified escape hatch over a rule left off globally. Type gymnastics are acceptable inside core packages when they buy plugin-author DX (the defineTool schema DSL is the canonical example).
Docs are part of every change: code changes update their README and JSDoc in the SAME change; a bilingual-pair edit updates the counterpart and re-records (i18n contract). The writing rules — document the current state never the history, one physical line per paragraph, one home per fact — and the word-budget gate live in docs/AGENTS.md.
Editing these instructions
AGENTS.md is the real file; CLAUDE.md is a symlink to it (root, packages/, examples/). Edit AGENTS.md, never the symlink. Keep it self-contained: state each principle inline instead of citing RFCs (they stay discoverable via the RFC index); linking high-level docs — architecture, testing, cookbooks — is fine. This file is budget-gated (verify-doc-budgets): condense first if it is possible without sacrificing clarity; truly needed additions may justify a ceiling raise.
Vendoring policy
vendor/ packages are pinned source copies (manifest with upstream SHAs in vendor/README.md). Update via the sync procedure there; re-apply or retire the logged local modifications; rerun pnpm run test && pnpm run build.