Merge branch 'master' into codex/document-host-sandbox-retries

This commit is contained in:
hypatiamay
2026-07-16 09:49:26 +08:00
committed by GitHub
398 changed files with 21905 additions and 3739 deletions
+12 -5
View File
@@ -19,19 +19,26 @@ description: Use when reviewing a pull request in the deepseek-harness repo —
## Blocking requirements
1. **Docs match the code.** Config, defaults, errors, wire fields, events, and public behavior update the package README and JSDoc in the same diff. Comments state non-obvious contracts; flag implementation narration, test walkthroughs, review history, and duplicated rationale for deletion or a link to their one home.
2. **Core type docs match.** Changes to spine or seam vocabulary update the appropriate [core-data-structures](../../../docs/core-data-structures/core.md) page and any `type-equiv` entry. Internal types need no catalog entry.
3. **Registrations clean up.** A new registry contribution has a test that disposes its owner and observes removal.
4. **Required gates pass.** Trust the [current readiness sequence](../../../AGENTS.md#run-the-ci-gates-locally-before-marking-a-pr-ready) and `pnpm run check:pre-push` for their enforced inventory; review the semantic gaps they cannot detect.
1. **New prose receives semantic review.** Use [dsh-prose-standard](../dsh-prose-standard/SKILL.md) to critically review every added or changed Markdown passage, JSDoc, comment, prompt, description, diagnostic, and visible string. Verify required coverage, accuracy, placement, and editorial quality against the owning code or behavior; automated checks do not establish those properties.
2. **Docs match the code.** Config, defaults, errors, wire fields, events, and public behavior update the package README and JSDoc in the same diff. Comments state non-obvious contracts; flag implementation narration, test walkthroughs, review history, and duplicated rationale for deletion or a link to their one home.
3. **Core type docs match.** Changes to spine or seam vocabulary update the appropriate [core-data-structures](../../../docs/core-data-structures/core.md) page and any `type-equiv` entry. Internal types need no catalog entry.
4. **Registrations clean up.** A new registry contribution has a test that disposes its owner and observes removal.
5. **Required gates pass.** Trust the [current readiness sequence](../../../AGENTS.md#run-the-ci-gates-locally-before-marking-a-pr-ready) and `pnpm run check:pre-push` for their enforced inventory; review the semantic gaps they cannot detect.
## Manual checks
- **Intent and seam contracts:** trace both sides of every changed interface. Confirm the implementation matches the PR and any RFC, including errors, cancellation, ownership, and disposal.
- **Lifecycle and concurrency:** for async setup, callbacks, processes, or teardown, apply [defensive-patterns.md](../../../docs/defensive-patterns.md). Check races before publication, cancellation during awaits, independent error reporting, callback containment, and quiescent disposal.
- **Lifecycle and concurrency:** for async setup, callbacks, processes, or teardown, apply [defensive-patterns.md](../../../docs/defensive-patterns.md). Check races before publication, cancellation during awaits, independent error reporting, callback containment, ownership before reentry, complete detach cleanup, and quiescent disposal.
- **Capability shape:** a swappable capability follows the interface / implementation / consumer split. Consumers depend on the interface, not a backend.
- **Scope, ownership, and necessity:** tie each abstraction, state machine, option, defensive copy, and compatibility path to a current contract or production consumer. Challenge unrelated features, speculative generality, and behavior placed outside its owning plugin or service.
- **Configuration:** deployment-varying timeouts, caps, models, URLs, paths, and retry counts are validated `Config` fields, not literals or `DEFAULT_*` constants.
- **Enforcement boundaries:** hidden schema fields, filtered prompts, facades, wrappers, and listener ordering are not authoritative enforcement when direct or alternate callers can bypass them. Exercise denial paths at the boundary that actually executes the operation.
- **Borrowed and derived state:** determine whether retained caller-owned values are borrowed or snapshotted by contract; do not demand copies at typed same-process seams. Materialize mutable values that cross queues, model/tool JSON, durable logs or files, workers, processes, or wire boundaries. Commit notifications and derived state only at the documented success boundary, and trace caches, prompts, UI echoes, replay, and query views to one authoritative source.
- **Bounds cover the final operation:** verify byte, token, item, and time limits at the boundary that owns the complete emitted or retained result, including wrappers and metadata. Probe tiny limits, exact thresholds, oversized single chunks, and multibyte text for byte limits.
- **Real entry path:** tests exercise the shipped Loader, bin, worker, ACP bridge, or subprocess where relevant. A hand-mounted plugin does not catch Loader export-shape failures; a function plugin must named-export its namespace and have no default export.
- **Test strength:** assertions fail on the intended regression and verify external state, logs, events, or disposal rather than restating the implementation or trusting an agent's report. Coverage is necessary but not evidence that the scenario is correct.
- **Changed checks have a negative control:** a new automated check, or a changed acceptance path in one, has a deliberately invalid case that reaches the real top-level runner and fails for the intended rule; a green happy path does not prove the check is wired.
- **Implemented RFCs match shipped reality:** when a PR implements a proposed RFC, move and rewrite it as present-tense shipped state in the same diff, then verify paths, names, and mechanisms against the implementation.
- **Transcript changes:** editor-visible or model-visible changes update snapshots or explain why no snapshot applies. Review golden diffs as behavior changes, not formatting noise.
- **Bilingual changes:** compare meaning and terminology on both sides; a green pairing hash does not prove translation quality.
+1 -1
View File
@@ -54,7 +54,7 @@ pnpm run test:snapshot
Run built-bin smoke tests after `pnpm run build` when app packages, app boot, package runtime imports, bin entries, loader behavior, or published artifact paths change.
```sh
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
pnpm exec vitest run --config vitest.e2e.config.ts packages/examples/stdio-demo/tests/built-bin.e2e.ts packages/examples/acp-demo/tests/built-bin.e2e.ts
```
Run real e2e when behavior depends on a real model/API, tool-use loop, ACP integration, prompt injection, or end-to-end agent UX. If `.env` is available, use it; do not print secrets.
+5 -4
View File
@@ -14,7 +14,7 @@ These are authoritative; read them at the source so this skill never drifts out
- **[docs/i18n/README.md](../../../docs/i18n/README.md)** — the pairing contract: the three-file pair (`foo.md`, `foo.zh.md`, `foo.i18n.yaml`), the consistency record's both-side blob hashes, the language-switcher lines, scope/exclusions, and the rollout manifest.
- **[docs/i18n/translation-rules.md](../../../docs/i18n/translation-rules.md)** — how to translate: faithfulness, structure preservation, terminology discipline, typography (MUST/SHOULD levels).
- **[docs/i18n/terminology.md](../../../docs/i18n/terminology.md)** — the terminology table, binding in both directions. Load it BEFORE translating, not when a term feels uncertain; the terms you don't notice are the ones that drift.
- **[docs/i18n/translation-prompt.md](../../../docs/i18n/translation-prompt.md)** — the automated pipeline's machine-consumed template. Agents using this skill do not render it; keep rules shared with this skill synchronized.
- **[docs/i18n/translation-prompt.md](../../../docs/i18n/translation-prompt.md)** — the automated pipeline's machine-consumed template. Agents using this skill do not render it; the renderer injects `translation-rules.md` so rules have only one home.
- **[dsh-prose-standard](../dsh-prose-standard/SKILL.md)** — required prose coverage and editorial judgment. Apply it to both sides without adding or dropping source propositions.
## Find the work
@@ -42,8 +42,9 @@ Do not process every file the same way:
- **Pass 1 — write, don't transpose.** Read a semantic unit, then restate it as a native technical author in the nearest [style sample's](../../../docs/i18n/style-samples.md) register. Preserve the required frame without forcing sentence-by-sentence correspondence.
- **Pass 2 — verify against the source, clause by clause.** Fidelity is checked here, not written in: confirm nothing was added or dropped, every term follows the table, and each code span survived verbatim. Fix by rewriting the sentence natively, not by patching words into it.
- Write only the final text to the file, never drafts or notes.
- Every term in [terminology.md](../../../docs/i18n/terminology.md) renders exactly as specified, in both directions, including first-occurrence annotations. A term the table misses: translate only with a citable precedent from a major Chinese OSS/vendor doc; otherwise keep the English and add it to the PR's 「待定术语」 list with your suggested rendering. Never invent a rendering inline — that decision belongs to a human and then to the table.
- Every term in [terminology.md](../../../docs/i18n/terminology.md) renders exactly as specified. For a Chinese target, use the Chinese and first-occurrence columns; an unlisted term needs a citable Chinese OSS/vendor precedent or stays English under 「待定术语」. For an English target, use the English column and an established English technical term; preserve an ambiguous source term with a short gloss and list it as pending. Never invent a rendering inline.
- Code blocks are byte-identical across the pair, comments included. Relative links keep their `.md` targets; only the switcher line links `.zh.md`.
- The pairing gate checks heading depths, fenced blocks, table row and column counts, list kinds, ordered-list starts, list item counts, and link targets. In Pass 2, manually verify list and table order, noncanonical list numbering, inline code, emphasis, meaning, terminology, and tone.
## Finish the pair
@@ -51,9 +52,9 @@ Do not process every file the same way:
2. Record consistency: `pnpm run verify-translation-pairing --write` recomputes and records both sides' full blob hashes in `foo.i18n.yaml`. The yaml diff in your PR is the reviewable statement "I confirmed these two say the same thing" — only run it after you actually have.
3. New batch landed? Add the `.md` paths to `required` in [scripts/translation-pairing.manifest.json](../../../scripts/translation-pairing.manifest.json) so the gate ratchets forward.
## Verify the gate, not your eyes
## Verify the mechanical and human halves
Run `pnpm run verify-translation-pairing`, then the rest of the Markdown gates (`pnpm run verify-md-wrap && pnpm run verify-md-links`, or full `pnpm run doc-sync` before the PR). Fix what they report; do not hand-check what they cover. What they can NOT check — whether the two sides truly say the same thing, terminology judgment calls, tone — is exactly what the PR reviewer will read for, so keep the PR reviewable: state which pairs are new vs minimally updated, and list 「待定术语」 prominently.
Run `pnpm run verify-translation-pairing`, then the rest of the Markdown gates (`pnpm run verify-md-wrap && pnpm run verify-md-links`, or full `pnpm run doc-sync` before the PR). Fix what they report and manually verify the obligations listed in Pass 2 that the gates do not encode. Keep the PR reviewable: state which pairs are new versus minimally updated and list 「待定术语」 prominently.
## How to respond to translation review
+8
View File
@@ -0,0 +1,8 @@
# Editor-side declaration of the repo's text conventions. Pairs with
# .gitattributes: that file pins what GIT produces (LF checkouts), this one
# pins what EDITORS write to disk — the one path git's filters cannot reach.
root = true
[*]
end_of_line = lf
insert_final_newline = true
+7
View File
@@ -0,0 +1,7 @@
# The repo's canonical text form is LF, enforced at checkout too: no smudge
# boundary between working tree and repo, so byte-level gates (verify-*
# comparisons, blob hashing, coverage offsets) see one form on every host.
# If a file class ever genuinely needs CRLF in the working tree (.bat/.cmd
# for cmd.exe), add a `*.bat text eol=crlf` override AFTER this line — the
# in-repo form stays LF; CRLF becomes checkout-time presentation only.
* text=auto eol=lf
+104 -4
View File
@@ -160,18 +160,118 @@ jobs:
- name: Run complete keyless Python suite
run: uv run --python 3.10 --group test --project python/sdk pytest
# Blocking Windows build lane: keep the already-green native build protected
# while the broader observational gate matrix below exposes the remaining
# portability work without blocking mainline merges.
windows-build:
runs-on: windows-2025
name: windows / build
steps:
- uses: actions/checkout@v6
- uses: actions/setup-node@v6
with:
node-version: ${{ env.PRIMARY_NODE_VERSION }}
- name: Enable corepack (pnpm)
run: corepack enable
- name: Install (immutable)
run: pnpm install --frozen-lockfile
- name: Build (tsc -b + tsdown)
run: pnpm run build
# Observational, non-blocking Windows static, lint, and artifact lanes. Coverage
# and snapshot stay Linux-only until their platform-specific runtime failures
# have dedicated support. Run the gates from native PowerShell: an MSYS parent
# would change the environment being measured. This job intentionally stays
# out of all-checks-passed.needs.
windows-gates:
continue-on-error: true
runs-on: windows-2025
name: windows node 24 / ${{ matrix.lane }}
env:
DSH_GATE_CONCURRENCY: ${{ matrix.gate_concurrency }}
DSH_PUBLINT_CONCURRENCY: ${{ matrix.publint_concurrency }}
DSH_ESLINT_CACHE: ${{ matrix.eslint_cache }}
strategy:
fail-fast: false
matrix:
include:
- lane: static
command: pnpm run check:ci:static
gate_concurrency: '4'
publint_concurrency: '8'
eslint_cache: ''
- lane: lint
command: pnpm run check:ci:lint
gate_concurrency: '1'
publint_concurrency: '8'
eslint_cache: '1'
- lane: artifacts
command: pnpm run check:ci:artifacts
gate_concurrency: '3'
publint_concurrency: '8'
eslint_cache: ''
steps:
- uses: actions/checkout@v6
- name: Enable Developer Mode (symlink support)
shell: pwsh
run: >-
reg add "HKLM\SOFTWARE\Microsoft\Windows\CurrentVersion\AppModelUnlock"
/t REG_DWORD /f /v "AllowDevelopmentWithoutDevLicense" /d "1"
- uses: actions/setup-node@v6
with:
node-version: ${{ env.PRIMARY_NODE_VERSION }}
- name: Enable corepack (pnpm)
shell: pwsh
run: corepack enable
- name: Resolve pnpm store path
id: pnpm-store
shell: pwsh
run: '"path=$(pnpm store path --silent)" >> $env:GITHUB_OUTPUT'
- uses: actions/cache@v4
with:
path: ${{ steps.pnpm-store.outputs.path }}
key: ${{ runner.os }}-node-${{ env.PRIMARY_NODE_VERSION }}-pnpm-${{ hashFiles('pnpm-lock.yaml') }}
restore-keys: |
${{ runner.os }}-node-${{ env.PRIMARY_NODE_VERSION }}-pnpm-
- name: Install (immutable)
shell: pwsh
run: pnpm install --frozen-lockfile
- uses: actions/cache@v4
if: matrix.lane == 'lint'
with:
path: .cache/eslint
key: ${{ runner.os }}-node-${{ env.PRIMARY_NODE_VERSION }}-eslint-${{ hashFiles('pnpm-lock.yaml', 'eslint.config.mjs', 'tsconfig.json', 'packages/*/*/tsconfig.json', 'examples/*/tsconfig.json') }}
restore-keys: |
${{ runner.os }}-node-${{ env.PRIMARY_NODE_VERSION }}-eslint-
- name: Run gates
shell: pwsh
run: ${{ matrix.command }}
# Single stable required check for branch protection: require "all checks
# passed" instead of enumerating matrix legs whose names change as lanes and
# node versions evolve. Every other job in THIS workflow must be listed in
# `needs` (`needs` cannot reach across workflow files; e2e.yml stays its own
# check). `if: always()` is load-bearing: without it a failed dependency
# node versions evolve. Every blocking job in THIS workflow must be listed in
# `needs`; explicitly observational jobs such as windows-gates stay out
# (`needs` cannot reach across workflow files; e2e.yml stays its own check).
# `if: always()` is load-bearing: without it a failed dependency
# would SKIP this job, and GitHub counts a skipped required check as passing
# — so this job always runs and fails on any non-success result, including
# 'cancelled' and 'skipped'.
all-checks-passed:
name: all checks passed
runs-on: ubuntu-latest
needs: [node-24, node-compat, python-sdk]
needs: [node-24, node-compat, python-sdk, windows-build]
if: always()
steps:
- name: Fail if any needed job did not succeed
+5 -4
View File
@@ -11,7 +11,7 @@ DeepSeek Harness SDK is a plugin-based agent harness on vendored Cordis: **every
```
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)
core/ product API spine: session, system-prompt, tools, agent, agent-loop
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
@@ -26,11 +26,12 @@ packages/ Harness packages at packages/<group>/<pkg>/, all named @deepseek-ai
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/stdio/JSON-RPC front doors; boot, approval, and interaction plugins
ui/ ACP/stdio/JSON-RPC bridges; boot, approval, interaction plugins
examples/ demo bundles (agent-spine + stdio/ACP/JSON-RPC bins) leaves load
support/ dev/test infrastructure packages
util/ zero-dependency utilities
python/ Python SDK and bundled runtime (see python/README.md)
examples/ Runnable demos: thin cordis.yml leaves over the app packages (see examples/AGENTS.md)
examples/ Runnable cordis.yml leaves over packages/examples bundles (see examples/AGENTS.md)
docs/ architecture, generated catalogs, RFCs, postmortems, cookbook (see docs/AGENTS.md)
scripts/ repo gates and generators
```
@@ -82,7 +83,7 @@ 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
pnpm exec vitest run --config vitest.e2e.config.ts packages/examples/stdio-demo/tests/built-bin.e2e.ts packages/examples/acp-demo/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 gate ([why](docs/testing.md)); report only commands actually run.
+1 -1
View File
@@ -3,4 +3,4 @@
# after editing either side, bring the other along and re-record with:
# pnpm run verify-translation-pairing --write
README.md: 53dd3896eb15800125673e7c44f7de02daca9376
README.zh.md: 2119dabf0ae2d2e16e274e44fda7cbbaec146dc9
README.zh.md: ab826f62658248249ec18c57b35c0065c0f909d1
+1 -1
View File
@@ -2,7 +2,7 @@
[English](README.md) | 中文
**DeepSeek Harness SDK** 是用于构建 agent harness 的 SDK,采取基于插件的设计。
**DeepSeek Harness SDK** 是用于构建 agent harness(智能体框架)的 SDK,采取基于插件的设计。
## 开发
+5 -3
View File
@@ -32,6 +32,7 @@ A harness is one [Cordis](cordis-primer.md) context. Packages contribute service
| `ctx.web` | [`web/`](../packages/web/README.md) | search/fetch provider registries |
| `ctx.compact` | [`compact/`](../packages/compact/README.md) | session-log compaction |
| `ctx.subagents` | [`subagent/`](../packages/subagent/README.md) | named delegation providers |
| `ctx.tasks` | [`tasks/`](../packages/tasks/README.md) | background task registry + generic `task_*` control tools |
| `ctx.workflows` | [`workflow/`](../packages/workflow/README.md) | script-driven multi-agent orchestration |
| `ctx.sessionPersistence` | [`session-persistence/`](../packages/session-persistence/README.md) | durable storage for session logs |
| `ctx.sessionQuery` | [`session-query/`](../packages/session-query/README.md) | live-preferred logical-corpus and exact-event reads |
@@ -106,11 +107,11 @@ Every session event is turn-enclosed. Reloading a crashed session preserves the
### Agent Handles
`ctx.agents` owns live agents and returns an `AgentHandle { agent, dispose() }`. `Agent` is the API other plugins drive: `send()` queues work, `steer()` injects mid-turn content, `inject()` appends context and opens a one-shot injection turn when idle, `cancel()` is the public stop primitive, and `whenIdle()` observes quiescence. The caller fiber and concrete factory provider structurally co-own programmatic lifecycles; a consumer handle is the only non-structural teardown capability, and every owner reaches the same awaited disposer.
`ctx.agents` owns live agents and returns `AgentHandle { agent, dispose() }`. Plugins drive `Agent` through `send()`, `steer()`, `inject()`, `cancel()`, and `whenIdle()`. The caller fiber and factory provider structurally co-own programmatic lifecycles; the consumer handle is the only other teardown capability, and all owners await one disposer.
### Agent Scope
Every live agent owns a scoped `agent.ctx`. Its registrations shadow same-named globals, receive only that agent's dispatches, and unwind with the agent. `CreateAgentOptions.setup(agentCtx)` composes the scope before publication. The [semantic-gates RFC](rfc/implemented/process/2026-07-14-typescript-program-backed-semantic-gates.md) defines typed resolvers that derive carrier checks from merged `Events` signatures and `scopeTarget`, eliminating the handwritten event table. See the [agent-scope RFC](rfc/implemented/architecture/2026-07-08-agent-scope-contexts.md); subagent composition controls are documented [separately](rfc/implemented/feature/2026-07-12-subagent-persona-tool-filter-and-depth.md).
Every live agent owns a scoped `agent.ctx`. Its registrations shadow same-named globals, receive only that agent's dispatches, and unwind with the agent; async effects such as background-task cleanup are awaited. `CreateAgentOptions.setup(agentCtx)` composes the scope before publication. The [semantic-gates RFC](rfc/implemented/process/2026-07-14-typescript-program-backed-semantic-gates.md) defines typed resolvers that derive carrier checks from merged `Events` signatures and `scopeTarget`, eliminating the handwritten event table. See the [agent-scope RFC](rfc/implemented/architecture/2026-07-08-agent-scope-contexts.md); subagent composition controls are documented [separately](rfc/implemented/feature/2026-07-12-subagent-persona-tool-filter-and-depth.md).
## State
@@ -138,7 +139,7 @@ Some seams bend the template deliberately. LLM keeps interface and consumer voca
### Bundles And Apps
`dsh-agent-core` is the default composition bundle: one plugin loading the shared spine ([README](../packages/core/agent-core/README.md)). App packages compose it with a front door and boot `bin`: `dsh-stdio-agent` for terminal REPL, and `dsh-acp-agent` for ACP over JSON-RPC stdio with no stdout logger ([ui/](../packages/ui/README.md)). `dsh-jsonrpc-agent` instead boots an external `cordis.yml`; the Python SDK injects the package default only when no explicit config channel is set and drives `dsh-jsonrpc` over line-delimited stdio JSON-RPC ([Python SDK](../python/README.md)). A deployment is a thin `cordis.yml` leaf: swappable backends, one app entry, and optional product tools ([examples/](../examples/AGENTS.md), [runnable wirings](cookbook/extension-cookbook.md#runnable-wirings), [graph atlas](graph-atlas.md)).
`dsh-agent-spine-demo` is the default composition bundle: one plugin loading the shared spine ([README](../packages/examples/agent-spine-demo/README.md)). App packages compose it with a front door and boot `bin`: `dsh-stdio-demo` for terminal REPL, and `dsh-acp-demo` for ACP over JSON-RPC stdio with no stdout logger ([ui/](../packages/ui/README.md)). `dsh-jsonrpc-agent` instead boots an external `cordis.yml`; the Python SDK injects the package default only when no explicit config channel is set and drives `dsh-jsonrpc` over line-delimited stdio JSON-RPC ([Python SDK](../python/README.md)). A deployment is a thin `cordis.yml` leaf: swappable backends, one app entry, and optional product tools ([examples/](../examples/AGENTS.md), [runnable wirings](cookbook/extension-cookbook.md#runnable-wirings), [graph atlas](graph-atlas.md)).
### Where New Behavior Goes
@@ -149,6 +150,7 @@ New behavior should attach to a documented extension point; changing the shipped
| Add a model provider | register an adapter on `ctx.llm` |
| Add a model-facing capability | register a tool on `ctx.tools`; schemas flow into prompt assembly |
| Add command execution | implement and register a `ctx.bash` backend |
| Add a long-running/background capability | register the work on `ctx.tasks`; the generic `task_*` tools collect/stop it |
| Add filesystem access or policy | implement a `ctx.fs` provider or listen on `fs/*` policy events |
| Confine spawned processes | a `ctx.sandbox` backend; consumers wrap their argv before spawning |
| Intercept prompts, requests, tool use, or continuation | listen on the relevant `agent/*` or `tools/*` waterfall; use serial `agent/turn-stop` for a monotonic terminal stop |
+17 -9
View File
@@ -40,13 +40,13 @@ flowchart LR
pkg_tool_todo["tool-todo"]
pkg_user_interaction["user-interaction"]
svc_userInteraction["ctx.userInteraction<br/>Human question/answer seam"]
pkg_stdio_agent["stdio-agent"]
pkg_stdio_demo["stdio-demo"]
pkg_skill["skill"]
svc_skills["ctx.skills<br/>Skill provider registry"]
pkg_skill_local["skill-local"]
svc_agents["ctx.agents<br/>Agent registry"]
svc_agentLoop["ctx.agentLoop<br/>Concrete loop driver"]
pkg_agent_core["agent-core"]
pkg_agent_spine_demo["agent-spine-demo"]
pkg_bash["bash"]
svc_bash["ctx.bash<br/>Bash executor seam"]
pkg_bash_local["bash-local"]
@@ -75,6 +75,9 @@ flowchart LR
pkg_subagent_fork["subagent-fork"]
pkg_subagent_acp["subagent-acp"]
pkg_subagent_mock["subagent-mock"]
pkg_tasks["tasks"]
svc_tasks["ctx.tasks<br/>Background task registry"]
pkg_tool_tasks["tool-tasks"]
pkg_web["web"]
svc_web["ctx.web<br/>Web access provider registry"]
pkg_web_search_exa["web-search-exa"]
@@ -113,13 +116,14 @@ flowchart LR
pkg_session_query --> svc_sessionQuery
pkg_skill --> svc_skills
pkg_skill_local --> svc_skills
pkg_stdio_agent --> svc_userInteraction
pkg_stdio_demo --> svc_userInteraction
pkg_subagent --> svc_subagents
pkg_subagent_acp --> svc_subagents
pkg_subagent_fork --> svc_subagents
pkg_subagent_mock --> svc_subagents
pkg_subagent_spawn --> svc_subagents
pkg_system_prompt --> svc_systemPrompt
pkg_tasks --> svc_tasks
pkg_tools --> svc_tools
pkg_user_interaction --> svc_userInteraction
pkg_web --> svc_web
@@ -129,11 +133,11 @@ flowchart LR
pkg_web_search_perplexity --> svc_web
pkg_workflow --> svc_workflows
pkg_workflow_workerthread --> svc_workflows
svc_agentLoop --> pkg_agent_core
svc_agentLoop --> pkg_agent_spine_demo
svc_agents --> pkg_acp
svc_agents --> pkg_agent_loop
svc_agents --> pkg_invariants
svc_agents --> pkg_stdio_agent
svc_agents --> pkg_stdio_demo
svc_agents --> pkg_subagent_inprocess
svc_approval --> pkg_tool_bash
svc_approval --> pkg_tools
@@ -162,6 +166,9 @@ flowchart LR
svc_systemPrompt --> pkg_tool_fs
svc_systemPrompt --> pkg_tool_web
svc_systemPrompt --> pkg_tools
svc_tasks --> pkg_tool_bash
svc_tasks --> pkg_tool_subagent
svc_tasks --> pkg_tool_tasks
svc_tools --> pkg_acp
svc_tools --> pkg_agent_loop
svc_tools --> pkg_tool_ask_user
@@ -173,7 +180,7 @@ flowchart LR
svc_tools --> pkg_tool_todo
svc_tools --> pkg_tool_web
svc_userInteraction --> pkg_acp
svc_userInteraction --> pkg_stdio_agent
svc_userInteraction --> pkg_stdio_demo
svc_userInteraction --> pkg_tool_ask_user
svc_web --> pkg_tool_web
svc_workflows --> pkg_tool_workflow
@@ -188,10 +195,10 @@ flowchart LR
| `ctx.sessionQuery` | `seam` | [`session-query`](../packages/session-query/session-query) | - | - | - | Resolves live and optional persisted logs into one logical corpus for exact reads. |
| `ctx.systemPrompt` | `core` | [`system-prompt`](../packages/core/system-prompt) | - | [`agent-loop`](../packages/core/agent-loop), [`tools`](../packages/core/tools), [`tool-fs`](../packages/fs/tool-fs), [`tool-web`](../packages/web/tool-web) | - | Collects prompt sections and model-facing tool schemas for each step. |
| `ctx.tools` | `core` | [`tools`](../packages/core/tools) | - | [`agent-loop`](../packages/core/agent-loop), [`tool-ask-user`](../packages/ui/tool-ask-user), [`tool-bash`](../packages/bash/tool-bash), [`tool-cordis`](../packages/cordis/tool-cordis), [`tool-fs`](../packages/fs/tool-fs), [`tool-skill`](../packages/skill/tool-skill), [`tool-subagent`](../packages/subagent/tool-subagent), [`tool-todo`](../packages/todo/tool-todo), [`tool-web`](../packages/web/tool-web), [`acp`](../packages/ui/acp) | - | Registers capabilities, owns Code Mode transport, and routes calls through pre-policy, monotonic guards, around dispatch, post-policy, and final-result observation. |
| `ctx.userInteraction` | `seam` | [`user-interaction`](../packages/ui/user-interaction) | [`stdio-agent`](../packages/ui/stdio-agent), [`acp`](../packages/ui/acp) | [`tool-ask-user`](../packages/ui/tool-ask-user), [`stdio-agent`](../packages/ui/stdio-agent), [`acp`](../packages/ui/acp) | - | UI front doors provide the active human-answer provider; tool-ask-user pauses a tool call on the provider-neutral ask() promise. |
| `ctx.userInteraction` | `seam` | [`user-interaction`](../packages/ui/user-interaction) | [`stdio-demo`](../packages/examples/stdio-demo), [`acp`](../packages/ui/acp) | [`tool-ask-user`](../packages/ui/tool-ask-user), [`stdio-demo`](../packages/examples/stdio-demo), [`acp`](../packages/ui/acp) | - | UI front doors provide the active human-answer provider; tool-ask-user pauses a tool call on the provider-neutral ask() promise. |
| `ctx.skills` | `seam` | [`skill`](../packages/skill/skill) | [`skill-local`](../packages/skill/skill-local) | [`tool-skill`](../packages/skill/tool-skill) | - | Merges provider skill catalogs; tool-skill renders the session-prefix catalog and loads complete skill bodies. |
| `ctx.agents` | `core` | [`agent`](../packages/core/agent) | - | [`agent-loop`](../packages/core/agent-loop), [`acp`](../packages/ui/acp), [`subagent-inprocess`](../packages/subagent/subagent-inprocess), [`stdio-agent`](../packages/ui/stdio-agent), [`invariants`](../packages/support/invariants) | - | Owns live Agent handles and the create/resume factory seam. |
| `ctx.agentLoop` | `bundle` | [`agent-loop`](../packages/core/agent-loop) | - | [`agent-core`](../packages/core/agent-core) | - | The one concrete loop plugin; extension packages depend on dsh-agent events and services, not on this package. |
| `ctx.agents` | `core` | [`agent`](../packages/core/agent) | - | [`agent-loop`](../packages/core/agent-loop), [`acp`](../packages/ui/acp), [`subagent-inprocess`](../packages/subagent/subagent-inprocess), [`stdio-demo`](../packages/examples/stdio-demo), [`invariants`](../packages/support/invariants) | - | Owns live Agent handles and the create/resume factory seam. |
| `ctx.agentLoop` | `bundle` | [`agent-loop`](../packages/core/agent-loop) | - | [`agent-spine-demo`](../packages/examples/agent-spine-demo) | - | The one concrete loop plugin; extension packages depend on dsh-agent events and services, not on this package. |
| `ctx.bash` | `seam` | [`bash`](../packages/bash/bash) | [`bash-local`](../packages/bash/bash-local), [`bash-sandbox`](../packages/bash/bash-sandbox) | [`tool-bash`](../packages/bash/tool-bash), [`hooks-claude`](../packages/hooks/hooks-claude), [`hooks-codex`](../packages/hooks/hooks-codex) | - | The model-facing bash tools and hook bridges consume this seam; sandboxed or remote executors replace bash-local without touching them. |
| `ctx.sandbox` | `seam` | [`sandbox`](../packages/sandbox/sandbox) | [`sandbox-local`](../packages/sandbox/sandbox-local) | [`bash-sandbox`](../packages/bash/bash-sandbox) | - | Consumers hand over the exact argv they are about to spawn; same-world backends wrap it under a per-call policy and report enforcement. |
| `ctx.approval` | `seam` | `approval` | [`acp`](../packages/ui/acp) | [`tools`](../packages/core/tools), [`tool-bash`](../packages/bash/tool-bash) | - | One-shot permission decisions dispatched over the `approval/request` waterfall; answerers are listeners (the ACP bridge for its own agents), absence fails closed to `unavailable`. |
@@ -200,6 +207,7 @@ flowchart LR
| `ctx.fs` | `seam` | [`fs`](../packages/fs/fs) | [`fs-local`](../packages/fs/fs-local) | [`tool-fs`](../packages/fs/tool-fs) | [`fs-policy`](../packages/fs/fs-policy) | tool-fs executes read/write/edit through ctx.fs; fs-policy contributes observed-state checks through the fs/* event gate. |
| `ctx.compact` | `seam` | [`compact`](../packages/compact/compact) | [`compact-basic`](../packages/compact/compact-basic) | [`compact-basic`](../packages/compact/compact-basic) | - | The basic backend currently consumes the pre-step event directly; a model-facing compact tool remains deferred. |
| `ctx.subagents` | `seam` | [`subagent`](../packages/subagent/subagent) | [`subagent-spawn`](../packages/subagent/subagent-spawn), [`subagent-fork`](../packages/subagent/subagent-fork), [`subagent-acp`](../packages/subagent/subagent-acp), [`subagent-mock`](../packages/support/subagent-mock) | [`tool-subagent`](../packages/subagent/tool-subagent) | - | Providers implement transports; tool-subagent exposes one configured provider as a model-facing tool name. |
| `ctx.tasks` | `core` | [`tasks`](../packages/tasks/tasks) | - | [`tool-bash`](../packages/bash/tool-bash), [`tool-subagent`](../packages/subagent/tool-subagent), [`tool-tasks`](../packages/tasks/tool-tasks) | - | Producers (tool-bash background commands, tool-subagent background delegations) register running work; tool-tasks is the model-facing control surface that reads, lists, and kills it. |
| `ctx.web` | `seam` | [`web`](../packages/web/web) | [`web-search-exa`](../packages/web/web-search-exa), [`web-search-perplexity`](../packages/web/web-search-perplexity), [`web-search-deepseek`](../packages/web/web-search-deepseek), [`web-fetch-local`](../packages/web/web-fetch-local) | [`tool-web`](../packages/web/tool-web) | - | Search and fetch providers register into one ctx.web seam; tool-web owns the stable model-facing names. |
| `ctx.workflows` | `seam` | [`workflow`](../packages/workflow/workflow) | [`workflow-workerthread`](../packages/workflow/workflow-workerthread) | [`tool-workflow`](../packages/workflow/tool-workflow) | - | One engine per context (bash shape, no named-provider registry); the worker-thread engine fans agent() calls out through ctx.subagents. |
+181 -88
View File
@@ -27,7 +27,7 @@ Depends on: `Stream` (`@agentclientprotocol/sdk`)
Source: [`packages/ui/acp/src/index.ts:203`](../packages/ui/acp/src/index.ts)
## `@deepseek-ai/dsh-acp-agent`
## `@deepseek-ai/dsh-acp-demo`
```ts config-catalog
/**
@@ -37,7 +37,7 @@ Source: [`packages/ui/acp/src/index.ts:203`](../packages/ui/acp/src/index.ts)
* deployment persona (forwarded to the system-prompt plugin); `toolOrder` is
* the explicit model-facing tool order (forwarded to the system-prompt plugin);
* `tools` is the tool registry's config (its presentation `mode`, forwarded
* through agent-core); `persistenceRoot` is the JSONL backend's directory.
* through agent-spine-demo); `persistenceRoot` is the JSONL backend's directory.
*/
export interface Config {
/** Model name for ACP-created agents (must have a registered adapter). */
@@ -46,58 +46,22 @@ export interface Config {
persona?: string
/** Explicit model-facing tool order (the system-prompt plugin's `toolOrder` config; see dsh-system-prompt). */
toolOrder?: string[]
/** Tool-registry config — its presentation `mode` (forwarded through agent-core; see dsh-tools). */
/** Tool-registry config — its presentation `mode` (forwarded through agent-spine-demo; see dsh-tools). */
tools?: ToolsConfig
/** Directory the JSONL session backend writes under. Defaults to `./.sessions`. */
persistenceRoot?: string
/** Skill registry, local-provider, and model-facing consumer config forwarded to agent-core. */
/** Skill registry, local-provider, and model-facing consumer config forwarded to agent-spine-demo. */
skills?: agentCore.SkillConfig
/** Model-facing bash tool config forwarded through agent-core. */
toolBash?: NonNullable<agentCore.Config['toolBash']>
/** Generic background-task control-tool config forwarded through agent-core. */
toolTasks?: NonNullable<agentCore.Config['toolTasks']>
}
```
Depends on: [`agentCore`](../packages/core/agent-core/src/index.ts) · [`ToolsConfig`](#deepseek-aidsh-tools)
Depends on: [`agentCore`](../packages/examples/agent-spine-demo/src/index.ts) · [`ToolsConfig`](#deepseek-aidsh-tools)
Source: [`packages/ui/acp-agent/src/index.ts:31`](../packages/ui/acp-agent/src/index.ts)
## `@deepseek-ai/dsh-agent-core`
```ts config-catalog
/**
* Bundle config: each field forwarded verbatim to the child that owns it — `agents` to the
* agent loop (an app that pre-creates no agents, like the ACP bridge, omits it),
* `persona` and `toolOrder` to the system-prompt plugin (the deployment's persona section and
* the explicit model-facing tool order), the `tools` object to the tool registry (its
* presentation `mode`), and `skills` to the skill registry/local provider/tool consumer.
* The schema intersects the owners' schemas, which supply defaults for every
* optional input and keep validation from drifting.
*/
export interface Config {
/** The agent-loop `agents` list (see dsh-agent-loop's `Config`). */
agents?: AgentLoopConfig['agents']
/** The deployment persona (see dsh-system-prompt's `Config`). */
persona?: SystemPromptConfig['persona']
/** The explicit model-facing tool order (see dsh-system-prompt's `Config`). */
toolOrder?: SystemPromptConfig['toolOrder']
/** The tool registry's config — its presentation `mode` (see dsh-tools' `Config`). */
tools?: ToolsConfig
/** Skill registry, local provider, and model-facing consumer config. */
skills?: SkillConfig
}
/** Skill bundle config forwarded to the registry, local provider, and model-facing consumer. */
export interface SkillConfig {
/** Registry-level discovery cache settings. */
registry?: SkillRegistryConfig
/** Local filesystem skill provider settings. */
local?: SkillLocal.Config
/** Model-facing skill catalog and tool settings. */
tool?: toolSkill.Config
}
```
Depends on: [`AgentLoopConfig`](#deepseek-aidsh-agent-loop) · [`SkillLocal`](../packages/skill/skill-local/src/index.ts) · [`SkillRegistryConfig`](#deepseek-aidsh-skill) · [`SystemPromptConfig`](#deepseek-aidsh-system-prompt) · [`ToolsConfig`](#deepseek-aidsh-tools) · [`toolSkill`](../packages/skill/tool-skill/src/index.ts)
Source: [`packages/core/agent-core/src/index.ts:46`](../packages/core/agent-core/src/index.ts)
Source: [`packages/examples/acp-demo/src/index.ts:31`](../packages/examples/acp-demo/src/index.ts)
## `@deepseek-ai/dsh-agent-loop`
@@ -122,6 +86,56 @@ Depends on: [`AgentId`](../packages/core/agent/src/index.ts) · [`AgentOptions`]
Source: [`packages/core/agent-loop/src/index.ts:322`](../packages/core/agent-loop/src/index.ts)
## `@deepseek-ai/dsh-agent-spine-demo`
```ts config-catalog
/**
* Bundle config: each field forwarded verbatim to the child that owns it —
* `agents` to the agent loop (an app that pre-creates no agents, like the ACP
* bridge, simply omits it), `persona` and `toolOrder` to the system-prompt
* plugin (the deployment's persona section and the explicit model-facing tool
* order), the `tools` object to the tool registry (its presentation `mode`),
* and `toolBash`/`toolTasks` to the two model-facing tool plugins this bundle
* owns. Producer opt-in stays producer-local: `toolBash` configures bash only;
* future background-capable tools remain independently composed plugins.
* Every field is optional INPUT here because each owner's schema
* supplies the default (`[]` / `''` / absent — lexicographic / `native`); the
* schema is the INTERSECTION of the owners' own schemas (the registry's
* nested under its `tools` key), so validation and defaulting can never
* drift from them.
*/
export interface Config {
/** The agent-loop `agents` list (see dsh-agent-loop's `Config`). */
agents?: AgentLoopConfig['agents']
/** The deployment persona (see dsh-system-prompt's `Config`). */
persona?: SystemPromptConfig['persona']
/** The explicit model-facing tool order (see dsh-system-prompt's `Config`). */
toolOrder?: SystemPromptConfig['toolOrder']
/** The tool registry's config — its presentation `mode` (see dsh-tools' `Config`). */
tools?: ToolsConfig
/** Skill registry, local provider, and model-facing consumer config. */
skills?: SkillConfig
/** Model-facing bash tool config, including this producer's background opt-in. */
toolBash?: toolBash.Config
/** Generic background-task control-tool wait bounds. */
toolTasks?: toolTasks.Config
}
/** Skill bundle config forwarded to the registry, local provider, and model-facing consumer. */
export interface SkillConfig {
/** Registry-level discovery cache settings. */
registry?: SkillRegistryConfig
/** Local filesystem skill provider settings. */
local?: SkillLocal.Config
/** Model-facing skill catalog and tool settings. */
tool?: toolSkill.Config
}
```
Depends on: [`AgentLoopConfig`](#deepseek-aidsh-agent-loop) · [`SkillLocal`](../packages/skill/skill-local/src/index.ts) · [`SkillRegistryConfig`](#deepseek-aidsh-skill) · [`SystemPromptConfig`](#deepseek-aidsh-system-prompt) · [`toolBash`](../packages/bash/tool-bash/src/index.ts) · [`ToolsConfig`](#deepseek-aidsh-tools) · [`toolSkill`](../packages/skill/tool-skill/src/index.ts) · [`toolTasks`](../packages/tasks/tool-tasks/src/index.ts)
Source: [`packages/examples/agent-spine-demo/src/index.ts:55`](../packages/examples/agent-spine-demo/src/index.ts)
## `@deepseek-ai/dsh-bash-local`
```ts config-catalog
@@ -140,7 +154,7 @@ export interface Config {
}
```
Source: [`packages/bash/bash-local/src/index.ts:18`](../packages/bash/bash-local/src/index.ts)
Source: [`packages/bash/bash-local/src/index.ts:17`](../packages/bash/bash-local/src/index.ts)
## `@deepseek-ai/dsh-bash-sandbox`
@@ -151,7 +165,7 @@ Requires: `sandbox`
* Plugin config: the local executor's knobs plus the sandbox policy. All
* optional — `static Config` supplies the defaults (`mode: 'read-only'` is the
* fail-safe default; an example that wants a workspace-writable agent opts in
* explicitly). The runner choice is NOT configured here: which platform
* explicitly). The runner choice is not configured here: which platform
* backend confines the command is the `ctx.sandbox` provider's config.
*/
export interface Config extends LocalConfig {
@@ -167,7 +181,7 @@ export interface Config extends LocalConfig {
Depends on: [`LocalConfig`](#deepseek-aidsh-bash-local) · [`SandboxMode`](core-data-structures/sandbox.md)
Source: [`packages/bash/bash-sandbox/src/index.ts:26`](../packages/bash/bash-sandbox/src/index.ts)
Source: [`packages/bash/bash-sandbox/src/index.ts:27`](../packages/bash/bash-sandbox/src/index.ts)
## `@deepseek-ai/dsh-code-runtime-worker`
@@ -419,6 +433,57 @@ export interface Config {
Source: [`packages/support/llm-replay/src/index.ts:306`](../packages/support/llm-replay/src/index.ts)
## `@deepseek-ai/dsh-mcp-client`
Requires: `tools`
```ts config-catalog
/** Discriminated union of all supported MCP transport configurations. */
export type Config = StdioConfig | StreamableHttpConfig
/** Config for connecting to an MCP server via a spawned child process over stdio. */
export interface StdioConfig {
/** Transport type: spawn a child process and communicate over stdio. */
transport: 'stdio'
/**
* Stable local namespace for this server's model-facing tool names
* (`mcp__<serverName>__<rawName>`). Must match `[A-Za-z0-9_-]{1,32}` and be
* unique across live mcp-client instances.
*/
serverName: string
/** Executable to spawn. */
command: string
/** Arguments passed to the command. */
args: string[]
/** Extra env vars merged on top of scrubbed ambient env. */
env: Record<string, string>
/** Working directory for the child process. */
cwd: string
/** Timeout per callTool invocation (ms). */
toolCallTimeoutMs: number
}
/** Config for connecting to an MCP server over Streamable HTTP (SSE). */
export interface StreamableHttpConfig {
/** Transport type: connect to an MCP server over Streamable HTTP (SSE). */
transport: 'streamable-http'
/**
* Stable local namespace for this server's model-facing tool names
* (`mcp__<serverName>__<rawName>`). Must match `[A-Za-z0-9_-]{1,32}` and be
* unique across live mcp-client instances.
*/
serverName: string
/** MCP server URL. */
url: string
/** Extra headers (e.g. auth tokens). */
headers: Record<string, string>
/** Timeout per callTool invocation (ms). */
toolCallTimeoutMs: number
}
```
Source: [`packages/mcp/mcp-client/src/index.ts:91`](../packages/mcp/mcp-client/src/index.ts)
## `@deepseek-ai/dsh-permission`
Requires: `bash` · `approval`
@@ -509,7 +574,7 @@ export interface Config {
}
```
Source: [`packages/sandbox/sandbox-local/src/index.ts:20`](../packages/sandbox/sandbox-local/src/index.ts)
Source: [`packages/sandbox/sandbox-local/src/index.ts:19`](../packages/sandbox/sandbox-local/src/index.ts)
## `@deepseek-ai/dsh-session-persistence-jsonl`
@@ -624,13 +689,13 @@ export interface Config {
Source: [`packages/ui/stdio/src/index.ts:30`](../packages/ui/stdio/src/index.ts)
## `@deepseek-ai/dsh-stdio-agent`
## `@deepseek-ai/dsh-stdio-demo`
```ts config-catalog
/**
* App config: the swappable per-demo values, each routed to where the app wires
* it. `model`/`resumeSessionId` configure the pre-created `main` agent (through
* {@link @deepseek-ai/dsh-agent-core}'s forwarded `agents` list); `persona` is
* {@link @deepseek-ai/dsh-agent-spine-demo}'s forwarded `agents` list); `persona` is
* the deployment persona (forwarded to the system-prompt plugin); `toolOrder`
* is the explicit model-facing tool order (forwarded to the system-prompt plugin);
* fresh sessions use `process.cwd()` as their workspace cwd; resumed sessions
@@ -644,14 +709,18 @@ export interface Config {
persona?: string
/** Explicit model-facing tool order (the system-prompt plugin's `toolOrder` config; see dsh-system-prompt). */
toolOrder?: string[]
/** Tool-registry config — its presentation `mode` (forwarded through agent-core; see dsh-tools). */
/** Tool-registry config — its presentation `mode` (forwarded through agent-spine-demo; see dsh-tools). */
tools?: ToolsConfig
/** Directory the JSONL session backend writes under. Defaults to `./.sessions`. */
persistenceRoot?: string
/** stdin-chat banner printed once on start. Defaults to `'ready.'`. */
welcome?: string
/** Skill registry, local-provider, and model-facing consumer config forwarded to agent-core. */
/** Skill registry, local-provider, and model-facing consumer config forwarded to agent-spine-demo. */
skills?: agentCore.SkillConfig
/** Model-facing bash tool config forwarded through agent-core. */
toolBash?: NonNullable<agentCore.Config['toolBash']>
/** Generic background-task control-tool config forwarded through agent-core. */
toolTasks?: NonNullable<agentCore.Config['toolTasks']>
/**
* If set, the `main` agent RESUMES this persisted session id instead of
* starting fresh. Sourced from an env var in the leaf `cordis.yml`
@@ -661,9 +730,9 @@ export interface Config {
}
```
Depends on: [`agentCore`](../packages/core/agent-core/src/index.ts) · [`ToolsConfig`](#deepseek-aidsh-tools)
Depends on: [`agentCore`](../packages/examples/agent-spine-demo/src/index.ts) · [`ToolsConfig`](#deepseek-aidsh-tools)
Source: [`packages/ui/stdio-agent/src/index.ts:36`](../packages/ui/stdio-agent/src/index.ts)
Source: [`packages/examples/stdio-demo/src/index.ts:36`](../packages/examples/stdio-demo/src/index.ts)
## `@deepseek-ai/dsh-subagent-acp`
@@ -812,6 +881,20 @@ export interface Config {
Source: [`packages/context/time-context/src/index.ts:22`](../packages/context/time-context/src/index.ts)
## `@deepseek-ai/dsh-tool-bash`
Requires: `tools` · `bash` · `systemPrompt`
```ts config-catalog
/** Configures whether the model may background commands. */
export interface Config {
/** Expose `run_in_background` (default true); disabled calls are also rejected. */
enableRunInBackground?: boolean
}
```
Source: [`packages/bash/tool-bash/src/index.ts:30`](../packages/bash/tool-bash/src/index.ts)
## `@deepseek-ai/dsh-tool-cordis`
Requires: `tools`
@@ -874,34 +957,29 @@ export interface Config {
/** The `ctx.subagents` provider name to start runs on (e.g. `spawn`, `acp`). */
provider: string
/**
* The model-facing tool name to register (default `subagent`). To expose more
* than one transport, load this plugin once per provider — each load MUST set
* a distinct `toolName` (the tool registry rejects a duplicate name), e.g.
* `{ provider: 'spawn', toolName: 'subagent' }` and
* `{ provider: 'acp', toolName: 'subagent_acp' }`.
* Model-facing tool name (default `subagent`). Each loaded instance must use
* a distinct name.
*/
toolName?: string
/**
* Default per-child agent options (model) applied to every spawned child.
* Omitted fields fall back to the child loop's own defaults.
* Expose `run_in_background` (default true). Disabled instances omit the
* parameter and reject forced background calls.
*/
enableRunInBackground?: boolean
/**
* Agent options applied to every child; omitted fields use child-loop defaults.
*/
agentOptions?: AgentOptions
/**
* Per-child persona applied to every child this tool spawns: a scoped
* `deployment:persona` section shadowing the deployment's persona for the
* child alone. Requires the bound provider's `persona` capability
* (in-process backends support it; a request against one that doesn't is
* rejected at start). Omitted ⇒ the child renders the deployment persona.
* Per-child persona that shadows `deployment:persona`. Requires the
* provider's `persona` capability; omission preserves the deployment persona.
*/
persona?: string
/**
* Tool scoping applied to every child this tool spawns (see
* `SubagentStartRequest.toolFilter`): the named global tools vanish from
* the child's prompt AND refuse to execute. Requires the provider's
* `toolFilter` capability. Unknown names fail the spawn loudly. Note the
* child otherwise sees every global tool — including this delegation tool
* itself; `deny`-listing it (or setting `maxDepth`) is how a deployment
* bounds recursion.
* Tool filter applied to every child. Filtered tools disappear from its
* prompt and reject execution. Requires the provider's `toolFilter`
* capability; unknown names fail startup. Children otherwise see this tool,
* so deny it or set `maxDepth` to bound recursion.
*/
toolFilter?: {
/** Global tool names the child keeps; everything else is removed. */
@@ -910,12 +988,8 @@ export interface Config {
deny?: string[]
}
/**
* Recursion cap applied to every child this tool spawns (see
* `SubagentStartRequest.maxDepth`): a spawn whose child would sit deeper
* than this in the delegation tree is rejected. Requires the provider's
* `depthLimit` capability. Must be a non-negative safe integer and is
* validated when the plugin loads. Omitted ⇒ unbounded (bound it in
* deployments that expose this tool to children).
* Maximum child depth. Requires the provider's `depthLimit` capability and a
* non-negative safe integer. Omission is unbounded.
*/
maxDepth?: number
}
@@ -923,7 +997,23 @@ export interface Config {
Depends on: [`AgentOptions`](../packages/core/agent/src/index.ts)
Source: [`packages/subagent/tool-subagent/src/index.ts:24`](../packages/subagent/tool-subagent/src/index.ts)
Source: [`packages/subagent/tool-subagent/src/index.ts:23`](../packages/subagent/tool-subagent/src/index.ts)
## `@deepseek-ai/dsh-tool-tasks`
Requires: `tools` · `tasks` · `systemPrompt`
```ts config-catalog
/** Configures bounded `task_output` waits. */
export interface Config {
/** Wait duration applied when `task_output` sets `wait` without `timeout_ms` (default 30s). */
waitTimeoutMs?: number
/** Hard cap on any single wait; a larger model-supplied `timeout_ms` is clamped down to it (default 10min). */
maxWaitTimeoutMs?: number
}
```
Source: [`packages/tasks/tool-tasks/src/index.ts:21`](../packages/tasks/tool-tasks/src/index.ts)
## `@deepseek-ai/dsh-tool-web`
@@ -1057,7 +1147,7 @@ export interface Config {
}
```
Source: [`packages/web/web-fetch-local/src/index.ts:36`](../packages/web/web-fetch-local/src/index.ts)
Source: [`packages/web/web-fetch-local/src/index.ts:34`](../packages/web/web-fetch-local/src/index.ts)
## `@deepseek-ai/dsh-web-search-deepseek`
@@ -1081,7 +1171,7 @@ export interface Config {
}
```
Source: [`packages/web/web-search-deepseek/src/index.ts:40`](../packages/web/web-search-deepseek/src/index.ts)
Source: [`packages/web/web-search-deepseek/src/index.ts:38`](../packages/web/web-search-deepseek/src/index.ts)
## `@deepseek-ai/dsh-web-search-exa`
@@ -1103,7 +1193,7 @@ export interface Config {
}
```
Source: [`packages/web/web-search-exa/src/index.ts:39`](../packages/web/web-search-exa/src/index.ts)
Source: [`packages/web/web-search-exa/src/index.ts:37`](../packages/web/web-search-exa/src/index.ts)
## `@deepseek-ai/dsh-web-search-perplexity`
@@ -1125,7 +1215,7 @@ export interface Config {
}
```
Source: [`packages/web/web-search-perplexity/src/index.ts:33`](../packages/web/web-search-perplexity/src/index.ts)
Source: [`packages/web/web-search-perplexity/src/index.ts:31`](../packages/web/web-search-perplexity/src/index.ts)
## `@deepseek-ai/dsh-workflow-workerthread`
@@ -1165,9 +1255,9 @@ These load from a `cordis.yml` entry with no `config:` block; they declare no co
- `@deepseek-ai/dsh-llm` ([`packages/llm/llm/src/index.ts`](../packages/llm/llm/src/index.ts))
- `@deepseek-ai/dsh-session` ([`packages/core/session/src/index.ts`](../packages/core/session/src/index.ts))
- `@deepseek-ai/dsh-subagent` ([`packages/subagent/subagent/src/index.ts`](../packages/subagent/subagent/src/index.ts))
- `@deepseek-ai/dsh-tasks` ([`packages/tasks/tasks/src/index.ts`](../packages/tasks/tasks/src/index.ts))
- `@deepseek-ai/dsh-timeout-policy` — requires `tools` ([`packages/timeout/timeout-policy/src/index.ts`](../packages/timeout/timeout-policy/src/index.ts))
- `@deepseek-ai/dsh-tool-ask-user` — requires `tools` · `userInteraction` ([`packages/ui/tool-ask-user/src/index.ts`](../packages/ui/tool-ask-user/src/index.ts))
- `@deepseek-ai/dsh-tool-bash` — requires `tools` · `bash` · `systemPrompt` ([`packages/bash/tool-bash/src/index.ts`](../packages/bash/tool-bash/src/index.ts))
- `@deepseek-ai/dsh-tool-todo` — requires `tools` ([`packages/todo/tool-todo/src/index.ts`](../packages/todo/tool-todo/src/index.ts))
- `@deepseek-ai/dsh-user-interaction` ([`packages/ui/user-interaction/src/index.ts`](../packages/ui/user-interaction/src/index.ts))
@@ -1187,13 +1277,16 @@ Abstract service classes — a deployment loads a concrete implementation packag
Imported as libraries by other packages; a `cordis.yml` cannot load them.
- `@deepseek-ai/create-sdk` ([`packages/sdk/create-sdk/src/index.ts`](../packages/sdk/create-sdk/src/index.ts))
- `@deepseek-ai/dsh-acp-snapshot` ([`packages/support/acp-snapshot/src/index.ts`](../packages/support/acp-snapshot/src/index.ts))
- `@deepseek-ai/dsh-app-boot` ([`packages/ui/app-boot/src/index.ts`](../packages/ui/app-boot/src/index.ts))
- `@deepseek-ai/dsh-brand` ([`packages/util/brand/src/index.ts`](../packages/util/brand/src/index.ts))
- `@deepseek-ai/dsh-helper` ([`packages/sdk/helper/src/index.ts`](../packages/sdk/helper/src/index.ts))
- `@deepseek-ai/dsh-hook-protocol` ([`packages/hooks/hook-protocol/src/index.ts`](../packages/hooks/hook-protocol/src/index.ts))
- `@deepseek-ai/dsh-jsonrpc-agent` ([`packages/ui/jsonrpc-agent/src/index.ts`](../packages/ui/jsonrpc-agent/src/index.ts))
- `@deepseek-ai/dsh-jsonrpc-demo` ([`packages/examples/jsonrpc-demo/src/index.ts`](../packages/examples/jsonrpc-demo/src/index.ts))
- `@deepseek-ai/dsh-loader-smoke` ([`packages/support/loader-smoke/src/index.ts`](../packages/support/loader-smoke/src/index.ts))
- `@deepseek-ai/dsh-scope` ([`packages/core/scope/src/index.ts`](../packages/core/scope/src/index.ts))
- `@deepseek-ai/dsh-scripts` ([`packages/sdk/scripts/src/index.ts`](../packages/sdk/scripts/src/index.ts))
- `@deepseek-ai/dsh-subagent-inprocess` ([`packages/subagent/subagent-inprocess/src/index.ts`](../packages/subagent/subagent-inprocess/src/index.ts))
- `@deepseek-ai/dsh-subagent-subprocess` ([`packages/subagent/subagent-subprocess/src/index.ts`](../packages/subagent/subagent-subprocess/src/index.ts))
- `@deepseek-ai/dsh-timeout` ([`packages/util/timeout/src/index.ts`](../packages/util/timeout/src/index.ts))
+2 -2
View File
@@ -2,5 +2,5 @@
# side as of the last confirmed-consistent state. Both languages carry equal authority;
# after editing either side, bring the other along and re-record with:
# pnpm run verify-translation-pairing --write
adding-a-tool.md: 4920c98894326fb8eab3b3d5df298baf1da33c1d
adding-a-tool.zh.md: 3caf1e62f15f2f15103836b4d3f22be20ba02385
adding-a-tool.md: da214702939e01fedf3d0d69be7560bbafe0696a
adding-a-tool.zh.md: b216d18b1593cd7e6074685bd39684f1b9694eac
+2 -2
View File
@@ -45,9 +45,9 @@ Registration is effect-based: disposing the plugin fiber unregisters the tool (w
## Long-running work
Follow tool-bash's background pattern: a `run_in_background` flag returns a task id immediately; companion tools poll incrementally and kill; completion notices arrive via `agent.inject()`. Bound buffers and spill full output to disk so nothing is silently lost.
Gate `run_in_background` with producer config, reject a pre-aborted call, then register through `ctx.tasks.start({ kind, label, owner: exec.agent, run })`. The runtime validates ownership and control-surface availability before `run()` starts work, then supplies the id, session fence, generic control tools, notices, and owner cleanup.
> TODO: each tool reimplements this background pattern by hand today. At some point we need a generic long-running-tool layer that handles task ids, incremental polling, kill, and completion notices uniformly.
The producer supplies synchronous `cancel`, non-rejecting `done` that settles after resource cleanup, and optional consuming `readOutput` with bounded-output formatting. Once the id is returned, use a task-owned cancellation signal rather than `exec.signal`. See the [background task runtime RFC](../rfc/implemented/architecture/2026-06-20-generic-long-running-tool-runtime.md) and `dsh-tool-bash` for a stream producer.
## Execution policy and observation
+2 -2
View File
@@ -45,9 +45,9 @@ export function apply(ctx: Context) {
## 长时间运行的工作
遵循 tool-bash 的后台模式:`run_in_background` 标志立即返回一个 task id;配套工具增量轮询和终止;完成通知通过 `agent.inject()` 到达。限定缓冲区大小,将完整输出溢写到磁盘,避免静默丢失
通过 producer 配置控制 `run_in_background`,拒绝已预先中止的调用,然后使用 `ctx.tasks.start({ kind, label, owner: exec.agent, run })` 注册任务。运行时会在 `run()` 启动工作前校验 owner 和控制面是否可用,随后提供 id、会话围栏、通用控制工具、通知和 owner cleanup
> TODO: 目前每个工具都手动重新实现这套后台模式。未来需要一个通用的长时间运行工具层,统一处理 task id、增量轮询、终止和完成通知
producer 提供同步的 `cancel`、在资源清理后 settle 且不 reject 的 `done`,以及可选的消费式 `readOutput`(负责有界输出的格式化)。返回 id 后,应使用 task 自有的取消信号,而不是 `exec.signal`。流式 producer 的示例和完整契约见[后台 task 运行时 RFC](../rfc/implemented/architecture/2026-06-20-generic-long-running-tool-runtime.md)与 `dsh-tool-bash`
## 执行策略与观测
+2 -2
View File
@@ -2,5 +2,5 @@
# side as of the last confirmed-consistent state. Both languages carry equal authority;
# after editing either side, bring the other along and re-record with:
# pnpm run verify-translation-pairing --write
extension-cookbook.md: 40ee22b352c884d7f295c87726c54ab8e166c844
extension-cookbook.zh.md: 4e5bc68c973649574bcb2404bea00096eb9ca41f
extension-cookbook.md: 3474bc116b43f9be57b52e947f9cf99730f7e796
extension-cookbook.zh.md: 1a605b20fe4e171a948ac2a046193a2f4d884e44
+1 -1
View File
@@ -87,7 +87,7 @@ export function apply(ctx: Context) {
## Runnable wirings
Three complete examples load their plugin trees from `cordis.yml`: [`examples/echo-agent`](../../examples/echo-agent) (mock model + echo tool — the all-mock skeleton check, `pnpm run demo:echo`), [`examples/coding-agent`](../../examples/coding-agent) (DeepSeek V4 + the bash tool suite behind a terminal REPL UI, `pnpm run demo:repl`), and [`examples/acp-agent`](../../examples/acp-agent) (an agent exposed as an ACP server over JSON-RPC stdio — the client-driver shape, `pnpm run demo:acp`). Each leaf is just its swappable backends plus an app-package entry: the stdio demos load [`@deepseek-ai/dsh-stdio-agent`](../../packages/ui/stdio-agent), the ACP demo loads [`@deepseek-ai/dsh-acp-agent`](../../packages/ui/acp-agent), and both app packages share the spine via the [`@deepseek-ai/dsh-agent-core`](../../packages/core/agent-core) bundle.
Three complete examples load their plugin trees from `cordis.yml`: [`examples/echo-agent`](../../examples/echo-agent) (mock model + echo tool — the all-mock skeleton check, `pnpm run demo:echo`), [`examples/coding-agent`](../../examples/coding-agent) (DeepSeek V4 + the bash tool suite behind a terminal REPL UI, `pnpm run demo:repl`), and [`examples/acp-agent`](../../examples/acp-agent) (an agent exposed as an ACP server over JSON-RPC stdio — the client-driver shape, `pnpm run demo:acp`). Each leaf is just its swappable backends plus an app-package entry: the stdio demos load [`@deepseek-ai/dsh-stdio-demo`](../../packages/examples/stdio-demo), the ACP demo loads [`@deepseek-ai/dsh-acp-demo`](../../packages/examples/acp-demo), and both app packages share the spine via the [`@deepseek-ai/dsh-agent-spine-demo`](../../packages/examples/agent-spine-demo) bundle.
## The feature → mechanism map
+1 -1
View File
@@ -87,7 +87,7 @@ export function apply(ctx: Context) {
## 可运行的组装示例
三个完整示例从 `cordis.yml` 加载各自的插件树:[`examples/echo-agent`](../../examples/echo-agent)mock 模型 + echo 工具——全 mock 骨架检查,`pnpm run demo:echo`)、[`examples/coding-agent`](../../examples/coding-agent)DeepSeek V4 + bash 工具套件,配合终端 REPL UI,`pnpm run demo:repl`)、[`examples/acp-agent`](../../examples/acp-agent)(通过 JSON-RPC stdio 暴露为 ACP 服务器的 agent——客户端驱动形态,`pnpm run demo:acp`)。每个叶子只是其可替换后端加一个 app 包入口:stdio 演示加载 [`@deepseek-ai/dsh-stdio-agent`](../../packages/ui/stdio-agent)ACP 演示加载 [`@deepseek-ai/dsh-acp-agent`](../../packages/ui/acp-agent),两个 app 包通过 [`@deepseek-ai/dsh-agent-core`](../../packages/core/agent-core) bundle 共享主干。
三个完整示例从 `cordis.yml` 加载各自的插件树:[`examples/echo-agent`](../../examples/echo-agent)mock 模型 + echo 工具——全 mock 骨架检查,`pnpm run demo:echo`)、[`examples/coding-agent`](../../examples/coding-agent)DeepSeek V4 + bash 工具套件,配合终端 REPL UI,`pnpm run demo:repl`)、[`examples/acp-agent`](../../examples/acp-agent)(通过 JSON-RPC stdio 暴露为 ACP 服务器的 agent——客户端驱动形态,`pnpm run demo:acp`)。每个叶子只是其可替换后端加一个 app 包入口:stdio 演示加载 [`@deepseek-ai/dsh-stdio-demo`](../../packages/examples/stdio-demo)ACP 演示加载 [`@deepseek-ai/dsh-acp-demo`](../../packages/examples/acp-demo),两个 app 包通过 [`@deepseek-ai/dsh-agent-spine-demo`](../../packages/examples/agent-spine-demo) bundle 共享主干。
## 功能→机制映射
@@ -0,0 +1,62 @@
# Maintaining the dsh-code-review skill
The [`dsh-code-review`](../../.agents/skills/dsh-code-review/SKILL.md) skill is kept current by a single designated operator running a private periodic maintenance tool. This cookbook is the entry point for that operator — and for anyone taking over the role — and for repo contributors who want to understand why skill updates arrive as small periodic PRs rather than one-off audits. The workflow itself is specified in the [human-review skill-maintenance RFC](../rfc/proposed/process/2026-07-13-human-review-skill-maintenance.md).
## What the maintainer receives
Run the private tool daily with a two-UTC-day overlap; until the proposed scheduler has completed its acceptance run, the operator invokes the wrapper manually at the same cadence. A manual weekly recovery run uses a seven-day window. The workflow:
1. It selects PRs merged in the chosen window (default two UTC days for the daily cadence, seven for weekly) whose merge commit is reachable from `origin/master`. PRs whose merge commit is not reachable (stacked branches whose parent was squashed) or that exceed a 250-commit acquisition cap are logged to `skipped-pulls.json` and skipped rather than aborting the run.
2. It collects pre-merge human review feedback with commit anchors (inline comments and review submissions), then compares feedback-time and final landed PR patches. It does not acquire PR conversation comments because current GitHub state cannot give them a force-push-safe feedback-time baseline, and it excludes target-branch-only changes from adoption evidence.
3. Two independently configured reviewer adapters classify provenance and adoption, then classify agreed-adopted items against the current skill.
4. The primary adapter drafts a complete revised `SKILL.md`; both adapters review the same diff; blocking findings loop until both approve.
5. `pnpm run doc-sync` and `pnpm run lint` run against the candidate before the tool declares success.
Each run stores its artifacts on the operator's machine. The saved diff, candidate `SKILL.md`, and promotion manifest land under `~/dsh-code-review-outputs/` named by timestamp. The manifest records the source master commit and skill blob, source feedback IDs and URLs, landed evidence ranges, adapter verdicts, and gate results; raw per-adapter I/O stays in a private temp directory whose path is written to the notification and to the daily log under `~/Library/Logs/dsh-code-review-maintainer/`. The maintenance worktree itself is restored clean after every run so the operator is never tempted to edit the maintenance copy in place.
## What the operator does with a candidate diff
When a run produces a candidate, a macOS notification arrives with a `dsh-code-review-promote <timestamp>` hint.
1. **Read the diff on its own merits.** Do not defer to "the reviewers approved" — the maintainer contract is that the operator is the final judgment. Look for checklist bloat, historical prose, unsupported extrapolation from a single incident, and duplicated coverage with existing skill or authoritative-doc content.
```sh
ls ~/dsh-code-review-outputs/ # every candidate ever produced
less ~/dsh-code-review-outputs/2026-07-16T02-00-00Z.diff
less ~/dsh-code-review-outputs/2026-07-16T02-00-00Z.SKILL.md
less ~/dsh-code-review-outputs/2026-07-16T02-00-00Z.manifest.json
```
2. **Cross-check against the run artifacts.** The promotion manifest maps each proposed rule to source feedback and landed evidence; detailed per-adapter I/O, consensus, and adopted evidence live under the run's private temp directory (path shown in the log). Spot-check at least one candidate: does the linked human comment actually support the added rule? Does the linked PR actually adopt it?
3. **Decide one of three:**
- **Discard.** Delete the saved candidate. The tool re-considers the same feedback on the next run under whatever the current skill then says.
```sh
rm ~/dsh-code-review-outputs/2026-07-16T02-00-00Z.{diff,SKILL.md,manifest.json}
```
- **Batch.** Keep the candidate aside if the update is small and could combine with a future one. The source-skill check still applies; rerun the analysis or manually rebase and re-review the diff if `master` changes first.
- **Promote.** From a clean `master` checkout of the repo, run the promote helper. It refreshes `master`, verifies that the current skill matches the recorded source blob, applies the saved diff, and opens a draft PR whose body carries the manifest's provenance summary. It stops on skill drift rather than overwriting newer guidance; the operator still reviews the PR on GitHub and either merges it or closes it.
```sh
cd ~/path/to/deepseek-harness # clean master
dsh-code-review-promote 2026-07-16T02-00-00Z
```
4. **Do not commit adapter output verbatim.** Small edits during promotion — tightening wording, removing an example that only makes sense with the source PR's context, folding a rule into an existing one — are expected and preserve the "reviewer judgment" the workflow depends on. Amend the branch before merging.
## When a run produces no candidate
That is the common case after every nonempty classification stage has produced at least one valid adapter result. The tool records "no candidate" in its daily log, sends no notification (to avoid alert fatigue), and moves on. Days without a skill update are the workflow behaving correctly, not a stall.
## Interruptions and handoff
The mechanism lives on one machine. Interruptions the operator handles as they arise:
- **Daily run missed.** The two-day overlap window catches one skipped day automatically; longer gaps recover by running the wrapper manually with `DSH_CODE_REVIEW_SINCE=<Nd>`. Overlapping windows are idempotent: guidance already in the current skill is classified `covered` and does not re-enter as a candidate.
- **Adapter provider outage.** The tool refuses to run when the two reviewer commands resolve to byte-identical executables. A single batch whose adapter response fails schema or id validation is failed closed at the batch level (every item in the batch marked unclear) and the run continues; the raw output is preserved for debugging. If either adapter produces no valid result for any nonempty batch in an operation, the run fails, writes a failure record, and notifies the operator; it never collapses a total-provider outage into "no candidate."
- **Handoff to another maintainer.** Open a follow-up RFC that supersedes the current one: either move the mechanism into the repository or record the new operator's private setup. Do not silently transfer the tool — the "single-maintainer bus factor" in the RFC's Risks section is the reason the handoff needs a documented decision.
## Where the operator's private setup lives
The tool source, reviewer adapters, provider credentials, and scheduler are the operator's private infrastructure and are outside this repository by design (see the RFC's "Where the mechanism lives" section). This cookbook and the RFC describe **what the workflow guarantees**; **how** those guarantees are implemented is a private-infrastructure concern. If you are the new operator, the RFC's `## Proposal` sections are the specification you build against.
+4 -4
View File
@@ -289,7 +289,7 @@ A ready child settled. Scope-filtered dispatch uses the same delegating parent c
'subagent/end'(this: Scoped<SubagentService>, info: SubagentRunEndInfo): void
```
Source: [`packages/subagent/subagent/src/index.ts:92`](../../packages/subagent/subagent/src/index.ts)
Source: [`packages/subagent/subagent/src/index.ts:108`](../../packages/subagent/subagent/src/index.ts)
### `subagent/provider-added` — emit
@@ -299,7 +299,7 @@ A provider became resolvable in the registry.
'subagent/provider-added'(provider: SubagentProvider): void
```
Source: [`packages/subagent/subagent/src/index.ts:66`](../../packages/subagent/subagent/src/index.ts)
Source: [`packages/subagent/subagent/src/index.ts:82`](../../packages/subagent/subagent/src/index.ts)
### `subagent/provider-removed` — emit
@@ -309,7 +309,7 @@ A provider left the registry. Accepted runs remain holder-owned.
'subagent/provider-removed'(name: string): void
```
Source: [`packages/subagent/subagent/src/index.ts:72`](../../packages/subagent/subagent/src/index.ts)
Source: [`packages/subagent/subagent/src/index.ts:88`](../../packages/subagent/subagent/src/index.ts)
### `subagent/start` — emit
@@ -319,7 +319,7 @@ A provider established a ready child. For in-process providers, `ctx.agents.get(
'subagent/start'(this: Scoped<SubagentService>, info: SubagentRunInfo): void
```
Source: [`packages/subagent/subagent/src/index.ts:83`](../../packages/subagent/subagent/src/index.ts)
Source: [`packages/subagent/subagent/src/index.ts:99`](../../packages/subagent/subagent/src/index.ts)
## `system-prompt/*`
+31 -11
View File
@@ -54,23 +54,24 @@ Source: [`packages/ui/user-approval/src/index.ts:229`](../../packages/ui/user-ap
## `ctx.bash` — `BashExecutor` (abstract seam)
Registers one `ctx.bash` implementation. Runtime command failures resolve as BashRunResult; only infrastructure failures reject. Background starts return immediately without a timeout, report completion exactly once while live, and remain cancellable by signal or kill. Output reads are incremental and flag lost buffered data; disposal kills and awaits all tasks.
Abstract bash execution service. Subclass, implement the abstract methods, and load the subclass as a plugin — it registers as `ctx.bash` (one implementation per context; loading a second throws, which is cordis' standard duplicate-service behavior).
Implementations must honor these semantics:
- run rejects only for infrastructure failures. Nonzero exits, timeout kills, and abort kills resolve with a BashRunResult.
- start returns immediately; no timeout applies to background processes. `done` settles at process close and never rejects; spawn failures settle as `killed` with the error on stderr.
- BashProcess.readOutput is incremental: consecutive reads never repeat output. Lossy reads report truncation and available spill files.
- Disposal kills all running background processes and awaits their exit.
```ts cordis-catalog
abstract resolve(request: BashExecRequest): BashExecSpec
abstract run(spec: BashExecSpec): Promise<BashRunResult>
abstract start(spec: BashExecSpec): BashTask
abstract get(id: BashTaskId): BashTask | undefined
abstract ownerOf(id: BashTaskId): OwnerToken | undefined
abstract list(): BashTask[]
abstract readOutput(id: BashTaskId): BashTaskRead
abstract kill(id: BashTaskId): boolean
onTaskDone(listener: BashTaskListener): () => void
abstract start(spec: BashExecSpec): BashProcess
```
Types: [BashExecRequest](../core-data-structures/bash.md) · [BashExecSpec](../core-data-structures/bash.md) · [BashRunResult](../core-data-structures/bash.md) · [BashTask](../core-data-structures/bash.md) · [BashTaskRead](../core-data-structures/bash.md)
Types: [BashExecRequest](../core-data-structures/bash.md) · [BashExecSpec](../core-data-structures/bash.md) · [BashRunResult](../core-data-structures/bash.md)
Source: [`packages/bash/bash/src/index.ts:38`](../../packages/bash/bash/src/index.ts)
Source: [`packages/bash/bash/src/index.ts:46`](../../packages/bash/bash/src/index.ts)
## `ctx.codeRuntime` — `CodeRuntime` (abstract seam)
@@ -226,7 +227,7 @@ list(): string[]
async start(name: string, request: SubagentStartRequest): Promise<SubagentRun>
```
Source: [`packages/subagent/subagent/src/index.ts:125`](../../packages/subagent/subagent/src/index.ts)
Source: [`packages/subagent/subagent/src/index.ts:141`](../../packages/subagent/subagent/src/index.ts)
## `ctx.systemPrompt` — `SystemPrompt`
@@ -241,6 +242,25 @@ async assemble(context: AssembleContext = {}): Promise<PromptAssembly>
Source: [`packages/core/system-prompt/src/index.ts:209`](../../packages/core/system-prompt/src/index.ts)
## `ctx.tasks` — `TaskService`
The `tasks` service: the runtime-global background task registry. See the module doc for the ownership, isolation, and lifecycle contracts.
```ts cordis-catalog
start(spec: TaskStart): TaskId
list(caller?: Agent): TaskSnapshot[]
get(id: TaskId, caller?: Agent): TaskSnapshot
read(id: TaskId, caller?: Agent): TaskRead
kill(id: TaskId, caller?: Agent, reason?: string): 'requested' | 'already-finished'
async wait(id: TaskId, timeoutMs: number, caller?: Agent, signal?: AbortSignal): Promise<TaskSnapshot>
onTaskDone(listener: TaskDoneListener): () => void
attachSurface(name: string): () => void
```
Types: [Agent](../core-data-structures/core.md)
Source: [`packages/tasks/tasks/src/index.ts:76`](../../packages/tasks/tasks/src/index.ts)
## `ctx.tools` — `ToolRegistry`
Tool registry and execution pipeline. Scoped registrations shadow globals; one visibility resolver feeds presentation, lookup, and dispatch.
+31 -53
View File
@@ -1,6 +1,6 @@
# Bash Executor
The bash execution seam — the canonical [capability seam](../rfc/implemented/architecture/2026-06-13-capability-seams.md) example, split across three packages: interface ([dsh-bash](../../packages/bash/bash), `ctx.bash`), implementation ([dsh-bash-local](../../packages/bash/bash-local), local subprocesses), and consumer ([dsh-tool-bash](../../packages/bash/tool-bash), the `bash`/`bash_output`/`bash_kill` tool schemas). Bash is **one optional capability**, not part of the agent-loop spine — so its vocabulary lives here, not in [core.md](core.md). A sandboxed, containerized, or remote backend is a sibling package implementing the same interface.
The bash execution seam is split across interface ([dsh-bash](../../packages/bash/bash), `ctx.bash`), implementations ([dsh-bash-local](../../packages/bash/bash-local) and [dsh-bash-sandbox](../../packages/bash/bash-sandbox)), and consumer ([dsh-tool-bash](../../packages/bash/tool-bash), the `bash` schema). Generic background-task ids, ownership, and controls live in [tasks.md](tasks.md); this seam returns a task-free process handle.
Source: [`packages/bash/bash/src/types.ts`](../../packages/bash/bash/src/types.ts)
@@ -35,15 +35,6 @@ interface BashExecRequest {
* uses shell syntax like `FOO=bar cmd`).
*/
env?: Record<string, string> | undefined
/**
* Opaque OWNER token for a background task — the consumer's isolation key
* (the tool layer passes the owning agent's `session.header.id`). The
* executor stores it on the task and exposes it via {@link BashExecutor.ownerOf};
* the executor itself NEVER interprets it (no access policy lives in the
* seam — that is the consumer's job). Absent for foreground runs and for an
* ownerless background start (a non-agent caller).
*/
owner?: OwnerToken | undefined
/**
* Explicit per-call sandbox-policy input, overriding the executor's
* configured default mode for THIS call. Never a silent default: a
@@ -70,10 +61,8 @@ interface BashExecSpec {
signal?: AbortSignal | undefined
/**
* Bytes to write to the command's stdin (then close it), carried through
* verbatim from {@link BashExecRequest.stdin}. OPTIONAL on the resolved spec
* (unlike `owner`): it has no config default, so a missing one means "no
* stdin" — the safe, ordinary case — not a silent footgun, so it stays a
* plain optional rather than required-but-nullable (see the request field).
* verbatim from {@link BashExecRequest.stdin}. It has no config default, so
* a missing value means "no stdin" and remains an ordinary optional.
*/
stdin?: string | undefined
/**
@@ -85,19 +74,10 @@ interface BashExecSpec {
*/
env?: Record<string, string> | undefined
/**
* Opaque owner token, REQUIRED-but-nullable (mirrors `workdir`/`timeoutMs`
* being required on the resolved spec): {@link BashExecutor.resolve} carries
* the request's `owner` through, defaulting a missing one to `undefined`. A
* required field makes a forgotten owner a VISIBLE `undefined` rather than a
* silently-absent property that yields an unowned (cross-session-readable)
* task. `start()` stores it; `run()` (foreground) ignores it.
*/
owner: OwnerToken | undefined
/**
* The sandbox mode this call executes under, REQUIRED-but-nullable for the
* same visibility reason as `owner`. A sandboxing executor's `resolve()`
* stamps the effective mode (the request's explicit override, else its
* configured default) so `run()`/`start()` read the spec, never the config;
* The sandbox mode this call executes under, required-but-nullable so every
* resolved spec states its policy. A sandboxing executor's `resolve()` stamps
* the effective mode (the request's explicit override, else its configured
* default) so `run()`/`start()` read the spec, never the config;
* a non-sandboxing executor carries the request value through verbatim and
* ignores it (`undefined` under such an executor means what its README says:
* unconfined execution).
@@ -106,11 +86,7 @@ interface BashExecSpec {
}
```
The `owner` token is the isolation key: the executor stores it but never interprets it (access policy is the consumer's job), so a background task started by one agent isn't readable cross-session. A required-but-nullable field makes a forgotten owner a visible `undefined` rather than a silently-unowned task.
Trusted in-process plugins use `stdin` and `env` for hook payloads and hook-specific variables. The model-facing bash tool constructs requests from its named schema fields and exposes neither input because shell syntax already provides equivalent power; tests guard against a future `...args` spread. This is request-shape discipline, not a security boundary: `dsh-bash-local` scrubs ambient credentials regardless of these fields, then overlays explicit values already held by the caller. See [the bash stdin/env RFC](../rfc/implemented/architecture/2026-06-30-bash-stdin-env-trusted-plugin-surface.md).
Both ids the seam handles are [branded](core.md) (zero-cost `string` brands, the same machinery as `SessionId`/`AgentId`): `BashTaskId` (a tracked background task, generated `bash-N` by the local executor) and `OwnerToken` (the opaque isolation key). `OwnerToken` is deliberately a DISTINCT brand from `SessionId`, not an alias: the bash seam is a capability seam that must not know what an owner token *means*, so it never imports `dsh-session`'s vocabulary — the `dsh-tool-bash` consumer is the single boundary that casts the owning agent's `SessionId` into an `OwnerToken`. Branding both stops a raw `string` (or a `BashTaskId` where an `OwnerToken` is expected, or vice versa) from slipping through the type checker on the model-facing `task_id` path.
`stdin` and `env` are trusted in-process plugin inputs and are not exposed by `dsh-tool-bash`. The local executor scrubs ambient credentials before merging explicit caller-supplied env. See [the bash-stdin-env RFC](../rfc/implemented/architecture/2026-06-30-bash-stdin-env-trusted-plugin-surface.md).
## Foreground runs: `BashRunResult`
@@ -154,9 +130,9 @@ interface CollectedOutput {
## File sandbox: `BashSandboxInfo`
A sandbox-consuming executor (`dsh-bash-sandbox`) exposes its configured fallback through `BashExecutor.sandboxMode`. The tool layer folds each agent session's durable `bash/sandbox-mode` override, stamps the effective mode onto the request, and may replace it for one user-approved strictly wider call. It deliberately neither states the standing mode nor narrates switches; a denial result names the mode that command actually ran under. The mode/enforcement vocabulary is owned and cataloged by the [`@deepseek-ai/dsh-sandbox` seam](sandbox.md), whose provider wraps the executor's argv; modes govern FILE effects only, not network or process visibility.
A sandbox-consuming executor exposes its configured fallback through `BashExecutor.sandboxMode`. The tool layer folds each session's durable `bash/sandbox-mode` override and may replace it for one user-approved strictly wider call. The mode/enforcement vocabulary is owned by the [`@deepseek-ai/dsh-sandbox` seam](sandbox.md); modes govern file effects only.
A sandboxed run always reports the facts it executed under on `BashRunResult.sandbox`: `denied` is the executor's conservative classification of a failure as sandbox-caused (a failed exit whose stderr carries a filesystem-permission signature — never a clean exit or a signal kill), read from the collected stderr tail; `enforcement` reports how completely the selected backend governs the mode's file effects (`SandboxEnforcement = 'full' | 'partial'` — `partial` when an older Landlock ABI governs only a subset of the requested accesses; absent under `danger-full-access`, where nothing is confined); `runnerFailed` marks the opposite of a denial — the sandbox RUNNER itself failed and the command never ran (stamped only on settled background tasks; a foreground run surfaces the same condition as the thrown `SANDBOX_UNAVAILABLE` error):
A sandboxed run reports its mode, conservative denial classification, and enforcement completeness. `runnerFailed` marks a sandbox runner failure before the command ran; foreground execution throws `SANDBOX_UNAVAILABLE`, while a settled background process has only its facts channel.
```ts type-equiv
interface BashSandboxInfo {
@@ -195,38 +171,40 @@ interface BashSandboxInfo {
One more piece completes the vocabulary: the `SANDBOX_UNAVAILABLE` error code (owned by the [sandbox seam](sandbox.md)) is what the `ctx.sandbox` provider throws — and the executor propagates — when a confined mode has no usable backend. A selected runner refusing its profile reaches the same fail-closed foreground error; a settled background task records `runnerFailed`. The model receives denial/runner facts in results, learns the effective mode only when a denial marker names it, and can request a one-shot strictly wider retry through `sandbox_permissions` plus `justification`; `ctx.approval` must grant that exact call before anything executes. The complete policy and switching design is the [sandbox RFC](../rfc/implemented/feature/2026-07-06-sandbox.md).
## Background tasks: `BashTask`
## Background processes: `BashProcess`
A long-running command started with `start()` is tracked as a `BashTask`. `BashTaskStatus` is `'running' | 'completed' | 'killed'`; `done` resolves when the underlying process closes and never rejects. A sandboxing executor stamps `sandbox` once the task settles — classification runs against the settled task's collected stderr — so the field is absent while running and under an unsandboxed executor.
`start()` returns a handle with no id or owner. `dsh-tool-bash` adapts it into `ctx.tasks.start()` hooks; the generic runtime then owns task identity and lifecycle. `done` resolves when the process closes and never rejects, reads remain valid after settlement, and sandbox facts are stamped before `done` resolves.
```ts type-equiv
interface BashTask {
readonly id: BashTaskId
status: BashTaskStatus
interface BashProcess {
/** Process lifecycle state (settled exactly once). */
status: BashProcessStatus
/** Exit code once finished (null = killed by signal / still running). */
exitCode: number | null
/** Terminating signal name, when signal-killed. */
signal: NodeJS.Signals | null
/** Resolves when the underlying process closes (never rejects). */
/** Resolves when the underlying process closes (never rejects — a spawn failure settles as `killed` with the error on stderr). */
readonly done: Promise<void>
/**
* Sandbox facts for this task's execution, stamped by a sandboxing executor
* once the task settles and BEFORE completion listeners are notified — an
* `onTaskDone` consumer and a `done` awaiter both see it. Denial
* classification runs against the settled task's collected stderr, so the
* field cannot exist earlier: absent while the task is running and under an
* executor that does not sandbox. See {@link BashSandboxInfo} for the
* `denied` semantics.
*/
/** Sandbox facts, stamped once a confined process settles. */
sandbox?: BashSandboxInfo
/**
* Read output produced since the previous read (consuming — consecutive
* reads never re-deliver). Reads that lost data flag `lossy` and point at
* full-stream spill files when available.
*/
readOutput(): BashProcessRead
/**
* Kill the process group. Returns false when it had already finished
* (no-op); idempotent.
*/
kill(): boolean
}
```
`readOutput()` returns an incremental `BashTaskRead` — the output produced since the previous read, with a `lossy` flag when truncation dropped unread bytes:
`readOutput()` returns the incremental delta and spill recovery facts:
```ts type-equiv
interface BashTaskRead {
task: BashTask
interface BashProcessRead {
/** Output produced since the previous read (stderr in a marked section). */
delta: string
/** True when truncation dropped unread bytes the delta cannot include. */
@@ -240,4 +218,4 @@ interface BashTaskRead {
## The service
`BashExecutor` (`ctx.bash`, abstract — defined in [`packages/bash/bash/src/index.ts`](../../packages/bash/bash/src/index.ts)) mirrors the `LlmService`/`LlmAdapter` split: `resolve` (request → spec), `run` (foreground), `start` (background), `get`/`ownerOf`/`list`/`readOutput`/`kill`, and `onTaskDone` (a `BashTaskListener` completion callback). Spawned commands get a **scrubbed env** (dropping `*KEY*`/`*SECRET*`/`*TOKEN*`) and spill files use a private 0700 dir with random names and owner-only opens — model output never gets the ambient environment or a predictable path. The implementation that provides all this is `dsh-bash-local`; the model-facing `bash`/`bash_output`/`bash_kill` schemas that call it are in `dsh-tool-bash` (and present as terminals via the [tool-presentation vocabulary](tools.md#tool-presentation-ui-vocabulary)).
`BashExecutor` owns `resolve`, foreground `run`, background-process `start`, and the `sandboxMode` capability fact. `dsh-bash-local` owns process groups, timeout/abort handling, bounded collectors, spill files, credential scrubbing, and disposal quiescence. `dsh-tool-bash` owns model-facing rendering and adapts background handles into the [generic task runtime](tasks.md).
+3 -3
View File
@@ -24,7 +24,7 @@ Everything else is documented on a **sub-page**, not here. The rule that draws t
| [tools.md](tools.md) | `ToolDefinition` full fields, the schema DSL, `ToolExecution`/`ToolResult`, tool-presentation UI types, and the guarded execution pipeline |
| [user-interaction.md](user-interaction.md) | the UI-backed human question/answer seam: `AskUserQuestionRequest`, answer/options vocabulary, provider API, error taxonomy |
| [approval.md](approval.md) | the one-shot user-approval seam: `ApprovalRequest`, `ApprovalOutcome`, per-session policy, audit and answerer contracts |
| [bash.md](bash.md) | the bash executor seam: `BashExecRequest`/`Spec`, `BashRunResult`, background `BashTask`s |
| [bash.md](bash.md) | the bash executor seam: `BashExecRequest`/`Spec`, `BashRunResult`, background `BashProcess` handles |
| [sandbox.md](sandbox.md) | the process-confinement seam: file-effect modes, `SandboxPolicy`, `ConfinedArgv`, enforcement and fail-closed errors |
| [code-runtime.md](code-runtime.md) | the code-execution seam: `CodeRunRequest`/`Result`, binding namespaces, captured logs, the `CodeRunFailure` taxonomy |
| [filesystem.md](filesystem.md) | the filesystem seam: `FsTarget`, read/write/edit outcomes, observed-file state, `FsErrorCode` |
@@ -76,7 +76,7 @@ Two large discriminated unions are the ones consumers `switch` over most: **`Str
IDs that cross package boundaries are **branded** — structurally strings, but non-interchangeable at the type level (an `AgentId` can't be passed where a `CallId` is expected). Construction goes through a per-type factory; comparison, logging, and JSON behave as ordinary strings.
The `Branded<B>` primitive lives in its own type-only package, [dsh-brand](../../packages/util/brand) (no runtime code, no harness-package dependency), so any package can brand the ids it owns without depending on an unrelated capability package (e.g. dsh-bash brands `BashTaskId`/`OwnerToken` via dsh-brand alone, never pulling in dsh-llm).
The `Branded<B>` primitive lives in its own type-only package, [dsh-brand](../../packages/util/brand) (no runtime code, no harness-package dependency), so any package can brand the ids it owns without depending on an unrelated capability package.
Source: [`packages/util/brand/src/index.ts`](../../packages/util/brand/src/index.ts)
@@ -84,7 +84,7 @@ Source: [`packages/util/brand/src/index.ts`](../../packages/util/brand/src/index
type Branded<B extends string> = string & { readonly [BRAND]: B }
```
The three core IDs: `CallId` (correlates a tool call with its result; dsh-llm), `SessionId` (dsh-session), `AgentId` (dsh-agent). Each is `Branded<'CallId'>` etc. plus a same-named factory function. Capability seams brand their own ids too — see `BashTaskId`/`OwnerToken` in [bash.md](bash.md).
The three core IDs are `CallId`, `SessionId`, and `AgentId`. Capability packages brand their own ids too, such as `TaskId` in [tasks.md](tasks.md).
## Content blocks and messages
+129
View File
@@ -0,0 +1,129 @@
# Background Task Runtime
Types shared by long-running producers, `ctx.tasks`, and task control surfaces. The [runtime RFC](../rfc/implemented/architecture/2026-06-20-generic-long-running-tool-runtime.md) owns the design; this page records the literal shapes from [`packages/tasks/tasks/src/types.ts`](../../packages/tasks/tasks/src/types.ts).
## Ids and status
`TaskId` is a [branded id](core.md#branded-ids) generated as `<kind>-N`. Access control relies on owner authorization, not id secrecy. `TaskKind` derives from a merge-extensible map; the registry treats kinds as opaque id namespaces.
```ts type-equiv
interface TaskKindMap {
bash: 'bash'
subagent: 'subagent'
}
```
`TaskStatus` is `'running' | 'stopping' | 'completed' | 'killed' | 'failed'`; producer-specific facts belong in `TaskSnapshot.detail`.
## Producer contract
`TaskStart` declares identity and a starter. The runtime finishes preflight before calling `run()` and commits without a later failable step. Producers own execution resources; the runtime owns identity, access, and lifecycle state.
```ts type-equiv
interface TaskStart {
/** Producer kind — also the id prefix (`bash`, `subagent`, …). */
kind: TaskKind
/** One-line model-facing label (the command; the delegation description). */
label: string
/**
* Owning live agent. Access is fenced by its session id, and agent disposal
* cancels and awaits the task. The instance must be the one currently
* registered under its agent id. Omitting the owner creates an unowned task,
* open to any caller until service disposal.
*/
owner?: Agent
/**
* Start the work after preflight and synchronously return its hooks. Called
* once; a throw leaves nothing registered, and the producer must clean up any
* partially started resources.
*/
run(): TaskHooks
}
```
`TaskHooks.done` is the quiescence boundary. Optional `readOutput` distinguishes consuming stream tasks from final-output-only tasks.
```ts type-equiv
interface TaskHooks {
/**
* Request termination. Must be synchronous, idempotent, and eventually settle
* {@link done}; throws propagate. The optional reason is forwarded verbatim.
*/
cancel(reason?: string): void
/**
* Resolves after the producer releases its resources, not merely when work
* finishes. Must not reject; the runtime converts a rejection to `failed`.
* If teardown cancellation throws, the runtime may force-fail only the
* registry record without claiming that the work stopped.
*/
done: Promise<TaskOutcome>
/**
* Consume output produced since the previous call. The producer formats
* truncation and spill notices. Absence marks a final-output-only task; each
* task has one consuming cursor.
*/
readOutput?(): string
}
```
```ts type-equiv
interface TaskOutcome {
/** How the task ended: finished (`completed`), cancelled (`killed`), or broke (`failed`). */
status: 'completed' | 'killed' | 'failed'
/** Kind-specific detail rendered into status lines ('exit code: 3', 'max-tokens'). */
detail?: string
/** Final output for tasks without `readOutput`; stream tasks leave it unset. */
output?: string
}
```
## Consumer views
Snapshots are fresh read-only projections. `ownerSession` carries the shared `SessionId` used for authorization; completion listeners separately receive the exact owner object used for lifecycle cleanup. `reported` suppresses a completion notice after another surface has delivered or committed to deliver the terminal state.
```ts type-equiv
interface TaskSnapshot {
/** The registry-issued id (`<kind>-N`). */
id: TaskId
/** The producer kind the task was registered with. */
kind: TaskKind
/** The producer-supplied one-line label. */
label: string
/**
* Owner session id used for authorization and correlation; absent for
* unowned tasks. Completion listeners receive the exact {@link Agent}
* separately through {@link TaskDoneListener}.
*/
ownerSession?: SessionId
/** Current lifecycle state. */
status: TaskStatus
/** Kind-specific status detail, present once the producer supplied one (usually terminal). */
detail?: string
/** Epoch ms when the task was registered. */
startedAt: number
/** Epoch ms when the task settled; absent while `running`/`stopping`. */
finishedAt?: number
/**
* True when a kill, read, or wait has reported or committed to report the
* terminal state. Completion surfaces suppress redundant notices when set.
*/
reported: boolean
}
```
```ts type-equiv
interface TaskRead {
/**
* Stream kinds: the consuming delta since the previous read. Final-output
* kinds: empty while live, the terminal {@link TaskOutcome.output} (or
* empty) once settled — idempotent, never consumed.
*/
text: string
/** The task's state at read time. */
snapshot: TaskSnapshot
}
```
## Service behavior
[`TaskService`](../../packages/tasks/tasks/src/index.ts) provides atomic `start`, caller-scoped `get` and `list`, `read`, `kill`, bounded `wait`, contained `onTaskDone` listeners, and the `attachSurface` availability fence. Authorization compares owner sessions; owner cleanup selects the exact registered `Agent` instance. See [`dsh-tasks`](../../packages/tasks/tasks/README.md) for the package contract and [`dsh-tool-tasks`](../../packages/tasks/tool-tasks/README.md) for the model-facing surface.
+1 -1
View File
@@ -232,4 +232,4 @@ How a tool wants its call shown in a UI (an editor tool-call card, a CLI log lin
`ToolCallKind` (`'read' | 'edit' | 'delete' | 'move' | 'search' | 'execute' | 'fetch' | 'other'`) picks an icon on a generic card. `FileLocation` (`{ path, line? }`) and `FileDiff` (`{ path, oldText, newText }`) are the shared file-card vocabulary. The design is pinned in [the render-intent-union RFC](../rfc/implemented/architecture/2026-07-02-tool-render-intent-union.md); the ACP bridge maps a `diff` card to a `{ type: 'diff' }` content block, a `terminal` card to the `_meta` terminal convention, and relativizes a file card's title against the session cwd.
The full presentation field docs live in [`packages/core/tools/src/presentation.ts`](../../packages/core/tools/src/presentation.ts). The bash tool's own schemas (`bash`/`bash_output`/`bash_kill`) and the executor they drive are on [bash.md](bash.md).
The full presentation field docs live in [`packages/core/tools/src/presentation.ts`](../../packages/core/tools/src/presentation.ts). The `bash` schema and executor are on [bash.md](bash.md); generic background controls are on [tasks.md](tasks.md).
@@ -1,6 +1,6 @@
# User Interaction
The user-interaction seam of [dsh-user-interaction](../../packages/ui/user-interaction). It is the provider-neutral vocabulary a tool or permission plugin uses when it needs the human to answer before the agent can continue. UI surfaces provide the active `UserInteractionProvider`: `dsh-stdio-agent` renders questions in readline, and `dsh-acp` maps them to ACP form elicitations.
The user-interaction seam of [dsh-user-interaction](../../packages/ui/user-interaction). It is the provider-neutral vocabulary a tool or permission plugin uses when it needs the human to answer before the agent can continue. UI surfaces provide the active `UserInteractionProvider`: `dsh-stdio-demo` renders questions in readline, and `dsh-acp` maps them to ACP form elicitations.
Source: [`packages/ui/user-interaction/src/index.ts`](../../packages/ui/user-interaction/src/index.ts)
+2 -2
View File
@@ -2,5 +2,5 @@
# side as of the last confirmed-consistent state. Both languages carry equal authority;
# after editing either side, bring the other along and re-record with:
# pnpm run verify-translation-pairing --write
development.md: 376df72c14b59b8ac8e42b31f19442040d6a47a7
development.zh.md: 676df50a8a1ab2eff3ec829b5d55904626fa3f5e
development.md: b3c338f03f548b4de4b676850732731323d611f1
development.zh.md: 20f5c585dd378a7b0a3bd6b0af8ebf7dc5e0fd3c
+2 -2
View File
@@ -19,7 +19,7 @@ Install dependencies from the repo root:
pnpm install
```
The install also runs the root `postinstall` script, which installs lefthook from the repo dev dependency through `scripts/install-lefthook.mjs`; the wrapper uses lefthook's reviewed `--force` mode so linked worktrees with an existing `core.hooksPath` do not fail normal `pnpm run …` commands.
The install also runs the root `postinstall` script, which installs lefthook from the repo dev dependency through `scripts/install-lefthook.mjs`; the wrapper script uses lefthook's reviewed `--force` mode so linked worktrees with an existing `core.hooksPath` do not fail normal `pnpm run …` commands.
If hooks are missing because dependencies were restored from cache or `postinstall` was skipped, install them manually:
@@ -59,7 +59,7 @@ DEEPSEEK_BASE_URL=https://... # optional
lefthook is configured in `lefthook.yml` as an early local checkpoint before review:
- `pre-commit` runs staged-file ESLint fixes, `pnpm run typecheck`, and the vendor manifest guard.
- `pre-push` runs `pnpm run check:pre-push`, whose scheduler runs unit tests, snapshot tests, build, module-graph freshness, and the member gates of `pnpm run hygiene` and `pnpm run doc-sync` concurrently.
- `pre-push` runs `pnpm run check:pre-push`, whose scheduler runs runtime-closure verification, unit tests, duplication detection, snapshot tests, build, module-graph freshness, and the member gates of `pnpm run hygiene` and `pnpm run doc-sync` concurrently.
The vendor manifest guard checks that changes under `vendor/*/src` are staged with the matching `vendor/README.md` manifest update. See `vendor/README.md` before editing vendored code.
+3 -3
View File
@@ -9,7 +9,7 @@
- Node.js 支持 22.19+ 与 24+。CI 覆盖 22.19、24 和 26;见 [Node 引擎下限 RFC](rfc/implemented/process/2026-07-06-node-engine-floor.md)。
- 启用了 Corepack 的 pnpm。仓库在 `package.json` 中固定使用 `pnpm@11.7.0`;如果 `pnpm --version` 无法通过 Corepack 解析,请先运行 `corepack enable`
- Git。
- 可选:一个 DeepSeek API key,用于 REPL/ACP agent(智能体)演示和真实 API 的 e2e 测试。
- 可选:一个 DeepSeek API key,用于 REPL/ACPAgent Client Protocol agent(智能体)演示和真实 API 的 e2e 测试。
## 首次搭建
@@ -58,8 +58,8 @@ DEEPSEEK_BASE_URL=https://... # optional
lefthook 在 `lefthook.yml` 中配置,作为评审前的本地早期检查点:
- `pre-commit` 运行对暂存文件的 ESLint 修复、`pnpm run typecheck` 和 vendor manifest 守卫;
- `pre-push` 运行 `pnpm run check:pre-push`,其调度器并发运行单元测试、快照测试、构建、module-graph 新鲜度,以及 `pnpm run hygiene``pnpm run doc-sync` 的各成员门禁。
- `pre-commit` 运行对暂存文件的 ESLint 修复、`pnpm run typecheck` 和 vendor manifest(元数据清单)守卫;
- `pre-push` 运行 `pnpm run check:pre-push`,其调度器并发运行 runtime-closure 校验、单元测试、重复代码检查、快照测试、构建、module-graph 新鲜度,以及 `pnpm run hygiene``pnpm run doc-sync` 的各成员门禁。
vendor manifest 守卫检查 `vendor/*/src` 下的改动是否连同对应的 `vendor/README.md` manifest 更新一起暂存。请在编辑 vendor 代码前先阅读 `vendor/README.md`
+4 -4
View File
@@ -29,10 +29,10 @@ This matrix shows which packages dispatch each harness-owned event and which pac
| `session/disposed` | `emit` | [`packages/core/session/src/index.ts:57`](../packages/core/session/src/index.ts) | [`session`](../packages/core/session) (`events.dispatch`) | - |
| `session/event` | `emit` | [`packages/core/session/src/index.ts:69`](../packages/core/session/src/index.ts) | [`session`](../packages/core/session) (`events.dispatch`) | [`acp`](../packages/ui/acp), [`invariants`](../packages/support/invariants), [`jsonrpc`](../packages/ui/jsonrpc), [`session-persistence`](../packages/session-persistence/session-persistence), [`stdio`](../packages/ui/stdio) |
| `session/flush` | `parallel` | [`packages/core/session/src/index.ts:79`](../packages/core/session/src/index.ts) | [`session`](../packages/core/session) (`events.dispatch`) | [`session-persistence`](../packages/session-persistence/session-persistence) |
| `subagent/end` | `emit` | [`packages/subagent/subagent/src/index.ts:92`](../packages/subagent/subagent/src/index.ts) | [`subagent`](../packages/subagent/subagent) (`events.dispatch`) | [`hooks-claude`](../packages/hooks/hooks-claude), [`jsonrpc`](../packages/ui/jsonrpc) |
| `subagent/provider-added` | `emit` | [`packages/subagent/subagent/src/index.ts:66`](../packages/subagent/subagent/src/index.ts) | [`subagent`](../packages/subagent/subagent) (`emit`) | [`tool-subagent`](../packages/subagent/tool-subagent) |
| `subagent/provider-removed` | `emit` | [`packages/subagent/subagent/src/index.ts:72`](../packages/subagent/subagent/src/index.ts) | [`subagent`](../packages/subagent/subagent) (`events.dispatch`) | [`tool-subagent`](../packages/subagent/tool-subagent) |
| `subagent/start` | `emit` | [`packages/subagent/subagent/src/index.ts:83`](../packages/subagent/subagent/src/index.ts) | [`subagent`](../packages/subagent/subagent) (`events.dispatch`) | [`hooks-claude`](../packages/hooks/hooks-claude) |
| `subagent/end` | `emit` | [`packages/subagent/subagent/src/index.ts:108`](../packages/subagent/subagent/src/index.ts) | [`subagent`](../packages/subagent/subagent) (`events.dispatch`) | [`hooks-claude`](../packages/hooks/hooks-claude), [`jsonrpc`](../packages/ui/jsonrpc) |
| `subagent/provider-added` | `emit` | [`packages/subagent/subagent/src/index.ts:82`](../packages/subagent/subagent/src/index.ts) | [`subagent`](../packages/subagent/subagent) (`emit`) | [`tool-subagent`](../packages/subagent/tool-subagent) |
| `subagent/provider-removed` | `emit` | [`packages/subagent/subagent/src/index.ts:88`](../packages/subagent/subagent/src/index.ts) | [`subagent`](../packages/subagent/subagent) (`events.dispatch`) | [`tool-subagent`](../packages/subagent/tool-subagent) |
| `subagent/start` | `emit` | [`packages/subagent/subagent/src/index.ts:99`](../packages/subagent/subagent/src/index.ts) | [`subagent`](../packages/subagent/subagent) (`events.dispatch`) | [`hooks-claude`](../packages/hooks/hooks-claude) |
| `system-prompt/assemble` | `waterfall` | [`packages/core/system-prompt/src/index.ts:27`](../packages/core/system-prompt/src/index.ts) | [`system-prompt`](../packages/core/system-prompt) (`waterfall`) | - |
| `system-prompt/change` | `emit` | [`packages/core/system-prompt/src/index.ts:33`](../packages/core/system-prompt/src/index.ts) | [`system-prompt`](../packages/core/system-prompt) (`emit`) | - |
| `tools/change` | `emit` | [`packages/core/tools/src/index.ts:116`](../packages/core/tools/src/index.ts) | [`tools`](../packages/core/tools) (`emit`) | - |
+2 -2
View File
@@ -2,5 +2,5 @@
# side as of the last confirmed-consistent state. Both languages carry equal authority;
# after editing either side, bring the other along and re-record with:
# pnpm run verify-translation-pairing --write
README.md: 3a5965ab60a7bdb6345b6abe6815f7090fa98fe1
README.zh.md: 8e62b53ffefa25ad0bbf713889a2ec2ecdb7027a
README.md: 17bb1eeb67b4f5119a698fca23f12490c9378a7f
README.zh.md: c957a82bf420a942e2249942a2d9afc54ad950cf
+4 -4
View File
@@ -17,16 +17,16 @@ This repo's documentation is read by people and agents both inside and outside t
Blob hashes, not commit hashes, so the record is computable for files edited in the same PR (`git hash-object foo.md`) and consistency is a pure content comparison. The recorded hash also recovers the exact last-confirmed text of either side (`git cat-file -p <hash>`), so an out-of-sync pair is updated by diffing the edited side against its last-confirmed state and patching the counterpart minimally — never by re-translating whole files. After bringing the pair back in line, `pnpm run verify-translation-pairing --write` re-records both hashes; that yaml diff is the reviewable act of confirming consistency.
- **Language switcher.** Both files link to each other immediately after their H1 heading: the English file carries `English | [中文](foo.zh.md)` and the Chinese file carries `[English](foo.md) | 中文`.
- **Structure mirrors the counterpart.** Heading depths and order, list kinds, table columns, link targets, and verbatim code blocks match one to one across the pair — see [translation-rules.md](translation-rules.md) for the full preservation rules. Existing Markdown gates apply to `.zh.md` files unchanged (`verify-md-wrap`, `verify-md-links`).
- **Structure mirrors the counterpart.** Heading depths and order, list kinds, ordered-list starts, list item counts, table row and column counts, link targets, and verbatim code blocks match one to one across the pair — see [translation-rules.md](translation-rules.md) for the full preservation rules. Existing Markdown gates apply to `.zh.md` files unchanged (`verify-md-wrap`, `verify-md-links`).
## The gate: verify-translation-pairing
`pnpm run verify-translation-pairing` (part of `doc-sync`, so CI and the pre-push hook run it) enforces the contract mechanically:
1. Every file listed as `required` in [scripts/translation-pairing.manifest.json](../../scripts/translation-pairing.manifest.json) has a complete pair.
2. Every pair that exists at all — required or not — is complete and consistent: all three files present, each side's current blob hash equals the recorded one (editing either side without re-confirming the pair goes red), both sides carry the language switcher, and the structural signatures match in order — heading depths, verbatim code blocks (info string and content), table column counts, list kinds, and every link target apart from the switcher.
2. Every pair that exists at all — required or not — is complete and consistent: all three files present, each side's current blob hash equals the recorded one (editing either side without re-confirming the pair goes red), both sides carry the language switcher, and the structural signatures match in order — heading depths, verbatim code blocks (info string and content), table row and column counts, list kinds, ordered-list starts, item counts, and every link target apart from the switcher.
3. Files listed as `excluded` have no `.zh.md` and no `.i18n.yaml` at all.
4. Every date-named document (`yyyy-mm-dd-*.md`) dated on or after the manifest's `requiredSince` cutoff has a complete pair — new documents merge bilingual from birth.
4. Every date-named document (`yyyy-mm-dd-*.md`) dated on or after the manifest's `requiredSince` cutoff has a complete pair — new date-named RFCs merge bilingual from birth.
`pnpm run verify-translation-pairing --list` prints the current pairing state of every document in scope — missing, out-of-sync, or ok — and is the work list for translation batches. It never fails; it reports.
@@ -49,4 +49,4 @@ The gate's limit, stated plainly: **a green gate means the pair was confirmed co
## Division of labor
Counterparts here are produced by an agent running [dsh-translate-docs](../../.agents/skills/dsh-translate-docs/SKILL.md) and reviewed by a human — inference is cheap here, review attention is the scarce resource. The gate exists so that neither the agent nor the reviewer has to remember the contract: pair completeness, consistency, and structure are checked mechanically, and review attention goes to translation quality and terminology, where human judgment is the whole point.
Counterparts here are produced by an agent running [dsh-translate-docs](../../.agents/skills/dsh-translate-docs/SKILL.md) and reviewed by a human — inference is cheap here, review attention is the scarce resource. The gate checks pair completeness, recorded hashes, switchers, and its documented structural signature. Review still owns translation quality, terminology, and structural requirements that the signature does not encode. The prompt contract is executable: [scripts/translation-prompt.ts](../../scripts/translation-prompt.ts) renders the canonical rules into either direction and strictly parses the three-field XML response, while `verify-translation-prompt` exercises both render directions, the checked-in example, and the CDATA split rule in `doc-sync`.
+8 -7
View File
@@ -2,12 +2,12 @@
[English](README.md) | 中文
本仓库的文档会被公司内外的人和 agent(智能体)阅读,因此 README 与 docs 目录树以英文和简体中文双语维护。本页定义配对契约、强制门禁与推进策略;[translation-rules.md](translation-rules.md) 定义如何翻译;[terminology.md](terminology.md) 是术语真源。仓的 agent 工作流见 [.agents/skills/dsh-translate-docs](../../.agents/skills/dsh-translate-docs/SKILL.md)。
本仓库的文档会被公司内外的人和 agent(智能体)阅读,因此 README 与 docs 目录树以英文和简体中文双语维护。本页定义配对契约、强制门禁与推进策略;[translation-rules.md](translation-rules.md) 定义如何翻译;[terminology.md](terminology.md) 是术语真源。仓库内置的 agent 工作流见 [.agents/skills/dsh-translate-docs](../../.agents/skills/dsh-translate-docs/SKILL.md)。
## 配对契约
- **两种语言同权。**一篇文档可以先用任一语言撰写和评审——先写中文的 RFC 与先写英文的一样正当——另一侧由它翻译而来。两个文件谁也不高于谁;约束它们的是二者必须说同样的话。
- **一对文档是三个同目录文件。**英文 `foo.md`、中文 `foo.zh.md`,加一份一致性记录 `foo.i18n.yaml`,都在同一目录。不用语言目录,不用独立翻译仓库,不用中英混排的单文件。配对整体合入:PR 永远不会只带一种语言而缺其余两个文件。
- **一对文档是三个同目录文件。**英文 `foo.md`、中文 `foo.zh.md`,加一份一致性记录 `foo.i18n.yaml`,都在同一目录。不用语言目录,不用独立翻译仓库,不用中英混排的单文件。配对整体合入:PRPull Request永远不会只带一种语言而缺其余两个文件。
- **一致性记录。**`foo.i18n.yaml` 保存两侧文件在上一次被确认「说同样的话」时各自的完整 git blob hash
```yaml
@@ -17,15 +17,16 @@
用 blob hash 而不是 commit hash,这样同一个 PR 里改动的文件也能算出记录(`git hash-object foo.md`),一致性是纯内容比较。记录的 hash 还能还原任一侧上次确认时的确切文本(`git cat-file -p <hash>`),所以失去同步的配对是「把被改的一侧与其上次确认状态做 diff、再最小化地修补另一侧」——从不整篇重译。两侧对齐后,`pnpm run verify-translation-pairing --write` 重新记录两个 hash;那份 yaml diff 就是「确认一致」这个动作本身,可以被评审。
- **语言切换行。**两个文件在各自 H1 标题之后立即互链:英文文件带 `English | [中文](foo.zh.md)`,中文文件带 `[English](foo.md) | 中文`。
- **结构与另一侧一一对应。**标题深度与顺序、列表类型、表格列、链接目标与逐字节一致的代码块在配对两侧一一对应——完整保持规则见 [translation-rules.md](translation-rules.md)。既有 Markdown 门禁对 `.zh.md` 文件原样生效(`verify-md-wrap`、`verify-md-links`)。
- **结构与另一侧一一对应。**标题深度与顺序、列表类型、有序列表起始编号、列表项数量、表格行列数、链接目标与逐字节一致的代码块在配对两侧一一对应——完整保持规则见 [translation-rules.md](translation-rules.md)。既有 Markdown 门禁对 `.zh.md` 文件原样生效(`verify-md-wrap`、`verify-md-links`)。
## 门禁:verify-translation-pairing
`pnpm run verify-translation-pairing``doc-sync` 的一环,因此 CI 和 pre-push 钩子都会运行)机械地强制执行这份契约:
`pnpm run verify-translation-pairing``doc-sync`(文档同步门禁)的一环,因此 CI 和 pre-push 钩子都会运行)机械地强制执行这份契约:
1. [scripts/translation-pairing.manifest.json](../../scripts/translation-pairing.manifest.json) 中 `required` 列出的每个文件都有完整配对。
2. 任何已存在的配对——无论是否 required——都完整且一致:三个文件齐全、每一侧的当前 blob hash 等于记录值(改了任一侧而没重新确认配对就变红)、双方都带语言切换行、结构签名按序一致——标题深度、逐字节一致的代码块(信息字符串与内容)、表格列数、列表类型,以及除切换行之外的每个链接目标。
2. 任何已存在的配对——无论是否 required——都完整且一致:三个文件齐全、每一侧的当前 blob hash 等于记录值(改了任一侧而没重新确认配对就变红)、双方都带语言切换行、结构签名按序一致——标题深度、逐字节一致的代码块(信息字符串与内容)、表格列数、列表类型、有序列表起始编号、列表项数量,以及除切换行之外的每个链接目标。
3. 列为 `excluded` 的文件完全没有 `.zh.md`,也没有 `.i18n.yaml`。
4. 凡文件名符合 `yyyy-mm-dd-*.md` 且日期不早于 manifest(元数据清单)中 `requiredSince` 分界日期的文档,都必须有完整配对——新建的日期命名 RFC 从创建起便须配齐中英文。
`pnpm run verify-translation-pairing --list` 打印范围内每篇文档的当前配对状态——missing、out-of-sync 或 ok——是翻译批次的工作清单。它从不失败;它只报告。
@@ -44,8 +45,8 @@
- `docs/i18n/terminology.md` 与 [style-samples.md](style-samples.md)——二者本身即为中英对照文档。
- [translation-prompt.md](translation-prompt.md)——自动翻译流水线的 prompt 模板;正文逐字进入模型请求,配对翻译会改变流水线行为。
**推进**:以日期命名的文档(`yyyy-mm-dd-*.md`,即 RFC),只要标注日期等于或晚于 manifest 的 `requiredSince` 分界日期,合入时就必须配齐双语文件。更早日期的文件属于待翻清单(backlog),包括分界前夜创建的文件。RFC 文件名记录首次提出日期,因此倒填日期绕过分界属于评审可见的违规。manifest 中的 `required` 列表是当前执行红线,并非全量覆盖这一最终目标。翻译批次将路径加入 `required`,使门禁只向前收紧。未列入的文档仍可通过 `--list` 查看,而任何已存在的配对都受完整契约约束。后续修改必须同步更新两侧,因此 `required` 的扩展速度不能超过翻译评审的承载能力。
**推进**:以日期命名的文档(`yyyy-mm-dd-*.md`,即 RFC),只要标注日期等于或晚于 manifest 的 `requiredSince` 分界日期,合入时就必须配齐双语文件。更早日期的文件属于 backlog(待翻清单),包括分界前夜创建的文件。RFC 文件名记录首次提出日期,因此倒填日期绕过分界属于评审可见的违规。manifest 中的 `required` 列表是当前执行红线,并非全量覆盖这一最终目标。翻译批次将路径加入 `required`,使门禁只向前收紧。未列入的文档仍可通过 `--list` 查看,而任何已存在的配对都受完整契约约束。后续修改必须同步更新两侧,因此 `required` 的扩展速度不能超过翻译评审的承载能力。
## 分工
这里的对侧译文由运行 [dsh-translate-docs](../../.agents/skills/dsh-translate-docs/SKILL.md) 的 agent 产出、由人评审——在这里推理(inference)很便宜,评审注意力才是稀缺资源。门禁的存在让 agent 和评审者都不必记住契约:配对完整性、一致性和结构由机械检查兜底,评审注意力投向翻译质量术语——这正是人的判断的用武之地
对侧译文由运行 [dsh-translate-docs](../../.agents/skills/dsh-translate-docs/SKILL.md) 的 agent 生成,再由人评审在这里推理(inference)很便宜,评审注意力才是稀缺资源。门禁负责检查配对是否完整、记录的 hash、语言切换行以及本文列出的结构签名;翻译质量术语和签名未涵盖的结构要求仍由评审把关。prompt 契约也有可执行实现:[scripts/translation-prompt.ts](../../scripts/translation-prompt.ts) 会把权威规则渲染到英译中或中译英的 prompt 中,并严格解析包含三个字段的 XML 响应;`doc-sync` 中的 `verify-translation-prompt` 会检查两个渲染方向、仓库内示例与 CDATA 拆分规则
+13 -13
View File
@@ -1,8 +1,8 @@
# 翻译语体样例(style samples
本文件是翻译语体的校准锚点:每组样例是一段英文原文与一段人工定稿的中文译文,覆盖本仓库文档的主要文体。**译文的语体以这些样例为准**——它们的效力高于任何对语气的文字描述。翻译或评审时对照最接近的文体样例;样例与规则冲突时,样例胜出。本文件中英对照、自成双语,不参与配对(见 [README.md](README.md) 排除清单)。
本文件是翻译语体的校准锚点:每组样例是一段英文原文与一段人工定稿的中文译文,覆盖本仓库文档的主要文体。**译文的语体以这些样例为准**——文体样例的效力高于对语气的文字描述,但术语表、忠实性与结构规则仍然优先。翻译或评审时对照最接近的文体样例。本文件中英对照、自成双语,不参与配对(见 [README.md](README.md) 排除清单)。
维护方式:人工评审校准出新的金标段落后追加到对应文体;样例只增不改,改动需评审人签字(PR 评审即签字)
维护方式:人工评审校准出新的金标段落后追加到对应文体;发现语义、结构或术语错误时直接修正。新增或修正样例都需经过 PR 评审。
## ① 架构叙述
@@ -10,13 +10,13 @@
本文介绍 DeepSeek Harness 整体架构,它是 **DeepSeek Code** 的底层基座。微内核设计讨论中确立了核心设计准则:**一切皆插件**。内核刻意做得极精简,仅包含少量抽象服务,外加一个实体循环插件 `dsh-agent-loop`。所有产品功能均基于本文定义的扩展接口开发为独立插件,无需改动主循环逻辑。
> Dependency rule: extension plugins depend on interfaces, never on `dsh-agent-loop` (the loop is swappable); the sanctioned exception is the composition bundle `dsh-agent-core`, whose job is assembling the concrete spine.
> Dependency rule: extension plugins depend on interfaces, never on `dsh-agent-loop` (the loop is swappable); the sanctioned exception is the composition bundle `dsh-agent-spine-demo`, whose job is assembling the concrete spine.
依赖约束规范:各类扩展插件仅依赖抽象接口,严禁直接依赖 `dsh-agent-loop`(该主循环支持替换实现);唯一允许的特例是组合包 `dsh-agent-core`,它的职责是组装整套实体主干。
依赖约束规范:各类扩展插件仅依赖抽象接口,严禁直接依赖 `dsh-agent-loop`(该主循环支持替换实现);唯一允许的特例是组合包 `dsh-agent-spine-demo`,它的职责是组装整套实体主干。
> This document covers **behavior**; type shapes live in [core-data-structures/](../core-data-structures/core.md), the per-event/service reference in the [generated catalog](../cordis-catalog/events.md), per-package contracts in the package READMEs ([map](../../packages/README.md)).
本文档描述整体行为逻辑;类型定义存放于 [core-data-structures/](../core-data-structures/core.md);各类事件、服务的详细参考见[生成目录](../cordis-catalog/events.md);各 package 对外约束协议写在对应包的 README[索引](../../packages/README.md))。
本文档描述整体行为逻辑;类型定义存放于 [core-data-structures/](../core-data-structures/core.md);各类事件、服务的详细参考见[生成目录](../cordis-catalog/events.md);各包(package)的对外契约写在相应的 README[索引](../../packages/README.md))。
## ② 防御模式规则
@@ -26,11 +26,11 @@
> **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 prove disposal waited (pid gone right after `await fiber.dispose()`), not merely that the process eventually dies.
**销毁操作必须等待所有任务完全停稳,不能仅下发终止指令就返回**——若销毁逻辑仅发送终止中断信号,不等任务停止就直接退出,会产生孤儿进程。清理逻辑需设为异步,等待所有子任务彻底退出(先发终止信号,再等待执行完成);在执行终止操作前先关闭监听器与通知注册表,延迟到达的完成事件不再触发任何通知。测试要验证销毁流程确实完成等待:执行完 `await fiber.dispose()` 后进程 PID 立即消失,不能仅校验进程最终会自行消亡。
**dispose(资源释放)必须等待所有任务完全停稳,不能仅下发终止指令就返回**:如果清理过程只发出终止中断信号,不等任务停止就返回,就会留下孤儿进程。清理应采用异步方式,等待所有子任务彻底退出(先发终止信号,再等待退出);发出信号前应先关闭监听器与通知注册表,使延迟到达的完成事件不再触发通知。测试要证明 dispose 的确等到清理完成:执行完 `await fiber.dispose()` 后进程 PID 立即消失,不能只检查进程最终会自行消亡。
> **Async state is not synchronous state** — `agent.send()` does not flip status before returning; 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 observe the transition (saw `running` THEN `idle`) rather than counting actions you assume map 1:1 to turns.
**异步状态不等同于同步瞬时状态**——调用 `agent.send()` 不会在返回前同步更新状态;后台任务完成时与轮次边界存在竞态;调用 `reader.close()`可能是读到文件末尾,也可能是资源销毁触发。切勿仅凭刚查询到的状态来阻断流程;生命周期逻辑应基于真实触发的事件 promise 驱动`agent/status``task.done`,观测完整状态切换(先 `running``idle`),而非主观认定操作和执行轮次一一对应(主循环会批量处理排队消息)
**异步状态不等同于同步瞬时状态**调用 `agent.send()` 不会在返回前同步更新状态;后台任务完成时与轮次边界存在竞态;`reader.close()`会在读到文件末尾时触发,也会在资源释放时触发。切勿把刚刚发起的状态变更当成已经生效,据此控制流程;生命周期逻辑应以实际触发的事件和已完成的 promise`agent/status``task.done`为准,并观察完整状态变化(先 `running``idle`),不要根据操作次数推断操作与轮次一一对应
## ③ 测试政策清单
@@ -40,15 +40,15 @@
> We are DeepSeek — do not ration real-API tests. A no-key test proves the plumbing; only a with-key run proves the agent works against a real model. Write many: real prompts that write files, multi-turn conversations, tool use, cancellation mid-stream. Cheapest and highest-value are **smoke tests** that boot the real example, send one real prompt, and check the world — they catch the "green unit tests, broken product" class that mocks structurally cannot. The self-skip exists only so secretless CI and keyless contributors aren't blocked; it is not a cost signal.
我们是 DeepSeek:真实接口相关测试不得刻意缩减用例数量。无密钥测试仅能验证底层通路;只有携带有效密钥执行的用例,才能确认 agent 可正常对接真实模型。请大量编写此类测试:包含文件写入类真实提示词、多轮对话、工具调用、流式中途取消等场景。
我们是 DeepSeek:真实接口相关测试不得刻意缩减用例数量。无密钥测试仅能验证底层通路;只有携带有效密钥执行的用例,才能确认 agent(智能体)可正常对接真实模型。请大量编写此类测试:包含文件写入类真实提示词、多轮对话、工具调用、流式中途取消等场景。
成本最低、收益最高的是**冒烟测试**:拉起完整真实示例,发送一条真实提示并校验整体运行状态。这类用例能捕获一类问题——单元测试全部绿灯,但产品实际运行故障,单靠 mock 完全无法发现这类缺陷。
成本最低、收益最高的是**冒烟测试**:拉起完整真实示例,发送一条真实提示,并检查文件、进程等外部可观察结果。这类用例能捕获一类问题——单元测试全部绿灯,但产品实际运行故障,单靠 mock 完全无法发现这类缺陷。
自带自动跳过逻辑,仅用于保障无密钥 CI 环境、无权限贡献者不会被流程拦截,不代表可以以此为由削减真实接口测试投入。
> **Prefer the real implementation over a mock** — Mock only the genuinely expensive or non-deterministic boundary (the LLM adapter, the network, the clock); keep everything downstream real. A hand-rolled stand-in proves the bridge moves bytes, not that the shipping tool behaves as asserted — the two drift while the test stays green.
**优先使用真实实现,而非 mock 替身**——仅对开销极大、结果不确定的边界模块做 mock(LLM 适配器、网络、时钟),其余下游组件全部使用真实实现。手写的 mock 替身只能验证数据通路能传输字节,无法保证线上工具符合预期逻辑;长期下来业务逻辑与 mock 实现会出现偏差,但测试仍会显示通过。
**优先使用真实实现,而非 mock 替身**——仅对开销极大、结果不确定的边界模块做 mock(LLM(大语言模型)适配器、网络、时钟),其余下游组件全部使用真实实现。手写的 mock 替身只能验证数据通路能传输字节,无法保证线上工具符合预期逻辑;长期下来业务逻辑与 mock 实现会出现偏差,但测试仍会显示通过。
## ④ 机制描述
@@ -60,7 +60,7 @@
> The gate's limit, stated plainly: a green gate means the pair was confirmed consistent at these exact contents, not that the confirmation was sound. It checks hashes and shape; it cannot judge whether the two sides actually say the same thing — that is the reviewer's half of the contract. A re-recorded pair with a sloppy counterpart passes the gate; it must not pass review.
明确门禁校验边界:门禁校验通过,仅代表两份文档哈希与结构完全匹配,不代表译文内容准确无误。门禁仅校验哈希与结构,无法判断双语表意是否统一——译文质量把关是评审人的责任。即便译文粗糙、表意偏差,只要哈希匹配,门禁就会放行,但这类 PR 绝不能通过人工评审。
门禁的边界很明确:通过门禁只说明两侧文件当前的 blob hash 与伴随记录吻合,并且结构签名一致,也就是说,这组内容曾被确认一致;它不代表这次确认可靠。门禁无法判断两种语言是否真正表达了相同的意思;这部分契约要由评审人把关。即使译文粗糙、表意有误,重新记录配对后仍能通过门禁,但绝不能通过人工评审。
## ⑥ RFC 论证
@@ -70,9 +70,9 @@
## ⑦ 推进策略(长段拆分示范)
> **Rollout**: new documents don't wait for a batch — a date-named document dated on or after the manifest's `requiredSince` cutoff must merge with its pair, so everything new is bilingual from birth. For the back-catalog, the `required` list in the manifest is the enforcement frontier, not the goal. […] Pairing a document is a commitment: every later edit to either side must carry the counterpart along, so grow the frontier at the pace translation review is actually resourced, not ahead of it.
> **Rollout**: date-named RFCs don't wait for a batch — one dated on or after the manifest's `requiredSince` cutoff must merge with its pair, so each new date-named RFC is bilingual from birth. For the back-catalog, the `required` list in the manifest is the enforcement frontier, not the goal. […] Pairing a document is a commitment: every later edit to either side must carry the counterpart along, so grow the frontier at the pace translation review is actually resourced, not ahead of it.
**推进**新增文档不再走批量分批翻译流程。以日期命名的文档,若其标注日期等于或晚于 manifest `requiredSince` 分界时间,提交合入时必须配套对应的双语译文文件——所有新文档从创建起就要求中英双语齐备。对存量文档manifest 内的强制翻译列表只是当执行红线,并非最终目标。(……)文档完成双语配对等同于一份长期约束承诺:后续只要修改任一版本,就必须同步更新对应另一语种文件。因此强制翻译范围的推进节奏,要匹配翻译评审实际可投入人力,切勿超前铺开
**推进**日期命名的 RFC 无需等待批量翻译。只要文件名中的日期不早于 manifest(元数据清单)的 `requiredSince` 分界日期,合入时必须配齐中英文,因此此类 RFC 从创建起就要求双语齐备。对存量文档manifest 中的 `required` 列表只是当前的执行红线,并非最终目标。(……)一旦文档完成配对,后续修改任一侧都必须同步更新另一侧。因此,应根据实际可投入的翻译评审能力逐步扩展执行红线,不能超前
## 从样例提炼的要点
+13 -4
View File
@@ -33,7 +33,7 @@
| English | 中文 | 首次出现 | 不要译作 | 备注 |
|---|---|---|---|---|
| agent | agent | agent(智能体) | | |
| agent harness | agent harness | | | agent 组合词(agent harness/workflow/loop/skill 等)整体保留英文;未括注过 agent 时首现按 agent 行处理 |
| agent harness | agent harness | agent harness(智能体框架) | | agent 组合词(agent harness/workflow/loop/skill 等)整体保留英文;未括注过 agent 时首现按对应组合词或 agent 行处理 |
| agent loop | agent loop | agent loop(智能体循环) | | |
| backlog | backlog | backlog(待翻清单) | | 仅在双语翻译语境里括注`待翻清单` |
| blob hash | blob hash | | | `git hash-object` 的结果 |
@@ -76,13 +76,16 @@
| backend | 后端 | | | |
| background task | 后台任务 | | | |
| block | 块 | | | |
| build target | 构建目标 | | | |
| cancel | 取消 | | | |
| capability | 能 | | | |
| feature | 能 | | 能力 | SDK 产品与工程模型中的可管理产品单元 |
| feature option | 功能选项 | | variant | 一项 SDK 功能内有限、可选择的实现或配置 |
| checkpoint | 检查点 | | | |
| chunk | 分片 | | | |
| compaction | 压缩 | 压缩(compaction | | |
| companion tool | 配套工具 | | | |
| config | 配置 | | | |
| Cordis plugin config | Cordis 插件配置 | | | Cordis 插件公开的 `Config` 对象或配置结构 |
| config key | 配置键 | | | Cordis 插件配置中的单个字段 |
| consumer | 消费方 | | | |
| content block | 内容块 | | | |
| Cookbook | 实操手册 | | | 文档标题用语 |
@@ -90,10 +93,13 @@
| counterpart | 对侧文件 | | 对应物、配对物 | 双语配对语境;泛指"另一侧"时可写「另一侧」 |
| context compaction | 上下文压缩 | 上下文压缩(context compaction | | |
| contract | 契约 | | | 如:`pairing contract``配对契约` |
| Cordis config entry | Cordis 配置项 | | | 指 `cordis.yml` 插件列表中的一项;插件实现本身写`Cordis 插件` |
| Cordis plugin | Cordis 插件 | | | Cordis 加载的插件实现,不指 `cordis.yml` 中的一项配置 |
| coverage | 覆盖率 | | | |
| crash recovery | 崩溃恢复 | | | |
| deploy root | 部署根目录 | | | |
| durability | 持久性 | | | |
| feature requirement | 功能依赖 | | | 功能或功能选项通过 `requires` 声明的关系 |
| enforcement frontier | 执行红线 | | 强制边界 | i18n 配对机制用语:manifest `required` 清单所划的门禁生效范围;与金标样例(style-samples ⑦)一致 |
| event | 事件 | | | |
| event log | 事件日志 | | | |
@@ -104,7 +110,7 @@
| extension point | 扩展点 | | | 注意与 `seam` 区分 |
| fail-fast | 快速失败 | | | |
| fenced code block | 围栏代码块 | | | 沿用 MDN 中文翻译 |
| fingerprint | 指纹 | | | i18n 配对机制用语:`.zh.md` 首行记录英文源 blob hash 的注释 |
| fingerprint | 指纹 | | | 通用内容指纹;双语配对机制使用 sidecar record 记录两侧 blob hash |
| finish reason | 结束原因 | | | |
| foreground run | 前台运行 | | | |
| freshness | 新鲜度 | | | 沿用 MDN 中文翻译;在本项目中指译文相对源文的同步状态 |
@@ -122,6 +128,7 @@
| mod | 模组 | | | |
| model provider | 模型提供方 | | | |
| module | 模块 | | | |
| npm dependency | NPM 依赖 | | | `package.json` 中的包关系;`dependencies`、`devDependencies` 等字段保持原样 |
| orphan | 遗留 | | 孤儿、孤立 | 指英文源已不存在的 `.zh.md`(如「遗留译文」);进程语境按 OS 惯用语译「孤儿进程」 |
| orphan branch | 孤立分支 | | 孤儿分支 | 沿用 git 官方中文翻译 |
| package | 包 | 包(package | | 指 npm 包(`@deepseek-ai/dsh-*`);`package.json` 等代码标识保持原样 |
@@ -171,3 +178,5 @@
| vocabulary | 词汇 | | | |
| wire format | 协议格式 | 协议格式(wire format | | |
| workflow | 工作流 | | | |
| wrapper | 包装层 | | | 软件层或 SDK 包装层 |
| wrapper script | 包装脚本 | | | 可执行脚本包装层 |
+51 -126
View File
@@ -1,24 +1,27 @@
# Translation prompt (pipeline asset)
本文件是自动翻译流水线的 prompt 模板; `# Translation Prompt` 的正文逐字进入模型请求,因此不参与双语配对(见 [README.md](README.md) 排除清单)。渲染时[terminology.md](terminology.md) 整表填入 `{{terminology}}`。[style-samples.md](style-samples.md) 定义文体,模板内嵌的 Examples 仅抽样问题类型;两者冲突时以文体样例为准。修改本文件即修改翻译行为,需正常 PR 评审。
本文件是自动翻译流水线使用的 prompt 模板; `# Translation Prompt` 开始的正文逐字进入模型请求,因此本文件不参与双语配对(见 [README.md](README.md) 排除清单)。渲染时会把 [translation-rules.md](translation-rules.md) 全文填入 `{{translation_rules}}`,把 [terminology.md](terminology.md) 整表填入 `{{terminology}}`,以免模板另存一份规则而日后失去同步。[style-samples.md](style-samples.md) 定义文体,模板的 Examples 只用于说明典型问题;术语表、忠实性和结构规则优先于样例,样例只在这些硬性约束内决定文体。修改本文件会改变翻译行为,需正常经过 PR 评审。
## 占位符契约
流水线渲染模板时替换以下占位符,除此之外不做任何文本处理
流水线渲染模板时替换以下占位符,除此之外不改写系统消息
| 占位符 | 填入内容 | 来源 |
|---|---|---|
| `{{source_lang}}` | 源语言名(`English` / `Chinese` | 由改动侧文件推断:`.zh.md` 被改则为 `Chinese` |
| `{{target_lang}}` | 目标语言名(`Chinese` / `English` | 与 `{{source_lang}}` 相对 |
| `{{translation_rules}}` | [translation-rules.md](translation-rules.md) 全文(Markdown 原文) | 渲染时读取仓库当前版本,不缓存 |
| `{{terminology}}` | [terminology.md](terminology.md) 的完整表格(Markdown 原文) | 渲染时读取仓库当前版本,不缓存 |
| `{{source_filename}}` | 源文档的 basename(如 `foo.md`) | 由流水线从待译文件路径取得 |
| `{{source_filename_zh}}` | 中文侧 basename(如 `foo.zh.md` | `{{source_filename}}` 派生 |
| `{{source_filename}}` | 源文档的 basename(如 `foo.md``foo.zh.md`) | 由流水线从待译文件路径取得 |
| `{{source_filename_zh}}` | 中文侧 basename(如 `foo.zh.md` | 英文源追加 `.zh`;中文源使用自身 basename |
流水线仅支持上表占位符,并按整篇文档翻译。它不支持 `{{to}}``{{title_prompt}}``{{summary_prompt}}``{{terms_prompt}}``{{imt_style_guide}}``%%` 分段协议;输出采用下方三段 XML
例如,英译中时若源文件是 `foo.md``{{source_filename}}``foo.md``{{source_filename_zh}}``foo.zh.md`;中译英时若源文件是 `foo.zh.md`,两个占位符都填 `foo.zh.md`
流水线只识别上表中的占位符,并且一次翻译整篇文档。它不支持 `{{to}}``{{title_prompt}}``{{summary_prompt}}``{{terms_prompt}}``{{imt_style_guide}}``%%` 分段协议。输出必须是一个以 `<dsh-translation-response>` 为根元素的 XML 文档;三个子元素中的 Markdown 内容都放在 CDATA 中。内容出现 `]]>` 时写成 `]]]]><![CDATA[>`XML 解析后仍会还原为原文。
## Few-shot 金标
流水线的 few-shot 是**整文档**的中英对照,不是模板内嵌的句子级正误例(那是最小抽样)。few-shot 集取自以下 5 组人工定稿的配对文档,以仓库当前版本为准随仓库更新:
流水线使用**整文档**的中英对照作为 few-shot,不是模板内嵌的句子级正误例。以下 5 组配对文档均经过人工评审,并以仓库当前版本为准随仓库一同更新:
- `README.md``README.zh.md`
- `docs/development.md``docs/development.zh.md`
@@ -26,136 +29,58 @@
- `docs/i18n/translation-rules.md``docs/i18n/translation-rules.zh.md`
- `docs/rfc/implemented/process/2026-07-02-bilingual-docs-and-pairing-gate.md` ↔ 对应 `.zh.md`
注入方式:在系统消息(本模板)之后、待译文档之前,每组作为一轮示例对话——user 消息源文档全文,assistant 消息为定稿译文全文(不带三段 XML 包装;只有真实请求要求三段输出)。上下文紧张时按上列顺序从后往前裁剪组数。这 5 组也是评审校准锚点(见 [style-samples.md](style-samples.md)),改动任何一组改变流水线行为。
注入时按当前翻译方向选择每组的源侧与目标侧:user 消息包含源文档全文,assistant 消息采用模板正文规定的 XML 协议;`translation``final` 都放入目标文档全文,`review``- [None] No corrections.`。CDATA 遵循上文的 `]]>` 拆分规则。上下文不足时,按上列顺序从后往前删减示例组数。这 5 组也是评审校准锚点改动任何一组都会改变流水线行为。
## 模板正文
````text
# Translation Prompt
You are a senior technical translator specializing in LLM and agent development documentation. Your task is to translate the given source document from {{source_lang}} to {{target_lang}}, producing natural, professional technical prose.
You are a senior technical translator specializing in LLM and agent development documentation. Translate the complete source document from {{source_lang}} to {{target_lang}} as natural, professional technical prose.
## Quality Requirements
## Binding Translation Rules
### Structure and Format Preservation
- Output a complete translated document that maintains exactly the same structure as the source: heading hierarchy, list shape, table columns, link targets, and code blocks.
- Fenced code blocks must be byte-identical to the source, including ALL comments inside them. Do NOT translate comments inside code blocks. This is a hard rule with no exceptions.
- Inline code spans (commands, flags, paths, API names, version numbers) must be kept verbatim. Never translate or reformat them.
- Every relative link must point to the same target as in the source. Link text is translated; link targets are not.
- Language switcher line: the source document's filename is `{{source_filename}}`. When translating into Chinese, write `[English]({{source_filename}}) | 中文` immediately after the H1 heading. When translating into English, write `English | [中文]({{source_filename_zh}})` immediately after the H1. Emit this line even when the source file has no switcher yet (a brand-new pair); when the source does have one, flip the link direction — never copy it unchanged.
- After a closing bold marker `**`, insert a space before the next character when that character is a Latin letter or digit. Never insert a space before full-width (Chinese) punctuation.
The canonical repository rules below are injected verbatim. Apply every direction-appropriate requirement. In those rules, the authored document is the source for this request and the generated document is its counterpart.
### Tone and Style
- The translation must read as if originally written in the target language by a native speaker. If an expression sounds like a word-for-word rendering from the source language, rephrase it.
- Write in a professional, formal tone appropriate for developer documentation. Never use colloquial or casual expressions.
- Use polite imperative forms where the text instructs the reader to do something.
- Keep the author's register: concise stays concise, detailed stays detailed.
{{translation_rules}}
### Sentence Structure
- Break long sentences with commas or semicolons. Avoid run-on sentences.
- Prefer active voice. Convert passive constructions to active if it reads more naturally.
- Translate meaning, not words. Restructure sentences where the target language grammar requires it.
- Do not invent words or expressions that do not exist in natural technical writing of the target language.
## Request-Specific Structure
### Word Choice
- Prefer precise, formal vocabulary over casual or colloquial alternatives.
- When multiple synonyms exist, choose the one most commonly used in professional technical documentation of the target language.
- Avoid slang, internal jargon, or overly literal translations that would not be recognized by the general developer audience.
- Do not use the same word to translate two different source-language terms that carry distinct meanings.
- Avoid repeating the same verb in close proximity; vary word choice for readability.
- The source basename is `{{source_filename}}`. When translating into Chinese, write `[English]({{source_filename}}) | 中文` immediately after the H1. When translating into English, write `English | [中文]({{source_filename_zh}})` immediately after the H1.
- Emit the switcher for a new pair and flip an existing switcher; never copy it unchanged.
#### When translating into Chinese
- When a number modifies a noun, always include a Chinese classifier or measure word (量词). For example: "three-package seam" → "由三个 package 构成的 seam", not "三 package seam".
## Binding Terminology
### Punctuation
#### When translating into Chinese
- Use full-width Chinese punctuation in prose: `,。:;?!()「」`.
- Strongly prefer replacing all em-dashes (——) with colons, periods, commas, or parentheses. Keep an em-dash only if no other punctuation works at all.
- Use enumeration commas (、) between parallel items, not regular commas.
- List item endings: use semicolons or no punctuation. Do not end list items with commas.
- Put one half-width space between Chinese text and Latin words/numbers.
- For RFC 2119 keywords (MUST, MUST NOT, SHOULD, MAY), render the corresponding Chinese term in italics: *必须*、*禁止*、*应当*、*可以*.
#### When translating into English
- Use half-width English punctuation and standard English spacing. Preserve full-width punctuation only in verbatim Chinese text.
- Convert enumeration commas (、) to English commas; convert 「」 quotes to English double quotes.
- Render the terminology table's English column the same way the Chinese column binds the other direction: listed terms use exactly the table's English form; first-occurrence glosses do not carry over (English prose never glosses an English term with Chinese).
- Chinese topic-comment sentences and dropped subjects become explicit English subjects; prefer concise declaratives over nominalizations.
- Do not transliterate Chinese engineering idioms literally: render the underlying concept (误报 → false positive, 执行红线 → enforcement frontier), consulting the terminology table first.
- Keep the register of institutional developer documentation: contractions are acceptable, marketing language and hedging (very, quite, simply) are not.
## Terminology
A terminology table is provided below. Follow it strictly:
- Render every listed term exactly as specified.
- First occurrence: write as shown in the "首次出现" column (with parenthetical gloss). Subsequent occurrences: write only the part before the parentheses.
- If a term has already been glossed as part of a compound term, do not gloss it again when it appears alone later.
- NEVER use translations listed in the "不要译作" column.
- For technical terms not in the table: keep them in the source language. Do not invent a translation. This rule applies to terminology only; for general prose, freely restructure and paraphrase for natural expression.
Apply the current table below exactly as required by the injected translation rules.
{{terminology}}
## Output Format
Produce your output in three XML sections:
Return exactly one well-formed XML document with this root and these three child elements. Do not wrap it in a Markdown code fence. Put all Markdown and review text inside CDATA. If any content contains the CDATA terminator, split it as `]]]]><![CDATA[>` so XML parsing reconstructs the original `]]>` sequence.
```xml
<translation>
(Complete translation of the source document)
</translation>
<review>
(Self-review notes, one correction per line with category tag, e.g.)
- [Tone] "旁挂记录" → "伴随记录"(生造词)
- [Sentence] 第 3 段补充逗号断句
- [Punctuation] 两处破折号替换为冒号
- 无修正
</review>
<final>
(Final translation after corrections)
</final>
<dsh-translation-response version="1">
<translation><![CDATA[
(Complete first-pass translation)
]]></translation>
<review><![CDATA[
- [Tone] Replaced a literal rendering with the established target-language phrasing.
- [Terminology] Applied the binding sidecar record term.
]]></review>
<final><![CDATA[
(Complete corrected translation)
]]></final>
</dsh-translation-response>
```
## Self-Review Instructions
After writing `<translation>`, re-read it in the target language only, without looking at the source. Check by category:
**Structure**
- Is the heading hierarchy, list shape, and code block content identical to the source?
- Are ALL comments inside code blocks left untranslated (byte-identical to source)?
- Is the language switcher line correctly flipped (not copied from source)?
- Are link targets preserved and bold markers followed by a space?
**Tone & Style**
- Does every sentence read as if originally written by a native speaker?
- Is there any colloquial, casual, or overly informal phrasing?
**Sentence Structure**
- Are there run-on sentences that need breaking?
- Are there stiff passive constructions that should be converted to active voice?
**Word Choice**
- Are there overly literal translations that sound unnatural?
- Is the same target-language word used to translate two distinct source concepts?
- Is any slang or internal jargon present?
**Terminology**
- Are first-occurrence glosses correctly applied (not missing, not repeated)?
- Are any "不要译作" forbidden translations present?
- Are unlisted terms correctly kept in the source language?
**Punctuation** (when target is Chinese)
- Are there em-dashes that should be replaced with colons, periods, or commas?
- Are list items ending with commas instead of semicolons?
- Are RFC 2119 keywords rendered in italics?
Record corrections in `<review>` with category tags. Then output the corrected version in `<final>`. If no corrections are needed, write "无修正" in `<review>` and copy the translation unchanged into `<final>`.
After writing `<translation>`, re-read it in the target language without looking at the source. Then apply the injected translation rules as a clause-by-clause comparison against the source and record actual corrections in English inside `<review>`. Apply every recorded correction in `<final>`. If no correction is needed, write only `- [None] No corrections.` in `<review>` and copy `<translation>` unchanged into `<final>`.
## Examples
Below are representative examples of common problems and their corrections. Follow the "Good" versions.
Follow the Good versions; these sentence-level examples illustrate error categories, not the assistant-message wire format.
### Colloquial verb → Professional verb
- Source: `The repo pins pnpm@11.7.0 in package.json`
@@ -177,40 +102,40 @@ Below are representative examples of common problems and their corrections. Foll
- Bad: `旁挂记录两侧 blob hash,使一致性可检查`
- Good: `伴随记录保存两侧 blob hash,使一致性可检查`
### Em-dash → Colon/period
- Source: `FIXME — an issue that should block a new release. A release should not ship with an open FIXME unless reviewers explicitly agree the change can be merged anyway.`
- Bad: `FIXME——应当阻塞新版本发布的问题。除非评审者明确同意可以照常合入,发布不应带着未解决的 FIXME 出门。`
- Good: `FIXME:应当阻塞新版本发布的问题。除非评审者明确同意该更改可以合并,否则发布版本不应包含未解决的 FIXME。`
### Overly literal → Meaningful rendering
- Source: `awkward phrasing is easier to hear without the source anchoring you`
- Bad: `没有源文锚着,别扭的表述更容易被听出来`
- Good: `不对照原文时,更容易察觉别扭的表达`
### Terminology — do not translate what should be kept in English
### Terminology — keep the binding English form
- Source: `typed service seams, and explicit extension points`
- Bad: `类型化的服务 seam(扩展点)与显式扩展点`
- Good: `类型化的服务 seam 与显式扩展点`
### Slang/jargon → Professional phrasing
### Slang → Professional phrasing
- Source: `The committed agent workflow lives in .agents/skills/dsh-translate-docs`
- Bad: `进仓的 agent 工作流见 .agents/skills/dsh-translate-docs`
- Good: `仓库内置的 agent 工作流见 .agents/skills/dsh-translate-docs`
### "For humans" — translate the intent, not the word
- Source: `For humans, start with the development guide`
- Bad: `对于人工读者,请先从开发指南开始`"人工读者"生硬)
- Good: `面向开发者:请先阅读开发指南`"开发者"自然,且中文里冒号在此处更自然)
### Chinese → English — idiomatic subject and predicate
- Source: `门禁绿并不代表译文内容正确。`
- Bad: `The gate green does not represent that the translation content is correct.`
- Good: `A green gate does not mean the translation is correct.`
### Code block comments — NEVER translate
### Code block comments — never translate
- Source code block contains: `# REPL agent demo (needs DEEPSEEK_API_KEY)`
- Bad: `# REPL agent 演示(需要 DEEPSEEK_API_KEY`
- Good: `# REPL agent demo (needs DEEPSEEK_API_KEY)` (keep exactly as-is, byte-for-byte)
- Good: `# REPL agent demo (needs DEEPSEEK_API_KEY)` (byte-identical)
### Language switcher — flip direction
- Source file (English) has: `English | [中文](README.zh.md)`
- Bad (copying source unchanged): `English | [中文](README.zh.md)`
- Good (flipped for Chinese file): `[English](README.md) | 中文`
### Language switcher — English to Chinese
- Source: `English | [中文](README.zh.md)`
- Bad: `English | [中文](README.zh.md)`
- Good: `[English](README.md) | 中文`
### Language switcher — Chinese to English
- Source: `[English](README.md) | 中文`
- Bad: `[English](README.md) | 中文`
- Good: `English | [中文](README.zh.md)`
---
+2 -2
View File
@@ -2,5 +2,5 @@
# side as of the last confirmed-consistent state. Both languages carry equal authority;
# after editing either side, bring the other along and re-record with:
# pnpm run verify-translation-pairing --write
translation-rules.md: 3e99aa5432ccd904f702238e9b802a9ed9bf6832
translation-rules.zh.md: 1d8dcb04f1015af2db46c2e25b44f5cd147b6ecc
translation-rules.md: fb6aa9ac05bebe68ff9213af99f64457bdb1ad6f
translation-rules.zh.md: 04dd0a704e19502c676ea0966437870c5af0624f
+13 -12
View File
@@ -6,27 +6,27 @@ How to translate between the two sides of a documentation pair in this repo. Bot
## Faithfulness
- The counterpart MUST say what the authored side says — no added behavior, prerequisites, warnings, version claims, or examples, and no dropped ones. If the pair disagrees on substance, neither language wins by default: fix the side that is wrong, then bring the other along in the same change.
- The counterpart SHOULD read as natural technical writing in its own language, not word-by-word gloss. Translate meaning, restructure sentences where the target grammar wants it, and keep the author's register — terse stays terse.
- The counterpart *MUST* say what the authored side says — no added behavior, prerequisites, warnings, version claims, or examples, and no dropped ones. If the pair disagrees on substance, neither language wins by default: fix the side that is wrong, then bring the other along in the same change.
- The counterpart *SHOULD* read as natural technical writing in its own language, not word-by-word gloss. Translate meaning, restructure sentences where the target grammar wants it, and keep the author's register — terse stays terse.
- Do not translate the untranslatable: if a sentence resists natural rendering because it leans on an idiom of the source language, translate the idea, not the idiom.
## Voice
- The register is calibrated by [style-samples.md](style-samples.md) — human-approved gold pairs, one per document genre. The counterpart MUST match the register of the nearest sample; where a sample and a prose rule here disagree, the sample wins. The target is institutional technical Chinese: complete sentences, declarative, neither chatty nor academic.
- The register is calibrated by [style-samples.md](style-samples.md) — human-approved gold pairs, one per document genre. The counterpart MUST match the target-language side of the nearest sample; where its voice and a prose voice rule disagree, the sample wins. Chinese targets use institutional technical Chinese; English targets use concise professional developer prose.
- Write as a native technical author restating the content, not as a translator transposing sentences. Then verify against the source clause by clause: nothing added, nothing dropped — fluency never justifies losing a clause.
- Give sentences an explicit agent: where the English uses a passive or an abstract subject, name the actor (系统、门禁、评审人).
- Prefer established Chinese engineering idiom over calques (误报/漏检 for false positive/negative, 执行红线 for enforcement frontier); localize metaphors instead of transplanting them, and unpack English noun chains into verb clauses.
- Give sentences an explicit actor when the target language would otherwise obscure it; for Chinese, replace vague passives or abstract subjects with the actual actor (系统、门禁、评审人).
- Prefer established target-language engineering idiom over calques (误报/漏检 for false positive/negative, 执行红线 for enforcement frontier); localize metaphors instead of transplanting them, and unpack noun chains where the target language requires it.
- Split long paragraphs by semantic unit — one idea per paragraph. Paragraph boundaries MAY differ from the source; the structural signature does not count paragraphs.
- Category nouns render in Chinese with a first-mention English annotation (实操手册(cookbook); literal directory or file references stay code-formatted English.
- When translating into Chinese, category nouns use Chinese with a first-mention English annotation (实操手册(cookbook); when translating into English, use the conventional English category name. Literal directory or file references stay code-formatted English.
## Structure preservation
Shape is enforced by the pairing gate, so the writer never trades fluency against it — write naturally inside the frame. The paired files MUST match one to one in:
The pairing gate checks heading depths, fenced code blocks, table row and column counts, list kinds, ordered-list starts, list item counts, and link targets. Preserve the rest of the frame manually; the paired files MUST match one to one in:
- heading hierarchy (same levels, same order — heading TEXT is translated),
- list shape and numbering,
- tables (same columns, same row order; header cells translated per terminology),
- fenced code blocks — **byte-identical, including comments**; code is part of the verified surface (` ```ts ` blocks compile under `doc-typecheck`), and an edited comment is drift the fence-count gate cannot see,
- fenced code blocks — **byte-identical, including comments**; the pairing signature compares their info strings and contents, and ` ```ts ` blocks compile under `doc-typecheck`,
- inline code spans (commands, flags, config keys, file paths, event names, API names, version numbers) — verbatim, never translated or reformatted,
- links and anchors: every relative link MUST point at the same target in both files — by convention the `.md` path, not the `.zh.md` sibling — so links never dangle when one pair lands before its neighbors. The ONLY zh-specific link is the language switcher. Link TEXT is translated; the target is not.
@@ -34,9 +34,9 @@ The repo's Markdown conventions apply to `.zh.md` files unchanged: one physical
## Terminology
- [terminology.md](terminology.md) is the source of truth in both directions. Before translating, load it; while translating, every term it lists MUST be rendered exactly as it specifies, including its first-occurrence annotations (e.g. `agent(智能体)` on first mention, plain `agent` after) and its "不要译作" prohibitions. When the Chinese side is authored first, the English counterpart uses the table's English column the same way.
- A technical term NOT in the table MAY be translated only when a major Chinese-language OSS or vendor doc has an established rendering for it (K8s/Vue/MDN Chinese docs, 微软简中风格指南, big-tech project docs). Cite the precedent in the PR.
- A term with NO established precedent MUST stay in English in the translation and MUST be listed in the PR description under 「待定术语」(pending terms) with a suggested rendering for the reviewer to decide. MUST NOT invent a Chinese rendering inline — an unprecedented translation creates exactly the ambiguity the terminology table exists to prevent. Decided terms then land in [terminology.md](terminology.md) in the same PR or a follow-up.
- [terminology.md](terminology.md) is the source of truth in both directions. Before translating, load it; every listed term MUST follow its row and its "不要译作" prohibitions. A Chinese target uses the "中文" column and its "首次出现" annotation; an English target uses the "English" column without adding a Chinese gloss.
- For a Chinese target, an unlisted technical term MAY use an established rendering from a major Chinese-language OSS or vendor source (K8s/Vue/MDN Chinese docs, 微软简中风格指南, big-tech project docs), cited in the PR. Without such precedent it MUST stay in English and be listed under 「待定术语」(pending terms) with a suggested rendering.
- For an English target, use the established English technical term. If the source term has no unambiguous established equivalent, preserve it with a short explanatory gloss and list it under pending terms. Neither direction may invent a rendering inline; a decided term enters [terminology.md](terminology.md) in the same PR or a follow-up.
## Typography
@@ -44,6 +44,7 @@ These rules govern the Chinese side; the English side follows the repo's normal
- MUST put one half-width space between Chinese text and Latin words, and between Chinese text and numerals: `每个 plugin 注册 3 个 tool`。No space between a full-width punctuation mark and anything.
- MUST use full-width (Chinese) punctuation in Chinese prose: `,。:;?!()「」`. Half-width punctuation stays inside code spans, inside complete English sentences quoted as-is, and in numbers (`3.5`, `1,024`).
- Chinese prose *SHOULD* prefer colons, periods, commas, or parentheses over em dashes. Keep an em dash only when no other punctuation preserves the sentence naturally.
- Enumeration commas: a Chinese list of parallel items uses 顿号(、), not commas.
- MUST NOT use full-width digits or full-width Latin letters — `123` never, `123` always.
- Proper nouns keep their canonical casing: GitHub, TypeScript, DeepSeek — never `github`/`Github` unless quoting code.
@@ -54,7 +55,7 @@ These rules govern the Chinese side; the English side follows the repo's normal
- A pair is done when a bilingual engineer reading either file alone gets everything a reader of the other gets — same facts, same caveats, same tone — and nothing extra.
- Before handing off, self-check the result against this file and re-read the counterpart ALONE, without the source side by side; awkward phrasing is easier to hear without the source anchoring you.
- The mechanical contract (consistency record, switcher, structure, wrap, links) is checked by `pnpm run verify-translation-pairing` and the rest of `doc-sync` — run them; do not hand-verify what a gate covers.
- Run `pnpm run verify-translation-pairing` and the rest of `doc-sync` for records, switchers, heading depths, code blocks, table row and column counts, list kinds, ordered-list starts, list item counts, links, and repository Markdown rules. Manually verify list and table order, noncanonical list numbering, inline code, emphasis, meaning, terminology, and tone.
## References
+14 -13
View File
@@ -12,21 +12,21 @@
## 行文
- 语体以 [style-samples.md](style-samples.md) 为校准锚点——人工定稿的金标样例按文体各一组,译文必须对齐最接近的样例语体;样例与本文条款冲突,以样例为准。目标语体是规范的技术制度文:完整主谓、确定语气,不口语化也不学术腔
- 语体以 [style-samples.md](style-samples.md) 为校准锚点人工定稿的金标样例按文体各一组,译文必须参照文体最接近的样例,采用其中目标语言一侧的语体;如果样例与本文的行文规则冲突,以样例为准。译成中文时,采用规范的技术制度文;译成英文时,采用简洁、专业的开发者文档语体
- 以母语技术作者的身份重述内容,而不是逐句转写的译者。写完后逐句对照原文核验:不添加、不遗漏——流畅永远不是丢掉语义成分的理由。
- 给句子补显式执行主体:英文的被动句和抽象主语,中文写成「系统、门禁、评审人」等实际执行者主语。
- 优先使用中文工程惯用语而非直译(false positive/negative→误报/漏检、enforcement frontier→执行红线);隐喻做本地化替换而不是移植,英文名词链展开为动词句
- 如果直译会让执行主体含糊,请明确写出实际执行者;译成中文时,应由「系统、门禁、评审人」等实际执行者作主语,避免含糊的被动句或抽象主语。
- 优先采用目标语言中通行的工程表达,避免生硬直译(false positive/negative→误报/漏检、enforcement frontier→执行红线);隐喻应自然改写,名词链则按目标语言的习惯拆开
- 长段按语义单元拆分,一段一件事。段落边界可以与原文不同;结构签名不比对段落数。
- 类别名词中文并在首现括注英文(实操手册(cookbook));指目录或文件本身时保留代码体英文。
- 翻译为中文时,类别名词使用中文并在首现括注英文(实操手册(cookbook));翻译为英文时,使用通行的英文类别名。指目录或文件本身时保留代码体英文。
## 结构保持
结构一致性由配对门禁负责校验,译者无需为保持结构而牺牲行文的流畅:在既定框架内自然书写即可。配对的两个文件必须在以下方面一一对应:
配对门禁会检查标题深度、围栏代码块、表格行列数、列表类型、有序列表起始编号、列表项数量与链接目标;门禁未覆盖的结构仍需人工核对。两个配对文件必须在以下方面一一对应:
- 标题层级(相同级别、相同顺序;标题的**文字**要翻译);
- 列表形态与编号;
- 表格(相同的列、相同的行序;表头单元格按术语表翻译);
- 围栏代码块:**逐字节一致,包括注释**。代码属于受验证的范围(` ```ts ` 块要通过 `doc-typecheck` 编译),而被改动的注释是代码块计数门禁看不见的漂移
- 围栏代码块:**逐字节一致,包括注释**。配对签名比对信息字符串与内容,` ```ts `要通过 `doc-typecheck` 编译;
- 行内代码(命令、flag、配置键、文件路径、事件名、API 名、版本号):原样保留,从不翻译或重排;
- 链接与锚点:每个相对链接在两个文件中必须指向相同的目标(按约定是 `.md` 路径而非 `.zh.md` 兄弟文件),这样即使某对文档先于相邻文件落地,链接也不会悬空。唯一的 zh 特有链接是语言切换行。链接**文字**翻译;链接目标不翻。
@@ -34,18 +34,19 @@
## 术语
- [terminology.md](terminology.md) 是双向的术语真源。翻译前请先加载它;翻译过程中,表内的每个术语都*必须*严格按表中规定的译法呈现,包括首次出现的括注(如首现写 `agent(智能体)`,之后写 `agent`与「不要译作」禁项。中文先行撰写时,英文对侧同样按表中英文列使用术语
- 表中**没有**的技术术语只有当某个主要中文 OSS 或厂商文档已有成型译法时(K8s/Vue/MDN 中文文档、微软简中风格指南、大厂项目文档)才*可以*翻译。请在 PR 中注明先例出处
- **没有**成型先例的术语,译文中*必须*保留英文,并且*必须*在 PR 描述的「待定术语」下列出,附上建议译法交评审者定夺。*禁止*就地发明中文译法,因为无先例的翻译恰恰会制造术语表要防止的歧义。确定下来的术语随后在同一个 PR 或后续 PR 中入 [terminology.md](terminology.md)。
- [terminology.md](terminology.md) 是双向的术语真源。翻译前请先加载它;表内术语必须遵守对应行与「不要译作」禁项。译成中文时,采用「中文」列,并按「首次出现」列括注;译成英文时,采用「English」列,不加中文括注
- 译成中文时,术语表未收录的技术术语只有在主流中文 OSS 文档或厂商资料中已有通行译法时才可以翻译(K8s/Vue/MDN 中文文档、微软简中风格指南、大厂项目文档),并须在 PR 中注明出处;否则必须保留英文,并在 PR 描述的「待定术语」中给出建议译法
- 译成英文时,采用通行的英文技术术语。如果源术语没有明确的通行对应词,则保留原词、附上简短说明,并列入「待定术语」。两个方向都不得自行创造译法;确定后的术语在同一个 PR 或后续 PR 中入 [terminology.md](terminology.md)。
## 排版
本节规则约束中文一侧;英文一侧遵循仓库常规的 Markdown 约定(根 `AGENTS.md`)。以下中西文混排规则遵循 [MDN 简体中文翻译指南](https://github.com/mdn/translated-content/blob/main/docs/zh-cn/translation-guide.md)、[Kubernetes 中文本地化指南](https://kubernetes.io/zh-cn/docs/contribute/localization_zh/)、[Vue.js 中文翻译须知](https://github.com/vuejs-translations/docs-zh-cn/wiki/%E7%BF%BB%E8%AF%91%E9%A1%BB%E7%9F%A5) 与[中文文案排版指北](https://github.com/sparanoid/chinese-copywriting-guidelines)的跨项目共识,其根据是 [W3C clreq](https://www.w3.org/TR/clreq/) 与 GB/T 15834—2011
- *必须*在中文与拉丁词之间、中文与数字之间各留一个半角空格:`每个 plugin 注册 3 个 tool`。全角标点与任何字符之间不加空格。
- 中文行文*必须*使用全角(中文)标点:`,。:;?!()「」`。半角标点保留在代码内、按原样引用的完整英文句子内、以及数字内(`3.5``1,024`)。
- 必须在中文与拉丁词之间、中文与数字之间各留一个半角空格:`每个 plugin 注册 3 个 tool`。全角标点与任何字符之间不加空格。
- 中文行文必须使用全角(中文)标点:`,。:;?!()「」`。半角标点保留在代码内、按原样引用的完整英文句子内、以及数字内(`3.5``1,024`)。
- 中文行文*应当*优先使用冒号、句号、逗号或括号,尽量不用破折号;只有其他标点都无法自然表达时才保留破折号。
- 顿号:中文的并列项之间使用顿号(、),而非逗号。
- *禁止*使用全角数字或全角拉丁字母:永远不写 `123`,永远写 `123`
- 禁止使用全角数字或全角拉丁字母:永远不写 `123`,永远写 `123`
- 专有名词保持规范大小写:GitHub、TypeScript、DeepSeek。除非引用代码,否则绝不写 `github``Github`
- 第二人称用「你」,不用「您」(与 Vue、Kubernetes 中文约定及本仓库的直接语气一致)。
- 强调标记(`**加粗**``*斜体*`)落在与对侧相同的文字段上。中文没有斜体,渲染效果可能看不出差别,不要用引号或其他装饰替代。
@@ -54,7 +55,7 @@
- 一对文档的完成标准:一位双语工程师只读其中任一文件,能获得与另一文件读者完全相同的信息(相同的事实、相同的告诫、相同的语气),并且没有任何多余的内容。
- 交付前,请对照本文自查一遍,并**单独通读对侧文件**,不与源侧对照;不对照原文时,更容易察觉别扭的表达。
- 机械契约(一致性记录、切换行、结构、折行、链接)由 `pnpm run verify-translation-pairing` `doc-sync` 的其余门禁检查。请运行门禁;门禁已覆盖的内容无需手工核对。
- 请运行 `pnpm run verify-translation-pairing` `doc-sync` 的其余门禁。这些门禁会检查一致性记录、切换行、标题深度、代码块、表格行列数、列表类型、有序列表起始编号、列表项数量、链接及仓库 Markdown 规则;列表与表格的顺序、非常规列表编号、行内代码、强调标记、语义、术语和语体仍需人工核对。
## 参考资料
+72 -39
View File
@@ -18,7 +18,6 @@ flowchart TD
end
subgraph group_core["packages/core"]
pkg_agent["agent"]
pkg_agent_core["agent-core"]
pkg_agent_loop["agent-loop"]
pkg_scope["scope"]
pkg_session["session"]
@@ -94,13 +93,10 @@ flowchart TD
end
subgraph group_ui["packages/ui"]
pkg_acp["acp"]
pkg_acp_agent["acp-agent"]
pkg_app_boot["app-boot"]
pkg_jsonrpc["jsonrpc"]
pkg_jsonrpc_agent["jsonrpc-agent"]
pkg_permission["permission"]
pkg_stdio["stdio"]
pkg_stdio_agent["stdio-agent"]
pkg_tool_ask_user["tool-ask-user"]
pkg_user_approval["user-approval"]
pkg_user_interaction["user-interaction"]
@@ -112,13 +108,30 @@ flowchart TD
subgraph group_context["packages/context"]
pkg_time_context["time-context"]
end
subgraph group_examples["packages/examples"]
pkg_acp_demo["acp-demo"]
pkg_agent_spine_demo["agent-spine-demo"]
pkg_jsonrpc_demo["jsonrpc-demo"]
pkg_stdio_demo["stdio-demo"]
end
subgraph group_guard["packages/guard"]
pkg_repeat_tool_guard["repeat-tool-guard"]
end
subgraph group_mcp["packages/mcp"]
pkg_mcp_client["mcp-client"]
end
subgraph group_sandbox["packages/sandbox"]
pkg_sandbox["sandbox"]
pkg_sandbox_local["sandbox-local"]
end
subgraph group_sdk["packages/sdk"]
pkg_helper["helper"]
pkg_scripts["scripts"]
end
subgraph group_tasks["packages/tasks"]
pkg_tasks["tasks"]
pkg_tool_tasks["tool-tasks"]
end
subgraph group_workflow["packages/workflow"]
pkg_tool_workflow["tool-workflow"]
pkg_workflow["workflow"]
@@ -126,6 +139,8 @@ flowchart TD
end
pkg_llm --> pkg_brand
pkg_code_runtime_worker --> pkg_code_runtime
pkg_helper --> pkg_brand
pkg_scripts --> pkg_app_boot
pkg_llm_deepseek --> pkg_llm
pkg_llm_pi_ai --> pkg_llm
pkg_session --> pkg_brand
@@ -142,7 +157,6 @@ flowchart TD
pkg_agent --> pkg_scope
pkg_agent --> pkg_session
pkg_agent --> pkg_system_prompt
pkg_bash --> pkg_brand
pkg_bash --> pkg_sandbox
pkg_bash --> pkg_session
pkg_fs_local --> pkg_fs
@@ -190,6 +204,10 @@ flowchart TD
pkg_user_interaction --> pkg_llm
pkg_time_context --> pkg_agent
pkg_time_context --> pkg_system_prompt
pkg_tasks --> pkg_agent
pkg_tasks --> pkg_brand
pkg_tasks --> pkg_session
pkg_tasks --> pkg_timeout
pkg_workflow --> pkg_agent
pkg_workflow --> pkg_brand
pkg_workflow --> pkg_llm
@@ -223,6 +241,7 @@ flowchart TD
pkg_tool_bash --> pkg_llm
pkg_tool_bash --> pkg_sandbox
pkg_tool_bash --> pkg_system_prompt
pkg_tool_bash --> pkg_tasks
pkg_tool_bash --> pkg_tools
pkg_tool_bash --> pkg_user_approval
pkg_tool_fs --> pkg_fs
@@ -270,22 +289,17 @@ flowchart TD
pkg_tool_ask_user --> pkg_user_interaction
pkg_repeat_tool_guard --> pkg_agent
pkg_repeat_tool_guard --> pkg_tools
pkg_mcp_client --> pkg_llm
pkg_mcp_client --> pkg_tools
pkg_tool_tasks --> pkg_agent
pkg_tool_tasks --> pkg_system_prompt
pkg_tool_tasks --> pkg_tasks
pkg_tool_tasks --> pkg_tools
pkg_tool_workflow --> pkg_agent
pkg_tool_workflow --> pkg_llm
pkg_tool_workflow --> pkg_system_prompt
pkg_tool_workflow --> pkg_tools
pkg_tool_workflow --> pkg_workflow
pkg_agent_core --> pkg_agent
pkg_agent_core --> pkg_agent_loop
pkg_agent_core --> pkg_invariants
pkg_agent_core --> pkg_llm
pkg_agent_core --> pkg_session
pkg_agent_core --> pkg_skill
pkg_agent_core --> pkg_skill_local
pkg_agent_core --> pkg_system_prompt
pkg_agent_core --> pkg_tool_bash
pkg_agent_core --> pkg_tool_skill
pkg_agent_core --> pkg_tools
pkg_subagent_acp --> pkg_agent
pkg_subagent_acp --> pkg_llm
pkg_subagent_acp --> pkg_subagent
@@ -299,6 +313,7 @@ flowchart TD
pkg_tool_subagent --> pkg_agent
pkg_tool_subagent --> pkg_llm
pkg_tool_subagent --> pkg_subagent
pkg_tool_subagent --> pkg_tasks
pkg_tool_subagent --> pkg_tools
pkg_hooks_claude --> pkg_agent
pkg_hooks_claude --> pkg_hook_protocol
@@ -314,6 +329,19 @@ flowchart TD
pkg_jsonrpc --> pkg_llm_deepseek
pkg_jsonrpc --> pkg_session
pkg_jsonrpc --> pkg_subagent
pkg_agent_spine_demo --> pkg_agent
pkg_agent_spine_demo --> pkg_agent_loop
pkg_agent_spine_demo --> pkg_invariants
pkg_agent_spine_demo --> pkg_llm
pkg_agent_spine_demo --> pkg_session
pkg_agent_spine_demo --> pkg_skill
pkg_agent_spine_demo --> pkg_skill_local
pkg_agent_spine_demo --> pkg_system_prompt
pkg_agent_spine_demo --> pkg_tasks
pkg_agent_spine_demo --> pkg_tool_bash
pkg_agent_spine_demo --> pkg_tool_skill
pkg_agent_spine_demo --> pkg_tool_tasks
pkg_agent_spine_demo --> pkg_tools
pkg_workflow_workerthread --> pkg_agent
pkg_workflow_workerthread --> pkg_brand
pkg_workflow_workerthread --> pkg_llm
@@ -327,22 +355,22 @@ flowchart TD
pkg_subagent_fork --> pkg_subagent_inprocess
pkg_subagent_spawn --> pkg_subagent
pkg_subagent_spawn --> pkg_subagent_inprocess
pkg_acp_agent --> pkg_acp
pkg_acp_agent --> pkg_agent_core
pkg_acp_agent --> pkg_app_boot
pkg_acp_agent --> pkg_session_persistence_jsonl
pkg_acp_agent --> pkg_tools
pkg_acp_agent --> pkg_user_interaction
pkg_stdio_agent --> pkg_agent
pkg_stdio_agent --> pkg_agent_core
pkg_stdio_agent --> pkg_app_boot
pkg_stdio_agent --> pkg_llm
pkg_stdio_agent --> pkg_session
pkg_stdio_agent --> pkg_session_persistence_jsonl
pkg_stdio_agent --> pkg_stdio
pkg_stdio_agent --> pkg_tool_ask_user
pkg_stdio_agent --> pkg_tools
pkg_stdio_agent --> pkg_user_interaction
pkg_acp_demo --> pkg_acp
pkg_acp_demo --> pkg_agent_spine_demo
pkg_acp_demo --> pkg_app_boot
pkg_acp_demo --> pkg_session_persistence_jsonl
pkg_acp_demo --> pkg_tools
pkg_acp_demo --> pkg_user_interaction
pkg_stdio_demo --> pkg_agent
pkg_stdio_demo --> pkg_agent_spine_demo
pkg_stdio_demo --> pkg_app_boot
pkg_stdio_demo --> pkg_llm
pkg_stdio_demo --> pkg_session
pkg_stdio_demo --> pkg_session_persistence_jsonl
pkg_stdio_demo --> pkg_stdio
pkg_stdio_demo --> pkg_tool_ask_user
pkg_stdio_demo --> pkg_tools
pkg_stdio_demo --> pkg_user_interaction
```
| Package | Group | Depends on |
@@ -355,10 +383,12 @@ flowchart TD
| [`acp-snapshot`](../packages/support/acp-snapshot) | `support` | — |
| [`loader-smoke`](../packages/support/loader-smoke) | `support` | — |
| [`app-boot`](../packages/ui/app-boot) | `ui` | — |
| [`jsonrpc-agent`](../packages/ui/jsonrpc-agent) | `ui` | — |
| [`code-runtime`](../packages/code-runtime/code-runtime) | `code-runtime` | — |
| [`jsonrpc-demo`](../packages/examples/jsonrpc-demo) | `examples` | — |
| [`llm`](../packages/llm/llm) | `llm` | [`brand`](../packages/util/brand) |
| [`code-runtime-worker`](../packages/code-runtime/code-runtime-worker) | `code-runtime` | [`code-runtime`](../packages/code-runtime/code-runtime) |
| [`helper`](../packages/sdk/helper) | `sdk` | [`brand`](../packages/util/brand) |
| [`scripts`](../packages/sdk/scripts) | `sdk` | [`app-boot`](../packages/ui/app-boot) |
| [`llm-deepseek`](../packages/llm/llm-deepseek) | `llm` | [`llm`](../packages/llm/llm) |
| [`llm-pi-ai`](../packages/llm/llm-pi-ai) | `llm` | [`llm`](../packages/llm/llm) |
| [`session`](../packages/core/session) | `core` | [`brand`](../packages/util/brand), [`llm`](../packages/llm/llm), [`scope`](../packages/core/scope) |
@@ -367,7 +397,7 @@ flowchart TD
| [`web`](../packages/web/web) | `web` | [`llm`](../packages/llm/llm) |
| [`sandbox`](../packages/sandbox/sandbox) | `sandbox` | [`llm`](../packages/llm/llm) |
| [`agent`](../packages/core/agent) | `core` | [`brand`](../packages/util/brand), [`llm`](../packages/llm/llm), [`scope`](../packages/core/scope), [`session`](../packages/core/session), [`system-prompt`](../packages/core/system-prompt) |
| [`bash`](../packages/bash/bash) | `bash` | [`brand`](../packages/util/brand), [`sandbox`](../packages/sandbox/sandbox), [`session`](../packages/core/session) |
| [`bash`](../packages/bash/bash) | `bash` | [`sandbox`](../packages/sandbox/sandbox), [`session`](../packages/core/session) |
| [`fs-local`](../packages/fs/fs-local) | `fs` | [`fs`](../packages/fs/fs) |
| [`fs-policy`](../packages/fs/fs-policy) | `fs` | [`fs`](../packages/fs/fs) |
| [`skill-local`](../packages/skill/skill-local) | `skill` | [`fs`](../packages/fs/fs), [`skill`](../packages/skill/skill) |
@@ -389,13 +419,14 @@ flowchart TD
| [`user-approval`](../packages/ui/user-approval) | `ui` | [`agent`](../packages/core/agent), [`brand`](../packages/util/brand), [`llm`](../packages/llm/llm), [`scope`](../packages/core/scope), [`session`](../packages/core/session), [`system-prompt`](../packages/core/system-prompt) |
| [`user-interaction`](../packages/ui/user-interaction) | `ui` | [`agent`](../packages/core/agent), [`llm`](../packages/llm/llm) |
| [`time-context`](../packages/context/time-context) | `context` | [`agent`](../packages/core/agent), [`system-prompt`](../packages/core/system-prompt) |
| [`tasks`](../packages/tasks/tasks) | `tasks` | [`agent`](../packages/core/agent), [`brand`](../packages/util/brand), [`session`](../packages/core/session), [`timeout`](../packages/util/timeout) |
| [`workflow`](../packages/workflow/workflow) | `workflow` | [`agent`](../packages/core/agent), [`brand`](../packages/util/brand), [`llm`](../packages/llm/llm) |
| [`tools`](../packages/core/tools) | `core` | [`agent`](../packages/core/agent), [`code-runtime`](../packages/code-runtime/code-runtime), [`llm`](../packages/llm/llm), [`scope`](../packages/core/scope), [`session`](../packages/core/session), [`system-prompt`](../packages/core/system-prompt), [`user-approval`](../packages/ui/user-approval) |
| [`bash-sandbox`](../packages/bash/bash-sandbox) | `bash` | [`bash`](../packages/bash/bash), [`bash-local`](../packages/bash/bash-local), [`sandbox`](../packages/sandbox/sandbox) |
| [`permission`](../packages/ui/permission) | `ui` | [`bash`](../packages/bash/bash), [`sandbox`](../packages/sandbox/sandbox), [`session`](../packages/core/session), [`user-approval`](../packages/ui/user-approval) |
| [`stdio`](../packages/ui/stdio) | `ui` | [`agent`](../packages/core/agent), [`llm`](../packages/llm/llm), [`session`](../packages/core/session), [`user-interaction`](../packages/ui/user-interaction) |
| [`agent-loop`](../packages/core/agent-loop) | `core` | [`agent`](../packages/core/agent), [`llm`](../packages/llm/llm), [`scope`](../packages/core/scope), [`session`](../packages/core/session), [`session-persistence`](../packages/session-persistence/session-persistence), [`system-prompt`](../packages/core/system-prompt), [`tools`](../packages/core/tools) |
| [`tool-bash`](../packages/bash/tool-bash) | `bash` | [`agent`](../packages/core/agent), [`bash`](../packages/bash/bash), [`llm`](../packages/llm/llm), [`sandbox`](../packages/sandbox/sandbox), [`system-prompt`](../packages/core/system-prompt), [`tools`](../packages/core/tools), [`user-approval`](../packages/ui/user-approval) |
| [`tool-bash`](../packages/bash/tool-bash) | `bash` | [`agent`](../packages/core/agent), [`bash`](../packages/bash/bash), [`llm`](../packages/llm/llm), [`sandbox`](../packages/sandbox/sandbox), [`system-prompt`](../packages/core/system-prompt), [`tasks`](../packages/tasks/tasks), [`tools`](../packages/core/tools), [`user-approval`](../packages/ui/user-approval) |
| [`tool-fs`](../packages/fs/tool-fs) | `fs` | [`fs`](../packages/fs/fs), [`llm`](../packages/llm/llm), [`session`](../packages/core/session), [`system-prompt`](../packages/core/system-prompt), [`tools`](../packages/core/tools) |
| [`tool-skill`](../packages/skill/tool-skill) | `skill` | [`agent`](../packages/core/agent), [`llm`](../packages/llm/llm), [`skill`](../packages/skill/skill), [`tools`](../packages/core/tools) |
| [`subagent`](../packages/subagent/subagent) | `subagent` | [`agent`](../packages/core/agent), [`llm`](../packages/llm/llm), [`scope`](../packages/core/scope), [`tools`](../packages/core/tools) |
@@ -407,16 +438,18 @@ flowchart TD
| [`acp`](../packages/ui/acp) | `ui` | [`agent`](../packages/core/agent), [`bash`](../packages/bash/bash), [`llm`](../packages/llm/llm), [`permission`](../packages/ui/permission), [`sandbox`](../packages/sandbox/sandbox), [`session`](../packages/core/session), [`session-persistence`](../packages/session-persistence/session-persistence), [`tools`](../packages/core/tools), [`user-approval`](../packages/ui/user-approval), [`user-interaction`](../packages/ui/user-interaction) |
| [`tool-ask-user`](../packages/ui/tool-ask-user) | `ui` | [`agent`](../packages/core/agent), [`tools`](../packages/core/tools), [`user-interaction`](../packages/ui/user-interaction) |
| [`repeat-tool-guard`](../packages/guard/repeat-tool-guard) | `guard` | [`agent`](../packages/core/agent), [`tools`](../packages/core/tools) |
| [`mcp-client`](../packages/mcp/mcp-client) | `mcp` | [`llm`](../packages/llm/llm), [`tools`](../packages/core/tools) |
| [`tool-tasks`](../packages/tasks/tool-tasks) | `tasks` | [`agent`](../packages/core/agent), [`system-prompt`](../packages/core/system-prompt), [`tasks`](../packages/tasks/tasks), [`tools`](../packages/core/tools) |
| [`tool-workflow`](../packages/workflow/tool-workflow) | `workflow` | [`agent`](../packages/core/agent), [`llm`](../packages/llm/llm), [`system-prompt`](../packages/core/system-prompt), [`tools`](../packages/core/tools), [`workflow`](../packages/workflow/workflow) |
| [`agent-core`](../packages/core/agent-core) | `core` | [`agent`](../packages/core/agent), [`agent-loop`](../packages/core/agent-loop), [`invariants`](../packages/support/invariants), [`llm`](../packages/llm/llm), [`session`](../packages/core/session), [`skill`](../packages/skill/skill), [`skill-local`](../packages/skill/skill-local), [`system-prompt`](../packages/core/system-prompt), [`tool-bash`](../packages/bash/tool-bash), [`tool-skill`](../packages/skill/tool-skill), [`tools`](../packages/core/tools) |
| [`subagent-acp`](../packages/subagent/subagent-acp) | `subagent` | [`agent`](../packages/core/agent), [`llm`](../packages/llm/llm), [`subagent`](../packages/subagent/subagent), [`subagent-subprocess`](../packages/subagent/subagent-subprocess) |
| [`subagent-inprocess`](../packages/subagent/subagent-inprocess) | `subagent` | [`agent`](../packages/core/agent), [`llm`](../packages/llm/llm), [`session`](../packages/core/session), [`subagent`](../packages/subagent/subagent), [`system-prompt`](../packages/core/system-prompt), [`tools`](../packages/core/tools) |
| [`tool-subagent`](../packages/subagent/tool-subagent) | `subagent` | [`agent`](../packages/core/agent), [`llm`](../packages/llm/llm), [`subagent`](../packages/subagent/subagent), [`tools`](../packages/core/tools) |
| [`tool-subagent`](../packages/subagent/tool-subagent) | `subagent` | [`agent`](../packages/core/agent), [`llm`](../packages/llm/llm), [`subagent`](../packages/subagent/subagent), [`tasks`](../packages/tasks/tasks), [`tools`](../packages/core/tools) |
| [`hooks-claude`](../packages/hooks/hooks-claude) | `hooks` | [`agent`](../packages/core/agent), [`hook-protocol`](../packages/hooks/hook-protocol), [`llm`](../packages/llm/llm), [`session`](../packages/core/session), [`subagent`](../packages/subagent/subagent), [`tools`](../packages/core/tools) |
| [`subagent-mock`](../packages/support/subagent-mock) | `support` | [`agent`](../packages/core/agent), [`llm`](../packages/llm/llm), [`subagent`](../packages/subagent/subagent) |
| [`jsonrpc`](../packages/ui/jsonrpc) | `ui` | [`agent`](../packages/core/agent), [`llm`](../packages/llm/llm), [`llm-deepseek`](../packages/llm/llm-deepseek), [`session`](../packages/core/session), [`subagent`](../packages/subagent/subagent) |
| [`agent-spine-demo`](../packages/examples/agent-spine-demo) | `examples` | [`agent`](../packages/core/agent), [`agent-loop`](../packages/core/agent-loop), [`invariants`](../packages/support/invariants), [`llm`](../packages/llm/llm), [`session`](../packages/core/session), [`skill`](../packages/skill/skill), [`skill-local`](../packages/skill/skill-local), [`system-prompt`](../packages/core/system-prompt), [`tasks`](../packages/tasks/tasks), [`tool-bash`](../packages/bash/tool-bash), [`tool-skill`](../packages/skill/tool-skill), [`tool-tasks`](../packages/tasks/tool-tasks), [`tools`](../packages/core/tools) |
| [`workflow-workerthread`](../packages/workflow/workflow-workerthread) | `workflow` | [`agent`](../packages/core/agent), [`brand`](../packages/util/brand), [`llm`](../packages/llm/llm), [`session`](../packages/core/session), [`subagent`](../packages/subagent/subagent), [`tools`](../packages/core/tools), [`workflow`](../packages/workflow/workflow) |
| [`subagent-fork`](../packages/subagent/subagent-fork) | `subagent` | [`agent`](../packages/core/agent), [`session`](../packages/core/session), [`subagent`](../packages/subagent/subagent), [`subagent-inprocess`](../packages/subagent/subagent-inprocess) |
| [`subagent-spawn`](../packages/subagent/subagent-spawn) | `subagent` | [`subagent`](../packages/subagent/subagent), [`subagent-inprocess`](../packages/subagent/subagent-inprocess) |
| [`acp-agent`](../packages/ui/acp-agent) | `ui` | [`acp`](../packages/ui/acp), [`agent-core`](../packages/core/agent-core), [`app-boot`](../packages/ui/app-boot), [`session-persistence-jsonl`](../packages/session-persistence/session-persistence-jsonl), [`tools`](../packages/core/tools), [`user-interaction`](../packages/ui/user-interaction) |
| [`stdio-agent`](../packages/ui/stdio-agent) | `ui` | [`agent`](../packages/core/agent), [`agent-core`](../packages/core/agent-core), [`app-boot`](../packages/ui/app-boot), [`llm`](../packages/llm/llm), [`session`](../packages/core/session), [`session-persistence-jsonl`](../packages/session-persistence/session-persistence-jsonl), [`stdio`](../packages/ui/stdio), [`tool-ask-user`](../packages/ui/tool-ask-user), [`tools`](../packages/core/tools), [`user-interaction`](../packages/ui/user-interaction) |
| [`acp-demo`](../packages/examples/acp-demo) | `examples` | [`acp`](../packages/ui/acp), [`agent-spine-demo`](../packages/examples/agent-spine-demo), [`app-boot`](../packages/ui/app-boot), [`session-persistence-jsonl`](../packages/session-persistence/session-persistence-jsonl), [`tools`](../packages/core/tools), [`user-interaction`](../packages/ui/user-interaction) |
| [`stdio-demo`](../packages/examples/stdio-demo) | `examples` | [`agent`](../packages/core/agent), [`agent-spine-demo`](../packages/examples/agent-spine-demo), [`app-boot`](../packages/ui/app-boot), [`llm`](../packages/llm/llm), [`session`](../packages/core/session), [`session-persistence-jsonl`](../packages/session-persistence/session-persistence-jsonl), [`stdio`](../packages/ui/stdio), [`tool-ask-user`](../packages/ui/tool-ask-user), [`tools`](../packages/core/tools), [`user-interaction`](../packages/ui/user-interaction) |
+6 -1
View File
@@ -13,6 +13,7 @@ Generated by `pnpm run gen-rfc-index` from the RFC tree — never edit by hand;
| [Interactive side sessions and merge-back](proposed/feature/2026-07-08-interactive-side-sessions.md) | 2026-07-08 |
| [SQLite FTS5 session search](proposed/feature/2026-07-10-sqlite-session-query-provider.md) | 2026-07-10 |
| [Stream workflow progress through tool calls](proposed/feature/2026-07-13-stream-workflow-progress-through-tool-calls.md) | 2026-07-13 |
| [Developer-owned SDK projects](proposed/feature/2026-07-14-sdk-developer-projects.md) | 2026-07-14 |
### Simplification
@@ -27,7 +28,7 @@ Generated by `pnpm run gen-rfc-index` from the RFC tree — never edit by hand;
| Title | First proposed |
|---|---|
| [Runtime schemas for the event vocabulary (Zod vs the merge-extensible-map pattern)](proposed/architecture/2026-06-16-typed-event-schemas.md) | 2026-06-16 |
| [Extract a generic long-running tool runtime](proposed/architecture/2026-06-20-generic-long-running-tool-runtime.md) | 2026-06-20 |
| [SDK project editing architecture](proposed/architecture/2026-07-15-sdk-project-editing-architecture.md) | 2026-07-15 |
### Process
@@ -37,6 +38,7 @@ Generated by `pnpm run gen-rfc-index` from the RFC tree — never edit by hand;
| [Architectural conformance — dependency rules and the adapter kit](proposed/process/2026-06-11-architectural-conformance.md) | 2026-06-11 |
| [Supply chain checks and vendor drift verification](proposed/process/2026-06-11-supply-chain-and-vendor-drift.md) | 2026-06-11 |
| [Discover package inventories instead of maintaining static lists](proposed/process/2026-06-20-discover-package-inventory.md) | 2026-06-20 |
| [Periodic human-review maintenance for dsh-code-review](proposed/process/2026-07-13-human-review-skill-maintenance.md) | 2026-07-13 |
### Testing
@@ -71,7 +73,9 @@ Generated by `pnpm run gen-rfc-index` from the RFC tree — never edit by hand;
| [The approval seam — one-shot permission decisions over a waterfall of answerers](implemented/feature/2026-07-06-approval-seam.md) | 2026-07-06 |
| [Explicit model-facing tool order](implemented/feature/2026-07-06-explicit-tool-order.md) | 2026-07-06 |
| [The subprocess sandbox — confinement seam, native runners, escalation, and per-session modes](implemented/feature/2026-07-06-sandbox.md) | 2026-07-06 |
| [MCP client plugin — connect to external MCP servers and bridge their tools](implemented/feature/2026-07-07-mcp-client-plugin.md) | 2026-07-07 |
| [The session prefix — request-only messages in front of the derived history](implemented/feature/2026-07-07-session-prefix.md) | 2026-07-07 |
| [Background subagent tasks](implemented/feature/2026-07-08-background-subagent-tasks.md) | 2026-07-08 |
| [Repeat-tool-call guard plugin](implemented/feature/2026-07-08-repeat-tool-guard.md) | 2026-07-08 |
| [The self-referential cordis toolset](implemented/feature/2026-07-08-self-referential-cordis-toolset.md) | 2026-07-08 |
| [Exact session query service](implemented/feature/2026-07-10-session-query-service.md) | 2026-07-10 |
@@ -126,6 +130,7 @@ Generated by `pnpm run gen-rfc-index` from the RFC tree — never edit by hand;
| [Shared persistence write coordinator](implemented/architecture/2026-06-18-shared-persistence-write-coordinator.md) | 2026-06-18 |
| [Branded IDs everywhere they belong](implemented/architecture/2026-06-20-branded-ids.md) | 2026-06-20 |
| [Extract example apps into packages](implemented/architecture/2026-06-20-extract-example-app-packages.md) | 2026-06-20 |
| [The background task runtime (`ctx.tasks`) and generic task control tools](implemented/architecture/2026-06-20-generic-long-running-tool-runtime.md) | 2026-06-20 |
| [Reorganize packages into a modular hierarchy](implemented/architecture/2026-06-20-package-hierarchy.md) | 2026-06-20 |
| [Mandatory `User-Agent` attribution for provider requests](implemented/architecture/2026-06-21-mandatory-app-attribution-headers.md) | 2026-06-21 |
| [Web capability seam - stable tools over multiple providers](implemented/architecture/2026-06-24-web-capability-seam.md) | 2026-06-24 |
@@ -12,9 +12,9 @@ This is distinct from "who provides vs. needs a capability at runtime", which Co
A swappable capability is **three packages**:
1. **Interface** — an abstract service + the vocabulary types, owning the `ctx.<key>` and depending only on cordis (e.g. `dsh-bash`: `BashExecutor`, `BashRunResult`, `BashTask`).
1. **Interface** — an abstract service + the vocabulary types, owning the `ctx.<key>` and depending only on its vocabulary dependencies (e.g. `dsh-bash`: `BashExecutor`, `BashRunResult`, `BashProcess`).
2. **Implementation** — a concrete subclass loaded as a plugin (e.g. `dsh-bash-local`: subprocesses, process-group kills, spill-file truncation). Sandboxed/remote backends are sibling packages implementing the same interface.
3. **Consumer** — what the model and plugins see (e.g. `dsh-tool-bash`: the `bash`/`bash_output`/`bash_kill` tool schemas). Consumers `inject` the interface key and never import implementation types.
3. **Consumer** — what the model and plugins see (e.g. `dsh-tool-bash`: the `bash` schema, with background handles registered into the generic task runtime). Consumers `inject` the interface key and never import implementation types.
Implementation and consumer then evolve independently: a sandboxed executor replaces `dsh-bash-local` without touching a tool schema.
@@ -12,18 +12,18 @@ The leaf configs also owned a coupled front door. ACP requires stdout purity and
Each example is now **mostly an invocation of an app package**, splitting the wiring along the existing [interface / implementation / consumer seam](2026-06-13-capability-seams.md): the **app package owns the composition**, the leaf `cordis.yml` owns only the **swappable choices** (which LLM adapter, which bash executor, model, prompt, persistence root).
- **`@deepseek-ai/dsh-agent-core`** ([packages/core/agent-core](../../../../packages/core/agent-core)) composes the providerless, executor-less, UI-less spine and forwards the loop's agent-list config. Its dependency on the concrete loop is intentional because this package composes the spine rather than extending it; swapping the loop means supplying another bundle.
- **`@deepseek-ai/dsh-stdio-agent`** ([packages/ui/stdio-agent](../../../../packages/ui/stdio-agent)) and **`@deepseek-ai/dsh-acp-agent`** ([packages/ui/acp-agent](../../../../packages/ui/acp-agent)) bake in their front doors. Stdio includes `ui-stdio`, a console logger, and `main`; ACP includes the bridge and JSONL persistence but no stdout logger or pre-created agent. Leaves may add plugins, but the safe composition is now the default artifact.
- **`start.ts` is gone.** Each app package exposes a `bin` (`dsh-stdio-agent` / `dsh-acp-agent`); the `demo:*` scripts invoke it (e.g. `dsh-stdio-agent ./cordis.yml`). The Loader-boot tail, `.env` loading, and fail-loud guards live in the shared [`@deepseek-ai/dsh-app-boot`](../../../../packages/ui/app-boot) package (unit-tested under the per-file coverage gate — see [share the app bins' boot glue](../simplification/2026-07-04-share-app-bin-boot-glue.md)); each bin is a thin self-executing composition over those helpers plus its app-specific lifecycle (the ACP bin: snapshot-mode selection and stdin-dispose). The `bin.ts` files themselves stay coverage-excluded (self-executing CLI entries, like the old `start.ts`) and are driven by the keyless Loader-path tests.
- **`@deepseek-ai/dsh-agent-spine-demo`** ([packages/examples/agent-spine-demo](../../../../packages/examples/agent-spine-demo)) composes the providerless, executor-less, UI-less spine and forwards the loop's agent-list config. Its dependency on the concrete loop is intentional because this package composes the spine rather than extending it; swapping the loop means supplying another bundle.
- **`@deepseek-ai/dsh-stdio-demo`** ([packages/examples/stdio-demo](../../../../packages/examples/stdio-demo)) and **`@deepseek-ai/dsh-acp-demo`** ([packages/examples/acp-demo](../../../../packages/examples/acp-demo)) bake in their front doors. Stdio includes `ui-stdio`, a console logger, and `main`; ACP includes the bridge and JSONL persistence but no stdout logger or pre-created agent. Leaves may add plugins, but the safe composition is now the default artifact.
- **`start.ts` is gone.** Each app package exposes a `bin` (`dsh-stdio-demo` / `dsh-acp-demo`); the `demo:*` scripts invoke it (e.g. `dsh-stdio-demo ./cordis.yml`). The Loader-boot tail, `.env` loading, and fail-loud guards live in the shared [`@deepseek-ai/dsh-app-boot`](../../../../packages/ui/app-boot) package (unit-tested under the per-file coverage gate — see [share the app bins' boot glue](../simplification/2026-07-04-share-app-bin-boot-glue.md)); each bin is a thin self-executing composition over those helpers plus its app-specific lifecycle (the ACP bin: snapshot-mode selection and stdin-dispose). The `bin.ts` files themselves stay coverage-excluded (self-executing CLI entries, like the old `start.ts`) and are driven by the keyless Loader-path tests.
- **Each leaf `cordis.yml` collapses** to backends + config: the LLM adapter (`llm-deepseek` with apiKey/models, or `llm-replay`), the bash executor (`bash-local`), `hmr` for the stdio demos (see the amendment below), and one app entry carrying the app's config (model, system prompt, persistence root — surfaced as the app package's own `Config`, which routes each value to wherever the app wires it: stdio onto its pre-created agent, acp onto the bridge plugin).
- **echo-agent folds onto `dsh-stdio-agent`**, swapping the LLM backend to the local `mock-llm` and adding the local `echo-tool` (plus `bash-local`, which the spine's `tool-bash` injects) at the leaf — the clean demonstration of "swap the backend, keep the app". `mock-llm.ts` / `echo-tool.ts` stay as example-local teaching plugins.
- **`base.yml`, `base-core.yml`, and `acp-agent/acp-tail.yml` are retired** — the spine they shared now lives in `dsh-agent-core`.
- **echo-agent folds onto `dsh-stdio-demo`**, swapping the LLM backend to the local `mock-llm` and adding the local `echo-tool` (plus `bash-local`, which the spine's `tool-bash` injects) at the leaf — the clean demonstration of "swap the backend, keep the app". `mock-llm.ts` / `echo-tool.ts` stay as example-local teaching plugins.
- **`base.yml`, `base-core.yml`, and `acp-agent/acp-tail.yml` are retired** — the spine they shared now lives in `dsh-agent-spine-demo`.
`bash-local` and the LLM adapter stay **leaf choices**: the bundle ships `tool-bash` (the consumer schema), the leaf picks the executor implementation, so a sandboxed executor or replay adapter swaps in without touching the app.
### Amendment on implementation: `hmr` stays a leaf entry
The proposal listed `hmr` among the stdio app's baked-in front-door cluster. Validating against the code, baking `hmr` into the `dsh-stdio-agent` package fights cordis in two ways, so it ships as a **leaf `cordis.yml` entry** instead:
The proposal listed `hmr` among the stdio app's baked-in front-door cluster. Validating against the code, baking `hmr` into the `dsh-stdio-demo` package fights cordis in two ways, so it ships as a **leaf `cordis.yml` entry** instead:
1. `@cordisjs/plugin-hmr` is a Loader-only, subprocess-only dev plugin — its constructor throws without `node --expose-internals` + a live `loader` service, so it can only run in the real `demo:*`/bin subprocess, never in the in-process unit/coverage tier.
2. The in-process test tier (vitest) cannot even *import* the vendored `hmr` module (its class-decorator `@Inject` form fails under Vite's transform), so a package whose `apply` statically imported it could never satisfy the per-file 100% coverage gate on its headline function.
@@ -45,11 +45,11 @@ The old `base*.yml`/`acp-tail.yml` includes already deduped the *config*, but a
## Consequences
- **The bare-plugin-tree pedagogy.** echo-agent's inlined `cordis.yml` showed every plugin at once; the spine now lives behind a bundle, so seeing the whole tree means opening `dsh-agent-core`. The app package's README carries that teaching weight.
- **The bare-plugin-tree pedagogy.** echo-agent's inlined `cordis.yml` showed every plugin at once; the spine now lives behind a bundle, so seeing the whole tree means opening `dsh-agent-spine-demo`. The app package's README carries that teaching weight.
- **A layer of indirection.** "What does this demo load?" becomes a package read, not a single YAML scan.
## Related
- Supersedes [Make the shared example base providerless](../../rejected/architecture/2026-06-20-providerless-example-base.md): renaming `base.yml` to the providerless core is moot once the spine moves into `dsh-agent-core` and the `base*.yml` files are deleted.
- Supersedes [Make the shared example base providerless](../../rejected/architecture/2026-06-20-providerless-example-base.md): renaming `base.yml` to the providerless core is moot once the spine moves into `dsh-agent-spine-demo` and the `base*.yml` files are deleted.
- Builds on the [capability-seams](2026-06-13-capability-seams.md) interface/implementation/consumer split — backends and presentation stay leaf choices; the spine is the shared bundle.
- Complements [Reorganize packages into a modular hierarchy](2026-06-20-package-hierarchy.md): the new app/core packages slot into existing groups under that hierarchy (`core` for the reusable spine bundle, `ui` for the app-specific front doors).
@@ -0,0 +1,128 @@
# RFC: The background task runtime (`ctx.tasks`) and generic task control tools
Status: implemented
## Problem
Background bash originally combined two responsibilities: the bash executor ran processes and also managed task ids, ownership, incremental reads, cancellation, completion listeners, and model-facing control tools. Adding background subagents required the same lifecycle and interaction contract. Implementing that contract independently for every long-running capability would duplicate isolation, cleanup, notification, and prompt behavior while teaching the model a different collect-and-stop protocol for each producer.
The task registry, control tools, and completion notices form one harness capability. Bash and subagents should supply execution-specific hooks without owning generic task behavior.
## Decision
The `tasks/` package group owns background-task semantics:
- `@deepseek-ai/dsh-tasks` registers running work as `ctx.tasks` and owns task ids, authorization, snapshots, reads, cancellation, waiting, completion listeners, and cleanup.
- `@deepseek-ai/dsh-tool-tasks` exposes `task_output`, `task_list`, and `task_kill`, injects completion notices, and supplies the background-task system-prompt guidance.
Long-running tools are producers. `dsh-tool-bash` adapts a `BashProcess` into incremental output and process cancellation; `dsh-tool-subagent` adapts a child run into final output and child disposal. The execution seams remain independent of sessions and the task registry.
`TaskService` is a concrete, process-local service. TODO(task-service-backend): separate its public contract from the implementation when a second backend defines the required lifecycle; a systemd-backed runtime is one plausible driver, but this PR does not speculate about its durability, reconnect, ownership, or observation semantics.
## Runtime contract
The literal types live in the [task data-structure catalog](../../../core-data-structures/tasks.md). A producer calls `ctx.tasks.start()` with a kind, label, optional owning `Agent`, and a `run()` function. The runtime completes all failable preflight work before calling `run()` and invokes it once. After `run()` returns hooks, registration commits without another failable step; a producer cannot start work that lacks a collectable task id.
The producer hooks define three responsibilities:
- `cancel(reason?)` synchronously requests termination, is idempotent, and must cause `done` to settle.
- `done` never rejects and settles only after the producer has released the task's resources.
- Optional `readOutput()` returns the next consuming output delta. Omitting it declares a final-output task whose terminal result comes from `TaskOutcome.output`.
Statuses are `running`, `stopping`, `completed`, `killed`, and `failed`. Producer-specific information such as an exit code or stop reason belongs in `detail`; the registry does not interpret it. Task kinds form a merge-extensible string union, and task ids are branded and generated as `<kind>-N`, with a counter per kind.
The runtime attaches one continuation to `done`, records the first terminal outcome, resolves waiters, and invokes completion listeners with per-listener error containment. First-wins settlement matters during teardown: if `cancel` throws, the runtime force-fails the record and warns that work may be orphaned rather than waiting forever for a promise that may never settle. A later producer outcome cannot overwrite that diagnosis or notify twice. A `cancel` that returns without eventually settling `done` still blocks teardown because the runtime cannot distinguish it from a slow, valid stop.
Task registrations are not effects of the producer tool fiber. Reloading a tool or control-surface plugin therefore does not kill work owned by an agent and backend. The task service's own disposal cancels all live tasks and awaits contract-compliant producers.
## Authorization and owner lifecycle
Task ids are runtime-global and predictable, so every access is authorized by the registry. `get`, `read`, `wait`, and `kill` accept the calling `Agent`; `list` returns only tasks visible to that caller. An owned task is accessible only to the exact owning session. Unowned tasks are open to non-agent callers and die with the task service.
The snapshot stores the owner's branded `SessionId` for authorization, while lifecycle operations retain the exact live `Agent` instance. These identities serve different purposes: session equality grants access, but exact object identity selects cleanup and completion delivery. Reusing an agent or session id cannot redirect an old scope's cleanup or notices to a replacement.
The first task for an owner attaches one asynchronous effect to `owner.ctx`. Agent-scope disposal cancels that owner's live tasks, awaits their terminal records, and removes their snapshots. This effect survives producer reloads and joins the agent's existing quiescence boundary. The task service retains the effect disposer so service reload can detach callbacks from still-live agent scopes after global teardown.
For contract-compliant producers, `AgentHandle.dispose()` resolves only after owned background work has stopped. Work intended to outlive an agent must be started unowned; survival across runtime restarts requires a separate durable-job design.
## Service surface
`TaskService` provides:
- `start(spec)` for preflighted, atomic registration.
- `get(id, caller?)` and `list(caller?)` for non-consuming snapshots.
- `read(id, caller?)` for a consuming stream delta or an idempotent final result.
- `kill(id, caller?, reason?)` for cancellation.
- `wait(id, timeoutMs, caller?, signal?)` for bounded terminal waiting.
- `onTaskDone(listener)` for effect-scoped observation with exact-owner delivery and listener containment.
- `attachSurface(name)` for the control-surface availability fence.
`wait` returns the terminal snapshot when the task settles or the live snapshot when its timeout expires. Aborting a wait cancels only that wait. If settlement has already assigned terminal delivery to the waiter, the terminal snapshot still wins. Waiters unregister synchronously on abort so a same-tick settlement cannot suppress a completion notice on behalf of a reader that receives nothing.
A producer loaded without any control surface would let callers start work they cannot collect or stop. `dsh-tool-tasks` therefore calls `attachSurface()` for its lifetime, and `start()` fails before producer execution when no surface is attached. This check occurs at start rather than plugin load because sibling plugins may activate concurrently. Custom non-model surfaces can attach themselves without teaching the registry tool names.
## Model-facing control surface
`dsh-tool-tasks` registers three kind-independent tools with generic ACP cards:
- `task_output(task_id, wait?, timeout_ms?)` reads output and always appends `[status: ...]`. Stream tasks return only output since the previous read; final-output tasks return their result after settlement. Reads are non-blocking unless `wait: true`, whose timeout is defaulted and capped by plugin config. A wait timeout reports the still-running status and does not stop the task.
- `task_list()` returns caller-visible tasks as `<id> [<kind>] <status> — <label>`, or `(no background tasks)`.
- `task_kill(task_id, reason?)` requests cancellation immediately. The optional logged reason is forwarded to the producer. Terminal tasks report their existing status; a throwing producer cancel fails the call and leaves the task running.
Stream reads share one task-scoped consuming cursor because the owning model is the intended reader. A UI or multiple independent readers need a separate non-consuming observation API; sharing this cursor would let readers consume one another's output.
The system prompt tells the model to retain task ids, continue independent work instead of busy-polling or duplicating a running task, collect relevant tasks before its final answer, and kill work that no longer matters. Completion injects a logged `context/message` into the exact owner's session; it becomes durable context for the next request but does not wake an idle agent.
The runtime marks a terminal task `reported` when a read or wait delivers it, when a live waiter has claimed delivery at settlement, or when the model explicitly kills it. Reported tasks do not inject redundant completion notices. Listener failures are logged independently, do not stop later listeners, and are not awaited by waiters or teardown.
## Producer opt-in
Each producer owns whether its schema exposes `run_in_background` through defaulted config. `dsh-tool-bash` and each `dsh-tool-subagent` instance use `enableRunInBackground`, defaulting to true. A disabled instance omits the parameter and also rejects a forced background argument at execution because the generic argument validator permits undeclared keys. Schema omission advertises the capability; the execution check enforces it.
`ctx.tasks` does not rewrite producer schemas. A bundle forwards configuration only for producers it owns. If a background call reaches `start()` without an attached surface, the runtime fence fails before execution.
## Producer integrations
The bash seam exposes `resolve`, `run`, and `start`. `start(spec)` returns a `BashProcess` with incremental reads, cancellation, exit facts, and a non-rejecting quiescence promise. The local executor retains live handles only so its own disposal can kill and join processes. Foreground callers continue to use `resolve` and `run` directly.
For background bash, `dsh-tool-bash` registers the calling agent as owner. Its hooks map `kill()` to cancellation, `done` to a completed or killed `TaskOutcome`, and `readOutput()` to the process's bounded incremental output plus spill and sandbox notices. Generic task tools own ids, status lines, listing, waiting, and completion notices.
For background subagents, `dsh-tool-subagent` creates a task-owned `AbortController` and begins provider startup inside the task starter. Cancellation aborts the same signal before or after provider readiness. `done` awaits both the child result and child disposal, maps completed output to a final result, maps abort to `killed`, and maps other stop reasons or infrastructure failures to `failed`. Intermediate child history remains in the child session and is not exposed through `readOutput()`.
## Alternatives considered
### Per-capability control tools
Separate bash and subagent output/stop tools duplicate ids, isolation, cleanup, notification, and guidance while increasing the model's schema and protocol burden. One runtime keeps execution-specific behavior in producers without cloning the task lifecycle.
### An immediate abstract task-runtime backend
The current `TaskStart.run()` contract passes in-process callbacks and exact `Agent` objects. A durable backend changes identity, restart, ownership, and observation semantics, so extracting an interface before a second implementation exists would freeze the wrong boundary.
### Consumer-owned authorization or cleanup events
Consumer-owned checks invite inconsistent or missing isolation on each new surface. A broadcast cleanup event makes every listener filter every agent and provides no registration disposer. Central authorization plus one owner-scoped effect gives every consumer the same fence and an awaited, removable lifecycle hook.
### Blocking output or a separate wait tool
Blocking by default would serialize the parent while background work runs. Waiting without reading would add another model call and schema without returning useful information. `task_output(wait: true)` makes blocking explicit and combines it with result delivery.
The wait uses the shared deadline primitives but not the generic tool-timeout policy. A wait timeout is a successful observation that returns `[status: running]`; the generic policy would replace it with a timeout error. No tool-call timeout controls task lifetime after a task id has been returned.
### Runtime-owned output sinks
A push sink would centralize buffering, but bash already owns bounded buffers, truncation, and spill files behind its executor seam. Pulling formatted deltas preserves that ownership. A durable backend that owns storage may justify revisiting the producer interface.
### Random ids, promotion, or lifecycle session events
Authorization, not unguessability, is the access boundary, and ids do not derive filesystem paths; sequential branded ids keep transcripts readable. Foreground-to-background promotion requires a user interaction contract the SDK does not prescribe. Starts, reads, and notices are already logged as tool and context events, so dedicated task session events would duplicate model-visible facts.
## Testing
Unit coverage pins preflight atomicity, per-kind ids, stream and final reads, wait timeout and abort races, cancellation, first-wins settlement, listener containment, notice suppression, owner isolation, stale owner instances, owner cleanup, service teardown, and the no-surface fence. Producer tests cover bash process mapping, subagent startup cancellation, terminal mapping, and disposal. Snapshot coverage pins the control-tool schemas and prompt guidance.
## Consequences
Bash commands and subagents share one id vocabulary, listing, notice format, prompt habit, and set of control tools. New long-running producers implement execution hooks instead of another registry and tool family. The [tool cookbook](../../../cookbook/adding-a-tool.md) points producers to this contract.
Owned background bash now stops with its agent instead of surviving it. Background processes have no executor timeout; callers must kill irrelevant work or rely on owner/service disposal. Stream reads support one consuming reader, completion notices do not wake idle agents, and a producer that returns from `cancel` without settling `done` can still stall teardown. Durable jobs, independent observation cursors, and foreground promotion remain separate designs.
@@ -43,7 +43,7 @@ interface TerminalResultView { card: 'terminal'; title?: string; output?: string
### Producer mapping
- `dsh-tool-fs` read → `generic` (`kind:'read'`, a follow-along `location`); write → `diff` (`oldText:null`); edit → `diff` (`oldText:old_string || null`, `newText:new_string ?? ''`). This mirrors `claude-agent-acp`'s `toolInfoFromToolUse` Read/Write/Edit arms field-for-field.
- `dsh-tool-bash` foreground → `terminal` call + `terminal` result; `run_in_background` and `bash_output`/`bash_kill` → `generic`.
- `dsh-tool-bash` foreground → `terminal` call + `terminal` result; `run_in_background` → `generic`. The generic `task_*` controls own their own generic cards.
- `dsh-tool-todo` → `generic`.
### Terminal fallback ownership
@@ -78,7 +78,7 @@ No new session event is needed for reconstructability: `TOOL_TIMEOUT` is the fin
`bash` stays on the current backend timeout path. `dsh-tool-bash` continues to expose `timeoutMs` and `run_in_background`; `dsh-bash-local` continues to use `@deepseek-ai/dsh-timeout` for `BASH_TIMEOUT`; hook bridges continue to call `runHook()` and pass `timeoutMs` through `ctx.bash`. This keeps foreground/background/hook behavior stable.
`read`, `write`, `edit`, `todo_write`, `bash_output`, and `bash_kill` do not opt into tool-call timeout: they are local filesystem or short registry/session operations where a deadline would be best-effort only or unnecessary.
`read`, `write`, `edit`, `todo_write`, `task_list`, and `task_kill` do not opt into tool-call timeout. `task_output` owns its bounded wait because a wait timeout is a successful live-status result, not a tool failure.
A future model-facing grep/glob tool can be implemented on top of `ctx.bash` without importing `@deepseek-ai/dsh-timeout`: it forwards `exec.signal` to `ctx.bash`, and declares its own `timeoutMs` (from its plugin's config) for the enforcer to apply. If bash-local's backend timeout becomes a problem for such a tool, the bash seam can later add a caller-owned-deadline mode; that is outside this cut.
@@ -2,5 +2,5 @@
# side as of the last confirmed-consistent state. Both languages carry equal authority;
# after editing either side, bring the other along and re-record with:
# pnpm run verify-translation-pairing --write
2026-07-10-single-file-executable-sdk-runtime-distribution.md: 372058dc04c4a36e82f5a5a6f5ef1af48068e4e3
2026-07-10-single-file-executable-sdk-runtime-distribution.zh.md: cd12a65d185e8cdeafc4d04faad4a3349c6150d4
2026-07-10-single-file-executable-sdk-runtime-distribution.md: b177af24e988c6a314db522b8de0d1c09e30464f
2026-07-10-single-file-executable-sdk-runtime-distribution.zh.md: 0b964e8a748e4adcc32c017957e5294a3f258365
@@ -23,12 +23,12 @@ The exe is packaged with the **`--sea` (enhanced SEA) mode** of [@yao-pkg/pkg](h
Terminology reminder: pkg's `/snapshot` VFS has nothing to do with this repo's testing-system "snapshot" (ACP replay goldens, `$DSH_SNAPSHOT`); this document says "VFS" for the former.
### The serving surface is a plugin: the two packages ui/jsonrpc + ui/jsonrpc-agent
### The serving surface is a plugin: the two packages ui/jsonrpc + examples/jsonrpc-demo
The deterministic protocol implementation (`server.ts` / `transport.ts`) lands as two packages on the existing `ui/acp` + `ui/acp-agent` pattern — the serving surface is itself a plugin:
The deterministic protocol implementation (`server.ts` / `transport.ts`) lands as two packages on the existing `ui/acp` + `examples/acp-demo` pattern — the serving surface is itself a plugin:
- [`packages/ui/jsonrpc`](../../../../packages/ui/jsonrpc/README.md) (`@deepseek-ai/dsh-jsonrpc`): the pure protocol plugin; on apply it mounts `HarnessSdkServer` plus a line-delimited JSON-RPC transport on the process stdio, with disposal through `ctx.effect()`. Whether to serve is decided by `cordis.yml`; a yml that does not mount it is a legitimate process that does not serve. Protocol-level exit belongs to the plugin (after answering the `shutdown` request it disposes its own fiber, then `exit(0)`; an HMR-style unload only stops the service without exiting the process).
- [`packages/ui/jsonrpc-agent`](../../../../packages/ui/jsonrpc-agent/README.md) (`@deepseek-ai/dsh-jsonrpc-agent`): a thin app bin — `installFailLoud` + `loadEnv` + config discovery + `boot()` from [`dsh-app-boot`](../../../../packages/ui/app-boot/src/index.ts), done once boot completes; the server is brought up by the `dsh-jsonrpc` entry in the yml. Its only dependency is app-boot. Process-level exit belongs to the bin (stdin EOF/SIGTERM → dispose then 0, SIGINT → 130).
- [`packages/examples/jsonrpc-demo`](../../../../packages/examples/jsonrpc-demo/README.md) (`@deepseek-ai/dsh-jsonrpc-demo`): a thin app bin — `installFailLoud` + `loadEnv` + config discovery + `boot()` from [`dsh-app-boot`](../../../../packages/ui/app-boot/src/index.ts), done once boot completes; the server is brought up by the `dsh-jsonrpc` entry in the yml. Its only dependency is app-boot. Process-level exit belongs to the bin (stdin EOF/SIGTERM → dispose then 0, SIGINT → 130).
Config discovery has two channels and fails loudly when both are missing: the `DSH_CORDIS_CONFIG` environment variable first (the SDK client convention), then an argv positional argument; no default path and no built-in fallback whatsoever — "the plugins actually booted are decided by an external cordis.yml" is a hard semantic.
@@ -40,13 +40,13 @@ The deploy root is [`python/sdk-runtime/package.json`](../../../../python/sdk-ru
### Build pipeline and artifacts
[`scripts/build-exe-for-python-sdk.ts`](../../../../scripts/build-exe-for-python-sdk.ts): runtime closure verification → `pnpm run build` → (after clearing) `pnpm --filter dsh-jsonrpc-agent-pkg deploy --legacy --prod --config.node-linker=hoisted --config.auto-install-peers=false --config.link-workspace-packages=true` **directly into** `python/sdk-runtime/src/deepseek_harness_runtime/runtime/node/` → inject the pkg configuration (`bin` points at `node_modules/@deepseek-ai/dsh-jsonrpc-agent/lib/bin.js` inside the closure, `assets` is a full glob — dynamic import is invisible to pkg's static analysis, so everything must be packed in explicitly) → one `pkg --sea` per target → the executables `dsh-jsonrpc-agent-pkg-<platform>-<arch>` land in `dist-exe/` and are copied back into the runtime directory. CI treats them as intermediate test inputs and retains their platform wheels. All four deploy flags are grounded in measurement: `--legacy` is the mandatory path with inject-workspace-packages off; hoisted yields a zero-symlink file tree (most stable for the pkg VFS, physically guaranteeing a single cordis instance); disabling automatic peer installation keeps unpublished package names from triggering registry resolution; link-workspace-packages points the closure at workspace/vendor sources.
[`scripts/build-exe-for-python-sdk.ts`](../../../../scripts/build-exe-for-python-sdk.ts): runtime closure verification → `pnpm run build` → (after clearing) `pnpm --filter dsh-jsonrpc-agent-pkg deploy --legacy --prod --config.node-linker=hoisted --config.auto-install-peers=false --config.link-workspace-packages=true` **directly into** `python/sdk-runtime/src/deepseek_harness_runtime/runtime/node/` → inject the pkg configuration (`bin` points at `node_modules/@deepseek-ai/dsh-jsonrpc-demo/lib/bin.js` inside the closure, `assets` is a full glob — dynamic import is invisible to pkg's static analysis, so everything must be packed in explicitly) → one `pkg --sea` per target → the executables `dsh-jsonrpc-agent-pkg-<platform>-<arch>` land in `dist-exe/` and are copied back into the runtime directory. CI treats them as intermediate test inputs and retains their platform wheels. All four deploy flags are grounded in measurement: `--legacy` is the mandatory path with inject-workspace-packages off; hoisted yields a zero-symlink file tree (most stable for the pkg VFS, physically guaranteeing a single cordis instance); disabling automatic peer installation keeps unpublished package names from triggering registry resolution; link-workspace-packages points the closure at workspace/vendor sources.
CI: [`.github/workflows/build-exe-for-python-sdk.yml`](../../../../.github/workflows/build-exe-for-python-sdk.yml), triggered explicitly only — `workflow_dispatch`, or the `build-exe` label on a pull request; native builds on the three platforms linux-x64 / linux-arm64 (`ubuntu-24.04-arm`) / macos-arm64, with `~/.pkg-cache` cached; macOS ad-hoc signing is handled by pkg. Each leg drives a mock SSE model through the SDK with the default config and a custom `cordis.yml`, drives the exe directly over NDJSON JSON-RPC, verifies the JSONL and final response, and installs release-shaped wheels into a clean venv without `runtime_bin`; Linux additionally inspects GLIBC requirements and runs in a manylinux 2.28 container. A full three-target run retains four artifacts, each containing one release file: the platform-independent SDK wheel and three native runtime wheels; a subset dispatch retains the SDK wheel and selected runtime wheels. Bare executables and source bundles remain intermediate test inputs. [`.gitlab-ci.yml`](../../../../.gitlab-ci.yml) accepts only `python-vX.Y.Z` tag pipelines whose version matches the root `package.json`, builds one SDK wheel and three native runtime wheels, then a single serialized job checks and publishes all four to the project PyPI registry. Windows is a non-goal.
### Python SDK distribution: two carriers, exe for production, node for development
The Python SDK lives at [`python/`](../../../../python/README.md): `python/sdk` (the client) + `python/sdk-runtime` (the runtime carrier package). The runtime package's data directory holds three kinds of content: the checked-in default `runtime/cordis.yml`, the build-injected platform exe, and the build-injected `runtime/node/` closure tree. `resolve_bundled_launch_args()` automatic resolution **finds the exe only**; the node carrier is enabled only by an explicit `DSH_RUNTIME_MODE=node` (running `runtime/node/node_modules/@deepseek-ai/dsh-jsonrpc-agent/lib/bin.js`, requiring a system node ≥22.19), positioned as the development-verification channel for members of this repo, and does not enter wheel distributions.
The Python SDK lives at [`python/`](../../../../python/README.md): `python/sdk` (the client) + `python/sdk-runtime` (the runtime carrier package). The runtime package's data directory holds three kinds of content: the checked-in default `runtime/cordis.yml`, the build-injected platform exe, and the build-injected `runtime/node/` closure tree. `resolve_bundled_launch_args()` automatic resolution **finds the exe only**; the node carrier is enabled only by an explicit `DSH_RUNTIME_MODE=node` (running `runtime/node/node_modules/@deepseek-ai/dsh-jsonrpc-demo/lib/bin.js`, requiring a system node ≥22.19), positioned as the development-verification channel for members of this repo, and does not enter wheel distributions.
[`scripts/build-python-release.py`](../../../../scripts/build-python-release.py) reads the authoritative stable `X.Y.Z` from the repository root `package.json` and stages both packages at that version, with the SDK depending exactly on `deepseek-harness-runtime-bin==X.Y.Z`. An optional `python-vX.Y.Z` release tag is a consistency assertion and is rejected when it differs from the repository version; the source `pyproject.toml` development sentinel never determines a release version. The SDK is a `py3-none-any` wheel; the wheel-only runtime package contains exactly one exe and uses one of `py3-none-manylinux_2_28_x86_64`, `py3-none-manylinux_2_28_aarch64`, or `py3-none-macosx_11_0_arm64`. Its Hatch hook rejects sdists, universal tags, mixed executable payloads, and unsupported platforms.
@@ -54,7 +54,7 @@ The exe's "must be explicitly configured" hard semantic is unchanged; the zero-c
### Naming lineage
`@deepseek-ai/dsh-jsonrpc-agent` (the package) → `dsh-jsonrpc-agent` (the bin) → `dsh-jsonrpc-agent-pkg` (the closure manifest; no scope prefix, deliberately sidestepping the constraints' package-shape rules for `@deepseek-ai/dsh-*`) → `dsh-jsonrpc-agent-pkg-<platform>-<arch>` (the exe artifacts). The wire `serverInfo.name` stays `deepseek-harness-sdk-runtime` (a protocol-stable value); the Python dist names are `deepseek-harness` / `deepseek-harness-runtime-bin`.
`@deepseek-ai/dsh-jsonrpc-demo` (the package) → `dsh-jsonrpc-agent` (the bin) → `dsh-jsonrpc-agent-pkg` (the closure manifest; no scope prefix, deliberately sidestepping the constraints' package-shape rules for `@deepseek-ai/dsh-*`) → `dsh-jsonrpc-agent-pkg-<platform>-<arch>` (the exe artifacts). The wire `serverInfo.name` stays `deepseek-harness-sdk-runtime` (a protocol-stable value); the Python dist names are `deepseek-harness` / `deepseek-harness-runtime-bin`.
## Disposition of worker-style plugins
@@ -23,12 +23,12 @@ exe 使用 [@yao-pkg/pkg](https://github.com/yao-pkg/pkg)vercel/pkg 归档后
术语提醒:pkg 的 `/snapshot` VFS 与本仓库测试体系的“快照”(ACP 回放 golden、`$DSH_SNAPSHOT`)无关,本文用“VFS”指前者。
### 对外服务接口也是插件:ui/jsonrpc + ui/jsonrpc-agent 两包
### 对外服务接口也是插件:ui/jsonrpc + examples/jsonrpc-demo 两包
确定性协议实现(`server.ts` / `transport.ts`)按 `ui/acp` + `ui/acp-agent` 的既有模式落为两包——对外服务接口本身也是插件:
确定性协议实现(`server.ts` / `transport.ts`)按 `ui/acp` + `examples/acp-demo` 的既有模式落为两包——对外服务接口本身也是插件:
- [`packages/ui/jsonrpc`](../../../../packages/ui/jsonrpc/README.md)`@deepseek-ai/dsh-jsonrpc`):纯协议插件;执行 `apply` 时,在进程 stdio 上挂载 `HarnessSdkServer` 与按行传输的 JSON-RPC 层,资源释放走 `ctx.effect()`。是否提供服务由 `cordis.yml` 决定;未挂载该插件的配置会启动一个不提供此服务的合法进程。协议级退出归插件所有(应答 `shutdown` 请求后 dispose 自身 fiber,再调用 `exit(0)`;HMR 式卸载只停止服务,不退出进程)。
- [`packages/ui/jsonrpc-agent`](../../../../packages/ui/jsonrpc-agent/README.md)`@deepseek-ai/dsh-jsonrpc-agent`):轻量应用入口——`installFailLoud` + `loadEnv` + 配置发现 + [`dsh-app-boot`](../../../../packages/ui/app-boot/src/index.ts) 的 `boot()``boot()` 完成后入口即完成,服务器由 `cordis.yml` 中的 `dsh-jsonrpc` 条目启动。它只依赖 `app-boot`。进程级退出归 `bin` 所有(stdin EOF/SIGTERM → dispose 后返回 0SIGINT → 130)。
- [`packages/examples/jsonrpc-demo`](../../../../packages/examples/jsonrpc-demo/README.md)`@deepseek-ai/dsh-jsonrpc-demo`):轻量应用入口——`installFailLoud` + `loadEnv` + 配置发现 + [`dsh-app-boot`](../../../../packages/ui/app-boot/src/index.ts) 的 `boot()``boot()` 完成后入口即完成,服务器由 `cordis.yml` 中的 `dsh-jsonrpc` 条目启动。它只依赖 `app-boot`。进程级退出归 `bin` 所有(stdin EOF/SIGTERM → dispose 后返回 0SIGINT → 130)。
配置发现有两个通道,均缺失时立即报错:优先使用 `DSH_CORDIS_CONFIG` 环境变量(SDK 客户端约定),其次使用 argv 位置参数;没有默认路径或内置回退——“实际启动的插件由外部 `cordis.yml` 决定”是硬语义。
@@ -40,13 +40,13 @@ exe 的 VFS 内是**构建产物形态的真实包树**(各包的 `lib/` + 真
### 构建管线与产物
[`scripts/build-exe-for-python-sdk.ts`](../../../../scripts/build-exe-for-python-sdk.ts):运行时闭包校验 → `pnpm run build` →(清空后)`pnpm --filter dsh-jsonrpc-agent-pkg deploy --legacy --prod --config.node-linker=hoisted --config.auto-install-peers=false --config.link-workspace-packages=true` **直接写入** `python/sdk-runtime/src/deepseek_harness_runtime/runtime/node/` → 注入 pkg 配置(`bin` 指向闭包内的 `node_modules/@deepseek-ai/dsh-jsonrpc-agent/lib/bin.js``assets` 使用全量 glob,因为动态 `import()` 对 pkg 静态分析不可见,必须显式打入全部内容)→ 每个构建目标调用一次 `pkg --sea` → 可执行文件 `dsh-jsonrpc-agent-pkg-<platform>-<arch>` 写入 `dist-exe/`,并拷回运行时目录。CI 将这些文件作为测试中间输入,只保留对应平台的 wheel 包。四个部署标志都有实测依据:未启用 `inject-workspace-packages` 时必须使用 `--legacy``hoisted` 产出无符号链接的文件树(对 pkg VFS 最稳定,并从物理上保证只有一个 Cordis 实例);关闭对等依赖自动安装可避免未发布包名触发注册表解析;`link-workspace-packages` 让闭包指向工作区/vendor 源码。
[`scripts/build-exe-for-python-sdk.ts`](../../../../scripts/build-exe-for-python-sdk.ts):运行时闭包校验 → `pnpm run build` →(清空后)`pnpm --filter dsh-jsonrpc-agent-pkg deploy --legacy --prod --config.node-linker=hoisted --config.auto-install-peers=false --config.link-workspace-packages=true` **直接写入** `python/sdk-runtime/src/deepseek_harness_runtime/runtime/node/` → 注入 pkg 配置(`bin` 指向闭包内的 `node_modules/@deepseek-ai/dsh-jsonrpc-demo/lib/bin.js``assets` 使用全量 glob,因为动态 `import()` 对 pkg 静态分析不可见,必须显式打入全部内容)→ 每个构建目标调用一次 `pkg --sea` → 可执行文件 `dsh-jsonrpc-agent-pkg-<platform>-<arch>` 写入 `dist-exe/`,并拷回运行时目录。CI 将这些文件作为测试中间输入,只保留对应平台的 wheel 包。四个部署标志都有实测依据:未启用 `inject-workspace-packages` 时必须使用 `--legacy``hoisted` 产出无符号链接的文件树(对 pkg VFS 最稳定,并从物理上保证只有一个 Cordis 实例);关闭对等依赖自动安装可避免未发布包名触发注册表解析;`link-workspace-packages` 让闭包指向工作区/vendor 源码。
CI 使用 [`.github/workflows/build-exe-for-python-sdk.yml`](../../../../.github/workflows/build-exe-for-python-sdk.yml),且只允许显式触发:手动派发 `workflow_dispatch`,或给 PR 添加 `build-exe` 标签。linux-x64、linux-arm64`ubuntu-24.04-arm`)和 macos-arm64 三个平台分别进行原生构建,并缓存 `~/.pkg-cache`macOS 的 ad-hoc 签名由 pkg 处理。每个平台都使用模拟 SSE 模型,分别通过默认配置和自定义 `cordis.yml` 驱动 SDK,再通过 NDJSON JSON-RPC 直接驱动 exe,校验 JSONL 与最终响应;最后把发布形态的 wheel 包安装到干净的 venv 中,并在不传 `runtime_bin` 的情况下运行。Linux 还会检查 GLIBC 依赖,并在 manylinux 2.28 容器中运行。完整构建三个目标时保留 4 个产物,每个产物只含一个发布文件:平台无关的 SDK wheel 包与 3 个原生运行时 wheel 包;手动选择部分目标时保留 SDK wheel 与所选运行时 wheel。裸 exe 与源码包只作为测试中间输入。[`.gitlab-ci.yml`](../../../../.gitlab-ci.yml) 只接受版本与根目录 `package.json` 匹配的 `python-vX.Y.Z` 标签流水线,构建一个 SDK wheel 包和 3 个原生运行时 wheel 包,再由单个串行任务校验并将这 4 个文件发布到项目的 PyPI 注册表。Windows 不在目标范围内。
### Python SDK 分发:双载体,exe 用于生产,`node` 用于开发
Python SDK 位于 [`python/`](../../../../python/README.md)`python/sdk` 是客户端,`python/sdk-runtime` 是运行时载体包。运行时包的数据目录包含三类内容:检入的默认 `runtime/cordis.yml`、构建注入的平台 exe,以及构建注入的 `runtime/node/` 闭包树。`resolve_bundled_launch_args()` 的自动解析**只查找 exe**`node` 载体仅在显式设置 `DSH_RUNTIME_MODE=node` 时启用(运行 `runtime/node/node_modules/@deepseek-ai/dsh-jsonrpc-agent/lib/bin.js`,需要系统 Node ≥22.19),定位为本仓库成员的开发验证通道,不随 wheel 包分发。
Python SDK 位于 [`python/`](../../../../python/README.md)`python/sdk` 是客户端,`python/sdk-runtime` 是运行时载体包。运行时包的数据目录包含三类内容:检入的默认 `runtime/cordis.yml`、构建注入的平台 exe,以及构建注入的 `runtime/node/` 闭包树。`resolve_bundled_launch_args()` 的自动解析**只查找 exe**`node` 载体仅在显式设置 `DSH_RUNTIME_MODE=node` 时启用(运行 `runtime/node/node_modules/@deepseek-ai/dsh-jsonrpc-demo/lib/bin.js`,需要系统 Node ≥22.19),定位为本仓库成员的开发验证通道,不随 wheel 包分发。
[`scripts/build-python-release.py`](../../../../scripts/build-python-release.py) 从仓库根目录的 `package.json` 读取权威的稳定版本 `X.Y.Z`,以该版本暂存两个包,并让 SDK 精确依赖 `deepseek-harness-runtime-bin==X.Y.Z`。可选的 `python-vX.Y.Z` 发布标签只是一项一致性断言,与仓库版本不同时会被拒绝;源码 `pyproject.toml` 中的开发占位版本从不决定发布版本。SDK 是 `py3-none-any` wheel 包;只提供 wheel 包的运行时包恰好包含一个 exe,标签为 `py3-none-manylinux_2_28_x86_64``py3-none-manylinux_2_28_aarch64``py3-none-macosx_11_0_arm64`。其 Hatch 钩子拒绝 sdist、通用标签、混合可执行载荷以及不支持的平台。
@@ -54,7 +54,7 @@ exe“必须显式配置”的硬语义不变;零配置体验由包装层恢
### 命名血统
`@deepseek-ai/dsh-jsonrpc-agent`(包)→ `dsh-jsonrpc-agent``bin`)→ `dsh-jsonrpc-agent-pkg`(闭包清单;没有作用域前缀,刻意避开 `constraints``@deepseek-ai/dsh-*` 的包形状规则)→ `dsh-jsonrpc-agent-pkg-<platform>-<arch>`exe 产物)。协议字段 `serverInfo.name` 保持为 `deepseek-harness-sdk-runtime`(协议稳定值);Python 分发名为 `deepseek-harness` / `deepseek-harness-runtime-bin`
`@deepseek-ai/dsh-jsonrpc-demo`(包)→ `dsh-jsonrpc-agent``bin`)→ `dsh-jsonrpc-agent-pkg`(闭包清单;没有作用域前缀,刻意避开 `constraints``@deepseek-ai/dsh-*` 的包形状规则)→ `dsh-jsonrpc-agent-pkg-<platform>-<arch>`exe 产物)。协议字段 `serverInfo.name` 保持为 `deepseek-harness-sdk-runtime`(协议稳定值);Python 分发名为 `deepseek-harness` / `deepseek-harness-runtime-bin`
## 工作线程插件
@@ -67,6 +67,6 @@ The seam is tested through the real Cordis Loader/export path, which catches the
## Consequences
- **Recursion.** Without a bound, an in-process child can see the delegation tool and recurse. The in-process backends implement the optional absolute depth limit and scoped live-global `toolFilter`; ACP advertises both capabilities off and rejects such a request. The [subagent composition-controls RFC](2026-07-12-subagent-persona-tool-filter-and-depth.md) owns their exact semantics and security limits.
- **Blocking the parent turn.** Synchronous collect holds the parent's `runStep` open for the child's full duration. This is acceptable for the first cut; **background / poll / spill semantics are deferred to a future redesign that unifies long-running-tool handling across subagents AND bash** (a sub-agent and a long `bash` background task pose the same "the model started something slow, how does it collect later" problem, and should share one mechanism rather than each inventing its own).
- **Blocking the parent turn.** Foreground collection holds the parent's step open for the child's full duration. Background delegation uses the shared `ctx.tasks` runtime and generic `task_*` tools, the same collection mechanism as background bash; the subagent seam itself remains task-agnostic.
- **Live progress.** This cut surfaces only lifecycle + final result; a per-chunk child→parent update stream is deferred with the background redesign.
- **ACP client surface.** Proxying `fs`/`terminal` from the ACP child back to the parent (a shared-workspace mode) is future work; the first cut advertises neither, so the child self-serves in its own process.
@@ -20,7 +20,7 @@ Providers return `{ answers: [{ id, selected, custom? }] }`. `selected` is alway
## UI mappings
`dsh-stdio-agent`'s in-package readline module renders each question, shows each option's `description` on the next line, supports comma/space-separated numeric choices for `multi_select`, accepts free-form custom answers, and rejects pending questions on abort, provider disposal, or stdin EOF. A batched request is asked in order and resolved as one answer object. The stdio provider serializes simultaneous requests with an internal queue so only one prompt owns stdin at a time.
`dsh-stdio-demo`'s in-package readline module renders each question, shows each option's `description` on the next line, supports comma/space-separated numeric choices for `multi_select`, accepts free-form custom answers, and rejects pending questions on abort, provider disposal, or stdin EOF. A batched request is asked in order and resolved as one answer object. The stdio provider serializes simultaneous requests with an internal queue so only one prompt owns stdin at a time.
`dsh-acp` provides the same seam for ACP sessions. It routes an ask request from the calling `Agent` through the bridge's `agent→sessionId` reverse map and calls ACP `unstable_createElicitation` with a session-scoped form for each question. Single-select options become a `choice` string enum; `multi_select` options become a `choice` array enum; optionless questions use a required `custom` text field. If the client returns both `choice` and non-empty `custom`, the custom answer wins. ACP `decline`/`cancel`, a missing answer, a missing session, and a client without elicitation support all become structured `UserInteractionError`s.
@@ -46,4 +46,4 @@ The feature gives the model a powerful pause primitive, so prompt guidance matte
## Testing
Unit coverage pins provider registration/disposal, duplicate-provider rejection, abort-before-provider, empty-question rejection, structured tool errors through `ctx.tools.execute()`, batched answers, multi-select answers, custom answers, and the model schema including the removal of `value`, `recommended`, `allow_custom`, and `desc`. `dsh-stdio-agent` tests cover option descriptions, queued requests, EOF/abort cleanup, optionless free-form input, invalid option reprompts, duplicate multi-select numbers, and batched question flows. ACP bridge tests drive a real in-memory ACP connection with the real `ask_user_question` tool and verify selected-option, custom-overrides-choice, multi-select, and optionless free-form elicitation paths continue the agent loop.
Unit coverage pins provider registration/disposal, duplicate-provider rejection, abort-before-provider, empty-question rejection, structured tool errors through `ctx.tools.execute()`, batched answers, multi-select answers, custom answers, and the model schema including the removal of `value`, `recommended`, `allow_custom`, and `desc`. `dsh-stdio-demo` tests cover option descriptions, queued requests, EOF/abort cleanup, optionless free-form input, invalid option reprompts, duplicate multi-select numbers, and batched question flows. ACP bridge tests drive a real in-memory ACP connection with the real `ask_user_question` tool and verify selected-option, custom-overrides-choice, multi-select, and optionless free-form elicitation paths continue the agent loop.
@@ -10,7 +10,7 @@ DeepSeek Harness uses the same primitive so project-specific review, plugin-auth
## Decision
`@deepseek-ai/dsh-skill` is the pure provider registry (`ctx.skills`), `@deepseek-ai/dsh-skill-local` is the shipped local filesystem provider, and `@deepseek-ai/dsh-tool-skill` owns the session-prefix catalog and model-facing loader tool. `dsh-agent-core` loads the registry, local provider, and consumer by default so stdio and ACP apps get the same behavior while embedded or remote providers contribute skills without changing the registry or consumer. Its `skills` config forwards `registry`, `local`, and `tool` branches to those owners.
`@deepseek-ai/dsh-skill` is the pure provider registry (`ctx.skills`), `@deepseek-ai/dsh-skill-local` is the shipped local filesystem provider, and `@deepseek-ai/dsh-tool-skill` owns the session-prefix catalog and model-facing loader tool. `dsh-agent-spine-demo` loads the registry, local provider, and consumer by default so stdio and ACP apps get the same behavior while embedded or remote providers contribute skills without changing the registry or consumer. Its `skills` config forwards `registry`, `local`, and `tool` branches to those owners.
Provider plugins register synchronously during `apply()`. Provider membership is direct effect-owned state: registration and disposal invalidate completed catalogs synchronously, and discovery reads the current provider map on demand rather than observing registry-change events. Provider catalogs return ranked candidates from awaited `list()` calls, where remote providers perform initialization, authentication, and discovery while honoring the lookup abort signal. The registry validates each candidate, resolves same-name skills first-wins by rank, provider registration order, and provider-local order, then sorts summaries by skill name for deterministic consumers. It caches only completed catalog snapshots and retries when a provider/runtime revision changes during discovery, so an unload cannot freeze a stale, unresolvable skill into a session prefix. Runtime `ctx.skills.register(...)` remains a convenience for embedded in-process skills and uses project-over-user priority; `runtime` is reserved as the registry-owned provider name.
@@ -23,7 +23,7 @@ One `cordis.yml` entry mounts the seam. Not loading it is the fail-closed opt-ou
# policy: never # deployment default for sessions without an override; 'ask' when omitted
```
The entry alone provides mechanism, not a channel: with no answerer composed, every ask resolves `unavailable` and the asking tool call denies — fail-closed needs no configuration. Composing the ACP app (`@deepseek-ai/dsh-acp-agent`, as in [the acp-agent example's default tree](../../../../examples/acp-agent/README.md)) completes the loop: its bridge registers an answerer that prompts the owning editor session via `session/request_permission`, so a hook's `ask` or an escalation request surfaces as a one-shot Allow/Reject prompt attached to the already-streamed tool call. `policy: never` is the unattended stance — every ask auto-rejects deterministically, stated in the system prompt, no human in the loop. `policy` is validated against the closed list at plugin load; anything else throws.
The entry alone provides mechanism, not a channel: with no answerer composed, every ask resolves `unavailable` and the asking tool call denies — fail-closed needs no configuration. Composing the ACP app (`@deepseek-ai/dsh-acp-demo`, as in [the acp-agent example's default tree](../../../../examples/acp-agent/README.md)) completes the loop: its bridge registers an answerer that prompts the owning editor session via `session/request_permission`, so a hook's `ask` or an escalation request surfaces as a one-shot Allow/Reject prompt attached to the already-streamed tool call. `policy: never` is the unattended stance — every ask auto-rejects deterministically, stated in the system prompt, no human in the loop. `policy` is validated against the closed list at plugin load; anything else throws.
What a composed deployment observes: `allowed-once` lets exactly that call proceed; rejection, dismissal, and channel absence deny with three distinct reasons the model can tell apart; every ask lands a durable `approval/asked`/`approval/decided` pair on the asking agent's session log; nothing about a grant persists past the call that asked.
@@ -21,7 +21,7 @@ The system-prompt assembly owns the canonical model-facing tool order, exactly w
Scope is deliberately narrow: this fixes the REGISTRATION-ORDER race, not plugin behavior. A `system-prompt/assemble` listener may still add, remove, or rearrange tools — same as it may edit sections after their sort — and owns the determinism of what it emits; the waterfall contract already demands deterministic listeners (the reconstructability invariant would catch a listener that diverges between build and replay).
Config plumbing follows the `persona` precedent, and `toolOrder` sits beside it: the app configs (`dsh-stdio-agent`, `dsh-acp-agent`) accept the key and forward it through `dsh-agent-core` (whose schema is the intersection of the owners' schemas) to the `SystemPrompt` child. One schemastery footnote is load-bearing: a schemastery array defaults to `[]`, but an omitted `toolOrder` must stay ABSENT (= lexicographic) rather than become an explicitly-configured empty list (invalid — it lacks the rest entry), so every schema on the chain forces the default to `undefined`.
Config plumbing follows the `persona` precedent, and `toolOrder` sits beside it: the app configs (`dsh-stdio-demo`, `dsh-acp-demo`) accept the key and forward it through `dsh-agent-spine-demo` (whose schema is the intersection of the owners' schemas) to the `SystemPrompt` child. One schemastery footnote is load-bearing: a schemastery array defaults to `[]`, but an omitted `toolOrder` must stay ABSENT (= lexicographic) rather than become an explicitly-configured empty list (invalid — it lacks the rest entry), so every schema on the chain forces the default to `undefined`.
## Alternatives considered
@@ -70,7 +70,7 @@ Backend profiles share the mode contract but differ in necessary host grants. La
#### The bash consumer
`dsh-bash-sandbox` reuses local process execution and asks `ctx.sandbox` to wrap the exact bash argv. A kernel denial is a result fact independent of exit status and is inferred only from the selected wrap's stderr dialect. Runner failure outranks denial because it means the command never ran: foreground calls throw `SANDBOX_UNAVAILABLE`, while settled background tasks set `sandbox.runnerFailed` for `bash_output`. This keeps broken confinement distinct from both task failure and an enforced denial.
`dsh-bash-sandbox` extends `LocalBashExecutor` and hands `ctx.sandbox` the exact `['bash', '-c', command]` argv it is about to spawn. A denial is an orthogonal result fact, conservatively classified from the active runner's stderr dialect. A runner failure outranks denial: foreground execution throws `SANDBOX_UNAVAILABLE`; a settled `BashProcess` stamps `sandbox.runnerFailed`, and the bash producer renders it through generic `task_output`.
The model's view is result facts only: the static tool description explains the denial marker (`[sandbox: file access denied under <mode> mode]`), encourages attempting commands that may be denied, and forbids retrying around a denial; when the escalation fields are advertised, a denied result additionally carries the escalation hint itself, so the sanctioned same-turn retry is prompted at the decision point rather than depending on the model recalling the description (§ Escalation). No prompt section states the sandbox mode (§ Per-session modes).
@@ -78,13 +78,13 @@ The model's view is result facts only: the static tool description explains the
`BashExecRequest.sandboxMode` is an optional per-call input; resolved specs make the field explicit. `BashExecutor.sandboxMode` advertises whether the mounted executor can honor it, so only a confining composition exposes escalation. The seam accepts any explicit mode; the tool owns the wider-only escalation rule. Non-sandboxing executors remain honestly unconfined.
`SandboxBashExecutor.resolve()` stamps the effective mode — escalation grant > session override > configured default — so `run()`/`start()` read the spec, never the config. The `danger-full-access` branch, the confine call, and the result facts all key off the spec's mode, and the per-task facts map carries each task's mode alongside its wrap facts (`notifyTaskDone()` stamps from the map entry): one escalated call — foreground or background — reports the mode it ACTUALLY ran under while every neighbor keeps its own.
`SandboxBashExecutor.resolve()` stamps the effective mode — escalation grant > session override > configured default — so `run()`/`start()` read the spec, never the config. Per-process wrap facts are keyed by the returned `BashProcess`; `onProcessDone()` classifies stderr and stamps that handle before `done` resolves, so overlapping processes retain their own modes and runner dialects.
When a confining executor is mounted, `bash` advertises paired `sandbox_permissions` and `justification` fields. The schema exposes the full closed escalation vocabulary because effective mode is per-session; execution rejects any target that is not strictly wider than that call's effective mode. Approval resolves before execution. `allowed-once` stamps the granted mode onto only that request, while `rejected`, `cancelled`, `unavailable`, a missing approval service, or a missing agent all fail closed with distinct results. No grant is persisted.
Escalation is a same-turn retry of the denied command with the narrowest sufficient `sandbox_permissions` and a `justification`; the approval prompt is the consent step. It must be grounded in an actual denial, except when the session already observed the same denied access, and a disabled or rejected approval ends that command. The retry, approval decision, and result use existing tool and approval events. `dsh-tool-bash` owns the ask because the executor seam has neither the agent nor call id required for user interaction.
Left open, recorded for the phase that picks them up: what a grant's scope identity is beyond the sandbox mode — the exact call, a path, a command prefix, the session, a time window — the question `allow_always` grant storage must answer before that option can be advertised; and how escalation is defined for `run_in_background` denials that arrive via `bash_output`.
Left open: what a durable grant's scope identity is beyond the sandbox mode — exact call, path, command prefix, session, or time window — before an `allow_always` option can be advertised.
#### Per-session modes: the session log as the store
@@ -194,8 +194,8 @@ Costs and accepted limits:
- **`bwrap` is installed on my host but unusable (disabled unprivileged userns, an LSM denying `mount`) — what happens?** The chain probe is functional — it builds and enforces a real profile rather than checking `--version` — so a present-but-unusable `bwrap` fails its probe, selection falls to the registry-installed Landlock launcher, and the verdict is cached for the provider's lifetime.
- **Does the sandbox restrict network or process visibility?** No — `SandboxMode` claims FILE effects only; the bwrap profile deliberately does not unshare pid, and no backend claims network. Whether network restriction becomes its own knob is left open in § The seam.
- **Which tools actually run confined?** OS subprocesses through `ctx.bash` — the bash tools, and hook commands transitively. fs/web/todo execute in-process, where an `execve` wrapper is mechanically meaningless; their `read-only` semantics arrive with the cross-family deferred phase, and until then the contract says bash-only honestly.
- **Does a granted escalation persist, or cover background tasks?** Neither: the grant is consumed by the very call that asked (foreground or background), that one call reports the mode it actually ran under, and every neighbor keeps its own. How escalation should be DEFINED for a background denial that only surfaces later via `bash_output` is left open in § Escalation.
- **When does an editor's mode switch take effect?** Mid-turn: appended immediately, honored by the very next call's stamp. Idle: held on the bridge's session record, anchored at the next `agent/prompt-submit` inside its open turn, with N flips coalescing to at most one event (none if net-zero); a crash before anchoring reverts it and `session/load` reports the truth. The model is not told — its next command simply behaves under the new mode.
- **Does a granted escalation persist?** No. The grant is consumed by the exact foreground or background call that asked; every neighboring call keeps its own effective mode. A later background denial surfaces through `task_output` and may ground a new exact-command retry.
- **When does an editor's mode switch take effect?** Mid-turn: appended immediately, honored by the very next call's stamp. Idle: held on the bridge's session record, anchored at the next turn's `agent/prompt-submit`, with N flips coalescing to at most one event (none if net-zero); a crash before anchoring reverts it and `session/load` reports the truth. The model is not told — its next command simply behaves under the new mode.
- **What survives a restart — and what if the operator changed the config default while the process was down?** Overrides replay from the session log (`effective = fold ?? config`), so a resumed session keeps its modes with zero catch-up machinery; a default that drifted offline changes behavior the same way a switch does (the approval policy, being stated, is additionally narrated with operator/config attribution).
- **What does `enforcement: 'partial'` on a result mean?** The selected backend enforces the subset its kernel ABI governs — e.g. Landlock before ABI v3 does not govern path truncate — and says so structurally instead of refusing the host; the probe's report line distinguishes the cases. The bwrap and Seatbelt profiles govern every promised file effect by construction, so they always report `full`.
@@ -204,7 +204,7 @@ Costs and accepted limits:
In-repo precedents this design copies or contrasts with:
- [The capability-seams RFC](../architecture/2026-06-13-capability-seams.md) — the interface/implementation/consumer split and the "don't split preemptively" timing rule the second consumer satisfied.
- The `dsh-bash` request/spec split and its `owner` field ([the bash vocabulary catalog](../../../core-data-structures/bash.md)) — the per-call carrier template `sandboxMode` rides, and the explicit-`resolve()` defaulting convention.
- The `dsh-bash` request/spec split ([the bash vocabulary catalog](../../../core-data-structures/bash.md)) — the per-call carrier template `sandboxMode` rides, and the explicit-`resolve()` defaulting convention.
- [The approval seam RFC](2026-07-06-approval-seam.md) — the channel escalation asks through; its answerer waterfall, audit pair, and one-package rationale are recorded there.
- [Event-sourced sessions](../architecture/2026-06-11-event-sourced-sessions.md) and [the turn-enclosure invariant](../architecture/2026-06-15-turn-enclosure-invariant.md) — the log-as-store foundation the per-session modes fold over, and the commit boundary the anchoring design obeys.
- [The interception-seams RFC](2026-06-30-interception-seams.md) — the `tools/pre-execute` vocabulary the escalation gate deliberately does not reuse (an escalating call has no pre-execute moment of its own).
@@ -0,0 +1,212 @@
# RFC: MCP client plugin — connect to external MCP servers and bridge their tools
Status: implemented
## Problem
The harness had no way to consume tools from the MCP (Model Context Protocol) ecosystem. MCP is the emerging standard for tool servers — GitHub, filesystem, databases, code search, and hundreds of community servers expose tools via MCP. Users want to point the harness at one or more MCP servers and have their tools appear as native model-facing tools, without writing per-server glue code.
The `ToolRegistry` already accepts raw JSON Schema tool definitions (documented in `dsh-tools` README: "Raw JSON-Schema tool definitions (from MCP servers) are still accepted by `ToolRegistry.register()` directly"), and the extension cookbook sketches the intended pattern ("MCP | one plugin per server: discover tools → `ctx.tools.register()`"). The infrastructure was ready; the bridge plugin was missing.
## Decision
### Package
A single package `@deepseek-ai/dsh-mcp-client` at `packages/mcp/mcp-client/`. No capability-seam three-package split — there is no foreseeable second MCP client implementation, and the convention is "don't split preemptively" ([capability seams RFC](../../implemented/architecture/2026-06-13-capability-seams.md)).
### SDK
Use the official [`@modelcontextprotocol/sdk`](https://github.com/modelcontextprotocol/typescript-sdk) (`Client`, `StdioClientTransport`, `StreamableHTTPClientTransport`). The harness does not implement its own JSON-RPC — consistent with how ACP delegates to `@agentclientprotocol/sdk`.
### Scope
MCP Client only (no server side — ACP already covers the "expose harness as an agent" role). Bridge **Tools** only — Resources and Prompts are deferred (they require harness-side consumption mechanisms that don't exist yet, and design space is large).
### Plugin shape
Namespace plugin (named exports `name`/`inject`/`Config`/`apply`, no `export default`). `inject: ['tools']`. Each MCP server is one plugin instance in `cordis.yml` — the same package loaded N times with different configs, like `dsh-tool-subagent`.
### Configuration
Flat discriminated union on the `transport` field:
```typescript
interface StdioConfig {
transport: 'stdio'
serverName: string // required namespace, ^[A-Za-z0-9_-]{1,32}$
command: string
args?: string[]
env?: Record<string, string>
cwd?: string
toolCallTimeoutMs?: number // default 60_000
}
interface StreamableHttpConfig {
transport: 'streamable-http'
serverName: string // required namespace, ^[A-Za-z0-9_-]{1,32}$
url: string
headers?: Record<string, string>
toolCallTimeoutMs?: number // default 60_000
}
type Config = StdioConfig | StreamableHttpConfig
```
`serverName` is the stable local identity that namespaces this server's tools in the model-facing name (below). It is deliberately user configuration, NOT the remote `serverInfo.name`: the remote name is untrusted input, is not unique across deployments (prod and staging instances of one server report the same name), and may change on server upgrade — none of which may silently rename model-facing tools. A duplicate `serverName` across live instances is a configuration error: the later instance fails at load with an actionable message, never silent shadowing or skipping. A short `serverName` (`gh`) is also the knob for shortening public names.
Example `cordis.yml` usage:
```yaml
- id: mcp-github
name: '@deepseek-ai/dsh-mcp-client'
config:
serverName: github
transport: stdio
command: npx
args: ['-y', '@modelcontextprotocol/server-github']
env:
GITHUB_TOKEN: !!js process.env.GITHUB_TOKEN
- id: mcp-web
name: '@deepseek-ai/dsh-mcp-client'
config:
serverName: web
transport: streamable-http
url: http://localhost:3000/mcp
headers:
Authorization: !!js `Bearer ${process.env.MCP_TOKEN}`
```
The model sees `mcp__github__create_issue`, `mcp__github__search_code`, `mcp__web__search`.
### Lifecycle
Boot-time from `cordis.yml`. HMR (`@cordisjs/plugin-hmr`) provides hot-swap: editing the yml entry triggers dispose of the old instance (disconnects, unregisters tools) and creation of a new one (connects, discovers, registers). No runtime-dynamic API for now. Public names are pure functions of `(serverName, rawName)`, so an HMR swap that keeps `serverName` recreates identical model-facing names — session history and permission rules stay valid — and adding or removing an unrelated server never renames an existing tool.
### Tool discovery and registration
Every MCP tool has two names:
- `rawName` — the exact MCP `Tool.name`, used only on the wire (`tools/call`).
- `publicName` — the globally unique model-facing name registered in the `ToolRegistry`:
mcp__<serverName>__<rawName>
This server-qualified shape is the de-facto standard among multi-server agent clients — every surveyed end-user product qualifies MCP tools by server ([Claude Code](https://code.claude.com/docs/en/agent-sdk/mcp#tool-naming-convention) `mcp__github__list_issues`, [Codex](https://openai.com/index/unrolling-the-codex-agent-loop/) `mcp__weather__get-forecast`, [Gemini CLI](https://geminicli.com/docs/tools/mcp-server/#3-tool-naming-and-namespaces), [VS Code](https://github.com/microsoft/vscode/blob/ab9ec62c6a61e429a9abd612ff220c3f4834c9ea/src/vs/workbench/contrib/mcp/common/mcpServer.ts#L217-L260), [Cline](https://github.com/cline/cline/blob/52fdbb1d72f7324a28142a7ba7678d4b53c902f4/sdk/packages/core/src/extensions/mcp/name-transform.ts#L20-L35), [Roo Code](https://github.com/RooCodeInc/Roo-Code/blob/b867ec9145750d0ae1ff7f02d35406e9bf2a0b16/src/utils/mcp-name.ts#L117-L140), [Goose](https://github.com/block/goose/blob/b3a012cbdde854b0fe14f95b1c48543bf6517c0a/crates/goose/src/agents/extension_manager.rs#L1391-L1441), [OpenCode](https://github.com/anomalyco/opencode/blob/d199b1bff90282a4f9cd6251b5fc7b16875a52f6/packages/opencode/src/mcp/catalog.ts#L117-L120)); the exact `mcp__<server>__<tool>` spelling follows Claude Code and Codex. The `mcp__` marker keeps MCP registrations out of the native tools' namespace and gives permission/telemetry rules a stable shape (`mcp__*`, `mcp__github__*`).
1. On connect: drain `client.listTools()` pagination, derive every tool's `publicName`, then register each as a raw `ToolDefinition` via `ctx.tools.register()`. The MCP JSON Schema and description pass through unchanged (no `defineTool` DSL conversion); only the model-facing `name` is replaced.
2. Listen for `notifications/tools/list_changed` → re-run the same sync (dispose previous generation, register new). Deterministic names mean unchanged tools keep their names across re-syncs.
3. The executor closes over `rawName`; the public name is never sent to the server and never parsed to recover the raw name.
4. No `presentCall`/`presentResult` — the ACP bridge's generic-card fallback handles rendering.
5. Tools are transparent in the system prompt — no "[via MCP]" annotation beyond the name itself.
### Public name normalization
MCP allows tool names up to 128 characters including `.`; the DeepSeek function-name contract allows `[A-Za-z0-9_-]` and at most 64. Public names are normalized deterministically: invalid characters become `_`, and when replacement or truncation changed the name, a 12-hex-char SHA-256 hash of the `(serverName, rawName)` identity is appended so distinct MCP identities can never collapse into the same public name:
```typescript
function publicToolName(serverName: string, rawName: string): string {
const joined = `mcp__${serverName}__${rawName}`
const normalized = joined.replace(/[^A-Za-z0-9_-]/g, '_')
if (normalized === joined && normalized.length <= 64) return normalized
const hash = sha256(`${serverName}\0${rawName}`).slice(0, 12)
return `${normalized.slice(0, 64 - 13)}_${hash}`
}
```
### Name conflict handling
MCP guarantees tool-name uniqueness only [within one server](https://modelcontextprotocol.io/specification/2025-11-25/server/tools#tool-names); cross-server collisions are the norm, not the exception (a [Microsoft Research survey](https://www.microsoft.com/en-us/research/blog/tool-space-interference-in-the-mcp-era-designing-for-agent-compatibility-at-scale/#namespacing-issues-and-naming-ambiguity) of 1,470 servers found 775 colliding tool names; `search` alone appears in 32 servers, and the official GitHub server publishes bare `create_issue`). The always-on namespace makes collisions structurally impossible instead of handling them at collision time:
- Two servers publishing `search` coexist as `mcp__github__search` and `mcp__web__search`.
- A native harness tool named `search` is unaffected.
- Duplicate `serverName` config fails the later instance at load (see Configuration).
- A server listing the same tool name twice is an invalid tool list: the sync throws and the previous generation stays registered.
- A registry conflict during the swap can only mean a foreign tool squats on this server's `mcp__<serverName>__` namespace: the partial generation is rolled back (zero tools from this server) and the error is logged loudly.
Tools are never silently skipped; which tools are available never depends on plugin load order.
### Naming invariants
1. Every MCP tool has the stable identity `(serverName, rawName)`; every active identity has exactly one public name.
2. Public names are deterministic, globally unique, and satisfy the DeepSeek 64-char `[A-Za-z0-9_-]` contract.
3. MCP `tools/call` always receives the original raw name.
4. Connecting, disconnecting, or re-syncing an unrelated server never renames an existing tool.
5. Registration order never determines which tool is available.
### Tool execution
A unified `execute` handler for all tools from one MCP server:
1. Resolve `rawName` (the executor closes over it) and call `client.callTool({ name: rawName, arguments }, { signal: exec.signal })` with the configured timeout — the public name is never sent to the server.
2. Map the result:
- Multiple `text` content blocks → join with `'\n'` into a single `TextBlock` (required: `flattenText` uses `join('')` without separator, so multiple blocks would lose inter-block boundaries).
- `image` content blocks → discard with a `ctx.logger.warn` (the harness has no image content block type; [drop-image RFC](../../implemented/simplification/2026-07-04-drop-image-content-block.md)).
- `isError: true` → map to the harness `isError` result path (`{ content: [...], isError: true }`).
3. Cancellation: `exec.signal` (from the agent loop's cancel) is passed through to the MCP SDK's `callTool`, which sends `$/cancelRequest` to the server.
### Subprocess environment (stdio transport)
Replicate the `buildChildEnv` + `SENSITIVE_ENV_PATTERN` scrub from `dsh-subagent-acp`: filter ambient env (strip credential-shaped vars matching `/KEY|SECRET|TOKEN/i`), then merge `config.env` on top. Explicit env overrides survive the scrub.
### Disconnection / crash
No auto-reconnect. If the MCP server process exits or the transport closes:
1. The effect disposes → all registered tools are unregistered (fiber-scoped disposers).
2. Subsequent model calls to those tools → `ToolNotFoundError``isError: true`.
3. Recovery: user edits `cordis.yml` (triggers HMR reload) or restarts the harness.
This matches the ACP subagent pattern: "crash = terminal, report error, clean up, don't retry."
## Alternatives considered
### MCP Server side (expose harness tools to external MCP clients)
Deferred. The ACP bridge already exposes the harness as an agent server. Adding an MCP server layer would duplicate that with a different protocol, and the primary user need is consuming external tools, not exposing them.
### Capability-seam three-package split (interface / impl / consumer)
Rejected. There is no foreseeable alternative MCP client implementation — MCP has one protocol, one SDK. The convention is "don't split preemptively" until a second implementation appears.
### Auto-reconnect with exponential backoff
Rejected for v1. Adds complexity (partial-availability state where tools are registered but temporarily non-functional), and stdio process crashes usually indicate a configuration problem that retrying won't fix. HMR already provides the manual recovery path. Can be added as a future `reconnect: boolean` config if needed.
### Bridge Resources and Prompts
Deferred. Resources need a harness-side mechanism to decide WHEN to inject content (system prompt? on demand? model-triggered?). Prompts need a "prompt template" concept the harness lacks. Both require their own design; Tools are the high-value, low-risk starting point.
### Raw model-facing tool names with an optional `toolPrefix`
Rejected — this was the original proposal, built on the premise that "most MCP servers already use semantic prefixes in their tool names (e.g. `github_create_issue`)". The premise is false: the official GitHub server publishes `create_issue`, the reference filesystem server `read_file`, Sentry `search_issues` — and the Microsoft survey above shows collisions are common at ecosystem scale. Collision-time prefixing (or warn-and-skip) also makes the available tool set depend on plugin load order, and a tool could be silently renamed when an unrelated server is added — invalidating session history and permission rules mid-conversation. No surveyed multi-server agent product ships raw names.
### Server-only namespace (`github__create_issue`, no `mcp__` marker)
Rejected for v1. It prevents cross-server collisions but does not separate MCP registrations from native harness tools, and it forfeits MCP-wide policy shapes (`mcp__*`). The marker costs 5 characters; the `mcp__<server>__<tool>` spelling matches Claude Code and Codex, maximizing model familiarity. If the ToolRegistry later grows source-aware namespaces, dropping the literal marker can be revisited as a naming-policy change.
### Deriving the namespace from the server-announced `serverInfo.name`
Rejected. The remote name is untrusted, non-unique across deployments, and changeable on upgrade; tool identity and permission rules must not silently follow it. The namespace is local configuration.
### Preserve multiple TextBlocks in tool result
Rejected. `flattenText()` in the DeepSeek serializer uses `join('')` (no separator) when flattening `ContentBlock[]` to wire format. Multiple text blocks would silently lose inter-block boundaries — a correctness bug. All existing tools return a single TextBlock; the MCP bridge follows suit.
## Testing
Coverage is named per tier; each behavior lives at the cheapest tier that can express it.
- **Unit** (`tests/mcp-client.spec.ts`, `tests/apply.spec.ts`, mocked MCP SDK): the `publicToolName` algorithm (clean, normalize, truncate-and-hash, determinism, distinct-identity separation), raw-vs-public wire discipline, cross-server and native-tool coexistence, duplicate-`serverName` load failure and reservation release, invalid-tool-list rejection, generation swap/rollback, failed-re-sync retention, result mapping, cancellation, config schema validation. 100% per-file coverage gates the package.
- **E2E** (`tests/mcp-client.e2e.ts`, keyless): the real MCP protocol against the in-repo fixture server, `@modelcontextprotocol/server-everything`, and `@modelcontextprotocol/server-filesystem` over stdio, and against an in-process `StreamableHTTPServerTransport` server over Streamable HTTP — discovery under the namespace, dotted-name normalization end to end, execution round-trips, duplicate-`serverName` rejection, disposal.
- **Snapshot**: deliberately none. MCP tools introduce no new transcript surface — they register as raw `ToolDefinition`s and render through the ACP bridge's generic-card fallback, which the bridge's unit suite already pins (`packages/ui/acp/tests/stream-update.spec.ts`). Adding an MCP server to the snapshot example's `cordis.yml` would mutate the pinned `text-turn` system-prompt fixture (forcing a with-key re-record of every recorded golden) and make every replay depend on spawning an external MCP server process — for zero new rendering behavior. If a later change gives MCP tools their own render intent, that change names its snapshot coverage then.
## Consequences
- A `cordis.yml` entry per MCP server is the entire integration cost: `serverName: filesystem` + a stdio command (or a Streamable HTTP URL) puts `mcp__filesystem__read_file` in the model's tool list, callable, with the raw `read_file` on the wire.
- Public names are part of session history and permission/config surfaces; the naming algorithm is a v1 contract pinned by tests, and changing it after release is a breaking change.
- The `mcp__<serverName>__` qualifier costs tokens on every name. Accepted: descriptions and JSON schemas dominate tool-definition tokens, and the qualifier buys stable identity, collision isolation, and MCP-wide policy shapes (`mcp__*`, `mcp__github__*`).
- **MCP SDK stability**: the `@modelcontextprotocol/sdk` is still evolving; breaking changes require updating the bridge. The version is pinned, and the SDK is widely adopted (Claude Desktop, Cursor, VS Code) so breaking changes are unlikely to be silent.
- **Tool schema quality**: MCP servers may expose poorly-described tools (vague descriptions, incomplete JSON schemas). The harness passes them through as-is — garbage-in-garbage-out; that is the server author's responsibility, not the bridge's.
- **Stdio process management**: a misbehaving MCP server that ignores signals could wedge dispose. The Cordis fiber disposal has bounded quiescence; a stuck transport eventually times out at the framework level.
- Crash recovery is manual (HMR edit or restart) — accepted for v1; a `reconnect` config remains open as future work.
@@ -0,0 +1,62 @@
# RFC: Background subagent tasks
Status: implemented
## Problem
The [subagent seam](2026-06-21-subagent-capability-seam.md) returns a `SubagentRun`, but the model-facing tool originally collected every run synchronously. Independent, slow delegations therefore held the parent call open or ran serially.
Subagents need the same start, collect, list, stop, ownership, notification, and cleanup behavior as other long-running tools without adopting process-stream semantics. The child session remains the detailed trace; the parent needs the final answer and task status. A background child also outlives its starting tool call, so its cancellation and owner-disposal contracts must be explicit.
## Decision
Each `dsh-tool-subagent` instance may expose `run_in_background`, controlled by `enableRunInBackground` and enabled by default. A disabled instance omits the parameter and rejects a forced background argument at execution. Provider selection remains deployment configuration, so one instance still registers one distinctly named tool for one provider.
Background subagents use the [generic background task runtime](../architecture/2026-06-20-generic-long-running-tool-runtime.md). Collection, listing, cancellation, completion notices, and prompt guidance come from `task_output`, `task_list`, and `task_kill`; there are no subagent-specific companion tools.
Foreground calls retain their synchronous contract: await provider startup and `run.result`, return final text only for `completed`, map other terminal reasons to an errored tool result, and always dispose the run before returning.
For a background call, the tool validates the parent and refuses an already-aborted execution signal before calling `ctx.tasks.start()`. The task runtime preflights the control surface and owner cleanup before invoking the producer starter. That starter creates an independent `AbortController` and begins `ctx.subagents.start()`; after the id is returned, the tool-call signal no longer owns the child.
The task registration maps the subagent seam as follows:
- `kind` is `subagent`, `label` is the model-supplied description, and `owner` is the parent agent.
- `cancel(reason?)` aborts the task-owned controller. The same signal covers pending provider startup and the ready child.
- `done` awaits provider startup, the child result, and `run.dispose()`. Completed runs return final text, aborted runs become `killed`, and other stop reasons become `failed`. Startup, result, and disposal failures become failed outcomes rather than rejected task promises.
- `readOutput` is absent. While live, `task_output` returns status only; after settlement, it returns final output idempotently. Intermediate child activity remains in the child session.
## Lifecycle
A background subagent belongs to its parent agent and is not durable across owner closure. The task runtime attaches cleanup to the exact owner's scope. Agent disposal cancels the task and awaits startup rollback or child disposal before `AgentHandle.dispose()` resolves, preventing leaked child agents and sessions.
Completion notices target the exact owner captured at start. If owner teardown has already disposed the injection target, the notice is dropped; cleanup, not notification, is the lifecycle guarantee.
## Model guidance
The generic task prompt teaches the shared habit: retain ids, continue independent work instead of busy-polling, collect relevant tasks before answering, and kill irrelevant work. The subagent schema adds only that background mode returns a task id and that `task_output` collects the result. Authorization and owner cleanup enforce the runtime boundary independently of prompt compliance.
## Alternatives considered
### Subagent-specific wait, output, and stop tools
Capability-specific tools would duplicate the task protocol, teach another collect-and-stop habit, and complicate multiple provider instances. The generic runtime provides the required behavior without changing the tool's one-provider-per-instance shape.
### Survival after owner closure
Survival requires persistent task state, child-session recovery, a late-result delivery channel, and policy for abandoned owners. Owner-scoped cleanup gives process-local work a clear lifetime. Durable jobs require a separate design.
### No owner checks for isolated clients
Agents and logs may be session-scoped, but the task registry and predictable ids are runtime-global. The generic owner fence therefore applies to subagents like every other producer.
### Incremental child transcript output
Streaming child history into the parent would blur the log boundary and make provider behavior diverge. This surface exposes final output only; richer observation belongs to session or UI tooling.
## Testing
Unit coverage pins stop-reason mapping, dispose-before-report behavior, startup and result failures, pre-aborted refusal, detachment from the starting call's signal, cancellation before and after provider readiness, collection through the real task tools, the no-surface preflight fence, missing-runtime failure, and per-instance schema gating. Snapshot coverage pins the model-facing schemas.
## Consequences
The parent can fan out slow delegations and collect them through the same task controls used by bash. Child work no longer occupies the starting tool call, but it can consume resources until collected, killed, or owner-disposed. Prompt guidance encourages collection; owner cleanup provides the hard lifetime boundary. Deployments that require synchronous delegation can disable background mode per tool instance.
@@ -2,5 +2,5 @@
# side as of the last confirmed-consistent state. Both languages carry equal authority;
# after editing either side, bring the other along and re-record with:
# pnpm run verify-translation-pairing --write
2026-07-14-time-context-plugin.md: 13e0eff4b9d286ee562d7a0a2c0a3a659126ba3b
2026-07-14-time-context-plugin.zh.md: 5ee50a4d49eb9a7e00a09f70b436e15d72612b1f
2026-07-14-time-context-plugin.md: 105bf53550f087fdefb1e6fe0ec493f8628d3e18
2026-07-14-time-context-plugin.zh.md: 60e9004b1453e75e1bcd84870ad7f18d200a95d8
@@ -12,7 +12,7 @@ Prompt assembly can derive both facts per step from durable session timestamps,
## Decision
`@deepseek-ai/dsh-time-context` is an opt-in function plugin at `packages/context/time-context/`. The `context/` product group holds bounded request-context enrichments that define neither a tool nor a service. `dsh-agent-core` and shipped examples do not load the package; deployments mount it explicitly when its token and disclosure costs are acceptable.
`@deepseek-ai/dsh-time-context` is an opt-in function plugin at `packages/context/time-context/`. The `context/` product group holds bounded request-context enrichments that define neither a tool nor a service. `dsh-agent-spine-demo` and shipped examples do not load the package; deployments mount it explicitly when its token and disclosure costs are acceptable.
The plugin registers the global `context:time` system-prompt section at order 10, after the deployment persona and before tool guidance. For an active turn it emits an ISO-shaped timestamp with numeric UTC offset and IANA zone, plus a compact whole-second duration since the last model-visible message before the turn opened. Bare and idle assemblies receive an empty section.
@@ -45,7 +45,7 @@ Unit tests pin formatting, baselines, refresh policy, validation, per-agent stat
- **Refresh from a background timer** — rejected because a new value has no consumer outside request assembly. Timer-driven `agent.inject()` would create turns and wake idle sessions merely to report time passing.
- **Keep UTC as the omitted default** — rejected because an explicitly enabled clock should follow its deployment environment unless the operator chooses UTC. `timeZone: UTC` remains available when a deployment requires it.
- **Add a time-zone detection library** — rejected because Node's `Intl` runtime already exposes the process's IANA zone. Another dependency cannot infer a remote user's zone either.
- **Mount the plugin in `dsh-agent-core`** — rejected because time zone, disclosure, token budget, and freshness are deployment policy. Opt-in keeps default context stable.
- **Mount the plugin in `dsh-agent-spine-demo`** — rejected because time zone, disclosure, token budget, and freshness are deployment policy. Opt-in keeps default context stable.
- **Place the package in `core/`** — rejected because `core/` owns the product API spine, while this plugin is an optional leaf with no service key.
## Consequences
@@ -12,7 +12,7 @@ Status: implemented
## 决策
`@deepseek-ai/dsh-time-context` 是位于 `packages/context/time-context/`、需要显式启用的函数插件。`context/` 产品分组用于容纳既不定义工具、也不定义服务的有界请求上下文增强。`dsh-agent-core` 和仓库提供的示例都不会加载该 package;只有当 token 与信息披露成本可接受时,部署方才显式挂载它。
`@deepseek-ai/dsh-time-context` 是位于 `packages/context/time-context/`、需要显式启用的函数插件。`context/` 产品分组用于容纳既不定义工具、也不定义服务的有界请求上下文增强。`dsh-agent-spine-demo` 和仓库提供的示例都不会加载该 package;只有当 token 与信息披露成本可接受时,部署方才显式挂载它。
该插件注册顺序值为 10 的全局系统提示词区段 `context:time`,位置在部署方角色设定之后、工具指导之前。对于活跃轮次,它会输出带数字 UTC 偏移和 IANA 时区、形似 ISO 的时间戳,以及从轮次开始前最后一条模型可见消息起算的紧凑整秒时长。未绑定 agent 或 agent 处于空闲状态时,该区段为空。
@@ -45,7 +45,7 @@ agent loop(智能体循环)会在发送前通过 `request/header` 和 `reque
- **通过后台计时器刷新**——不予采纳,因为请求组装之外没有消费新值的对象。由计时器驱动 `agent.inject()` 会创建轮次,并且只为报告时间流逝就唤醒空闲会话。
- **省略配置时仍默认使用 UTC**——不予采纳,因为显式启用的时钟应跟随部署环境,除非运维方选择 UTC。需要 UTC 的部署仍可配置 `timeZone: UTC`
- **引入时区探测库**——不予采纳,因为 Node 的 `Intl` 运行时已经能够提供进程的 IANA 时区,而且额外依赖同样无法推断远程用户的时区。
- **在 `dsh-agent-core` 中挂载插件**——不予采纳,因为时区、信息披露、token 预算和新鲜度都属于部署策略。选择加入能保持默认上下文稳定。
- **在 `dsh-agent-spine-demo` 中挂载插件**——不予采纳,因为时区、信息披露、token 预算和新鲜度都属于部署策略。选择加入能保持默认上下文稳定。
- **将 package 放入 `core/`**——不予采纳,因为 `core/` 负责产品 API 主干,而该插件是没有服务键的可选叶节点。
## 后果
@@ -2,5 +2,5 @@
# side as of the last confirmed-consistent state. Both languages carry equal authority;
# after editing either side, bring the other along and re-record with:
# pnpm run verify-translation-pairing --write
2026-07-02-bilingual-docs-and-pairing-gate.md: 1e96622e7fb5694ab61772d68744394ef1aeb53a
2026-07-02-bilingual-docs-and-pairing-gate.zh.md: c752d76f12f556ce190bf80c4f3a531c0821be8e
2026-07-02-bilingual-docs-and-pairing-gate.md: 68c0f3bbc0472b0c96f9d64fc6b1b24ac7008795
2026-07-02-bilingual-docs-and-pairing-gate.zh.md: 2cf8f9b9c17d8a521d8674833e909b34f315cfe0
@@ -12,7 +12,7 @@ This repo's README and docs tree are read by people and agents inside and outsid
- **Paired sibling files with equal authority.** A documentation pair is three sibling files: English `foo.md`, Chinese `foo.zh.md`, and a consistency record `foo.i18n.yaml`. Neither language is canonical — a document may be authored and reviewed Chinese-first and translated to English afterwards, or the reverse; what binds the pair is that both sides must say the same thing, and pairs merge whole (both languages plus the record, never one alone). Policy: [docs/i18n/README.md](../../../i18n/README.md); translation rules: [docs/i18n/translation-rules.md](../../../i18n/translation-rules.md); terminology source of truth: [docs/i18n/terminology.md](../../../i18n/terminology.md).
- **A sidecar record of both blob hashes makes consistency checkable.** `foo.i18n.yaml` holds the full git blob hash of each side as of the last confirmed-consistent state. An edit to either side without re-confirming the pair is then mechanically detectable as a pure content comparison — no history lookup — and the hashes are computable for files edited in the same PR, which a commit-hash record is not. Re-recording (`verify-translation-pairing --write`) produces a reviewable yaml diff: confirming consistency is an explicit, visible act in the PR.
- **`verify-translation-pairing` joins `doc-sync`.** The gate ([scripts/verify-translation-pairing.ts](../../../../scripts/verify-translation-pairing.ts)) enforces: required pairs exist, every existing pair is complete (all three files) and consistent (both hashes match, switcher links both ways, structural signatures identical), and excluded (generated or bilingual-by-construction) files stay unpaired. The `required` list in [scripts/translation-pairing.manifest.json](../../../../scripts/translation-pairing.manifest.json) is a ratchet: each merged translation batch adds its files, so coverage only grows.
- **`verify-translation-pairing` joins `doc-sync`.** The gate ([scripts/verify-translation-pairing.ts](../../../../scripts/verify-translation-pairing.ts)) enforces: required pairs exist, every existing pair is complete (all three files) and consistent (both hashes match, switcher links both ways, structural signatures identical), excluded (generated or bilingual-by-construction) files stay unpaired, and date-named documents on or after the manifest's `requiredSince` cutoff have complete pairs. The `required` list in [scripts/translation-pairing.manifest.json](../../../../scripts/translation-pairing.manifest.json) is a ratchet: each merged translation batch adds its files, so coverage only grows.
- **Translation is agent work with human review.** The committed workflow is [.agents/skills/dsh-translate-docs](../../../../.agents/skills/dsh-translate-docs/SKILL.md), following the same pattern as [dsh-code-review](../../../../.agents/skills/dsh-code-review/SKILL.md): the skill carries the workflow and defers to the docs as sources of truth.
## Alternatives considered
@@ -34,5 +34,5 @@ Paired sibling files with locale suffixes are the dominant Chinese big-tech conv
- Every pair adds a third file to the tree. The record is machine-written (`--write`), so the cost is directory noise, not maintenance effort; in exchange, "who confirmed these consistent, and when" is answerable from git blame on the yaml.
- When the two sides disagree, no mechanical rule picks a winner — the PR review does. That is the price of equal authority, accepted deliberately: the alternative (a canonical language) forbids Chinese-first authoring.
- Generated docs (`cordis-catalog/`, `tool-catalog/`, `module-graph.md`) are excluded for now; the planned follow-up is to teach their generators to emit Chinese alongside English, at which point they leave the exclusion list.
- Rollout is incremental by design: documents outside `required` are visible backlog (`--list`), not red CI, so pairs land in reviewable batches without a big-bang PR. New documents are the exception — a date-named document dated on/after the manifest's `requiredSince` cutoff merges bilingual or not at all, so the backlog only ever shrinks.
- Rollout is incremental by design: documents outside `required` are visible backlog (`--list`), not red CI, so pairs land in reviewable batches without a big-bang PR. A date-named document dated on or after the manifest's `requiredSince` cutoff merges bilingual or not at all, so new date-named RFCs do not enlarge that backlog.
- The recorded hashes double as the update tool (`git cat-file -p <hash>` recovers either side's last-confirmed text for a minimal diff-based update), so re-translation of whole files is never forced by the mechanism.
@@ -11,9 +11,9 @@ Status: implemented
## 决策
- **配对兄弟文件,两种语言同权。** 一对文档由三个兄弟文件组成:英文 `foo.md`、中文 `foo.zh.md`,以及一份一致性记录 `foo.i18n.yaml`。没有哪种语言是正典:一篇文档可以先用中文撰写和评审、之后再译成英文,反之亦可;约束配对的是:两侧必须表达相同的内容,且配对整体合并(两种语言加记录,绝不单独落一侧)。政策见 [docs/i18n/README.md](../../../i18n/README.md);翻译规则见 [docs/i18n/translation-rules.md](../../../i18n/translation-rules.md);术语真源见 [docs/i18n/terminology.md](../../../i18n/terminology.md)。
- **伴随记录保存两侧 blob hash,使一致性可检查。** `foo.i18n.yaml` 保存两侧文件在上一次确认一致时各自的完整 git blob hash。此后修改了任一侧而未重新确认配对,都能被机械检测出来(纯内容比较,无需查询历史),而且同一个 PR 内改动的文件也能计算出 hash,commit hash 式的记录做不到这一点。重新记录(`verify-translation-pairing --write`)会产生一份可评审的 yaml diff:确认一致在 PR 中是一个显式、可见的动作。
- **`verify-translation-pairing` 加入 `doc-sync`。** 门禁([scripts/verify-translation-pairing.ts](../../../../scripts/verify-translation-pairing.ts))强制执行以下规则:required 的配对必须存在;任何已存在的配对必须完整(三个文件齐全)且一致(两个 hash 匹配、切换行双向互链、结构签名一致);被排除的文件(生成物或本身即双语的)保持不配对。[scripts/translation-pairing.manifest.json](../../../../scripts/translation-pairing.manifest.json) 中的 `required` 清单只进不退:每个合并的翻译批次将自己的文件加入其中,覆盖面只增不减。
- **翻译是 agent 的工作,由人评审。** 仓库内置的工作流是 [.agents/skills/dsh-translate-docs](../../../../.agents/skills/dsh-translate-docs/SKILL.md),与 [dsh-code-review](../../../../.agents/skills/dsh-code-review/SKILL.md) 模式相同:skill 承载工作流,并将文档作为真源。
- **伴随记录保存两侧 blob hash,使一致性可检查。** `foo.i18n.yaml` 保存两侧文件在上一次确认一致时各自的完整 git blob hash。此后修改了任一侧而未重新确认配对,都能被机械检测出来(纯内容比较,无需查询历史),而且同一个 PRPull Request内改动的文件也能计算出 hash,commit hash 式的记录做不到这一点。重新记录(`verify-translation-pairing --write`)会产生一份可评审的 yaml diff:确认一致在 PR 中是一个显式、可见的动作。
- **`verify-translation-pairing` 加入 `doc-sync`。** 门禁([scripts/verify-translation-pairing.ts](../../../../scripts/verify-translation-pairing.ts))强制执行以下规则:required 的配对必须存在;任何已存在的配对必须完整(三个文件齐全)且一致(两个 hash 匹配、切换行双向互链、结构签名一致);被排除的文件(生成物或本身即双语的)不得配对;凡文件名以日期开头且日期不早于 manifest(元数据清单)中 `requiredSince` 分界日期的文档,也必须有完整配对。[scripts/translation-pairing.manifest.json](../../../../scripts/translation-pairing.manifest.json) 中的 `required` 清单只进不退:每个合并的翻译批次将自己的文件加入其中,覆盖面只增不减。
- **翻译是 agent 的工作,由人评审。** 仓库内置的工作流是 [.agents/skills/dsh-translate-docs](../../../../.agents/skills/dsh-translate-docs/SKILL.md),与 [dsh-code-review](../../../../.agents/skills/dsh-code-review/SKILL.md) 模式相同:skill(技能)承载工作流,并将文档作为真源。
## 曾考虑的替代方案
@@ -34,5 +34,5 @@ Status: implemented
- 每个配对给目录树多添一个文件。记录由机器写入(`--write`),代价是目录噪音而非维护负担;换来的是「谁在何时确认过这对文档一致」可以从 yaml 的 git blame 直接回答。
- 两侧说法冲突时,没有机械规则裁决谁赢,由 PR 评审裁决。这是同权的代价,且是有意接受的:另一个选项(正典语言)会禁止中文先行撰写。
- 生成文档(`cordis-catalog/``tool-catalog/``module-graph.md`)暂被排除;计划中的后续工作是让生成器在输出英文的同时输出中文,届时将这些文件移出排除清单。
- 推进天然是渐进的:`required` 之外的文档是可见的 backlog`--list`),而非红色的 CI;因此配对按可评审的批次落地,无需一个巨型 PR。
- 推进天然是渐进的:`required` 之外的文档是可见的 backlog待翻清单,`--list`),而非红色的 CI;因此配对按可评审的批次落地,无需一个巨型 PR。凡文件名以日期开头且日期不早于 manifest 中 `requiredSince` 分界日期的文档,都必须配齐双语文件,因此新建的日期命名 RFC 不会增加这份 backlog。
- 记录的 hash 兼作更新工具(`git cat-file -p <hash>` 能还原任一侧上次确认的文本,用于基于 diff 的最小更新),因此这套机制从不强迫整篇重译。
@@ -31,7 +31,7 @@ The filesystem discovers the tool-package inventory and the completeness guard r
### Scope
Shipped product tool PACKAGES under `packages/*/tool-*`, each booted with its default config: `dsh-tool-bash` (`bash`, `bash_output`, `bash_kill`), `dsh-tool-todo` (`todo_write`), `dsh-tool-subagent` (`subagent`). The `examples/` demo tools (`echo`) are excluded, matching the cordis catalog's packages-only scope — a demo tool is not part of the product surface a reader is cataloguing.
Shipped product tool packages under `packages/*/tool-*`, each booted with its default config, including `dsh-tool-bash` (`bash`), `dsh-tool-tasks` (`task_output`, `task_list`, `task_kill`), and `dsh-tool-subagent` (`subagent`). Example-only tools are excluded.
The catalog unit is a package, not every configured tool instance. Each package boots once with default config; load-time aliases such as `subagent_fork` are noted without enumerating every deployment permutation. A deployment inventory is a separate, unbounded surface.
@@ -13,7 +13,7 @@ Set `engines.node` to `^22.19.0 || >=24.0.0` and test the keyless CI compatibili
Two Node features gate the source runtime:
- **`node:sqlite`** — `packages/session-persistence/session-persistence-sqlite` does a top-level `import { DatabaseSync } from 'node:sqlite'`. The module dropped its `--experimental-sqlite` flag requirement at **22.13** (LTS) and **23.4** (Current); before those, importing it throws at load.
- **Native TypeScript type-stripping** — the `packages/ui/stdio-agent/tests/built-bin.e2e.ts` smoke boots the published `lib/bin.js` under plain `node` (no tsx) and loads the example's `.ts` plugins (`mock-llm.ts`, `echo-tool.ts`). Type-stripping is the default from **22.18** (LTS) and **23.6** (Current); before those it needs `--experimental-strip-types`.
- **Native TypeScript type-stripping** — the `packages/examples/stdio-demo/tests/built-bin.e2e.ts` smoke boots the published `lib/bin.js` under plain `node` (no tsx) and loads the example's `.ts` plugins (`mock-llm.ts`, `echo-tool.ts`). Type-stripping is the default from **22.18** (LTS) and **23.6** (Current); before those it needs `--experimental-strip-types`.
Those source features clear on the 22.x line at **22.18**, but the installed Pi adapter dependency raises the advertised LTS floor. `@deepseek-ai/dsh-llm-pi-ai` depends on `@earendil-works/pi-ai@0.79.3`, whose package declares `engines.node >=22.19.0`, so the LTS floor is **22.19**. The 24.x branch remains `>=24.0.0`. The disjoint range excludes Node 23 entirely: Node 23.023.5 still has at least one flagged source feature, and the 23 line is non-LTS/EOL, so advertising `>=23.6` would add a dead release line and a CI leg no deployment should use.
@@ -4,7 +4,7 @@ Status: implemented
## Problem
The readline UI was a whole package (`@deepseek-ai/dsh-ui-stdio` under `packages/support/`) whose only runtime importer was the app package `@deepseek-ai/dsh-stdio-agent`. The examples reach the readline UI by loading the app, never by composing the helper themselves; every other repo reference was mechanical or descriptive surface that existed BECAUSE the package boundary existed — manifest and tsconfig entries, generated module-graph rows, dependency-graph and README rows, and doc comments naming the package. The ui group README recorded the support placement rationale ("exists chiefly for the examples and the coverage gate — `ui/` is reserved for surfaces shipped as product"), which left a standing tension: a shipped product app depending on a support package documented as NOT product surface.
The readline UI was a whole package (`@deepseek-ai/dsh-ui-stdio` under `packages/support/`) whose only runtime importer was the app package `@deepseek-ai/dsh-stdio-demo`. The examples reach the readline UI by loading the app, never by composing the helper themselves; every other repo reference was mechanical or descriptive surface that existed BECAUSE the package boundary existed — manifest and tsconfig entries, generated module-graph rows, dependency-graph and README rows, and doc comments naming the package. The ui group README recorded the support placement rationale ("exists chiefly for the examples and the coverage gate — `ui/` is reserved for surfaces shipped as product"), which left a standing tension: a shipped product app depending on a support package documented as NOT product surface.
The boundary bought package metadata, workspace and tsconfig references, module-graph rows, README entries, and publint surface for a helper that is not independently swappable: the stdio app's front-door cluster always includes the readline UI, and nothing else can meaningfully consume it.
@@ -6,7 +6,7 @@ Status: implemented
Two pieces of `dsh-acp` surface were unreachable from any shipped configuration:
1. **`AcpConfig.agentName` / `agentVersion`** (`packages/ui/acp/src/index.ts`). The shipped app package hands the bridge only `{ model }` (`packages/ui/acp-agent/src/index.ts`), so no leaf `cordis.yml` — the only production config surface — could set the knobs at all; they were settable solely by direct-mounting the bridge, which only a unit test did. Every snapshot golden — the hook-matrix scenarios included — pins the schema defaults (`deepseek-harness-acp` / `0.0.1`). The pair also carried a live `TODO(double-default)`: the literals existed twice (schema `.default(...)` plus `??` fallbacks), with the TODO asking to pick one home.
1. **`AcpConfig.agentName` / `agentVersion`** (`packages/ui/acp/src/index.ts`). The shipped app package hands the bridge only `{ model }` (`packages/examples/acp-demo/src/index.ts`), so no leaf `cordis.yml` — the only production config surface — could set the knobs at all; they were settable solely by direct-mounting the bridge, which only a unit test did. Every snapshot golden — the hook-matrix scenarios included — pins the schema defaults (`deepseek-harness-acp` / `0.0.1`). The pair also carried a live `TODO(double-default)`: the literals existed twice (schema `.default(...)` plus `??` fallbacks), with the TODO asking to pick one home.
2. **The `toolKindFor` name heuristic** (same file) special-cased `bash*`/`read*`/`write`/`edit*` tool names in the generic-fallback path. Since the [render-intent union](../architecture/2026-07-02-tool-render-intent-union.md), every first-party tool those arms matched ships its own `presentCall` carrying its kind, and the presenter-less production tools (`subagent`, `subagent_fork`) fell through to `other` anyway. The arms were production-reachable only when a tool declined to present its own call — a `presentCall` that THROWS (the containment fallback), or model arguments that fail the tool's schema so `defineTool`'s `presentCall` wrapper returns `undefined` (e.g. a `bash` call missing the required `description`) — and the bridge's own module doc states the design rule the heuristic violated: "the bridge never special-cases tool names".
## Decision
@@ -22,7 +22,7 @@ The machinery lives in [`packages/support/acp-snapshot`](../../../../packages/su
- **Copy the modules into each example** — the fork this RFC exists to prevent: the record/guard logic is exactly the code that must stay byte-identical across suites, and examples are outside the coverage gate, so each copy is also unmeasured.
- **A shared module directory under `examples/`** — keeps the code outside the coverage gate and forces relative imports across example boundaries, against the package-name import convention; `examples/` leaves stay thin by design.
- **A `/testing` subpath export of `dsh-acp-agent`** — couples test infrastructure into a product package's surface and dependency set; `packages/support/` exists precisely for real-but-lower-compatibility dev/test packages, with `dsh-llm-replay` as the precedent this package completes.
- **A `/testing` subpath export of `dsh-acp-demo`** — couples test infrastructure into a product package's surface and dependency set; `packages/support/` exists precisely for real-but-lower-compatibility dev/test packages, with `dsh-llm-replay` as the precedent this package completes.
- **Export raw test-body functions instead of a suite factory** — each example would re-own the `describe`/`it` skeleton (~80 lines of registration boilerplate per suite) for no flexibility gain; the factory keeps consumers to a scenario table plus one call, and the exported pure helpers preserve unit-testability inside the factory design.
- **An injectable ACP `Client` factory instead of declarative `permissionAnswers`** — maximally flexible, but it leaks SDK client construction to every consumer and reopens per-example drift in exactly the layer being unified; a declarative queue keeps `input.json` the single scripting surface and stays golden-normalizable.
- **Generalize beyond ACP (a transport-agnostic snapshot harness)** — no second transport exists; the harness is ACP-shaped end to end (SDK client, JSON-RPC frames, `session/update` waiters), and a speculative abstraction would be a seam split ahead of any consumer.
@@ -1,41 +0,0 @@
# RFC: Extract a generic long-running tool runtime
Status: proposed
## Problem
The bash capability seam supports both foreground commands and long-running background tasks. Background support is large: the abstract executor exposes `start`, `get`, `ownerOf`, `list`, `readOutput`, `kill`, and `onTaskDone`; the local executor tracks tasks, incremental reads, owner tokens, process cleanup, and completion listeners; the model sees three tools (`bash`, `bash_output`, `bash_kill`); the tool plugin injects completion notices back into the owning agent's session. The local executor fences task access behind owner tokens because predictable global task ids are a cross-session read/kill hazard.
The [tool cookbook](../../../cookbook/adding-a-tool.md) already points at the real design smell: background bash is really generic long-running-tool infrastructure living inside one tool. If future tools need background execution, polling, kill, ownership, and completion notices, those semantics should not be hidden in `dsh-bash`.
## Proposal
Move long-running task semantics above bash into a tool-agnostic runtime. Bash remains able to run background commands, but it stops owning the general concepts of task ids, ownership tokens, polling, cancellation, completion notifications, and model-facing "read/kill this task" commands.
The runtime should own:
- Stable task ids and owner tokens keyed to the calling session/agent.
- Registration of a long-running task with a producer for incremental output and a completion promise.
- Generic read/cancel/list operations with the same cross-session authorization rule for every tool.
- Completion notification injection into the owning session.
- Presentation hooks for pending/running/completed task state, with bash supplying only command-specific labels and output formatting.
`dsh-bash` then keeps the bash-specific execution contract: resolve a request into a command spec, run a foreground command, or start a process and hand its streams/process handle to the generic runtime. `dsh-tool-bash` keeps the model-facing command tool, but the follow-up operations become generic long-running-tool operations or a shared utility that bash registers with, rather than bespoke `bash_output`/`bash_kill` plumbing.
## Current seam consumption
Current consumers split cleanly: `dsh-tool-bash` uses the full foreground/background seam, while hook bridges use only foreground `resolve` and `run` with trusted `stdin` and `env`. `get` and `list` are test-only; `BashTask.done` is implementation-only for disposal, while production completion uses `onTaskDone`. An extracted runtime should expose one public completion mechanism, preserve the simple foreground path for hooks, and decide whether background `timeoutMs` belongs on `start`. If it owns process spawning, it should also centralize the duplicated credential scrub.
## Acceptance criteria
- The bash-specific packages no longer define the generic task registry, owner-token authorization, polling, cancellation, or completion-notification machinery.
- A shared long-running-task service or tool layer owns those semantics and is documented as the path for any future background-capable tool.
- Bash background behavior remains available through the shared layer, with tests proving cross-session isolation still holds.
- ACP and snapshot fixtures render background bash through the shared task vocabulary, not through bash-only lifecycle semantics.
- The [tool cookbook](../../../cookbook/adding-a-tool.md) points long-running tools at the shared runtime instead of telling each tool to invent its own task protocol.
## Risks
The bash package loses local ownership of an already-working background-task implementation, and the implementing PR may temporarily churn model-facing tool names or transcript presentation. That churn is worthwhile if it leaves one background-task contract instead of making every future long-running tool clone bash's private protocol.
<!-- rfc-format: alternatives-not-recorded (pre-format RFC) -->
@@ -0,0 +1,6 @@
# Bilingual-pair consistency record (docs/i18n/README.md): the git blob hash of each
# side as of the last confirmed-consistent state. Both languages carry equal authority;
# after editing either side, bring the other along and re-record with:
# pnpm run verify-translation-pairing --write
2026-07-15-sdk-project-editing-architecture.md: 985cc22c159c68801b78262aa96c7422bdfa1318
2026-07-15-sdk-project-editing-architecture.zh.md: 6a194e8e5f193e62bfc283fd93a5fde0367fe196
@@ -0,0 +1,129 @@
# RFC: SDK project editing architecture
Status: proposed
English | [中文](2026-07-15-sdk-project-editing-architecture.zh.md)
## Problem
[Developer-owned SDK projects](../feature/2026-07-14-sdk-developer-projects.md) are created through create, adjusted through config, and built and run through commands such as start. Initial creation, configuration changes, and build and runtime commands all need to understand features, feature options, npm dependencies, Cordis config entries, environment variables, package managers, local plugins, and several project files. If each project-reading and project-writing workflow uses a separate interpretation protocol, the SDK developer workflows become difficult to maintain.
## Proposal
The SDK uses one shared object-oriented project model. `SdkProject` is a read-only snapshot, and `ProjectEditSession` is the only mutation and commit boundary. Feature objects own their feature options, relationships, resource contributions, and current-state inspection. Create and config orchestrate only their respective user workflows and modify projects through the same domain operations.
Structured files are modified through document objects, while one-shot text artifacts are generated from complete templates. Questions are typed objects presented through clack. Diff calculation may remain an edit-session implementation detail, but it is not a public execution protocol that callers must assemble.
## Terminology
| Term | Usage in this RFC | Meaning |
|---|---|---|
| Feature | feature | A product unit curated and managed by the SDK; one feature may contain several feature options and contribute several Cordis config entries, npm dependencies, environment placeholders, and owned files |
| Feature option | feature option | A finite selectable implementation or configuration shape within one feature; feature rules may make options fixed, exclusive, or additive |
| Cordis plugin | Cordis plugin | A plugin implementation loaded by Cordis, usually exported by an npm package; it is not an item in `cordis.yml` |
| Cordis config entry | Cordis config entry | One item in the `cordis.yml` plugin list, identified as an instance by `id` and referring to a Cordis plugin through `name` |
| Cordis plugin config | Cordis plugin config | The configuration object or shape exposed by a Cordis plugin; an individual field owned and updated by a feature is a config key |
| config key | config key | One field in Cordis plugin config; a feature updates only the config keys it declares as owned and preserves unknown config keys |
| npm dependency | npm dependency | A package relationship in `package.json`; literal fields such as `dependencies` and `devDependencies` keep their names |
| Feature requirement | feature requirement | A relationship declared through `requires` by a feature or feature option |
## Package boundaries
| Package | Responsibility | Does not own |
|---|---|---|
| `@deepseek-ai/dsh-helper` | Edit sessions, feature configuration, project-template rendering, package-manager adaptation, and prompt interaction adaptation | Booting Cordis applications or deciding create/config terminal workflows |
| `@deepseek-ai/dsh-scripts` | `dsh-sdk start/dev/build/config`, process lifecycle, project entry loading, the config workflow, and its terminal-copy templates | Interpreting feature definitions directly or modifying YAML/JSON ASTs |
| `@deepseek-ai/create-sdk` | Arguments, question order, initial project creation, installation finish, and terminal-copy templates for `npm create @deepseek-ai/sdk` | Becoming a generated project's runtime npm dependency or providing a library API |
`@deepseek-ai/create-sdk` is the only exception to the repository's `@deepseek-ai/dsh-*` naming rule. npm's scoped-initializer convention requires that package name for `npm create @deepseek-ai/sdk`. The exception is a repository architecture fact and does not add a third developer product entrypoint.
The three packages export only the narrow entrypoints consumed by adjacent layers and provide no `src/*` deep imports. The scripts library entrypoint and build-config subpath serve generated code and project build configuration, while the developer product contract remains the `dsh-sdk` commands.
## Project aggregate and edit session
`SdkProject.create(root, request)` constructs a new project snapshot that has not been written, while `SdkProject.open(root)` loads an existing project. Open requires only readable root `package.json` and `cordis.yml` files; every other file is an optional resource. Both paths return the same read-only aggregate and distinguish their source through explicit origin state.
`project.edit()` clones project documents into a working copy. Domain commands such as install, configure, enable, disable, and addPlugin modify only the working copy. Each command immediately re-inspects its owning feature, and the final commit checks all relationships and files again.
```text
validate feature requirements and resource ownership
-> validate every affected document
-> compute changed and removed paths
-> compare existing files with the session's original text
-> write through one commit boundary
-> return a new SdkProject snapshot and ChangeSet
```
Validation failure or an external edit causes zero writes. “One commit” means only zero pre-write side effects and one write entrypoint. `ChangeSet` describes final feature, plugin, and file changes for Review & Apply and create completion.
## Features and resource ownership
A feature is a first-class behavior object. Shallow base classes implement install, configure, enable, disable, required/requires validation, and common state inspection. Features with fixed, exclusive, or additive feature options share these lifecycles. Only features whose resource contributions depend on project context or require custom round-tripping use dedicated behavior classes; other features declare their actual differences through standardized data.
Each feature contributes stable-keyed Cordis config entries, npm dependencies, environment placeholders, and owned files. The registry rejects two features that declare the same resource key during initialization. Different feature options within one feature may share resources, which that feature resolves from the final option set.
A Cordis config entry anchors feature installation. The npm package name assigns the entry to a feature, and the entry ID distinguishes several instances of one plugin package. An npm dependency without a feature-owned Cordis config entry leaves the feature uninstalled. Once a Cordis config entry exists, a missing npm dependency, unreadable Cordis plugin config, or resource conflict puts the feature into an inconsistent state; the config command shows diagnostics and refuses speculative modification.
Configuring the same feature option updates only its owned config keys and preserves unknown keys. Replacing a feature option removes old resources that are exclusive and still confirmable. If an old resource cannot be confirmed or an owned file was modified by the developer, the whole operation fails.
## Questions and workflows
TypeScript `Question<T>` objects keep defaults, validation, applicability, and types together. `PromptPort` is the only interface between the domain layer and the terminal library, and helper provides one thin `ClackPromptPort`. Create and config inject their own command-line input and output streams and retain ownership of cancellation, return, and completion semantics in their workflows.
Create keeps its stateful question order in one wizard, while config keeps final-state selection in one workflow. Both use the same feature configurator for feature options and dedicated inputs, so adding an ordinary feature, feature option, or parameter does not require changes to both entrypoints.
## Project documents and templates
Only structured files that helper reads or modifies have concrete document objects: `package.json`, `cordis.yml`, `.env`, `.env.example`, the root `tsconfig.json`, and the pnpm workspace file. Document objects own parsing, cloning, validation, and serialization. Concrete classes and modules use `*File` and `*-file.ts` names respectively. Business code does not manipulate YAML/JSON ASTs directly, and malformed shapes fail loudly at the owning document boundary.
README, entrypoint code, build configuration, `.gitignore`, and other one-shot text artifacts use one complete template per real file. Complete product copy such as CLI usage, creation and recovery messages, installation and retry guidance, and the default persona also comes from package-local templates owned by the package that presents it.
Helper provides the generic typed `TextTemplate` renderer, and caller packages load their own templates through package-local asset URLs.
Templates use Handlebars strict mode and `noEscape` without custom processing. File owners encode typed values for the target language. Template source escapes interpolation as `\{{model}}` when it must emit the downstream literal unchanged.
## Command and runtime boundary
Scripts supports `dsh-sdk start/dev/build/config`. Start dynamically loads a module target and calls its named entrypoint. Dev adds TypeScript and local-workspace source resolution before following the same path. Build invokes the project's installed tsdown. Config opens one edit session and commits after Review & Apply. Generated projects run `tsc -b` directly for typechecking.
HMR is an explicit Cordis config entry loaded by dev and start. Its required `node-addon-require-builtin` package is supplied transitively by the scripts package and is absent from the generated project's `package.json`.
Dev and start execute the developer entrypoint, where developer code handles command-line arguments and cwd. Developers pass `--model=<name>` and `--resume=<session-id>` to start the standard flow.
## Repository live-link mode
Create-sdk retains a hidden `--link-workspace` option for Harness repository development and e2e. The parser accepts it, but help, public flag lists, and ordinary user documentation omit it. It accepts no repository-path parameter; the repository root is derived upward from the executing create-sdk module.
Link mode preserves the ordinary project file shape. `@deepseek-ai/*` points into `packages/`, Cordis-related npm dependencies point into `vendor/`, and shared lower-level packages resolve to the same physical copy used by the repository so Cordis type merging cannot produce multiple module type definitions. npm uses `file:`, pnpm uses `link:` with automatic peer installation disabled, and Yarn uses `portal:` plus resolutions. Repository packages must be built first.
## Future work
- **Replaceable required spine roles.** The current `spine` owns the full implementation set, including SystemPrompt and LLMService, through one fixed feature option. Developers cannot replace or switch these roles and must edit Cordis config entries manually.
- **Service contracts and package declarations.** When replacing a builtin service, a Cordis plugin currently cannot declare the services it provides through `provides` metadata, so the SDK cannot assist configuration during development or check compatibility at runtime. A corresponding protocol remains to be designed.
- **Feature parameter descriptions.** Feature-specific inputs currently require handwritten declarations. The SDK cannot derive interactive parameters automatically from arbitrary Cordis plugin config or npm package.json information. Future declarative metadata may expose a limited parameter set without turning arbitrary Cordis plugin config into a generic form.
- **SDK application-level configuration.** The current project resource model describes Cordis config entries and config keys owned by individual Cordis plugins, so every SDK-managed setting must belong to one plugin. Cross-plugin or whole-application settings have no independent persistence location. Future work must define an application-level configuration document and its ownership, read, and mutation boundaries.
## Alternatives considered
**Keep the static Catalog and central engine.** This minimizes the initial rewrite, but feature parameters, round-tripping, owned files, and create/config reuse continue to accumulate in one coordinator. Splitting files shortens the file without consolidating responsibility.
**Use `wizard.json` and a generic Questionnaire.** Static forms cannot directly express feature requirements, option switches, existing-value refill, and project-resource changes. Types, gates, and dynamic options still connect through string registries and a procedural `run()`, creating another internal DSL.
**Expose the live-link flag.** The mode depends on Harness monorepo layout and unpublished packages and serves repository development only. Making it public would create a project-creation contract that the SDK cannot support outside the repository.
## Acceptance criteria
- Create and config modify projects only through `SdkProject` and `ProjectEditSession`; any business, document, or concurrency validation failure before writing leaves the filesystem unchanged
- Adding an ordinary feature, feature option, or parameter extends only its typed spec or owning behavior object, without adding a central switch to create or config workflows
- Helper owns the feature model, npm dependency and other resource configuration, and inconsistent-state detection
- Structured files change through `*File` document objects; one-shot files and complete product copy come from package-owned Handlebars templates, and business decisions do not enter a template DSL
- `dsh-sdk start/dev/build/config` is the runtime product surface, typecheck uses `tsc -b` directly, HMR is not injected by command mode, and only the scripts package transitively supplies `node-addon-require-builtin`
- `--link-workspace` exists only as a hidden repository-development option and preserves one module identity under npm, pnpm, and Yarn
## Risks
- Behavior objects and typed specs create two extension shapes. Dedicated classes must remain limited to features that truly depend on project context or custom behavior, or the design will grow a meaningless type hierarchy
- Optimistic concurrency checks and pre-write validation cannot recover from an I/O failure during writing; callers must still report a possible partial commit to the developer
- Hidden link mode depends on repository layout and package-manager link semantics and must change with either one
- The Cordis loader resolves `node-addon-require-builtin` from its own module path, so the scripts package must continue to satisfy that optional peer under npm, pnpm, and Yarn npm dependency layouts
- Handlebars `noEscape` makes typed model construction responsible for target-language encoding; new template fields must be escaped correctly at the owning boundary, and downstream Handlebars placeholders must be escaped explicitly in template source
@@ -0,0 +1,129 @@
# RFC: SDK 工程编辑架构
Status: proposed
[English](2026-07-15-sdk-project-editing-architecture.md) | 中文
## 问题
[开发者拥有的 SDK 工程](../feature/2026-07-14-sdk-developer-projects.md) 由 create 创建,可以通过 config 调整,并由 start 等命令构建和运行。初始创建、配置调整和编译运行都需要理解功能、功能选项、NPM 依赖、Cordis 配置项、环境变量、包管理器、本地插件和多个项目文件。如果读写项目的各个流程分别使用不同的解析协议,SDK 开发者流程会变得难以维护。
## 提案
SDK 使用一个共享的面向对象工程模型。`SdkProject` 是只读快照,`ProjectEditSession` 是唯一修改与提交边界;功能对象负责自身的功能选项、关系、资源贡献和现状识别;create 与 config 只编排各自的用户流程,并通过同一组领域操作修改工程。
结构化文件通过文档对象修改,一次性文本产物通过完整模板生成。问题由类型化对象表达,并使用 clack 交互。差异计算可以作为编辑会话的内部实现,但不成为要求调用方组装的公共执行协议。
## 术语
| 名词 | 本文用词 | 含义 |
|---|---|---|
| Feature | 功能 | SDK 人工策划和管理的产品单元;一项功能可以包含多个功能选项,并贡献多个 Cordis 配置项、NPM 依赖、环境变量占位和独占文件 |
| Feature option | 功能选项 | 一项功能内有限、可选择的实现或配置形状;根据功能规则可以固定、互斥或多选 |
| Cordis plugin | Cordis 插件 | Cordis 加载的插件实现,通常由一个 NPM 包导出;它不是 `cordis.yml` 中的一项配置 |
| Cordis config entry | Cordis 配置项 | `cordis.yml` 插件列表中的一项,通过 `id` 标识实例并通过 `name` 指向 Cordis 插件 |
| Cordis plugin config | Cordis 插件配置 | Cordis 插件公开的配置对象或配置结构;其中由功能拥有并更新的单个字段称为“配置键” |
| config key | 配置键 | Cordis 插件配置中的单个字段;功能只更新自己声明拥有的配置键,并保留未知配置键 |
| npm dependency | NPM 依赖 | `package.json` 中的包关系;`dependencies``devDependencies` 等字段保持原样 |
| Feature requirement | 功能依赖 | 功能或功能选项通过 `requires` 声明的关系 |
## Package 边界
| Package | 责任 | 不负责 |
|---|---|---|
| `@deepseek-ai/dsh-helper` | 编辑会话、功能配置、工程模板渲染、包管理适配和 prompt 交互适配 | 启动 Cordis 应用或决定 create/config 的终端流程 |
| `@deepseek-ai/dsh-scripts` | `dsh-sdk start/dev/build/config`、进程生命周期、项目入口加载、config 流程和所属终端文案模板 | 直接解释功能定义或修改 YAML/JSON AST |
| `@deepseek-ai/create-sdk` | `npm create @deepseek-ai/sdk` 的参数、问题顺序、首次工程创建、安装收尾和所属终端文案模板 | 成为生成工程的运行时 NPM 依赖或提供库 API |
`@deepseek-ai/create-sdk` 是仓库 `@deepseek-ai/dsh-*` 命名规则的唯一例外;npm scoped initializer 约定要求 `npm create @deepseek-ai/sdk` 对应这个 package 名。该例外是仓库架构事实,不增加第三个开发者产品入口。
三个 package 只导出相邻层实际使用的最小入口,不提供 `src/*` 深路径。scripts 的库入口与构建配置子路径服务生成代码和项目构建配置,但开发者产品合同仍由 `dsh-sdk` 命令承担。
## 工程聚合与编辑会话
`SdkProject.create(root, request)` 构造尚未写盘的新工程快照,`SdkProject.open(root)` 加载已有工程。open 只要求根 `package.json``cordis.yml` 可读,其余文件是按需存在的资源;两条路径返回同一种只读聚合,并通过显式 origin 区分来源。
`project.edit()` 克隆项目文档形成 working copy。install、configure、enable、disable 和 addPlugin 等领域命令只修改 working copy;命令完成后立即重新检查所属功能,最终 commit 再检查全部关系和文件。
```text
validate feature requirements and resource ownership
-> validate every affected document
-> compute changed and removed paths
-> compare existing files with the session's original text
-> write through one commit boundary
-> return a new SdkProject snapshot and ChangeSet
```
校验失败或检测到会话外修改时不写盘。“一次 commit”只表示写入前零副作用和单一写入口。`ChangeSet` 只描述功能、插件和文件的最终变化,用于 Review & Apply 与 create 收尾。
## 功能与资源所有权
功能是一等行为对象。浅层基类实现 install、configure、enable、disable、required/requires 校验和共同状态识别;固定功能选项、互斥功能选项与可多选功能选项共享这些生命周期。只有资源贡献依赖项目上下文或需要自定义 round-trip 的功能才使用专用行为类,其余功能通过标准化数据声明真正不同的部分。
每项功能贡献带稳定 key 的 Cordis 配置项、NPM 依赖、环境变量占位和独占文件。注册表初始化时拒绝不同功能声明同一个资源 key;同一功能的不同功能选项可以共享资源,并由该功能根据最终选项集合处理。
Cordis 配置项是功能安装锚点。NPM 包名判断配置项所属的功能,配置项 ID 区分同一插件包的多个实例;只有 NPM 依赖而没有功能拥有的 Cordis 配置项时,该功能仍视为未安装。Cordis 配置项存在后,缺失 NPM 依赖、无法读取的 Cordis 插件配置或资源冲突会使功能进入不一致状态,config 命令显示诊断并拒绝猜测式修改。
同一功能选项只更新其声明拥有的配置键,保留未知键。替换功能选项会删除旧功能选项独占且仍可确认的资源;无法确认旧资源或发现独占文件被用户修改时,整个操作失败。
## 问题与 workflow
问题由 TypeScript `Question<T>` 对象表达,默认值、校验、适用条件和类型留在同一个对象中。`PromptPort` 是领域层与终端库之间的唯一接口,helper 提供一份薄 `ClackPromptPort`create 和 config 注入各自的命令行输入输出流,并在各自流程中决定取消、返回和收尾语义。
create 的有状态问题顺序留在一个向导中,config 的最终状态选择留在一个流程中。两者通过同一个功能配置器收集功能选项与专用输入,因此增加一项普通功能、功能选项或参数不要求同时修改两个入口。
## 项目文档与模板
只有需要读取或修改的结构化文件拥有具体文档对象,包括 `package.json``cordis.yml``.env``.env.example`、根 `tsconfig.json` 和 pnpm workspace 文件。文档对象拥有解析、克隆、校验和序列化行为;具体类与模块分别使用 `*File``*-file.ts` 命名,业务层不直接操作 YAML/JSON AST,异常形状在所属文档边界 fail loud。
README、入口代码、构建配置、`.gitignore` 和其他一次性文本产物使用与真实文件一一对应的完整模板。CLI usage、创建结果与恢复提示、安装与重试指导以及默认 persona 等完整产品文案也由所属 package 的本地模板提供。
helper 提供通用的数据类型化 `TextTemplate` 模板渲染器,调用 package 通过本地 asset URL 加载自己的模板。
模板使用 Handlebars strict mode 与 `noEscape`,不进行自定义处理。文件对象负责把类型化数据值编码成目标语言文本;如果不希望插值,则源码以 `\{{model}}` 等转义形式输出下游。
## 命令与运行边界
scripts 支持 `dsh-sdk start/dev/build/config`。start 动态加载模块 target 并调用其命名入口;dev 在同一路径前增加 TypeScript 与本地 workspace 源码解析;build 调用工程安装的 tsdown;config 打开一个编辑会话并在 Review & Apply 后提交。typecheck 由生成工程直接执行 `tsc -b`
HMR 作为显式 Cordis 配置项由 dev 和 start 加载;它所需的 `node-addon-require-builtin` 由 scripts package 传递提供,不写入开发者工程的 `package.json`
dev/start 会执行开发者入口,在开发者代码中处理命令行参数、cwd,由开发者自行传入 `--model=<name>``--resume=<session-id>` 启动标准流程。
## 仓库本地链接模式
create-sdk 保留隐藏的 `--link-workspace` 选项供 Harness 仓库开发和 e2e 使用。该选项可以被解析,但不出现在 help、公开 flag 清单或普通用户文档中,也不接收仓库路径参数;仓库根从正在执行的 create-sdk 模块位置向上确定。
链接模式保持普通工程的文件形状。`@deepseek-ai/*` 指向 `packages/`Cordis 相关 NPM 依赖指向 `vendor/`,共享底层 package 锚定到仓库实际使用的同一物理拷贝,避免 Cordis 类型合并产生多个模块类型定义。npm 使用 `file:`pnpm 使用 `link:` 并关闭自动 peer 安装,Yarn 使用 `portal:` 与 resolutions;仓库 package 需要先构建。
## 后续工作
- **可替换的 required 主干角色。** 当前 `spine` 以一个固定功能选项拥有整组实现,包含 SystemPrompt、LLMService 等。无法让开发者对其进行替换和切换,只能手工修改 Cordis 配置项。
- **Service contract 与 package 声明。** 替换特定内建服务时,Cordis 插件目前无法通过 `provides` 元数据声明其提供的服务,因此 SDK 无法在开发阶段辅助配置,也无法在运行时检查兼容性。后续需要设计相应协议。
- **功能参数描述。** 当前功能的专用输入必须手工声明;SDK 无法从任意 Cordis 插件配置或 NPM package.json 信息中自动推导可交互参数。后续可以定义有限的声明式参数元数据,但不把任意 Cordis 插件配置转换成通用表单。
- **SDK 应用级配置。** 当前项目资源模型只描述 Cordis 配置项及单个 Cordis 插件拥有的配置键,因此所有受 SDK 管理的配置都必须归属某个插件。跨插件或面向整个 SDK 应用的设置没有独立持久化位置;后续需要定义应用级配置文档及其所有权、读取和修改边界。
## 曾考虑的替代方案
**保留静态 Catalog 与中心 engine。** 该方案改动最小,但功能参数、round-trip、独占文件和 create/config 复用都会继续进入同一个协调中心;拆文件只能缩短单文件,不能收拢职责。
**使用 `wizard.json` 与通用 Questionnaire。** 静态表单无法直接表达功能依赖、选项切换、已有值回填和项目资源变化;类型、gate 和动态 option 最终仍要通过字符串 registry 与过程式 `run()` 连接,形成新的内部 DSL。
**公开本地链接 flag。** 该模式依赖 Harness monorepo 布局和未发布 package,只服务仓库开发;公开后会形成无法对外兑现的项目创建合同,因此保持隐藏。
## 验收标准
- create 与 config 只通过 `SdkProject``ProjectEditSession` 修改工程,写入前的任何业务、文件或并发校验失败都不产生磁盘变化
- 新增普通功能、功能选项或参数只扩展类型化 spec 或所属行为对象,create/config 流程不增加中央 switch
- 功能模型、NPM 依赖与其他资源配置、不一致检测由 helper 统一实现
- 结构化文件通过 `*File` 文档对象修改;一次性文件和完整产品文案通过所属 package 的 Handlebars 模板生成,业务决策不进入模板 DSL
- `dsh-sdk start/dev/build/config` 是运行产品面,typecheck 直接使用 `tsc -b`HMR 不通过命令隐式注入,`node-addon-require-builtin` 只由 scripts package 传递提供
- `--link-workspace` 只作为隐藏的仓库开发选项存在,并对 npm、pnpm 和 Yarn 保持单一模块身份
## 风险
- 行为对象与类型化 spec 并存会形成两种扩展形状;专用类必须只用于确实依赖项目上下文或自定义的功能,否则会重新产生无意义的类型层次
- 乐观并发检查与写前校验不能解决写入中途的 I/O 故障,调用方仍需向开发者报告可能的部分提交
- 隐藏链接模式依赖仓库目录与 package manager 链接语义,仓库布局或工具行为变化时必须与实现一起更新
- Cordis loader 从自身模块路径加载 `node-addon-require-builtin`npm、pnpm 或 Yarn 的 NPM 依赖布局变化时,scripts package 必须继续满足该可选对等依赖(optional peer dependency
- Handlebars 的 `noEscape` 把目标语言编码责任交给 typed model 构造方;新增模板字段时必须在 owner 处完成正确转义,下游 Handlebars 占位符必须在模板源码中显式转义
@@ -0,0 +1,6 @@
# Bilingual-pair consistency record (docs/i18n/README.md): the git blob hash of each
# side as of the last confirmed-consistent state. Both languages carry equal authority;
# after editing either side, bring the other along and re-record with:
# pnpm run verify-translation-pairing --write
2026-07-14-sdk-developer-projects.md: 0b5fe876f92153e1ccf5bd8fe383464f5087f4b1
2026-07-14-sdk-developer-projects.zh.md: ec08f323acba1b9dc049182937fda34bfb50d4ee
@@ -0,0 +1,167 @@
# RFC: Developer-owned SDK projects
Status: proposed
English | [中文](2026-07-14-sdk-developer-projects.zh.md)
## Problem
DeepSeek Harness composes features through Cordis plugins, but building a runnable project from an empty directory still requires a developer to understand npm dependencies, the `cordis.yml` plugin set, environment variables, TypeScript builds, local-plugin workspaces, and runtime entrypoints together. These manual steps constrain one another: omitting any one can produce a project that installs but cannot be developed, develops but cannot be built, or builds but cannot start.
A one-shot generator reduces only the initial creation cost. If the generated result is hidden inside a preset or an uneditable CLI, advanced developers cannot reshape the plugin tree, change Cordis plugin config, or add project-specific behavior. If a generated project immediately leaves tool management altogether, developers must again maintain consistency across all npm dependencies and Cordis plugin config themselves.
Initial creation and later configuration address the same builtin feature set. When those workflows maintain separate feature lists, feature options, and npm dependencies, new Cordis plugins, npm packages, and Cordis plugin config changes make them diverge. Projects also need an ordinary local-plugin development path that participates in development, build, and start flows.
## Proposal
The SDK creates an ordinary, explicit TypeScript/Cordis project owned by its developer. `cordis.yml` is the only runtime plugin tree; development and production read the same file. The generated `package.json`, `cordis.yml`, TypeScript entrypoint, build configuration, and `plugins/*` remain directly editable instead of being hidden behind a preset.
The only developer product entrypoints are `npm create @deepseek-ai/sdk` and the `dsh-sdk` commands. The initializer performs initial creation, `dsh-sdk config` manages SDK-recognized builtin features afterward, and `dsh-sdk dev`, `dsh-sdk build`, and `dsh-sdk start` own development, build, and startup; this phase provides no `dsh-sdk create`. Create and config consume one manually authored feature definition, so each feature has one source for its feature options, npm dependencies, Cordis config entries, related files, and inspection rules. The [SDK project editing architecture](../architecture/2026-07-15-sdk-project-editing-architecture.md) defines terms such as feature and feature option.
The SDK offers interaction for feature selection and finite feature options only; it does not turn arbitrary Cordis plugin config into a generic form. A feature collects the small number of dedicated inputs required by its feature options. All other Cordis plugin config remains in `cordis.yml`, with comments documenting common edits, for direct developer control.
## Developer workflow
Initial creation collects information in an order where earlier answers determine later questions: target directory and package identity, model provider and credentials, run interface, builtin features and feature options, an optional local plugin, package manager, and whether to install npm dependencies and build. Command-line arguments suppress questions they already answer. Create and config require an interactive TTY in this phase, and cancelling creation writes nothing to the target directory.
```sh
npm create @deepseek-ai/sdk my-agent
cd my-agent
npm exec dsh-sdk dev index.ts
npm exec dsh-sdk config
npm exec dsh-sdk build
npm exec dsh-sdk start index.js
```
Create rejects every target path that already exists. After committing the project files, the CLI asks whether to install npm dependencies and build. An install or build failure preserves the generated project and prints commands that can retry the failed work.
Create also offers one `none / plugin / tool` choice. `plugin` creates a fixed `plugins/plugin` Cordis plugin, while `tool` creates a fixed `plugins/tool` model-facing tool; one project creation includes at most one local plugin. The operation updates the workspace, root npm dependency, TypeScript reference, build configuration, and `cordis.yml` together, and any pre-write validation failure leaves the project absent.
## Features supported during creation
The table is the developer-visible support set for this phase. A `required` feature is always present but may still offer finite feature options; a `default` feature is preselected in the feature tree; an `optional` feature is selected explicitly. The table describes the product support set, while the runtime registry remains the implementation source of truth.
| Feature | Create state | Feature options | Constraints and relationships |
|---|---|---|---|
| `provider` | required | `deepseek` (default) / `custom` | DeepSeek collects an API key; custom also collects a base URL, and a CLI option may override the model name |
| `app` | required | `stdio` (default) / `acp` / `embed` | Selects the run interface |
| `spine` | required | `default` | Timer, the LLM seam, session storage, system prompt, the tool registry, the agent registry, and the agent loop |
| `bash` | required | `local` (default) / `sandbox` | The two feature options are exclusive and independent of the run interface, and both install the model-facing bash tool; sandbox installs the local sandbox provider and sandboxed bash backend |
| `persistence` | required | `jsonl` (default) / `sqlite` | Every project selects exactly one persistence backend |
| `hmr` | default | `default` | Loads `@cordisjs/plugin-hmr`; dev and start both enable it with the plugin defaults |
| `fs` | default | `local` | Installs the local filesystem, policy, and model-facing tools; the process sandbox does not confine in-process fs tools |
| `todo` | default | `default` | Provides the `todo_write` tool |
| `skill` | default | `default` | Installs the skill registry, the local skill provider, and the model-facing skill tool |
| `web` | optional | `deepseek` (default) / `exa` / `perplexity` / `fetch-only` | Search feature options are exclusive; Exa and Perplexity collect their API keys; timeout policy is recommended |
| `subagent` | optional | `spawn` (default) / `fork`, multiple | This phase provides only in-process backends |
| `workflow` | optional | `workerthread` | Requires the subagent `spawn` feature option |
| `compact` | optional | `basic` | Uses SDK-provided context-compaction parameters |
| `hooks` | optional | `claude` (default) / `codex`, multiple | Each feature option creates a separate editable configuration file |
| `guard` | optional | `repeat-tool` | Provides repeated-tool-call reminders |
| `timeout-policy` | optional | `default` | Applies a uniform policy to tools that declare timeout budgets |
| `ask-user` | optional | `default` | Provides the `ask_user_question` tool; only `acp` and `stdio` can select it because those two feature options provide the injected user-interaction service |
Both `bash` feature options apply to ACP, stdio, and embed and are not selected by the run interface. The sandbox feature option writes no active config key and therefore keeps `dsh-bash-sandbox`'s `read-only` default. Generated `cordis.yml` includes a commented example that developers can change explicitly to `workspace-write`:
```yaml
- id: bash
name: '@deepseek-ai/dsh-bash-sandbox'
# Uncomment to allow writes under the project workspace.
# config:
# mode: workspace-write
# workspaceRoot: !!js process.cwd()
```
Feature contributions reference only single-plugin npm packages and never bundle packages such as `agent-spine-demo`, `stdio-demo`, or `acp-demo`. Plugins outside the table are not managed by create in this phase; advanced developers may still compose them by editing the ordinary project files directly.
## Generated project
With default answers, an npm project uses the DeepSeek provider, the stdio interface, local bash, JSONL persistence, and the preselected hmr, fs, todo, and skill features. Its initial tree is:
```text
my-agent/
├── .env
├── .env.example
├── .gitignore
├── README.md
├── cordis.yml
├── index.ts
├── package.json
├── tsconfig.base.json
├── tsconfig.json
└── tsdown.config.ts
```
`.env.example` always exists, and the SDK keeps its placeholders aligned with the current feature set. A gitignored `.env` is also created when a secret is captured or the developer confirms an empty credential to fill later. The SDK only appends differently named variables that are not already present in `.env` and never updates or removes existing contents. Feature-option changes may remove obsolete `.env.example` placeholders, while old credentials remain in `.env` for the developer to manage. pnpm and Yarn projects add their required workspace files, but do not fork the runtime plugin tree or TypeScript entrypoint.
Generated `package.json` provides the following scripts. `dev`, `build`, `start`, and `config` invoke `dsh-sdk`, while `typecheck` invokes TypeScript directly:
| Script | Behavior |
|---|---|
| `dev` | Run `dsh-sdk dev index.ts`, registering development-time resolution for TypeScript and local workspace plugins |
| `build` | Run `dsh-sdk build`, invoking the project's installed tsdown for the root entrypoint and `plugins/*` packages |
| `typecheck` | Run `tsc -b` directly |
| `start` | Run `dsh-sdk start index.js`, starting the built entrypoint without an implicit build |
| `config` | Run `dsh-sdk config` to edit the current project's feature tree |
`dsh-sdk start` and `dsh-sdk dev` accept a module target and forward arguments after `--` unchanged to the project entrypoint. Generic argument parsing uses Node `parseArgs()` with zero schema: valued flags use `--key=value`, bare flags become `true`, and `--no-*` becomes `false`.
- Stdio projects pass the selected model through `--model=<name>` and create or resume an agent according to optional `--resume=<session-id>`;
- ACP uses protocol `session/load`
- Embed uses the model written into the generated code.
Each feature-owned Cordis config entry keeps its developer-editable Cordis plugin config and explanatory comments in `cordis.yml`. When `dsh-sdk config` changes other features, it preserves unknown fields, formatting on untouched nodes, and comments. HMR is an ordinary leaf config entry: when the feature is selected, dev and start load the same watcher, and the command does not change the plugin tree implicitly.
## Post-creation configuration
`dsh-sdk config` requires only readable root `package.json` and `cordis.yml` files in the current directory. It inspects standard features and their current feature options, expresses the final desired state through one feature tree, and shows feature changes and affected files before Review & Apply.
`dsh-sdk config` can install missing features, enable or disable installed features, and switch finite feature options. Required features cannot be removed. An npm dependency change runs the project package manager's install once after the file commit; installation failure does not roll back committed project files.
The SDK modifies only Cordis config entries, config keys, npm dependencies, `.env.example` placeholders, and owned files explicitly owned by a feature. Updating the same feature option preserves unknown config keys in its Cordis config entries. Handwritten and third-party plugins support enable and disable by stable ID only. When a known feature has been edited into an incomplete, ambiguous, or otherwise unreadable shape, `dsh-sdk config` displays diagnostics and refuses automatic changes until the developer repairs it manually.
One config session accumulates every change in an in-memory working copy. Before Apply, it validates feature relationships, resource conflicts, and document shapes, then compares each affected existing file with the text read when the session opened. Validation failure or an external edit causes zero writes. Once physical writes begin, the SDK does not provide cross-file transactional rollback.
## Maintenance model
The SDK curates its builtin support set instead of exposing npm packages automatically by npm dependency name or directory convention. One feature may compose several Cordis config entries, feature options may share resources, and a feature option may declare a feature requirement on another feature or a specific feature option. Adding an ordinary feature or feature option does not require changes to both create and config command workflows.
## Future work
- `dsh-sdk add [package-spec]` unifies local-plugin creation with external Cordis plugin installation: without a package or repository source it creates a local plugin/tool, while a supplied source adds the npm dependency and `cordis.yml` config entry; the source model leaves room for GitHub repositories and other extensions
- Non-interactive create/config: both workflows require a TTY in this phase and provide no complete input contract for automation
- More feature-specific inputs: this product surface exposes only finite feature options, secrets, and a few dedicated values in this phase rather than a generic parameter interface for Cordis plugin config
## Alternatives considered
**An opaque preset or generator-owned project.** This shortens initial creation but hides the real plugin tree and build boundaries, prevents advanced developers from composing Cordis plugins directly, and makes project behavior depend on the CLI version rather than committed project files.
**A one-shot generator only.** Leaving all later maintenance manual redistributes feature requirements, feature-option switches, and multi-file updates. A config workflow over the shared registry retains continuing management for generated projects.
**Separate `cordis.yml` files for development and production.** Two plugin trees mean a successful development run does not demonstrate that production loads the same features. Dev adds only TypeScript and local-workspace resolution; runtime configuration remains singular.
**A generic form for arbitrary Cordis plugin config.** Cordis plugin config contains nested structures, expressions, and plugin-specific semantics. A generic form would become a second incomplete schema. The SDK manages finite feature options and dedicated secrets, while developers continue to edit complex config directly.
**A private local-plugin discovery protocol.** Ordinary package-manager workspaces, root npm dependencies, TypeScript references, and Cordis config entries already express the complete relationship. Another discovery protocol would create hidden state understood only by the SDK.
**A `dsh-sdk create` command for existing projects.** Create already provides one editable local-plugin skeleton, and later plugins can use ordinary workspace and Cordis mechanisms manually. A parallel command would add a second scaffolding product surface without adding composition functionality.
**Automatically expose every new Cordis plugin as a builtin.** An npm package cannot say how several plugins compose into one product feature, nor can it derive exclusivity, feature requirements, secrets, interface applicability, or security constraints. The support set requires human curation; automation is suitable only for checking whether candidates have been classified.
## Acceptance criteria
- `npm create @deepseek-ai/sdk` collects project identity, provider, interface, features, an optional local plugin, package manager, and installation choice in the documented order, and cancellation leaves the target path absent
- A default npm project has the documented tree and `dev`, `build`, `typecheck`, `start`, and `config` scripts, with dev and start sharing one `cordis.yml`
- Create offers the documented features and feature options; local and sandbox bash are exclusive with local as the default, the sandbox Cordis config entry retains the editable commented config example, and HMR is selected by default and loaded by both dev and start
- Create's `plugin` or `tool` choice creates at most one fixed-name local plugin and atomically updates its files and root-project relationships; this phase provides no `dsh-sdk create`
- `dsh-sdk config` reads the same support set from an existing project, installs, enables, disables, and switches supported feature options, preserves unknown config and comments, and refuses to modify inconsistent config
- `.env.example` reflects variables required by the current features; `.env` only appends missing differently named variables and never updates or removes existing contents
- npm, pnpm, and Yarn workspaces install, build, and start; local plugins resolve from source under dev and from built output under start
## Risks
- Developers can edit a builtin into a shape the registry cannot recognize; the SDK stops automating that feature instead of guessing and overwriting config
- Pre-write validation and external-edit detection do not provide transactional rollback once multi-file writes begin; an I/O failure can leave a partial commit requiring manual repair
- The sandbox feature option depends on an available local sandbox backend for the target platform; an unavailable backend must fail closed instead of falling back to unsandboxed execution
- HMR retains its filesystem watcher and hot-reload behavior under production start; this is the result of an explicit plugin choice, not an implicit development-only service
- The append-only `.env` policy retains credentials that are no longer used; the SDK does not decide when user-owned secret data is safe to delete
@@ -0,0 +1,167 @@
# RFC: 开发者拥有的 SDK 工程
Status: proposed
[English](2026-07-14-sdk-developer-projects.md) | 中文
## 问题
DeepSeek Harness 通过 Cordis 插件对功能进行组合,但从空目录开始搭建一个可运行工程仍要求开发者同时理解 NPM 依赖、`cordis.yml` 插件组、环境变量、TypeScript 构建、本地插件 workspace 和运行入口。手工步骤之间存在约束,漏掉任意一处都会得到能够安装却无法开发、能够开发却无法构建,或能够构建却无法启动的工程。
一次性生成器只能降低首次创建成本。若生成结果隐藏在 preset 或不可编辑的 CLI(命令行界面)内部,高级开发者无法调整插件树、修改 Cordis 插件配置或增加项目特有行为;若创建后的工程完全脱离工具管理,开发者又必须重新承担所有 NPM 依赖和 Cordis 插件配置的一致性工作。
初始创建和后续配置面对同一组内置功能。两条流程各自维护功能列表、功能选项和 NPM 依赖时,新增 Cordis 插件、NPM 包或调整配置会使二者逐渐分叉。工程还需要一条普通的本地插件开发路径,参与开发、构建和启动流程。
## 提案
SDK 创建一个普通、显式且归开发者所有的 TypeScript/Cordis 工程。`cordis.yml` 是唯一的运行时插件树;开发和生产读取同一份文件。工程中的 `package.json``cordis.yml`、TypeScript 入口、构建配置和 `plugins/*` 均可直接编辑,SDK 不把它们封装成不可见的 preset。
开发者产品入口只有 `npm create @deepseek-ai/sdk``dsh-sdk` 命令。前者负责首次创建,`dsh-sdk config` 在创建后管理 SDK 能识别的内置功能,`dsh-sdk dev``dsh-sdk build``dsh-sdk start` 负责开发、构建和启动;本期不提供 `dsh-sdk create`。create 与 config 使用同一份人工编写的功能定义,因此一项功能的功能选项、NPM 依赖、Cordis 配置项、相关文件和识别规则只有一个来源。功能、功能选项等名词由 [SDK 工程编辑架构](../architecture/2026-07-15-sdk-project-editing-architecture.md) 的术语表定义。
SDK 只为功能选择和有限功能选项提供交互,不尝试把任意 Cordis 插件配置变成通用表单。功能选项所需的少量专用输入由所属功能收集;其余 Cordis 插件配置留在 `cordis.yml` 中,并通过注释指明常用改法,由开发者直接修改。
## 开发者流程
首次创建按会影响后续问题集合的顺序收集信息:目标目录与 package 身份、模型提供方与凭据、运行接口、内置功能与功能选项、可选本地插件、包管理器,以及是否安装 NPM 依赖并构建。命令参数已提供的答案不重复询问;本期 create 和 config 都要求交互式 TTY,取消创建时不写入目标目录。
```sh
npm create @deepseek-ai/sdk my-agent
cd my-agent
npm exec dsh-sdk dev index.ts
npm exec dsh-sdk config
npm exec dsh-sdk build
npm exec dsh-sdk start index.js
```
create 拒绝任何已经存在的目标路径。工程文件提交成功后,CLI 询问是否安装 NPM 依赖并构建;安装或构建失败时保留生成结果,并打印可以重新执行的命令。
create 还提供一次 `none / plugin / tool` 选择。`plugin` 固定生成 `plugins/plugin` 的 Cordis 插件,`tool` 固定生成 `plugins/tool` 的模型工具;一次创建至多包含一个本地插件。生成操作同时更新 workspace、根 NPM 依赖、TypeScript reference、构建配置和 `cordis.yml`,任何写入前校验失败都不创建工程。
## 创建时支持的功能
下表是本期 create 面向开发者展示的支持集。`required` 始终存在但仍可切换有限功能选项;`default` 在选择树中预选;`optional` 由开发者主动选择。表格说明产品支持集,运行时注册表是实现的事实源。
| 功能 | create 状态 | 功能选项 | 限制与关系 |
|---|---|---|---|
| `provider` | required | `deepseek`(默认)/ `custom` | DeepSeek 收集 API keycustom 另收集 base URL,模型名可由 CLI 参数覆盖 |
| `app` | required | `stdio`(默认)/ `acp` / `embed` | 选择运行接口 |
| `spine` | required | `default` | timer、LLM seam、会话存储、系统提示词、工具注册表、agent 注册表,以及 agent loop |
| `bash` | required | `local`(默认)/ `sandbox` | 两个功能选项互斥、与运行接口正交,且都安装面向模型的 bash 工具;sandbox 安装本地沙箱提供方和沙箱 bash 后端 |
| `persistence` | required | `jsonl`(默认)/ `sqlite` | 每个工程恰好选择一个持久化后端 |
| `hmr` | default | `default` | 加载 `@cordisjs/plugin-hmr`;dev 和 start 都启用,使用插件默认配置 |
| `fs` | default | `local` | 安装本地文件系统、策略和模型工具;进程沙箱不约束进程内 fs 工具 |
| `todo` | default | `default` | 提供 `todo_write` 工具 |
| `skill` | default | `default` | 安装 skill(技能)注册表、本地 skill 提供方和面向模型的 skill 工具 |
| `web` | optional | `deepseek`(默认)/ `exa` / `perplexity` / `fetch-only` | 搜索功能选项互斥;Exa/Perplexity 收集各自 API key;建议同时启用 timeout policy |
| `subagent` | optional | `spawn`(默认)/ `fork`,可多选 | 本期只提供进程内后端 |
| `workflow` | optional | `workerthread` | 要求 subagent 的 `spawn` 功能选项 |
| `compact` | optional | `basic` | 使用 SDK 提供的上下文压缩参数 |
| `hooks` | optional | `claude`(默认)/ `codex`,可多选 | 各功能选项生成独立的可编辑配置文件 |
| `guard` | optional | `repeat-tool` | 提供重复工具调用提醒 |
| `timeout-policy` | optional | `default` | 对声明超时预算的工具执行统一策略 |
| `ask-user` | optional | `default` | 提供 `ask_user_question` 工具;注入的 user-interaction 服务由 acp/stdio 两个功能选项提供,因此仅这两个接口可选 |
`bash` 的两个功能选项都适用于 ACP、stdio 和 embed,不由运行接口决定。sandbox 功能选项不写任何生效的配置键,因而沿用 `dsh-bash-sandbox``read-only` 默认值;生成的 `cordis.yml` 保留注释示例,开发者可以显式改为 `workspace-write`
```yaml
- id: bash
name: '@deepseek-ai/dsh-bash-sandbox'
# Uncomment to allow writes under the project workspace.
# config:
# mode: workspace-write
# workspaceRoot: !!js process.cwd()
```
功能贡献只引用单插件 NPM 包,绝不引用 `agent-spine-demo``stdio-demo``acp-demo` 这类组合 NPM 包。表格之外的插件不由本期 create 管理;开发者仍可直接编辑普通工程文件进行高级组合。
## 生成工程
使用默认答案创建 npm 工程时,provider 为 DeepSeek,运行接口为 stdiobash 为 local,持久化为 JSONLhmr、fs、todo 与 skill 处于选中状态。初始目录树为:
```text
my-agent/
├── .env
├── .env.example
├── .gitignore
├── README.md
├── cordis.yml
├── index.ts
├── package.json
├── tsconfig.base.json
├── tsconfig.json
└── tsdown.config.ts
```
`.env.example` 始终存在,并由 SDK 根据当前功能维护占位。收集到 secret 或开发者确认稍后填写空凭据时,同时生成 gitignored `.env`。SDK 只向 `.env` 追加尚不存在的不同名变量,绝不覆盖或删除已有内容;切换功能选项可以清理 `.env.example` 中不再需要的占位,但旧凭据仍留在 `.env` 中供开发者自行处理。pnpm 和 Yarn 工程增加各自所需的 workspace 配置文件,但运行时插件树和 TypeScript 入口不分叉。
生成的 `package.json` 提供以下 scripts;其中 `dev``build``start``config` 调用 `dsh-sdk``typecheck` 直接调用 TypeScript
| script | 行为 |
|---|---|
| `dev` | 运行 `dsh-sdk dev index.ts`,为 TypeScript 和本地 workspace 插件注册开发期解析 |
| `build` | 运行 `dsh-sdk build`,调用工程安装的 tsdown 构建根入口和 `plugins/*` package |
| `typecheck` | 直接运行 `tsc -b` |
| `start` | 运行 `dsh-sdk start index.js`,启动已构建入口且不隐式构建 |
| `config` | 运行 `dsh-sdk config`,修改当前工程功能树 |
`dsh-sdk start``dsh-sdk dev` 可以接收模块 target,并把 `--` 后的参数原样转发给工程入口。通用参数解析使用 Node `parseArgs()` 的零 schema 模式:带值 flag 采用 `--key=value`bare flag 转换为 `true``--no-*` 转换为 `false`
- stdio 工程通过 `--model=<name>` 传入所选 model,并根据可选的 `--resume=<session-id>` 创建或恢复 agent
- acp 使用协议 `session/load`
- embed 使用生成代码中的 model。
每个功能拥有的 Cordis 配置项在 `cordis.yml` 中保留自己的可编辑 Cordis 插件配置和说明注释;`dsh-sdk config` 修改其他功能时必须保留未知字段、未修改节点的格式和注释。HMR(热模块替换)是普通叶子配置项:选择该功能后,dev 和 start 加载同一个 watcher,命令不隐式改变插件树。
## 创建后的配置
`dsh-sdk config` 只要求当前目录具有可读的根 `package.json``cordis.yml`。它检查标准功能及其当前功能选项,以一棵功能树表达最终目标状态,并在 Review & Apply 前展示功能变化和受影响文件。
`dsh-sdk config` 可以安装缺失功能、启停已安装功能和切换有限功能选项。required 功能不能取消。改变 NPM 依赖后只运行一次项目包管理器安装;安装失败不回滚已经提交的工程文件。
SDK 只修改功能明确拥有的 Cordis 配置项、配置键、NPM 依赖、`.env.example` 占位和独占文件。同一功能选项的更新保留 Cordis 配置项中的未知配置键;手写或第三方插件只支持按稳定 ID 启停。已知功能被手改成不完整、歧义或无法读取的形状时,`dsh-sdk config` 显示诊断并拒绝自动修改,直到开发者手工修复。
一次 config 会话在内存工作区上累计全部修改。Apply 前完成功能关系、资源冲突和文件形状校验,并比较受影响文件与会话打开时的原文;校验失败或检测到外部修改时不写盘。实际写盘开始后不提供跨文件事务回滚。
## 维护模型
Builtin 支持集由 SDK 人工策划,不根据 NPM 依赖名称或目录约定自动暴露。一个功能可以组合多个 Cordis 配置项,功能选项可以共享资源,并声明对其他功能或特定功能选项的功能依赖;新增普通功能或功能选项不应要求同时修改 create 和 config 两个命令流程。
## 后续工作
- `dsh-sdk add [package-spec]`:统一本地插件创建与外部 Cordis 插件接入;未指定 package 或仓库来源时创建本地 plugin/tool,指定来源时增加 NPM 依赖和 `cordis.yml` 配置项,来源模型为 GitHub 仓库等扩展保留空间
- 非交互 create/config:本期两个流程都要求 TTY,不提供供自动化调用的完整输入合同
- 更多功能专用参数输入:本期产品只展示有限功能选项、secret 和少量专用值,不为 Cordis 插件配置提供通用参数界面
## 曾考虑的替代方案
**不可编辑的 preset 或生成器托管工程。** 该方案可以缩短初次创建路径,但会隐藏真实插件树和构建边界,使高级开发者无法直接组合 Cordis 插件,也让项目行为依赖 CLI 版本而不是检入的工程文件。
**只提供一次性生成器。** 创建后完全依赖手工维护,会让功能依赖、功能选项切换和多文件更新再次分散;共享 registry 的 config 流程为生成工程保留持续管理机制。
**为开发和生产维护两份 `cordis.yml`。** 两份插件树会使开发成功无法证明生产加载相同功能;dev 只增加 TypeScript 与本地 workspace 解析,运行配置保持唯一。
**为任意 Cordis 插件配置生成通用表单。** Cordis 插件配置包含嵌套结构、表达式和插件特有语义,通用表单会形成第二套不完整 schema。SDK 只管理有限功能选项和专用 secret,复杂配置继续由开发者直接编辑。
**使用私有协议发现本地插件。** 普通 package manager workspace、根 NPM 依赖、TypeScript references 和 Cordis 配置项已能表达完整关系;额外发现协议会创造只能由 SDK 理解的隐藏状态。
**在现有工程中提供 `dsh-sdk create`。** create 已能生成一种可编辑的本地插件骨架,后续插件可以沿用普通 workspace 和 Cordis 机制手工添加;再提供同构命令会增加第二条脚手架产品面,却不增加新的组合功能。
**把每个新 Cordis 插件自动暴露为 builtin。** package 无法说明多个插件如何组合成一项产品功能,也无法推导互斥关系、功能依赖、secret、接口适用性和安全限制;支持集需要人工策划,自动化只适合检查候选是否完成分类。
## 验收标准
- `npm create @deepseek-ai/sdk` 按本文顺序收集项目身份、provider、interface、功能、可选本地插件、包管理器和安装选择,并在取消时保持目标路径不存在
- 默认 npm 工程具有本文目录树和 `dev``build``typecheck``start``config` scripts,且 dev/start 使用同一份 `cordis.yml`
- create 展示本文功能及功能选项;`bash` 的 local/sandbox 二选一且默认 localsandbox Cordis 配置项保留可编辑的注释配置示例;HMR 默认选中并同时由 dev/start 加载
- create 的 `plugin``tool` 选择至多生成一个固定名称的本地插件,并原子更新插件文件与根工程关系;本期不提供 `dsh-sdk create`
- `dsh-sdk config` 从现有工程读取同一支持集,能够安装、启停和切换支持的功能选项,保留未知配置与注释,并拒绝修改不一致配置
- `.env.example` 反映当前功能所需变量;`.env` 只追加缺失的不同名变量,从不覆盖或清理已有内容
- npm、pnpm 和 Yarn 生成的 workspace 能安装、构建和启动;本地插件在 dev 中使用源码,在 start 中使用构建产物
## 风险
- 开发者可以把 builtin 手改成 registry 无法识别的形状;SDK 选择停止自动化而不是猜测并覆盖配置
- 多文件写入前的校验和外部修改检测不能提供写入阶段的事务回滚;I/O 中途失败可能留下需要人工修复的部分提交
- sandbox 功能选项依赖目标平台存在可用的本地沙箱后端;后端不可用时必须 fail closed,不能退回无沙箱执行
- HMR 在生产启动中也保持文件 watcher 和热重载行为;这是显式插件选择的结果,不是仅限开发环境的隐式服务
- `.env` 的仅追加策略会保留已经不用的凭据,SDK 不判断这些用户数据何时可以安全删除
@@ -0,0 +1,83 @@
# RFC: Periodic human-review maintenance for dsh-code-review
Status: proposed
## Problem
The `dsh-code-review` skill records failure modes that require reviewer judgment, but one-off audits are expensive to repeat and easy to scope inconsistently. Treating every comment as a lesson produces checklist bloat; treating merge, thread resolution, or an author's “fixed” reply as proof of adoption promotes feedback that the final code may not implement. The maintenance process needs enough evidence and independent review to fail closed without requiring a webhook service, durable event state, or automatic repository promotion before the workflow has proven useful.
## Proposal
Periodic out-of-repo maintenance. A private tool, kept on the skill maintainer's machine rather than committed to this repository, runs against a clean full-history checkout at refreshed `origin/master`. The intended scheduler runs daily with a two-UTC-day overlap; manual runs accept another `--since` duration or repeated `--pr` arguments for an explicit set. The scan is idempotent against the current skill and stores no repository cursor. The only repository file changed by promotion is [.agents/skills/dsh-code-review/SKILL.md](../../../../.agents/skills/dsh-code-review/SKILL.md); the draft PR carries a provenance summary so reviewers can audit the source feedback and adoption evidence without the private adapter logs.
```mermaid
flowchart TD
A["Maintainer or scheduler runs the tool on origin/master"] --> B["List PRs merged in the overlap window"]
B --> C["Collect pre-merge User feedback and final PR evidence"]
C --> D["Two reviewers verify provenance and adoption"]
D --> E{"Both confirm human-authored and adopted?"}
E -- "No" --> F["Exclude or retain as unresolved"]
E -- "Yes" --> G["Two reviewers classify against the current skill"]
G --> H["Draft a complete candidate from agreed guidance"]
H --> I["Two reviewers inspect the same skill diff"]
I -- "Blocking finding" --> J["Bounded revision loop"]
J --> I
I -- "Both approve" --> K["Run documentation and lint checks"]
K --> L["Leave a reviewed local working-tree diff"]
```
### Acquisition contract
Each selected PR is filtered before any feedback is retrieved: its merge commit must be an ancestor of `origin/master`. Merge-commit reachability is the sole eligibility check — a stacked PR whose direct base is a feature branch is admitted whenever the base has since reached master, because the code the reviewer commented on is now on master regardless of the intermediate stack. The tool also resolves the landing merge's target parent; a landing shape it cannot reconstruct is logged to `skipped-pulls.json` and skipped. A single PR that fails preflight, acquisition, or evidence collection is skipped rather than aborting the whole run. The search stage fails loud when the window would exceed GitHub's 1,000-result search cap so no merged PR is silently omitted. The acquisition stage reads complete paginated connections for inline review comments, review submissions, and PR commits. PR conversation comments are not acquired because current GitHub state cannot prove which surviving commit preceded them after a force-push, so the adoption contract would exclude them unconditionally. The workflow admits acquired feedback only when GitHub reports the actor `type` as `User`, and only when both creation and last-edit timestamps strictly predate the PR merge (an equal-timestamp edit is treated as post-merge); review submissions use GraphQL `lastEditedAt` because the REST representation omits edit time.
### Adoption evidence
Each feedback item carries a stable source ID and bounded change evidence. When the reviewer's `commit_id` still belongs to the PR (force-push fail-closed), the tool selects the latest PR commit whose committer timestamp strictly predates the feedback as the baseline — not the reviewer's clicked commit, which may be an older commit. It never compares that baseline directly with the landing merge: such a diff includes unrelated changes from an advancing target branch. Instead, it gives the adoption reviewers two PR-specific patch snapshots. Let `B` be the feedback baseline, `T` the landing merge's target parent, and `M` the landing merge. The feedback-time snapshot is the tree diff from `merge-base(B, T)` to `B`; the final snapshot is the tree diff from `T` to `M`. A target-only change therefore appears in neither PR patch, while a change added to the PR after feedback appears only in the final snapshot. Force-pushed reviews, feedback that predates every surviving PR commit, and landing shapes whose target parent cannot be reconstructed are deterministically classified `unclear` before any reviewer sees them. Merge status, a resolved thread, an author's “fixed” reply, or a same-file edit is context rather than adoption proof; the PR author's own comments never reach the adapter as they cannot be adoption of themselves.
### Dual-reviewer classification and drafting
Two independently configured reviewer adapters classify every eligible item by provenance (`human-authored`, `forwarded-automation`, or `unclear`) and adoption (`adopted`, `rejected`, or `unclear`). Only matching `human-authored` plus `adopted` verdicts proceed. The adopted set then receives a second independent classification against the current skill: candidate, already covered, implementation-specific, or not feedback. A singleton may qualify; recurrence is not required. Disagreement receives one bounded re-evaluation and remains visible in run artifacts if unresolved. A single batch whose adapter output fails schema or id validation is failed closed at the batch level — every feedback item in it is marked unclear and routed to `excluded` — rather than aborting the whole run; the offending raw output is preserved under the run's private artifacts for debugging. If either adapter returns no valid result for any nonempty batch in an operation, the run exits non-zero and emits a failure record instead of reporting “no candidate.”
The primary adapter drafts from structured agreed guidance, never raw review text. It remains tool-free and read-only by adapter-author contract: it returns complete candidate file content, which the tool validates before writing the sole target. Both adapters then review the same complete skill diff; blocking findings return to a bounded revision loop, and both must approve the same revision. The tool rejects staged changes and edits outside the target skill both before running the documentation and lint gates and again before reporting success, so a gate or concurrent process that adds another path cannot slip through. It restores its own write on failure using best-effort compare-and-swap so a concurrent maintainer edit is not overwritten. On success it saves a candidate bundle containing the source `origin/master` commit, source skill blob ID, reviewed diff, complete candidate, source feedback IDs and URLs, landed evidence ranges, adapter verdicts, and gate results; it never commits, pushes, opens, or merges a PR.
### Reviewer adapter protocol
Each private executable receives a byte-bounded, versioned JSON request on stdin and returns byte-bounded, schema-conforming JSON on stdout. The tool refuses to run when the two reviewer commands resolve to byte-identical executables — a minimum-bar mechanical check; guaranteeing that primary and secondary are backed by independent providers or models is the deployment operator's responsibility. The `access` and `tools` fields are contract markers on the adapter author, not an OS sandbox: reviewer subprocesses spawn with a scrubbed environment, `cwd` set to a private run directory rather than the repository root, and feedback wrapped in a nonce-tagged `<untrusted-feedback nonce="…">` block that every prompt instructs the model to treat as data; the 128-bit nonce prevents an untrusted body from forging the closing tag. Every subprocess uses bounded, abort-aware process-tree cleanup. Adapter authors implement each operation as pure read-only inference — even the `edit` operation returns complete candidate content in JSON, which the tool validates and writes to the sole target. Every production `git`/`gh`/gate spawn also uses the scrubbed environment so a pre-push hook's routing variables cannot silently redirect the maintainer. Candidate writes and the failure rollback use best-effort compare-and-swap against the last written content; the rollback also unstages the target so an adapter- or gate-staged candidate cannot survive a failed run into a later commit.
### Promotion contract
The promote helper starts from a clean checkout at refreshed `origin/master` and refuses to apply a candidate when the current skill blob differs from the bundle's recorded source blob. The operator then reruns the maintenance analysis or manually rebases the diff and repeats the candidate review; the helper never replaces a newer `SKILL.md` with stale complete-file output. After applying a current candidate, it opens a draft PR whose body lists the source feedback URLs or IDs, the landed commit range used as adoption evidence, the originating run, gate results, and any operator edits. Raw adapter prompts and responses remain private, but repository reviewers receive enough provenance to judge whether each proposed rule follows from adopted human feedback.
### Where the mechanism lives
The tool source, adapter binaries, provider credentials, and intended daily scheduler are kept private to the maintainer's machine rather than committed to this repository. This document specifies the protocol; the reference implementation is private infrastructure. The mechanism serves a single skill maintained by a single operator, so the ongoing cost of vetting mechanism edits through repository review outweighs any provenance benefit. If the mechanism is ever handed off to a second maintainer, that handoff is a follow-up RFC that revises this decision — the operator doc at [docs/cookbook/maintaining-dsh-code-review.md](../../../cookbook/maintaining-dsh-code-review.md) is the entry point for anyone taking over.
## Alternatives considered
- **Ship the tool inside this repository.** Rejected for a single-maintainer scope: repository maintenance overhead (typecheck, lint, coverage, cross-cutting refactors) would exceed the value of committed provenance. Retained option for a later handoff.
- **Record every feedback-time PR head** — rejected: it improves causal isolation but requires a continuously running observer, durable event state, retries, and force-push reconciliation. Periodic maintenance uses reviewed-commit evidence where available and fails closed on broader whole-PR evidence.
- **Persist a processed-PR cursor** — rejected: an overlapping time-window scan is cheap and naturally idempotent against the current skill, while cursor state creates recovery and missed-event problems.
- **Run on every new comment** — rejected: review waves produce many related comments and lack the final artifact needed to judge adoption.
- **Treat merge or thread resolution as adoption** — rejected: a PR can merge with rejected, superseded, or intentionally unresolved feedback.
- **Create or merge repository changes automatically** — rejected: the tool first needs a track record of useful periodic output. The maintainer inspects and promotes the local diff through normal repository review.
- **Learn from bot findings that were fixed** — rejected: the source contract is human review feedback. Actor type is filtered before analysis, and human accounts forwarding automated findings are excluded by provenance review.
- **Use one reviewer as author and final judge** — rejected: independent verdicts expose unsupported generalization before it reaches the skill.
## Acceptance criteria
Promotion from `proposed/` to `implemented/` requires all of the following to be observed in a real end-to-end run against this repository:
- The private tool runs from a clean detached checkout at refreshed `origin/master` and either reports "no candidate" or produces a working-tree diff limited to `.agents/skills/dsh-code-review/SKILL.md`. **Observed on 2026-07-15:** 62 merged PRs scanned, 5 skipped (unreachable merge commit or >250-commit acquisition cap), 426 human feedback items considered, 0 candidates surfaced.
- Both reviewer adapters are independently configured (distinct providers or models) and complete an analyze / adopt / review pass without user intervention. **Observed on 2026-07-15:** distinct primary/secondary adapters completed adoption + analysis in ~8 minutes; batch fail-closed handled one adapter id-hallucination without aborting the run.
- A scheduler triggers the tool without an interactive terminal, and a candidate diff (or a "no candidate" record) reaches the operator through a durable notification channel.
- A controlled acquisition case advances the target branch with a feedback-matching change after the feedback baseline; the reviewer evidence excludes that target-only change while retaining a later PR-owned change.
- The promote helper rejects a candidate after the source skill changes, and a current candidate opens a draft PR with the provenance summary defined above.
- At least one candidate diff produced by this workflow is inspected by the operator and promoted to `master` through a normal repository PR review. That PR is the evidence that the workflow can turn adopted feedback into shipped skill guidance.
## Risks
- **Causality inferred from committer timestamps.** The feedback-commit baseline is selected by comparing GitHub commit timestamps with feedback creation timestamps; committer clock skew and rewrites still leave a residual false-adoption window. Cross-referencing GitHub's PR event stream would tighten this but requires event acquisition beyond the scope of the periodic tool.
- **Two-non-candidate classifications routed to `excluded` without a dispute round.** When both classifiers say "not a candidate" but disagree on which non-candidate reason applies (for example `covered` vs `specific`), the item is excluded rather than re-evaluated. Both classifiers agree the item does not become new reviewer behavior, so a dispute round would not change the outcome.
- **Dual-reviewer independence beyond byte-hash distinctness is a deployment contract.** The tool refuses to run when the two commands resolve to byte-identical executables, but cannot verify that two distinct wrappers back different providers or models. Operators must configure independent primary and secondary adapters.
- **Best-effort compare-and-swap for candidate writes and rollback.** File-based CAS on POSIX is not truly atomic; the window is one event-loop tick. The tool targets single-user periodic maintenance and a truly concurrent editor is out of scope.
- **Single-maintainer bus factor.** Because the mechanism lives on one machine, its interruption stops skill maintenance entirely until the operator restores service or hands off to a new maintainer through a follow-up RFC.
@@ -1,6 +1,6 @@
# RFC: Make the shared example base providerless
Status: rejected — superseded by [Extract example apps into packages](../../implemented/architecture/2026-06-20-extract-example-app-packages.md), which moves the spine into a `dsh-agent-core` bundle and deletes the `base*.yml` files, so there is no shared base YAML left to rename.
Status: rejected — superseded by [Extract example apps into packages](../../implemented/architecture/2026-06-20-extract-example-app-packages.md), which moves the spine into a `dsh-agent-spine-demo` bundle and deletes the `base*.yml` files, so there is no shared base YAML left to rename.
## Problem
@@ -12,7 +12,7 @@ This solves a real problem, but in a narrow and leaky way. A spill path is a pro
Keep tail truncation, drop full-output spill files. A bash result contains the bounded tail plus a clear truncation marker; no path is emitted. If users need full-output recovery, add a generic artifact/blob service with explicit ownership, cleanup, and UI rendering, then let bash attach large outputs to that service.
This proposal can land independently of [a generic long-running tool runtime](../../proposed/architecture/2026-06-20-generic-long-running-tool-runtime.md). If background tasks stay, `bash_output` should still report that output was dropped, but without advertising a spill path.
This proposal can land independently of [a generic long-running tool runtime](../../implemented/architecture/2026-06-20-generic-long-running-tool-runtime.md). If background tasks stay, `bash_output` should still report that output was dropped, but without advertising a spill path.
## Acceptance criteria
+81 -47
View File
@@ -17,11 +17,12 @@ This table connects model-visible tool names to the plugin package and service s
| --- | --- | --- | --- | --- | --- |
| `@deepseek-ai/dsh-tool-ask-user` | `ask_user_question` | `ctx.tools`, `ctx.userInteraction` | `tool/call`, `tool/result after a UI/provider answers the question` | - | ask_user_question pauses the tool call until the active UI provider returns a human answer. |
| `@deepseek-ai/dsh-tools` | `run_code` | `ctx.tools`, `ctx.codeRuntime (execution time)`, `ctx.systemPrompt` | `tool/call`, `one tool/code-dispatch per bridged sub-call`, `tool/result` | - | Owned by the tool registry as a reserved transport outside filterable capability layers under `mode: code` / `mode: both` (see the Code Mode RFC). Under `code` it is the registry's only wire contribution; the other visible capabilities are declared in a generated TypeScript SDK section, and a program calls them through serialized bindings that re-enter the complete guarded tool pipeline and link each nested execution to this outer result. |
| `@deepseek-ai/dsh-tool-bash` | `bash`, `bash_kill`, `bash_output` | `ctx.tools`, `ctx.bash` | `tool/call`, `tool/result`, `context/message via agent.inject() for background completion notices` | - | The bash/bash_output/bash_kill tools are model-facing consumers of the bash executor seam. |
| `@deepseek-ai/dsh-tool-bash` | `bash` | `ctx.tools`, `ctx.bash`, `ctx.tasks at call time for run_in_background` | `tool/call`, `tool/result` | - | The bash tool is the model-facing consumer of the bash executor seam. A `run_in_background` run registers with the generic `ctx.tasks` runtime and is collected/stopped through the `task_*` tools from `@deepseek-ai/dsh-tool-tasks`; the `enableRunInBackground` config (default true) removes the parameter entirely when disabled. |
| `@deepseek-ai/dsh-tool-cordis` | `cordis_inspect`, `cordis_mount`, `cordis_unmount` | `ctx.tools` | `tool/call`, `tool/result`, `live plugin-tree mutations (mount/unmount)` | - | Ships in examples/cordis-agent only (a deliberate opt-in — mounted code gets the real ctx, see docs/rfc/implemented/feature/2026-07-08-self-referential-cordis-toolset.md). Plugins the model mounts may register ADDITIONAL model-visible tools at runtime; the request-header ToolsDelta logs those tool-set changes. |
| `@deepseek-ai/dsh-tool-fs` | `edit`, `read`, `write` | `ctx.tools`, `ctx.fs`, `ctx.systemPrompt` | `tool/call`, `fs/write-intent or fs/edit-intent for mutations`, `fs/observed after successful file operations`, `tool/result` | - | The read-before-write/edit policy is added by `@deepseek-ai/dsh-fs-policy` (an `fs/*` event-gate plugin, no schema change); a deployment that loads these tools is expected to also load it. The tool schemas above are identical with or without the policy plugin. |
| `@deepseek-ai/dsh-tool-skill` | `skill` | `ctx.tools`, `ctx.skills` | `tool/call`, `tool/result` | - | - |
| `@deepseek-ai/dsh-tool-subagent` | `subagent` | `ctx.tools`, `ctx.subagents` | `tool/call`, `tool/result`, `child session events through the chosen provider` | `subagent`, `subagent_fork` | The registered tool name is the load-time `toolName` config (default `subagent`); the schema above is that default. The shipped example agents load this package once per subagent backend, so the model additionally sees `subagent_fork` (bound to the fork backend) with an identical schema — see `examples/coding-agent/cordis.yml` and `examples/acp-agent/cordis.yml`. |
| `@deepseek-ai/dsh-tool-tasks` | `task_kill`, `task_list`, `task_output` | `ctx.tools`, `ctx.tasks`, `ctx.systemPrompt` | `tool/call`, `tool/result`, `context/message via agent.inject() for background completion notices` | - | The kind-agnostic background-task control surface: a background bash command and a background subagent are read, listed, and killed through the same three tools. Loading the plugin attaches the control surface that arms producers' `ctx.tasks.start()`. |
| `@deepseek-ai/dsh-tool-todo` | `todo_write` | `ctx.tools`, `owning Agent session` | `tool/call`, `todo/write`, `tool/result` | - | todo_write is session-owned state; UIs render the latest todo/write event as a checklist or ACP plan. |
| `@deepseek-ai/dsh-tool-workflow` | `workflow` | `ctx.tools`, `ctx.workflows`, `ctx.systemPrompt`, `a calling Agent (exec.agent parents the script children)` | `tool/call`, `tool/result` | - | - |
| `@deepseek-ai/dsh-tool-web` | `web_fetch`, `web_search` | `ctx.tools`, `ctx.web`, `ctx.systemPrompt` | `tool/call`, `tool/result` | - | web_search and web_fetch keep provider selection behind ctx.web so model-visible schemas stay stable across backend swaps. |
@@ -125,7 +126,7 @@ Owned by the tool registry as a reserved transport outside filterable capability
### `bash`
Execute a bash command (`bash -c`) and return its stdout/stderr. Each call runs in a fresh shell: no state (cwd, variables, functions) persists between calls — pass `workdir` instead of using `cd`. Non-zero exits are reported as `[exit code: N]`. Commands may run under a file sandbox; a blocked file operation is reported as `[sandbox: file access denied under <mode> mode]` — a policy denial, not a bug in the command; do not retry another way (a background task reports the same marker via bash_output once it has finished). Long output is truncated to its tail; the full output is saved to a file whose path is reported when available. Set `run_in_background: true` for long-running commands: the call returns a task id immediately; poll it with `bash_output` and stop it with `bash_kill`.
Execute a bash command (`bash -c`) and return its stdout/stderr. Each call runs in a fresh shell: no state (cwd, variables, functions) persists between calls — pass `workdir` instead of using `cd`. Non-zero exits are reported as `[exit code: N]`. Commands may run under a file sandbox; a blocked file operation is reported as `[sandbox: file access denied under <mode> mode]` — a policy denial, not a bug in the command; do not retry another way. Long output is truncated to its tail; the full output is saved to a file whose path is reported when available. Set `run_in_background: true` for long-running commands: the call returns a task id immediately; read its output with `task_output` and stop it with `task_kill`.
```json
{
@@ -149,7 +150,7 @@ Execute a bash command (`bash -c`) and return its stdout/stderr. Each call runs
},
"run_in_background": {
"type": "boolean",
"description": "Run in the background and return a task id immediately. No timeout applies."
"description": "Run in the background and return a task id immediately (collect with task_output, stop with task_kill). No timeout applies."
}
},
"required": [
@@ -161,49 +162,7 @@ Execute a bash command (`bash -c`) and return its stdout/stderr. Each call runs
Source: [`packages/bash/tool-bash/src/index.ts`](../packages/bash/tool-bash/src/index.ts)
### `bash_kill`
Ask the executor to kill a running background bash task by task id.
```json
{
"type": "object",
"properties": {
"task_id": {
"type": "string",
"description": "Task id returned by the bash tool."
}
},
"required": [
"task_id"
]
}
```
Source: [`packages/bash/tool-bash/src/index.ts`](../packages/bash/tool-bash/src/index.ts)
### `bash_output`
Read new output from a background bash task started with `bash` + `run_in_background`. Returns only output produced since the previous bash_output call, plus the task status. Tasks keep running while you do other work; poll again later for more output.
```json
{
"type": "object",
"properties": {
"task_id": {
"type": "string",
"description": "Task id returned by the bash tool."
}
},
"required": [
"task_id"
]
}
```
Source: [`packages/bash/tool-bash/src/index.ts`](../packages/bash/tool-bash/src/index.ts)
The bash/bash_output/bash_kill tools are model-facing consumers of the bash executor seam.
The bash tool is the model-facing consumer of the bash executor seam. A `run_in_background` run registers with the generic `ctx.tasks` runtime and is collected/stopped through the `task_*` tools from `@deepseek-ai/dsh-tool-tasks`; the `enableRunInBackground` config (default true) removes the parameter entirely when disabled.
## `@deepseek-ai/dsh-tool-cordis`
@@ -398,7 +357,7 @@ Source: [`packages/skill/tool-skill/src/index.ts`](../packages/skill/tool-skill/
### `subagent`
Delegate a self-contained task to a subagent (a separate agent that works in its own context) and return its final result. Use this to offload focused, independent work — research, a scoped implementation, an analysis — so it does not consume this conversation's context. The subagent runs to completion and you receive only its final answer, not its intermediate steps. Give it a complete, standalone prompt: it does not see this conversation.
Delegate a self-contained task to a subagent (a separate agent that works in its own context) and return its final result. Use this to offload focused, independent work — research, a scoped implementation, an analysis — so it does not consume this conversation's context. The subagent runs to completion and you receive only its final answer, not its intermediate steps. Give it a complete, standalone prompt: it does not see this conversation. Set `run_in_background: true` to return a task id; collect with `task_output` and stop with `task_kill`.
```json
{
@@ -411,6 +370,10 @@ Delegate a self-contained task to a subagent (a separate agent that works in its
"prompt": {
"type": "string",
"description": "The complete, self-contained task for the subagent. It does not share this conversation's context, so include everything it needs."
},
"run_in_background": {
"type": "boolean",
"description": "Run as a background task and return its id; collect with task_output or stop with task_kill."
}
},
"required": [
@@ -424,6 +387,77 @@ Source: [`packages/subagent/tool-subagent/src/index.ts`](../packages/subagent/to
The registered tool name is the load-time `toolName` config (default `subagent`); the schema above is that default. The shipped example agents load this package once per subagent backend, so the model additionally sees `subagent_fork` (bound to the fork backend) with an identical schema — see `examples/coding-agent/cordis.yml` and `examples/acp-agent/cordis.yml`.
## `@deepseek-ai/dsh-tool-tasks`
### `task_kill`
Request cancellation of a running background task by task id. Returns immediately; the task settles as killed once its work actually stops.
```json
{
"type": "object",
"properties": {
"task_id": {
"type": "string",
"description": "Task id returned by the tool that started the background work."
},
"reason": {
"type": "string",
"description": "Optional short reason, recorded in the log and forwarded to the task."
}
},
"required": [
"task_id"
]
}
```
Source: [`packages/tasks/tool-tasks/src/index.ts`](../packages/tasks/tool-tasks/src/index.ts)
### `task_list`
List your background tasks (running and finished) with their ids, kinds, and statuses.
```json
{
"type": "object",
"properties": {}
}
```
Source: [`packages/tasks/tool-tasks/src/index.ts`](../packages/tasks/tool-tasks/src/index.ts)
### `task_output`
Read a background task. Stream tasks return only output since the previous read; final-output tasks return their result after settlement. Every response ends with `[status: ...]`. Reads are non-blocking unless `wait: true`, which waits up to the configured cap.
```json
{
"type": "object",
"properties": {
"task_id": {
"type": "string",
"description": "Task id returned by the tool that started the background work."
},
"wait": {
"type": "boolean",
"description": "Block until the task reaches a terminal status or the timeout expires. A timed-out wait returns [status: running] and leaves the task alive."
},
"timeout_ms": {
"type": "number",
"description": "Max wait in milliseconds (only meaningful with wait: true). Defaults to the configured wait timeout; capped by the configured maximum."
}
},
"required": [
"task_id"
]
}
```
Source: [`packages/tasks/tool-tasks/src/index.ts`](../packages/tasks/tool-tasks/src/index.ts)
The kind-agnostic background-task control surface: a background bash command and a background subagent are read, listed, and killed through the same three tools. Loading the plugin attaches the control surface that arms producers' `ctx.tasks.start()`.
## `@deepseek-ai/dsh-tool-todo`
### `todo_write`
+1 -1
View File
@@ -74,7 +74,7 @@ export default tseslint.config(
// --- tests: same rules, minus the friction that fights test ergonomics --
{
files: ['packages/*/*/tests/**/*.ts', 'examples/*/tests/**/*.ts'],
files: ['packages/*/*/tests/**/*.ts', 'examples/*/tests/**/*.ts', 'scripts/**/*.spec.ts'],
extends: [
...tseslint.configs.strictTypeChecked,
],
+2 -2
View File
@@ -1,8 +1,8 @@
# AGENTS.md — Examples
Runnable harness compositions. **Examples are not workspaces:** private package stubs are not built. App bins load each `cordis.yml` through `tsx`; package names resolve through root `tsconfig.json` paths, not `node_modules`.
Runnable harness compositions. **Examples are NOT workspaces**: their private `package.json` files are dependency-free stubs, and the cordis Loader boots each `cordis.yml` unbuilt through `tsx` plus the root tsconfig paths.
Keep wiring, demo fixtures, and e2e/snapshot scenarios here. Move reusable logic into `packages/`, with coverage and a README. App bins own bootstrapping; examples have no `start.ts`.
Extract reusable logic into `packages/`, where per-file coverage and README gates apply. Examples keep only `cordis.yml` wiring, demo artifacts, and e2e/snapshot scenarios; app package bins own boot glue.
## E2E smokes
+5 -5
View File
@@ -1,12 +1,12 @@
# Examples
Runnable demos (not workspaces) that showcase how the harness is wired. Each example is a **thin leaf**: a `cordis.yml` that picks the swappable backends (an LLM adapter, a bash executor), loads one app package, and may add optional product tools or demo-only mocks. The composition — the spine, the front-door cluster, and the boot glue — lives in the app packages ([`@deepseek-ai/dsh-stdio-agent`](../packages/ui/stdio-agent), [`@deepseek-ai/dsh-acp-agent`](../packages/ui/acp-agent)) and the [`@deepseek-ai/dsh-agent-core`](../packages/core/agent-core) bundle they share. There is no `start.ts`; the `demo:*` scripts invoke each app package's `bin`.
Runnable demos (not workspaces) that showcase how the harness is wired. Each example is a **thin leaf**: a `cordis.yml` that picks the swappable backends (an LLM adapter, a bash executor), loads one app package, and may add optional product tools or demo-only mocks. The composition — the spine, the front-door cluster, and the boot glue — lives in the app packages ([`@deepseek-ai/dsh-stdio-demo`](../packages/examples/stdio-demo), [`@deepseek-ai/dsh-acp-demo`](../packages/examples/acp-demo)) and the [`@deepseek-ai/dsh-agent-spine-demo`](../packages/examples/agent-spine-demo) bundle they share. There is no `start.ts`; the `demo:*` scripts invoke each app package's `bin`.
## echo-agent
A mock model + echo tool on the stdio chat app — the all-mock skeleton. The leaf swaps `dsh-stdio-agent`'s LLM backend to a local `mock-echo` adapter and adds a local `echo` tool. Demonstrates:
A mock model + echo tool on the stdio chat app — the all-mock skeleton. The leaf swaps `dsh-stdio-demo`'s LLM backend to a local `mock-echo` adapter and adds a local `echo` tool. Demonstrates:
- A thin leaf `cordis.yml` loading the `@deepseek-ai/dsh-stdio-agent` app
- A thin leaf `cordis.yml` loading the `@deepseek-ai/dsh-stdio-demo` app
- Registering a mock `LlmAdapter` (streaming scripted responses)
- Registering a tool via `ctx.tools.register()`
- "Swap the backend, keep the app" — the only difference from `coding-agent` is the adapter
@@ -15,7 +15,7 @@ Run with: `pnpm run demo:echo`. When prompted, type "echo <something>" to trigge
## coding-agent
A REPL agent demo: DeepSeek V4 + the `read`/`write`/`edit` filesystem tools + the bash tool suite, `subagent` delegation, and the `todo_write` task tracker on the same `@deepseek-ai/dsh-stdio-agent` app. The UI is a terminal readline REPL.
A REPL agent demo: DeepSeek V4 + the `read`/`write`/`edit` filesystem tools + the bash tool suite, `subagent` delegation, and the `todo_write` task tracker on the same `@deepseek-ai/dsh-stdio-demo` app. The UI is a terminal readline REPL.
Run with: `pnpm run demo:repl` (needs `DEEPSEEK_API_KEY` in the environment or a gitignored repo-root `.env`). See [coding-agent/README.md](coding-agent/README.md) for details.
@@ -29,7 +29,7 @@ Run with: `pnpm run demo:cordis` (needs `DEEPSEEK_API_KEY`). See [cordis-agent/R
## acp-agent
An agent demo exposed as an **Agent Client Protocol (ACP)** server over JSON-RPC stdio, via the [`@deepseek-ai/dsh-acp-agent`](../packages/ui/acp-agent) app — drive it from Zed or any other ACP client. Also the home of the keyless snapshot tests.
An agent demo exposed as an **Agent Client Protocol (ACP)** server over JSON-RPC stdio, via the [`@deepseek-ai/dsh-acp-demo`](../packages/examples/acp-demo) app — drive it from Zed or any other ACP client. Also the home of the keyless snapshot tests.
Run with: `pnpm run demo:acp` (needs `DEEPSEEK_API_KEY`); `pnpm run demo:code-mode acp` boots the same server in Code Mode via the `code-mode.cordis.yml` overlay. See [acp-agent/README.md](acp-agent/README.md) for the Zed setup and the snapshot-test design.
+1 -1
View File
@@ -11,7 +11,7 @@ The leaf config loads the ACP app, DeepSeek adapter, sandboxed bash, approval an
## stdout is the protocol
This example loads **no stdout logger**`stdout` carries the JSON-RPC frames, and any other write corrupts them. `@deepseek-ai/dsh-acp-agent` includes no logger entry, so this leaf has none to get wrong by default; do not add one (use a stderr exporter if you need logs).
This example loads **no stdout logger**`stdout` carries the JSON-RPC frames, and any other write corrupts them. `@deepseek-ai/dsh-acp-demo` includes no logger entry, so this leaf has none to get wrong by default; do not add one (use a stderr exporter if you need logs).
## Zed configuration
@@ -8,7 +8,7 @@
name: '@deepseek-ai/dsh-llm-deepseek'
disabled: true
- id: acp-agent
name: '@deepseek-ai/dsh-acp-agent'
name: '@deepseek-ai/dsh-acp-demo'
config:
model: deepseek-v4-flash
persistenceRoot: !!js process.env.DSH_SNAPSHOT_SESSIONS_ROOT ?? './.sessions'
+1 -1
View File
@@ -6,7 +6,7 @@
path: ./cordis.yml
patches:
- id: acp-agent
name: '@deepseek-ai/dsh-acp-agent'
name: '@deepseek-ai/dsh-acp-demo'
config:
model: deepseek-v4-flash
persistenceRoot: !!js process.env.DSH_SNAPSHOT_SESSIONS_ROOT ?? './.sessions'
@@ -10,7 +10,7 @@
name: '@deepseek-ai/dsh-llm-deepseek'
disabled: true
- id: acp-agent
name: '@deepseek-ai/dsh-acp-agent'
name: '@deepseek-ai/dsh-acp-demo'
config:
model: deepseek-v4-flash
persistenceRoot: !!js process.env.DSH_SNAPSHOT_SESSIONS_ROOT ?? './.sessions'
+1 -1
View File
@@ -8,7 +8,7 @@
path: ./cordis.yml
patches:
- id: acp-agent
name: '@deepseek-ai/dsh-acp-agent'
name: '@deepseek-ai/dsh-acp-demo'
config:
model: deepseek-v4-flash
persistenceRoot: !!js process.env.DSH_SNAPSHOT_SESSIONS_ROOT ?? './.sessions'
@@ -10,7 +10,7 @@
name: '@deepseek-ai/dsh-llm-deepseek'
disabled: true
- id: acp-agent
name: '@deepseek-ai/dsh-acp-agent'
name: '@deepseek-ai/dsh-acp-demo'
config:
model: deepseek-v4-flash
persistenceRoot: !!js process.env.DSH_SNAPSHOT_SESSIONS_ROOT ?? './.sessions'
+1 -1
View File
@@ -9,7 +9,7 @@
path: ./cordis.yml
patches:
- id: acp-agent
name: '@deepseek-ai/dsh-acp-agent'
name: '@deepseek-ai/dsh-acp-demo'
config:
model: deepseek-v4-flash
persistenceRoot: !!js process.env.DSH_SNAPSHOT_SESSIONS_ROOT ?? './.sessions'
+3 -3
View File
@@ -18,9 +18,9 @@ flowchart LR
cfg --> plugin_acp_approval
plugin_acp_permission["permission<br/>@deepseek-ai/dsh-permission"]
cfg --> plugin_acp_permission
plugin_acp_acp_agent["acp-agent<br/>@deepseek-ai/dsh-acp-agent"]
plugin_acp_acp_agent["acp-agent<br/>@deepseek-ai/dsh-acp-demo"]
cfg --> plugin_acp_acp_agent
plugin_acp_acp_agent --> bundle_agent_core["@deepseek-ai/dsh-agent-core"]
plugin_acp_acp_agent --> bundle_agent_core["@deepseek-ai/dsh-agent-spine-demo"]
plugin_acp_acp_agent --> bundle_jsonl["@deepseek-ai/dsh-session-persistence-jsonl"]
plugin_acp_acp_agent --> frontdoor_acp["@deepseek-ai/dsh-acp<br/>JSON-RPC stdio bridge<br/>sessions created by client"]
bundle_agent_core --> spine_llm["ctx.llm"]
@@ -58,7 +58,7 @@ flowchart LR
| `bash` | `@deepseek-ai/dsh-bash-sandbox` |
| `approval` | `@deepseek-ai/dsh-user-approval` |
| `permission` | `@deepseek-ai/dsh-permission` |
| `acp-agent` | `@deepseek-ai/dsh-acp-agent` |
| `acp-agent` | `@deepseek-ai/dsh-acp-demo` |
| `subagent` | `@deepseek-ai/dsh-subagent` |
| `subagent-spawn` | `@deepseek-ai/dsh-subagent-spawn` |
| `subagent-fork` | `@deepseek-ai/dsh-subagent-fork` |
+1 -1
View File
@@ -37,7 +37,7 @@
# Persistence root: $DSH_SNAPSHOT_SESSIONS_ROOT when the snapshot harness sets it
# (so it can harvest / isolate the log), else ./.sessions for the demo.
- id: acp-agent
name: '@deepseek-ai/dsh-acp-agent'
name: '@deepseek-ai/dsh-acp-demo'
config:
model: deepseek-v4-flash
persistenceRoot: !!js process.env.DSH_SNAPSHOT_SESSIONS_ROOT ?? './.sessions'
+1 -1
View File
@@ -23,7 +23,7 @@ import {
*/
// The child runs from a temp cwd, so its bin and config path are absolute.
const binScript = fileURLToPath(new URL('../../../packages/ui/acp-agent/src/bin.ts', import.meta.url))
const binScript = fileURLToPath(new URL('../../../packages/examples/acp-demo/src/bin.ts', import.meta.url))
const configPath = fileURLToPath(new URL('../cordis.yml', import.meta.url))
// Resolve tsx absolutely because the subprocess runs outside the repo.
const tsxLoader = fileURLToPath(import.meta.resolve('tsx'))
+2 -2
View File
@@ -13,11 +13,11 @@ import { defineAcpSnapshotSuite, type Scenario, type SnapshotSuiteOptions } from
* docs/rfc/implemented/testing/2026-06-19-acp-snapshot-tests.md.
*/
// The dsh-acp-agent bin (the demo:acp entry), this example's cordis.yml, and
// The dsh-acp-demo bin (the demo:acp entry), this example's cordis.yml, and
// the repo-root tsconfig (four levels up from examples/acp-agent/tests) — all
// ABSOLUTE: the subprocess cwd is a temp dir outside the repo.
const AGENT = {
binScript: fileURLToPath(new URL('../../../packages/ui/acp-agent/src/bin.ts', import.meta.url)),
binScript: fileURLToPath(new URL('../../../packages/examples/acp-demo/src/bin.ts', import.meta.url)),
configPath: fileURLToPath(new URL('../cordis.yml', import.meta.url)),
tsconfigPath: fileURLToPath(new URL('../../../tsconfig.json', import.meta.url)),
}
+1 -1
View File
@@ -26,7 +26,7 @@ import {
* or runner support self-skip; real denial markers remain on sandbox e2e tiers.
*/
const binScript = fileURLToPath(new URL('../../../packages/ui/acp-agent/src/bin.ts', import.meta.url))
const binScript = fileURLToPath(new URL('../../../packages/examples/acp-demo/src/bin.ts', import.meta.url))
const configPath = fileURLToPath(new URL('../cordis.yml', import.meta.url))
const tsxLoader = fileURLToPath(import.meta.resolve('tsx'))
// The subprocess runs from a temp cwd outside the repo; point tsx at the repo
+1 -1
View File
@@ -23,7 +23,7 @@ import {
* The test owns and disposes the ACP subprocess.
*/
const binScript = fileURLToPath(new URL('../../../packages/ui/acp-agent/src/bin.ts', import.meta.url))
const binScript = fileURLToPath(new URL('../../../packages/examples/acp-demo/src/bin.ts', import.meta.url))
const configPath = fileURLToPath(new URL('../cordis.yml', import.meta.url))
const tsxLoader = fileURLToPath(import.meta.resolve('tsx'))
const repoTsconfig = fileURLToPath(new URL('../../../tsconfig.json', import.meta.url))
@@ -7,6 +7,8 @@ Verify your work by running the code or tests. Keep answers brief and factual.
Check the [exit code: N] marker on every bash result; investigate failures before moving on.
Track every background task id you start. You are notified in-session when a task finishes — do not busy-poll or sleep on one; keep working on independent steps and do not duplicate a running task's work. Before giving a final answer, collect every still-relevant task with task_output (set wait: true only when you are genuinely blocked on it), and task_kill tasks that stopped mattering.
Approval prompts are disabled in this session: actions that require approval are rejected automatically — do not request sandbox escalation (do not set `sandbox_permissions`).
<!-- dsh-user-approval-policy:never -->
@@ -25,7 +27,7 @@ The available tools:
```ts
declare const tools: {
/** Execute a bash command (`bash -c`) and return its stdout/stderr. Each call runs in a fresh shell: no state (cwd, variables, functions) persists between calls — pass `workdir` instead of using `cd`. Non-zero exits are reported as `[exit code: N]`. Commands may run under a file sandbox; a blocked file operation is reported as `[sandbox: file access denied under <mode> mode]` — a policy denial, not a bug in the command; do not retry another way (a background task reports the same marker via bash_output once it has finished). Long output is truncated to its tail; the full output is saved to a file whose path is reported when available. Set `run_in_background: true` for long-running commands: the call returns a task id immediately; poll it with `bash_output` and stop it with `bash_kill`. Attempting a command the sandbox may deny is safe and expected: run it and read the marker rather than assuming the denial. When a command is denied and a wider mode would let it succeed, escalate immediately in the same turn — the one sanctioned exception to a denial: retry the exact same command once with `sandbox_permissions` (the narrowest wider mode that suffices) plus a one-sentence `justification`. Do not detour through chat to ask permission first — the approval prompt raised by that retry is how the user consents. If the session states approval prompts are disabled, there is no exception: a denial is final — do not set `sandbox_permissions`. Never escalate speculatively: ground the request in a real denial — normally the one this command just hit; escalating up front is fine only when this session already denied the same access. A rejected escalation is final for that command — stop and explain, never work around it — but it does not forbid attempting or escalating other commands later. */
/** Execute a bash command (`bash -c`) and return its stdout/stderr. Each call runs in a fresh shell: no state (cwd, variables, functions) persists between calls — pass `workdir` instead of using `cd`. Non-zero exits are reported as `[exit code: N]`. Commands may run under a file sandbox; a blocked file operation is reported as `[sandbox: file access denied under <mode> mode]` — a policy denial, not a bug in the command; do not retry another way. Long output is truncated to its tail; the full output is saved to a file whose path is reported when available. Set `run_in_background: true` for long-running commands: the call returns a task id immediately; read its output with `task_output` and stop it with `task_kill`. Attempting a command the sandbox may deny is safe and expected: run it and read the marker rather than assuming the denial. When a command is denied and a wider mode would let it succeed, escalate immediately in the same turn — the one sanctioned exception to a denial: retry the exact same command once with `sandbox_permissions` (the narrowest wider mode that suffices) plus a one-sentence `justification`. Do not detour through chat to ask permission first — the approval prompt raised by that retry is how the user consents. If the session states approval prompts are disabled, there is no exception: a denial is final — do not set `sandbox_permissions`. Never escalate speculatively: ground the request in a real denial — normally the one this command just hit; escalating up front is fine only when this session already denied the same access. A rejected escalation is final for that command — stop and explain, never work around it — but it does not forbid attempting or escalating other commands later. */
bash(args: {
/** The bash command to execute. */
command: string;
@@ -35,23 +37,13 @@ declare const tools: {
timeoutMs?: number;
/** Working directory for this command. Defaults to the session workspace; a relative path is resolved against it. */
workdir?: string;
/** Run in the background and return a task id immediately. No timeout applies. */
/** Run in the background and return a task id immediately (collect with task_output, stop with task_kill). No timeout applies. */
run_in_background?: boolean;
/** The wider sandbox mode this command needs. Only valid as a one-shot retry of a command the sandbox just denied; requires justification and user approval. */
sandbox_permissions?: "workspace-write" | "danger-full-access";
/** Required with sandbox_permissions: one sentence for the user explaining why this exact command needs the wider access. */
justification?: string;
}): Promise<string>;
/** Ask the executor to kill a running background bash task by task id. */
bash_kill(args: {
/** Task id returned by the bash tool. */
task_id: string;
}): Promise<string>;
/** Read new output from a background bash task started with `bash` + `run_in_background`. Returns only output produced since the previous bash_output call, plus the task status. Tasks keep running while you do other work; poll again later for more output. */
bash_output(args: {
/** Task id returned by the bash tool. */
task_id: string;
}): Promise<string>;
/** Inspect the live cordis runtime that is running THIS agent. Read-only. Sections: `services` (every provided ctx service and the plugin fiber that owns it), `plugins` (a flat list of the loaded plugins with their lifecycle states), `tools` (the model-facing tools currently registered, i.e. what you can call), `dynamic` (plugins you mounted via cordis_mount: id, name, state, provided services, awaited services), `api` (method signatures AND argument/return type shapes for every LIVE service — read this before writing plugin code that calls a service), `events` (every harness event with its dispatch mode and exact signature — pick listener targets here). Omit `what` to get all six sections. */
cordis_inspect(args: {
/** Limit the report to one section. Omit for all sections. */
@@ -72,19 +64,41 @@ declare const tools: {
/** The exact skill name from the available skills list. */
name: string;
}): Promise<string>;
/** Delegate a self-contained task to a subagent (a separate agent that works in its own context) and return its final result. Use this to offload focused, independent work — research, a scoped implementation, an analysis — so it does not consume this conversation's context. The subagent runs to completion and you receive only its final answer, not its intermediate steps. Give it a complete, standalone prompt: it does not see this conversation. */
/** Delegate a self-contained task to a subagent (a separate agent that works in its own context) and return its final result. Use this to offload focused, independent work — research, a scoped implementation, an analysis — so it does not consume this conversation's context. The subagent runs to completion and you receive only its final answer, not its intermediate steps. Give it a complete, standalone prompt: it does not see this conversation. Set `run_in_background: true` to return a task id; collect with `task_output` and stop with `task_kill`. */
subagent(args: {
/** A short (3-5 word) description of the delegated task, for display. */
description: string;
/** The complete, self-contained task for the subagent. It does not share this conversation's context, so include everything it needs. */
prompt: string;
/** Run as a background task and return its id; collect with task_output or stop with task_kill. */
run_in_background?: boolean;
}): Promise<string>;
/** Delegate a task to a subagent that INHERITS this conversation: a child agent seeded with all completed turns so far (it does not see the current in-flight turn), returning only its final result. Use this when the subtask builds on this conversation's context — a follow-up analysis, a review, a continuation — without consuming this conversation's context for the work itself. You receive only its final answer, not its intermediate steps. */
/** Delegate a task to a subagent that inherits this conversation: a child agent seeded with all completed turns so far (it does not see the current in-flight turn), returning only its final result. Use this when the subtask builds on this conversation's context — a follow-up analysis, a review, a continuation — without consuming this conversation's context for the work itself. You receive only its final answer, not its intermediate steps. Set `run_in_background: true` to return a task id; collect with `task_output` and stop with `task_kill`. */
subagent_fork(args: {
/** A short (3-5 word) description of the delegated task, for display. */
description: string;
/** The task for the subagent. It already sees this conversation's completed turns, so build on them freely and state only what is new. */
prompt: string;
/** Run as a background task and return its id; collect with task_output or stop with task_kill. */
run_in_background?: boolean;
}): Promise<string>;
/** Request cancellation of a running background task by task id. Returns immediately; the task settles as killed once its work actually stops. */
task_kill(args: {
/** Task id returned by the tool that started the background work. */
task_id: string;
/** Optional short reason, recorded in the log and forwarded to the task. */
reason?: string;
}): Promise<string>;
/** List your background tasks (running and finished) with their ids, kinds, and statuses. */
task_list(args: Record<string, unknown>): Promise<string>;
/** Read a background task. Stream tasks return only output since the previous read; final-output tasks return their result after settlement. Every response ends with `[status: ...]`. Reads are non-blocking unless `wait: true`, which waits up to the configured cap. */
task_output(args: {
/** Task id returned by the tool that started the background work. */
task_id: string;
/** Block until the task reaches a terminal status or the timeout expires. A timed-out wait returns [status: running] and leaves the task alive. */
wait?: boolean;
/** Max wait in milliseconds (only meaningful with wait: true). Defaults to the configured wait timeout; capped by the configured maximum. */
timeout_ms?: number;
}): Promise<string>;
/** Record and update a structured task list for the current work. Send the ENTIRE list every call — it REPLACES the previous list (there are no partial updates, no per-item edits). Use it to plan multi-step work and show progress: add one todo per concrete step before you start. Keep AT MOST ONE todo `in_progress` at a time; while work remains, exactly one active task should be `in_progress`. Mark a todo `completed` the moment it is done (do not batch completions), and allow no `in_progress` item only once all work is complete. Skip the list for trivial single-step tasks. Statuses: `pending` (not started), `in_progress` (being worked on now), `completed` (finished). */
todo_write(args: {
@@ -2,7 +2,7 @@
"initial": [
{
"name": "bash",
"description": "Execute a bash command (`bash -c`) and return its stdout/stderr. Each call runs in a fresh shell: no state (cwd, variables, functions) persists between calls — pass `workdir` instead of using `cd`. Non-zero exits are reported as `[exit code: N]`. Commands may run under a file sandbox; a blocked file operation is reported as `[sandbox: file access denied under <mode> mode]` — a policy denial, not a bug in the command; do not retry another way (a background task reports the same marker via bash_output once it has finished). Long output is truncated to its tail; the full output is saved to a file whose path is reported when available. Set `run_in_background: true` for long-running commands: the call returns a task id immediately; poll it with `bash_output` and stop it with `bash_kill`. Attempting a command the sandbox may deny is safe and expected: run it and read the marker rather than assuming the denial. When a command is denied and a wider mode would let it succeed, escalate immediately in the same turn — the one sanctioned exception to a denial: retry the exact same command once with `sandbox_permissions` (the narrowest wider mode that suffices) plus a one-sentence `justification`. Do not detour through chat to ask permission first — the approval prompt raised by that retry is how the user consents. If the session states approval prompts are disabled, there is no exception: a denial is final — do not set `sandbox_permissions`. Never escalate speculatively: ground the request in a real denial — normally the one this command just hit; escalating up front is fine only when this session already denied the same access. A rejected escalation is final for that command — stop and explain, never work around it — but it does not forbid attempting or escalating other commands later.",
"description": "Execute a bash command (`bash -c`) and return its stdout/stderr. Each call runs in a fresh shell: no state (cwd, variables, functions) persists between calls — pass `workdir` instead of using `cd`. Non-zero exits are reported as `[exit code: N]`. Commands may run under a file sandbox; a blocked file operation is reported as `[sandbox: file access denied under <mode> mode]` — a policy denial, not a bug in the command; do not retry another way. Long output is truncated to its tail; the full output is saved to a file whose path is reported when available. Set `run_in_background: true` for long-running commands: the call returns a task id immediately; read its output with `task_output` and stop it with `task_kill`. Attempting a command the sandbox may deny is safe and expected: run it and read the marker rather than assuming the denial. When a command is denied and a wider mode would let it succeed, escalate immediately in the same turn — the one sanctioned exception to a denial: retry the exact same command once with `sandbox_permissions` (the narrowest wider mode that suffices) plus a one-sentence `justification`. Do not detour through chat to ask permission first — the approval prompt raised by that retry is how the user consents. If the session states approval prompts are disabled, there is no exception: a denial is final — do not set `sandbox_permissions`. Never escalate speculatively: ground the request in a real denial — normally the one this command just hit; escalating up front is fine only when this session already denied the same access. A rejected escalation is final for that command — stop and explain, never work around it — but it does not forbid attempting or escalating other commands later.",
"parameters": {
"type": "object",
"properties": {
@@ -24,7 +24,7 @@
},
"run_in_background": {
"type": "boolean",
"description": "Run in the background and return a task id immediately. No timeout applies."
"description": "Run in the background and return a task id immediately (collect with task_output, stop with task_kill). No timeout applies."
},
"sandbox_permissions": {
"type": "string",
@@ -45,38 +45,6 @@
]
}
},
{
"name": "bash_kill",
"description": "Ask the executor to kill a running background bash task by task id.",
"parameters": {
"type": "object",
"properties": {
"task_id": {
"type": "string",
"description": "Task id returned by the bash tool."
}
},
"required": [
"task_id"
]
}
},
{
"name": "bash_output",
"description": "Read new output from a background bash task started with `bash` + `run_in_background`. Returns only output produced since the previous bash_output call, plus the task status. Tasks keep running while you do other work; poll again later for more output.",
"parameters": {
"type": "object",
"properties": {
"task_id": {
"type": "string",
"description": "Task id returned by the bash tool."
}
},
"required": [
"task_id"
]
}
},
{
"name": "cordis_inspect",
"description": "Inspect the live cordis runtime that is running THIS agent. Read-only. Sections: `services` (every provided ctx service and the plugin fiber that owns it), `plugins` (a flat list of the loaded plugins with their lifecycle states), `tools` (the model-facing tools currently registered, i.e. what you can call), `dynamic` (plugins you mounted via cordis_mount: id, name, state, provided services, awaited services), `api` (method signatures AND argument/return type shapes for every LIVE service — read this before writing plugin code that calls a service), `events` (every harness event with its dispatch mode and exact signature — pick listener targets here). Omit `what` to get all six sections.",
@@ -164,7 +132,7 @@
},
{
"name": "subagent",
"description": "Delegate a self-contained task to a subagent (a separate agent that works in its own context) and return its final result. Use this to offload focused, independent work — research, a scoped implementation, an analysis — so it does not consume this conversation's context. The subagent runs to completion and you receive only its final answer, not its intermediate steps. Give it a complete, standalone prompt: it does not see this conversation.",
"description": "Delegate a self-contained task to a subagent (a separate agent that works in its own context) and return its final result. Use this to offload focused, independent work — research, a scoped implementation, an analysis — so it does not consume this conversation's context. The subagent runs to completion and you receive only its final answer, not its intermediate steps. Give it a complete, standalone prompt: it does not see this conversation. Set `run_in_background: true` to return a task id; collect with `task_output` and stop with `task_kill`.",
"parameters": {
"type": "object",
"properties": {
@@ -175,6 +143,10 @@
"prompt": {
"type": "string",
"description": "The complete, self-contained task for the subagent. It does not share this conversation's context, so include everything it needs."
},
"run_in_background": {
"type": "boolean",
"description": "Run as a background task and return its id; collect with task_output or stop with task_kill."
}
},
"required": [
@@ -185,7 +157,7 @@
},
{
"name": "subagent_fork",
"description": "Delegate a task to a subagent that INHERITS this conversation: a child agent seeded with all completed turns so far (it does not see the current in-flight turn), returning only its final result. Use this when the subtask builds on this conversation's context — a follow-up analysis, a review, a continuation — without consuming this conversation's context for the work itself. You receive only its final answer, not its intermediate steps.",
"description": "Delegate a task to a subagent that inherits this conversation: a child agent seeded with all completed turns so far (it does not see the current in-flight turn), returning only its final result. Use this when the subtask builds on this conversation's context — a follow-up analysis, a review, a continuation — without consuming this conversation's context for the work itself. You receive only its final answer, not its intermediate steps. Set `run_in_background: true` to return a task id; collect with `task_output` and stop with `task_kill`.",
"parameters": {
"type": "object",
"properties": {
@@ -196,6 +168,10 @@
"prompt": {
"type": "string",
"description": "The task for the subagent. It already sees this conversation's completed turns, so build on them freely and state only what is new."
},
"run_in_background": {
"type": "boolean",
"description": "Run as a background task and return its id; collect with task_output or stop with task_kill."
}
},
"required": [
@@ -204,6 +180,58 @@
]
}
},
{
"name": "task_kill",
"description": "Request cancellation of a running background task by task id. Returns immediately; the task settles as killed once its work actually stops.",
"parameters": {
"type": "object",
"properties": {
"task_id": {
"type": "string",
"description": "Task id returned by the tool that started the background work."
},
"reason": {
"type": "string",
"description": "Optional short reason, recorded in the log and forwarded to the task."
}
},
"required": [
"task_id"
]
}
},
{
"name": "task_list",
"description": "List your background tasks (running and finished) with their ids, kinds, and statuses.",
"parameters": {
"type": "object",
"properties": {}
}
},
{
"name": "task_output",
"description": "Read a background task. Stream tasks return only output since the previous read; final-output tasks return their result after settlement. Every response ends with `[status: ...]`. Reads are non-blocking unless `wait: true`, which waits up to the configured cap.",
"parameters": {
"type": "object",
"properties": {
"task_id": {
"type": "string",
"description": "Task id returned by the tool that started the background work."
},
"wait": {
"type": "boolean",
"description": "Block until the task reaches a terminal status or the timeout expires. A timed-out wait returns [status: running] and leaves the task alive."
},
"timeout_ms": {
"type": "number",
"description": "Max wait in milliseconds (only meaningful with wait: true). Defaults to the configured wait timeout; capped by the configured maximum."
}
},
"required": [
"task_id"
]
}
},
{
"name": "todo_write",
"description": "Record and update a structured task list for the current work. Send the ENTIRE list every call — it REPLACES the previous list (there are no partial updates, no per-item edits). Use it to plan multi-step work and show progress: add one todo per concrete step before you start. Keep AT MOST ONE todo `in_progress` at a time; while work remains, exactly one active task should be `in_progress`. Mark a todo `completed` the moment it is done (do not batch completions), and allow no `in_progress` item only once all work is complete. Skip the list for trivial single-step tasks. Statuses: `pending` (not started), `in_progress` (being worked on now), `completed` (finished).",
@@ -7,6 +7,8 @@ Verify your work by running the code or tests. Keep answers brief and factual.
Check the [exit code: N] marker on every bash result; investigate failures before moving on.
Track every background task id you start. You are notified in-session when a task finishes — do not busy-poll or sleep on one; keep working on independent steps and do not duplicate a running task's work. Before giving a final answer, collect every still-relevant task with task_output (set wait: true only when you are genuinely blocked on it), and task_kill tasks that stopped mattering.
Approval prompts are disabled in this session: actions that require approval are rejected automatically — do not request sandbox escalation (do not set `sandbox_permissions`).
<!-- dsh-user-approval-policy:never -->
@@ -25,7 +27,7 @@ The available tools:
```ts
declare const tools: {
/** Execute a bash command (`bash -c`) and return its stdout/stderr. Each call runs in a fresh shell: no state (cwd, variables, functions) persists between calls — pass `workdir` instead of using `cd`. Non-zero exits are reported as `[exit code: N]`. Commands may run under a file sandbox; a blocked file operation is reported as `[sandbox: file access denied under <mode> mode]` — a policy denial, not a bug in the command; do not retry another way (a background task reports the same marker via bash_output once it has finished). Long output is truncated to its tail; the full output is saved to a file whose path is reported when available. Set `run_in_background: true` for long-running commands: the call returns a task id immediately; poll it with `bash_output` and stop it with `bash_kill`. Attempting a command the sandbox may deny is safe and expected: run it and read the marker rather than assuming the denial. When a command is denied and a wider mode would let it succeed, escalate immediately in the same turn — the one sanctioned exception to a denial: retry the exact same command once with `sandbox_permissions` (the narrowest wider mode that suffices) plus a one-sentence `justification`. Do not detour through chat to ask permission first — the approval prompt raised by that retry is how the user consents. If the session states approval prompts are disabled, there is no exception: a denial is final — do not set `sandbox_permissions`. Never escalate speculatively: ground the request in a real denial — normally the one this command just hit; escalating up front is fine only when this session already denied the same access. A rejected escalation is final for that command — stop and explain, never work around it — but it does not forbid attempting or escalating other commands later. */
/** Execute a bash command (`bash -c`) and return its stdout/stderr. Each call runs in a fresh shell: no state (cwd, variables, functions) persists between calls — pass `workdir` instead of using `cd`. Non-zero exits are reported as `[exit code: N]`. Commands may run under a file sandbox; a blocked file operation is reported as `[sandbox: file access denied under <mode> mode]` — a policy denial, not a bug in the command; do not retry another way. Long output is truncated to its tail; the full output is saved to a file whose path is reported when available. Set `run_in_background: true` for long-running commands: the call returns a task id immediately; read its output with `task_output` and stop it with `task_kill`. Attempting a command the sandbox may deny is safe and expected: run it and read the marker rather than assuming the denial. When a command is denied and a wider mode would let it succeed, escalate immediately in the same turn — the one sanctioned exception to a denial: retry the exact same command once with `sandbox_permissions` (the narrowest wider mode that suffices) plus a one-sentence `justification`. Do not detour through chat to ask permission first — the approval prompt raised by that retry is how the user consents. If the session states approval prompts are disabled, there is no exception: a denial is final — do not set `sandbox_permissions`. Never escalate speculatively: ground the request in a real denial — normally the one this command just hit; escalating up front is fine only when this session already denied the same access. A rejected escalation is final for that command — stop and explain, never work around it — but it does not forbid attempting or escalating other commands later. */
bash(args: {
/** The bash command to execute. */
command: string;
@@ -35,41 +37,53 @@ declare const tools: {
timeoutMs?: number;
/** Working directory for this command. Defaults to the session workspace; a relative path is resolved against it. */
workdir?: string;
/** Run in the background and return a task id immediately. No timeout applies. */
/** Run in the background and return a task id immediately (collect with task_output, stop with task_kill). No timeout applies. */
run_in_background?: boolean;
/** The wider sandbox mode this command needs. Only valid as a one-shot retry of a command the sandbox just denied; requires justification and user approval. */
sandbox_permissions?: "workspace-write" | "danger-full-access";
/** Required with sandbox_permissions: one sentence for the user explaining why this exact command needs the wider access. */
justification?: string;
}): Promise<string>;
/** Ask the executor to kill a running background bash task by task id. */
bash_kill(args: {
/** Task id returned by the bash tool. */
task_id: string;
}): Promise<string>;
/** Read new output from a background bash task started with `bash` + `run_in_background`. Returns only output produced since the previous bash_output call, plus the task status. Tasks keep running while you do other work; poll again later for more output. */
bash_output(args: {
/** Task id returned by the bash tool. */
task_id: string;
}): Promise<string>;
/** Load the full instructions for an available skill. Call this with the exact skill name from the session skill catalog before acting on a task that names or clearly matches that skill. */
skill(args: {
/** The exact skill name from the available skills list. */
name: string;
}): Promise<string>;
/** Delegate a self-contained task to a subagent (a separate agent that works in its own context) and return its final result. Use this to offload focused, independent work — research, a scoped implementation, an analysis — so it does not consume this conversation's context. The subagent runs to completion and you receive only its final answer, not its intermediate steps. Give it a complete, standalone prompt: it does not see this conversation. */
/** Delegate a self-contained task to a subagent (a separate agent that works in its own context) and return its final result. Use this to offload focused, independent work — research, a scoped implementation, an analysis — so it does not consume this conversation's context. The subagent runs to completion and you receive only its final answer, not its intermediate steps. Give it a complete, standalone prompt: it does not see this conversation. Set `run_in_background: true` to return a task id; collect with `task_output` and stop with `task_kill`. */
subagent(args: {
/** A short (3-5 word) description of the delegated task, for display. */
description: string;
/** The complete, self-contained task for the subagent. It does not share this conversation's context, so include everything it needs. */
prompt: string;
/** Run as a background task and return its id; collect with task_output or stop with task_kill. */
run_in_background?: boolean;
}): Promise<string>;
/** Delegate a task to a subagent that INHERITS this conversation: a child agent seeded with all completed turns so far (it does not see the current in-flight turn), returning only its final result. Use this when the subtask builds on this conversation's context — a follow-up analysis, a review, a continuation — without consuming this conversation's context for the work itself. You receive only its final answer, not its intermediate steps. */
/** Delegate a task to a subagent that inherits this conversation: a child agent seeded with all completed turns so far (it does not see the current in-flight turn), returning only its final result. Use this when the subtask builds on this conversation's context — a follow-up analysis, a review, a continuation — without consuming this conversation's context for the work itself. You receive only its final answer, not its intermediate steps. Set `run_in_background: true` to return a task id; collect with `task_output` and stop with `task_kill`. */
subagent_fork(args: {
/** A short (3-5 word) description of the delegated task, for display. */
description: string;
/** The task for the subagent. It already sees this conversation's completed turns, so build on them freely and state only what is new. */
prompt: string;
/** Run as a background task and return its id; collect with task_output or stop with task_kill. */
run_in_background?: boolean;
}): Promise<string>;
/** Request cancellation of a running background task by task id. Returns immediately; the task settles as killed once its work actually stops. */
task_kill(args: {
/** Task id returned by the tool that started the background work. */
task_id: string;
/** Optional short reason, recorded in the log and forwarded to the task. */
reason?: string;
}): Promise<string>;
/** List your background tasks (running and finished) with their ids, kinds, and statuses. */
task_list(args: Record<string, unknown>): Promise<string>;
/** Read a background task. Stream tasks return only output since the previous read; final-output tasks return their result after settlement. Every response ends with `[status: ...]`. Reads are non-blocking unless `wait: true`, which waits up to the configured cap. */
task_output(args: {
/** Task id returned by the tool that started the background work. */
task_id: string;
/** Block until the task reaches a terminal status or the timeout expires. A timed-out wait returns [status: running] and leaves the task alive. */
wait?: boolean;
/** Max wait in milliseconds (only meaningful with wait: true). Defaults to the configured wait timeout; capped by the configured maximum. */
timeout_ms?: number;
}): Promise<string>;
/** Record and update a structured task list for the current work. Send the ENTIRE list every call — it REPLACES the previous list (there are no partial updates, no per-item edits). Use it to plan multi-step work and show progress: add one todo per concrete step before you start. Keep AT MOST ONE todo `in_progress` at a time; while work remains, exactly one active task should be `in_progress`. Mark a todo `completed` the moment it is done (do not batch completions), and allow no `in_progress` item only once all work is complete. Skip the list for trivial single-step tasks. Statuses: `pending` (not started), `in_progress` (being worked on now), `completed` (finished). */
todo_write(args: {
@@ -2,7 +2,7 @@
"initial": [
{
"name": "bash",
"description": "Execute a bash command (`bash -c`) and return its stdout/stderr. Each call runs in a fresh shell: no state (cwd, variables, functions) persists between calls — pass `workdir` instead of using `cd`. Non-zero exits are reported as `[exit code: N]`. Commands may run under a file sandbox; a blocked file operation is reported as `[sandbox: file access denied under <mode> mode]` — a policy denial, not a bug in the command; do not retry another way (a background task reports the same marker via bash_output once it has finished). Long output is truncated to its tail; the full output is saved to a file whose path is reported when available. Set `run_in_background: true` for long-running commands: the call returns a task id immediately; poll it with `bash_output` and stop it with `bash_kill`. Attempting a command the sandbox may deny is safe and expected: run it and read the marker rather than assuming the denial. When a command is denied and a wider mode would let it succeed, escalate immediately in the same turn — the one sanctioned exception to a denial: retry the exact same command once with `sandbox_permissions` (the narrowest wider mode that suffices) plus a one-sentence `justification`. Do not detour through chat to ask permission first — the approval prompt raised by that retry is how the user consents. If the session states approval prompts are disabled, there is no exception: a denial is final — do not set `sandbox_permissions`. Never escalate speculatively: ground the request in a real denial — normally the one this command just hit; escalating up front is fine only when this session already denied the same access. A rejected escalation is final for that command — stop and explain, never work around it — but it does not forbid attempting or escalating other commands later.",
"description": "Execute a bash command (`bash -c`) and return its stdout/stderr. Each call runs in a fresh shell: no state (cwd, variables, functions) persists between calls — pass `workdir` instead of using `cd`. Non-zero exits are reported as `[exit code: N]`. Commands may run under a file sandbox; a blocked file operation is reported as `[sandbox: file access denied under <mode> mode]` — a policy denial, not a bug in the command; do not retry another way. Long output is truncated to its tail; the full output is saved to a file whose path is reported when available. Set `run_in_background: true` for long-running commands: the call returns a task id immediately; read its output with `task_output` and stop it with `task_kill`. Attempting a command the sandbox may deny is safe and expected: run it and read the marker rather than assuming the denial. When a command is denied and a wider mode would let it succeed, escalate immediately in the same turn — the one sanctioned exception to a denial: retry the exact same command once with `sandbox_permissions` (the narrowest wider mode that suffices) plus a one-sentence `justification`. Do not detour through chat to ask permission first — the approval prompt raised by that retry is how the user consents. If the session states approval prompts are disabled, there is no exception: a denial is final — do not set `sandbox_permissions`. Never escalate speculatively: ground the request in a real denial — normally the one this command just hit; escalating up front is fine only when this session already denied the same access. A rejected escalation is final for that command — stop and explain, never work around it — but it does not forbid attempting or escalating other commands later.",
"parameters": {
"type": "object",
"properties": {
@@ -24,7 +24,7 @@
},
"run_in_background": {
"type": "boolean",
"description": "Run in the background and return a task id immediately. No timeout applies."
"description": "Run in the background and return a task id immediately (collect with task_output, stop with task_kill). No timeout applies."
},
"sandbox_permissions": {
"type": "string",
@@ -45,38 +45,6 @@
]
}
},
{
"name": "bash_kill",
"description": "Ask the executor to kill a running background bash task by task id.",
"parameters": {
"type": "object",
"properties": {
"task_id": {
"type": "string",
"description": "Task id returned by the bash tool."
}
},
"required": [
"task_id"
]
}
},
{
"name": "bash_output",
"description": "Read new output from a background bash task started with `bash` + `run_in_background`. Returns only output produced since the previous bash_output call, plus the task status. Tasks keep running while you do other work; poll again later for more output.",
"parameters": {
"type": "object",
"properties": {
"task_id": {
"type": "string",
"description": "Task id returned by the bash tool."
}
},
"required": [
"task_id"
]
}
},
{
"name": "run_code",
"description": "Execute a TypeScript program against the available tools. Write the BODY of an async function (erasable syntax only; top-level `await` and `return` work) and call tools as `await tools.name(args)` per the declarations in the system prompt. Only what you print or return comes back — curate it.",
@@ -111,7 +79,7 @@
},
{
"name": "subagent",
"description": "Delegate a self-contained task to a subagent (a separate agent that works in its own context) and return its final result. Use this to offload focused, independent work — research, a scoped implementation, an analysis — so it does not consume this conversation's context. The subagent runs to completion and you receive only its final answer, not its intermediate steps. Give it a complete, standalone prompt: it does not see this conversation.",
"description": "Delegate a self-contained task to a subagent (a separate agent that works in its own context) and return its final result. Use this to offload focused, independent work — research, a scoped implementation, an analysis — so it does not consume this conversation's context. The subagent runs to completion and you receive only its final answer, not its intermediate steps. Give it a complete, standalone prompt: it does not see this conversation. Set `run_in_background: true` to return a task id; collect with `task_output` and stop with `task_kill`.",
"parameters": {
"type": "object",
"properties": {
@@ -122,6 +90,10 @@
"prompt": {
"type": "string",
"description": "The complete, self-contained task for the subagent. It does not share this conversation's context, so include everything it needs."
},
"run_in_background": {
"type": "boolean",
"description": "Run as a background task and return its id; collect with task_output or stop with task_kill."
}
},
"required": [
@@ -132,7 +104,7 @@
},
{
"name": "subagent_fork",
"description": "Delegate a task to a subagent that INHERITS this conversation: a child agent seeded with all completed turns so far (it does not see the current in-flight turn), returning only its final result. Use this when the subtask builds on this conversation's context — a follow-up analysis, a review, a continuation — without consuming this conversation's context for the work itself. You receive only its final answer, not its intermediate steps.",
"description": "Delegate a task to a subagent that inherits this conversation: a child agent seeded with all completed turns so far (it does not see the current in-flight turn), returning only its final result. Use this when the subtask builds on this conversation's context — a follow-up analysis, a review, a continuation — without consuming this conversation's context for the work itself. You receive only its final answer, not its intermediate steps. Set `run_in_background: true` to return a task id; collect with `task_output` and stop with `task_kill`.",
"parameters": {
"type": "object",
"properties": {
@@ -143,6 +115,10 @@
"prompt": {
"type": "string",
"description": "The task for the subagent. It already sees this conversation's completed turns, so build on them freely and state only what is new."
},
"run_in_background": {
"type": "boolean",
"description": "Run as a background task and return its id; collect with task_output or stop with task_kill."
}
},
"required": [
@@ -151,6 +127,58 @@
]
}
},
{
"name": "task_kill",
"description": "Request cancellation of a running background task by task id. Returns immediately; the task settles as killed once its work actually stops.",
"parameters": {
"type": "object",
"properties": {
"task_id": {
"type": "string",
"description": "Task id returned by the tool that started the background work."
},
"reason": {
"type": "string",
"description": "Optional short reason, recorded in the log and forwarded to the task."
}
},
"required": [
"task_id"
]
}
},
{
"name": "task_list",
"description": "List your background tasks (running and finished) with their ids, kinds, and statuses.",
"parameters": {
"type": "object",
"properties": {}
}
},
{
"name": "task_output",
"description": "Read a background task. Stream tasks return only output since the previous read; final-output tasks return their result after settlement. Every response ends with `[status: ...]`. Reads are non-blocking unless `wait: true`, which waits up to the configured cap.",
"parameters": {
"type": "object",
"properties": {
"task_id": {
"type": "string",
"description": "Task id returned by the tool that started the background work."
},
"wait": {
"type": "boolean",
"description": "Block until the task reaches a terminal status or the timeout expires. A timed-out wait returns [status: running] and leaves the task alive."
},
"timeout_ms": {
"type": "number",
"description": "Max wait in milliseconds (only meaningful with wait: true). Defaults to the configured wait timeout; capped by the configured maximum."
}
},
"required": [
"task_id"
]
}
},
{
"name": "todo_write",
"description": "Record and update a structured task list for the current work. Send the ENTIRE list every call — it REPLACES the previous list (there are no partial updates, no per-item edits). Use it to plan multi-step work and show progress: add one todo per concrete step before you start. Keep AT MOST ONE todo `in_progress` at a time; while work remains, exactly one active task should be `in_progress`. Mark a todo `completed` the moment it is done (do not batch completions), and allow no `in_progress` item only once all work is complete. Skip the list for trivial single-step tasks. Statuses: `pending` (not started), `in_progress` (being worked on now), `completed` (finished).",
@@ -7,6 +7,8 @@ Verify your work by running the code or tests. Keep answers brief and factual.
Check the [exit code: N] marker on every bash result; investigate failures before moving on.
Track every background task id you start. You are notified in-session when a task finishes — do not busy-poll or sleep on one; keep working on independent steps and do not duplicate a running task's work. Before giving a final answer, collect every still-relevant task with task_output (set wait: true only when you are genuinely blocked on it), and task_kill tasks that stopped mattering.
Approval prompts are disabled in this session: actions that require approval are rejected automatically — do not request sandbox escalation (do not set `sandbox_permissions`).
<!-- dsh-user-approval-policy:never -->
@@ -25,7 +27,7 @@ The available tools:
```ts
declare const tools: {
/** Execute a bash command (`bash -c`) and return its stdout/stderr. Each call runs in a fresh shell: no state (cwd, variables, functions) persists between calls — pass `workdir` instead of using `cd`. Non-zero exits are reported as `[exit code: N]`. Commands may run under a file sandbox; a blocked file operation is reported as `[sandbox: file access denied under <mode> mode]` — a policy denial, not a bug in the command; do not retry another way (a background task reports the same marker via bash_output once it has finished). Long output is truncated to its tail; the full output is saved to a file whose path is reported when available. Set `run_in_background: true` for long-running commands: the call returns a task id immediately; poll it with `bash_output` and stop it with `bash_kill`. Attempting a command the sandbox may deny is safe and expected: run it and read the marker rather than assuming the denial. When a command is denied and a wider mode would let it succeed, escalate immediately in the same turn — the one sanctioned exception to a denial: retry the exact same command once with `sandbox_permissions` (the narrowest wider mode that suffices) plus a one-sentence `justification`. Do not detour through chat to ask permission first — the approval prompt raised by that retry is how the user consents. If the session states approval prompts are disabled, there is no exception: a denial is final — do not set `sandbox_permissions`. Never escalate speculatively: ground the request in a real denial — normally the one this command just hit; escalating up front is fine only when this session already denied the same access. A rejected escalation is final for that command — stop and explain, never work around it — but it does not forbid attempting or escalating other commands later. */
/** Execute a bash command (`bash -c`) and return its stdout/stderr. Each call runs in a fresh shell: no state (cwd, variables, functions) persists between calls — pass `workdir` instead of using `cd`. Non-zero exits are reported as `[exit code: N]`. Commands may run under a file sandbox; a blocked file operation is reported as `[sandbox: file access denied under <mode> mode]` — a policy denial, not a bug in the command; do not retry another way. Long output is truncated to its tail; the full output is saved to a file whose path is reported when available. Set `run_in_background: true` for long-running commands: the call returns a task id immediately; read its output with `task_output` and stop it with `task_kill`. Attempting a command the sandbox may deny is safe and expected: run it and read the marker rather than assuming the denial. When a command is denied and a wider mode would let it succeed, escalate immediately in the same turn — the one sanctioned exception to a denial: retry the exact same command once with `sandbox_permissions` (the narrowest wider mode that suffices) plus a one-sentence `justification`. Do not detour through chat to ask permission first — the approval prompt raised by that retry is how the user consents. If the session states approval prompts are disabled, there is no exception: a denial is final — do not set `sandbox_permissions`. Never escalate speculatively: ground the request in a real denial — normally the one this command just hit; escalating up front is fine only when this session already denied the same access. A rejected escalation is final for that command — stop and explain, never work around it — but it does not forbid attempting or escalating other commands later. */
bash(args: {
/** The bash command to execute. */
command: string;
@@ -35,41 +37,53 @@ declare const tools: {
timeoutMs?: number;
/** Working directory for this command. Defaults to the session workspace; a relative path is resolved against it. */
workdir?: string;
/** Run in the background and return a task id immediately. No timeout applies. */
/** Run in the background and return a task id immediately (collect with task_output, stop with task_kill). No timeout applies. */
run_in_background?: boolean;
/** The wider sandbox mode this command needs. Only valid as a one-shot retry of a command the sandbox just denied; requires justification and user approval. */
sandbox_permissions?: "workspace-write" | "danger-full-access";
/** Required with sandbox_permissions: one sentence for the user explaining why this exact command needs the wider access. */
justification?: string;
}): Promise<string>;
/** Ask the executor to kill a running background bash task by task id. */
bash_kill(args: {
/** Task id returned by the bash tool. */
task_id: string;
}): Promise<string>;
/** Read new output from a background bash task started with `bash` + `run_in_background`. Returns only output produced since the previous bash_output call, plus the task status. Tasks keep running while you do other work; poll again later for more output. */
bash_output(args: {
/** Task id returned by the bash tool. */
task_id: string;
}): Promise<string>;
/** Load the full instructions for an available skill. Call this with the exact skill name from the session skill catalog before acting on a task that names or clearly matches that skill. */
skill(args: {
/** The exact skill name from the available skills list. */
name: string;
}): Promise<string>;
/** Delegate a self-contained task to a subagent (a separate agent that works in its own context) and return its final result. Use this to offload focused, independent work — research, a scoped implementation, an analysis — so it does not consume this conversation's context. The subagent runs to completion and you receive only its final answer, not its intermediate steps. Give it a complete, standalone prompt: it does not see this conversation. */
/** Delegate a self-contained task to a subagent (a separate agent that works in its own context) and return its final result. Use this to offload focused, independent work — research, a scoped implementation, an analysis — so it does not consume this conversation's context. The subagent runs to completion and you receive only its final answer, not its intermediate steps. Give it a complete, standalone prompt: it does not see this conversation. Set `run_in_background: true` to return a task id; collect with `task_output` and stop with `task_kill`. */
subagent(args: {
/** A short (3-5 word) description of the delegated task, for display. */
description: string;
/** The complete, self-contained task for the subagent. It does not share this conversation's context, so include everything it needs. */
prompt: string;
/** Run as a background task and return its id; collect with task_output or stop with task_kill. */
run_in_background?: boolean;
}): Promise<string>;
/** Delegate a task to a subagent that INHERITS this conversation: a child agent seeded with all completed turns so far (it does not see the current in-flight turn), returning only its final result. Use this when the subtask builds on this conversation's context — a follow-up analysis, a review, a continuation — without consuming this conversation's context for the work itself. You receive only its final answer, not its intermediate steps. */
/** Delegate a task to a subagent that inherits this conversation: a child agent seeded with all completed turns so far (it does not see the current in-flight turn), returning only its final result. Use this when the subtask builds on this conversation's context — a follow-up analysis, a review, a continuation — without consuming this conversation's context for the work itself. You receive only its final answer, not its intermediate steps. Set `run_in_background: true` to return a task id; collect with `task_output` and stop with `task_kill`. */
subagent_fork(args: {
/** A short (3-5 word) description of the delegated task, for display. */
description: string;
/** The task for the subagent. It already sees this conversation's completed turns, so build on them freely and state only what is new. */
prompt: string;
/** Run as a background task and return its id; collect with task_output or stop with task_kill. */
run_in_background?: boolean;
}): Promise<string>;
/** Request cancellation of a running background task by task id. Returns immediately; the task settles as killed once its work actually stops. */
task_kill(args: {
/** Task id returned by the tool that started the background work. */
task_id: string;
/** Optional short reason, recorded in the log and forwarded to the task. */
reason?: string;
}): Promise<string>;
/** List your background tasks (running and finished) with their ids, kinds, and statuses. */
task_list(args: Record<string, unknown>): Promise<string>;
/** Read a background task. Stream tasks return only output since the previous read; final-output tasks return their result after settlement. Every response ends with `[status: ...]`. Reads are non-blocking unless `wait: true`, which waits up to the configured cap. */
task_output(args: {
/** Task id returned by the tool that started the background work. */
task_id: string;
/** Block until the task reaches a terminal status or the timeout expires. A timed-out wait returns [status: running] and leaves the task alive. */
wait?: boolean;
/** Max wait in milliseconds (only meaningful with wait: true). Defaults to the configured wait timeout; capped by the configured maximum. */
timeout_ms?: number;
}): Promise<string>;
/** Record and update a structured task list for the current work. Send the ENTIRE list every call — it REPLACES the previous list (there are no partial updates, no per-item edits). Use it to plan multi-step work and show progress: add one todo per concrete step before you start. Keep AT MOST ONE todo `in_progress` at a time; while work remains, exactly one active task should be `in_progress`. Mark a todo `completed` the moment it is done (do not batch completions), and allow no `in_progress` item only once all work is complete. Skip the list for trivial single-step tasks. Statuses: `pending` (not started), `in_progress` (being worked on now), `completed` (finished). */
todo_write(args: {

Some files were not shown because too many files have changed in this diff Show More