From cacfae3caecee1ef7364457137adbdac87d3575b Mon Sep 17 00:00:00 2001 From: Tianyi Cui <53024+tianyicui@users.noreply.github.com> Date: Thu, 11 Jun 2026 10:55:05 +0800 Subject: [PATCH] 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. --- AGENTS.md | 73 +++++++++- README.md | 10 +- docs/architecture.md | 320 +++++++++++++++++++++++++++++++++++++++++++ 3 files changed, 401 insertions(+), 2 deletions(-) create mode 100644 docs/architecture.md diff --git a/AGENTS.md b/AGENTS.md index 9aab29ac17..25c3902ea7 100644 --- a/AGENTS.md +++ b/AGENTS.md @@ -4,9 +4,80 @@ This is the monorepo for the DeepSeek Harness group. It currently hosts the code ## Architecture -This codebase is based on the **Cordis** framework. All necessary Cordis dependencies are copied into this monorepo as vendored source instead of being depended on via npm. +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. ## 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"). + +## 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-: + llm/ abstract LLM service + content-block vocabulary (no real adapter yet) + 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 +examples/ Runnable demos (not workspaces). echo-agent = mock model + echo + tool + stdio UI + JSONL persistence, wired via cordis.yml. +docs/ architecture.md — the design doc. +scripts/ build.ts — dumble JS bundling for all packages. +``` + +## Commands + +```sh +yarn install # Yarn 4 workspaces (node-modules linker), node >= 24 +yarn test # vitest run (packages/*/tests/**/*.spec.ts) +yarn typecheck # tsc -b tsconfig.build.json (declarations only) +yarn build # typecheck + dumble JS bundles into each package's lib/ +yarn demo # run examples/echo-agent (needs --expose-internals, the + # script passes it; type "echo hi" to see a tool call) +``` + +Dev/test/demo run **unbuilt** via tsx + the `paths` map in the root +`tsconfig.json` (`vitest` resolves through `tsconfig.test.json`). Building is +only needed for publishing/consumption outside the repo. + +## Conventions + +- **Package naming**: every npm package in this repo is `@deepseek-ai/dsh-` + (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. +- **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. +- **Tests**: vitest, colocated under `packages//tests/*.spec.ts`. Every + registry needs an HMR-safety test (dispose the contributing fiber, assert + cleanup). + +## 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`. diff --git a/README.md b/README.md index ed8e0a2388..61dc4e6a06 100644 --- a/README.md +++ b/README.md @@ -8,4 +8,12 @@ Monorepo for the DeepSeek Harness group. ## Development -This monorepo is built on the [Cordis](https://github.com/cordiverse/cordis) framework. See [AGENTS.md](AGENTS.md) for codebase conventions and agent instructions. +This monorepo is built on the [Cordis](https://github.com/cordiverse/cordis) framework (vendored as source under `vendor/`), microkernel-style: everything is a plugin. + +```sh +yarn install +yarn test # vitest +yarn demo # runnable echo-agent example +``` + +See [AGENTS.md](AGENTS.md) for layout, commands, and conventions, and [docs/architecture.md](docs/architecture.md) for the design. diff --git a/docs/architecture.md b/docs/architecture.md new file mode 100644 index 0000000000..fe64413633 --- /dev/null +++ b/docs/architecture.md @@ -0,0 +1,320 @@ +# DeepSeek Harness Architecture + +This document describes the phase-1 architecture of the DeepSeek Harness — the +foundation of **DeepSeek Code**. The governing principle, from the +[microkernel design discussion][microkernel-doc], is: + +> **Microkernel approach. Everything is a plugin.** + +The harness core is deliberately tiny: a handful of abstract services plus one +concrete plugin (the agent loop). Every product feature — tools, hooks, +compaction, sandboxing, UI, persistence, sub-agents, MCP, skills — is meant to +be written as a plugin against the extension surface described here, without +modifying the loop. + +Requirement context: [Coding Harness MVP 需求分析][mvp-doc]. + +[microkernel-doc]: https://trtgsjkv6r.feishu.cn/wiki/VS9Lw1kQki6mDJk2UHocyuphnsc +[mvp-doc]: https://trtgsjkv6r.feishu.cn/wiki/ZwK6wfBE9i91V6kzMGYcgRGanxg + +## Layering + +``` +┌─────────────────────────────────────────────────────────────┐ +│ future plugins: tools, hooks, compaction, sandbox, UI, MCP… │ +├─────────────────────────────────────────────────────────────┤ +│ @deepseek-ai/dsh-agent-loop (the ONE concrete plugin) │ +├─────────────────────────────────────────────────────────────┤ +│ @deepseek-ai/dsh-agent (vocabulary + registry) │ +│ @deepseek-ai/dsh-tools (registry + exec waterfall)│ +│ @deepseek-ai/dsh-system-prompt (assembly registry) │ +│ @deepseek-ai/dsh-session (event-sourced log) │ +│ @deepseek-ai/dsh-llm (abstract model service) │ +├─────────────────────────────────────────────────────────────┤ +│ vendor/: cordis, loader, include, group, timer, hmr, │ +│ logger-console, cosmokit, schemastery │ +└─────────────────────────────────────────────────────────────┘ +``` + +Dependency rule: plugins depend on interface packages, never on +`dsh-agent-loop`. The loop itself is swappable — UI/hook/tool plugins keep +working against the `dsh-agent` vocabulary if the loop is replaced. + +## Service map + +| ctx key | Class | Package | Role | +|---|---|---|---| +| `ctx.llm` | `LlmService` | dsh-llm | adapter registry; `stream()` / `streamBlocks()` / `generate()` | +| `ctx.sessions` | `SessionStore` | dsh-session | creates/holds event-sourced `Session`s | +| `ctx.systemPrompt` | `SystemPrompt` | dsh-system-prompt | ordered sections + tool schemas → `assemble()` | +| `ctx.tools` | `ToolRegistry` | dsh-tools | tool definitions; `execute()` through waterfall | +| `ctx.agents` | `AgentRegistry` | dsh-agent | live `Agent` handles | +| `ctx.agentLoop` | `AgentLoop` | dsh-agent-loop | creates `LoopAgent`s and drives their loops | + +All registrations (`registerAdapter`, `section`, `tools`, `register`, …) go +through `ctx.effect()` and return disposers, so plugin hot-reload (vendored +HMR) and fiber disposal clean up automatically. + +## The vocabulary (dsh-llm) + +Messages are arrays of typed **content blocks** (`text`, `reasoning`, +`tool-call`, `tool-result`, `image`); the union is derived from the +merge-extensible `ContentBlockMap`, so plugins can add block types via +declaration merging. The same merge-extensible-map pattern is used for +`MessageSource`, `FinishReason`, `TurnTrigger`, and `TurnEndReason` — typed +sum types instead of strings. + +Streaming is a raw chunk protocol (`block-start`, `text-delta`, +`reasoning-delta`, `tool-call-delta`, `block-end`, `usage`, `finish`). +`BlockAssembler` is the single shared implementation that assembles chunks +into blocks/messages; the loop logs raw chunks (replay fidelity) while feeding +the same chunks through an assembler. + +`LlmAdapter` is the provider seam: subclass, implement `stream()`, call +`ctx.llm.registerAdapter(models, adapter)`. +**TODO**: the DeepSeek V4 adapter is the first real adapter (next phase); the +streaming protocol gets a careful review then. + +## Event-sourced sessions (dsh-session) + +A `Session` is an append-only log of typed `SessionEvent`s — the single source +of truth. The LLM message history is *derived* from the log +(`deriveMessages()`): + +- `user/message` → user message +- `assistant/message` → assistant message (raw `assistant/chunk` events are + replay/UI data and are skipped in derivation) +- `tool/result` → user message carrying a `tool-result` block +- `context/message`, `steering/message` → user-role messages wrapped in a + tagged envelope (``) at their chronological + position — the "system-reminder" pattern; models distinguish them from real + user prompts by the envelope. **TODO(review)**: revisit the envelope once a + real adapter exists. + +Replay/fork = `ctx.sessions.create(id, seedEvents)`. Trace/telemetry = listen +to `session/event`. + +**Durability seam**: `session/event` is a synchronous notification; +persistence plugins buffer (write-behind) and drain at the awaited +`session/flush` checkpoint the loop fires at every turn end (see +`examples/echo-agent/src/session-jsonl.ts` for the pattern). +**TODO**: real persistence backends (JSONL per session dir, sqlite) are a +future phase. + +## Prompt assembly (dsh-system-prompt) + +Plugins contribute `PromptSection`s (named, ordered, static or computed) and +tool-schema providers. `assemble()` returns a `PromptAssembly { sections, +tools }` through the `system-prompt/assemble` waterfall. + +Tool schemas are deliberately **part of the assembly**: "what the model is +told it can do" is one coherent thing managed here, even though adapters +transmit schemas as the wire-level `tools` field rather than prompt text. + +## Tool pipeline (dsh-tools) + +`ToolRegistry.register()` takes schema + `execute()`. The registry feeds its +schemas into the system-prompt assembly automatically. + +`execute()` runs through the **`tools/execute` waterfall** — the single seam +where sandbox, permission, hooks, and plan-mode plugins wrap or veto a call. +This collapses Claude Code's validate → PreToolUse → permission → execute → +PostToolUse pipeline into ordered waterfall listeners. + +**TODO**: tool shapes get revisited when real tools land (e.g. a +concurrency-safety hint for parallel execution; phase 1 executes tool calls +sequentially). + +## Agents (dsh-agent) and the loop (dsh-agent-loop) + +`Agent` is the handle every plugin programs against: + +- `send(content)` — queued message; starts a turn when idle, else next turn +- `steer(content)` — mid-turn injection, drained **between steps**; behaves + like `send` when idle +- `inject(content)` — in-session context (`context/message` event) without + triggering a turn; the next request sees it (Claude Code attachment / + system-reminder analog) +- `abort(reason)` — aborts the in-flight step via `AbortSignal` +- `session`, `status`, `options` + +**TODO(sub-agents)**: `spawn`/`fork` land on `AgentLoop.create()` — fork seeds +the child Session with the parent's event log, spawn starts fresh; children +are ordinary `Agent` handles so `steer()` and event subscription work +uniformly. Inter-agent channels beyond these primitives are deliberately +deferred. + +### Loop lifecycle (session / turn / step) + +- **Session**: the whole event log of one agent. +- **Turn**: triggered by ≥1 queued message; runs steps until the model stops + requesting tools and no plugin requests continuation. +- **Step**: one model request + its tool executions. + +``` +forever: + wait for queued messages (idle) + TURN: drain queued → session('user/message'…) → 'turn/start' → emit agent/turn-start + STEP loop: + emit agent/step-start + assembly = ctx.systemPrompt.assemble() ⟵ waterfall system-prompt/assemble + req = {model, system, tools, messages: session.deriveMessages(), signal} + req = waterfall agent/request ⟵ hooks, compaction, model switch + stream ctx.llm.stream(req) ⟵ waterfall llm/stream (raw chunks) + session('assistant/chunk'); emit agent/stream-chunk + session('assistant/message', 'usage') + msg = waterfall agent/step-result ⟵ post-process before tool dispatch + each tool-call (sequential): + session('tool/call'); ctx.tools.execute() ⟵ waterfall tools/execute + session('tool/result') + drain steering → session('steering/message'); emit agent/steering + emit agent/step-end + cont = waterfall agent/turn-continuation(default = hadToolCalls || steered) + if !cont: break + session('turn/end'); emit agent/turn-end + await ctx.parallel('session/flush', session) ⟵ durability checkpoint + idle (emit agent/status) unless more queued +``` + +### Event taxonomy + +Declared in `@deepseek-ai/dsh-agent` (so nothing depends on the loop package). + +| Event | Mode | Purpose | +|---|---|---| +| `agent/created` / `agent/disposed` / `agent/status` / `agent/queued` | emit | lifecycle + inbox notifications | +| `agent/turn-start` / `agent/turn-end` / `agent/step-start` / `agent/step-end` | emit | boundaries | +| `agent/request` | **waterfall** | mutate the final `GenerateOptions` before the model call | +| `agent/stream-chunk` | emit | token-level UI/log feed | +| `agent/step-result` | **waterfall** | post-process the assistant message before tool dispatch | +| `agent/steering` | emit | steering content injected | +| `agent/turn-continuation` | **waterfall** | override the continue/stop decision | +| `agent/error` | emit | step/turn errors | +| `tools/execute` (dsh-tools) | **waterfall** | wrap/veto/sandbox tool execution | +| `llm/stream` / `llm/generate` (dsh-llm) | **waterfall** | model-call interception | +| `system-prompt/assemble` (dsh-system-prompt) | **waterfall** | mutate the assembly | +| `session/created` / `session/event` (dsh-session) | emit | session lifecycle + log feed | +| `session/flush` (dsh-session) | parallel (awaited) | durability checkpoint | + +### Cordis waterfall semantics (important) + +`ctx.waterfall` is **around-middleware**, not a value reducer. Each listener +receives `(...args, next)`: + +- call `next()` to delegate to later listeners (and ultimately the core + behavior), possibly wrapping it; +- return a value **without** calling `next()` to short-circuit (veto); +- listeners run in registration order; `prepend: true` jumps the queue. + +Mutate the passed-in object (e.g. `options.model = '…'`) or return a +replacement value — both compose. + +## Plugin sanity checklist + +Every MVP feature (including the TODO-marked ones), with the mechanism that +implements it **without modifying the loop**: + +| MVP feature | Plugin mechanism | +|---|---| +| Hook system (user + project level) | listeners on `agent/request`, `agent/step-result`, `tools/execute`, `agent/turn-continuation`; a hooks plugin bridges config files to shell commands | +| `/goal` | force-continue via `agent/turn-continuation` + `steer()` reminders | +| `/loop` | on `agent/turn-end`, `send()` the next iteration; or force-continue | +| Dynamic workflow | orchestrator plugin on `agent/turn-end` / `agent/step-end` driving `send`/`steer` (+ sub-agents later) | +| Queued + steering messages | core `Agent.send()` / `Agent.steer()` | +| Context compaction (auto + manual) | wrap `agent/request`: measure tokens, rewrite `req.messages`, append merged `compaction/*` session events; manual = a command plugin invoking the same routine | +| System prompt configurability | `ctx.systemPrompt.section()` with ordering | +| AGENTS.md (root) | a section provider reading the file | +| AGENTS.md (subdir, on-touch) + file-change notices | `agent.inject()` from a watcher / tool-result listener | +| Built-in tools (Read/Write/Edit/Bash/…) | `ctx.tools.register()`; schemas flow into the assembly automatically | +| ToolSearch / progressive disclosure | wrap `agent/request`, filter `req.tools` | +| Tool sandbox (landlock / sandbox-exec) | wrap `tools/execute` | +| Permission system / AskUserQuestion | wrap `tools/execute` (veto or ask); register an ask tool | +| Plan mode | wrap `tools/execute` (deny writes) + `agent/request` (inject mode prompt) | +| Sub-agents (spawn / fork / steer) | TODO seam on `AgentLoop.create()`; fork = seed Session with parent events; `steer()` on the child handle | +| MCP | one plugin per server: discover tools → `ctx.tools.register()` | +| Skills | section + tool registration; `inject()` skill content on invocation | +| Memory | section provider + tool | +| Scheduled tasks (cron) | plugin registers model-callable scheduling tools; timer fires → `send(…, {source: {kind: 'cron', …}})` when idle / `inject()` notification when busy | +| UI (GUI; CLI emits JSONL) | listen `agent/stream-chunk` + `session/event`; input → `send()` | +| Telemetry / replayable trace | `session/event` → JSONL; replay = `sessions.create(id, seed)` | +| DeepSeek V4 (and other) models | `LlmAdapter` subclass via `registerAdapter` | +| Plugin hot-reload | every registration is a `ctx.effect` → vendored HMR just works | + +## Extension cookbook + +### A tool plugin + +```ts +import type { Context } from 'cordis' + +export const name = 'my-tool' +export const inject = ['tools'] + +export function apply(ctx: Context) { + ctx.tools.register({ + name: 'read_file', + description: 'Read a file from disk.', + parameters: { type: 'object', properties: { path: { type: 'string' } }, required: ['path'] }, + async execute(args: any) { + const text = await readFile(args.path, 'utf8') + return [{ type: 'text', text }] + }, + }) +} +``` + +### A hook plugin (permission gate) + +```ts +export const name = 'permission-gate' + +export function apply(ctx: Context) { + ctx.on('tools/execute', async (exec, next) => { + if (!(await isAllowed(exec))) { + return { + callId: exec.callId, + content: [{ type: 'text', text: 'Denied by policy.' }], + isError: true, + } + } + return next() + }) +} +``` + +### A UI plugin + +```ts +export const name = 'my-ui' +export const inject = ['agents'] + +export function apply(ctx: Context) { + ctx.on('agent/stream-chunk', (agent, turn, step, chunk) => { + if (chunk.type === 'text-delta') render(chunk.text) + }) + onUserInput(text => ctx.agents.get('main')?.send([{ type: 'text', text }])) +} +``` + +A complete runnable wiring lives in [`examples/echo-agent`](../examples/echo-agent) +(mock model + echo tool + stdio UI + JSONL persistence, loaded from +`cordis.yml` with HMR). + +## Deferred work (TODO) + +Tracked here deliberately — each is designed-for but not implemented: + +- **Sub-agent spawn/fork semantics** (seam: `AgentLoop.create()`); inter-agent + channels beyond `send`/`steer`/events. +- **Persistence backends** (JSONL session dirs, sqlite) on the + `session/event` + `session/flush` seam. +- **Compaction implementation** (auto thresholds, summarization prompts) on + the `agent/request` seam, with its session-event types added by declaration + merging. +- **DeepSeek V4 adapter** — first real `LlmAdapter`; triggers the + streaming-protocol review (`TODO(review)` markers in dsh-llm). +- **Parallel tool execution** (concurrency-safety hints on ToolDefinition). +- **Session branching/tree** (pi-style entry tree) if needed beyond seed-based + forking. +- **Session event vocabulary review** once the loop and a persistence plugin + coexist (`TODO(review)` in dsh-session).